react-mnemonic 1.0.0-beta.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/dist/index.js CHANGED
@@ -1,7 +1,33 @@
1
- import { createContext, useMemo, useEffect, useCallback, useSyncExternalStore, useRef, useContext } from 'react';
1
+ import { createContext, useMemo, useEffect, useState, useCallback, useSyncExternalStore, useRef, useContext } from 'react';
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
 
4
4
  // src/Mnemonic/provider.tsx
5
+
6
+ // src/Mnemonic/runtime.ts
7
+ function getGlobalProcess() {
8
+ return globalThis.process;
9
+ }
10
+ function getRuntimeNodeEnv() {
11
+ const runtimeProcess = getGlobalProcess();
12
+ if (runtimeProcess?.env?.NODE_ENV !== void 0) {
13
+ return runtimeProcess.env.NODE_ENV;
14
+ }
15
+ return void 0;
16
+ }
17
+ function getNativeBrowserStorages() {
18
+ const globalWindow = globalThis.window;
19
+ if (!globalWindow) return [];
20
+ const storages = [];
21
+ const addStorage = (getter) => {
22
+ try {
23
+ storages.push(getter());
24
+ } catch {
25
+ }
26
+ };
27
+ addStorage(() => globalWindow.localStorage);
28
+ addStorage(() => globalWindow.sessionStorage);
29
+ return storages;
30
+ }
5
31
  var MnemonicContext = createContext(null);
