@genkit-ai/ai
Version:
Genkit AI framework generative AI APIs.
1 lines • 39.8 kB
Source Map (JSON)
{"version":3,"sources":["../src/session-stores.ts"],"sourcesContent":["/**\n * Copyright 2026 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { GenkitError } from '@genkit-ai/core';\nimport * as fs from 'fs';\nimport * as fsp from 'fs/promises';\nimport * as path from 'path';\nimport {\n assertValidSessionId,\n type GetSnapshotOptions,\n type SessionSnapshot,\n type SessionStore,\n type SessionStoreOptions,\n type SnapshotMutator,\n} from './session.mjs';\n\n/**\n * Normalizes and validates {@link GetSnapshotOptions}.\n *\n * Enforces that exactly one of `snapshotId` / `sessionId` is provided and,\n * when a `sessionId` is given, that it is a valid UUID.\n *\n * @throws `INVALID_ARGUMENT` when neither or both are provided.\n */\nfunction normalizeGetSnapshotOptions(opts: GetSnapshotOptions): {\n snapshotId?: string;\n sessionId?: string;\n} {\n const { snapshotId, sessionId } = opts;\n if (!!snapshotId === !!sessionId) {\n throw new GenkitError({\n status: 'INVALID_ARGUMENT',\n message:\n `getSnapshot requires exactly one of 'snapshotId' or 'sessionId' ` +\n `(got ${snapshotId ? 'snapshotId' : 'neither'}${sessionId ? ' and sessionId' : ''}).`,\n });\n }\n if (sessionId) {\n assertValidSessionId(sessionId);\n }\n return { snapshotId, sessionId };\n}\n\n/**\n * Selects the latest leaf snapshot from a set belonging to one session.\n *\n * A \"leaf\" is a snapshot that no other snapshot points to as its `parentId`.\n * A healthy linear session has exactly one leaf - the latest turn.\n *\n * The leaf is returned regardless of `status`; resumability (only `done`\n * snapshots are resumable) is enforced by the agent, which walks back over a\n * non-resumable leaf (e.g. a failed turn) to the last-good snapshot.\n *\n * - Returns `undefined` when `snapshots` is empty.\n * - Returns the single leaf when the history is linear.\n * - When the history has branched (more than one leaf, e.g. after a\n * regenerate) the behavior depends on `rejectBranching`:\n * - `false` (default): returns the most-recently created leaf (by\n * `createdAt`). This keeps `sessionId` lookups cheap and forgiving.\n * - `true`: throws `FAILED_PRECONDITION`, since there is no unambiguous\n * \"latest\". Opt into this in dev to surface accidental branching early.\n */\nfunction selectLeafSnapshot<S>(\n snapshots: SessionSnapshot<S>[],\n sessionId: string,\n rejectBranching = false\n): SessionSnapshot<S> | undefined {\n if (snapshots.length === 0) return undefined;\n\n const parentIds = new Set<string>();\n for (const snap of snapshots) {\n if (snap.parentId) parentIds.add(snap.parentId);\n }\n const leaves = snapshots.filter((s) => !parentIds.has(s.snapshotId));\n\n // A single-snapshot session, or any chain, collapses to one leaf.\n if (leaves.length === 1) return leaves[0];\n\n if (leaves.length === 0) {\n // Cyclic / corrupt history - every snapshot is someone's parent.\n throw new GenkitError({\n status: 'FAILED_PRECONDITION',\n message:\n `Session '${sessionId}' has no leaf snapshot (corrupt or cyclic ` +\n `history). Resume by snapshotId instead.`,\n });\n }\n\n if (rejectBranching) {\n throw new GenkitError({\n status: 'FAILED_PRECONDITION',\n message:\n `Session '${sessionId}' has branching snapshots (${leaves.length} ` +\n `leaves), so there is no single latest snapshot. This happens when a ` +\n `conversation is branched (e.g. regenerate). Resume by snapshotId instead.`,\n });\n }\n\n // Default: pick the most-recently created leaf. `createdAt` is an ISO-8601\n // timestamp, so lexicographic comparison matches chronological order.\n return leaves.reduce((latest, snap) =>\n snap.createdAt > latest.createdAt ? snap : latest\n );\n}\n\n/**\n * In-memory implementation of persistent Session Store.\n */\nexport class InMemorySessionStore<S = unknown> implements SessionStore<S> {\n private snapshots = new Map<string, SessionSnapshot<S>>();\n private listeners = new Map<\n string,\n Array<(snapshot: SessionSnapshot<S>) => void>\n >();\n private rejectBranchingSessions: boolean;\n\n /**\n * @param options.rejectBranchingSessions When `true`, a `sessionId` lookup\n * that resolves to a branched history (more than one leaf) throws\n * `FAILED_PRECONDITION` instead of returning the latest leaf. Defaults to\n * `false`; opt in (e.g. in dev) to surface accidental branching early.\n */\n constructor(options?: { rejectBranchingSessions?: boolean }) {\n this.rejectBranchingSessions = options?.rejectBranchingSessions ?? false;\n }\n\n async getSnapshot(\n opts: GetSnapshotOptions\n ): Promise<SessionSnapshot<S> | undefined> {\n const { snapshotId, sessionId } = normalizeGetSnapshotOptions(opts);\n\n if (snapshotId) {\n const snap = this.snapshots.get(snapshotId);\n if (!snap) return undefined;\n return structuredClone(snap);\n }\n\n // sessionId lookup: gather every snapshot belonging to this session and\n // resolve the single leaf (latest) snapshot.\n const owned: SessionSnapshot<S>[] = [];\n for (const snap of this.snapshots.values()) {\n if ((snap.sessionId ?? snap.state?.sessionId) === sessionId) {\n owned.push(snap);\n }\n }\n const leaf = selectLeafSnapshot(\n owned,\n sessionId!,\n this.rejectBranchingSessions\n );\n return leaf ? structuredClone(leaf) : undefined;\n }\n\n async saveSnapshot(\n snapshotId: string | undefined,\n mutator: SnapshotMutator<S>,\n options?: SessionStoreOptions\n ): Promise<string | null> {\n const current = snapshotId ? this.snapshots.get(snapshotId) : undefined;\n\n const result = mutator(current ? structuredClone(current) : undefined);\n if (result === null) return null;\n\n // Determine the final ID. The runtime normally supplies a snapshotId, but\n // fall back to a fresh UUID for direct store users who omit it.\n const id =\n snapshotId || result.snapshotId || globalThis.crypto.randomUUID();\n const full: SessionSnapshot<S> = {\n ...result,\n snapshotId: id,\n };\n\n this.snapshots.set(id, structuredClone(full));\n const snapshotListeners = this.listeners.get(id);\n if (snapshotListeners) {\n for (const listener of snapshotListeners) {\n listener(structuredClone(full));\n }\n }\n return id;\n }\n\n onSnapshotStateChange(\n snapshotId: string,\n callback: (snapshot: SessionSnapshot<S>) => void,\n options?: SessionStoreOptions\n ): void | (() => void) {\n if (!this.listeners.has(snapshotId)) {\n this.listeners.set(snapshotId, []);\n }\n this.listeners.get(snapshotId)!.push(callback);\n return () => {\n const list = this.listeners.get(snapshotId);\n if (list) {\n const index = list.indexOf(callback);\n if (index >= 0) list.splice(index, 1);\n }\n };\n }\n}\n\n/**\n * Default interval (ms) for the polling fallback used by\n * {@link FileSessionStore.onSnapshotStateChange}.\n */\nconst DEFAULT_SNAPSHOT_WATCH_POLL_INTERVAL_MS = 2000;\n\n/**\n * Validates that an id is a plain file basename and not a path that could\n * escape the (per-tenant) prefix directory.\n *\n * Ids (`snapshotId`, `sessionId`) can arrive straight off the wire (the\n * abort/getSnapshot actions accept a bare string), so without this an id like\n * `../../foo` would let a caller read or write outside the prefix and break\n * per-tenant isolation.\n */\nfunction assertSafeId(id: string, label: string): void {\n if (\n !id ||\n id.includes('/') ||\n id.includes('\\\\') ||\n id.includes('\\0') ||\n id === '.' ||\n id === '..' ||\n path.basename(id) !== id\n ) {\n throw new GenkitError({\n status: 'INVALID_ARGUMENT',\n message: `Invalid ${label}: \"${id}\". A ${label} must be a plain file name (no path separators or \"..\").`,\n });\n }\n}\n\n/**\n * Validates that a snapshotId is a plain file basename and not a path that\n * could escape the (per-tenant) prefix directory.\n */\nfunction assertSafeSnapshotId(snapshotId: string): void {\n assertSafeId(snapshotId, 'snapshotId');\n}\n\n/**\n * The per-session pointer file. Records the current leaf snapshot id for a\n * session so a `getSnapshot({ sessionId })` lookup is a single pointer read\n * followed by one snapshot read, instead of scanning and parsing every snapshot\n * file in the prefix directory.\n */\ninterface PointerDoc {\n currentSnapshotId: string;\n updatedAt: string;\n}\n\n/**\n * Hidden sub-directory (within each prefix) holding the per-session\n * {@link PointerDoc} files. It is a directory, so the snapshot scan in\n * {@link FileSessionStore.getLatestSnapshotForSession} - which only considers\n * `*.json` *files* - naturally skips it.\n */\nconst POINTERS_SUBDIR = '.pointers';\n\n/**\n * A Node.js file-system backed session snapshot store.\n *\n * Snapshots are stored as flat JSON files keyed by their `snapshotId`, under an\n * optional per-tenant sub-directory `prefix`:\n *\n * File layout: `dirPath/<prefix>/<snapshotId>.json`\n *\n * `getSnapshot({ sessionId })` resolves the session's current leaf via a tiny\n * per-session pointer file (`<prefix>/.pointers/<sessionId>.json`, see\n * {@link PointerDoc}) - one pointer read plus one snapshot read. When the\n * pointer is missing (e.g. a legacy store) or stale it transparently falls back\n * to scanning the prefix directory and selecting the single leaf whose\n * `sessionId` matches, then rewrites the pointer so subsequent lookups are fast\n * again.\n */\nexport class FileSessionStore<S = unknown> implements SessionStore<S> {\n private dirPath: string;\n private maxPersistedChainLength?: number;\n private snapshotPathPrefix?: (options?: SessionStoreOptions) => string;\n private rejectBranchingSessions: boolean;\n private snapshotWatchPollIntervalMs: number;\n\n /**\n * Per-file write locks. The {@link SessionStore} contract (and the\n * abort-aware mutator that branches on `current.status`) assumes\n * read-modify-write is atomic, but on the file system a read and the\n * `writeFile` below it are not. Without a lock two concurrent saves can\n * read the same `current` and the later write clobbers the earlier one\n * (e.g. a `completed` write overwriting a concurrent `aborted`). We\n * serialize saves per resolved file path with a simple promise chain.\n */\n private writeLocks = new Map<string, Promise<unknown>>();\n\n /**\n * @param dirPath Directory where snapshot JSON files are stored.\n * @param options.maxPersistedChainLength When set, snapshots older than this\n * many entries in a chain are automatically deleted on each save.\n * @param options.snapshotPathPrefix Returns a sub-directory prefix derived\n * from the call's {@link SessionStoreOptions} (e.g. the authenticated user\n * id from `options.context`), useful for multi-tenant isolation: all reads\n * and writes are scoped to that prefix, so one tenant can never see\n * another's snapshots. Defaults to `\"global\"`.\n * @param options.rejectBranchingSessions When `true`, a `sessionId` lookup\n * that resolves to a branched history (more than one leaf) throws\n * `FAILED_PRECONDITION` instead of returning the latest leaf. Defaults to\n * `false`; opt in (e.g. in dev) to surface accidental branching early.\n * @param options.snapshotWatchPollIntervalMs Polling interval (ms) for the\n * {@link FileSessionStore.onSnapshotStateChange} fallback that backstops\n * `fs.watch` (which can miss events on some filesystems, e.g. network\n * mounts). Defaults to {@link DEFAULT_SNAPSHOT_WATCH_POLL_INTERVAL_MS}.\n */\n constructor(\n dirPath: string,\n options?: {\n maxPersistedChainLength?: number;\n snapshotPathPrefix?: (options?: SessionStoreOptions) => string;\n rejectBranchingSessions?: boolean;\n snapshotWatchPollIntervalMs?: number;\n }\n ) {\n this.dirPath = path.resolve(dirPath);\n fs.mkdirSync(this.dirPath, { recursive: true });\n this.maxPersistedChainLength = options?.maxPersistedChainLength;\n this.snapshotPathPrefix = options?.snapshotPathPrefix;\n this.rejectBranchingSessions = options?.rejectBranchingSessions ?? false;\n this.snapshotWatchPollIntervalMs =\n options?.snapshotWatchPollIntervalMs ??\n DEFAULT_SNAPSHOT_WATCH_POLL_INTERVAL_MS;\n }\n\n private async ensureDir(dir: string): Promise<void> {\n await fsp.mkdir(dir, { recursive: true });\n }\n\n /** Resolves the (per-tenant) directory snapshots are stored under. */\n private prefixDir(options?: SessionStoreOptions): string {\n const prefix = this.snapshotPathPrefix\n ? this.snapshotPathPrefix(options)\n : 'global';\n return path.join(this.dirPath, prefix);\n }\n\n /**\n * Resolves the file path for a given snapshotId: `<prefix>/<snapshotId>.json`.\n */\n private async getFilePath(\n snapshotId: string,\n options?: SessionStoreOptions\n ): Promise<string> {\n assertSafeSnapshotId(snapshotId);\n const dir = this.prefixDir(options);\n await this.ensureDir(dir);\n // Defense in depth: even after the basename check, confirm the resolved\n // path stays inside the prefix directory.\n const filePath = path.join(dir, `${snapshotId}.json`);\n const resolvedDir = path.resolve(dir);\n const resolved = path.resolve(filePath);\n if (\n resolved !== path.join(resolvedDir, `${snapshotId}.json`) ||\n !resolved.startsWith(resolvedDir + path.sep)\n ) {\n throw new GenkitError({\n status: 'INVALID_ARGUMENT',\n message: `Invalid snapshotId: \"${snapshotId}\". Resolved path escapes the snapshot directory.`,\n });\n }\n return filePath;\n }\n\n /** Resolves the (per-tenant) directory holding per-session pointer files. */\n private pointersDir(options?: SessionStoreOptions): string {\n return path.join(this.prefixDir(options), POINTERS_SUBDIR);\n }\n\n /**\n * Resolves the pointer file path for a session, validating `sessionId` is a\n * plain basename so it can never escape the pointers directory. Pure: it does\n * not create the directory, so the read path stays side-effect free. The\n * write path calls {@link ensureDir} before writing.\n */\n private getPointerPath(\n sessionId: string,\n options?: SessionStoreOptions\n ): string {\n assertSafeId(sessionId, 'sessionId');\n return path.join(this.pointersDir(options), `${sessionId}.json`);\n }\n\n /**\n * Reads the per-session {@link PointerDoc}, or `undefined` when it is missing\n * (legacy store / not yet written) or unreadable / corrupt - callers fall\n * back to a full directory scan in that case. Best-effort: any IO/parse error\n * resolves to `undefined` so the optimization can never make a lookup (or\n * save) fail where the scan-only baseline would have succeeded. An invalid\n * `sessionId` still throws (path validation is resolved outside the try) so it\n * fails fast rather than silently being ignored.\n */\n private async readPointer(\n sessionId: string,\n options?: SessionStoreOptions\n ): Promise<PointerDoc | undefined> {\n const filePath = this.getPointerPath(sessionId, options);\n let contents: string;\n try {\n contents = await fsp.readFile(filePath, 'utf-8');\n } catch {\n // Missing / unreadable pointer: fall back to the scan.\n return undefined;\n }\n try {\n const parsed = JSON.parse(contents) as PointerDoc;\n // Guard the type too: a non-string id would later throw in `assertSafeId`\n // on the fast path instead of falling back to the scan.\n return typeof parsed?.currentSnapshotId === 'string' ? parsed : undefined;\n } catch {\n // Partially written / corrupt pointer: treat as absent and rescan.\n return undefined;\n }\n }\n\n /**\n * Atomically writes the per-session {@link PointerDoc}. Best-effort: a\n * pointer write failure is swallowed since the pointer is only an\n * optimization - `sessionId` lookups still self-heal via the full scan. An\n * invalid `sessionId` still throws (path validation is resolved outside the\n * try) so it fails fast rather than silently being ignored.\n */\n private async writePointer(\n sessionId: string,\n currentSnapshotId: string,\n options?: SessionStoreOptions\n ): Promise<void> {\n const filePath = this.getPointerPath(sessionId, options);\n const dir = this.pointersDir(options);\n try {\n await this.ensureDir(dir);\n const pointer: PointerDoc = {\n currentSnapshotId,\n updatedAt: new Date().toISOString(),\n };\n await this.atomicWrite(filePath, JSON.stringify(pointer, null, 2));\n } catch {\n // Ignore: the scan fallback keeps `sessionId` lookups correct.\n }\n }\n\n /**\n * Serializes async work per resolved file path so a read-modify-write in\n * {@link saveSnapshot} is not interleaved with a concurrent one for the same\n * snapshot (see {@link writeLocks}).\n */\n private async withFileLock<T>(\n filePath: string,\n fn: () => Promise<T>\n ): Promise<T> {\n const prev = this.writeLocks.get(filePath) ?? Promise.resolve();\n // Wait for any in-flight save of this file, ignoring its result/error so a\n // prior failure doesn't poison the lock for subsequent callers.\n const gate = prev.then(\n () => undefined,\n () => undefined\n );\n this.writeLocks.set(filePath, gate);\n await gate;\n try {\n return await fn();\n } finally {\n // If no one chained after us we are the tail; drop the entry to avoid\n // leaking a map entry per snapshotId.\n if (this.writeLocks.get(filePath) === gate) {\n this.writeLocks.delete(filePath);\n }\n }\n }\n\n async getSnapshot(\n opts: GetSnapshotOptions\n ): Promise<SessionSnapshot<S> | undefined> {\n const { snapshotId, sessionId } = normalizeGetSnapshotOptions(opts);\n\n if (sessionId) {\n return this.getLatestSnapshotForSession(sessionId, opts);\n }\n\n const filePath = await this.getFilePath(snapshotId!, opts);\n try {\n const fileContents = await fsp.readFile(filePath, 'utf-8');\n return JSON.parse(fileContents) as SessionSnapshot<S>;\n } catch (e: unknown) {\n if ((e as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw e;\n }\n }\n\n /**\n * Loads a single snapshot file by its id (no sessionId branch). Used by\n * internal traversal (parent chains) where we always have a concrete id.\n */\n private async getSnapshotById(\n snapshotId: string,\n options?: SessionStoreOptions\n ): Promise<SessionSnapshot<S> | undefined> {\n const filePath = await this.getFilePath(snapshotId, options);\n try {\n const fileContents = await fsp.readFile(filePath, 'utf-8');\n return JSON.parse(fileContents) as SessionSnapshot<S>;\n } catch (e: unknown) {\n if ((e as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw e;\n }\n }\n\n /**\n * Resolves the latest (leaf) snapshot for a session.\n *\n * Fast path: read the per-session pointer file and load the leaf it names -\n * one pointer read plus one snapshot read, independent of session count /\n * length. The pointer is skipped (and the scan used) when\n * `rejectBranchingSessions` is set, since detecting branches requires seeing\n * every leaf.\n *\n * Fallback (no/stale/corrupt pointer, or branch detection): scan every\n * snapshot file in the prefix directory, keep those whose `sessionId`\n * matches, select the single leaf, and refresh the pointer so later lookups\n * take the fast path.\n *\n * Known limitation: the fast path trusts the pointer when the snapshot it\n * names still exists and belongs to the session - it does not re-verify that\n * it is the actual leaf. So if a save succeeds but the subsequent (best-effort)\n * `writePointer` does not (crash/disk error), or two new saves for the same\n * session race and the older one writes the pointer last, the pointer can\n * linger on a valid-but-older same-session snapshot and lookups return it\n * until the next save advances the pointer. This is the accepted trade-off for\n * a best-effort cache: verifying leaf-ness on every read would reintroduce the\n * full scan the pointer exists to avoid. Callers needing strict guarantees can\n * resume by `snapshotId`, or set `rejectBranchingSessions` (which always\n * scans).\n */\n private async getLatestSnapshotForSession(\n sessionId: string,\n options?: SessionStoreOptions\n ): Promise<SessionSnapshot<S> | undefined> {\n // Fast path via the pointer file (skipped when we must detect branching).\n if (!this.rejectBranchingSessions) {\n const pointer = await this.readPointer(sessionId, options);\n if (pointer) {\n const snap = await this.getSnapshotById(\n pointer.currentSnapshotId,\n options\n );\n // Honor the pointer only when the leaf still exists and belongs to this\n // session; otherwise it is stale - fall through to the scan, which also\n // rewrites the pointer.\n if (snap && (snap.sessionId ?? snap.state?.sessionId) === sessionId) {\n return snap;\n }\n }\n }\n\n const dir = this.prefixDir(options);\n\n let files: string[];\n try {\n files = await fsp.readdir(dir);\n } catch (e: unknown) {\n if ((e as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw e;\n }\n\n const snapshots: SessionSnapshot<S>[] = [];\n for (const file of files) {\n if (!file.endsWith('.json')) continue;\n try {\n const contents = await fsp.readFile(path.join(dir, file), 'utf-8');\n const snap = JSON.parse(contents) as SessionSnapshot<S>;\n if ((snap.sessionId ?? snap.state?.sessionId) === sessionId) {\n snapshots.push(snap);\n }\n } catch (e: unknown) {\n if ((e as NodeJS.ErrnoException).code === 'ENOENT') continue;\n throw e;\n }\n }\n\n const leaf = selectLeafSnapshot(\n snapshots,\n sessionId,\n this.rejectBranchingSessions\n );\n if (!leaf) return undefined;\n\n // Refresh the pointer so subsequent lookups take the fast path.\n // Best-effort.\n await this.writePointer(sessionId, leaf.snapshotId, options);\n return leaf;\n }\n\n async saveSnapshot(\n snapshotId: string | undefined,\n mutator: SnapshotMutator<S>,\n options?: SessionStoreOptions\n ): Promise<string | null> {\n // When an ID is supplied the read-modify-write below must be serialized\n // against concurrent saves of the same snapshot, otherwise a later write\n // (e.g. `completed`) can clobber an earlier concurrent one (e.g.\n // `aborted`). New (UUID) snapshots have no contender, so skip the lock.\n if (snapshotId) {\n const lockPath = await this.getFilePath(snapshotId, options);\n return this.withFileLock(lockPath, () =>\n this.saveSnapshotUnlocked(snapshotId, mutator, options)\n );\n }\n return this.saveSnapshotUnlocked(snapshotId, mutator, options);\n }\n\n private async saveSnapshotUnlocked(\n snapshotId: string | undefined,\n mutator: SnapshotMutator<S>,\n options?: SessionStoreOptions\n ): Promise<string | null> {\n // Read the current snapshot when an ID is provided.\n const current = snapshotId\n ? await this.getSnapshotById(snapshotId, options)\n : undefined;\n\n const snapshot = mutator(current);\n if (snapshot === null) return null;\n\n // Determine the final ID. The runtime normally supplies a snapshotId, but\n // fall back to a fresh UUID for direct store users who omit it.\n const id =\n snapshotId || snapshot.snapshotId || globalThis.crypto.randomUUID();\n\n const full: SessionSnapshot<S> = {\n ...snapshot,\n snapshotId: id,\n };\n const filePath = await this.getFilePath(id, options);\n await this.atomicWrite(filePath, JSON.stringify(full, null, 2));\n\n // Maintain the per-session pointer so `sessionId` lookups stay fast (one\n // pointer read plus one snapshot read). Only a brand-new snapshot (`!current`)\n // is a new leaf, so only then do we advance the pointer; rewriting an\n // existing snapshot (heartbeat / status update) never changes leaf-ness, so\n // we leave the pointer alone. Snapshots without a `sessionId` are not\n // addressable by session, so skip the pointer.\n const sessionId = full.sessionId ?? full.state?.sessionId;\n if (sessionId && !current) {\n await this.writePointer(sessionId, id, options);\n }\n\n if (this.maxPersistedChainLength && this.maxPersistedChainLength > 0) {\n let cur: SessionSnapshot<S> | undefined = full;\n const chain: string[] = [];\n\n while (cur) {\n chain.push(cur.snapshotId);\n if (cur.parentId) {\n cur = await this.getSnapshotById(cur.parentId, options);\n } else {\n break;\n }\n }\n\n if (chain.length > this.maxPersistedChainLength) {\n for (let i = this.maxPersistedChainLength; i < chain.length; i++) {\n const pathToDelete = await this.getFilePath(chain[i], options);\n await fsp.unlink(pathToDelete).catch((e: unknown) => {\n if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e;\n });\n }\n }\n }\n\n return id;\n }\n\n /**\n * Writes `contents` to `filePath` atomically: write to a temp file in the\n * same directory, then rename over the target. `rename` is atomic on POSIX\n * and Windows, so a concurrent reader in {@link getSnapshot} never observes a\n * half-written (torn) file.\n */\n private async atomicWrite(filePath: string, contents: string): Promise<void> {\n const tmpPath = `${filePath}.${process.pid}.${globalThis.crypto.randomUUID()}.tmp`;\n try {\n await fsp.writeFile(tmpPath, contents, 'utf-8');\n await fsp.rename(tmpPath, filePath);\n } catch (e) {\n await fsp.unlink(tmpPath).catch(() => {});\n throw e;\n }\n }\n\n /**\n * Watches a single snapshot file for changes and invokes `callback` with the\n * parsed snapshot whenever it changes.\n *\n * Unlike {@link InMemorySessionStore}, file-backed snapshots are frequently\n * mutated by a *different* process (e.g. the request handler that received an\n * abort writes `status: 'aborted'`, while a detached background worker is the\n * one watching). Detecting that requires observing the filesystem rather than\n * in-process `saveSnapshot` calls.\n *\n * Reliability comes from two layers:\n * - `fs.watch` on the (per-tenant) prefix directory, filtered to the target\n * `<snapshotId>.json`. This is low latency but can miss events on some\n * filesystems (network mounts, certain container volumes).\n * - A polling fallback (`snapshotWatchPollIntervalMs`) that re-reads the file\n * on an interval, backstopping any events `fs.watch` drops. Its timer is\n * `unref`'d so it never keeps the process alive on its own.\n *\n * Callbacks are de-duplicated by serialized content, so the noisy/duplicate\n * events `fs.watch` emits collapse into one callback per real change.\n * Transient read errors (e.g. a partially written file mid-rewrite, or a\n * not-yet-created file) are swallowed; the next event/poll re-reads.\n *\n * @returns An unsubscribe function that stops watching and polling.\n */\n onSnapshotStateChange(\n snapshotId: string,\n callback: (snapshot: SessionSnapshot<S>) => void,\n options?: SessionStoreOptions\n ): void | (() => void) {\n const dir = this.prefixDir(options);\n fs.mkdirSync(dir, { recursive: true });\n const fileName = `${snapshotId}.json`;\n const filePath = path.join(dir, fileName);\n\n let closed = false;\n let lastSerialized: string | undefined;\n\n // Re-read the file and fire the callback only when the content actually\n // changed. `fs.watch` fires multiple events per write, so dedupe by\n // serialized content.\n const emitIfChanged = async () => {\n if (closed) return;\n let contents: string;\n try {\n contents = await fsp.readFile(filePath, 'utf-8');\n } catch (e: unknown) {\n // Missing file (not yet created) or a transient read error during a\n // concurrent rewrite: ignore and wait for the next event/poll.\n return;\n }\n if (closed || contents === lastSerialized) return;\n let snapshot: SessionSnapshot<S>;\n try {\n snapshot = JSON.parse(contents) as SessionSnapshot<S>;\n } catch {\n // Partially written file mid-rewrite: skip without updating\n // lastSerialized so the next event/poll re-reads the complete file.\n return;\n }\n lastSerialized = contents;\n callback(snapshot);\n };\n\n // Watch the directory (not the file) so this still works before the file\n // exists and survives atomic rename-replace writes that swap the inode.\n let watcher: fs.FSWatcher | undefined;\n try {\n watcher = fs.watch(dir, (_event, changed) => {\n // `changed` can be null on some platforms; re-check in that case.\n if (!changed || changed === fileName) void emitIfChanged();\n });\n } catch {\n // Some environments disallow fs.watch; the polling fallback covers us.\n }\n\n // Polling fallback: backstops events fs.watch may drop. `unref` so the\n // timer never keeps the process alive on its own.\n const pollTimer = setInterval(() => {\n void emitIfChanged();\n }, this.snapshotWatchPollIntervalMs);\n pollTimer.unref?.();\n\n // Surface the current state immediately (if the file already exists).\n void emitIfChanged();\n\n return () => {\n closed = true;\n watcher?.close();\n clearInterval(pollTimer);\n };\n }\n}\n"],"mappings":"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","names":[]}