Current section
Files
Jump to
Current section
Files
src/kraft_ws_util.erl
-module(kraft_ws_util).
% API
-export([setup/4]).
-export([callbacks/2]).
-export([handshake/2]).
-export([call/3]).
-export([raw_call/3]).
% Callbacks
-behaviour(cowboy_websocket).
-export([init/2]).
-export([websocket_init/1]).
-export([websocket_handle/2]).
-export([websocket_info/2]).
-export([terminate/3]).
%--- API -----------------------------------------------------------------------
setup(UserOpts, App, Handler, MState) ->
Opts = maps:merge(default_opts(), UserOpts),
{
?MODULE,
#{
opts => Opts,
app => App,
module => callback_module(Opts),
handler => Handler,
state => MState
}
}.
callbacks(Callbacks, #{handler := Handler} = State0) ->
CheckCallback = fun(C, Cs) ->
maps:put(C, lists:member(C, Handler:module_info(exports)), Cs)
end,
Exported = lists:foldl(CheckCallback, #{}, Callbacks),
State0#{callbacks => Exported}.
handshake(Req, #{callbacks := #{{handshake, 3} := false}} = State0) ->
{cowboy_websocket, Req, State0};
handshake(Req, #{handler := Handler, state := MState0} = State0) ->
Conn0 = kraft_conn:new(Req, State0),
case Handler:handshake({Req, MState0}, kraft_conn:params(Conn0), MState0) of
{reply, Code, Headers, Body} ->
Resp = cowboy_req:reply(Code, Headers, Body, Req),
{ok, Resp, State0};
{ok, MState1} ->
{cowboy_websocket, Req, State0#{state => MState1}}
end.
call(info, _Args, #{callbacks := #{{info, 2} := false}} = State0) ->
{[], State0};
call(terminate, _Args, #{callbacks := #{{terminate, 2} := false}} = State0) ->
{[], State0};
call(Func, Args, State0) ->
{Commands, MState1} = raw_call(Func, Args, State0),
{Commands, State0#{state => MState1}}.
raw_call(terminate, _Args, #{callbacks := #{{terminate, 2} := false}}) ->
ok;
raw_call(Func, Args, #{handler := Handler, state := MState0}) ->
erlang:apply(Handler, Func, Args ++ [MState0]).
%--- Callbacks -----------------------------------------------------------------
init(Req, State0) ->
State1 = kraft_ws_util:callbacks(
[{handshake, 3}, {info, 2}, {terminate, 2}],
State0
),
Conn = kraft_conn:new(Req, State0),
kraft_ws_util:handshake(Req, State1#{conn => Conn}).
websocket_init(#{conn := Conn} = State0) ->
State1 = trigger_ping(State0),
module(init, [Conn], State1).
websocket_handle(pong, #{ping := #{}} = State0) ->
{[], State0};
websocket_handle(Frame, State0) ->
module(handle, [Frame], State0).
websocket_info('$kraft_ws_ping', State0) ->
State1 = trigger_ping(State0),
{[ping], State1};
websocket_info(Info, State0) ->
module(info, [Info], State0).
terminate(Reason, Req, State0) ->
cancel_ping(State0),
module(terminate, [Reason, Req], State0),
ok.
%--- Internal ------------------------------------------------------------------
default_opts() -> #{type => raw, ping => #{interval => 30_000}}.
callback_module(#{type := raw}) -> kraft_ws;
callback_module(#{type := json}) -> kraft_ws_json;
callback_module(#{type := json_rpc}) -> kraft_ws_jsonrpc;
callback_module(#{type := Other}) -> error({invalid_kraft_ws_type, Other}).
trigger_ping(#{opts := #{ping := disabled}} = State0) ->
State0;
trigger_ping(#{ping := #{target := Last}} = State0) ->
#{opts := #{ping := #{interval := Interval}}} = State0,
Target = Last + Interval,
Ref = erlang:send_after(Target, self(), '$kraft_ws_ping', [{abs, true}]),
mapz:deep_merge(State0, #{ping => #{timer => Ref, target => Target}});
trigger_ping(State0) ->
trigger_ping(State0#{
ping => #{target => erlang:monotonic_time(millisecond)}
}).
cancel_ping(#{ping := #{timer := Ref} = Ping} = State0) ->
erlang:cancel_timer(Ref, [{info, false}]),
State0#{ping => Ping#{timer => undefined}};
cancel_ping(State0) ->
State0.
module(Func, Args, #{module := Module} = State0) ->
erlang:apply(Module, Func, Args ++ [State0]).