KeyStorage and change listeners
KeyStorage<T> binds one typed value to one string key. Operations are synchronous; notifications are dispatched asynchronously through an event bus. It does not automatically watch native browser storage events.
Choose the backend and event bus independently: sharing a backend does not share the cached value or subscriptions. Two instances using the same key and backend but different default buses can retain different cached values until reload(). Share a bus as well when both instances must observe writes made through KeyStorage.
Options
new KeyStorage<T>(options: KeyStorageOptions<T>):
| Option | Default | Meaning |
|---|---|---|
key: string | Required | Backend storage key. |
storage?: Storage | getStorage() | Browser localStorage or a fresh memory store outside browsers. |
serializer?: Serializer<string, T> | jsonSerializer | String codec for reads, writes, and automatic broadcast snapshots. |
eventBus?: TypedEventBus<StorageEvent<T>> | New SerialTypedEventBus('KeyStorage:' + key) | The same name alone does not share instances. A created bus is closed by destroy(); a passed bus is not. |
defaultValue?: T | null | Returned when backend value is absent; not automatically persisted. |
Reads, writes, and lifetime
| Method | Return | Behavior |
|---|---|---|
get() | T | null | Return non-null cache; otherwise read and deserialize backend or return default. An undeserializable value is removed with a warning and read as absent. |
reload() | T | null | Read the backend again, bypassing the cache (for a value another tab may have written before its event arrived). Keeps the cached object when the stored text is unchanged; returns the default when the key is absent. |
set(value: T) | void | Read old value, serialize/snapshot, write backend, update cache, emit {oldValue, newValue}. set(undefined) is remove(). |
remove() | void | Read old value, remove backend key, clear cache, emit newValue: null; later get may return the default. |
addListener(handler) | RemoveStorageListener = () => void | Register named EventHandler<StorageEvent<T>>; the returned function removes this handler only while it is still the one registered under its name. |
destroy() | void | Remove this instance's internal cache listener and close the bus this instance created (the default bus, or one a subclass marks with the protected ownEventBus()). Does not delete the key or close a bus passed in eventBus. |
StorageEvent<T> has optional newValue and oldValue, each allowing null. StorageListenable<T> exposes addListener. Use unique handler names: a duplicate name is rejected by the underlying bus and its returned remover does nothing, so it never removes the listener that holds the name. The instance's internal cache listener runs before every other listener (order Number.MIN_SAFE_INTEGER), so a slow listener never lets an older event overwrite a newer cached value. Unsubscribe external listeners yourself before destroying the owning bus.
Backend read/write, JSON parsing, and serialization failures propagate synchronously; writes are not rolled back after notification failures. Emit rejections are caught and logged with console.warn, while ordinary serial handler failures are already isolated by the bus. A cached non-null value is not refreshed after direct backend mutations; call reload() to re-read it. get() returns object references; mutating one does not automatically persist it.
Sharing and broadcasting
Pass the same local bus to coordinate instances explicitly. For cross-context notifications use BroadcastTypedEventBus. KeyStorage installs a serializer-backed wire transformer if a broadcast bus has none. It snapshots the serialized new value before local handlers mutate objects, preserves custom types on the receiver, and can fall back from DataCloneError to the string snapshot. Invalid old snapshots become unavailable without discarding a valid new value. deserializeLegacy can decode old structured messages.
A shared broadcast bus requires the same storage key for its lifetime. With automatic transformation it also requires the same serializer instance (default JSON instances are reconciled). Violations throw during construction. A preexisting custom transformer remains the caller's responsibility. Incoming events update the in-memory cache; they do not write the receiving backend. Destroying a KeyStorage leaves shared bus ownership intact.
Complete example
import { KeyStorage, InMemoryStorage } from '@ahoo-wang/fetcher-storage';
const settings = new KeyStorage<{ theme: string }>({
key: 'settings',
storage: new InMemoryStorage(),
defaultValue: { theme: 'system' },
});
const removeListener = settings.addListener({
name: 'settings-ui',
handle: event => {
console.log(event.newValue);
},
});
settings.set({ theme: 'dark' });
console.assert(settings.get()?.theme === 'dark');
settings.remove();
console.assert(settings.get()?.theme === 'system');
removeListener();
settings.destroy();Public symbols and source
| Symbol | Implementation |
|---|---|
StorageEvent | keyStorage.ts:28 |
RemoveStorageListener | keyStorage.ts:36 |
StorageListenable | keyStorage.ts:38 |
KeyStorageOptions | keyStorage.ts:52 |
KeyStorage | keyStorage.ts:91 |
