@k8ordo/state 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,6 +40,11 @@ const analyzeSchema = (schema, slot) => {
40
40
  defaults
41
41
  };
42
42
  };
43
+ const pickKeys = (info, data) => {
44
+ const values = {};
45
+ for (const key of info.keys) values[key] = data[key];
46
+ return values;
47
+ };
43
48
  /**
44
49
  * Whole-parse first; when that fails, the values are user-editable or stale
45
50
  * input, so one broken field falls back to its own default instead of taking
@@ -47,17 +52,11 @@ const analyzeSchema = (schema, slot) => {
47
52
  */
48
53
  const parseWithSalvage = (info, raw) => {
49
54
  const whole = safeParse(info.schema, raw);
50
- if (whole.success) {
51
- const values = {};
52
- for (const key of info.keys) values[key] = whole.data[key];
53
- return values;
54
- }
55
- const salvaged = {};
56
- for (const key of info.keys) if (key in raw) {
57
- const field = safeParse(info.shape[key], raw[key]);
58
- salvaged[key] = field.success ? field.data : info.defaults[key];
59
- } else salvaged[key] = info.defaults[key];
60
- return safeParse(info.schema, salvaged).success ? salvaged : { ...info.defaults };
55
+ if (whole.success) return pickKeys(info, whole.data);
56
+ const kept = {};
57
+ for (const key of info.keys) if (key in raw && safeParse(info.shape[key], raw[key]).success) kept[key] = raw[key];
58
+ const salvaged = safeParse(info.schema, kept);
59
+ return salvaged.success ? pickKeys(info, salvaged.data) : { ...info.defaults };
61
60
  };
62
61
  //#endregion
63
62
  export { analyzeSchema, isRecord, parseWithSalvage, sameValue };
