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 +399 -96
- package/dist/index.cjs +1672 -471
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +963 -45
- package/dist/index.d.ts +963 -45
- package/dist/index.js +1663 -473
- package/dist/index.js.map +1 -1
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -3,32 +3,44 @@
|
|
|
3
3
|
Persistent, type-safe state management for React.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/react-mnemonic)
|
|
6
|
-
[](https://thirtytwobits.github.io/react-mnemonic/)
|
|
7
|
+
[](https://sonarcloud.io/summary/new_code?id=thirtytwobits_react-mnemonic)
|
|
8
|
+
[](https://www.npmjs.com/package/react-mnemonic)
|
|
7
9
|
[](./LICENSE.md)
|
|
8
10
|
[](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** --
|
|
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** --
|
|
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
|
-
### `
|
|
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
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
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.
|
|
325
|
-
|
|
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 =
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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,
|