/** * Schema-evolution helpers (P5.2). * * Problem: a deployed workflow's checkpoint outputs were serialized by v1 of * the user's types; the user ships v2 code whose types changed shape. On * recovery, a memoized checkpoint would deserialize straight into the wrong * shape unless something upgrades it first. * * This module lets user code register a `PayloadCodec` per step name (an * activity type, a signal name, etc. — whatever string is passed as * `step_name`/`activityType` at the call site) or globally (`'*'`). When a * codec is registered for a name, the worker wraps that step's output in an * envelope: `{ $mfv: version, value }`. A plain payload with no envelope is * implicitly version 0 — full back-compat with every checkpoint written * before this feature existed, and with steps that never register a codec. * * On memoized-result hydration (the recovery path in workflow-context.ts and * context-lite.ts), if a codec is registered for the step name and the * stored version is older than the codec's current version, `up()` runs * (possibly chained through multiple codecs) before the value reaches * workflow code. A stored version NEWER than the registered codec's version * is a loud error — Design Law 4's spirit applies here too: never silently * hand v3 data to v2 code. * * Design Law 1 is untouched: step_key/step_seq/occurrence-index never * include or depend on the payload version. The envelope only wraps the * VALUE half of a checkpoint, not its identity. */ /** * A codec that upgrades a payload from an older version to `version`. * * `up` may be called with a payload from ANY older version (not just * `version - 1`) — implementations that only know how to upgrade from the * immediately-prior version should register one `PayloadCodec` per version * step and let `CodecRegistry` chain them (see `register`). */ export interface PayloadCodec { /** The version this codec's `up()` produces. Must be a positive integer. */ version: number; /** * Upgrade `older` (serialized at `fromVersion`) to the shape this codec's * `version` expects. Called once per hydration with the payload as it * existed at `fromVersion` — for multi-step upgrades (e.g. v0 -> v2 with * codecs registered for v1 and v2), `CodecRegistry` chains calls so each * codec only ever receives the immediately-prior version's shape. */ up(older: unknown, fromVersion: number): unknown; } /** The magic envelope key. Exported so tooling (e.g. the migrate-checkpoints CLI) can recognize it without importing the whole registry. */ export declare const ENVELOPE_KEY = "$mfv"; /** Wire shape written by the worker when a codec is registered for a step name. */ export interface VersionedEnvelope { [ENVELOPE_KEY]: number; value: unknown; } /** Un-enveloped (or falsy) payloads are implicitly version 0. */ export declare const UNVERSIONED = 0; /** * Thrown when a stored checkpoint's payload version is NEWER than the * currently-registered codec's version for that step name — i.e. this * process is running OLDER code than whatever wrote the checkpoint. Never * silently hand higher-version data to lower-version code (Design Law 4 * spirit: fail loudly, don't fabricate). */ export declare class PayloadVersionSkewError extends Error { readonly stepName: string; readonly storedVersion: number; readonly registeredVersion: number; constructor(stepName: string, storedVersion: number, registeredVersion: number); } /** * Registry of versioned payload codecs, keyed by step name (with an * optional `'*'` global fallback consulted when no exact name match exists). * * One registry per SDK process (import the singleton `codecs` export below); * tests may construct their own instance for isolation. */ export declare class CodecRegistry { private byName; /** * Register a codec for `name` (an activity type / step name), or for * every step name if `name` is `'*'`. Multiple codecs may be registered * for the same name at increasing versions — `up()` chains through them * in ascending version order so a v0 payload can reach a v3 codec via * v1 -> v2 -> v3 intermediate upgrades. */ register(name: string, codec: PayloadCodec): void; /** Remove all codecs for `name` (or everything, if `name` is omitted). Test helper. */ clear(name?: string): void; private chain; /** The highest version registered for `name` (falling back to `'*'`), or `UNVERSIONED` if none. */ currentVersion(name: string): number; /** * Wrap `value` in a version envelope if a codec is registered for `name`. * Returns `value` unchanged (no envelope) when no codec is registered — * byte-identical to pre-P5.2 behavior for every step that doesn't opt in. */ envelopeFor(name: string, value: unknown): unknown; /** * Unwrap a hydrated payload, running any registered `up()` chain to bring * it to the current version before returning it to workflow code. * * - No envelope (plain payload) => implicitly version 0; if codecs are * registered for `name` starting above 0, they still run (a v0 codec * upgrades un-enveloped legacy data exactly like enveloped v0 data). * - Envelope with version === current => unwrapped value, no upgrade. * - Envelope with version < current => chained `up()` through every * registered codec strictly greater than the stored version, in * ascending order. * - Envelope with version > current => `PayloadVersionSkewError` (loud, * never silent — this process's code is older than the data). * - No codec registered for `name` at all => value passed through as-is * (enveloped or not), since there's nothing to upgrade to. This keeps * the "no-codec path is byte-identical to today" guarantee even if a * payload was, unusually, already enveloped by another process. */ hydrate(name: string, value: unknown): unknown; } /** Process-wide default registry. Most apps only need this singleton. */ export declare const codecs: CodecRegistry;