Skip to content

ITimeoutStore

ITimeoutStore is the persistence interface backing scheduled saga timeouts. When a process handler calls ctx.requestTimeout(...), the bus persists a TimeoutRecord here. A background poller calls claimDue on a fixed interval, claims any timeouts whose runAt has elapsed, dispatches them back into the saga, and deletes them.

Implementations must claim atomically: two pollers (or two hosts running the same bus) must never fire the same timeout twice.

import type {
ITimeoutStore,
TimeoutRecord,
} from '@serviceconnect/core';
export interface TimeoutRecord {
readonly id: string;
readonly name: string;
readonly sagaCorrelationId: string;
readonly sagaDataType: string;
readonly runAt: Date;
readonly payload?: Readonly<Record<string, unknown>>;
}
export interface ITimeoutStore {
schedule(record: Omit<TimeoutRecord, 'id'>): Promise<TimeoutRecord>;
claimDue(now: Date, limit: number): Promise<readonly TimeoutRecord[]>;
delete(id: string): Promise<void>;
}
  • schedule(record) — persist a new timeout. The caller passes everything except the id; the store assigns one and returns the full TimeoutRecord. The payload field is opaque — implementations should round-trip it unchanged so the saga handler sees whatever was passed to requestTimeout.
  • claimDue(now, limit) — return up to limit timeouts whose runAt <= now. Implementations must atomically claim the rows they return (e.g. via findAndUpdate with a claimedAt column, a SELECT FOR UPDATE SKIP LOCKED in SQL, or an equivalent guard) so two pollers do not both fire the same timeout. Returning rows in runAt order is recommended but not required.
  • delete(id) — remove a timeout. Called by the timeout poller after the timeout has been fired (published) successfully. The framework does not currently delete still-pending timeouts when a saga completes, so scheduled-but-unfired timeouts for a completed saga are not cleaned up automatically. Safe to no-op if the row no longer exists.