@rotorsoft/act 1.32.26 → 1.32.28

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.
Files changed (57) hide show
  1. package/README.md +2 -3
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/@types/act.d.ts +7 -152
  4. package/dist/@types/act.d.ts.map +1 -1
  5. package/dist/@types/adapters/in-memory-store.d.ts +0 -21
  6. package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
  7. package/dist/@types/builders/act-builder.d.ts +0 -25
  8. package/dist/@types/builders/act-builder.d.ts.map +1 -1
  9. package/dist/@types/builders/build-classify.d.ts +4 -4
  10. package/dist/@types/builders/build-classify.d.ts.map +1 -1
  11. package/dist/@types/builders/projection-builder.d.ts.map +1 -1
  12. package/dist/@types/builders/reaction-builder.d.ts +3 -3
  13. package/dist/@types/builders/reaction-builder.d.ts.map +1 -1
  14. package/dist/@types/builders/slice-builder.d.ts +1 -12
  15. package/dist/@types/builders/slice-builder.d.ts.map +1 -1
  16. package/dist/@types/builders/state-builder.d.ts +0 -189
  17. package/dist/@types/builders/state-builder.d.ts.map +1 -1
  18. package/dist/@types/config.d.ts +0 -27
  19. package/dist/@types/config.d.ts.map +1 -1
  20. package/dist/@types/internal/circuit-breaker.d.ts +2 -0
  21. package/dist/@types/internal/circuit-breaker.d.ts.map +1 -1
  22. package/dist/@types/internal/close-cycle.d.ts +2 -1
  23. package/dist/@types/internal/close-cycle.d.ts.map +1 -1
  24. package/dist/@types/internal/drain-cycle.d.ts +13 -4
  25. package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
  26. package/dist/@types/internal/index.d.ts +1 -1
  27. package/dist/@types/internal/index.d.ts.map +1 -1
  28. package/dist/@types/internal/settle.d.ts +2 -1
  29. package/dist/@types/internal/settle.d.ts.map +1 -1
  30. package/dist/@types/ports.d.ts +0 -7
  31. package/dist/@types/ports.d.ts.map +1 -1
  32. package/dist/@types/test/sandbox.d.ts +0 -14
  33. package/dist/@types/test/sandbox.d.ts.map +1 -1
  34. package/dist/@types/types/action.d.ts +0 -27
  35. package/dist/@types/types/action.d.ts.map +1 -1
  36. package/dist/@types/types/errors.d.ts +1 -92
  37. package/dist/@types/types/errors.d.ts.map +1 -1
  38. package/dist/@types/types/ports.d.ts +0 -70
  39. package/dist/@types/types/ports.d.ts.map +1 -1
  40. package/dist/@types/types/reaction.d.ts +1 -34
  41. package/dist/@types/types/reaction.d.ts.map +1 -1
  42. package/dist/{chunk-QXW3JV2H.js → chunk-5FXT63KF.js} +2 -2
  43. package/dist/chunk-5FXT63KF.js.map +1 -0
  44. package/dist/{chunk-XSG2NW4J.js → chunk-F7K7SIG6.js} +1 -1
  45. package/dist/chunk-F7K7SIG6.js.map +1 -0
  46. package/dist/index.cjs +83 -215
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.js +85 -217
  49. package/dist/index.js.map +1 -1
  50. package/dist/test/index.cjs.map +1 -1
  51. package/dist/test/index.js +2 -2
  52. package/dist/test/index.js.map +1 -1
  53. package/dist/types/index.cjs.map +1 -1
  54. package/dist/types/index.js +1 -1
  55. package/package.json +1 -4
  56. package/dist/chunk-QXW3JV2H.js.map +0 -1
  57. package/dist/chunk-XSG2NW4J.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types/errors.ts","../src/types/schemas.ts","../src/internal/sensitive.ts","../src/types/index.ts"],"sourcesContent":["import type {\n Actor,\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_INVARIANT`: Invariant validation error\n * - `ERR_CONCURRENCY`: Optimistic concurrency validation error on commits\n */\nexport const Errors = {\n ValidationError: \"ERR_VALIDATION\",\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 business rule (invariant) is violated during action execution.\n *\n * Invariants are conditions that must hold true for an action to succeed.\n * They're checked after loading the current state but before emitting events.\n * This error provides complete context about what action was attempted and\n * why it was rejected.\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 invariant violations\n * ```typescript\n * import { InvariantError } 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 InvariantError) {\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 Invariant} for defining business rules\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 invariant was checked */\n readonly snapshot: Snapshot<TState, TEvents>;\n /** Human-readable description of why the invariant failed */\n readonly description: string;\n\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 as string} failed invariant: ${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 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","import { type ZodObject, type ZodRawShape, z } from \"zod\";\n// Deep-path import (vs `../internal/index.js`) is deliberate — `_registry` is\n// a side-effect-free leaf, and going through the internal barrel would pull\n// tracing.ts → config.ts in at type-schema load time and crash on TDZ when a\n// test imports a public schema before config is initialized.\nimport { _mark_sensitive, _registry } from \"../internal/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\n/**\n * Sensitive-data foundation re-exports.\n *\n * - `REDACTED` / `SHREDDED` — sentinels placed in `event.data[field]`\n * when the caller isn't authorized to see a sensitive field\n * (`.discloses(predicate)` returned `false` or none was declared —\n * recoverable) or the underlying PII was wiped (`Store.forget_pii` —\n * irrecoverable).\n * top-level field names marked via {@link sensitive}. A pure,\n * read-only helper that inspects the out-of-band sensitive registry (a\n * process-global `WeakMap`) `sensitive()` populates, otherwise\n * unreachable from outside the package. Surfaced for adapters and\n * tooling that must reflect input sensitivity on their own wire\n * surface — e.g. the `@rotorsoft/act-http/openapi` emitter marks these\n * fields `writeOnly` + `format: password` so generated clients and\n * Swagger UI don't echo PII freely. Returns `[]` for non-object\n * schemas or objects with no sensitive fields; top-level shape only.\n */\nexport { pii_fields, REDACTED, SHREDDED } from \"../internal/sensitive.js\";\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 // used to lose it silently and write 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 * @module sensitive\n * @category Internal\n *\n * Internal mechanics for the sensitive-data foundation.\n * The public surface (`sensitive(zodType)`) lives at `libs/act/src/sensitive.ts`\n * and re-exports `REDACTED` / `SHREDDED` from here; this module holds the\n * registry plus the helpers the orchestrator calls during commit, load, and\n * handler dispatch.\n *\n * - `_registry` — process-global `z.registry<{ sensitive: true }>()`. Public\n * `sensitive()` adds to it; the helpers in this module read it.\n * - `pii_fields(schema)` — walk a Zod schema's top-level shape, return the\n * keys marked via `sensitive(...)`.\n * - `pii_gate(event, fields, predicate, actor)` — produce the external\n * view: plaintext when authorized, `[REDACTED]` when not, `[SHREDDED]`\n * when the underlying pii column is null.\n * - `pii_strip(event, fields)` — remove sensitive keys entirely\n * before invoking projection / reaction handlers.\n *\n * @internal\n */\n\nimport { z } from \"zod\";\nimport type { Actor, Committed, Schemas } from \"../types/index.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 *\n * Re-exported from `libs/act/src/sensitive.ts` as part of the public surface.\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 *\n * Re-exported from `libs/act/src/sensitive.ts` as part of the public surface.\n */\nexport const SHREDDED = \"[SHREDDED]\" as const;\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 *\n * @internal\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 *\n * @internal\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 *\n * @internal\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 *\n * @internal\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 *\n * @internal\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 names of an event's sensitive fields — {@link pii_schemas} keyed.\n *\n * @internal — consumed by the registry's `sensitive_fields(event_name)` lookup,\n * and public through `types/schemas.ts`, where act-http's OpenAPI emitter uses\n * it to mark request-body properties `writeOnly`.\n */\nexport function pii_fields(schema: z.ZodType): readonly string[] {\n return Object.keys(pii_schemas(schema));\n}\n\n/**\n * Split an emitted event's `data` into `data` (non-sensitive) + `pii`\n * (sensitive) using the field list precomputed at build time. Used by the\n * State's `_pii_split` decorator just before `Store.commit`.\n *\n * Single forward pass over `Object.keys(validated)` — same shape as the\n * spread-and-delete-free implementation in slice 3, just hoisted out of the\n * orchestrator hot path so it's only invoked when the State actually has a\n * sensitive event.\n *\n * @internal\n */\nexport function pii_split<TName, TData extends Record<string, unknown>>(\n emitted: { name: TName; data: TData },\n fields: readonly string[]\n): { name: TName; data: TData; pii: Record<string, unknown> } {\n const data = { ...emitted.data };\n const pii: Record<string, unknown> = {};\n for (const f of fields) {\n if (f in data) {\n pii[f] = data[f];\n delete data[f];\n }\n }\n return { name: emitted.name, data, pii };\n}\n\n/**\n * Build the **external view** of a committed event — the form returned by\n * `load()`, `query()`, `query_array()`, and the snapshot in `do()`'s reply.\n *\n * Only ever reached for events that declare sensitive fields — the sole caller\n * is {@link make_gate}, which the builder invokes exclusively for sensitive\n * events; non-sensitive events short-circuit to {@link IDENTITY_GATE} before\n * they get here. `fields` is therefore guaranteed non-empty (same contract as\n * {@link pii_strip}).\n *\n * - Event whose `pii` payload is null/undefined → substitute {@link SHREDDED}\n * for each declared field. Irrecoverable, so no predicate check.\n * - Event with a `pii` payload, predicate returns `true` → merge `pii` into\n * `data` (plaintext).\n * - Event with a `pii` payload, predicate returns `false` OR no predicate\n * declared (framework default-deny) → substitute {@link REDACTED} for each\n * declared field.\n *\n * @internal\n */\nfunction pii_gate<TEvents extends Schemas, TKey extends keyof TEvents>(\n event: Committed<TEvents, TKey>,\n fields: readonly string[],\n predicate: ((event: any, actor: Actor) => boolean) | null,\n actor: Actor | undefined\n): Committed<TEvents, TKey> {\n const data = event.data as Record<string, unknown>;\n // The external view NEVER carries the isolated `pii` sidecar — dropping it\n // is the whole point of the gate. Keeping it (an earlier `...event` spread)\n // leaked plaintext PII on every gated read surface (`load`, `query`,\n // `query_array`) even while `data` was correctly redacted. Strip it\n // once here; the plaintext lives in `data` only on the authorized path.\n const { pii, ...rest } = event as Committed<TEvents, TKey> & {\n pii?: Record<string, unknown> | null;\n };\n if (pii == null) {\n const shredded: Record<string, unknown> = { ...data };\n for (const f of fields) shredded[f] = SHREDDED;\n return { ...rest, data: shredded as Committed<TEvents, TKey>[\"data\"] };\n }\n // Plaintext path requires both an actor AND a predicate that allows. Missing\n // either → default-deny → REDACTED.\n const allowed = !!actor && !!predicate && predicate(event, actor);\n if (allowed) {\n return {\n ...rest,\n data: { ...data, ...pii } as Committed<TEvents, TKey>[\"data\"],\n };\n }\n const redacted: Record<string, unknown> = { ...data };\n for (const f of fields) redacted[f] = REDACTED;\n return { ...rest, data: redacted as Committed<TEvents, TKey>[\"data\"] };\n}\n\n/**\n * A prebuilt per-event read gate: given a committed event and the reading\n * actor, return the caller-visible form. This is the single gating primitive\n * the builder prebuilds for **every** read surface — both the actor-less\n * `query` / `query_array` (which pass no actor → default-deny) and the\n * actor-aware `load` / `do`-return view (which pass the reader). Non-sensitive\n * events use the shared {@link IDENTITY_GATE}; sensitive events use a redactor\n * built by {@link make_gate} that closes over the field list and the state's\n * disclosure predicate, so the read path never recomputes the sensitive-field\n * lookup nor allocates per event.\n *\n * The `actor` is optional so the actor-less surfaces can call `gate(event)`.\n *\n * @internal\n */\nexport type EventGate = <TEvents extends Schemas, TKey extends keyof TEvents>(\n event: Committed<TEvents, TKey>,\n actor?: Actor\n) => Committed<TEvents, TKey>;\n\n/**\n * Shared zero-cost gate for every event with no `sensitive(...)` fields — a\n * single frozen reference the builder hands back for non-sensitive events, so\n * the common path is one `Map` miss and an identity call, no allocation. This\n * is the \"by default, return the event\" half of the prebuilt per-event gate.\n *\n * @internal\n */\nexport const IDENTITY_GATE: EventGate = (event) => event;\n\n/**\n * Prebuild a read gate for a sensitive event, capturing its field list and the\n * disclosure predicate once at build time. The returned closure defers to\n * {@link pii_gate} with the reading actor supplied per call:\n *\n * - `predicate = null` (the actor-less `query` surfaces, or a state that never\n * declared `.discloses`) → default-deny: declared fields come back\n * {@link REDACTED} (or {@link SHREDDED} once the pii column is forgotten).\n * - `predicate` set + an authorized actor → plaintext merged into `data`.\n *\n * Either way the isolated `pii` sidecar is dropped. The builder stores one gate\n * per sensitive event (per state for the load path; predicate-less for the\n * query path); non-sensitive events fall back to {@link IDENTITY_GATE}.\n *\n * @internal\n */\nexport function make_gate(\n fields: readonly string[],\n predicate: ((event: any, actor: Actor) => boolean) | null\n): EventGate {\n return (event, actor) => pii_gate(event, fields, predicate, actor);\n}\n\n/**\n * Build the **handler view** — sensitive keys removed entirely from `data`\n * and the `pii` field dropped from the event. Used before invoking projection\n * handlers and reaction handlers, which never see PII by framework rule.\n *\n * Different from {@link pii_gate} (which substitutes {@link REDACTED} or\n * {@link SHREDDED}) — projection tables and reaction sinks shouldn't even\n * structurally observe the keys, so a handler that mistakenly writes\n * `event.data.email` into a column would get `undefined`, not a sentinel\n * string that looks like real data. The strictness is deliberate.\n *\n * Reactions that genuinely need PII (e.g. a welcome-email reaction reading\n * `email`) opt back in by explicitly calling `app.load(stream, { actor:\n * system_actor })` inside the handler — pulling PII through the gate at the\n * call site makes the security-relevant path visible in code review.\n *\n * @internal\n */\nexport function pii_strip<\n TEvents extends Schemas,\n TKey extends keyof TEvents & string,\n>(\n event: Committed<TEvents, TKey>,\n fields: readonly string[]\n): Committed<TEvents, TKey> {\n // Contract: `fields` is non-empty. `build_handle` / `build_handle_batch`\n // filter on `fields.length > 0` before invocation.\n const data = event.data as Record<string, unknown>;\n const stripped: Record<string, unknown> = {};\n for (const k of Object.keys(data)) {\n if (!fields.includes(k)) stripped[k] = data[k];\n }\n const { pii: _drop_pii, ...rest } = event as Committed<TEvents, TKey> & {\n pii?: unknown;\n };\n return {\n ...rest,\n data: stripped as Committed<TEvents, TKey>[\"data\"],\n } as Committed<TEvents, TKey>;\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 type * from \"./action.js\";\nexport type * from \"./audit.js\";\nexport * from \"./errors.js\";\nexport type * from \"./ports.js\";\nexport type * 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"],"mappings":"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","names":["z","z"]}