6
32
  function useMnemonic() {
7
33
  const context = useContext(MnemonicContext);
@@ -11,20 +37,399 @@ function useMnemonic() {
11
37
  return context;
12
38
  }
13
39
  function defaultBrowserStorage() {
14
- if (typeof window === "undefined") return void 0;
40
+ const globalWindow = globalThis.window;
41
+ if (globalWindow === void 0) return void 0;
15
42
  try {
16
- return window.localStorage;
43
+ return globalWindow.localStorage;
17
44
  } catch {
18
45
  return void 0;
19
46
  }
20
47
  }
48
+ function detectEnumerableStorage(storage) {
49
+ if (!storage) return false;
50
+ try {
51
+ return typeof storage.length === "number" && typeof storage.key === "function";
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+ function isProductionRuntime() {
57
+ const env = getRuntimeNodeEnv();
58
+ if (env === void 0) {
59
+ return true;
60
+ }
61
+ return env === "production";
62
+ }
63
+ function weakRefConstructor() {
64
+ const ctor = globalThis.WeakRef;
65
+ return typeof ctor === "function" ? ctor : null;
66
+ }
67
+ function hasFinalizationRegistry() {
68
+ return typeof globalThis.FinalizationRegistry === "function";
69
+ }
70
+ function isPromiseLike(value) {
71
+ if (value == null) return false;
72
+ if (typeof value !== "object" && typeof value !== "function") return false;
73
+ return typeof value.then === "function";
74
+ }
75
+ function getCrossTabSyncMode(requestedStorage, activeStorage) {
76
+ const isExplicitNativeBrowserStorage = activeStorage !== void 0 && requestedStorage !== void 0 && getNativeBrowserStorages().includes(activeStorage);
77
+ if (requestedStorage === void 0 && activeStorage !== void 0 || isExplicitNativeBrowserStorage) {
78
+ return "browser-storage-event";
79
+ }
80
+ if (typeof activeStorage?.onExternalChange === "function") {
81
+ return "custom-external-change";
82
+ }
83
+ return "none";
84
+ }
85
+ function getDevToolsWindow() {
86
+ return globalThis.window;
87
+ }
88
+ function sanitizeDevToolsRoot(root) {
89
+ const reserved = /* @__PURE__ */ new Set(["providers", "resolve", "list", "capabilities", "__meta"]);
90
+ for (const key of Object.keys(root)) {
91
+ if (reserved.has(key)) continue;
92
+ const descriptor = Object.getOwnPropertyDescriptor(root, key);
93
+ if (descriptor && !descriptor.configurable) continue;
94
+ try {
95
+ delete root[key];
96
+ } catch {
97
+ }
98
+ }
99
+ }
100
+ function ensureDevToolsRoot(enableDevTools) {
101
+ if (!enableDevTools) return null;
102
+ const globalWindow = getDevToolsWindow();
103
+ if (!globalWindow) return null;
104
+ const weakRefSupported = weakRefConstructor() !== null;
105
+ const finalizationRegistrySupported = hasFinalizationRegistry();
106
+ const existing = globalWindow.__REACT_MNEMONIC_DEVTOOLS__;
107
+ const root = existing && typeof existing === "object" ? existing : {};
108
+ sanitizeDevToolsRoot(root);
109
+ if (!root.providers || typeof root.providers !== "object") {
110
+ root.providers = {};
111
+ }
112
+ if (!root.capabilities || typeof root.capabilities !== "object") {
113
+ root.capabilities = {};
114
+ }
115
+ const capabilities = root.capabilities;
116
+ capabilities.weakRef = weakRefSupported;
117
+ capabilities.finalizationRegistry = finalizationRegistrySupported;
118
+ if (!root.__meta || typeof root.__meta !== "object") {
119
+ root.__meta = {
120
+ version: 0,
121
+ lastUpdated: Date.now(),
122
+ lastChange: ""
123
+ };
124
+ }
125
+ const meta = root.__meta;
126
+ if (typeof meta.version !== "number" || !Number.isFinite(meta.version)) {
127
+ meta.version = 0;
128
+ }
129
+ if (typeof meta.lastUpdated !== "number" || !Number.isFinite(meta.lastUpdated)) {
130
+ meta.lastUpdated = Date.now();
131
+ }
132
+ if (typeof meta.lastChange !== "string") {
133
+ meta.lastChange = "";
134
+ }
135
+ const providers = root.providers;
136
+ if (typeof root.resolve !== "function") {
137
+ root.resolve = (namespace) => {
138
+ const entry = providers[namespace];
139
+ if (!entry || typeof entry.weakRef?.deref !== "function") return null;
140
+ const live = entry.weakRef.deref();
141
+ if (live) {
142
+ entry.lastSeenAt = Date.now();
143
+ entry.staleSince = null;
144
+ return live;
145
+ }
146
+ entry.staleSince ?? (entry.staleSince = Date.now());
147
+ return null;
148
+ };
149
+ }
150
+ if (typeof root.list !== "function") {
151
+ root.list = () => Object.entries(providers).map(([namespace, entry]) => {
152
+ const live = typeof entry.weakRef?.deref === "function" ? entry.weakRef.deref() : void 0;
153
+ const available = Boolean(live);
154
+ if (available) {
155
+ entry.lastSeenAt = Date.now();
156
+ entry.staleSince = null;
157
+ } else {
158
+ entry.staleSince ?? (entry.staleSince = Date.now());
159
+ }
160
+ return {
161
+ namespace,
162
+ available,
163
+ registeredAt: entry.registeredAt,
164
+ lastSeenAt: entry.lastSeenAt,
165
+ staleSince: entry.staleSince
166
+ };
167
+ }).sort((left, right) => left.namespace.localeCompare(right.namespace));
168
+ }
169
+ globalWindow.__REACT_MNEMONIC_DEVTOOLS__ = root;
170
+ return root;
171
+ }
172
+ function bumpDevToolsVersion(root, namespace, reason) {
173
+ if (!root) return;
174
+ root.__meta.version += 1;
175
+ root.__meta.lastUpdated = Date.now();
176
+ root.__meta.lastChange = `${namespace}.${reason}`;
177
+ }
178
+ function decodeDevToolsValue(raw) {
179
+ try {
180
+ return JSON.parse(raw);
181
+ } catch {
182
+ return raw;
183
+ }
184
+ }
185
+ function readStorageRaw(storage, storageKey, callbacks) {
186
+ if (!storage) return null;
187
+ try {
188
+ const raw = storage.getItem(storageKey);
189
+ if (isPromiseLike(raw)) {
190
+ callbacks.onAsyncViolation("getItem", raw);
191
+ return null;
192
+ }
193
+ callbacks.onAccessSuccess();
194
+ return raw;
195
+ } catch (error) {
196
+ callbacks.onAccessError(error);
197
+ return null;
198
+ }
199
+ }
200
+ function enumerateNamespaceKeys(storage, prefix, callbacks) {
201
+ if (!storage) {
202
+ return [];
203
+ }
204
+ const keys = [];
205
+ try {
206
+ const storageLength = storage.length;
207
+ const getStorageKey = storage.key;
208
+ if (typeof storageLength !== "number" || typeof getStorageKey !== "function") {
209
+ return [];
210
+ }
211
+ for (let index = 0; index < storageLength; index++) {
212
+ const fullKey = getStorageKey.call(storage, index);
213
+ if (!fullKey?.startsWith(prefix)) continue;
214
+ keys.push(fullKey.slice(prefix.length));
215
+ }
216
+ callbacks.onAccessSuccess();
217
+ } catch (error) {
218
+ callbacks.onAccessError(error);
219
+ }
220
+ return keys;
221
+ }
222
+ function syncCacheEntryFromStorage({
223
+ key,
224
+ storageKey,
225
+ storage,
226
+ cache,
227
+ emit,
228
+ callbacks
229
+ }) {
230
+ const fresh = readStorageRaw(storage, storageKey, callbacks);
231
+ const cached = cache.get(key) ?? null;
232
+ if (fresh === cached) {
233
+ return false;
234
+ }
235
+ cache.set(key, fresh);
236
+ emit(key);
237
+ return true;
238
+ }
239
+ function reloadNamedKeysFromStorage({
240
+ changedKeys,
241
+ prefix,
242
+ storage,
243
+ listeners,
244
+ cache,
245
+ emit,
246
+ callbacks
247
+ }) {
248
+ let changed = false;
249
+ for (const fullStorageKey of changedKeys) {
250
+ if (!fullStorageKey.startsWith(prefix)) continue;
251
+ const key = fullStorageKey.slice(prefix.length);
252
+ const listenerSet = listeners.get(key);
253
+ if (listenerSet && listenerSet.size > 0) {
254
+ changed = syncCacheEntryFromStorage({
255
+ key,
256
+ storageKey: fullStorageKey,
257
+ storage,
258
+ cache,
259
+ emit,
260
+ callbacks
261
+ }) || changed;
262
+ continue;
263
+ }
264
+ if (cache.has(key)) {
265
+ cache.delete(key);
266
+ }
267
+ }
268
+ return changed;
269
+ }
270
+ function reloadSubscribedKeysFromStorage({
271
+ prefix,
272
+ storage,
273
+ listeners,
274
+ cache,
275
+ emit,
276
+ callbacks
277
+ }) {
278
+ let changed = false;
279
+ for (const [key, listenerSet] of listeners) {
280
+ if (listenerSet.size === 0) continue;
281
+ changed = syncCacheEntryFromStorage({
282
+ key,
283
+ storageKey: `${prefix}${key}`,
284
+ storage,
285
+ cache,
286
+ emit,
287
+ callbacks
288
+ }) || changed;
289
+ }
290
+ for (const key of cache.keys()) {
291
+ const listenerSet = listeners.get(key);
292
+ if (listenerSet && listenerSet.size > 0) continue;
293
+ cache.delete(key);
294
+ }
295
+ return changed;
296
+ }
297
+ function createDevToolsProviderApi({
298
+ store,
299
+ dump,
300
+ keys,
301
+ readThrough,
302
+ writeRaw,
303
+ removeRaw
304
+ }) {
305
+ return {
306
+ getStore: () => store,
307
+ dump: () => {
308
+ const data = dump();
309
+ console.table(
310
+ Object.entries(data).map(([key, value]) => ({
311
+ key,
312
+ value,
313
+ decoded: decodeDevToolsValue(value)
314
+ }))
315
+ );
316
+ return data;
317
+ },
318
+ get: (key) => {
319
+ const raw = readThrough(key);
320
+ if (raw == null) return void 0;
321
+ return decodeDevToolsValue(raw);
322
+ },
323
+ set: (key, value) => {
324
+ writeRaw(key, JSON.stringify(value));
325
+ },
326
+ remove: (key) => removeRaw(key),
327
+ clear: () => {
328
+ for (const key of keys()) {
329
+ removeRaw(key);
330
+ }
331
+ },
332
+ keys
333
+ };
334
+ }
335
+ function createReloadFromStorage({
336
+ storage,
337
+ hasAsyncContractViolation,
338
+ prefix,
339
+ listeners,
340
+ cache,
341
+ emit,
342
+ callbacks,
343
+ devToolsRoot,
344
+ namespace
345
+ }) {
346
+ return (changedKeys) => {
347
+ if (!storage || hasAsyncContractViolation()) return;
348
+ if (changedKeys?.length === 0) return;
349
+ const isFullReload = changedKeys === void 0;
350
+ const changed = isFullReload ? reloadSubscribedKeysFromStorage({
351
+ prefix,
352
+ storage,
353
+ listeners,
354
+ cache,
355
+ emit,
356
+ callbacks
357
+ }) : reloadNamedKeysFromStorage({
358
+ changedKeys,
359
+ prefix,
360
+ storage,
361
+ listeners,
362
+ cache,
363
+ emit,
364
+ callbacks
365
+ });
366
+ if (changed) {
367
+ bumpDevToolsVersion(devToolsRoot, namespace, isFullReload ? "reload:full" : "reload:granular");
368
+ }
369
+ };
370
+ }
371
+ function registerDevToolsProvider({
372
+ devToolsRoot,
373
+ namespace,
374
+ store,
375
+ dump,
376
+ keys,
377
+ readThrough,
378
+ writeRaw,
379
+ removeRaw
380
+ }) {
381
+ let infoMessage = `[Mnemonic DevTools] Namespace "${namespace}" available via window.__REACT_MNEMONIC_DEVTOOLS__.resolve("${namespace}")`;
382
+ if (!devToolsRoot.capabilities.weakRef) {
383
+ console.info(
384
+ `[Mnemonic DevTools] WeakRef is not available; registry provider "${namespace}" was not registered.`
385
+ );
386
+ return;
387
+ }
388
+ const existingLive = devToolsRoot.resolve(namespace);
389
+ if (existingLive) {
390
+ const duplicateMessage = `[Mnemonic DevTools] Duplicate provider namespace "${namespace}" detected. Each window must have at most one live MnemonicProvider per namespace.`;
391
+ if (!isProductionRuntime()) {
392
+ throw new Error(duplicateMessage);
393
+ }
394
+ console.warn(`${duplicateMessage} Keeping the first provider and ignoring the duplicate.`);
395
+ console.info(
396
+ `[Mnemonic DevTools] Namespace "${namespace}" already registered. Keeping existing provider reference.`
397
+ );
398
+ return;
399
+ }
400
+ const providerApi = createDevToolsProviderApi({
401
+ store,
402
+ dump,
403
+ keys,
404
+ readThrough,
405
+ writeRaw,
406
+ removeRaw
407
+ });
408
+ const WeakRefCtor = weakRefConstructor();
409
+ if (!WeakRefCtor) {
410
+ console.info(`[Mnemonic DevTools] WeakRef became unavailable while registering "${namespace}".`);
411
+ return;
412
+ }
413
+ store.__devToolsProviderApiHold = providerApi;
414
+ const now = Date.now();
415
+ devToolsRoot.providers[namespace] = {
416
+ namespace,
417
+ weakRef: new WeakRefCtor(providerApi),
418
+ registeredAt: now,
419
+ lastSeenAt: now,
420
+ staleSince: null
421
+ };
422
+ bumpDevToolsVersion(devToolsRoot, namespace, "registry:namespace-registered");
423
+ console.info(infoMessage);
424
+ }
21
425
  function MnemonicProvider({
22
426
  children,
23
427
  namespace,
24
428
  storage,
25
429
  enableDevTools = false,
26
430
  schemaMode = "default",
27
- schemaRegistry
431
+ schemaRegistry,
432
+ ssr
28
433
  }) {
29
434
  if (schemaMode === "strict" && !schemaRegistry) {
30
435
  throw new Error("MnemonicProvider strict mode requires schemaRegistry");
@@ -35,124 +440,21 @@ function MnemonicProvider({
35
440
  const store = useMemo(() => {
36
441
  const prefix = `${namespace}.`;
37
442
  const st = storage ?? defaultBrowserStorage();
443
+ const ssrHydration = ssr?.hydration ?? "immediate";
444
+ const devToolsRoot = ensureDevToolsRoot(enableDevTools);
445
+ const canEnumerateKeys = detectEnumerableStorage(st);
446
+ const crossTabSyncMode = getCrossTabSyncMode(storage, st);
38
447
  const cache = /* @__PURE__ */ new Map();
39
448
  const listeners = /* @__PURE__ */ new Map();
40
449
  let quotaErrorLogged = false;
41
450
  let accessErrorLogged = false;
42
- const detectEnumerableStorage = () => {
43
- if (!st) return false;
44
- try {
45
- return typeof st.length === "number" && typeof st.key === "function";
46
- } catch {
47
- return false;
48
- }
49
- };
50
- const canEnumerateKeys = detectEnumerableStorage();
51
- const isProductionRuntime = () => {
52
- const env = globalThis?.process?.env?.NODE_ENV;
53
- if (typeof env !== "string") {
54
- return true;
55
- }
56
- return env === "production";
57
- };
58
- const weakRefConstructor = () => {
59
- const ctor = globalThis?.WeakRef;
60
- return typeof ctor === "function" ? ctor : null;
61
- };
62
- const hasFinalizationRegistry = () => typeof globalThis?.FinalizationRegistry === "function";
63
- const ensureDevToolsRoot = () => {
64
- if (!enableDevTools || typeof window === "undefined") return null;
65
- const weakRefSupported = weakRefConstructor() !== null;
66
- const finalizationRegistrySupported = hasFinalizationRegistry();
67
- const globalWindow = window;
68
- const rawExisting = globalWindow.__REACT_MNEMONIC_DEVTOOLS__;
69
- const root = rawExisting && typeof rawExisting === "object" ? rawExisting : {};
70
- const reserved = /* @__PURE__ */ new Set(["providers", "resolve", "list", "capabilities", "__meta"]);
71
- for (const key of Object.keys(root)) {
72
- if (!reserved.has(key)) {
73
- const descriptor = Object.getOwnPropertyDescriptor(root, key);
74
- if (!descriptor || descriptor.configurable) {
75
- try {
76
- delete root[key];
77
- } catch {
78
- }
79
- }
80
- }
81
- }
82
- if (!root.providers || typeof root.providers !== "object") {
83
- root.providers = {};
84
- }
85
- if (!root.capabilities || typeof root.capabilities !== "object") {
86
- root.capabilities = {};
87
- }
88
- root.capabilities.weakRef = weakRefSupported;
89
- root.capabilities.finalizationRegistry = finalizationRegistrySupported;
90
- if (!root.__meta || typeof root.__meta !== "object") {
91
- root.__meta = {
92
- version: 0,
93
- lastUpdated: Date.now(),
94
- lastChange: ""
95
- };
96
- }
97
- if (typeof root.__meta.version !== "number" || !Number.isFinite(root.__meta.version)) {
98
- root.__meta.version = 0;
99
- }
100
- if (typeof root.__meta.lastUpdated !== "number" || !Number.isFinite(root.__meta.lastUpdated)) {
101
- root.__meta.lastUpdated = Date.now();
102
- }
103
- if (typeof root.__meta.lastChange !== "string") {
104
- root.__meta.lastChange = "";
105
- }
106
- if (typeof root.resolve !== "function") {
107
- root.resolve = (ns) => {
108
- const entry = root.providers[ns];
109
- if (!entry || !entry.weakRef || typeof entry.weakRef.deref !== "function") return null;
110
- const live = entry.weakRef.deref();
111
- if (live) {
112
- entry.lastSeenAt = Date.now();
113
- entry.staleSince = null;
114
- return live;
115
- }
116
- if (entry.staleSince === null) {
117
- entry.staleSince = Date.now();
118
- }
119
- return null;
120
- };
121
- }
122
- if (typeof root.list !== "function") {
123
- root.list = () => {
124
- const entries = root.providers;
125
- const out = [];
126
- for (const [ns, entry] of Object.entries(entries)) {
127
- const live = entry && entry.weakRef && typeof entry.weakRef.deref === "function" ? entry.weakRef.deref() : void 0;
128
- const available = Boolean(live);
129
- if (available) {
130
- entry.lastSeenAt = Date.now();
131
- entry.staleSince = null;
132
- } else if (entry.staleSince === null) {
133
- entry.staleSince = Date.now();
134
- }
135
- out.push({
136
- namespace: ns,
137
- available,
138
- registeredAt: entry.registeredAt,
139
- lastSeenAt: entry.lastSeenAt,
140
- staleSince: entry.staleSince
141
- });
142
- }
143
- out.sort((a, b) => a.namespace.localeCompare(b.namespace));
144
- return out;
145
- };
146
- }
147
- globalWindow.__REACT_MNEMONIC_DEVTOOLS__ = root;
148
- return root;
149
- };
150
- const bumpDevToolsVersion = (reason) => {
151
- const root = ensureDevToolsRoot();
152
- if (!root) return;
153
- root.__meta.version += 1;
154
- root.__meta.lastUpdated = Date.now();
155
- root.__meta.lastChange = `${namespace}.${reason}`;
451
+ let asyncContractViolationDetected = false;
452
+ const storageAccessCallbacks = {
453
+ onAccessError: (err) => logAccessError(err),
454
+ onAccessSuccess: () => {
455
+ accessErrorLogged = false;
456
+ },
457
+ onAsyncViolation: (method, thenable) => handleAsyncStorageContractViolation(method, thenable)
156
458
  };
157
459
  const fullKey = (key) => prefix + key;
158
460
  const emit = (key) => {
@@ -168,30 +470,36 @@ function MnemonicProvider({
168
470
  accessErrorLogged = true;
169
471
  }
170
472
  };
473
+ const handleAsyncStorageContractViolation = (method, thenable) => {
474
+ asyncContractViolationDetected = true;
475
+ void Promise.resolve(thenable).catch(() => void 0);
476
+ if (accessErrorLogged) return;
477
+ console.error(
478
+ `[Mnemonic] StorageLike.${method} returned a Promise. StorageLike must remain synchronous for react-mnemonic v1. Wrap async persistence behind a synchronous cache facade instead.`
479
+ );
480
+ accessErrorLogged = true;
481
+ };
171
482
  const readThrough = (key) => {
172
483
  if (cache.has(key)) return cache.get(key) ?? null;
173
- if (!st) {
174
- cache.set(key, null);
175
- return null;
176
- }
177
- try {
178
- const raw = st.getItem(fullKey(key));
179
- cache.set(key, raw);
180
- accessErrorLogged = false;
181
- return raw;
182
- } catch (err) {
183
- logAccessError(err);
484
+ if (!st || asyncContractViolationDetected) {
184
485
  cache.set(key, null);
185
486
  return null;
186
487
  }
488
+ const raw = readStorageRaw(st, fullKey(key), storageAccessCallbacks);
489
+ cache.set(key, raw);
490
+ return raw;
187
491
  };
188
492
  const writeRaw = (key, raw) => {
189
493
  cache.set(key, raw);
190
- if (st) {
494
+ if (st && !asyncContractViolationDetected) {
191
495
  try {
192
- st.setItem(fullKey(key), raw);
193
- quotaErrorLogged = false;
194
- accessErrorLogged = false;
496
+ const result = st.setItem(fullKey(key), raw);
497
+ if (isPromiseLike(result)) {
498
+ handleAsyncStorageContractViolation("setItem", result);
499
+ } else {
500
+ quotaErrorLogged = false;
501
+ accessErrorLogged = false;
502
+ }
195
503
  } catch (err) {
196
504
  if (!quotaErrorLogged && err instanceof DOMException && err.name === "QuotaExceededError") {
197
505
  console.error(
@@ -203,20 +511,24 @@ function MnemonicProvider({
203
511
  }
204
512
  }
205
513
  emit(key);
206
- bumpDevToolsVersion(`set:${key}`);
514
+ bumpDevToolsVersion(devToolsRoot, namespace, `set:${key}`);
207
515
  };
208
516
  const removeRaw = (key) => {
209
517
  cache.set(key, null);
210
- if (st) {
518
+ if (st && !asyncContractViolationDetected) {
211
519
  try {
212
- st.removeItem(fullKey(key));
213
- accessErrorLogged = false;
520
+ const result = st.removeItem(fullKey(key));
521
+ if (isPromiseLike(result)) {
522
+ handleAsyncStorageContractViolation("removeItem", result);
523
+ } else {
524
+ accessErrorLogged = false;
525
+ }
214
526
  } catch (err) {
215
527
  logAccessError(err);
216
528
  }
217
529
  }
218
530
  emit(key);
219
- bumpDevToolsVersion(`remove:${key}`);
531
+ bumpDevToolsVersion(devToolsRoot, namespace, `remove:${key}`);
220
532
  };
221
533
  const subscribeRaw = (key, listener) => {
222
534
  let set = listeners.get(key);
@@ -235,22 +547,11 @@ function MnemonicProvider({
235
547
  };
236
548
  const getRawSnapshot = (key) => readThrough(key);
237
549
  const keys = () => {
238
- if (!canEnumerateKeys || !st) return [];
239
- const out = [];
240
- try {
241
- const storageLength = st.length;
242
- const getStorageKey = st.key;
243
- if (typeof storageLength !== "number" || typeof getStorageKey !== "function") return [];
244
- for (let i = 0; i < storageLength; i++) {
245
- const k = getStorageKey.call(st, i);
246
- if (!k) continue;
247
- if (k.startsWith(prefix)) out.push(k.slice(prefix.length));
248
- }
249
- accessErrorLogged = false;
250
- } catch (err) {
251
- logAccessError(err);
550
+ if (asyncContractViolationDetected) {
551
+ return Array.from(cache.entries()).filter(([, value]) => value != null).map(([key]) => key);
252
552
  }
253
- return out;
553
+ if (!canEnumerateKeys) return [];
554
+ return enumerateNamespaceKeys(st, prefix, storageAccessCallbacks);
254
555
  };
255
556
  const dump = () => {
256
557
  const out = {};
@@ -260,65 +561,17 @@ function MnemonicProvider({
260
561
  }
261
562
  return out;
262
563
  };
263
- const reloadFromStorage = (changedKeys) => {
264
- if (!st) return;
265
- let changed = false;
266
- if (changedKeys !== void 0 && changedKeys.length === 0) return;
267
- if (changedKeys !== void 0) {
268
- for (const fk of changedKeys) {
269
- if (!fk.startsWith(prefix)) continue;
270
- const key = fk.slice(prefix.length);
271
- const listenerSet = listeners.get(key);
272
- if (listenerSet && listenerSet.size > 0) {
273
- let fresh;
274
- try {
275
- fresh = st.getItem(fk);
276
- accessErrorLogged = false;
277
- } catch (err) {
278
- logAccessError(err);
279
- fresh = null;
280
- }
281
- const cached = cache.get(key) ?? null;
282
- if (fresh !== cached) {
283
- cache.set(key, fresh);
284
- emit(key);
285
- changed = true;
286
- }
287
- } else if (cache.has(key)) {
288
- cache.delete(key);
289
- }
290
- }
291
- if (changed) {
292
- bumpDevToolsVersion("reload:granular");
293
- }
294
- return;
295
- }
296
- for (const [key, listenerSet] of listeners) {
297
- if (listenerSet.size === 0) continue;
298
- let fresh;
299
- try {
300
- fresh = st.getItem(fullKey(key));
301
- accessErrorLogged = false;
302
- } catch (err) {
303
- logAccessError(err);
304
- fresh = null;
305
- }
306
- const cached = cache.get(key) ?? null;
307
- if (fresh !== cached) {
308
- cache.set(key, fresh);
309
- emit(key);
310
- changed = true;
311
- }
312
- }
313
- for (const key of cache.keys()) {
314
- if (!listeners.has(key) || listeners.get(key).size === 0) {
315
- cache.delete(key);
316
- }
317
- }
318
- if (changed) {
319
- bumpDevToolsVersion("reload:full");
320
- }
321
- };
564
+ const reloadFromStorage = createReloadFromStorage({
565
+ storage: st,
566
+ hasAsyncContractViolation: () => asyncContractViolationDetected,
567
+ prefix,
568
+ listeners,
569
+ cache,
570
+ emit,
571
+ callbacks: storageAccessCallbacks,
572
+ devToolsRoot,
573
+ namespace
574
+ });
322
575
  const store2 = {
323
576
  prefix,
324
577
  canEnumerateKeys,
@@ -330,92 +583,24 @@ function MnemonicProvider({
330
583
  dump,
331
584
  reloadFromStorage,
332
585
  schemaMode,
586
+ ssrHydration,
587
+ crossTabSyncMode,
333
588
  ...schemaRegistry ? { schemaRegistry } : {}
334
589
  };
335
- if (enableDevTools && typeof window !== "undefined") {
336
- const root = ensureDevToolsRoot();
337
- let infoMessage = `[Mnemonic DevTools] Namespace "${namespace}" available via window.__REACT_MNEMONIC_DEVTOOLS__.resolve("${namespace}")`;
338
- if (root) {
339
- if (!root.capabilities.weakRef) {
340
- infoMessage = `[Mnemonic DevTools] WeakRef is not available; registry provider "${namespace}" was not registered.`;
341
- } else {
342
- const existingLive = root.resolve(namespace);
343
- if (existingLive) {
344
- const duplicateMessage = `[Mnemonic DevTools] Duplicate provider namespace "${namespace}" detected. Each window must have at most one live MnemonicProvider per namespace.`;
345
- if (!isProductionRuntime()) {
346
- throw new Error(duplicateMessage);
347
- }
348
- console.warn(`${duplicateMessage} Keeping the first provider and ignoring the duplicate.`);
349
- infoMessage = `[Mnemonic DevTools] Namespace "${namespace}" already registered. Keeping existing provider reference.`;
350
- } else {
351
- const providerApi = {
352
- /** Access the underlying store instance */
353
- getStore: () => store2,
354
- /** Dump all key-value pairs and display as a console table */
355
- dump: () => {
356
- const data = dump();
357
- console.table(
358
- Object.entries(data).map(([key, value]) => ({
359
- key,
360
- value,
361
- decoded: (() => {
362
- try {
363
- return JSON.parse(value);
364
- } catch {
365
- return value;
366
- }
367
- })()
368
- }))
369
- );
370
- return data;
371
- },
372
- /** Get a decoded value by key */
373
- get: (key) => {
374
- const raw = readThrough(key);
375
- if (raw == null) return void 0;
376
- try {
377
- return JSON.parse(raw);
378
- } catch {
379
- return raw;
380
- }
381
- },
382
- /** Set a value by key (automatically JSON-encoded) */
383
- set: (key, value) => {
384
- writeRaw(key, JSON.stringify(value));
385
- },
386
- /** Remove a key from storage */
387
- remove: (key) => removeRaw(key),
388
- /** Clear all keys in this namespace */
389
- clear: () => {
390
- for (const k of keys()) {
391
- removeRaw(k);
392
- }
393
- },
394
- /** List all keys in this namespace */
395
- keys
396
- };
397
- const WeakRefCtor = weakRefConstructor();
398
- if (!WeakRefCtor) {
399
- infoMessage = `[Mnemonic DevTools] WeakRef became unavailable while registering "${namespace}".`;
400
- } else {
401
- store2.__devToolsProviderApiHold = providerApi;
402
- root.providers[namespace] = {
403
- namespace,
404
- weakRef: new WeakRefCtor(providerApi),
405
- registeredAt: Date.now(),
406
- lastSeenAt: Date.now(),
407
- staleSince: null
408
- };
409
- bumpDevToolsVersion("registry:namespace-registered");
410
- infoMessage = `[Mnemonic DevTools] Namespace "${namespace}" available via window.__REACT_MNEMONIC_DEVTOOLS__.resolve("${namespace}")`;
411
- }
412
- }
413
- }
414
- }
415
- console.info(infoMessage);
590
+ if (devToolsRoot) {
591
+ registerDevToolsProvider({
592
+ devToolsRoot,
593
+ namespace,
594
+ store: store2,
595
+ dump,
596
+ keys,
597
+ readThrough,
598
+ writeRaw,
599
+ removeRaw
600
+ });
416
601
  }
417
602
  return store2;
418
- }, [namespace, storage, enableDevTools, schemaMode, schemaRegistry]);
603
+ }, [namespace, storage, enableDevTools, schemaMode, schemaRegistry, ssr?.hydration]);
419
604
  useEffect(() => {
420
605
  if (!storage?.onExternalChange) return;
421
606
  return storage.onExternalChange((changedKeys) => store.reloadFromStorage(changedKeys));
@@ -485,30 +670,34 @@ function matchesType(value, type) {
485
670
  return false;
486
671
  }
487
672
  }
673
+ function jsonDeepEqualArray(a, b) {
674
+ if (a.length !== b.length) return false;
675
+ for (let i = 0; i < a.length; i++) {
676
+ if (!jsonDeepEqual(a[i], b[i])) return false;
677
+ }
678
+ return true;
679
+ }
680
+ function jsonDeepEqualObject(a, b) {
681
+ const aKeys = Object.keys(a);
682
+ const bKeys = Object.keys(b);
683
+ if (aKeys.length !== bKeys.length) return false;
684
+ for (const key of aKeys) {
685
+ if (!objectHasOwn(b, key)) return false;
686
+ if (!jsonDeepEqual(a[key], b[key])) return false;
687
+ }
688
+ return true;
689
+ }
488
690
  function jsonDeepEqual(a, b) {
489
691
  if (a === b) return true;
490
692
  if (a === null || b === null) return false;
491
693
  if (typeof a !== typeof b) return false;
492
694
  if (Array.isArray(a)) {
493
695
  if (!Array.isArray(b)) return false;
494
- if (a.length !== b.length) return false;
495
- for (let i = 0; i < a.length; i++) {
496
- if (!jsonDeepEqual(a[i], b[i])) return false;
497
- }
498
- return true;
696
+ return jsonDeepEqualArray(a, b);
499
697
  }
500
698
  if (typeof a === "object") {
501
699
  if (Array.isArray(b)) return false;
502
- const aObj = a;
503
- const bObj = b;
504
- const aKeys = Object.keys(aObj);
505
- const bKeys = Object.keys(bObj);
506
- if (aKeys.length !== bKeys.length) return false;
507
- for (const key of aKeys) {
508
- if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;
509
- if (!jsonDeepEqual(aObj[key], bObj[key])) return false;
510
- }
511
- return true;
700
+ return jsonDeepEqualObject(a, b);
512
701
  }
513
702
  return false;
514
703
  }
@@ -523,206 +712,283 @@ function compileSchema(schema) {
523
712
  function isJsonPrimitive(value) {
524
713
  return value === null || typeof value !== "object";
525
714
  }
715
+ function isJsonObjectRecord(value) {
716
+ return typeof value === "object" && value !== null && !Array.isArray(value);
717
+ }
718
+ function objectHasOwn(value, property) {
719
+ const hasOwn = Object.hasOwn;
720
+ if (typeof hasOwn === "function") {
721
+ return hasOwn(value, property);
722
+ }
723
+ return Object.getOwnPropertyDescriptor(value, property) !== void 0;
724
+ }
526
725
  function buildValidator(schema) {
527
- const resolvedTypes = schema.type !== void 0 ? Array.isArray(schema.type) ? schema.type : [schema.type] : null;
528
- const typeLabel = resolvedTypes !== null ? JSON.stringify(schema.type) : "";
529
- const enumMembers = schema.enum;
530
- let enumPrimitiveSet = null;
531
- let enumComplexMembers = null;
532
- if (enumMembers !== void 0) {
533
- const primitives = [];
534
- const complex = [];
535
- for (const member of enumMembers) {
536
- if (isJsonPrimitive(member)) {
537
- primitives.push(member);
538
- } else {
539
- complex.push(member);
540
- }
726
+ const typeStep = buildTypeValidationStep(schema);
727
+ const validationSteps = [
728
+ buildEnumValidationStep(schema),
729
+ buildConstValidationStep(schema),
730
+ buildNumberValidationStep(schema),
731
+ buildStringValidationStep(schema),
732
+ buildObjectValidationStep(schema),
733
+ buildArrayValidationStep(schema)
734
+ ].filter((step) => step !== null);
735
+ if (typeStep === null && validationSteps.length === 0) {
736
+ return (_value, _path) => [];
737
+ }
738
+ return (value, path = "") => {
739
+ const errors = [];
740
+ if (typeStep && !typeStep(value, path, errors)) {
741
+ return errors;
742
+ }
743
+ for (const step of validationSteps) {
744
+ step(value, path, errors);
745
+ }
746
+ return errors;
747
+ };
748
+ }
749
+ function buildTypeValidationStep(schema) {
750
+ if (schema.type === void 0) {
751
+ return null;
752
+ }
753
+ const resolvedTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
754
+ const typeLabel = JSON.stringify(schema.type);
755
+ return (value, path, errors) => {
756
+ if (resolvedTypes.some((type) => matchesType(value, type))) {
757
+ return true;
758
+ }
759
+ errors.push({
760
+ path,
761
+ message: `Expected type ${typeLabel}, got ${jsonTypeLabel(value)}`,
762
+ keyword: "type"
763
+ });
764
+ return false;
765
+ };
766
+ }
767
+ function buildEnumValidationStep(schema) {
768
+ if (schema.enum === void 0) {
769
+ return null;
770
+ }
771
+ const enumPrimitiveSet = new Set(schema.enum.filter((member) => isJsonPrimitive(member)));
772
+ const enumComplexMembers = schema.enum.filter((member) => !isJsonPrimitive(member));
773
+ return (value, path, errors) => {
774
+ const primitiveMatch = isJsonPrimitive(value) && enumPrimitiveSet.has(value);
775
+ const complexMatch = !primitiveMatch && enumComplexMembers.some((entry) => jsonDeepEqual(value, entry));
776
+ if (primitiveMatch || complexMatch) {
777
+ return;
541
778
  }
542
- if (primitives.length > 0) enumPrimitiveSet = new Set(primitives);
543
- if (complex.length > 0) enumComplexMembers = complex;
779
+ errors.push({
780
+ path,
781
+ message: `Value does not match any enum member`,
782
+ keyword: "enum"
783
+ });
784
+ };
785
+ }
786
+ function buildConstValidationStep(schema) {
787
+ if (!("const" in schema)) {
788
+ return null;
544
789
  }
545
- const hasConst = "const" in schema;
546
- const constValue = schema.const;
790
+ return (value, path, errors) => {
791
+ if (jsonDeepEqual(value, schema.const)) {
792
+ return;
793
+ }
794
+ errors.push({
795
+ path,
796
+ message: `Value does not match const`,
797
+ keyword: "const"
798
+ });
799
+ };
800
+ }
801
+ function buildNumberValidationStep(schema) {
547
802
  const hasMinimum = schema.minimum !== void 0;
548
- const minimum = schema.minimum;
549
803
  const hasMaximum = schema.maximum !== void 0;
550
- const maximum = schema.maximum;
551
804
  const hasExMin = schema.exclusiveMinimum !== void 0;
552
- const exMin = schema.exclusiveMinimum;
553
805
  const hasExMax = schema.exclusiveMaximum !== void 0;
806
+ if (!hasMinimum && !hasMaximum && !hasExMin && !hasExMax) {
807
+ return null;
808
+ }
809
+ const minimum = schema.minimum;
810
+ const maximum = schema.maximum;
811
+ const exMin = schema.exclusiveMinimum;
554
812
  const exMax = schema.exclusiveMaximum;
555
- const hasNumberConstraints = hasMinimum || hasMaximum || hasExMin || hasExMax;
813
+ return (value, path, errors) => {
814
+ if (typeof value !== "number") {
815
+ return;
816
+ }
817
+ if (hasMinimum && value < minimum) {
818
+ errors.push({
819
+ path,
820
+ message: `Value ${value} is less than minimum ${minimum}`,
821
+ keyword: "minimum"
822
+ });
823
+ }
824
+ if (hasMaximum && value > maximum) {
825
+ errors.push({
826
+ path,
827
+ message: `Value ${value} is greater than maximum ${maximum}`,
828
+ keyword: "maximum"
829
+ });
830
+ }
831
+ if (hasExMin && value <= exMin) {
832
+ errors.push({
833
+ path,
834
+ message: `Value ${value} is not greater than exclusiveMinimum ${exMin}`,
835
+ keyword: "exclusiveMinimum"
836
+ });
837
+ }
838
+ if (hasExMax && value >= exMax) {
839
+ errors.push({
840
+ path,
841
+ message: `Value ${value} is not less than exclusiveMaximum ${exMax}`,
842
+ keyword: "exclusiveMaximum"
843
+ });
844
+ }
845
+ };
846
+ }
847
+ function buildStringValidationStep(schema) {
556
848
  const hasMinLength = schema.minLength !== void 0;
557
- const minLen = schema.minLength;
558
849
  const hasMaxLength = schema.maxLength !== void 0;
559
- const maxLen = schema.maxLength;
560
- const hasStringConstraints = hasMinLength || hasMaxLength;
561
- const requiredKeys = schema.required;
562
- const hasRequired = requiredKeys !== void 0 && requiredKeys.length > 0;
563
- const hasProperties = schema.properties !== void 0;
564
- const propertyValidators = hasProperties ? Object.entries(schema.properties).map(
565
- ([name, propSchema]) => [name, compileSchema(propSchema)]
566
- ) : null;
850
+ if (!hasMinLength && !hasMaxLength) {
851
+ return null;
852
+ }
853
+ const minLength = schema.minLength;
854
+ const maxLength = schema.maxLength;
855
+ return (value, path, errors) => {
856
+ if (typeof value !== "string") {
857
+ return;
858
+ }
859
+ if (hasMinLength && value.length < minLength) {
860
+ errors.push({
861
+ path,
862
+ message: `String length ${value.length} is less than minLength ${minLength}`,
863
+ keyword: "minLength"
864
+ });
865
+ }
866
+ if (hasMaxLength && value.length > maxLength) {
867
+ errors.push({
868
+ path,
869
+ message: `String length ${value.length} is greater than maxLength ${maxLength}`,
870
+ keyword: "maxLength"
871
+ });
872
+ }
873
+ };
874
+ }
875
+ function buildObjectValidationStep(schema) {
876
+ const requiredKeys = schema.required ?? [];
877
+ const propertyValidators = schema.properties ? Object.entries(schema.properties).map(([name, propertySchema]) => [
878
+ name,
879
+ compileSchema(propertySchema)
880
+ ]) : null;
567
881
  const checkAdditional = schema.additionalProperties !== void 0 && schema.additionalProperties !== true;
568
882
  const additionalIsFalse = schema.additionalProperties === false;
569
883
  const additionalValidator = checkAdditional && !additionalIsFalse ? compileSchema(schema.additionalProperties) : null;
570
- const definedPropKeys = checkAdditional ? new Set(schema.properties ? Object.keys(schema.properties) : []) : null;
571
- const hasObjectConstraints = hasRequired || hasProperties || checkAdditional;
572
- const hasMinItems = schema.minItems !== void 0;
573
- const minItems = schema.minItems;
574
- const hasMaxItems = schema.maxItems !== void 0;
575
- const maxItems = schema.maxItems;
576
- const itemsValidator = schema.items !== void 0 ? compileSchema(schema.items) : null;
577
- const hasArrayConstraints = hasMinItems || hasMaxItems || itemsValidator !== null;
578
- if (resolvedTypes === null && enumMembers === void 0 && !hasConst && !hasNumberConstraints && !hasStringConstraints && !hasObjectConstraints && !hasArrayConstraints) {
579
- return (_value, _path) => [];
884
+ const definedPropKeys = checkAdditional ? new Set(Object.keys(schema.properties ?? {})) : null;
885
+ const objectValidationSteps = [];
886
+ if (requiredKeys.length > 0) {
887
+ objectValidationSteps.push(createRequiredPropertyStep(requiredKeys));
580
888
  }
581
- return (value, path = "") => {
582
- const errors = [];
583
- if (resolvedTypes !== null) {
584
- const matched = resolvedTypes.some((t) => matchesType(value, t));
585
- if (!matched) {
586
- errors.push({
587
- path,
588
- message: `Expected type ${typeLabel}, got ${jsonTypeLabel(value)}`,
589
- keyword: "type"
590
- });
591
- return errors;
592
- }
889
+ if (propertyValidators !== null) {
890
+ objectValidationSteps.push(createDeclaredPropertyStep(propertyValidators));
891
+ }
892
+ if (checkAdditional) {
893
+ objectValidationSteps.push(
894
+ createAdditionalPropertyStep({
895
+ additionalIsFalse,
896
+ additionalValidator,
897
+ definedPropKeys: definedPropKeys ?? /* @__PURE__ */ new Set()
898
+ })
899
+ );
900
+ }
901
+ if (objectValidationSteps.length === 0) {
902
+ return null;
903
+ }
904
+ return (value, path, errors) => {
905
+ if (!isJsonObjectRecord(value)) {
906
+ return;
593
907
  }
594
- if (enumMembers !== void 0) {
595
- let matched = false;
596
- if (enumPrimitiveSet !== null && isJsonPrimitive(value)) {
597
- matched = enumPrimitiveSet.has(value);
598
- }
599
- if (!matched && enumComplexMembers !== null) {
600
- matched = enumComplexMembers.some((entry) => jsonDeepEqual(value, entry));
601
- }
602
- if (!matched) {
603
- errors.push({
604
- path,
605
- message: `Value does not match any enum member`,
606
- keyword: "enum"
607
- });
608
- }
908
+ for (const step of objectValidationSteps) {
909
+ step(value, path, errors);
609
910
  }
610
- if (hasConst) {
611
- if (!jsonDeepEqual(value, constValue)) {
612
- errors.push({
613
- path,
614
- message: `Value does not match const`,
615
- keyword: "const"
616
- });
911
+ };
912
+ }
913
+ function createRequiredPropertyStep(requiredKeys) {
914
+ return (value, path, errors) => {
915
+ for (const requiredKey of requiredKeys) {
916
+ if (objectHasOwn(value, requiredKey)) {
917
+ continue;
617
918
  }
919
+ errors.push({
920
+ path,
921
+ message: `Missing required property "${requiredKey}"`,
922
+ keyword: "required"
923
+ });
618
924
  }
619
- if (hasNumberConstraints && typeof value === "number") {
620
- if (hasMinimum && value < minimum) {
621
- errors.push({
622
- path,
623
- message: `Value ${value} is less than minimum ${minimum}`,
624
- keyword: "minimum"
625
- });
626
- }
627
- if (hasMaximum && value > maximum) {
628
- errors.push({
629
- path,
630
- message: `Value ${value} is greater than maximum ${maximum}`,
631
- keyword: "maximum"
632
- });
633
- }
634
- if (hasExMin && value <= exMin) {
635
- errors.push({
636
- path,
637
- message: `Value ${value} is not greater than exclusiveMinimum ${exMin}`,
638
- keyword: "exclusiveMinimum"
639
- });
640
- }
641
- if (hasExMax && value >= exMax) {
642
- errors.push({
643
- path,
644
- message: `Value ${value} is not less than exclusiveMaximum ${exMax}`,
645
- keyword: "exclusiveMaximum"
646
- });
925
+ };
926
+ }
927
+ function createDeclaredPropertyStep(propertyValidators) {
928
+ return (value, path, errors) => {
929
+ for (const [propertyName, validator] of propertyValidators) {
930
+ if (!objectHasOwn(value, propertyName)) {
931
+ continue;
647
932
  }
933
+ errors.push(...validator(value[propertyName], `${path}/${propertyName}`));
648
934
  }
649
- if (hasStringConstraints && typeof value === "string") {
650
- if (hasMinLength && value.length < minLen) {
651
- errors.push({
652
- path,
653
- message: `String length ${value.length} is less than minLength ${minLen}`,
654
- keyword: "minLength"
655
- });
935
+ };
936
+ }
937
+ function createAdditionalPropertyStep({
938
+ additionalIsFalse,
939
+ additionalValidator,
940
+ definedPropKeys
941
+ }) {
942
+ return (value, path, errors) => {
943
+ for (const objectKey of Object.keys(value)) {
944
+ if (definedPropKeys.has(objectKey)) {
945
+ continue;
656
946
  }
657
- if (hasMaxLength && value.length > maxLen) {
947
+ if (additionalIsFalse) {
658
948
  errors.push({
659
949
  path,
660
- message: `String length ${value.length} is greater than maxLength ${maxLen}`,
661
- keyword: "maxLength"
950
+ message: `Additional property "${objectKey}" is not allowed`,
951
+ keyword: "additionalProperties"
662
952
  });
953
+ continue;
663
954
  }
955
+ errors.push(...additionalValidator(value[objectKey], `${path}/${objectKey}`));
664
956
  }
665
- if (hasObjectConstraints && typeof value === "object" && value !== null && !Array.isArray(value)) {
666
- const obj = value;
667
- if (hasRequired) {
668
- for (const reqKey of requiredKeys) {
669
- if (!Object.prototype.hasOwnProperty.call(obj, reqKey)) {
670
- errors.push({
671
- path,
672
- message: `Missing required property "${reqKey}"`,
673
- keyword: "required"
674
- });
675
- }
676
- }
677
- }
678
- if (propertyValidators !== null) {
679
- for (const [propName, propValidator] of propertyValidators) {
680
- if (Object.prototype.hasOwnProperty.call(obj, propName)) {
681
- const propErrors = propValidator(obj[propName], `${path}/${propName}`);
682
- errors.push(...propErrors);
683
- }
684
- }
685
- }
686
- if (checkAdditional) {
687
- for (const objKey of Object.keys(obj)) {
688
- if (!definedPropKeys.has(objKey)) {
689
- if (additionalIsFalse) {
690
- errors.push({
691
- path,
692
- message: `Additional property "${objKey}" is not allowed`,
693
- keyword: "additionalProperties"
694
- });
695
- } else {
696
- const propErrors = additionalValidator(obj[objKey], `${path}/${objKey}`);
697
- errors.push(...propErrors);
698
- }
699
- }
700
- }
701
- }
957
+ };
958
+ }
959
+ function buildArrayValidationStep(schema) {
960
+ const hasMinItems = schema.minItems !== void 0;
961
+ const hasMaxItems = schema.maxItems !== void 0;
962
+ const itemsValidator = schema.items ? compileSchema(schema.items) : null;
963
+ if (!hasMinItems && !hasMaxItems && itemsValidator === null) {
964
+ return null;
965
+ }
966
+ const minItems = schema.minItems;
967
+ const maxItems = schema.maxItems;
968
+ return (value, path, errors) => {
969
+ if (!Array.isArray(value)) {
970
+ return;
702
971
  }
703
- if (hasArrayConstraints && Array.isArray(value)) {
704
- if (hasMinItems && value.length < minItems) {
705
- errors.push({
706
- path,
707
- message: `Array length ${value.length} is less than minItems ${minItems}`,
708
- keyword: "minItems"
709
- });
710
- }
711
- if (hasMaxItems && value.length > maxItems) {
712
- errors.push({
713
- path,
714
- message: `Array length ${value.length} is greater than maxItems ${maxItems}`,
715
- keyword: "maxItems"
716
- });
717
- }
718
- if (itemsValidator !== null) {
719
- for (let i = 0; i < value.length; i++) {
720
- const itemErrors = itemsValidator(value[i], `${path}/${i}`);
721
- errors.push(...itemErrors);
722
- }
723
- }
972
+ if (hasMinItems && value.length < minItems) {
973
+ errors.push({
974
+ path,
975
+ message: `Array length ${value.length} is less than minItems ${minItems}`,
976
+ keyword: "minItems"
977
+ });
978
+ }
979
+ if (hasMaxItems && value.length > maxItems) {
980
+ errors.push({
981
+ path,
982
+ message: `Array length ${value.length} is greater than maxItems ${maxItems}`,
983
+ keyword: "maxItems"
984
+ });
985
+ }
986
+ if (itemsValidator === null) {
987
+ return;
988
+ }
989
+ for (const [index, item] of value.entries()) {
990
+ errors.push(...itemsValidator(item, `${path}/${index}`));
724
991
  }
725
- return errors;
726
992
  };
727
993
  }
728
994
  function validateJsonSchema(value, schema, path = "") {
@@ -741,7 +1007,7 @@ function inferJsonSchema(sample) {
741
1007
  case "string":
742
1008
  return { type: "string" };
743
1009
  case "number":
744
- return Number.isInteger(sample) ? { type: "number" } : { type: "number" };
1010
+ return { type: "number" };
745
1011
  case "boolean":
746
1012
  return { type: "boolean" };
747
1013
  case "object":
@@ -752,21 +1018,173 @@ function inferJsonSchema(sample) {
752
1018
  }
753
1019
 
754
1020
  // src/Mnemonic/use.ts
755
- function useMnemonicKey(key, options) {
1021
+ var SSR_SNAPSHOT_TOKEN = /* @__PURE__ */ Symbol("mnemonic:ssr-snapshot");
1022
+ var diagnosticContractRegistry = /* @__PURE__ */ new WeakMap();
1023
+ var diagnosticWarningRegistry = /* @__PURE__ */ new WeakMap();
1024
+ var diagnosticObjectIds = /* @__PURE__ */ new WeakMap();
1025
+ var nextDiagnosticObjectId = 1;
1026
+ function serializeEnvelope(version, payload) {
1027
+ return JSON.stringify({
1028
+ version,
1029
+ payload
1030
+ });
1031
+ }
1032
+ function withReadMetadata(value, rewriteRaw, pendingSchema) {
1033
+ const result = { value };
1034
+ if (rewriteRaw !== void 0) result.rewriteRaw = rewriteRaw;
1035
+ if (pendingSchema !== void 0) result.pendingSchema = pendingSchema;
1036
+ return result;
1037
+ }
1038
+ function isDevelopmentRuntime() {
1039
+ return getRuntimeNodeEnv() === "development";
1040
+ }
1041
+ function getDiagnosticWarnings(api) {
1042
+ let warnings = diagnosticWarningRegistry.get(api);
1043
+ if (!warnings) {
1044
+ warnings = /* @__PURE__ */ new Set();
1045
+ diagnosticWarningRegistry.set(api, warnings);
1046
+ }
1047
+ return warnings;
1048
+ }
1049
+ function warnOnce(api, id, message) {
1050
+ const warnings = getDiagnosticWarnings(api);
1051
+ if (warnings.has(id)) return;
1052
+ warnings.add(id);
1053
+ console.warn(message);
1054
+ }
1055
+ function stableDiagnosticValue(value) {
1056
+ if (typeof value === "function") {
1057
+ const source = Function.prototype.toString.call(value).split(/\s+/).join(" ").trim();
1058
+ const name = value.name || "anonymous";
1059
+ return `[factory:${name}/${value.length}:${source}]`;
1060
+ }
1061
+ if (typeof value === "bigint") return `${value.toString()}n`;
1062
+ if (typeof value === "symbol") return value.toString();
1063
+ if (value === void 0) return "undefined";
1064
+ try {
1065
+ return JSON.stringify(value);
1066
+ } catch {
1067
+ const tag = Object.prototype.toString.call(value);
1068
+ if (value !== null && (typeof value === "object" || typeof value === "function")) {
1069
+ return `${tag}#${getDiagnosticObjectId(value)}`;
1070
+ }
1071
+ return tag;
1072
+ }
1073
+ }
1074
+ function isObjectLike(value) {
1075
+ return value !== null && (typeof value === "object" || typeof value === "function");
1076
+ }
1077
+ function objectHasOwn2(value, property) {
1078
+ const hasOwn = Object.hasOwn;
1079
+ if (typeof hasOwn === "function") {
1080
+ return hasOwn(value, property);
1081
+ }
1082
+ return Object.getOwnPropertyDescriptor(value, property) !== void 0;
1083
+ }
1084
+ function getDiagnosticObjectId(value) {
1085
+ const existing = diagnosticObjectIds.get(value);
1086
+ if (existing !== void 0) return existing;
1087
+ const id = nextDiagnosticObjectId++;
1088
+ diagnosticObjectIds.set(value, id);
1089
+ return id;
1090
+ }
1091
+ function buildContractFingerprint({
1092
+ api,
1093
+ key,
1094
+ defaultValue,
1095
+ codecOpt,
1096
+ schema,
1097
+ reconcile,
1098
+ listenCrossTab,
1099
+ ssrOptions
1100
+ }) {
1101
+ const codecSignature = codecOpt == null || !isObjectLike(codecOpt) ? "default-json-codec" : `codec:${stableDiagnosticValue(codecOpt.encode)}:${stableDiagnosticValue(codecOpt.decode)}`;
1102
+ const reconcileSignature = reconcile == null || !isObjectLike(reconcile) ? "no-reconcile" : `reconcile:${stableDiagnosticValue(reconcile)}`;
1103
+ return JSON.stringify({
1104
+ key,
1105
+ defaultValue: stableDiagnosticValue(defaultValue),
1106
+ codec: codecSignature,
1107
+ schemaVersion: schema?.version ?? null,
1108
+ listenCrossTab: Boolean(listenCrossTab),
1109
+ reconcile: reconcileSignature,
1110
+ ssrHydration: ssrOptions?.hydration ?? null,
1111
+ hasServerValue: ssrOptions?.serverValue !== void 0,
1112
+ providerHydration: api.ssrHydration ?? null
1113
+ });
1114
+ }
1115
+ function resolveMnemonicKeyArgs(keyOrDescriptor, options) {
1116
+ if (typeof keyOrDescriptor !== "string") {
1117
+ return keyOrDescriptor;
1118
+ }
1119
+ if (!options) {
1120
+ throw new Error("useMnemonicKey requires options when called with a string key");
1121
+ }
1122
+ return {
1123
+ key: keyOrDescriptor,
1124
+ options
1125
+ };
1126
+ }
1127
+ function useMnemonicKey(keyOrDescriptor, options) {
1128
+ const descriptor = resolveMnemonicKeyArgs(keyOrDescriptor, options);
1129
+ const key = descriptor.key;
1130
+ const resolvedOptions = descriptor.options;
756
1131
  const api = useMnemonic();
757
- const { defaultValue, onMount, onChange, listenCrossTab, codec: codecOpt, schema, reconcile } = options;
1132
+ const {
1133
+ defaultValue,
1134
+ onMount,
1135
+ onChange,
1136
+ listenCrossTab,
1137
+ codec: codecOpt,
1138
+ schema,
1139
+ reconcile,
1140
+ ssr: ssrOptions
1141
+ } = resolvedOptions;
758
1142
  const codec = codecOpt ?? JSONCodec;
759
1143
  const schemaMode = api.schemaMode;
760
1144
  const schemaRegistry = api.schemaRegistry;
1145
+ const hydrationMode = ssrOptions?.hydration ?? api.ssrHydration;
1146
+ const [hasMounted, setHasMounted] = useState(hydrationMode !== "client-only");
1147
+ const developmentRuntime = isDevelopmentRuntime();
1148
+ const contractFingerprint = useMemo(
1149
+ () => developmentRuntime ? buildContractFingerprint({
1150
+ api,
1151
+ key,
1152
+ defaultValue,
1153
+ codecOpt,
1154
+ schema,
1155
+ reconcile,
1156
+ listenCrossTab,
1157
+ ssrOptions
1158
+ }) : null,
1159
+ [
1160
+ developmentRuntime,
1161
+ api,
1162
+ key,
1163
+ defaultValue,
1164
+ codecOpt,
1165
+ schema?.version,
1166
+ reconcile,
1167
+ listenCrossTab,
1168
+ ssrOptions?.hydration,
1169
+ ssrOptions?.serverValue
1170
+ ]
1171
+ );
761
1172
  const getFallback = useCallback(
762
1173
  (error) => typeof defaultValue === "function" ? defaultValue(error) : defaultValue,
763
1174
  [defaultValue]
764
1175
  );
1176
+ const getServerValue = useCallback(() => {
1177
+ const serverValue = ssrOptions?.serverValue;
1178
+ if (serverValue === void 0) {
1179
+ return getFallback();
1180
+ }
1181
+ return typeof serverValue === "function" ? serverValue() : serverValue;
1182
+ }, [getFallback, ssrOptions?.serverValue]);
765
1183
  const parseEnvelope = useCallback(
766
1184
  (rawText) => {
767
1185
  try {
768
1186
  const parsed = JSON.parse(rawText);
769
- if (typeof parsed !== "object" || parsed == null || !Number.isInteger(parsed.version) || parsed.version < 0 || !Object.prototype.hasOwnProperty.call(parsed, "payload")) {
1187
+ if (typeof parsed !== "object" || parsed == null || !Number.isInteger(parsed.version) || parsed.version < 0 || !objectHasOwn2(parsed, "payload")) {
770
1188
  return {
771
1189
  ok: false,
772
1190
  error: new SchemaError("INVALID_ENVELOPE", `Invalid envelope for key "${key}"`)
@@ -784,12 +1202,6 @@ function useMnemonicKey(key, options) {
784
1202
  );
785
1203
  const decodeStringPayload = useCallback(
786
1204
  (payload, activeCodec) => {
787
- if (typeof payload !== "string") {
788
- throw new SchemaError(
789
- "INVALID_ENVELOPE",
790
- `Envelope payload must be a string for codec-managed key "${key}"`
791
- );
792
- }
793
1205
  try {
794
1206
  return activeCodec.decode(payload);
795
1207
  } catch (err) {
@@ -853,21 +1265,28 @@ function useMnemonicKey(key, options) {
853
1265
  },
854
1266
  [schemaRegistry, registryCache, key]
855
1267
  );
1268
+ const buildFallbackResult = useCallback(
1269
+ (error) => ({
1270
+ value: getFallback(error)
1271
+ }),
1272
+ [getFallback]
1273
+ );
1274
+ const buildSchemaManagedResult = useCallback(
1275
+ (version, value2) => serializeEnvelope(version, value2),
1276
+ []
1277
+ );
1278
+ const resolveTargetWriteSchema = useCallback(() => {
1279
+ const explicitVersion = schema?.version;
1280
+ const latestSchema = getLatestSchemaForKey();
1281
+ if (explicitVersion === void 0) return latestSchema;
1282
+ const explicitSchema = getSchemaForVersion(explicitVersion);
1283
+ if (explicitSchema) return explicitSchema;
1284
+ return schemaMode === "strict" ? void 0 : latestSchema;
1285
+ }, [getLatestSchemaForKey, getSchemaForVersion, schema?.version, schemaMode]);
856
1286
  const encodeForWrite = useCallback(
857
1287
  (nextValue) => {
858
1288
  const explicitVersion = schema?.version;
859
- const latestSchema = getLatestSchemaForKey();
860
- const explicitSchema = explicitVersion !== void 0 ? getSchemaForVersion(explicitVersion) : void 0;
861
- let targetSchema = explicitSchema;
862
- if (!targetSchema) {
863
- if (explicitVersion !== void 0) {
864
- if (schemaMode !== "strict") {
865
- targetSchema = latestSchema;
866
- }
867
- } else {
868
- targetSchema = latestSchema;
869
- }
870
- }
1289
+ const targetSchema = resolveTargetWriteSchema();
871
1290
  if (!targetSchema) {
872
1291
  if (explicitVersion !== void 0 && schemaMode === "strict") {
873
1292
  throw new SchemaError(
@@ -875,11 +1294,7 @@ function useMnemonicKey(key, options) {
875
1294
  `Write requires schema for key "${key}" in strict mode`
876
1295
  );
877
1296
  }
878
- const envelope2 = {
879
- version: 0,
880
- payload: codec.encode(nextValue)
881
- };
882
- return JSON.stringify(envelope2);
1297
+ return serializeEnvelope(0, codec.encode(nextValue));
883
1298
  }
884
1299
  let valueToStore = nextValue;
885
1300
  const writeMigration = schemaRegistry?.getWriteMigration?.(key, targetSchema.version);
@@ -891,11 +1306,7 @@ function useMnemonicKey(key, options) {
891
1306
  }
892
1307
  }
893
1308
  validateAgainstSchema(valueToStore, targetSchema.schema);
894
- const envelope = {
895
- version: targetSchema.version,
896
- payload: valueToStore
897
- };
898
- return JSON.stringify(envelope);
1309
+ return buildSchemaManagedResult(targetSchema.version, valueToStore);
899
1310
  },
900
1311
  [
901
1312
  schema?.version,
@@ -904,8 +1315,8 @@ function useMnemonicKey(key, options) {
904
1315
  codec,
905
1316
  schemaRegistry,
906
1317
  validateAgainstSchema,
907
- getLatestSchemaForKey,
908
- getSchemaForVersion
1318
+ resolveTargetWriteSchema,
1319
+ buildSchemaManagedResult
909
1320
  ]
910
1321
  );
911
1322
  const applyReconcile = useCallback(
@@ -919,135 +1330,112 @@ function useMnemonicKey(key, options) {
919
1330
  derivePendingSchema
920
1331
  }) => {
921
1332
  if (!reconcile) {
922
- const result = { value: value2 };
923
- if (rewriteRaw !== void 0) result.rewriteRaw = rewriteRaw;
924
- if (pendingSchema !== void 0) result.pendingSchema = pendingSchema;
925
- return result;
1333
+ return withReadMetadata(value2, rewriteRaw, pendingSchema);
926
1334
  }
927
1335
  const context = {
928
1336
  key,
929
1337
  persistedVersion,
930
1338
  ...latestVersion === void 0 ? {} : { latestVersion }
931
1339
  };
932
- let baselineSerialized;
933
- if (serializeForPersist) {
1340
+ const baselineSerialized = (() => {
934
1341
  try {
935
- baselineSerialized = serializeForPersist(value2);
1342
+ return serializeForPersist(value2);
936
1343
  } catch {
937
- baselineSerialized = rewriteRaw;
1344
+ return rewriteRaw;
938
1345
  }
939
- }
1346
+ })();
940
1347
  try {
941
1348
  const reconciled = reconcile(value2, context);
942
1349
  const nextPendingSchema = derivePendingSchema ? derivePendingSchema(reconciled) : pendingSchema;
943
- if (!serializeForPersist) {
944
- const result2 = { value: reconciled };
945
- if (rewriteRaw !== void 0) result2.rewriteRaw = rewriteRaw;
946
- if (nextPendingSchema !== void 0) result2.pendingSchema = nextPendingSchema;
947
- return result2;
948
- }
949
1350
  const nextSerialized = serializeForPersist(reconciled);
950
1351
  const nextRewriteRaw = baselineSerialized === void 0 || nextSerialized !== baselineSerialized ? nextSerialized : rewriteRaw;
951
- const result = { value: reconciled };
952
- if (nextRewriteRaw !== void 0) result.rewriteRaw = nextRewriteRaw;
953
- if (nextPendingSchema !== void 0) result.pendingSchema = nextPendingSchema;
954
- return result;
1352
+ return withReadMetadata(reconciled, nextRewriteRaw, nextPendingSchema);
955
1353
  } catch (err) {
956
1354
  const typedErr = err instanceof SchemaError ? err : new SchemaError("RECONCILE_FAILED", `Reconciliation failed for key "${key}"`, err);
957
- return { value: getFallback(typedErr) };
1355
+ return buildFallbackResult(typedErr);
958
1356
  }
959
1357
  },
960
- [getFallback, key, reconcile]
1358
+ [buildFallbackResult, key, reconcile]
961
1359
  );
962
- const decodeForRead = useCallback(
963
- (rawText) => {
964
- if (rawText == null) return { value: getFallback() };
965
- const parsed = parseEnvelope(rawText);
966
- if (!parsed.ok) return { value: getFallback(parsed.error) };
967
- const envelope = parsed.envelope;
968
- const schemaForVersion = getSchemaForVersion(envelope.version);
969
- const latestSchema = getLatestSchemaForKey();
970
- if (schemaMode === "strict" && !schemaForVersion) {
971
- return {
972
- value: getFallback(
973
- new SchemaError("SCHEMA_NOT_FOUND", `No schema for key "${key}" v${envelope.version}`)
1360
+ const decodeAutoschemaEnvelope = useCallback(
1361
+ (envelope, latestSchema) => {
1362
+ if (latestSchema) {
1363
+ return buildFallbackResult(
1364
+ new SchemaError("SCHEMA_NOT_FOUND", `No schema for key "${key}" v${envelope.version}`)
1365
+ );
1366
+ }
1367
+ if (!schemaRegistry || typeof schemaRegistry.registerSchema !== "function") {
1368
+ return buildFallbackResult(
1369
+ new SchemaError(
1370
+ "MODE_CONFIGURATION_INVALID",
1371
+ `Autoschema mode requires schema registry registration for key "${key}"`
974
1372
  )
975
- };
1373
+ );
976
1374
  }
977
- if (schemaMode === "autoschema" && !schemaForVersion) {
978
- if (latestSchema) {
979
- return {
980
- value: getFallback(
981
- new SchemaError("SCHEMA_NOT_FOUND", `No schema for key "${key}" v${envelope.version}`)
982
- )
983
- };
984
- }
985
- if (!schemaRegistry || typeof schemaRegistry.registerSchema !== "function") {
986
- return {
987
- value: getFallback(
988
- new SchemaError(
989
- "MODE_CONFIGURATION_INVALID",
990
- `Autoschema mode requires schema registry registration for key "${key}"`
991
- )
992
- )
993
- };
994
- }
995
- try {
996
- const decoded2 = typeof envelope.payload === "string" ? decodeStringPayload(envelope.payload, codec) : envelope.payload;
997
- const inferSchemaForValue = (value2) => ({
998
- key,
999
- version: 1,
1000
- schema: inferJsonSchema(value2)
1001
- });
1002
- const inferred = inferSchemaForValue(decoded2);
1003
- return applyReconcile({
1004
- value: decoded2,
1005
- pendingSchema: inferred,
1006
- rewriteRaw: JSON.stringify({
1007
- version: inferred.version,
1008
- payload: decoded2
1009
- }),
1010
- persistedVersion: envelope.version,
1011
- serializeForPersist: (value2) => JSON.stringify({
1012
- version: inferred.version,
1013
- payload: value2
1014
- }),
1015
- derivePendingSchema: inferSchemaForValue
1016
- });
1017
- } catch (err) {
1018
- const typedErr = err instanceof SchemaError || err instanceof CodecError ? err : new SchemaError("TYPE_MISMATCH", `Autoschema inference failed for key "${key}"`, err);
1019
- return { value: getFallback(typedErr) };
1020
- }
1375
+ try {
1376
+ const decoded2 = typeof envelope.payload === "string" ? decodeStringPayload(envelope.payload, codec) : envelope.payload;
1377
+ const inferSchemaForValue = (value2) => ({
1378
+ key,
1379
+ version: 1,
1380
+ schema: inferJsonSchema(value2)
1381
+ });
1382
+ const inferred = inferSchemaForValue(decoded2);
1383
+ return applyReconcile({
1384
+ value: decoded2,
1385
+ pendingSchema: inferred,
1386
+ rewriteRaw: buildSchemaManagedResult(inferred.version, decoded2),
1387
+ persistedVersion: envelope.version,
1388
+ serializeForPersist: (value2) => buildSchemaManagedResult(inferred.version, value2),
1389
+ derivePendingSchema: inferSchemaForValue
1390
+ });
1391
+ } catch (err) {
1392
+ const typedErr = err instanceof SchemaError || err instanceof CodecError ? err : new SchemaError("TYPE_MISMATCH", `Autoschema inference failed for key "${key}"`, err);
1393
+ return buildFallbackResult(typedErr);
1021
1394
  }
1022
- if (!schemaForVersion) {
1023
- if (typeof envelope.payload !== "string") {
1024
- return applyReconcile({
1025
- value: envelope.payload,
1026
- persistedVersion: envelope.version,
1027
- ...latestSchema ? { latestVersion: latestSchema.version } : {},
1028
- serializeForPersist: encodeForWrite
1029
- });
1030
- }
1031
- try {
1032
- const decoded2 = decodeStringPayload(envelope.payload, codec);
1033
- return applyReconcile({
1034
- value: decoded2,
1035
- persistedVersion: envelope.version,
1036
- ...latestSchema ? { latestVersion: latestSchema.version } : {},
1037
- serializeForPersist: encodeForWrite
1038
- });
1039
- } catch (err) {
1040
- const typedErr = err instanceof SchemaError || err instanceof CodecError ? err : new CodecError(`Codec decode failed for key "${key}"`, err);
1041
- return { value: getFallback(typedErr) };
1042
- }
1395
+ },
1396
+ [
1397
+ applyReconcile,
1398
+ buildFallbackResult,
1399
+ buildSchemaManagedResult,
1400
+ codec,
1401
+ decodeStringPayload,
1402
+ key,
1403
+ schemaRegistry
1404
+ ]
1405
+ );
1406
+ const decodeCodecManagedEnvelope = useCallback(
1407
+ (envelope, latestSchema) => {
1408
+ if (typeof envelope.payload !== "string") {
1409
+ return applyReconcile({
1410
+ value: envelope.payload,
1411
+ persistedVersion: envelope.version,
1412
+ ...latestSchema ? { latestVersion: latestSchema.version } : {},
1413
+ serializeForPersist: encodeForWrite
1414
+ });
1415
+ }
1416
+ try {
1417
+ const decoded2 = decodeStringPayload(envelope.payload, codec);
1418
+ return applyReconcile({
1419
+ value: decoded2,
1420
+ persistedVersion: envelope.version,
1421
+ ...latestSchema ? { latestVersion: latestSchema.version } : {},
1422
+ serializeForPersist: encodeForWrite
1423
+ });
1424
+ } catch (err) {
1425
+ return buildFallbackResult(err);
1043
1426
  }
1427
+ },
1428
+ [applyReconcile, buildFallbackResult, codec, decodeStringPayload, encodeForWrite]
1429
+ );
1430
+ const decodeSchemaManagedEnvelope = useCallback(
1431
+ (envelope, schemaForVersion, latestSchema) => {
1044
1432
  let current;
1045
1433
  try {
1046
1434
  current = envelope.payload;
1047
1435
  validateAgainstSchema(current, schemaForVersion.schema);
1048
1436
  } catch (err) {
1049
1437
  const typedErr = err instanceof SchemaError || err instanceof CodecError ? err : new SchemaError("TYPE_MISMATCH", `Schema decode failed for key "${key}"`, err);
1050
- return { value: getFallback(typedErr) };
1438
+ return buildFallbackResult(typedErr);
1051
1439
  }
1052
1440
  if (!latestSchema || envelope.version >= latestSchema.version) {
1053
1441
  return applyReconcile({
@@ -1059,14 +1447,12 @@ function useMnemonicKey(key, options) {
1059
1447
  }
1060
1448
  const path = getMigrationPathForKey(envelope.version, latestSchema.version);
1061
1449
  if (!path) {
1062
- return {
1063
- value: getFallback(
1064
- new SchemaError(
1065
- "MIGRATION_PATH_NOT_FOUND",
1066
- `No migration path for key "${key}" from v${envelope.version} to v${latestSchema.version}`
1067
- )
1450
+ return buildFallbackResult(
1451
+ new SchemaError(
1452
+ "MIGRATION_PATH_NOT_FOUND",
1453
+ `No migration path for key "${key}" from v${envelope.version} to v${latestSchema.version}`
1068
1454
  )
1069
- };
1455
+ );
1070
1456
  }
1071
1457
  try {
1072
1458
  let migrated = current;
@@ -1076,43 +1462,140 @@ function useMnemonicKey(key, options) {
1076
1462
  validateAgainstSchema(migrated, latestSchema.schema);
1077
1463
  return applyReconcile({
1078
1464
  value: migrated,
1079
- rewriteRaw: JSON.stringify({
1080
- version: latestSchema.version,
1081
- payload: migrated
1082
- }),
1465
+ rewriteRaw: buildSchemaManagedResult(latestSchema.version, migrated),
1083
1466
  persistedVersion: envelope.version,
1084
1467
  latestVersion: latestSchema.version,
1085
1468
  serializeForPersist: encodeForWrite
1086
1469
  });
1087
1470
  } catch (err) {
1088
1471
  const typedErr = err instanceof SchemaError || err instanceof CodecError ? err : new SchemaError("MIGRATION_FAILED", `Migration failed for key "${key}"`, err);
1089
- return { value: getFallback(typedErr) };
1472
+ return buildFallbackResult(typedErr);
1090
1473
  }
1091
1474
  },
1092
1475
  [
1093
1476
  applyReconcile,
1094
- codec,
1095
- decodeStringPayload,
1477
+ buildFallbackResult,
1478
+ buildSchemaManagedResult,
1096
1479
  encodeForWrite,
1097
- getFallback,
1480
+ getMigrationPathForKey,
1098
1481
  key,
1482
+ validateAgainstSchema
1483
+ ]
1484
+ );
1485
+ const decodeForRead = useCallback(
1486
+ (rawText) => {
1487
+ if (rawText == null) return buildFallbackResult();
1488
+ const parsed = parseEnvelope(rawText);
1489
+ if (!parsed.ok) return buildFallbackResult(parsed.error);
1490
+ const envelope = parsed.envelope;
1491
+ const schemaForVersion = getSchemaForVersion(envelope.version);
1492
+ const latestSchema = getLatestSchemaForKey();
1493
+ if (schemaMode === "strict" && !schemaForVersion) {
1494
+ return buildFallbackResult(
1495
+ new SchemaError("SCHEMA_NOT_FOUND", `No schema for key "${key}" v${envelope.version}`)
1496
+ );
1497
+ }
1498
+ if (schemaMode === "autoschema" && !schemaForVersion) {
1499
+ return decodeAutoschemaEnvelope(envelope, latestSchema);
1500
+ }
1501
+ if (!schemaForVersion) {
1502
+ return decodeCodecManagedEnvelope(envelope, latestSchema);
1503
+ }
1504
+ return decodeSchemaManagedEnvelope(envelope, schemaForVersion, latestSchema);
1505
+ },
1506
+ [
1507
+ buildFallbackResult,
1508
+ decodeAutoschemaEnvelope,
1509
+ decodeCodecManagedEnvelope,
1510
+ decodeSchemaManagedEnvelope,
1099
1511
  parseEnvelope,
1100
1512
  schemaMode,
1101
- schemaRegistry,
1102
1513
  getSchemaForVersion,
1103
- getLatestSchemaForKey,
1104
- getMigrationPathForKey,
1105
- validateAgainstSchema
1514
+ getLatestSchemaForKey
1106
1515
  ]
1107
1516
  );
1517
+ const getServerRawSnapshot = useCallback(
1518
+ () => ssrOptions?.serverValue === void 0 ? null : SSR_SNAPSHOT_TOKEN,
1519
+ [ssrOptions?.serverValue]
1520
+ );
1521
+ const deferStorageRead = hydrationMode === "client-only" && !hasMounted;
1522
+ const subscribe = useCallback(
1523
+ (listener) => {
1524
+ if (deferStorageRead) {
1525
+ return () => void 0;
1526
+ }
1527
+ return api.subscribeRaw(key, listener);
1528
+ },
1529
+ [api, deferStorageRead, key]
1530
+ );
1108
1531
  const raw = useSyncExternalStore(
1109
- (listener) => api.subscribeRaw(key, listener),
1110
- () => api.getRawSnapshot(key),
1111
- () => null
1112
- // SSR snapshot - no storage in server environment
1532
+ subscribe,
1533
+ () => deferStorageRead ? getServerRawSnapshot() : api.getRawSnapshot(key),
1534
+ getServerRawSnapshot
1113
1535
  );
1114
- const decoded = useMemo(() => decodeForRead(raw), [decodeForRead, raw]);
1536
+ const decoded = useMemo(() => {
1537
+ if (raw === SSR_SNAPSHOT_TOKEN) {
1538
+ return {
1539
+ value: getServerValue(),
1540
+ rewriteRaw: void 0,
1541
+ pendingSchema: void 0
1542
+ };
1543
+ }
1544
+ return decodeForRead(raw);
1545
+ }, [decodeForRead, getServerValue, raw]);
1115
1546
  const value = decoded.value;
1547
+ useEffect(() => {
1548
+ if (!developmentRuntime) return;
1549
+ if (listenCrossTab && (api.crossTabSyncMode ?? "none") === "none" && globalThis.window !== void 0) {
1550
+ warnOnce(
1551
+ api,
1552
+ `listenCrossTab:${key}`,
1553
+ `[Mnemonic] useMnemonicKey("${key}") enabled listenCrossTab, but the active storage backend may not be able to notify external changes. If you're using a custom Storage-like wrapper around localStorage, ensure it forwards browser "storage" events or implements storage.onExternalChange(...); otherwise, use localStorage or implement storage.onExternalChange(...) on your custom backend.`
1554
+ );
1555
+ }
1556
+ if (codecOpt && schema?.version !== void 0 && api.schemaRegistry) {
1557
+ warnOnce(
1558
+ api,
1559
+ `codec+schema:${key}`,
1560
+ `[Mnemonic] useMnemonicKey("${key}") received both a custom codec and schema.version. Schema-managed reads/writes do not use the codec path. Remove the codec for schema-managed storage, or remove schema.version if you intended codec-only persistence.`
1561
+ );
1562
+ }
1563
+ let keyContracts = diagnosticContractRegistry.get(api);
1564
+ if (!keyContracts) {
1565
+ keyContracts = /* @__PURE__ */ new Map();
1566
+ diagnosticContractRegistry.set(api, keyContracts);
1567
+ }
1568
+ if (contractFingerprint === null) {
1569
+ return;
1570
+ }
1571
+ const previousContract = keyContracts.get(key);
1572
+ if (previousContract === void 0) {
1573
+ keyContracts.set(key, contractFingerprint);
1574
+ return;
1575
+ }
1576
+ if (previousContract === contractFingerprint) {
1577
+ return;
1578
+ }
1579
+ warnOnce(
1580
+ api,
1581
+ `contract-conflict:${key}`,
1582
+ `[Mnemonic] Conflicting useMnemonicKey contracts detected for key "${key}" in namespace "${api.prefix.slice(0, -1)}". Reuse a shared descriptor with defineMnemonicKey(...) or align defaultValue/codec/schema/reconcile options so every consumer describes the same persisted contract.`
1583
+ );
1584
+ }, [
1585
+ api,
1586
+ key,
1587
+ developmentRuntime,
1588
+ contractFingerprint,
1589
+ listenCrossTab,
1590
+ codecOpt,
1591
+ schema?.version,
1592
+ api.schemaRegistry,
1593
+ api.crossTabSyncMode
1594
+ ]);
1595
+ useEffect(() => {
1596
+ if (hasMounted) return;
1597
+ setHasMounted(true);
1598
+ }, [hasMounted]);
1116
1599
  useEffect(() => {
1117
1600
  if (decoded.rewriteRaw && decoded.rewriteRaw !== raw) {
1118
1601
  api.setRaw(key, decoded.rewriteRaw);
@@ -1142,7 +1625,8 @@ function useMnemonicKey(key, options) {
1142
1625
  }, [value, onChange]);
1143
1626
  useEffect(() => {
1144
1627
  if (!listenCrossTab) return;
1145
- if (typeof window === "undefined") return;
1628
+ const globalWindow = globalThis.window;
1629
+ if (globalWindow === void 0) return;
1146
1630
  const storageKey = api.prefix + key;
1147
1631
  const handler = (e) => {
1148
1632
  if (e.key === null) {
@@ -1156,8 +1640,8 @@ function useMnemonicKey(key, options) {
1156
1640
  }
1157
1641
  api.setRaw(key, e.newValue);
1158
1642
  };
1159
- window.addEventListener("storage", handler);
1160
- return () => window.removeEventListener("storage", handler);
1643
+ globalWindow.addEventListener("storage", handler);
1644
+ return () => globalWindow.removeEventListener("storage", handler);
1161
1645
  }, [listenCrossTab, api, key]);
1162
1646
  const set = useMemo(() => {
1163
1647
  return (next) => {
@@ -1219,6 +1703,20 @@ function useMnemonicKey(key, options) {
1219
1703
  function uniqueKeys(keys) {
1220
1704
  return [...new Set(keys)];
1221
1705
  }
1706
+ function isDevelopmentRuntime2() {
1707
+ return getRuntimeNodeEnv() === "development";
1708
+ }
1709
+ var recoveryDiagnosticWarnings = /* @__PURE__ */ new WeakMap();
1710
+ function warnRecoveryOnce(api, id, message) {
1711
+ let warnings = recoveryDiagnosticWarnings.get(api);
1712
+ if (!warnings) {
1713
+ warnings = /* @__PURE__ */ new Set();
1714
+ recoveryDiagnosticWarnings.set(api, warnings);
1715
+ }
1716
+ if (warnings.has(id)) return;
1717
+ warnings.add(id);
1718
+ console.warn(message);
1719
+ }
1222
1720
  function useMnemonicRecovery(options = {}) {
1223
1721
  const api = useMnemonic();
1224
1722
  const { onRecover } = options;
@@ -1252,15 +1750,29 @@ function useMnemonicRecovery(options = {}) {
1252
1750
  );
1253
1751
  const clearAll = useCallback(() => {
1254
1752
  if (!api.canEnumerateKeys) {
1753
+ if (isDevelopmentRuntime2()) {
1754
+ warnRecoveryOnce(
1755
+ api,
1756
+ "recovery-clear-all-non-enumerable",
1757
+ `[Mnemonic] clearAll() requires an enumerable storage backend in namespace "${namespace}". Use clearKeys([...]) with an explicit durable-key list, or supply a storage backend that implements length and key(index).`
1758
+ );
1759
+ }
1255
1760
  throw new Error(
1256
1761
  "clearAll requires an enumerable storage backend. Use clearKeys([...]) with an explicit key list instead."
1257
1762
  );
1258
1763
  }
1259
1764
  return clearResolvedKeys("clear-all", api.keys());
1260
- }, [api, clearResolvedKeys]);
1765
+ }, [api, clearResolvedKeys, namespace]);
1261
1766
  const clearMatching = useCallback(
1262
1767
  (predicate) => {
1263
1768
  if (!api.canEnumerateKeys) {
1769
+ if (isDevelopmentRuntime2()) {
1770
+ warnRecoveryOnce(
1771
+ api,
1772
+ "recovery-clear-matching-non-enumerable",
1773
+ `[Mnemonic] clearMatching() requires an enumerable storage backend in namespace "${namespace}". Use clearKeys([...]) with an explicit durable-key list, or supply a storage backend that implements length and key(index).`
1774
+ );
1775
+ }
1264
1776
  throw new Error(
1265
1777
  "clearMatching requires an enumerable storage backend. Use clearKeys([...]) with an explicit key list instead."
1266
1778
  );
@@ -1270,7 +1782,7 @@ function useMnemonicRecovery(options = {}) {
1270
1782
  api.keys().filter((key) => predicate(key))
1271
1783
  );
1272
1784
  },
1273
- [api, clearResolvedKeys]
1785
+ [api, clearResolvedKeys, namespace]
1274
1786
  );
1275
1787
  return useMemo(
1276
1788
  () => ({
@@ -1285,6 +1797,23 @@ function useMnemonicRecovery(options = {}) {
1285
1797
  );
1286
1798
  }
1287
1799
 
1800
+ // src/Mnemonic/key.ts
1801
+ function defineMnemonicKey(keyOrSchema, options) {
1802
+ if (typeof keyOrSchema !== "string") {
1803
+ return Object.freeze({
1804
+ key: keyOrSchema.key,
1805
+ options: {
1806
+ ...options,
1807
+ schema: { version: keyOrSchema.version }
1808
+ }
1809
+ });
1810
+ }
1811
+ return Object.freeze({
1812
+ key: keyOrSchema,
1813
+ options
1814
+ });
1815
+ }
1816
+
1288
1817
  // src/Mnemonic/schema-registry.ts
1289
1818
  function schemaVersionKey(key, version) {
1290
1819
  return `${key}:${version}`;
@@ -1374,6 +1903,184 @@ function createSchemaRegistry(options = {}) {
1374
1903
  };
1375
1904
  }
1376
1905
 
1906
+ // src/Mnemonic/schema-helpers.ts
1907
+ function defineKeySchema(key, version, schema) {
1908
+ return Object.freeze({
1909
+ key,
1910
+ version,
1911
+ schema
1912
+ });
1913
+ }
1914
+ function defineMigration(fromSchema, toSchema, migrate) {
1915
+ if (fromSchema.key !== toSchema.key) {
1916
+ throw new SchemaError(
1917
+ "MIGRATION_GRAPH_INVALID",
1918
+ `Migration schemas must target the same key: "${fromSchema.key}" !== "${toSchema.key}"`
1919
+ );
1920
+ }
1921
+ return Object.freeze({
1922
+ key: fromSchema.key,
1923
+ fromVersion: fromSchema.version,
1924
+ toVersion: toSchema.version,
1925
+ migrate
1926
+ });
1927
+ }
1928
+ function defineWriteMigration(schema, migrate) {
1929
+ return Object.freeze({
1930
+ key: schema.key,
1931
+ fromVersion: schema.version,
1932
+ toVersion: schema.version,
1933
+ migrate
1934
+ });
1935
+ }
1936
+
1937
+ // src/Mnemonic/typed-schema.ts
1938
+ var optionalSchemaMarker = /* @__PURE__ */ Symbol("mnemonicOptionalSchema");
1939
+ function cloneSchema(schema) {
1940
+ const clone = { ...schema };
1941
+ if (schema[optionalSchemaMarker]) {
1942
+ Object.defineProperty(clone, optionalSchemaMarker, {
1943
+ value: true,
1944
+ enumerable: false,
1945
+ configurable: false
1946
+ });
1947
+ }
1948
+ return clone;
1949
+ }
1950
+ function isOptionalSchema(schema) {
1951
+ return Boolean(schema[optionalSchemaMarker]);
1952
+ }
1953
+ function markOptional(schema) {
1954
+ const clone = cloneSchema(schema);
1955
+ Object.defineProperty(clone, optionalSchemaMarker, {
1956
+ value: true,
1957
+ enumerable: false,
1958
+ configurable: false
1959
+ });
1960
+ return clone;
1961
+ }
1962
+ function withoutOptionalMarker(schema) {
1963
+ const clone = { ...schema };
1964
+ delete clone[optionalSchemaMarker];
1965
+ return clone;
1966
+ }
1967
+ function withType(type, extra = {}) {
1968
+ return {
1969
+ type,
1970
+ ...extra
1971
+ };
1972
+ }
1973
+ function toTypeArray(type) {
1974
+ if (type === void 0) return null;
1975
+ return Array.isArray(type) ? [...type] : [type];
1976
+ }
1977
+ function appendNullType(type) {
1978
+ const types = toTypeArray(type);
1979
+ if (types === null) {
1980
+ return void 0;
1981
+ }
1982
+ return types.includes("null") ? types : [...types, "null"];
1983
+ }
1984
+ function nullableSchema(schema) {
1985
+ if (schema.enum) {
1986
+ const nullableType = appendNullType(schema.type);
1987
+ return {
1988
+ ...schema,
1989
+ ...nullableType ? { type: nullableType } : {},
1990
+ enum: schema.enum.includes(null) ? schema.enum : [...schema.enum, null]
1991
+ };
1992
+ }
1993
+ if ("const" in schema) {
1994
+ const { const: constValue, ...rest } = schema;
1995
+ const nullableType = appendNullType(rest.type);
1996
+ if (constValue === null || constValue === void 0) {
1997
+ return {
1998
+ ...schema,
1999
+ ...nullableType ? { type: nullableType } : {}
2000
+ };
2001
+ }
2002
+ return {
2003
+ ...rest,
2004
+ ...nullableType ? { type: nullableType } : {},
2005
+ enum: [constValue, null]
2006
+ };
2007
+ }
2008
+ const types = toTypeArray(schema.type);
2009
+ if (types === null) {
2010
+ throw new SchemaError(
2011
+ "MODE_CONFIGURATION_INVALID",
2012
+ "mnemonicSchema.nullable(...) requires a schema with type, enum, or const"
2013
+ );
2014
+ }
2015
+ return {
2016
+ ...schema,
2017
+ type: types.includes("null") ? types : [...types, "null"]
2018
+ };
2019
+ }
2020
+ var mnemonicSchema = {
2021
+ string(options = {}) {
2022
+ return withType("string", options);
2023
+ },
2024
+ number(options = {}) {
2025
+ return withType("number", options);
2026
+ },
2027
+ integer(options = {}) {
2028
+ return withType("integer", options);
2029
+ },
2030
+ boolean() {
2031
+ return withType("boolean");
2032
+ },
2033
+ nullValue() {
2034
+ return withType("null");
2035
+ },
2036
+ literal(value) {
2037
+ return {
2038
+ const: value
2039
+ };
2040
+ },
2041
+ enum(values) {
2042
+ return {
2043
+ enum: values
2044
+ };
2045
+ },
2046
+ optional(schema) {
2047
+ return markOptional(schema);
2048
+ },
2049
+ nullable(schema) {
2050
+ return nullableSchema(schema);
2051
+ },
2052
+ array(itemSchema, options = {}) {
2053
+ return withType("array", {
2054
+ items: withoutOptionalMarker(itemSchema),
2055
+ ...options
2056
+ });
2057
+ },
2058
+ object(shape, options = {}) {
2059
+ const properties = {};
2060
+ const required = [];
2061
+ for (const [name, schema] of Object.entries(shape)) {
2062
+ properties[name] = withoutOptionalMarker(schema);
2063
+ if (!isOptionalSchema(schema)) {
2064
+ required.push(name);
2065
+ }
2066
+ }
2067
+ const result = {
2068
+ type: "object",
2069
+ properties,
2070
+ ...options
2071
+ };
2072
+ if (required.length > 0) {
2073
+ result.required = required;
2074
+ }
2075
+ return result;
2076
+ },
2077
+ record(valueSchema) {
2078
+ return withType("object", {
2079
+ additionalProperties: withoutOptionalMarker(valueSchema)
2080
+ });
2081
+ }
2082
+ };
2083
+
1377
2084
  // src/Mnemonic/structural-migrations.ts
1378
2085
  function resolveHelpers(helpers) {
1379
2086
  if (helpers) return helpers;
@@ -1465,6 +2172,6 @@ function dedupeChildrenBy(root, getKey, helpers) {
1465
2172
  return visit(root);
1466
2173
  }
1467
2174
 
1468
- export { CodecError, JSONCodec, MnemonicProvider, SchemaError, compileSchema, createCodec, createSchemaRegistry, dedupeChildrenBy, findNodeById, insertChildIfMissing, renameNode, useMnemonicKey, useMnemonicRecovery, validateJsonSchema };
2175
+ export { CodecError, JSONCodec, MnemonicProvider, SchemaError, compileSchema, createCodec, createSchemaRegistry, dedupeChildrenBy, defineKeySchema, defineMigration, defineMnemonicKey, defineWriteMigration, findNodeById, insertChildIfMissing, mnemonicSchema, renameNode, useMnemonicKey, useMnemonicRecovery, validateJsonSchema };
1469
2176
  //# sourceMappingURL=index.js.map
1470
2177
  //# sourceMappingURL=index.js.map