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.js CHANGED
@@ -39,6 +39,121 @@ function MnemonicProvider({
39
39
  const listeners = /* @__PURE__ */ new Map();
40
40
  let quotaErrorLogged = false;
41
41
  let accessErrorLogged = false;
42
+ const detectEnumerableStorage = () => {
43
+ if (!st) return false;
44
+ try {
45
+ return typeof st.length === "number" && typeof st.key === "function";
46
+ } catch {
47
+ return false;
48
+ }
49
+ };
50
+ const canEnumerateKeys = detectEnumerableStorage();
51
+ const isProductionRuntime = () => {
52
+ const env = globalThis?.process?.env?.NODE_ENV;
53
+ if (typeof env !== "string") {
54
+ return true;
55
+ }
56
+ return env === "production";
57
+ };
58
+ const weakRefConstructor = () => {
59
+ const ctor = globalThis?.WeakRef;
60
+ return typeof ctor === "function" ? ctor : null;
61
+ };
62
+ const hasFinalizationRegistry = () => typeof globalThis?.FinalizationRegistry === "function";
63
+ const ensureDevToolsRoot = () => {
64
+ if (!enableDevTools || typeof window === "undefined") return null;
65
+ const weakRefSupported = weakRefConstructor() !== null;
66
+ const finalizationRegistrySupported = hasFinalizationRegistry();
67
+ const globalWindow = window;
68
+ const rawExisting = globalWindow.__REACT_MNEMONIC_DEVTOOLS__;
69
+ const root = rawExisting && typeof rawExisting === "object" ? rawExisting : {};
70
+ const reserved = /* @__PURE__ */ new Set(["providers", "resolve", "list", "capabilities", "__meta"]);
71
+ for (const key of Object.keys(root)) {
72
+ if (!reserved.has(key)) {
73
+ const descriptor = Object.getOwnPropertyDescriptor(root, key);
74
+ if (!descriptor || descriptor.configurable) {
75
+ try {
76
+ delete root[key];
77
+ } catch {
78
+ }
79
+ }
80
+ }
81
+ }
82
+ if (!root.providers || typeof root.providers !== "object") {
83
+ root.providers = {};
84
+ }
85
+ if (!root.capabilities || typeof root.capabilities !== "object") {
86
+ root.capabilities = {};
87
+ }
88
+ root.capabilities.weakRef = weakRefSupported;
89
+ root.capabilities.finalizationRegistry = finalizationRegistrySupported;
90
+ if (!root.__meta || typeof root.__meta !== "object") {
91
+ root.__meta = {
92
+ version: 0,
93
+ lastUpdated: Date.now(),
94
+ lastChange: ""
95
+ };
96
+ }
97
+ if (typeof root.__meta.version !== "number" || !Number.isFinite(root.__meta.version)) {
98
+ root.__meta.version = 0;
99
+ }
100
+ if (typeof root.__meta.lastUpdated !== "number" || !Number.isFinite(root.__meta.lastUpdated)) {
101
+ root.__meta.lastUpdated = Date.now();
102
+ }
103
+ if (typeof root.__meta.lastChange !== "string") {
104
+ root.__meta.lastChange = "";
105
+ }
106
+ if (typeof root.resolve !== "function") {
107
+ root.resolve = (ns) => {
108
+ const entry = root.providers[ns];
109
+ if (!entry || !entry.weakRef || typeof entry.weakRef.deref !== "function") return null;
110
+ const live = entry.weakRef.deref();
111
+ if (live) {
112
+ entry.lastSeenAt = Date.now();
113
+ entry.staleSince = null;
114
+ return live;
115
+ }
116
+ if (entry.staleSince === null) {
117
+ entry.staleSince = Date.now();
118
+ }
119
+ return null;
120
+ };
121
+ }
122
+ if (typeof root.list !== "function") {
123
+ root.list = () => {
124
+ const entries = root.providers;
125
+ const out = [];
126
+ for (const [ns, entry] of Object.entries(entries)) {
127
+ const live = entry && entry.weakRef && typeof entry.weakRef.deref === "function" ? entry.weakRef.deref() : void 0;
128
+ const available = Boolean(live);
129
+ if (available) {
130
+ entry.lastSeenAt = Date.now();
131
+ entry.staleSince = null;
132
+ } else if (entry.staleSince === null) {
133
+ entry.staleSince = Date.now();
134
+ }
135
+ out.push({
136
+ namespace: ns,
137
+ available,
138
+ registeredAt: entry.registeredAt,
139
+ lastSeenAt: entry.lastSeenAt,
140
+ staleSince: entry.staleSince
141
+ });
142
+ }
143
+ out.sort((a, b) => a.namespace.localeCompare(b.namespace));
144
+ return out;
145
+ };
146
+ }
147
+ globalWindow.__REACT_MNEMONIC_DEVTOOLS__ = root;
148
+ return root;
149
+ };
150
+ const bumpDevToolsVersion = (reason) => {
151
+ const root = ensureDevToolsRoot();
152
+ if (!root) return;
153
+ root.__meta.version += 1;
154
+ root.__meta.lastUpdated = Date.now();
155
+ root.__meta.lastChange = `${namespace}.${reason}`;
156
+ };
42
157
  const fullKey = (key) => prefix + key;
43
158
  const emit = (key) => {
44
159
  const set = listeners.get(key);
@@ -88,6 +203,7 @@ function MnemonicProvider({
88
203
  }
89
204
  }
90
205
  emit(key);
206
+ bumpDevToolsVersion(`set:${key}`);
91
207
  };
92
208
  const removeRaw = (key) => {
93
209
  cache.set(key, null);
@@ -100,6 +216,7 @@ function MnemonicProvider({
100
216
  }
101
217
  }
102
218
  emit(key);
219
+ bumpDevToolsVersion(`remove:${key}`);
103
220
  };
104
221
  const subscribeRaw = (key, listener) => {
105
222
  let set = listeners.get(key);
@@ -118,11 +235,14 @@ function MnemonicProvider({
118
235
  };
119
236
  const getRawSnapshot = (key) => readThrough(key);
120
237
  const keys = () => {
121
- if (!st || typeof st.length !== "number" || typeof st.key !== "function") return [];
238
+ if (!canEnumerateKeys || !st) return [];
122
239
  const out = [];
123
240
  try {
124
- for (let i = 0; i < st.length; i++) {
125
- const k = st.key(i);
241
+ const storageLength = st.length;
242
+ const getStorageKey = st.key;
243
+ if (typeof storageLength !== "number" || typeof getStorageKey !== "function") return [];
244
+ for (let i = 0; i < storageLength; i++) {
245
+ const k = getStorageKey.call(st, i);
126
246
  if (!k) continue;
127
247
  if (k.startsWith(prefix)) out.push(k.slice(prefix.length));
128
248
  }
@@ -142,6 +262,7 @@ function MnemonicProvider({
142
262
  };
143
263
  const reloadFromStorage = (changedKeys) => {
144
264
  if (!st) return;
265
+ let changed = false;
145
266
  if (changedKeys !== void 0 && changedKeys.length === 0) return;
146
267
  if (changedKeys !== void 0) {
147
268
  for (const fk of changedKeys) {
@@ -161,11 +282,15 @@ function MnemonicProvider({
161
282
  if (fresh !== cached) {
162
283
  cache.set(key, fresh);
163
284
  emit(key);
285
+ changed = true;
164
286
  }
165
287
  } else if (cache.has(key)) {
166
288
  cache.delete(key);
167
289
  }
168
290
  }
291
+ if (changed) {
292
+ bumpDevToolsVersion("reload:granular");
293
+ }
169
294
  return;
170
295
  }
171
296
  for (const [key, listenerSet] of listeners) {
@@ -182,6 +307,7 @@ function MnemonicProvider({
182
307
  if (fresh !== cached) {
183
308
  cache.set(key, fresh);
184
309
  emit(key);
310
+ changed = true;
185
311
  }
186
312
  }
187
313
  for (const key of cache.keys()) {
@@ -189,9 +315,13 @@ function MnemonicProvider({
189
315
  cache.delete(key);
190
316
  }
191
317
  }
318
+ if (changed) {
319
+ bumpDevToolsVersion("reload:full");
320
+ }
192
321
  };
193
322
  const store2 = {
194
323
  prefix,
324
+ canEnumerateKeys,
195
325
  subscribeRaw,
196
326
  getRawSnapshot,
197
327
  setRaw: writeRaw,
@@ -203,56 +333,86 @@ function MnemonicProvider({
203
333
  ...schemaRegistry ? { schemaRegistry } : {}
204
334
  };
205
335
  if (enableDevTools && typeof window !== "undefined") {
206
- window.__REACT_MNEMONIC_DEVTOOLS__ = window.__REACT_MNEMONIC_DEVTOOLS__ || {};
207
- window.__REACT_MNEMONIC_DEVTOOLS__[namespace] = {
208
- /** Access the underlying store instance */
209
- getStore: () => store2,
210
- /** Dump all key-value pairs and display as a console table */
211
- dump: () => {
212
- const data = dump();
213
- console.table(
214
- Object.entries(data).map(([key, value]) => ({
215
- key,
216
- value,
217
- decoded: (() => {
336
+ const root = ensureDevToolsRoot();
337
+ let infoMessage = `[Mnemonic DevTools] Namespace "${namespace}" available via window.__REACT_MNEMONIC_DEVTOOLS__.resolve("${namespace}")`;
338
+ if (root) {
339
+ if (!root.capabilities.weakRef) {
340
+ infoMessage = `[Mnemonic DevTools] WeakRef is not available; registry provider "${namespace}" was not registered.`;
341
+ } else {
342
+ const existingLive = root.resolve(namespace);
343
+ if (existingLive) {
344
+ const duplicateMessage = `[Mnemonic DevTools] Duplicate provider namespace "${namespace}" detected. Each window must have at most one live MnemonicProvider per namespace.`;
345
+ if (!isProductionRuntime()) {
346
+ throw new Error(duplicateMessage);
347
+ }
348
+ console.warn(`${duplicateMessage} Keeping the first provider and ignoring the duplicate.`);
349
+ infoMessage = `[Mnemonic DevTools] Namespace "${namespace}" already registered. Keeping existing provider reference.`;
350
+ } else {
351
+ const providerApi = {
352
+ /** Access the underlying store instance */
353
+ getStore: () => store2,
354
+ /** Dump all key-value pairs and display as a console table */
355
+ dump: () => {
356
+ const data = dump();
357
+ console.table(
358
+ Object.entries(data).map(([key, value]) => ({
359
+ key,
360
+ value,
361
+ decoded: (() => {
362
+ try {
363
+ return JSON.parse(value);
364
+ } catch {
365
+ return value;
366
+ }
367
+ })()
368
+ }))
369
+ );
370
+ return data;
371
+ },
372
+ /** Get a decoded value by key */
373
+ get: (key) => {
374
+ const raw = readThrough(key);
375
+ if (raw == null) return void 0;
218
376
  try {
219
- return JSON.parse(value);
377
+ return JSON.parse(raw);
220
378
  } catch {
221
- return value;
379
+ return raw;
222
380
  }
223
- })()
224
- }))
225
- );
226
- return data;
227
- },
228
- /** Get a decoded value by key */
229
- get: (key) => {
230
- const raw = readThrough(key);
231
- if (raw == null) return void 0;
232
- try {
233
- return JSON.parse(raw);
234
- } catch {
235
- return raw;
236
- }
237
- },
238
- /** Set a value by key (automatically JSON-encoded) */
239
- set: (key, value) => {
240
- writeRaw(key, JSON.stringify(value));
241
- },
242
- /** Remove a key from storage */
243
- remove: (key) => removeRaw(key),
244
- /** Clear all keys in this namespace */
245
- clear: () => {
246
- for (const k of keys()) {
247
- removeRaw(k);
381
+ },
382
+ /** Set a value by key (automatically JSON-encoded) */
383
+ set: (key, value) => {
384
+ writeRaw(key, JSON.stringify(value));
385
+ },
386
+ /** Remove a key from storage */
387
+ remove: (key) => removeRaw(key),
388
+ /** Clear all keys in this namespace */
389
+ clear: () => {
390
+ for (const k of keys()) {
391
+ removeRaw(k);
392
+ }
393
+ },
394
+ /** List all keys in this namespace */
395
+ keys
396
+ };
397
+ const WeakRefCtor = weakRefConstructor();
398
+ if (!WeakRefCtor) {
399
+ infoMessage = `[Mnemonic DevTools] WeakRef became unavailable while registering "${namespace}".`;
400
+ } else {
401
+ store2.__devToolsProviderApiHold = providerApi;
402
+ root.providers[namespace] = {
403
+ namespace,
404
+ weakRef: new WeakRefCtor(providerApi),
405
+ registeredAt: Date.now(),
406
+ lastSeenAt: Date.now(),
407
+ staleSince: null
408
+ };
409
+ bumpDevToolsVersion("registry:namespace-registered");
410
+ infoMessage = `[Mnemonic DevTools] Namespace "${namespace}" available via window.__REACT_MNEMONIC_DEVTOOLS__.resolve("${namespace}")`;
411
+ }
248
412
  }
249
- },
250
- /** List all keys in this namespace */
251
- keys
252
- };
253
- console.info(
254
- `[Mnemonic DevTools] Namespace "${namespace}" available at window.__REACT_MNEMONIC_DEVTOOLS__.${namespace}`
255
- );
413
+ }
414
+ }
415
+ console.info(infoMessage);
256
416
  }
257
417
  return store2;
258
418
  }, [namespace, storage, enableDevTools, schemaMode, schemaRegistry]);
@@ -594,7 +754,7 @@ function inferJsonSchema(sample) {
594
754
  // src/Mnemonic/use.ts
595
755
  function useMnemonicKey(key, options) {
596
756
  const api = useMnemonic();
597
- const { defaultValue, onMount, onChange, listenCrossTab, codec: codecOpt, schema } = options;
757
+ const { defaultValue, onMount, onChange, listenCrossTab, codec: codecOpt, schema, reconcile } = options;
598
758
  const codec = codecOpt ?? JSONCodec;
599
759
  const schemaMode = api.schemaMode;
600
760
  const schemaRegistry = api.schemaRegistry;
@@ -693,6 +853,112 @@ function useMnemonicKey(key, options) {
693
853
  },
694
854
  [schemaRegistry, registryCache, key]
695
855
  );
856
+ const encodeForWrite = useCallback(
857
+ (nextValue) => {
858
+ const explicitVersion = schema?.version;
859
+ const latestSchema = getLatestSchemaForKey();
860
+ const explicitSchema = explicitVersion !== void 0 ? getSchemaForVersion(explicitVersion) : void 0;
861
+ let targetSchema = explicitSchema;
862
+ if (!targetSchema) {
863
+ if (explicitVersion !== void 0) {
864
+ if (schemaMode !== "strict") {
865
+ targetSchema = latestSchema;
866
+ }
867
+ } else {
868
+ targetSchema = latestSchema;
869
+ }
870
+ }
871
+ if (!targetSchema) {
872
+ if (explicitVersion !== void 0 && schemaMode === "strict") {
873
+ throw new SchemaError(
874
+ "WRITE_SCHEMA_REQUIRED",
875
+ `Write requires schema for key "${key}" in strict mode`
876
+ );
877
+ }
878
+ const envelope2 = {
879
+ version: 0,
880
+ payload: codec.encode(nextValue)
881
+ };
882
+ return JSON.stringify(envelope2);
883
+ }
884
+ let valueToStore = nextValue;
885
+ const writeMigration = schemaRegistry?.getWriteMigration?.(key, targetSchema.version);
886
+ if (writeMigration) {
887
+ try {
888
+ valueToStore = writeMigration.migrate(valueToStore);
889
+ } catch (err) {
890
+ throw err instanceof SchemaError ? err : new SchemaError("MIGRATION_FAILED", `Write-time migration failed for key "${key}"`, err);
891
+ }
892
+ }
893
+ validateAgainstSchema(valueToStore, targetSchema.schema);
894
+ const envelope = {
895
+ version: targetSchema.version,
896
+ payload: valueToStore
897
+ };
898
+ return JSON.stringify(envelope);
899
+ },
900
+ [
901
+ schema?.version,
902
+ key,
903
+ schemaMode,
904
+ codec,
905
+ schemaRegistry,
906
+ validateAgainstSchema,
907
+ getLatestSchemaForKey,
908
+ getSchemaForVersion
909
+ ]
910
+ );
911
+ const applyReconcile = useCallback(
912
+ ({
913
+ value: value2,
914
+ rewriteRaw,
915
+ pendingSchema,
916
+ persistedVersion,
917
+ latestVersion,
918
+ serializeForPersist,
919
+ derivePendingSchema
920
+ }) => {
921
+ if (!reconcile) {
922
+ const result = { value: value2 };
923
+ if (rewriteRaw !== void 0) result.rewriteRaw = rewriteRaw;
924
+ if (pendingSchema !== void 0) result.pendingSchema = pendingSchema;
925
+ return result;
926
+ }
927
+ const context = {
928
+ key,
929
+ persistedVersion,
930
+ ...latestVersion === void 0 ? {} : { latestVersion }
931
+ };
932
+ let baselineSerialized;
933
+ if (serializeForPersist) {
934
+ try {
935
+ baselineSerialized = serializeForPersist(value2);
936
+ } catch {
937
+ baselineSerialized = rewriteRaw;
938
+ }
939
+ }
940
+ try {
941
+ const reconciled = reconcile(value2, context);
942
+ const nextPendingSchema = derivePendingSchema ? derivePendingSchema(reconciled) : pendingSchema;
943
+ if (!serializeForPersist) {
944
+ const result2 = { value: reconciled };
945
+ if (rewriteRaw !== void 0) result2.rewriteRaw = rewriteRaw;
946
+ if (nextPendingSchema !== void 0) result2.pendingSchema = nextPendingSchema;
947
+ return result2;
948
+ }
949
+ const nextSerialized = serializeForPersist(reconciled);
950
+ const nextRewriteRaw = baselineSerialized === void 0 || nextSerialized !== baselineSerialized ? nextSerialized : rewriteRaw;
951
+ const result = { value: reconciled };
952
+ if (nextRewriteRaw !== void 0) result.rewriteRaw = nextRewriteRaw;
953
+ if (nextPendingSchema !== void 0) result.pendingSchema = nextPendingSchema;
954
+ return result;
955
+ } catch (err) {
956
+ const typedErr = err instanceof SchemaError ? err : new SchemaError("RECONCILE_FAILED", `Reconciliation failed for key "${key}"`, err);
957
+ return { value: getFallback(typedErr) };
958
+ }
959
+ },
960
+ [getFallback, key, reconcile]
961
+ );
696
962
  const decodeForRead = useCallback(
697
963
  (rawText) => {
698
964
  if (rawText == null) return { value: getFallback() };
@@ -728,21 +994,26 @@ function useMnemonicKey(key, options) {
728
994
  }
729
995
  try {
730
996
  const decoded2 = typeof envelope.payload === "string" ? decodeStringPayload(envelope.payload, codec) : envelope.payload;
731
- const inferredJsonSchema = inferJsonSchema(decoded2);
732
- const inferred = {
997
+ const inferSchemaForValue = (value2) => ({
733
998
  key,
734
999
  version: 1,
735
- schema: inferredJsonSchema
736
- };
737
- const rewriteEnvelope = {
738
- version: inferred.version,
739
- payload: decoded2
740
- };
741
- return {
1000
+ schema: inferJsonSchema(value2)
1001
+ });
1002
+ const inferred = inferSchemaForValue(decoded2);
1003
+ return applyReconcile({
742
1004
  value: decoded2,
743
1005
  pendingSchema: inferred,
744
- rewriteRaw: JSON.stringify(rewriteEnvelope)
745
- };
1006
+ rewriteRaw: JSON.stringify({
1007
+ version: inferred.version,
1008
+ payload: decoded2
1009
+ }),
1010
+ persistedVersion: envelope.version,
1011
+ serializeForPersist: (value2) => JSON.stringify({
1012
+ version: inferred.version,
1013
+ payload: value2
1014
+ }),
1015
+ derivePendingSchema: inferSchemaForValue
1016
+ });
746
1017
  } catch (err) {
747
1018
  const typedErr = err instanceof SchemaError || err instanceof CodecError ? err : new SchemaError("TYPE_MISMATCH", `Autoschema inference failed for key "${key}"`, err);
748
1019
  return { value: getFallback(typedErr) };
@@ -750,11 +1021,21 @@ function useMnemonicKey(key, options) {
750
1021
  }
751
1022
  if (!schemaForVersion) {
752
1023
  if (typeof envelope.payload !== "string") {
753
- return { value: envelope.payload };
1024
+ return applyReconcile({
1025
+ value: envelope.payload,
1026
+ persistedVersion: envelope.version,
1027
+ ...latestSchema ? { latestVersion: latestSchema.version } : {},
1028
+ serializeForPersist: encodeForWrite
1029
+ });
754
1030
  }
755
1031
  try {
756
1032
  const decoded2 = decodeStringPayload(envelope.payload, codec);
757
- return { value: decoded2 };
1033
+ return applyReconcile({
1034
+ value: decoded2,
1035
+ persistedVersion: envelope.version,
1036
+ ...latestSchema ? { latestVersion: latestSchema.version } : {},
1037
+ serializeForPersist: encodeForWrite
1038
+ });
758
1039
  } catch (err) {
759
1040
  const typedErr = err instanceof SchemaError || err instanceof CodecError ? err : new CodecError(`Codec decode failed for key "${key}"`, err);
760
1041
  return { value: getFallback(typedErr) };
@@ -769,7 +1050,12 @@ function useMnemonicKey(key, options) {
769
1050
  return { value: getFallback(typedErr) };
770
1051
  }
771
1052
  if (!latestSchema || envelope.version >= latestSchema.version) {
772
- return { value: current };
1053
+ return applyReconcile({
1054
+ value: current,
1055
+ persistedVersion: envelope.version,
1056
+ ...latestSchema ? { latestVersion: latestSchema.version } : {},
1057
+ serializeForPersist: encodeForWrite
1058
+ });
773
1059
  }
774
1060
  const path = getMigrationPathForKey(envelope.version, latestSchema.version);
775
1061
  if (!path) {
@@ -788,22 +1074,26 @@ function useMnemonicKey(key, options) {
788
1074
  migrated = step.migrate(migrated);
789
1075
  }
790
1076
  validateAgainstSchema(migrated, latestSchema.schema);
791
- const rewriteEnvelope = {
792
- version: latestSchema.version,
793
- payload: migrated
794
- };
795
- return {
1077
+ return applyReconcile({
796
1078
  value: migrated,
797
- rewriteRaw: JSON.stringify(rewriteEnvelope)
798
- };
1079
+ rewriteRaw: JSON.stringify({
1080
+ version: latestSchema.version,
1081
+ payload: migrated
1082
+ }),
1083
+ persistedVersion: envelope.version,
1084
+ latestVersion: latestSchema.version,
1085
+ serializeForPersist: encodeForWrite
1086
+ });
799
1087
  } catch (err) {
800
1088
  const typedErr = err instanceof SchemaError || err instanceof CodecError ? err : new SchemaError("MIGRATION_FAILED", `Migration failed for key "${key}"`, err);
801
1089
  return { value: getFallback(typedErr) };
802
1090
  }
803
1091
  },
804
1092
  [
1093
+ applyReconcile,
805
1094
  codec,
806
1095
  decodeStringPayload,
1096
+ encodeForWrite,
807
1097
  getFallback,
808
1098
  key,
809
1099
  parseEnvelope,
@@ -815,61 +1105,6 @@ function useMnemonicKey(key, options) {
815
1105
  validateAgainstSchema
816
1106
  ]
817
1107
  );
818
- const encodeForWrite = useCallback(
819
- (nextValue) => {
820
- const explicitVersion = schema?.version;
821
- const latestSchema = getLatestSchemaForKey();
822
- const explicitSchema = explicitVersion !== void 0 ? getSchemaForVersion(explicitVersion) : void 0;
823
- let targetSchema = explicitSchema;
824
- if (!targetSchema) {
825
- if (explicitVersion !== void 0) {
826
- if (schemaMode !== "strict") {
827
- targetSchema = latestSchema;
828
- }
829
- } else {
830
- targetSchema = latestSchema;
831
- }
832
- }
833
- if (!targetSchema) {
834
- if (explicitVersion !== void 0 && schemaMode === "strict") {
835
- throw new SchemaError(
836
- "WRITE_SCHEMA_REQUIRED",
837
- `Write requires schema for key "${key}" in strict mode`
838
- );
839
- }
840
- const envelope2 = {
841
- version: 0,
842
- payload: codec.encode(nextValue)
843
- };
844
- return JSON.stringify(envelope2);
845
- }
846
- let valueToStore = nextValue;
847
- const writeMigration = schemaRegistry?.getWriteMigration?.(key, targetSchema.version);
848
- if (writeMigration) {
849
- try {
850
- valueToStore = writeMigration.migrate(valueToStore);
851
- } catch (err) {
852
- throw err instanceof SchemaError ? err : new SchemaError("MIGRATION_FAILED", `Write-time migration failed for key "${key}"`, err);
853
- }
854
- }
855
- validateAgainstSchema(valueToStore, targetSchema.schema);
856
- const envelope = {
857
- version: targetSchema.version,
858
- payload: valueToStore
859
- };
860
- return JSON.stringify(envelope);
861
- },
862
- [
863
- schema?.version,
864
- key,
865
- schemaMode,
866
- codec,
867
- schemaRegistry,
868
- validateAgainstSchema,
869
- getLatestSchemaForKey,
870
- getSchemaForVersion
871
- ]
872
- );
873
1108
  const raw = useSyncExternalStore(
874
1109
  (listener) => api.subscribeRaw(key, listener),
875
1110
  () => api.getRawSnapshot(key),
@@ -981,7 +1216,255 @@ function useMnemonicKey(key, options) {
981
1216
  [value, set, reset, remove]
982
1217
  );
983
1218
  }
1219
+ function uniqueKeys(keys) {
1220
+ return [...new Set(keys)];
1221
+ }
1222
+ function useMnemonicRecovery(options = {}) {
1223
+ const api = useMnemonic();
1224
+ const { onRecover } = options;
1225
+ const namespace = useMemo(() => api.prefix.endsWith(".") ? api.prefix.slice(0, -1) : api.prefix, [api.prefix]);
1226
+ const emitRecovery = useCallback(
1227
+ (action, clearedKeys) => {
1228
+ const event = {
1229
+ action,
1230
+ namespace,
1231
+ clearedKeys
1232
+ };
1233
+ onRecover?.(event);
1234
+ },
1235
+ [namespace, onRecover]
1236
+ );
1237
+ const listKeys = useCallback(() => api.keys(), [api]);
1238
+ const clearResolvedKeys = useCallback(
1239
+ (action, keys) => {
1240
+ const clearedKeys = uniqueKeys(keys);
1241
+ for (const key of clearedKeys) {
1242
+ api.removeRaw(key);
1243
+ }
1244
+ emitRecovery(action, clearedKeys);
1245
+ return clearedKeys;
1246
+ },
1247
+ [api, emitRecovery]
1248
+ );
1249
+ const clearKeys = useCallback(
1250
+ (keys) => clearResolvedKeys("clear-keys", keys),
1251
+ [clearResolvedKeys]
1252
+ );
1253
+ const clearAll = useCallback(() => {
1254
+ if (!api.canEnumerateKeys) {
1255
+ throw new Error(
1256
+ "clearAll requires an enumerable storage backend. Use clearKeys([...]) with an explicit key list instead."
1257
+ );
1258
+ }
1259
+ return clearResolvedKeys("clear-all", api.keys());
1260
+ }, [api, clearResolvedKeys]);
1261
+ const clearMatching = useCallback(
1262
+ (predicate) => {
1263
+ if (!api.canEnumerateKeys) {
1264
+ throw new Error(
1265
+ "clearMatching requires an enumerable storage backend. Use clearKeys([...]) with an explicit key list instead."
1266
+ );
1267
+ }
1268
+ return clearResolvedKeys(
1269
+ "clear-matching",
1270
+ api.keys().filter((key) => predicate(key))
1271
+ );
1272
+ },
1273
+ [api, clearResolvedKeys]
1274
+ );
1275
+ return useMemo(
1276
+ () => ({
1277
+ namespace,
1278
+ canEnumerateKeys: api.canEnumerateKeys,
1279
+ listKeys,
1280
+ clearAll,
1281
+ clearKeys,
1282
+ clearMatching
1283
+ }),
1284
+ [namespace, api.canEnumerateKeys, listKeys, clearAll, clearKeys, clearMatching]
1285
+ );
1286
+ }
1287
+
1288
+ // src/Mnemonic/schema-registry.ts
1289
+ function schemaVersionKey(key, version) {
1290
+ return `${key}:${version}`;
1291
+ }
1292
+ function migrationVersionKey(key, fromVersion) {
1293
+ return `${key}:${fromVersion}`;
1294
+ }
1295
+ function validateVersion(value, label) {
1296
+ if (!Number.isInteger(value) || value < 0) {
1297
+ throw new SchemaError("MIGRATION_GRAPH_INVALID", `${label} must be a non-negative integer`);
1298
+ }
1299
+ }
1300
+ function createSchemaRegistry(options = {}) {
1301
+ const { schemas = [], migrations = [] } = options;
1302
+ const schemasByKeyAndVersion = /* @__PURE__ */ new Map();
1303
+ const latestSchemaByKey = /* @__PURE__ */ new Map();
1304
+ const writeMigrationsByKeyAndVersion = /* @__PURE__ */ new Map();
1305
+ const migrationsByKeyAndFromVersion = /* @__PURE__ */ new Map();
1306
+ for (const schema of schemas) {
1307
+ validateVersion(schema.version, `Schema version for key "${schema.key}"`);
1308
+ const id = schemaVersionKey(schema.key, schema.version);
1309
+ if (schemasByKeyAndVersion.has(id)) {
1310
+ throw new SchemaError(
1311
+ "SCHEMA_REGISTRATION_CONFLICT",
1312
+ `Duplicate schema registered for key "${schema.key}" version ${schema.version}`
1313
+ );
1314
+ }
1315
+ schemasByKeyAndVersion.set(id, schema);
1316
+ const currentLatest = latestSchemaByKey.get(schema.key);
1317
+ if (!currentLatest || schema.version > currentLatest.version) {
1318
+ latestSchemaByKey.set(schema.key, schema);
1319
+ }
1320
+ }
1321
+ for (const migration of migrations) {
1322
+ validateVersion(migration.fromVersion, `Migration fromVersion for key "${migration.key}"`);
1323
+ validateVersion(migration.toVersion, `Migration toVersion for key "${migration.key}"`);
1324
+ if (migration.toVersion < migration.fromVersion) {
1325
+ throw new SchemaError(
1326
+ "MIGRATION_GRAPH_INVALID",
1327
+ `Backward migration "${migration.key}" ${migration.fromVersion} -> ${migration.toVersion} is not supported`
1328
+ );
1329
+ }
1330
+ if (migration.fromVersion === migration.toVersion) {
1331
+ const id = schemaVersionKey(migration.key, migration.fromVersion);
1332
+ if (writeMigrationsByKeyAndVersion.has(id)) {
1333
+ throw new SchemaError(
1334
+ "MIGRATION_GRAPH_INVALID",
1335
+ `Duplicate write migration registered for key "${migration.key}" version ${migration.fromVersion}`
1336
+ );
1337
+ }
1338
+ writeMigrationsByKeyAndVersion.set(id, migration);
1339
+ continue;
1340
+ }
1341
+ const edgeKey = migrationVersionKey(migration.key, migration.fromVersion);
1342
+ if (migrationsByKeyAndFromVersion.has(edgeKey)) {
1343
+ const existing = migrationsByKeyAndFromVersion.get(edgeKey);
1344
+ throw new SchemaError(
1345
+ "MIGRATION_GRAPH_INVALID",
1346
+ `Ambiguous migration graph for key "${migration.key}" at version ${migration.fromVersion}: ${existing.fromVersion} -> ${existing.toVersion} conflicts with ${migration.fromVersion} -> ${migration.toVersion}`
1347
+ );
1348
+ }
1349
+ migrationsByKeyAndFromVersion.set(edgeKey, migration);
1350
+ }
1351
+ return {
1352
+ getSchema(key, version) {
1353
+ return schemasByKeyAndVersion.get(schemaVersionKey(key, version));
1354
+ },
1355
+ getLatestSchema(key) {
1356
+ return latestSchemaByKey.get(key);
1357
+ },
1358
+ getMigrationPath(key, fromVersion, toVersion) {
1359
+ if (fromVersion === toVersion) return [];
1360
+ if (toVersion < fromVersion) return null;
1361
+ const path = [];
1362
+ let currentVersion = fromVersion;
1363
+ while (currentVersion < toVersion) {
1364
+ const next = migrationsByKeyAndFromVersion.get(migrationVersionKey(key, currentVersion));
1365
+ if (!next) return null;
1366
+ path.push(next);
1367
+ currentVersion = next.toVersion;
1368
+ }
1369
+ return currentVersion === toVersion ? path : null;
1370
+ },
1371
+ getWriteMigration(key, version) {
1372
+ return writeMigrationsByKeyAndVersion.get(schemaVersionKey(key, version));
1373
+ }
1374
+ };
1375
+ }
1376
+
1377
+ // src/Mnemonic/structural-migrations.ts
1378
+ function resolveHelpers(helpers) {
1379
+ if (helpers) return helpers;
1380
+ return {
1381
+ getId: (node) => node.id,
1382
+ getChildren: (node) => node.children,
1383
+ withChildren: (node, children) => ({ ...node, children }),
1384
+ withId: (node, id) => ({ ...node, id })
1385
+ };
1386
+ }
1387
+ function findNodeById(root, id, helpers) {
1388
+ const tree = resolveHelpers(helpers);
1389
+ if (tree.getId(root) === id) return root;
1390
+ for (const child of tree.getChildren(root) ?? []) {
1391
+ const match = findNodeById(child, id, tree);
1392
+ if (match) return match;
1393
+ }
1394
+ return void 0;
1395
+ }
1396
+ function insertChildIfMissing(root, parentId, child, helpers) {
1397
+ const tree = resolveHelpers(helpers);
1398
+ const childId = tree.getId(child);
1399
+ const visit = (node) => {
1400
+ if (tree.getId(node) === parentId) {
1401
+ const children2 = [...tree.getChildren(node) ?? []];
1402
+ if (children2.some((existing) => tree.getId(existing) === childId)) {
1403
+ return [node, false];
1404
+ }
1405
+ return [tree.withChildren(node, [...children2, child]), true];
1406
+ }
1407
+ const children = tree.getChildren(node);
1408
+ if (!children?.length) return [node, false];
1409
+ let inserted = false;
1410
+ let changed = false;
1411
+ const nextChildren = children.map((existingChild) => {
1412
+ if (inserted) return existingChild;
1413
+ const [nextChild, didInsert] = visit(existingChild);
1414
+ inserted || (inserted = didInsert);
1415
+ changed || (changed = nextChild !== existingChild);
1416
+ return nextChild;
1417
+ });
1418
+ if (!changed) return [node, inserted];
1419
+ return [tree.withChildren(node, nextChildren), inserted];
1420
+ };
1421
+ return visit(root)[0];
1422
+ }
1423
+ function renameNode(root, currentId, nextId, helpers) {
1424
+ const tree = resolveHelpers(helpers);
1425
+ if (currentId === nextId) return root;
1426
+ if (!findNodeById(root, currentId, tree)) return root;
1427
+ if (findNodeById(root, nextId, tree)) return root;
1428
+ const visit = (node) => {
1429
+ let nextNode = tree.getId(node) === currentId ? tree.withId(node, nextId) : node;
1430
+ const children = tree.getChildren(nextNode);
1431
+ if (!children?.length) return nextNode;
1432
+ let changed = nextNode !== node;
1433
+ const nextChildren = children.map((child) => {
1434
+ const nextChild = visit(child);
1435
+ changed || (changed = nextChild !== child);
1436
+ return nextChild;
1437
+ });
1438
+ if (!changed) return node;
1439
+ return tree.withChildren(nextNode, nextChildren);
1440
+ };
1441
+ return visit(root);
1442
+ }
1443
+ function dedupeChildrenBy(root, getKey, helpers) {
1444
+ const tree = resolveHelpers(helpers);
1445
+ const visit = (node) => {
1446
+ const children = tree.getChildren(node);
1447
+ if (!children?.length) return node;
1448
+ let changed = false;
1449
+ const seen = /* @__PURE__ */ new Set();
1450
+ const nextChildren = [];
1451
+ for (const child of children) {
1452
+ const normalizedChild = visit(child);
1453
+ changed || (changed = normalizedChild !== child);
1454
+ const key = getKey(normalizedChild);
1455
+ if (seen.has(key)) {
1456
+ changed = true;
1457
+ continue;
1458
+ }
1459
+ seen.add(key);
1460
+ nextChildren.push(normalizedChild);
1461
+ }
1462
+ if (!changed && nextChildren.length === children.length) return node;
1463
+ return tree.withChildren(node, nextChildren);
1464
+ };
1465
+ return visit(root);
1466
+ }
984
1467
 
985
- export { CodecError, JSONCodec, MnemonicProvider, SchemaError, compileSchema, createCodec, useMnemonicKey, validateJsonSchema };
1468
+ export { CodecError, JSONCodec, MnemonicProvider, SchemaError, compileSchema, createCodec, createSchemaRegistry, dedupeChildrenBy, findNodeById, insertChildIfMissing, renameNode, useMnemonicKey, useMnemonicRecovery, validateJsonSchema };
986
1469
  //# sourceMappingURL=index.js.map
987
1470
  //# sourceMappingURL=index.js.map