# vue-storage-kit — AI Reference Reactive browser storage (localStorage/sessionStorage/memory/IndexedDB) with TTL, schema migrations, encryption (AES-GCM), HMAC signing, gzip/deflate compression, cross-tab sync (BroadcastChannel + leader election), undo/redo history, debounce/throttle writes, quota-exceeded recovery, a Pinia persist plugin, and a React adapter — all built on one framework-agnostic engine. Version 0.2.6. This document is hand-written for AI agents and other tools that generate code against this package: every signature, default, and behavior note below is verified directly against the TypeScript source (not summarized from prose docs), and prose is kept to the minimum needed to use the API correctly. For human-readable narrative docs (why you'd reach for each piece, worked examples), see the interactive site instead: - Full docs (EN): https://npm.vuecraft.ru/en/packages/vue-storage-kit/guide/overview - Full docs (RU): https://npm.vuecraft.ru/packages/vue-storage-kit/guide/overview - GitHub: https://github.com/macrulezru/vue-storage-kit - npm: https://www.npmjs.com/package/vue-storage-kit Links below starting with "/" are relative to https://npm.vuecraft.ru. --- ## 1. Package map | Import path | Contains | |---|---| | `vue-storage-kit` | Vue composables, plugin, adapters, `TTLManager`/`SchemaManager`, storage utils (sections 3–9). | | `vue-storage-kit/react` | `useStorage` React hook — same engine, different return shape (section 8). | | `vue-storage-kit/pinia` | `createPiniaPersist` (section 10). | | `vue-storage-kit/crypto` | `encrypt`/`decrypt`/`sign`/`verify`/`reencrypt`/`rotateEncryptedKey` (section 6). | | `vue-storage-kit/compress` | `compress`/`decompress`/`isCompressed`/`CompressAdapter` (section 6.3). | | `vue-storage-kit/sync` | `TabSync`/`LeaderElection` classes directly, for advanced use outside `useStorage`'s built-in `sync` option (section 5). | | `vue-storage-kit/devtools` | `setupDevtools(app)` — **manual call required**, not automatic (section 12). | | `vue-storage-kit/nuxt` | Nuxt module — auto-imports + an SSR-aware `useCookie` replacement (section 13). | | `vue-storage-kit/testing` | `mockStorage`, `resetStorageState`, `seedEnvelope`, `flushAsync` (section 14). | **No app-level plugin install is required to use `useStorage()` itself.** `VueStoragePlugin` (section 4) is only needed for global `prefix`/ `defaultTarget`/`defaultSerializer`/`defaultEncrypt`/`onError` — omit it entirely for per-call options only. --- ## 2. The sharing/caching model — read this before anything else **Every `useStorage()` call (and `useStorageList()`, which is built on it) for the same `key` + `target` — across ANY number of Vue components, AND across the separate React `useStorage` hook, in the same page — shares ONE underlying `StorageEngine` instance**, reference-counted (`src/engine/engineCache.ts`), disposed only when the last consumer releases it. Two consequences that are easy to miss: 1. **Only the FIRST caller's `options` for a given key+target actually take effect.** A second `useStorage('user', {...differentTtl...})` call for an already-cached `key`+`target` silently reuses the first call's engine — its own `ttl`/`encrypt`/`compress`/`sign`/`sync`/`debounce`/ `throttle`/`history`/`version`/`migrations`/`defaultValue` are never read at all, only its ref-count is bumped. 2. **On the Vue side there's a SECOND cache layer on top of that** (`wrapperCache` in `useStorage.ts`): two Vue components asking for the same key+target get the literal SAME `Ref` object, not just two refs backed by the same data. `target` defaults to `'local'` — two calls for the same `key` but different `target`s are genuinely independent (different cache-key `` `${target}:${key}` ``). --- ## 3. `StorageOptions` — full option reference ```ts interface StorageOptions { target?: 'local' | 'session' | 'memory' | 'indexeddb' // default 'local' serializer?: Serializer // default: createJSONSerializer() — see 3.1 ttl?: number // ms; default: never expires version?: number // default 1 migrations?: Migration[] // { version, up(data), down?(data) }[] — see 3.2 encrypt?: boolean | EncryptOptions // AES-GCM — see section 6.1 compress?: boolean | CompressOptions // gzip/deflate/deflate-raw — see 6.3 sign?: boolean | SignOptions // HMAC-SHA256 integrity check — see 6.2 sync?: boolean | SyncOptions // cross-tab — see section 5 debounce?: number // ms — coalesce writes; local `value` still updates immediately throttle?: number // ms — guarantees a write during continuous changes; WINS over debounce if both set history?: number // past-values kept in memory for undo()/redo(); not persisted, resets on reload. 0/omitted = disabled evictOnQuota?: boolean | { max?: number } // LRU-evict this adapter's OTHER keys on QuotaExceededError — see 3.4. Off by default onError?: (err: StorageError) => void onExpire?: (key: string) => void onMigrate?: (from: number, to: number) => void defaultValue: T // REQUIRED } ``` ### 3.1 Default serializer (`createJSONSerializer`) Not plain `JSON.stringify`/`parse` — a custom one that additionally round-trips `Date`, `Map`, `Set`, `bigint`, and `undefined` (tagged as `{ __type: 'Date'|'Map'|'Set'|'BigInt'|'undefined', value }` internally, via a pre-processing walk — not a JSON.stringify replacer, since `Date.prototype.toJSON()` would otherwise convert dates to strings before a replacer ever saw the original type). **Providing your own `serializer` loses this — plain `JSON.stringify` does not survive `Date`/`Map`/`Set`/ `bigint`/`undefined` round-trips**, so a custom serializer needs to replicate whatever subset it actually needs. ### 3.2 Migrations (`SchemaManager.migrate`) Runs whenever the stored envelope's `v` differs from the current `version` — supports both **upgrade** (`fromVersion < version`, chains every `migration.up` with `version` in `(fromVersion, targetVersion]`, in ascending order) and **downgrade** (`fromVersion > version`, chains every `migration.down` with `version` in `(targetVersion, fromVersion]`, in DESCENDING order — fails the whole migration, reporting `'migration-failed'` and falling back to `defaultValue`, if ANY migration in the downgrade chain is missing a `down`). A thrown error from any `up`/`down` step aborts the chain the same way. A successful migration is written back to storage immediately (preserving the ORIGINAL envelope's `ts`, not a fresh timestamp — see section 5's staleness-ordering note). ### 3.3 debounce vs throttle vs history `debounce`/`throttle` only delay the PERSISTED write — the reactive `value` (or React's `value`/`setValue` snapshot) updates synchronously, immediately, regardless. `throttle` wins if both are set (checked first in `scheduleWrite`). A pending debounced/throttled write is flushed (not dropped) on `dispose()`/component unmount. `history` records a (`structuredClone`-based, with a serialize/deserialize-round-trip fallback for values `structuredClone` can't handle — e.g. a Vue reactive `Proxy`, which is exactly what a deep-reactive object/array value arrives as) snapshot on every `setValue()`, purely in memory — reloading the page loses undo/redo history entirely, independent of whether the VALUE itself persisted. ### 3.4 Quota-exceeded recovery On `QuotaExceededError` from the adapter, in order: (1) run this adapter's own TTL sweep (`TTLManager.cleanExpired`) and retry once; (2) if `evictOnQuota` is set, evict this adapter's own OTHER keys one at a time — oldest write-timestamp first (read from a plaintext `exp`/`ts` header every write is prefixed with, so this works across keys with different encryption settings) — retrying after each eviction, up to `max` evictions (default 1 if `evictOnQuota: true`). Still fails → `'quota-exceeded'` error, write is dropped. Off by default because deleting unrelated keys is a real side effect that must be opted into. --- ## 4. `useStorage` (Vue) — composable & plugin ```ts function useStorage(key: string, options: StorageOptions): UseStorageReturn function useStorage(def: StorageKeyDef): UseStorageReturn // from defineStorageKey(key, options) — reusable typed key function defineStorageKey(key: string, options: StorageOptions): StorageKeyDef interface UseStorageReturn { value: Ref // writing this calls engine.setValue() — deep-watched, flush: 'sync' isReady: Ref // true once the initial read (and cross-tab subscribe, if sync) has settled error: Ref expiry: ComputedRef canUndo: ComputedRef; canRedo: ComputedRef remove(): void // clears storage AND resets `value` to defaultValue; also clears pending debounce/throttle refresh(): Promise // re-reads from storage and applies it (bypasses cache/pending writes) undo(): void; redo(): void // no-op without `history` set } useLocalStorage(key, defaultValue, opts?): UseStorageReturn // sugar: { ...opts, target: 'local', defaultValue } useSessionStorage(key, defaultValue, opts?): UseStorageReturn // target: 'session' ``` `VueStoragePlugin` (`app.use(VueStoragePlugin, options)`, entirely optional — see section 2): ```ts interface VueStoragePluginOptions { prefix?: string // prepended to every useStorage() key app-wide defaultTarget?: StorageTarget defaultSerializer?: Serializer defaultEncrypt?: EncryptOptions // merged under an explicit per-call `encrypt` object; used as-is if a call passes `encrypt: true` onError?: (err: StorageError) => void // runs IN ADDITION TO a per-call onError, not instead of it } ``` Global options are read ONCE per `useStorage()` call (via `getGlobalOptions()`, a plain module-level variable set by `VueStoragePlugin.install()`) — installing/re-installing the plugin after a `useStorage()` call has already run does not retroactively affect it. --- ## 5. Cross-tab sync (`SyncOptions`, `sync: true | SyncOptions`) ```ts interface SyncOptions { channel?: string /* default 'vue-storage-kit' */; leader?: boolean; debounce?: number /* default 50 */ } ``` Uses `BroadcastChannel` where available; falls back to the `window` `'storage'` event (localStorage/sessionStorage only — the fallback can't sync a `memory`/`indexeddb` target across tabs) where it isn't. Conflict resolution is **last-write-wins by envelope `ts`**, with one tiebreaker: on an EXACT `ts` tie, the elected leader's version wins over a non-leader's (`leader: true` opts into `LeaderElection`, which uses the Web Locks API — `navigator.locks` — and falls back to "every tab is leader" if that API is unavailable, e.g. older Safari). `debounce` here coalesces outgoing BROADCAST messages per key (separate from `StorageOptions.debounce`, which coalesces the actual storage WRITE) — a pending debounced broadcast is still flushed on `TabSync.stop()` (engine `dispose()`), not dropped. A subtle but real correctness detail: the engine tracks its own `lastAppliedTs` across ALL sources (local writes, disk reads, accepted sync messages) — not just what `TabSync` itself has sent/received — so a stale, still-debounced cross-tab message arriving after this tab already read a newer value directly from storage is correctly rejected, something `TabSync`'s own per-key timestamp map alone couldn't guarantee (it has no visibility into plain disk reads). --- ## 6. Encryption, signing, compression ### 6.1 Encryption (`encrypt: true | EncryptOptions`, `vue-storage-kit/crypto`) ```ts interface EncryptOptions { password?: string; key?: CryptoKey; iterations?: number /* default 100000, PBKDF2 */ } function encrypt(data: string, opts: EncryptOptions): Promise // AES-GCM-256; output: base64(salt[16] + iv[12] + ciphertext) function decrypt(raw: string, opts: EncryptOptions): Promise function reencrypt(raw: string, oldOpts: EncryptOptions, newOpts: EncryptOptions): Promise // decrypt then re-encrypt under new opts — touches only the outermost encryption layer function rotateEncryptedKey(target: StorageTarget, key: string, oldOpts: EncryptOptions, newOpts: EncryptOptions, signOpts?: SignOptions): Promise // reads the live stored value for `key`, re-encrypts it in place. MUST pass `signOpts` if that key was ALSO written with `sign` set // (signing wraps the outermost layer — sign(encrypt(...)) — omitting signOpts fails decrypt() with a confusing low-level error, not a clear one) ``` Requires `crypto.subtle` (Web Crypto) — no fallback; unavailable → `'crypto-error'`. Derived keys are cached in-process by `` `${password}:${iterations}:${salt}` `` to avoid re-running PBKDF2 on every write. ### 6.2 Signing (`sign: true | SignOptions`) ```ts interface SignOptions { password?: string; key?: CryptoKey; iterations?: number /* default 100000 */ } function sign(data: string, opts: SignOptions): Promise // HMAC-SHA256; format: `${data}.${saltB64}.${macB64}` function verify(signed: string, opts: SignOptions): Promise // throws if missing/invalid — StorageEngine catches this and reports 'signature-invalid', falling back to defaultValue ``` Integrity only, NOT confidentiality — signed data is still plaintext- visible. Combine with `encrypt` for both; `sign` always wraps the OUTERMOST layer (applied after compress+encrypt on write, verified before decrypt+decompress on read) — matches `rotateEncryptedKey`'s note above. ### 6.3 Compression (`compress: true | CompressOptions`, `vue-storage-kit/compress`) ```ts type CompressionAlgorithm = 'gzip' | 'deflate' | 'deflate-raw' function compress(data: string, opts?: CompressOptions): Promise // prefixed `vsk::` + base64; passes through UNCOMPRESSED (no error) if CompressionStream is unavailable or doesn't support the algorithm function decompress(data: string, opts?: CompressOptions): Promise function isCompressed(data: string): boolean class CompressAdapter implements StorageAdapter // wraps any adapter; getItem/setItem pass through untouched — use getDecompressed()/setCompressed() explicitly to actually (de)compress ``` Unlike encrypt/sign, an unsupported runtime degrades SILENTLY to pass-through on `compress()` (no error, no reported failure) — a `decompress()` call on genuinely-compressed data in a runtime that can't decode it returns the data unchanged too, which then fails the subsequent `JSON.parse` and surfaces as an ordinary `'parse-error'` (same fallback path as any other corrupted envelope). ### 6.4 Read/write pipeline order (exact) - **Write**: serialize value → JSON-stringify the envelope `{v, d, exp, ts}` → compress → encrypt → sign → prepend a small ALWAYS-PLAINTEXT `exp`/`ts` header (`` `${JSON.stringify({exp,ts})}|${payload}` ``) → adapter `setItem`. - **Read**: adapter `getItem` → strip the plaintext header → verify sign → decrypt → decompress → `JSON.parse` the envelope → check TTL expiry → check schema version (migrate if needed) → deserialize `d`. The plaintext header exists specifically so TTL sweeps and `evictOnQuota`'s LRU eviction can read `exp`/`ts` for ANY key in an adapter — including ones written with different/unknown encryption settings — without needing to decrypt them first. --- ## 7. Storage adapters & targets ```ts interface StorageAdapter { getItem(key): Promise; setItem(key, val): Promise; removeItem(key): Promise; keys(): Promise } ``` - `'local'`/`'session'` → thin wrappers over `window.localStorage`/ `sessionStorage`. - `'memory'` → an in-process `Map`, one shared singleton per `StorageAdapterFactory` — every `target: 'memory'` key in the same page shares this one store. - `'indexeddb'` → a shared single object store (fixed DB `vue-storage-kit` / store `kv`) used as a plain key-value view. For a custom database/store name, indexes, or direct `IDBObjectStore` access, use `useIndexedDB()`/`useIDBRef()` instead (section 9) — those bypass the `StorageEngine` entirely (no TTL/encrypt/compress/sign/sync/history). - **SSR**: `StorageAdapterFactory.get()` returns the `memory` adapter for EVERY target (`'local'` included) whenever `window === undefined` — there's no separate "SSR adapter"; a server-rendered `useStorage({ target: 'local' })` transparently reads/writes memory-only state during that render. ```ts StorageAdapterFactory.get(target): StorageAdapter // per-target singleton, lazily created StorageAdapterFactory._reset(): void // testing only — see section 14 ``` --- ## 8. `useStorage` (React, `vue-storage-kit/react`) ```ts function useStorage(key: string, options: StorageOptions): UseStorageResult interface UseStorageResult { value: T setValue: (value: T | ((prev: T) => T)) => void // functional-update form supported isReady: boolean; error: StorageError | null; expiry: Date | null canUndo: boolean; canRedo: boolean remove(): void; refresh(): Promise; undo(): void; redo(): void } ``` Built on `useSyncExternalStore` (concurrent-rendering safe) over the SAME `engineCache` Vue uses (section 2) — a React and a Vue `useStorage()` call for the same key+target in the same app share one engine. **The engine is acquired once, at first render** (`useRef`), and **does NOT react to `key`/`options.target` changing across re-renders** — mount a new component instance (e.g. via a React `key` prop) to switch keys, the same pattern React already recommends for "reset this state" scenarios. Return shape is structurally different from Vue's: plain `value`+ `setValue` (React convention) instead of a `Ref`. --- ## 9. Other composables ```ts useIndexedDB(dbName, storeName, onError?, options?: { indexes?: IDBIndexDefinition[]; version?: number }): { get(key): Promise; set(key, value): Promise; delete(key): Promise keys(): Promise; getAll(): Promise; clear(): Promise; count(): Promise transaction(fn: (store: IDBObjectStore) => IDBRequest): Promise getByIndex(indexName, value): Promise; getAllByIndex(indexName, value): Promise } useIDBRef(dbName, storeName, key, defaultValue): { value: Ref; isReady: Ref; error: Ref } // Both use their OWN dedicated database/store (not the shared 'indexeddb' target from section 7) — no TTL/encrypt/compress/sign/sync/history. useCookie(name, options: CookieOptions): Ref // interface CookieOptions { expires?: Date | number /* number = DAYS, not ms */; path?; domain?; secure?; sameSite?; httpOnly?; serializer?; defaultValue: T } // Does NOT go through StorageEngine at all — no TTL/version/migrations/encrypt/compress/sign/sync/debounce/throttle/history support. // httpOnly is accepted but silently has no effect client-side (document.cookie can never set it) — only the Nuxt SSR useCookie (section 13) can actually honor it. useStorageList(key, options?: Omit,'defaultValue'> & { keyField?: keyof T & string /* default 'id' */ }): { items: Ref; isReady; error add(item): void; update(id, patch: Partial): void; remove(id): void find(id): ComputedRef; findAll(predicate): ComputedRef; clear(): void; set(items): void } // A thin wrapper over useStorage(key, {...options, defaultValue: []}) — inherits ALL of section 2's sharing behavior for the same key+target. useStorageKeys(prefix?: string, target?: StorageTarget = 'local'): { keys: Ref; isReady: Ref; refresh(): Promise } // Lists ADAPTER keys (raw storage keys), not useStorage() cache-keys. Auto re-scans on the window 'storage' event. useBroadcastChannel(channelName): { isSupported: boolean; lastMessage: ShallowRef; messages: Ref; post(message): void; close(): void } // Raw BroadcastChannel wrapper, unrelated to useStorage()'s own `sync` option/TabSync — use this for your own app-level cross-tab messaging instead. ``` --- ## 10. Pinia persistence (`vue-storage-kit/pinia`) ```ts function createPiniaPersist(opts?: PiniaPersistOptions): (ctx: PiniaPluginContext) => void // pinia.use(createPiniaPersist()) interface PiniaPersistOptions { key?: string // default: the store's own $id target?: StorageTarget // default 'local' pick?: string[]; omit?: string[] // mutually applicable — pick wins if both given (pick checked first) serializer?: Serializer beforeRestore?, afterRestore?: (ctx: PiniaPluginContext) => void onError?: (err: StorageError) => void } ``` **Independent of `StorageEngine`/`useStorage()` entirely** — its own restore/persist logic, going straight through `StorageAdapterFactory`, no TTL/version/migrations/encrypt/compress/sign/sync/debounce/throttle/ history support, and NOT covered by section 2's engine-sharing cache (one Pinia plugin instance per store, not shared with a `useStorage()` call using the same key). Persists via `store.$subscribe(..., { flush: 'sync' })` on every mutation — `flush: 'sync'` isn't just a latency nicety here: a mutation made in the same synchronous tick as the store's creation needs to mark `hasExternalWrite` before the async restore's `getItem()` continuation runs, or the restore would silently overwrite that same-tick mutation. --- ## 11. Storage-wide utilities (root package) ```ts getStorageQuota(): Promise<{ quota: number; usage: number; usagePercent: number }> // navigator.storage.estimate(); zeros if unavailable exportStorage(target?: StorageTarget = 'local', prefix?: string): Promise> // RAW stored strings (still wrapped/encrypted/etc — not decrypted values) importStorage(snapshot, target?, options?: { overwrite?: boolean /* default true */ }): Promise clearStorage(target?: StorageTarget = 'local', prefix?: string): Promise ``` `exportStorage`/`importStorage` round-trip the raw adapter strings verbatim (including the plaintext TTL header and any compress/encrypt/ sign transforms) — a straight adapter-to-adapter copy, not a decrypt-then-reencrypt operation; importing into a context with different `encrypt`/`sign` options attached to the same key will fail to read back correctly through `useStorage()`. --- ## 12. Vue DevTools (`vue-storage-kit/devtools`) ```ts function setupDevtools(app: App): void ``` **Manual call required** — nothing in this package installs it automatically (unlike some sibling packages in this author's ecosystem whose plugin wires devtools in automatically for you). Lists every live engine from the SAME shared `engineCache` Vue and React both populate (section 2) — a React-created engine shows up here too. Polls every 1000ms in addition to reactive pushes, specifically because a cross-tab sync update can change an engine's value without any Vue-observable update firing. --- ## 13. Nuxt module (`vue-storage-kit/nuxt`) ```ts // nuxt.config.ts export default defineNuxtConfig({ modules: ['vue-storage-kit/nuxt'], storageKit: { prefix: '', autoImports: true }, // config key: 'storageKit'; both fields optional, autoImports default true }) ``` - Auto-imports `useStorage`, `useLocalStorage`, `useSessionStorage`, `useIndexedDB`, `useIDBRef` from the base package, PLUS a **replacement `useCookie`** sourced from the module's own runtime instead of the base package — SSR-aware: on the server it reads/writes through the current H3 request/response (`useRequestEvent()`, called synchronously — Nuxt's request context doesn't survive an `await` boundary) via dynamically- imported `h3`'s `setCookie`, so SSR output reflects real cookie state and `httpOnly` actually works (impossible from `document.cookie`); on the client it behaves exactly like the base composable. - Registers a plugin that `app.use(VueStoragePlugin, { prefix: config.public.storageKit.prefix })` automatically — you don't call `app.use(VueStoragePlugin)` yourself in a Nuxt app. - Devtools (section 12) is NOT wired by this module — still needs a manual `setupDevtools(app)` call if wanted. --- ## 14. Testing (`vue-storage-kit/testing`) ```ts mockStorage(adapter?: StorageAdapter = new MemoryStorageAdapter()): { adapter: StorageAdapter; restore(): void } // redirects StorageAdapterFactory.get() to always return `adapter`, for EVERY target, until restore() is called resetStorageState(): void // clears the shared useStorage()/engineCache (Vue AND React) and StorageAdapterFactory's per-target singletons — call in beforeEach to prevent cross-test leakage of shared engines/Refs (section 2) seedEnvelope(adapter, key, value, opts?: { version?; exp?; ts?; serializer? }): Promise // writes a raw {v,d,exp,ts} envelope directly — WITHOUT the TTLManager plaintext meta header (fine: readFromStorage's header-parse falls back to treating the whole string as the envelope when no header is found) seedExpiredEnvelope(adapter, key, value, opts?): Promise // seedEnvelope with exp defaulted to 1000ms in the past flushAsync(ms?: number = 10): Promise // waits out pending timers/microtasks (debounce/throttle windows, dynamic import()s) — pick ms comfortably larger than any debounce/throttle under test ``` --- ## 15. Consolidated gotcha list Cross-cutting facts most likely to produce subtly wrong generated code if missed — each is explained in full where it first applies above, listed here for a fast pre-flight check: 1. **The single biggest one**: `useStorage()` (Vue or React) for an already-live `key`+`target` silently ignores every option the SECOND caller passes — only the first caller's options ever apply, for as long as any consumer keeps that engine alive (section 2). 2. `throttle` wins over `debounce` if both are set on the same call — not additive, not an error, just silently one wins (3.3). 3. A custom `serializer` loses the default's `Date`/`Map`/`Set`/`bigint`/ `undefined` round-trip support — plain `JSON.stringify` alone does not survive those (3.1). 4. `CookieOptions.expires` as a plain number is **days**, not milliseconds — inconsistent with `StorageOptions.ttl`, which is ms (9, 13). 5. `httpOnly` on the base (client-only) `useCookie()` is silently a no-op — only the Nuxt SSR replacement can actually set it (9, 13). 6. `useIndexedDB()`/`useIDBRef()` and `useStorage({ target: 'indexeddb' })` use COMPLETELY DIFFERENT IndexedDB databases/stores — they never see each other's data (7, 9). 7. `exportStorage()`/`importStorage()` move raw, still-transformed strings — importing into a differently-configured `encrypt`/`sign` context breaks reads through `useStorage()` (11). 8. `createPiniaPersist()` is a completely separate persistence mechanism from `useStorage()` — no TTL/encrypt/compress/sign/sync, and not covered by the engine-sharing cache even if it happens to use the same storage `key` (10). 9. `compress()` degrades silently to pass-through when unsupported; `encrypt()`/`sign()` do NOT — they report a `'crypto-error'`/fail loudly instead. Don't assume compression failing is visible the same way encryption failing is (6.1, 6.3). 10. SSR: `StorageAdapterFactory` returns the in-memory adapter for every `target`, including `'local'`, whenever there's no `window` — a server-rendered read/write silently never touches real localStorage/sessionStorage (7). 11. `evictOnQuota` deletes OTHER keys in the same adapter (oldest write first) — off by default specifically because it's a real, unrelated- data-loss side effect that must be opted into (3.4). 12. React's `useStorage` does not react to `key`/`target` changing across re-renders — it's captured once at first render (8).