const require_store = require("./store.cjs");
const require_namespace = require("./namespace.cjs");
//#region src/stream/message-metadata-tracker.ts
/**
* Per-message checkpoint metadata projection.
*
* # What this module is
*
* The protocol emits a `checkpoints` event immediately *before* its
* companion `values` event for the same superstep:
*
*   1. `checkpoints` — `{ id, parent_id?, step?, source? }`
*   2. `values`      — `{ messages, ... }`  (same namespace)
*
* Both events carry the same `seq` ordering but live on different
* channels, so the controller can't atomically observe them. This
* tracker bridges the gap by buffering each `checkpoints` envelope
* keyed on its namespace, then consuming it when the matching values
* payload arrives. Once paired, the consumer (typically the
* controller) writes a {@link MessageMetadata} record under each
* message id.
*
* # Why fork / edit flows need this
*
* Surfacing `parentCheckpointId` per-message lets UI flows like
* "edit a message and re-run" call
* `submit(input, { forkFrom: checkpointId })` without making the
* caller juggle thread state. Each message remembers the checkpoint
* it was first observed at, so a "fork from this message" UI can read
* `useMessageMetadata(stream, msg.id)` directly.
*
* # Lifecycle
*
*   - `bufferCheckpoint(ns, data)` — store the envelope until the
*     companion values event arrives.
*   - `consumeCheckpoint(ns)`      — read-and-clear the envelope when
*     the values event lands. Returning `undefined` signals "no
*     metadata to attach" — older snapshots without a paired
*     checkpoint are still applied to the store, just without
*     `parentCheckpointId`.
*   - `recordMessages(msgs, meta)` — write metadata for the supplied
*     message ids if it differs from what's already stored.
*   - `reset()`                    — clear everything (called on
*     thread rebind / dispose).
*
* The buffer is read-and-cleared on consumption so a values event that
* arrives without a fresh checkpoint envelope doesn't reuse stale
* metadata from a previous superstep.
*/
/**
* Frozen empty map used as the store's initial value. Keeping the
* reference stable avoids spurious `setSnapshot` notifications on
* `reset()` for consumers that haven't observed any metadata yet.
*/
const EMPTY_METADATA_MAP = /* @__PURE__ */ new Map();
/**
* Tracks checkpoint-derived metadata for messages.
*
* Owns one {@link StreamStore} mapping `messageId → MessageMetadata`
* plus a per-namespace buffer of pending checkpoint envelopes. The
* controller wires it up via three call sites:
*
*   1. `controller.#onRootEvent("checkpoints")`
*      → `bufferCheckpoint(namespace, data)`
*   2. `controller.#onRootEvent("values")`
*      → `consumeCheckpoint(namespace)` then
*      `recordMessages(values.messages, { parentCheckpointId })`
*   3. `controller.#teardownThread`
*      → `reset()`
*
* @see useMessageMetadata - The framework hook that reads from
*   {@link MessageMetadataTracker.store}.
*/
var MessageMetadataTracker = class {
	/** Observable map of messageId → metadata for framework consumers. */
	store = new require_store.StreamStore(EMPTY_METADATA_MAP);
	/**
	* Pending checkpoint envelopes awaiting their companion values
	* event. Keyed by `namespaceKey(namespace)` so a deeply-nested
	* checkpoint at one namespace doesn't collide with a root-level
	* checkpoint emitted in the same tick.
	*/
	#pendingCheckpointByNamespace = /* @__PURE__ */ new Map();
	/**
	* Ids of messages currently in the `"pending"` optimistic state.
	* Maintained alongside the metadata map so the controller can cheaply
	* (a) flip ids to `"sent"` when the server echoes them and (b) flip
	* any leftover ids to `"sent"` / `"failed"` at run terminal, without
	* scanning the whole metadata map.
	*/
	#pendingOptimisticIds = /* @__PURE__ */ new Set();
	/**
	* Drop all buffered checkpoints and reset the metadata map to the
	* shared empty instance. Called on thread rebind / dispose so a new
	* thread's metadata can't bleed into the old one.
	*/
	reset() {
		this.#pendingCheckpointByNamespace.clear();
		this.#pendingOptimisticIds.clear();
		this.store.setState(() => EMPTY_METADATA_MAP);
	}
	/**
	* Buffer a `checkpoints` event for later pairing with its values
	* companion.
	*
	* Defensive against missing / malformed payloads:
	*
	*   - `data == null`     → no-op (some upstream nodes elide the
	*     payload entirely; we keep the previous buffered envelope so
	*     the next consume call still wins).
	*   - `id` not a string  → no-op.
	*   - `parent_id` not a string → omitted from the envelope.
	*
	* @param namespace - Event namespace (used as the buffer key).
	* @param data      - Raw checkpoints payload.
	*/
	bufferCheckpoint(namespace, data) {
		if (data == null || typeof data.id !== "string") return;
		const envelope = { id: data.id };
		if (typeof data.parent_id === "string") envelope.parent_id = data.parent_id;
		if (typeof data.step === "number") envelope.step = data.step;
		this.#pendingCheckpointByNamespace.set(require_namespace.namespaceKey(namespace), envelope);
	}
	/**
	* Read-and-clear the buffered checkpoint envelope for `namespace`.
	*
	* Always pairs with a single {@link bufferCheckpoint} call: a values
	* event without a matching buffered checkpoint returns `undefined`
	* (meaning "no metadata to attach"), and the next checkpoint event
	* starts fresh rather than reusing stale data.
	*
	* @param namespace - Event namespace to consume.
	* @returns The buffered envelope, or `undefined` when none was buffered.
	*/
	consumeCheckpoint(namespace) {
		const key = require_namespace.namespaceKey(namespace);
		const checkpoint = this.#pendingCheckpointByNamespace.get(key);
		if (checkpoint != null) this.#pendingCheckpointByNamespace.delete(key);
		return checkpoint;
	}
	/**
	* Record metadata for a list of messages.
	*
	* Skips messages whose existing entry already matches `metadata`;
	* those without an `id` (or with a non-string id) are silently
	* ignored — there's nothing to key the metadata on. The store is
	* only updated when at least one entry actually changed, so
	* reapplying the same values snapshot is cheap.
	*
	* @param messages - Messages from the latest values payload.
	* @param metadata - Metadata to attach (currently just
	*   `parentCheckpointId`).
	*/
	recordMessages(messages, metadata) {
		const current = this.store.getSnapshot();
		let changed = false;
		const next = new Map(current);
		for (const msg of messages) {
			const id = msg?.id;
			if (typeof id !== "string" || id.length === 0) continue;
			const prev = next.get(id);
			if (prev != null && prev.parentCheckpointId === metadata.parentCheckpointId) continue;
			next.set(id, {
				...prev,
				...metadata
			});
			changed = true;
		}
		if (changed) this.store.setState(() => next);
	}
	/**
	* Mark a set of message ids as optimistically `"pending"`.
	*
	* Called from {@link StreamController}'s optimistic submit path right
	* after the messages are appended to the projection, so a UI can
	* render a "sending…" affordance via
	* `useMessageMetadata(stream, id).optimisticStatus`.
	*
	* @param ids - Message ids that were just applied optimistically.
	*/
	markPending(ids) {
		let changed = false;
		const next = new Map(this.store.getSnapshot());
		for (const id of ids) {
			if (typeof id !== "string" || id.length === 0) continue;
			this.#pendingOptimisticIds.add(id);
			const prev = next.get(id);
			if (prev?.optimisticStatus === "pending") continue;
			next.set(id, {
				parentCheckpointId: prev?.parentCheckpointId,
				...prev,
				optimisticStatus: "pending"
			});
			changed = true;
		}
		if (changed) this.store.setState(() => next);
	}
	/**
	* Transition the given ids out of `"pending"`.
	*
	* Only ids currently tracked as pending are affected, so passing a
	* full server `values.messages` id list (to flip echoed messages to
	* `"sent"`) never stamps a status onto ordinary server messages.
	*
	* @param ids    - Candidate ids (e.g. all ids in a server snapshot,
	*   or the ids echoed by a single submit).
	* @param status - Terminal optimistic status (`"sent"` / `"failed"`).
	*/
	resolvePending(ids, status) {
		let changed = false;
		const next = new Map(this.store.getSnapshot());
		for (const id of ids) {
			if (!this.#pendingOptimisticIds.has(id)) continue;
			this.#pendingOptimisticIds.delete(id);
			const prev = next.get(id);
			next.set(id, {
				parentCheckpointId: prev?.parentCheckpointId,
				...prev,
				optimisticStatus: status
			});
			changed = true;
		}
		if (changed) this.store.setState(() => next);
	}
	/**
	* Snapshot of ids whose optimistic status is `"pending"` or
	* `"failed"` — i.e. messages applied locally that the server has not
	* echoed. Used by {@link StreamController.hydrate} to drop
	* never-persisted optimistic messages so a reload converges to
	* server truth.
	*/
	unpersistedOptimisticIds() {
		const ids = new Set(this.#pendingOptimisticIds);
		for (const [id, meta] of this.store.getSnapshot()) if (meta.optimisticStatus === "failed") ids.add(id);
		return ids;
	}
	/**
	* Drop all metadata for the given ids. Called after never-persisted
	* optimistic messages are removed from the projection on
	* {@link StreamController.hydrate}, so their status doesn't linger.
	*
	* @param ids - Message ids to forget.
	*/
	forget(ids) {
		let changed = false;
		const next = new Map(this.store.getSnapshot());
		for (const id of ids) {
			this.#pendingOptimisticIds.delete(id);
			if (next.delete(id)) changed = true;
		}
		if (changed) this.store.setState(() => next);
	}
};
//#endregion
exports.MessageMetadataTracker = MessageMetadataTracker;

//# sourceMappingURL=message-metadata-tracker.cjs.map