nusm 0.1.0 → 1.1.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.
Files changed (98) hide show
  1. package/README.md +66 -7
  2. package/dist/adapters/indexDbAdapter.d.ts.map +1 -1
  3. package/dist/adapters/localStorageAdapter.d.ts.map +1 -1
  4. package/dist/adapters/sessionStorageAdapter.d.ts.map +1 -1
  5. package/dist/adapters/storageAdapter.d.ts.map +1 -1
  6. package/dist/devtools/client.d.ts +12 -10
  7. package/dist/devtools/client.d.ts.map +1 -1
  8. package/dist/devtools/client.js +50 -10
  9. package/dist/devtools/client.js.map +1 -1
  10. package/dist/devtools/core.d.ts +9 -0
  11. package/dist/devtools/core.d.ts.map +1 -0
  12. package/dist/devtools/core.js +38 -0
  13. package/dist/devtools/core.js.map +1 -0
  14. package/dist/devtools/flatten.d.ts +4 -0
  15. package/dist/devtools/flatten.d.ts.map +1 -0
  16. package/dist/devtools/flatten.js +145 -0
  17. package/dist/devtools/flatten.js.map +1 -0
  18. package/dist/devtools/index.d.ts +4 -0
  19. package/dist/devtools/index.d.ts.map +1 -0
  20. package/dist/devtools/index.js +3 -0
  21. package/dist/devtools/index.js.map +1 -0
  22. package/dist/devtools/nusm-logo.d.ts +4 -0
  23. package/dist/devtools/nusm-logo.d.ts.map +1 -0
  24. package/dist/devtools/nusm-logo.js +5 -0
  25. package/dist/devtools/nusm-logo.js.map +1 -0
  26. package/dist/devtools/panel-controls.d.ts +13 -0
  27. package/dist/devtools/panel-controls.d.ts.map +1 -0
  28. package/dist/devtools/panel-controls.js +32 -0
  29. package/dist/devtools/panel-controls.js.map +1 -0
  30. package/dist/devtools/panel-detail.d.ts +15 -0
  31. package/dist/devtools/panel-detail.d.ts.map +1 -0
  32. package/dist/devtools/panel-detail.js +114 -0
  33. package/dist/devtools/panel-detail.js.map +1 -0
  34. package/dist/devtools/panel-helpers.d.ts +16 -0
  35. package/dist/devtools/panel-helpers.d.ts.map +1 -0
  36. package/dist/devtools/panel-helpers.js +40 -0
  37. package/dist/devtools/panel-helpers.js.map +1 -0
  38. package/dist/devtools/panel-list.d.ts +13 -0
  39. package/dist/devtools/panel-list.d.ts.map +1 -0
  40. package/dist/devtools/panel-list.js +28 -0
  41. package/dist/devtools/panel-list.js.map +1 -0
  42. package/dist/devtools/panel-model.d.ts +19 -0
  43. package/dist/devtools/panel-model.d.ts.map +1 -0
  44. package/dist/devtools/panel-model.js +53 -0
  45. package/dist/devtools/panel-model.js.map +1 -0
  46. package/dist/devtools/panel.d.ts +3 -0
  47. package/dist/devtools/panel.d.ts.map +1 -0
  48. package/dist/devtools/panel.js +429 -0
  49. package/dist/devtools/panel.js.map +1 -0
  50. package/dist/devtools/path.d.ts +5 -0
  51. package/dist/devtools/path.d.ts.map +1 -0
  52. package/dist/devtools/path.js +142 -0
  53. package/dist/devtools/path.js.map +1 -0
  54. package/dist/devtools/plugin.d.ts +14 -2
  55. package/dist/devtools/plugin.d.ts.map +1 -1
  56. package/dist/devtools/plugin.js +10 -56
  57. package/dist/devtools/plugin.js.map +1 -1
  58. package/dist/devtools/projection.d.ts +8 -0
  59. package/dist/devtools/projection.d.ts.map +1 -0
  60. package/dist/devtools/projection.js +72 -0
  61. package/dist/devtools/projection.js.map +1 -0
  62. package/dist/devtools/protocol.d.ts +4 -0
  63. package/dist/devtools/protocol.d.ts.map +1 -0
  64. package/dist/devtools/protocol.js +58 -0
  65. package/dist/devtools/protocol.js.map +1 -0
  66. package/dist/devtools/serialize.d.ts +3 -0
  67. package/dist/devtools/serialize.d.ts.map +1 -0
  68. package/dist/devtools/serialize.js +71 -0
  69. package/dist/devtools/serialize.js.map +1 -0
  70. package/dist/devtools/types.d.ts +60 -0
  71. package/dist/devtools/types.d.ts.map +1 -0
  72. package/dist/devtools/types.js +2 -0
  73. package/dist/devtools/types.js.map +1 -0
  74. package/dist/index.d.ts +2 -3
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +1 -2
  77. package/dist/index.js.map +1 -1
  78. package/dist/nusm.d.ts +5 -0
  79. package/dist/nusm.d.ts.map +1 -0
  80. package/dist/nusm.js +1026 -0
  81. package/dist/nusm.js.map +1 -0
  82. package/dist/react/index.d.ts +6 -8
  83. package/dist/react/index.d.ts.map +1 -1
  84. package/dist/react/index.js +16 -28
  85. package/dist/react/index.js.map +1 -1
  86. package/dist/types.d.ts +29 -15
  87. package/dist/types.d.ts.map +1 -1
  88. package/docs/UPGRADING.md +53 -0
  89. package/docs/devtools.md +139 -0
  90. package/package.json +61 -24
  91. package/dist/core/createNusmStore.d.ts +0 -3
  92. package/dist/core/createNusmStore.d.ts.map +0 -1
  93. package/dist/core/createNusmStore.js +0 -491
  94. package/dist/core/createNusmStore.js.map +0 -1
  95. package/dist/utils.d.ts +0 -5
  96. package/dist/utils.d.ts.map +0 -1
  97. package/dist/utils.js +0 -30
  98. package/dist/utils.js.map +0 -1
