react-mnemonic 0.1.1-alpha.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.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright Scott Dixon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,497 @@
1
+ # react-mnemonic
2
+
3
+ Persistent, type-safe state management for React.
4
+
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)
7
+ [![license](https://img.shields.io/npm/l/react-mnemonic.svg)](./LICENSE.md)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](https://www.typescriptlang.org/)
9
+
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
+ **react-mnemonic** gives your React components persistent memory. Values survive
17
+ page refreshes, synchronize across tabs, and stay type-safe end-to-end -- all
18
+ through a single hook that works like `useState`.
19
+
20
+ ## Features
21
+
22
+ - **`useState`-like API** -- `useMnemonicKey` returns `{ value, set, reset, remove }`
23
+ - **JSON Schema validation** -- optional schema-based validation using a built-in JSON Schema subset
24
+ - **Namespace isolation** -- `MnemonicProvider` prefixes every key to prevent collisions
25
+ - **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.)
27
+ - **Schema versioning and migration** -- upgrade stored data with versioned schemas and migration rules
28
+ - **Write-time normalization** -- migrations where `fromVersion === toVersion` run on every write
29
+ - **Lifecycle callbacks** -- `onMount` and `onChange` hooks
30
+ - **DevTools** -- inspect and mutate state from the browser console
31
+ - **SSR-safe** -- returns defaults when `window` is unavailable
32
+ - **Tree-shakeable, zero dependencies** -- ships ESM + CJS with full TypeScript declarations
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ npm install react-mnemonic
38
+ ```
39
+
40
+ ```bash
41
+ yarn add react-mnemonic
42
+ ```
43
+
44
+ ```bash
45
+ pnpm add react-mnemonic
46
+ ```
47
+
48
+ ### Peer dependencies
49
+
50
+ React 18 or later is required.
51
+
52
+ ```json
53
+ {
54
+ "peerDependencies": {
55
+ "react": ">=18",
56
+ "react-dom": ">=18"
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## Quick start
62
+
63
+ Wrap your app in a `MnemonicProvider`, then call `useMnemonicKey` anywhere inside it.
64
+
65
+ ```tsx
66
+ import { MnemonicProvider, useMnemonicKey } from "react-mnemonic";
67
+
68
+ function Counter() {
69
+ const { value: count, set } = useMnemonicKey("count", {
70
+ defaultValue: 0,
71
+ });
72
+
73
+ return (
74
+ <div>
75
+ <p>Count: {count}</p>
76
+ <button onClick={() => set((c) => c + 1)}>Increment</button>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ export default function App() {
82
+ return (
83
+ <MnemonicProvider namespace="my-app">
84
+ <Counter />
85
+ </MnemonicProvider>
86
+ );
87
+ }
88
+ ```
89
+
90
+ The counter value persists in `localStorage` under the key `my-app.count` and
91
+ survives full page reloads.
92
+
93
+ ## API
94
+
95
+ ### `<MnemonicProvider>`
96
+
97
+ Context provider that scopes storage keys under a namespace.
98
+
99
+ ```tsx
100
+ <MnemonicProvider
101
+ namespace="my-app" // key prefix (required)
102
+ storage={localStorage} // StorageLike backend (default: localStorage)
103
+ schemaMode="default" // "default" | "strict" | "autoschema" (default: "default")
104
+ schemaRegistry={registry} // optional SchemaRegistry for versioned schemas
105
+ enableDevTools={false} // expose console helpers (default: false)
106
+ >
107
+ {children}
108
+ </MnemonicProvider>
109
+ ```
110
+
111
+ Multiple providers with different namespaces can coexist in the same app.
112
+
113
+ ### `useMnemonicKey<T>(key, options)`
114
+
115
+ Hook for reading and writing a single persistent value.
116
+
117
+ ```ts
118
+ const { value, set, reset, remove } = useMnemonicKey<T>(key, options);
119
+ ```
120
+
121
+ | Return | Type | Description |
122
+ | -------- | ------------------------------------ | --------------------------------------------- |
123
+ | `value` | `T` | Current decoded value (or default) |
124
+ | `set` | `(next: T \| (cur: T) => T) => void` | Update the value (direct or updater function) |
125
+ | `reset` | `() => void` | Reset to `defaultValue` and persist it |
126
+ | `remove` | `() => void` | Delete the key from storage entirely |
127
+
128
+ #### Options
129
+
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 |
138
+
139
+ ### Codecs
140
+
141
+ The default codec is `JSONCodec`, which handles all JSON-serializable values.
142
+ You can create custom codecs using `createCodec` for types that need special
143
+ serialization (e.g., `Date`, `Set`, `Map`).
144
+
145
+ Using a custom codec bypasses JSON Schema validation -- the codec is a low-level
146
+ escape hatch for when you need full control over serialization.
147
+
148
+ ```ts
149
+ import { createCodec } from "react-mnemonic";
150
+
151
+ const DateCodec = createCodec<Date>(
152
+ (date) => date.toISOString(),
153
+ (str) => new Date(str),
154
+ );
155
+ ```
156
+
157
+ ### `StorageLike`
158
+
159
+ The interface your custom storage backend must satisfy.
160
+
161
+ ```ts
162
+ interface StorageLike {
163
+ getItem(key: string): string | null;
164
+ setItem(key: string, value: string): void;
165
+ removeItem(key: string): void;
166
+ key?(index: number): string | null;
167
+ readonly length?: number;
168
+ onExternalChange?: (callback: (changedKeys?: string[]) => void) => () => void;
169
+ }
170
+ ```
171
+
172
+ `onExternalChange` enables cross-tab sync for non-localStorage backends (e.g.
173
+ IndexedDB over `BroadcastChannel`). The library handles all error cases
174
+ internally -- see the `StorageLike` JSDoc for the full error-handling contract.
175
+
176
+ ### `validateJsonSchema(schema, value)`
177
+
178
+ Validate an arbitrary value against a JSON Schema (the same subset used by the
179
+ hook). Returns an array of validation errors, empty when the value is valid.
180
+
181
+ ```ts
182
+ import { validateJsonSchema } from "react-mnemonic";
183
+
184
+ const errors = validateJsonSchema(
185
+ { type: "object", properties: { name: { type: "string" } }, required: ["name"] },
186
+ { name: 42 },
187
+ );
188
+ // [{ path: ".name", message: 'Expected type "string"' }]
189
+ ```
190
+
191
+ ### `compileSchema(schema)`
192
+
193
+ Pre-compile a JSON Schema into a reusable validator function. The compiled
194
+ validator is cached by schema reference (via `WeakMap`), so calling
195
+ `compileSchema` twice with the same object returns the identical function.
196
+
197
+ ```ts
198
+ import { compileSchema } from "react-mnemonic";
199
+ import type { CompiledValidator } from "react-mnemonic";
200
+
201
+ const validate: CompiledValidator = compileSchema({
202
+ type: "object",
203
+ properties: {
204
+ name: { type: "string", minLength: 1 },
205
+ age: { type: "number", minimum: 0 },
206
+ },
207
+ required: ["name"],
208
+ });
209
+
210
+ validate({ name: "Alice", age: 30 }); // []
211
+ validate({ age: -1 }); // [{ path: "", … }, { path: ".age", … }]
212
+ ```
213
+
214
+ This is useful when you validate the same schema frequently outside of the hook
215
+ (e.g. in form validation or server responses).
216
+
217
+ ### Error classes
218
+
219
+ | Class | Thrown when |
220
+ | ------------- | ------------------------------------ |
221
+ | `CodecError` | Encoding or decoding fails |
222
+ | `SchemaError` | Schema validation or migration fails |
223
+
224
+ Both are passed to `defaultValue` factories so you can inspect or log the
225
+ failure reason.
226
+
227
+ ## Usage examples
228
+
229
+ ### Cross-tab theme sync
230
+
231
+ ```tsx
232
+ const { value: theme, set } = useMnemonicKey<"light" | "dark">("theme", {
233
+ defaultValue: "light",
234
+ listenCrossTab: true,
235
+ onChange: (t) => {
236
+ document.documentElement.setAttribute("data-theme", t);
237
+ },
238
+ });
239
+ ```
240
+
241
+ ### Error-aware defaults
242
+
243
+ ```tsx
244
+ import { useMnemonicKey, CodecError, SchemaError } from "react-mnemonic";
245
+
246
+ const getDefault = (error?: CodecError | SchemaError) => {
247
+ if (error instanceof CodecError) {
248
+ console.warn("Corrupt stored data:", error.message);
249
+ }
250
+ if (error instanceof SchemaError) {
251
+ console.warn("Schema validation failed:", error.message);
252
+ }
253
+ return { count: 0 };
254
+ };
255
+
256
+ const { value } = useMnemonicKey("counter", { defaultValue: getDefault });
257
+ ```
258
+
259
+ ## Schema modes and versioning
260
+
261
+ Mnemonic supports optional schema versioning through `schemaMode` and an
262
+ optional `schemaRegistry`.
263
+
264
+ - `default`: Schemas are optional. Reads use a schema when one exists for the
265
+ stored version, otherwise the hook codec. Writes use the highest registered
266
+ schema for the key; if no schemas are registered, writes use an unversioned
267
+ (v0) envelope.
268
+ - `strict`: Every stored version must have a registered schema. Reads without a
269
+ matching schema fall back to `defaultValue` with a `SchemaError`.
270
+ Writes require a registered schema when any schemas exist, but fall back to
271
+ a v0 envelope when the registry has none.
272
+ - `autoschema`: Like `default`, but if no schema exists for a key, the first
273
+ successful read infers and registers a v1 schema. Subsequent reads/writes use
274
+ that schema.
275
+
276
+ Version `0` is valid for schemas and migrations. Schemas at version `0` are
277
+ treated like any other version.
278
+
279
+ ### JSON Schema validation
280
+
281
+ Schemas use a subset of JSON Schema for validation. The supported keywords are:
282
+
283
+ - `type` (including array form for nullable types, e.g., `["string", "null"]`)
284
+ - `enum`, `const`
285
+ - `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`
286
+ - `minLength`, `maxLength`
287
+ - `properties`, `required`, `additionalProperties`
288
+ - `items`, `minItems`, `maxItems`
289
+
290
+ ```ts
291
+ // Schema definition -- fully serializable JSON, no functions
292
+ const schema: KeySchema = {
293
+ key: "profile",
294
+ version: 1,
295
+ schema: {
296
+ type: "object",
297
+ properties: {
298
+ name: { type: "string", minLength: 1 },
299
+ email: { type: "string" },
300
+ age: { type: "number", minimum: 0 },
301
+ },
302
+ required: ["name", "email"],
303
+ },
304
+ };
305
+ ```
306
+
307
+ ### Write-time migrations (normalizers)
308
+
309
+ A migration where `fromVersion === toVersion` runs on every write, acting as a
310
+ normalizer. This is useful for trimming whitespace, lowercasing strings, etc.
311
+
312
+ ```ts
313
+ const normalizer: MigrationRule = {
314
+ key: "name",
315
+ fromVersion: 1,
316
+ toVersion: 1,
317
+ migrate: (value) => String(value).trim().toLowerCase(),
318
+ };
319
+ ```
320
+
321
+ ### Example schema registry
322
+
323
+ 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.
326
+
327
+ ```tsx
328
+ import {
329
+ MnemonicProvider,
330
+ useMnemonicKey,
331
+ type SchemaRegistry,
332
+ type KeySchema,
333
+ type MigrationRule,
334
+ } from "react-mnemonic";
335
+
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);
364
+ },
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"],
374
+ },
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() };
384
+ },
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" },
396
+ },
397
+ required: ["name", "email", "migratedAt"],
398
+ },
399
+ });
400
+
401
+ function ProfileEditor() {
402
+ const { value, set } = useMnemonicKey<{ name: string; email: string; migratedAt: string }>("profile", {
403
+ defaultValue: { name: "", email: "", migratedAt: "" },
404
+ });
405
+ return <input value={value.name} onChange={(e) => set({ ...value, name: e.target.value })} />;
406
+ }
407
+
408
+ <MnemonicProvider namespace="app" schemaMode="default" schemaRegistry={registry}>
409
+ <ProfileEditor />
410
+ </MnemonicProvider>;
411
+ ```
412
+
413
+ ### Registry immutability
414
+
415
+ In `default` and `strict` modes, the schema registry is treated as immutable for
416
+ the lifetime of the provider. The hook caches registry lookups to keep read and
417
+ write hot paths fast. To ship new schemas or migrations, publish a new app
418
+ version and remount the provider.
419
+
420
+ `autoschema` remains mutable because inferred schemas are registered at runtime.
421
+
422
+ ### Custom storage backend
423
+
424
+ ```tsx
425
+ import { MnemonicProvider } from "react-mnemonic";
426
+ import type { StorageLike } from "react-mnemonic";
427
+
428
+ 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
+ },
437
+ };
438
+
439
+ <MnemonicProvider namespace="my-app" storage={idbStorage}>
440
+ <App />
441
+ </MnemonicProvider>
442
+ ```
443
+
444
+ ### DevTools
445
+
446
+ Enable the console inspector in development:
447
+
448
+ ```tsx
449
+ <MnemonicProvider namespace="app" enableDevTools={process.env.NODE_ENV === "development"}>
450
+ ```
451
+
452
+ Then in the browser console:
453
+
454
+ ```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
461
+ ```
462
+
463
+ ## TypeScript
464
+
465
+ The library is written in strict TypeScript and ships its own declarations.
466
+ All public types are re-exported from the package root:
467
+
468
+ ```ts
469
+ import type {
470
+ Codec,
471
+ StorageLike,
472
+ MnemonicProviderOptions,
473
+ MnemonicProviderProps,
474
+ UseMnemonicKeyOptions,
475
+ KeySchema,
476
+ MigrationRule,
477
+ MigrationPath,
478
+ SchemaRegistry,
479
+ SchemaMode,
480
+ JsonSchema,
481
+ JsonSchemaType,
482
+ JsonSchemaValidationError,
483
+ CompiledValidator,
484
+ } from "react-mnemonic";
485
+ ```
486
+
487
+ ## Disclaimer
488
+
489
+ THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
490
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
491
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. The authors make no guarantees
492
+ regarding the reliability, availability, or suitability of this library for any
493
+ particular use case. See the [MIT License](./LICENSE.md) for full terms.
494
+
495
+ ## License
496
+
497
+ [MIT](./LICENSE.md) -- Copyright Scott Dixon