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/README.md +171 -85
- package/dist/index.cjs +615 -126
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +578 -10
- package/dist/index.d.ts +578 -10
- package/dist/index.js +610 -127
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 (!
|
|
238
|
+
if (!canEnumerateKeys || !st) return [];
|
|
122
239
|
const out = [];
|
|
123
240
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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(
|
|
377
|
+
return JSON.parse(raw);
|
|
220
378
|
} catch {
|
|
221
|
-
return
|
|
379
|
+
return raw;
|
|
222
380
|
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
|
732
|
-
const inferred = {
|
|
997
|
+
const inferSchemaForValue = (value2) => ({
|
|
733
998
|
key,
|
|
734
999
|
version: 1,
|
|
735
|
-
schema:
|
|
736
|
-
};
|
|
737
|
-
const
|
|
738
|
-
|
|
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(
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
792
|
-
version: latestSchema.version,
|
|
793
|
-
payload: migrated
|
|
794
|
-
};
|
|
795
|
-
return {
|
|
1077
|
+
return applyReconcile({
|
|
796
1078
|
value: migrated,
|
|
797
|
-
rewriteRaw: JSON.stringify(
|
|
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
|