src/lightspeed@async.erl

-module(lightspeed@async).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lightspeed/async.gleam").
-export([new/1, connected/1, assign_async/2, connect/1, disconnect/1, state_label/1, succeed/3, fail/3, cancel/3, state/2, error_to_string/1]).
-export_type([async_state/1, async_error/0, async_entry/1, runtime/1]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(" LiveView-style async assign/task compatibility model.\n").

-type async_state(GFS) :: idle |
    pending_disconnected |
    {loading, integer()} |
    {succeeded, GFS} |
    {failed, binary()} |
    {cancelled, binary()}.

-type async_error() :: {unknown_task, binary()} |
    {invalid_transition, binary(), binary(), binary()}.

-type async_entry(GFT) :: {async_entry, binary(), async_state(GFT)}.

-opaque runtime(GFU) :: {runtime, boolean(), integer(), list(async_entry(GFU))}.

-file("src/lightspeed/async.gleam", 32).
?DOC(" Build a runtime.\n").
-spec new(boolean()) -> runtime(any()).
new(Connected) ->
    {runtime, Connected, 1, []}.

-file("src/lightspeed/async.gleam", 37).
?DOC(" Connection status.\n").
-spec connected(runtime(any())) -> boolean().
connected(Runtime) ->
    erlang:element(2, Runtime).

-file("src/lightspeed/async.gleam", 203).
-spec put_entry(list(async_entry(GIA)), binary(), async_state(GIA)) -> list(async_entry(GIA)).
put_entry(Entries_rev, Key, State) ->
    case Entries_rev of
        [] ->
            [{async_entry, Key, State}];

        [Entry | Rest] ->
            case erlang:element(2, Entry) =:= Key of
                true ->
                    [{async_entry, erlang:element(2, Entry), State} | Rest];

                false ->
                    [Entry | put_entry(Rest, Key, State)]
            end
    end.

-file("src/lightspeed/async.gleam", 45).
?DOC(
    " Start one async assignment.\n"
    "\n"
    " - when connected: enters `Loading(ref)`\n"
    " - when disconnected: enters `PendingDisconnected`\n"
).
-spec assign_async(runtime(GFZ), binary()) -> {runtime(GFZ), async_state(GFZ)}.
assign_async(Runtime, Key) ->
    case erlang:element(2, Runtime) of
        true ->
            Ref = erlang:element(3, Runtime),
            State = {loading, Ref},
            Entries_rev = put_entry(erlang:element(4, Runtime), Key, State),
            {{runtime, erlang:element(2, Runtime), Ref + 1, Entries_rev}, State};

        false ->
            State@1 = pending_disconnected,
            Entries_rev@1 = put_entry(erlang:element(4, Runtime), Key, State@1),
            {{runtime,
                    erlang:element(2, Runtime),
                    erlang:element(3, Runtime),
                    Entries_rev@1},
                State@1}
    end.

-file("src/lightspeed/async.gleam", 218).
-spec start_pending(list(async_entry(GIG)), integer(), list(async_entry(GIG))) -> {integer(),
    list(async_entry(GIG))}.
start_pending(Entries, Next_ref, Entries_rev) ->
    case Entries of
        [] ->
            {Next_ref, Entries_rev};

        [Entry | Rest] ->
            case erlang:element(3, Entry) of
                pending_disconnected ->
                    start_pending(
                        Rest,
                        Next_ref + 1,
                        [{async_entry,
                                erlang:element(2, Entry),
                                {loading, Next_ref}} |
                            Entries_rev]
                    );

                _ ->
                    start_pending(Rest, Next_ref, [Entry | Entries_rev])
            end
    end.

-file("src/lightspeed/async.gleam", 66).
?DOC(" Mark runtime connected and start pending async assignments.\n").
-spec connect(runtime(GGD)) -> runtime(GGD).
connect(Runtime) ->
    {Next_ref, Entries_rev} = start_pending(
        lists:reverse(erlang:element(4, Runtime)),
        erlang:element(3, Runtime),
        []
    ),
    {runtime, true, Next_ref, Entries_rev}.

-file("src/lightspeed/async.gleam", 74).
?DOC(" Mark runtime disconnected.\n").
-spec disconnect(runtime(GGG)) -> runtime(GGG).
disconnect(Runtime) ->
    {runtime, false, erlang:element(3, Runtime), erlang:element(4, Runtime)}.

-file("src/lightspeed/async.gleam", 145).
?DOC(" Stable state label for logs and assertions.\n").
-spec state_label(async_state(any())) -> binary().
state_label(State) ->
    case State of
        idle ->
            <<"idle"/utf8>>;

        pending_disconnected ->
            <<"pending_disconnected"/utf8>>;

        {loading, _} ->
            <<"loading"/utf8>>;

        {succeeded, _} ->
            <<"succeeded"/utf8>>;

        {failed, _} ->
            <<"failed"/utf8>>;

        {cancelled, _} ->
            <<"cancelled"/utf8>>
    end.

-file("src/lightspeed/async.gleam", 253).
-spec reverse_into(list(GIS), list(GIS)) -> list(GIS).
reverse_into(Left, Right) ->
    case Left of
        [] ->
            Right;

        [Entry | Rest] ->
            reverse_into(Rest, [Entry | Right])
    end.

-file("src/lightspeed/async.gleam", 177).
-spec update_state(
    list(async_entry(GHN)),
    binary(),
    fun((async_state(GHN)) -> {ok, async_state(GHN)} | {error, async_error()}),
    list(async_entry(GHN))
) -> {ok, list(async_entry(GHN))} | {error, async_error()}.
update_state(Entries_rev, Key, With, Seen_rev) ->
    case Entries_rev of
        [] ->
            {error, {unknown_task, Key}};

        [Entry | Rest] ->
            case erlang:element(2, Entry) =:= Key of
                false ->
                    update_state(Rest, Key, With, [Entry | Seen_rev]);

                true ->
                    case With(erlang:element(3, Entry)) of
                        {error, Error} ->
                            {error, Error};

                        {ok, State} ->
                            {ok,
                                reverse_into(
                                    Seen_rev,
                                    [{async_entry,
                                            erlang:element(2, Entry),
                                            State} |
                                        Rest]
                                )}
                    end
            end
    end.

-file("src/lightspeed/async.gleam", 165).
-spec transition(
    runtime(GHE),
    binary(),
    binary(),
    fun((async_state(GHE)) -> {ok, async_state(GHE)} | {error, async_error()})
) -> {runtime(GHE), {ok, nil} | {error, async_error()}}.
transition(Runtime, Key, _, With) ->
    case update_state(erlang:element(4, Runtime), Key, With, []) of
        {error, Error} ->
            {Runtime, {error, Error}};

        {ok, Entries_rev} ->
            {{runtime,
                    erlang:element(2, Runtime),
                    erlang:element(3, Runtime),
                    Entries_rev},
                {ok, nil}}
    end.

-file("src/lightspeed/async.gleam", 79).
?DOC(" Resolve one async assignment with a success value.\n").
-spec succeed(runtime(GGJ), binary(), GGJ) -> {runtime(GGJ),
    {ok, nil} | {error, async_error()}}.
succeed(Runtime, Key, Value) ->
    transition(Runtime, Key, <<"succeed"/utf8>>, fun(State) -> case State of
                {loading, _} ->
                    {ok, {succeeded, Value}};

                _ ->
                    {error,
                        {invalid_transition,
                            Key,
                            state_label(State),
                            <<"succeed"/utf8>>}}
            end end).

-file("src/lightspeed/async.gleam", 98).
?DOC(" Resolve one async assignment with a failure.\n").
-spec fail(runtime(GGO), binary(), binary()) -> {runtime(GGO),
    {ok, nil} | {error, async_error()}}.
fail(Runtime, Key, Reason) ->
    transition(Runtime, Key, <<"fail"/utf8>>, fun(State) -> case State of
                {loading, _} ->
                    {ok, {failed, Reason}};

                _ ->
                    {error,
                        {invalid_transition,
                            Key,
                            state_label(State),
                            <<"fail"/utf8>>}}
            end end).

-file("src/lightspeed/async.gleam", 117).
?DOC(" Cancel one async assignment.\n").
-spec cancel(runtime(GGT), binary(), binary()) -> {runtime(GGT),
    {ok, nil} | {error, async_error()}}.
cancel(Runtime, Key, Reason) ->
    transition(Runtime, Key, <<"cancel"/utf8>>, fun(State) -> case State of
                pending_disconnected ->
                    {ok, {cancelled, Reason}};

                {loading, _} ->
                    {ok, {cancelled, Reason}};

                _ ->
                    {error,
                        {invalid_transition,
                            Key,
                            state_label(State),
                            <<"cancel"/utf8>>}}
            end end).

-file("src/lightspeed/async.gleam", 239).
-spec find_state(list(async_entry(GIN)), binary()) -> gleam@option:option(async_state(GIN)).
find_state(Entries_rev, Key) ->
    case Entries_rev of
        [] ->
            none;

        [Entry | Rest] ->
            case erlang:element(2, Entry) =:= Key of
                true ->
                    {some, erlang:element(3, Entry)};

                false ->
                    find_state(Rest, Key)
            end
    end.

-file("src/lightspeed/async.gleam", 137).
?DOC(" Lookup current state for one task key.\n").
-spec state(runtime(GGY), binary()) -> gleam@option:option(async_state(GGY)).
state(Runtime, Key) ->
    find_state(erlang:element(4, Runtime), Key).

-file("src/lightspeed/async.gleam", 157).
?DOC(" Stable error string for assertions and logs.\n").
-spec error_to_string(async_error()) -> binary().
error_to_string(Error) ->
    case Error of
        {unknown_task, Key} ->
            <<"unknown_task:"/utf8, Key/binary>>;

        {invalid_transition, Key@1, State, Action} ->
            <<<<<<<<<<"invalid_transition:"/utf8, Key@1/binary>>/binary,
                            ":"/utf8>>/binary,
                        State/binary>>/binary,
                    ":"/utf8>>/binary,
                Action/binary>>
    end.