Consumes {:send_delayed, %Statifier.Effect.SendDelayed{}} into a
uniquely-keyed job on the host's Oban instance.
This is the scheduling and cancellation half of the durable-timer recipe
in statifier-ex's docs/durable-timers.md: the host reads the effect off
a live session's subscriber stream (or a direct interpreter drive) and
hands it here with the session scope and its StatifierOban.Config.
When the job fires, StatifierOban.Timer.Worker feeds the event back
through the config's StatifierOban.Timer.Delivery module, behind the
mandatory run-liveness check (st-ADR-0054 decision 4) - the delivery
module travels on the job's meta, so it is fixed at schedule time.
Two contract rules from statifier-ex are enforced at this door:
- Only a
niltarget is schedulable (st-ADR-0055): any other target's route is resolved inside the session and does not travel on the effect, so a non-niltarget is a typed error here - the caller should not have offered it. The effect stream is observational; leaving such a send to the library costs nothing. delay_msis relative, milliseconds from the moment the send was scheduled, so the fire time is computed at insert (DateTime.utc_now/0plus the delay), never re-derived later.
Summary
Types
Why a SendDelayed effect could not be scheduled.
Functions
Consumes one %Cancel{} into cancellation of every matching timer job.
Schedules one %SendDelayed{} as one Oban job, unique per dedup key.
Types
@type schedule_error() :: {:non_self_target, String.t()} | StatifierOban.Timer.Key.error() | Ecto.Changeset.t()
Why a SendDelayed effect could not be scheduled.
Functions
@spec cancel( StatifierOban.Config.t(), StatifierOban.Timer.Key.scope(), Statifier.Effect.Cancel.t() ) :: {:ok, non_neg_integer()} | {:error, StatifierOban.Timer.Key.error()}
Consumes one %Cancel{} into cancellation of every matching timer job.
Consumes the effect vocabulary's {:cancel, %Cancel{}}, never the
instruction vocabulary's {:cancel_timers, ...} (st-ADR-0054). The match
is the cancellation key {scope, send_id} from
StatifierOban.Timer.Key, read off each stored job's args, and it may
legitimately hit several jobs: spec 6.3 cancels every delayed send under
a sendid, and an author-written id executed twice stores two jobs
under one send_id. Returns {:ok, count} with the number of jobs
cancelled; a cancel matching nothing is {:ok, 0}, a no-op rather than
an error - the same shape as Timers.take/2's {[], timers} in
statifier-ex.
A cancel racing job execution resolves to whichever transition commits
first, and stays resolved: Oban.cancel_all_jobs/2 cancels any matched
job still in a non-terminal state - an executing one is killed - while
a job that already reached a terminal state keeps its outcome and is not
counted. That is spec-faithful: a real-time <cancel> can lose to a
timer that already fired, and sob-2hx.5's run-liveness check at delivery
is the guard on that side, not this one.
The match ignores the queue, exactly as the dedup key's uniqueness does: a host that moved its timers queue can still cancel jobs stored under the old name.
@spec schedule( StatifierOban.Config.t(), StatifierOban.Timer.Key.scope(), Statifier.Effect.SendDelayed.t() ) :: {:ok, Oban.Job.t()} | {:error, schedule_error()}
Schedules one %SendDelayed{} as one Oban job, unique per dedup key.
scope follows StatifierOban.Timer.Key: ctx.session_id for a live
session, or the host's own durable run id for a process-less host -
always the caller's to supply, never derived.
Inserting the same scope and effect again returns {:ok, job} with
job.conflict? set and leaves exactly one stored job: the at-least-once
no-op the dedup key exists for. The job is inserted into the host's
:timers_queue, scheduled at now plus delay_ms, carrying the
config's delivery module in its meta - meta is not part of the unique
fields, so a replay under a reconfigured delivery still conflicts with
the stored job (same scheduling decision) rather than inserting a
second one.