react-mnemonic 0.1.1-alpha.0 → 1.1.0-beta0

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.
package/README.md CHANGED
@@ -3,32 +3,44 @@
3
3
  Persistent, type-safe state management for React.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/react-mnemonic.svg)](https://www.npmjs.com/package/react-mnemonic)
6
- [![bundle size](https://img.shields.io/bundlephobia/minzip/react-mnemonic)](https://bundlephobia.com/package/react-mnemonic)
6
+ [![docs](https://img.shields.io/badge/docs-online-0A7EA4.svg)](https://thirtytwobits.github.io/react-mnemonic/)
7
+ [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=thirtytwobits_react-mnemonic&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=thirtytwobits_react-mnemonic)
8
+ [![dependencies](https://img.shields.io/badge/dependencies-0-success.svg)](https://www.npmjs.com/package/react-mnemonic)
7
9
  [![license](https://img.shields.io/npm/l/react-mnemonic.svg)](./LICENSE.md)
8
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](https://www.typescriptlang.org/)
9
11
 
10
- > **⚠️ Alpha Software** — This library is in active development. APIs may
11
- > change between releases without prior notice. Use in production at your own
12
- > risk.
13
-
14
- ---
15
-
16
12
  **react-mnemonic** gives your React components persistent memory. Values survive
17
13
  page refreshes, synchronize across tabs, and stay type-safe end-to-end -- all
18
14
  through a single hook that works like `useState`.
19
15
 
16
+ `1.1.0-beta0` is the current prerelease on the npm `latest` dist-tag. This minor
17
+ `beta0` cut lands ahead of the `beta1` stabilization milestone, which is
18
+ expected to receive only fixes and polish before the first production release.
19
+
20
+ If you are evaluating alternatives, see the
21
+ [Comparison Guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/comparisons-and-benchmarks).
22
+ It publishes reproducible bundle measurements, an AI-friendliness benchmark
23
+ built around one-shot app-building prompts, and controlled SSR/API tradeoff
24
+ notes for `react-mnemonic`, Zustand persist, Jotai `atomWithStorage`,
25
+ `use-local-storage-state`, and `usehooks-ts`.
26
+
20
27
  ## Features
21
28
 
22
29
  - **`useState`-like API** -- `useMnemonicKey` returns `{ value, set, reset, remove }`
23
30
  - **JSON Schema validation** -- optional schema-based validation using a built-in JSON Schema subset
24
31
  - **Namespace isolation** -- `MnemonicProvider` prefixes every key to prevent collisions
25
32
  - **Cross-tab sync** -- opt-in `listenCrossTab` uses the browser `storage` event
26
- - **Pluggable storage** -- bring your own backend via the `StorageLike` interface (IndexedDB, sessionStorage, etc.)
33
+ - **Pluggable storage** -- use `localStorage`, `sessionStorage`, or a synchronous `StorageLike` facade over custom persistence
27
34
  - **Schema versioning and migration** -- upgrade stored data with versioned schemas and migration rules
35
+ - **Typed schema cohesion helpers** -- define one schema object and reuse it across runtime validation, key descriptors, and migrations
36
+ - **Structural migration helpers** -- optional tree utilities for idempotent insert/rename/dedupe migration steps
37
+ - **Read-time reconciliation** -- selectively enforce new defaults on persisted values without clearing the whole key
38
+ - **Recovery helpers** -- build user-facing soft reset and hard reset flows with namespace-scoped clear helpers
28
39
  - **Write-time normalization** -- migrations where `fromVersion === toVersion` run on every write
40
+ - **First-class key descriptors** -- define canonical, reusable key contracts once with `defineMnemonicKey(...)`
29
41
  - **Lifecycle callbacks** -- `onMount` and `onChange` hooks
30
42
  - **DevTools** -- inspect and mutate state from the browser console
31
- - **SSR-safe** -- returns defaults when `window` is unavailable
43
+ - **SSR-safe with explicit controls** -- defaults to `defaultValue` on the server, with optional `ssr.serverValue` and `client-only` hydration
32
44
  - **Tree-shakeable, zero dependencies** -- ships ESM + CJS with full TypeScript declarations
33
45
 
34
46
  ## Installation
@@ -47,7 +59,8 @@ pnpm add react-mnemonic
47
59
 
48
60
  ### Peer dependencies
49
61
 
50
- React 18 or later is required.
62
+ React 18 or later is required. CI verifies packaged-consumer installs against
63
+ React 18 and React 19.
51
64
 
52
65
  ```json
53
66
  {
@@ -90,6 +103,173 @@ export default function App() {
90
103
  The counter value persists in `localStorage` under the key `my-app.count` and
91
104
  survives full page reloads.
92
105
 
106
+ In server-rendered apps, `useMnemonicKey(...)` renders `defaultValue` on the
107
+ server by default and then hydrates to persisted storage on the client. When
108
+ you need a deterministic server placeholder or want to delay storage reads
109
+ until after mount, use `ssr.serverValue` and `ssr.hydration: "client-only"`.
110
+ See the
111
+ [Server Rendering guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/server-rendering)
112
+ for Next.js and Remix examples.
113
+
114
+ If the same key is used in multiple components, consider defining it once with
115
+ `defineMnemonicKey(...)` and reusing that descriptor everywhere. This keeps the
116
+ contract importable and explicit for both humans and AI-assisted tooling.
117
+
118
+ For a deterministic implementation reference aimed at AI agents and advanced
119
+ users, see the
120
+ [AI docs overview](https://thirtytwobits.github.io/react-mnemonic/docs/ai).
121
+ The canonical agent-facing prose lives under
122
+ [`website/docs/ai/`](https://github.com/thirtytwobits/react-mnemonic/tree/main/website/docs/ai),
123
+ with compact retrieval surfaces published as
124
+ [`llms.txt`](https://thirtytwobits.github.io/react-mnemonic/llms.txt),
125
+ [`llms-full.txt`](https://thirtytwobits.github.io/react-mnemonic/llms-full.txt),
126
+ and
127
+ [`ai-contract.json`](https://thirtytwobits.github.io/react-mnemonic/ai-contract.json).
128
+ Agents should import published types from `react-mnemonic` and must not invent
129
+ local `.d.ts` shims for the package.
130
+
131
+ If you need evidence for where `react-mnemonic` is heavier, more explicit, or
132
+ better suited to SSR-sensitive persistence than lighter hooks, see the
133
+ [Comparison Guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/comparisons-and-benchmarks).
134
+
135
+ Persist only the durable slice of your app state. `useMnemonicKey` stores
136
+ whatever you pass to `set`, so keep transient UI state like loading flags,
137
+ hover state, and draft search text in plain React state unless you explicitly
138
+ want them to rehydrate after reload. See the
139
+ [Persisted vs Ephemeral State guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/persisted-vs-ephemeral-state)
140
+ for patterns and an interactive example.
141
+
142
+ For self-service recovery UX, pair your per-key hooks with
143
+ `useMnemonicRecovery` so users can clear stale filters, reset broken settings,
144
+ or fully wipe a namespace without opening DevTools. See the
145
+ [Reset and Recovery guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/reset-and-recovery)
146
+ for soft-reset and hard-reset recipes.
147
+
148
+ If a field must stay cleared across reloads, model it as nullable and persist
149
+ `null` explicitly. `remove()` deletes the key and falls back to `defaultValue`,
150
+ while `reset()` writes the default again. See the
151
+ [Clearable Persisted Values guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/clearable-persisted-values)
152
+ for the canonical nullable pattern.
153
+
154
+ ## Canonical key definitions
155
+
156
+ When the same persisted key appears in more than one component, define it once
157
+ and reuse it:
158
+
159
+ ```tsx
160
+ import { defineMnemonicKey, useMnemonicKey } from "react-mnemonic";
161
+
162
+ export const themeKey = defineMnemonicKey("theme", {
163
+ defaultValue: "light" as "light" | "dark",
164
+ listenCrossTab: true,
165
+ });
166
+
167
+ function ThemeToggle() {
168
+ const { value: theme, set } = useMnemonicKey(themeKey);
169
+ return <button onClick={() => set(theme === "light" ? "dark" : "light")}>{theme}</button>;
170
+ }
171
+
172
+ function ThemePreview() {
173
+ const { value: theme } = useMnemonicKey(themeKey);
174
+ return <p>Current theme: {theme}</p>;
175
+ }
176
+ ```
177
+
178
+ Descriptors help with:
179
+
180
+ - keeping one canonical key contract per persisted value
181
+ - reusing the same `defaultValue`, `schema`, `codec`, and `reconcile` logic
182
+ - making AI-assisted code generation and refactors less ambiguous
183
+
184
+ The original lightweight form still works:
185
+
186
+ ```ts
187
+ const { value, set } = useMnemonicKey("theme", {
188
+ defaultValue: "light" as "light" | "dark",
189
+ });
190
+ ```
191
+
192
+ ## Single source of truth schemas
193
+
194
+ If you want the same schema object to drive both runtime validation and
195
+ TypeScript inference, use the typed schema helpers:
196
+
197
+ ```tsx
198
+ import {
199
+ MnemonicProvider,
200
+ createSchemaRegistry,
201
+ defineKeySchema,
202
+ defineMnemonicKey,
203
+ defineMigration,
204
+ mnemonicSchema,
205
+ useMnemonicKey,
206
+ } from "react-mnemonic";
207
+
208
+ const profileV1 = defineKeySchema(
209
+ "profile",
210
+ 1,
211
+ mnemonicSchema.object({
212
+ name: mnemonicSchema.string(),
213
+ }),
214
+ );
215
+
216
+ const profileV2 = defineKeySchema(
217
+ "profile",
218
+ 2,
219
+ mnemonicSchema.object({
220
+ name: mnemonicSchema.string(),
221
+ email: mnemonicSchema.string(),
222
+ }),
223
+ );
224
+
225
+ const profileKey = defineMnemonicKey(profileV2, {
226
+ defaultValue: { name: "", email: "" },
227
+ reconcile: (value) => ({
228
+ ...value,
229
+ email: value.email.trim().toLowerCase(),
230
+ }),
231
+ });
232
+
233
+ const registry = createSchemaRegistry({
234
+ schemas: [profileV1, profileV2],
235
+ migrations: [
236
+ defineMigration(profileV1, profileV2, (value) => ({
237
+ ...value,
238
+ email: "",
239
+ })),
240
+ ],
241
+ });
242
+
243
+ function ProfileForm() {
244
+ const { value: profile, set } = useMnemonicKey(profileKey);
245
+ return <button onClick={() => set({ ...profile, email: "hello@example.com" })}>Save</button>;
246
+ }
247
+
248
+ export default function App() {
249
+ return (
250
+ <MnemonicProvider namespace="my-app" schemaMode="default" schemaRegistry={registry}>
251
+ <ProfileForm />
252
+ </MnemonicProvider>
253
+ );
254
+ }
255
+ ```
256
+
257
+ This path gives you:
258
+
259
+ - one schema object reused in the registry, key descriptor, and migrations
260
+ - inferred types for `defaultValue`, `value`, `set`, and `reconcile`
261
+ - typed migration callbacks via `defineMigration(...)`
262
+
263
+ Tradeoffs versus the lightweight JSON-only path:
264
+
265
+ - more setup upfront
266
+ - best fit when a key is schema-managed and long-lived
267
+ - not necessary for simple keys where `defaultValue` inference is already enough
268
+
269
+ For a side-by-side comparison against competing persistence libraries, including
270
+ bundle-size measurements and qualitative SSR/API notes, see the
271
+ [Comparison Guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/comparisons-and-benchmarks).
272
+
93
273
  ## API
94
274
 
95
275
  ### `<MnemonicProvider>`
@@ -110,11 +290,42 @@ Context provider that scopes storage keys under a namespace.
110
290
 
111
291
  Multiple providers with different namespaces can coexist in the same app.
112
292
 
113
- ### `useMnemonicKey<T>(key, options)`
293
+ ### `defineMnemonicKey(key, options)`
294
+
295
+ Define a reusable descriptor for a single persisted key.
296
+
297
+ ```ts
298
+ const themeKey = defineMnemonicKey("theme", {
299
+ defaultValue: "light" as "light" | "dark",
300
+ listenCrossTab: true,
301
+ });
302
+ ```
303
+
304
+ The returned descriptor can be imported and passed directly to `useMnemonicKey`.
305
+
306
+ ### `mnemonicSchema`, `defineKeySchema`, and `defineMigration`
307
+
308
+ Use these helpers when you want a schema-managed key to have one source of
309
+ truth for runtime validation and TypeScript inference.
310
+
311
+ ```ts
312
+ const themeSchema = defineKeySchema("theme", 1, mnemonicSchema.enum(["light", "dark"] as const));
313
+
314
+ const themeKey = defineMnemonicKey(themeSchema, {
315
+ defaultValue: "light",
316
+ });
317
+ ```
318
+
319
+ `defineMigration(fromSchema, toSchema, migrate)` infers the migration callback
320
+ from the source and target schemas, while `defineWriteMigration(schema, migrate)`
321
+ creates a typed same-version normalizer.
322
+
323
+ ### `useMnemonicKey<T>(descriptor)` / `useMnemonicKey<T>(key, options)`
114
324
 
115
325
  Hook for reading and writing a single persistent value.
116
326
 
117
327
  ```ts
328
+ const { value, set, reset, remove } = useMnemonicKey(themeKey);
118
329
  const { value, set, reset, remove } = useMnemonicKey<T>(key, options);
119
330
  ```
120
331
 
@@ -125,16 +336,48 @@ const { value, set, reset, remove } = useMnemonicKey<T>(key, options);
125
336
  | `reset` | `() => void` | Reset to `defaultValue` and persist it |
126
337
  | `remove` | `() => void` | Delete the key from storage entirely |
127
338
 
339
+ For clearable fields, remember the semantic split:
340
+
341
+ - `set(null)` persists a cleared value and stays cleared after reload
342
+ - `remove()` deletes the key, so the next read falls back to `defaultValue`
343
+ - `reset()` persists `defaultValue`
344
+
128
345
  #### Options
129
346
 
130
- | Option | Type | Default | Description |
131
- | ---------------- | ------------------------------------------------- | ----------- | --------------------------------------------------- |
132
- | `defaultValue` | `T \| ((error?: CodecError \| SchemaError) => T)` | _required_ | Fallback value or error-aware factory |
133
- | `codec` | `Codec<T>` | `JSONCodec` | Encode/decode strategy (bypasses schema validation) |
134
- | `onMount` | `(value: T) => void` | -- | Called once with the initial value |
135
- | `onChange` | `(value: T, prev: T) => void` | -- | Called on every value change |
136
- | `listenCrossTab` | `boolean` | `false` | Sync via the browser `storage` event |
137
- | `schema` | `{ version?: number }` | -- | Pin writes to a specific schema version |
347
+ | Option | Type | Default | Description |
348
+ | ---------------- | ------------------------------------------------- | ----------- | ------------------------------------------------------------- |
349
+ | `defaultValue` | `T \| ((error?: CodecError \| SchemaError) => T)` | _required_ | Fallback value or error-aware factory |
350
+ | `codec` | `Codec<T>` | `JSONCodec` | Encode/decode strategy (bypasses schema validation) |
351
+ | `reconcile` | `(value: T, context: ReconcileContext) => T` | -- | Adjust a persisted value after read and persist if it changes |
352
+ | `onMount` | `(value: T) => void` | -- | Called once with the initial value |
353
+ | `onChange` | `(value: T, prev: T) => void` | -- | Called on every value change |
354
+ | `listenCrossTab` | `boolean` | `false` | Sync via the browser `storage` event |
355
+ | `schema` | `{ version?: number }` | -- | Pin writes to a specific schema version |
356
+
357
+ ### `useMnemonicRecovery(options)`
358
+
359
+ Hook for namespace-scoped recovery actions such as "clear saved filters" or
360
+ "reset all persisted app data".
361
+
362
+ ```ts
363
+ const { namespace, canEnumerateKeys, listKeys, clearAll, clearKeys, clearMatching } = useMnemonicRecovery({
364
+ onRecover: (event) => console.log(event.action, event.clearedKeys),
365
+ });
366
+ ```
367
+
368
+ | Return | Type | Description |
369
+ | ------------------ | --------------------------------------------------- | ----------------------------------------------------- |
370
+ | `namespace` | `string` | Current provider namespace |
371
+ | `canEnumerateKeys` | `boolean` | Whether the storage backend can list namespace keys |
372
+ | `listKeys` | `() => string[]` | List visible unprefixed keys in the current namespace |
373
+ | `clearAll` | `() => string[]` | Clear every key in the namespace |
374
+ | `clearKeys` | `(keys: readonly string[]) => string[]` | Clear an explicit set of unprefixed keys |
375
+ | `clearMatching` | `(predicate: (key: string) => boolean) => string[]` | Clear keys whose names match a predicate |
376
+
377
+ `clearAll()` and `clearMatching()` require an enumerable storage backend such
378
+ as `localStorage` or `sessionStorage`. If your custom storage does not support
379
+ `length` and `key(index)`, use `clearKeys([...])` with the explicit durable-key
380
+ list your app owns.
138
381
 
139
382
  ### Codecs
140
383
 
@@ -169,9 +412,18 @@ interface StorageLike {
169
412
  }
170
413
  ```
171
414
 
415
+ `StorageLike` is intentionally synchronous in v1 because Mnemonic's core
416
+ store is built on React's synchronous snapshot contract. Async backends are
417
+ still possible, but they need a synchronous facade: keep an in-memory cache for
418
+ `getItem`/`setItem`/`removeItem`, then flush to IndexedDB or another async
419
+ system outside the hook contract. Promise-returning `StorageLike` methods are
420
+ unsupported and are treated as a storage misuse fallback at runtime.
421
+
172
422
  `onExternalChange` enables cross-tab sync for non-localStorage backends (e.g.
173
423
  IndexedDB over `BroadcastChannel`). The library handles all error cases
174
424
  internally -- see the `StorageLike` JSDoc for the full error-handling contract.
425
+ Namespace-wide recovery helpers can only enumerate keys when the backend also
426
+ implements `length` and `key(index)`.
175
427
 
176
428
  ### `validateJsonSchema(schema, value)`
177
429
 
@@ -318,84 +570,109 @@ const normalizer: MigrationRule = {
318
570
  };
319
571
  ```
320
572
 
573
+ ### Reconciliation
574
+
575
+ Use `reconcile` when you want to keep persisted data but selectively enforce
576
+ new application defaults after the value has been decoded and any read-time
577
+ migrations have already run.
578
+
579
+ ```ts
580
+ const { value } = useMnemonicKey("preferences", {
581
+ defaultValue: { theme: "dark", density: "comfortable", accents: true },
582
+ reconcile: (persisted, { persistedVersion }) => ({
583
+ ...persisted,
584
+ accents: persistedVersion === 0 ? true : persisted.accents,
585
+ }),
586
+ });
587
+ ```
588
+
589
+ Use a schema migration when the stored shape must move from one explicit version
590
+ to another. Use `reconcile` for conditional, field-level policy changes such as
591
+ rolling out a new default while preserving the rest of a user's stored data.
592
+
593
+ ### Structural migration helpers
594
+
595
+ For layout-like data that already uses `id` and `children`, Mnemonic ships
596
+ optional pure helpers for common idempotent migration steps:
597
+
598
+ ```ts
599
+ import { insertChildIfMissing, renameNode, dedupeChildrenBy } from "react-mnemonic";
600
+
601
+ const migrated = dedupeChildrenBy(
602
+ renameNode(insertChildIfMissing(layout, "sidebar", { id: "search", title: "Search" }), "prefs", "preferences"),
603
+ (node) => node.id,
604
+ );
605
+ ```
606
+
607
+ Use these inside your `MigrationRule.migrate` functions when you want repeatable
608
+ tree edits without hand-writing the same traversal logic each time. See the
609
+ [Schema Migration guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/schema-migration)
610
+ for a cookbook example and custom adapter usage.
611
+
321
612
  ### Example schema registry
322
613
 
323
614
  A schema registry stores versioned schemas for each key, and resolves migration
324
- paths to upgrade stored data. Schemas are plain JSON (serializable); migrations
325
- are procedural functions.
615
+ paths to upgrade stored data. For the common immutable case, use
616
+ `createSchemaRegistry(...)` instead of hand-rolling the indexing boilerplate.
326
617
 
327
618
  ```tsx
328
619
  import {
620
+ createSchemaRegistry,
329
621
  MnemonicProvider,
330
622
  useMnemonicKey,
331
- type SchemaRegistry,
332
623
  type KeySchema,
333
624
  type MigrationRule,
334
625
  } from "react-mnemonic";
335
626
 
336
- const schemas = new Map<string, KeySchema>();
337
- const migrations: MigrationRule[] = [];
338
-
339
- const registry: SchemaRegistry = {
340
- getSchema: (key, version) => schemas.get(`${key}:${version}`),
341
- getLatestSchema: (key) =>
342
- Array.from(schemas.values())
343
- .filter((schema) => schema.key === key)
344
- .sort((a, b) => b.version - a.version)[0],
345
- getMigrationPath: (key, fromVersion, toVersion) => {
346
- const byKey = migrations.filter((rule) => rule.key === key);
347
- const path: MigrationRule[] = [];
348
- let cur = fromVersion;
349
- while (cur < toVersion) {
350
- const next = byKey.find((rule) => rule.fromVersion === cur);
351
- if (!next) return null;
352
- path.push(next);
353
- cur = next.toVersion;
354
- }
355
- return path;
356
- },
357
- getWriteMigration: (key, version) => {
358
- return migrations.find((r) => r.key === key && r.fromVersion === version && r.toVersion === version);
359
- },
360
- registerSchema: (schema) => {
361
- const id = `${schema.key}:${schema.version}`;
362
- if (schemas.has(id)) throw new Error(`Schema already registered for ${id}`);
363
- schemas.set(id, schema);
627
+ const schemas: KeySchema[] = [
628
+ {
629
+ key: "profile",
630
+ version: 1,
631
+ schema: {
632
+ type: "object",
633
+ properties: { name: { type: "string" }, email: { type: "string" } },
634
+ required: ["name", "email"],
635
+ },
364
636
  },
365
- };
366
-
367
- registry.registerSchema({
368
- key: "profile",
369
- version: 1,
370
- schema: {
371
- type: "object",
372
- properties: { name: { type: "string" }, email: { type: "string" } },
373
- required: ["name", "email"],
637
+ {
638
+ key: "profile",
639
+ version: 2,
640
+ schema: {
641
+ type: "object",
642
+ properties: {
643
+ name: { type: "string" },
644
+ email: { type: "string" },
645
+ migratedAt: { type: "string" },
646
+ },
647
+ required: ["name", "email", "migratedAt"],
648
+ },
374
649
  },
375
- });
376
-
377
- migrations.push({
378
- key: "profile",
379
- fromVersion: 1,
380
- toVersion: 2,
381
- migrate: (value) => {
382
- const v1 = value as { name: string; email: string };
383
- return { ...v1, migratedAt: new Date().toISOString() };
650
+ ];
651
+
652
+ const migrations: MigrationRule[] = [
653
+ {
654
+ key: "profile",
655
+ fromVersion: 1,
656
+ toVersion: 2,
657
+ migrate: (value) => {
658
+ const v1 = value as { name: string; email: string };
659
+ return { ...v1, migratedAt: new Date().toISOString() };
660
+ },
384
661
  },
385
- });
386
-
387
- registry.registerSchema({
388
- key: "profile",
389
- version: 2,
390
- schema: {
391
- type: "object",
392
- properties: {
393
- name: { type: "string" },
394
- email: { type: "string" },
395
- migratedAt: { type: "string" },
662
+ {
663
+ key: "profile",
664
+ fromVersion: 2,
665
+ toVersion: 2,
666
+ migrate: (value) => {
667
+ const profile = value as { name: string; email: string; migratedAt: string };
668
+ return { ...profile, email: profile.email.trim().toLowerCase() };
396
669
  },
397
- required: ["name", "email", "migratedAt"],
398
670
  },
671
+ ];
672
+
673
+ const registry = createSchemaRegistry({
674
+ schemas,
675
+ migrations,
399
676
  });
400
677
 
401
678
  function ProfileEditor() {
@@ -410,6 +687,11 @@ function ProfileEditor() {
410
687
  </MnemonicProvider>;
411
688
  ```
412
689
 
690
+ `createSchemaRegistry` validates duplicate schemas and ambiguous migration
691
+ graphs up front. If you need runtime schema registration for
692
+ `schemaMode="autoschema"`, keep a custom mutable `SchemaRegistry`
693
+ implementation.
694
+
413
695
  ### Registry immutability
414
696
 
415
697
  In `default` and `strict` modes, the schema registry is treated as immutable for
@@ -425,22 +707,40 @@ version and remount the provider.
425
707
  import { MnemonicProvider } from "react-mnemonic";
426
708
  import type { StorageLike } from "react-mnemonic";
427
709
 
710
+ const cache = new Map<string, string>();
711
+ const queueIndexedDbWrite = (key: string, value: string) => {
712
+ // application-specific async persistence
713
+ };
714
+ const queueIndexedDbDelete = (key: string) => {
715
+ // application-specific async persistence
716
+ };
717
+
428
718
  const idbStorage: StorageLike = {
429
- getItem: (key) => /* read from IndexedDB */,
430
- setItem: (key, value) => /* write to IndexedDB */,
431
- removeItem: (key) => /* delete from IndexedDB */,
432
- onExternalChange: (cb) => {
433
- const bc = new BroadcastChannel("my-app-sync");
434
- bc.onmessage = (e) => cb(e.data.keys);
435
- return () => bc.close();
436
- },
719
+ getItem: (key) => cache.get(key) ?? null,
720
+ setItem: (key, value) => {
721
+ cache.set(key, value);
722
+ queueIndexedDbWrite(key, value);
723
+ },
724
+ removeItem: (key) => {
725
+ cache.delete(key);
726
+ queueIndexedDbDelete(key);
727
+ },
728
+ onExternalChange: (cb) => {
729
+ const bc = new BroadcastChannel("my-app-sync");
730
+ bc.onmessage = (e) => cb(e.data.keys);
731
+ return () => bc.close();
732
+ },
437
733
  };
438
734
 
439
735
  <MnemonicProvider namespace="my-app" storage={idbStorage}>
440
- <App />
441
- </MnemonicProvider>
736
+ <App />
737
+ </MnemonicProvider>;
442
738
  ```
443
739
 
740
+ If your real persistence layer is async, initialize that adapter before
741
+ rendering the provider and return a synchronous `StorageLike` facade, like the
742
+ IndexedDB demo in the docs site.
743
+
444
744
  ### DevTools
445
745
 
446
746
  Enable the console inspector in development:
@@ -452,12 +752,14 @@ Enable the console inspector in development:
452
752
  Then in the browser console:
453
753
 
454
754
  ```js
455
- __REACT_MNEMONIC_DEVTOOLS__.app.dump(); // table of all keys
456
- __REACT_MNEMONIC_DEVTOOLS__.app.get("theme"); // read a decoded value
457
- __REACT_MNEMONIC_DEVTOOLS__.app.set("theme", "dark"); // write
458
- __REACT_MNEMONIC_DEVTOOLS__.app.remove("theme"); // delete
459
- __REACT_MNEMONIC_DEVTOOLS__.app.keys(); // list all keys
460
- __REACT_MNEMONIC_DEVTOOLS__.app.clear(); // remove all keys
755
+ const app = window.__REACT_MNEMONIC_DEVTOOLS__?.resolve("app");
756
+
757
+ app?.dump(); // table of all keys
758
+ app?.get("theme"); // read a decoded value
759
+ app?.set("theme", "dark"); // write
760
+ app?.remove("theme"); // delete
761
+ app?.keys(); // list all keys
762
+ app?.clear(); // remove all keys
461
763
  ```
462
764
 
463
765
  ## TypeScript
@@ -469,6 +771,7 @@ All public types are re-exported from the package root:
469
771
  import type {
470
772
  Codec,
471
773
  StorageLike,
774
+ MnemonicKeyDescriptor,
472
775
  MnemonicProviderOptions,
473
776
  MnemonicProviderProps,
474
777
  UseMnemonicKeyOptions,