package/dist/nusm.js ADDED
@@ -0,0 +1,1026 @@
1
+ import createDeepmerge from "@fastify/deepmerge";
2
+ import { AsyncDebouncer } from "@tanstack/pacer";
3
+ import { batch, Store } from "@tanstack/store";
4
+ import { deepEqual } from "fast-equals";
5
+ import { createNusmDevtoolsEmitter } from "./devtools/client";
6
+ import { removeValueAtPath, setValueAtPath } from "./devtools/path";
7
+ import { isRoundTrippableForDevtools } from "./devtools/serialize";
8
+ const deepMerge = createDeepmerge({
9
+ mergeArray: () => (_target, source) => source,
10
+ });
11
+ const defaultResolveKey = (params) => {
12
+ if (params.kind === "entire") {
13
+ return `nusm:${params.storeId}:entire`;
14
+ }
15
+ return `nusm:${params.storeId}:slice:${params.sliceKey}`;
16
+ };
17
+ const createDataRecord = () => Object.create(null);
18
+ const defineRecordValue = (record, key, value) => {
19
+ Object.defineProperty(record, key, {
20
+ configurable: true,
21
+ enumerable: true,
22
+ value,
23
+ writable: true,
24
+ });
25
+ };
26
+ const devtoolsStoreSequenceKey = Symbol.for("nusm.devtools.store-sequence");
27
+ const nextDevtoolsInstanceId = () => {
28
+ const registry = globalThis;
29
+ const current = registry[devtoolsStoreSequenceKey];
30
+ const sequence = (typeof current === "bigint" && current >= 0n ? current : 0n) + 1n;
31
+ registry[devtoolsStoreSequenceKey] = sequence;
32
+ return `nusm-instance-${sequence}`;
33
+ };
34
+ const resolveStoreId = (options) => {
35
+ if (options?.storeId)
36
+ return options.storeId;
37
+ const devtoolsName = typeof options?.devtools === "object" ? options.devtools.name : undefined;
38
+ if (devtoolsName) {
39
+ const normalized = devtoolsName
40
+ .trim()
41
+ .toLowerCase()
42
+ .replace(/\s+/g, "-")
43
+ .replace(/[^a-z0-9-_]/g, "");
44
+ return normalized || undefined;
45
+ }
46
+ return undefined;
47
+ };
48
+ const shouldDiscardPersisted = (hydrate) => {
49
+ const discard = hydrate?.discardPersisted;
50
+ if (typeof discard === "function")
51
+ return discard();
52
+ return Boolean(discard);
53
+ };
54
+ const resolveValidateResult = (validateResult, persisted) => {
55
+ if (typeof validateResult === "boolean") {
56
+ return { ok: validateResult, value: persisted };
57
+ }
58
+ return {
59
+ ok: validateResult.ok,
60
+ value: validateResult.value ?? persisted,
61
+ };
62
+ };
63
+ const resolveCommandStatus = (action, snapshotError) => {
64
+ if (!snapshotError)
65
+ return "success";
66
+ if (action === "refresh")
67
+ return "error";
68
+ return "success_with_warning";
69
+ };
70
+ // fallow-ignore-next-line unused-export
71
+ export { batch, Store };
72
+ // fallow-ignore-next-line complexity
73
+ export function createNusmStore(initialState, options) {
74
+ const adapter = options?.adapter;
75
+ const persist = options?.persist;
76
+ const strategy = persist?.strategy ?? "entire";
77
+ const slices = persist?.slices ?? [];
78
+ const hydrateConfig = persist?.hydrate;
79
+ const onError = options?.onError;
80
+ const storeId = resolveStoreId(options);
81
+ const devtoolsInstanceId = nextDevtoolsInstanceId();
82
+ const storeIdValue = storeId ?? devtoolsInstanceId;
83
+ const configuredEventLogCap = typeof options?.devtools === "object"
84
+ ? options.devtools.eventLogCap
85
+ : undefined, eventLogCap = Number.isSafeInteger(configuredEventLogCap) &&
86
+ (configuredEventLogCap ?? 0) >= 1
87
+ ? Math.min(configuredEventLogCap ?? 100, 10_000)
88
+ : 100, devtoolsEmitter = createNusmDevtoolsEmitter(Boolean(options?.devtools), devtoolsInstanceId);
89
+ let lastFlushAt;
90
+ if (adapter && !storeId) {
91
+ throw new Error("nusm requires a stable storeId or devtools name when persistence is enabled.");
92
+ }
93
+ const store = new Store(adapter ? undefined : initialState);
94
+ let readyResolve = () => undefined;
95
+ let readyReject = () => undefined;
96
+ const ready = new Promise((resolve, reject) => {
97
+ readyResolve = resolve;
98
+ readyReject = reject;
99
+ });
100
+ let isReady = false;
101
+ let suppressPersist = false;
102
+ const hydrationStatus = {
103
+ byKey: createDataRecord(),
104
+ overall: adapter ? "pending" : "not_configured",
105
+ };
106
+ const getHydrationSnapshot = () => {
107
+ const byKey = createDataRecord();
108
+ for (const [key, value] of Object.entries(hydrationStatus.byKey))
109
+ defineRecordValue(byKey, key, value);
110
+ return Object.freeze({
111
+ byKey: Object.freeze(byKey),
112
+ overall: hydrationStatus.overall,
113
+ });
114
+ };
115
+ const resolveAdapterKey = (params) => {
116
+ if (!storeId)
117
+ return "";
118
+ return adapter?.resolveKey
119
+ ? adapter.resolveKey({ storeId, ...params })
120
+ : defaultResolveKey({ storeId, ...params });
121
+ };
122
+ const persistenceUnits = new Map();
123
+ const resolvedSliceKeys = new Map();
124
+ let resolvedEntireKey;
125
+ const registerPersistenceUnit = (key, unit) => {
126
+ if (typeof key !== "string" || key.length === 0) {
127
+ throw new Error("Persistence adapter keys must be non-empty strings.");
128
+ }
129
+ const existing = persistenceUnits.get(key);
130
+ if (existing) {
131
+ const existingName = existing.sliceKey ?? existing.kind;
132
+ const unitName = unit.sliceKey ?? unit.kind;
133
+ throw new Error(`Persistence units "${existingName}" and "${unitName}" resolve to the same adapter key "${key}".`);
134
+ }
135
+ persistenceUnits.set(key, unit);
136
+ };
137
+ const resolveKey = (params) => {
138
+ const key = params.kind === "entire"
139
+ ? resolvedEntireKey
140
+ : params.sliceKey === undefined
141
+ ? undefined
142
+ : resolvedSliceKeys.get(params.sliceKey);
143
+ if (key === undefined) {
144
+ throw new Error("Persistence key was requested before configuration completed.");
145
+ }
146
+ return key;
147
+ };
148
+ if (adapter) {
149
+ if (strategy === "entire") {
150
+ const key = resolveAdapterKey({ kind: "entire" });
151
+ registerPersistenceUnit(key, { kind: "entire" });
152
+ resolvedEntireKey = key;
153
+ hydrationStatus.byKey.entire = "pending";
154
+ }
155
+ else {
156
+ const logicalSliceKeys = new Set();
157
+ for (const slice of slices) {
158
+ if (logicalSliceKeys.has(slice.key)) {
159
+ throw new Error(`Persist slice key "${slice.key}" is configured more than once.`);
160
+ }
161
+ logicalSliceKeys.add(slice.key);
162
+ }
163
+ for (const slice of slices) {
164
+ const key = resolveAdapterKey({ kind: "slice", sliceKey: slice.key });
165
+ registerPersistenceUnit(key, { kind: "slice", sliceKey: slice.key });
166
+ resolvedSliceKeys.set(slice.key, key);
167
+ defineRecordValue(hydrationStatus.byKey, slice.key, "pending");
168
+ }
169
+ }
170
+ }
171
+ const recentWrites = new Map();
172
+ let persistenceGeneration = 0;
173
+ const recentWriteWindowMs = adapter?.name.toLowerCase().includes("index")
174
+ ? 1000
175
+ : 500;
176
+ const queuedPayloads = new Map();
177
+ const queuedGenerations = new Map();
178
+ const recordAdapterWrite = async (key, operation) => {
179
+ const writeState = recentWrites.get(key) ?? { active: 0 };
180
+ writeState.active += 1;
181
+ recentWrites.set(key, writeState);
182
+ try {
183
+ await Promise.resolve(operation());
184
+ writeState.completedAt = Date.now();
185
+ lastFlushAt = writeState.completedAt;
186
+ }
187
+ catch (error) {
188
+ if (writeState.active === 1 && writeState.completedAt === undefined)
189
+ recentWrites.delete(key);
190
+ throw error;
191
+ }
192
+ finally {
193
+ writeState.active -= 1;
194
+ }
195
+ };
196
+ const persistHydrationBaseline = async (entry) => {
197
+ if (!adapter)
198
+ return;
199
+ devtoolsEmitter?.emitEvent({
200
+ key: entry.key,
201
+ sliceKey: entry.sliceKey,
202
+ storeId: storeIdValue,
203
+ ts: Date.now(),
204
+ type: "persist_scheduled",
205
+ });
206
+ devtoolsEmitter?.emitEvent({
207
+ storeId: storeIdValue,
208
+ ts: Date.now(),
209
+ type: "persist_flush_start",
210
+ });
211
+ try {
212
+ await recordAdapterWrite(entry.key, () => adapter.setItem(entry.key, entry.payload));
213
+ devtoolsEmitter?.emitEvent({
214
+ key: entry.key,
215
+ sliceKey: entry.sliceKey,
216
+ storeId: storeIdValue,
217
+ ts: Date.now(),
218
+ type: "persist_flush_ok",
219
+ });
220
+ }
221
+ catch (error) {
222
+ devtoolsEmitter?.emitEvent({
223
+ detail: error,
224
+ key: entry.key,
225
+ sliceKey: entry.sliceKey,
226
+ storeId: storeIdValue,
227
+ ts: Date.now(),
228
+ type: "persist_flush_error",
229
+ });
230
+ throw error;
231
+ }
232
+ };
233
+ const performFlushQueue = async () => {
234
+ if (!adapter || queuedPayloads.size === 0)
235
+ return;
236
+ devtoolsEmitter?.emitEvent({
237
+ storeId: storeIdValue,
238
+ ts: Date.now(),
239
+ type: "persist_flush_start",
240
+ });
241
+ const entries = Array.from(queuedPayloads.values()).map((entry) => ({
242
+ ...entry,
243
+ generation: queuedGenerations.get(entry.key),
244
+ }));
245
+ queuedPayloads.clear();
246
+ queuedGenerations.clear();
247
+ await Promise.all(entries.map(async (entry) => {
248
+ if (entry.generation !== persistenceGeneration)
249
+ return;
250
+ try {
251
+ await recordAdapterWrite(entry.key, () => adapter.setItem(entry.key, entry.payload));
252
+ devtoolsEmitter?.emitEvent({
253
+ key: entry.key,
254
+ sliceKey: entry.sliceKey,
255
+ storeId: storeIdValue,
256
+ ts: Date.now(),
257
+ type: "persist_flush_ok",
258
+ });
259
+ }
260
+ catch (error) {
261
+ devtoolsEmitter?.emitEvent({
262
+ detail: error,
263
+ key: entry.key,
264
+ sliceKey: entry.sliceKey,
265
+ storeId: storeIdValue,
266
+ ts: Date.now(),
267
+ type: "persist_flush_error",
268
+ });
269
+ onError?.(error);
270
+ }
271
+ }));
272
+ emitDevtoolsSnapshot();
273
+ };
274
+ let activeFlush;
275
+ const serializePersistenceOperation = (operation) => {
276
+ const requested = activeFlush ? activeFlush.then(operation) : operation();
277
+ const recoverableTail = requested.catch(() => undefined);
278
+ activeFlush = recoverableTail;
279
+ void recoverableTail.then(() => {
280
+ if (activeFlush === recoverableTail)
281
+ activeFlush = undefined;
282
+ });
283
+ return requested;
284
+ };
285
+ const flushQueue = () => serializePersistenceOperation(performFlushQueue);
286
+ const pacerConfig = adapter?.pacer;
287
+ const pacer = adapter && pacerConfig !== false
288
+ ? new AsyncDebouncer(async () => {
289
+ await flushQueue();
290
+ }, {
291
+ leading: pacerConfig?.leading ?? false,
292
+ trailing: pacerConfig?.trailing ?? true,
293
+ wait: pacerConfig?.wait ?? 0,
294
+ })
295
+ : null;
296
+ const scheduleFlush = () => {
297
+ if (!adapter)
298
+ return;
299
+ if (pacerConfig === false) {
300
+ void flushQueue();
301
+ return;
302
+ }
303
+ pacer?.maybeExecute();
304
+ };
305
+ const enqueuePersist = (entry) => {
306
+ queuedPayloads.set(entry.key, entry);
307
+ queuedGenerations.set(entry.key, persistenceGeneration);
308
+ devtoolsEmitter?.emitEvent({
309
+ key: entry.key,
310
+ sliceKey: entry.sliceKey,
311
+ storeId: storeIdValue,
312
+ ts: Date.now(),
313
+ type: "persist_scheduled",
314
+ });
315
+ scheduleFlush();
316
+ };
317
+ let previousState = store.state;
318
+ let supersededPersistenceState;
319
+ const subscribeToStore = () => {
320
+ store.subscribe((currentVal) => {
321
+ const prevVal = previousState;
322
+ previousState = currentVal;
323
+ if (suppressPersist)
324
+ return;
325
+ void emitDevtoolsSnapshot();
326
+ if (!adapter)
327
+ return;
328
+ if (supersededPersistenceState &&
329
+ Object.is(currentVal, supersededPersistenceState.value)) {
330
+ supersededPersistenceState = undefined;
331
+ return;
332
+ }
333
+ supersededPersistenceState = undefined;
334
+ if (strategy === "entire") {
335
+ const key = resolveKey({ kind: "entire" });
336
+ enqueuePersist({ key, kind: "entire", payload: currentVal });
337
+ return;
338
+ }
339
+ for (const slice of slices) {
340
+ const prevSlice = slice.select(prevVal);
341
+ const nextSlice = slice.select(currentVal);
342
+ if (Object.is(prevSlice, nextSlice))
343
+ continue;
344
+ const key = resolveKey({ kind: "slice", sliceKey: slice.key });
345
+ enqueuePersist({
346
+ key,
347
+ kind: "slice",
348
+ payload: nextSlice,
349
+ sliceKey: slice.key,
350
+ });
351
+ }
352
+ });
353
+ };
354
+ const applyState = (nextState) => {
355
+ suppressPersist = true;
356
+ store.setState(() => nextState);
357
+ suppressPersist = false;
358
+ };
359
+ // fallow-ignore-next-line complexity
360
+ const hydrateEntire = async () => {
361
+ const key = resolveKey({ kind: "entire" });
362
+ if (!adapter)
363
+ return initialState;
364
+ if (shouldDiscardPersisted(hydrateConfig)) {
365
+ hydrationStatus.byKey.entire = "discarded";
366
+ devtoolsEmitter?.emitEvent({
367
+ storeId: storeIdValue,
368
+ ts: Date.now(),
369
+ type: "hydrate_discarded",
370
+ });
371
+ return initialState;
372
+ }
373
+ try {
374
+ const raw = await adapter.getItem(key);
375
+ if (raw == null) {
376
+ await persistHydrationBaseline({ key, payload: initialState });
377
+ hydrationStatus.byKey.entire = "hydrated";
378
+ return initialState;
379
+ }
380
+ let persistedValue = raw;
381
+ if (hydrateConfig?.validate) {
382
+ const result = resolveValidateResult(hydrateConfig.validate(persistedValue), persistedValue);
383
+ if (!result.ok) {
384
+ hydrationStatus.byKey.entire = "discarded";
385
+ devtoolsEmitter?.emitEvent({
386
+ storeId: storeIdValue,
387
+ ts: Date.now(),
388
+ type: "hydrate_discarded",
389
+ });
390
+ return initialState;
391
+ }
392
+ persistedValue = result.value;
393
+ }
394
+ const merged = hydrateConfig?.merge
395
+ ? hydrateConfig.merge({
396
+ initial: initialState,
397
+ persisted: persistedValue,
398
+ })
399
+ : deepMerge(initialState, persistedValue);
400
+ hydrationStatus.byKey.entire = "hydrated";
401
+ return merged;
402
+ }
403
+ catch (error) {
404
+ hydrationStatus.byKey.entire = "error";
405
+ devtoolsEmitter?.emitEvent({
406
+ detail: error,
407
+ storeId: storeIdValue,
408
+ ts: Date.now(),
409
+ type: "hydrate_error",
410
+ });
411
+ onError?.(error);
412
+ return initialState;
413
+ }
414
+ };
415
+ // fallow-ignore-next-line complexity
416
+ const hydrateSlices = async () => {
417
+ let nextState = initialState;
418
+ const missingSlices = [];
419
+ const hydratedSliceValues = [];
420
+ if (shouldDiscardPersisted(hydrateConfig)) {
421
+ for (const slice of slices) {
422
+ defineRecordValue(hydrationStatus.byKey, slice.key, "discarded");
423
+ }
424
+ devtoolsEmitter?.emitEvent({
425
+ storeId: storeIdValue,
426
+ ts: Date.now(),
427
+ type: "hydrate_discarded",
428
+ });
429
+ return nextState;
430
+ }
431
+ for (const slice of slices) {
432
+ const key = resolveKey({ kind: "slice", sliceKey: slice.key });
433
+ try {
434
+ const raw = await adapter?.getItem(key);
435
+ if (raw == null) {
436
+ missingSlices.push(slice);
437
+ continue;
438
+ }
439
+ let persistedValue = raw;
440
+ if (hydrateConfig?.validate) {
441
+ const result = resolveValidateResult(hydrateConfig.validate(persistedValue), persistedValue);
442
+ if (!result.ok) {
443
+ defineRecordValue(hydrationStatus.byKey, slice.key, "discarded");
444
+ devtoolsEmitter?.emitEvent({
445
+ sliceKey: slice.key,
446
+ storeId: storeIdValue,
447
+ ts: Date.now(),
448
+ type: "hydrate_discarded",
449
+ });
450
+ continue;
451
+ }
452
+ persistedValue = result.value;
453
+ }
454
+ nextState = slice.apply(nextState, persistedValue);
455
+ hydratedSliceValues.push({ slice, value: persistedValue });
456
+ defineRecordValue(hydrationStatus.byKey, slice.key, "hydrated");
457
+ }
458
+ catch (error) {
459
+ defineRecordValue(hydrationStatus.byKey, slice.key, "error");
460
+ devtoolsEmitter?.emitEvent({
461
+ detail: error,
462
+ sliceKey: slice.key,
463
+ storeId: storeIdValue,
464
+ ts: Date.now(),
465
+ type: "hydrate_error",
466
+ });
467
+ onError?.(error);
468
+ }
469
+ }
470
+ const missingBaselines = missingSlices.map((slice) => ({
471
+ baseline: slice.select(initialState),
472
+ key: resolveKey({ kind: "slice", sliceKey: slice.key }),
473
+ slice,
474
+ }));
475
+ let baselineState = nextState;
476
+ for (const { baseline, slice } of missingBaselines) {
477
+ baselineState = slice.apply(baselineState, baseline);
478
+ }
479
+ const expectedSliceValues = [
480
+ ...hydratedSliceValues,
481
+ ...missingBaselines.map(({ baseline, slice }) => ({
482
+ slice,
483
+ value: baseline,
484
+ })),
485
+ ];
486
+ const nonIsolatedSlice = expectedSliceValues.find(({ slice, value }) => !deepEqual(slice.select(baselineState), value));
487
+ if (nonIsolatedSlice) {
488
+ throw new Error(`Persist slice "${nonIsolatedSlice.slice.key}" is not isolated: applying slice values changed its selected persisted value.`);
489
+ }
490
+ nextState = baselineState;
491
+ for (const { baseline, key, slice } of missingBaselines) {
492
+ try {
493
+ await persistHydrationBaseline({
494
+ key,
495
+ payload: baseline,
496
+ sliceKey: slice.key,
497
+ });
498
+ defineRecordValue(hydrationStatus.byKey, slice.key, "hydrated");
499
+ }
500
+ catch (error) {
501
+ defineRecordValue(hydrationStatus.byKey, slice.key, "error");
502
+ devtoolsEmitter?.emitEvent({
503
+ detail: error,
504
+ sliceKey: slice.key,
505
+ storeId: storeIdValue,
506
+ ts: Date.now(),
507
+ type: "hydrate_error",
508
+ });
509
+ onError?.(error);
510
+ }
511
+ }
512
+ return nextState;
513
+ };
514
+ const finalizeHydration = () => {
515
+ const statuses = Object.values(hydrationStatus.byKey);
516
+ if (statuses.includes("error")) {
517
+ hydrationStatus.overall = "error";
518
+ }
519
+ else if (statuses.includes("discarded")) {
520
+ hydrationStatus.overall = "discarded";
521
+ }
522
+ else {
523
+ hydrationStatus.overall = adapter ? "hydrated" : "not_configured";
524
+ }
525
+ };
526
+ const hydrate = async () => {
527
+ if (!adapter) {
528
+ isReady = true;
529
+ readyResolve();
530
+ emitDevtoolsSnapshot();
531
+ return;
532
+ }
533
+ devtoolsEmitter?.emitEvent({
534
+ storeId: storeIdValue,
535
+ ts: Date.now(),
536
+ type: "hydrate_start",
537
+ });
538
+ try {
539
+ const hydratedState = strategy === "entire" ? await hydrateEntire() : await hydrateSlices();
540
+ applyState(hydratedState);
541
+ finalizeHydration();
542
+ devtoolsEmitter?.emitEvent({
543
+ storeId: storeIdValue,
544
+ ts: Date.now(),
545
+ type: "hydrate_applied",
546
+ });
547
+ isReady = true;
548
+ readyResolve();
549
+ devtoolsEmitter?.emitHydration(storeIdValue, getHydrationSnapshot());
550
+ emitDevtoolsSnapshot();
551
+ }
552
+ catch (error) {
553
+ hydrationStatus.overall = "error";
554
+ devtoolsEmitter?.emitEvent({
555
+ detail: error,
556
+ storeId: storeIdValue,
557
+ ts: Date.now(),
558
+ type: "hydrate_error",
559
+ });
560
+ onError?.(error);
561
+ readyReject(error);
562
+ }
563
+ };
564
+ // fallow-ignore-next-line complexity
565
+ const handleAdapterEvent = async (event) => {
566
+ if (!adapter)
567
+ return;
568
+ if (event.type !== "clear" && event.key) {
569
+ const writeState = recentWrites.get(event.key);
570
+ if (writeState &&
571
+ (writeState.active > 0 ||
572
+ (writeState.completedAt !== undefined &&
573
+ Date.now() - writeState.completedAt < recentWriteWindowMs))) {
574
+ return;
575
+ }
576
+ }
577
+ const applyExternalState = (nextState) => {
578
+ suppressPersist = true;
579
+ store.setState(() => nextState);
580
+ suppressPersist = false;
581
+ devtoolsEmitter?.emitEvent({
582
+ key: event.key,
583
+ storeId: storeIdValue,
584
+ ts: Date.now(),
585
+ type: "adapter_external_event",
586
+ });
587
+ emitDevtoolsSnapshot();
588
+ };
589
+ if (event.type === "clear") {
590
+ if (strategy === "entire") {
591
+ applyExternalState(initialState);
592
+ return;
593
+ }
594
+ let nextState = store.state;
595
+ for (const slice of slices) {
596
+ nextState = slice.apply(nextState, slice.select(initialState));
597
+ }
598
+ applyExternalState(nextState);
599
+ return;
600
+ }
601
+ if (!event.key)
602
+ return;
603
+ const unit = persistenceUnits.get(event.key);
604
+ if (!unit)
605
+ return;
606
+ if (event.type === "remove") {
607
+ if (unit.kind === "entire") {
608
+ applyExternalState(initialState);
609
+ }
610
+ else {
611
+ const slice = slices.find((candidate) => candidate.key === unit.sliceKey);
612
+ if (slice) {
613
+ const resetValue = slice.select(initialState);
614
+ applyExternalState(slice.apply(store.state, resetValue));
615
+ }
616
+ }
617
+ return;
618
+ }
619
+ if (unit.kind === "entire") {
620
+ const raw = await adapter.getItem(event.key);
621
+ if (raw == null)
622
+ return;
623
+ const merged = hydrateConfig?.merge
624
+ ? hydrateConfig.merge({
625
+ initial: initialState,
626
+ persisted: raw,
627
+ })
628
+ : deepMerge(initialState, raw);
629
+ applyExternalState(merged);
630
+ }
631
+ else {
632
+ const slice = slices.find((candidate) => candidate.key === unit.sliceKey);
633
+ if (!slice)
634
+ return;
635
+ const raw = await adapter.getItem(event.key);
636
+ if (raw == null)
637
+ return;
638
+ applyExternalState(slice.apply(store.state, raw));
639
+ }
640
+ };
641
+ const pendingAdapterEvents = [];
642
+ if (adapter?.subscribe) {
643
+ adapter.subscribe((event) => {
644
+ if (!isReady) {
645
+ pendingAdapterEvents.push(event);
646
+ return;
647
+ }
648
+ void handleAdapterEvent(event);
649
+ });
650
+ }
651
+ // fallow-ignore-next-line complexity
652
+ const readPersistedSnapshot = async () => {
653
+ if (!adapter || !storeId)
654
+ return undefined;
655
+ if (strategy === "entire") {
656
+ const raw = await adapter.getItem(resolveKey({ kind: "entire" }));
657
+ return raw == null ? undefined : raw;
658
+ }
659
+ const result = createDataRecord();
660
+ for (const slice of slices) {
661
+ const raw = await adapter.getItem(resolveKey({ kind: "slice", sliceKey: slice.key }));
662
+ if (raw == null)
663
+ continue;
664
+ defineRecordValue(result, slice.key, raw);
665
+ }
666
+ return result;
667
+ };
668
+ const comparePersistenceValue = (memory, persisted) => deepEqual(memory, persisted) ? "synchronized" : "diverged";
669
+ const resolveSliceSynchronization = (persisted) => {
670
+ if (typeof persisted !== "object" ||
671
+ persisted === null ||
672
+ Array.isArray(persisted))
673
+ return "diverged";
674
+ for (const slice of slices) {
675
+ const descriptor = Object.getOwnPropertyDescriptor(persisted, slice.key);
676
+ if (!descriptor || !("value" in descriptor))
677
+ return "diverged";
678
+ if (comparePersistenceValue(slice.select(store.state), descriptor.value) ===
679
+ "diverged")
680
+ return "diverged";
681
+ }
682
+ return "synchronized";
683
+ };
684
+ const resolveSynchronization = (persisted) => {
685
+ if (!adapter)
686
+ return "not_applicable";
687
+ try {
688
+ return strategy === "entire"
689
+ ? comparePersistenceValue(store.state, persisted)
690
+ : resolveSliceSynchronization(persisted);
691
+ }
692
+ catch {
693
+ return "unknown";
694
+ }
695
+ };
696
+ let snapshotSequence = 0;
697
+ const emitDevtoolsSnapshot = async () => {
698
+ if (!devtoolsEmitter)
699
+ return undefined;
700
+ const sequence = ++snapshotSequence;
701
+ try {
702
+ const persisted = await readPersistedSnapshot();
703
+ if (sequence !== snapshotSequence)
704
+ return undefined;
705
+ devtoolsEmitter.emitSnapshot({
706
+ adapterName: adapter?.name,
707
+ eventLogCap,
708
+ hydration: getHydrationSnapshot(),
709
+ initial: initialState,
710
+ isReady,
711
+ lastFlushAt,
712
+ memory: store.state,
713
+ pendingKeys: Array.from(queuedPayloads.keys()),
714
+ persisted,
715
+ persistenceStrategy: adapter ? strategy : undefined,
716
+ storeId: storeIdValue,
717
+ synchronization: resolveSynchronization(persisted),
718
+ });
719
+ return undefined;
720
+ }
721
+ catch (error) {
722
+ const message = error instanceof Error ? error.message : String(error);
723
+ devtoolsEmitter.emitEvent({
724
+ detail: message,
725
+ storeId: storeIdValue,
726
+ ts: Date.now(),
727
+ type: "devtools_snapshot_error",
728
+ });
729
+ onError?.(error);
730
+ return message;
731
+ }
732
+ };
733
+ const supersedePendingPersistence = (operation) => {
734
+ supersededPersistenceState = { value: store.state };
735
+ persistenceGeneration += 1;
736
+ for (const key of persistenceUnits.keys()) {
737
+ queuedPayloads.delete(key);
738
+ queuedGenerations.delete(key);
739
+ }
740
+ return serializePersistenceOperation(operation);
741
+ };
742
+ const assertDevtoolsValue = (value) => {
743
+ if (!isRoundTrippableForDevtools(value))
744
+ throw new Error("Devtools mutations require round-trippable JSON data.");
745
+ try {
746
+ structuredClone(value);
747
+ }
748
+ catch {
749
+ throw new Error("Devtools mutations do not support Proxy values.");
750
+ }
751
+ };
752
+ const requirePersistenceAdapter = () => {
753
+ if (!adapter || !storeId)
754
+ throw new Error("This store has no configured persistence adapter.");
755
+ return adapter;
756
+ };
757
+ const createSliceWriteUnits = (value) => {
758
+ if (typeof value !== "object" || value === null || Array.isArray(value))
759
+ throw new Error("Slice persistence requires an object keyed by slice name.");
760
+ const values = value;
761
+ const expectedKeys = slices.map((slice) => slice.key).sort();
762
+ const receivedKeys = Object.keys(values).sort();
763
+ const keysMatch = expectedKeys.length === receivedKeys.length &&
764
+ expectedKeys.every((key, index) => key === receivedKeys[index]);
765
+ if (!keysMatch)
766
+ throw new Error(`Slice persistence requires exactly these keys: ${expectedKeys.join(", ")}.`);
767
+ return slices.map((slice) => ({
768
+ key: resolveKey({ kind: "slice", sliceKey: slice.key }),
769
+ value: values[slice.key],
770
+ }));
771
+ };
772
+ const rollbackSliceWrites = async (persistenceAdapter, committed, writeError) => {
773
+ let rollbackError;
774
+ for (const { prior, unit } of committed.reverse()) {
775
+ try {
776
+ if (prior == null)
777
+ await recordAdapterWrite(unit.key, () => persistenceAdapter.removeItem(unit.key));
778
+ else
779
+ await recordAdapterWrite(unit.key, () => persistenceAdapter.setItem(unit.key, prior));
780
+ }
781
+ catch (error) {
782
+ rollbackError = rollbackError ?? error;
783
+ }
784
+ }
785
+ if (rollbackError)
786
+ throw new AggregateError([writeError, rollbackError], "Slice write failed and rollback was incomplete.");
787
+ throw writeError;
788
+ };
789
+ const writeSliceUnits = async (persistenceAdapter, units) => {
790
+ const previous = await Promise.all(units.map((unit) => persistenceAdapter.getItem(unit.key)));
791
+ const committed = [];
792
+ try {
793
+ for (const [index, unit] of units.entries()) {
794
+ await recordAdapterWrite(unit.key, () => persistenceAdapter.setItem(unit.key, unit.value));
795
+ committed.push({ prior: previous[index], unit });
796
+ }
797
+ }
798
+ catch (writeError) {
799
+ await rollbackSliceWrites(persistenceAdapter, committed, writeError);
800
+ }
801
+ };
802
+ const writePersistedValue = async (value) => {
803
+ const persistenceAdapter = requirePersistenceAdapter();
804
+ assertDevtoolsValue(value);
805
+ if (strategy === "entire") {
806
+ const key = resolveKey({ kind: "entire" });
807
+ await recordAdapterWrite(key, () => persistenceAdapter.setItem(key, value));
808
+ return;
809
+ }
810
+ await writeSliceUnits(persistenceAdapter, createSliceWriteUnits(value));
811
+ };
812
+ const replacePersistedValue = (value) => supersedePendingPersistence(() => writePersistedValue(value));
813
+ const readPersistedMutationBase = async () => {
814
+ if (strategy !== "entire")
815
+ throw new Error("Whole persisted mutation is only valid for entire persistence.");
816
+ return readPersistedSnapshot();
817
+ };
818
+ const mutatePersistedValue = (mutation) => supersedePendingPersistence(async () => {
819
+ const persisted = await readPersistedMutationBase();
820
+ assertDevtoolsValue(persisted);
821
+ await writePersistedValue(mutation(persisted));
822
+ });
823
+ const setPersistedValueAtPath = (path, value) => {
824
+ if (strategy === "entire") {
825
+ return mutatePersistedValue((persisted) => setValueAtPath(persisted, path, value));
826
+ }
827
+ if (!adapter || !storeId) {
828
+ throw new Error("This store has no configured persistence adapter.");
829
+ }
830
+ const [sliceKey, ...nestedPath] = path;
831
+ const slice = slices.find((candidate) => typeof sliceKey === "string" && candidate.key === sliceKey);
832
+ if (!slice)
833
+ throw new Error("Persisted slice path must start with a configured slice key.");
834
+ return supersedePendingPersistence(async () => {
835
+ const key = resolveKey({ kind: "slice", sliceKey: slice.key });
836
+ if (nestedPath.length === 0) {
837
+ assertDevtoolsValue(value);
838
+ await recordAdapterWrite(key, () => adapter.setItem(key, value));
839
+ return;
840
+ }
841
+ const current = await adapter.getItem(key);
842
+ const base = current == null ? slice.select(initialState) : current;
843
+ assertDevtoolsValue(base);
844
+ const next = setValueAtPath(base, nestedPath, value);
845
+ assertDevtoolsValue(next);
846
+ await recordAdapterWrite(key, () => adapter.setItem(key, next));
847
+ });
848
+ };
849
+ const removePersistedValueAtPath = (path) => {
850
+ if (strategy === "entire") {
851
+ return mutatePersistedValue((persisted) => removeValueAtPath(persisted, path));
852
+ }
853
+ if (!adapter || !storeId) {
854
+ throw new Error("This store has no configured persistence adapter.");
855
+ }
856
+ const [sliceKey, ...nestedPath] = path;
857
+ const slice = slices.find((candidate) => typeof sliceKey === "string" && candidate.key === sliceKey);
858
+ if (!slice)
859
+ throw new Error("Persisted slice path must start with a configured slice key.");
860
+ return supersedePendingPersistence(async () => {
861
+ const key = resolveKey({ kind: "slice", sliceKey: slice.key });
862
+ if (nestedPath.length === 0) {
863
+ await recordAdapterWrite(key, () => adapter.removeItem(key));
864
+ return;
865
+ }
866
+ const current = await adapter.getItem(key);
867
+ if (current == null)
868
+ throw new Error("Cannot remove a path from a missing persisted slice.");
869
+ assertDevtoolsValue(current);
870
+ const next = removeValueAtPath(current, nestedPath);
871
+ assertDevtoolsValue(next);
872
+ await recordAdapterWrite(key, () => adapter.setItem(key, next));
873
+ });
874
+ };
875
+ const performPathMutation = async (command) => {
876
+ if (command.action === "set_path") {
877
+ if (command.location === "persisted") {
878
+ await setPersistedValueAtPath(command.path, command.value);
879
+ return;
880
+ }
881
+ assertDevtoolsValue(store.state);
882
+ const nextValue = setValueAtPath(store.state, command.path, command.value);
883
+ assertDevtoolsValue(nextValue);
884
+ applyState(nextValue);
885
+ return;
886
+ }
887
+ if (command.action !== "remove_path")
888
+ return;
889
+ if (command.location === "persisted") {
890
+ await removePersistedValueAtPath(command.path);
891
+ return;
892
+ }
893
+ assertDevtoolsValue(store.state);
894
+ const nextValue = removeValueAtPath(store.state, command.path);
895
+ assertDevtoolsValue(nextValue);
896
+ applyState(nextValue);
897
+ };
898
+ const performDevtoolsMutation = async (command) => {
899
+ if (command.action === "refresh" || command.action === "refresh_all")
900
+ return;
901
+ if (!isReady)
902
+ throw new Error("Store hydration must finish before Devtools mutations.");
903
+ if (command.action === "replace_memory") {
904
+ assertDevtoolsValue(command.value);
905
+ applyState(command.value);
906
+ return;
907
+ }
908
+ if (command.action === "reset_memory") {
909
+ applyState(initialState);
910
+ return;
911
+ }
912
+ if (command.action === "replace_persisted") {
913
+ await replacePersistedValue(command.value);
914
+ return;
915
+ }
916
+ await performPathMutation(command);
917
+ };
918
+ const assertNonEmptyCommandString = (value, field) => {
919
+ if (typeof value !== "string" || value.length === 0)
920
+ throw new Error(`Devtools ${field} must be a non-empty string.`);
921
+ };
922
+ const assertDevtoolsPath = (path) => {
923
+ if (!Array.isArray(path) || path.length === 0)
924
+ throw new Error("Devtools command path must contain safe string or index segments.");
925
+ const valid = path.every((segment) => typeof segment === "string" ||
926
+ (typeof segment === "number" &&
927
+ Number.isInteger(segment) &&
928
+ segment >= 0));
929
+ if (!valid)
930
+ throw new Error("Devtools command path must contain safe string or index segments.");
931
+ };
932
+ const assertDevtoolsCommand = (command) => {
933
+ const record = command;
934
+ const actions = new Set([
935
+ "refresh_all",
936
+ "refresh",
937
+ "reset_memory",
938
+ "replace_memory",
939
+ "replace_persisted",
940
+ "set_path",
941
+ "remove_path",
942
+ ]);
943
+ assertNonEmptyCommandString(record.commandId, "commandId");
944
+ if (typeof record.action !== "string" || !actions.has(record.action))
945
+ throw new Error("Unknown Devtools command action.");
946
+ if (record.action === "refresh_all")
947
+ return;
948
+ (() => {
949
+ assertNonEmptyCommandString(record.instanceId, "command instanceId");
950
+ assertNonEmptyCommandString(record.storeId, "command storeId");
951
+ if (record.storeId !== storeIdValue)
952
+ throw new Error("Devtools command storeId does not match its instance.");
953
+ })();
954
+ if (record.action !== "set_path" && record.action !== "remove_path")
955
+ return;
956
+ if (record.location !== "memory" && record.location !== "persisted")
957
+ throw new Error("Devtools command location must be memory or persisted.");
958
+ assertDevtoolsPath(record.path);
959
+ };
960
+ const applyDevtoolsCommand = async (command) => {
961
+ const record = command;
962
+ if (record.action !== "refresh_all" &&
963
+ record.instanceId !== devtoolsInstanceId)
964
+ return;
965
+ try {
966
+ assertDevtoolsCommand(command);
967
+ await performDevtoolsMutation(command);
968
+ }
969
+ catch (error) {
970
+ const message = error instanceof Error ? error.message : String(error);
971
+ devtoolsEmitter?.emitEvent({
972
+ detail: message,
973
+ storeId: storeIdValue,
974
+ ts: Date.now(),
975
+ type: "devtools_command_error",
976
+ });
977
+ if (typeof record.commandId === "string" &&
978
+ typeof record.action === "string")
979
+ devtoolsEmitter?.emitCommandResult({
980
+ action: command.action,
981
+ commandId: record.commandId,
982
+ error: message,
983
+ status: "error",
984
+ storeId: storeIdValue,
985
+ });
986
+ onError?.(error);
987
+ return;
988
+ }
989
+ const snapshotError = await emitDevtoolsSnapshot();
990
+ devtoolsEmitter?.emitEvent({
991
+ detail: command.action,
992
+ storeId: storeIdValue,
993
+ ts: Date.now(),
994
+ type: "devtools_command",
995
+ });
996
+ devtoolsEmitter?.emitCommandResult({
997
+ action: command.action,
998
+ commandId: command.commandId,
999
+ error: snapshotError,
1000
+ status: resolveCommandStatus(command.action, snapshotError),
1001
+ storeId: storeIdValue,
1002
+ });
1003
+ };
1004
+ devtoolsEmitter?.onCommand((command) => {
1005
+ void applyDevtoolsCommand(command);
1006
+ });
1007
+ subscribeToStore();
1008
+ void hydrate().then(async () => {
1009
+ if (pendingAdapterEvents.length > 0) {
1010
+ for (const event of pendingAdapterEvents.splice(0)) {
1011
+ await handleAdapterEvent(event);
1012
+ }
1013
+ }
1014
+ });
1015
+ // extend store with our ready promise
1016
+ (() => {
1017
+ Object.defineProperty(store, "devtoolsInstanceId", {
1018
+ value: devtoolsInstanceId,
1019
+ });
1020
+ Object.defineProperty(store, "isReady", { get: () => isReady });
1021
+ })();
1022
+ Object.defineProperty(store, "hydration", { get: getHydrationSnapshot });
1023
+ store.ready = ready;
1024
+ return store;
1025
+ }
1026
+ //# sourceMappingURL=nusm.js.map