package/dist/index.cjs CHANGED
@@ -1406,10 +1406,8 @@ var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
1406
1406
  function classify_registry(registry, states) {
1407
1407
  const statics = /* @__PURE__ */ new Map();
1408
1408
  const target_lanes = /* @__PURE__ */ new Map();
1409
- const reactive_events = /* @__PURE__ */ new Set();
1410
1409
  const event_to_lanes = /* @__PURE__ */ new Map();
1411
1410
  for (const [name, register] of Object.entries(registry.events)) {
1412
- if (register.reactions.size > 0) reactive_events.add(name);
1413
1411
  for (const reaction of register.reactions.values()) {
1414
1412
  if (typeof reaction.resolver === "function") {
1415
1413
  event_to_lanes.set(name, ALL_LANES);
@@ -1447,7 +1445,6 @@ function classify_registry(registry, states) {
1447
1445
  }
1448
1446
  return {
1449
1447
  static_targets: [...statics.values()],
1450
- reactive_events,
1451
1448
  event_to_state,
1452
1449
  event_to_lanes
1453
1450
  };
@@ -2460,6 +2457,10 @@ var CircuitBreaker = class {
2460
2457
  get failing() {
2461
2458
  return this._failures > 0;
2462
2459
  }
2460
+ /** How long the store is left alone after a failure before a retry. */
2461
+ get cooldown_ms() {
2462
+ return this._cooldown_ms;
2463
+ }
2463
2464
  /** Current state given the wall-clock `now`. */
2464
2465
  state(now) {
2465
2466
  if (this._opened_at === void 0) return "closed";
@@ -2604,7 +2605,7 @@ async function run_full_closes(full, target_map, deps, skipped) {
2604
2605
  }
2605
2606
  const safe = await partition_by_safety(
2606
2607
  stream_info,
2607
- deps.reactive_events_size,
2608
+ deps.reactive,
2608
2609
  skipped,
2609
2610
  deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE,
2610
2611
  deps.catch_up_correlation
@@ -2641,8 +2642,8 @@ async function run_full_closes(full, target_map, deps, skipped) {
2641
2642
  );
2642
2643
  }
2643
2644
  async function run_windowed_closes(windowed, deps, skipped) {
2644
- const checkpoint = deps.reactive_events_size > 0 ? await deps.catch_up_correlation(Number.MAX_SAFE_INTEGER) : -1;
2645
- const min_at = deps.reactive_events_size > 0 ? await probe_min_watermarks(
2645
+ const checkpoint = deps.reactive ? await deps.catch_up_correlation(Number.MAX_SAFE_INTEGER) : -1;
2646
+ const min_at = deps.reactive ? await probe_min_watermarks(
2646
2647
  windowed.map((t) => t.stream),
2647
2648
  deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE,
2648
2649
  checkpoint
@@ -2730,8 +2731,8 @@ async function scan_stream_heads(streams) {
2730
2731
  }
2731
2732
  return out;
2732
2733
  }
2733
- async function partition_by_safety(stream_info, reactive_events_size, skipped, page_size, catch_up_correlation2) {
2734
- if (reactive_events_size === 0) return [...stream_info.keys()];
2734
+ async function partition_by_safety(stream_info, reactive, skipped, page_size, catch_up_correlation2) {
2735
+ if (!reactive) return [...stream_info.keys()];
2735
2736
  let needed = -1;
2736
2737
  for (const info of stream_info.values())
2737
2738
  needed = Math.max(needed, info.max_id);
@@ -4010,20 +4011,24 @@ function fetch_failure_policy(registry) {
4010
4011
  }
4011
4012
  return { blockOnError, maxRetries };
4012
4013
  }
4013
- function resolver_failed(lease, { error, options }) {
4014
- log().error(error);
4015
- const block2 = options.blockOnError && lease.retry >= options.maxRetries;
4014
+ function finalize(lease, handled, at, error, options, logger, failed_at) {
4015
+ if (!error) return { lease, handled, acked_at: at };
4016
+ logger.error(error);
4017
+ const non_retryable = error instanceof NonRetryableError;
4018
+ const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
4019
+ if (block2)
4020
+ logger.error(
4021
+ non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
4022
+ );
4023
+ const next_attempt_at = block2 ? void 0 : Date.now() + compute_backoff_delay(lease.retry, options.backoff ?? DEFAULT_BACKOFF);
4016
4024
  return {
4017
4025
  lease,
4018
- handled: 0,
4019
- acked_at: lease.at,
4020
- error,
4021
- ...block2 ? { block: true } : {
4022
- next_attempt_at: Date.now() + compute_backoff_delay(
4023
- lease.retry,
4024
- options.backoff ?? DEFAULT_BACKOFF
4025
- )
4026
- }
4026
+ handled,
4027
+ acked_at: at,
4028
+ error: error.message,
4029
+ block: block2,
4030
+ next_attempt_at,
4031
+ failed_at
4027
4032
  };
4028
4033
  }
4029
4034
  function warn_misrouted(seen, stream, handler, event) {
@@ -4071,7 +4076,9 @@ async function run_drain_cycle(deps, cycle) {
4071
4076
  resolved = dynamic ? resolver(event) : resolver;
4072
4077
  } catch (error) {
4073
4078
  failed ??= {
4074
- error: `Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}`,
4079
+ error: new Error(
4080
+ `Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}`
4081
+ ),
4075
4082
  options: reaction.options
4076
4083
  };
4077
4084
  return false;
@@ -4093,23 +4100,29 @@ async function run_drain_cycle(deps, cycle) {
4093
4100
  leased.map((lease) => {
4094
4101
  const entry = fetch_map.get(lease.stream);
4095
4102
  if (entry.fetch.error !== void 0) {
4096
- const error = `Fetch failed for ${lease.stream}: ${entry.fetch.error}`;
4097
- log().error(error);
4098
4103
  const policy = fetch_failure_policy(registry);
4099
- const block2 = !store_failing && policy.blockOnError && lease.retry >= policy.maxRetries;
4100
- const failed = {
4101
- lease,
4102
- handled: 0,
4103
- acked_at: lease.at,
4104
- error,
4105
- ...block2 ? { block: true } : {
4106
- next_attempt_at: Date.now() + compute_backoff_delay(lease.retry, DEFAULT_BACKOFF)
4107
- }
4108
- };
4109
- return Promise.resolve(failed);
4104
+ return Promise.resolve(
4105
+ finalize(
4106
+ lease,
4107
+ 0,
4108
+ lease.at,
4109
+ new Error(`Fetch failed for ${lease.stream}: ${entry.fetch.error}`),
4110
+ store_failing ? { ...policy, blockOnError: false } : policy,
4111
+ deps.logger
4112
+ )
4113
+ );
4110
4114
  }
4111
4115
  if (entry.failed)
4112
- return Promise.resolve(resolver_failed(lease, entry.failed));
4116
+ return Promise.resolve(
4117
+ finalize(
4118
+ lease,
4119
+ 0,
4120
+ lease.at,
4121
+ entry.failed.error,
4122
+ entry.failed.options,
4123
+ deps.logger
4124
+ )
4125
+ );
4113
4126
  const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
4114
4127
  const { payloads } = entry;
4115
4128
  const exhausted = budget_exhausted(
@@ -4148,18 +4161,23 @@ var EMPTY_DRAIN = {
4148
4161
  acked: [],
4149
4162
  blocked: []
4150
4163
  };
4164
+ var RETRY = "";
4151
4165
  var DrainController = class {
4152
4166
  _armed = false;
4153
4167
  _locked = false;
4154
4168
  _ratio = 0.5;
4155
4169
  /**
4156
- * Local wake at the earliest parked (backoff or defer) visit; the schedule
4157
- * itself is persisted. On an idle app nothing else would drain it. A due
4158
- * entry stays until handled or until a later pass claims nothing.
4170
+ * Local wake at the earliest parked (backoff or defer) visit, or at the
4171
+ * lease expiry after a failed pass (the {@link RETRY} key); the schedule
4172
+ * itself is persisted. On an idle app nothing else would drain it. It
4173
+ * drains until a pass leases nothing, and an entry stays until handled or
4174
+ * until a later pass claims nothing.
4159
4175
  */
4160
4176
  _defer = new DeferTimer(() => {
4161
4177
  this._armed = true;
4162
- void this._deps.run_scoped(() => this.drain());
4178
+ void this._deps.run_scoped(async () => {
4179
+ while ((await this.drain()).leased.length > 0) ;
4180
+ });
4163
4181
  });
4164
4182
  _worker;
4165
4183
  _misrouted = /* @__PURE__ */ new Set();
@@ -4256,10 +4274,12 @@ var DrainController = class {
4256
4274
  if (acked.length) this._deps.on_acked(acked);
4257
4275
  if (closeable.length) await this._deps.on_close(closeable);
4258
4276
  this._deps.breaker.passed();
4259
- const has_errors = handled.some(({ error }) => error);
4260
- if (!acked.length && !blocked.length && !has_errors) this._armed = false;
4261
4277
  return { fetched, leased, acked, blocked };
4262
4278
  } catch (error) {
4279
+ this._defer.set(
4280
+ RETRY,
4281
+ Date.now() + Math.max(leaseMillis, this._deps.breaker.cooldown_ms)
4282
+ );
4263
4283
  this._deps.breaker.failed(Date.now(), error);
4264
4284
  return EMPTY_DRAIN;
4265
4285
  } finally {
@@ -4383,7 +4403,8 @@ var SettleLoop = class {
4383
4403
  /**
4384
4404
  * Schedule a settle pass. Multiple calls inside the debounce window
4385
4405
  * coalesce into one cycle. The cycle runs correlate→drain in a loop
4386
- * until no progress is made (no new subscriptions, no acks, no blocks)
4406
+ * until no progress is made (no new subscriptions, no events read, no
4407
+ * streams leased)
4387
4408
  * or `maxPasses` is reached, then emits the `"settled"` lifecycle event
4388
4409
  * via {@link SettleDeps.on_settled}.
4389
4410
  */
@@ -4423,7 +4444,7 @@ var SettleLoop = class {
4423
4444
  acked: [...settled_drain.acked, ...drain.acked],
4424
4445
  blocked: [...settled_drain.blocked, ...drain.blocked]
4425
4446
  } : drain;
4426
- const made_progress = subscribed > 0 || drain.acked.length > 0 || drain.blocked.length > 0 || last_id > after_before;
4447
+ const made_progress = subscribed > 0 || drain.leased.length > 0 || last_id > after_before;
4427
4448
  if (!made_progress) break;
4428
4449
  }
4429
4450
  if (settled_drain) this._deps.on_settled(settled_drain);
@@ -4460,26 +4481,6 @@ var SettleLoop = class {
4460
4481
  };
4461
4482
 
4462
4483
  // src/builders/reaction-builder.ts
4463
- function finalize(lease, handled, at, error, options, logger, failed_at) {
4464
- if (!error) return { lease, handled, acked_at: at };
4465
- logger.error(error);
4466
- const non_retryable = error instanceof NonRetryableError;
4467
- const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
4468
- if (block2)
4469
- logger.error(
4470
- non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
4471
- );
4472
- const next_attempt_at = block2 ? void 0 : Date.now() + compute_backoff_delay(lease.retry, options.backoff ?? DEFAULT_BACKOFF);
4473
- return {
4474
- lease,
4475
- handled,
4476
- acked_at: at,
4477
- error: error.message,
4478
- block: block2,
4479
- next_attempt_at,
4480
- failed_at
4481
- };
4482
- }
4483
4484
  function build_handle(deps) {
4484
4485
  const { logger, reaction_scope } = deps;
4485
4486
  return async (lease, payloads) => {
@@ -4589,7 +4590,8 @@ var Act = class {
4589
4590
  _breaker;
4590
4591
  _listen;
4591
4592
  _drain;
4592
- _reactive_events;
4593
+ /** Whether any event has a reaction; nothing arms without one. */
4594
+ _reactive;
4593
4595
  _drain_controllers;
4594
4596
  _correlate;
4595
4597
  _settle;
@@ -4719,7 +4721,7 @@ var Act = class {
4719
4721
  });
4720
4722
  this._handle_batch = build_handle_batch(this._logger);
4721
4723
  const classification = classify_registry(this.registry, this._states);
4722
- this._reactive_events = classification.reactive_events;
4724
+ this._reactive = classification.event_to_lanes.size > 0;
4723
4725
  this._event_to_state = classification.event_to_state;
4724
4726
  this._event_to_lanes = classification.event_to_lanes;
4725
4727
  this._listen = options.listen !== false;
@@ -4826,9 +4828,8 @@ var Act = class {
4826
4828
  declared_lanes: new Set(this._declared_lanes),
4827
4829
  // Built after the controllers, so their keys are the active lanes.
4828
4830
  shard: this._drain_controllers.size < this._declared_lanes.length ? [...this._drain_controllers.keys()] : void 0,
4829
- on_init: () => {
4830
- if (this._drain && this._reactive_events.size > 0) this._arm_all();
4831
- },
4831
+ // Only reached on a draining app: writer-only ones never correlate.
4832
+ on_init: () => this._arm_all(),
4832
4833
  on_init_async: this._drain ? () => this._seed_persisted_defers() : void 0
4833
4834
  });
4834
4835
  }
@@ -4925,7 +4926,7 @@ var Act = class {
4925
4926
  }
4926
4927
  /** Failures are logged, never thrown: `notify` is a hint. */
4927
4928
  async _wire_notify(s) {
4928
- if (this._reactive_events.size === 0) return void 0;
4929
+ if (!this._reactive) return void 0;
4929
4930
  if (!s.notify) return void 0;
4930
4931
  if (!this._listen) return void 0;
4931
4932
  try {
@@ -4985,46 +4986,6 @@ var Act = class {
4985
4986
  * console.log(snapshots[0].state.count); // Current count after increment
4986
4987
  * ```
4987
4988
  *
4988
- * @example With error handling
4989
- * ```typescript
4990
- * try {
4991
- * await app.do(
4992
- * "withdraw",
4993
- * { stream: "account-123", actor: { id: "user1", name: "Alice" } },
4994
- * { amount: 1000 }
4995
- * );
4996
- * } catch (error) {
4997
- * if (error instanceof InvariantError) {
4998
- * console.error("Business rule violated:", error.description);
4999
- * } else if (error instanceof ConcurrencyError) {
5000
- * console.error("Concurrent modification detected, retry...");
5001
- * } else if (error instanceof ValidationError) {
5002
- * console.error("Invalid payload:", error.details);
5003
- * }
5004
- * }
5005
- * ```
5006
- *
5007
- * @example Reaction triggering another action (reactingTo auto-injected)
5008
- * ```typescript
5009
- * const app = act()
5010
- * .withState(Order)
5011
- * .withState(Inventory)
5012
- * .on("OrderPlaced")
5013
- * .do(async function reduceInventory(event, _stream, app) {
5014
- * // Inside reaction handlers, reactingTo is auto-injected when omitted.
5015
- * // The triggering event is used by default, maintaining the correlation chain.
5016
- * await app.do(
5017
- * "reduceStock",
5018
- * { stream: "inventory-1", actor: { id: "sys", name: "system" } },
5019
- * { amount: event.data.items.length }
5020
- * );
5021
- * // To use a different correlation, pass reactingTo explicitly:
5022
- * // await app.do("reduceStock", target, payload, { reactingTo: customEvent });
5023
- * })
5024
- * .to("inventory-1")
5025
- * .build();
5026
- * ```
5027
- *
5028
4989
  * @see {@link Target} for target structure
5029
4990
  * @see {@link Snapshot} for return value structure
5030
4991
  * @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
@@ -5040,10 +5001,9 @@ var Act = class {
5040
5001
  payload,
5041
5002
  do_options
5042
5003
  );
5043
- if (this._reactive_events.size > 0)
5044
- this._arm_for_event_names(
5045
- snapshots.map((s) => s.event.name)
5046
- );
5004
+ this._arm_for_event_names(
5005
+ snapshots.map((s) => s.event.name)
5006
+ );
5047
5007
  this.emit("committed", snapshots);
5048
5008
  return snapshots;
5049
5009
  });
@@ -5090,26 +5050,6 @@ var Act = class {
5090
5050
  * console.log(`Found ${count} events from ${first?.id} to ${last?.id}`);
5091
5051
  * ```
5092
5052
  *
5093
- * @example Query specific event types
5094
- * ```typescript
5095
- * const { count } = await app.query(
5096
- * { name: "UserCreated", limit: 100 },
5097
- * (event) => {
5098
- * console.log("User created:", event.data.email);
5099
- * }
5100
- * );
5101
- * ```
5102
- *
5103
- * @example Query events in time range
5104
- * ```typescript
5105
- * const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
5106
- * const { count } = await app.query({
5107
- * created_after: yesterday,
5108
- * stream: "user-123"
5109
- * });
5110
- * console.log(`User had ${count} events in last 24 hours`);
5111
- * ```
5112
- *
5113
5053
  * @see {@link query_array} for loading events into memory
5114
5054
  */
5115
5055
  async query(query, callback) {
@@ -5141,14 +5081,6 @@ var Act = class {
5141
5081
  * events.forEach(event => console.log(event.name, event.data));
5142
5082
  * ```
5143
5083
  *
5144
- * @example Get recent events
5145
- * ```typescript
5146
- * const recent = await app.query_array({
5147
- * stream: "user-123",
5148
- * limit: 10
5149
- * });
5150
- * ```
5151
- *
5152
5084
  * @see {@link query} for large result sets
5153
5085
  */
5154
5086
  async query_array(query) {
@@ -5219,12 +5151,6 @@ var Act = class {
5219
5151
  * await app.drain();
5220
5152
  * ```
5221
5153
  *
5222
- * @example In production, prefer settle()
5223
- * ```typescript
5224
- * await app.do("CreateItem", target, input);
5225
- * app.settle(); // debounced correlate→drain, emits "settled"
5226
- * ```
5227
- *
5228
5154
  * @see {@link settle} for debounced correlate→drain with lifecycle events
5229
5155
  * @see {@link correlate} for dynamic stream discovery
5230
5156
  * @see {@link start_correlations} for automatic correlation
@@ -5236,6 +5162,7 @@ var Act = class {
5236
5162
  return this._scoped(() => this._drain_all(options));
5237
5163
  }
5238
5164
  _arm_all() {
5165
+ if (!this._reactive) return;
5239
5166
  this._correlate.arm();
5240
5167
  for (const c of this._drain_controllers.values()) c.arm();
5241
5168
  }
@@ -5291,37 +5218,20 @@ var Act = class {
5291
5218
  * on reaction resolvers. It then registers these streams so they'll be picked up by
5292
5219
  * the next drain cycle.
5293
5220
  *
5294
- * @param query - Query filter to scan for new correlations
5295
5221
  * @param query - Scan filter — see {@link Query} for fields (typically
5296
5222
  * `{ after: <event-id>, limit: <count> }`)
5297
- * @returns Object with newly leased streams and last scanned event ID
5223
+ * @returns The number of newly subscribed streams and the last scanned event id
5298
5224
  *
5299
5225
  * @example Manual correlation
5300
5226
  * ```typescript
5301
5227
  * // Scan for new streams
5302
- * const { leased, last_id } = await app.correlate({ after: 0, limit: 100 });
5303
- * console.log(`Found ${leased.length} new streams`);
5228
+ * const { subscribed, last_id } = await app.correlate({ after: 0, limit: 100 });
5229
+ * console.log(`Subscribed ${subscribed} new streams`);
5304
5230
  *
5305
5231
  * // Save last_id for next scan
5306
5232
  * await saveCheckpoint(last_id);
5307
5233
  * ```
5308
5234
  *
5309
- * @example Dynamic stream creation
5310
- * ```typescript
5311
- * const app = act()
5312
- * .withState(User)
5313
- * .withState(UserStats)
5314
- * .on("UserLoggedIn")
5315
- * .do(async (event) => ["incrementLoginCount", {}])
5316
- * .to((event) => ({
5317
- * target: `stats-${event.stream}` // Dynamic target per user
5318
- * }))
5319
- * .build();
5320
- *
5321
- * // Discover stats streams as users log in
5322
- * await app.correlate();
5323
- * ```
5324
- *
5325
5235
  * @see {@link start_correlations} for automatic periodic correlation
5326
5236
  * @see {@link stop_correlations} to stop automatic correlation
5327
5237
  */
@@ -5334,8 +5244,7 @@ var Act = class {
5334
5244
  if (!this._drain) return { subscribed: 0, last_id: -1, scanned: false };
5335
5245
  return this._scoped(async () => {
5336
5246
  const { subscribed, last_id, marked, scanned } = await this._correlate.correlate(query, lease);
5337
- if ((subscribed > 0 || marked > 0) && this._reactive_events.size > 0)
5338
- this._arm_all();
5247
+ if (subscribed > 0 || marked > 0) this._arm_all();
5339
5248
  if (subscribed > 0) this._on_discovered?.(subscribed);
5340
5249
  return { subscribed, last_id, scanned };
5341
5250
  });
@@ -5427,12 +5336,6 @@ var Act = class {
5427
5336
  * app.settle({ eventLimit: 1000 }); // emits "settled" when fully replayed
5428
5337
  * ```
5429
5338
  *
5430
- * @example Rebuild a projection (tests / scripts)
5431
- * ```typescript
5432
- * await app.reset(["my-projection"]);
5433
- * await app.drain({ eventLimit: 1000 }); // small streams: one pass is enough
5434
- * ```
5435
- *
5436
5339
  * @see {@link Store.reset} for the underlying store primitive
5437
5340
  * @see {@link settle} for the debounced full-catch-up loop
5438
5341
  */
@@ -5441,7 +5344,7 @@ var Act = class {
5441
5344
  const count = await store2().reset(input);
5442
5345
  for (const handler of this._batch_handlers.values())
5443
5346
  handler[FOLD_RESET]?.();
5444
- if (count > 0 && this._reactive_events.size > 0) this._arm_all();
5347
+ if (count > 0) this._arm_all();
5445
5348
  return count;
5446
5349
  });
5447
5350
  }
@@ -5475,7 +5378,7 @@ var Act = class {
5475
5378
  async unblock(input) {
5476
5379
  return this._scoped(async () => {
5477
5380
  const count = await store2().unblock(input);
5478
- if (count > 0 && this._reactive_events.size > 0) this._arm_all();
5381
+ if (count > 0) this._arm_all();
5479
5382
  return count;
5480
5383
  });
5481
5384
  }
@@ -5511,11 +5414,6 @@ var Act = class {
5511
5414
  * await cache().clear(); // operator's responsibility
5512
5415
  * ```
5513
5416
  *
5514
- * @example Copy one store into another
5515
- * ```typescript
5516
- * await app.restore(pg, {}, sqlite);
5517
- * ```
5518
- *
5519
5417
  * @see {@link Store.restore} for the underlying driver-pattern primitive.
5520
5418
  */
5521
5419
  async restore(source, opts = {}, sink) {
@@ -5654,16 +5552,6 @@ var Act = class {
5654
5552
  * await app.prioritize({ stream: "^proj-orders$", stream_exact: false }, 10);
5655
5553
  * ```
5656
5554
  *
5657
- * @example Drop all audit projections to background
5658
- * ```typescript
5659
- * await app.prioritize({ source: "^audit-" }, -5);
5660
- * ```
5661
- *
5662
- * @example Reset everyone to default
5663
- * ```typescript
5664
- * await app.prioritize({}, 0);
5665
- * ```
5666
- *
5667
5555
  * @see {@link Store.prioritize} for the underlying primitive
5668
5556
  * @see {@link claim} for how priority biases scheduling
5669
5557
  */
@@ -5674,7 +5562,7 @@ var Act = class {
5674
5562
  async _run_close(targets) {
5675
5563
  const close_actor = { id: "$close", name: "close" };
5676
5564
  const result = await run_close_cycle(targets, {
5677
- reactive_events_size: this._reactive_events.size,
5565
+ reactive: this._reactive,
5678
5566
  catch_up_correlation: (until) => catch_up_correlation(
5679
5567
  this._correlate,
5680
5568
  (limit) => this.correlate({ limit }),
@@ -5727,27 +5615,6 @@ var Act = class {
5727
5615
  * { stream: "order-456" },
5728
5616
  * ]);
5729
5617
  * ```
5730
- *
5731
- * @example Close with restart (state loaded automatically after guard)
5732
- * ```typescript
5733
- * await app.close([
5734
- * { stream: "counter-1", restart: true },
5735
- * { stream: "counter-2" }, // tombstoned
5736
- * ]);
5737
- * ```
5738
- *
5739
- * @example Windowed close — keep the last 180 days of real events
5740
- * ```typescript
5741
- * const cutoff = new Date();
5742
- * cutoff.setDate(cutoff.getDate() - 180);
5743
- * await app.close([
5744
- * {
5745
- * stream: "ledger-acme",
5746
- * before: cutoff,
5747
- * archive: async () => { await archiveToS3("ledger-acme", cutoff); },
5748
- * },
5749
- * ]);
5750
- * ```
5751
5618
  */
5752
5619
  async close(targets) {
5753
5620
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
@@ -5759,7 +5626,8 @@ var Act = class {
5759
5626
  * Call this after `app.do()` (or `app.reset()`) to schedule a background
5760
5627
  * drain. Multiple rapid calls within the debounce window are coalesced
5761
5628
  * into a single cycle. Runs correlate→drain in a loop until a pass makes
5762
- * no progress — no new subscriptions, no acks, no blocks — then emits
5629
+ * no progress — no new subscriptions, no events read, no streams leased —
5630
+ * then emits
5763
5631
  * the `"settled"` lifecycle event. This means a single `settle()` call
5764
5632
  * fully catches up paginated streams (e.g. after `reset()` on a long
5765
5633
  * projection) without forcing callers to loop.