Packages

A gleam server-side rendered web framework with live updating UI

Current section

Files

Jump to
gliew src gliew.erl
Raw

src/gliew.erl

-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>>;
<<A:8, Rest/bitstring>> ->
<<(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
).