Skip to main content

src/aion@workflow@timer.erl

-module(aion@workflow@timer).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/aion/workflow/timer.gleam").
-export([timer_id/1, sleep/1, start_timer/2, cancel_timer/1, with_timeout/2]).
-export_type([timer_ref/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(" Durable workflow timers over canonical `Duration` values.\n").

-opaque timer_ref() :: {timer_ref, binary()}.

-file("src/aion/workflow/timer.gleam", 20).
?DOC(" Return the engine timer identifier carried by a named timer reference.\n").
-spec timer_id(timer_ref()) -> binary().
timer_id(Reference) ->
    erlang:element(2, Reference).

-file("src/aion/workflow/timer.gleam", 105).
-spec duration_to_boundary(aion@duration:duration()) -> binary().
duration_to_boundary(Duration) ->
    _pipe = Duration,
    _pipe@1 = aion@duration:to_milliseconds(_pipe),
    erlang:integer_to_binary(_pipe@1).

-file("src/aion/workflow/timer.gleam", 34).
?DOC(
    " Wait for an anonymous durable timer to fire.\n"
    "\n"
    " The timer is durable: it survives engine restart and, during replay, returns\n"
    " instantly once the matching timer-fired event is already present in history.\n"
    " Anonymous sleeps are not separately cancellable; cancelling a sleep means\n"
    " cancelling the workflow that is blocked on it (AT D3). Use `start_timer` when\n"
    " workflow code needs a named timer that can be cancelled independently.\n"
    " The await is a yield point: pending workflow queries are serviced by the\n"
    " query pump while the timer is parked. `with_timeout` needs no pump of its\n"
    " own — the awaits running inside its operation are the yield points.\n"
).
-spec sleep(aion@duration:duration()) -> {ok, nil} |
    {error, aion@error:engine_error()}.
sleep(Duration) ->
    Boundary = duration_to_boundary(Duration),
    case aion@internal@pump:run(
        fun() -> aion@internal@pump:shield(aion_flow_ffi:sleep(Boundary)) end
    ) of
        {ok, _} ->
            {ok, nil};

        {error, Raw_error} ->
            {error, {engine_failure, Raw_error}}
    end.

-file("src/aion/workflow/timer.gleam", 52).
?DOC(
    " Start a named durable timer and return its cancellable reference.\n"
    "\n"
    " The supplied `name` is the named timer identifier handed to AT. The SDK does\n"
    " not invent a default duration or rewrite the identifier; engine-side timer ID\n"
    " validation remains authoritative.\n"
).
-spec start_timer(binary(), aion@duration:duration()) -> {ok, timer_ref()} |
    {error, aion@error:engine_error()}.
start_timer(Name, Duration) ->
    case aion_flow_ffi:start_timer(Name, duration_to_boundary(Duration)) of
        {ok, Timer_id} ->
            {ok, {timer_ref, Timer_id}};

        {error, Raw_error} ->
            {error, {engine_failure, Raw_error}}
    end.

-file("src/aion/workflow/timer.gleam", 68).
?DOC(
    " Cancel a named durable timer.\n"
    "\n"
    " AT treats cancelling an already-fired or already-cancelled named timer as an\n"
    " idempotent no-op, so a successful engine response is always returned as\n"
    " `Ok(Nil)`. Anonymous sleeps cannot be cancelled through this function because\n"
    " they never produce a `TimerRef`.\n"
).
-spec cancel_timer(timer_ref()) -> {ok, nil} |
    {error, aion@error:engine_error()}.
cancel_timer(Reference) ->
    case aion_flow_ffi:cancel_timer(erlang:element(2, Reference)) of
        {ok, _} ->
            {ok, nil};

        {error, Raw_error} ->
            {error, {engine_failure, Raw_error}}
    end.

-file("src/aion/workflow/timer.gleam", 85).
?DOC(
    " Run an awaiting operation with a durable deadline.\n"
    "\n"
    " The operation is a thunk so the engine/test FFI can establish the timeout\n"
    " before the await begins. If the operation completes before the deadline its\n"
    " `Ok` value is returned. If the operation returns its own typed error, that\n"
    " error is wrapped in `InnerError`. If AT reports that the deadline expired\n"
    " (the engine's `timeout:`-tagged result), the result is\n"
    " `TimedOutError(TimedOut(...))`. Any other engine error is surfaced as\n"
    " `TimeoutEngineFailure` — an infrastructure fault must never be mistaken\n"
    " for a deadline expiry.\n"
).
-spec with_timeout(
    fun(() -> {ok, FJH} | {error, FJI}),
    aion@duration:duration()
) -> {ok, FJH} | {error, aion@error:timeout_result_error(FJI)}.
with_timeout(Operation, Deadline) ->
    case aion_flow_ffi:with_timeout(duration_to_boundary(Deadline), Operation) of
        {ok, {ok, Value}} ->
            {ok, Value};

        {ok, {error, Inner_error}} ->
            {error, {inner_error, Inner_error}};

        {error, Raw_error} ->
            case gleam_stdlib:string_starts_with(Raw_error, <<"timeout:"/utf8>>) of
                true ->
                    {error,
                        {timed_out_error,
                            {timed_out, gleam@string:drop_start(Raw_error, 8)}}};

                false ->
                    {error, {timeout_engine_failure, Raw_error}}
            end
    end.