git:20260818.2ff7b22 to git:20260924.b36c6c8

36 added, 15 removed. Audit A to A.

---
name: fetcher-storage
description: >
- Add typed cross-environment storage with `@ahoo-wang/fetcher-storage`: `KeyStorage`, serializers, defaults, listeners, localStorage with in-memory fallback, custom sessionStorage, and optional cross-tab event buses. Use for persistent values or storage-backed React/CoSec flows.
+ Persist one typed value per key with `@ahoo-wang/fetcher-storage`: `KeyStorage` with defaults, serializers, change listeners, caching, `InMemoryStorage` for tests/SSR, and optional cross-tab sync through a broadcast bus. Use for "remember this setting/token/draft" or to keep a stored value such as a theme in sync across browser tabs. For notifications without a stored value use fetcher-eventbus.
---
# fetcher-storage
- ## Workflow
+ ## Decisions
- 1. Choose the storage backend first: browser storage when available, in-memory fallback for non-browser contexts or tests.
- 2. Use `KeyStorage` for one logical value per key and pair it with an explicit serializer.
- 3. Use `jsonSerializer` for objects and typed identity serializers only when values already have the target representation.
- 4. Register listeners with the EventHandler object pattern and dispose them during cleanup.
- 5. Load `references/api.md` for exact options, serializer patterns, and cross-tab behavior.
+ - **Backend**: `KeyStorage` uses `getStorage()` by default — `localStorage` when `window` exists, otherwise a fresh, unshared `InMemoryStorage` per call. It does **not** fall back when `localStorage` exists but is blocked. Pass `storage: new InMemoryStorage()` in tests and SSR for determinism.
+ - **Serializer**: the default is `jsonSerializer`; leave it unless values need a custom format. `typedIdentitySerializer<T>()` only makes sense for `KeyStorage<string>`, since `Storage` holds strings.
+ - **Cross-tab sync is opt-in**: the default `eventBus` is a local `SerialTypedEventBus('KeyStorage:{key}')`. Pass `eventBus: new BroadcastTypedEventBus({ delegate: new SerialTypedEventBus('app:theme') })` to sync; native `storage` events are never used.
- ## Key Practices
+ ## Gotchas a capable model gets wrong
- - Keep keys stable and namespaced by app or domain.
- - Provide `defaultValue` when consumers should not handle missing storage explicitly.
- - Use in-memory storage for deterministic tests instead of relying on ambient browser state.
+ - `get()` caches. Writes that bypass this instance or its bus (direct `localStorage.setItem`) are not seen once a value is cached.
+ - `defaultValue` is returned when nothing is stored but is never written or cached.
+ - `addListener(handler)` returns a remover function; a duplicate handler `name` is ignored silently, and its remover then deletes the handler registered first under that name.
+ - `destroy()` only detaches the internal cache handler; listeners and the bus stay alive.
+ - One broadcast bus serves one key (and one serializer instance); sharing it across keys throws.
+ - Installing needs the peers: `@ahoo-wang/fetcher-eventbus`, which itself needs `@ahoo-wang/fetcher`.
+ ## Minimal example
+
+ ```ts
+ import { InMemoryStorage, KeyStorage } from '@ahoo-wang/fetcher-storage';
+
+ const theme = new KeyStorage<{ mode: 'light' | 'dark' }>({
+ key: 'app:theme',
+ defaultValue: { mode: 'light' },
+ storage: new InMemoryStorage(), // omit in the browser to use localStorage
+ });
+ const remove = theme.addListener({
+ name: 'theme-log',
+ handle: e => console.log(e.oldValue, '->', e.newValue),
+ });
+ theme.set({ mode: 'dark' });
+ theme.get(); // { mode: 'dark' }
+ remove();
+ theme.destroy();
+ ```
+
## References
- - `references/api.md`: Detailed package API, examples, and edge-case guidance. Load it only when the task needs KeyStorage options, listener APIs, serializers, in-memory storage, cross-tab sync, installation, and quick-start examples.
+ - `references/api.md`: `KeyStorageOptions`, listener API, serializers, `InMemoryStorage`, environment detection and cross-tab setup. Load it for exact signatures.
## Related Skills
- - $fetcher-eventbus: Use for lower-level event bus behavior behind storage listeners.
- - $fetcher-react-hooks: Use when React state should bind to KeyStorage.
- - $fetcher-cosec-auth: Use for JWT and device-id storage patterns.
+ - $fetcher-eventbus: the buses and messengers underneath storage events.
+ - $fetcher-react-hooks: `useKeyStorage` / `useImmerKeyStorage` bind a `KeyStorage` to component state.
+ - $fetcher-cosec-auth: token, device-id and space-id storages built on `KeyStorage`.