src/quicer_lib.erl

%%--------------------------------------------------------------------
%% Copyright (c) 2022-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(quicer_lib).
-include("quicer_types.hrl").

-export_type([
    cb_ret/0,
    cb_state/0
]).

-type cb_ret() :: cb_ret_noreply() | cb_ret_reply().
-type cb_state() :: term().

-type cb_ret_reply() ::
    {reply, Reply :: term(), cb_state()}
    | {reply, Reply :: term(), cb_state(), action()}
    | cb_ret_stop_reply().

%% ok and update cb_state
-type cb_ret_noreply() ::
    {ok, cb_state()}
    %% error handling per callback
    | {error, Reason :: term(), cb_state()}
    | {action(), cb_state()}
    | cb_ret_stop_noreply().

-type cb_ret_stop_noreply() :: {stop, Reason :: term(), cb_state()}.
-type cb_ret_stop_reply() :: {stop, Reason :: term(), Reply :: term(), cb_state()}.

-type action() :: hibernate | timeout() | {continue, Continue :: term()}.

-export([
    default_cb_ret/2,
    handle_dgram_send_states/1,
    handle_dgram_send_states/3,
    probe/2
]).

-spec default_cb_ret(cb_ret(), State :: term()) ->
    {reply, NewState :: term()}
    | {reply, NewState :: term(), action()}
    | {noreply, NewState :: term()}
    | {noreply, NewState :: term(), action()}
    | {stop, Reason :: term(), Reply :: term(), NewState :: term()}
    | {stop, Reason :: term(), NewState :: term()}.
default_cb_ret({ok, NewCBState}, State) ->
    %% ok
    {noreply, State#{callback_state := NewCBState}};
default_cb_ret({hibernate, NewCBState}, State) ->
    %% hibernate
    {noreply, State#{callback_state := NewCBState}, hibernate};
default_cb_ret({Timeout, NewCBState}, State) when is_integer(Timeout) ->
    %% timeout
    {noreply, State#{callback_state := NewCBState}, Timeout};
default_cb_ret({{continue, _} = Continue, NewCBState}, State) ->
    %% continue
    {noreply, State#{callback_state := NewCBState}, Continue};
default_cb_ret({stop, Reason, NewCBState}, State) ->
    %% stop
    {stop, Reason, State#{callback_state := NewCBState}};
default_cb_ret({reply, Reply, NewCBState, Action}, State) ->
    {reply, Reply, State#{callback_state := NewCBState}, Action};
default_cb_ret({reply, Reply, NewCBState}, State) ->
    {reply, Reply, State#{callback_state := NewCBState}}.

-spec probe(connection_handle(), timeout()) -> probe_res().
probe(Conn, Timeout) ->
    case quicer_nif:send_dgram(Conn, <<>>, _IsSync = 1) of
        {ok, _Len} ->
            handle_dgram_send_states(Conn, probe_dgram_send_cb(), Timeout);
        {error, E} ->
            {error, dgram_send_error, E};
        E ->
            E
    end.

-spec handle_dgram_send_states(connection_handle()) ->
    ok
    | {error,
        dgram_send_canceled
        | dgram_send_unknown
        | dgram_send_lost_discarded}.
handle_dgram_send_states(Conn) ->
    handle_dgram_send_states(init, Conn, default_dgram_suspect_lost_cb(), 5000).

-type lost_suspect_callback() ::
    {fun((connection_handle(), term(), term()) -> term()), term()}
    | {atom(), term()}.
-spec handle_dgram_send_states(connection_handle(), lost_suspect_callback(), timeout()) -> any().
handle_dgram_send_states(Conn, {_CBFun, _CBState} = CB, Timeout) ->
    handle_dgram_send_states(init, Conn, CB, Timeout).

handle_dgram_send_states(init, Conn, {Fun, CallbackState}, Timeout) ->
    receive
        {quic, dgram_send_state, Conn, #{state := ?QUIC_DATAGRAM_SEND_SENT}} ->
            NewCBState = Fun(Conn, ?QUIC_DATAGRAM_SEND_SENT, CallbackState),
            handle_dgram_send_states(sent, Conn, {Fun, NewCBState}, Timeout);
        {quic, dgram_send_state, Conn, #{state := Final}} ->
            Fun(Conn, Final, CallbackState)
    after 5000 ->
        %% @TODO proper test caught this, may fire a bug report to msquic
        Fun(Conn, timeout, CallbackState)
    end;
handle_dgram_send_states(sent, Conn, {Fun, CallbackState}, Timeout) ->
    receive
        {quic, dgram_send_state, Conn, #{state := ?QUIC_DATAGRAM_SEND_LOST_SUSPECT}} ->
            %% Lost suspected, call the callback for the return hits.
            %% however, we still need to wait for the final state.
            NewCBState = Fun(Conn, ?QUIC_DATAGRAM_SEND_LOST_SUSPECT, CallbackState),
            receive
                {quic, dgram_send_state, Conn, #{state := EState}} ->
                    Fun(Conn, EState, NewCBState)
            after Timeout ->
                %% @TODO proper test caught this, may fire a bug report to msquic
                Fun(Conn, timeout, CallbackState)
            end;
        {quic, dgram_send_state, Conn, #{state := Final}} ->
            Fun(Conn, Final, CallbackState)
    after Timeout ->
        %% @TODO proper test caught this, may fire a bug report to msquic
        Fun(Conn, timeout, CallbackState)
    end.

%% Default Callback for Datagram Send lost suspected
default_dgram_suspect_lost_cb() ->
    Fun = fun(_Conn, _, _CallbackState) ->
        %% just return ok, even it is lost, we don't care.
        ok
    end,
    {Fun, undefined}.

probe_dgram_send_cb() ->
    Fun = fun
        (_Conn, ?QUIC_DATAGRAM_SEND_SENT, CallbackState) ->
            CallbackState#probe_state{sent_at = ts_ms()};
        (_Conn, ?QUIC_DATAGRAM_SEND_LOST_SUSPECT, CallbackState) ->
            CallbackState#probe_state{suspect_lost_at = ts_ms()};
        (_Conn, State, CallbackState) ->
            CallbackState#probe_state{
                final_at = ts_ms(),
                final = State
            }
    end,
    {Fun, #probe_state{}}.

ts_ms() ->
    erlang:monotonic_time(millisecond).