Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
# @suaveplan/storage-browser-opfs - Browser OPFS AbstractStorage adapter for @suaveplan/storage: origin-scoped persistent file storage via navigator.storage.getDirectory(), AbortSignal everywhere, strict key sanitisation, native Web Streams, native partial reads + partial writes, DOMException → typed OpfsError mapping, full OTel surface.
Browser-tier `AbstractStorage` adapter backed by the Origin Private File System (OPFS). Provides persistent, origin-scoped key-value storage for browser consumers (vault snapshots, drive chunk caches, in-app i18n catalogues, SQLite-WASM backing files), with full `AbstractStorage` conformance, native Web Streams, native partial reads + partial writes, AbortSignal threading, Zod-validated options, structured `OpfsError` boundary, and a complete OpenTelemetry surface.
<!-- begin:auto-docs -->
## Documentation tree
- [filename-codec](./src/filename-codec/filename-codec.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [filename-codec](./src/filename-codec/filename-codec.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [in-memory-opfs](./src/in-memory-opfs/in-memory-opfs.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [in-memory-opfs](./src/in-memory-opfs/in-memory-opfs.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [key-sanitizer](./src/key-sanitizer/key-sanitizer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [key-sanitizer](./src/key-sanitizer/key-sanitizer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [opfs-adapter](./src/opfs-adapter/opfs-adapter.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [opfs-adapter](./src/opfs-adapter/opfs-adapter.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [opfs-storage](./src/opfs-storage/opfs-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - opfs-storage.ops — Hosts pure / nearly-pure helpers…
- [opfs-storage.errors](./src/opfs-storage/opfs-storage.errors.md) — Encapsulates the mapping between WHATWG `DOMException` shapes thrown by the OPFS API and the typed `OpfsError` codes th…
- [opfs-storage](./src/opfs-storage/opfs-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - opfs-storage.ops — Hosts pure / nearly-pure helpers…
- [opfs-storage.ops](./src/opfs-storage/opfs-storage.ops.md) — Hosts pure / nearly-pure helpers that `OpfsStorage` calls but that would push the class file over the 300-line hard bud…
- [opfs-storage.streams](./src/opfs-storage/opfs-storage.streams.md) — Factors the `createReadStream` / `createWriteStream` builders out of the main `OpfsStorage` class so the class stays un…
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-codes](./src/error-codes.md) — Single source of truth for every error code the `@suaveplan/storage-browser-opfs` package can throw. The registry uses…
<!-- end:auto-docs -->
- [filename-codec](./src/filename-codec/filename-codec.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory-opfs](./src/in-memory-opfs/in-memory-opfs.md) — <!-- begin:auto-docs --> _(no submodules)_ - [in-memory-opfs](./src/in-memory-opfs/in-memory-opfs.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-sanitizer](./src/key-sanitizer/key-sanitizer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [key-sanitizer](./src/key-sanitizer/key-sanitizer.md) — <!-- begin:auto-docs --> _(no submodules)_- [opfs-adapter](./src/opfs-adapter/opfs-adapter.md) — <!-- begin:auto-docs --> _(no submodules)_ - [opfs-adapter](./src/opfs-adapter/opfs-adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- [opfs-storage](./src/opfs-storage/opfs-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ - [opfs-storage.errors](./src/opfs-storage/opfs-storage.errors.md) — Encapsulates the mapping between WHATWG `DOMException` shapes thrown by the OPFS API and the typed `OpfsError` codes th…
- [opfs-storage](./src/opfs-storage/opfs-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ - [opfs-storage.ops](./src/opfs-storage/opfs-storage.ops.md) — Hosts pure / nearly-pure helpers that `OpfsStorage` calls but that would push the class file over the 300-line hard bud…
- [opfs-storage.streams](./src/opfs-storage/opfs-storage.streams.md) — Factors the `createReadStream` / `createWriteStream` builders out of the main `OpfsStorage` class so the class stays un…
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_ - [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Single source of truth for every error code the `@suaveplan/storage-browser-opfs` package can throw. The registry uses…
## Install
```bash
bun add @suaveplan/storage-browser-opfs
```
## Quick start
```ts
import {
createOpfsStorage,
isOpfsAvailable,
} from "@suaveplan/storage-browser-opfs";
if (!isOpfsAvailable()) {
throw new Error("This browser does not support OPFS");
}
const storage = createOpfsStorage({ directory: "myapp" });
await storage.set("user/42", new TextEncoder().encode("hello"));
const back = await storage.get("user/42");
```
## Factory integration
```ts
import { registerOpfsProvider } from "@suaveplan/storage-browser-opfs/registry";
import { createStorage } from "@suaveplan/storage/factory";
registerOpfsProvider();
const storage = createStorage({ type: "opfs", config: { directory: "myapp" } });
```
The package does NOT self-register at module-evaluation time — call `registerOpfsProvider()` explicitly so `sideEffects: false` bundlers can tree-shake the factory path when not used.
## Capabilities
| Capability | Value | Notes |
|---|---|---|
| `partialReads` | `true` | Via `File.slice(...).arrayBuffer()`. |
| `partialWrites` | `true` | Via `FileSystemWritableFileStream.seek()`. |
| `nativeStreams` | `true` | `File.stream()` for read; buffered close-on-commit for write. |
| `atomicWrites` | `false` | Close-as-commit semantics within a single realm; no cross-realm locking. |
| `contentAddressable` | `false` | Keys are caller-supplied strings. |
## Modules
- [`src/error-codes.ts`](./src/error-codes.md) — `OPFS_CODES` registry + `OpfsError`.
- [`src/key-sanitizer/key-sanitizer.ts`](./src/key-sanitizer/key-sanitizer.md) — strict key boundary.
- [`src/filename-codec/filename-codec.ts`](./src/filename-codec/filename-codec.md) — caller-key ↔ OPFS-filename round-trip.
- [`src/opfs-adapter/opfs-adapter.ts`](./src/opfs-adapter/opfs-adapter.md) — `defaultOpfs()` + `isOpfsAvailable()`.
- [`src/opfs-storage/opfs-storage.ts`](./src/opfs-storage/opfs-storage.md) — the `OpfsStorage` class.
- [`src/opfs-storage/opfs-storage.errors.ts`](./src/opfs-storage/opfs-storage.errors.md) — DOMException mapping.
- [`src/opfs-storage/opfs-storage.ops.ts`](./src/opfs-storage/opfs-storage.ops.md) — internal helpers.
- [`src/opfs-storage/opfs-storage.streams.ts`](./src/opfs-storage/opfs-storage.streams.md) — `ReadableStream`/`WritableStream` builders.
- [`src/otel/otel.ts`](./src/otel/otel.md) — OpenTelemetry instrument set.
- [`src/registry/registry.ts`](./src/registry/registry.md) — `registerOpfsProvider()`.
- [`src/in-memory-opfs/in-memory-opfs.ts`](./src/in-memory-opfs/in-memory-opfs.md) — real-implementation test double, exposed under `./in-memory-opfs` for downstream test packages.
- [`src/types/opfs-like.types.ts`](./src/types/opfs-like.types.md) — structural OPFS surface.
- [`src/types/opfs-storage.types.ts`](./src/types/opfs-storage.types.md) — option boundary.
## License
MIT.

View File

@@ -0,0 +1,54 @@
# `error-codes` — OPFS error registry
## Purpose
Single source of truth for every error code the `@suaveplan/storage-browser-opfs` package can throw. The registry uses `defineCodes("OPFS", [...])` from `@suaveplan/error` so each code is namespaced (`OPFS.NOT_FOUND`, `OPFS.QUOTA_EXCEEDED`, etc.), frozen at module load, and surfaced via a typed `OpfsErrorCode` union. Centralising the registry keeps the boundary error vocabulary tiny, prevents drift between the implementation and the spec, and lets callers pattern-match on a stable string code rather than parsing message text.
## Features
- Nine frozen codes covering every observable failure mode: `NOT_FOUND`, `PERMISSION_DENIED`, `QUOTA_EXCEEDED`, `UNAVAILABLE`, `INVALID_KEY`, `KEY_TOO_LONG`, `INVALID_OPTIONS`, `ABORTED`, `IO_FAILED`.
- `OpfsError` extends `SuaveplanError<OpfsErrorCode>` with an optional `key` discriminant.
- `cause` chain preserved from upstream `DOMException` instances.
- TypeScript-narrowed `OpfsErrorCode` union for exhaustive switches.
## Usage: Basic Example
```ts
import { OPFS_CODES, OpfsError } from "@suaveplan/storage-browser-opfs";
throw new OpfsError(OPFS_CODES.NOT_FOUND, "missing entry", { key: "user/42" });
```
## Usage: Advanced Example
```ts
import {
OPFS_CODES,
OpfsError,
type OpfsErrorCode,
} from "@suaveplan/storage-browser-opfs";
function handle(err: unknown): "retry" | "fail" | "ignore" {
if (!(err instanceof OpfsError)) return "fail";
const code: OpfsErrorCode = err.code;
switch (code) {
case OPFS_CODES.NOT_FOUND:
return "ignore";
case OPFS_CODES.QUOTA_EXCEEDED:
case OPFS_CODES.IO_FAILED:
return "retry";
default:
return "fail";
}
}
```
## API Reference
- `OPFS_CODES` — frozen `Readonly<Record<CodeName, "OPFS.CodeName">>` produced by `defineCodes`.
- `OpfsErrorCode` — the union type of every member of `OPFS_CODES`.
- `OpfsError extends SuaveplanError<OpfsErrorCode>` — constructor `(code, message, options?)` where `options.key` carries the offending key for correlation and `options.cause` chains the original error.
## Implementation Notes
The registry is frozen at module load — adding a new code requires changing this file, which forces the spec delta to be updated in lockstep. `OpfsError.name` is set to `"OpfsError"` so devtools display useful labels. The `key` discriminant is the only optional field on the error class because no other piece of error context is universal across the nine paths; callers needing more should consult `cause`.

View File

@@ -0,0 +1,69 @@
# `filename-codec` — caller key ↔ OPFS filename
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
OPFS filenames inherit filesystem-level constraints that caller keys must not (case-insensitivity on some platforms, no embedded `/` separator within a single entry name, byte-encoding ambiguity across browsers). This module wraps each key in RFC 4648 base64url over its UTF-8 byte representation, producing OPFS-safe filenames that round-trip every caller key — including unicode, mixed scripts, slashes, and any byte the caller threw at us. Storing the codec as a separate module keeps the algorithm independently testable (property-based round-trip suite) and replaceable (a future hash-addressed mode can plug in without touching `OpfsStorage`).
## Features
- RFC 4648 base64url with `+`→`-`, `/`→`_`, padding stripped.
- UTF-8 fatal decode rejects malformed byte sequences with a typed `OpfsError`.
- Round-trip invariant proven by a `fast-check` property test (200 random inputs).
- Output regex: `^[A-Za-z0-9_-]*$` — accepted by every OPFS implementation.
- Pure / synchronous / zero workspace dependencies.
## Usage: Basic Example
```ts
import {
decodeFilename,
encodeFilename,
} from "@suaveplan/storage-browser-opfs/filename-codec";
const filename = encodeFilename("orders/2026/05.json"); // "b3JkZXJzLzIwMjYvMDUuanNvbg"
const original = decodeFilename(filename); // "orders/2026/05.json"
```
## Usage: Advanced Example
```ts
import {
decodeFilename,
encodeFilename,
OPFS_CODES,
OpfsError,
} from "@suaveplan/storage-browser-opfs";
function listKeysSafely(entries: { name: string }[]): string[] {
const out: string[] = [];
for (const entry of entries) {
try {
out.push(decodeFilename(entry.name));
} catch (err) {
// Skip entries that aren't ours — a foreign tool may have written
// something to the OPFS directory we share.
if (
err instanceof OpfsError &&
err.code === OPFS_CODES.INVALID_KEY
) {
continue;
}
throw err;
}
}
return out;
}
```
## API Reference
- `encodeFilename(key: string): string` — converts to base64url-without-padding.
- `decodeFilename(filename: string): string` — throws `OpfsError(OPFS.INVALID_KEY)` for malformed base64 or invalid UTF-8.
## Implementation Notes
The codec is deliberately byte-identical to the WHATWG spec for base64url to keep filenames stable across runtimes — the same OPFS directory written from Chrome must round-trip from Safari and Firefox. `btoa`/`atob` are universally available in browsers and in Node.js 20+, so no polyfill is needed at runtime. The fatal `TextDecoder` makes the codec self-healing: a foreign tool that writes non-base64 entries to the OPFS directory will be skipped by `decodeFilename`-using callers rather than corrupting their key view.

View File

@@ -0,0 +1,60 @@
# `in-memory-opfs` — real `OpfsLike` test double
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
A faithful in-memory implementation of the `OpfsLike` structural interface and its companion directory / file / writable-stream surfaces. The double exists because the test suite for this package — and for any downstream consumer of `OpfsStorage` — needs to exercise the full OPFS API without booting a real browser. CLAUDE.md forbids `vi.mock` / `vi.stubGlobal`, so the test double is a real implementation that the test code constructs and injects via the `opfs` option. The implementation preserves bytes verbatim, honours per-file quotas, supports the seek-then-write pattern `OpfsStorage.setPartial` exercises, and emits the same `DOMException`-shaped errors (`NotFoundError`, `QuotaExceededError`, `NotAllowedError`) the real browser throws.
Exposing the double under the `./in-memory-opfs` subpath lets downstream packages (e.g. test fixtures, contract conformance suites) reuse it without re-implementing the (subtle) WHATWG semantics around `keepExistingData`, `seek`, and stream lifecycle.
## Features
- Full coverage of every `OpfsLike` / `OpfsDirectoryHandle` / `OpfsFileHandle` / `OpfsFile` / `OpfsWritableStream` method that `OpfsStorage` calls.
- Faithful `seek()` semantics: position cursor on the underlying buffer, growable on overflow, zero-fill on sparse extension.
- `keepExistingData: true` honoured for the seek-then-patch flow.
- Configurable per-file quota; over-quota writes throw `QuotaExceededDomException` matching the real browser's error name.
- Injectable clock for deterministic `lastModified` in tests.
## Usage: Basic Example
```ts
import { createInMemoryOpfs } from "@suaveplan/storage-browser-opfs/in-memory-opfs";
import { OpfsStorage } from "@suaveplan/storage-browser-opfs";
const opfs = createInMemoryOpfs();
const storage = new OpfsStorage({ opfs });
await storage.set("k", new Uint8Array([1, 2, 3]));
```
## Usage: Advanced Example
```ts
import {
createInMemoryOpfs,
QuotaExceededDomException,
} from "@suaveplan/storage-browser-opfs/in-memory-opfs";
import { OpfsStorage, OPFS_CODES, OpfsError } from "@suaveplan/storage-browser-opfs";
const opfs = createInMemoryOpfs({ quotaBytes: 16, now: () => 1_700_000_000_000 });
const storage = new OpfsStorage({ opfs });
try {
await storage.set("k", new Uint8Array(32));
} catch (err) {
if (err instanceof OpfsError) {
// err.code === OPFS_CODES.QUOTA_EXCEEDED, err.cause instanceof QuotaExceededDomException
}
}
```
## API Reference
- `createInMemoryOpfs(options?: InMemoryOpfsOptions): OpfsLike` — `options.quotaBytes` (default 100 MiB), `options.now` (default `Date.now`).
- `InMemoryOpfsDirectoryHandle` — the concrete root-directory class.
- `NotFoundDomException`, `QuotaExceededDomException`, `NotAllowedDomException` — `Error` subclasses with the real browser's `.name` strings.
## Implementation Notes
The double's buffer grows on demand (`Uint8Array` reallocation) to support sparse writes from `seek` past the current end-of-file — the gap is implicitly zero-filled because `Uint8Array` defaults to zero. The writable stream wraps a real `WritableStream<Uint8Array>` so callers get genuine back-pressure and abort semantics; `seek` mutates the cursor state shared between the writer closure and the wrapper class. Per-file quota is enforced at write time so over-quota writes fail synchronously, matching the real browser. `entries()` yields children in insertion order — the WHATWG spec doesn't guarantee an order, but stable iteration keeps the test suite deterministic.

View File

@@ -0,0 +1,63 @@
# `key-sanitizer` — strict caller-key boundary
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`sanitizeKey(key)` is the single validation funnel through which every caller-supplied key passes before any OPFS call is issued. It enforces the storage-browser-opfs boundary contract: keys must be non-empty strings of bounded length, must not contain control characters or path-traversal segments, must not be absolute, and must use POSIX separators internally. Centralising this guard prevents adapter callers from accidentally exfiltrating data outside the configured OPFS sub-directory or constructing filenames the OPFS API will silently misinterpret.
## Features
- Rejects empty strings, non-string input, keys over 1024 chars, control chars (0x00-0x1F, 0x7F), absolute POSIX paths, and `..` traversal segments.
- Normalises Windows `\` separators to POSIX `/` so cross-platform callers see consistent layout.
- Throws typed `OpfsError(OPFS.INVALID_KEY | OPFS.KEY_TOO_LONG)` with the offending key captured for log correlation.
- Pure / synchronous / dependency-free — safe to call in hot paths.
## Usage: Basic Example
```ts
import { sanitizeKey } from "@suaveplan/storage-browser-opfs/key-sanitizer";
const safe = sanitizeKey("orders/2026/05/12.json");
// → "orders/2026/05/12.json"
```
## Usage: Advanced Example
```ts
import {
OPFS_CODES,
OpfsError,
sanitizeKey,
} from "@suaveplan/storage-browser-opfs";
function safeKeyOrNull(input: unknown): string | null {
if (typeof input !== "string") return null;
try {
return sanitizeKey(input);
} catch (err) {
if (
err instanceof OpfsError &&
(err.code === OPFS_CODES.INVALID_KEY ||
err.code === OPFS_CODES.KEY_TOO_LONG)
) {
return null;
}
throw err;
}
}
```
## API Reference
`sanitizeKey(key: string): string`
- **Throws** `OpfsError(OPFS.INVALID_KEY)` for empty / non-string / control-char / absolute / traversal-segment input.
- **Throws** `OpfsError(OPFS.KEY_TOO_LONG)` for keys with `length > 1024`.
- **Returns** the normalised POSIX-separator key on success.
## Implementation Notes
The control-char scan is a single linear pass over `charCodeAt(i)` — it costs less than a regex compile for typical key sizes. The traversal check splits on `/` after backslash normalisation to ensure forms like `a\..\b` are rejected, not just `a/../b`. Windows drive-letter prefixes (`C:\…`) are NOT rejected here because OPFS has no notion of drive letters and the leading `:` is harmless to OPFS; `storage-file` does reject them because the Node.js fs layer would interpret them.

View File

@@ -0,0 +1,57 @@
# `opfs-adapter` — default `OpfsLike` and feature detector
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the production binding of the `OpfsLike` structural interface to the browser's `navigator.storage.getDirectory()` API, plus a side-effect-free feature detector consumers call before constructing an `OpfsStorage`. Keeping this in its own module — rather than inside `OpfsStorage` — lets the storage class take an injected `OpfsLike` and stay environment-agnostic, while the production path resolves the real browser API lazily on first `getDirectory()`.
## Features
- `defaultOpfs()` returns an `OpfsLike` whose `getDirectory()` calls the live browser API.
- Missing API surfaces as `OpfsError(OPFS.UNAVAILABLE)` rather than `TypeError` from a missing-property access.
- `isOpfsAvailable()` is synchronous, has no side effects, and never throws — safe to call during render.
- Resolves `navigator` from `globalThis` so the module loads cleanly in Node, Bun, and worker environments.
## Usage: Basic Example
```ts
import {
defaultOpfs,
isOpfsAvailable,
OpfsStorage,
} from "@suaveplan/storage-browser-opfs";
if (!isOpfsAvailable()) {
// Fall back to MemoryStorage or another adapter.
}
const storage = new OpfsStorage({ opfs: defaultOpfs() });
```
## Usage: Advanced Example
```ts
import {
defaultOpfs,
isOpfsAvailable,
MemoryStorage,
OpfsStorage,
} from "@suaveplan/storage-browser-opfs";
import type { AbstractStorage } from "@suaveplan/storage/abstract";
export function pickStorage(): AbstractStorage {
if (isOpfsAvailable()) return new OpfsStorage({ opfs: defaultOpfs() });
return new MemoryStorage();
}
```
## API Reference
- `defaultOpfs(): OpfsLike` — `getDirectory()` throws `OpfsError(OPFS.UNAVAILABLE)` when missing.
- `isOpfsAvailable(): boolean` — pure feature detector.
## Implementation Notes
`defaultOpfs()` returns a fresh `OpfsLike` object on every call so callers can hold different references; the underlying `getDirectory` getter is read at each invocation so consumers that polyfill `navigator.storage` after this module loads still benefit. The Boolean detector deliberately checks `typeof getDirectory === "function"` rather than `"getDirectory" in storage` to weed out polyfills that stub the property without a callable.

View File

@@ -0,0 +1,58 @@
# `opfs-storage.errors` — DOMException → OpfsError boundary
## Purpose
Encapsulates the mapping between WHATWG `DOMException` shapes thrown by the OPFS API and the typed `OpfsError` codes the storage class surfaces. Centralising the mapping keeps the main `OpfsStorage` class focused on the happy path and prevents the boundary contract from drifting as new error kinds emerge. The helpers also handle the `AbortSignal` short-circuit so every method invocation can call a single `assertNotAborted` guard at the top.
## Features
- `isNotFoundError(err)` — detects browser `NotFoundError` (including from the in-memory test double).
- `mapOpfsError(err, key)` — maps `NotAllowedError → OPFS.PERMISSION_DENIED`, `QuotaExceededError → OPFS.QUOTA_EXCEEDED`, anything else → `OPFS.IO_FAILED` with `cause` preserved. Pre-existing `OpfsError` instances pass through unchanged so callee-thrown codes (e.g. `OPFS.UNAVAILABLE` from `defaultOpfs()`) reach the boundary.
- `assertNotAborted(signal, key)` — throws `OPFS.ABORTED` when the signal is already aborted before any OPFS call is issued.
## Usage: Basic Example
```ts
import {
assertNotAborted,
isNotFoundError,
mapOpfsError,
} from "./opfs-storage.errors.js";
async function readBytes(handle: OpfsFileHandle, signal?: AbortSignal) {
assertNotAborted(signal, "k");
try {
return new Uint8Array(await (await handle.getFile()).arrayBuffer());
} catch (err) {
if (isNotFoundError(err)) return undefined;
throw mapOpfsError(err, "k");
}
}
```
## Usage: Advanced Example
```ts
import { OPFS_CODES, OpfsError } from "@suaveplan/storage-browser-opfs";
import { mapOpfsError } from "./opfs-storage.errors.js";
try {
await someOpfsOperation();
} catch (err) {
const mapped = mapOpfsError(err, "user/42");
if (mapped.code === OPFS_CODES.QUOTA_EXCEEDED) {
// Surface a user-facing "you're out of space" message.
}
throw mapped;
}
```
## API Reference
- `isNotFoundError(err: unknown): boolean`.
- `mapOpfsError(err: unknown, key: string | undefined): OpfsError`.
- `assertNotAborted(signal: AbortSignal | undefined, key: string | undefined): void` — throws `OpfsError(OPFS.ABORTED)`.
## Implementation Notes
The detection branch tests `err.name === "NotFoundError"` rather than `err instanceof DOMException` because the in-memory test double cannot extend `DOMException` (the global isn't constructable in every runtime). Using `.name` lets the mapping work uniformly against both real `DOMException` instances and the test double's `Error` subclasses. `mapOpfsError` always preserves the underlying error via `cause` so callers can inspect the original `DOMException` when needed.

View File

@@ -0,0 +1,83 @@
# `opfs-storage` — `OpfsStorage` AbstractStorage adapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - opfs-storage.ops — Hosts pure / nearly-pure helpers…
- [opfs-storage.ops](./opfs-storage.ops.md) — Hosts pure / nearly-pure helpers that `OpfsStorage` calls but that would push the class file over the 300-line hard bud…
- [opfs-storage.streams](./opfs-storage.streams.md) — Factors the `createReadStream` / `createWriteStream` builders out of the main `OpfsStorage` class so the class stays un…
- [opfs-storage.ops](./opfs-storage.ops.md) — Hosts pure / nearly-pure helpers that `OpfsStorage` calls but that would push the class file over the 300-line hard bud…
- [opfs-storage.streams](./opfs-storage.streams.md) — Factors the `createReadStream` / `createWriteStream` builders out of the main `OpfsStorage` class so the class stays un…
## Purpose
`OpfsStorage` is the browser-tier implementation of `AbstractStorage` from `@suaveplan/storage`, backed by the Origin Private File System (OPFS) — the standard origin-scoped persistent file storage primitive accessed via `navigator.storage.getDirectory()`. It exposes the full abstract surface (`get`/`set`/`delete`/`exists`/`list`/`clear`/`getWithMetadata`/`getMetadata`/`getSize`/`getPartial`/`setPartial`/`getItemSize`/`createReadStream`/`createWriteStream`), threads `AbortSignal` through every async path, validates every caller key against `sanitizeKey`, maps `DOMException`s to typed `OpfsError` codes at the boundary, and records OTel metrics for every operation.
## Features
- Extends `AbstractStorage` byte-for-byte — `instanceof AbstractStorage` is `true` and every abstract method is implemented natively.
- `capabilities: { partialReads: true, partialWrites: true, nativeStreams: true, atomicWrites: false, contentAddressable: false }`.
- Lazy directory handle: `navigator.storage.getDirectory()` and `getDirectoryHandle(directory, { create: true })` run only on the first method call.
- `defaultOpfs()` adapter resolves the real browser API; tests inject `createInMemoryOpfs()`.
- Zod-validated `OpfsStorageOptions` at the constructor boundary throws `OPFS.INVALID_OPTIONS` on malformed input.
- DOMException mapping: `NotFoundError → undefined/false`, `NotAllowedError → OPFS.PERMISSION_DENIED`, `QuotaExceededError → OPFS.QUOTA_EXCEEDED`, anything else → `OPFS.IO_FAILED` with `cause`.
- OTel: per-op span via `getInstruments().tracer`, duration histogram, op-kind counter, IO-bytes histogram for read/write paths.
## Usage: Basic Example
```ts
import {
createOpfsStorage,
isOpfsAvailable,
} from "@suaveplan/storage-browser-opfs";
if (isOpfsAvailable()) {
const storage = createOpfsStorage({ directory: "myapp" });
await storage.set("user/42", new TextEncoder().encode("hello"));
const back = await storage.get("user/42");
console.log(new TextDecoder().decode(back));
}
```
## Usage: Advanced Example
```ts
import {
createOpfsStorage,
OPFS_CODES,
OpfsError,
} from "@suaveplan/storage-browser-opfs";
import { createInMemoryOpfs } from "@suaveplan/storage-browser-opfs/in-memory-opfs";
const opfs = createInMemoryOpfs({ quotaBytes: 1 << 20 });
const clock = () => 1_700_000_000_000;
const storage = createOpfsStorage({ directory: "test", opfs, clock });
// Streaming write
const ac = new AbortController();
const writer = storage.createWriteStream("blob", ac.signal).getWriter();
await writer.write(new Uint8Array(32_000));
await writer.close();
// Partial read of bytes [10..20) without loading the full value
const slice = await storage.getPartial("blob", 10, 10);
try {
await storage.set("big", new Uint8Array(2 << 20)); // exceeds quota
} catch (err) {
if (err instanceof OpfsError && err.code === OPFS_CODES.QUOTA_EXCEEDED) {
// surface a "you're out of space" message
}
}
```
## API Reference
- `class OpfsStorage extends AbstractStorage` — constructor `(options?: OpfsStorageOptions)`.
- `createOpfsStorage(options?: OpfsStorageOptions): OpfsStorage`.
- Every abstract method from `AbstractStorage`. All accept `signal?: AbortSignal`.
- `capabilities`: literal-typed `StorageCapabilities` (see Features).
- Throws `OpfsError` with one of the 9 frozen `OPFS_CODES` at any boundary.
## Implementation Notes
The directory handle is cached after the first resolution to amortise the cost of `navigator.storage.getDirectory()` (the WHATWG spec doesn't require it to be cheap; some browsers do disk IO). The cache is per-instance, so multiple `OpfsStorage` instances over the same directory each resolve independently — sharing is the caller's responsibility. `set` performs a single `createWritable` + `write` + `close` so the write commits atomically per spec; we set `capabilities.atomicWrites: false` anyway because the abstract contract's "atomic" means "atomic with respect to concurrent readers within the same realm" and OPFS does not provide cross-realm locking. `setPartial` opens with `keepExistingData: true` so the seek-then-write doesn't truncate. `createReadStream`/`createWriteStream` are delegated to `opfs-storage.streams` to keep the class file within budget.

View File

@@ -0,0 +1,58 @@
# `opfs-storage.ops` — internal helpers for OpfsStorage
## Purpose
Hosts pure / nearly-pure helpers that `OpfsStorage` calls but that would push the class file over the 300-line hard budget. Extracting them keeps the class focused on lifecycle orchestration and the helpers individually testable in isolation. Each helper has a single responsibility — listing keys, paginating a list result, building a metadata record, validating numeric arguments, or feature-gating `getFileHandle` to a non-throwing variant.
## Features
- `listAllKeys(dir)` — enumerates file entries and decodes their filenames back to caller keys, silently skipping foreign entries.
- `paginateKeys(keys, options)` — applies `prefix`/`offset`/`limit` and produces a `StorageListResult` with stable sort order.
- `metadataFromFile(file, clock?)` — builds the `StorageMetadata` shape required by `AbstractStorage.getMetadata`.
- `assertNonNegInt(key, label, value)` — boundary guard for partial-read/write offset and length.
- `tryGetFileHandle(dir, filename, key)` — non-throwing `getFileHandle` that returns `undefined` for missing entries and maps other errors.
## Usage: Basic Example
```ts
import {
listAllKeys,
paginateKeys,
metadataFromFile,
} from "./opfs-storage.ops.js";
const all = await listAllKeys(dir);
const page = paginateKeys(all.map((e) => e.key), { prefix: "user/" });
```
## Usage: Advanced Example
```ts
import {
assertNonNegInt,
metadataFromFile,
tryGetFileHandle,
} from "./opfs-storage.ops.js";
import type { Clock } from "@suaveplan/types";
async function safeStat(dir: OpfsDirectoryHandle, key: string, fname: string, clock: Clock) {
const handle = await tryGetFileHandle(dir, fname, key);
if (!handle) return null;
const file = await handle.getFile();
return metadataFromFile(file, clock);
}
assertNonNegInt("user/42", "offset", 0);
```
## API Reference
- `listAllKeys(dir): Promise<{ key, handle }[]>` — silently skips entries whose filename isn't valid base64url.
- `paginateKeys(keys, options?): StorageListResult`.
- `metadataFromFile(file, clock?): StorageMetadata`.
- `assertNonNegInt(key, label, value): void` — throws `OpfsError(OPFS.INVALID_KEY)`.
- `tryGetFileHandle(dir, filename, key): Promise<OpfsFileHandle | undefined>`.
## Implementation Notes
`paginateKeys` sorts the filtered slice before paginating because `OpfsDirectoryHandle.entries()` makes no ordering guarantees. Sorting ensures `list` is deterministic across repeated calls without forcing the storage class to memoise. `metadataFromFile` falls back to `clock?.now()` only when the file's `lastModified` is missing (zero) — the in-memory test double always returns a valid stamp, so this fallback exists for unusual real-browser cases. `tryGetFileHandle` is preferred over a direct `try/catch` at every call site because the error-mapping logic deserves a single home.

View File

@@ -0,0 +1,49 @@
# `opfs-storage.streams` — Web Streams over OPFS
## Purpose
Factors the `createReadStream` / `createWriteStream` builders out of the main `OpfsStorage` class so the class stays under the 300-line file budget and the stream contract is independently testable. Both builders honour `AbortSignal`, map underlying errors through `mapOpfsError`, and preserve the close-as-commit semantic of the WHATWG File System Access spec (the writable stream buffers chunks and writes them in `close()`, so a half-written file is never observable).
## Features
- `buildReadStream({ key, signal, getDir })` — returns a `ReadableStream<Uint8Array>` over `File.stream()`; missing entries close cleanly instead of erroring.
- `buildWriteStream({ key, signal, getDir })` — returns a `WritableStream<Uint8Array>` that buffers caller chunks and commits them atomically on `close()`.
- `AbortSignal` honoured before each chunk read/write — already-aborted signals throw `OPFS.ABORTED` synchronously.
- `mapOpfsError` invoked at every error boundary so callers receive typed `OpfsError` instances.
## Usage: Basic Example
```ts
import { buildReadStream } from "./opfs-storage.streams.js";
const stream = buildReadStream({ key: "a", signal: undefined, getDir });
const reader = stream.getReader();
for (;;) {
const { value, done } = await reader.read();
if (done) break;
// process value
}
```
## Usage: Advanced Example
```ts
import { buildWriteStream } from "./opfs-storage.streams.js";
const ac = new AbortController();
const stream = buildWriteStream({ key: "a", signal: ac.signal, getDir });
const writer = stream.getWriter();
await writer.write(new Uint8Array([1, 2, 3]));
await writer.write(new Uint8Array([4, 5]));
await writer.close(); // single OPFS write happens here
```
## API Reference
- `StreamArgs { key, signal, getDir }`.
- `buildReadStream(args): ReadableStream<Uint8Array>`.
- `buildWriteStream(args): WritableStream<Uint8Array>`.
## Implementation Notes
`buildWriteStream` collects chunks in a plain array instead of streaming them straight to OPFS because OPFS's `FileSystemWritableFileStream` does NOT expose a public byte-pipe-through-WHATWG surface in every browser yet — Safari historically required `keepExistingData`-or-`truncate` decisions up front. Buffering is consistent across browsers, and the memory cost is bounded by the caller's writer back-pressure. For very large writes consumers should use `set()` with a pre-built `Uint8Array` or `setPartial()` for streaming updates.

View File

@@ -0,0 +1,56 @@
# `otel` — OpenTelemetry instrumentation for OPFS storage
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Centralises every metric, histogram, and span emitted by `OpfsStorage` behind a single lazy instrument factory. Lazy construction lets consumers install a custom global `MeterProvider` / `TracerProvider` before the storage path executes, and lets tests pin a real in-memory `MeterProvider` against which the recorded attribute set is asserted. The module exposes a small `recordOp({op, outcome, durationMs, bytes?, direction?})` helper that every operation in the storage class calls once.
## Features
- Six instruments: 4 counters (`reads_total`, `writes_total`, `deletes_total`, `errors_total`) and 2 histograms (`op_duration_ms`, `io_bytes`).
- Stable label space: `{op, outcome}` on every counter / duration, `{op, direction}` on `io_bytes`.
- Aborted operations count toward `errors_total` with `outcome: "aborted"` so SLO dashboards can split them out.
- `setStorageBrowserOpfsMeterProvider(provider)` for runtime provider injection (used by `@suaveplan/opentelemetry`).
- `resetInstruments()` rebuilds the cache between test blocks.
## Usage: Basic Example
```ts
import { recordOp } from "@suaveplan/storage-browser-opfs/otel";
recordOp({ op: "set", outcome: "success", durationMs: 12, bytes: 100, direction: "write" });
```
## Usage: Advanced Example
```ts
import { metrics } from "@opentelemetry/api";
import {
getInstruments,
setStorageBrowserOpfsMeterProvider,
resetInstruments,
} from "@suaveplan/storage-browser-opfs/otel";
// In a Bun/Node test:
class CapturingMeterProvider { /* real in-memory MeterProvider */ }
const provider = new CapturingMeterProvider();
setStorageBrowserOpfsMeterProvider(provider);
resetInstruments();
const { writesTotal } = getInstruments(true);
writesTotal.add(1, { op: "set", outcome: "success" });
```
## API Reference
- `setStorageBrowserOpfsMeterProvider(provider | undefined): void`.
- `getInstruments(force?: boolean): StorageBrowserOpfsInstruments` — cached unless `force` is `true`.
- `recordOp(input: RecordOpInput): void`.
- `resetInstruments(): void`.
- Types: `StorageBrowserOpfsOp` (14 op names), `StorageBrowserOpfsOpOutcome` (`success | error | aborted`), `RecordOpInput`.
## Implementation Notes
The dispatch in `recordOp` is intentionally a flat `if/else` chain rather than a lookup table: TypeScript can statically verify exhaustiveness over `StorageBrowserOpfsOp`, and the cost of a 14-arm chain is negligible compared to the underlying counter add. `aborted` outcomes are routed to `errors_total` (not a separate counter) on purpose — operationally they're failures from the caller's perspective even though they're not exceptions on the server side.

View File

@@ -0,0 +1,50 @@
# `registry` — factory registration for `OpfsStorage`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Hooks `OpfsStorage` into `@suaveplan/storage`'s factory registry via module augmentation so callers can construct the adapter through `createStorage({ type: "opfs", config })`. Registration is gated behind an explicit `registerOpfsProvider()` function instead of an unconditional side-effect import: `sideEffects: false` bundlers must be able to tree-shake the registry path when a consumer holds a direct `new OpfsStorage(...)` reference but never goes through the factory.
## Features
- `declare module "@suaveplan/storage/factory"` adds typed slots `opfs: OpfsStorage` and `opfs: OpfsStorageOptions`.
- `registerOpfsProvider()` is idempotent — repeat calls are no-ops.
- Re-registers cleanly after `clearStorageProviders()`.
## Usage: Basic Example
```ts
import { registerOpfsProvider } from "@suaveplan/storage-browser-opfs/registry";
import { createStorage } from "@suaveplan/storage/factory";
registerOpfsProvider();
const storage = createStorage({ type: "opfs", config: { directory: "myapp" } });
```
## Usage: Advanced Example
```ts
import {
clearStorageProviders,
createStorage,
hasStorageProvider,
} from "@suaveplan/storage/factory";
import { registerOpfsProvider } from "@suaveplan/storage-browser-opfs/registry";
// In a fresh test isolate:
clearStorageProviders();
registerOpfsProvider();
console.log(hasStorageProvider("opfs")); // true
const storage = createStorage({ type: "opfs", config: { directory: "test" } });
```
## API Reference
- `registerOpfsProvider(): void` — idempotent registration.
## Implementation Notes
The module augmentation is in the same file as the registration function so consumers get the type slot the moment they `import` either symbol. Idempotency is enforced by a `hasStorageProvider("opfs")` short-circuit before re-registering — the underlying factory map handles ties safely either way, but the short-circuit makes the contract explicit and observable.

View File

@@ -0,0 +1,48 @@
# `opfs-like.types` — structural view of the OPFS API surface
## Purpose
Declares a minimal structural interface (`OpfsLike`, `OpfsDirectoryHandle`, `OpfsFileHandle`, `OpfsFile`, `OpfsWritableStream`) that `OpfsStorage` consumes. Working against a local interface — rather than `lib.dom`'s globals directly — keeps the adapter consumable from packages that compile without DOM lib, lets tests substitute a real in-memory implementation, and pins the contract to the subset of methods we actually call (so future browser additions don't ripple through this package as breakages).
## Features
- One interface per OPFS surface: directory handle, file handle, file, writable stream.
- Every method is a strict subset of the corresponding WHATWG File System Access API method, so the browser globals satisfy our interface without `unknown`-casts.
- `OpfsLike` is the smallest possible root: a single `getDirectory()` method, lazy by design.
## Usage: Basic Example
```ts
import type { OpfsLike } from "@suaveplan/storage-browser-opfs/types";
declare const myOpfs: OpfsLike;
const root = await myOpfs.getDirectory();
```
## Usage: Advanced Example
```ts
import type {
OpfsDirectoryHandle,
OpfsLike,
} from "@suaveplan/storage-browser-opfs/types";
class StaticDirOpfs implements OpfsLike {
public constructor(private readonly handle: OpfsDirectoryHandle) {}
public getDirectory(): Promise<OpfsDirectoryHandle> {
return Promise.resolve(this.handle);
}
}
```
## API Reference
- `OpfsLike.getDirectory(): Promise<OpfsDirectoryHandle>` — resolves the origin's OPFS root.
- `OpfsDirectoryHandle.getFileHandle(name, { create? })` / `getDirectoryHandle(name, { create? })` / `removeEntry(name, { recursive? })` / `entries()`.
- `OpfsFileHandle.getFile()` / `createWritable({ keepExistingData? })`.
- `OpfsFile.size`, `lastModified`, `arrayBuffer()`, `slice(start?, end?)`, `stream()`.
- `OpfsWritableStream extends WritableStream<Uint8Array>` with `seek(position)`.
## Implementation Notes
The interface intentionally omits any method `OpfsStorage` does not call. Adding new operations (e.g. `move`, `remove`) requires updating both this interface and the in-memory test double in lockstep. The native browser types are nominally compatible at the structural level — assignability flows from `FileSystemDirectoryHandle` to `OpfsDirectoryHandle` automatically.

View File

@@ -0,0 +1,46 @@
# `opfs-storage.types` — option boundary for OpfsStorage
## Purpose
Declares the constructor option shape `OpfsStorageOptions` and the matching `OpfsStorageOptionsSchema` Zod schema. The schema is the runtime gate at the constructor boundary — malformed input becomes `OpfsError(OPFS.INVALID_OPTIONS)` with the underlying `ZodError` preserved as `cause`. This keeps invalid configuration from corrupting the OPFS layer with hard-to-diagnose downstream failures.
## Features
- `directory`: caller-overridable sub-directory under OPFS root (default `"storage"`).
- `opfs`: injectable `OpfsLike` adapter for tests and progressive enhancement.
- `clock`: injectable `Clock` so metadata timestamps are deterministic in tests.
- Zod schema accepts structural adapter shapes as `unknown` (Zod cannot fully model interfaces), letting TypeScript enforce the contract at call sites while still validating the literal-typed fields.
## Usage: Basic Example
```ts
import {
OpfsStorage,
type OpfsStorageOptions,
} from "@suaveplan/storage-browser-opfs";
const options: OpfsStorageOptions = { directory: "myapp" };
const storage = new OpfsStorage(options);
```
## Usage: Advanced Example
```ts
import { OpfsStorageOptionsSchema } from "@suaveplan/storage-browser-opfs/types";
function loadOpfsConfigFromEnv(): OpfsStorageOptions {
const raw = JSON.parse(process.env.OPFS_CONFIG ?? "{}");
const parsed = OpfsStorageOptionsSchema.safeParse(raw);
if (!parsed.success) throw parsed.error;
return parsed.data;
}
```
## API Reference
- `interface OpfsStorageOptions { directory?: string; opfs?: OpfsLike; clock?: Clock }`.
- `OpfsStorageOptionsSchema: ZodObject` — `directory: string().min(1).optional()`, structural adapters loose-typed.
## Implementation Notes
The schema-level looseness on `opfs`/`clock` is intentional: Zod cannot fully describe the structural shape of an interface like `OpfsLike` without losing the link to the TypeScript type, and runtime checks of every adapter method would impose a per-call cost without catching anything TypeScript doesn't already catch. The `directory.min(1)` rule is the one boundary that needs runtime enforcement because OPFS itself accepts the empty string and returns the root, which would silently break key isolation.