Sorted snapshot of subscriptions for debugging. Sorted by
(triggerName, fireAt, actionName) for stable inspection.
A copied, stably-sorted array of the current subscriptions.
Fire a trigger. Dispatches each matching subscription once, from a snapshot taken at the start of the call. Active-GM only.
For updateWorldTime, only due subscriptions (fireAt undefined or
<= worldTime) match, and they dispatch in ascending fireAt order. For
all other triggers, every matching subscription dispatches once in
insertion order. Subscriptions added by a handler mid-dispatch are not
re-fired within this call.
The trigger context describing the event being fired.
The scheduled world time a (uuid, actionName) subscription will fire at, or
undefined when there is no such subscription (or it carries no fireAt).
Answers on any client for documents that client can see.
The subscribed document's UUID.
The subscription actionName.
The absolute world-time fireAt, or undefined.
Resolve uuid and offer actionName on its logic — post an
owner-gated [Perform] reminder card rather than performing the action.
The single consent primitive (issue #579): the queue reminds; the human
performs. The card's button is addressed to uuid (an item, actor, or
any document with a logic.speaker), so the owner's click runs the same
action on that document's logic. Errors are logged and swallowed so one
failure cannot abort a batch.
Reused by dispatchOne (time/combat subscriptions) and by the
scene-region bridge (issue #593), which offers a region-authored action to
the entering token's actor.
The document whose logic runs the action on [Perform].
The action shortcode to offer.
The trigger context revived as the action's scope on click.
Its payload.visibility === "gm" whispers the reminder to the GM.
Optionaloptions: { dedupeAt?: number }Offer options.
OptionaldedupeAt?: numberOffers a given (uuid, actionName, dedupeAt)
occurrence once (time-scheduled reminders); omit to offer every call.
Convenience: schedule a one-shot dispatch when world time reaches
fireAt. Equivalent to subscribing to updateWorldTime with
oneShot: true.
Recurring schedules are the consumer's responsibility: the handler
advances the document's persisted anchor and its finalize() re-registers
the next occurrence (see the class overview).
The document to dispatch to when the time is reached.
The subscription actionName passed to the handler.
The world time at or after which to fire.
Optionalpayload: Record<string, unknown>Optional payload forwarded to the handler.
OptionalsceneUuid: stringOptional scene the schedule is bound to (issue #590); offered only while that scene is active. Omit for a world-wide schedule.
Register or overwrite a subscription. Runs on all clients (the queue is a projection of document state); only fire is GM-gated.
If a subscription with the same (uuid, actionName) already exists, its fields
are replaced.
The subscription to register or overwrite.
Seconds from the current world time until a (uuid, actionName) subscription
fires: positive for the future, negative for the past, 0 for now.
undefined when the subscription is absent or has no fireAt.
The subscribed document's UUID.
The subscription actionName.
Signed seconds until fire, or undefined.
In-memory trigger dispatcher for SoHL.
Accessible at runtime as
sohl.events.Overview
Documents subscribe to triggers — named lifecycle moments shared with Foundry's
CONFIG.ActiveEffect.expiryEventsvocabulary (updateWorldTime,combatStart,combatEnd,roundStart,roundEnd,turnStart,turnEnd, plus any custom names registered viaregisterSohlTrigger). When a trigger fires, matching subscriptions dispatch by executing the action named by the subscription'sactionNameon the owning document's logic — the trigger context (with the subscription'spayload) becomes the action's scope. An event therefore reuses the very same action a user could invoke manually.Time scheduling is one trigger among many: a
scheduleAt(uuid, actionName, fireAt, payload)call creates a one-shot subscription onupdateWorldTimethat fires when world time reachesfireAt.Identity and overwrite semantics
Each subscription is uniquely identified by
(uuid, actionName). Re-subscribing with the same identity overwrites the previous entry. Documents re-register their subscriptions on every preparation cycle, so only the most recent registration matters.Populate everywhere, fire on the active GM only
The queue is a pure projection of document state: every client — GM or player — populates its own copy from its own document preparation, so
subscribe/unsubscribe/scheduleAtrun on all clients. A player's queue is therefore a permission-scoped subset of the active GM's (it holds only the subscriptions for documents that client can see), which is exactly enough to answer sheet-side date queries (nextFireTime / timeUntil) locally.Only
fireis gated to the active GM. Nothing else evolves schedule state, so no GM-only side effect can drift the clients out of sync — schedule changes flow through document updates (which replicate) and their re-preparation.Single-pass dispatch (no cascade)
A
fire(ctx)dispatches each matching subscription once, from a snapshot taken at the start of the call. ForupdateWorldTime, "matching" additionally requires the subscription be due (fireAtundefined or<= worldTime), and due subscriptions dispatch in ascendingfireAtorder. Subscriptions a handler adds mid-dispatch are not re-fired within the same call — they wait for the nextfire.The queue deliberately does not catch up recurring events over a large time jump. That is the consuming document's responsibility: its handler resolves every elapsed interval in
(lastAnchor, worldTime]in one pass and persists the advanced anchor, and itsfinalize()re-registers the single next occurrence beyondworldTime. See the Event Queue reference doc for the contract.Loop protection
Re-entrant
fire(...)for the same trigger name is capped atMAX_TRIGGER_DEPTH(16); beyond that the call aborts with an error. This backstops non-time loops (e.g. acombatStarthandler that firescombatStart). Because there is no cascade, no same-tick guard is needed: a handler that re-arms during dispatch simply schedules a subscription that the nextfiresees.Predicate error policy
Predicates that throw are caught and logged; that dispatch is skipped; the subscription is not removed. Predicates may legitimately fail on stale data (e.g., resolving an effect that was just deleted) and one bad tick should not permanently disarm a check.