src/lightspeed@transport@wisp_websocket.erl

-module(lightspeed@transport@wisp_websocket).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lightspeed/transport/wisp_websocket.gleam").
-export([connect_with_hooks/4, connect/3, reconnect_with_hooks/4, reconnect/3, receive_with_hooks/4, 'receive'/3, session_state/1]).
-export_type([web_socket_request/0, adapter_state/0, connect_result/0, receive_result/0]).

-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(" Wisp-style WebSocket adapter for Lightspeed session transport.\n").

-type web_socket_request() :: {web_socket_request,
        binary(),
        binary(),
        binary(),
        integer()}.

-type adapter_state() :: {adapter_state,
        lightspeed@agent@session:session(),
        binary(),
        binary()}.

-type connect_result() :: {connected, adapter_state(), list(binary())} |
    {rejected, lightspeed@transport@contract:adapter_error()}.

-type receive_result() :: {receive_result, adapter_state(), list(binary())}.

-file("src/lightspeed/transport/wisp_websocket.gleam", 313).
-spec patch_to_frame(lightspeed@agent@session:patch_envelope()) -> lightspeed@protocol:frame().
patch_to_frame(Patch) ->
    Ref = lightspeed@agent@session:patch_ref(Patch),
    Html = lightspeed@diff:encode(lightspeed@agent@session:patch(Patch)),
    {diff, Ref, Html}.

-file("src/lightspeed/transport/wisp_websocket.gleam", 299).
-spec encode_outbox(list(lightspeed@agent@session:outbox_message())) -> list(binary()).
encode_outbox(Outbox) ->
    case Outbox of
        [] ->
            [];

        [Entry | Rest] ->
            case Entry of
                {outbox_patch, Patch} ->
                    [lightspeed@protocol:encode(patch_to_frame(Patch)) |
                        encode_outbox(Rest)];

                {outbox_telemetry, _} ->
                    encode_outbox(Rest)
            end
    end.

-file("src/lightspeed/transport/wisp_websocket.gleam", 320).
-spec ensure_protection(
    lightspeed@agent@session:session(),
    binary(),
    web_socket_request(),
    lightspeed@transport@contract:protection_hook()
) -> {ok, nil} | {error, lightspeed@transport@contract:adapter_error()}.
ensure_protection(Session_state, Owner, Request, Protection_hook) ->
    Context = {protection_context,
        lightspeed@agent@session:id(Session_state),
        Owner,
        erlang:element(2, Request),
        erlang:element(3, Request),
        erlang:element(4, Request)},
    case lightspeed@transport@contract:protect(Protection_hook, Context) of
        protected ->
            {ok, nil};

        {rejected, Reason} ->
            {error, {protection_rejected, Reason}}
    end.

-file("src/lightspeed/transport/wisp_websocket.gleam", 216).
-spec authenticate_owner(
    lightspeed@agent@session:session(),
    web_socket_request(),
    lightspeed@transport@contract:auth_hook()
) -> {ok, binary()} | {error, lightspeed@transport@contract:adapter_error()}.
authenticate_owner(Session_state, Request, Auth_hook) ->
    Context = {auth_context,
        lightspeed@agent@session:id(Session_state),
        erlang:element(2, Request),
        erlang:element(3, Request),
        erlang:element(4, Request)},
    case lightspeed@transport@contract:authenticate(Auth_hook, Context) of
        {denied, Reason} ->
            {error, {authentication_failed, Reason}};

        {authorized, Owner} ->
            case Owner =:= lightspeed@agent@session:owner(Session_state) of
                true ->
                    {ok, Owner};

                false ->
                    {error, {invalid_adapter_state, <<"owner_mismatch"/utf8>>}}
            end
    end.

