Current section

Files

Jump to
sprocket src sprocket@runtime.erl
Raw

src/sprocket@runtime.erl

-module(sprocket@runtime).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-export([stop/1, get_reconciled/1, process_event/3, process_event_immediate/3, process_client_hook/5, render_update/1, reconcile_immediate/1, start/3]).
-export_type([rendered_update/0, state/0, message/0]).
-type rendered_update() :: {full_update,
sprocket@internal@reconcile:reconciled_element()} |
{patch_update, sprocket@internal@patch:patch()}.
-opaque state() :: {state,
sprocket@context:context(),
sprocket@context:updater(rendered_update()),
gleam@option:option(sprocket@internal@reconcile:reconciled_element()),
gleam@erlang@process:subject(ids@cuid:message())}.
-opaque message() :: shutdown |
{get_reconciled,
gleam@erlang@process:subject(gleam@option:option(sprocket@internal@reconcile:reconciled_element()))} |
{process_event, binary(), gleam@option:option(binary())} |
{process_event_immediate,
gleam@erlang@process:subject({ok, nil} | {error, nil}),
binary(),
gleam@option:option(binary())} |
{process_client_hook,
binary(),
binary(),
gleam@option:option(gleam@dynamic:dynamic_()),
fun((binary(), gleam@option:option(binary())) -> {ok, nil} |
{error, nil})} |
{update_hook_state,
sprocket@internal@utils@unique:unique(),
fun((sprocket@context:hook()) -> sprocket@context:hook())} |
{reconcile_immediate,
gleam@erlang@process:subject(sprocket@internal@reconcile:reconciled_element())} |
render_update.
-spec stop(gleam@erlang@process:subject(message())) -> nil.
stop(Actor) ->
sprocket@internal@logger:debug(<<"actor.send Shutdown"/utf8>>),
gleam@otp@actor:send(Actor, shutdown).
-spec get_reconciled(gleam@erlang@process:subject(message())) -> gleam@option:option(sprocket@internal@reconcile:reconciled_element()).
get_reconciled(Actor) ->
sprocket@internal@logger:debug(<<"process.try_call GetReconciled"/utf8>>),
case gleam@erlang@process:try_call(
Actor,
fun(_capture) -> {get_reconciled, _capture} end,
1000
) of
{ok, Rendered} ->
Rendered;
{error, Err} ->
sprocket@internal@logger:error(
<<"Error getting rendered view from runtime actor"/utf8>>
),
gleam@io:debug(Err),
erlang:error(#{gleam_error => panic,
message => <<"panic expression evaluated"/utf8>>,
module => <<"sprocket/runtime"/utf8>>,
function => <<"get_reconciled"/utf8>>,
line => 320})
end.
-spec process_event(
gleam@erlang@process:subject(message()),
binary(),
gleam@option:option(binary())
) -> nil.
process_event(Actor, Id, Payload) ->
sprocket@internal@logger:debug(<<"process.try_call ProcessEvent"/utf8>>),
gleam@otp@actor:send(Actor, {process_event, Id, Payload}).
-spec process_event_immediate(
gleam@erlang@process:subject(message()),
binary(),
gleam@option:option(binary())
) -> {ok, nil} | {error, nil}.
process_event_immediate(Actor, Id, Payload) ->
sprocket@internal@logger:debug(
<<"process.try_call ProcessEventImmediate"/utf8>>
),
case gleam@erlang@process:try_call(
Actor,
fun(_capture) -> {process_event_immediate, _capture, Id, Payload} end,
1000
) of
{ok, Result} ->
Result;
{error, Err} ->
sprocket@internal@logger:error(
<<"Error processing event from runtime actor"/utf8>>
),
gleam@io:debug(Err),
{error, nil}
end.
-spec process_client_hook(
gleam@erlang@process:subject(message()),
binary(),
binary(),
gleam@option:option(gleam@dynamic:dynamic_()),
fun((binary(), gleam@option:option(binary())) -> {ok, nil} | {error, nil})
) -> nil.
process_client_hook(Actor, Id, Event, Payload, Reply_dispatcher) ->
sprocket@internal@logger:debug(<<"process.try_call GetClientHook"/utf8>>),
gleam@otp@actor:send(
Actor,
{process_client_hook, Id, Event, Payload, Reply_dispatcher}
).
-spec render_update(gleam@erlang@process:subject(message())) -> nil.
render_update(Actor) ->
sprocket@internal@logger:debug(<<"actor.send RenderUpdate"/utf8>>),
gleam@otp@actor:send(Actor, render_update).
-spec reconcile_immediate(gleam@erlang@process:subject(message())) -> sprocket@internal@reconcile:reconciled_element().
reconcile_immediate(Actor) ->
sprocket@internal@logger:debug(<<"process.try_call Reconcile"/utf8>>),
case gleam@erlang@process:try_call(
Actor,
fun(_capture) -> {reconcile_immediate, _capture} end,
1000
) of
{ok, Reconciled} ->
Reconciled;
{error, Err} ->
sprocket@internal@logger:error(
<<"Error reconciling view from runtime actor"/utf8>>
),
gleam@io:debug(Err),
erlang:error(#{gleam_error => panic,
message => <<"panic expression evaluated"/utf8>>,
module => <<"sprocket/runtime"/utf8>>,
function => <<"reconcile_immediate"/utf8>>,
line => 379})
end.
-spec build_hooks_map(
sprocket@internal@reconcile:reconciled_element(),
gleam@dict:dict(sprocket@internal@utils@unique:unique(), sprocket@context:hook())
) -> gleam@dict:dict(sprocket@internal@utils@unique:unique(), sprocket@context:hook()).
build_hooks_map(Node, Acc) ->
case Node of
{reconciled_component, _, _, _, Hooks, El} ->
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@dict:insert(Acc@1, Id, Hook@1);
{memo, Id@1, _, _} ->
gleam@dict:insert(Acc@1, Id@1, Hook@1);
{handler, Id@2, _} ->
gleam@dict:insert(Acc@1, Id@2, Hook@1);
{effect, Id@3, _, _, _} ->
gleam@dict:insert(Acc@1, Id@3, Hook@1);
{reducer, Id@4, _, _} ->
gleam@dict:insert(Acc@1, Id@4, Hook@1);
{state, Id@5, _} ->
gleam@dict:insert(Acc@1, Id@5, Hook@1);
{client, Id@6, _, _} ->
gleam@dict:insert(Acc@1, Id@6, Hook@1)
end
end
),
build_hooks_map(El, Acc@2);
{reconciled_element, _, _, _, Children} ->
gleam@list:fold(
Children,
Acc,
fun(Acc@3, Child) ->
gleam@dict:merge(Acc@3, build_hooks_map(Child, Acc@3))
end
);
{reconciled_fragment, _, Children@1} ->
gleam@list:fold(
Children@1,
Acc,
fun(Acc@4, Child@1) ->
gleam@dict:merge(Acc@4, build_hooks_map(Child@1, Acc@4))
end
);
_ ->
Acc
end.
-spec cleanup_hooks(sprocket@internal@reconcile:reconciled_element()) -> nil.
cleanup_hooks(Rendered) ->
_pipe = build_hooks_map(Rendered, gleam@dict:new()),
_pipe@1 = gleam@dict: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 run_cleanup_for_disposed_hooks(
sprocket@internal@reconcile:reconciled_element(),
sprocket@internal@reconcile:reconciled_element()
) -> nil.
run_cleanup_for_disposed_hooks(Prev_rendered, Rendered) ->
Prev_hooks = build_hooks_map(Prev_rendered, gleam@dict:new()),
New_hooks = build_hooks_map(Rendered, gleam@dict:new()),
Removed_hooks = begin
_pipe = Prev_hooks,
_pipe@1 = gleam@dict:keys(_pipe),
gleam@list:filter(
_pipe@1,
fun(Id) -> not gleam@dict:has_key(New_hooks, Id) end
)
end,
_pipe@2 = Removed_hooks,
gleam@list:each(
_pipe@2,
fun(Id@1) -> case gleam@dict: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@context: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))),
list(gleam@dynamic:dynamic_()),
gleam@option:option(sprocket@context:effect_result())
) -> sprocket@context:effect_result().
run_effect(Effect_fn, Deps, Prev) ->
case Prev of
{some, {effect_result, Cleanup, {some, Prev_deps}}} ->
case sprocket@context:compare_deps(Prev_deps, Deps) of
{changed, _} ->
maybe_cleanup_and_rerun_effect(
Cleanup,
Effect_fn,
{some, Deps}
);
unchanged ->
{effect_result, Cleanup, {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.
-spec find_reconciled_hook(
sprocket@internal@reconcile:reconciled_element(),
fun((sprocket@context:hook()) -> boolean())
) -> gleam@option:option(sprocket@context:hook()).
find_reconciled_hook(Node, Find_by) ->
case Node of
{reconciled_component, _, _, _, Hooks, El} ->
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};
_ ->
find_reconciled_hook(El, Find_by)
end;
{reconciled_element, _, _, _, Children} ->
gleam@list:fold(Children, none, fun(Acc, Child) -> case Acc of
{some, _} ->
Acc;
_ ->
find_reconciled_hook(Child, Find_by)
end end);
{reconciled_fragment, _, Children@1} ->
gleam@list:fold(
Children@1,
none,
fun(Acc@1, Child@1) -> case Acc@1 of
{some, _} ->
Acc@1;
_ ->
find_reconciled_hook(Child@1, Find_by)
end end
);
_ ->
none
end.
-spec process_next_hook(
sprocket@internal@utils@ordered_map:ordered_map_iter(integer(), sprocket@context:hook()),
{list(sprocket@internal@utils@ordered_map:keyed_item(integer(), sprocket@context:hook())),
gleam@dict:dict(integer(), sprocket@context:hook()),
integer()},
fun((sprocket@context:hook()) -> sprocket@context:hook())
) -> {list(sprocket@internal@utils@ordered_map:keyed_item(integer(), sprocket@context:hook())),
gleam@dict:dict(integer(), sprocket@context: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@dict:insert(By_index, Index, Updated),
Size + 1},
Process_hook
);
{error, _} ->
Acc
end.
-spec process_hooks(
sprocket@internal@utils@ordered_map:ordered_map(integer(), sprocket@context:hook()),
fun((sprocket@context:hook()) -> sprocket@context:hook())
) -> sprocket@internal@utils@ordered_map:ordered_map(integer(), sprocket@context: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@dict:new(), 0}, Process_hook)
end,
sprocket@internal@utils@ordered_map:from(
lists:reverse(R_ordered),
By_index,
Size
).
-spec traverse_rendered_hooks(
sprocket@internal@reconcile:reconciled_element(),
fun((sprocket@context:hook()) -> sprocket@context:hook())
) -> sprocket@internal@reconcile:reconciled_element().
traverse_rendered_hooks(Node, Process_hook) ->
case Node of
{reconciled_component, Fc, Key, Props, Hooks, El} ->
Processed_hooks = process_hooks(Hooks, Process_hook),
{reconciled_component,
Fc,
Key,
Props,
Processed_hooks,
traverse_rendered_hooks(El, Process_hook)};
{reconciled_element, Tag, Key@1, Hooks@1, Children} ->
R_children = gleam@list:fold(
Children,
[],
fun(Acc, Child) ->
[traverse_rendered_hooks(Child, Process_hook) | Acc]
end
),
{reconciled_element, Tag, Key@1, Hooks@1, lists:reverse(R_children)};
{reconciled_fragment, Key@2, 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
),
{reconciled_fragment, Key@2, lists:reverse(R_children@1)};
_ ->
Node
end.
-spec process_state_hooks(
sprocket@internal@reconcile:reconciled_element(),
fun((sprocket@context:hook()) -> sprocket@context:hook())
) -> sprocket@internal@reconcile:reconciled_element().
process_state_hooks(Rendered, Process_hook) ->
traverse_rendered_hooks(Rendered, Process_hook).
-spec run_effects(sprocket@internal@reconcile:reconciled_element()) -> sprocket@internal@reconcile:reconciled_element().
run_effects(Rendered) ->
process_state_hooks(Rendered, fun(Hook) -> case Hook of
{effect, Id, Effect_fn, Deps, Prev} ->
Result = run_effect(Effect_fn, Deps, Prev),
{effect, Id, Effect_fn, Deps, {some, Result}};
Other ->
Other
end end).
-spec do_reconciliation(
sprocket@context:context(),
sprocket@context:element(),
gleam@option:option(sprocket@internal@reconcile:reconciled_element())
) -> {sprocket@context:context(),
sprocket@internal@reconcile:reconciled_element()}.
do_reconciliation(Ctx, View, Prev) ->
sprocket@internal@utils@timer:timed_operation(
<<"runtime.reconcile"/utf8>>,
fun() ->
{reconciled_result, Ctx@1, Reconciled} = begin
_pipe = Ctx,
_pipe@1 = sprocket@context:prepare_for_reconciliation(_pipe),
sprocket@internal@reconcilers@recursive:reconcile(
_pipe@1,
View,
none,
Prev
)
end,
gleam@option:map(
Prev,
fun(Prev@1) ->
run_cleanup_for_disposed_hooks(Prev@1, Reconciled)
end
),
Reconciled@1 = run_effects(Reconciled),
{Ctx@1, Reconciled@1}
end
).
-spec handle_message(message(), state()) -> gleam@otp@actor:next(message(), state()).
handle_message(Message, State) ->
case Message of
shutdown ->
case erlang:element(4, State) of
{some, Reconciled} ->
cleanup_hooks(Reconciled),
nil;
_ ->
nil
end,
{stop, normal};
{get_reconciled, Reply_with} ->
gleam@otp@actor:send(Reply_with, erlang:element(4, State)),
gleam@otp@actor:continue(State);
{process_event, Id, Payload} ->
Handler = gleam@list:find(
erlang:element(4, erlang:element(2, State)),
fun(H) ->
{identifiable_handler, I, _} = H,
sprocket@internal@utils@unique:to_string(I) =:= Id
end
),
case Handler of
{ok, {identifiable_handler, _, Handler_fn}} ->
_pipe = Payload,
_pipe@1 = gleam@option:map(
_pipe,
fun sprocket@context:callback_param_from_string/1
),
Handler_fn(_pipe@1),
gleam@otp@actor:continue(State);
_ ->
sprocket@internal@logger:error(
<<"No handler found with id: "/utf8, Id/binary>>
),
gleam@otp@actor:continue(State)
end;
{process_event_immediate, Reply_with@1, Id@1, Payload@1} ->
Handler@1 = gleam@list:find(
erlang:element(4, erlang:element(2, State)),
fun(H@1) ->
{identifiable_handler, I@1, _} = H@1,
sprocket@internal@utils@unique:to_string(I@1) =:= Id@1
end
),
case Handler@1 of
{ok, {identifiable_handler, _, Handler_fn@1}} ->
_pipe@2 = Payload@1,
_pipe@3 = gleam@option:map(
_pipe@2,
fun sprocket@context:callback_param_from_string/1
),
Handler_fn@1(_pipe@3),
gleam@otp@actor:send(Reply_with@1, {ok, nil}),
gleam@otp@actor:continue(State);
_ ->
sprocket@internal@logger:error(
<<"No handler found with id: "/utf8, Id@1/binary>>
),
gleam@otp@actor:send(Reply_with@1, {error, nil}),
gleam@otp@actor:continue(State)
end;
{process_client_hook, Id@2, Event, Payload@2, Reply_dispatcher} ->
Client_hook = case erlang:element(4, State) of
{some, Reconciled@1} ->
Hook@1 = find_reconciled_hook(
Reconciled@1,
fun(Hook) -> case Hook of
{client, I@2, _, _} ->
sprocket@internal@utils@unique:to_string(
I@2
)
=:= Id@2;
_ ->
false
end end
),
gleam@option:to_result(Hook@1, nil);
none ->
sprocket@internal@logger:error(
<<"Runtime must be reconciled before processing client hooks"/utf8>>
),
{error, nil}
end,
case Client_hook of
{ok, {client, _, _, Handle_event}} ->
gleam@option:map(
Handle_event,
fun(Handle_event@1) ->
Handle_event@1(Event, Payload@2, Reply_dispatcher)
end
),
gleam@otp@actor:continue(State);
_ ->
sprocket@internal@logger:error(
<<"No client hook found with id: "/utf8, Id@2/binary>>
),
gleam@otp@actor:continue(State)
end;
{update_hook_state, Hook_id, Update_fn} ->
Updated = begin
_pipe@4 = erlang:element(4, State),
gleam@option:map(
_pipe@4,
fun(Node) ->
traverse_rendered_hooks(
Node,
fun(Hook@2) -> case Hook@2 of
{state, Id@3, _} ->
case Id@3 =:= Hook_id of
true ->
Update_fn(Hook@2);
false ->
Hook@2
end;
_ ->
Hook@2
end end
)
end
)
end,
gleam@otp@actor:continue(erlang:setelement(4, State, Updated));
{reconcile_immediate, Reply_with@2} ->
Prev_reconciled = erlang:element(4, State),
View = erlang:element(2, erlang:element(2, State)),
{Ctx, Reconciled@2} = do_reconciliation(
erlang:element(2, State),
View,
Prev_reconciled
),
gleam@otp@actor:send(Reply_with@2, Reconciled@2),
gleam@otp@actor:continue(
erlang:setelement(
4,
erlang:setelement(2, State, Ctx),
{some, Reconciled@2}
)
);
render_update ->
Prev_reconciled@1 = erlang:element(4, State),
Updater = erlang:element(3, State),
View@1 = erlang:element(2, erlang:element(2, State)),
{Ctx@1, Reconciled@3} = do_reconciliation(
erlang:element(2, State),
View@1,
Prev_reconciled@1
),
case Prev_reconciled@1 of
{some, Prev_reconciled@2} ->
Update = sprocket@internal@patch:create(
Prev_reconciled@2,
Reconciled@3
),
case (erlang:element(2, Updater))({patch_update, Update}) of
{ok, _} ->
nil;
{error, _} ->
sprocket@internal@logger:error(
<<"Failed to send update patch!"/utf8>>
),
nil
end;
none ->
case (erlang:element(2, Updater))(
{full_update, Reconciled@3}
) of
{ok, _} ->
nil;
{error, _} ->
sprocket@internal@logger:error(
<<"Failed to send full reconciled view!"/utf8>>
),
nil
end
end,
gleam@otp@actor:continue(
erlang:setelement(
4,
erlang:setelement(2, State, Ctx@1),
{some, Reconciled@3}
)
)
end.
-spec start(
sprocket@context:element(),
sprocket@context:updater(rendered_update()),
gleam@option:option(sprocket@context:dispatcher())
) -> {ok, gleam@erlang@process:subject(message())} |
{error, gleam@otp@actor:start_error()}.
start(View, Updater, Dispatcher) ->
Init = fun() ->
Self = gleam@erlang@process:new_subject(),
Render_update = fun() ->
sprocket@internal@logger:debug(<<"actor.send RenderUpdate"/utf8>>),
gleam@otp@actor:send(Self, render_update)
end,
Update_hook = fun(Id, Updater@1) ->
sprocket@internal@logger:debug(
<<"actor.send UpdateHookState"/utf8>>
),
gleam@otp@actor:send(Self, {update_hook_state, Id, Updater@1})
end,
_assert_subject = begin
_pipe = ids@cuid:start(),
gleam@result:map_error(
_pipe,
fun(Error) ->
sprocket@internal@logger:error(
<<"runtime.start: error starting cuid process"/utf8>>
),
Error
end
)
end,
{ok, Cuid_channel} = 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/runtime"/utf8>>,
function => <<"start"/utf8>>,
line => 275})
end,
State = {state,
sprocket@context:new(
View,
Cuid_channel,
Dispatcher,
Render_update,
Update_hook
),
Updater,
none,
Cuid_channel},
Selector = gleam@erlang@process:selecting(
gleam_erlang_ffi:new_selector(),
Self,
fun gleam@function:identity/1
),
{ready, State, Selector}
end,
gleam@otp@actor:start_spec({spec, Init, 1000, fun handle_message/2}).