Storage
createStorage() is a typed, observable key-value store over a StorageProvider. One seam, six adapters.
The store
import { createLocalStorage } from '@studiometa/js-toolkit';
interface Prefs {
theme: 'light' | 'dark';
seen: string[];
}
const prefs = createLocalStorage<Prefs>({ prefix: 'app:' });
prefs.set('theme', 'dark');
prefs.get('theme'); // 'light' | 'dark' | undefined
prefs.get('theme', 'light'); // 'light' | 'dark'
prefs.has('seen');
prefs.keys();
prefs.delete('seen');
prefs.clear();createStorage() owns everything a consumer thinks of as storage: key namespacing through prefix, serialization in both directions, a Signal per key created on the first subscription, and the reference-counted wiring that keeps keys in sync. A provider moves strings only.
Observing a key
const unsubscribe = prefs.subscribe(
'theme',
(value) => {
document.documentElement.dataset.theme = value ?? 'light';
},
{ immediate: true },
);destroy() releases every subscription and the shared listeners at once.
In a component, hand the unsubscribe back from mounted():
mounted() {
return prefs.subscribe('theme', (value) => this.apply(value));
}The six adapters
| Adapter | Backed by |
|---|---|
localStorageProvider | localStorage |
sessionStorageProvider | sessionStorage |
memoryStorageProvider / createMemoryStorageProvider() | a Map |
createFallbackProvider(...providers) | reads from the first that holds the key, writes to all |
urlSearchParamsProvider | location.search |
urlSearchParamsInHashProvider | location.hash, read as search params |
The two URL adapters rebuild the whole location on each write, so a search write keeps the hash and a hash write keeps the query string. They take one option, push, which chooses history.pushState over the default replaceState.
Four presets remove an argument from every call site: createLocalStorage(), createSessionStorage(), createUrlSearchParamsStorage() and createUrlSearchParamsInHashStorage().
A factory exists only where its product has state
createMemoryStorageProvider() holds a Map. createFallbackProvider() and the two URL factories take arguments. There is no createLocalStorageProvider() — the instance is enough.
Failures are diagnostics, never throws
A built-in provider reports its own failures and never throws. A full quota or a refused storage area becomes a storage.access-failed diagnostic and the method returns its fallback: null, false, [] or undefined. It reports once per operation, not once per area.
The store's own failures have their own codes:
storage.serialize-failed— nothing is written.storage.deserialize-failed— the default is returned.
types.ts states the same contract for a custom provider.
Syncing with the outside
syncEvents is a list of window event names and nothing more. A provider declares how a change made outside this instance announces itself:
| Provider | syncEvents |
|---|---|
| web storage | storage (another tab) |
| URL search params | popstate |
| URL hash | popstate, hashchange |
createStorage() subscribes one shared, reference-counted listener per name while at least one key is observed, and re-reads every observed key when it fires. The event carries no usable state, so the subscriber re-reads.
A known gap
A provider whose changes arrive on a BroadcastChannel or through an observer has no way to announce them yet.
A custom backend
Six synchronous string methods, plus an optional syncEvents:
import { createStorage, type StorageProvider } from '@studiometa/js-toolkit';
const cookieProvider: StorageProvider = {
get: (key) => new URLSearchParams(document.cookie.replaceAll('; ', '&')).get(key),
set: (key, value) => {
document.cookie = `${key}=${value};path=/`;
},
remove: (key) => {
document.cookie = `${key}=;path=/;max-age=0`;
},
has: (key) => document.cookie.includes(`${key}=`),
keys: () => [...new URLSearchParams(document.cookie.replaceAll('; ', '&')).keys()],
clear: () => {},
};
const store = createStorage({ provider: cookieProvider });One storage instance runs in Node over the memory provider, which is what test/package-node-consumer.js exercises.