@@ -0,0 +1,37 @@
1
+ import { StateSchema } from "./schema/object.mjs";
2
+ import { RowCodec, Versioning } from "./row/codec.mjs";
3
+ //#region src/storage-state.d.ts
4
+ type StorageState<Kind extends 'local' | 'session', Schema extends StateSchema> = {
5
+ kind: Kind;
6
+ /** Identity of this state: the storage key and the store registry slot. */
7
+ key: string;
8
+ /** The schema as passed. */
9
+ schema: Schema;
10
+ /** The Web Storage key the values are written under: `k8ordo-state:<key>`. */
11
+ storageKey: string;
12
+ /**
13
+ * A JavaScript expression, for an inline `<script>`, that evaluates in the
14
+ * browser to the stored object — or `null` when nothing is stored, the JSON
15
+ * is corrupt, the value is not an object, storage cannot be read, or (for a
16
+ * versioned local state) the row was written by another version. Neither
17
+ * the schema nor `migrate` runs there: treat the result as untrusted and
18
+ * read only the fields you need, each with its own fallback.
19
+ */
20
+ inlineRead: () => string;
21
+ };
22
+ /**
23
+ * App-scope state that lives in localStorage: device-persistent, shared
24
+ * across tabs, never page-bound — which is why it is its own definition kind
25
+ * instead of a slot on a page state.
26
+ */
27
+ export type LocalState<Schema extends StateSchema = StateSchema> = StorageState<'local', Schema>;
28
+ /**
29
+ * App-scope state that lives in sessionStorage: kept through reloads of the
30
+ * tab and gone when the tab closes, shared with no other tab — built the way
31
+ * local state is, over the other Web Storage area.
32
+ */
33
+ export type SessionState<Schema extends StateSchema = StateSchema> = StorageState<'session', Schema>;
34
+ export declare const storageCodecOf: (def: LocalState | SessionState) => RowCodec;
35
+ export declare const defineLocalState: <Schema extends StateSchema>(key: string, schema: Schema, versioning?: Versioning<Schema>) => LocalState<Schema>;
36
+ export declare const defineSessionState: <Schema extends StateSchema>(key: string, schema: Schema) => SessionState<Schema>;
37
+ //#endregion
@@ -0,0 +1,37 @@
1
+ import { createRowCodec } from "./row/codec.mjs";
2
+ //#region src/storage-state.ts
3
+ const STORAGE_KEY_PREFIX = "k8ordo-state:";
4
+ /**
5
+ * The key as a JavaScript string literal. `JSON.stringify` escapes it for
6
+ * JavaScript; `<` is escaped on top so the literal can never close the inline
7
+ * `<script>` it is written into, whatever the key contains.
8
+ */
9
+ const literalOf = (text) => JSON.stringify(text).replaceAll("<", String.raw`\u003c`);
10
+ const isObjectExpression = (value) => `${value}!==null&&typeof ${value}==="object"&&!Array.isArray(${value})`;
11
+ const inlineReadOf = (area, storageKey, version) => {
12
+ const row = `JSON.parse(${area}.getItem(${literalOf(storageKey)}))`;
13
+ return version === void 0 ? `(()=>{try{const v=${row};return ${isObjectExpression("v")}?v:null}catch{return null}})()` : `(()=>{try{const r=${row};const v=Array.isArray(r)&&r.length===2&&r[0]===${String(version)}?r[1]:null;return ${isObjectExpression("v")}?v:null}catch{return null}})()`;
14
+ };
15
+ const codecs = /* @__PURE__ */ new WeakMap();
16
+ const storageCodecOf = (def) => {
17
+ const codec = codecs.get(def);
18
+ if (codec === void 0) throw new TypeError(`"${def.key}" was not created by ${def.kind === "local" ? "defineLocalState" : "defineSessionState"} of this module instance`);
19
+ return codec;
20
+ };
21
+ const defineStorageState = (kind, area, key, schema, versioning) => {
22
+ const storageKey = `${STORAGE_KEY_PREFIX}${key}`;
23
+ const codec = createRowCodec(schema, kind, key, versioning);
24
+ const def = {
25
+ kind,
26
+ key,
27
+ schema,
28
+ storageKey,
29
+ inlineRead: () => inlineReadOf(area, storageKey, codec.version)
30
+ };
31
+ codecs.set(def, codec);
32
+ return def;
33
+ };
34
+ const defineLocalState = (key, schema, versioning) => defineStorageState("local", "localStorage", key, schema, versioning);
35
+ const defineSessionState = (key, schema) => defineStorageState("session", "sessionStorage", key, schema, void 0);
36
+ //#endregion
37
+ export { defineLocalState, defineSessionState, storageCodecOf };
@@ -0,0 +1,12 @@
1
+ import { StateValues } from "../schema/object.mjs";
2
+ import { CookieState } from "../cookie-state.mjs";
3
+ import { Store } from "./core.mjs";
4
+ //#region src/store/cookie-store.d.ts
5
+ export declare const cookieStoreOf: (def: CookieState) => Store;
6
+ /**
7
+ * What SSR and the hydration render see: the values the server read from the
8
+ * request's cookies when the caller passed them down; the defaults otherwise.
9
+ * Pure on purpose: the server must never touch `document` or create a store.
10
+ */
11
+ export declare const cookieInitialSnapshot: (def: CookieState, initialCookie: Readonly<StateValues> | undefined) => StateValues;
12
+ //#endregion
@@ -0,0 +1,129 @@
1
+ import { cookieCodecOf, parseCookieText } from "../cookie-state.mjs";
2
+ import { createHandle, createStoreCore, resolvePatch } from "./core.mjs";
3
+ import { getOrCreateStore } from "./registry.mjs";
4
+ //#region src/store/cookie-store.ts
5
+ const MAX_AGE = 3456e4;
6
+ const rawCookie = (name) => {
7
+ for (const part of document.cookie.split(";")) {
8
+ const at = part.indexOf("=");
9
+ if (at !== -1 && part.slice(0, at).trim() === name) return part.slice(at + 1).trim();
10
+ }
11
+ };
12
+ const decoded = (raw) => {
13
+ if (raw === void 0) return void 0;
14
+ try {
15
+ return decodeURIComponent(raw);
16
+ } catch {
17
+ return;
18
+ }
19
+ };
20
+ const createCookieStore = (def) => {
21
+ const codec = cookieCodecOf(def);
22
+ const { cookieName } = def;
23
+ const readRow = () => codec.read(parseCookieText(decoded(rawCookie(cookieName))));
24
+ const read = () => readRow().values;
25
+ let pending = null;
26
+ let handle = null;
27
+ let writing = 0;
28
+ let missed = false;
29
+ const sync = () => {
30
+ if (writing > 0) {
31
+ missed = true;
32
+ return;
33
+ }
34
+ missed = false;
35
+ const { values: fresh, stale } = readRow();
36
+ if (stale) writeBack();
37
+ core.applyNext(pending === null ? fresh : {
38
+ ...fresh,
39
+ ...pending
40
+ });
41
+ };
42
+ const onChange = (event) => {
43
+ if ([...event.changed, ...event.deleted].some((cookie) => cookie.name === cookieName)) sync();
44
+ };
45
+ cookieStore.addEventListener("change", onChange);
46
+ const write = async (patch, current) => {
47
+ try {
48
+ const target = codec.salvage({
49
+ ...read(),
50
+ ...patch
51
+ });
52
+ const init = {
53
+ name: cookieName,
54
+ value: encodeURIComponent(JSON.stringify(codec.row(target))),
55
+ path: "/",
56
+ sameSite: "lax",
57
+ maxAge: MAX_AGE
58
+ };
59
+ await cookieStore.set(init);
60
+ } catch (error) {
61
+ writing -= 1;
62
+ if (missed) sync();
63
+ current.fail(error);
64
+ return;
65
+ }
66
+ writing -= 1;
67
+ sync();
68
+ current.settle();
69
+ };
70
+ let writes = Promise.resolve();
71
+ const enqueue = (patch, current) => {
72
+ writing += 1;
73
+ writes = writes.then(() => write(patch, current));
74
+ };
75
+ const flush = () => {
76
+ const patch = pending;
77
+ const current = handle;
78
+ pending = null;
79
+ handle = null;
80
+ enqueue(patch, current);
81
+ };
82
+ function writeBack() {
83
+ enqueue({}, createHandle());
84
+ }
85
+ const first = readRow();
86
+ const core = createStoreCore(codec.keys, first.values);
87
+ if (first.stale) writeBack();
88
+ const update = (patch) => {
89
+ const resolved = resolvePatch(patch, core.snapshot(), codec.keys, def.key);
90
+ if (pending === null) {
91
+ pending = {};
92
+ handle = createHandle();
93
+ queueMicrotask(flush);
94
+ }
95
+ Object.assign(pending, resolved);
96
+ const echoed = codec.salvage({
97
+ ...core.snapshot(),
98
+ ...pending
99
+ });
100
+ for (const key of Object.keys(pending)) pending[key] = echoed[key];
101
+ core.applyNext(echoed);
102
+ return handle.external;
103
+ };
104
+ return {
105
+ subscribe: core.subscribe,
106
+ getSnapshot: core.getSnapshot,
107
+ update,
108
+ dispose: () => {
109
+ cookieStore.removeEventListener("change", onChange);
110
+ core.clear();
111
+ }
112
+ };
113
+ };
114
+ const cookieStoreOf = (def) => getOrCreateStore("cookie", def.key, () => createCookieStore(def));
115
+ /**
116
+ * What SSR and the hydration render see: the values the server read from the
117
+ * request's cookies when the caller passed them down; the defaults otherwise.
118
+ * Pure on purpose: the server must never touch `document` or create a store.
119
+ */
120
+ const cookieInitialSnapshot = (def, initialCookie) => {
121
+ const codec = cookieCodecOf(def);
122
+ const base = { ...codec.defaults };
123
+ if (initialCookie !== void 0) {
124
+ for (const key of codec.keys) if (key in initialCookie) base[key] = initialCookie[key];
125
+ }
126
+ return base;
127
+ };
128
+ //#endregion
129
+ export { cookieInitialSnapshot, cookieStoreOf };
@@ -4,17 +4,18 @@ import { StateValues } from "../schema/object.mjs";
4
4
  * Mirrors `navigation.navigate()`'s own return shape: an object holding the
