# State: the store

> Plainkit 0.28.0. Generated by scripts/build-skills.mjs from dist/js/store.js, dist/js/store-extras.js and dist/js/settings.js. Do not edit: change the source and run the generator.

Per-module state that is namespaced, versioned, validated, subscribable and optionally saved in `localStorage`. Saved and cross-tab data is untrusted: a corrupt, oversized, wrong-type, unknown-key, newer or unmigratable value falls back to the defaults with one logged warning and never throws. A module sees only its own keys; other modules read its `publish` keys through `store.read(id)`, as a read-only copy. Never store secrets in it. The header of `js/store.js`, verbatim:

```text
State store: per-module namespaced, versioned, validated state with optional persistence. Framework-free; never throws on data.

  const store = createStore({ prefix: 'pk', version: 1 });
  const gallery = store.module('gallery', { defaults: { scale: 1, width: 'wide' }, schema: { scale: { min: 0.5, max: 2 }, width: { enum: ['narrow', 'wide'] } },
                                            persist: ['scale', 'width'], publish: ['scale'] });
  gallery.set('scale', 1.25); gallery.subscribe(state => paint(state)); store.read('gallery').scale;   // a read-only copy of published keys only

The data part of a module spec is plain JSON (version, defaults, schema, persist, publish, legacy), so it can move unchanged into the small shared JS/Blazor
manifest schema (#346, decision Q2); only `migrate(data, fromVersion)` is code, and the opt-in helpers in js/store-extras.js (withLegacy, syncTabs) wrap a spec.
Persisted as JSON { v, data } under the storage key `<prefix>.<moduleId>` (the envelope PlainKit.Blazor's store will write too); only `persist` keys are written.
The type of a key is the type of its default; a schema entry adds enum, min, max and maxLength (of the value as JSON, default 1024).
Stored and cross-tab values are untrusted: corrupt, oversized (64 KB), wrong-type, unknown-key, newer or unmigratable data falls back to the defaults
with ONE logged warning per load, and blocked or full storage keeps the state in memory (warned once). A module gets a facade for its own namespace only;
other modules see its `publish` keys through store.read(id). set/patch return false (and warn) instead of throwing; a bad module id throws (a code mistake).
get, set, patch, subscribe (returns its unsubscribe; called with the whole state after a change), reset, destroy; store.listeners() counts live subscriptions.
Nothing is evaluated or turned into markup; keep secrets out (it is plain localStorage). Values you get are shared: do not mutate them.
```

## Opt-in helpers

They wrap a module spec, so a page that needs neither ships neither. The header of `js/store-extras.js`, verbatim:

```text
Opt-in helpers for the state store (js/store.js, #348), kept out of it so a page that needs neither pays for neither. Each wraps a module spec:

  store.module('gallery', syncTabs(withLegacy({ defaults, persist, legacy: { scale: 'pk-gallery-scale', inspector: 'pk-gallery-inspector' } })));

withLegacy(spec)   when nothing is stored yet, reads each `spec.legacy` key ({ stateKey: 'old-storage-key' }) ONCE and offers the values to the store,
                   which validates them like stored ones (the old key is left alone until the site adopts the store). Old values are plain strings:
                   a string default takes the text as it is, a boolean takes 1/0/true/false, everything else is parsed as JSON. Anything else is
                   passed on as text, so the schema rejects it with the store's one warning. Today's keys: pk-site-theme, pk-gallery-scale,
                   pk-gallery-width, pk-gallery-inspector (1/0), pk-gallery-inspector-w, pk-theme-overrides, pk-layout-builder-draft, pk-example-nav.
syncTabs(spec)     cross-tab sync: another tab's write to the module's key arrives as a `storage` event (no polling) and is validated like any read;
                   a key removed in another tab resets to the defaults. The listener is removed by the module's or the store's destroy().
```

## Exports

- `dist/js/store.js`: `createStore`
- `dist/js/store-extras.js`: `syncTabs`, `withLegacy`
- `dist/js/settings.js`: `readSetting`, `tell`, `writeSetting`

## Use it

```js
import { createStore } from './plainkit/js/store.js';
import { syncTabs } from './plainkit/js/store-extras.js';

const store = createStore({ prefix: 'myapp', version: 1 });
const prefs = store.module('prefs', syncTabs({
    defaults: { scale: 1, density: 'comfortable' },
    schema: { scale: { min: 0.5, max: 2 }, density: { enum: ['comfortable', 'compact'] } },
    persist: ['scale', 'density'],
    publish: ['density'],
}));
prefs.set('scale', 1.25);                          // false (and a warning) when the value is invalid
const off = prefs.subscribe(state => paint(state)); // call off() to unsubscribe
store.read('prefs').density;                         // what another module may read
```

## Change the saved shape

Raise `version` and give the module a `migrate(data, fromVersion)` that returns the new data; its result is validated like any saved value. Without a working `migrate` an older saved copy falls back to the defaults.

## Blazor

The Blazor store (`IPkStore`) is a later step of the app-framework work and will read and write the same `{ v, data }` envelope. Nothing in this reference applies to Blazor yet.
