Storage providers
A provider moves strings only.
interface StorageProvider {
get(key: string): string | null;
set(key: string, value: string): void;
remove(key: string): void;
has(key: string): boolean;
keys(): string[];
clear(): void;
syncEvents?: readonly string[];
}The six adapters
| Adapter | Backed by | syncEvents |
|---|---|---|
localStorageProvider | localStorage | storage |
sessionStorageProvider | sessionStorage | storage |
memoryStorageProvider / createMemoryStorageProvider() | a Map | — |
createFallbackProvider(...providers) | reads from the first that holds the key, writes to all | the union |
urlSearchParamsProvider / createUrlSearchParamsProvider(options?) | location.search | popstate |
urlSearchParamsInHashProvider / createUrlSearchParamsInHashProvider(options?) | location.hash, as params | popstate, hashchange |
import {
createFallbackProvider,
createMemoryStorageProvider,
createStorage,
localStorageProvider,
memoryStorageProvider,
} from '@studiometa/js-toolkit';
// Try localStorage, fall back to memory when it is refused.
createStorage({
provider: createFallbackProvider(localStorageProvider, memoryStorageProvider),
});
// A private Map, not the shared singleton.
createStorage({ provider: createMemoryStorageProvider() });The memory singleton is shared
memoryStorageProvider is one Map for the whole page. Two stores over it see each other's keys unless they use different prefixes. For a store per component, use createMemoryStorageProvider().
createFallbackProvider()
Reads from the first provider that holds the key and writes to all of them. That is what makes "use localStorage where it works, memory where it does not" one line rather than a branch at every call site.
A factory exists only where its product has state
| Has state or arguments | Does not |
|---|---|
createMemoryStorageProvider() — a Map | localStorageProvider |
createFallbackProvider(...) | sessionStorageProvider |
createUrlSearchParamsProvider(options?) | memoryStorageProvider |
createUrlSearchParamsInHashProvider(options?) | the two bare URL instances |
createLocalStorageProvider() and createSessionStorageProvider() are removed; the instances stay.
A built-in provider never throws
guard() turns a full quota or a refused area into a storage.access-failed diagnostic and returns the method's fallback:
| Method | Fallback |
|---|---|
get | null |
has | false |
keys | [] |
set, remove, clear | undefined |
It reports once per operation, not once per area. The area is resolved per call, inside the guard, because the getter itself throws when storage is denied — localStorage in a blocked third-party frame throws on access, not on use.
types.ts states the same contract for a custom provider: report, return the fallback, do not throw.
The URL adapters
They rebuild the whole location on each write, so a search write keeps the hash and a hash write keeps the query string. Their one option is push, which chooses history.pushState over the default replaceState.
Writing your own
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 });Six synchronous string methods. Namespacing, serialization, signals and sync wiring are createStorage()'s — do none of it here.
Tested at the seam
providers.spec.ts drives each adapter through the six methods for real, including a setItem that throws, an area getter that throws, push against history.length, and the syncEvents names of each adapter.