@rotorsoft/act 1.10.1 → 1.10.2
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 +10 -7
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/builders/act-builder.d.ts.map +1 -1
- package/dist/@types/builders/slice-builder.d.ts +1 -1
- package/dist/@types/builders/state-builder.d.ts.map +1 -1
- package/dist/@types/internal/event-sourcing.d.ts +7 -7
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +1 -1
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/reactions.d.ts.map +1 -1
- package/dist/@types/internal/sensitive.d.ts +6 -23
- package/dist/@types/internal/sensitive.d.ts.map +1 -1
- package/dist/@types/internal/tracing.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +78 -16
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/registry.d.ts +12 -0
- package/dist/@types/types/registry.d.ts.map +1 -1
- package/dist/{chunk-3Z2HU726.js → chunk-5PWLGL5E.js} +2 -2
- package/dist/{chunk-BY5JPOZR.js → chunk-NGVWJXEF.js} +9 -24
- package/dist/{chunk-BY5JPOZR.js.map → chunk-NGVWJXEF.js.map} +1 -1
- package/dist/index.cjs +91 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +85 -98
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +2 -2
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +4 -4
- package/dist/test/index.js.map +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +1 -1
- package/package.json +2 -2
- /package/dist/{chunk-3Z2HU726.js.map → chunk-5PWLGL5E.js.map} +0 -0
|
@@ -1 +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} 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 * @example Logging validation details\n * ```typescript\n * try {\n * await app.do(\"updateProfile\", target, payload);\n * } catch (error) {\n * if (error instanceof ValidationError) {\n * error.details.errors.forEach((err) => {\n * console.error(`Field ${err.path.join(\".\")}: ${err.message}`);\n * });\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 * @example User-friendly error messages\n * ```typescript\n * try {\n * await app.do(\"closeTicket\", target, payload);\n * } catch (error) {\n * if (error instanceof InvariantError) {\n * // Present friendly message to user\n * if (error.description === \"Ticket must be open\") {\n * return { error: \"This ticket is already closed\" };\n * } else if (error.description === \"Not authorized\") {\n * return { error: \"You don't have permission to close this ticket\" };\n * }\n * }\n * }\n * ```\n *\n * @example Logging with context\n * ```typescript\n * try {\n * await app.do(\"transfer\", target, { to: \"account2\", amount: 500 });\n * } catch (error) {\n * if (error instanceof InvariantError) {\n * logger.error({\n * action: error.action,\n * stream: error.target.stream,\n * actor: error.target.actor,\n * reason: error.description,\n * balance: error.snapshot.state.balance,\n * attempted: error.payload.amount\n * }, \"Invariant violation\");\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 * @example Logging concurrency conflicts\n * ```typescript\n * try {\n * await app.do(\"updateInventory\", target, payload);\n * } catch (error) {\n * if (error instanceof ConcurrencyError) {\n * logger.warn({\n * stream: error.stream,\n * expectedVersion: error.expectedVersion,\n * actualVersion: error.lastVersion,\n * events: error.events.map(e => e.name)\n * }, \"Concurrent modification detected\");\n * }\n * }\n * ```\n *\n * @example User feedback for conflicts\n * ```typescript\n * try {\n * await app.do(\"editDocument\", target, { content: newContent });\n * } catch (error) {\n * if (error instanceof ConcurrencyError) {\n * return {\n * error: \"This document was modified by another user. Please refresh and try again.\",\n * code: \"CONCURRENT_MODIFICATION\"\n * };\n * }\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 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 (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 *\n * @example Marking validation failures as non-retryable\n * ```typescript\n * .on(\"PaymentReceived\")\n * .do(async (event) => {\n * const parsed = Schema.safeParse(event.data);\n * if (!parsed.success) {\n * throw new NonRetryableError(\"payment payload failed validation\", {\n * cause: parsed.error,\n * });\n * }\n * // ... handle parsed payload\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 { _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 * 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 * Part of the sensitive-data foundation (#855 / epic #566).\n */\nexport { 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 (#855 / epic #566).\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 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 stream_exact: z.boolean().optional(),\n })\n .readonly();\n","/**\n * @module sensitive\n * @category Internal\n *\n * Internal mechanics for the sensitive-data foundation (#855 / epic #566).\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_merge(event, fields)` — produce the reducer view (pii merged\n * into data; `[SHREDDED]` if pii column is null).\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 * 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 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 the list of sensitive field names from an event's Zod schema.\n *\n * Walks the top-level shape of a `z.object({...})` and returns the keys whose\n * schema (after unwrapping optional/nullable/default wrappers) was marked via\n * `sensitive(...)`. Returns an empty array 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 * @internal — consumed by the registry's `sensitive_fields(event_name)` lookup.\n */\nexport function pii_fields(schema: z.ZodType): readonly string[] {\n const shape = (schema as { shape?: Record<string, z.ZodType> }).shape;\n if (!shape || typeof shape !== \"object\") return [];\n const fields: string[] = [];\n for (const key of Object.keys(shape)) {\n if (is_pii(shape[key])) fields.push(key);\n }\n return fields;\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(\n emitted: { name: unknown; data: unknown },\n fields: readonly string[]\n): { name: unknown; data: unknown; pii: Record<string, unknown> } {\n const rec = emitted.data as Record<string, unknown>;\n const clean: Record<string, unknown> = {};\n const pii: Record<string, unknown> = {};\n for (const k of Object.keys(rec)) {\n if (fields.includes(k)) pii[k] = rec[k];\n else clean[k] = rec[k];\n }\n return { name: emitted.name, data: clean, pii };\n}\n\n/**\n * Build the **reducer view** of a committed event — sensitive fields merged\n * back into `data` so per-state reducers always see plaintext.\n *\n * - Event with no `pii` payload AND no schema-declared sensitive fields →\n * return the event unchanged (zero-cost path).\n * - Event with a `pii` payload → merge into `data` (plaintext for the reducer).\n * - Event whose schema *declares* sensitive fields but `pii` is null/undefined\n * (post-`forget_pii`) → substitute {@link SHREDDED} for each declared field.\n *\n * Used inside `load()` before invoking the reducer chain. Reducer-visible PII\n * is by design — the reducer is the source of truth for derived state. The\n * external view returned to callers is separately gated by {@link pii_gate}.\n *\n * @internal\n */\nexport function pii_merge<\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. Callers (the State's `_pii_merge`\n // decorator) filter on `fields_by_event.get(name)` before invocation, so the\n // empty-fields short-circuit lives at the caller, not here.\n const data = event.data as Record<string, unknown>;\n const pii = event.pii;\n if (pii != null) {\n return {\n ...event,\n data: { ...data, ...pii } as Committed<TEvents, TKey>[\"data\"],\n };\n }\n // Schema declared sensitive fields but the pii payload is gone — shredded.\n const shredded: Record<string, unknown> = { ...data };\n for (const f of fields) shredded[f] = SHREDDED;\n return {\n ...event,\n data: shredded as Committed<TEvents, TKey>[\"data\"],\n };\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 * - Event with no schema-declared sensitive fields → returned unchanged\n * (zero-cost path).\n * - Event whose `pii` payload is null/undefined AND schema declares sensitive\n * fields → substitute {@link SHREDDED} for each declared field. Irrecoverable,\n * 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 */\nexport function pii_gate<\n TEvents extends Schemas,\n TKey extends keyof TEvents & string,\n>(\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 // Contract: `fields` is non-empty. The State's `_pii_gate` decorator\n // filters on `fields_by_event.get(name)` before invocation.\n const data = event.data as Record<string, unknown>;\n if (event.pii == null) {\n const shredded: Record<string, unknown> = { ...data };\n for (const f of fields) shredded[f] = SHREDDED;\n return {\n ...event,\n data: shredded as Committed<TEvents, TKey>[\"data\"],\n };\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 ...event,\n data: {\n ...data,\n ...(event.pii as Record<string, unknown>),\n } 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 {\n ...event,\n data: redacted as Committed<TEvents, TKey>[\"data\"],\n };\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"]}
|
|
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} 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 * @example Logging validation details\n * ```typescript\n * try {\n * await app.do(\"updateProfile\", target, payload);\n * } catch (error) {\n * if (error instanceof ValidationError) {\n * error.details.errors.forEach((err) => {\n * console.error(`Field ${err.path.join(\".\")}: ${err.message}`);\n * });\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 * @example User-friendly error messages\n * ```typescript\n * try {\n * await app.do(\"closeTicket\", target, payload);\n * } catch (error) {\n * if (error instanceof InvariantError) {\n * // Present friendly message to user\n * if (error.description === \"Ticket must be open\") {\n * return { error: \"This ticket is already closed\" };\n * } else if (error.description === \"Not authorized\") {\n * return { error: \"You don't have permission to close this ticket\" };\n * }\n * }\n * }\n * ```\n *\n * @example Logging with context\n * ```typescript\n * try {\n * await app.do(\"transfer\", target, { to: \"account2\", amount: 500 });\n * } catch (error) {\n * if (error instanceof InvariantError) {\n * logger.error({\n * action: error.action,\n * stream: error.target.stream,\n * actor: error.target.actor,\n * reason: error.description,\n * balance: error.snapshot.state.balance,\n * attempted: error.payload.amount\n * }, \"Invariant violation\");\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 * @example Logging concurrency conflicts\n * ```typescript\n * try {\n * await app.do(\"updateInventory\", target, payload);\n * } catch (error) {\n * if (error instanceof ConcurrencyError) {\n * logger.warn({\n * stream: error.stream,\n * expectedVersion: error.expectedVersion,\n * actualVersion: error.lastVersion,\n * events: error.events.map(e => e.name)\n * }, \"Concurrent modification detected\");\n * }\n * }\n * ```\n *\n * @example User feedback for conflicts\n * ```typescript\n * try {\n * await app.do(\"editDocument\", target, { content: newContent });\n * } catch (error) {\n * if (error instanceof ConcurrencyError) {\n * return {\n * error: \"This document was modified by another user. Please refresh and try again.\",\n * code: \"CONCURRENT_MODIFICATION\"\n * };\n * }\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 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 (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 *\n * @example Marking validation failures as non-retryable\n * ```typescript\n * .on(\"PaymentReceived\")\n * .do(async (event) => {\n * const parsed = Schema.safeParse(event.data);\n * if (!parsed.success) {\n * throw new NonRetryableError(\"payment payload failed validation\", {\n * cause: parsed.error,\n * });\n * }\n * // ... handle parsed payload\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 { _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 * 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 * Part of the sensitive-data foundation (#855 / epic #566).\n */\nexport { 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 (#855 / epic #566).\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 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 stream_exact: z.boolean().optional(),\n })\n .readonly();\n","/**\n * @module sensitive\n * @category Internal\n *\n * Internal mechanics for the sensitive-data foundation (#855 / epic #566).\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_merge(event, fields)` — produce the reducer view (pii merged\n * into data; `[SHREDDED]` if pii column is null).\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 * 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 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 the list of sensitive field names from an event's Zod schema.\n *\n * Walks the top-level shape of a `z.object({...})` and returns the keys whose\n * schema (after unwrapping optional/nullable/default wrappers) was marked via\n * `sensitive(...)`. Returns an empty array 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 * @internal — consumed by the registry's `sensitive_fields(event_name)` lookup.\n */\nexport function pii_fields(schema: z.ZodType): readonly string[] {\n const shape = (schema as { shape?: Record<string, z.ZodType> }).shape;\n if (!shape || typeof shape !== \"object\") return [];\n const fields: string[] = [];\n for (const key of Object.keys(shape)) {\n if (is_pii(shape[key])) fields.push(key);\n }\n return fields;\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 * - Event with no schema-declared sensitive fields → returned unchanged\n * (zero-cost path).\n * - Event whose `pii` payload is null/undefined AND schema declares sensitive\n * fields → substitute {@link SHREDDED} for each declared field. Irrecoverable,\n * 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 */\nexport function 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 if (fields.length === 0) return event;\n const data = event.data as Record<string, unknown>;\n if (event.pii == null) {\n const shredded: Record<string, unknown> = { ...data };\n for (const f of fields) shredded[f] = SHREDDED;\n return {\n ...event,\n data: shredded as Committed<TEvents, TKey>[\"data\"],\n };\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 ...event,\n data: {\n ...data,\n ...(event.pii as Record<string, unknown>),\n } 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 {\n ...event,\n data: redacted as Committed<TEvents, TKey>[\"data\"],\n };\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
|
@@ -416,32 +416,18 @@ function pii_fields(schema) {
|
|
|
416
416
|
return fields;
|
|
417
417
|
}
|
|
418
418
|
function pii_split(emitted, fields) {
|
|
419
|
-
const
|
|
420
|
-
const clean = {};
|
|
419
|
+
const data = { ...emitted.data };
|
|
421
420
|
const pii = {};
|
|
422
|
-
for (const
|
|
423
|
-
if (
|
|
424
|
-
|
|
421
|
+
for (const f of fields) {
|
|
422
|
+
if (f in data) {
|
|
423
|
+
pii[f] = data[f];
|
|
424
|
+
delete data[f];
|
|
425
|
+
}
|
|
425
426
|
}
|
|
426
|
-
return { name: emitted.name, data
|
|
427
|
-
}
|
|
428
|
-
function pii_merge(event, fields) {
|
|
429
|
-
const data = event.data;
|
|
430
|
-
const pii = event.pii;
|
|
431
|
-
if (pii != null) {
|
|
432
|
-
return {
|
|
433
|
-
...event,
|
|
434
|
-
data: { ...data, ...pii }
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
const shredded = { ...data };
|
|
438
|
-
for (const f of fields) shredded[f] = SHREDDED;
|
|
439
|
-
return {
|
|
440
|
-
...event,
|
|
441
|
-
data: shredded
|
|
442
|
-
};
|
|
427
|
+
return { name: emitted.name, data, pii };
|
|
443
428
|
}
|
|
444
429
|
function pii_gate(event, fields, predicate, actor) {
|
|
430
|
+
if (fields.length === 0) return event;
|
|
445
431
|
const data = event.data;
|
|
446
432
|
if (event.pii == null) {
|
|
447
433
|
const shredded = { ...data };
|
|
@@ -2020,7 +2006,7 @@ async function load_restart_seeds(guarded, target_map, stream_info, event_to_sta
|
|
|
2020
2006
|
);
|
|
2021
2007
|
return;
|
|
2022
2008
|
}
|
|
2023
|
-
const snap2 = await load2(owner_state, stream);
|
|
2009
|
+
const snap2 = await load2(owner_state, { stream });
|
|
2024
2010
|
seed_states.set(stream, snap2.state);
|
|
2025
2011
|
})
|
|
2026
2012
|
);
|
|
@@ -2384,7 +2370,6 @@ async function scan(source, opts = {}, callback) {
|
|
|
2384
2370
|
migrated = {
|
|
2385
2371
|
...event,
|
|
2386
2372
|
name: migration.to,
|
|
2387
|
-
// biome-ignore lint/suspicious/noExplicitAny: migration target shape is caller-defined
|
|
2388
2373
|
data: new_data
|
|
2389
2374
|
};
|
|
2390
2375
|
migrated_count++;
|
|
@@ -2433,39 +2418,37 @@ async function scan(source, opts = {}, callback) {
|
|
|
2433
2418
|
}
|
|
2434
2419
|
};
|
|
2435
2420
|
}
|
|
2436
|
-
async function load(me,
|
|
2421
|
+
async function load(me, target, callback) {
|
|
2422
|
+
const { stream, actor, asOf } = target;
|
|
2437
2423
|
const time_travel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
2438
2424
|
const cached = time_travel ? void 0 : await cache2().get(stream);
|
|
2439
2425
|
const cache_hit = !!cached;
|
|
2440
2426
|
let state2 = cached?.state ?? (me.init ? me.init() : {});
|
|
2441
2427
|
let patches = cached?.patches ?? 0;
|
|
2442
2428
|
let snaps = cached?.snaps ?? 0;
|
|
2443
|
-
let version = cached?.version ?? -1;
|
|
2444
2429
|
let replayed = 0;
|
|
2445
2430
|
let event;
|
|
2446
2431
|
await store2().query(
|
|
2447
|
-
(
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
if (e.name === SNAP_EVENT) {
|
|
2452
|
-
state2 = e.data;
|
|
2432
|
+
(raw) => {
|
|
2433
|
+
event = me.view(raw, actor);
|
|
2434
|
+
if (event.name === SNAP_EVENT) {
|
|
2435
|
+
state2 = event.data;
|
|
2453
2436
|
snaps++;
|
|
2454
2437
|
patches = 0;
|
|
2455
2438
|
replayed++;
|
|
2456
|
-
} else if (me.patch[
|
|
2457
|
-
state2 = (0, import_act_patch.patch)(state2, me.patch[
|
|
2439
|
+
} else if (me.patch[event.name]) {
|
|
2440
|
+
state2 = (0, import_act_patch.patch)(state2, me.patch[event.name](event, state2));
|
|
2458
2441
|
patches++;
|
|
2459
2442
|
replayed++;
|
|
2460
|
-
} else if (
|
|
2443
|
+
} else if (event.name !== TOMBSTONE_EVENT) {
|
|
2461
2444
|
log().warn(
|
|
2462
|
-
`Skipping unknown event "${String(
|
|
2445
|
+
`Skipping unknown event "${String(event.name)}" on stream "${stream}" (id=${event.id}) \u2014 no reducer in state "${me.name}"`
|
|
2463
2446
|
);
|
|
2464
2447
|
}
|
|
2465
2448
|
callback?.({
|
|
2466
2449
|
event,
|
|
2467
2450
|
state: state2,
|
|
2468
|
-
version,
|
|
2451
|
+
version: event.version,
|
|
2469
2452
|
patches,
|
|
2470
2453
|
snaps,
|
|
2471
2454
|
cache_hit,
|
|
@@ -2478,39 +2461,43 @@ async function load(me, stream, callback, asOf, actor) {
|
|
|
2478
2461
|
...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
|
|
2479
2462
|
}
|
|
2480
2463
|
);
|
|
2481
|
-
if (replayed > 0 && !time_travel && event) {
|
|
2464
|
+
if (replayed > 0 && !time_travel && event && !me.pii_aware) {
|
|
2482
2465
|
await cache2().set(stream, {
|
|
2483
2466
|
state: state2,
|
|
2484
|
-
version,
|
|
2467
|
+
version: event.version,
|
|
2485
2468
|
event_id: event.id,
|
|
2486
2469
|
patches,
|
|
2487
2470
|
snaps
|
|
2488
2471
|
});
|
|
2489
2472
|
}
|
|
2490
|
-
return {
|
|
2473
|
+
return {
|
|
2474
|
+
event,
|
|
2475
|
+
state: state2,
|
|
2476
|
+
version: event?.version ?? cached?.version ?? -1,
|
|
2477
|
+
patches,
|
|
2478
|
+
snaps,
|
|
2479
|
+
cache_hit,
|
|
2480
|
+
replayed
|
|
2481
|
+
};
|
|
2491
2482
|
}
|
|
2492
|
-
async function action(me, action2, target, payload,
|
|
2483
|
+
async function action(me, action2, target, payload, options) {
|
|
2493
2484
|
const { stream, expectedVersion, actor } = target;
|
|
2494
2485
|
if (!stream) throw new Error("Missing target stream");
|
|
2495
|
-
const
|
|
2486
|
+
const reactingTo = options?.reactingTo;
|
|
2487
|
+
const correlator = options?.correlator ?? default_correlator;
|
|
2488
|
+
const validated = validate(action2, payload, me.actions[action2]);
|
|
2496
2489
|
const opts = me.options?.[action2];
|
|
2497
2490
|
const max_retries = opts?.maxRetries ?? 0;
|
|
2498
2491
|
for (let attempt = 0; ; attempt++) {
|
|
2499
2492
|
try {
|
|
2500
|
-
const snapshot = await load(
|
|
2501
|
-
me,
|
|
2502
|
-
stream,
|
|
2503
|
-
void 0,
|
|
2504
|
-
void 0,
|
|
2505
|
-
target.actor
|
|
2506
|
-
);
|
|
2493
|
+
const snapshot = await load(me, { stream, actor: target.actor });
|
|
2507
2494
|
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
2508
2495
|
throw new StreamClosedError(stream);
|
|
2509
2496
|
const expected = expectedVersion ?? snapshot.event?.version;
|
|
2510
2497
|
if (me.given) {
|
|
2511
2498
|
const invariants = me.given[action2] || [];
|
|
2512
|
-
invariants.forEach(({ valid, description }) => {
|
|
2513
|
-
if (!
|
|
2499
|
+
invariants.forEach(({ valid: valid2, description }) => {
|
|
2500
|
+
if (!valid2(snapshot.state, actor))
|
|
2514
2501
|
throw new InvariantError(
|
|
2515
2502
|
action2,
|
|
2516
2503
|
validated,
|
|
@@ -2526,24 +2513,11 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
2526
2513
|
return [snapshot];
|
|
2527
2514
|
}
|
|
2528
2515
|
const tuples = Array.isArray(result[0]) ? result : [result];
|
|
2529
|
-
const
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
const evt = name;
|
|
2535
|
-
if (deprecated.has(evt) && !warned.has(evt)) {
|
|
2536
|
-
warned.add(evt);
|
|
2537
|
-
log().warn(
|
|
2538
|
-
`Action "${String(action2)}" emitted deprecated event "${evt}". A newer version exists in the registry \u2014 update the action's .emit() to target the current version. (warned once per process)`
|
|
2539
|
-
);
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
const emitted = tuples.map(([name, data]) => {
|
|
2544
|
-
const validated2 = skipValidation ? data : validate(name, data, me.events[name]);
|
|
2545
|
-
return me.message({ name, data: validated2 });
|
|
2546
|
-
});
|
|
2516
|
+
const valid = tuples.map(([name, data]) => ({
|
|
2517
|
+
name,
|
|
2518
|
+
data: validate(name, data, me.events[name])
|
|
2519
|
+
}));
|
|
2520
|
+
const emitted = valid.map((e) => me.message(e));
|
|
2547
2521
|
const meta = {
|
|
2548
2522
|
correlation: reactingTo?.meta.correlation || correlator({
|
|
2549
2523
|
action: action2,
|
|
@@ -2584,12 +2558,13 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
2584
2558
|
throw error;
|
|
2585
2559
|
}
|
|
2586
2560
|
let { state: state2, patches } = snapshot;
|
|
2587
|
-
const snapshots = committed.map((
|
|
2561
|
+
const snapshots = committed.map((row, i) => {
|
|
2562
|
+
const event = { ...row, data: valid[i].data };
|
|
2588
2563
|
const p = me.patch[event.name](event, state2);
|
|
2589
2564
|
state2 = (0, import_act_patch.patch)(state2, p);
|
|
2590
2565
|
patches++;
|
|
2591
2566
|
return {
|
|
2592
|
-
event
|
|
2567
|
+
event,
|
|
2593
2568
|
state: state2,
|
|
2594
2569
|
version: event.version,
|
|
2595
2570
|
patches,
|
|
@@ -2601,13 +2576,15 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
2601
2576
|
});
|
|
2602
2577
|
const last = snapshots.at(-1);
|
|
2603
2578
|
const snapped = me.snap?.(last);
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2579
|
+
if (!me.pii_aware) {
|
|
2580
|
+
cache2().set(stream, {
|
|
2581
|
+
state: last.state,
|
|
2582
|
+
version: last.event.version,
|
|
2583
|
+
event_id: last.event.id,
|
|
2584
|
+
patches: snapped ? 0 : last.patches,
|
|
2585
|
+
snaps: snapped ? last.snaps + 1 : last.snaps
|
|
2586
|
+
}).catch((err) => log().error(err));
|
|
2587
|
+
}
|
|
2611
2588
|
if (snapped) void snap(last);
|
|
2612
2589
|
return snapshots;
|
|
2613
2590
|
} catch (error) {
|
|
@@ -2674,15 +2651,7 @@ var traced = (inner, exit, entry) => (async (...args) => {
|
|
|
2674
2651
|
return result;
|
|
2675
2652
|
});
|
|
2676
2653
|
function build_es(logger, correlator = default_correlator) {
|
|
2677
|
-
const bound_action = (me, action_name, target, payload,
|
|
2678
|
-
me,
|
|
2679
|
-
action_name,
|
|
2680
|
-
target,
|
|
2681
|
-
payload,
|
|
2682
|
-
reactingTo,
|
|
2683
|
-
skipValidation,
|
|
2684
|
-
correlator
|
|
2685
|
-
);
|
|
2654
|
+
const bound_action = (me, action_name, target, payload, options) => action(me, action_name, target, payload, { correlator, ...options });
|
|
2686
2655
|
if (logger.level !== "trace") {
|
|
2687
2656
|
return {
|
|
2688
2657
|
snap,
|
|
@@ -2701,7 +2670,7 @@ function build_es(logger, correlator = default_correlator) {
|
|
|
2701
2670
|
)
|
|
2702
2671
|
);
|
|
2703
2672
|
}),
|
|
2704
|
-
load: traced(load, (result, _me,
|
|
2673
|
+
load: traced(load, (result, _me, target) => {
|
|
2705
2674
|
const stats = stats_marker(
|
|
2706
2675
|
result.version,
|
|
2707
2676
|
result.replayed,
|
|
@@ -2712,7 +2681,7 @@ function build_es(logger, correlator = default_correlator) {
|
|
|
2712
2681
|
es_caption(
|
|
2713
2682
|
"load",
|
|
2714
2683
|
C_GREEN,
|
|
2715
|
-
`${stream}${as_of_marker(asOf)} ${cache_marker(result.cache_hit)} ${stats}`
|
|
2684
|
+
`${target.stream}${as_of_marker(target.asOf)} ${cache_marker(result.cache_hit)} ${stats}`
|
|
2716
2685
|
)
|
|
2717
2686
|
);
|
|
2718
2687
|
}),
|
|
@@ -3200,13 +3169,10 @@ function build_handle(deps) {
|
|
|
3200
3169
|
};
|
|
3201
3170
|
for (const payload of payloads) {
|
|
3202
3171
|
const { event, handler } = payload;
|
|
3203
|
-
scoped_app.do = (action2, target, action_payload,
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
reactingTo ?? event,
|
|
3208
|
-
skipValidation
|
|
3209
|
-
);
|
|
3172
|
+
scoped_app.do = (action2, target, action_payload, options) => bound_do(action2, target, action_payload, {
|
|
3173
|
+
...options,
|
|
3174
|
+
reactingTo: options?.reactingTo ?? event
|
|
3175
|
+
});
|
|
3210
3176
|
try {
|
|
3211
3177
|
await handler(event, stream, scoped_app);
|
|
3212
3178
|
at = event.id;
|
|
@@ -3612,8 +3578,9 @@ var Act = class {
|
|
|
3612
3578
|
* @param action - The name of the action to execute
|
|
3613
3579
|
* @param target - Target specification with stream ID and actor context
|
|
3614
3580
|
* @param payload - Action payload matching the action's schema
|
|
3615
|
-
* @param
|
|
3616
|
-
*
|
|
3581
|
+
* @param options - Per-call dispatch options ({@link DoOptions}) —
|
|
3582
|
+
* `reactingTo` to thread correlation, `correlator` to override the
|
|
3583
|
+
* framework or orchestrator-level correlator for this call only.
|
|
3617
3584
|
* @returns Array of snapshots for all affected states (usually one)
|
|
3618
3585
|
*
|
|
3619
3586
|
* @throws {ValidationError} If payload doesn't match action schema
|
|
@@ -3668,7 +3635,7 @@ var Act = class {
|
|
|
3668
3635
|
* { amount: event.data.items.length }
|
|
3669
3636
|
* );
|
|
3670
3637
|
* // To use a different correlation, pass reactingTo explicitly:
|
|
3671
|
-
* // await app.do("reduceStock", target, payload, customEvent);
|
|
3638
|
+
* // await app.do("reduceStock", target, payload, { reactingTo: customEvent });
|
|
3672
3639
|
* })
|
|
3673
3640
|
* .to("inventory-1")
|
|
3674
3641
|
* .build();
|
|
@@ -3678,15 +3645,14 @@ var Act = class {
|
|
|
3678
3645
|
* @see {@link Snapshot} for return value structure
|
|
3679
3646
|
* @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
|
|
3680
3647
|
*/
|
|
3681
|
-
async do(action2, target, payload,
|
|
3648
|
+
async do(action2, target, payload, options) {
|
|
3682
3649
|
return this._scoped(async () => {
|
|
3683
3650
|
const snapshots = await this._es.action(
|
|
3684
3651
|
this.registry.actions[action2],
|
|
3685
3652
|
action2,
|
|
3686
3653
|
target,
|
|
3687
3654
|
payload,
|
|
3688
|
-
|
|
3689
|
-
skipValidation
|
|
3655
|
+
options
|
|
3690
3656
|
);
|
|
3691
3657
|
if (this._reactive_events.size > 0)
|
|
3692
3658
|
this._arm_for_event_names(
|
|
@@ -3696,7 +3662,7 @@ var Act = class {
|
|
|
3696
3662
|
return snapshots;
|
|
3697
3663
|
});
|
|
3698
3664
|
}
|
|
3699
|
-
async load(stateOrName,
|
|
3665
|
+
async load(stateOrName, streamOrTarget, callback, asOf) {
|
|
3700
3666
|
return this._scoped(async () => {
|
|
3701
3667
|
let merged;
|
|
3702
3668
|
if (typeof stateOrName === "string") {
|
|
@@ -3706,7 +3672,12 @@ var Act = class {
|
|
|
3706
3672
|
} else {
|
|
3707
3673
|
merged = this._states.get(stateOrName.name) || stateOrName;
|
|
3708
3674
|
}
|
|
3709
|
-
|
|
3675
|
+
const target = typeof streamOrTarget === "string" ? {
|
|
3676
|
+
stream: streamOrTarget,
|
|
3677
|
+
actor: void 0,
|
|
3678
|
+
asOf
|
|
3679
|
+
} : streamOrTarget;
|
|
3680
|
+
return await this._es.load(merged, target, callback);
|
|
3710
3681
|
});
|
|
3711
3682
|
}
|
|
3712
3683
|
/**
|
|
@@ -4430,11 +4401,14 @@ function act() {
|
|
|
4430
4401
|
const states = /* @__PURE__ */ new Map();
|
|
4431
4402
|
const _sf = /* @__PURE__ */ new Map();
|
|
4432
4403
|
const _dp = /* @__PURE__ */ new Map();
|
|
4404
|
+
const _de = /* @__PURE__ */ new Map();
|
|
4405
|
+
const EMPTY_DEPRECATED = /* @__PURE__ */ new Set();
|
|
4433
4406
|
const registry = {
|
|
4434
4407
|
actions: {},
|
|
4435
4408
|
events: {},
|
|
4436
4409
|
sensitive_fields: (event_name) => _sf.get(event_name) ?? [],
|
|
4437
|
-
disclosure_predicate: (state_name) => _dp.get(state_name) ?? null
|
|
4410
|
+
disclosure_predicate: (state_name) => _dp.get(state_name) ?? null,
|
|
4411
|
+
deprecated_events: (state_name) => _de.get(state_name) ?? EMPTY_DEPRECATED
|
|
4438
4412
|
};
|
|
4439
4413
|
const pending_projections = [];
|
|
4440
4414
|
const batch_handlers = /* @__PURE__ */ new Map();
|
|
@@ -4446,7 +4420,7 @@ function act() {
|
|
|
4446
4420
|
const event_names = Object.keys(state2.events);
|
|
4447
4421
|
const deprecated = deprecated_event_names(event_names);
|
|
4448
4422
|
if (deprecated.size === 0) continue;
|
|
4449
|
-
state2.
|
|
4423
|
+
_de.set(state2.name, deprecated);
|
|
4450
4424
|
for (const name of deprecated) {
|
|
4451
4425
|
const current = current_version_of(name, event_names);
|
|
4452
4426
|
deprecation_summary.push({
|
|
@@ -4574,18 +4548,17 @@ function act() {
|
|
|
4574
4548
|
);
|
|
4575
4549
|
}
|
|
4576
4550
|
const disclose = state2.disclose ?? null;
|
|
4577
|
-
state2.
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4551
|
+
state2.pii_aware = true;
|
|
4552
|
+
state2.view = (event, actor) => pii_gate(
|
|
4553
|
+
event,
|
|
4554
|
+
fields_by_event.get(event.name) ?? [],
|
|
4555
|
+
disclose,
|
|
4556
|
+
actor
|
|
4557
|
+
);
|
|
4581
4558
|
state2.message = (validated) => {
|
|
4582
4559
|
const fields = fields_by_event.get(validated.name);
|
|
4583
4560
|
return fields ? pii_split(validated, fields) : validated;
|
|
4584
4561
|
};
|
|
4585
|
-
for (const [event_name, fields] of fields_by_event) {
|
|
4586
|
-
const original = state2.patch[event_name];
|
|
4587
|
-
state2.patch[event_name] = (event, s) => original(pii_merge(event, fields), s);
|
|
4588
|
-
}
|
|
4589
4562
|
}
|
|
4590
4563
|
for (const [target, original] of batch_handlers) {
|
|
4591
4564
|
const wrapped = async (events, stream) => {
|
|
@@ -4765,9 +4738,10 @@ function state(entry) {
|
|
|
4765
4738
|
init,
|
|
4766
4739
|
patch: default_patch,
|
|
4767
4740
|
on: {},
|
|
4768
|
-
// Step delegates initialized as identity
|
|
4769
|
-
//
|
|
4770
|
-
// the gate / split.
|
|
4741
|
+
// Step delegates initialized as identity, `pii_aware` false.
|
|
4742
|
+
// `act().build()` flips both on states with `sensitive(...)`
|
|
4743
|
+
// events to bake in the gate / split and gate the cache write.
|
|
4744
|
+
pii_aware: false,
|
|
4771
4745
|
view: (event) => event,
|
|
4772
4746
|
message: (validated) => validated
|
|
4773
4747
|
};
|