# 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`. - 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.