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