@rotorsoft/act 1.35.3 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +19 -39
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts +0 -4
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/builders/act-builder.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-reaction.d.ts +2 -2
- package/dist/@types/internal/circuit-breaker.d.ts +0 -7
- package/dist/@types/internal/circuit-breaker.d.ts.map +1 -1
- package/dist/@types/internal/correlate-cycle.d.ts +0 -18
- package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
- package/dist/@types/internal/drain-cycle.d.ts +0 -14
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/event-sourcing.d.ts +1 -1
- package/dist/@types/internal/settle.d.ts +0 -9
- package/dist/@types/internal/settle.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +2 -2
- package/dist/@types/types/errors.d.ts +11 -26
- package/dist/@types/types/errors.d.ts.map +1 -1
- package/dist/{chunk-H2QN755D.js → chunk-A3XDAB7T.js} +1 -1
- package/dist/chunk-A3XDAB7T.js.map +1 -0
- package/dist/{chunk-SVVVS6GA.js → chunk-BRJZQ4IY.js} +8 -8
- package/dist/chunk-BRJZQ4IY.js.map +1 -0
- package/dist/{chunk-F2YAA4SE.cjs → chunk-C67XTW3W.cjs} +19 -19
- package/dist/{chunk-F2YAA4SE.cjs.map → chunk-C67XTW3W.cjs.map} +1 -1
- package/dist/{chunk-4KDMNSPJ.cjs → chunk-NXURHBSB.cjs} +1 -1
- package/dist/chunk-NXURHBSB.cjs.map +1 -0
- package/dist/index.cjs +264 -243
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +122 -101
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +4 -4
- package/dist/test/index.js +2 -2
- package/dist/types/index.cjs +2 -2
- package/dist/types/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-4KDMNSPJ.cjs.map +0 -1
- package/dist/chunk-H2QN755D.js.map +0 -1
- package/dist/chunk-SVVVS6GA.js.map +0 -1
package/dist/test/index.cjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
var
|
|
5
|
-
require('../chunk-
|
|
4
|
+
var _chunkC67XTW3Wcjs = require('../chunk-C67XTW3W.cjs');
|
|
5
|
+
require('../chunk-NXURHBSB.cjs');
|
|
6
6
|
|
|
7
7
|
// src/test/sandbox.ts
|
|
8
8
|
var _vitest = require('vitest');
|
|
9
9
|
async function sandbox(builder, options = {}) {
|
|
10
|
-
const store = options.store ? await options.store() : new (0,
|
|
11
|
-
const cache = options.cache ? await options.cache() : new (0,
|
|
10
|
+
const store = options.store ? await options.store() : new (0, _chunkC67XTW3Wcjs.InMemoryStore)();
|
|
11
|
+
const cache = options.cache ? await options.cache() : new (0, _chunkC67XTW3Wcjs.InMemoryCache)();
|
|
12
12
|
await store.seed();
|
|
13
13
|
const app = builder.build({
|
|
14
14
|
...options.actOptions,
|
package/dist/test/index.js
CHANGED
package/dist/types/index.cjs
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
var
|
|
30
|
+
var _chunkNXURHBSBcjs = require('../chunk-NXURHBSB.cjs');
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
|
|
@@ -57,5 +57,5 @@ var _chunk4KDMNSPJcjs = require('../chunk-4KDMNSPJ.cjs');
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
exports.ActorSchema =
|
|
60
|
+
exports.ActorSchema = _chunkNXURHBSBcjs.ActorSchema; exports.CausationEventSchema = _chunkNXURHBSBcjs.CausationEventSchema; exports.CommittedMetaSchema = _chunkNXURHBSBcjs.CommittedMetaSchema; exports.ConcurrencyError = _chunkNXURHBSBcjs.ConcurrencyError; exports.DEFAULT_LANE = _chunkNXURHBSBcjs.DEFAULT_LANE; exports.DEFAULT_MAX_SUBSCRIBED_STREAMS = _chunkNXURHBSBcjs.DEFAULT_MAX_SUBSCRIBED_STREAMS; exports.DEFAULT_SETTLE_DEBOUNCE_MS = _chunkNXURHBSBcjs.DEFAULT_SETTLE_DEBOUNCE_MS; exports.DeferSignal = _chunkNXURHBSBcjs.DeferSignal; exports.Environments = _chunkNXURHBSBcjs.Environments; exports.Errors = _chunkNXURHBSBcjs.Errors; exports.EventMetaSchema = _chunkNXURHBSBcjs.EventMetaSchema; exports.ExitCodes = _chunkNXURHBSBcjs.ExitCodes; exports.InvariantError = _chunkNXURHBSBcjs.InvariantError; exports.LogLevels = _chunkNXURHBSBcjs.LogLevels; exports.NonRetryableError = _chunkNXURHBSBcjs.NonRetryableError; exports.PreconditionError = _chunkNXURHBSBcjs.PreconditionError; exports.QuerySchema = _chunkNXURHBSBcjs.QuerySchema; exports.REDACTED = _chunkNXURHBSBcjs.REDACTED; exports.SHREDDED = _chunkNXURHBSBcjs.SHREDDED; exports.SNAP_EVENT = _chunkNXURHBSBcjs.SNAP_EVENT; exports.StoreError = _chunkNXURHBSBcjs.StoreError; exports.StreamClosedError = _chunkNXURHBSBcjs.StreamClosedError; exports.TOMBSTONE_EVENT = _chunkNXURHBSBcjs.TOMBSTONE_EVENT; exports.TargetSchema = _chunkNXURHBSBcjs.TargetSchema; exports.ValidationError = _chunkNXURHBSBcjs.ValidationError; exports.ZodEmpty = _chunkNXURHBSBcjs.ZodEmpty; exports.pii_fields = _chunkNXURHBSBcjs.pii_fields; exports.sensitive = _chunkNXURHBSBcjs.sensitive;
|
|
61
61
|
//# sourceMappingURL=index.cjs.map
|
package/dist/types/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/act-root/act-root/libs/act/dist/chunk-4KDMNSPJ.cjs","../src/types/act.ts","../src/types/errors.ts","../src/types/ports.ts","../src/types/reaction.ts","../src/types/schemas.ts","../src/sensitive.ts","../src/types/index.ts"],"names":["z"],"mappings":"AAAA;ACsBO,IAAM,2BAAA,EAA6B,EAAA;AAMnC,IAAM,+BAAA,EAAiC,GAAA;ADzB9C;AACA;AEiBO,IAAM,OAAA,EAAS;AAAA,EACpB,eAAA,EAAiB,gBAAA;AAAA,EACjB,iBAAA,EAAmB,kBAAA;AAAA;AAAA,EAEnB,cAAA,EAAgB,eAAA;AAAA,EAChB,gBAAA,EAAkB,iBAAA;AAAA,EAClB,iBAAA,EAAmB,mBAAA;AAAA,EACnB,iBAAA,EAAmB,mBAAA;AAAA,EACnB,UAAA,EAAY;AACd,CAAA;AA8BO,IAAM,gBAAA,EAAN,MAAA,QAA8B,MAAM;AAAA;AAAA,EAEzB;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EAEhB,WAAA,CAAY,MAAA,EAAgB,OAAA,EAAc,OAAA,EAAc;AACtD,IAAA,KAAA,CAAM,CAAA,QAAA,EAAW,MAAM,CAAA,QAAA,CAAU,CAAA;AACjC,IAAA,IAAA,CAAK,KAAA,EAAO,MAAA,CAAO,eAAA;AACnB,IAAA,IAAA,CAAK,OAAA,EAAS,MAAA;AACd,IAAA,IAAA,CAAK,QAAA,EAAU,OAAA;AACf,IAAA,IAAA,CAAK,QAAA,EAAU,OAAA;AAAA,EACjB;AACF,CAAA;AAoBO,IAAM,eAAA,EAAN,MAAA,QAMG,MAAM;AAAA;AAAA,EAEL;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOT,WAAA,CACE,MAAA,EACA,OAAA,EACA,MAAA,EACA,QAAA,EACA,WAAA,EACA,KAAA,EAAqC,WAAA,EACrC;AACA,IAAA,KAAA,CAAM,CAAA,EAAA;AACD,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACP,EAAA;AACF;AA0Ca;AAOX,EAAA;AAOQ,IAAA;AACD,IAAA;AACP,EAAA;AACF;AAyCa;AAA+B;AAE1B,EAAA;AAAA;AAEA,EAAA;AAAA;AAEA,EAAA;AAAA;AAEA,EAAA;AAEhB,EAAA;AAUE,IAAA;AACE,MAAA;AAGI,QAAA;AACD,MAAA;AACL,IAAA;AACK,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACP,EAAA;AACF;AAyBa;AAAgC;AAE3B,EAAA;AAEhB,EAAA;AACQ,IAAA;AACD,IAAA;AACA,IAAA;AACP,EAAA;AACF;AAyBa;AAAyB;AAEpB,EAAA;AAEhB,EAAA;AACQ,IAAA;AACD,IAAA;AACA,IAAA;AACP,EAAA;AACF;AAoCa;AAAgC;AAElB,EAAA;AAEzB,EAAA;AACQ,IAAA;AACD,IAAA;AACA,IAAA;AACP,EAAA;AACF;AAyBa;AAA0B;AAAA;AAAA;AAAA;AAK5B,EAAA;AAET,EAAA;AACQ,IAAA;AACD,IAAA;AACA,IAAA;AACP,EAAA;AACF;AFtSW;AACA;AGkvCE;AASA;AHxvCF;AACA;AIuTE;AJrTF;AACA;AKnIgC;ALqIhC;AACA;AM/HF;AAWI;AAaP;AAMU;AACR,EAAA;AAKG,EAAA;AACX;AAWS;AACH,EAAA;AACG,EAAA;AACD,IAAA;AAGE,IAAA;AAKF,IAAA;AACE,IAAA;AACD,IAAA;AACC,IAAA;AACR,EAAA;AACF;AAwBgB;AACR,EAAA;AACF,EAAA;AACI,IAAA;AACN,IAAA;AACM,MAAA;AACC,IAAA;AACT,EAAA;AACM,EAAA;AACF,EAAA;AACI,IAAA;AACN,IAAA;AACE,MAAA;AACE,QAAA;AACF,MAAA;AACE,QAAA;AACG,IAAA;AACT,EAAA;AACQ,EAAA;AACV;AAQgB;AACP,EAAA;AACT;ANoDW;AACA;AKpKE;AAUA;AAOA;AA4BG;AACd,EAAA;AAIA,EAAA;AACO,EAAA;AACT;AAKa;AAEH,EAAA;AACE,EAAA;AAGT;AAKU;AAEDA,EAAAA;AACD,EAAA;AACP,EAAA;AAGD;AAKU;AACL,EAAA;AACE,EAAA;AACAA,EAAAA;AACT;AAKY;AAET,EAAA;AACA,EAAA;AACE,IAAA;AACO,IAAA;AACR,EAAA;AAEF;AAKU;AAEH,EAAA;AACEA,EAAAA;AACCA,EAAAA;AACAA,EAAAA;AACH,EAAA;AAEP;AAiBU;AAEDA,EAAAA;AACDA,EAAAA;AACCA,EAAAA;AACDA,EAAAA;AACAA,EAAAA;AACP,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAED;ALyEQ;AACA;AO/LE;AACX,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAWa;AACX,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAYa;AP4KF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/act-root/act-root/libs/act/dist/chunk-4KDMNSPJ.cjs","sourcesContent":[null,"/**\n * Public contracts of the orchestrator: the options `act().build()` takes,\n * the lifecycle events an `Act` emits, and the public defaults behind them.\n * The `Act` class implements them in `../act.ts`; their Zod schemas and\n * resolvers live in `../internal/config.ts`.\n */\nimport type {\n CloseResult,\n Correlator,\n Schema,\n Schemas,\n Snapshot,\n} from \"./action.js\";\nimport type { Cache, Store, StoreNotification } from \"./ports.js\";\nimport type { BlockedLease, Drain, Lease } from \"./reaction.js\";\nimport type { SchemaRegister } from \"./registry.js\";\n\n/**\n * Default debounce window (ms) for `settle()` when neither the per-call\n * `SettleOptions.debounceMs` nor `ActOptions.settleDebounceMs` is set.\n * Coalesces commits in the same tick and small bursts.\n */\nexport const DEFAULT_SETTLE_DEBOUNCE_MS = 10;\n/**\n * Default LRU cap for the subscribed-streams cache. Apps that mint many\n * dynamic targets (one per aggregate) should override it with\n * `ActOptions.maxSubscribedStreams`.\n */\nexport const DEFAULT_MAX_SUBSCRIBED_STREAMS = 1000;\n\n/** Per-Act ports bag. Both required together — a shared cache across stores would collide on stream keys. */\nexport type Scoped = {\n readonly store: Store;\n readonly cache: Cache;\n};\n\n/** Circuit breaker state, reported with every `error` lifecycle event. */\nexport type CircuitState = \"closed\" | \"open\" | \"half-open\";\n\n/** Public, all-optional circuit-breaker options bag for `act().build()`. */\nexport type CircuitBreakerOptions = {\n failureThreshold?: number;\n cooldownMs?: number;\n};\n\n/** Resolved autoclose configuration after validation + default expansion. */\nexport type AutocloseConfig = {\n autocloseWindow?: { start: number; end: number; timeZone: string };\n};\n\n/**\n * Lifecycle events emitted by {@link Act}, mapped to their payload type.\n * Drives the typing of `emit` / `on` / `off` — the event-name argument\n * narrows its payload at the call site.\n *\n * The first parameter is kept (unused) for arity compatibility: `committed`\n * carries snapshots of whichever state each action targeted, so its honest\n * element type is `Snapshot<Schema, TEvents>` — the register map itself was\n * never the shape of any snapshot's state.\n */\nexport type ActLifecycleEvents<\n _TSchemaReg extends SchemaRegister<TActions>,\n TEvents extends Schemas,\n TActions extends Schemas,\n> = {\n committed: Snapshot<Schema, TEvents>[];\n acked: Lease[];\n blocked: BlockedLease[];\n settled: Drain<TEvents>;\n closed: CloseResult;\n /**\n * A **different process** committed an event to the same backing store.\n *\n * Fires only when the configured store implements\n * {@link Store.notify} and there is at least one registered reaction.\n * The orchestrator uses the same signal internally to wake `settle()`\n * (unless `autoSettle: false`); listeners get the raw payload for SSE\n * fan-out, dashboards, and audit logs.\n *\n * Local commits do *not* fire `notified` (use `committed` for those):\n * stores self-filter their own writes so this channel has a clean\n * cross-process semantic.\n */\n notified: StoreNotification;\n /**\n * A stream's sensitive-data payload was wiped via {@link Act.forget}.\n * Fires exactly once per successful `forget(stream)` call — idempotent\n * second calls (no PII left on the stream) return `eventCount: 0` and\n * do NOT re-emit. Apps that never call `forget()` never see this event.\n *\n * Listeners use it for the compliance side of GDPR / CCPA: audit log,\n * downstream cache busts, projection-side wipes that the framework\n * doesn't reach (e.g., search indexes, ETL caches). The framework's own\n * cache is invalidated by `forget()` itself before the event fires.\n */\n forgotten: { stream: string; at: Date; eventCount: number };\n /**\n * A store operation failed during the drain loop. Fires on\n * every failed drain cycle — typically a {@link StoreError} from a\n * degraded backend — carrying the orchestrator circuit breaker's state\n * after the failure (`open` means the drain loop has backed off and will\n * retry after the cooldown). Listen to alert on a degraded store; the\n * framework logs the same error regardless. Emitted only when a listener\n * is registered (Node's `EventEmitter` throws on an unhandled `\"error\"`).\n */\n error: { error: unknown; circuit: CircuitState };\n};\n\n/**\n * Options for {@link Act} construction (passed via {@link ActBuilder.build}).\n *\n * @property maxSubscribedStreams - Cap for the LRU tracking what each\n * dynamically resolved reaction target was last subscribed at. Statically\n * declared targets are held outside it and never evicted, so their lane\n * and priority stay owned by the build-time subscribe.\n * Default: {@link DEFAULT_MAX_SUBSCRIBED_STREAMS}.\n * @property settleDebounceMs - Debounce window (ms) used by `settle()` when\n * the caller doesn't pass `SettleOptions.debounceMs`. Tune this once per\n * Act instance instead of threading the value through every call site.\n * Default: {@link DEFAULT_SETTLE_DEBOUNCE_MS}.\n * @property autoSettle - Settle on its own after a change that frees\n * reaction work: a commit, `reset()`, `unblock()`, `restore()` or a\n * notification from another process. Set `false` when you walk the\n * pipeline yourself with `correlate()` / `drain()` (tests that check its\n * steps, tools); the event-sourcing guide's settle section lists the\n * timers that still run. Default: `true`.\n */\nexport type ActOptions<TLanes extends string = string> = {\n readonly maxSubscribedStreams?: number;\n readonly settleDebounceMs?: number;\n readonly autoSettle?: boolean;\n /**\n * Per-Act ports. When set, this Act runs against the\n * provided store + cache instead of the singletons — threaded via\n * AsyncLocalStorage so internals are unchanged. Both are required\n * together (a shared cache across distinct stores would collide on\n * stream keys). Omit for the singleton path.\n */\n readonly scoped?: Scoped;\n /**\n * Correlation-id generator for originating actions. When\n * omitted, Act uses {@link default_correlator}, which produces a\n * readable, time-monotonic-within-window, lowercase id of the form\n * `{state[:4]}-{action[:4]}-{ts}{rnd}` (18 chars).\n *\n * Reactions inherit `reactingTo.meta.correlation` so the chain stays\n * intact — the delegate is only consulted on originating commits and\n * for the close-the-books transaction.\n */\n readonly correlator?: Correlator;\n /** Restrict this process to a subset of declared lanes. */\n readonly onlyLanes?: ReadonlyArray<TLanes>;\n /**\n * Subscribe to {@link Store.notify} on this instance. Defaults\n * to `true`. Set `false` on instances that only commit and never\n * react — the subscriber-connection budget is the practical scaling\n * ceiling for the notify/listen pattern, and writer-only fleets\n * spend it for nothing when they subscribe to a channel they never\n * read. Commits still emit notifications (that's part of the\n * store's commit protocol); only the subscriber side is gated.\n */\n readonly listen?: boolean;\n /**\n * Run the local reaction pipeline on this instance. Defaults\n * to `true`. Set `false` on writer-only or sidecar instances: drain\n * controllers' auto-cycle workers don't start, `correlate()` /\n * `drain()` / `settle()` become no-ops, and the notify handler\n * skips its drain-wakeup arm (but still emits the `notified`\n * lifecycle event so observability sidecars work).\n */\n readonly drain?: boolean;\n /**\n * Orchestrator circuit breaker for the drain loop. After\n * `failureThreshold` consecutive store failures the breaker opens and\n * the drain loop skips `claim()` for `cooldownMs` instead of hammering a\n * down backend, then allows a half-open trial. Out-of-range values throw\n * a `ZodError` at `act().build()`. Defaults: threshold 5, cooldown 30s.\n */\n readonly circuitBreaker?: CircuitBreakerOptions;\n /**\n * Optional off-hours window restricting when autoclose evaluates. A\n * synthesized autoclose reaction that triggers outside the window\n * defers to the next instant the window opens — derived from the\n * window itself, no polling cadence. Hours are `[0, 23]` integers in\n * `timeZone` (IANA, default `\"UTC\"`, DST-correct); `start > end` is\n * an overnight window (e.g. `{ start: 22, end: 6 }`). Omit to\n * evaluate regardless of clock time.\n */\n readonly autocloseWindow?: {\n readonly start: number;\n readonly end: number;\n readonly timeZone?: string;\n };\n /**\n * Ignored: every action checks the state it leaves against the state's\n * schema before it commits, so a broken invariant refuses the action\n * with a {@link ValidationError} and nothing is stored. Loads, replays and\n * projection folds don't check.\n *\n * @deprecated The check is always on. Remove the option; removed in 2.0.\n */\n readonly validateFoldedState?: boolean;\n};\n","import type {\n Actor,\n DeferWhen,\n Message,\n Schema,\n Schemas,\n Snapshot,\n Target,\n} from \"./action.js\";\n\n/**\n * @packageDocumentation\n * @module act/types\n * @category Types\n * Application error type constants and error classes for the Act Framework.\n *\n * - `ERR_VALIDATION`: Schema validation error\n * - `ERR_PRECONDITION`: A precondition refused the action\n * - `ERR_INVARIANT`: A deprecated `.given` rule refused the action\n * - `ERR_CONCURRENCY`: Optimistic concurrency validation error on commits\n */\nexport const Errors = {\n ValidationError: \"ERR_VALIDATION\",\n PreconditionError: \"ERR_PRECONDITION\",\n /** @deprecated `.given` rules are preconditions; use `PreconditionError`. Removed in 2.0. */\n InvariantError: \"ERR_INVARIANT\",\n ConcurrencyError: \"ERR_CONCURRENCY\",\n StreamClosedError: \"ERR_STREAM_CLOSED\",\n NonRetryableError: \"ERR_NON_RETRYABLE\",\n StoreError: \"ERR_STORE\",\n} as const;\n\n/**\n * Thrown when an action or event payload fails Zod schema validation.\n *\n * This error indicates that data doesn't match the expected schema defined\n * for an action or event. The `details` property contains the Zod validation\n * error with specific information about what failed.\n *\n * @example Catching validation errors\n * ```typescript\n * import { ValidationError } from \"@rotorsoft/act\";\n *\n * try {\n * await app.do(\"createUser\", target, {\n * email: \"invalid-email\", // Missing @ symbol\n * age: -5 // Negative age\n * });\n * } catch (error) {\n * if (error instanceof ValidationError) {\n * console.error(\"Validation failed for:\", error.target);\n * console.error(\"Invalid payload:\", error.payload);\n * console.error(\"Validation details:\", error.details);\n * // details contains Zod error with field-level info\n * }\n * }\n * ```\n *\n * @see {@link https://zod.dev | Zod documentation} for validation details\n */\nexport class ValidationError extends Error {\n /** The type of target being validated (e.g., \"action\", \"event\") */\n public readonly target: string;\n /** The invalid payload that failed validation */\n public readonly payload: any;\n /** Zod validation error details */\n public readonly details: any;\n\n constructor(target: string, payload: any, details: any) {\n super(`Invalid ${target} payload`);\n this.name = Errors.ValidationError;\n this.target = target;\n this.payload = payload;\n this.details = details;\n }\n}\n\n/**\n * Thrown when a rule declared with the deprecated `.given([...])` refuses\n * an action, with the message `\"<action> failed invariant: <description>\"`.\n *\n * Those rules were always preconditions, not invariants: what must hold\n * after every action lives in the state's schema. {@link PreconditionError}\n * extends this class, so `instanceof InvariantError` still catches every\n * refused action.\n *\n * @deprecated Declare the rule with `.requires([...])` and catch\n * {@link PreconditionError}. Removed in 2.0.\n *\n * @template TState - State schema type\n * @template TEvents - Event schemas type\n * @template TActions - Action schemas type\n * @template TKey - Action name\n * @template TActor - Actor type extending base Actor\n */\nexport class InvariantError<\n TState extends Schema,\n TEvents extends Schemas,\n TActions extends Schemas,\n TKey extends keyof TActions,\n TActor extends Actor = Actor,\n> extends Error {\n /** The action that was attempted */\n readonly action: TKey;\n /** The action payload that was provided */\n readonly payload: Readonly<TActions[TKey]>;\n /** The target stream and actor context */\n readonly target: Target<TActor>;\n /** The current state snapshot when the rule was checked */\n readonly snapshot: Snapshot<TState, TEvents>;\n /** Human-readable description of the rule that refused the action */\n readonly description: string;\n\n /**\n * @param rule - Which layer refused the action: a `.given([...])` rule\n * (the default) or a precondition from `.requires([...])`. It names the\n * layer in the message.\n */\n constructor(\n action: TKey,\n payload: Readonly<TActions[TKey]>,\n target: Target<TActor>,\n snapshot: Snapshot<TState, TEvents>,\n description: string,\n rule: \"invariant\" | \"precondition\" = \"invariant\"\n ) {\n super(`${action as string} failed ${rule}: ${description}`);\n this.name = Errors.InvariantError;\n this.action = action;\n this.payload = payload;\n this.target = target;\n this.snapshot = snapshot;\n this.description = description;\n }\n}\n\n/**\n * Thrown when a precondition from `.requires([...])` refuses an action,\n * with the message `\"<action> failed precondition: <description>\"`.\n *\n * Preconditions are what the caller must meet for the action to run now.\n * They're checked after loading the current state and before the handler\n * emits anything, so nothing is stored. Invariants (what must hold after\n * every action) live in the state's schema instead; a broken one throws\n * {@link ValidationError}.\n *\n * It extends the deprecated {@link InvariantError} until 2.0, so code that\n * catches `InvariantError` keeps catching every refused action.\n *\n * @template TState - State schema type\n * @template TEvents - Event schemas type\n * @template TActions - Action schemas type\n * @template TKey - Action name\n * @template TActor - Actor type extending base Actor\n *\n * @example Catching a refused action\n * ```typescript\n * import { PreconditionError } from \"@rotorsoft/act\";\n *\n * try {\n * await app.do(\"withdraw\",\n * { stream: \"account-123\", actor: { id: \"user1\", name: \"Alice\" } },\n * { amount: 1000 }\n * );\n * } catch (error) {\n * if (error instanceof PreconditionError) {\n * console.error(\"Action:\", error.action);\n * console.error(\"Reason:\", error.description);\n * console.error(\"Current state:\", error.snapshot.state);\n * console.error(\"Attempted payload:\", error.payload);\n * }\n * }\n * ```\n *\n * @see {@link Precondition} for declaring them\n */\nexport class PreconditionError<\n TState extends Schema,\n TEvents extends Schemas,\n TActions extends Schemas,\n TKey extends keyof TActions,\n TActor extends Actor = Actor,\n> extends InvariantError<TState, TEvents, TActions, TKey, TActor> {\n constructor(\n action: TKey,\n payload: Readonly<TActions[TKey]>,\n target: Target<TActor>,\n snapshot: Snapshot<TState, TEvents>,\n description: string\n ) {\n super(action, payload, target, snapshot, description, \"precondition\");\n this.name = Errors.PreconditionError;\n }\n}\n\n/**\n * Thrown when optimistic concurrency control detects a conflict.\n *\n * This error occurs when trying to commit events to a stream that has been\n * modified by another process since it was last loaded. The version number\n * doesn't match expectations, indicating a concurrent modification.\n *\n * This is a normal occurrence in distributed systems and should be handled\n * by reloading the current state and retrying the action.\n *\n * @example Handling concurrency conflicts with retry\n * ```typescript\n * import { ConcurrencyError } from \"@rotorsoft/act\";\n *\n * async function transferWithRetry(from, to, amount, maxRetries = 3) {\n * for (let attempt = 0; attempt < maxRetries; attempt++) {\n * try {\n * await app.do(\"transfer\",\n * { stream: from, actor: currentUser },\n * { to, amount }\n * );\n * return { success: true };\n * } catch (error) {\n * if (error instanceof ConcurrencyError) {\n * if (attempt < maxRetries - 1) {\n * console.log(`Concurrent modification detected, retrying... (${attempt + 1}/${maxRetries})`);\n * await sleep(100 * Math.pow(2, attempt)); // Exponential backoff\n * continue;\n * }\n * }\n * throw error;\n * }\n * }\n * return { success: false, reason: \"Too many concurrent modifications\" };\n * }\n * ```\n *\n * @see {@link Store.commit} for version checking details\n */\nexport class ConcurrencyError extends Error {\n /** The stream that had the concurrent modification */\n public readonly stream: string;\n /** The actual current version in the store */\n public readonly lastVersion: number;\n /** The events that were being committed */\n public readonly events: Message<Schemas, keyof Schemas>[];\n /** The version number that was expected */\n public readonly expectedVersion: number;\n\n constructor(\n stream: string,\n lastVersion: number,\n events: Message<Schemas, keyof Schemas>[],\n expectedVersion: number\n ) {\n // Message lists stream + event names only. Payloads remain accessible\n // via `error.events` for callers who need them — keeping them out of\n // the message avoids MB-scale strings on contended writes and keeps\n // potentially-sensitive data out of log streams.\n super(\n `Concurrency error committing \"${events\n .map((e) => `${stream}.${e.name}`)\n .join(\n \", \"\n )}\". Expected version ${expectedVersion} but found version ${lastVersion}.`\n );\n this.name = Errors.ConcurrencyError;\n this.stream = stream;\n this.lastVersion = lastVersion;\n this.events = events;\n this.expectedVersion = expectedVersion;\n }\n}\n\n/**\n * Thrown when attempting to write to a stream that has been closed\n * with a tombstone event.\n *\n * A tombstoned stream is permanently closed — no further actions can\n * be executed against it. The only way to reopen a tombstoned stream\n * is through `Act.close()` with a `restart` callback.\n *\n * @example\n * ```typescript\n * import { StreamClosedError } from \"@rotorsoft/act\";\n *\n * try {\n * await app.do(\"updateTicket\", target, payload);\n * } catch (error) {\n * if (error instanceof StreamClosedError) {\n * console.error(`Stream ${error.stream} is closed`);\n * }\n * }\n * ```\n *\n * @see {@link Act.close} for closing streams\n */\nexport class StreamClosedError extends Error {\n /** The stream that is closed */\n public readonly stream: string;\n\n constructor(stream: string) {\n super(`Stream \"${stream}\" is closed (tombstoned)`);\n this.name = Errors.StreamClosedError;\n this.stream = stream;\n }\n}\n\n/**\n * Thrown by a {@link Store} adapter when an infrastructure operation fails\n * for a reason that is *not* a domain condition — a dropped connection, a\n * transaction rollback, a query timeout. It is the typed boundary between\n * \"the store is unavailable/degraded\" and the domain errors above\n * ({@link ConcurrencyError}, {@link StreamClosedError}), which describe\n * legitimate outcomes the caller should branch on.\n *\n * Adapters wrap their driver errors in `StoreError` (preserving the\n * original via `cause`) so the orchestrator can distinguish a degraded\n * backend from \"no work\" and react accordingly — see the drain circuit\n * breaker, which trips on repeated `StoreError`s and surfaces an\n * `error` lifecycle event instead of silently spinning on a down\n * database.\n *\n * @example\n * ```typescript\n * app.on(\"error\", ({ error, circuit }) => {\n * if (error instanceof StoreError)\n * alert(`store ${error.operation} failing; circuit=${circuit}`);\n * });\n * ```\n */\nexport class StoreError extends Error {\n /** The store operation that failed (e.g. `\"claim\"`, `\"ack\"`, `\"commit\"`). */\n public readonly operation: string;\n\n constructor(operation: string, options?: { cause?: unknown }) {\n super(`Store operation \"${operation}\" failed`, options);\n this.name = Errors.StoreError;\n this.operation = operation;\n }\n}\n\n/**\n * Thrown by a reaction handler to signal that the failure is permanent\n * and the drain pipeline should block the stream immediately, without\n * consuming the rest of the `maxRetries` budget.\n *\n * The drain finalizer detects `instanceof NonRetryableError` and forces\n * `block = options.blockOnError` regardless of `lease.retry`. When\n * `blockOnError` is `false`, behavior is unchanged (drain keeps retrying\n * forever) — the class never overrides the operator's explicit \"never\n * block\" choice.\n *\n * Use this for failures the handler *knows* won't get better on retry:\n * a 4xx from a webhook, a `ZodError` on malformed input, a \"user\n * deleted\" 404 from a downstream API. Use regular `Error` (or a\n * subclass) for transient failures so the existing retry-with-backoff\n * loop applies.\n *\n * @example Wrapping a permanent downstream error\n * ```typescript\n * import { NonRetryableError } from \"@rotorsoft/act\";\n *\n * .on(\"OrderConfirmed\")\n * .do(async function notifyPartner(event) {\n * const res = await fetch(url, ...);\n * if (res.status >= 400 && res.status < 500) {\n * throw new NonRetryableError(\n * `webhook ${url} responded ${res.status}`,\n * { cause: await res.text() }\n * );\n * }\n * if (!res.ok) throw new Error(`webhook ${url} responded ${res.status}`);\n * })\n * ```\n */\nexport class NonRetryableError extends Error {\n /** The original failure, if any. Mirrors the standard `Error.cause` shape. */\n public override readonly cause?: unknown;\n\n constructor(message: string, options?: { cause?: unknown }) {\n super(message);\n this.name = Errors.NonRetryableError;\n this.cause = options?.cause;\n }\n}\n\n/**\n * The control-flow signal a reaction handler throws to *defer* itself.\n * Unlike an error, a defer is not a failure: the dispatcher\n * (`build_handle`) catches it and produces a\n * `HandleResult.defer` — the triggering events stay pending (watermark not\n * advanced), `retry` is not bumped, and the drain re-visits the stream at the\n * resolved due-time.\n *\n * `DeferSignal` is the **imperative escape hatch** and is re-exported from the\n * package root, so reaction code can throw it directly when a static\n * `.defer(when)` step isn't expressive enough (a deadline computed from loaded\n * state, say). It carries the *unresolved* {@link DeferWhen}; the dispatcher\n * resolves it against the triggering event it is already dispatching (via\n * `resolve_defer_at`), which is what anchors `{ after }` and the `at` function\n * form to that event and keeps the due-time derivable. Fully dynamic times go\n * through `{ at: someDate }`.\n *\n * Modeled as an `Error` subclass — like `NonRetryableError` — so it rides the\n * existing `try/catch` in the handler loop instead of needing a separate\n * return channel through every dispatcher signature. The compiled autoclose\n * reaction throws the same signal.\n */\n\nexport class DeferSignal extends Error {\n /**\n * The unresolved schedule. The dispatcher turns this into an absolute\n * due-time by resolving it against the triggering event.\n */\n readonly when: DeferWhen;\n\n constructor(when: DeferWhen) {\n super(\"reaction deferred\");\n this.name = \"DeferSignal\";\n this.when = when;\n }\n}\n","/**\n * @packageDocumentation\n * @module act/types\n * @category Types\n * Types and interfaces for event store ports and disposables in the Act Framework.\n */\nimport type {\n Committed,\n EventMeta,\n EventSource,\n Message,\n Query,\n Schema,\n Schemas,\n} from \"./action.js\";\nimport type { BlockedLease, Lease } from \"./reaction.js\";\n\n/**\n * A function that disposes of a resource asynchronously.\n * @returns Promise that resolves when disposal is complete.\n */\nexport type Disposer = () => Promise<void>;\n\n/**\n * An object that can be disposed of asynchronously.\n */\nexport type Disposable = { dispose: Disposer };\n\n// ---------------------------------------------------------------------------\n// Logger\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal logger port compatible with pino, winston, bunyan, and console.\n *\n * Each log method accepts either:\n * - `(msg: string)` — plain message\n * - `(obj: unknown, msg?: string)` — structured data with optional message\n *\n * Implementations should respect `level` to gate output.\n *\n * @see {@link ConsoleLogger} for the default implementation\n * @see {@link https://www.npmjs.com/package/@rotorsoft/act-pino | @rotorsoft/act-pino} for the Pino adapter\n */\nexport interface Logger extends Disposable {\n level: string;\n fatal(obj: unknown, msg?: string): void;\n fatal(msg: string): void;\n error(obj: unknown, msg?: string): void;\n error(msg: string): void;\n warn(obj: unknown, msg?: string): void;\n warn(msg: string): void;\n info(obj: unknown, msg?: string): void;\n info(msg: string): void;\n debug(obj: unknown, msg?: string): void;\n debug(msg: string): void;\n trace(obj: unknown, msg?: string): void;\n trace(msg: string): void;\n child(bindings: Record<string, unknown>): Logger;\n}\n\n// ---------------------------------------------------------------------------\n// Store\n// ---------------------------------------------------------------------------\n\n/**\n * Result of a {@link Store.subscribe} call.\n *\n * Named rather than written inline because three places state it — the port,\n * the drain's helper, and every adapter — and they drifted apart the moment\n * one of them grew a field.\n */\nexport type SubscribeResult = {\n subscribed: number;\n watermark: number;\n /**\n * Whether the caller holds the correlation lease and should scan.\n *\n * `undefined` when no `correlator` was supplied — no lease was requested,\n * so there is no answer and the caller scans as it always has.\n *\n * Acquiring MUST be atomic. A read-then-write lets two workers both see\n * an expired lease and both believe they hold it, which is precisely the\n * duplication the lease removes.\n *\n * Released by expiry only: a crash and a clean stop then take the same\n * path, so the recovery path is the common one. A holder can still hand\n * it back early by renewing with a minimal `millis`.\n */\n correlating?: boolean;\n /**\n * The correlate checkpoint: how far `correlate` has **read** the\n * event log, or `-1` when it has never advanced.\n *\n * Distinct from a subscription's `at` (how far a *target* has been\n * processed) — a run of events resolving to no target moves this and no\n * watermark. It rides `subscribe`'s existing return and is advanced by\n * the same call's `correlated_at` argument, so maintaining it costs no\n * store round trip of its own: `correlate` already calls `subscribe`\n * with the targets each scan discovered.\n *\n * Adapters keep it in their own relation, never as a subscription, so no\n * stream-scoped surface (`prioritize`, `reset`, `unblock`,\n * `query_streams`, `blocked_streams`) ever counts it.\n *\n * **Keyed per correlator** when a `correlator` is supplied, and\n * shared otherwise. Correlators that look for different things read the\n * log for different reasons, so one inheriting another's position at cold\n * start could skip events it needed. A key with no row yet falls back to\n * the shared value, so an upgrade does not re-read history.\n */\n correlated_at: number;\n};\n\n/**\n * Result of a {@link Store.truncate} operation, keyed by stream name.\n * Each entry contains the number of deleted events and the committed\n * seed event (snapshot or tombstone).\n *\n * Windowed entries (targets carrying a `before` boundary) echo the\n * boundary back as `before`, and `committed` is the **surviving\n * boundary `__snapshot__`** — an event the app wrote earlier, not a\n * new seed. Windowed no-ops (no qualifying snapshot) are absent from\n * the map entirely.\n */\nexport type TruncateResult = Map<\n string,\n {\n deleted: number;\n committed: Committed<Schemas, keyof Schemas>;\n before?: Date;\n }\n>;\n\n/**\n * Payload delivered by {@link Store.notify} when a **different process**\n * commits one or more events to the same backing store.\n *\n * Notifications are emitted **per commit transaction**, not per event —\n * a single commit of N events produces one notification carrying all N\n * `events`. This matches transactional semantics, minimizes wire wakeups,\n * and lets handlers reason about atomic batches.\n *\n * Stores that implement `notify` self-filter their own commits — handlers\n * receive notifications only for cross-process activity. This is the signal\n * that lets a horizontally-scaled Act deployment wake `settle()` immediately\n * on remote commits, instead of waiting for the next poll/debounce cycle.\n *\n * @property stream - Stream that was committed to\n * @property events - Events in this commit (id + name), in commit order\n */\nexport type StoreNotification = {\n readonly stream: string;\n readonly events: ReadonlyArray<{\n readonly id: number;\n readonly name: string;\n }>;\n};\n\n/**\n * Disposer returned by {@link Store.notify} subscriptions. Releases the\n * underlying listener (e.g., the dedicated PG `LISTEN` client). May be\n * synchronous or asynchronous — callers should `await` either way.\n */\nexport type NotifyDisposer = () => void | Promise<void>;\n\n/**\n * Subscription position for a registered stream.\n *\n * Streamed by {@link Store.query_streams} to power operational dashboards\n * (projection lag, blocked subscriptions, in-flight leases). The shape\n * mirrors what every adapter already tracks on its `streams` table.\n *\n * @property stream - The subscription target (projection or reaction stream)\n * @property source - Optional source (literal name or regex pattern) the reaction consumes from\n * @property at - Last processed event id watermark (-1 for fresh streams)\n * @property retry - Current retry counter\n * @property blocked - True when the stream is blocked by a poison message\n * @property error - Last error message (empty string when none)\n * @property leased_by - Current lease holder UUID (when leased)\n * @property leased_until - Lease expiration timestamp (when leased)\n * @property priority - Scheduling priority (default 0). Biases the\n * lagging-frontier `claim()` ordering — see {@link Store.prioritize}.\n * @property lane - Drain lane bound to the stream\n * @property deferred_at - Persisted next-visit time (ms since epoch) when\n * the stream is held out of {@link claim} by a `defer` outcome; omitted\n * when the stream carries no active defer schedule. Read at cold start to\n * re-seed the in-process defer timer so an idle deferred stream re-arms\n * its drain across a restart — the schedule outlives the process\n * memory it was derived from.\n */\nexport type StreamPosition = {\n readonly stream: string;\n readonly source?: string;\n readonly at: number;\n readonly retry: number;\n readonly blocked: boolean;\n readonly error: string;\n readonly priority: number;\n readonly leased_by?: string;\n readonly leased_until?: Date;\n readonly lane?: string;\n readonly deferred_at?: number;\n /**\n * The stream's **work mark** — see {@link SubscribeInput.correlated_at}.\n * `at < correlated_at` is the same predicate {@link Store.claim} applies,\n * so a reader can tell \"behind the head\" (`at` lags an event id) from\n * \"has unconsumed work\" without probing the event log. `undefined` means\n * the row carries no mark yet: unknown, not \"no work\".\n */\n readonly correlated_at?: number;\n};\n\n/**\n * Filter options for {@link Store.query_streams}.\n *\n * Mirrors the {@link Query} pattern used by {@link Store.query} — pass\n * filters server-side to keep the cost low on large tables (e.g., dynamic\n * reactions producing one subscription per aggregate).\n *\n * **What the store can filter:** the columns it actually persists —\n * `stream`, `source`, `blocked`. Higher-level classification (\"is this a\n * projection vs a reaction?\", \"is this a static or dynamic resolver?\")\n * is an orchestrator concern; the streams table doesn't store kinds.\n * Layer that on top by joining results with `Act`'s built-in registry.\n *\n * @property stream - Stream-name filter. Interpreted as a regex by\n * default — same anchor semantics across all stores (PG `~`, SQLite\n * anchor-aware `LIKE`, InMemory JS `RegExp`). **Anchors are caller-\n * controlled**: `^foo` for prefix, `foo$` for suffix, `^foo$` for\n * whole-string match. A plain string `foo` is a substring match\n * (matches any stream containing `foo`). Pass `stream_exact: true`\n * for a fast literal-equality comparison that skips regex compilation.\n * **Portable grammar:** the subset guaranteed to match identically on\n * every adapter is `^` / `$` anchors, `.` (any single character),\n * `.*` (any run of characters), and literal characters. Adapters\n * whose native engine compiles richer regex (PG POSIX, InMemory\n * `RegExp`) MAY accept more; an adapter that cannot express a pattern\n * exactly MUST throw `ValidationError` rather than silently\n * approximate — a silently-wrong match is the worst failure mode when\n * the filter drives `reset` / `unblock`. Pinned by the TCK's\n * stream-filter-grammar suite.\n * @property stream_exact - Treat `stream` as a literal string instead\n * of a regex.\n * @property source - Source-stream filter. Same anchor semantics and\n * portable grammar as `stream`. Useful to isolate dynamic-reaction\n * subscriptions tied to a particular aggregate stream. Pass\n * `source_exact: true` for literal equality.\n * @property source_exact - Use exact match instead of pattern match for\n * `source`.\n * @property source_matches - **Best-effort** reverse-match narrowing:\n * prefer streams whose stored `source` **pattern matches at least one**\n * of the given names (i.e. `name ~ source`, the inverse of `source`\n * above which is `source ~ pattern`). A subscription with an absent or\n * empty `source` (no source constraint — it consumes from every\n * stream) ALWAYS qualifies, regardless of the names. Used by the\n * close-cycle safety probe to fetch only the subscriptions that could\n * consume from a set of close-target streams instead of scanning the\n * whole subscriptions table. **Callers must treat it as a hint, not an\n * exact filter:** a\n * store that can't express reverse-regex (e.g. an anchor-aware `LIKE`\n * approximation) MAY return a superset — typically by ignoring the\n * filter entirely — so callers that need exactness re-verify in\n * process (the probe already does). Stores that honor it server-side\n * advertise `source_matches` in the TCK's `StoreCapabilities`, which\n * gates the narrowing-behavior tests; correctness never depends on the\n * flag, only the row count fetched.\n * @property blocked - Restrict to blocked (`true`) or unblocked (`false`)\n * streams. Omit for all.\n * @property lane - Restrict to streams in this drain lane. Exact match.\n * @property after - Keyset pagination cursor: returns only streams with\n * `stream > after` (lexicographic). Pass the last seen `stream` to fetch\n * the next page.\n * @property limit - Max rows to return (default: 100). A bound, applied\n * before a row is emitted: `limit: 0` returns none. A non-negative integer: other values aren't validated, and adapters treat them differently.\n */\nexport type QueryStreams = {\n readonly stream?: string;\n readonly stream_exact?: boolean;\n readonly source?: string;\n readonly source_exact?: boolean;\n readonly source_matches?: ReadonlyArray<string>;\n readonly blocked?: boolean;\n readonly lane?: string;\n readonly after?: string;\n readonly limit?: number;\n};\n\n/**\n * Result of a {@link Store.query_streams} call.\n *\n * @property maxEventId - Highest event id in the store (-1 when empty).\n * An upper bound on how far any subscription could be behind — **not** a\n * lag measurement. A subscription's watermark advances only over events\n * that resolve to it, so a reader handling a subset of a state's events\n * sits permanently below this with nothing pending. For \"does this\n * subscription have work waiting?\", compare its `at` against its own\n * `correlated_at` (see {@link StreamPosition.correlated_at}); fall back to\n * this only for a row that carries no mark yet.\n * @property count - Number of stream positions delivered to the callback.\n */\nexport type QueryStreamsResult = {\n readonly maxEventId: number;\n readonly count: number;\n};\n\n/**\n * Filter shape for bulk operations on registered streams. Used by\n * {@link Store.prioritize}, {@link Store.reset}, and {@link Store.unblock}\n * — anything that operates on a set of streams selected by pattern or\n * exact match rather than enumerated by name.\n *\n * Same shape as {@link QueryStreams} but without pagination — bulk\n * UPDATEs don't paginate. Empty filter (`{}`) matches **every**\n * registered stream; treat as a footgun and use sparingly.\n *\n * @property stream - Stream-name filter (regex by default; `stream_exact`\n * for equality). Same portable grammar as {@link QueryStreams.stream}:\n * `^` / `$` anchors, `.`, `.*`, and literal characters are guaranteed\n * across adapters; anything richer either matches with full regex\n * semantics or throws `ValidationError` — never a silent\n * approximation, since these filters drive bulk `reset` / `unblock`.\n * @property stream_exact - Exact-match instead of regex.\n * @property source - Source-stream filter (regex by default;\n * `source_exact` for equality). Same portable grammar as `stream`.\n * @property source_exact - Exact-match instead of regex.\n * @property blocked - Restrict to blocked / unblocked streams. Omit\n * for both.\n */\nexport type StreamFilter = Pick<\n QueryStreams,\n \"stream\" | \"stream_exact\" | \"source\" | \"source_exact\" | \"blocked\" | \"lane\"\n>;\n\n/**\n * Alias of {@link StreamFilter}. Retained for backward compatibility\n * with code that imports the filter by its prioritize-specific name.\n * Prefer `StreamFilter` for new code.\n */\nexport type PrioritizeFilter = StreamFilter;\n\n/**\n * Framework-internal event names — written by the runtime, not by user\n * code. Snapshots are seeded by `truncate()`; tombstones by `close()`.\n */\nexport type FrameworkEventName = typeof SNAP_EVENT | typeof TOMBSTONE_EVENT;\n\n/**\n * Union of all event names valid for a given schema set: user-declared\n * event names plus the framework-internal markers. Used by\n * {@link QueryStatsOptions.exclude} so callers can mix domain events and\n * framework markers in the same filter list without `as string` casts\n * or stringly-typed mistakes (e.g. `\"__tombsotne__\"` typos fail at\n * compile time).\n *\n * @template E - Event schemas; defaults to {@link Schemas}.\n */\nexport type EventName<E extends Schemas = Schemas> =\n | (keyof E & string)\n | FrameworkEventName;\n\n/**\n * Per-stream aggregated stats returned by {@link Store.query_stats}.\n *\n * `head` and `tail` follow the **git-log convention**, not the Unix\n * `head`/`tail` convention:\n * - `head` — the **latest** event (highest id), always present.\n * - `tail` — the **earliest** event (lowest id), opt-in via\n * {@link QueryStatsOptions.tail}.\n *\n * `head`/`tail` never carry `pii`. `query_stats` is an operator-introspection\n * surface with no actor context and no disclosure gate, so — like any\n * un-gated read — it omits the pii sidecar on every adapter. Route\n * pii through `load` (gated) instead.\n *\n * @template E - Event schemas; defaults to {@link Schemas} when the caller\n * does not narrow.\n * @property head - Latest non-excluded event for the stream.\n * @property tail - Earliest non-excluded event for the stream, when\n * `options.tail` is true.\n * @property count - Total non-excluded event count for the stream, when\n * `options.count` is true.\n * @property names - Sparse map of event name → count of events with\n * that name, when `options.names` is true. Keys are typed as\n * {@link EventName | EventName<E>} so typos on lookup\n * (e.g. `stats.names?.[\"TicktOpened\"]`) fail at compile time when the\n * caller narrows `E`. Empty object never returned — a stream with no\n * matching events is absent from the result map entirely.\n */\nexport type StreamStats<E extends Schemas = Schemas> = {\n readonly head: Committed<E, keyof E>;\n readonly tail?: Committed<E, keyof E>;\n readonly count?: number;\n readonly names?: Readonly<Partial<Record<EventName<E>, number>>>;\n};\n\n/**\n * Options for {@link Store.query_stats}. All stat fields default to\n * `false` except `head`, which is always returned.\n *\n * **Cost model:** With no opt-in flags (or `tail` alone), each requested\n * stat resolves via an index-backed lookup — O(K) cost where K is the\n * number of matched streams. Setting `count` and/or `names` triggers a\n * full event scan over the matched streams (O(N) where N is total events);\n * both share the same scan and so requesting one is the same cost as\n * requesting both.\n *\n * @template E - Event schemas; defaults to {@link Schemas}. When the caller\n * narrows `E`, `exclude` is type-checked against the schema's event names\n * — typos like `[\"TOMBSTON_EVENT\"]` fail at compile time.\n * @property tail - Include the earliest non-excluded event per stream.\n * Cheap when alone (indexed); free when `count`/`names` also set\n * (already scanning).\n * @property count - Include the total non-excluded event count per stream.\n * Triggers full scan.\n * @property names - Include a `name → count` map per stream. Triggers\n * full scan (shares cost with `count`).\n * @property exclude - Event names to skip — e.g.\n * `[TOMBSTONE_EVENT, SNAP_EVENT]` to ignore framework markers. Applies\n * to all returned stats (head, tail, count, names) consistently.\n * @property before - Time-travel cutoff: only consider events with\n * `id < before`. Omitted = current state. Useful for \"what did this\n * stream look like at event N?\" historical queries without changing\n * the call shape. Cheap on both code paths (cheap-heads path narrows\n * the index scan; full-scan path adds a `WHERE id < ?` predicate).\n * @property after - Keyset pagination cursor: return only streams whose\n * name sorts strictly after this value (lexicographic). Pass the last\n * stream name from the previous page to fetch the next. Pairs with\n * `limit`; the result `Map` preserves stream-name order so the next\n * cursor is `[...result.keys()].at(-1)`.\n * @property limit - Maximum number of streams to return; omitted, every\n * matching stream (see the writing-a-store guide § Paginating\n * `query_stats`). A bound, applied before a stream is added to the\n * result: `limit: 0` returns none. A non-negative integer: other values aren't validated, and adapters treat them differently.\n */\nexport type QueryStatsOptions<E extends Schemas = Schemas> = {\n readonly tail?: boolean;\n readonly count?: boolean;\n readonly names?: boolean;\n readonly exclude?: ReadonlyArray<EventName<E>>;\n readonly before?: number;\n readonly after?: string;\n readonly limit?: number;\n};\n\n/**\n * One target stream to register with {@link Store.subscribe}.\n */\nexport type SubscribeInput = {\n readonly stream: string;\n readonly source?: string;\n /**\n * Optional scheduling priority for the lagging-frontier `claim()`\n * ordering. Default `0`. When the same stream is subscribed by multiple\n * reactions with different priorities, implementations must keep the\n * **maximum** so the highest-priority reaction wins. Use\n * {@link Store.prioritize} for runtime overrides that ignore this max —\n * operator-driven changes.\n */\n readonly priority?: number;\n /**\n * Drain lane. Default `\"default\"`.\n *\n * Written on the same rule as {@link priority}: the lane rides the max,\n * so a subscribe whose priority is **at or above** the stored priority\n * sets the lane, and one below it leaves the lane alone. The highest\n * priority registered for a stream therefore owns its lane, durably —\n * the same outcome the correlate scan applies within one pass, made a\n * property of the row so a caller that has forgotten what a stream\n * carries cannot re-lane it by resolving to it at a lower priority.\n *\n * Re-laning stays restart-driven: a restart re-subscribes at the same\n * declared priority, and equal priority writes the lane. A stream whose\n * stored priority was raised out of band by {@link Store.prioritize}\n * keeps its lane until a subscribe reaches that priority.\n */\n readonly lane?: string;\n /**\n * Highest event id observed to resolve to this target — the stream's\n * **work mark**. Applied as `correlated_at = GREATEST(correlated_at, N)`\n * for every value including zero and negatives; omitted leaves the stored\n * value untouched.\n *\n * A mark is an assertion about the log: `correlate` sets it to the id of a\n * real event that resolved to this target, so `at < correlated_at` implies a\n * fetch returns work. `correlate` is the only component entitled to write\n * it — the store cannot resolve user-code targets, and `notify` is\n * best-effort — which is what the name records.\n *\n * Eligibility follows from it: a stream is claimable iff `at < correlated_at`.\n * `NULL` means the row has never been correlated, so it is not claimable\n * until `correlate` marks it.\n */\n readonly correlated_at?: number;\n};\n\n/**\n * Interface for event store implementations.\n *\n * The Store interface defines the contract for persistence adapters in Act.\n * Implementations must provide event storage, querying, and distributed processing\n * capabilities through leasing and watermark tracking.\n *\n * Act includes two built-in implementations:\n * - **InMemoryStore**: For development and testing\n * - **PostgresStore**: For production use with PostgreSQL\n *\n * Custom stores can be implemented for other databases or event log systems.\n *\n * @example Using a custom store\n * ```typescript\n * import { store } from \"@rotorsoft/act\";\n * import { PostgresStore } from \"@rotorsoft/act-pg\";\n *\n * // Replace the default in-memory store\n * store(new PostgresStore({\n * host: \"localhost\",\n * port: 5432,\n * database: \"myapp\",\n * user: \"postgres\",\n * password: \"secret\"\n * }));\n *\n * const app = act()\n * .withState(Counter)\n * .build();\n * ```\n *\n * @see {@link InMemoryStore} for the default implementation\n * @see {@link PostgresStore} for the PostgreSQL implementation\n */\nexport interface Store extends Disposable, EventSource {\n /**\n * Initializes or resets the store.\n *\n * Used primarily for testing to ensure a clean state between tests.\n * For production stores, this might create necessary tables or indexes.\n *\n * @example\n * ```typescript\n * // Reset store between tests\n * beforeEach(async () => {\n * await store().seed();\n * });\n * ```\n */\n seed: () => Promise<void>;\n /**\n * Drops all data from the store.\n *\n * Dangerous operation that deletes all events and state. Use with extreme caution,\n * primarily for testing or development environments.\n *\n * @example\n * ```typescript\n * // Clean up after tests\n * afterAll(async () => {\n * await store().drop();\n * });\n * ```\n */\n drop: () => Promise<void>;\n\n /**\n * Commits one or more events to a stream atomically.\n *\n * This is the core method for persisting events. It must:\n * - Assign global sequence IDs to events\n * - Increment the stream version\n * - Check optimistic concurrency if expectedVersion is provided\n * - Store events atomically (all or nothing)\n * - Attach metadata (id, stream, version, created timestamp)\n *\n * @template E - Event schemas\n * @param stream - The stream ID to commit to\n * @param msgs - Array of messages (events) to commit\n * @param meta - Event metadata (correlation, causation)\n * @param expectedVersion - Expected current version for optimistic concurrency;\n * `-1` expects an empty stream, `undefined` skips the check\n * @returns Array of committed events with full metadata\n *\n * @throws {ConcurrencyError} If expectedVersion doesn't match current version\n *\n * @example\n * ```typescript\n * const events = await store().commit(\n * \"user-123\",\n * [{ name: \"UserCreated\", data: { email: \"user@example.com\" } }],\n * { correlation: \"req-456\", causation: { action: {...} } },\n * -1 // Expect an empty (new) stream\n * );\n * ```\n */\n commit: <E extends Schemas>(\n stream: string,\n msgs: Message<E, keyof E>[],\n meta: EventMeta,\n expectedVersion?: number\n ) => Promise<Committed<E, keyof E>[]>;\n\n /**\n * Queries events from the store with optional filtering.\n *\n * Calls the callback for each matching event. The callback approach allows\n * processing large result sets without loading everything into memory.\n *\n * **Payload dates round-trip as `Date`.** A `Date` committed inside event\n * `data` (or `meta`) is returned as a `Date`, not an ISO string — every\n * adapter revives ISO-8601 strings on read. A consequence: a plain string\n * that happens to match ISO-8601 exactly is revived to a `Date` too, and a\n * timezone-less ISO string is parsed in the reader's local time. Keep\n * ISO-shaped strings you want to stay strings out of event payloads, or\n * carry them in a wrapper field.\n *\n * @template E - Event schemas\n * @param callback - Function invoked for each matching event\n * @param query - Optional filter criteria; see {@link Query} for the fields.\n * @returns Total number of events processed\n *\n * @example Query all events for a stream\n * ```typescript\n * let count = 0;\n * await store().query(\n * (event) => {\n * console.log(event.name, event.data);\n * count++;\n * },\n * { stream: \"user-123\" }\n * );\n * console.log(`Found ${count} events`);\n * ```\n */\n query: <E extends Schemas>(\n callback: (event: Committed<E, keyof E>) => void,\n query?: Query\n ) => Promise<number>;\n\n /**\n * Atomically discovers and leases streams for reaction processing.\n *\n * Atomically discovers a stream and acquires a lease in one round-trip,\n * eliminating the race that exists when discovery and locking are separate\n * calls (a competing worker can grab the stream between the two).\n *\n * PostgresStore uses `FOR UPDATE SKIP LOCKED` for zero-contention competing\n * consumer semantics — workers never block each other, each grabbing different\n * streams atomically. InMemoryStore fuses its poll+lease logic equivalently.\n *\n * Claim reads only the subscription rows; it never matches a `source`\n * against the event log. Correlate does that when it decides which\n * targets to mark, and the drain uses the same rule when it fetches the\n * claimed stream's events. A **literal** source (no regex\n * metacharacter — the common case, and every autoclose/dynamic-resolver\n * source) matches by string equality: the fast, index-friendly path, and\n * exact so `\"s1\"` never matches `\"s12\"`. A **pattern** source (carrying\n * `^ $ . * + ? ( ) [ ] { } | \\`, e.g. a static `^(A|B)$` reaction) is\n * compiled as a regex and matched against candidate stream names.\n * Adapters that cannot faithfully run an arbitrary regex (SQLite) reject\n * a non-portable pattern at {@link subscribe} time rather than silently\n * never fetching its events.\n *\n * Every granted lease **counts against the stream's retry budget**: claim\n * increments the stream's retry counter and only {@link ack} resets it, so\n * a timed-out lease reclaimed by any worker marches the stream toward\n * `blockOnError` exactly like a handler failure.\n *\n * Used by `Act.drain()` as the primary stream acquisition method.\n *\n * @param lagging - Max streams from the lagging frontier (ascending watermark)\n * @param leading - Max streams from the leading frontier (descending watermark)\n * @param by - Unique lease holder identifier (UUID)\n * @param millis - How long the lease stands, in milliseconds. It bounds\n * the expiry only — the holder (`by`) is recorded whatever the value,\n * so a zero-length lease is granted, expires immediately, and its\n * {@link ack} still lands. A store that skipped recording the holder\n * would hand out a lease whose every ack is dropped, parking the\n * watermark and redelivering every event on every drain.\n * @param lane - Optional lane filter\n * @returns Array of successfully leased streams with metadata\n *\n * @example\n * ```typescript\n * const leased = await store().claim(5, 5, randomUUID(), 10000);\n * leased.forEach(({ stream, at, lagging }) => {\n * console.log(`Leased ${stream} at ${at} (lagging: ${lagging})`);\n * });\n * ```\n *\n * @see {@link subscribe} for registering new streams (used by correlate)\n * @see {@link ack} for acknowledging completion\n * @see {@link block} for blocking failed streams\n */\n claim: (\n lagging: number,\n leading: number,\n by: string,\n millis: number,\n lane?: string\n ) => Promise<Lease[]>;\n\n /**\n * Registers streams for event processing.\n *\n * Upserts stream entries so they become visible to {@link claim}. Used by\n * `correlate()` to register dynamically discovered reaction target streams.\n *\n * Also returns the current maximum watermark across all subscribed streams,\n * used internally for correlation checkpoint initialization on cold start.\n *\n * @param streams - Streams to register with optional source hint — a\n * literal stream name (matched by equality when the drain fetches the\n * stream's events) or a regex pattern (compiled and matched against\n * stream names); non-portable patterns are rejected here on SQLite.\n *\n * **At most one entry per stream.** A subscription row is keyed by\n * stream and its `source` is set once at creation, so a second entry\n * for the same stream could only ever contribute its priority — and\n * an adapter that merges the batch in one statement cannot do even\n * that (`UPDATE ... FROM` may touch a target row only once, so\n * Postgres picks an arbitrary source row). Callers de-duplicate\n * before calling; adapters may assume uniqueness.\n * @returns `subscribed` count of newly registered streams, `watermark` max `at` across all streams\n *\n * @example\n * ```typescript\n * const { subscribed, watermark } = await store().subscribe([\n * { stream: \"stats-user-1\", source: \"user-1\" },\n * { stream: \"stats-user-2\", source: \"user-2\", priority: 10 },\n * ]);\n * ```\n *\n * @see {@link claim} for discovering and leasing registered streams\n * @see {@link prioritize} for changing priority after subscription\n */\n subscribe: (\n streams: SubscribeInput[],\n /**\n * Advance the correlate checkpoint to this event id — correlate's own\n * watermark, in the same id space as a subscription's `at`.\n *\n * `correlate` is the only component that knows how far it has read, and\n * it already calls `subscribe` with the targets a scan discovered, so the\n * checkpoint is written by its own producer in its own call. Nothing else\n * may set it: a checkpoint derived from processing progress runs *ahead*\n * of the read cursor and silently skips discovery.\n *\n * **Persists only when greater than the stored value.** A lower or equal\n * value is ignored rather than written, so a worker whose cursor lags\n * cannot rewind it and re-sending the same value is a no-op. Omitted\n * leaves it untouched.\n */\n correlated_at?: number,\n /**\n * Identifies the calling correlator, and asks for the correlation lease.\n *\n * Correlation is duplicated work without it: every worker keeps its own\n * in-memory scan position, so N workers wake on the same commit and each\n * reads the whole range and writes the same marks — measured at exactly\n * N reads and N mark-writes per committed event. The marks are\n * idempotent, so this is waste rather than error, but the writes land on\n * the rows {@link claim} locks and churn the partial index the design\n * depends on.\n *\n * It rides `subscribe` rather than a verb of its own because correlate\n * already calls this every pass: the lease is a property of the\n * checkpoint write that already happens.\n *\n * - `key` scopes the lease to interchangeable correlators. Workers\n * running the same application are; a worker deployed with a subset of\n * the reactions is not, and must scan for itself or its targets are\n * never marked. Callers derive it from the registry; stores treat it\n * as opaque, and it also selects which checkpoint row this caller\n * reads and advances.\n * - `by` identifies this worker. Passing the same value renews rather\n * than fails, so acquiring and extending are one call.\n * - `millis` is how long the lease is held from now.\n *\n * Omitted entirely, no lease is taken and the caller reads and advances\n * the shared checkpoint.\n */\n correlator?: { key: string; by: string; millis: number }\n ) => Promise<SubscribeResult>;\n\n /**\n * Finalizes leased streams **atomically**: acknowledges the ones\n * that processed successfully and persists defer schedules for the ones\n * that chose to be re-visited later — one call, one transaction.\n *\n * Every entry advances the watermark to `at` (the last event handled this\n * cycle) and releases the lease. An entry without {@link Lease.due} is a\n * plain **ack**: it also resets `retry` to `-1` and clears any schedule. An\n * entry *with* `due` additionally **defers** the remainder: the stream's\n * `deferred_at` is set to `due` (ms since epoch) and `retry` to the entry's\n * own {@link Lease.retry}. Advance and defer are independent legs, so a\n * partial-progress backoff/defer advances past the events it handled — that\n * prefix never re-runs — while the failing tail waits for `deferred_at` (a\n * hold passes `at` = the current watermark, making the advance a no-op). A\n * caller deferring a *deliberate* re-visit passes `retry: -1` (the same\n * semantics as {@link defer} — a defer is not a failure); a caller pacing a\n * *retry* backoff passes the climbing counter so the retry budget keeps\n * accruing across windows until it blocks. All-or-nothing is the contract: a\n * failure must leave every watermark and every schedule untouched, so a drain\n * cycle's outcomes can never land partially (an acked close request must not\n * survive a lost defer, and vice versa).\n *\n * @param leases - Leases to finalize; `due`-carrying entries defer, the\n * rest ack. **At most one entry per stream.** Each lease is for one\n * stream, and an adapter that applies the batch in one statement cannot\n * apply two entries to the same row (`UPDATE … FROM` may touch a target\n * row once, so Postgres keeps an arbitrary one). Callers de-duplicate\n * before calling, as the drain does; adapters may assume uniqueness.\n * @returns The acknowledged leases (deferred entries are not returned)\n *\n * @example\n * ```typescript\n * const leased = await store().claim(5, 5, randomUUID(), 10000);\n * // Ack most streams at ID 150; hold order-42 until half past\n * await store().ack(leased.map(l =>\n * l.stream === \"order-42\"\n * ? { ...l, due: Date.now() + 30 * 60_000 }\n * : { ...l, at: 150 }\n * ));\n * ```\n *\n * @see {@link claim} for acquiring leases\n * @see {@link defer} for the standalone (operator-facing) schedule write\n */\n ack: (leases: Lease[]) => Promise<Lease[]>;\n\n /**\n * Blocks streams after persistent processing failures.\n *\n * Blocked streams won't be returned by {@link claim} until manually unblocked.\n * This prevents poison messages from repeatedly failing and consuming resources.\n *\n * Streams are typically blocked when:\n * - Max retries reached\n * - `blockOnError` option is true\n * - Handler throws an error\n *\n * @param leases - Leases to block with error messages. **At most one entry\n * per stream**, for the same reason as {@link ack}; adapters may assume\n * uniqueness.\n * @returns Blocked leases\n *\n * @example\n * ```typescript\n * try {\n * await processEvents(lease);\n * await store().ack([lease]);\n * } catch (error) {\n * if (lease.retry >= 3) {\n * await store().block([{\n * ...lease,\n * error: error.message\n * }]);\n * }\n * }\n * ```\n *\n * @see {@link claim} for lease management\n */\n block: (leases: BlockedLease[]) => Promise<BlockedLease[]>;\n\n /**\n * Operator verb: bulk-pause streams until a future time without advancing\n * their watermark. The drain itself never calls this — a reaction's defer\n * outcome is persisted atomically by {@link ack} via due-marked leases;\n * this standalone verb exists for operator-driven scheduling (\"hold every\n * `webhook-.*` stream until the maintenance window ends\"), completing the\n * recovery family: **reset / unblock / prioritize / defer**.\n *\n * Sets `deferred_at` on each matched stream. {@link claim} **skips** any\n * stream whose `deferred_at` is still in the future, so a paused stream\n * is not re-claimed (and `retry` is never bumped) until the due-time\n * passes, at which point the same pending events are re-delivered. Unlike\n * in-process backoff, this is durable, shared store state — every\n * competing worker honors the skip.\n *\n * {@link claim} treats a deferred stream as due once the **claiming\n * caller's** clock reaches `deferred_at`, not a clock of the store's own\n * (such as a database `NOW()`): the orchestrator's defer timer wakes on\n * the caller's clock, and the claim it triggers must agree with it.\n *\n * The schedule is cleared whenever the watermark moves or the stream is\n * recovered: {@link ack}, {@link block}, {@link reset}, and {@link unblock}\n * all reset `deferred_at`. Re-deferring simply overwrites it.\n *\n * Accepts an explicit list of stream names or a {@link StreamFilter}\n * (regex by default), the same shape as {@link reset}/{@link unblock}.\n *\n * @param input - Stream names or a {@link StreamFilter} selecting streams\n * @param deferred_at - Wall-clock time (ms since epoch) to revisit the streams\n * @returns Count of streams whose `deferred_at` was set\n *\n * @example\n * ```typescript\n * // Pause every webhook delivery stream during downstream maintenance\n * await store().defer({ stream: \"^webhook-\" }, Date.now() + 30 * 60_000);\n * ```\n */\n defer: (\n input: string[] | StreamFilter,\n deferred_at: number\n ) => Promise<number>;\n\n /**\n * Resets watermarks for the given streams to -1, making them eligible\n * for replay from the beginning. Also clears retry, blocked, error,\n * and lease state so the streams can be claimed immediately.\n *\n * **Prefer `Act.reset()` over calling this directly.** This primitive\n * only resets the store; it does not raise the orchestrator's internal\n * drain-armed flag, so a settled `Act` instance will short-circuit and\n * skip the replay. `Act.reset()` wraps this and arms the flag.\n *\n * Accepts either an explicit list of stream names or a\n * {@link StreamFilter} for bulk operations (e.g., \"rebuild every\n * blocked stream\"). The filter form is the same shape used by\n * {@link prioritize} and {@link query_streams}. An empty filter\n * (`{}`) matches every registered stream — typically a footgun for\n * `reset`; prefer narrower filters like `{ blocked: true }`.\n *\n * @param input - Stream names or a {@link StreamFilter}\n * @returns Count of streams that were actually reset\n *\n * @example\n * ```typescript\n * // By name\n * await app.reset([\"my-projection\"]);\n *\n * // By filter — rebuild every blocked stream in a projection family\n * await app.reset({ stream: \"^proj-orders-\", blocked: true });\n *\n * // Low-level (does NOT trigger replay on settled apps)\n * await store().reset([\"my-projection\"]);\n * ```\n *\n * @see {@link Act.reset} for the high-level rebuild API that wraps\n * this primitive and schedules the replay\n */\n reset: (input: string[] | StreamFilter) => Promise<number>;\n\n /**\n * Clears the blocked flag on streams without replaying their history.\n * Sets `blocked = false`, `retry = -1` (no attempt yet), `error = null`, and\n * clears any lease bookkeeping. The `at` watermark stays where it\n * was — the stream resumes from the next event after the last\n * successful ack, not from zero.\n *\n * The distinction from {@link reset} matters: `reset()` is for\n * projection rebuilds (replay from event 0); `unblock()` is for\n * recovering from a poison message after the operator fixes the\n * underlying issue. Use `unblock()` when you don't want to re-process\n * history.\n *\n * **Prefer `Act.unblock()` over calling this directly.** Like\n * `reset()`, this primitive doesn't raise the orchestrator's internal\n * drain-armed flag — a settled `Act` instance will short-circuit and\n * skip the resume. `Act.unblock()` wraps this and arms the flag.\n *\n * Only streams that were actually blocked at call time count toward\n * the return value; already-unblocked streams and unknown stream\n * names are silently skipped. The atomic single-statement update\n * makes the call safe to issue concurrently with `claim()` — workers\n * holding a `FOR UPDATE SKIP LOCKED` lock won't see partial state.\n *\n * Accepts either an explicit list of stream names or a\n * {@link StreamFilter} for bulk recovery (e.g., \"unblock every\n * blocked order projection\"). The `blocked = true` predicate is\n * always applied — passing `blocked: false` in the filter matches\n * nothing. An empty filter (`{}`) means \"unblock everything that's\n * blocked,\" which is a sane post-incident bulk recovery.\n *\n * @param input - Stream names or a {@link StreamFilter}\n * @returns Count of streams that were actually flipped (were blocked)\n *\n * @example\n * ```typescript\n * // By name (single targeted recovery)\n * await app.unblock([\"webhooks-out-customer-42\"]);\n *\n * // By filter — unblock every blocked stream in a family\n * await app.unblock({ stream: \"^webhooks-out-\" });\n *\n * // Post-incident: unblock everything that's blocked\n * await app.unblock({});\n *\n * // Low-level (does NOT trigger resume on settled apps)\n * await store().unblock([\"webhooks-out-customer-42\"]);\n * ```\n *\n * @see {@link Act.unblock} for the high-level recovery API\n * @see {@link reset} for the rebuild-from-zero alternative\n */\n unblock: (input: string[] | StreamFilter) => Promise<number>;\n\n /**\n * Bulk-update the scheduling priority of streams matching a filter.\n *\n * Used by {@link Act.prioritize} for operator runtime control over\n * lagging-frontier `claim()` ordering. Unlike {@link subscribe},\n * which keeps the per-stream priority at the `max()` of all\n * registered reactions targeting that stream, `prioritize` sets the\n * priority **directly** to `priority` for matching rows — letting\n * operators override the build-time scheduling policy.\n *\n * Filter semantics mirror {@link query_streams}: `stream`/`source`\n * are regex by default, exact with the `*_exact` flags. `blocked`\n * restricts to blocked or unblocked rows. Omitted fields don't\n * filter. An **empty filter** (`{}`) updates every registered\n * stream — useful for \"reset all priorities to N\" but a footgun\n * otherwise.\n *\n * @param filter - {@link PrioritizeFilter} selecting which streams\n * to update. Required (use `{}` to target all).\n * @param priority - New priority value. Set as-is — no `max()`,\n * no clamp.\n * @returns Count of streams whose priority was changed.\n *\n * @example Boost a specific replay\n * ```typescript\n * await store().prioritize(\n * { stream: \"^projection-orders$\", stream_exact: false },\n * 10\n * );\n * ```\n *\n * @see {@link Act.prioritize} for the orchestrator-level wrapper\n * @see {@link claim} for how priority biases stream scheduling\n */\n prioritize: (filter: StreamFilter, priority: number) => Promise<number>;\n\n /**\n * Atomically truncates streams and seeds each with a final event.\n *\n * For each **full** target (no `before`), in a single transaction:\n * 1. Deletes all events for the stream\n * 2. Inserts a `__snapshot__` (when `snapshot` is provided) or\n * `__tombstone__` event as the sole event on the stream\n *\n * **Subscriptions are not touched**, so the event log and the\n * subscriptions can live on different systems. A retired stream's\n * subscription is inert (commits to a tombstoned stream are refused, so its\n * work mark never rises and {@link claim} never returns it) and keeps each\n * consumer's final watermark. Deleting those rows is optional housekeeping.\n\n * A **windowed** target (`before` set) is a pure prefix delete behind\n * a real snapshot the app wrote — no seed, no tombstone, and the\n * streams table is left untouched. The store finds the closest safe\n * boundary — the latest `__snapshot__` with `created < before` and,\n * when `max_id` is given, `id <= max_id` — and deletes events with\n * `id <` that snapshot's id, keeping the snapshot + tail. No\n * qualifying snapshot ⇒ no-op (the stream is absent from the result).\n * Because `load()` resets state at each snapshot on replay, events\n * below the boundary contribute nothing to any load result — deleting\n * them cannot change what `load()` returns. `snapshot`/`meta` must be\n * omitted on windowed targets; `before` takes precedence when both\n * appear.\n *\n * **Windowed targets are applied before full ones**, and each full target\n * pairs its own delete with its own seed. Both only matter for inputs the\n * framework never produces — `close-cycle` dedups by stream before calling\n * — but `Store` is a public port, so the order is contract rather than\n * accident: a stream listed as both windowed and full ends with its\n * FULL entry in the result, and a stream listed twice as full leaves\n * exactly one seed carrying the last target's snapshot, reporting only that\n * pass's own delete count. Deleting once for a deduped set and seeding once\n * per entry would write two `version: 0` rows, which the SQL adapters'\n * unique `(stream, version)` constraint rejects outright.\n *\n * @param targets - Streams to truncate; full targets carry optional\n * snapshot state and meta, windowed targets carry `before` (and\n * optionally `max_id`, the min consumer watermark cap)\n * @returns Map keyed by stream name, each entry with `deleted` count\n * and `committed` event (the new seed, or the surviving boundary\n * snapshot on windowed entries, which also echo `before`)\n *\n * @see {@link Act.close} for the high-level close-the-books API that\n * orchestrates safety checks, archive callbacks, and atomic\n * truncate+seed\n */\n truncate: (\n targets: Array<{\n stream: string;\n snapshot?: Schema;\n meta?: EventMeta;\n before?: Date;\n max_id?: number;\n }>\n ) => Promise<TruncateResult>;\n\n /**\n * Atomically wipe the store and commit a fresh sequence of events\n * inside a single transaction.\n *\n * **Capability-gated.** Adapters may or may not implement restore.\n * Third-party stores that can't atomically wipe-and-rebuild in one\n * transaction can omit it.\n *\n * **Driver pattern.** The adapter is a thin transactional wrapper:\n * open the transaction (PG `BEGIN`, SQLite `BEGIN IMMEDIATE`, an\n * in-process snapshot for {@link InMemoryStore}), truncate the\n * events + streams/subscriptions tables, hand the orchestrator a\n * per-event insert callback by invoking `driver(callback)`, then\n * commit or roll back. Any throw inside `driver` rolls back the\n * transaction — the store ends byte-for-byte unchanged from the\n * pre-call state.\n *\n * The framework's scan loop (in `internal/event-sourcing.ts`) is\n * what calls `callback` repeatedly: it iterates the source,\n * validates each event, applies `drop_snapshots`, fires\n * `on_progress`, rewrites `meta.causation.event.id` through the\n * per-call `old → new` map, and counts kept/dropped. Adapters\n * never see that logic — their job is the transaction lifecycle\n * plus the adapter-specific `callback` body.\n *\n * **Lossless `created`.** The `callback` receives the event's\n * original timestamp; adapters write it through verbatim. This is\n * the property that makes restore a viable backup/migration\n * primitive — distinct from {@link commit}, which always stamps\n * `now()`.\n *\n * **Renumbered `id`.** Adapters reseed ids densely (`1..N` on\n * PG/SQLite, `0..N-1` on InMemory). The source's original ids are\n * used by the orchestrator as causation lookup keys but never\n * written through.\n *\n * **No subscription preservation.** Both the events and the\n * streams/subscriptions tables are wiped. Reactions re-subscribe\n * via the orchestrator's normal `correlate()` path on the next\n * settle cycle.\n *\n * **Correlate checkpoints reset.** Every correlate checkpoint (the shared\n * one and each keyed correlator's) is an event id, so restore sets them\n * back to -1 in the same transaction; left at their old values they would\n * sit above every renumbered event and correlate would never resolve the\n * restored events' reactions. Correlator leases are kept. The\n * `Act` that ran the restore restarts its own correlation; any other\n * process running against the same store must be restarted, as for any\n * offline restore.\n *\n * **Cache.** Restore does not touch the {@link Cache} port —\n * callers must `cache().clear()` after restore to avoid serving\n * stale snapshots. Documented; not enforced.\n *\n * @param driver - Orchestrator-supplied iteration callback. The\n * adapter calls `driver(callback)` exactly once, from inside its\n * transaction. The `callback` argument is the adapter's per-event\n * insert hook — it receives the event with `meta.causation`\n * already rewritten to the new id space and returns the new id\n * the adapter assigned. The driver is purely transactional from\n * the adapter's perspective — kept/dropped counts and timing live\n * in `Act.restore`.\n *\n * @see {@link Act.restore} for the public entry point.\n * @see {@link truncate} for the single-stream snapshot/tombstone\n * primitive (different operation — restore wipes the whole store)\n */\n restore?: (\n driver: (\n callback: (event: Committed<Schemas, keyof Schemas>) => Promise<number>\n ) => Promise<void>\n ) => Promise<void>;\n\n /**\n * Streams registered subscription positions to a callback, plus the\n * highest event id in the store.\n *\n * Read-only introspection for operational dashboards (Store /\n * Subscriptions tab, projection lag, blocked subscriptions). Avoids\n * forcing apps to open a second connection and run raw SQL against\n * adapter-specific schemas.\n *\n * Mirrors the {@link Store.query} callback pattern: the callback is\n * invoked once per matching position, allowing large result sets to be\n * processed without buffering. Results are ordered by `stream` name; use\n * `query.after` (last seen stream name) for keyset pagination on big\n * tables (dynamic reactions can produce one subscription per aggregate).\n *\n * @param callback - Invoked once per matching {@link StreamPosition}.\n * @param query - Optional {@link QueryStreams} filter (default `limit: 100`).\n * @returns `maxEventId` and the `count` of positions emitted.\n *\n * @example List blocked streams with their lag\n * ```typescript\n * const { maxEventId } = await store().query_streams(\n * (s) => console.log(`${s.stream}: lag=${maxEventId - s.at} ${s.error}`),\n * { blocked: true, limit: 50 }\n * );\n * ```\n */\n query_streams: (\n callback: (position: StreamPosition) => void,\n query?: QueryStreams\n ) => Promise<QueryStreamsResult>;\n\n /**\n * Per-stream aggregated stats — single round trip per adapter.\n *\n * Returns the latest event (`head`) plus opt-in extras (`tail`, `count`,\n * `names`) for each stream selected by `input`. Streams with no\n * qualifying events are absent from the result map.\n *\n * **Cost model.** With no opt-in flags, the call uses an index-backed\n * head lookup per stream — O(K) where K is the number of matched\n * streams. `tail` alone stays in the cheap tier. Setting `count` and/or\n * `names` triggers a full event scan over the matched streams (O(N)\n * where N is total events); both stats share that scan, so requesting\n * one or both is the same cost.\n *\n * **`input`.** Either an explicit `string[]` of stream names, or a\n * narrow event-stream selector `{ stream?, stream_exact? }` for\n * pattern-based or exact-name matching. **Subscription-level filters\n * (`source`, `blocked`) are intentionally not accepted here** — they\n * describe subscriptions, not events, and conflating the two would\n * silently exclude unsubscribed event streams. For\n * \"stats for all blocked subscriptions\" compose explicitly:\n * `query_streams({blocked: true})` → collect names → `query_stats(names)`.\n *\n * **`head` vs `tail` naming.** Follows the git-log convention: `head`\n * is the latest event (highest id), `tail` is the earliest (lowest id).\n * This is the **opposite** of the Unix `head`/`tail` commands.\n *\n * **Framework markers.** Snapshots (`__snapshot__`) and tombstones\n * (`__tombstone__`) are real events and are included by default —\n * intentional, so schema-evolution tooling can count them. To exclude\n * them, pass them in `options.exclude` (typed against {@link EventName})\n * so typos are compile-time errors.\n *\n * **Snapshot counts come from `names`.** When `names: true` and snapshots\n * are not in `exclude`, `result.names[\"__snapshot__\"]` is the snapshot\n * count for that stream — no separate field needed. Validates snapshot\n * policy at scale: `names[\"__snapshot__\"] / count` should match the\n * configured snap predicate's expected ratio.\n *\n * **Time travel.** `options.before` narrows to events with `id < before`,\n * answering \"what did this stream look like at event N?\" without\n * special call shape.\n *\n * **Ordering + pagination.** The returned `Map` is ordered by stream\n * name. `options.after` (exclusive cursor) + `options.limit` keyset-\n * paginate over that order — pass the last key of one page as the\n * `after` of the next. `limit` defaults to unbounded, so callers that\n * omit it keep receiving every matching stream in one call.\n *\n * @example Cheap heads — close-cycle pattern (one round trip, no scan)\n * ```typescript\n * const stats = await store().query_stats(streams, {\n * exclude: [TOMBSTONE_EVENT],\n * });\n * for (const [stream, { head }] of stats) {\n * // head.id, head.version, head.name\n * }\n * ```\n *\n * @template E - Event schemas. Narrow at the call site to type-check\n * `exclude` against your event names (typos fail at compile time).\n *\n * @param input - Stream names or a filter selecting the streams to stat.\n * @param options - Opt-in stat fields, event-name exclusions, and\n * time-travel cutoff. See {@link QueryStatsOptions}.\n * @returns Map keyed by stream name. Streams with no qualifying events\n * (after `exclude` and `before` are applied) are absent.\n *\n * @see {@link QueryStatsOptions} for the cost-aware option surface\n * @see {@link StreamStats} for the per-stream result shape\n * @see {@link EventName} for the typed exclude entries\n */\n query_stats: <E extends Schemas>(\n input: string[] | Pick<StreamFilter, \"stream\" | \"stream_exact\">,\n options?: QueryStatsOptions<E>\n ) => Promise<Map<string, StreamStats<E>>>;\n\n /**\n * Optional cross-process commit notifications.\n *\n * When implemented, the {@link Act} orchestrator subscribes once at build\n * time and routes notifications to wake `settle()` automatically — so a\n * remote worker's commit triggers reactions on this process without\n * waiting for the debounce/poll cycle. Subscribers also receive each\n * notification on the `\"notified\"` lifecycle event for fan-out\n * (SSE pushes, audit logs, dashboards).\n *\n * **Self-filtering contract:** implementations must skip their own\n * commits. The handler fires only for commits originating from a\n * **different process** writing to the same backing store. This keeps\n * the local fast path (`do()` already arms drain) free of duplicate\n * wake-ups and gives `\"notified\"` a clean cross-process semantic.\n * Enforced by the `notify` capability cases in `@rotorsoft/act-tck`,\n * along with one-notification-per-commit batch delivery.\n *\n * **Hint, not a contract:** the orchestrator never depends on `notify`\n * for correctness. If absent, dropped, or the store omits it, the\n * existing debounce/poll path still drains correctly — `notify` only\n * lowers cross-process p99 reaction latency.\n *\n * Adapter status (Act 0.x):\n * - {@link PostgresStore}: implemented via `LISTEN`/`NOTIFY` on the\n * `act_commit` channel\n * - {@link InMemoryStore}: not implemented (single-process — no remote\n * writers exist)\n * - `SqliteStore`: not implemented (single-node by design)\n *\n * @param handler Callback invoked once per remote commit\n * @returns Disposer releasing the underlying listener\n */\n notify?: (\n handler: (notification: StoreNotification) => void\n ) => NotifyDisposer | Promise<NotifyDisposer>;\n\n /**\n * Wipe the sensitive-data payload for every event on the stream — the\n * physical-erasure side of the sensitive-data epic. Sets the\n * adapter's PII column (or equivalent) to `NULL` for the stream's\n * events; `events.data` and the rest of the row are never touched.\n *\n * Returns the count of rows wiped. Idempotent: a second call on an\n * already-wiped stream returns `0` without error.\n *\n * Capability-gated via `pii_isolation` in `@rotorsoft/act-tck`'s\n * `StoreCapabilities`. Adapters that can't UPDATE rows (Kafka,\n * append-only object-storage logs) declare `pii_isolation: false`\n * and omit this method; `app.forget(stream)` throws on those\n * adapters at orchestrator level.\n *\n * Append-only invariant on `events.data` is preserved — only the\n * separately-isolated PII column is mutated. Disk reclamation is\n * adapter-dependent (PG autovacuum reclaims lazily; SQLite needs\n * `PRAGMA incremental_vacuum` or `VACUUM`). For strict-deletion\n * jurisdictions the production checklist documents the operator\n * step.\n *\n * Encryption-at-rest is the operator's DB-layer concern (pgcrypto,\n * RDS TDE, Cloud SQL TDE, SQLite SEE) — not an application-level\n * port. The framework's job is isolation + erasure.\n *\n * @param stream Target stream\n * @returns Count of events whose PII column was set to NULL\n */\n forget_pii?: (stream: string) => Promise<number>;\n}\n\n// ---------------------------------------------------------------------------\n// Cache\n// ---------------------------------------------------------------------------\n\n/**\n * A cached snapshot entry for a stream.\n *\n * Carries its own `stream` key so an entry is self-describing when it\n * travels away from the cache map — state projections flush entries\n * outward as-is, one per dirty stream.\n *\n * @template TState - The state schema type\n */\nexport interface CacheEntry<TState extends Schema> {\n readonly stream: string;\n readonly state: TState;\n readonly version: number;\n readonly event_id: number;\n readonly patches: number;\n readonly snaps: number;\n}\n\n/**\n * Cache port for storing stream snapshots in-process.\n *\n * Implementations should provide fast key-value access with bounded memory.\n * The async interface is forward-compatible with external caches (e.g., Redis).\n *\n * @template TState - The state schema type\n */\nexport interface Cache extends Disposable {\n get<TState extends Schema>(\n stream: string\n ): Promise<CacheEntry<TState> | undefined>;\n set<TState extends Schema>(\n stream: string,\n entry: CacheEntry<TState>\n ): Promise<void>;\n invalidate(stream: string): Promise<void>;\n clear(): Promise<void>;\n}\n\n/**\n * Event name used internally for snapshot events in the event store.\n * Snapshot events store a full state checkpoint, enabling efficient cold-start\n * recovery without replaying the entire event stream.\n */\nexport const SNAP_EVENT = \"__snapshot__\";\n\n/**\n * Event name used internally for tombstone events in the event store.\n * A tombstone marks a stream as permanently closed — no further writes\n * are permitted until the stream is explicitly restarted via `close()`.\n *\n * @see {@link Act.close} for the close-the-books API\n */\nexport const TOMBSTONE_EVENT = \"__tombstone__\";\n","/**\n * @packageDocumentation\n * @module act/types\n * @category Types\n * Types for reactions, leases, and fetch results in the Act Framework.\n */\nimport type {\n Actor,\n BackoffOptions,\n Committed,\n IAct,\n Query,\n Schema,\n Schemas,\n Snapshot,\n} from \"./action.js\";\n\n/**\n * Reaction handler function that processes committed events.\n *\n * Reaction handlers respond to events asynchronously. They can:\n * - Perform side effects (send emails, call APIs, log, etc.)\n * - Return an action tuple to trigger another action\n * - Return `void` or `undefined` for side-effect-only reactions\n *\n * Handlers are called during drain cycles and support automatic retries\n * with configurable error handling.\n *\n * @template TEvents - Event schemas\n * @template TKey - Event name\n * @template TActions - Action schemas (defaults to Schemas for stored reactions)\n * @template TActor - Actor type extending base Actor\n * @param event - The committed event that triggered this reaction\n * @param stream - The target stream name for this reaction\n * @returns Promise resolving to an action tuple or void\n *\n * @example Side effect only\n * ```typescript\n * const sendEmail: ReactionHandler<Events, \"UserCreated\"> = async (event) => {\n * await emailService.send(event.data.email, \"Welcome!\");\n * };\n * ```\n *\n * @see {@link Reaction} for complete reaction configuration\n */\nexport type ReactionHandler<\n TEvents extends Schemas,\n TKey extends keyof TEvents,\n TActions extends Schemas = Schemas,\n TActor extends Actor = Actor,\n> = (\n event: Committed<TEvents, TKey>,\n stream: string,\n app: IAct<TEvents, TActions, TActor>\n) => Promise<Snapshot<Schema, TEvents> | void>;\n\n/**\n * Resolver for determining which stream a reaction should target.\n *\n * Resolvers enable dynamic reaction routing based on event content. They can be:\n * - **Static**: Always route to the same target stream\n * - **Dynamic**: Determine target based on event data at runtime\n *\n * Resolvers can also specify source streams for optimization, allowing the drain\n * process to efficiently fetch only relevant events. An optional `priority`\n * biases the lagging-frontier `claim()` ordering — see {@link Resolved.priority}.\n *\n * @template TEvents - Event schemas\n * @template TKey - Event name\n * @param event - The committed event (for dynamic resolvers)\n * @returns Target stream configuration or undefined to skip\n *\n * @example Static target\n * ```typescript\n * .on(\"UserCreated\")\n * .do(sendWelcomeEmail)\n * .to(\"email-queue\") // Static target\n * ```\n *\n * @see {@link Reaction} for complete reaction configuration\n * @see {@link Resolved} for the resolved-target shape\n */\nexport type ReactionResolver<\n TEvents extends Schemas,\n TKey extends keyof TEvents,\n TLane extends string = string,\n> =\n | Resolved<TLane> // static\n | ((event: Committed<TEvents, TKey>) => Resolved<TLane> | undefined); // dynamic\n\n/**\n * Resolver output shape — what `.to(...)` returns for a static or dynamic\n * resolver.\n *\n * @property target - Stream name that processes this reaction\n * @property source - Optional source the target consumes from. A\n * **literal** name (no regex metacharacter) is matched by string\n * equality in the store's claim/fetch path — the fast, index-friendly\n * path, and exact so `\"s1\"` never matches `\"s12\"`. A name carrying regex\n * metacharacters (e.g. `^(A|B)$`) is compiled as a `RegExp` and matched\n * against candidate streams with the caller's own anchoring\n * @property priority - Optional scheduling hint. The lagging-frontier\n * `claim()` orders streams by `priority DESC, at ASC`, so a higher value\n * makes the stream win lease slots ahead of equal-watermark peers under\n * saturation. Default `0` — behavior identical to current dual-frontier.\n * Only meaningful when `streamLimit` is binding (more candidate streams\n * than the worker can claim per cycle); idle systems are unaffected.\n * See `libs/act-pg/PERFORMANCE.md` for the benchmark that motivated this\n * knob.\n * @property lane - Optional drain lane. Defaults to `\"default\"`.\n */\nexport type Resolved<TLane extends string = string> = {\n readonly target: string;\n readonly source?: string;\n readonly priority?: number;\n readonly lane?: TLane;\n};\n\n/**\n * Build-time configuration for a drain lane.\n *\n * @property name - Lane name (`\"default\"` is reserved for the implicit lane)\n * @property leaseMillis - Lease window for `claim()` calls in this lane\n * @property streamLimit - Max streams claimed per cycle in this lane\n * @property cycleMs - Cycle frequency for this lane's controller\n */\nexport type LaneConfig<TName extends string = string> = {\n readonly name: TName;\n readonly leaseMillis?: number;\n readonly streamLimit?: number;\n readonly cycleMs?: number;\n};\n\n/**\n * Options for reaction processing.\n *\n * For the shared retry-pacing shape see {@link BackoffOptions} and\n * {@link BackoffStrategy} in `./action.js`.\n *\n * The backoff schedule is persisted on the stream, so every competing\n * worker honors it and `retry` advances once per real attempt.\n *\n * @property blockOnError - Whether to block on error.\n * @property maxRetries - Retries after the first attempt: a failing stream\n * is attempted `maxRetries + 1` times before it blocks.\n * @property backoff - Optional retry pacing. When omitted, a retry waits a\n * fixed 10 seconds. Either way, no worker re-attempts the stream before\n * the delay; the lease never sets retry timing.\n */\nexport type ReactionOptions = {\n readonly blockOnError: boolean;\n readonly maxRetries: number;\n readonly backoff?: BackoffOptions;\n};\n\n/**\n * Distributive mapped type that produces a proper discriminated union of\n * committed events. Unlike `Committed<TEvents, keyof TEvents>` (where\n * `name` and `data` are independent unions), each variant correlates\n * `name` with its corresponding `data` — enabling `switch (event.name)`\n * to narrow both fields correctly.\n *\n * @template TEvents - Event schemas\n *\n * @example Exhaustive switch\n * ```typescript\n * for (const event of events) {\n * switch (event.name) {\n * case \"TicketOpened\":\n * event.data; // typed as TicketOpened's schema\n * break;\n * case \"TicketClosed\":\n * event.data; // typed as TicketClosed's schema\n * break;\n * default:\n * const _: never = event; // compile error if a case is missing\n * }\n * }\n * ```\n */\nexport type BatchEvent<TEvents extends Schemas> = {\n [K in keyof TEvents]: Committed<TEvents, K>;\n}[keyof TEvents];\n\n/**\n * Batch handler for projections that processes multiple events in a single call.\n *\n * Receives the full ordered array of all event types declared on the projection,\n * enabling bulk DB operations (batch INSERT/UPDATE) in a single transaction.\n * The handler is always called when defined — even for a single event.\n *\n * @template TEvents - Event schemas (all events declared on the projection)\n * @param events - Ordered array of committed events (discriminated union)\n * @param stream - The target stream name\n *\n * @see {@link BatchEvent} for the discriminated union type\n */\nexport type BatchHandler<TEvents extends Schemas> = (\n events: ReadonlyArray<BatchEvent<TEvents>>,\n stream: string\n) => Promise<void>;\n\n/**\n * Options for `projection(name).of(state, options)` — both deterministic:\n * behavior is a pure function of the event sequence and these two bounds.\n *\n * @property flushEvery - Events folded between flush rounds (default 1000)\n * @property maxCachedStates - LRU bound on in-memory folded states; the\n * evictee is flushed before it is dropped (default 10000)\n */\nexport type FoldOptions = Readonly<{\n flushEvery?: number;\n maxCachedStates?: number;\n}>;\n\n/**\n * Defines a reaction to an event.\n * @template TEvents - Event schemas.\n * @template TKey - Event name.\n * @template TActions - Action schemas.\n * @template TActor - Actor type extending base Actor.\n * @property handler - The reaction handler.\n * @property resolver - The reaction resolver.\n * @property options - The reaction options.\n */\nexport type Reaction<\n TEvents extends Schemas,\n TKey extends keyof TEvents = keyof TEvents,\n TActions extends Schemas = Schemas,\n TActor extends Actor = Actor,\n> = {\n readonly handler: ReactionHandler<TEvents, TKey, TActions, TActor>;\n /**\n * Mutable so the builder's `.do()` → `.to()` chain can patch the resolver\n * in place (registered once with the default `_this_` resolver in `.do()`,\n * overwritten in `.to()` if present). After build-time the field is\n * effectively immutable; runtime consumers only read it.\n */\n resolver: ReactionResolver<TEvents, TKey>;\n readonly options: ReactionOptions;\n};\n\n/**\n * Payload for a reaction.\n * @template TEvents - Event schemas.\n * @property handler - The reaction handler.\n * @property resolver - The reaction resolver.\n * @property options - The reaction options.\n * @property event - The committed event triggering the reaction.\n * @property source - The source stream.\n */\nexport type ReactionPayload<TEvents extends Schemas> = Reaction<TEvents> & {\n readonly event: Committed<TEvents, keyof TEvents>;\n readonly source?: string;\n};\n\n/**\n * Result of fetching events from the store for processing.\n * @template TEvents - Event schemas.\n * @property stream - The stream name\n * @property source - The source (literal name or regex pattern), or undefined when sourcing from all streams.\n * @property at - The last event sequence number processed by the stream.\n * @property lagging - Whether the stream is lagging behind.\n * @property events - The list of next committed events to be processed by the stream.\n */\nexport type Fetch<TEvents extends Schemas> = Array<{\n readonly stream: string;\n readonly source?: string;\n readonly at: number;\n readonly lagging: boolean;\n readonly events: Committed<TEvents, keyof TEvents>[];\n /**\n * Why this stream's read failed, when it did. Present only on failure,\n * and then `events` is empty.\n *\n * Reads are per-stream but ran under one `Promise.all`, so a single\n * stream's failure rejected the whole cycle and every healthy stream\n * leased alongside it got nothing — a corrupt `pii` payload on one\n * aggregate stalled unrelated reactions. Isolating the failure\n * here keeps the blast radius on the stream that caused it: the cycle\n * records the error, submits no ack (so the watermark holds), and the\n * stream accrues `retry` until `blockOnError` quarantines it the usual\n * way — visible in `blocked_streams()` instead of silent.\n */\n readonly error?: string;\n}>;\n\n/**\n * Lease information for distributed stream processing.\n *\n * Leases prevent concurrent processing of the same stream by multiple workers.\n * When a worker acquires a lease, it has exclusive rights to process events\n * for that stream until the lease expires or is acknowledged.\n *\n * The drain process uses leases to:\n * - Prevent race conditions in distributed setups\n * - Track processing progress (watermark)\n * - Manage retries on failures\n * - Balance load between lagging and leading streams\n *\n * @property stream - The target stream name being processed\n * @property source - Optional source the subscription consumes from (literal name or regex pattern)\n * @property at - Watermark: last successfully processed event ID\n * @property by - Unique identifier of the lease holder (UUID)\n * @property retry - Number of retry attempts (0 = first attempt)\n * @property lagging - Whether this stream is behind (lagging frontier)\n * @property lane - Drain lane the stream is bound to\n *\n * @example\n * ```typescript\n * app.on(\"acked\", (leases) => {\n * leases.forEach(lease => {\n * console.log(`Processed ${lease.stream} up to event ${lease.at}`);\n * });\n * });\n *\n * app.on(\"blocked\", (blocked) => {\n * blocked.forEach(({ stream, retry, error }) => {\n * console.error(`Stream ${stream} blocked after ${retry} retries: ${error}`);\n * });\n * });\n * ```\n *\n * @see {@link Drain} for drain cycle results\n */\nexport type Lease = {\n readonly stream: string;\n readonly source?: string;\n readonly at: number;\n readonly by: string;\n readonly retry: number;\n readonly lagging: boolean;\n readonly lane?: string;\n /**\n * Defer marker on the finalize path. When set on a lease passed to\n * {@link Store.ack}, the entry is *deferred* alongside its watermark\n * advance: the adapter advances the watermark to `at` (the last event\n * handled this cycle) **and** persists `due` (ms since epoch) as the\n * stream's `deferred_at`, setting `retry` to the entry's own value —\n * atomically with the other entries' acks. Advance and defer are\n * independent legs, so a partial-progress defer keeps the handled prefix\n * (it never re-runs on redelivery) while the remainder waits for `due`.\n * Deferred entries are not part of ack's return value. Never set\n * on leases returned by `claim` or carried by lifecycle events.\n */\n readonly due?: number;\n};\n\n/**\n * A {@link Lease} augmented with the failure reason that pushed it past\n * its retry budget. Yielded by {@link Drain.blocked}, emitted on the\n * `\"blocked\"` lifecycle event, and accepted by {@link Store.block}.\n */\nexport type BlockedLease = Lease & { readonly error: string };\n\n/**\n * Options for draining events from the store.\n * @property streamLimit - Maximum number of streams to fetch.\n * @property eventLimit - Maximum number of events to fetch per stream.\n * @property leaseMillis - Maximum lease duration (in milliseconds).\n */\nexport type DrainOptions = {\n readonly streamLimit?: number;\n readonly eventLimit?: number;\n readonly leaseMillis?: number;\n};\n\n/**\n * Drain results\n * @property fetched - The fetched events.\n * @property leased - The leased events.\n * @property acked - The acked events.\n * @property blocked - The blocked events (with error).\n */\nexport type Drain<TEvents extends Schemas> = {\n readonly fetched: Fetch<TEvents>;\n readonly leased: Lease[];\n readonly acked: Lease[];\n readonly blocked: BlockedLease[];\n};\n\n/**\n * Options for the debounced settle cycle.\n *\n * Extends {@link DrainOptions} with parameters that control the debounce\n * window, the correlation query, and the maximum number of correlate→drain\n * passes.\n *\n * @property debounceMs - Debounce window in milliseconds (default: 10). A\n * pass already scheduled keeps its time unless this is shorter.\n * @property correlate - Only `limit` is used: the most events each\n * correlation scan reads (default 100). Every pass scans from the\n * correlation checkpoint, which every reaction shares, so filters and\n * `after` are ignored.\n * @property maxPasses - Cap on correlate→drain loops (default: `Infinity`).\n * Settle exits early as soon as a pass makes no progress (it read no\n * events and leased no streams), so the cap only matters in\n * pathological cases. It caps one settle: a reaction chain that keeps\n * committing schedules new passes of its own, which `maxPasses` doesn't\n * cover. `shutdown()` stops any of them after the pass in flight.\n */\nexport type SettleOptions = DrainOptions & {\n readonly debounceMs?: number;\n readonly correlate?: Query;\n readonly maxPasses?: number;\n};\n\n/**\n * Options for {@link IAct.shutdown}.\n *\n * @property graceMs - How long teardown waits for drain cycles already in\n * flight, in milliseconds. A reaction handler parked on an `await` (an\n * HTTP call, a database write) holds its stream's lease until it acks, so\n * abandoning it means the replacement worker cannot claim that stream\n * until the lease expires — up to `leaseMillis` of dead time per in-flight\n * stream on every rolling deploy, with the completing handler's round of\n * work discarded and redelivered.\n *\n * Defaults to the largest `leaseMillis` configured across the Act's lanes\n * (capped at 30s), because a lane's lease is already the operator's\n * statement of how long a handler may legitimately hold a stream. Lanes\n * that pinned no lease contribute the same 10s default `drain()` uses.\n *\n * `0` means: stop scheduling and return without\n * waiting. Whatever the budget, it is a ceiling and not a delay — teardown\n * proceeds the moment the last in-flight cycle finishes.\n */\nexport type ShutdownOptions = {\n readonly graceMs?: number;\n};\n\n/**\n * Name of the implicit lane every reaction lands in unless its `.to({lane})`\n * declaration says otherwise. An Act that never calls `.withLane(...)` has\n * only this lane.\n *\n * Persisted on `streams.lane` and threaded as the strict-typed default in\n * builder generics — `lane?: TLanes` always includes `\"default\"`.\n */\nexport const DEFAULT_LANE = \"default\";\n","import { type ZodObject, type ZodRawShape, z } from \"zod\";\nimport { _mark_sensitive, _registry } from \"../sensitive.js\";\n\n/**\n * @packageDocumentation\n * @module act/types\n * @category Types\n * Zod schemas and helpers for the Act Framework.\n */\n\n/**\n * An empty Zod schema (no properties).\n */\nexport const ZodEmpty = z.record(z.string(), z.never());\n\nexport { pii_fields } from \"../sensitive.js\";\n\n/**\n * Sentinel placed in `event.data[field]` when the caller isn't authorized to\n * see the sensitive field — either `.discloses(predicate)` returned `false`,\n * or no predicate was declared (framework default-deny). Recoverable: a\n * properly-authorized read returns the plaintext.\n */\nexport const REDACTED = \"[REDACTED]\" as const;\n\n/**\n * Sentinel placed in `event.data[field]` when the underlying PII payload has\n * been wiped via `Store.forget_pii(stream)` — the row's pii column is `NULL`\n * and the original plaintext is gone forever. Irrecoverable.\n */\nexport const SHREDDED = \"[SHREDDED]\" as const;\n\n/**\n * Mark a Zod schema as sensitive. Returns the same schema instance — the\n * marker is registered out-of-band so the static type is preserved and the\n * call site reads as a pure annotation.\n *\n * Idempotent: re-wrapping an already-sensitive schema is a no-op.\n *\n * The marker is what the orchestrator inspects to split event payloads into\n * `data` + `pii` on commit, gate reads via `.discloses`, and strip handler\n * payloads. Part of the sensitive-data foundation.\n *\n * @example\n * ```ts\n * import { z } from \"zod\";\n * import { state, sensitive } from \"@rotorsoft/act\";\n *\n * const UserRegistered = z.object({\n * email: sensitive(z.string()),\n * name: sensitive(z.string()),\n * plan: z.enum([\"free\", \"pro\"]), // not sensitive — stays in events.data\n * });\n * ```\n *\n * @param schema - The Zod schema to mark sensitive.\n * @returns The same schema instance, unmodified at the type level.\n */\nexport function sensitive<T extends z.ZodType>(schema: T): T {\n _registry.add(schema, { sensitive: true });\n // Also stamp the def, so the marker survives the clone Zod produces for\n // any refinement chained after this call (`sensitive(z.string()).min(1)`);\n // without it the field would be written as plaintext into `events.data`.\n _mark_sensitive(schema);\n return schema;\n}\n\n/**\n * Zod schema for an actor (user, system, etc.).\n */\nexport const ActorSchema = z\n .object({\n id: z.string(),\n name: z.string(),\n })\n .loose()\n .readonly();\n\n/**\n * Zod schema for a target (stream and actor info).\n */\nexport const TargetSchema = z\n .object({\n stream: z.string(),\n actor: ActorSchema,\n expectedVersion: z.number().optional(),\n })\n .loose()\n .readonly();\n\n/**\n * Zod schema for causation event metadata.\n */\nexport const CausationEventSchema = z.object({\n id: z.number(),\n name: z.string(),\n stream: z.string(),\n});\n\n/**\n * Zod schema for event metadata (correlation and causation).\n */\nexport const EventMetaSchema = z\n .object({\n correlation: z.string(),\n causation: z.object({\n action: TargetSchema.and(z.object({ name: z.string() })).optional(),\n event: CausationEventSchema.optional(),\n }),\n })\n .readonly();\n\n/**\n * Zod schema for committed event metadata (id, stream, version, created, meta).\n */\nexport const CommittedMetaSchema = z\n .object({\n id: z.number(),\n stream: z.string(),\n version: z.number(),\n created: z.date(),\n meta: EventMetaSchema,\n })\n .readonly();\n\n/**\n * Type representing the full state schema for a domain.\n * @property events - Map of event names to Zod schemas.\n * @property actions - Map of action names to Zod schemas.\n * @property state - Zod schema for the state object.\n */\nexport type StateSchema = Readonly<{\n events: Record<string, ZodObject<ZodRawShape> | typeof ZodEmpty>;\n actions: Record<string, ZodObject<ZodRawShape> | typeof ZodEmpty>;\n state: ZodObject<ZodRawShape>;\n}>;\n\n/**\n * Query options for event store queries.\n */\nexport const QuerySchema = z\n .object({\n stream: z.string().optional(),\n names: z.string().array().optional(),\n before: z.number().optional(),\n after: z.number().optional(),\n limit: z.number().optional(),\n created_before: z.date().optional(),\n created_after: z.date().optional(),\n backward: z.boolean().optional(),\n correlation: z.string().optional(),\n with_snaps: z.boolean().optional(),\n with_pii: z.boolean().optional(),\n stream_exact: z.boolean().optional(),\n })\n .readonly();\n","/**\n * The sensitive-data marker: the registry `sensitive(zodType)` writes to,\n * and the lookups that read it. The public `sensitive()` and the two\n * sentinels live in `types/schemas.ts`, which re-exports `pii_fields` from\n * here. The transforms that apply the marker at commit, load and dispatch\n * live in `internal/pii.ts`.\n */\nimport { z } from \"zod\";\n\n/**\n * Process-global registry holding every Zod schema marked sensitive. Backed\n * by a `WeakMap`, so wrapper-created instances (`.optional()`, `.nullable()`,\n * `.default()`) that chain off a marked schema produce *new* schema instances\n * the registry doesn't track; the field walker handles those via unwrap.\n *\n * Exported so the public `sensitive(zodType)` wrapper can call `_registry.add`.\n * Underscore prefix marks \"framework-private, don't touch from user code.\"\n */\nexport const _registry = z.registry<{ sensitive: true }>();\n\n/**\n * Marker key stamped onto a schema's own `def`. Zod clones a schema on every\n * refinement (`.min()`, `.email()`, `.trim()`, `.describe()`, `.refine()`,\n * `.transform()`, …) via `{...def}`, which copies own symbol keys — so a\n * marker on the def survives the whole chain, while the `_registry` WeakMap\n * (keyed on the *instance*) does not.\n *\n * The registry is still populated and still consulted: it covers schemas\n * marked before this key existed, and it is the mechanism the public\n * `sensitive()` doc-comment describes.\n */\nconst _SENSITIVE = Symbol.for(\"act.sensitive\");\n\n/**\n * Stamp the def-level marker. Called by the public `sensitive()` alongside\n * `_registry.add`.\n */\nexport function _mark_sensitive(schema: z.ZodType): void {\n const def = (\n schema as unknown as {\n _zod?: { def?: Record<PropertyKey, unknown> };\n }\n )._zod?.def;\n if (def) def[_SENSITIVE] = true;\n}\n\n/**\n * True when the given schema was marked via `sensitive(...)`.\n *\n * Walks through Zod wrapper layers (`.optional()`, `.nullable()`,\n * `.default()`, `.readonly()`) by following `_def.innerType` until it reaches\n * a non-wrapper schema, then checks the registry. Wrappers create new schema\n * instances; the marker lives on the *inner* schema the user wrapped, so we\n * test that one.\n */\nfunction is_pii(schema: z.ZodType): boolean {\n let cur: z.ZodType = schema;\n while (true) {\n if (_registry.has(cur)) return true;\n // Def-level marker: survives the clone a refinement produces, which the\n // instance-keyed registry above cannot.\n const def = (\n cur as unknown as {\n _zod?: { def?: Record<PropertyKey, unknown> };\n }\n )._zod?.def;\n if (def?.[_SENSITIVE] === true) return true;\n const inner = (cur as { _def?: { innerType?: z.ZodType } })._def?.innerType;\n if (!inner || inner === cur) return false;\n cur = inner;\n }\n}\n\n/**\n * Derive an event's sensitive fields, as the declared schema of each.\n *\n * Walks the top-level shape of a `z.object({...})` and keeps the keys whose\n * schema (after unwrapping optional/nullable/default wrappers) was marked via\n * `sensitive(...)`. Returns an empty object for non-object schemas or events\n * with no sensitive fields — the common-case zero-cost path.\n *\n * Only the top-level shape is walked. Sensitive fields nested inside a\n * `z.object` declared inside the event payload would require recursive\n * descent; that's deferred until a real callsite needs it.\n *\n * A union event has no top-level shape, so the options are walked and merged:\n * a key sensitive in any variant must be split, because the stored payload\n * could be that variant.\n * The first variant to declare a key wins, which only matters to a caller that\n * wants the schema rather than the name.\n *\n * Returning the schemas rather than just the names is what lets a caller do\n * something per field — the event builder asks each one whether it holds a\n * date, so the `pii` sidecar's dates can be revived like any other.\n */\nexport function pii_schemas(schema: z.ZodType): Record<string, z.ZodType> {\n const shape = (schema as { shape?: Record<string, z.ZodType> }).shape;\n if (shape && typeof shape === \"object\") {\n const fields: Record<string, z.ZodType> = {};\n for (const key of Object.keys(shape))\n if (is_pii(shape[key])) fields[key] = shape[key];\n return fields;\n }\n const options = (schema as { options?: unknown }).options;\n if (Array.isArray(options)) {\n const fields: Record<string, z.ZodType> = {};\n for (const option of options)\n for (const [key, field] of Object.entries(\n pii_schemas(option as z.ZodType)\n ))\n fields[key] ??= field;\n return fields;\n }\n return {};\n}\n\n/**\n * The top-level field names of a schema marked with `sensitive()`. Adapters\n * and tooling use it to reflect sensitivity on their own wire surface (the\n * act-http OpenAPI emitter marks these fields `writeOnly`). Returns `[]`\n * for a schema with no sensitive fields.\n */\nexport function pii_fields(schema: z.ZodType): readonly string[] {\n return Object.keys(pii_schemas(schema));\n}\n","/**\n * @packageDocumentation\n * @module act/types\n * Barrel file for Act Framework core types.\n *\n * Re-exports all major type definitions for actions, errors, ports, reactions, registries, and schemas.\n * Also defines common environment and log level types/constants for configuration and logging.\n *\n * @remarks\n * Import from this module to access all core framework types in one place.\n */\nexport * from \"./act.js\";\nexport type * from \"./action.js\";\nexport type * from \"./audit.js\";\nexport type {\n ActBuilder,\n ActionBuilder,\n Projection,\n ProjectionBuilder,\n Slice,\n SliceBuilder,\n StateBuilder,\n} from \"./builders.js\";\nexport * from \"./errors.js\";\nexport type * from \"./ports.js\";\nexport { SNAP_EVENT, TOMBSTONE_EVENT } from \"./ports.js\";\nexport type * from \"./reaction.js\";\nexport { DEFAULT_LANE } from \"./reaction.js\";\nexport type * from \"./registry.js\";\nexport * from \"./schemas.js\";\n\n/**\n * Supported runtime environments for the framework.\n * - `development`: Local development\n * - `test`: Automated testing\n * - `staging`: Pre-production\n * - `production`: Live/production\n */\nexport const Environments = [\n \"development\",\n \"test\",\n \"staging\",\n \"production\",\n] as const;\n\n/**\n * Type representing a valid environment string.\n */\nexport type Environment = (typeof Environments)[number];\n\n/**\n * Supported log levels for framework logging.\n * - `fatal`, `error`, `warn`, `info`, `debug`, `trace`\n */\nexport const LogLevels = [\n \"fatal\",\n \"error\",\n \"warn\",\n \"info\",\n \"debug\",\n \"trace\",\n] as const;\n\n/**\n * Type representing a valid log level string.\n */\nexport type LogLevel = (typeof LogLevels)[number];\n\n/**\n * List of exit codes for process termination, as passed to\n * {@link disposeAndExit} by the signal handlers and by callers that shut\n * down on their own.\n */\nexport const ExitCodes = [\"ERROR\", \"EXIT\"] as const;\n\n/**\n * Type for allowed exit codes.\n *\n * - `\"ERROR\"` — abnormal termination (uncaught exception, unhandled rejection)\n * - `\"EXIT\"` — clean shutdown (SIGINT, SIGTERM, or manual trigger)\n */\nexport type ExitCode = (typeof ExitCodes)[number];\n\n/**\n * The validated `package.json` metadata `config()` reads.\n */\nexport type Package = {\n name: string;\n version: string;\n description?: string;\n author?: { name: string; email?: string } | string;\n license?: string;\n dependencies?: Record<string, string>;\n};\n\n/** The validated Act Framework configuration `config()` returns. */\nexport type Config = Package & {\n env: Environment;\n logLevel: LogLevel;\n logSingleLine: boolean;\n sleepMs: number;\n};\n"]}
|