@netscript/plugin-sagas-core
Saga DSL, runtime ports, adapters, telemetry, config, and testing primitives for NetScript sagas
plugins. This page is written against the package's public surface reported by deno doc. For the
full index of packages and plugins return to the reference overview.
Sagas are the honest answer to distributed transactions: a sequence of steps, each with a
compensation, driven by messages that may arrive twice or out of order. defineSaga builds a
frozen, typed definition — state, handlers, compensations, signals, queries — and the runtime drives
it through explicit ports for storage, transport, clock, and idempotency. Nothing is global:
applications inject their own durability, and tests inject deterministic in-memory doubles.
This is the core that the deployable @netscript/plugin-sagas plugin binds to
a NetScript host. Use it directly for custom hosts, libraries, and tests.
Exports
The package publishes twenty-one entrypoints. The root path carries the userland DSL; the remaining subpaths expose the layers a host, adapter author, or test harness composes.
| Export specifier | Module | Exports | Purpose |
|---|---|---|---|
@netscript/plugin-sagas-core |
./mod.ts |
45 | The userland saga DSL — defineSaga, the cascaded-message constructors, signals, queries, and the definition types they produce (documented below). |
@netscript/plugin-sagas-core/builders |
./src/builders/mod.ts |
31 | The builder layer behind the DSL, for tooling that constructs definitions programmatically. |
@netscript/plugin-sagas-core/domain |
./src/domain/mod.ts |
48 | Saga domain vocabulary and policy defaults (DEFAULT_RETRY_POLICY, DEFAULT_IDEMPOTENCY_WINDOW_MS, DEFAULT_RETRY_MAX_ATTEMPTS). |
@netscript/plugin-sagas-core/ports |
./src/ports/mod.ts |
70 | The port interfaces the runtime depends on — store, bus, transport, clock, idempotency, telemetry. |
@netscript/plugin-sagas-core/runtime |
./src/runtime/mod.ts |
87 | The engine: createSagaRuntime, createSagaEngine, createSagaCompensator, createSagaScheduler, and the idempotency-key helpers. |
@netscript/plugin-sagas-core/adapters |
./src/adapters/mod.ts |
72 | Concrete port adapters, including createSagaBusBridge. |
@netscript/plugin-sagas-core/transports |
./src/transports/mod.ts |
49 | Saga bus transports (createNetScriptRedisTransport, createGarnetListTransport) with their message and delayed-entry codecs. |
@netscript/plugin-sagas-core/stores |
./src/stores/mod.ts |
81 | KV-backed instance, scheduler, and applied-key stores, openSagaRuntimeKv, and the shared canonical-store selectors and Prisma-client predicate. |
@netscript/plugin-sagas-core/stores/backends |
./src/stores/saga-store-backend.ts |
9 | Backend selectors and projection env/value/type contracts without importing persistent adapters. |
@netscript/plugin-sagas-core/middleware |
./src/middleware/mod.ts |
30 | Host middleware — createSagaMiddleware, createSSEEventsMiddleware, emitSagaEvent. |
@netscript/plugin-sagas-core/integration/workers |
./src/integration/workers/mod.ts |
23 | Selected-schema pure worker effect constructors plus explicit trigger helpers for callers outside synchronous handlers. |
@netscript/plugin-sagas-core/integration/publisher |
./src/integration/publisher/mod.ts |
11 | Non-throwing publisher contracts plus publishSagaOrThrow for an explicit exception boundary. |
@netscript/plugin-sagas-core/integration/commands |
./commands.ts |
17 | Checked existing-publisher sink for the service command relay. |
@netscript/plugin-sagas-core/telemetry |
./src/telemetry/mod.ts |
43 | Telemetry attributes and instrumentation helpers, including an OpenTelemetry tracer factory. |
@netscript/plugin-sagas-core/config |
./src/config/mod.ts |
24 | defineSagaConfig and the saga runtime configuration schemas. |
@netscript/plugin-sagas-core/contracts/v1 |
./src/contracts/v1/mod.ts |
31 | Version 1 saga API schemas and contract route types (sagasContract, sagasContractV1). |
@netscript/plugin-sagas-core/streams |
./src/streams/mod.ts |
17 | Durable stream schemas for projected saga instance records (sagasStreamSchema). |
@netscript/plugin-sagas-core/presets |
./src/presets/mod.ts |
9 | Preset composition helpers — startSagas, startSagaHandlers. |
@netscript/plugin-sagas-core/abstracts |
./src/abstracts/mod.ts |
62 | Abstract runtime contracts and reserved extension-point base classes. |
@netscript/plugin-sagas-core/testing |
./src/testing/mod.ts |
67 | createTestSagaRuntime plus in-memory bus and store doubles for deterministic verification. |
@netscript/plugin-sagas-core/agent |
./src/agent/mod.ts |
6 | defineAgent — the agent-shaped builder over the same saga definition. |
Export counts are the symbol counts deno doc reports for each entrypoint; subpaths overlap where a
type is re-exported through more than one layer.
The stores subpath exposes isPrismaSagaStoreClient(value) for the canonical-store delegate
contract shared by API and runner bootstrap validation. Query delegates remain a separate API
requirement. resolveSagaStoreBackend accepts an optional defaultBackend used only when neither
environment nor appsettings selects a backend; invalid configured values always fail. Runner backend
selection controls canonical state; Prisma selection keeps replay reservations and applied keys in a
separate KV handle that is closed on shutdown.
Root surface (@netscript/plugin-sagas-core)
Defining a saga
| Symbol | Kind | Description |
|---|---|---|
defineSaga |
function | Start a userland saga definition chain. |
SagaBuilder |
interface | The userland fluent saga builder. |
SagaBuilderPhase |
type alias | Typestate phase for the userland saga builder. |
SagaDefinition |
type alias | Frozen saga definition produced by the fluent DSL. |
SagaState |
type alias | Base state shape accepted by saga definitions. |
SagaContext |
type alias | Handler context passed to pure saga projections. |
SagaHandler |
type alias | Synchronous saga handler that returns cascaded messages. |
SagaEvent |
type alias | Event shape inferred by defineSaga().on(type, handler). |
SagaMessage |
type alias | Base event or command delivered to a saga handler. |
Cascaded messages
A handler is a pure projection: it returns cascaded messages rather than performing effects. These constructors keep their existing cascade meanings. Opted-in ordinary transition handlers can also return the explicit pure worker effects documented below.
| Symbol | Kind | Description |
|---|---|---|
send |
function | Create a cascade that republishes an internal saga message onto the saga bus. |
schedule |
function | Create a cascaded scheduled message. |
sagaComplete |
function | Create a terminal saga completion message. |
sagaFail |
function | Create a terminal saga failure message. |
sagaCompensate |
function | Create a cascaded compensation message. |
spawn |
function | Rejects an unsupported child-saga spawn request — spawn cascades are not implemented. |
CASCADED_MESSAGE_KINDS |
variable | Cascaded message kinds emitted by saga handlers. |
CascadedMessage |
type alias | Message emitted by a saga handler as its only side-effect ledger. |
CascadedMessageKind |
type alias | Cascaded message discriminator. |
CascadedMessageOptions |
type alias | Common options accepted by cascaded message constructors. |
CascadedMessageTarget |
type alias | Cascaded message target for jobs, sagas, or arbitrary runtime adapters. |
SendOptions |
type alias | Options for republishing an internal saga message onto the saga bus. |
SagaScheduleDelay |
type alias | Delay accepted by the schedule() cascaded-message constructor. |
SpawnOptions |
type alias | Options reserved for the unsupported spawn() cascade. |
Signals and queries
| Symbol | Kind | Description |
|---|---|---|
defineSignal |
function | Define a signal that can be sent to a running saga instance. |
defineQuery |
function | Define a synchronous read-only query for a running saga instance. |
SignalDefinition |
type alias | Signal definition reserved by the public DSL. |
QueryDefinition |
type alias | Query definition reserved by the public DSL. |
SagaSignalHandler |
type alias | Signal handler reserved by the userland saga DSL. |
SagaQueryHandler |
type alias | Synchronous query handler reserved by the userland saga DSL. |
SyncQueryResult |
type alias | Synchronous query result accepted by onQuery; promises are rejected at type level. |
Correlation and identity
| Symbol | Kind | Description |
|---|---|---|
SagaCorrelation |
type alias | Extracts a correlation key from an incoming saga message. |
SagaCorrelationKey |
type alias | Branded correlation key used to route messages to saga instances. |
SagaCorrelationRule |
type alias | Named correlation rule stored on a saga definition. |
SagaId |
type alias | Branded saga definition identifier. |
SagaInstanceId |
type alias | Branded saga instance identifier. |
SagaMessageId |
type alias | Branded message identifier for runtime and diagnostics records. |
Policies
| Symbol | Kind | Description |
|---|---|---|
RetryPolicy |
type alias | Retry policy for saga handlers and cascaded messages. |
SagaConcurrencyOptions |
type alias | Concurrency options accepted by the saga builder. |
SagaConcurrencyPolicy |
type alias | Concurrency policy for a saga definition. |
SAGA_DURABILITY_TIERS |
variable | Durability tiers supported by saga definitions. |
SagaDurabilityTier |
type alias | Saga durability tier. |
Handlers are synchronous
A saga handler is synchronous and returns cascaded messages; it does not await. That is what makes
replay deterministic and what keeps the compensation path a pure function of the transcript. Work
that must happen outside the handler — enqueueing a worker job, calling a service — is expressed as
a cascaded message the runtime dispatches, or through the explicit
@netscript/plugin-sagas-core/integration/workers helpers.
spawn() is present in the surface but rejects: child-saga spawn cascades are unsupported, and
calling it raises rather than silently succeeding. Its return type is never, so a handler that
returns spawn(...) does not type-check into the cascade union by accident.
Composing a runtime
@netscript/plugin-sagas-core/runtime exposes createSagaRuntime, which takes explicit ports
rather than reaching for globals. The store, transport, clock, and idempotency edges are all
injected, so the same definitions run against Redis or Garnet in production
(@netscript/plugin-sagas-core/transports) and against in-memory doubles in tests
(@netscript/plugin-sagas-core/testing).
@netscript/plugin-sagas-core/presets collapses the common case: startSagas composes a runtime
and starts it, and startSagaHandlers binds a definition set to an already-composed runtime.
Related pages
@netscript/plugin-sagas— the deployable plugin that binds this core to a NetScript host.@netscript/plugin-workers-core— the worker primitives theintegration/workershelpers dispatch to.@netscript/plugin-streams-core— the producer behind the projected instance stream.
Back to the reference overview.
Checked command sink
createSagaCommandOutboxSink adapts the existing SagaPublisherPort through publishSagaOrThrow.
It forwards stable outbox message/idempotency identity, correlation and W3C context, checks type and
acceptance time, and returns only normalized identity/time. Rejected, unavailable, mismatched and
malformed results cannot settle. The supplied publisher owns transport timeout; cancellation is
cooperative around its awaited call. A post-publication crash can redeliver the same identity, so
consumer side effects require independent idempotency. Construction starts no work and requires no
permissions; supplied publishers own their permissions.
| Symbol | Kind | Description |
|---|---|---|
createSagaCommandOutboxSink |
function | Compose a checked existing saga publisher sink. |
SagaCommandSinkOptions |
type alias | Registry identity and supplied checked publisher. |
CommandJson |
type alias | Service-owned decoded canonical payload. |
CommandTraceContext |
type alias | Service-owned validated W3C fields. |
CommandOutboxDelivery |
type alias | Service-owned decoded delivery. |
CommandOutboxSink |
interface | Service-owned documented acceptance boundary. |
CommandOutboxAcceptance |
type alias | Service-owned normalized checked identity/time. |
Atomic worker-command effects
| Symbol | Kind | Description |
|---|---|---|
WORKER_COMMAND_EFFECT_KINDS |
constant | Finite distinct worker-job and worker-task effect tags. |
WorkerCommandEffect |
type alias | Pure intent privately bound to a selected worker definition and detached payload. |
SagaTransitionEffect |
type alias | Ordinary transition ledger union of existing cascades and explicit worker commands. |
SagaTransitionHandler |
type alias | Synchronous typed transition projection. |
workerJobEffect |
function | Binds a selected schema-backed job to its required payload before persistence. |
workerTaskEffect |
function | Binds a selected runtime-schema task to its required payload. |
WorkerCommandEffectOptions |
type alias | Host-owned destination/topic routing. |
WorkerJobEffectDefinition |
type alias | Selected branded job plus its runtime payload schema. |
WorkerTaskEffectDefinition |
type alias | Selected branded task plus its runtime payload schema. |
SagaTransitionCommitPort |
interface | Explicit atomic optimistic transition, replay and outbox boundary. |
SagaTransitionCommitRequest |
type alias | Expected version and detached complete local row set. |
SagaTransitionCommitResult |
type alias | Commit result; false means already committed inbound replay with no new writes. |
SagaTransitionStore |
type alias | Existing saga store with the explicit atomic capability. |
createPrismaSagaTransitionStore |
function | Named first-party true-callback PostgreSQL store from ./stores. |
PrismaSagaTransitionStore |
interface | Atomic store with detached diagnostic state/history reads. |
PrismaSagaTransitionStoreOptions |
type alias | Required finite physical transaction timeout. |
SAGA_RUNTIME_CORRELATION_SELECTOR |
constant | Existing shipped Prisma correlation selector, curated through stores. |
Select .durableWorkerCommands() on the saga definition. Registration refuses KV and the legacy
unbound Prisma store before any handler/storage work; the test memory store and named bound
PostgreSQL store satisfy the capability. send() stays an internal saga-message cascade. Worker
effects are valid only in ordinary top-level transitions; compensation and nested scheduled cascades
retain their existing ledger.
Selected schema validation and bounded canonical I-JSON encoding precede all transition writes. Non-JSON prototypes/accessors and schema transformations changing canonical identity are refused. Stable versioned SHA-256 command identities include saga id, instance id, next version and original effect ordinal. State CAS, correlation, history, outbound intents and hashed inbound marker share one commit. Failed work retains retry eligibility; committed replay writes nothing. The marker is limited to this protocol and does not close general Prisma idempotency parity debt.
Migrate the shipped saga runtime/replay models and command outbox before selecting PostgreSQL. The host supplies the real generated interactive callback and owns resources; the database bound writer reuses the existing command-outbox append SQL. Configure the generic relay's worker sink topic map with the same selected definition id; routing is explicit host policy. The relay handles leases, bounded retries, drain, checked receipts and settlement. Delivery is at least once; one effective application additionally requires durable worker idempotency.
The generated-client consumer example is
packages/plugin-sagas-core/tests/fixtures/transition-store/saga-relay-cohort.ts.template. It uses
the generic command-outbox relay and worker sink with the producer, native worker dispatcher,
persisted Deno KV execution/applied state, first-party worker stream producer and file-backed
Durable Streams server. Progress survives storage/server restart; completion uses
publishSagaOrThrow(). No progress event is mirrored into command outbox rows. The example
demonstrates source integration; deploy the capability only when the producer and its command-outbox
relay prerequisites are available together in the application's pinned release.
KV retention
Topic groups declare retention.completedDays (default 7) and retention.archiveToDb (default
false). Open pending, running, and compensating instances retain canonical state,
correlations, transition history, applied replay keys, and the KV query projection until they become
terminal. activeDays is a legacy active-window hint; it does not expire live replay data.
Terminal completed, failed, cancelled, and compensated instances expire after
completedDays, measured from the terminal timestamp. Canonical state, forward and reverse
correlations, new terminal transitions, and saga_instances query documents use KV expireIn. The
saga service applies the same deadline to earlier transitions and applied keys in atomic pages of at
most 10 entries (12 checks and 11 mutations including state and cursor), conservatively bounded for
Deno KV's mutation byte budget even for full-size values. A persistent sagas/retention cursor
resumes that work after restart and is deleted once all history has received its TTL. Low-level
KvSagaStore callers must drive cleanupRetention(); it returns whether work remains. The shipped
durable runtime drains backlog without a polling delay, backs off from 100 ms to 30 seconds while
idle, and logs/retries transient sweep failures with exponential backoff. Retention failures do not
prevent runtime startup. A backlog can delay migration of historical keys, so backend expiry alone
is guaranteed only after their terminal deadline has been assigned. No API read or owner app session
drives cleanup.
Transport reservations already use their separate deduplication TTL. With a KV query projection,
archiveToDb: true explicitly enables the optional Prisma archive for that saga group and requires
a host Prisma client. The default archiveToDb: false skips it, keeping KV-only installs DB-free.
With Sagas.Projection.Backend: prisma, the Prisma query model remains required runtime state so
listInstances, getInstance, and stream hydration stay populated independently of archival.
Migration: KvSagaAppliedKeyStore.activeTtlMs is deprecated and no longer expires open replay
markers. Configure completedRetentionDays to match the canonical terminal window instead. Markers
survive open instances and receive their deadline only after terminal state.
The default saga runner stores new scheduled cascades in the existing KV polling queue under
queue:pending/sagas-scheduled; ready messages still use the configured delivery provider. Each
scheduled cascade carries its originating durable instance id separately from the destination
correlation. Pending, running, and compensating instances keep their timers. Terminal transitions,
including sagaComplete() alongside schedule(), still enqueue follow-ups. Timers remain eligible
for delivery until the origin's completedAt (or terminal updatedAt) plus completedDays
deadline, regardless of their destination. After that deadline, or once canonical state has expired,
pending and due timers are cancelled. SagaQueueScheduler.retentionDrops counts every successful
pending key deletion and every due-message drop; lost CAS races do not increase it. A cancellable
scheduler binds canonical state and its retention policy before startup; the existing background
retention loop inspects at most 101 pending keys and cancels at most 100 per pass. A persistent
queue cursor resumes across restart and prevents an active prefix from starving terminal cleanup.
Cancellation uses per-key CAS, so it does not remove a concurrently rewritten message. Backlog is
drained without an idle delay; idle and failure waits use the existing 100 ms to 30 second backoff.
Cleanup requires no API read or owner session. Due timers check canonical state before forwarding to
the delivery queue; an already forwarded/in-flight delivery remains subject to the engine's existing
terminal checks.
Custom runner delivery queues preserve their existing native-delay behavior unless the host supplies
scheduledQueue with the cancelPending capability. Low-level SagaQueueScheduler callers supply
that queue, bind the canonical store with matching completedRetentionDays, and drive
cleanupRetention(). Without a bound store or cancellation capability, startup logs a warning and
uses the ready queue's native delays. This preserves injected-engine and custom-factory runner
configurations. Cleanup before binding returns false; a second start is rejected. The independent
core SagaScheduler uses KvSagaSchedulerStore from ./stores for bounded retention. Bind
canonical state and the matching completedRetentionDays policy before startup; low-level callers
drive cleanupRetention() from a background service loop. The shipped durable runtime drives this
hook for an injected scheduler. A pass inspects at most ten records and persists its cursor across
restart. Pending, claimed, dispatched, and failed records receive KV TTL anchored to the origin's
terminal deadline; expired scoped records are deleted with CAS. Open instances and all unscoped
records, including new schedules without an origin, survive without TTL. Due delivery checks
canonical origin state; missing state cancels scoped records. Replay keyed by origin, message
idempotency key (or message id), and scheduled time does not renew the deadline or replace outcomes.
Unkeyed schedules have distinct identities. Claim leases recover after 30 seconds; stale claim
outcomes cannot replace newer claims or recreate deleted records. The host owns the KV adapter and
injects the same clock into scheduler and adapter. A time-ordered due index honors the caller's
batch limit and atomically re-keys claims to their lease deadline; retained outcomes never delay due
work. Adapter-private claim tokens survive deadline updates, and empty claims make no KV writes.
Custom stores need the optional cancellation and cleanup capabilities when binding retention. All
core schedulers, including unbound ones, use content-hashed replay identities; unbound schedulers do
not enforce originating-instance retention. No database archive or app read drives this path. KV
canonical reads are batched in native pages of at most ten keys. Only explicitly scoped new messages
are cancellation candidates; legacy unscoped queue messages and native-provider delayed entries need
separate migration.
Terminal records written before the retention policy was installed still need a migration/backfill. This leaf proves new scoped delayed-message cancellation; legacy saga/trigger terminal-family backfill, new and legacy unscoped schedules, and native-provider delayed messages remain separate acceptance scope for issue #2109.
Exports: discovery metadata
The @netscript/plugin-sagas-core/discovery entrypoint (./src/builders/discovery.ts) exports
NETSCRIPT_CONTRIBUTION_BUILDERS. Pass these immutable patterns through the plugin SDK
additionalBuilders option. The CLI composes first-party metadata before walking the project, so
single-plugin updates succeed regardless of workers/streams order.
| Export specifier | Module |
|---|---|
@netscript/plugin-sagas-core/discovery |
./src/builders/discovery.ts |