Skip to content

createStorage

ts
createStorage<T extends object = Record<string, unknown>>(options?: StorageOptions): StorageInstance<T>

A typed, observable key-value store.

ts
interface StorageOptions {
  provider?: StorageProvider;
  serializer?: StorageSerializer;
  prefix?: string;
}

Usage

ts
import { 
createStorage
,
memoryStorageProvider
} from '@studiometa/js-toolkit';
interface Prefs {
theme
: 'light' | 'dark';
seen
: string[];
} const
prefs
=
createStorage
<Prefs>({
provider
:
memoryStorageProvider
,
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
();
prefs
.
destroy
();

The instance

ts
interface StorageInstance<T extends object = Record<string, unknown>> {
  get<K extends keyof T>(key: K): T[K] | undefined;
  get<K extends keyof T>(key: K, defaultValue: T[K]): T[K];
  set<K extends keyof T>(key: K, value: T[K]): void;
  delete<K extends keyof T>(key: K): void;
  has<K extends keyof T>(key: K): boolean;
  keys(): (keyof T)[];
  clear(): void;
  subscribe<K extends keyof T>(
    key: K,
    callback: (value: T[K] | undefined) => void,
    options?: { immediate?: boolean },
  ): () => void;
  destroy(): void;
}

The two get overloads are the whole difference between "it may not be there" and "here is what to use if it is not".

Observing a key

ts
const 
unsubscribe
=
prefs
.
subscribe
(
'theme', (
value
) => {
document
.
documentElement
.
dataset
.
theme
=
value
?? 'light';
}, {
immediate
: true },
);

A Signal is created per key, on the first subscription, so a store nobody observes holds nothing.

In a component, hand the unsubscribe back from mounted():

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

destroy()

Releases every subscription and the shared sync listeners at once. A store that outlives the page needs no call; a store scoped to a component does.

prefix

Namespaces every key in the underlying provider, so two stores can share one backing area without colliding. The prefix is the store's business — a provider never sees an unprefixed key and never adds one.

serializer

ts
interface StorageSerializer {
  serialize(value: unknown): string;
  deserialize(value: string): unknown;
}

Defaults to jsonSerializer. Failures are reported, not thrown:

  • storage.serialize-failednothing is written;
  • storage.deserialize-failed — the default is returned.

Syncing with the outside

syncEvents on the provider is a list of window event names and nothing more. 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 rather than trusting a payload.

A known gap

A provider whose changes arrive on a BroadcastChannel or through an observer has no way to announce them yet.

In Node

One storage instance runs in Node over the memory provider, which is what test/package-node-consumer.js exercises. Nothing in the store touches window unless a provider's syncEvents asks for it.

MIT Licensed