Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
# Add @suaveplan/storage-browser-opfs — Browser OPFS AbstractStorage adapter
**Package**: `@suaveplan/storage-browser-opfs`
**Status**: PROPOSED
**Type**: new-package
## Why
Genesis ships `@suaveplan/storage` (AbstractStorage core with in-memory adapter), `@suaveplan/storage-s3` (object-store adapter) and `@suaveplan/storage-file` (Node filesystem adapter), but no browser-tier persistent backend. Browser consumers (vault snapshots, drive-template chunk caches, i18n-backend-storage when running in-app, future client-side SQLite via `@sqlite.org/sqlite-wasm`) need an origin-scoped persistent file store. The Origin Private File System (OPFS) — `navigator.storage.getDirectory()` — is the standard browser primitive for this. The upstream `../suaveplan/packages/browser/storage-browser-opfs` ships an `OPFSStorage` that pre-dates the genesis `AbstractStorage` contract: it uses the legacy abstract surface, has no AbortSignal, throws raw `StorageError` with stringly codes, lacks a quota-error mapping, lacks OTel, has no Zod boundary, side-effect-registers in `index.ts` (which breaks under `sideEffects: false` bundlers — documented in the upstream README as a known limitation), and has no `getPartial`/`setPartial`/`createReadStream`/`createWriteStream`. This change rebuilds it fresh against the current `AbstractStorage` contract.
## What changes
- ADDED: `OpfsStorage` class extending `AbstractStorage` — origin-scoped key-value storage rooted at a configurable sub-directory of `navigator.storage.getDirectory()`.
- ADDED: `createOpfsStorage(options)` factory.
- ADDED: `OpfsStorageOptions` interface + `OpfsStorageOptionsSchema` Zod schema; throws `OPFS.INVALID_OPTIONS` on malformed input.
- ADDED: `OpfsLike` structural interface (`{ getDirectory(): Promise<FileSystemDirectoryHandle> }`) + `defaultOpfs()` adapter pointing at `navigator.storage.getDirectory()`. Injectable so tests substitute a real in-memory `FileSystemDirectoryHandle`-shaped implementation rather than mocking the DOM.
- ADDED: `sanitizeKey(key)` strict key boundary — rejects empty, `..`, absolute paths (`/`), null bytes, control chars (0x00–0x1F, 0x7F), and `KEY_TOO_LONG` over 1024 chars; normalises Windows `\` separators to POSIX `/`.
- ADDED: `OPFS_CODES` registry via `defineCodes("OPFS", [...])` — 9 codes: `NOT_FOUND`, `PERMISSION_DENIED`, `QUOTA_EXCEEDED`, `UNAVAILABLE`, `INVALID_KEY`, `KEY_TOO_LONG`, `INVALID_OPTIONS`, `ABORTED`, `IO_FAILED`.
- ADDED: `OpfsError extends SuaveplanError<OpfsErrorCode>` with `key` discriminant.
- ADDED: Module-augmentation registry — `declare module "@suaveplan/storage/factory"` adds `opfs: OpfsStorage` to `StorageProviderRegistry` and `opfs: OpfsStorageOptions` to `StorageProviderConfig`; `registerOpfsProvider()` registers the constructor with the factory.
- ADDED: Native Web Streams via `createReadStream` (`File.stream()` from `FileSystemFileHandle.getFile()`) and `createWriteStream` (`FileSystemWritableFileStream` from `FileSystemFileHandle.createWritable()`); declared `capabilities.nativeStreams: true`.
- ADDED: `getPartial(offset, length)` via `File.slice(offset, offset+length).arrayBuffer()` — declared `capabilities.partialReads: true`.
- ADDED: `setPartial(offset, bytes)` via `FileSystemWritableFileStream.seek()` + `.write()` — declared `capabilities.partialWrites: true`.
- ADDED: DOMException mapping — `NotFoundError` → `undefined`/`false` return per AbstractStorage contract; `NotAllowedError` → `OPFS.PERMISSION_DENIED`; `QuotaExceededError` → `OPFS.QUOTA_EXCEEDED`; missing `navigator.storage` → `OPFS.UNAVAILABLE`.
- ADDED: Filename codec — base64url over UTF-8 bytes — preserves any caller key (slashes, unicode, control-safe) through the OPFS filename layer; `encode`/`decode` round-trip property test.
- ADDED: OTel surface — `storage_browser_opfs.op_duration_ms{op,outcome}` + `storage_browser_opfs.io_bytes{op,direction}` histograms; `storage_browser_opfs.{reads,writes,deletes,errors}_total{outcome}` counters; per-op span on `@suaveplan/storage-browser-opfs` tracer.
## Out of scope
- Quota-pre-flight via `navigator.storage.estimate()` — listed in the upstream enterprise plan, but `QuotaExceededError` mapping at the boundary covers the observable failure mode and avoids racing the estimate against an in-flight write. Can be added as a follow-up if a consumer needs it.
- `FileSystemSyncAccessHandle` (Web Workers only) — the async OPFS API covers every public method on `AbstractStorage`; sync access handles are a perf optimisation for SQLite-WASM, which already opens them directly via `@sqlite.org/sqlite-wasm`.
- Encryption-at-rest — orthogonal concern handled by `@suaveplan/drive-plugin-chunks` (AEAD per chunk) and `@suaveplan/crypto-vault` (sealed container). Storage layer stores bytes verbatim.
- Cross-tab coordination / locking — out of scope; OPFS has no built-in lock primitive, and consumers needing it should layer `@suaveplan/idempotency` or `BroadcastChannel` on top.
## Source mapping
This change derives from `../suaveplan/enterprise/storage-browser-opfs.md` (120 lines) and rebuilds `../suaveplan/packages/browser/storage-browser-opfs/src/opfs.ts` (232 lines) against the current `AbstractStorage` contract.
| Requirement | Source lines | Notes |
|---|---|---|
| OPFS browser backend purpose | L10-L12 | "Origin Private File System (OPFS) storage backend — persistent, origin-scoped, browser-only file storage with quota awareness and streaming read/write." — preserved as the package's single responsibility. |
| Browser-only enforcement | L37 | `"browser"` field in `package.json`; tier-guarded by no Node-only imports in module graph. |
| Storage adapter conformance | L38 | `OpfsStorage extends AbstractStorage`; same conformance suite shape as `storage-file`. |
| Quota awareness via QuotaExceededError mapping | L39 | Implemented as boundary error mapping; pre-flight estimate is explicitly out of scope (see above). |
| Streaming read/write | L40 | Native Web Streams via `createReadStream`/`createWriteStream`; `capabilities.nativeStreams: true`. |
| Atomic writes (write-to-temp-then-rename) | L41 | Genesis deviation: OPFS does not expose `rename` between sibling files in the same directory in all browsers; `FileSystemWritableFileStream` writes are committed atomically on `.close()` per the spec, so we set `capabilities.atomicWrites: false` and document the close-as-commit semantic. |
| Zod schemas for options | L42 | `OpfsStorageOptionsSchema` published; runtime validation at constructor boundary throws `OPFS.INVALID_OPTIONS`. |
| Property-based tests | L43 | fast-check suite covering filename codec round-trip and key sanitiser. |
| Deterministic — injectable clock | L44 | Constructor takes optional `clock: Clock`; metadata `createdAt`/`modifiedAt` use it. |
| OpenTelemetry instrumentation | L45 | Per-op spans, counters, histograms emitted from production paths. |
| Structured errors with stable codes | L46 | `defineCodes("OPFS", [9 codes])`. |
| AbortSignal + backpressure | L47 | AbortSignal threaded through every public async method; pre-aborted signals throw `OPFS.ABORTED` before issuing any OPFS call. |
| Progressive enhancement | L50 | `OpfsLike` adapter throws `OPFS.UNAVAILABLE` when `navigator.storage.getDirectory` is missing; consumers can also call exported `isOpfsAvailable()`. |
| File-size budget | L56 | ≤300 LOC hard / ≤500 LOC test. |
| No mocks | L57 | Tests use a real in-memory `FileSystemDirectoryHandle`-shaped implementation. |
| `sideEffects: false` | L59 | Set in `package.json`. |
| Workspace deps + Bun catalog | L60 | All deps use `workspace:^` or `catalog:` refs. |
| Biome clean | L62 | Enforced by `bun run check`. |
| TypeScript strict, no `any` | L63 | Adopted. |
| Public API stability via Zod | L64 | `OpfsStorageOptionsSchema` exported. |
| Error hierarchy via `@suaveplan/error` | L65 | `OpfsError extends SuaveplanError<OpfsErrorCode>`. |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| Extend `AbstractStorage` not legacy `StorageLike` | The genesis storage core replaces the legacy interface with a richer abstract base. Every genesis adapter (`memory`, `s3`, `file`) extends `AbstractStorage`; OPFS MUST conform for module-augmentation registry compatibility. |
| Explicit `registerOpfsProvider()` function | Upstream `index.ts` calls `registerStorageProvider("opfs", …)` at module-evaluation time, which breaks under `sideEffects: false` tree-shaking. Genesis exports an explicit registration function (matching `registerNodeFsProvider()` in `storage-file`) so consumers control when registration happens. |
| `getPartial`/`setPartial`/streams declared in capabilities | `AbstractStorage` declares these abstract — they must have native implementations. OPFS supports both natively via `File.slice` and `FileSystemWritableFileStream.seek`. |
| Base64url filename codec | OPFS filenames cannot contain `/` (OPFS treats `/` as nested-dir separator) and have other restrictions per browser. Upstream used base64 with `+`/`/` → `-`/`_` substitutions. We adopt RFC 4648 base64url unmodified for stable, well-tested round-trip. |
| `OPFS.UNAVAILABLE` separate code | Lets consumers feature-detect cleanly (try-construct, catch `OPFS.UNAVAILABLE`, fall back to `MemoryStorage`) without confusing IO errors with the absent-API case. |
## Verification approach
Every requirement in the spec delta carries a `**Verified by:**` line referencing concrete `it(...)` titles in these files:
- `src/opfs-storage/opfs-storage.test.ts` — CRUD, metadata, list, clear, getSize, getPartial, setPartial, native streams, AbortSignal, key sanitisation integration, DOMException mapping, OTel happy paths.
- `src/opfs-storage/opfs-storage.spec-conformance.test.ts` — type-level conformance against `AbstractStorage`, capabilities matrix, OTel instruments observed via real in-memory `MeterProvider`, idempotent registry registration, error-code registry exhaustion.
- `src/key-sanitizer/key-sanitizer.test.ts` — empty, traversal, absolute, null bytes, control chars, overlong, separator normalisation, valid paths.
- `src/opfs-adapter/opfs-adapter.test.ts` — `defaultOpfs()` throws `OPFS.UNAVAILABLE` when `navigator.storage` is absent; resolves the directory handle when present.
- `src/filename-codec/filename-codec.test.ts` — base64url round-trip; fast-check property: `decode(encode(k)) === k` for arbitrary string keys including unicode, slashes, control chars.
- `src/otel/otel.test.ts` — `recordOp` emits histogram + counter with the documented attribute set against a real in-memory `MeterProvider`.
- `src/registry/registry.test.ts` — registers `opfs` provider, `createStorage({ type: "opfs", config })` returns `OpfsStorage`, idempotent re-registration.
- `src/in-memory-opfs/in-memory-opfs.test.ts` — the in-memory `FileSystemDirectoryHandle`-shaped test double itself, ensuring it faithfully implements every OPFS surface exercised by `OpfsStorage` (no contract drift).
## Impact
### Affected dependents
The package is new — no dependents currently. No action needed.
- No dependents (verified by `bunx turbo ls --filter=...@suaveplan/storage-browser-opfs` returning the package itself).
### Affected upstreams
- `@suaveplan/storage` — `StorageProviderRegistry` module-augmented to add `opfs: OpfsStorage` slot. Additive only; no breaking change. [x] no action needed (purely additive type slot).