Current section
Files
Jump to
Current section
Files
src/sprocket@sprocket.erl
-module(sprocket@sprocket).
-compile([no_auto_import, nowarn_unused_vars]).
-export([stop/1, get_id/1, get_rendered/1, get_handler/2, get_client_hook/2, render/1, render_update/1, start/4]).
-export_type([state/0, message/0]).
-type state() :: {state,
sprocket@internal@utils@unique:unique(),
gleam@option:option(gleam@erlang@process:subject(message())),
gleam@option:option(fun(() -> nil)),
sprocket@context:context(),
gleam@option:option(sprocket@context:updater(sprocket@internal@patch:patch())),
gleam@option:option(sprocket@render:rendered_element())}.
-type message() :: shutdown |
{set_self, gleam@erlang@process:subject(message())} |
{begin_self_destruct, integer()} |
cancel_self_destruct |
{get_rendered,
gleam@erlang@process:subject(gleam@option:option(sprocket@render:rendered_element()))} |
{get_id,
gleam@erlang@process:subject(sprocket@internal@utils@unique:unique())} |
{set_render_update, fun(() -> nil)} |
{render, gleam@erlang@process:subject(sprocket@render:rendered_element())} |
render_update |
{set_update_hook,
fun((sprocket@internal@utils@unique:unique(), fun((sprocket@hooks:hook()) -> sprocket@hooks:hook())) -> nil)} |
{update_hook,
sprocket@internal@utils@unique:unique(),
fun((sprocket@hooks:hook()) -> sprocket@hooks:hook())} |
{get_event_handler,
gleam@erlang@process:subject({ok, sprocket@context:event_handler()} |
{error, nil}),
sprocket@internal@utils@unique:unique()} |
{get_client_hook,
gleam@erlang@process:subject({ok, sprocket@hooks:hook()} | {error, nil}),
sprocket@internal@utils@unique:unique()}.
-spec stop(gleam@erlang@process:subject(message())) -> nil.
stop(Actor) ->
gleam@otp@actor:send(Actor, shutdown).
-spec get_id(gleam@erlang@process:subject(message())) -> sprocket@internal@utils@unique:unique().
get_id(Actor) ->
gleam@otp@actor:call(
Actor,
fun(_capture) -> {get_id, _capture} end,
sprocket@internal@constants:call_timeout()
).
-spec get_rendered(gleam@erlang@process:subject(message())) -> gleam@option:option(sprocket@render:rendered_element()).
get_rendered(Actor) ->
gleam@otp@actor:call(
Actor,
fun(_capture) -> {get_rendered, _capture} end,
sprocket@internal@constants:call_timeout()
).
-spec get_handler(gleam@erlang@process:subject(message()), binary()) -> {ok,
sprocket@context:event_handler()} |
{error, nil}.
get_handler(Actor, Id) ->
gleam@otp@actor:call(
Actor,
fun(_capture) ->
{get_event_handler,
_capture,
sprocket@internal@utils@unique:from_string(Id)}
end,
sprocket@internal@constants:call_timeout()
).
-spec get_client_hook(gleam@erlang@process:subject(message()), binary()) -> {ok,
sprocket@hooks:hook()} |
{error, nil}.
get_client_hook(Actor, Id) ->
gleam@otp@actor:call(
Actor,
fun(_capture) ->
{get_client_hook,
_capture,
sprocket@internal@utils@unique:from_string(Id)}
end,
sprocket@internal@constants:call_timeout()
).
-spec render(gleam@erlang@process:subject(message())) -> sprocket@render:rendered_element().
render(Actor) ->
gleam@otp@actor:call(
Actor,
fun(_capture) -> {render, _capture} end,
sprocket@internal@constants:call_timeout()
).
-spec render_update(gleam@erlang@process:subject(message())) -> nil.
render_update(Actor) ->
gleam@otp@actor:send(Actor, render_update).
-spec build_hooks_map(
sprocket@render:rendered_element(),
gleam@map:map_(sprocket@internal@utils@unique:unique(), sprocket@hooks:hook())
) -> gleam@map:map_(sprocket@internal@utils@unique:unique(), sprocket@hooks:hook()).
build_hooks_map(Node, Acc) ->
case Node of
{rendered_component, _, _, _, Hooks, Children} ->
Acc@2 = sprocket@internal@utils@ordered_map:fold(
Hooks,
Acc,
fun(Acc@1, Hook) ->
{keyed_item, _, Hook@1} = Hook,
case Hook@1 of
{callback, Id, _, _, _} ->
gleam@map:insert(Acc@1, Id, Hook@1);
{effect, Id@1, _, _, _} ->
gleam@map:insert(Acc@1, Id@1, Hook@1);
{reducer, Id@2, _, _} ->
gleam@map:insert(Acc@1, Id@2, Hook@1);
{state, Id@3, _} ->
gleam@map:insert(Acc@1, Id@3, Hook@1);
{client, Id@4, _, _} ->
gleam@map:insert(Acc@1, Id@4, Hook@1)
end
end
),
gleam@list:fold(
Children,
Acc@2,
fun(Acc@3, Child) ->
gleam@map:merge(Acc@3, build_hooks_map(Child, Acc@3))
end
);
{rendered_element, _, _, _, Children@1} ->
gleam@list:fold(
Children@1,
Acc,
fun(Acc@4, Child@1) ->
gleam@map:merge(Acc@4, build_hooks_map(Child@1, Acc@4))
end
);
_ ->
Acc
end.
-spec cleanup_hooks(sprocket@render:rendered_element()) -> nil.
cleanup_hooks(Rendered) ->
_pipe = build_hooks_map(Rendered, gleam@map:new()),
_pipe@1 = gleam@map:values(_pipe),
gleam@list:each(_pipe@1, fun(Hook) -> case Hook of
{effect, _, _, _, Prev} ->
case Prev of
{some, {effect_result, {some, Cleanup}, _}} ->
Cleanup();
_ ->
nil
end;
{reducer, _, _, Cleanup@1} ->
Cleanup@1();
_ ->
nil
end end).
-spec cleanup_disposed_hooks(
sprocket@render:rendered_element(),
sprocket@render:rendered_element()
) -> nil.
cleanup_disposed_hooks(Prev_rendered, Rendered) ->
Prev_hooks = build_hooks_map(Prev_rendered, gleam@map:new()),
New_hooks = build_hooks_map(Rendered, gleam@map:new()),
Removed_hooks = begin
_pipe = Prev_hooks,
_pipe@1 = gleam@map:keys(_pipe),
gleam@list:filter(
_pipe@1,
fun(Id) -> not gleam@map:has_key(New_hooks, Id) end
)
end,
_pipe@2 = Removed_hooks,
gleam@list:each(
_pipe@2,
fun(Id@1) -> case gleam@map:get(Prev_hooks, Id@1) of
{ok, {effect, _, _, _, Prev}} ->
case Prev of
{some, {effect_result, {some, Cleanup}, _}} ->
Cleanup();
_ ->
nil
end;
{ok, {reducer, _, _, Cleanup@1}} ->
Cleanup@1();
_ ->
nil
end end
).
-spec maybe_cleanup_and_rerun_effect(
gleam@option:option(fun(() -> nil)),
fun(() -> gleam@option:option(fun(() -> nil))),
gleam@option:option(list(gleam@dynamic:dynamic_()))
) -> sprocket@hooks:effect_result().
maybe_cleanup_and_rerun_effect(Cleanup, Effect_fn, Deps) ->
case Cleanup of
{some, Cleanup_fn} ->
Cleanup_fn(),
{effect_result, Effect_fn(), Deps};
_ ->
{effect_result, Effect_fn(), Deps}
end.
-spec run_effect(
fun(() -> gleam@option:option(fun(() -> nil))),
sprocket@hooks:hook_trigger(),
gleam@option:option(sprocket@hooks:effect_result())
) -> sprocket@hooks:effect_result().
run_effect(Effect_fn, Trigger, Prev) ->
case Trigger of
on_mount ->
case Prev of
{some, Prev_effect_result} ->
Prev_effect_result;
none ->
{effect_result, Effect_fn(), {some, []}};
_ ->
sprocket@internal@exceptions:throw_on_unexpected_hook_result(
{<<"handle_effect"/utf8>>, Prev}
)
end;
on_update ->
case Prev of
{some, {effect_result, Cleanup, _}} ->
maybe_cleanup_and_rerun_effect(Cleanup, Effect_fn, none);
_ ->
{effect_result, Effect_fn(), none}
end;
{with_deps, Deps} ->
case Prev of
{some, {effect_result, Cleanup@1, {some, Prev_deps}}} ->
case sprocket@hooks:compare_deps(Prev_deps, Deps) of
{changed, _} ->
maybe_cleanup_and_rerun_effect(
Cleanup@1,
Effect_fn,
{some, Deps}
);
unchanged ->
{effect_result, Cleanup@1, {some, Deps}}
end;
none ->
maybe_cleanup_and_rerun_effect(
none,
Effect_fn,
{some, Deps}
);
_ ->
sprocket@internal@exceptions:throw_on_unexpected_hook_result(
{<<"handle_effect"/utf8>>, Prev}
)
end
end.
-spec find_rendered_hook(
sprocket@render:rendered_element(),
fun((sprocket@hooks:hook()) -> boolean())
) -> gleam@option:option(sprocket@hooks:hook()).
find_rendered_hook(Node, Find_by) ->
case Node of
{rendered_component, _, _, _, Hooks, Children} ->
case sprocket@internal@utils@ordered_map:find(
Hooks,
fun(Keyed_item) -> Find_by(erlang:element(3, Keyed_item)) end
) of
{ok, {keyed_item, _, Hook}} ->
{some, Hook};
_ ->
gleam@list:fold(
Children,
none,
fun(Acc, Child) -> case Acc of
{some, _} ->
Acc;
_ ->
find_rendered_hook(Child, Find_by)
end end
)
end;
{rendered_element, _, _, _, Children@1} ->
gleam@list:fold(
Children@1,
none,
fun(Acc@1, Child@1) -> case Acc@1 of
{some, _} ->
Acc@1;
_ ->
find_rendered_hook(Child@1, Find_by)
end end
);
_ ->
none
end.
-spec process_next_hook(
sprocket@internal@utils@ordered_map:ordered_map_iter(integer(), sprocket@hooks:hook()),
{list(sprocket@internal@utils@ordered_map:keyed_item(integer(), sprocket@hooks:hook())),
gleam@map:map_(integer(), sprocket@hooks:hook()),
integer()},
fun((sprocket@hooks:hook()) -> sprocket@hooks:hook())
) -> {list(sprocket@internal@utils@ordered_map:keyed_item(integer(), sprocket@hooks:hook())),
gleam@map:map_(integer(), sprocket@hooks:hook()),
integer()}.
process_next_hook(Iter, Acc, Process_hook) ->
case sprocket@internal@utils@ordered_map:next(Iter) of
{ok, {Iter@1, {keyed_item, Index, Hook}}} ->
{Ordered, By_index, Size} = Acc,
Updated = Process_hook(Hook),
process_next_hook(
Iter@1,
{[{keyed_item, Index, Updated} | Ordered],
gleam@map:insert(By_index, Index, Updated),
Size + 1},
Process_hook
);
{error, _} ->
Acc
end.
-spec process_hooks(
sprocket@internal@utils@ordered_map:ordered_map(integer(), sprocket@hooks:hook()),
fun((sprocket@hooks:hook()) -> sprocket@hooks:hook())
) -> sprocket@internal@utils@ordered_map:ordered_map(integer(), sprocket@hooks:hook()).
process_hooks(Hooks, Process_hook) ->
{R_ordered, By_index, Size} = begin
_pipe = Hooks,
_pipe@1 = sprocket@internal@utils@ordered_map:iter(_pipe),
process_next_hook(_pipe@1, {[], gleam@map:new(), 0}, Process_hook)
end,
sprocket@internal@utils@ordered_map:from(
gleam@list:reverse(R_ordered),
By_index,
Size
).
-spec traverse_rendered_hooks(
sprocket@render:rendered_element(),
fun((sprocket@hooks:hook()) -> sprocket@hooks:hook())
) -> sprocket@render:rendered_element().
traverse_rendered_hooks(Node, Process_hook) ->
case Node of
{rendered_component, Fc, Key, Props, Hooks, Children} ->
Processed_hooks = process_hooks(Hooks, Process_hook),
R_children = gleam@list:fold(
Children,
[],
fun(Acc, Child) ->
[traverse_rendered_hooks(Child, Process_hook) | Acc]
end
),
{rendered_component,
Fc,
Key,
Props,
Processed_hooks,
gleam@list:reverse(R_children)};
{rendered_element, Tag, Key@1, Hooks@1, Children@1} ->
R_children@1 = gleam@list:fold(
Children@1,
[],
fun(Acc@1, Child@1) ->
[traverse_rendered_hooks(Child@1, Process_hook) | Acc@1]
end
),
{rendered_element,
Tag,
Key@1,
Hooks@1,
gleam@list:reverse(R_children@1)};
_ ->
Node
end.
-spec process_state_hooks(
state(),
fun((sprocket@hooks:hook()) -> sprocket@hooks:hook())
) -> state().
process_state_hooks(State, Process_hook) ->
Rendered = gleam@option:map(
erlang:element(7, State),
fun(Node) -> traverse_rendered_hooks(Node, Process_hook) end
),
erlang:setelement(7, State, Rendered).
-spec run_effects(state()) -> state().
run_effects(State) ->
process_state_hooks(State, fun(Hook) -> case Hook of
{effect, Id, Effect_fn, Trigger, Prev} ->
Result = run_effect(Effect_fn, Trigger, Prev),
{effect, Id, Effect_fn, Trigger, {some, Result}};
Other ->
Other
end end).
-spec update_hook_state(
state(),
sprocket@internal@utils@unique:unique(),
fun((sprocket@hooks:hook()) -> sprocket@hooks:hook())
) -> state().
update_hook_state(State, Hook_id, Update) ->
Rendered = gleam@option:map(
erlang:element(7, State),
fun(Node) -> traverse_rendered_hooks(Node, fun(Hook) -> case Hook of
{state, Id, _} ->
case Id =:= Hook_id of
true ->
Update(Hook);
false ->
Hook
end;
_ ->
Hook
end end) end
),
erlang:setelement(7, State, Rendered).
-spec handle_message(message(), state()) -> gleam@otp@actor:next(message(), state()).
handle_message(Message, State) ->
case Message of
shutdown ->
case erlang:element(7, State) of
{some, Rendered} ->
cleanup_hooks(Rendered),
nil;
_ ->
nil
end,
{stop, normal};
{set_self, Self} ->
gleam@otp@actor:continue(erlang:setelement(3, State, {some, Self}));
{begin_self_destruct, Timeout} ->
case erlang:element(3, State) of
{some, Self@1} ->
Cancel = sprocket@internal@utils@timer:interval(
Timeout,
fun() -> gleam@otp@actor:send(Self@1, shutdown) end
),
gleam@otp@actor:continue(
erlang:setelement(4, State, {some, Cancel})
);
_ ->
gleam@otp@actor:continue(State)
end;
cancel_self_destruct ->
case erlang:element(4, State) of
{some, Cancel@1} ->
Cancel@1(),
gleam@otp@actor:continue(erlang:setelement(4, State, none));
_ ->
gleam@otp@actor:continue(State)
end,
gleam@otp@actor:continue(State);
{get_rendered, Reply_with} ->
gleam@otp@actor:send(Reply_with, erlang:element(7, State)),
gleam@otp@actor:continue(State);
{get_id, Reply_with@1} ->
gleam@otp@actor:send(Reply_with@1, erlang:element(2, State)),
gleam@otp@actor:continue(State);
{set_render_update, Render_update} ->
gleam@otp@actor:continue(
erlang:setelement(
5,
State,
erlang:setelement(
5,
erlang:element(5, State),
Render_update
)
)
);
{render, Reply_with@2} ->
State@1 = case State of
{state,
_,
_,
_,
{context, View, _, _, _, _, _} = Ctx,
_,
Prev_rendered} ->
{render_result, Ctx@1, Rendered@1} = begin
_pipe = Ctx,
_pipe@1 = sprocket@context:reset_for_render(_pipe),
sprocket@render:live_render(
_pipe@1,
View,
none,
Prev_rendered
)
end,
gleam@otp@actor:send(Reply_with@2, Rendered@1),
case Prev_rendered of
{some, Prev_rendered@1} ->
cleanup_disposed_hooks(Prev_rendered@1, Rendered@1);
_ ->
nil
end,
run_effects(
erlang:setelement(
7,
erlang:setelement(5, State, Ctx@1),
{some, Rendered@1}
)
);
_ ->
sprocket@internal@logger:error(
<<"No view found! A view must be provided to render."/utf8>>
),
State
end,
gleam@otp@actor:continue(State@1);
render_update ->
case State of
{state,
_,
_,
_,
{context, View@1, _, _, _, _, _} = Ctx@2,
{some, Updater},
{some, Prev_rendered@2}} ->
{render_result, Ctx@3, Rendered@2} = begin
_pipe@2 = Ctx@2,
_pipe@3 = sprocket@context:reset_for_render(_pipe@2),
sprocket@render:live_render(
_pipe@3,
View@1,
none,
{some, Prev_rendered@2}
)
end,
Update = sprocket@internal@patch:create(
Prev_rendered@2,
Rendered@2
),
case (erlang:element(2, Updater))(Update) of
{ok, _} ->
nil;
{error, _} ->
sprocket@internal@logger:error(
<<"Failed to send update patch!"/utf8>>
),
nil
end,
cleanup_disposed_hooks(Prev_rendered@2, Rendered@2),
State@2 = run_effects(
erlang:setelement(
7,
erlang:setelement(5, State, Ctx@3),
{some, Rendered@2}
)
),
gleam@otp@actor:continue(State@2);
{state, _, _, _, _, none, _} ->
sprocket@internal@logger:error(
<<"No updater found! An updater must be provided to send updates to the client."/utf8>>
),
gleam@otp@actor:continue(State);
{state, _, _, _, _, _, none} ->
sprocket@internal@logger:error(
<<"No previous render found! View must be rendered at least once before updates can be sent."/utf8>>
),
gleam@otp@actor:continue(State);
_ ->
sprocket@internal@logger:error(
<<"No view found! A view must be provided to render."/utf8>>
),
gleam@otp@actor:continue(State)
end;
{set_update_hook, Update_hook} ->
gleam@otp@actor:continue(
erlang:setelement(
5,
State,
erlang:setelement(6, erlang:element(5, State), Update_hook)
)
);
{update_hook, Id, Updater@1} ->
gleam@otp@actor:continue(update_hook_state(State, Id, Updater@1));
{get_event_handler, Reply_with@3, Id@1} ->
Handler = gleam@list:find(
erlang:element(4, erlang:element(5, State)),
fun(H) ->
{event_handler, I, _} = H,
I =:= Id@1
end
),
gleam@erlang@process:send(Reply_with@3, Handler),
gleam@otp@actor:continue(State);
{get_client_hook, Reply_with@4, Id@2} ->
case erlang:element(7, State) of
{some, Rendered@3} ->
Hook@1 = find_rendered_hook(
Rendered@3,
fun(Hook) -> case Hook of
{client, I@1, _, _} ->
sprocket@internal@utils@unique:equals(
I@1,
Id@2
);
_ ->
false
end end
),
gleam@erlang@process:send(
Reply_with@4,
gleam@option:to_result(Hook@1, nil)
);
none ->
gleam@erlang@process:send(Reply_with@4, {error, nil})
end,
gleam@otp@actor:continue(State)
end.
-spec start(
sprocket@internal@utils@unique:unique(),
sprocket@context:element(),
gleam@option:option(sprocket@context:updater(sprocket@internal@patch:patch())),
gleam@option:option(sprocket@context:dispatcher())
) -> gleam@erlang@process:subject(message()).
start(Id, View, Updater, Dispatcher) ->
_assert_subject = gleam@otp@actor:start(
{state,
Id,
none,
none,
sprocket@context:new(View, Dispatcher),
Updater,
none},
fun handle_message/2
),
{ok, Actor} = case _assert_subject of
{ok, _} -> _assert_subject;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Assertion pattern match failed"/utf8>>,
value => _assert_fail,
module => <<"sprocket/sprocket"/utf8>>,
function => <<"start"/utf8>>,
line => 255})
end,
gleam@otp@actor:send(Actor, {set_self, Actor}),
gleam@otp@actor:send(
Actor,
{set_render_update,
fun() -> gleam@otp@actor:send(Actor, render_update) end}
),
gleam@otp@actor:send(
Actor,
{set_update_hook,
fun(Id@1, Updater@1) ->
gleam@otp@actor:send(Actor, {update_hook, Id@1, Updater@1})
end}
),
Actor.