-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 => 321}) 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 => 380}) 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))), sprocket@context:hook_trigger(), gleam@option:option(sprocket@context:effect_result()) ) -> sprocket@context: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, []}} 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@context: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_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( gleam@list: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, gleam@list: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, gleam@list: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, Trigger, Prev} -> Result = run_effect(Effect_fn, Trigger, Prev), {effect, Id, Effect_fn, Trigger, {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 => 276}) 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}).