Skip to content

Storage

createStorage() is a typed, observable key-value store over a StorageProvider. One seam, six adapters.

The store

ts
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

ts
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():

js
mounted() {
  return prefs.subscribe('theme', (value) => this.apply(value));
}

The six adapters

AdapterBacked by
localStorageProviderlocalStorage
sessionStorageProvidersessionStorage
memoryStorageProvider / createMemoryStorageProvider()a Map
createFallbackProvider(...providers)reads from the first that holds the key, writes to all
urlSearchParamsProviderlocation.search
urlSearchParamsInHashProviderlocation.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:

ProvidersyncEvents
web storagestorage (another tab)
URL search paramspopstate
URL hashpopstate, 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:

ts
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.

MIT Licensed