5
5
  * promises rather than a promise, so fire-and-forget callers ignore it
6
6
  * without tripping no-floating-promises, and the 5% that await pick a stage.
7
- * Slots with no navigation behind them return both promises pre-settled —
8
- * the type stays uniform across every kind of state.
7
+ * Slots with no navigation behind them return the same pair, settled or
8
+ * rejected by the store itself — the type stays uniform across every kind of
9
+ * state.
9
10
  */
10
- type UpdateHandle = {
11
+ export type UpdateHandle = {
11
12
  /** The write is in its home (history entry, storage, memory). */
12
13
  committed: Promise<void>;
13
14
  /** All downstream work (a router's intercept fetch/render) is done. */
14
15
  finished: Promise<void>;
15
16
  };
16
- type Patch = StateValues | ((current: StateValues) => StateValues);
17
- type Store = {
17
+ export type Patch = StateValues | ((current: StateValues) => StateValues);
18
+ export type Store = {
18
19
  subscribe: (keys: readonly string[] | null, notify: () => void) => () => void;
19
20
  getSnapshot: (sig: string, keys: readonly string[] | null) => StateValues;
20
21
  update: (patch: Patch, options?: {
@@ -22,7 +23,7 @@ type Store = {
22
23
  }) => UpdateHandle;
23
24
  dispose: () => void;
24
25
  };
25
- type Handle = {
26
+ export type Handle = {
26
27
  external: UpdateHandle;
27
28
  settle: () => void;
28
29
  fail: (error: unknown) => void;
@@ -31,8 +32,8 @@ type Handle = {
31
32
  finished?: Promise<unknown> | undefined;
32
33
  }) => void;
33
34
  };
34
- declare const createHandle: () => Handle;
35
- type StoreCore = {
35
+ export declare const createHandle: () => Handle;
36
+ export type StoreCore = {
36
37
  snapshot: () => StateValues;
37
38
  applyNext: (next: StateValues) => void;
38
39
  subscribe: Store['subscribe'];
@@ -44,11 +45,10 @@ type StoreCore = {
44
45
  * exact per-key change detection, and notification only to the listeners
45
46
  * whose subscribed keys actually changed.
46
47
  */
47
- declare const createStoreCore: (keys: readonly string[], initial: StateValues) => StoreCore;
48
+ export declare const createStoreCore: (keys: readonly string[], initial: StateValues) => StoreCore;
48
49
  /**
49
50
  * The unknown-field guard every store shares: the typed API already prevents
50
51
  * this, so reaching it means an untyped caller — fail loudly, not silently.
51
52
  */
52
- declare const resolvePatch: (patch: Patch, current: StateValues, keys: readonly string[], label: string) => StateValues;
53
- //#endregion
54
- export { Handle, Patch, Store, StoreCore, UpdateHandle, createHandle, createStoreCore, resolvePatch };
53
+ export declare const resolvePatch: (patch: Patch, current: StateValues, keys: readonly string[], label: string) => StateValues;
54
+ //#endregion
@@ -2,8 +2,7 @@ import { StateValues } from "../schema/object.mjs";
2
2
  import { MemoryState } from "../memory-state.mjs";
3
3
  import { Store } from "./core.mjs";
4
4
  //#region src/store/memory-store.d.ts
5
- declare const memoryStoreOf: (def: MemoryState) => Store;
5
+ export declare const memoryStoreOf: (def: MemoryState) => Store;
6
6
  /** SSR renders the initial values — static, so no cross-request concern. */
7
- declare const memoryInitialSnapshot: (def: MemoryState) => StateValues;
8
- //#endregion
9
- export { memoryInitialSnapshot, memoryStoreOf };
7
+ export declare const memoryInitialSnapshot: (def: MemoryState) => StateValues;
8
+ //#endregion
@@ -2,20 +2,21 @@ import { StateValues } from "../schema/object.mjs";
2
2
  import { PageState } from "../page-state.mjs";
3
3
  import { Store } from "./core.mjs";
4
4
  //#region src/store/page-store.d.ts
5
- type UpdateOptions = {
5
+ export type UpdateOptions = {
6
6
  /**
7
7
  * `replace` by default: an update is a refinement of the current entry.
8
- * Opt into `push` for the updates the back button should undo.
8
+ * Opt into `push` for the updates the back button should undo. It applies
9
+ * only when a url value changes; an entry-only write rewrites the current
10
+ * entry in place.
9
11
  */
10
12
  history?: 'push' | 'replace';
11
13
  };
12
- declare const pageStoreOf: (def: PageState) => Store;
14
+ export declare const pageStoreOf: (def: PageState) => Store;
13
15
  /**
14
16
  * What SSR and the hydration render see: the RSC-parsed url when the caller
15
17
  * passed one down; defaults for everything else — the entry slot never
16
18
  * reaches the server. Pure on purpose: the server must never touch
17
19
  * `navigation` or create a store.
18
20
  */
19
- declare const pageInitialSnapshot: (def: PageState, initialUrl: Readonly<StateValues> | undefined) => StateValues;
20
- //#endregion
21
- export { UpdateOptions, pageInitialSnapshot, pageStoreOf };
21
+ export declare const pageInitialSnapshot: (def: PageState, initialUrl: Readonly<StateValues> | undefined) => StateValues;
22
+ //#endregion
@@ -51,37 +51,33 @@ const createPageStore = (def) => {
51
51
  pending = null;
52
52
  pendingPush = false;
53
53
  handle = null;
54
- const url = new URL(location.href);
55
- const stored = storedEntryState();
56
- const liveUrl = page.url === null ? {} : page.url.parse(url.searchParams);
57
- const liveEntry = page.entry === null ? {} : page.entry.parse(isRecord(stored) ? stored[def.key] : void 0);
58
- const target = canonical({
59
- ...liveUrl,
60
- ...liveEntry,
61
- ...patch
62
- });
63
- const urlChanged = page.url?.keys.some((key) => !sameValue(liveUrl[key], target[key])) ?? false;
64
- const entryChanged = page.entry?.keys.some((key) => !sameValue(liveEntry[key], target[key])) ?? false;
65
- if (!urlChanged && !entryChanged) {
66
- core.applyNext(target);
67
- current.settle();
68
- return;
69
- }
70
- const state = carryState(stored, target);
71
- if (!urlChanged) {
72
- try {
54
+ try {
55
+ const url = new URL(location.href);
56
+ const stored = storedEntryState();
57
+ const liveUrl = page.url === null ? {} : page.url.parse(url.searchParams);
58
+ const liveEntry = page.entry === null ? {} : page.entry.parse(isRecord(stored) ? stored[def.key] : void 0);
59
+ const target = canonical({
60
+ ...liveUrl,
61
+ ...liveEntry,
62
+ ...patch
63
+ });
64
+ const urlChanged = page.url?.keys.some((key) => !sameValue(liveUrl[key], target[key])) ?? false;
65
+ const entryChanged = page.entry?.keys.some((key) => !sameValue(liveEntry[key], target[key])) ?? false;
66
+ if (!urlChanged && !entryChanged) {
67
+ core.applyNext(target);
68
+ current.settle();
69
+ return;
70
+ }
71
+ const state = carryState(stored, target);
72
+ if (!urlChanged) {
73
73
  navigation.updateCurrentEntry({ state });
74
74
  current.settle();
75
- } catch (error) {
76
- current.fail(error);
75
+ return;
76
+ }
77
+ if (page.url !== null) {
78
+ for (const key of page.url.keys) url.searchParams.delete(key);
79
+ for (const [key, value] of new URLSearchParams(page.url.search(target))) url.searchParams.append(key, value);
77
80
  }
78
- return;
79
- }
80
- if (page.url !== null) {
81
- for (const key of page.url.keys) url.searchParams.delete(key);
82
- for (const [key, value] of new URLSearchParams(page.url.search(target))) url.searchParams.append(key, value);
83
- }
84
- try {
85
81
  current.adopt(navigation.navigate(url.href, {
86
82
  history: push ? "push" : "replace",
87
83
  state
@@ -92,6 +88,11 @@ const createPageStore = (def) => {
92
88
  };
93
89
  const update = (patch, options) => {
94
90
  const resolved = resolvePatch(patch, core.snapshot(), page.keys, def.key);
91
+ const echoed = canonical({
92
+ ...core.snapshot(),
93
+ ...pending,
94
+ ...resolved
95
+ });
95
96
  if (pending === null) {
96
97
  pending = {};
97
98
  handle = createHandle();
@@ -99,10 +100,6 @@ const createPageStore = (def) => {
99
100
  }
100
101
  Object.assign(pending, resolved);
101
102
  if (options?.history === "push") pendingPush = true;
102
- const echoed = canonical({
103
- ...core.snapshot(),
104
- ...pending
105
- });
106
103
  for (const key of Object.keys(pending)) pending[key] = echoed[key];
107
104
  core.applyNext(echoed);
108
105
  return handle.external;
@@ -1,5 +1,5 @@
1
1
  //#region src/store/registry.d.ts
2
- declare const getOrCreateStore: <Store extends {
2
+ export declare const getOrCreateStore: <Store extends {
3
3
  dispose?: () => void;
4
4
  }>(scope: string, key: string, create: () => Store) => Store;
5
5
  /**
@@ -7,6 +7,5 @@ declare const getOrCreateStore: <Store extends {
7
7
  * would carry state from one test into the next. Unmount components first —
8
8
  * mounted hooks keep their old store through closures.
9
9
  */
10
- declare const resetStateRegistry: () => void;
11
- //#endregion
12
- export { getOrCreateStore, resetStateRegistry };
10
+ export declare const resetStateRegistry: () => void;
11
+ //#endregion
@@ -0,0 +1,9 @@
1
+ import { StateValues } from "../schema/object.mjs";
2
+ import { LocalState, SessionState } from "../storage-state.mjs";
3
+ import { Store } from "./core.mjs";
4
+ //#region src/store/storage-store.d.ts
5
+ export declare const localStoreOf: (def: LocalState) => Store;
6
+ export declare const sessionStoreOf: (def: SessionState) => Store;
7
+ /** SSR and hydration see the defaults — the server has no Web Storage. */
8
+ export declare const storageInitialSnapshot: (def: LocalState | SessionState) => StateValues;
9
+ //#endregion
@@ -0,0 +1,97 @@
1
+ import { storageCodecOf } from "../storage-state.mjs";
2
+ import { createHandle, createStoreCore, resolvePatch } from "./core.mjs";
3
+ import { getOrCreateStore } from "./registry.mjs";
4
+ //#region src/store/storage-store.ts
5
+ const createStorageStore = (def, storage) => {
6
+ const codec = storageCodecOf(def);
7
+ const { storageKey } = def;
8
+ const write = (values) => {
9
+ storage.setItem(storageKey, JSON.stringify(codec.row(values)));
10
+ };
11
+ const readRow = () => {
12
+ const text = storage.getItem(storageKey);
13
+ let row;
14
+ try {
15
+ row = text === null ? void 0 : JSON.parse(text);
16
+ } catch {
17
+ row = void 0;
18
+ }
19
+ return codec.read(row);
20
+ };
21
+ const read = () => readRow().values;
22
+ const upgrade = () => {
23
+ queueMicrotask(() => {
24
+ const { values, stale } = readRow();
25
+ if (!stale) return;
26
+ try {
27
+ write(values);
28
+ } catch {}
29
+ });
30
+ };
31
+ const first = readRow();
32
+ const core = createStoreCore(codec.keys, first.values);
33
+ if (first.stale) upgrade();
34
+ let pending = null;
35
+ let handle = null;
36
+ const onStorage = (event) => {
37
+ if (event.storageArea !== storage) return;
38
+ if (event.key !== storageKey && event.key !== null) return;
39
+ const { values: fresh, stale } = readRow();
40
+ if (stale) upgrade();
41
+ core.applyNext(pending === null ? fresh : {
42
+ ...fresh,
43
+ ...pending
44
+ });
45
+ };
46
+ window.addEventListener("storage", onStorage);
47
+ const flush = () => {
48
+ const patch = pending;
49
+ const current = handle;
50
+ pending = null;
51
+ handle = null;
52
+ try {
53
+ const target = codec.salvage({
54
+ ...read(),
55
+ ...patch
56
+ });
57
+ const values = {};
58
+ for (const key of codec.keys) values[key] = target[key];
59
+ write(values);
60
+ core.applyNext(target);
61
+ current.settle();
62
+ } catch (error) {
63
+ current.fail(error);
64
+ }
65
+ };
66
+ const update = (patch) => {
67
+ const resolved = resolvePatch(patch, core.snapshot(), codec.keys, def.key);
68
+ if (pending === null) {
69
+ pending = {};
70
+ handle = createHandle();
71
+ queueMicrotask(flush);
72
+ }
73
+ Object.assign(pending, resolved);
74
+ const echoed = codec.salvage({
75
+ ...core.snapshot(),
76
+ ...pending
77
+ });
78
+ for (const key of Object.keys(pending)) pending[key] = echoed[key];
79
+ core.applyNext(echoed);
80
+ return handle.external;
81
+ };
82
+ return {
83
+ subscribe: core.subscribe,
84
+ getSnapshot: core.getSnapshot,
85
+ update,
86
+ dispose: () => {
87
+ window.removeEventListener("storage", onStorage);
88
+ core.clear();
89
+ }
90
+ };
91
+ };
92
+ const localStoreOf = (def) => getOrCreateStore("local", def.key, () => createStorageStore(def, localStorage));
93
+ const sessionStoreOf = (def) => getOrCreateStore("session", def.key, () => createStorageStore(def, sessionStorage));
94
+ /** SSR and hydration see the defaults — the server has no Web Storage. */
95
+ const storageInitialSnapshot = (def) => ({ ...storageCodecOf(def).defaults });
96
+ //#endregion
97
+ export { localStoreOf, sessionStoreOf, storageInitialSnapshot };
@@ -1,8 +1,8 @@
1
1
  import { StateSchema, StateValues } from "../schema/object.mjs";
2
2
  //#region src/url/codec.d.ts
3
3
  /** `URLSearchParams`, or the object shape frameworks hand to a page. */
4
- type UrlInput = URLSearchParams | Readonly<Record<string, string | readonly string[] | undefined>>;
5
- type UrlCodec = {
4
+ export type UrlInput = URLSearchParams | Readonly<Record<string, string | readonly string[] | undefined>>;
5
+ export type UrlCodec = {
6
6
  keys: readonly string[];
7
7
  defaults: Readonly<StateValues>;
8
8
  parse: (input: UrlInput) => StateValues;
@@ -10,6 +10,5 @@ type UrlCodec = {
10
10
  /** Runs already-typed values through the schema, salvage included. */
11
11
  salvage: (values: Readonly<StateValues>) => StateValues;
12
12
  };
13
- declare const createUrlCodec: (schema: StateSchema) => UrlCodec;
14
- //#endregion
15
- export { UrlCodec, UrlInput, createUrlCodec };
13
+ export declare const createUrlCodec: (schema: StateSchema) => UrlCodec;
14
+ //#endregion