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