react-mnemonic 0.1.1-alpha.0 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -41,6 +41,121 @@ function MnemonicProvider({
41
41
  const listeners = /* @__PURE__ */ new Map();
42
42
  let quotaErrorLogged = false;
43
43
  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}`;
158
+ };
44
159
  const fullKey = (key) => prefix + key;
45
160
  const emit = (key) => {
46
161
  const set = listeners.get(key);
@@ -90,6 +205,7 @@ function MnemonicProvider({
90
205
  }
91
206
  }
92
207
  emit(key);
208
+ bumpDevToolsVersion(`set:${key}`);
93
209
  };
94
210
  const removeRaw = (key) => {
95
211
  cache.set(key, null);
@@ -102,6 +218,7 @@ function MnemonicProvider({
102
218
  }
103
219
  }
104
220
  emit(key);
221
+ bumpDevToolsVersion(`remove:${key}`);
105
222
  };
106
223
  const subscribeRaw = (key, listener) => {
107
224
  let set = listeners.get(key);
@@ -120,11 +237,14 @@ function MnemonicProvider({
120
237
  };
121
238
  const getRawSnapshot = (key) => readThrough(key);
122
239
  const keys = () => {
123
- if (!st || typeof st.length !== "number" || typeof st.key !== "function") return [];
240
+ if (!canEnumerateKeys || !st) return [];
124
241
  const out = [];
125
242
  try {
126
- for (let i = 0; i < st.length; i++) {
127
- const k = st.key(i);
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);
128
248
  if (!k) continue;
129
249
  if (k.startsWith(prefix)) out.push(k.slice(prefix.length));
130
250
  }
@@ -144,6 +264,7 @@ function MnemonicProvider({
144
264
  };
145
265
  const reloadFromStorage = (changedKeys) => {
146
266
  if (!st) return;
267
+ let changed = false;
147
268
  if (changedKeys !== void 0 && changedKeys.length === 0) return;
148
269
  if (changedKeys !== void 0) {
149
270
  for (const fk of changedKeys) {
@@ -163,11 +284,15 @@ function MnemonicProvider({
163
284
  if (fresh !== cached) {
164
285
  cache.set(key, fresh);
165
286
  emit(key);
287
+ changed = true;
166
288
  }
167
289
  } else if (cache.has(key)) {
168
290
  cache.delete(key);
169
291
  }
170
292
  }
293
+ if (changed) {
294
+ bumpDevToolsVersion("reload:granular");
295
+ }
171
296
  return;
172
297
  }
173
298
  for (const [key, listenerSet] of listeners) {
@@ -184,6 +309,7 @@ function MnemonicProvider({
184
309
  if (fresh !== cached) {
185
310
  cache.set(key, fresh);
186
311
  emit(key);
312
+ changed = true;
187
313
  }
188
314
  }
189
315
  for (const key of cache.keys()) {
@@ -191,9 +317,13 @@ function MnemonicProvider({
191
317
  cache.delete(key);
192
318
  }
193
319
  }
320
+ if (changed) {
321
+ bumpDevToolsVersion("reload:full");
322
+ }
194
323
  };
195
324
  const store2 = {
196
325
  prefix,
326
+ canEnumerateKeys,
197
327
  subscribeRaw,
198
328
  getRawSnapshot,
199
329
  setRaw: writeRaw,
@@ -205,56 +335,86 @@ function MnemonicProvider({
205
335
  ...schemaRegistry ? { schemaRegistry } : {}
206
336
  };
207
337
  if (enableDevTools && typeof window !== "undefined") {
208
- window.__REACT_MNEMONIC_DEVTOOLS__ = window.__REACT_MNEMONIC_DEVTOOLS__ || {};
209
- window.__REACT_MNEMONIC_DEVTOOLS__[namespace] = {
210
- /** Access the underlying store instance */
211
- getStore: () => store2,
212
- /** Dump all key-value pairs and display as a console table */
213
- dump: () => {
214
- const data = dump();
215
- console.table(
216
- Object.entries(data).map(([key, value]) => ({
217
- key,
218
- value,
219
- decoded: (() => {
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;
220
378
  try {
221
- return JSON.parse(value);
379
+ return JSON.parse(raw);
222
380
  } catch {
223
- return value;
381
+ return raw;
224
382
  }
225
- })()
226
- }))
227
- );
228
- return data;
229
- },
230
- /** Get a decoded value by key */
231
- get: (key) => {
232
- const raw = readThrough(key);
233
- if (raw == null) return void 0;
234
- try {
235
- return JSON.parse(raw);
236
- } catch {
237
- return raw;
238
- }
239
- },
240
- /** Set a value by key (automatically JSON-encoded) */
241
- set: (key, value) => {
242
- writeRaw(key, JSON.stringify(value));
243
- },
244
- /** Remove a key from storage */
245
- remove: (key) => removeRaw(key),
246
- /** Clear all keys in this namespace */
247
- clear: () => {
248
- for (const k of keys()) {
249
- removeRaw(k);
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
+ }
250
414
  }
251
- },
252
- /** List all keys in this namespace */
253
- keys
254
- };
255
- console.info(
256
- `[Mnemonic DevTools] Namespace "${namespace}" available at window.__REACT_MNEMONIC_DEVTOOLS__.${namespace}`
257
- );
415
+ }
416
+ }
417
+ console.info(infoMessage);
258
418
  }
259
419
  return store2;
260
420
  }, [namespace, storage, enableDevTools, schemaMode, schemaRegistry]);
@@ -596,7 +756,7 @@ function inferJsonSchema(sample) {
596
756
  // src/Mnemonic/use.ts
597
757
  function useMnemonicKey(key, options) {
598
758
  const api = useMnemonic();
599
- const { defaultValue, onMount, onChange, listenCrossTab, codec: codecOpt, schema } = options;
759
+ const { defaultValue, onMount, onChange, listenCrossTab, codec: codecOpt, schema, reconcile } = options;
600
760
  const codec = codecOpt ?? JSONCodec;
601
761
  const schemaMode = api.schemaMode;
602
762
  const schemaRegistry = api.schemaRegistry;
@@ -695,6 +855,112 @@ function useMnemonicKey(key, options) {
695
855
  },
696
856
  [schemaRegistry, registryCache, key]
697
857
  );
858
+ const encodeForWrite = react.useCallback(
859
+ (nextValue) => {
860
+ 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
+ }
873
+ if (!targetSchema) {
874
+ if (explicitVersion !== void 0 && schemaMode === "strict") {
875
+ throw new SchemaError(
876
+ "WRITE_SCHEMA_REQUIRED",
877
+ `Write requires schema for key "${key}" in strict mode`
878
+ );
879
+ }
880
+ const envelope2 = {
881
+ version: 0,
882
+ payload: codec.encode(nextValue)
883
+ };
884
+ return JSON.stringify(envelope2);
885
+ }
886
+ let valueToStore = nextValue;
887
+ const writeMigration = schemaRegistry?.getWriteMigration?.(key, targetSchema.version);
888
+ if (writeMigration) {
889
+ try {
890
+ valueToStore = writeMigration.migrate(valueToStore);
891
+ } catch (err) {
892
+ throw err instanceof SchemaError ? err : new SchemaError("MIGRATION_FAILED", `Write-time migration failed for key "${key}"`, err);
893
+ }
894
+ }
895
+ validateAgainstSchema(valueToStore, targetSchema.schema);
896
+ const envelope = {
897
+ version: targetSchema.version,
898
+ payload: valueToStore
899
+ };
900
+ return JSON.stringify(envelope);
901
+ },
902
+ [
903
+ schema?.version,
904
+ key,
905
+ schemaMode,
906
+ codec,
907
+ schemaRegistry,
908
+ validateAgainstSchema,
909
+ getLatestSchemaForKey,
910
+ getSchemaForVersion
911
+ ]
912
+ );
913
+ const applyReconcile = react.useCallback(
914
+ ({
915
+ value: value2,
916
+ rewriteRaw,
917
+ pendingSchema,
918
+ persistedVersion,
919
+ latestVersion,
920
+ serializeForPersist,
921
+ derivePendingSchema
922
+ }) => {
923
+ 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;
928
+ }
929
+ const context = {
930
+ key,
931
+ persistedVersion,
932
+ ...latestVersion === void 0 ? {} : { latestVersion }
933
+ };
934
+ let baselineSerialized;
935
+ if (serializeForPersist) {
936
+ try {
937
+ baselineSerialized = serializeForPersist(value2);
938
+ } catch {
939
+ baselineSerialized = rewriteRaw;
940
+ }
941
+ }
942
+ try {
943
+ const reconciled = reconcile(value2, context);
944
+ 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
+ const nextSerialized = serializeForPersist(reconciled);
952
+ 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;
957
+ } catch (err) {
958
+ const typedErr = err instanceof SchemaError ? err : new SchemaError("RECONCILE_FAILED", `Reconciliation failed for key "${key}"`, err);
959
+ return { value: getFallback(typedErr) };
960
+ }
961
+ },
962
+ [getFallback, key, reconcile]
963
+ );
698
964
  const decodeForRead = react.useCallback(
699
965
  (rawText) => {
700
966
  if (rawText == null) return { value: getFallback() };
@@ -730,21 +996,26 @@ function useMnemonicKey(key, options) {
730
996
  }
731
997
  try {
732
998
  const decoded2 = typeof envelope.payload === "string" ? decodeStringPayload(envelope.payload, codec) : envelope.payload;
733
- const inferredJsonSchema = inferJsonSchema(decoded2);
734
- const inferred = {
999
+ const inferSchemaForValue = (value2) => ({
735
1000
  key,
736
1001
  version: 1,
737
- schema: inferredJsonSchema
738
- };
739
- const rewriteEnvelope = {
740
- version: inferred.version,
741
- payload: decoded2
742
- };
743
- return {
1002
+ schema: inferJsonSchema(value2)
1003
+ });
1004
+ const inferred = inferSchemaForValue(decoded2);
1005
+ return applyReconcile({
744
1006
  value: decoded2,
745
1007
  pendingSchema: inferred,
746
- rewriteRaw: JSON.stringify(rewriteEnvelope)
747
- };
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
+ });
748
1019
  } catch (err) {
749
1020
  const typedErr = err instanceof SchemaError || err instanceof CodecError ? err : new SchemaError("TYPE_MISMATCH", `Autoschema inference failed for key "${key}"`, err);
750
1021
  return { value: getFallback(typedErr) };
@@ -752,11 +1023,21 @@ function useMnemonicKey(key, options) {
752
1023
  }
753
1024
  if (!schemaForVersion) {
754
1025
  if (typeof envelope.payload !== "string") {
755
- return { value: envelope.payload };
1026
+ return applyReconcile({
1027
+ value: envelope.payload,
1028
+ persistedVersion: envelope.version,
1029
+ ...latestSchema ? { latestVersion: latestSchema.version } : {},
1030
+ serializeForPersist: encodeForWrite
1031
+ });
756
1032
  }
757
1033
  try {
758
1034
  const decoded2 = decodeStringPayload(envelope.payload, codec);
759
- return { value: decoded2 };
1035
+ return applyReconcile({
1036
+ value: decoded2,
1037
+ persistedVersion: envelope.version,
1038
+ ...latestSchema ? { latestVersion: latestSchema.version } : {},
1039
+ serializeForPersist: encodeForWrite
1040
+ });
760
1041
  } catch (err) {
761
1042
  const typedErr = err instanceof SchemaError || err instanceof CodecError ? err : new CodecError(`Codec decode failed for key "${key}"`, err);
762
1043
  return { value: getFallback(typedErr) };
@@ -771,7 +1052,12 @@ function useMnemonicKey(key, options) {
771
1052
  return { value: getFallback(typedErr) };
772
1053
  }
773
1054
  if (!latestSchema || envelope.version >= latestSchema.version) {
774
- return { value: current };
1055
+ return applyReconcile({
1056
+ value: current,
1057
+ persistedVersion: envelope.version,
1058
+ ...latestSchema ? { latestVersion: latestSchema.version } : {},
1059
+ serializeForPersist: encodeForWrite
1060
+ });
775
1061
  }
776
1062
  const path = getMigrationPathForKey(envelope.version, latestSchema.version);
777
1063
  if (!path) {
@@ -790,22 +1076,26 @@ function useMnemonicKey(key, options) {
790
1076
  migrated = step.migrate(migrated);
791
1077
  }
792
1078
  validateAgainstSchema(migrated, latestSchema.schema);
793
- const rewriteEnvelope = {
794
- version: latestSchema.version,
795
- payload: migrated
796
- };
797
- return {
1079
+ return applyReconcile({
798
1080
  value: migrated,
799
- rewriteRaw: JSON.stringify(rewriteEnvelope)
800
- };
1081
+ rewriteRaw: JSON.stringify({
1082
+ version: latestSchema.version,
1083
+ payload: migrated
1084
+ }),
1085
+ persistedVersion: envelope.version,
1086
+ latestVersion: latestSchema.version,
1087
+ serializeForPersist: encodeForWrite
1088
+ });
801
1089
  } catch (err) {
802
1090
  const typedErr = err instanceof SchemaError || err instanceof CodecError ? err : new SchemaError("MIGRATION_FAILED", `Migration failed for key "${key}"`, err);
803
1091
  return { value: getFallback(typedErr) };
804
1092
  }
805
1093
  },
806
1094
  [
1095
+ applyReconcile,
807
1096
  codec,
808
1097
  decodeStringPayload,
1098
+ encodeForWrite,
809
1099
  getFallback,
810
1100
  key,
811
1101
  parseEnvelope,
@@ -817,61 +1107,6 @@ function useMnemonicKey(key, options) {
817
1107
  validateAgainstSchema
818
1108
  ]
819
1109
  );
820
- const encodeForWrite = react.useCallback(
821
- (nextValue) => {
822
- const explicitVersion = schema?.version;
823
- const latestSchema = getLatestSchemaForKey();
824
- const explicitSchema = explicitVersion !== void 0 ? getSchemaForVersion(explicitVersion) : void 0;
825
- let targetSchema = explicitSchema;
826
- if (!targetSchema) {
827
- if (explicitVersion !== void 0) {
828
- if (schemaMode !== "strict") {
829
- targetSchema = latestSchema;
830
- }
831
- } else {
832
- targetSchema = latestSchema;
833
- }
834
- }
835
- if (!targetSchema) {
836
- if (explicitVersion !== void 0 && schemaMode === "strict") {
837
- throw new SchemaError(
838
- "WRITE_SCHEMA_REQUIRED",
839
- `Write requires schema for key "${key}" in strict mode`
840
- );
841
- }
842
- const envelope2 = {
843
- version: 0,
844
- payload: codec.encode(nextValue)
845
- };
846
- return JSON.stringify(envelope2);
847
- }
848
- let valueToStore = nextValue;
849
- const writeMigration = schemaRegistry?.getWriteMigration?.(key, targetSchema.version);
850
- if (writeMigration) {
851
- try {
852
- valueToStore = writeMigration.migrate(valueToStore);
853
- } catch (err) {
854
- throw err instanceof SchemaError ? err : new SchemaError("MIGRATION_FAILED", `Write-time migration failed for key "${key}"`, err);
855
- }
856
- }
857
- validateAgainstSchema(valueToStore, targetSchema.schema);
858
- const envelope = {
859
- version: targetSchema.version,
860
- payload: valueToStore
861
- };
862
- return JSON.stringify(envelope);
863
- },
864
- [
865
- schema?.version,
866
- key,
867
- schemaMode,
868
- codec,
869
- schemaRegistry,
870
- validateAgainstSchema,
871
- getLatestSchemaForKey,
872
- getSchemaForVersion
873
- ]
874
- );
875
1110
  const raw = react.useSyncExternalStore(
876
1111
  (listener) => api.subscribeRaw(key, listener),
877
1112
  () => api.getRawSnapshot(key),
@@ -983,6 +1218,254 @@ function useMnemonicKey(key, options) {
983
1218
  [value, set, reset, remove]
984
1219
  );
985
1220
  }
1221
+ function uniqueKeys(keys) {
1222
+ return [...new Set(keys)];
1223
+ }
1224
+ function useMnemonicRecovery(options = {}) {
1225
+ const api = useMnemonic();
1226
+ const { onRecover } = options;
1227
+ const namespace = react.useMemo(() => api.prefix.endsWith(".") ? api.prefix.slice(0, -1) : api.prefix, [api.prefix]);
1228
+ const emitRecovery = react.useCallback(
1229
+ (action, clearedKeys) => {
1230
+ const event = {
1231
+ action,
1232
+ namespace,
1233
+ clearedKeys
1234
+ };
1235
+ onRecover?.(event);
1236
+ },
1237
+ [namespace, onRecover]
1238
+ );
1239
+ const listKeys = react.useCallback(() => api.keys(), [api]);
1240
+ const clearResolvedKeys = react.useCallback(
1241
+ (action, keys) => {
1242
+ const clearedKeys = uniqueKeys(keys);
1243
+ for (const key of clearedKeys) {
1244
+ api.removeRaw(key);
1245
+ }
1246
+ emitRecovery(action, clearedKeys);
1247
+ return clearedKeys;
1248
+ },
1249
+ [api, emitRecovery]
1250
+ );
1251
+ const clearKeys = react.useCallback(
1252
+ (keys) => clearResolvedKeys("clear-keys", keys),
1253
+ [clearResolvedKeys]
1254
+ );
1255
+ const clearAll = react.useCallback(() => {
1256
+ if (!api.canEnumerateKeys) {
1257
+ throw new Error(
1258
+ "clearAll requires an enumerable storage backend. Use clearKeys([...]) with an explicit key list instead."
1259
+ );
1260
+ }
1261
+ return clearResolvedKeys("clear-all", api.keys());
1262
+ }, [api, clearResolvedKeys]);
1263
+ const clearMatching = react.useCallback(
1264
+ (predicate) => {
1265
+ if (!api.canEnumerateKeys) {
1266
+ throw new Error(
1267
+ "clearMatching requires an enumerable storage backend. Use clearKeys([...]) with an explicit key list instead."
1268
+ );
1269
+ }
1270
+ return clearResolvedKeys(
1271
+ "clear-matching",
1272
+ api.keys().filter((key) => predicate(key))
1273
+ );
1274
+ },
1275
+ [api, clearResolvedKeys]
1276
+ );
1277
+ return react.useMemo(
1278
+ () => ({
1279
+ namespace,
1280
+ canEnumerateKeys: api.canEnumerateKeys,
1281
+ listKeys,
1282
+ clearAll,
1283
+ clearKeys,
1284
+ clearMatching
1285
+ }),
1286
+ [namespace, api.canEnumerateKeys, listKeys, clearAll, clearKeys, clearMatching]
1287
+ );
1288
+ }
1289
+
1290
+ // src/Mnemonic/schema-registry.ts
1291
+ function schemaVersionKey(key, version) {
1292
+ return `${key}:${version}`;
1293
+ }
1294
+ function migrationVersionKey(key, fromVersion) {
1295
+ return `${key}:${fromVersion}`;
1296
+ }
1297
+ function validateVersion(value, label) {
1298
+ if (!Number.isInteger(value) || value < 0) {
1299
+ throw new SchemaError("MIGRATION_GRAPH_INVALID", `${label} must be a non-negative integer`);
1300
+ }
1301
+ }
1302
+ function createSchemaRegistry(options = {}) {
1303
+ const { schemas = [], migrations = [] } = options;
1304
+ const schemasByKeyAndVersion = /* @__PURE__ */ new Map();
1305
+ const latestSchemaByKey = /* @__PURE__ */ new Map();
1306
+ const writeMigrationsByKeyAndVersion = /* @__PURE__ */ new Map();
1307
+ const migrationsByKeyAndFromVersion = /* @__PURE__ */ new Map();
1308
+ for (const schema of schemas) {
1309
+ validateVersion(schema.version, `Schema version for key "${schema.key}"`);
1310
+ const id = schemaVersionKey(schema.key, schema.version);
1311
+ if (schemasByKeyAndVersion.has(id)) {
1312
+ throw new SchemaError(
1313
+ "SCHEMA_REGISTRATION_CONFLICT",
1314
+ `Duplicate schema registered for key "${schema.key}" version ${schema.version}`
1315
+ );
1316
+ }
1317
+ schemasByKeyAndVersion.set(id, schema);
1318
+ const currentLatest = latestSchemaByKey.get(schema.key);
1319
+ if (!currentLatest || schema.version > currentLatest.version) {
1320
+ latestSchemaByKey.set(schema.key, schema);
1321
+ }
1322
+ }
1323
+ for (const migration of migrations) {
1324
+ validateVersion(migration.fromVersion, `Migration fromVersion for key "${migration.key}"`);
1325
+ validateVersion(migration.toVersion, `Migration toVersion for key "${migration.key}"`);
1326
+ if (migration.toVersion < migration.fromVersion) {
1327
+ throw new SchemaError(
1328
+ "MIGRATION_GRAPH_INVALID",
1329
+ `Backward migration "${migration.key}" ${migration.fromVersion} -> ${migration.toVersion} is not supported`
1330
+ );
1331
+ }
1332
+ if (migration.fromVersion === migration.toVersion) {
1333
+ const id = schemaVersionKey(migration.key, migration.fromVersion);
1334
+ if (writeMigrationsByKeyAndVersion.has(id)) {
1335
+ throw new SchemaError(
1336
+ "MIGRATION_GRAPH_INVALID",
1337
+ `Duplicate write migration registered for key "${migration.key}" version ${migration.fromVersion}`
1338
+ );
1339
+ }
1340
+ writeMigrationsByKeyAndVersion.set(id, migration);
1341
+ continue;
1342
+ }
1343
+ const edgeKey = migrationVersionKey(migration.key, migration.fromVersion);
1344
+ if (migrationsByKeyAndFromVersion.has(edgeKey)) {
1345
+ const existing = migrationsByKeyAndFromVersion.get(edgeKey);
1346
+ throw new SchemaError(
1347
+ "MIGRATION_GRAPH_INVALID",
1348
+ `Ambiguous migration graph for key "${migration.key}" at version ${migration.fromVersion}: ${existing.fromVersion} -> ${existing.toVersion} conflicts with ${migration.fromVersion} -> ${migration.toVersion}`
1349
+ );
1350
+ }
1351
+ migrationsByKeyAndFromVersion.set(edgeKey, migration);
1352
+ }
1353
+ return {
1354
+ getSchema(key, version) {
1355
+ return schemasByKeyAndVersion.get(schemaVersionKey(key, version));
1356
+ },
1357
+ getLatestSchema(key) {
1358
+ return latestSchemaByKey.get(key);
1359
+ },
1360
+ getMigrationPath(key, fromVersion, toVersion) {
1361
+ if (fromVersion === toVersion) return [];
1362
+ if (toVersion < fromVersion) return null;
1363
+ const path = [];
1364
+ let currentVersion = fromVersion;
1365
+ while (currentVersion < toVersion) {
1366
+ const next = migrationsByKeyAndFromVersion.get(migrationVersionKey(key, currentVersion));
1367
+ if (!next) return null;
1368
+ path.push(next);
1369
+ currentVersion = next.toVersion;
1370
+ }
1371
+ return currentVersion === toVersion ? path : null;
1372
+ },
1373
+ getWriteMigration(key, version) {
1374
+ return writeMigrationsByKeyAndVersion.get(schemaVersionKey(key, version));
1375
+ }
1376
+ };
1377
+ }
1378
+
1379
+ // src/Mnemonic/structural-migrations.ts
1380
+ function resolveHelpers(helpers) {
1381
+ if (helpers) return helpers;
1382
+ return {
1383
+ getId: (node) => node.id,
1384
+ getChildren: (node) => node.children,
1385
+ withChildren: (node, children) => ({ ...node, children }),
1386
+ withId: (node, id) => ({ ...node, id })
1387
+ };
1388
+ }
1389
+ function findNodeById(root, id, helpers) {
1390
+ const tree = resolveHelpers(helpers);
1391
+ if (tree.getId(root) === id) return root;
1392
+ for (const child of tree.getChildren(root) ?? []) {
1393
+ const match = findNodeById(child, id, tree);
1394
+ if (match) return match;
1395
+ }
1396
+ return void 0;
1397
+ }
1398
+ function insertChildIfMissing(root, parentId, child, helpers) {
1399
+ const tree = resolveHelpers(helpers);
1400
+ const childId = tree.getId(child);
1401
+ const visit = (node) => {
1402
+ if (tree.getId(node) === parentId) {
1403
+ const children2 = [...tree.getChildren(node) ?? []];
1404
+ if (children2.some((existing) => tree.getId(existing) === childId)) {
1405
+ return [node, false];
1406
+ }
1407
+ return [tree.withChildren(node, [...children2, child]), true];
1408
+ }
1409
+ const children = tree.getChildren(node);
1410
+ if (!children?.length) return [node, false];
1411
+ let inserted = false;
1412
+ let changed = false;
1413
+ const nextChildren = children.map((existingChild) => {
1414
+ if (inserted) return existingChild;
1415
+ const [nextChild, didInsert] = visit(existingChild);
1416
+ inserted || (inserted = didInsert);
1417
+ changed || (changed = nextChild !== existingChild);
1418
+ return nextChild;
1419
+ });
1420
+ if (!changed) return [node, inserted];
1421
+ return [tree.withChildren(node, nextChildren), inserted];
1422
+ };
1423
+ return visit(root)[0];
1424
+ }
1425
+ function renameNode(root, currentId, nextId, helpers) {
1426
+ const tree = resolveHelpers(helpers);
1427
+ if (currentId === nextId) return root;
1428
+ if (!findNodeById(root, currentId, tree)) return root;
1429
+ if (findNodeById(root, nextId, tree)) return root;
1430
+ const visit = (node) => {
1431
+ let nextNode = tree.getId(node) === currentId ? tree.withId(node, nextId) : node;
1432
+ const children = tree.getChildren(nextNode);
1433
+ if (!children?.length) return nextNode;
1434
+ let changed = nextNode !== node;
1435
+ const nextChildren = children.map((child) => {
1436
+ const nextChild = visit(child);
1437
+ changed || (changed = nextChild !== child);
1438
+ return nextChild;
1439
+ });
1440
+ if (!changed) return node;
1441
+ return tree.withChildren(nextNode, nextChildren);
1442
+ };
1443
+ return visit(root);
1444
+ }
1445
+ function dedupeChildrenBy(root, getKey, helpers) {
1446
+ const tree = resolveHelpers(helpers);
1447
+ const visit = (node) => {
1448
+ const children = tree.getChildren(node);
1449
+ if (!children?.length) return node;
1450
+ let changed = false;
1451
+ const seen = /* @__PURE__ */ new Set();
1452
+ const nextChildren = [];
1453
+ for (const child of children) {
1454
+ const normalizedChild = visit(child);
1455
+ changed || (changed = normalizedChild !== child);
1456
+ const key = getKey(normalizedChild);
1457
+ if (seen.has(key)) {
1458
+ changed = true;
1459
+ continue;
1460
+ }
1461
+ seen.add(key);
1462
+ nextChildren.push(normalizedChild);
1463
+ }
1464
+ if (!changed && nextChildren.length === children.length) return node;
1465
+ return tree.withChildren(node, nextChildren);
1466
+ };
1467
+ return visit(root);
1468
+ }
986
1469
 
987
1470
  exports.CodecError = CodecError;
988
1471
  exports.JSONCodec = JSONCodec;
@@ -990,7 +1473,13 @@ exports.MnemonicProvider = MnemonicProvider;
990
1473
  exports.SchemaError = SchemaError;
991
1474
  exports.compileSchema = compileSchema;
992
1475
  exports.createCodec = createCodec;
1476
+ exports.createSchemaRegistry = createSchemaRegistry;
1477
+ exports.dedupeChildrenBy = dedupeChildrenBy;
1478
+ exports.findNodeById = findNodeById;
1479
+ exports.insertChildIfMissing = insertChildIfMissing;
1480
+ exports.renameNode = renameNode;
993
1481
  exports.useMnemonicKey = useMnemonicKey;
1482
+ exports.useMnemonicRecovery = useMnemonicRecovery;
994
1483
  exports.validateJsonSchema = validateJsonSchema;
995
1484
  //# sourceMappingURL=index.cjs.map
996
1485
  //# sourceMappingURL=index.cjs.map