-file("src/lightspeed/transport/wisp_websocket.gleam", 49).
?DOC(" Authenticate, enforce protection hooks, and mount a WebSocket session.\n").
-spec connect_with_hooks(
    lightspeed@agent@session:session(),
    web_socket_request(),
    lightspeed@transport@contract:auth_hook(),
    lightspeed@transport@contract:protection_hook()
) -> connect_result().
connect_with_hooks(Session_state, Request, Auth_hook, Protection_hook) ->
    case authenticate_owner(Session_state, Request, Auth_hook) of
        {error, Error} ->
            {rejected, Error};

        {ok, Owner} ->
            case ensure_protection(
                Session_state,
                Owner,
                Request,
                Protection_hook
            ) of
                {error, Error@1} ->
                    {rejected, Error@1};

                {ok, nil} ->
                    Connected = lightspeed@agent@session:handle(
                        Session_state,
                        {inbox_message,
                            Owner,
                            {connect,
                                erlang:element(2, Request),
                                erlang:element(3, Request),
                                erlang:element(5, Request)}}
                    ),
                    {Connected@1, Outbox} = lightspeed@agent@session:flush_outbox(
                        Connected
                    ),
                    {connected,
                        {adapter_state,
                            Connected@1,
                            Owner,
                            erlang:element(3, Request)},
                        [lightspeed@protocol:encode(lightspeed@protocol:hello()) |
                            encode_outbox(Outbox)]}
            end
    end.

-file("src/lightspeed/transport/wisp_websocket.gleam", 35).
?DOC(" Authenticate and mount a WebSocket session.\n").
-spec connect(
    lightspeed@agent@session:session(),
    web_socket_request(),
    lightspeed@transport@contract:auth_hook()
) -> connect_result().
connect(Session_state, Request, Auth_hook) ->
    connect_with_hooks(
        Session_state,
        Request,
        Auth_hook,
        lightspeed@transport@contract:allow_protection()
    ).

-file("src/lightspeed/transport/wisp_websocket.gleam", 100).
?DOC(" Authenticate, enforce protection hooks, and reconnect a WebSocket session.\n").
-spec reconnect_with_hooks(
    adapter_state(),
    web_socket_request(),
    lightspeed@transport@contract:auth_hook(),
    lightspeed@transport@contract:protection_hook()
) -> connect_result().
reconnect_with_hooks(State, Request, Auth_hook, Protection_hook) ->
    case authenticate_owner(erlang:element(2, State), Request, Auth_hook) of
        {error, Error} ->
            {rejected, Error};

        {ok, Owner} ->
            case ensure_protection(
                erlang:element(2, State),
                Owner,
                Request,
                Protection_hook
            ) of
                {error, Error@1} ->
                    {rejected, Error@1};

                {ok, nil} ->
                    Reconnected = lightspeed@agent@session:handle(
                        erlang:element(2, State),
                        {inbox_message,
                            Owner,
                            {reconnect,
                                erlang:element(2, Request),
                                erlang:element(5, Request)}}
                    ),
                    {Reconnected@1, Outbox} = lightspeed@agent@session:flush_outbox(
                        Reconnected
                    ),
                    {connected,
                        {adapter_state,
                            Reconnected@1,
                            Owner,
                            erlang:element(3, Request)},
                        [lightspeed@protocol:encode(lightspeed@protocol:hello()) |
                            encode_outbox(Outbox)]}
            end
    end.

-file("src/lightspeed/transport/wisp_websocket.gleam", 91).
?DOC(" Authenticate and reconnect a WebSocket session.\n").
-spec reconnect(
    adapter_state(),
    web_socket_request(),
    lightspeed@transport@contract:auth_hook()
) -> connect_result().
reconnect(State, Request, Auth_hook) ->
    reconnect_with_hooks(
        State,
        Request,
        Auth_hook,
        lightspeed@transport@contract:allow_protection()
    ).

-file("src/lightspeed/transport/wisp_websocket.gleam", 295).
-spec failure_only(adapter_state(), lightspeed@protocol:frame()) -> receive_result().
failure_only(State, Frame) ->
    {receive_result, State, [lightspeed@protocol:encode(Frame)]}.

-file("src/lightspeed/transport/wisp_websocket.gleam", 275).
-spec apply_and_flush(adapter_state(), lightspeed@agent@session:inbox_event()) -> receive_result().
apply_and_flush(State, Event) ->
    Next = lightspeed@agent@session:handle(
        erlang:element(2, State),
        {inbox_message, erlang:element(3, State), Event}
    ),
    {Next@1, Outbox} = lightspeed@agent@session:flush_outbox(Next),
    {receive_result,
        {adapter_state,
            Next@1,
            erlang:element(3, State),
            erlang:element(4, State)},
        encode_outbox(Outbox)}.

