-module(gliew). -compile([no_auto_import, nowarn_unused_vars]). -export([mount/3, serve/2]). -export_type([event/0, loop_state/0, session/0, message/0, worker_state/0, worker_message/0]). -opaque event() :: {mount, fun((gleam@erlang@process:selector(worker_message())) -> gleam@erlang@process:selector(worker_message()))}. -type loop_state() :: {loop_state, gleam@map:map_(binary(), session())}. -type session() :: {session, binary(), binary(), list(fun((gleam@erlang@process:selector(worker_message())) -> gleam@erlang@process:selector(worker_message()))), nakai@html:node_(event())}. -type message() :: {render_tree, gleam@erlang@process:subject(binary()), gleam@http@request:request(mist@internal@http:body()), nakai@html:node_(event())} | {check_connect, gleam@erlang@process:subject(boolean()), binary(), binary()} | {do_connect, binary(), gleam@erlang@process:subject(glisten@handler:handler_message())}. -type worker_state() :: {worker_state, gleam@erlang@process:subject(glisten@handler:handler_message()), nakai@html:node_(event())}. -type worker_message() :: {live_update, binary()}. -spec insert_event(event(), nakai@html:node_(event())) -> nakai@html:node_(event()). insert_event(Event, Node) -> case Node of {element, Tag, Attrs, Children} -> _pipe = Attrs, _pipe@1 = gleam@list:prepend( _pipe, {event, <<"gliew-event"/utf8>>, Event} ), {element, Tag, _pipe@1, Children}; {leaf_element, Tag@1, Attrs@1} -> _pipe@2 = Attrs@1, _pipe@3 = gleam@list:prepend( _pipe@2, {event, <<"gliew-event"/utf8>>, Event} ), {leaf_element, Tag@1, _pipe@3}; Other -> Other end. -spec find_id(list(nakai@html@attrs:attr(event()))) -> {ok, binary()} | {error, nil}. find_id(Attrs) -> _pipe = Attrs, gleam@list:find_map(_pipe, fun(Attr) -> case Attr of {attr, <<"id"/utf8>>, Id} -> {ok, Id}; _ -> {error, nil} end end). -spec has_id(list(nakai@html@attrs:attr(any()))) -> boolean(). has_id(Attrs) -> _pipe = Attrs, gleam@list:any(_pipe, fun(A) -> case A of {attr, <<"id"/utf8>>, _} -> true; _ -> false end end). -spec extract_event(list(nakai@html@attrs:attr(event()))) -> {ok, event()} | {error, nil}. extract_event(Attrs) -> _pipe = Attrs, gleam@list:find_map(_pipe, fun(Attr) -> case Attr of {event, <<"gliew-event"/utf8>>, Event} -> {ok, Event}; _ -> {error, nil} end end). -spec wrap_live_view(binary(), binary(), binary()) -> binary(). wrap_live_view(Markup, Session_id, Csrf) -> _pipe = nakai@html:'div'( [{attr, <<"hx-ext"/utf8>>, <<"ws"/utf8>>}, {attr, <<"ws-connect"/utf8>>, <<<<<<"/connect?session="/utf8, Session_id/binary>>/binary, "&csrf="/utf8>>/binary, Csrf/binary>>}, {attr, <<"hx-ext"/utf8>>, <<"morph"/utf8>>}], [{unsafe_text, Markup}] ), nakai:to_string(_pipe). -spec render_tree( gleam@erlang@process:subject(message()), gleam@http@request:request(mist@internal@http:body()), nakai@html:node_(event()) ) -> binary(). render_tree(Subject, Request, Tree) -> gleam@erlang@process:call( Subject, fun(_capture) -> {render_tree, _capture, Request, Tree} end, 1000 ). -spec check_connect(gleam@erlang@process:subject(message()), binary(), binary()) -> boolean(). check_connect(Subject, Id, Csrf) -> gleam@erlang@process:call( Subject, fun(_capture) -> {check_connect, _capture, Id, Csrf} end, 1000 ). -spec do_connect( gleam@erlang@process:subject(message()), binary(), gleam@erlang@process:subject(glisten@handler:handler_message()) ) -> nil. do_connect(Subject, Id, Socket) -> gleam@erlang@process:send(Subject, {do_connect, Id, Socket}). -spec worker_loop(worker_message(), worker_state()) -> gleam@otp@actor:next(worker_state()). worker_loop(Msg, State) -> case Msg of {live_update, Markup} -> mist@websocket:send( erlang:element(2, State), {text_message, Markup} ), {continue, State} end. -spec apply_selects( gleam@erlang@process:selector(worker_message()), list(fun((gleam@erlang@process:selector(worker_message())) -> gleam@erlang@process:selector(worker_message()))) ) -> gleam@erlang@process:selector(worker_message()). apply_selects(Selector, Selects) -> _pipe = Selector, gleam@list:fold( Selects, _pipe, fun(Selector@1, Selecting) -> Selecting(Selector@1) end ). -spec start_worker( gleam@erlang@process:subject(glisten@handler:handler_message()), list(fun((gleam@erlang@process:selector(worker_message())) -> gleam@erlang@process:selector(worker_message()))), nakai@html:node_(event()) ) -> {ok, gleam@erlang@process:subject(worker_message())} | {error, gleam@otp@actor:start_error()}. start_worker(Socket, Selects, Tree) -> gleam@otp@actor:start_spec( {spec, fun() -> Selector = begin _pipe = gleam_erlang_ffi:new_selector(), apply_selects(_pipe, Selects) end, {ready, {worker_state, Socket, Tree}, Selector} end, 1000, fun worker_loop/2} ). -spec upgrade_connection( gleam@erlang@process:subject(message()), binary(), binary() ) -> mist@internal@handler:handler_response(). upgrade_connection(Manager, Session, Csrf) -> case check_connect(Manager, Session, Csrf) of false -> _pipe = gleam@http@response:new(401), mist:empty_response(_pipe); true -> _pipe@1 = fun(_, _) -> {ok, nil} end, _pipe@2 = mist@websocket:with_handler(_pipe@1), _pipe@3 = mist@websocket:on_init( _pipe@2, fun(Subject) -> do_connect(Manager, Session, Subject) end ), mist:upgrade(_pipe@3) end. -spec get_params(list({binary(), binary()})) -> {ok, {binary(), binary()}} | {error, nil}. get_params(Params) -> Pmap = gleam@map:from_list(Params), gleam@result:then( gleam@map:get(Pmap, <<"session"/utf8>>), fun(Session) -> gleam@result:then( gleam@map:get(Pmap, <<"csrf"/utf8>>), fun(Csrf) -> {ok, {Session, Csrf}} end ) end ). -spec parse_params(gleam@http@request:request(mist@internal@http:body())) -> {ok, {binary(), binary()}} | {error, nil}. parse_params(Req) -> case erlang:element(9, Req) of {some, Params} -> case gleam@uri:parse_query(Params) of {ok, Params@1} -> _pipe@1 = gleam@list:map( Params@1, fun(P) -> {erlang:element(1, P), begin _pipe = erlang:element(2, P), gleam@string:replace( _pipe, <<" "/utf8>>, <<"+"/utf8>> ) end} end ), get_params(_pipe@1); {error, nil} -> {error, nil} end; none -> {error, nil} end. -spec handle_ws_connect( gleam@erlang@process:subject(message()), gleam@http@request:request(mist@internal@http:body()) ) -> mist@internal@handler:handler_response(). handle_ws_connect(Manager, Req) -> case parse_params(Req) of {ok, {Id, Csrf}} -> upgrade_connection(Manager, Id, Csrf); {error, nil} -> _pipe = gleam@http@response:new(401), mist:empty_response(_pipe) end. -spec handler_func( gleam@erlang@process:subject(message()), fun((gleam@http@request:request(mist@internal@http:body())) -> nakai@html:node_(event())) ) -> fun((glisten@handler:handler_message(), glisten@handler:loop_state(mist@internal@handler:state())) -> gleam@otp@actor:next(glisten@handler:loop_state(mist@internal@handler:state()))). handler_func(Manager, Handler) -> _pipe@2 = fun(Req) -> case {erlang:element(2, Req), erlang:element(8, Req)} of {get, <<"/connect"/utf8>>} -> handle_ws_connect(Manager, Req); {_, _} -> View = Handler(Req), Body = begin _pipe = {html, [], [{head, [{element, <<"script"/utf8>>, [nakai@html@attrs:src( <<"https://unpkg.com/htmx.org@1.9.2/dist/htmx.min.js"/utf8>> )], []}, {element, <<"script"/utf8>>, [nakai@html@attrs:src( <<"https://unpkg.com/htmx.org@1.9.2/dist/ext/ws.js"/utf8>> )], []}, {element, <<"script"/utf8>>, [nakai@html@attrs:src( <<"https://unpkg.com/idiomorph@0.0.8/dist/idiomorph-ext.min.js"/utf8>> )], []}]}, {body, [], [{unsafe_text, render_tree(Manager, Req, View)}]}]}, nakai:to_string(_pipe) end, _pipe@1 = gleam@http@response:new(200), mist:bit_builder_response( _pipe@1, gleam@bit_builder:from_string(Body) ) end end, mist:handler_func(_pipe@2). -spec to_hex_string(bitstring()) -> binary(). to_hex_string(Bstr) -> case Bstr of <<>> -> <<""/utf8>>; <> -> <<(begin _pipe = gleam@int:to_base16(A), gleam@string:lowercase(_pipe) end)/binary, (to_hex_string(Rest))/binary>> end. -spec random_string(integer()) -> binary(). random_string(Len) -> _pipe = crypto:strong_rand_bytes(Len), to_hex_string(_pipe). -spec random_id() -> binary(). random_id() -> <<"g-"/utf8, (random_string(3))/binary>>. -spec ensure_id( list(nakai@html@attrs:attr(event())), gleam@option:option(binary()) ) -> list(nakai@html@attrs:attr(event())). ensure_id(Attrs, Id) -> case has_id(Attrs) of true -> case Id of {some, Id@1} -> gleam@list:map(Attrs, fun(Attr) -> case Attr of {attr, <<"id"/utf8>>, _} -> nakai@html@attrs:id(Id@1); Other -> Other end end); none -> Attrs end; false -> _pipe = Attrs, gleam@list:prepend( _pipe, nakai@html@attrs:id( begin _pipe@1 = Id, gleam@option:unwrap(_pipe@1, random_id()) end ) ) end. -spec process_tree(nakai@html:node_(event()), gleam@option:option(binary())) -> nakai@html:node_(event()). process_tree(Node, Id) -> case Node of {element, Tag, Attrs, Children} -> _pipe = Attrs, _pipe@1 = ensure_id(_pipe, Id), {element, Tag, _pipe@1, Children}; {leaf_element, Tag@1, Attrs@1} -> _pipe@2 = Attrs@1, _pipe@3 = ensure_id(_pipe@2, Id), {leaf_element, Tag@1, _pipe@3}; Other -> Other end. -spec extract_id(nakai@html:node_(event())) -> binary(). extract_id(Node) -> _pipe = case Node of {element, _, Attrs, _} -> find_id(Attrs); {leaf_element, _, Attrs@1} -> find_id(Attrs@1); _ -> {error, nil} end, gleam@result:unwrap(_pipe, random_id()). -spec mount( fun((LUT) -> gleam@erlang@process:subject(LUU)), LUT, fun((gleam@option:option(LUU)) -> nakai@html:node_(event())) ) -> nakai@html:node_(event()). mount(Mount, Context, Render) -> Tree = begin _pipe = Render(none), process_tree(_pipe, none) end, Id = extract_id(Tree), _pipe@5 = fun(Selector) -> Subject = Mount(Context), _pipe@1 = Selector, gleam@erlang@process:selecting( _pipe@1, Subject, fun(Val) -> _pipe@2 = Render({some, Val}), _pipe@3 = process_tree(_pipe@2, {some, Id}), _pipe@4 = nakai:to_inline_string(_pipe@3), {live_update, _pipe@4} end ) end, _pipe@6 = {mount, _pipe@5}, insert_event(_pipe@6, Tree). -spec extract_selects( list(fun((gleam@erlang@process:selector(worker_message())) -> gleam@erlang@process:selector(worker_message()))), nakai@html:node_(event()) ) -> list(fun((gleam@erlang@process:selector(worker_message())) -> gleam@erlang@process:selector(worker_message()))). extract_selects(Selects, Node) -> case Node of {element, _, Attrs, Children} -> case extract_event(Attrs) of {ok, {mount, Selector}} -> _pipe = Selects, gleam@list:prepend(_pipe, Selector); {error, nil} -> _pipe@1 = Children, gleam@list:fold(_pipe@1, Selects, fun extract_selects/2) end; {leaf_element, _, Attrs@1} -> case extract_event(Attrs@1) of {ok, {mount, Selector@1}} -> _pipe@2 = Selects, gleam@list:prepend(_pipe@2, Selector@1); {error, nil} -> Selects end; _ -> Selects end. -spec loop(message(), loop_state()) -> gleam@otp@actor:next(loop_state()). loop(Message, State) -> case Message of {render_tree, From, _, Tree} -> case extract_selects([], Tree) of [] -> gleam@erlang@process:send( From, begin _pipe = Tree, nakai:to_inline_string(_pipe) end ), {continue, State}; Selects -> Sess_id = <<"gliew-"/utf8, (random_string(10))/binary>>, Csrf = <<"g-"/utf8, (random_string(24))/binary>>, gleam@erlang@process:send( From, begin _pipe@1 = Tree, _pipe@2 = nakai:to_inline_string(_pipe@1), wrap_live_view(_pipe@2, Sess_id, Csrf) end ), {continue, {loop_state, begin _pipe@3 = erlang:element(2, State), gleam@map:insert( _pipe@3, Sess_id, {session, Sess_id, Csrf, Selects, Tree} ) end}} end; {check_connect, From@1, Id, Csrf@1} -> case begin _pipe@4 = erlang:element(2, State), gleam@map:get(_pipe@4, Id) end of {ok, Sess} -> gleam@erlang@process:send( From@1, erlang:element(3, Sess) =:= Csrf@1 ); {error, nil} -> gleam@erlang@process:send(From@1, false) end, {continue, State}; {do_connect, Id@1, Socket} -> case begin _pipe@5 = erlang:element(2, State), gleam@map:get(_pipe@5, Id@1) end of {ok, Sess@1} -> _ = start_worker( Socket, erlang:element(4, Sess@1), erlang:element(5, Sess@1) ), nil; {error, nil} -> nil end, {continue, State} end. -spec start_manager() -> {ok, gleam@erlang@process:subject(message())} | {error, gleam@otp@actor:start_error()}. start_manager() -> gleam@otp@actor:start({loop_state, gleam@map:new()}, fun loop/2). -spec serve( integer(), fun((gleam@http@request:request(mist@internal@http:body())) -> nakai@html:node_(event())) ) -> {ok, nil} | {error, glisten:start_error()}. serve(Port, Handler) -> gleam@result:'try'( begin _pipe = start_manager(), gleam@result:map_error(_pipe, fun(Err) -> case Err of init_timeout -> acceptor_timeout; {init_failed, Reason} -> {acceptor_failed, Reason}; {init_crashed, Any} -> {acceptor_crashed, Any} end end) end, fun(Manager) -> mist:serve(Port, handler_func(Manager, Handler)) end ).