Skip to content

Storage providers

A provider moves strings only.

ts
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

AdapterBacked bysyncEvents
localStorageProviderlocalStoragestorage
sessionStorageProvidersessionStoragestorage
memoryStorageProvider / createMemoryStorageProvider()a Map
createFallbackProvider(...providers)reads from the first that holds the key, writes to allthe union
urlSearchParamsProvider / createUrlSearchParamsProvider(options?)location.searchpopstate
urlSearchParamsInHashProvider / createUrlSearchParamsInHashProvider(options?)location.hash, as paramspopstate, hashchange
ts
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 argumentsDoes not
createMemoryStorageProvider() — a MaplocalStorageProvider
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:

MethodFallback
getnull
hasfalse
keys[]
set, remove, clearundefined

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

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
});

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.

MIT Licensed