-file("src/lightspeed/transport/wisp_websocket.gleam", 341).
-spec enforce_rate_limit(
    adapter_state(),
    binary(),
    integer(),
    lightspeed@transport@contract:rate_limit_hook()
) -> {ok, nil} | {error, lightspeed@transport@contract:adapter_error()}.
enforce_rate_limit(State, Event_name, Now_ms, Rate_limit_hook) ->
    Context = {rate_limit_context,
        lightspeed@agent@session:id(erlang:element(2, State)),
        erlang:element(3, State),
        Event_name,
        Now_ms},
    case lightspeed@transport@contract:limit_rate(Rate_limit_hook, Context) of
        rate_allowed ->
            {ok, nil};

        {limited, Reason, Retry_after_ms} ->
            {error, {rate_limited, Reason, Retry_after_ms}}
    end.

-file("src/lightspeed/transport/wisp_websocket.gleam", 240).
-spec apply_event_frame(
    adapter_state(),
    binary(),
    binary(),
    binary(),
    integer(),
    lightspeed@transport@contract:rate_limit_hook()
) -> receive_result().
apply_event_frame(State, Ref, Name, Payload, Now_ms, Rate_limit_hook) ->
    case enforce_rate_limit(State, Name, Now_ms, Rate_limit_hook) of
        {error, Error} ->
            failure_only(
                State,
                {failure,
                    Ref,
                    lightspeed@transport@contract:error_to_string(Error)}
            );

        {ok, nil} ->
            case Name of
                <<"increment"/utf8>> ->
                    apply_and_flush(State, increment);

                <<"decrement"/utf8>> ->
                    apply_and_flush(State, decrement);

                <<"heartbeat"/utf8>> ->
                    apply_and_flush(State, {heartbeat, Now_ms});

                <<"shutdown"/utf8>> ->
                    apply_and_flush(State, {shutdown, Payload});

                _ ->
                    failure_only(
                        State,
                        {failure,
                            Ref,
                            lightspeed@transport@contract:error_to_string(
                                {unsupported_client_event, Name}
                            )}
                    )
            end
    end.

-file("src/lightspeed/transport/wisp_websocket.gleam", 150).
?DOC(" Receive one client frame payload with a rate-limit hook.\n").
-spec receive_with_hooks(
    adapter_state(),
    binary(),
    integer(),
    lightspeed@transport@contract:rate_limit_hook()
) -> receive_result().
receive_with_hooks(State, Payload, Now_ms, Rate_limit_hook) ->
    case lightspeed@protocol:decode(Payload) of
        {error, Decode_error} ->
            failure_only(
                State,
                {failure,
                    <<""/utf8>>,
                    lightspeed@transport@contract:error_to_string(
                        {protocol_decode_failed, Decode_error}
                    )}
            );

        {ok, Frame} ->
            case Frame of
                {ack, Ref} ->
                    apply_and_flush(State, {ack, Ref});

                {event, Ref@1, Name, Payload@1} ->
                    apply_event_frame(
                        State,
                        Ref@1,
                        Name,
                        Payload@1,
                        Now_ms,
                        Rate_limit_hook
                    );

                {hello, _, _} ->
                    failure_only(
                        State,
                        {failure,
                            <<""/utf8>>,
                            lightspeed@transport@contract:error_to_string(
                                {unsupported_client_frame, <<"hello"/utf8>>}
                            )}
                    );

                {diff, Ref@2, _} ->
                    failure_only(
                        State,
                        {failure,
                            Ref@2,
                            lightspeed@transport@contract:error_to_string(
                                {unsupported_client_frame, <<"diff"/utf8>>}
                            )}
                    );

                {failure, Ref@3, _} ->
                    failure_only(
                        State,
                        {failure,
                            Ref@3,
                            lightspeed@transport@contract:error_to_string(
                                {unsupported_client_frame, <<"failure"/utf8>>}
                            )}
                    )
            end
    end.

-file("src/lightspeed/transport/wisp_websocket.gleam", 141).
?DOC(" Receive one client frame payload and return outbound server frames.\n").
-spec 'receive'(adapter_state(), binary(), integer()) -> receive_result().
'receive'(State, Payload, Now_ms) ->
    receive_with_hooks(
        State,
        Payload,
        Now_ms,
        lightspeed@transport@contract:allow_rate_limit()
    ).

-file("src/lightspeed/transport/wisp_websocket.gleam", 212).
?DOC(" Access session state.\n").
-spec session_state(adapter_state()) -> lightspeed@agent@session:session().
session_state(State) ->
    erlang:element(2, State).