-module(stratus). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([websocket/3, with_init_timeout/2, on_close/2, send_message/2, send_text_message/2, send_binary_message/2, close/1, initialize/1]). -export_type([connection/0, internal_message/1, message/1, builder/2, state/1, handshake_error/0]). -opaque connection() :: {connection, stratus@internal@socket:socket(), stratus@internal@transport:transport()}. -opaque internal_message(KBK) :: {user_message, KBK} | {err, stratus@internal@socket:socket_reason()} | {data, bitstring()} | closed | shutdown. -type message(KBL) :: {text, binary()} | {binary, bitstring()} | {user, KBL}. -opaque builder(KBM, KBN) :: {builder, gleam@uri:uri(), gleam@option:option(integer()), fun(() -> {KBM, gleam@option:option(gleam@erlang@process:selector(KBN))}), fun((message(KBN), KBM, connection()) -> gleam@otp@actor:next(KBN, KBM)), fun((KBM) -> nil)}. -type state(KBO) :: {state, bitstring(), gleam@option:option(gramps:data_frame()), stratus@internal@socket:socket(), KBO}. -type handshake_error() :: {sock, stratus@internal@socket:socket_reason()} | {protocol, bitstring()}. -spec from_socket_message(stratus@internal@socket:socket_message()) -> internal_message(any()). from_socket_message(Msg) -> case Msg of {data, Bits} -> {data, Bits}; closed -> closed; {err, Reason} -> {err, Reason} end. -spec websocket( gleam@uri:uri(), fun(() -> {KBR, gleam@option:option(gleam@erlang@process:selector(KBS))}), fun((message(KBS), KBR, connection()) -> gleam@otp@actor:next(KBS, KBR)) ) -> builder(KBR, KBS). websocket(Uri, Init, Loop) -> {builder, Uri, none, Init, Loop, fun(_) -> nil end}. -spec with_init_timeout(builder(KCA, KCB), integer()) -> builder(KCA, KCB). with_init_timeout(Builder, Timeout) -> erlang:setelement(3, Builder, {some, Timeout}). -spec on_close(builder(KCG, KCH), fun((KCG) -> nil)) -> builder(KCG, KCH). on_close(Builder, On_close) -> erlang:setelement(6, Builder, On_close). -spec send_message(gleam@erlang@process:subject(internal_message(KCU)), KCU) -> nil. send_message(Subject, Message) -> gleam@erlang@process:send(Subject, {user_message, Message}). -spec send_text_message(connection(), binary()) -> {ok, nil} | {error, stratus@internal@socket:socket_reason()}. send_text_message(Conn, Msg) -> Frame = gramps:to_text_frame(Msg, true), stratus@internal@transport:send( erlang:element(3, Conn), erlang:element(2, Conn), Frame ). -spec send_binary_message(connection(), bitstring()) -> {ok, nil} | {error, stratus@internal@socket:socket_reason()}. send_binary_message(Conn, Msg) -> Frame = gramps:to_binary_frame(Msg, true), stratus@internal@transport:send( erlang:element(3, Conn), erlang:element(2, Conn), Frame ). -spec close(connection()) -> {ok, nil} | {error, stratus@internal@socket:socket_reason()}. close(Conn) -> Frame = gramps:frame_to_bytes_builder( {control, {close_frame, 0, <<>>}}, {some, <<>>} ), stratus@internal@transport:send( erlang:element(3, Conn), erlang:element(2, Conn), Frame ). -spec make_upgrade(binary(), binary(), binary()) -> gleam@bytes_builder:bytes_builder(). make_upgrade(Path, Host, Origin) -> _pipe = gleam@bytes_builder:new(), _pipe@1 = gleam@bytes_builder:append_string( _pipe, <<<<"GET "/utf8, Path/binary>>/binary, " HTTP/1.1\r\n"/utf8>> ), _pipe@2 = gleam@bytes_builder:append_string( _pipe@1, <<<<"Host: "/utf8, Host/binary>>/binary, "\r\n"/utf8>> ), _pipe@3 = gleam@bytes_builder:append_string( _pipe@2, <<"Upgrade: websocket\r\n"/utf8>> ), _pipe@4 = gleam@bytes_builder:append_string( _pipe@3, <<"Connection: Upgrade\r\n"/utf8>> ), _pipe@5 = gleam@bytes_builder:append_string( _pipe@4, <<<<"Sec-WebSocket-Key: "/utf8, (<<"dGhlIHNhbXBsZSBub25jZQ=="/utf8>>)/binary>>/binary, "\r\n"/utf8>> ), _pipe@6 = gleam@bytes_builder:append_string( _pipe@5, <<"Sec-WebSocket-Version: 13\r\n"/utf8>> ), _pipe@7 = gleam@bytes_builder:append_string( _pipe@6, <<<<"Origin: "/utf8, Origin/binary>>/binary, "\r\n"/utf8>> ), gleam@bytes_builder:append_string(_pipe@7, <<"\r\n"/utf8>>). -spec perform_handshake( stratus@internal@transport:transport(), binary(), integer(), binary(), binary(), integer() ) -> {ok, stratus@internal@socket:socket()} | {error, handshake_error()}. perform_handshake(Transport, Host, Port, Path, Origin, Timeout) -> Opts = stratus@internal@socket:convert_options( gleam@list:append( [{'receive', once}, {packets_of, binary}, {send_timeout, 30000}, {send_timeout_close, true}, {reuseaddr, true}, {nodelay, true}], [{'receive', pull}] ) ), gleam@result:'try'( gleam@result:map_error( stratus@internal@transport:connect( Transport, unicode:characters_to_list(Host), Port, Opts ), fun(Field@0) -> {sock, Field@0} end ), fun(Socket) -> gleam@result:'try'( gleam@result:map_error( stratus@internal@transport:send( Transport, Socket, make_upgrade(Path, Host, Origin) ), fun(Field@0) -> {sock, Field@0} end ), fun(_) -> gleam@result:'try'( gleam@result:map_error( stratus@internal@transport:receive_timeout( Transport, Socket, 0, Timeout ), fun(Field@0) -> {sock, Field@0} end ), fun(Resp) -> case Resp of <<"HTTP/1.1 101 Switching Protocols"/utf8, _/bitstring>> -> {ok, Socket}; _ -> {error, {protocol, Resp}} end end ) end ) end ). -spec initialize(builder(any(), KCN)) -> {ok, gleam@erlang@process:subject(internal_message(KCN))} | {error, gleam@otp@actor:start_error()}. initialize(Builder) -> Transport = case erlang:element(2, erlang:element(2, Builder)) of {some, <<"wss"/utf8>>} -> ssl; _ -> tcp end, Host = gleam@option:unwrap( erlang:element(4, erlang:element(2, Builder)), <<"localhost"/utf8>> ), Port = gleam@option:lazy_unwrap( erlang:element(5, erlang:element(2, Builder)), fun() -> case Transport of ssl -> 443; tcp -> 80 end end ), Origin = case {erlang:element(2, erlang:element(2, Builder)), Port} of {{some, <<"wss"/utf8>>}, 443} -> <<"https://"/utf8, Host/binary>>; {{some, <<"ws"/utf8>>}, 80} -> <<"http://"/utf8, Host/binary>>; {{some, <<"wss"/utf8>>}, _} -> <<<<<<"https://"/utf8, Host/binary>>/binary, ":"/utf8>>/binary, (gleam@int:to_string(Port))/binary>>; {_, _} -> <<<<<<"http://"/utf8, Host/binary>>/binary, ":"/utf8>>/binary, (gleam@int:to_string(Port))/binary>> end, Timeout = gleam@option:unwrap(erlang:element(3, Builder), 5000), gleam@otp@actor:start_spec( {spec, fun() -> _pipe = perform_handshake( Transport, Host, Port, erlang:element(6, erlang:element(2, Builder)), Origin, Timeout ), _pipe@1 = gleam@result:'try'( _pipe, fun(Socket) -> case stratus@internal@transport:set_opts( Transport, Socket, stratus@internal@socket:convert_options( [{'receive', once}] ) ) of {ok, _} -> {ok, Socket}; {error, Reason} -> {error, {sock, Reason}} end end ), _pipe@4 = gleam@result:map( _pipe@1, fun(Socket@1) -> {User_state, User_selector} = (erlang:element( 4, Builder ))(), Selector@1 = case User_selector of {some, Selector} -> _pipe@2 = Selector, _pipe@3 = gleam_erlang_ffi:map_selector( _pipe@2, fun(Field@0) -> {user_message, Field@0} end ), gleam_erlang_ffi:merge_selector( _pipe@3, gleam_erlang_ffi:map_selector( stratus@internal@socket:selector(), fun from_socket_message/1 ) ); _ -> gleam_erlang_ffi:map_selector( stratus@internal@socket:selector(), fun from_socket_message/1 ) end, {ready, {state, <<>>, none, Socket@1, User_state}, Selector@1} end ), _pipe@5 = gleam@result:map_error( _pipe@4, fun(Err) -> {failed, gleam@string:inspect(Err)} end ), gleam@result:unwrap_both(_pipe@5) end, Timeout, fun(Msg, State) -> Conn = {connection, erlang:element(4, State), Transport}, case Msg of {user_message, User_message} -> case (erlang:element(5, Builder))( {user, User_message}, erlang:element(5, State), Conn ) of {continue, User_state@1, User_selector@1} -> New_state = erlang:setelement( 5, State, User_state@1 ), case User_selector@1 of {some, User_selector@2} -> Selector@2 = begin _pipe@6 = User_selector@2, _pipe@7 = gleam_erlang_ffi:map_selector( _pipe@6, fun(Field@0) -> {user_message, Field@0} end ), gleam_erlang_ffi:merge_selector( _pipe@7, gleam_erlang_ffi:map_selector( stratus@internal@socket:selector( ), fun from_socket_message/1 ) ) end, {continue, New_state, {some, Selector@2}}; _ -> gleam@otp@actor:continue(New_state) end; {stop, Reason@1} -> {stop, Reason@1} end; {err, Reason@2} -> {stop, {abnormal, gleam@string:inspect(Reason@2)}}; {data, Bits} -> _pipe@8 = gramps:frame_from_message( gleam@bit_array:append( erlang:element(2, State), Bits ) ), _pipe@11 = gleam@result:map( _pipe@8, fun(Data) -> {Parsed_frame, Rest} = Data, Frame = case Parsed_frame of {complete, {data, {text_frame, _, Data@1}}} -> _assert_subject = gleam@bit_array:to_string( Data@1 ), {ok, Str} = 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 => <<"stratus"/utf8>>, function => <<"initialize"/utf8>>, line => 232} ) end, {text, Str}; {complete, {data, {binary_frame, _, Data@2}}} -> {binary, Data@2}; _ -> erlang:error(#{gleam_error => panic, message => <<"Incomplete messages not supported right now"/utf8>>, module => <<"stratus"/utf8>>, function => <<"initialize"/utf8>>, line => 237}) end, case (erlang:element(5, Builder))( Frame, erlang:element(5, State), Conn ) of {continue, User_state@2, User_selector@3} -> _assert_subject@1 = stratus@internal@transport:set_opts( Transport, erlang:element(4, State), stratus@internal@socket:convert_options( [{'receive', once}] ) ), {ok, _} = case _assert_subject@1 of {ok, _} -> _assert_subject@1; _assert_fail@1 -> erlang:error( #{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@1, module => <<"stratus"/utf8>>, function => <<"initialize"/utf8>>, line => 242} ) end, New_state@1 = erlang:setelement( 2, erlang:setelement( 5, State, User_state@2 ), Rest ), case User_selector@3 of {some, User_selector@4} -> Selector@3 = begin _pipe@9 = User_selector@4, _pipe@10 = gleam_erlang_ffi:map_selector( _pipe@9, fun(Field@0) -> {user_message, Field@0} end ), gleam_erlang_ffi:merge_selector( _pipe@10, gleam_erlang_ffi:map_selector( stratus@internal@socket:selector( ), fun from_socket_message/1 ) ) end, {continue, New_state@1, {some, Selector@3}}; _ -> gleam@otp@actor:continue( New_state@1 ) end; {stop, Reason@3} -> {stop, Reason@3} end end ), gleam@result:lazy_unwrap( _pipe@11, fun() -> _assert_subject@2 = stratus@internal@transport:set_opts( Transport, erlang:element(4, State), stratus@internal@socket:convert_options( [{'receive', once}] ) ), {ok, _} = case _assert_subject@2 of {ok, _} -> _assert_subject@2; _assert_fail@2 -> erlang:error( #{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@2, module => <<"stratus"/utf8>>, function => <<"initialize"/utf8>>, line => 268} ) end, gleam@otp@actor:continue( erlang:setelement( 2, State, gleam@bit_array:append( erlang:element(2, State), Bits ) ) ) end ); closed -> (erlang:element(6, Builder))(erlang:element(5, State)), {stop, normal}; shutdown -> {stop, normal} end end} ).