{"version":3,"file":"opfs.cjs","names":["#handle","#threshold","#name","#load","#readRange","#ready","#init","#buildStreamingIndex","#resolveRoot","#prefix","#defaultThreshold","#keyFor","#getRoot","#writeStreamViaSyncHandle","#writeStreamViaWritable","#writeViaSyncHandle","#writeViaWritable","#isNotFoundError","#root"],"sources":["../../../src/batteries/storage/opfs/index.ts"],"sourcesContent":["/**\n * Browser-only Origin Private File System storage for spooled artifacts.\n *\n * @module @nhtio/adk/batteries/storage/opfs\n *\n * @remarks\n * Opt-in **browser-only** storage battery backed by the\n * [Origin Private File System](https://developer.mozilla.org/docs/Web/API/File_System_API/Origin_private_file_system)\n * (OPFS). Provides {@link OpfsSpoolReader} (a {@link @nhtio/adk!SpoolReader} over a `OpfsFileHandle`)\n * and {@link OpfsSpoolStore} (a `write(callId, bytes) → reader` persistence layer that wraps an\n * OPFS directory).\n *\n * The reader has two modes selected lazily on first method invocation based on the size of the\n * underlying file:\n *\n * - **Eager mode** — when `file.size` is below `streamThresholdBytes` (default 10 MiB), the\n *   reader calls `file.text()` once, splits the content on `\\n`, and caches lines + byte count.\n *   All subsequent calls resolve from memory.\n * - **Streaming mode** — when `file.size` meets or exceeds the threshold, the reader streams the\n *   file once via `file.stream().getReader()` to build a line-offset index (`number[]` of byte\n *   offsets per line), then serves each `line(i)` request by slicing the underlying `Blob` —\n *   `Blob.slice(start, end).text()` decodes only the requested range, no head-of-file scan.\n *   Caps RAM at one index + one line buffer regardless of file size.\n *\n * The store auto-selects its write API by execution scope:\n *\n * - In **worker scopes** (`self instanceof WorkerGlobalScope`), it acquires a\n *   `FileSystemSyncAccessHandle` and writes synchronously. Sync handles are the only API\n *   available in workers and the fastest path for the spool-write hot path.\n * - On the **main thread**, it uses `OpfsFileHandle.createWritable()` and the async\n *   stream API. Sync access handles are not exposed on the main thread.\n *\n * This module assumes a browser-equivalent runtime — `navigator.storage`,\n * `OpfsFileHandle`, `TextEncoder`/`TextDecoder`, and `Blob` must all exist. It must not\n * be imported from Node code; do so and you will fail at resolve time when `navigator` is\n * referenced.\n *\n * @example\n * ```ts\n * import { OpfsSpoolStore } from '@nhtio/adk/batteries/storage/opfs'\n *\n * const store = new OpfsSpoolStore({ keyPrefix: 'agent-runs/' })\n * const reader = await store.write(callId, bytes)\n * const Ctor = tool.artifactConstructor?.() ?? SpooledArtifact\n * const artifact = new Ctor(reader)\n * ```\n */\n\nimport { isInstanceOf } from '@nhtio/adk/guards'\nimport type { ReaderDescriptor, SpoolReader, SpoolStore } from '@nhtio/adk/common'\n\n/**\n * Resolver tag for the OPFS spool reader handle. The locator carries the OPFS file `name`; the live OPFS\n * root directory is re-injected by the consumer-registered resolver on decode (the name alone cannot\n * re-open the file).\n */\nexport const SPOOL_READER_TAG_OPFS = 'spool:opfs'\n\n// The project's tsconfig limits `lib` to `ESNext`, so the DOM and File System Access types\n// referenced below are not in scope by default — neither `tsc --noEmit` nor the downstream dts\n// pipeline (api-extractor) can see them. Re-declare here the **minimum** surface this module\n// touches via a local handle-shape interface. Public API uses `OpfsFileHandle` /\n// `OpfsDirectoryHandle` instead of the DOM globals so the published `.d.ts` is self-contained\n// and consumers do not have to chase the lib graph.\n\n/**\n * Minimal subset of the\n * [File System Access](https://developer.mozilla.org/docs/Web/API/File_System_API)\n * `OpfsFileHandle` interface that this module touches at runtime. Structurally compatible\n * with the DOM-lib `OpfsFileHandle` — at call sites you pass real OPFS handles directly.\n */\nexport interface OpfsFileHandle {\n  /** Discriminant: always `'file'`. */\n  readonly kind: 'file'\n  /** The entry's name. */\n  readonly name: string\n  /** Resolve a readable {@link OpfsFile} snapshot of the handle's contents. */\n  getFile(): Promise<OpfsFile>\n  /** Open a writable stream that replaces the file's contents. */\n  createWritable(): Promise<OpfsWritableFileStream>\n}\n\n/**\n * Minimal subset of the\n * [File System Access](https://developer.mozilla.org/docs/Web/API/File_System_API)\n * `OpfsDirectoryHandle` interface that this module touches at runtime. Structurally\n * compatible with the DOM-lib `OpfsDirectoryHandle` — at call sites you pass real OPFS\n * handles directly.\n */\nexport interface OpfsDirectoryHandle {\n  /** Discriminant: always `'directory'`. */\n  readonly kind: 'directory'\n  /** The directory's name. */\n  readonly name: string\n  /** Resolve a child file handle, optionally creating it. */\n  getFileHandle(name: string, options?: { create?: boolean }): Promise<OpfsFileHandle>\n  /** Resolve a child directory handle, optionally creating it. */\n  getDirectoryHandle(name: string, options?: { create?: boolean }): Promise<OpfsDirectoryHandle>\n  /** Remove a child entry, optionally recursively. */\n  removeEntry(name: string, options?: { recursive?: boolean }): Promise<void>\n}\n\n/**\n * Minimal subset of the DOM `FileSystemWritableFileStream` interface used by the OPFS battery's\n * main-thread write path.\n */\nexport interface OpfsWritableFileStream {\n  /** Append/write a chunk to the stream. */\n  write(data: Uint8Array | ArrayBufferView | ArrayBuffer | string): Promise<void>\n  /** Flush and close the stream, committing the written contents. */\n  close(): Promise<void>\n}\n\n/**\n * Minimal subset of the DOM `Blob` interface used by {@link OpfsSpoolReader} streaming-mode\n * random-access reads. Real OPFS handles return a `File` here; we narrow to the methods we\n * actually call.\n */\nexport interface OpfsBlob {\n  /** Byte length of the blob. */\n  readonly size: number\n  /** Return a sub-range of the blob as a new blob. */\n  slice(start?: number, end?: number, contentType?: string): OpfsBlob\n  /** Read the blob's contents as text. */\n  text(): Promise<string>\n  /** Open a readable byte stream over the blob. */\n  stream(): OpfsReadableStream\n}\n\n/**\n * Minimal subset of the DOM `File` interface used by {@link OpfsSpoolReader}.\n */\nexport interface OpfsFile extends OpfsBlob {\n  /** The file's name. */\n  readonly name: string\n}\n\n/**\n * Minimal subset of the DOM `ReadableStream<Uint8Array>` interface used by streaming-mode\n * index construction.\n */\nexport interface OpfsReadableStream {\n  /** Acquire a reader over the stream. */\n  getReader(): OpfsReadableStreamReader\n}\n\n/**\n * Minimal subset of the DOM `ReadableStreamDefaultReader<Uint8Array>` interface used by\n * streaming-mode index construction.\n */\nexport interface OpfsReadableStreamReader {\n  /** Read the next chunk, or signal end-of-stream with `done: true`. */\n  read(): Promise<{ done: false; value: Uint8Array } | { done: true; value: undefined }>\n  /** Release the reader's lock on the stream. */\n  releaseLock(): void\n}\n\ndeclare const navigator: {\n  storage: { getDirectory(): Promise<OpfsDirectoryHandle> }\n}\ndeclare class TextEncoder {\n  encode(input?: string): Uint8Array\n}\ndeclare const self: unknown\ndeclare const WorkerGlobalScope: { new (): unknown } | undefined\n\ninterface FileSystemSyncAccessHandle {\n  truncate(newSize: number): void\n  write(buffer: Uint8Array | ArrayBuffer | ArrayBufferView, options?: { at?: number }): number\n  flush(): void\n  close(): void\n}\ninterface OpfsFileHandleWithSyncAccess extends OpfsFileHandle {\n  createSyncAccessHandle(): Promise<FileSystemSyncAccessHandle>\n}\n\nconst DEFAULT_STREAM_THRESHOLD_BYTES = 10 * 1024 * 1024 // 10 MiB\n\nconst LF = 0x0a // '\\n'\n\nconst isNonNegativeFiniteNumber = (n: unknown): n is number =>\n  typeof n === 'number' && Number.isFinite(n) && n >= 0\n\n/**\n * Constructor options for {@link OpfsSpoolReader}.\n */\nexport interface OpfsSpoolReaderOptions {\n  /**\n   * Byte-length threshold that switches between eager and streaming modes.\n   *\n   * @remarks\n   * - Below the threshold → eager (whole-file in memory).\n   * - At or above the threshold → streaming (line-offset index + per-line slice reads).\n   *\n   * Set to `0` to force streaming mode; set to `Number.POSITIVE_INFINITY` to force eager mode.\n   *\n   * @defaultValue `10 * 1024 * 1024` (10 MiB)\n   */\n  streamThresholdBytes?: number\n\n  /**\n   * The stable OPFS file name (key) this reader was opened from.\n   *\n   * @remarks\n   * Optional. When supplied, the reader can emit a serialisable {@link @nhtio/adk!ReaderDescriptor} via\n   * `describe()` so a {@link @nhtio/adk!SpooledArtifact} backed by it round-trips through\n   * `encode()`/`decode()`. {@link OpfsSpoolStore} threads this in automatically; a reader constructed\n   * from a bare file handle with no `name` is not describable (the opaque handle has no serialisable\n   * locator) and the artifact cannot be encoded.\n   */\n  name?: string\n}\n\ninterface EagerState {\n  mode: 'eager'\n  lines: string[]\n  bytes: number\n  content: string\n}\n\ninterface StreamingState {\n  mode: 'streaming'\n  file: OpfsFile\n  /**\n   * Byte offsets where each line *starts*. Length equals lineCount + 1; the final entry equals\n   * the total byte length. So `offsets[i + 1] - offsets[i]` is the byte length of line `i`\n   * including any trailing `\\n`.\n   */\n  offsets: number[]\n  bytes: number\n}\n\ntype ReaderState = EagerState | StreamingState\n\n/**\n * Returns `true` when the current global scope is a Web Worker (`DedicatedWorkerGlobalScope`,\n * `SharedWorkerGlobalScope`, or `ServiceWorkerGlobalScope` all inherit from `WorkerGlobalScope`).\n *\n * @remarks\n * The check is needed at runtime because `FileSystemSyncAccessHandle` is only exposed in worker\n * scopes — calling it from the main thread throws. We pick the write strategy based on the\n * answer here.\n *\n * @internal\n */\nconst isWorkerScope = (): boolean => {\n  if (typeof WorkerGlobalScope === 'undefined') return false\n  // eslint-disable-next-line adk/use-is-instance-of -- native built-in narrowing on `self`; no cross-realm risk\n  return self instanceof WorkerGlobalScope\n}\n\n/**\n * Reads an OPFS-backed file as a {@link @nhtio/adk!SpoolReader}.\n *\n * @remarks\n * Constructor is **not** async — but the first method call awaits a private readiness promise\n * that fetches the underlying `File` (and in eager mode, its contents). Subsequent calls reuse\n * the cached state. This keeps construction call sites synchronous while still doing real I/O\n * lazily.\n *\n * All four `SpoolReader` methods on this reader return promises. The `SpoolReader` contract\n * supports both sync and async return; consumers of `SpooledArtifact` handle either.\n */\nexport class OpfsSpoolReader implements SpoolReader {\n  readonly #handle: OpfsFileHandle\n  readonly #threshold: number\n  readonly #name: string | undefined\n  #ready: Promise<ReaderState> | undefined\n\n  constructor(handle: OpfsFileHandle, opts: OpfsSpoolReaderOptions = {}) {\n    this.#handle = handle\n    this.#name = opts.name\n    const raw = opts.streamThresholdBytes ?? DEFAULT_STREAM_THRESHOLD_BYTES\n    // Allow `Infinity` (forces eager) but reject anything non-finite-negative.\n    if (typeof raw !== 'number' || Number.isNaN(raw) || raw < 0) {\n      throw new TypeError(\n        `OpfsSpoolReader: streamThresholdBytes must be a non-negative number or Infinity, got ${String(raw)}`\n      )\n    }\n    this.#threshold = raw\n  }\n\n  /**\n   * Returns `true` if `value` is an {@link OpfsSpoolReader} instance.\n   *\n   * @remarks\n   * Uses {@link @nhtio/adk!isInstanceOf} for cross-realm safety.\n   *\n   * @param value - The value to test.\n   * @returns `true` when `value` is an {@link OpfsSpoolReader} instance.\n   */\n  public static isOpfsSpoolReader(value: unknown): value is OpfsSpoolReader {\n    return isInstanceOf(value, 'OpfsSpoolReader', OpfsSpoolReader)\n  }\n\n  async line(index: number): Promise<string | undefined> {\n    const state = await this.#load()\n    if (state.mode === 'eager') return state.lines[index]\n    if (index < 0 || index >= state.offsets.length - 1) return undefined\n    return this.#readRange(state.file, state.offsets[index], state.offsets[index + 1])\n  }\n\n  async byteLength(): Promise<number> {\n    const state = await this.#load()\n    return state.bytes\n  }\n\n  async lineCount(): Promise<number> {\n    const state = await this.#load()\n    return state.mode === 'eager' ? state.lines.length : state.offsets.length - 1\n  }\n\n  /**\n   * Returns the full underlying content as a single decoded string, byte-faithful to the source.\n   *\n   * @remarks\n   * In **eager mode** the content is already cached at first-call load and this method is\n   * effectively a property access. In **streaming mode** there is no cache: the file is re-read\n   * (as a single `File.text()` call) on every invocation. Use `SpooledArtifact.asString()`\n   * judiciously on large streaming-mode artifacts.\n   */\n  async readAll(): Promise<string> {\n    const state = await this.#load()\n    if (state.mode === 'eager') return state.content\n    return state.file.text()\n  }\n\n  describe(): ReaderDescriptor | undefined {\n    // Describable only when the store threaded the stable file name through. A reader built from a bare\n    // opaque handle has no serialisable locator → undefined → encoding the artifact throws\n    // E_READER_NOT_DESCRIBABLE. The live OPFS root is re-injected by the registered resolver on decode.\n    if (typeof this.#name !== 'string') return undefined\n    return {\n      tag: SPOOL_READER_TAG_OPFS,\n      locator: { name: this.#name, streamThresholdBytes: this.#threshold },\n    }\n  }\n\n  /**\n   * Lazily initialise the reader's mode-specific state. Called by every public method; the\n   * promise is cached so the work runs at most once.\n   */\n  #load(): Promise<ReaderState> {\n    if (!this.#ready) this.#ready = this.#init()\n    return this.#ready\n  }\n\n  async #init(): Promise<ReaderState> {\n    const file = await this.#handle.getFile()\n    const bytes = file.size\n    if (!isNonNegativeFiniteNumber(bytes)) {\n      throw new Error(`OpfsSpoolReader: file handle returned a non-finite size (${String(bytes)})`)\n    }\n    if (bytes < this.#threshold) {\n      // Eager — pull the whole thing into memory.\n      const content = await file.text()\n      const lines = content === '' ? [] : content.split('\\n')\n      return { mode: 'eager', lines, bytes, content }\n    }\n    // Streaming — build a line-offset index by scanning bytes once.\n    return this.#buildStreamingIndex(file, bytes)\n  }\n\n  async #buildStreamingIndex(file: OpfsFile, bytes: number): Promise<StreamingState> {\n    // Edge case first — an empty file is one offset (the EOF), zero lines.\n    if (bytes === 0) return { mode: 'streaming', file, offsets: [0], bytes }\n\n    // offsets[i] is the byte position where line `i` starts. offsets[lineCount] is one-past-end.\n    // For \"a\\nb\\nc\" → offsets=[0, 2, 4, 5] (3 lines).\n    // For \"a\\nb\\n\"  → offsets=[0, 2, 4, 4] (3 lines, last is the trailing empty line). This\n    // mirrors `String.prototype.split('\\n')` semantics so streaming and eager agree.\n    const offsets: number[] = [0]\n    let position = 0\n    let lastByte = -1\n    const reader = file.stream().getReader()\n    try {\n      for (;;) {\n        const { done, value } = await reader.read()\n        if (done) break\n        for (const byte of value) {\n          position++\n          if (byte === LF) offsets.push(position)\n          lastByte = byte\n        }\n      }\n    } finally {\n      reader.releaseLock()\n    }\n    // If the file ends on a newline, the byte after the LF is the start of an empty trailing\n    // line — record it. If it doesn't, the final line's end is the EOF and we need to push\n    // it so line(N-1) can read up to bytes.\n    if (lastByte === LF) offsets.push(position)\n    else if (offsets[offsets.length - 1] !== position) offsets.push(position)\n    return { mode: 'streaming', file, offsets, bytes }\n  }\n\n  /**\n   * Slices the byte range `[start, end)` from the backing file and returns it as a UTF-8\n   * string, stripping a trailing `\\n` if present.\n   *\n   * @remarks\n   * `Blob.slice` is O(1) metadata; `Blob.text()` only decodes the slice. The line-offset index\n   * brackets each line *with* its trailing LF (so `offsets[i+1]` points at the start of the\n   * next line) and the `SpoolReader` contract returns lines *without* their trailing newline,\n   * so we strip a single trailing LF if present.\n   */\n  async #readRange(file: OpfsFile, start: number, end: number): Promise<string> {\n    if (start === end) return ''\n    const slice = file.slice(start, end)\n    const text = await slice.text()\n    if (text.length > 0 && text.charCodeAt(text.length - 1) === LF) {\n      return text.slice(0, -1)\n    }\n    return text\n  }\n}\n\n/**\n * Constructor options for {@link OpfsSpoolStore}.\n */\nexport interface OpfsSpoolStoreOptions {\n  /**\n   * Optional thunk that resolves the {@link OpfsDirectoryHandle} used as the store root.\n   *\n   * @remarks\n   * When omitted, the store resolves the root via `navigator.storage.getDirectory()` on its\n   * first filesystem call. Override for tests (to point at a per-suite subdirectory) or to\n   * scope the store to a nested directory inside OPFS.\n   *\n   * The thunk is invoked at most once per store; the returned handle is memoised.\n   */\n  directory?: () => Promise<OpfsDirectoryHandle>\n\n  /**\n   * Optional filename prefix prepended to every `callId`.\n   *\n   * @remarks\n   * Prefix is a **filename prefix**, not a subdirectory — `keyPrefix: 'agent-runs/'` produces\n   * a file literally named `agent-runs/<callId>` at the root, not a nested directory. (OPFS\n   * filenames may not contain `/`, so use a non-`/` separator like `-` if you want a flat\n   * namespace.) This mirrors the `keyPrefix` semantics in the flydrive and in-memory batteries.\n   *\n   * @defaultValue `\"\"`\n   */\n  keyPrefix?: string\n\n  /**\n   * Default `streamThresholdBytes` for readers produced by `write()` and `read()`. Individual\n   * calls may override via their own `opts` argument.\n   *\n   * @defaultValue `10 * 1024 * 1024` (10 MiB)\n   */\n  streamThresholdBytes?: number\n}\n\n/**\n * \"Give bytes, get a reader\" persistence layer over an OPFS directory.\n *\n * @remarks\n * `write(callId, bytes)` resolves the root directory (lazily, on first call), opens or creates\n * the file named `keyPrefix + callId`, then writes via the API matching the current scope:\n * a `FileSystemSyncAccessHandle` in worker scopes, `OpfsFileHandle.createWritable()` on\n * the main thread. A fresh {@link OpfsSpoolReader} pointed at the same file is returned.\n *\n * `read(callId)` returns a reader without re-writing; `delete(callId)` removes the entry.\n *\n * The store is otherwise stateless — it owns no in-memory cache of writes. Multiple\n * `OpfsSpoolStore` instances sharing the same root directory and key prefix see the same data.\n *\n * @example\n * ```ts\n * import { OpfsSpoolStore } from '@nhtio/adk/batteries/storage/opfs'\n *\n * const store = new OpfsSpoolStore({ keyPrefix: 'agent-runs/' })\n *\n * const bytes = await tool.executor(ctx)(args)\n * const reader = await store.write(callId, bytes)\n * const Ctor = tool.artifactConstructor?.() ?? SpooledArtifact\n * const artifact = new Ctor(reader)\n * ```\n */\nexport class OpfsSpoolStore implements SpoolStore {\n  readonly #resolveRoot: () => Promise<OpfsDirectoryHandle>\n  readonly #prefix: string\n  readonly #defaultThreshold: number\n  #root: OpfsDirectoryHandle | undefined\n\n  constructor(opts: OpfsSpoolStoreOptions = {}) {\n    this.#resolveRoot = opts.directory ?? (() => navigator.storage.getDirectory())\n    this.#prefix = opts.keyPrefix ?? ''\n    // Fail fast on a path separator in the prefix. keyPrefix is a FILENAME prefix (it becomes\n    // `keyPrefix + callId` as ONE OPFS file name), and OPFS file names may not contain '/' or '\\' —\n    // getFileHandle would otherwise throw \"Name is not allowed\" on EVERY spooled write at runtime,\n    // a silent footgun (e.g. `keyPrefix: 'agent-spool/'` expecting a subdirectory). Reject it here\n    // with an actionable message instead. Use a flat prefix like 'agent-spool-' for namespacing.\n    if (this.#prefix.includes('/') || this.#prefix.includes('\\\\')) {\n      throw new RangeError(\n        `OpfsSpoolStore keyPrefix is a filename prefix, not a subdirectory, and must not contain '/' or '\\\\': received ${JSON.stringify(\n          this.#prefix\n        )}. Use a flat prefix such as '${this.#prefix.replace(/[/\\\\]+/g, '-')}'.`\n      )\n    }\n    this.#defaultThreshold = opts.streamThresholdBytes ?? DEFAULT_STREAM_THRESHOLD_BYTES\n  }\n\n  /**\n   * Returns `true` if `value` is an {@link OpfsSpoolStore} instance.\n   *\n   * @remarks\n   * Uses {@link @nhtio/adk!isInstanceOf} for cross-realm safety.\n   *\n   * @param value - The value to test.\n   * @returns `true` when `value` is an {@link OpfsSpoolStore} instance.\n   */\n  public static isOpfsSpoolStore(value: unknown): value is OpfsSpoolStore {\n    return isInstanceOf(value, 'OpfsSpoolStore', OpfsSpoolStore)\n  }\n\n  /**\n   * Persists `bytes` under `callId` and returns a reader bound to the stored key.\n   *\n   * @remarks\n   * `string` input is encoded as UTF-8; `Uint8Array` is stored byte-faithfully;\n   * `ReadableStream<Uint8Array>` is written incrementally — the stream is consumed chunk-by-chunk\n   * straight to OPFS without first materializing the whole payload in memory, which is the point\n   * of accepting a stream for a durable store.\n   *\n   * @param callId - Identifier used to retrieve the bytes via {@link OpfsSpoolStore.read}.\n   * @param bytes - The bytes to store, as a `string`, `Uint8Array`, or `ReadableStream<Uint8Array>`.\n   * @param opts - Per-call override for `streamThresholdBytes`.\n   * @returns An {@link OpfsSpoolReader} over the stored bytes.\n   */\n  async write(\n    callId: string,\n    bytes: string | Uint8Array | ReadableStream<Uint8Array>,\n    opts?: OpfsSpoolReaderOptions\n  ): Promise<OpfsSpoolReader> {\n    const name = this.#keyFor(callId)\n    const root = await this.#getRoot()\n    const handle = await root.getFileHandle(name, { create: true })\n    if (isInstanceOf(bytes, 'ReadableStream', ReadableStream)) {\n      if (isWorkerScope()) {\n        await this.#writeStreamViaSyncHandle(handle, bytes)\n      } else {\n        await this.#writeStreamViaWritable(handle, bytes)\n      }\n    } else {\n      const payload = typeof bytes === 'string' ? new TextEncoder().encode(bytes) : bytes\n      if (isWorkerScope()) {\n        await this.#writeViaSyncHandle(handle, payload)\n      } else {\n        await this.#writeViaWritable(handle, payload)\n      }\n    }\n    return new OpfsSpoolReader(handle, {\n      name,\n      streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold,\n    })\n  }\n\n  /**\n   * Returns a reader over the bytes previously written under `callId`.\n   *\n   * @remarks\n   * Returns `undefined` if the file does not exist.\n   *\n   * @param callId - Identifier supplied to a prior {@link OpfsSpoolStore.write} call.\n   * @param opts - Per-call override for `streamThresholdBytes`.\n   * @returns An {@link OpfsSpoolReader}, or `undefined` if the key is missing.\n   */\n  async read(callId: string, opts?: OpfsSpoolReaderOptions): Promise<OpfsSpoolReader | undefined> {\n    const name = this.#keyFor(callId)\n    const root = await this.#getRoot()\n    let handle: OpfsFileHandle\n    try {\n      handle = await root.getFileHandle(name)\n    } catch (err) {\n      if (this.#isNotFoundError(err)) return undefined\n      throw err\n    }\n    return new OpfsSpoolReader(handle, {\n      name,\n      streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold,\n    })\n  }\n\n  /**\n   * Removes the entry under `callId`.\n   *\n   * @param callId - Identifier whose entry should be removed.\n   * @returns `true` if the entry existed and was removed; `false` if it didn't exist.\n   */\n  async delete(callId: string): Promise<boolean> {\n    const name = this.#keyFor(callId)\n    const root = await this.#getRoot()\n    try {\n      await root.removeEntry(name)\n      return true\n    } catch (err) {\n      if (this.#isNotFoundError(err)) return false\n      throw err\n    }\n  }\n\n  /**\n   * Returns `true` if a file is present under `callId`.\n   *\n   * @param callId - Identifier to test.\n   * @returns `true` when the file exists, `false` otherwise.\n   */\n  async has(callId: string): Promise<boolean> {\n    const name = this.#keyFor(callId)\n    const root = await this.#getRoot()\n    try {\n      await root.getFileHandle(name)\n      return true\n    } catch (err) {\n      if (this.#isNotFoundError(err)) return false\n      throw err\n    }\n  }\n\n  /**\n   * Returns the full filename for a given `callId` (i.e. `keyPrefix + callId`).\n   *\n   * @remarks\n   * Useful for tests or for callers that want to interact with the underlying OPFS directory\n   * directly.\n   */\n  keyFor(callId: string): string {\n    return this.#keyFor(callId)\n  }\n\n  #keyFor(callId: string): string {\n    return this.#prefix + callId\n  }\n\n  async #getRoot(): Promise<OpfsDirectoryHandle> {\n    if (!this.#root) this.#root = await this.#resolveRoot()\n    return this.#root\n  }\n\n  async #writeViaSyncHandle(handle: OpfsFileHandle, payload: Uint8Array): Promise<void> {\n    const sync = await (handle as OpfsFileHandleWithSyncAccess).createSyncAccessHandle()\n    try {\n      sync.truncate(0)\n      sync.write(payload, { at: 0 })\n      sync.flush()\n    } finally {\n      sync.close()\n    }\n  }\n\n  async #writeViaWritable(handle: OpfsFileHandle, payload: Uint8Array): Promise<void> {\n    const writable = await handle.createWritable()\n    try {\n      await writable.write(payload)\n    } finally {\n      await writable.close()\n    }\n  }\n\n  async #writeStreamViaWritable(\n    handle: OpfsFileHandle,\n    stream: ReadableStream<Uint8Array>\n  ): Promise<void> {\n    const writable = await handle.createWritable()\n    try {\n      const reader = stream.getReader()\n      try {\n        for (;;) {\n          const { done, value } = await reader.read()\n          if (done) break\n          if (value) await writable.write(value)\n        }\n      } finally {\n        reader.releaseLock()\n      }\n    } finally {\n      await writable.close()\n    }\n  }\n\n  async #writeStreamViaSyncHandle(\n    handle: OpfsFileHandle,\n    stream: ReadableStream<Uint8Array>\n  ): Promise<void> {\n    const sync = await (handle as OpfsFileHandleWithSyncAccess).createSyncAccessHandle()\n    try {\n      sync.truncate(0)\n      let at = 0\n      const reader = stream.getReader()\n      try {\n        for (;;) {\n          const { done, value } = await reader.read()\n          if (done) break\n          if (value && value.byteLength > 0) {\n            sync.write(value, { at })\n            at += value.byteLength\n          }\n        }\n      } finally {\n        reader.releaseLock()\n      }\n      sync.flush()\n    } finally {\n      sync.close()\n    }\n  }\n\n  #isNotFoundError(err: unknown): boolean {\n    if (err === null || typeof err !== 'object') return false\n    const name = (err as { name?: unknown }).name\n    return name === 'NotFoundError'\n  }\n}\n"],"mappings":"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"}