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,81 @@
# Migrate storage-browser-localstorage into genesis
**Package**: `@suaveplan/storage-browser-localstorage`
**Status**: PROPOSED
**Type**: new-package (migration from suaveplan)
## Why
The upstream `@suaveplan/storage-browser` package combines two distinct browser-tier storage backends (IndexedDB and the synchronous `localStorage` API) behind a single barrel and a shared `sideEffects`-coupled registry. Genesis is splitting the package into two focused, tree-shake-clean siblings — `@suaveplan/storage-browser-indexeddb` and `@suaveplan/storage-browser-localstorage` — that each register explicitly against the `@suaveplan/storage` factory. This change ships the LocalStorage half. Splitting reduces bundle size for consumers that only need one backend, removes the import-time side-effect smell flagged by the upstream enterprise audit, and produces a typed `LOCAL_STORAGE.*` error registry aligned with the OPFS sibling that landed today.
## What changes
- ADDED: `LocalStorageStorage` class extending `AbstractStorage` from `@suaveplan/storage/abstract`. Synchronous `Storage` API presented through the AbstractStorage async surface.
- ADDED: `createLocalStorageStorage(options?)` factory.
- ADDED: `LocalStorageStorageOptions` + `LocalStorageStorageOptionsSchema` Zod boundary.
- ADDED: `StorageLike` structural interface mirroring the DOM `Storage` shape (`getItem`/`setItem`/`removeItem`/`clear`/`length`/`key(i)`) — injectable adapter.
- ADDED: `defaultLocalStorage()` adapter wrapping `globalThis.localStorage`, plus `isLocalStorageAvailable()` feature detector with sentinel-write probe.
- ADDED: `createInMemoryLocalStorage()` real-implementation `Storage` adapter for testing (no mocks).
- ADDED: `LOCAL_STORAGE_CODES` registry (`NOT_FOUND`, `PERMISSION_DENIED`, `QUOTA_EXCEEDED`, `UNAVAILABLE`, `INVALID_KEY`, `KEY_TOO_LONG`, `INVALID_OPTIONS`, `ABORTED`, `IO_FAILED`, `CORRUPT_PAYLOAD`) + `LocalStorageError extends SuaveplanError<LocalStorageErrorCode>`.
- ADDED: `sanitizeKey(key)` boundary (rejects empty/non-string/control-char/absolute/`..`/null-byte; caps at 256 chars).
- ADDED: `encodeEnvelope` / `decodeEnvelope` — base64 payload + JSON metadata wire format with `expiresAt` TTL.
- ADDED: `registerLocalStorageProvider()` explicit-call factory registration under slot `"localstorage"`.
- ADDED: Full OTel surface — `storage_browser_localstorage.{reads,writes,deletes,errors}_total`, `op_duration_ms`, `io_bytes` instruments with `setStorageBrowserLocalStorageMeterProvider` override + `resetInstruments`.
## Out of scope
- IndexedDB backend — separate parallel migration `storage-browser-indexeddb`.
- Compression / encryption wrappers — deferred; out of scope for the base adapter migration.
- BroadcastChannel cross-tab sync — deferred; future feature, separate change.
- Proactive TTL sweep timer — deferred; this migration ships lazy-on-read expiry only.
- React hooks layer — separate `storage-browser-react` consumer, future package.
## Source mapping
This change derives from `../suaveplan/enterprise/storage-browser.md`.
| Requirement | Source lines | Notes |
|---|---|---|
| AbstractStorage extension with async API over Uint8Array | L12 | Upstream describes both adapters extending `AbstractStorage`; preserved verbatim for LocalStorage. |
| Base64 + JSON envelope wire format | L12, L24 | Reworded — kept Base64-of-`Uint8Array` payload, JSON envelope `{ v, payload, meta, expiresAt? }`. |
| Namespace prefix + TTL expiry on read | L24 | Verbatim contract — `${namespace}:${key}` keyspace, lazy delete on get. |
| Prefix-based listing with pagination | L12 | Verbatim — `list({ prefix, limit, offset })` iterates `localStorage.key(i)`. |
| Metadata: size, mimeType, createdAt, updatedAt, custom | L12 | Verbatim. |
| `StorageQuotaError` quota wrapping | L12 | Extended — genesis uses typed `LOCAL_STORAGE.QUOTA_EXCEEDED` covering `code 22` and `code 1014` (Firefox) in addition to `QuotaExceededError`. |
| Explicit `registerLocalStorageProvider()` factory registration (no module-load side effects) | L48, L84 | Extended — upstream had import-time side effects; the audit's "Fix `sideEffects` vs. registry modules" recommendation is implemented as an explicit-call function so `sideEffects: false` holds. |
| Sub-path exports `./localstorage`, `./registry` | L49, L102 | Extended — every feature module gets its own subpath per genesis §6. |
| Co-located `.md` docs for every implementation file | L50, L86, L100 | Verbatim — required gate. |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `AbortSignal` threading on every public method | Genesis baseline added to every storage adapter since OPFS; pre-aborted signals throw `LOCAL_STORAGE.ABORTED` before any `localStorage` call. |
| Injectable `Storage` adapter (`StorageLike` interface) | Required by the genesis "no mocks" rule (CLAUDE.md §10.2) — tests must run against a real in-memory implementation rather than `vi.stubGlobal`. |
| Injectable `Clock` from `@suaveplan/types` | Genesis cross-package contract — every TTL/timestamp surface accepts a `Clock` for deterministic tests. |
| Zod boundary schemas on options | Genesis `@suaveplan/validation`-aligned baseline. |
| Typed `LOCAL_STORAGE_CODES` registry via `defineCodes` | Genesis baseline (every package owns a frozen registry); upstream used a single `StorageQuotaError`. |
| Sentinel-write availability probe with `UNAVAILABLE` error | Old Safari private mode throws on `setItem`; without a probe, callers hit a generic `Error` from the first write. |
| Full OTel surface (instruments + counters + histograms) | Genesis baseline; upstream had no observability. |
| `CORRUPT_PAYLOAD` error code | Genesis hardening — malformed base64 / JSON envelope returns a typed boundary error instead of silently swallowing as `null`. |
## Verification approach
- `src/local-storage/local-storage.test.ts` — CRUD, list/pagination, metadata, TTL expiry, namespace isolation, AbortSignal pre-abort, key boundary, corrupt-payload graceful degrade.
- `src/local-storage/local-storage.spec-conformance.test.ts` — `AbstractStorage` instance, capabilities matrix, every `LOCAL_STORAGE_CODES.*` thrown by a production path, OTel instruments emitted through a real in-memory `MeterProvider`.
- `src/key-sanitizer/key-sanitizer.test.ts` — every boundary rule.
- `src/envelope/envelope.test.ts` — round-trip, corrupt detection.
- `src/in-memory-storage/in-memory-storage.test.ts` — fixture self-test (length, key(i), getItem/setItem/removeItem, clear, quota).
- `src/local-storage-adapter/local-storage-adapter.test.ts` — `defaultLocalStorage()` + `isLocalStorageAvailable()` probe.
- `src/otel/otel.test.ts` — recorder semantics, per-op counter routing.
- `src/registry/registry.test.ts` — `registerLocalStorageProvider()` idempotency + `createStorage({ type: "localstorage", ... })` roundtrip.
## Migration impact
None — additive only. New package; no existing genesis consumers. Downstream genesis packages that need browser localStorage may import this package once available.
## Impact
### Affected dependents
`bunx turbo ls --filter=...@suaveplan/storage-browser-localstorage` — no dependents (package is brand new on the `genesis` branch). No work needed in any other package.