{"version":3,"file":"message-metadata-tracker.cjs","names":["StreamStore","#pendingCheckpointByNamespace","namespaceKey"],"sources":["../../src/stream/message-metadata-tracker.ts"],"sourcesContent":["/**\n * Per-message checkpoint metadata projection.\n *\n * # What this module is\n *\n * The protocol emits a `checkpoints` event immediately *before* its\n * companion `values` event for the same superstep:\n *\n *   1. `checkpoints` — `{ id, parent_id?, step?, source? }`\n *   2. `values`      — `{ messages, ... }`  (same namespace)\n *\n * Both events carry the same `seq` ordering but live on different\n * channels, so the controller can't atomically observe them. This\n * tracker bridges the gap by buffering each `checkpoints` envelope\n * keyed on its namespace, then consuming it when the matching values\n * payload arrives. Once paired, the consumer (typically the\n * controller) writes a {@link MessageMetadata} record under each\n * message id.\n *\n * # Why fork / edit flows need this\n *\n * Surfacing `parentCheckpointId` per-message lets UI flows like\n * \"edit a message and re-run\" call\n * `submit(input, { forkFrom: { checkpointId } })` without making the\n * caller juggle thread state. Each message remembers the checkpoint\n * it was first observed at, so a \"fork from this message\" UI can read\n * `useMessageMetadata(stream, msg.id)` directly.\n *\n * # Lifecycle\n *\n *   - `bufferCheckpoint(ns, data)` — store the envelope until the\n *     companion values event arrives.\n *   - `consumeCheckpoint(ns)`      — read-and-clear the envelope when\n *     the values event lands. Returning `undefined` signals \"no\n *     metadata to attach\" — older snapshots without a paired\n *     checkpoint are still applied to the store, just without\n *     `parentCheckpointId`.\n *   - `recordMessages(msgs, meta)` — write metadata for the supplied\n *     message ids if it differs from what's already stored.\n *   - `reset()`                    — clear everything (called on\n *     thread rebind / dispose).\n *\n * The buffer is read-and-cleared on consumption so a values event that\n * arrives without a fresh checkpoint envelope doesn't reuse stale\n * metadata from a previous superstep.\n */\nimport { StreamStore } from \"./store.js\";\nimport { namespaceKey } from \"./namespace.js\";\n\n/**\n * Metadata tracked per message id. Surfaced to applications via\n * `useMessageMetadata(stream, messageId)`.\n */\nexport interface MessageMetadata {\n  /**\n   * Checkpoint id the message's *parent* was at when this message was\n   * observed. Drives fork / edit flows\n   * (`submit(input, { forkFrom: { checkpointId } })`).\n   *\n   * `undefined` when the message was observed without a paired\n   * checkpoint envelope (e.g. before checkpoints rolled out, or when\n   * the caller stripped them upstream).\n   */\n  readonly parentCheckpointId: string | undefined;\n}\n\n/**\n * Read-only map exposed via {@link MessageMetadataTracker.store}.\n */\nexport type MessageMetadataMap = ReadonlyMap<string, MessageMetadata>;\n\n/**\n * Lightweight envelope mirroring the on-wire `checkpoints` event.\n *\n * The protocol payload may include additional fields (`step`,\n * `source`, etc.) — we only carry what the per-message metadata\n * actually needs.\n */\nexport interface CheckpointEnvelope {\n  /** Checkpoint id this superstep wrote. */\n  readonly id: string;\n  /**\n   * Parent checkpoint id, when present. Becomes\n   * {@link MessageMetadata.parentCheckpointId} on the next values event.\n   */\n  readonly parent_id?: string;\n}\n\n/**\n * Frozen empty map used as the store's initial value. Keeping the\n * reference stable avoids spurious `setSnapshot` notifications on\n * `reset()` for consumers that haven't observed any metadata yet.\n */\nconst EMPTY_METADATA_MAP: MessageMetadataMap = new Map();\n\n/**\n * Tracks checkpoint-derived metadata for messages.\n *\n * Owns one {@link StreamStore} mapping `messageId → MessageMetadata`\n * plus a per-namespace buffer of pending checkpoint envelopes. The\n * controller wires it up via three call sites:\n *\n *   1. `controller.#onRootEvent(\"checkpoints\")`\n *      → `bufferCheckpoint(namespace, data)`\n *   2. `controller.#onRootEvent(\"values\")`\n *      → `consumeCheckpoint(namespace)` then\n *      `recordMessages(values.messages, { parentCheckpointId })`\n *   3. `controller.#teardownThread`\n *      → `reset()`\n *\n * @see useMessageMetadata - The framework hook that reads from\n *   {@link MessageMetadataTracker.store}.\n */\nexport class MessageMetadataTracker {\n  /** Observable map of messageId → metadata for framework consumers. */\n  readonly store = new StreamStore<MessageMetadataMap>(EMPTY_METADATA_MAP);\n\n  /**\n   * Pending checkpoint envelopes awaiting their companion values\n   * event. Keyed by `namespaceKey(namespace)` so a deeply-nested\n   * checkpoint at one namespace doesn't collide with a root-level\n   * checkpoint emitted in the same tick.\n   */\n  readonly #pendingCheckpointByNamespace = new Map<\n    string,\n    CheckpointEnvelope\n  >();\n\n  /**\n   * Drop all buffered checkpoints and reset the metadata map to the\n   * shared empty instance. Called on thread rebind / dispose so a new\n   * thread's metadata can't bleed into the old one.\n   */\n  reset(): void {\n    this.#pendingCheckpointByNamespace.clear();\n    this.store.setState(() => EMPTY_METADATA_MAP);\n  }\n\n  /**\n   * Buffer a `checkpoints` event for later pairing with its values\n   * companion.\n   *\n   * Defensive against missing / malformed payloads:\n   *\n   *   - `data == null`     → no-op (some upstream nodes elide the\n   *     payload entirely; we keep the previous buffered envelope so\n   *     the next consume call still wins).\n   *   - `id` not a string  → no-op.\n   *   - `parent_id` not a string → omitted from the envelope.\n   *\n   * @param namespace - Event namespace (used as the buffer key).\n   * @param data      - Raw checkpoints payload.\n   */\n  bufferCheckpoint(\n    namespace: readonly string[],\n    data: { id?: unknown; parent_id?: unknown } | null\n  ): void {\n    if (data == null || typeof data.id !== \"string\") return;\n    const envelope: CheckpointEnvelope = { id: data.id };\n    if (typeof data.parent_id === \"string\") {\n      (envelope as { parent_id?: string }).parent_id = data.parent_id;\n    }\n    this.#pendingCheckpointByNamespace.set(namespaceKey(namespace), envelope);\n  }\n\n  /**\n   * Read-and-clear the buffered checkpoint envelope for `namespace`.\n   *\n   * Always pairs with a single {@link bufferCheckpoint} call: a values\n   * event without a matching buffered checkpoint returns `undefined`\n   * (meaning \"no metadata to attach\"), and the next checkpoint event\n   * starts fresh rather than reusing stale data.\n   *\n   * @param namespace - Event namespace to consume.\n   * @returns The buffered envelope, or `undefined` when none was buffered.\n   */\n  consumeCheckpoint(\n    namespace: readonly string[]\n  ): CheckpointEnvelope | undefined {\n    const key = namespaceKey(namespace);\n    const checkpoint = this.#pendingCheckpointByNamespace.get(key);\n    if (checkpoint != null) this.#pendingCheckpointByNamespace.delete(key);\n    return checkpoint;\n  }\n\n  /**\n   * Record metadata for a list of messages.\n   *\n   * Skips messages whose existing entry already matches `metadata`;\n   * those without an `id` (or with a non-string id) are silently\n   * ignored — there's nothing to key the metadata on. The store is\n   * only updated when at least one entry actually changed, so\n   * reapplying the same values snapshot is cheap.\n   *\n   * @param messages - Messages from the latest values payload.\n   * @param metadata - Metadata to attach (currently just\n   *   `parentCheckpointId`).\n   */\n  recordMessages(\n    messages: Array<{ id?: string }>,\n    metadata: MessageMetadata\n  ): void {\n    const current = this.store.getSnapshot();\n    let changed = false;\n    const next = new Map(current);\n    for (const msg of messages) {\n      const id = msg?.id;\n      if (typeof id !== \"string\" || id.length === 0) continue;\n      const prev = next.get(id);\n      if (\n        prev != null &&\n        prev.parentCheckpointId === metadata.parentCheckpointId\n      ) {\n        continue;\n      }\n      next.set(id, { ...prev, ...metadata });\n      changed = true;\n    }\n    if (changed) this.store.setState(() => next);\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,MAAM,qCAAyC,IAAI,KAAK;;;;;;;;;;;;;;;;;;;AAoBxD,IAAa,yBAAb,MAAoC;;CAElC,QAAiB,IAAIA,cAAAA,YAAgC,mBAAmB;;;;;;;CAQxE,gDAAyC,IAAI,KAG1C;;;;;;CAOH,QAAc;AACZ,QAAA,6BAAmC,OAAO;AAC1C,OAAK,MAAM,eAAe,mBAAmB;;;;;;;;;;;;;;;;;CAkB/C,iBACE,WACA,MACM;AACN,MAAI,QAAQ,QAAQ,OAAO,KAAK,OAAO,SAAU;EACjD,MAAM,WAA+B,EAAE,IAAI,KAAK,IAAI;AACpD,MAAI,OAAO,KAAK,cAAc,SAC3B,UAAoC,YAAY,KAAK;AAExD,QAAA,6BAAmC,IAAIE,kBAAAA,aAAa,UAAU,EAAE,SAAS;;;;;;;;;;;;;CAc3E,kBACE,WACgC;EAChC,MAAM,MAAMA,kBAAAA,aAAa,UAAU;EACnC,MAAM,aAAa,MAAA,6BAAmC,IAAI,IAAI;AAC9D,MAAI,cAAc,KAAM,OAAA,6BAAmC,OAAO,IAAI;AACtE,SAAO;;;;;;;;;;;;;;;CAgBT,eACE,UACA,UACM;EACN,MAAM,UAAU,KAAK,MAAM,aAAa;EACxC,IAAI,UAAU;EACd,MAAM,OAAO,IAAI,IAAI,QAAQ;AAC7B,OAAK,MAAM,OAAO,UAAU;GAC1B,MAAM,KAAK,KAAK;AAChB,OAAI,OAAO,OAAO,YAAY,GAAG,WAAW,EAAG;GAC/C,MAAM,OAAO,KAAK,IAAI,GAAG;AACzB,OACE,QAAQ,QACR,KAAK,uBAAuB,SAAS,mBAErC;AAEF,QAAK,IAAI,IAAI;IAAE,GAAG;IAAM,GAAG;IAAU,CAAC;AACtC,aAAU;;AAEZ,MAAI,QAAS,MAAK,MAAM,eAAe,KAAK"}