Skip to main content
0.0.x

@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.


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