-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).