@ibodr/store 0.0.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.mjs ADDED
@@ -0,0 +1,4367 @@
1
+ import { registerDrawLibraryVersion, assert, STRUCTURED_CLONE_OBJECT_PROTOTYPE, objectMapEntries, structuredClone, uniqueId, objectMapValues, areArraysShallowEqual, isEqual, throttleToNextFrame, filterEntries, getOwnProperty, objectMapKeys, WeakCache, Result, exhaustiveSwitchError } from '@ibodr/utils';
2
+ import { atom, UNINITIALIZED, transact, computed, isUninitialized, RESET_VALUE, withDiff, EMPTY_ARRAY, reactor } from '@ibodr/state';
3
+
4
+ // src/index.ts
5
+
6
+ // src/lib/ImmutableMap.ts
7
+ function smi(i32) {
8
+ return i32 >>> 1 & 1073741824 | i32 & 3221225471;
9
+ }
10
+ var defaultValueOf = Object.prototype.valueOf;
11
+ function hash(o) {
12
+ if (o == null) {
13
+ return hashNullish(o);
14
+ }
15
+ if (typeof o.hashCode === "function") {
16
+ return smi(o.hashCode(o));
17
+ }
18
+ const v = valueOf(o);
19
+ if (v == null) {
20
+ return hashNullish(v);
21
+ }
22
+ switch (typeof v) {
23
+ case "boolean":
24
+ return v ? 1108378657 : 1108378656;
25
+ case "number":
26
+ return hashNumber(v);
27
+ case "string":
28
+ return cachedHashString(v);
29
+ case "object":
30
+ case "function":
31
+ return hashJSObj(v);
32
+ case "symbol":
33
+ return hashSymbol(v);
34
+ default:
35
+ if (typeof v.toString === "function") {
36
+ return hashString(v.toString());
37
+ }
38
+ throw new Error("Value type " + typeof v + " cannot be hashed.");
39
+ }
40
+ }
41
+ function hashNullish(nullish) {
42
+ return nullish === null ? 1108378658 : (
43
+ /* undefined */
44
+ 1108378659
45
+ );
46
+ }
47
+ function hashNumber(n) {
48
+ if (n !== n || n === Infinity) {
49
+ return 0;
50
+ }
51
+ let hash2 = n | 0;
52
+ if (hash2 !== n) {
53
+ hash2 ^= n * 4294967295;
54
+ }
55
+ while (n > 4294967295) {
56
+ n /= 4294967295;
57
+ hash2 ^= n;
58
+ }
59
+ return smi(hash2);
60
+ }
61
+ function cachedHashString(string) {
62
+ let hashed = stringHashCache[string];
63
+ if (hashed === void 0) {
64
+ hashed = hashString(string);
65
+ if (stringHashCacheCount === STRING_HASH_CACHE_SIZE) {
66
+ stringHashCacheCount = 0;
67
+ stringHashCache = {};
68
+ }
69
+ stringHashCache[string] = hashed;
70
+ stringHashCacheCount++;
71
+ }
72
+ return hashed;
73
+ }
74
+ function hashString(string) {
75
+ let hashed = 0;
76
+ for (let ii = 0; ii < string.length; ii++) {
77
+ hashed = 31 * hashed + string.charCodeAt(ii) | 0;
78
+ }
79
+ return smi(hashed);
80
+ }
81
+ function hashSymbol(sym) {
82
+ let hashed = symbolMap[sym];
83
+ if (hashed !== void 0) {
84
+ return hashed;
85
+ }
86
+ hashed = nextHash();
87
+ symbolMap[sym] = hashed;
88
+ return hashed;
89
+ }
90
+ function hashJSObj(obj) {
91
+ let hashed = weakMap.get(obj);
92
+ if (hashed !== void 0) {
93
+ return hashed;
94
+ }
95
+ hashed = nextHash();
96
+ weakMap.set(obj, hashed);
97
+ return hashed;
98
+ }
99
+ function valueOf(obj) {
100
+ return obj.valueOf !== defaultValueOf && typeof obj.valueOf === "function" ? obj.valueOf(obj) : obj;
101
+ }
102
+ function nextHash() {
103
+ const nextHash2 = ++_objHashUID;
104
+ if (_objHashUID & 1073741824) {
105
+ _objHashUID = 0;
106
+ }
107
+ return nextHash2;
108
+ }
109
+ var weakMap = /* @__PURE__ */ new WeakMap();
110
+ var symbolMap = /* @__PURE__ */ Object.create(null);
111
+ var _objHashUID = 0;
112
+ var stringHashCache = {};
113
+ var stringHashCacheCount = 0;
114
+ var STRING_HASH_CACHE_SIZE = 24e3;
115
+ var SHIFT = 5;
116
+ var SIZE = 1 << SHIFT;
117
+ var MASK = SIZE - 1;
118
+ var NOT_SET = {};
119
+ function MakeRef() {
120
+ return { value: false };
121
+ }
122
+ function SetRef(ref) {
123
+ if (ref) {
124
+ ref.value = true;
125
+ }
126
+ }
127
+ function arrCopy(arr, offset = 0) {
128
+ return arr.slice(offset);
129
+ }
130
+ var OwnerID = class {
131
+ };
132
+ var ImmutableMap = class _ImmutableMap {
133
+ // @pragma Construction
134
+ // @ts-ignore
135
+ _root;
136
+ // @ts-ignore
137
+ size;
138
+ // @ts-ignore
139
+ __ownerID;
140
+ // @ts-ignore
141
+ __hash;
142
+ // @ts-ignore
143
+ __altered;
144
+ /**
145
+ * Creates a new ImmutableMap instance.
146
+ *
147
+ * @param value - An iterable of key-value pairs to populate the map, or null/undefined for an empty map
148
+ * @example
149
+ * ```ts
150
+ * // Create from array of pairs
151
+ * const map1 = new ImmutableMap([['a', 1], ['b', 2]])
152
+ *
153
+ * // Create empty map
154
+ * const map2 = new ImmutableMap()
155
+ *
156
+ * // Create from another map
157
+ * const map3 = new ImmutableMap(map1)
158
+ * ```
159
+ */
160
+ constructor(value) {
161
+ return value === void 0 || value === null ? emptyMap() : value instanceof _ImmutableMap ? value : emptyMap().withMutations((map) => {
162
+ for (const [k, v] of value) {
163
+ map.set(k, v);
164
+ }
165
+ });
166
+ }
167
+ /**
168
+ * Gets the value associated with the specified key, with a fallback value.
169
+ *
170
+ * @param k - The key to look up
171
+ * @param notSetValue - The value to return if the key is not found
172
+ * @returns The value associated with the key, or the fallback value if not found
173
+ * @example
174
+ * ```ts
175
+ * const map = new ImmutableMap([['key1', 'value1']])
176
+ * console.log(map.get('key1', 'default')) // 'value1'
177
+ * console.log(map.get('missing', 'default')) // 'default'
178
+ * ```
179
+ */
180
+ get(k, notSetValue) {
181
+ return this._root ? this._root.get(0, void 0, k, notSetValue) : notSetValue;
182
+ }
183
+ /**
184
+ * Returns a new ImmutableMap with the specified key-value pair added or updated.
185
+ * If the key already exists, its value is replaced. Otherwise, a new entry is created.
186
+ *
187
+ * @param k - The key to set
188
+ * @param v - The value to associate with the key
189
+ * @returns A new ImmutableMap with the key-value pair set
190
+ * @example
191
+ * ```ts
192
+ * const map = new ImmutableMap([['a', 1]])
193
+ * const updated = map.set('b', 2) // New map with both 'a' and 'b'
194
+ * const replaced = map.set('a', 10) // New map with 'a' updated to 10
195
+ * ```
196
+ */
197
+ set(k, v) {
198
+ return updateMap(this, k, v);
199
+ }
200
+ /**
201
+ * Returns a new ImmutableMap with the specified key removed.
202
+ * If the key doesn't exist, returns the same map instance.
203
+ *
204
+ * @param k - The key to remove
205
+ * @returns A new ImmutableMap with the key removed, or the same instance if key not found
206
+ * @example
207
+ * ```ts
208
+ * const map = new ImmutableMap([['a', 1], ['b', 2]])
209
+ * const smaller = map.delete('a') // New map with only 'b'
210
+ * const same = map.delete('missing') // Returns original map
211
+ * ```
212
+ */
213
+ delete(k) {
214
+ return updateMap(this, k, NOT_SET);
215
+ }
216
+ /**
217
+ * Returns a new ImmutableMap with all specified keys removed.
218
+ * This is more efficient than calling delete() multiple times.
219
+ *
220
+ * @param keys - An iterable of keys to remove
221
+ * @returns A new ImmutableMap with all specified keys removed
222
+ * @example
223
+ * ```ts
224
+ * const map = new ImmutableMap([['a', 1], ['b', 2], ['c', 3]])
225
+ * const smaller = map.deleteAll(['a', 'c']) // New map with only 'b'
226
+ * ```
227
+ */
228
+ deleteAll(keys) {
229
+ return this.withMutations((map) => {
230
+ for (const key of keys) {
231
+ map.delete(key);
232
+ }
233
+ });
234
+ }
235
+ __ensureOwner(ownerID) {
236
+ if (ownerID === this.__ownerID) {
237
+ return this;
238
+ }
239
+ if (!ownerID) {
240
+ if (this.size === 0) {
241
+ return emptyMap();
242
+ }
243
+ this.__ownerID = ownerID;
244
+ this.__altered = false;
245
+ return this;
246
+ }
247
+ return makeMap(this.size, this._root, ownerID, this.__hash);
248
+ }
249
+ /**
250
+ * Applies multiple mutations efficiently by creating a mutable copy,
251
+ * applying all changes, then returning an immutable result.
252
+ * This is more efficient than chaining multiple set/delete operations.
253
+ *
254
+ * @param fn - Function that receives a mutable copy and applies changes
255
+ * @returns A new ImmutableMap with all mutations applied, or the same instance if no changes
256
+ * @example
257
+ * ```ts
258
+ * const map = new ImmutableMap([['a', 1]])
259
+ * const updated = map.withMutations(mutable => {
260
+ * mutable.set('b', 2)
261
+ * mutable.set('c', 3)
262
+ * mutable.delete('a')
263
+ * }) // Efficiently applies all changes at once
264
+ * ```
265
+ */
266
+ withMutations(fn) {
267
+ const mutable = this.asMutable();
268
+ fn(mutable);
269
+ return mutable.wasAltered() ? mutable.__ensureOwner(this.__ownerID) : this;
270
+ }
271
+ /**
272
+ * Checks if this map instance has been altered during a mutation operation.
273
+ * This is used internally to optimize mutations.
274
+ *
275
+ * @returns True if the map was altered, false otherwise
276
+ * @internal
277
+ */
278
+ wasAltered() {
279
+ return this.__altered;
280
+ }
281
+ /**
282
+ * Returns a mutable copy of this map that can be efficiently modified.
283
+ * Multiple changes to the mutable copy are batched together.
284
+ *
285
+ * @returns A mutable copy of this map
286
+ * @internal
287
+ */
288
+ asMutable() {
289
+ return this.__ownerID ? this : this.__ensureOwner(new OwnerID());
290
+ }
291
+ /**
292
+ * Makes the map iterable, yielding key-value pairs.
293
+ *
294
+ * @returns An iterator over [key, value] pairs
295
+ * @example
296
+ * ```ts
297
+ * const map = new ImmutableMap([['a', 1], ['b', 2]])
298
+ * for (const [key, value] of map) {
299
+ * console.log(key, value) // 'a' 1, then 'b' 2
300
+ * }
301
+ * ```
302
+ */
303
+ [Symbol.iterator]() {
304
+ return this.entries()[Symbol.iterator]();
305
+ }
306
+ /**
307
+ * Returns an iterable of key-value pairs.
308
+ *
309
+ * @returns An iterable over [key, value] pairs
310
+ * @example
311
+ * ```ts
312
+ * const map = new ImmutableMap([['a', 1], ['b', 2]])
313
+ * const entries = Array.from(map.entries()) // [['a', 1], ['b', 2]]
314
+ * ```
315
+ */
316
+ entries() {
317
+ return new MapIterator(this, ITERATE_ENTRIES, false);
318
+ }
319
+ /**
320
+ * Returns an iterable of keys.
321
+ *
322
+ * @returns An iterable over keys
323
+ * @example
324
+ * ```ts
325
+ * const map = new ImmutableMap([['a', 1], ['b', 2]])
326
+ * const keys = Array.from(map.keys()) // ['a', 'b']
327
+ * ```
328
+ */
329
+ keys() {
330
+ return new MapIterator(this, ITERATE_KEYS, false);
331
+ }
332
+ /**
333
+ * Returns an iterable of values.
334
+ *
335
+ * @returns An iterable over values
336
+ * @example
337
+ * ```ts
338
+ * const map = new ImmutableMap([['a', 1], ['b', 2]])
339
+ * const values = Array.from(map.values()) // [1, 2]
340
+ * ```
341
+ */
342
+ values() {
343
+ return new MapIterator(this, ITERATE_VALUES, false);
344
+ }
345
+ };
346
+ var ArrayMapNode = class _ArrayMapNode {
347
+ constructor(ownerID, entries) {
348
+ this.ownerID = ownerID;
349
+ this.entries = entries;
350
+ }
351
+ ownerID;
352
+ entries;
353
+ get(_shift, _keyHash, key, notSetValue) {
354
+ const entries = this.entries;
355
+ for (let ii = 0, len = entries.length; ii < len; ii++) {
356
+ if (Object.is(key, entries[ii][0])) {
357
+ return entries[ii][1];
358
+ }
359
+ }
360
+ return notSetValue;
361
+ }
362
+ update(ownerID, _shift, _keyHash, key, value, didChangeSize, didAlter) {
363
+ const removed = value === NOT_SET;
364
+ const entries = this.entries;
365
+ let idx = 0;
366
+ const len = entries.length;
367
+ for (; idx < len; idx++) {
368
+ if (Object.is(key, entries[idx][0])) {
369
+ break;
370
+ }
371
+ }
372
+ const exists = idx < len;
373
+ if (exists ? entries[idx][1] === value : removed) {
374
+ return this;
375
+ }
376
+ SetRef(didAlter);
377
+ if (removed || !exists) SetRef(didChangeSize);
378
+ if (removed && entries.length === 1) {
379
+ return;
380
+ }
381
+ if (!exists && !removed && entries.length >= MAX_ARRAY_MAP_SIZE) {
382
+ return createNodes(ownerID, entries, key, value);
383
+ }
384
+ const isEditable = ownerID && ownerID === this.ownerID;
385
+ const newEntries = isEditable ? entries : arrCopy(entries);
386
+ if (exists) {
387
+ if (removed) {
388
+ if (idx === len - 1) {
389
+ newEntries.pop();
390
+ } else {
391
+ newEntries[idx] = newEntries.pop();
392
+ }
393
+ } else {
394
+ newEntries[idx] = [key, value];
395
+ }
396
+ } else {
397
+ newEntries.push([key, value]);
398
+ }
399
+ if (isEditable) {
400
+ this.entries = newEntries;
401
+ return this;
402
+ }
403
+ return new _ArrayMapNode(ownerID, newEntries);
404
+ }
405
+ };
406
+ var BitmapIndexedNode = class _BitmapIndexedNode {
407
+ constructor(ownerID, bitmap, nodes) {
408
+ this.ownerID = ownerID;
409
+ this.bitmap = bitmap;
410
+ this.nodes = nodes;
411
+ }
412
+ ownerID;
413
+ bitmap;
414
+ nodes;
415
+ get(shift, keyHash, key, notSetValue) {
416
+ if (keyHash === void 0) {
417
+ keyHash = hash(key);
418
+ }
419
+ const bit = 1 << ((shift === 0 ? keyHash : keyHash >>> shift) & MASK);
420
+ const bitmap = this.bitmap;
421
+ return (bitmap & bit) === 0 ? notSetValue : this.nodes[popCount(bitmap & bit - 1)].get(shift + SHIFT, keyHash, key, notSetValue);
422
+ }
423
+ update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
424
+ if (keyHash === void 0) {
425
+ keyHash = hash(key);
426
+ }
427
+ const keyHashFrag = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
428
+ const bit = 1 << keyHashFrag;
429
+ const bitmap = this.bitmap;
430
+ const exists = (bitmap & bit) !== 0;
431
+ if (!exists && value === NOT_SET) {
432
+ return this;
433
+ }
434
+ const idx = popCount(bitmap & bit - 1);
435
+ const nodes = this.nodes;
436
+ const node = exists ? nodes[idx] : void 0;
437
+ const newNode = updateNode(
438
+ node,
439
+ ownerID,
440
+ shift + SHIFT,
441
+ keyHash,
442
+ key,
443
+ value,
444
+ didChangeSize,
445
+ didAlter
446
+ );
447
+ if (newNode === node) {
448
+ return this;
449
+ }
450
+ if (!exists && newNode && nodes.length >= MAX_BITMAP_INDEXED_SIZE) {
451
+ return expandNodes(ownerID, nodes, bitmap, keyHashFrag, newNode);
452
+ }
453
+ if (exists && !newNode && nodes.length === 2 && isLeafNode(nodes[idx ^ 1])) {
454
+ return nodes[idx ^ 1];
455
+ }
456
+ if (exists && newNode && nodes.length === 1 && isLeafNode(newNode)) {
457
+ return newNode;
458
+ }
459
+ const isEditable = ownerID && ownerID === this.ownerID;
460
+ const newBitmap = exists ? newNode ? bitmap : bitmap ^ bit : bitmap | bit;
461
+ const newNodes = exists ? newNode ? setAt(nodes, idx, newNode, isEditable) : spliceOut(nodes, idx, isEditable) : spliceIn(nodes, idx, newNode, isEditable);
462
+ if (isEditable) {
463
+ this.bitmap = newBitmap;
464
+ this.nodes = newNodes;
465
+ return this;
466
+ }
467
+ return new _BitmapIndexedNode(ownerID, newBitmap, newNodes);
468
+ }
469
+ };
470
+ var HashArrayMapNode = class _HashArrayMapNode {
471
+ constructor(ownerID, count, nodes) {
472
+ this.ownerID = ownerID;
473
+ this.count = count;
474
+ this.nodes = nodes;
475
+ }
476
+ ownerID;
477
+ count;
478
+ nodes;
479
+ get(shift, keyHash, key, notSetValue) {
480
+ if (keyHash === void 0) {
481
+ keyHash = hash(key);
482
+ }
483
+ const idx = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
484
+ const node = this.nodes[idx];
485
+ return node ? node.get(shift + SHIFT, keyHash, key, notSetValue) : notSetValue;
486
+ }
487
+ update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
488
+ if (keyHash === void 0) {
489
+ keyHash = hash(key);
490
+ }
491
+ const idx = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
492
+ const removed = value === NOT_SET;
493
+ const nodes = this.nodes;
494
+ const node = nodes[idx];
495
+ if (removed && !node) {
496
+ return this;
497
+ }
498
+ const newNode = updateNode(
499
+ node,
500
+ ownerID,
501
+ shift + SHIFT,
502
+ keyHash,
503
+ key,
504
+ value,
505
+ didChangeSize,
506
+ didAlter
507
+ );
508
+ if (newNode === node) {
509
+ return this;
510
+ }
511
+ let newCount = this.count;
512
+ if (!node) {
513
+ newCount++;
514
+ } else if (!newNode) {
515
+ newCount--;
516
+ if (newCount < MIN_HASH_ARRAY_MAP_SIZE) {
517
+ return packNodes(ownerID, nodes, newCount, idx);
518
+ }
519
+ }
520
+ const isEditable = ownerID && ownerID === this.ownerID;
521
+ const newNodes = setAt(nodes, idx, newNode, isEditable);
522
+ if (isEditable) {
523
+ this.count = newCount;
524
+ this.nodes = newNodes;
525
+ return this;
526
+ }
527
+ return new _HashArrayMapNode(ownerID, newCount, newNodes);
528
+ }
529
+ };
530
+ var HashCollisionNode = class _HashCollisionNode {
531
+ constructor(ownerID, keyHash, entries) {
532
+ this.ownerID = ownerID;
533
+ this.keyHash = keyHash;
534
+ this.entries = entries;
535
+ }
536
+ ownerID;
537
+ keyHash;
538
+ entries;
539
+ get(shift, keyHash, key, notSetValue) {
540
+ const entries = this.entries;
541
+ for (let ii = 0, len = entries.length; ii < len; ii++) {
542
+ if (Object.is(key, entries[ii][0])) {
543
+ return entries[ii][1];
544
+ }
545
+ }
546
+ return notSetValue;
547
+ }
548
+ update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
549
+ if (keyHash === void 0) {
550
+ keyHash = hash(key);
551
+ }
552
+ const removed = value === NOT_SET;
553
+ if (keyHash !== this.keyHash) {
554
+ if (removed) {
555
+ return this;
556
+ }
557
+ SetRef(didAlter);
558
+ SetRef(didChangeSize);
559
+ return mergeIntoNode(this, ownerID, shift, keyHash, [key, value]);
560
+ }
561
+ const entries = this.entries;
562
+ let idx = 0;
563
+ const len = entries.length;
564
+ for (; idx < len; idx++) {
565
+ if (Object.is(key, entries[idx][0])) {
566
+ break;
567
+ }
568
+ }
569
+ const exists = idx < len;
570
+ if (exists ? entries[idx][1] === value : removed) {
571
+ return this;
572
+ }
573
+ SetRef(didAlter);
574
+ if (removed || !exists) SetRef(didChangeSize);
575
+ if (removed && len === 2) {
576
+ return new ValueNode(ownerID, this.keyHash, entries[idx ^ 1]);
577
+ }
578
+ const isEditable = ownerID && ownerID === this.ownerID;
579
+ const newEntries = isEditable ? entries : arrCopy(entries);
580
+ if (exists) {
581
+ if (removed) {
582
+ if (idx === len - 1) {
583
+ newEntries.pop();
584
+ } else {
585
+ newEntries[idx] = newEntries.pop();
586
+ }
587
+ } else {
588
+ newEntries[idx] = [key, value];
589
+ }
590
+ } else {
591
+ newEntries.push([key, value]);
592
+ }
593
+ if (isEditable) {
594
+ this.entries = newEntries;
595
+ return this;
596
+ }
597
+ return new _HashCollisionNode(ownerID, this.keyHash, newEntries);
598
+ }
599
+ };
600
+ var ValueNode = class _ValueNode {
601
+ constructor(ownerID, keyHash, entry) {
602
+ this.ownerID = ownerID;
603
+ this.keyHash = keyHash;
604
+ this.entry = entry;
605
+ }
606
+ ownerID;
607
+ keyHash;
608
+ entry;
609
+ get(shift, keyHash, key, notSetValue) {
610
+ return Object.is(key, this.entry[0]) ? this.entry[1] : notSetValue;
611
+ }
612
+ update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
613
+ const removed = value === NOT_SET;
614
+ const keyMatch = Object.is(key, this.entry[0]);
615
+ if (keyMatch ? value === this.entry[1] : removed) {
616
+ return this;
617
+ }
618
+ SetRef(didAlter);
619
+ if (removed) {
620
+ SetRef(didChangeSize);
621
+ return;
622
+ }
623
+ if (keyMatch) {
624
+ if (ownerID && ownerID === this.ownerID) {
625
+ this.entry[1] = value;
626
+ return this;
627
+ }
628
+ return new _ValueNode(ownerID, this.keyHash, [key, value]);
629
+ }
630
+ SetRef(didChangeSize);
631
+ return mergeIntoNode(this, ownerID, shift, hash(key), [key, value]);
632
+ }
633
+ };
634
+ var MapIterator = class {
635
+ constructor(map, _type, _reverse) {
636
+ this._type = _type;
637
+ this._reverse = _reverse;
638
+ this._stack = map._root && mapIteratorFrame(map._root);
639
+ }
640
+ _type;
641
+ _reverse;
642
+ _stack;
643
+ [Symbol.iterator]() {
644
+ return this;
645
+ }
646
+ next() {
647
+ const type = this._type;
648
+ let stack = this._stack;
649
+ while (stack) {
650
+ const node = stack.node;
651
+ const index = stack.index++;
652
+ let maxIndex;
653
+ if (node.entry) {
654
+ if (index === 0) {
655
+ return mapIteratorValue(type, node.entry);
656
+ }
657
+ } else if ("entries" in node && node.entries) {
658
+ maxIndex = node.entries.length - 1;
659
+ if (index <= maxIndex) {
660
+ return mapIteratorValue(type, node.entries[this._reverse ? maxIndex - index : index]);
661
+ }
662
+ } else {
663
+ maxIndex = node.nodes.length - 1;
664
+ if (index <= maxIndex) {
665
+ const subNode = node.nodes[this._reverse ? maxIndex - index : index];
666
+ if (subNode) {
667
+ if (subNode.entry) {
668
+ return mapIteratorValue(type, subNode.entry);
669
+ }
670
+ stack = this._stack = mapIteratorFrame(subNode, stack);
671
+ }
672
+ continue;
673
+ }
674
+ }
675
+ stack = this._stack = this._stack.__prev;
676
+ }
677
+ return iteratorDone();
678
+ }
679
+ };
680
+ function mapIteratorValue(type, entry) {
681
+ return iteratorValue(type, entry[0], entry[1]);
682
+ }
683
+ function mapIteratorFrame(node, prev) {
684
+ return {
685
+ node,
686
+ index: 0,
687
+ __prev: prev
688
+ };
689
+ }
690
+ var ITERATE_KEYS = 0;
691
+ var ITERATE_VALUES = 1;
692
+ var ITERATE_ENTRIES = 2;
693
+ function iteratorValue(type, k, v, iteratorResult) {
694
+ const value = type === ITERATE_KEYS ? k : type === ITERATE_VALUES ? v : [k, v];
695
+ if (iteratorResult) {
696
+ iteratorResult.value = value;
697
+ } else {
698
+ iteratorResult = { value, done: false };
699
+ }
700
+ return iteratorResult;
701
+ }
702
+ function iteratorDone() {
703
+ return { value: void 0, done: true };
704
+ }
705
+ function makeMap(size, root, ownerID, hash2) {
706
+ const map = Object.create(ImmutableMap.prototype);
707
+ map.size = size;
708
+ map._root = root;
709
+ map.__ownerID = ownerID;
710
+ map.__hash = hash2;
711
+ map.__altered = false;
712
+ return map;
713
+ }
714
+ var EMPTY_MAP;
715
+ function emptyMap() {
716
+ return EMPTY_MAP || (EMPTY_MAP = makeMap(0));
717
+ }
718
+ function updateMap(map, k, v) {
719
+ let newRoot;
720
+ let newSize;
721
+ if (!map._root) {
722
+ if (v === NOT_SET) {
723
+ return map;
724
+ }
725
+ newSize = 1;
726
+ newRoot = new ArrayMapNode(map.__ownerID, [[k, v]]);
727
+ } else {
728
+ const didChangeSize = MakeRef();
729
+ const didAlter = MakeRef();
730
+ newRoot = updateNode(map._root, map.__ownerID, 0, void 0, k, v, didChangeSize, didAlter);
731
+ if (!didAlter.value) {
732
+ return map;
733
+ }
734
+ newSize = map.size + (didChangeSize.value ? v === NOT_SET ? -1 : 1 : 0);
735
+ }
736
+ if (map.__ownerID) {
737
+ map.size = newSize;
738
+ map._root = newRoot;
739
+ map.__hash = void 0;
740
+ map.__altered = true;
741
+ return map;
742
+ }
743
+ return newRoot ? makeMap(newSize, newRoot) : emptyMap();
744
+ }
745
+ function updateNode(node, ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
746
+ if (!node) {
747
+ if (value === NOT_SET) {
748
+ return node;
749
+ }
750
+ SetRef(didAlter);
751
+ SetRef(didChangeSize);
752
+ return new ValueNode(ownerID, keyHash, [key, value]);
753
+ }
754
+ return node.update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter);
755
+ }
756
+ function isLeafNode(node) {
757
+ return node.constructor === ValueNode || node.constructor === HashCollisionNode;
758
+ }
759
+ function mergeIntoNode(node, ownerID, shift, keyHash, entry) {
760
+ if (node.keyHash === keyHash) {
761
+ return new HashCollisionNode(ownerID, keyHash, [node.entry, entry]);
762
+ }
763
+ const idx1 = (shift === 0 ? node.keyHash : node.keyHash >>> shift) & MASK;
764
+ const idx2 = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
765
+ let newNode;
766
+ const nodes = idx1 === idx2 ? [mergeIntoNode(node, ownerID, shift + SHIFT, keyHash, entry)] : (newNode = new ValueNode(ownerID, keyHash, entry), idx1 < idx2 ? [node, newNode] : [newNode, node]);
767
+ return new BitmapIndexedNode(ownerID, 1 << idx1 | 1 << idx2, nodes);
768
+ }
769
+ function createNodes(ownerID, entries, key, value) {
770
+ if (!ownerID) {
771
+ ownerID = new OwnerID();
772
+ }
773
+ let node = new ValueNode(ownerID, hash(key), [key, value]);
774
+ for (let ii = 0; ii < entries.length; ii++) {
775
+ const entry = entries[ii];
776
+ node = node.update(ownerID, 0, void 0, entry[0], entry[1]);
777
+ }
778
+ return node;
779
+ }
780
+ function packNodes(ownerID, nodes, count, excluding) {
781
+ let bitmap = 0;
782
+ let packedII = 0;
783
+ const packedNodes = new Array(count);
784
+ for (let ii = 0, bit = 1, len = nodes.length; ii < len; ii++, bit <<= 1) {
785
+ const node = nodes[ii];
786
+ if (node !== void 0 && ii !== excluding) {
787
+ bitmap |= bit;
788
+ packedNodes[packedII++] = node;
789
+ }
790
+ }
791
+ return new BitmapIndexedNode(ownerID, bitmap, packedNodes);
792
+ }
793
+ function expandNodes(ownerID, nodes, bitmap, including, node) {
794
+ let count = 0;
795
+ const expandedNodes = new Array(SIZE);
796
+ for (let ii = 0; bitmap !== 0; ii++, bitmap >>>= 1) {
797
+ expandedNodes[ii] = bitmap & 1 ? nodes[count++] : void 0;
798
+ }
799
+ expandedNodes[including] = node;
800
+ return new HashArrayMapNode(ownerID, count + 1, expandedNodes);
801
+ }
802
+ function popCount(x) {
803
+ x -= x >> 1 & 1431655765;
804
+ x = (x & 858993459) + (x >> 2 & 858993459);
805
+ x = x + (x >> 4) & 252645135;
806
+ x += x >> 8;
807
+ x += x >> 16;
808
+ return x & 127;
809
+ }
810
+ function setAt(array, idx, val, canEdit) {
811
+ const newArray = canEdit ? array : arrCopy(array);
812
+ newArray[idx] = val;
813
+ return newArray;
814
+ }
815
+ function spliceIn(array, idx, val, canEdit) {
816
+ const newLen = array.length + 1;
817
+ if (canEdit && idx + 1 === newLen) {
818
+ array[idx] = val;
819
+ return array;
820
+ }
821
+ const newArray = new Array(newLen);
822
+ let after = 0;
823
+ for (let ii = 0; ii < newLen; ii++) {
824
+ if (ii === idx) {
825
+ newArray[ii] = val;
826
+ after = -1;
827
+ } else {
828
+ newArray[ii] = array[ii + after];
829
+ }
830
+ }
831
+ return newArray;
832
+ }
833
+ function spliceOut(array, idx, canEdit) {
834
+ const newLen = array.length - 1;
835
+ if (canEdit && idx === newLen) {
836
+ array.pop();
837
+ return array;
838
+ }
839
+ const newArray = new Array(newLen);
840
+ let after = 0;
841
+ for (let ii = 0; ii < newLen; ii++) {
842
+ if (ii === idx) {
843
+ after = 1;
844
+ }
845
+ newArray[ii] = array[ii + after];
846
+ }
847
+ return newArray;
848
+ }
849
+ var MAX_ARRAY_MAP_SIZE = SIZE / 4;
850
+ var MAX_BITMAP_INDEXED_SIZE = SIZE / 2;
851
+ var MIN_HASH_ARRAY_MAP_SIZE = SIZE / 4;
852
+
853
+ // src/lib/AtomMap.ts
854
+ var AtomMap = class {
855
+ /**
856
+ * Creates a new AtomMap instance.
857
+ *
858
+ * name - A unique name for this map, used for atom identification
859
+ * entries - Optional initial entries to populate the map with
860
+ * @example
861
+ * ```ts
862
+ * // Create an empty map
863
+ * const map = new AtomMap('userMap')
864
+ *
865
+ * // Create a map with initial data
866
+ * const initialData: [string, number][] = [['a', 1], ['b', 2]]
867
+ * const mapWithData = new AtomMap('numbersMap', initialData)
868
+ * ```
869
+ */
870
+ constructor(name, entries) {
871
+ this.name = name;
872
+ let atoms = emptyMap();
873
+ if (entries) {
874
+ atoms = atoms.withMutations((atoms2) => {
875
+ for (const [k, v] of entries) {
876
+ atoms2.set(k, atom(`${name}:${String(k)}`, v));
877
+ }
878
+ });
879
+ }
880
+ this.atoms = atom(`${name}:atoms`, atoms);
881
+ }
882
+ name;
883
+ atoms;
884
+ /**
885
+ * Retrieves the underlying atom for a given key.
886
+ *
887
+ * @param key - The key to retrieve the atom for
888
+ * @returns The atom containing the value, or undefined if the key doesn't exist
889
+ * @internal
890
+ */
891
+ getAtom(key) {
892
+ const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key);
893
+ if (!valueAtom) {
894
+ this.atoms.get();
895
+ return void 0;
896
+ }
897
+ return valueAtom;
898
+ }
899
+ /**
900
+ * Gets the value associated with a key. Returns undefined if the key doesn't exist.
901
+ * This method is reactive and will cause reactive contexts to update when the value changes.
902
+ *
903
+ * @param key - The key to retrieve the value for
904
+ * @returns The value associated with the key, or undefined if not found
905
+ * @example
906
+ * ```ts
907
+ * const map = new AtomMap('myMap')
908
+ * map.set('name', 'Alice')
909
+ * console.log(map.get('name')) // 'Alice'
910
+ * console.log(map.get('missing')) // undefined
911
+ * ```
912
+ */
913
+ get(key) {
914
+ const value = this.getAtom(key)?.get();
915
+ assert(value !== UNINITIALIZED);
916
+ return value;
917
+ }
918
+ /**
919
+ * Gets the value associated with a key without creating reactive dependencies.
920
+ * This method will not cause reactive contexts to update when the value changes.
921
+ *
922
+ * @param key - The key to retrieve the value for
923
+ * @returns The value associated with the key, or undefined if not found
924
+ * @example
925
+ * ```ts
926
+ * const map = new AtomMap('myMap')
927
+ * map.set('count', 42)
928
+ * const value = map.__unsafe__getWithoutCapture('count') // No reactive subscription
929
+ * ```
930
+ */
931
+ __unsafe__getWithoutCapture(key) {
932
+ const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key);
933
+ if (!valueAtom) return void 0;
934
+ const value = valueAtom.__unsafe__getWithoutCapture();
935
+ assert(value !== UNINITIALIZED);
936
+ return value;
937
+ }
938
+ /**
939
+ * Checks whether a key exists in the map.
940
+ * This method is reactive and will cause reactive contexts to update when keys are added or removed.
941
+ *
942
+ * @param key - The key to check for
943
+ * @returns True if the key exists in the map, false otherwise
944
+ * @example
945
+ * ```ts
946
+ * const map = new AtomMap('myMap')
947
+ * console.log(map.has('name')) // false
948
+ * map.set('name', 'Alice')
949
+ * console.log(map.has('name')) // true
950
+ * ```
951
+ */
952
+ has(key) {
953
+ const valueAtom = this.getAtom(key);
954
+ if (!valueAtom) {
955
+ return false;
956
+ }
957
+ return valueAtom.get() !== UNINITIALIZED;
958
+ }
959
+ /**
960
+ * Checks whether a key exists in the map without creating reactive dependencies.
961
+ * This method will not cause reactive contexts to update when keys are added or removed.
962
+ *
963
+ * @param key - The key to check for
964
+ * @returns True if the key exists in the map, false otherwise
965
+ * @example
966
+ * ```ts
967
+ * const map = new AtomMap('myMap')
968
+ * map.set('active', true)
969
+ * const exists = map.__unsafe__hasWithoutCapture('active') // No reactive subscription
970
+ * ```
971
+ */
972
+ __unsafe__hasWithoutCapture(key) {
973
+ const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key);
974
+ if (!valueAtom) return false;
975
+ assert(valueAtom.__unsafe__getWithoutCapture() !== UNINITIALIZED);
976
+ return true;
977
+ }
978
+ /**
979
+ * Sets a value for the given key. If the key already exists, its value is updated.
980
+ * If the key doesn't exist, a new entry is created.
981
+ *
982
+ * @param key - The key to set the value for
983
+ * @param value - The value to associate with the key
984
+ * @returns This AtomMap instance for method chaining
985
+ * @example
986
+ * ```ts
987
+ * const map = new AtomMap('myMap')
988
+ * map.set('name', 'Alice').set('age', 30)
989
+ * ```
990
+ */
991
+ set(key, value) {
992
+ const existingAtom = this.atoms.__unsafe__getWithoutCapture().get(key);
993
+ if (existingAtom) {
994
+ existingAtom.set(value);
995
+ } else {
996
+ this.atoms.update((atoms) => {
997
+ return atoms.set(key, atom(`${this.name}:${String(key)}`, value));
998
+ });
999
+ }
1000
+ return this;
1001
+ }
1002
+ /**
1003
+ * Updates an existing value using an updater function.
1004
+ *
1005
+ * @param key - The key of the value to update
1006
+ * @param updater - A function that receives the current value and returns the new value
1007
+ * @throws Error if the key doesn't exist in the map
1008
+ * @example
1009
+ * ```ts
1010
+ * const map = new AtomMap('myMap')
1011
+ * map.set('count', 5)
1012
+ * map.update('count', count => count + 1) // count is now 6
1013
+ * ```
1014
+ */
1015
+ update(key, updater) {
1016
+ const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key);
1017
+ if (!valueAtom) {
1018
+ throw new Error(`AtomMap: key ${key} not found`);
1019
+ }
1020
+ const value = valueAtom.__unsafe__getWithoutCapture();
1021
+ assert(value !== UNINITIALIZED);
1022
+ valueAtom.set(updater(value));
1023
+ }
1024
+ /**
1025
+ * Removes a key-value pair from the map.
1026
+ *
1027
+ * @param key - The key to remove
1028
+ * @returns True if the key existed and was removed, false if it didn't exist
1029
+ * @example
1030
+ * ```ts
1031
+ * const map = new AtomMap('myMap')
1032
+ * map.set('temp', 'value')
1033
+ * console.log(map.delete('temp')) // true
1034
+ * console.log(map.delete('missing')) // false
1035
+ * ```
1036
+ */
1037
+ delete(key) {
1038
+ const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key);
1039
+ if (!valueAtom) {
1040
+ return false;
1041
+ }
1042
+ transact(() => {
1043
+ valueAtom.set(UNINITIALIZED);
1044
+ this.atoms.update((atoms) => {
1045
+ return atoms.delete(key);
1046
+ });
1047
+ });
1048
+ return true;
1049
+ }
1050
+ /**
1051
+ * Removes multiple key-value pairs from the map in a single transaction.
1052
+ *
1053
+ * @param keys - An iterable of keys to remove
1054
+ * @returns An array of [key, value] pairs that were actually deleted
1055
+ * @example
1056
+ * ```ts
1057
+ * const map = new AtomMap('myMap')
1058
+ * map.set('a', 1).set('b', 2).set('c', 3)
1059
+ * const deleted = map.deleteMany(['a', 'c', 'missing'])
1060
+ * console.log(deleted) // [['a', 1], ['c', 3]]
1061
+ * ```
1062
+ */
1063
+ deleteMany(keys) {
1064
+ return transact(() => {
1065
+ const deleted = [];
1066
+ const newAtoms = this.atoms.get().withMutations((atoms) => {
1067
+ for (const key of keys) {
1068
+ const valueAtom = atoms.get(key);
1069
+ if (!valueAtom) continue;
1070
+ const oldValue = valueAtom.get();
1071
+ assert(oldValue !== UNINITIALIZED);
1072
+ deleted.push([key, oldValue]);
1073
+ atoms.delete(key);
1074
+ valueAtom.set(UNINITIALIZED);
1075
+ }
1076
+ });
1077
+ if (deleted.length) {
1078
+ this.atoms.set(newAtoms);
1079
+ }
1080
+ return deleted;
1081
+ });
1082
+ }
1083
+ /**
1084
+ * Removes all key-value pairs from the map.
1085
+ *
1086
+ * @example
1087
+ * ```ts
1088
+ * const map = new AtomMap('myMap')
1089
+ * map.set('a', 1).set('b', 2)
1090
+ * map.clear()
1091
+ * console.log(map.size) // 0
1092
+ * ```
1093
+ */
1094
+ clear() {
1095
+ return transact(() => {
1096
+ for (const valueAtom of this.atoms.__unsafe__getWithoutCapture().values()) {
1097
+ valueAtom.set(UNINITIALIZED);
1098
+ }
1099
+ this.atoms.set(emptyMap());
1100
+ });
1101
+ }
1102
+ /**
1103
+ * Returns an iterator that yields [key, value] pairs for each entry in the map.
1104
+ * This method is reactive and will cause reactive contexts to update when entries change.
1105
+ *
1106
+ * @returns A generator that yields [key, value] tuples
1107
+ * @example
1108
+ * ```ts
1109
+ * const map = new AtomMap('myMap')
1110
+ * map.set('a', 1).set('b', 2)
1111
+ * for (const [key, value] of map.entries()) {
1112
+ * console.log(`${key}: ${value}`)
1113
+ * }
1114
+ * ```
1115
+ */
1116
+ *entries() {
1117
+ for (const [key, valueAtom] of this.atoms.get()) {
1118
+ const value = valueAtom.get();
1119
+ assert(value !== UNINITIALIZED);
1120
+ yield [key, value];
1121
+ }
1122
+ }
1123
+ /**
1124
+ * Returns an iterator that yields all keys in the map.
1125
+ * This method is reactive and will cause reactive contexts to update when keys change.
1126
+ *
1127
+ * @returns A generator that yields keys
1128
+ * @example
1129
+ * ```ts
1130
+ * const map = new AtomMap('myMap')
1131
+ * map.set('name', 'Alice').set('age', 30)
1132
+ * for (const key of map.keys()) {
1133
+ * console.log(key) // 'name', 'age'
1134
+ * }
1135
+ * ```
1136
+ */
1137
+ *keys() {
1138
+ for (const key of this.atoms.get().keys()) {
1139
+ yield key;
1140
+ }
1141
+ }
1142
+ /**
1143
+ * Returns an iterator that yields all values in the map.
1144
+ * This method is reactive and will cause reactive contexts to update when values change.
1145
+ *
1146
+ * @returns A generator that yields values
1147
+ * @example
1148
+ * ```ts
1149
+ * const map = new AtomMap('myMap')
1150
+ * map.set('name', 'Alice').set('age', 30)
1151
+ * for (const value of map.values()) {
1152
+ * console.log(value) // 'Alice', 30
1153
+ * }
1154
+ * ```
1155
+ */
1156
+ *values() {
1157
+ for (const valueAtom of this.atoms.get().values()) {
1158
+ const value = valueAtom.get();
1159
+ assert(value !== UNINITIALIZED);
1160
+ yield value;
1161
+ }
1162
+ }
1163
+ /**
1164
+ * The number of key-value pairs in the map.
1165
+ * This property is reactive and will cause reactive contexts to update when the size changes.
1166
+ *
1167
+ * @returns The number of entries in the map
1168
+ * @example
1169
+ * ```ts
1170
+ * const map = new AtomMap('myMap')
1171
+ * console.log(map.size) // 0
1172
+ * map.set('a', 1)
1173
+ * console.log(map.size) // 1
1174
+ * ```
1175
+ */
1176
+ // eslint-disable-next-line tldraw/no-setter-getter
1177
+ get size() {
1178
+ return this.atoms.get().size;
1179
+ }
1180
+ /**
1181
+ * Executes a provided function once for each key-value pair in the map.
1182
+ * This method is reactive and will cause reactive contexts to update when entries change.
1183
+ *
1184
+ * @param callbackfn - Function to execute for each entry
1185
+ * - value - The value of the current entry
1186
+ * - key - The key of the current entry
1187
+ * - map - The AtomMap being traversed
1188
+ * @param thisArg - Value to use as `this` when executing the callback
1189
+ * @example
1190
+ * ```ts
1191
+ * const map = new AtomMap('myMap')
1192
+ * map.set('a', 1).set('b', 2)
1193
+ * map.forEach((value, key) => {
1194
+ * console.log(`${key} = ${value}`)
1195
+ * })
1196
+ * ```
1197
+ */
1198
+ forEach(callbackfn, thisArg) {
1199
+ for (const [key, value] of this.entries()) {
1200
+ callbackfn.call(thisArg, value, key, this);
1201
+ }
1202
+ }
1203
+ /**
1204
+ * Returns the default iterator for the map, which is the same as entries().
1205
+ * This allows the map to be used in for...of loops and other iterable contexts.
1206
+ *
1207
+ * @returns The same iterator as entries()
1208
+ * @example
1209
+ * ```ts
1210
+ * const map = new AtomMap('myMap')
1211
+ * map.set('a', 1).set('b', 2)
1212
+ *
1213
+ * // These are equivalent:
1214
+ * for (const [key, value] of map) {
1215
+ * console.log(`${key}: ${value}`)
1216
+ * }
1217
+ *
1218
+ * for (const [key, value] of map.entries()) {
1219
+ * console.log(`${key}: ${value}`)
1220
+ * }
1221
+ * ```
1222
+ */
1223
+ [Symbol.iterator]() {
1224
+ return this.entries();
1225
+ }
1226
+ /**
1227
+ * The string tag used by Object.prototype.toString for this class.
1228
+ *
1229
+ * @example
1230
+ * ```ts
1231
+ * const map = new AtomMap('myMap')
1232
+ * console.log(Object.prototype.toString.call(map)) // '[object AtomMap]'
1233
+ * ```
1234
+ */
1235
+ [Symbol.toStringTag] = "AtomMap";
1236
+ };
1237
+
1238
+ // src/lib/AtomSet.ts
1239
+ var AtomSet = class {
1240
+ constructor(name, keys) {
1241
+ this.name = name;
1242
+ const entries = keys ? Array.from(keys, (k) => [k, k]) : void 0;
1243
+ this.map = new AtomMap(name, entries);
1244
+ }
1245
+ name;
1246
+ map;
1247
+ add(value) {
1248
+ this.map.set(value, value);
1249
+ return this;
1250
+ }
1251
+ clear() {
1252
+ this.map.clear();
1253
+ }
1254
+ delete(value) {
1255
+ return this.map.delete(value);
1256
+ }
1257
+ forEach(callbackfn, thisArg) {
1258
+ for (const value of this) {
1259
+ callbackfn.call(thisArg, value, value, this);
1260
+ }
1261
+ }
1262
+ has(value) {
1263
+ return this.map.has(value);
1264
+ }
1265
+ // eslint-disable-next-line tldraw/no-setter-getter
1266
+ get size() {
1267
+ return this.map.size;
1268
+ }
1269
+ entries() {
1270
+ return this.map.entries();
1271
+ }
1272
+ keys() {
1273
+ return this.map.keys();
1274
+ }
1275
+ values() {
1276
+ return this.map.keys();
1277
+ }
1278
+ [Symbol.iterator]() {
1279
+ return this.map.keys();
1280
+ }
1281
+ [Symbol.toStringTag] = "AtomSet";
1282
+ };
1283
+
1284
+ // src/lib/isDev.ts
1285
+ var _isDev = false;
1286
+ try {
1287
+ _isDev = true;
1288
+ } catch (_e) {
1289
+ }
1290
+ try {
1291
+ _isDev = _isDev || import.meta.env.DEV || import.meta.env.TEST || import.meta.env.MODE === "development" || import.meta.env.MODE === "test";
1292
+ } catch (_e) {
1293
+ }
1294
+ function isDev() {
1295
+ return _isDev;
1296
+ }
1297
+
1298
+ // src/lib/devFreeze.ts
1299
+ function devFreeze(object) {
1300
+ if (!isDev()) return object;
1301
+ const proto = Object.getPrototypeOf(object);
1302
+ if (proto && !(Array.isArray(object) || proto === Object.prototype || proto === null || proto === STRUCTURED_CLONE_OBJECT_PROTOTYPE)) {
1303
+ console.error("cannot include non-js data in a record", object);
1304
+ throw new Error("cannot include non-js data in a record");
1305
+ }
1306
+ if (Object.isFrozen(object)) {
1307
+ return object;
1308
+ }
1309
+ const propNames = Object.getOwnPropertyNames(object);
1310
+ for (const name of propNames) {
1311
+ const value = object[name];
1312
+ if (value && typeof value === "object") {
1313
+ devFreeze(value);
1314
+ }
1315
+ }
1316
+ return Object.freeze(object);
1317
+ }
1318
+
1319
+ // src/lib/IncrementalSetConstructor.ts
1320
+ var IncrementalSetConstructor = class {
1321
+ constructor(previousValue) {
1322
+ this.previousValue = previousValue;
1323
+ }
1324
+ previousValue;
1325
+ /**
1326
+ * The next value of the set.
1327
+ *
1328
+ * @internal
1329
+ */
1330
+ nextValue;
1331
+ /**
1332
+ * The diff of the set.
1333
+ *
1334
+ * @internal
1335
+ */
1336
+ diff;
1337
+ /**
1338
+ * Gets the result of the incremental set construction if any changes were made.
1339
+ * Returns undefined if no additions or removals occurred.
1340
+ *
1341
+ * @returns An object containing the final set value and the diff of changes,
1342
+ * or undefined if no changes were made
1343
+ *
1344
+ * @example
1345
+ * ```ts
1346
+ * const constructor = new IncrementalSetConstructor(new Set(['a', 'b']))
1347
+ * constructor.add('c')
1348
+ *
1349
+ * const result = constructor.get()
1350
+ * // result = {
1351
+ * // value: Set(['a', 'b', 'c']),
1352
+ * // diff: { added: Set(['c']) }
1353
+ * // }
1354
+ * ```
1355
+ *
1356
+ * @public
1357
+ */
1358
+ get() {
1359
+ const numRemoved = this.diff?.removed?.size ?? 0;
1360
+ const numAdded = this.diff?.added?.size ?? 0;
1361
+ if (numRemoved === 0 && numAdded === 0) {
1362
+ return void 0;
1363
+ }
1364
+ return { value: this.nextValue, diff: this.diff };
1365
+ }
1366
+ /**
1367
+ * Add an item to the set.
1368
+ *
1369
+ * @param item - The item to add.
1370
+ * @param wasAlreadyPresent - Whether the item was already present in the set.
1371
+ * @internal
1372
+ */
1373
+ _add(item, wasAlreadyPresent) {
1374
+ this.nextValue ??= new Set(this.previousValue);
1375
+ this.nextValue.add(item);
1376
+ this.diff ??= {};
1377
+ if (wasAlreadyPresent) {
1378
+ this.diff.removed?.delete(item);
1379
+ } else {
1380
+ this.diff.added ??= /* @__PURE__ */ new Set();
1381
+ this.diff.added.add(item);
1382
+ }
1383
+ }
1384
+ /**
1385
+ * Adds an item to the set. If the item was already present in the original set
1386
+ * and was previously removed during this construction, it will be restored.
1387
+ * If the item is already present and wasn't removed, this is a no-op.
1388
+ *
1389
+ * @param item - The item to add to the set
1390
+ *
1391
+ * @example
1392
+ * ```ts
1393
+ * const constructor = new IncrementalSetConstructor(new Set(['a', 'b']))
1394
+ * constructor.add('c') // Adds new item
1395
+ * constructor.add('a') // No-op, already present
1396
+ * constructor.remove('b')
1397
+ * constructor.add('b') // Restores previously removed item
1398
+ * ```
1399
+ *
1400
+ * @public
1401
+ */
1402
+ add(item) {
1403
+ const wasAlreadyPresent = this.previousValue.has(item);
1404
+ if (wasAlreadyPresent) {
1405
+ const wasRemoved = this.diff?.removed?.has(item);
1406
+ if (!wasRemoved) return;
1407
+ return this._add(item, wasAlreadyPresent);
1408
+ }
1409
+ const isCurrentlyPresent = this.nextValue?.has(item);
1410
+ if (isCurrentlyPresent) return;
1411
+ this._add(item, wasAlreadyPresent);
1412
+ }
1413
+ /**
1414
+ * Remove an item from the set.
1415
+ *
1416
+ * @param item - The item to remove.
1417
+ * @param wasAlreadyPresent - Whether the item was already present in the set.
1418
+ * @internal
1419
+ */
1420
+ _remove(item, wasAlreadyPresent) {
1421
+ this.nextValue ??= new Set(this.previousValue);
1422
+ this.nextValue.delete(item);
1423
+ this.diff ??= {};
1424
+ if (wasAlreadyPresent) {
1425
+ this.diff.removed ??= /* @__PURE__ */ new Set();
1426
+ this.diff.removed.add(item);
1427
+ } else {
1428
+ this.diff.added?.delete(item);
1429
+ }
1430
+ }
1431
+ /**
1432
+ * Removes an item from the set. If the item wasn't present in the original set
1433
+ * and was added during this construction, it will be removed from the added diff.
1434
+ * If the item is not present at all, this is a no-op.
1435
+ *
1436
+ * @param item - The item to remove from the set
1437
+ *
1438
+ * @example
1439
+ * ```ts
1440
+ * const constructor = new IncrementalSetConstructor(new Set(['a', 'b']))
1441
+ * constructor.remove('a') // Removes existing item
1442
+ * constructor.remove('c') // No-op, not present
1443
+ * constructor.add('d')
1444
+ * constructor.remove('d') // Removes recently added item
1445
+ * ```
1446
+ *
1447
+ * @public
1448
+ */
1449
+ remove(item) {
1450
+ const wasAlreadyPresent = this.previousValue.has(item);
1451
+ if (!wasAlreadyPresent) {
1452
+ const wasAdded = this.diff?.added?.has(item);
1453
+ if (!wasAdded) return;
1454
+ return this._remove(item, wasAlreadyPresent);
1455
+ }
1456
+ const hasAlreadyBeenRemoved = this.diff?.removed?.has(item);
1457
+ if (hasAlreadyBeenRemoved) return;
1458
+ this._remove(item, wasAlreadyPresent);
1459
+ }
1460
+ };
1461
+ function squashDependsOn(sequence) {
1462
+ const result = [];
1463
+ for (let i = sequence.length - 1; i >= 0; i--) {
1464
+ const elem = sequence[i];
1465
+ if (!("id" in elem)) {
1466
+ const dependsOn = elem.dependsOn;
1467
+ const prev = result[0];
1468
+ if (prev) {
1469
+ result[0] = {
1470
+ ...prev,
1471
+ dependsOn: dependsOn.concat(prev.dependsOn ?? [])
1472
+ };
1473
+ }
1474
+ } else {
1475
+ result.unshift(elem);
1476
+ }
1477
+ }
1478
+ return result;
1479
+ }
1480
+ function createMigrationSequence({
1481
+ sequence,
1482
+ sequenceId,
1483
+ retroactive = true
1484
+ }) {
1485
+ const migrations = {
1486
+ sequenceId,
1487
+ retroactive,
1488
+ sequence: squashDependsOn(sequence)
1489
+ };
1490
+ validateMigrations(migrations);
1491
+ return migrations;
1492
+ }
1493
+ function createMigrationIds(sequenceId, versions) {
1494
+ return Object.fromEntries(
1495
+ objectMapEntries(versions).map(([key, version]) => [key, `${sequenceId}/${version}`])
1496
+ );
1497
+ }
1498
+ function createRecordMigrationSequence(opts) {
1499
+ const sequenceId = opts.sequenceId;
1500
+ return createMigrationSequence({
1501
+ sequenceId,
1502
+ retroactive: opts.retroactive ?? true,
1503
+ sequence: opts.sequence.map(
1504
+ (m) => "id" in m ? {
1505
+ ...m,
1506
+ scope: "record",
1507
+ filter: (r) => r.typeName === opts.recordType && (m.filter?.(r) ?? true) && (opts.filter?.(r) ?? true)
1508
+ } : m
1509
+ )
1510
+ });
1511
+ }
1512
+ function sortMigrations(migrations) {
1513
+ if (migrations.length === 0) return [];
1514
+ const byId = new Map(migrations.map((m) => [m.id, m]));
1515
+ const dependents = /* @__PURE__ */ new Map();
1516
+ const inDegree = /* @__PURE__ */ new Map();
1517
+ const explicitDeps = /* @__PURE__ */ new Map();
1518
+ for (const m of migrations) {
1519
+ inDegree.set(m.id, 0);
1520
+ dependents.set(m.id, /* @__PURE__ */ new Set());
1521
+ explicitDeps.set(m.id, /* @__PURE__ */ new Set());
1522
+ }
1523
+ for (const m of migrations) {
1524
+ const { version, sequenceId } = parseMigrationId(m.id);
1525
+ const prevId = `${sequenceId}/${version - 1}`;
1526
+ if (byId.has(prevId)) {
1527
+ dependents.get(prevId).add(m.id);
1528
+ inDegree.set(m.id, inDegree.get(m.id) + 1);
1529
+ }
1530
+ if (m.dependsOn) {
1531
+ for (const depId of m.dependsOn) {
1532
+ if (byId.has(depId)) {
1533
+ dependents.get(depId).add(m.id);
1534
+ explicitDeps.get(m.id).add(depId);
1535
+ inDegree.set(m.id, inDegree.get(m.id) + 1);
1536
+ }
1537
+ }
1538
+ }
1539
+ }
1540
+ const ready = migrations.filter((m) => inDegree.get(m.id) === 0);
1541
+ const result = [];
1542
+ const processed = /* @__PURE__ */ new Set();
1543
+ while (ready.length > 0) {
1544
+ let bestCandidate;
1545
+ let bestCandidateScore = -Infinity;
1546
+ for (const m of ready) {
1547
+ let urgencyScore = 0;
1548
+ for (const depId of dependents.get(m.id) || []) {
1549
+ if (!processed.has(depId)) {
1550
+ urgencyScore += 1;
1551
+ if (explicitDeps.get(depId).has(m.id)) {
1552
+ urgencyScore += 100;
1553
+ }
1554
+ }
1555
+ }
1556
+ if (urgencyScore > bestCandidateScore || // Tiebreaker: prefer lower sequence/version
1557
+ urgencyScore === bestCandidateScore && m.id.localeCompare(bestCandidate?.id ?? "") < 0) {
1558
+ bestCandidate = m;
1559
+ bestCandidateScore = urgencyScore;
1560
+ }
1561
+ }
1562
+ const nextMigration = bestCandidate;
1563
+ ready.splice(ready.indexOf(nextMigration), 1);
1564
+ result.push(nextMigration);
1565
+ processed.add(nextMigration.id);
1566
+ for (const depId of dependents.get(nextMigration.id) || []) {
1567
+ if (!processed.has(depId)) {
1568
+ inDegree.set(depId, inDegree.get(depId) - 1);
1569
+ if (inDegree.get(depId) === 0) {
1570
+ ready.push(byId.get(depId));
1571
+ }
1572
+ }
1573
+ }
1574
+ }
1575
+ if (result.length !== migrations.length) {
1576
+ const unprocessed = migrations.filter((m) => !processed.has(m.id));
1577
+ assert(false, `Circular dependency in migrations: ${unprocessed[0].id}`);
1578
+ }
1579
+ return result;
1580
+ }
1581
+ function parseMigrationId(id) {
1582
+ const [sequenceId, version] = id.split("/");
1583
+ return { sequenceId, version: parseInt(version) };
1584
+ }
1585
+ function validateMigrationId(id, expectedSequenceId) {
1586
+ if (expectedSequenceId) {
1587
+ assert(
1588
+ id.startsWith(expectedSequenceId + "/"),
1589
+ `Every migration in sequence '${expectedSequenceId}' must have an id starting with '${expectedSequenceId}/'. Got invalid id: '${id}'`
1590
+ );
1591
+ }
1592
+ assert(id.match(/^(.*?)\/(0|[1-9]\d*)$/), `Invalid migration id: '${id}'`);
1593
+ }
1594
+ function validateMigrations(migrations) {
1595
+ assert(
1596
+ !migrations.sequenceId.includes("/"),
1597
+ `sequenceId cannot contain a '/', got ${migrations.sequenceId}`
1598
+ );
1599
+ assert(migrations.sequenceId.length, "sequenceId must be a non-empty string");
1600
+ if (migrations.sequence.length === 0) {
1601
+ return;
1602
+ }
1603
+ validateMigrationId(migrations.sequence[0].id, migrations.sequenceId);
1604
+ let n = parseMigrationId(migrations.sequence[0].id).version;
1605
+ assert(
1606
+ n === 1,
1607
+ `Expected the first migrationId to be '${migrations.sequenceId}/1' but got '${migrations.sequence[0].id}'`
1608
+ );
1609
+ for (let i = 1; i < migrations.sequence.length; i++) {
1610
+ const id = migrations.sequence[i].id;
1611
+ validateMigrationId(id, migrations.sequenceId);
1612
+ const m = parseMigrationId(id).version;
1613
+ assert(
1614
+ m === n + 1,
1615
+ `Migration id numbers must increase in increments of 1, expected ${migrations.sequenceId}/${n + 1} but got '${migrations.sequence[i].id}'`
1616
+ );
1617
+ n = m;
1618
+ }
1619
+ }
1620
+ var MigrationFailureReason = {
1621
+ IncompatibleSubtype: "incompatible-subtype",
1622
+ UnknownType: "unknown-type",
1623
+ TargetVersionTooNew: "target-version-too-new",
1624
+ TargetVersionTooOld: "target-version-too-old",
1625
+ MigrationError: "migration-error",
1626
+ UnrecognizedSubtype: "unrecognized-subtype"
1627
+ };
1628
+ function createEmptyRecordsDiff() {
1629
+ return { added: {}, updated: {}, removed: {} };
1630
+ }
1631
+ function reverseRecordsDiff(diff) {
1632
+ const result = { added: diff.removed, removed: diff.added, updated: {} };
1633
+ for (const [from, to] of Object.values(diff.updated)) {
1634
+ result.updated[from.id] = [to, from];
1635
+ }
1636
+ return result;
1637
+ }
1638
+ function isRecordsDiffEmpty(diff) {
1639
+ return Object.keys(diff.added).length === 0 && Object.keys(diff.updated).length === 0 && Object.keys(diff.removed).length === 0;
1640
+ }
1641
+ function squashRecordDiffs(diffs, options) {
1642
+ const result = options?.mutateFirstDiff ? diffs[0] : { added: {}, removed: {}, updated: {} };
1643
+ squashRecordDiffsMutable(result, options?.mutateFirstDiff ? diffs.slice(1) : diffs);
1644
+ return result;
1645
+ }
1646
+ function squashRecordDiffsMutable(target, diffs) {
1647
+ for (const diff of diffs) {
1648
+ for (const [id, value] of objectMapEntries(diff.added)) {
1649
+ if (target.removed[id]) {
1650
+ const original = target.removed[id];
1651
+ delete target.removed[id];
1652
+ if (original !== value) {
1653
+ target.updated[id] = [original, value];
1654
+ }
1655
+ } else {
1656
+ target.added[id] = value;
1657
+ }
1658
+ }
1659
+ for (const [id, [_from, to]] of objectMapEntries(diff.updated)) {
1660
+ if (target.added[id]) {
1661
+ target.added[id] = to;
1662
+ delete target.updated[id];
1663
+ delete target.removed[id];
1664
+ continue;
1665
+ }
1666
+ if (target.updated[id]) {
1667
+ target.updated[id] = [target.updated[id][0], to];
1668
+ delete target.removed[id];
1669
+ continue;
1670
+ }
1671
+ target.updated[id] = diff.updated[id];
1672
+ delete target.removed[id];
1673
+ }
1674
+ for (const [id, value] of objectMapEntries(diff.removed)) {
1675
+ if (target.added[id]) {
1676
+ delete target.added[id];
1677
+ } else if (target.updated[id]) {
1678
+ target.removed[id] = target.updated[id][0];
1679
+ delete target.updated[id];
1680
+ } else {
1681
+ target.removed[id] = value;
1682
+ }
1683
+ }
1684
+ }
1685
+ }
1686
+ var RecordType = class _RecordType {
1687
+ /**
1688
+ * Creates a new RecordType instance.
1689
+ *
1690
+ * typeName - The unique type name for records created by this RecordType
1691
+ * config - Configuration object for the RecordType
1692
+ * - createDefaultProperties - Function that returns default properties for new records
1693
+ * - validator - Optional validator function for record validation
1694
+ * - scope - Optional scope determining persistence behavior (defaults to 'document')
1695
+ * - ephemeralKeys - Optional mapping of property names to ephemeral status
1696
+ * @public
1697
+ */
1698
+ constructor(typeName, config) {
1699
+ this.typeName = typeName;
1700
+ this.createDefaultProperties = config.createDefaultProperties;
1701
+ this.validator = config.validator ?? { validate: (r) => r };
1702
+ this.scope = config.scope ?? "document";
1703
+ this.ephemeralKeys = config.ephemeralKeys;
1704
+ const ephemeralKeySet = /* @__PURE__ */ new Set();
1705
+ if (config.ephemeralKeys) {
1706
+ for (const [key, isEphemeral] of objectMapEntries(config.ephemeralKeys)) {
1707
+ if (isEphemeral) ephemeralKeySet.add(key);
1708
+ }
1709
+ }
1710
+ this.ephemeralKeySet = ephemeralKeySet;
1711
+ }
1712
+ typeName;
1713
+ /**
1714
+ * Factory function that creates default properties for new records.
1715
+ * @public
1716
+ */
1717
+ createDefaultProperties;
1718
+ /**
1719
+ * Validator function used to validate records of this type.
1720
+ * @public
1721
+ */
1722
+ validator;
1723
+ /**
1724
+ * Optional configuration specifying which record properties are ephemeral.
1725
+ * Ephemeral properties are not included in snapshots or synchronization.
1726
+ * @public
1727
+ */
1728
+ ephemeralKeys;
1729
+ /**
1730
+ * Set of property names that are marked as ephemeral for efficient lookup.
1731
+ * @public
1732
+ */
1733
+ ephemeralKeySet;
1734
+ /**
1735
+ * The scope that determines how records of this type are persisted and synchronized.
1736
+ * @public
1737
+ */
1738
+ scope;
1739
+ /**
1740
+ * Creates a new record of this type with the given properties.
1741
+ *
1742
+ * Properties are merged with default properties from the RecordType configuration.
1743
+ * If no id is provided, a unique id will be generated automatically.
1744
+ *
1745
+ * @example
1746
+ * ```ts
1747
+ * const book = Book.create({
1748
+ * title: 'The Great Gatsby',
1749
+ * author: 'F. Scott Fitzgerald'
1750
+ * })
1751
+ * // Result: { id: 'book:abc123', typeName: 'book', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', inStock: true }
1752
+ * ```
1753
+ *
1754
+ * @param properties - The properties for the new record, including both required and optional fields
1755
+ * @returns The newly created record with generated id and typeName
1756
+ * @public
1757
+ */
1758
+ create(properties) {
1759
+ const result = {
1760
+ ...this.createDefaultProperties(),
1761
+ id: "id" in properties ? properties.id : this.createId()
1762
+ };
1763
+ for (const [k, v] of Object.entries(properties)) {
1764
+ if (v !== void 0) {
1765
+ result[k] = v;
1766
+ }
1767
+ }
1768
+ result.typeName = this.typeName;
1769
+ return result;
1770
+ }
1771
+ /**
1772
+ * Creates a deep copy of an existing record with a new unique id.
1773
+ *
1774
+ * This method performs a deep clone of all properties while generating a fresh id,
1775
+ * making it useful for duplicating records without id conflicts.
1776
+ *
1777
+ * @example
1778
+ * ```ts
1779
+ * const originalBook = Book.create({ title: '1984', author: 'George Orwell' })
1780
+ * const duplicatedBook = Book.clone(originalBook)
1781
+ * // duplicatedBook has same properties but different id
1782
+ * ```
1783
+ *
1784
+ * @param record - The record to clone
1785
+ * @returns A new record with the same properties but a different id
1786
+ * @public
1787
+ */
1788
+ clone(record) {
1789
+ return { ...structuredClone(record), id: this.createId() };
1790
+ }
1791
+ /**
1792
+ * Create a new ID for this record type.
1793
+ *
1794
+ * @example
1795
+ *
1796
+ * ```ts
1797
+ * const id = recordType.createId()
1798
+ * ```
1799
+ *
1800
+ * @returns The new ID.
1801
+ * @public
1802
+ */
1803
+ createId(customUniquePart) {
1804
+ return this.typeName + ":" + (customUniquePart ?? uniqueId());
1805
+ }
1806
+ /**
1807
+ * Extracts the unique identifier part from a full record id.
1808
+ *
1809
+ * Record ids have the format `typeName:uniquePart`. This method returns just the unique part.
1810
+ *
1811
+ * @example
1812
+ * ```ts
1813
+ * const bookId = Book.createId() // 'book:abc123'
1814
+ * const uniquePart = Book.parseId(bookId) // 'abc123'
1815
+ * ```
1816
+ *
1817
+ * @param id - The full record id to parse
1818
+ * @returns The unique identifier portion after the colon
1819
+ * @throws Error if the id is not valid for this record type
1820
+ * @public
1821
+ */
1822
+ parseId(id) {
1823
+ if (!this.isId(id)) {
1824
+ throw new Error(`ID "${id}" is not a valid ID for type "${this.typeName}"`);
1825
+ }
1826
+ return id.slice(this.typeName.length + 1);
1827
+ }
1828
+ /**
1829
+ * Type guard that checks whether a record belongs to this RecordType.
1830
+ *
1831
+ * This method performs a runtime check by comparing the record's typeName
1832
+ * against this RecordType's typeName.
1833
+ *
1834
+ * @example
1835
+ * ```ts
1836
+ * if (Book.isInstance(someRecord)) {
1837
+ * // someRecord is now typed as a book record
1838
+ * console.log(someRecord.title)
1839
+ * }
1840
+ * ```
1841
+ *
1842
+ * @param record - The record to check, may be undefined
1843
+ * @returns True if the record is an instance of this record type
1844
+ * @public
1845
+ */
1846
+ isInstance(record) {
1847
+ return record?.typeName === this.typeName;
1848
+ }
1849
+ /**
1850
+ * Type guard that checks whether an id string belongs to this RecordType.
1851
+ *
1852
+ * Validates that the id starts with this RecordType's typeName followed by a colon.
1853
+ * This is more efficient than parsing the full id when you only need to verify the type.
1854
+ *
1855
+ * @example
1856
+ * ```ts
1857
+ * if (Book.isId(someId)) {
1858
+ * // someId is now typed as IdOf<BookRecord>
1859
+ * const book = store.get(someId)
1860
+ * }
1861
+ * ```
1862
+ *
1863
+ * @param id - The id string to check, may be undefined
1864
+ * @returns True if the id belongs to this record type
1865
+ * @public
1866
+ */
1867
+ isId(id) {
1868
+ if (!id) return false;
1869
+ for (let i = 0; i < this.typeName.length; i++) {
1870
+ if (id[i] !== this.typeName[i]) return false;
1871
+ }
1872
+ return id[this.typeName.length] === ":";
1873
+ }
1874
+ /**
1875
+ * Create a new RecordType that has the same type name as this RecordType and includes the given
1876
+ * default properties.
1877
+ *
1878
+ * @example
1879
+ *
1880
+ * ```ts
1881
+ * const authorType = createRecordType('author', () => ({ living: true }))
1882
+ * const deadAuthorType = authorType.withDefaultProperties({ living: false })
1883
+ * ```
1884
+ *
1885
+ * @param createDefaultProperties - A function that returns the default properties of the new RecordType.
1886
+ * @returns The new RecordType.
1887
+ */
1888
+ withDefaultProperties(createDefaultProperties) {
1889
+ return new _RecordType(this.typeName, {
1890
+ createDefaultProperties,
1891
+ validator: this.validator,
1892
+ scope: this.scope,
1893
+ ephemeralKeys: this.ephemeralKeys
1894
+ });
1895
+ }
1896
+ /**
1897
+ * Validates a record against this RecordType's validator and returns it with proper typing.
1898
+ *
1899
+ * This method runs the configured validator function and throws an error if validation fails.
1900
+ * If a previous version of the record is provided, it may use optimized validation.
1901
+ *
1902
+ * @example
1903
+ * ```ts
1904
+ * try {
1905
+ * const validBook = Book.validate(untrustedData)
1906
+ * // validBook is now properly typed and validated
1907
+ * } catch (error) {
1908
+ * console.log('Validation failed:', error.message)
1909
+ * }
1910
+ * ```
1911
+ *
1912
+ * @param record - The unknown record data to validate
1913
+ * @param recordBefore - Optional previous version for optimized validation
1914
+ * @returns The validated and properly typed record
1915
+ * @throws Error if validation fails
1916
+ * @public
1917
+ */
1918
+ validate(record, recordBefore) {
1919
+ if (recordBefore && this.validator.validateUsingKnownGoodVersion) {
1920
+ return this.validator.validateUsingKnownGoodVersion(recordBefore, record);
1921
+ }
1922
+ return this.validator.validate(record);
1923
+ }
1924
+ };
1925
+ function createRecordType(typeName, config) {
1926
+ return new RecordType(typeName, {
1927
+ createDefaultProperties: () => ({}),
1928
+ validator: config.validator,
1929
+ scope: config.scope,
1930
+ ephemeralKeys: config.ephemeralKeys
1931
+ });
1932
+ }
1933
+ function assertIdType(id, type) {
1934
+ if (!id || !type.isId(id)) {
1935
+ throw new Error(`string ${JSON.stringify(id)} is not a valid ${type.typeName} id`);
1936
+ }
1937
+ }
1938
+
1939
+ // src/lib/setUtils.ts
1940
+ function intersectSets(sets) {
1941
+ if (sets.length === 0) return /* @__PURE__ */ new Set();
1942
+ const first = sets[0];
1943
+ const rest = sets.slice(1);
1944
+ const result = /* @__PURE__ */ new Set();
1945
+ for (const val of first) {
1946
+ if (rest.every((set) => set.has(val))) {
1947
+ result.add(val);
1948
+ }
1949
+ }
1950
+ return result;
1951
+ }
1952
+ function diffSets(prev, next) {
1953
+ const result = {};
1954
+ for (const val of next) {
1955
+ if (!prev.has(val)) {
1956
+ result.added ??= /* @__PURE__ */ new Set();
1957
+ result.added.add(val);
1958
+ }
1959
+ }
1960
+ for (const val of prev) {
1961
+ if (!next.has(val)) {
1962
+ result.removed ??= /* @__PURE__ */ new Set();
1963
+ result.removed.add(val);
1964
+ }
1965
+ }
1966
+ return result.added || result.removed ? result : void 0;
1967
+ }
1968
+
1969
+ // src/lib/executeQuery.ts
1970
+ function isQueryValueMatcher(value) {
1971
+ if (typeof value !== "object" || value === null) return false;
1972
+ return "eq" in value || "neq" in value || "gt" in value;
1973
+ }
1974
+ function extractMatcherPaths(query, prefix = "") {
1975
+ const paths = [];
1976
+ for (const [key, value] of Object.entries(query)) {
1977
+ const currentPath = prefix ? `${prefix}\\${key}` : key;
1978
+ if (isQueryValueMatcher(value)) {
1979
+ paths.push({ path: currentPath, matcher: value });
1980
+ } else if (typeof value === "object" && value !== null) {
1981
+ paths.push(...extractMatcherPaths(value, currentPath));
1982
+ }
1983
+ }
1984
+ return paths;
1985
+ }
1986
+ function objectMatchesQuery(query, object) {
1987
+ for (const [key, matcher] of Object.entries(query)) {
1988
+ const value = object[key];
1989
+ if (isQueryValueMatcher(matcher)) {
1990
+ if ("eq" in matcher && value !== matcher.eq) return false;
1991
+ if ("neq" in matcher && value === matcher.neq) return false;
1992
+ if ("gt" in matcher && (typeof value !== "number" || value <= matcher.gt)) return false;
1993
+ continue;
1994
+ }
1995
+ if (typeof value !== "object" || value === null) return false;
1996
+ if (!objectMatchesQuery(matcher, value)) {
1997
+ return false;
1998
+ }
1999
+ }
2000
+ return true;
2001
+ }
2002
+ function executeQuery(store, typeName, query) {
2003
+ const matcherPaths = extractMatcherPaths(query);
2004
+ const matchIds = Object.fromEntries(matcherPaths.map(({ path }) => [path, /* @__PURE__ */ new Set()]));
2005
+ for (const { path, matcher } of matcherPaths) {
2006
+ const index = store.index(typeName, path);
2007
+ if ("eq" in matcher) {
2008
+ const ids = index.get().get(matcher.eq);
2009
+ if (ids) {
2010
+ for (const id of ids) {
2011
+ matchIds[path].add(id);
2012
+ }
2013
+ }
2014
+ } else if ("neq" in matcher) {
2015
+ for (const [value, ids] of index.get()) {
2016
+ if (value !== matcher.neq) {
2017
+ for (const id of ids) {
2018
+ matchIds[path].add(id);
2019
+ }
2020
+ }
2021
+ }
2022
+ } else if ("gt" in matcher) {
2023
+ for (const [value, ids] of index.get()) {
2024
+ if (typeof value === "number" && value > matcher.gt) {
2025
+ for (const id of ids) {
2026
+ matchIds[path].add(id);
2027
+ }
2028
+ }
2029
+ }
2030
+ }
2031
+ if (matchIds[path].size === 0) {
2032
+ return /* @__PURE__ */ new Set();
2033
+ }
2034
+ }
2035
+ return intersectSets(Object.values(matchIds));
2036
+ }
2037
+
2038
+ // src/lib/StoreQueries.ts
2039
+ var StoreQueries = class {
2040
+ /**
2041
+ * Creates a new StoreQueries instance.
2042
+ *
2043
+ * recordMap - The atom map containing all records in the store
2044
+ * history - The atom tracking the store's change history with diffs
2045
+ *
2046
+ * @internal
2047
+ */
2048
+ constructor(recordMap, history) {
2049
+ this.recordMap = recordMap;
2050
+ this.history = history;
2051
+ }
2052
+ recordMap;
2053
+ history;
2054
+ /**
2055
+ * A cache of derivations (indexes).
2056
+ *
2057
+ * @internal
2058
+ */
2059
+ indexCache = /* @__PURE__ */ new Map();
2060
+ /**
2061
+ * A cache of derivations (filtered histories).
2062
+ *
2063
+ * @internal
2064
+ */
2065
+ historyCache = /* @__PURE__ */ new Map();
2066
+ /**
2067
+ * @internal
2068
+ */
2069
+ getAllIdsForType(typeName) {
2070
+ const ids = /* @__PURE__ */ new Set();
2071
+ for (const record of this.recordMap.values()) {
2072
+ if (record.typeName === typeName) {
2073
+ ids.add(record.id);
2074
+ }
2075
+ }
2076
+ return ids;
2077
+ }
2078
+ /**
2079
+ * @internal
2080
+ */
2081
+ getRecordById(typeName, id) {
2082
+ const record = this.recordMap.get(id);
2083
+ if (record && record.typeName === typeName) {
2084
+ return record;
2085
+ }
2086
+ return void 0;
2087
+ }
2088
+ /**
2089
+ * Helper to extract nested property value using pre-split path parts.
2090
+ * @internal
2091
+ */
2092
+ getNestedValue(obj, pathParts) {
2093
+ let current = obj;
2094
+ for (const part of pathParts) {
2095
+ if (current == null || typeof current !== "object") return void 0;
2096
+ current = current[part];
2097
+ }
2098
+ return current;
2099
+ }
2100
+ /**
2101
+ * Creates a reactive computed that tracks the change history for records of a specific type.
2102
+ * The returned computed provides incremental diffs showing what records of the given type
2103
+ * have been added, updated, or removed.
2104
+ *
2105
+ * @param typeName - The type name to filter the history by
2106
+ * @returns A computed value containing the current epoch and diffs of changes for the specified type
2107
+ *
2108
+ * @example
2109
+ * ```ts
2110
+ * // Track changes to book records only
2111
+ * const bookHistory = store.query.filterHistory('book')
2112
+ *
2113
+ * // React to book changes
2114
+ * react('book-changes', () => {
2115
+ * const currentEpoch = bookHistory.get()
2116
+ * console.log('Books updated at epoch:', currentEpoch)
2117
+ * })
2118
+ * ```
2119
+ *
2120
+ * @public
2121
+ */
2122
+ filterHistory(typeName) {
2123
+ if (this.historyCache.has(typeName)) {
2124
+ return this.historyCache.get(typeName);
2125
+ }
2126
+ const filtered = computed(
2127
+ "filterHistory:" + typeName,
2128
+ (lastValue, lastComputedEpoch) => {
2129
+ if (isUninitialized(lastValue)) {
2130
+ return this.history.get();
2131
+ }
2132
+ const diff = this.history.getDiffSince(lastComputedEpoch);
2133
+ if (diff === RESET_VALUE) return this.history.get();
2134
+ const res = { added: {}, removed: {}, updated: {} };
2135
+ let numAdded = 0;
2136
+ let numRemoved = 0;
2137
+ let numUpdated = 0;
2138
+ for (const changes of diff) {
2139
+ for (const added of objectMapValues(changes.added)) {
2140
+ if (added.typeName === typeName) {
2141
+ if (res.removed[added.id]) {
2142
+ const original = res.removed[added.id];
2143
+ delete res.removed[added.id];
2144
+ numRemoved--;
2145
+ if (original !== added) {
2146
+ res.updated[added.id] = [original, added];
2147
+ numUpdated++;
2148
+ }
2149
+ } else {
2150
+ res.added[added.id] = added;
2151
+ numAdded++;
2152
+ }
2153
+ }
2154
+ }
2155
+ for (const [from, to] of objectMapValues(changes.updated)) {
2156
+ if (to.typeName === typeName) {
2157
+ if (res.added[to.id]) {
2158
+ res.added[to.id] = to;
2159
+ } else if (res.updated[to.id]) {
2160
+ res.updated[to.id] = [res.updated[to.id][0], to];
2161
+ } else {
2162
+ res.updated[to.id] = [from, to];
2163
+ numUpdated++;
2164
+ }
2165
+ }
2166
+ }
2167
+ for (const removed of objectMapValues(changes.removed)) {
2168
+ if (removed.typeName === typeName) {
2169
+ if (res.added[removed.id]) {
2170
+ delete res.added[removed.id];
2171
+ numAdded--;
2172
+ } else if (res.updated[removed.id]) {
2173
+ res.removed[removed.id] = res.updated[removed.id][0];
2174
+ delete res.updated[removed.id];
2175
+ numUpdated--;
2176
+ numRemoved++;
2177
+ } else {
2178
+ res.removed[removed.id] = removed;
2179
+ numRemoved++;
2180
+ }
2181
+ }
2182
+ }
2183
+ }
2184
+ if (numAdded || numRemoved || numUpdated) {
2185
+ return withDiff(this.history.get(), res);
2186
+ } else {
2187
+ return lastValue;
2188
+ }
2189
+ },
2190
+ { historyLength: 100 }
2191
+ );
2192
+ this.historyCache.set(typeName, filtered);
2193
+ return filtered;
2194
+ }
2195
+ /**
2196
+ * Creates a reactive index that maps property values to sets of record IDs for efficient lookups.
2197
+ * The index automatically updates when records are added, updated, or removed, and results are cached
2198
+ * for performance.
2199
+ *
2200
+ * Supports nested property paths using backslash separator (e.g., 'metadata\\sessionId').
2201
+ *
2202
+ * @param typeName - The type name of records to index
2203
+ * @param path - The property name or backslash-delimited path to index by
2204
+ * @returns A reactive computed containing the index map with change diffs
2205
+ *
2206
+ * @example
2207
+ * ```ts
2208
+ * // Create an index of books by author ID
2209
+ * const booksByAuthor = store.query.index('book', 'authorId')
2210
+ *
2211
+ * // Get all books by a specific author
2212
+ * const authorBooks = booksByAuthor.get().get('author:leguin')
2213
+ * console.log(authorBooks) // Set<RecordId<Book>>
2214
+ *
2215
+ * // Index by nested property using backslash separator
2216
+ * const booksBySession = store.query.index('book', 'metadata\\sessionId')
2217
+ * const sessionBooks = booksBySession.get().get('session:alpha')
2218
+ * ```
2219
+ *
2220
+ * @public
2221
+ */
2222
+ index(typeName, path) {
2223
+ const cacheKey = typeName + ":" + path;
2224
+ if (this.indexCache.has(cacheKey)) {
2225
+ return this.indexCache.get(cacheKey);
2226
+ }
2227
+ const index = this.__uncached_createIndex(typeName, path);
2228
+ this.indexCache.set(cacheKey, index);
2229
+ return index;
2230
+ }
2231
+ /**
2232
+ * Creates a new index without checking the cache. This method performs the actual work
2233
+ * of building the reactive index computation that tracks property values to record ID sets.
2234
+ *
2235
+ * Supports nested property paths using backslash separator.
2236
+ *
2237
+ * @param typeName - The type name of records to index
2238
+ * @param path - The property name or backslash-delimited path to index by
2239
+ * @returns A reactive computed containing the index map with change diffs
2240
+ *
2241
+ * @internal
2242
+ */
2243
+ __uncached_createIndex(typeName, path) {
2244
+ const typeHistory = this.filterHistory(typeName);
2245
+ const pathParts = path.split("\\");
2246
+ const getPropertyValue = pathParts.length > 1 ? (obj) => this.getNestedValue(obj, pathParts) : (obj) => obj[path];
2247
+ const fromScratch = () => {
2248
+ typeHistory.get();
2249
+ const res = /* @__PURE__ */ new Map();
2250
+ for (const record of this.recordMap.values()) {
2251
+ if (record.typeName === typeName) {
2252
+ const value = getPropertyValue(record);
2253
+ if (value !== void 0) {
2254
+ if (!res.has(value)) {
2255
+ res.set(value, /* @__PURE__ */ new Set());
2256
+ }
2257
+ res.get(value).add(record.id);
2258
+ }
2259
+ }
2260
+ }
2261
+ return res;
2262
+ };
2263
+ return computed(
2264
+ "index:" + typeName + ":" + path,
2265
+ (prevValue, lastComputedEpoch) => {
2266
+ if (isUninitialized(prevValue)) return fromScratch();
2267
+ const history = typeHistory.getDiffSince(lastComputedEpoch);
2268
+ if (history === RESET_VALUE) {
2269
+ return fromScratch();
2270
+ }
2271
+ const setConstructors = /* @__PURE__ */ new Map();
2272
+ const add = (value, id) => {
2273
+ let setConstructor = setConstructors.get(value);
2274
+ if (!setConstructor)
2275
+ setConstructor = new IncrementalSetConstructor(
2276
+ prevValue.get(value) ?? /* @__PURE__ */ new Set()
2277
+ );
2278
+ setConstructor.add(id);
2279
+ setConstructors.set(value, setConstructor);
2280
+ };
2281
+ const remove2 = (value, id) => {
2282
+ let set = setConstructors.get(value);
2283
+ if (!set) set = new IncrementalSetConstructor(prevValue.get(value) ?? /* @__PURE__ */ new Set());
2284
+ set.remove(id);
2285
+ setConstructors.set(value, set);
2286
+ };
2287
+ for (const changes of history) {
2288
+ for (const record of objectMapValues(changes.added)) {
2289
+ if (record.typeName === typeName) {
2290
+ const value = getPropertyValue(record);
2291
+ if (value !== void 0) {
2292
+ add(value, record.id);
2293
+ }
2294
+ }
2295
+ }
2296
+ for (const [from, to] of objectMapValues(changes.updated)) {
2297
+ if (to.typeName === typeName) {
2298
+ const prev = getPropertyValue(from);
2299
+ const next = getPropertyValue(to);
2300
+ if (prev !== next) {
2301
+ if (prev !== void 0) {
2302
+ remove2(prev, to.id);
2303
+ }
2304
+ if (next !== void 0) {
2305
+ add(next, to.id);
2306
+ }
2307
+ }
2308
+ }
2309
+ }
2310
+ for (const record of objectMapValues(changes.removed)) {
2311
+ if (record.typeName === typeName) {
2312
+ const value = getPropertyValue(record);
2313
+ if (value !== void 0) {
2314
+ remove2(value, record.id);
2315
+ }
2316
+ }
2317
+ }
2318
+ }
2319
+ let nextValue = void 0;
2320
+ let nextDiff = void 0;
2321
+ for (const [value, setConstructor] of setConstructors) {
2322
+ const result = setConstructor.get();
2323
+ if (!result) continue;
2324
+ if (!nextValue) nextValue = new Map(prevValue);
2325
+ if (!nextDiff) nextDiff = /* @__PURE__ */ new Map();
2326
+ if (result.value.size === 0) {
2327
+ nextValue.delete(value);
2328
+ } else {
2329
+ nextValue.set(value, result.value);
2330
+ }
2331
+ nextDiff.set(value, result.diff);
2332
+ }
2333
+ if (nextValue && nextDiff) {
2334
+ return withDiff(nextValue, nextDiff);
2335
+ }
2336
+ return prevValue;
2337
+ },
2338
+ { historyLength: 100 }
2339
+ );
2340
+ }
2341
+ /**
2342
+ * Creates a reactive query that returns the first record matching the given query criteria.
2343
+ * Returns undefined if no matching record is found. The query automatically updates
2344
+ * when records change.
2345
+ *
2346
+ * @param typeName - The type name of records to query
2347
+ * @param queryCreator - Function that returns the query expression object to match against
2348
+ * @param name - Optional name for the query computation (used for debugging)
2349
+ * @returns A computed value containing the first matching record or undefined
2350
+ *
2351
+ * @example
2352
+ * ```ts
2353
+ * // Find the first book with a specific title
2354
+ * const bookLatheOfHeaven = store.query.record('book', () => ({ title: { eq: 'The Lathe of Heaven' } }))
2355
+ * console.log(bookLatheOfHeaven.get()?.title) // 'The Lathe of Heaven' or undefined
2356
+ *
2357
+ * // Find any book in stock
2358
+ * const anyInStockBook = store.query.record('book', () => ({ inStock: { eq: true } }))
2359
+ * ```
2360
+ *
2361
+ * @public
2362
+ */
2363
+ record(typeName, queryCreator = () => ({}), name = "record:" + typeName + (queryCreator ? ":" + queryCreator.toString() : "")) {
2364
+ const ids = this.ids(typeName, queryCreator, name);
2365
+ return computed(name, () => {
2366
+ for (const id of ids.get()) {
2367
+ return this.recordMap.get(id);
2368
+ }
2369
+ return void 0;
2370
+ });
2371
+ }
2372
+ /**
2373
+ * Creates a reactive query that returns an array of all records matching the given query criteria.
2374
+ * The array automatically updates when records are added, updated, or removed.
2375
+ *
2376
+ * @param typeName - The type name of records to query
2377
+ * @param queryCreator - Function that returns the query expression object to match against
2378
+ * @param name - Optional name for the query computation (used for debugging)
2379
+ * @returns A computed value containing an array of all matching records
2380
+ *
2381
+ * @example
2382
+ * ```ts
2383
+ * // Get all books in stock
2384
+ * const inStockBooks = store.query.records('book', () => ({ inStock: { eq: true } }))
2385
+ * console.log(inStockBooks.get()) // Book[]
2386
+ *
2387
+ * // Get all books by a specific author
2388
+ * const leguinBooks = store.query.records('book', () => ({ authorId: { eq: 'author:leguin' } }))
2389
+ *
2390
+ * // Get all books (no filter)
2391
+ * const allBooks = store.query.records('book')
2392
+ * ```
2393
+ *
2394
+ * @public
2395
+ */
2396
+ records(typeName, queryCreator = () => ({}), name = "records:" + typeName + (queryCreator ? ":" + queryCreator.toString() : "")) {
2397
+ const ids = this.ids(typeName, queryCreator, "ids:" + name);
2398
+ return computed(
2399
+ name,
2400
+ () => {
2401
+ return Array.from(ids.get(), (id) => this.recordMap.get(id));
2402
+ },
2403
+ {
2404
+ isEqual: areArraysShallowEqual
2405
+ }
2406
+ );
2407
+ }
2408
+ /**
2409
+ * Creates a reactive query that returns a set of record IDs matching the given query criteria.
2410
+ * This is more efficient than `records()` when you only need the IDs and not the full record objects.
2411
+ * The set automatically updates with collection diffs when records change.
2412
+ *
2413
+ * @param typeName - The type name of records to query
2414
+ * @param queryCreator - Function that returns the query expression object to match against
2415
+ * @param name - Optional name for the query computation (used for debugging)
2416
+ * @returns A computed value containing a set of matching record IDs with collection diffs
2417
+ *
2418
+ * @example
2419
+ * ```ts
2420
+ * // Get IDs of all books in stock
2421
+ * const inStockBookIds = store.query.ids('book', () => ({ inStock: { eq: true } }))
2422
+ * console.log(inStockBookIds.get()) // Set<RecordId<Book>>
2423
+ *
2424
+ * // Get all book IDs (no filter)
2425
+ * const allBookIds = store.query.ids('book')
2426
+ *
2427
+ * // Use with other queries for efficient lookups
2428
+ * const authorBookIds = store.query.ids('book', () => ({ authorId: { eq: 'author:leguin' } }))
2429
+ * ```
2430
+ *
2431
+ * @public
2432
+ */
2433
+ ids(typeName, queryCreator = () => ({}), name = "ids:" + typeName + (queryCreator ? ":" + queryCreator.toString() : "")) {
2434
+ const typeHistory = this.filterHistory(typeName);
2435
+ const fromScratch = () => {
2436
+ typeHistory.get();
2437
+ const query = queryCreator();
2438
+ if (Object.keys(query).length === 0) {
2439
+ return this.getAllIdsForType(typeName);
2440
+ }
2441
+ return executeQuery(this, typeName, query);
2442
+ };
2443
+ const fromScratchWithDiff = (prevValue) => {
2444
+ const nextValue = fromScratch();
2445
+ const diff = diffSets(prevValue, nextValue);
2446
+ if (diff) {
2447
+ return withDiff(nextValue, diff);
2448
+ } else {
2449
+ return prevValue;
2450
+ }
2451
+ };
2452
+ const cachedQuery = computed("ids_query:" + name, queryCreator, {
2453
+ isEqual
2454
+ });
2455
+ return computed(
2456
+ "query:" + name,
2457
+ (prevValue, lastComputedEpoch) => {
2458
+ const query = cachedQuery.get();
2459
+ if (isUninitialized(prevValue)) {
2460
+ return fromScratch();
2461
+ }
2462
+ if (lastComputedEpoch < cachedQuery.lastChangedEpoch) {
2463
+ return fromScratchWithDiff(prevValue);
2464
+ }
2465
+ const history = typeHistory.getDiffSince(lastComputedEpoch);
2466
+ if (history === RESET_VALUE) {
2467
+ return fromScratchWithDiff(prevValue);
2468
+ }
2469
+ const setConstructor = new IncrementalSetConstructor(
2470
+ prevValue
2471
+ );
2472
+ for (const changes of history) {
2473
+ for (const added of objectMapValues(changes.added)) {
2474
+ if (added.typeName === typeName && objectMatchesQuery(query, added)) {
2475
+ setConstructor.add(added.id);
2476
+ }
2477
+ }
2478
+ for (const [_, updated] of objectMapValues(changes.updated)) {
2479
+ if (updated.typeName === typeName) {
2480
+ if (objectMatchesQuery(query, updated)) {
2481
+ setConstructor.add(updated.id);
2482
+ } else {
2483
+ setConstructor.remove(updated.id);
2484
+ }
2485
+ }
2486
+ }
2487
+ for (const removed of objectMapValues(changes.removed)) {
2488
+ if (removed.typeName === typeName) {
2489
+ setConstructor.remove(removed.id);
2490
+ }
2491
+ }
2492
+ }
2493
+ const result = setConstructor.get();
2494
+ if (!result) {
2495
+ return prevValue;
2496
+ }
2497
+ return withDiff(result.value, result.diff);
2498
+ },
2499
+ { historyLength: 50 }
2500
+ );
2501
+ }
2502
+ /**
2503
+ * Executes a one-time query against the current store state and returns matching records.
2504
+ * This is a non-reactive query that returns results immediately without creating a computed value.
2505
+ * Use this when you need a snapshot of data at a specific point in time.
2506
+ *
2507
+ * @param typeName - The type name of records to query
2508
+ * @param query - The query expression object to match against
2509
+ * @returns An array of records that match the query at the current moment
2510
+ *
2511
+ * @example
2512
+ * ```ts
2513
+ * // Get current in-stock books (non-reactive)
2514
+ * const currentInStockBooks = store.query.exec('book', { inStock: { eq: true } })
2515
+ * console.log(currentInStockBooks) // Book[]
2516
+ *
2517
+ * // Unlike records(), this won't update when the data changes
2518
+ * const staticBookList = store.query.exec('book', { authorId: { eq: 'author:leguin' } })
2519
+ * ```
2520
+ *
2521
+ * @public
2522
+ */
2523
+ exec(typeName, query) {
2524
+ const ids = executeQuery(this, typeName, query);
2525
+ if (ids.size === 0) {
2526
+ return EMPTY_ARRAY;
2527
+ }
2528
+ return Array.from(ids, (id) => this.recordMap.get(id));
2529
+ }
2530
+ };
2531
+
2532
+ // src/lib/StoreSideEffects.ts
2533
+ var StoreSideEffects = class {
2534
+ /**
2535
+ * Creates a new side effects manager for the given store.
2536
+ *
2537
+ * store - The store instance to manage side effects for
2538
+ */
2539
+ constructor(store) {
2540
+ this.store = store;
2541
+ }
2542
+ store;
2543
+ _beforeCreateHandlers = {};
2544
+ _afterCreateHandlers = {};
2545
+ _beforeChangeHandlers = {};
2546
+ _afterChangeHandlers = {};
2547
+ _beforeDeleteHandlers = {};
2548
+ _afterDeleteHandlers = {};
2549
+ _operationCompleteHandlers = [];
2550
+ _isEnabled = true;
2551
+ /**
2552
+ * Checks whether side effects are currently enabled.
2553
+ * When disabled, all side effect handlers are bypassed.
2554
+ *
2555
+ * @returns `true` if side effects are enabled, `false` otherwise
2556
+ * @internal
2557
+ */
2558
+ isEnabled() {
2559
+ return this._isEnabled;
2560
+ }
2561
+ /**
2562
+ * Enables or disables side effects processing.
2563
+ * When disabled, no side effect handlers will be called.
2564
+ *
2565
+ * @param enabled - Whether to enable or disable side effects
2566
+ * @internal
2567
+ */
2568
+ setIsEnabled(enabled) {
2569
+ this._isEnabled = enabled;
2570
+ }
2571
+ /**
2572
+ * Processes all registered 'before create' handlers for a record.
2573
+ * Handlers are called in registration order and can transform the record.
2574
+ *
2575
+ * @param record - The record about to be created
2576
+ * @param source - Whether the change originated from 'user' or 'remote'
2577
+ * @returns The potentially modified record to actually create
2578
+ * @internal
2579
+ */
2580
+ handleBeforeCreate(record, source) {
2581
+ if (!this._isEnabled) return record;
2582
+ const handlers = this._beforeCreateHandlers[record.typeName];
2583
+ if (handlers) {
2584
+ let r = record;
2585
+ for (const handler of handlers) {
2586
+ r = handler(r, source);
2587
+ }
2588
+ return r;
2589
+ }
2590
+ return record;
2591
+ }
2592
+ /**
2593
+ * Processes all registered 'after create' handlers for a record.
2594
+ * Handlers are called in registration order after the record is created.
2595
+ *
2596
+ * @param record - The record that was created
2597
+ * @param source - Whether the change originated from 'user' or 'remote'
2598
+ * @internal
2599
+ */
2600
+ handleAfterCreate(record, source) {
2601
+ if (!this._isEnabled) return;
2602
+ const handlers = this._afterCreateHandlers[record.typeName];
2603
+ if (handlers) {
2604
+ for (const handler of handlers) {
2605
+ handler(record, source);
2606
+ }
2607
+ }
2608
+ }
2609
+ /**
2610
+ * Processes all registered 'before change' handlers for a record.
2611
+ * Handlers are called in registration order and can modify or block the change.
2612
+ *
2613
+ * @param prev - The current version of the record
2614
+ * @param next - The proposed new version of the record
2615
+ * @param source - Whether the change originated from 'user' or 'remote'
2616
+ * @returns The potentially modified record to actually store
2617
+ * @internal
2618
+ */
2619
+ handleBeforeChange(prev, next, source) {
2620
+ if (!this._isEnabled) return next;
2621
+ const handlers = this._beforeChangeHandlers[next.typeName];
2622
+ if (handlers) {
2623
+ let r = next;
2624
+ for (const handler of handlers) {
2625
+ r = handler(prev, r, source);
2626
+ }
2627
+ return r;
2628
+ }
2629
+ return next;
2630
+ }
2631
+ /**
2632
+ * Processes all registered 'after change' handlers for a record.
2633
+ * Handlers are called in registration order after the record is updated.
2634
+ *
2635
+ * @param prev - The previous version of the record
2636
+ * @param next - The new version of the record that was stored
2637
+ * @param source - Whether the change originated from 'user' or 'remote'
2638
+ * @internal
2639
+ */
2640
+ handleAfterChange(prev, next, source) {
2641
+ if (!this._isEnabled) return;
2642
+ const handlers = this._afterChangeHandlers[next.typeName];
2643
+ if (handlers) {
2644
+ for (const handler of handlers) {
2645
+ handler(prev, next, source);
2646
+ }
2647
+ }
2648
+ }
2649
+ /**
2650
+ * Processes all registered 'before delete' handlers for a record.
2651
+ * If any handler returns `false`, the deletion is prevented.
2652
+ *
2653
+ * @param record - The record about to be deleted
2654
+ * @param source - Whether the change originated from 'user' or 'remote'
2655
+ * @returns `true` to allow deletion, `false` to prevent it
2656
+ * @internal
2657
+ */
2658
+ handleBeforeDelete(record, source) {
2659
+ if (!this._isEnabled) return true;
2660
+ const handlers = this._beforeDeleteHandlers[record.typeName];
2661
+ if (handlers) {
2662
+ for (const handler of handlers) {
2663
+ if (handler(record, source) === false) {
2664
+ return false;
2665
+ }
2666
+ }
2667
+ }
2668
+ return true;
2669
+ }
2670
+ /**
2671
+ * Processes all registered 'after delete' handlers for a record.
2672
+ * Handlers are called in registration order after the record is deleted.
2673
+ *
2674
+ * @param record - The record that was deleted
2675
+ * @param source - Whether the change originated from 'user' or 'remote'
2676
+ * @internal
2677
+ */
2678
+ handleAfterDelete(record, source) {
2679
+ if (!this._isEnabled) return;
2680
+ const handlers = this._afterDeleteHandlers[record.typeName];
2681
+ if (handlers) {
2682
+ for (const handler of handlers) {
2683
+ handler(record, source);
2684
+ }
2685
+ }
2686
+ }
2687
+ /**
2688
+ * Processes all registered operation complete handlers.
2689
+ * Called after an atomic store operation finishes.
2690
+ *
2691
+ * @param source - Whether the operation originated from 'user' or 'remote'
2692
+ * @internal
2693
+ */
2694
+ handleOperationComplete(source) {
2695
+ if (!this._isEnabled) return;
2696
+ for (const handler of this._operationCompleteHandlers) {
2697
+ handler(source);
2698
+ }
2699
+ }
2700
+ /**
2701
+ * Internal helper for registering multiple side effect handlers at once and keeping them organized.
2702
+ * This provides a convenient way to register handlers for multiple record types and lifecycle events
2703
+ * in a single call, returning a single cleanup function.
2704
+ *
2705
+ * @param handlersByType - An object mapping record type names to their respective handlers
2706
+ * @returns A function that removes all registered handlers when called
2707
+ *
2708
+ * @example
2709
+ * ```ts
2710
+ * const cleanup = sideEffects.register({
2711
+ * shape: {
2712
+ * afterDelete: (shape) => console.log('Shape deleted:', shape.id),
2713
+ * beforeChange: (prev, next) => ({ ...next, lastModified: Date.now() })
2714
+ * },
2715
+ * arrow: {
2716
+ * afterCreate: (arrow) => updateConnectedShapes(arrow)
2717
+ * }
2718
+ * })
2719
+ *
2720
+ * // Later, remove all handlers
2721
+ * cleanup()
2722
+ * ```
2723
+ *
2724
+ * @internal
2725
+ */
2726
+ register(handlersByType) {
2727
+ const disposes = [];
2728
+ for (const [type, handlers] of Object.entries(handlersByType)) {
2729
+ if (handlers?.beforeCreate) {
2730
+ disposes.push(this.registerBeforeCreateHandler(type, handlers.beforeCreate));
2731
+ }
2732
+ if (handlers?.afterCreate) {
2733
+ disposes.push(this.registerAfterCreateHandler(type, handlers.afterCreate));
2734
+ }
2735
+ if (handlers?.beforeChange) {
2736
+ disposes.push(this.registerBeforeChangeHandler(type, handlers.beforeChange));
2737
+ }
2738
+ if (handlers?.afterChange) {
2739
+ disposes.push(this.registerAfterChangeHandler(type, handlers.afterChange));
2740
+ }
2741
+ if (handlers?.beforeDelete) {
2742
+ disposes.push(this.registerBeforeDeleteHandler(type, handlers.beforeDelete));
2743
+ }
2744
+ if (handlers?.afterDelete) {
2745
+ disposes.push(this.registerAfterDeleteHandler(type, handlers.afterDelete));
2746
+ }
2747
+ }
2748
+ return () => {
2749
+ for (const dispose of disposes) dispose();
2750
+ };
2751
+ }
2752
+ /**
2753
+ * Register a handler to be called before a record of a certain type is created. Return a
2754
+ * modified record from the handler to change the record that will be created.
2755
+ *
2756
+ * Use this handle only to modify the creation of the record itself. If you want to trigger a
2757
+ * side-effect on a different record (for example, moving one shape when another is created),
2758
+ * use {@link StoreSideEffects.registerAfterCreateHandler} instead.
2759
+ *
2760
+ * @example
2761
+ * ```ts
2762
+ * editor.sideEffects.registerBeforeCreateHandler('shape', (shape, source) => {
2763
+ * // only modify shapes created by the user
2764
+ * if (source !== 'user') return shape
2765
+ *
2766
+ * //by default, arrow shapes have no label. Let's make sure they always have a label.
2767
+ * if (shape.type === 'arrow') {
2768
+ * return {...shape, props: {...shape.props, text: 'an arrow'}}
2769
+ * }
2770
+ *
2771
+ * // other shapes get returned unmodified
2772
+ * return shape
2773
+ * })
2774
+ * ```
2775
+ *
2776
+ * @param typeName - The type of record to listen for
2777
+ * @param handler - The handler to call
2778
+ *
2779
+ * @returns A callback that removes the handler.
2780
+ */
2781
+ registerBeforeCreateHandler(typeName, handler) {
2782
+ const handlers = this._beforeCreateHandlers[typeName];
2783
+ if (!handlers) this._beforeCreateHandlers[typeName] = [];
2784
+ this._beforeCreateHandlers[typeName].push(handler);
2785
+ return () => remove(this._beforeCreateHandlers[typeName], handler);
2786
+ }
2787
+ /**
2788
+ * Register a handler to be called after a record is created. This is useful for side-effects
2789
+ * that would update _other_ records. If you want to modify the record being created use
2790
+ * {@link StoreSideEffects.registerBeforeCreateHandler} instead.
2791
+ *
2792
+ * @example
2793
+ * ```ts
2794
+ * editor.sideEffects.registerAfterCreateHandler('page', (page, source) => {
2795
+ * // Automatically create a shape when a page is created
2796
+ * editor.createShape({
2797
+ * id: createShapeId(),
2798
+ * type: 'text',
2799
+ * props: { richText: toRichText(page.name) },
2800
+ * })
2801
+ * })
2802
+ * ```
2803
+ *
2804
+ * @param typeName - The type of record to listen for
2805
+ * @param handler - The handler to call
2806
+ *
2807
+ * @returns A callback that removes the handler.
2808
+ */
2809
+ registerAfterCreateHandler(typeName, handler) {
2810
+ const handlers = this._afterCreateHandlers[typeName];
2811
+ if (!handlers) this._afterCreateHandlers[typeName] = [];
2812
+ this._afterCreateHandlers[typeName].push(handler);
2813
+ return () => remove(this._afterCreateHandlers[typeName], handler);
2814
+ }
2815
+ /**
2816
+ * Register a handler to be called before a record is changed. The handler is given the old and
2817
+ * new record - you can return a modified record to apply a different update, or the old record
2818
+ * to block the update entirely.
2819
+ *
2820
+ * Use this handler only for intercepting updates to the record itself. If you want to update
2821
+ * other records in response to a change, use
2822
+ * {@link StoreSideEffects.registerAfterChangeHandler} instead.
2823
+ *
2824
+ * @example
2825
+ * ```ts
2826
+ * editor.sideEffects.registerBeforeChangeHandler('shape', (prev, next, source) => {
2827
+ * if (next.isLocked && !prev.isLocked) {
2828
+ * // prevent shapes from ever being locked:
2829
+ * return prev
2830
+ * }
2831
+ * // other types of change are allowed
2832
+ * return next
2833
+ * })
2834
+ * ```
2835
+ *
2836
+ * @param typeName - The type of record to listen for
2837
+ * @param handler - The handler to call
2838
+ *
2839
+ * @returns A callback that removes the handler.
2840
+ */
2841
+ registerBeforeChangeHandler(typeName, handler) {
2842
+ const handlers = this._beforeChangeHandlers[typeName];
2843
+ if (!handlers) this._beforeChangeHandlers[typeName] = [];
2844
+ this._beforeChangeHandlers[typeName].push(handler);
2845
+ return () => remove(this._beforeChangeHandlers[typeName], handler);
2846
+ }
2847
+ /**
2848
+ * Register a handler to be called after a record is changed. This is useful for side-effects
2849
+ * that would update _other_ records - if you want to modify the record being changed, use
2850
+ * {@link StoreSideEffects.registerBeforeChangeHandler} instead.
2851
+ *
2852
+ * @example
2853
+ * ```ts
2854
+ * editor.sideEffects.registerAfterChangeHandler('shape', (prev, next, source) => {
2855
+ * if (next.props.color === 'red') {
2856
+ * // there can only be one red shape at a time:
2857
+ * const otherRedShapes = editor.getCurrentPageShapes().filter(s => s.props.color === 'red' && s.id !== next.id)
2858
+ * editor.updateShapes(otherRedShapes.map(s => ({...s, props: {...s.props, color: 'blue'}})))
2859
+ * }
2860
+ * })
2861
+ * ```
2862
+ *
2863
+ * @param typeName - The type of record to listen for
2864
+ * @param handler - The handler to call
2865
+ *
2866
+ * @returns A callback that removes the handler.
2867
+ */
2868
+ registerAfterChangeHandler(typeName, handler) {
2869
+ const handlers = this._afterChangeHandlers[typeName];
2870
+ if (!handlers) this._afterChangeHandlers[typeName] = [];
2871
+ this._afterChangeHandlers[typeName].push(handler);
2872
+ return () => remove(this._afterChangeHandlers[typeName], handler);
2873
+ }
2874
+ /**
2875
+ * Register a handler to be called before a record is deleted. The handler can return `false` to
2876
+ * prevent the deletion.
2877
+ *
2878
+ * Use this handler only for intercepting deletions of the record itself. If you want to do
2879
+ * something to other records in response to a deletion, use
2880
+ * {@link StoreSideEffects.registerAfterDeleteHandler} instead.
2881
+ *
2882
+ * @example
2883
+ * ```ts
2884
+ * editor.sideEffects.registerBeforeDeleteHandler('shape', (shape, source) => {
2885
+ * if (shape.props.color === 'red') {
2886
+ * // prevent red shapes from being deleted
2887
+ * return false
2888
+ * }
2889
+ * })
2890
+ * ```
2891
+ *
2892
+ * @param typeName - The type of record to listen for
2893
+ * @param handler - The handler to call
2894
+ *
2895
+ * @returns A callback that removes the handler.
2896
+ */
2897
+ registerBeforeDeleteHandler(typeName, handler) {
2898
+ const handlers = this._beforeDeleteHandlers[typeName];
2899
+ if (!handlers) this._beforeDeleteHandlers[typeName] = [];
2900
+ this._beforeDeleteHandlers[typeName].push(handler);
2901
+ return () => remove(this._beforeDeleteHandlers[typeName], handler);
2902
+ }
2903
+ /**
2904
+ * Register a handler to be called after a record is deleted. This is useful for side-effects
2905
+ * that would update _other_ records - if you want to block the deletion of the record itself,
2906
+ * use {@link StoreSideEffects.registerBeforeDeleteHandler} instead.
2907
+ *
2908
+ * @example
2909
+ * ```ts
2910
+ * editor.sideEffects.registerAfterDeleteHandler('shape', (shape, source) => {
2911
+ * // if the last shape in a frame is deleted, delete the frame too:
2912
+ * const parentFrame = editor.getShape(shape.parentId)
2913
+ * if (!parentFrame || parentFrame.type !== 'frame') return
2914
+ *
2915
+ * const siblings = editor.getSortedChildIdsForParent(parentFrame)
2916
+ * if (siblings.length === 0) {
2917
+ * editor.deleteShape(parentFrame.id)
2918
+ * }
2919
+ * })
2920
+ * ```
2921
+ *
2922
+ * @param typeName - The type of record to listen for
2923
+ * @param handler - The handler to call
2924
+ *
2925
+ * @returns A callback that removes the handler.
2926
+ */
2927
+ registerAfterDeleteHandler(typeName, handler) {
2928
+ const handlers = this._afterDeleteHandlers[typeName];
2929
+ if (!handlers) this._afterDeleteHandlers[typeName] = [];
2930
+ this._afterDeleteHandlers[typeName].push(handler);
2931
+ return () => remove(this._afterDeleteHandlers[typeName], handler);
2932
+ }
2933
+ /**
2934
+ * Register a handler to be called when a store completes an atomic operation.
2935
+ *
2936
+ * @example
2937
+ * ```ts
2938
+ * let count = 0
2939
+ *
2940
+ * editor.sideEffects.registerOperationCompleteHandler(() => count++)
2941
+ *
2942
+ * editor.selectAll()
2943
+ * expect(count).toBe(1)
2944
+ *
2945
+ * editor.store.atomic(() => {
2946
+ * editor.selectNone()
2947
+ * editor.selectAll()
2948
+ * })
2949
+ *
2950
+ * expect(count).toBe(2)
2951
+ * ```
2952
+ *
2953
+ * @param handler - The handler to call
2954
+ *
2955
+ * @returns A callback that removes the handler.
2956
+ *
2957
+ * @public
2958
+ */
2959
+ registerOperationCompleteHandler(handler) {
2960
+ this._operationCompleteHandlers.push(handler);
2961
+ return () => remove(this._operationCompleteHandlers, handler);
2962
+ }
2963
+ };
2964
+ function remove(array, item) {
2965
+ const index = array.indexOf(item);
2966
+ if (index >= 0) {
2967
+ array.splice(index, 1);
2968
+ }
2969
+ }
2970
+
2971
+ // src/lib/Store.ts
2972
+ var Store = class {
2973
+ /**
2974
+ * The unique identifier of the store instance.
2975
+ *
2976
+ * @public
2977
+ */
2978
+ id;
2979
+ /**
2980
+ * An AtomMap containing the stores records.
2981
+ *
2982
+ * @internal
2983
+ * @readonly
2984
+ */
2985
+ records;
2986
+ /**
2987
+ * An atom containing the store's history.
2988
+ *
2989
+ * @public
2990
+ * @readonly
2991
+ */
2992
+ history = atom("history", 0, {
2993
+ historyLength: 1e3
2994
+ });
2995
+ /**
2996
+ * Reactive queries and indexes for efficiently accessing store data.
2997
+ * Provides methods for filtering, indexing, and subscribing to subsets of records.
2998
+ *
2999
+ * @example
3000
+ * ```ts
3001
+ * // Create an index by a property
3002
+ * const booksByAuthor = store.query.index('book', 'author')
3003
+ *
3004
+ * // Get records matching criteria
3005
+ * const inStockBooks = store.query.records('book', () => ({
3006
+ * inStock: { eq: true }
3007
+ * }))
3008
+ * ```
3009
+ *
3010
+ * @public
3011
+ * @readonly
3012
+ */
3013
+ query;
3014
+ /**
3015
+ * A set containing listeners that have been added to this store.
3016
+ *
3017
+ * @internal
3018
+ */
3019
+ listeners = /* @__PURE__ */ new Set();
3020
+ /**
3021
+ * An array of history entries that have not yet been flushed.
3022
+ *
3023
+ * @internal
3024
+ */
3025
+ historyAccumulator = new HistoryAccumulator();
3026
+ /**
3027
+ * A reactor that responds to changes to the history by squashing the accumulated history and
3028
+ * notifying listeners of the changes.
3029
+ *
3030
+ * @internal
3031
+ */
3032
+ historyReactor;
3033
+ /**
3034
+ * Function to dispose of any in-flight timeouts.
3035
+ *
3036
+ * @internal
3037
+ */
3038
+ cancelHistoryReactor() {
3039
+ }
3040
+ /**
3041
+ * The schema that defines the structure and validation rules for records in this store.
3042
+ *
3043
+ * @public
3044
+ */
3045
+ schema;
3046
+ /**
3047
+ * Custom properties associated with this store instance.
3048
+ *
3049
+ * @public
3050
+ */
3051
+ props;
3052
+ /**
3053
+ * A mapping of record scopes to the set of record type names that belong to each scope.
3054
+ * Used to filter records by their persistence and synchronization behavior.
3055
+ *
3056
+ * @public
3057
+ */
3058
+ scopedTypes;
3059
+ /**
3060
+ * Side effects manager that handles lifecycle events for record operations.
3061
+ * Allows registration of callbacks for create, update, delete, and validation events.
3062
+ *
3063
+ * @example
3064
+ * ```ts
3065
+ * store.sideEffects.registerAfterCreateHandler('book', (book) => {
3066
+ * console.log('Book created:', book.title)
3067
+ * })
3068
+ * ```
3069
+ *
3070
+ * @public
3071
+ */
3072
+ sideEffects = new StoreSideEffects(this);
3073
+ /**
3074
+ * Creates a new Store instance.
3075
+ *
3076
+ * @example
3077
+ * ```ts
3078
+ * const store = new Store({
3079
+ * schema: StoreSchema.create({ book: Book }),
3080
+ * props: { appName: 'MyLibrary' },
3081
+ * initialData: savedData
3082
+ * })
3083
+ * ```
3084
+ *
3085
+ * @param config - Configuration object for the store
3086
+ */
3087
+ constructor(config) {
3088
+ const { initialData, schema, id } = config;
3089
+ this.id = id ?? uniqueId();
3090
+ this.schema = schema;
3091
+ this.props = config.props;
3092
+ if (initialData) {
3093
+ this.records = new AtomMap(
3094
+ "store",
3095
+ objectMapEntries(initialData).map(([id2, record]) => [
3096
+ id2,
3097
+ devFreeze(this.schema.validateRecord(this, record, "initialize", null))
3098
+ ])
3099
+ );
3100
+ } else {
3101
+ this.records = new AtomMap("store");
3102
+ }
3103
+ this.query = new StoreQueries(this.records, this.history);
3104
+ this.historyReactor = reactor(
3105
+ "Store.historyReactor",
3106
+ () => {
3107
+ this.history.get();
3108
+ this._flushHistory();
3109
+ },
3110
+ { scheduleEffect: (cb) => this.cancelHistoryReactor = throttleToNextFrame(cb) }
3111
+ );
3112
+ this.scopedTypes = {
3113
+ document: new Set(
3114
+ objectMapValues(this.schema.types).filter((t) => t.scope === "document").map((t) => t.typeName)
3115
+ ),
3116
+ session: new Set(
3117
+ objectMapValues(this.schema.types).filter((t) => t.scope === "session").map((t) => t.typeName)
3118
+ ),
3119
+ presence: new Set(
3120
+ objectMapValues(this.schema.types).filter((t) => t.scope === "presence").map((t) => t.typeName)
3121
+ )
3122
+ };
3123
+ }
3124
+ _flushHistory() {
3125
+ if (this.historyAccumulator.hasChanges()) {
3126
+ const entries = this.historyAccumulator.flush();
3127
+ for (const { changes, source } of entries) {
3128
+ let instanceChanges = null;
3129
+ let documentChanges = null;
3130
+ let presenceChanges = null;
3131
+ for (const { onHistory, filters } of this.listeners) {
3132
+ if (filters.source !== "all" && filters.source !== source) {
3133
+ continue;
3134
+ }
3135
+ if (filters.scope !== "all") {
3136
+ if (filters.scope === "document") {
3137
+ documentChanges ??= this.filterChangesByScope(changes, "document");
3138
+ if (!documentChanges) continue;
3139
+ onHistory({ changes: documentChanges, source });
3140
+ } else if (filters.scope === "session") {
3141
+ instanceChanges ??= this.filterChangesByScope(changes, "session");
3142
+ if (!instanceChanges) continue;
3143
+ onHistory({ changes: instanceChanges, source });
3144
+ } else {
3145
+ presenceChanges ??= this.filterChangesByScope(changes, "presence");
3146
+ if (!presenceChanges) continue;
3147
+ onHistory({ changes: presenceChanges, source });
3148
+ }
3149
+ } else {
3150
+ onHistory({ changes, source });
3151
+ }
3152
+ }
3153
+ }
3154
+ }
3155
+ }
3156
+ dispose() {
3157
+ this.cancelHistoryReactor();
3158
+ }
3159
+ /**
3160
+ * Filters out non-document changes from a diff. Returns null if there are no changes left.
3161
+ * @param change - the records diff
3162
+ * @param scope - the records scope
3163
+ * @returns
3164
+ */
3165
+ filterChangesByScope(change, scope) {
3166
+ const result = {
3167
+ added: filterEntries(change.added, (_, r) => this.scopedTypes[scope].has(r.typeName)),
3168
+ updated: filterEntries(change.updated, (_, r) => this.scopedTypes[scope].has(r[1].typeName)),
3169
+ removed: filterEntries(change.removed, (_, r) => this.scopedTypes[scope].has(r.typeName))
3170
+ };
3171
+ if (Object.keys(result.added).length === 0 && Object.keys(result.updated).length === 0 && Object.keys(result.removed).length === 0) {
3172
+ return null;
3173
+ }
3174
+ return result;
3175
+ }
3176
+ /**
3177
+ * Update the history with a diff of changes.
3178
+ *
3179
+ * @param changes - The changes to add to the history.
3180
+ */
3181
+ updateHistory(changes) {
3182
+ this.historyAccumulator.add({
3183
+ changes,
3184
+ source: this.isMergingRemoteChanges ? "remote" : "user"
3185
+ });
3186
+ if (this.listeners.size === 0) {
3187
+ this.historyAccumulator.clear();
3188
+ }
3189
+ this.history.set(this.history.get() + 1, changes);
3190
+ }
3191
+ validate(phase) {
3192
+ this.allRecords().forEach((record) => this.schema.validateRecord(this, record, phase, null));
3193
+ }
3194
+ /**
3195
+ * Add or update records in the store. If a record with the same ID already exists, it will be updated.
3196
+ * Otherwise, a new record will be created.
3197
+ *
3198
+ * @example
3199
+ * ```ts
3200
+ * // Add new records
3201
+ * const book = Book.create({ title: 'Lathe Of Heaven', author: 'Le Guin' })
3202
+ * store.put([book])
3203
+ *
3204
+ * // Update existing record
3205
+ * store.put([{ ...book, title: 'The Lathe of Heaven' }])
3206
+ * ```
3207
+ *
3208
+ * @param records - The records to add or update
3209
+ * @param phaseOverride - Override the validation phase (used internally)
3210
+ * @public
3211
+ */
3212
+ put(records, phaseOverride) {
3213
+ this.atomic(() => {
3214
+ const updates = {};
3215
+ const additions = {};
3216
+ let record;
3217
+ let didChange = false;
3218
+ const source = this.isMergingRemoteChanges ? "remote" : "user";
3219
+ for (let i = 0, n = records.length; i < n; i++) {
3220
+ record = records[i];
3221
+ const initialValue = this.records.__unsafe__getWithoutCapture(record.id);
3222
+ if (initialValue) {
3223
+ record = this.sideEffects.handleBeforeChange(initialValue, record, source);
3224
+ const validated = this.schema.validateRecord(
3225
+ this,
3226
+ record,
3227
+ phaseOverride ?? "updateRecord",
3228
+ initialValue
3229
+ );
3230
+ if (validated === initialValue) continue;
3231
+ record = devFreeze(record);
3232
+ this.records.set(record.id, record);
3233
+ didChange = true;
3234
+ updates[record.id] = [initialValue, record];
3235
+ this.addDiffForAfterEvent(initialValue, record);
3236
+ } else {
3237
+ record = this.sideEffects.handleBeforeCreate(record, source);
3238
+ didChange = true;
3239
+ record = this.schema.validateRecord(
3240
+ this,
3241
+ record,
3242
+ phaseOverride ?? "createRecord",
3243
+ null
3244
+ );
3245
+ record = devFreeze(record);
3246
+ additions[record.id] = record;
3247
+ this.addDiffForAfterEvent(null, record);
3248
+ this.records.set(record.id, record);
3249
+ }
3250
+ }
3251
+ if (!didChange) return;
3252
+ this.updateHistory({
3253
+ added: additions,
3254
+ updated: updates,
3255
+ removed: {}
3256
+ });
3257
+ });
3258
+ }
3259
+ /**
3260
+ * Remove records from the store by their IDs.
3261
+ *
3262
+ * @example
3263
+ * ```ts
3264
+ * // Remove a single record
3265
+ * store.remove([book.id])
3266
+ *
3267
+ * // Remove multiple records
3268
+ * store.remove([book1.id, book2.id, book3.id])
3269
+ * ```
3270
+ *
3271
+ * @param ids - The IDs of the records to remove
3272
+ * @public
3273
+ */
3274
+ remove(ids) {
3275
+ this.atomic(() => {
3276
+ const toDelete = new Set(ids);
3277
+ const source = this.isMergingRemoteChanges ? "remote" : "user";
3278
+ if (this.sideEffects.isEnabled()) {
3279
+ for (const id of ids) {
3280
+ const record = this.records.__unsafe__getWithoutCapture(id);
3281
+ if (!record) continue;
3282
+ if (this.sideEffects.handleBeforeDelete(record, source) === false) {
3283
+ toDelete.delete(id);
3284
+ }
3285
+ }
3286
+ }
3287
+ const actuallyDeleted = this.records.deleteMany(toDelete);
3288
+ if (actuallyDeleted.length === 0) return;
3289
+ const removed = {};
3290
+ for (const [id, record] of actuallyDeleted) {
3291
+ removed[id] = record;
3292
+ this.addDiffForAfterEvent(record, null);
3293
+ }
3294
+ this.updateHistory({ added: {}, updated: {}, removed });
3295
+ });
3296
+ }
3297
+ /**
3298
+ * Get a record by its ID. This creates a reactive subscription to the record.
3299
+ *
3300
+ * @example
3301
+ * ```ts
3302
+ * const book = store.get(bookId)
3303
+ * if (book) {
3304
+ * console.log(book.title)
3305
+ * }
3306
+ * ```
3307
+ *
3308
+ * @param id - The ID of the record to get
3309
+ * @returns The record if it exists, undefined otherwise
3310
+ * @public
3311
+ */
3312
+ get(id) {
3313
+ return this.records.get(id);
3314
+ }
3315
+ /**
3316
+ * Get a record by its ID without creating a reactive subscription.
3317
+ * Use this when you need to access a record but don't want reactive updates.
3318
+ *
3319
+ * @example
3320
+ * ```ts
3321
+ * // Won't trigger reactive updates when this record changes
3322
+ * const book = store.unsafeGetWithoutCapture(bookId)
3323
+ * ```
3324
+ *
3325
+ * @param id - The ID of the record to get
3326
+ * @returns The record if it exists, undefined otherwise
3327
+ * @public
3328
+ */
3329
+ unsafeGetWithoutCapture(id) {
3330
+ return this.records.__unsafe__getWithoutCapture(id);
3331
+ }
3332
+ /**
3333
+ * Serialize the store's records to a plain JavaScript object.
3334
+ * Only includes records matching the specified scope.
3335
+ *
3336
+ * @example
3337
+ * ```ts
3338
+ * // Serialize only document records (default)
3339
+ * const documentData = store.serialize('document')
3340
+ *
3341
+ * // Serialize all records
3342
+ * const allData = store.serialize('all')
3343
+ * ```
3344
+ *
3345
+ * @param scope - The scope of records to serialize. Defaults to 'document'
3346
+ * @returns The serialized store data
3347
+ * @public
3348
+ */
3349
+ serialize(scope = "document") {
3350
+ const result = {};
3351
+ for (const [id, record] of this.records) {
3352
+ if (scope === "all" || this.scopedTypes[scope].has(record.typeName)) {
3353
+ result[id] = record;
3354
+ }
3355
+ }
3356
+ return result;
3357
+ }
3358
+ /**
3359
+ * Get a serialized snapshot of the store and its schema.
3360
+ * This includes both the data and schema information needed for proper migration.
3361
+ *
3362
+ * @example
3363
+ * ```ts
3364
+ * const snapshot = store.getStoreSnapshot()
3365
+ * localStorage.setItem('myApp', JSON.stringify(snapshot))
3366
+ *
3367
+ * // Later...
3368
+ * const saved = JSON.parse(localStorage.getItem('myApp'))
3369
+ * store.loadStoreSnapshot(saved)
3370
+ * ```
3371
+ *
3372
+ * @param scope - The scope of records to serialize. Defaults to 'document'
3373
+ * @returns A snapshot containing both store data and schema information
3374
+ * @public
3375
+ */
3376
+ getStoreSnapshot(scope = "document") {
3377
+ return {
3378
+ store: this.serialize(scope),
3379
+ schema: this.schema.serialize()
3380
+ };
3381
+ }
3382
+ /**
3383
+ * Migrate a serialized snapshot to the current schema version.
3384
+ * This applies any necessary migrations to bring old data up to date.
3385
+ *
3386
+ * @example
3387
+ * ```ts
3388
+ * const oldSnapshot = JSON.parse(localStorage.getItem('myApp'))
3389
+ * const migratedSnapshot = store.migrateSnapshot(oldSnapshot)
3390
+ * ```
3391
+ *
3392
+ * @param snapshot - The snapshot to migrate
3393
+ * @returns The migrated snapshot with current schema version
3394
+ * @throws Error if migration fails
3395
+ * @public
3396
+ */
3397
+ migrateSnapshot(snapshot) {
3398
+ const migrationResult = this.schema.migrateStoreSnapshot(snapshot);
3399
+ if (migrationResult.type === "error") {
3400
+ throw new Error(`Failed to migrate snapshot: ${migrationResult.reason}`);
3401
+ }
3402
+ return {
3403
+ store: migrationResult.value,
3404
+ schema: this.schema.serialize()
3405
+ };
3406
+ }
3407
+ /**
3408
+ * Load a serialized snapshot into the store, replacing all current data.
3409
+ * The snapshot will be automatically migrated to the current schema version if needed.
3410
+ *
3411
+ * @example
3412
+ * ```ts
3413
+ * const snapshot = JSON.parse(localStorage.getItem('myApp'))
3414
+ * store.loadStoreSnapshot(snapshot)
3415
+ * ```
3416
+ *
3417
+ * @param snapshot - The snapshot to load
3418
+ * @throws Error if migration fails or snapshot is invalid
3419
+ * @public
3420
+ */
3421
+ loadStoreSnapshot(snapshot) {
3422
+ const migrationResult = this.schema.migrateStoreSnapshot(snapshot);
3423
+ if (migrationResult.type === "error") {
3424
+ throw new Error(`Failed to migrate snapshot: ${migrationResult.reason}`);
3425
+ }
3426
+ const prevSideEffectsEnabled = this.sideEffects.isEnabled();
3427
+ try {
3428
+ this.sideEffects.setIsEnabled(false);
3429
+ this.atomic(() => {
3430
+ this.clear();
3431
+ this.put(Object.values(migrationResult.value));
3432
+ this.ensureStoreIsUsable();
3433
+ });
3434
+ } finally {
3435
+ this.sideEffects.setIsEnabled(prevSideEffectsEnabled);
3436
+ }
3437
+ }
3438
+ /**
3439
+ * Get an array of all records in the store.
3440
+ *
3441
+ * @example
3442
+ * ```ts
3443
+ * const allRecords = store.allRecords()
3444
+ * const books = allRecords.filter(r => r.typeName === 'book')
3445
+ * ```
3446
+ *
3447
+ * @returns An array containing all records in the store
3448
+ * @public
3449
+ */
3450
+ allRecords() {
3451
+ return Array.from(this.records.values());
3452
+ }
3453
+ /**
3454
+ * Remove all records from the store.
3455
+ *
3456
+ * @example
3457
+ * ```ts
3458
+ * store.clear()
3459
+ * console.log(store.allRecords().length) // 0
3460
+ * ```
3461
+ *
3462
+ * @public
3463
+ */
3464
+ clear() {
3465
+ this.remove(Array.from(this.records.keys()));
3466
+ }
3467
+ /**
3468
+ * Update a single record using an updater function. To update multiple records at once,
3469
+ * use the `update` method of the `TypedStore` class.
3470
+ *
3471
+ * @example
3472
+ * ```ts
3473
+ * store.update(book.id, (book) => ({
3474
+ * ...book,
3475
+ * title: 'Updated Title'
3476
+ * }))
3477
+ * ```
3478
+ *
3479
+ * @param id - The ID of the record to update
3480
+ * @param updater - A function that receives the current record and returns the updated record
3481
+ * @public
3482
+ */
3483
+ update(id, updater) {
3484
+ const existing = this.unsafeGetWithoutCapture(id);
3485
+ if (!existing) {
3486
+ console.error(`Record ${id} not found. This is probably an error`);
3487
+ return;
3488
+ }
3489
+ this.put([updater(existing)]);
3490
+ }
3491
+ /**
3492
+ * Check whether a record with the given ID exists in the store.
3493
+ *
3494
+ * @example
3495
+ * ```ts
3496
+ * if (store.has(bookId)) {
3497
+ * console.log('Book exists!')
3498
+ * }
3499
+ * ```
3500
+ *
3501
+ * @param id - The ID of the record to check
3502
+ * @returns True if the record exists, false otherwise
3503
+ * @public
3504
+ */
3505
+ has(id) {
3506
+ return this.records.has(id);
3507
+ }
3508
+ /**
3509
+ * Add a listener that will be called when the store changes.
3510
+ * Returns a function to remove the listener.
3511
+ *
3512
+ * @example
3513
+ * ```ts
3514
+ * const removeListener = store.listen((entry) => {
3515
+ * console.log('Changes:', entry.changes)
3516
+ * console.log('Source:', entry.source)
3517
+ * })
3518
+ *
3519
+ * // Listen only to user changes to document records
3520
+ * const removeDocumentListener = store.listen(
3521
+ * (entry) => console.log('Document changed:', entry),
3522
+ * { source: 'user', scope: 'document' }
3523
+ * )
3524
+ *
3525
+ * // Later, remove the listener
3526
+ * removeListener()
3527
+ * ```
3528
+ *
3529
+ * @param onHistory - The listener function to call when changes occur
3530
+ * @param filters - Optional filters to control when the listener is called
3531
+ * @returns A function that removes the listener when called
3532
+ * @public
3533
+ */
3534
+ listen(onHistory, filters) {
3535
+ this._flushHistory();
3536
+ const listener = {
3537
+ onHistory,
3538
+ filters: {
3539
+ source: filters?.source ?? "all",
3540
+ scope: filters?.scope ?? "all"
3541
+ }
3542
+ };
3543
+ if (!this.historyReactor.scheduler.isActivelyListening) {
3544
+ this.historyReactor.start();
3545
+ this.historyReactor.scheduler.execute();
3546
+ }
3547
+ this.listeners.add(listener);
3548
+ return () => {
3549
+ this.listeners.delete(listener);
3550
+ if (this.listeners.size === 0) {
3551
+ this.historyReactor.stop();
3552
+ }
3553
+ };
3554
+ }
3555
+ isMergingRemoteChanges = false;
3556
+ /**
3557
+ * Merge changes from a remote source. Changes made within the provided function
3558
+ * will be marked with source 'remote' instead of 'user'.
3559
+ *
3560
+ * @example
3561
+ * ```ts
3562
+ * // Changes from sync/collaboration
3563
+ * store.mergeRemoteChanges(() => {
3564
+ * store.put(remoteRecords)
3565
+ * store.remove(deletedIds)
3566
+ * })
3567
+ * ```
3568
+ *
3569
+ * @param fn - A function that applies the remote changes
3570
+ * @public
3571
+ */
3572
+ mergeRemoteChanges(fn) {
3573
+ if (this.isMergingRemoteChanges) {
3574
+ return fn();
3575
+ }
3576
+ if (this._isInAtomicOp) {
3577
+ throw new Error("Cannot merge remote changes while in atomic operation");
3578
+ }
3579
+ try {
3580
+ this.atomic(fn, true, true);
3581
+ } finally {
3582
+ this.ensureStoreIsUsable();
3583
+ }
3584
+ }
3585
+ /**
3586
+ * Run `fn` and return a {@link RecordsDiff} of the changes that occurred as a result.
3587
+ */
3588
+ extractingChanges(fn) {
3589
+ const changes = [];
3590
+ const dispose = this.historyAccumulator.addInterceptor((entry) => changes.push(entry.changes));
3591
+ try {
3592
+ transact(fn);
3593
+ return squashRecordDiffs(changes);
3594
+ } finally {
3595
+ dispose();
3596
+ }
3597
+ }
3598
+ applyDiff(diff, {
3599
+ runCallbacks = true,
3600
+ ignoreEphemeralKeys = false
3601
+ } = {}) {
3602
+ this.atomic(() => {
3603
+ const toPut = objectMapValues(diff.added);
3604
+ for (const [_from, to] of objectMapValues(diff.updated)) {
3605
+ const type = this.schema.getType(to.typeName);
3606
+ if (ignoreEphemeralKeys && type.ephemeralKeySet.size) {
3607
+ const existing = this.get(to.id);
3608
+ if (!existing) {
3609
+ toPut.push(to);
3610
+ continue;
3611
+ }
3612
+ let changed = null;
3613
+ for (const [key, value] of Object.entries(to)) {
3614
+ if (type.ephemeralKeySet.has(key) || Object.is(value, getOwnProperty(existing, key))) {
3615
+ continue;
3616
+ }
3617
+ if (!changed) changed = { ...existing };
3618
+ changed[key] = value;
3619
+ }
3620
+ if (changed) toPut.push(changed);
3621
+ } else {
3622
+ toPut.push(to);
3623
+ }
3624
+ }
3625
+ const toRemove = objectMapKeys(diff.removed);
3626
+ if (toPut.length) {
3627
+ this.put(toPut);
3628
+ }
3629
+ if (toRemove.length) {
3630
+ this.remove(toRemove);
3631
+ }
3632
+ }, runCallbacks);
3633
+ }
3634
+ /**
3635
+ * Create a cache based on values in the store. Pass in a function that takes and ID and a
3636
+ * signal for the underlying record. Return a signal (usually a computed) for the cached value.
3637
+ * For simple derivations, use {@link Store.createComputedCache}. This function is useful if you
3638
+ * need more precise control over intermediate values.
3639
+ */
3640
+ createCache(create) {
3641
+ const cache = new WeakCache();
3642
+ return {
3643
+ get: (id) => {
3644
+ const atom3 = this.records.getAtom(id);
3645
+ if (!atom3) return void 0;
3646
+ return cache.get(atom3, () => create(id, atom3)).get();
3647
+ }
3648
+ };
3649
+ }
3650
+ /**
3651
+ * Create a computed cache.
3652
+ *
3653
+ * @param name - The name of the derivation cache.
3654
+ * @param derive - A function used to derive the value of the cache.
3655
+ * @param opts - Options for the computed cache.
3656
+ * @public
3657
+ */
3658
+ createComputedCache(name, derive, opts) {
3659
+ return this.createCache((id, record) => {
3660
+ const recordSignal = opts?.areRecordsEqual ? computed(`${name}:${id}:isEqual`, () => record.get(), { isEqual: opts.areRecordsEqual }) : record;
3661
+ return computed(
3662
+ name + ":" + id,
3663
+ () => {
3664
+ return derive(recordSignal.get());
3665
+ },
3666
+ {
3667
+ isEqual: opts?.areResultsEqual
3668
+ }
3669
+ );
3670
+ });
3671
+ }
3672
+ _integrityChecker;
3673
+ /** @internal */
3674
+ ensureStoreIsUsable() {
3675
+ this.atomic(() => {
3676
+ this._integrityChecker ??= this.schema.createIntegrityChecker(this);
3677
+ this._integrityChecker?.();
3678
+ });
3679
+ }
3680
+ _isPossiblyCorrupted = false;
3681
+ /** @internal */
3682
+ markAsPossiblyCorrupted() {
3683
+ this._isPossiblyCorrupted = true;
3684
+ }
3685
+ /** @internal */
3686
+ isPossiblyCorrupted() {
3687
+ return this._isPossiblyCorrupted;
3688
+ }
3689
+ pendingAfterEvents = null;
3690
+ addDiffForAfterEvent(before, after) {
3691
+ assert(this.pendingAfterEvents, "must be in event operation");
3692
+ if (before === after) return;
3693
+ if (before && after) assert(before.id === after.id);
3694
+ if (!before && !after) return;
3695
+ const id = (before || after).id;
3696
+ const existing = this.pendingAfterEvents.get(id);
3697
+ if (existing) {
3698
+ existing.after = after;
3699
+ } else {
3700
+ this.pendingAfterEvents.set(id, { before, after });
3701
+ }
3702
+ }
3703
+ flushAtomicCallbacks(isMergingRemoteChanges) {
3704
+ let updateDepth = 0;
3705
+ let source = isMergingRemoteChanges ? "remote" : "user";
3706
+ while (this.pendingAfterEvents) {
3707
+ const events = this.pendingAfterEvents;
3708
+ this.pendingAfterEvents = null;
3709
+ if (!this.sideEffects.isEnabled()) continue;
3710
+ updateDepth++;
3711
+ if (updateDepth > 100) {
3712
+ throw new Error("Maximum store update depth exceeded, bailing out");
3713
+ }
3714
+ for (const { before, after } of events.values()) {
3715
+ if (before && after && before !== after && !isEqual(before, after)) {
3716
+ this.sideEffects.handleAfterChange(before, after, source);
3717
+ } else if (before && !after) {
3718
+ this.sideEffects.handleAfterDelete(before, source);
3719
+ } else if (!before && after) {
3720
+ this.sideEffects.handleAfterCreate(after, source);
3721
+ }
3722
+ }
3723
+ if (!this.pendingAfterEvents) {
3724
+ this.sideEffects.handleOperationComplete(source);
3725
+ } else {
3726
+ source = "user";
3727
+ }
3728
+ }
3729
+ }
3730
+ _isInAtomicOp = false;
3731
+ /** @internal */
3732
+ atomic(fn, runCallbacks = true, isMergingRemoteChanges = false) {
3733
+ return transact(() => {
3734
+ if (this._isInAtomicOp) {
3735
+ if (!this.pendingAfterEvents) this.pendingAfterEvents = /* @__PURE__ */ new Map();
3736
+ const prevSideEffectsEnabled2 = this.sideEffects.isEnabled();
3737
+ assert(!isMergingRemoteChanges, "cannot call mergeRemoteChanges while in atomic operation");
3738
+ try {
3739
+ if (prevSideEffectsEnabled2 && !runCallbacks) {
3740
+ this.sideEffects.setIsEnabled(false);
3741
+ }
3742
+ return fn();
3743
+ } finally {
3744
+ this.sideEffects.setIsEnabled(prevSideEffectsEnabled2);
3745
+ }
3746
+ }
3747
+ this.pendingAfterEvents = /* @__PURE__ */ new Map();
3748
+ const prevSideEffectsEnabled = this.sideEffects.isEnabled();
3749
+ this.sideEffects.setIsEnabled(runCallbacks ?? prevSideEffectsEnabled);
3750
+ this._isInAtomicOp = true;
3751
+ if (isMergingRemoteChanges) {
3752
+ this.isMergingRemoteChanges = true;
3753
+ }
3754
+ try {
3755
+ const result = fn();
3756
+ this.isMergingRemoteChanges = false;
3757
+ this.flushAtomicCallbacks(isMergingRemoteChanges);
3758
+ return result;
3759
+ } finally {
3760
+ this.pendingAfterEvents = null;
3761
+ this.sideEffects.setIsEnabled(prevSideEffectsEnabled);
3762
+ this._isInAtomicOp = false;
3763
+ this.isMergingRemoteChanges = false;
3764
+ }
3765
+ });
3766
+ }
3767
+ /** @internal */
3768
+ addHistoryInterceptor(fn) {
3769
+ return this.historyAccumulator.addInterceptor(
3770
+ (entry) => fn(entry, this.isMergingRemoteChanges ? "remote" : "user")
3771
+ );
3772
+ }
3773
+ };
3774
+ function squashHistoryEntries(entries) {
3775
+ if (entries.length === 0) return [];
3776
+ const chunked = [];
3777
+ let chunk = [entries[0]];
3778
+ let entry;
3779
+ for (let i = 1, n = entries.length; i < n; i++) {
3780
+ entry = entries[i];
3781
+ if (chunk[0].source !== entry.source) {
3782
+ chunked.push(chunk);
3783
+ chunk = [];
3784
+ }
3785
+ chunk.push(entry);
3786
+ }
3787
+ chunked.push(chunk);
3788
+ return devFreeze(
3789
+ chunked.map((chunk2) => ({
3790
+ source: chunk2[0].source,
3791
+ changes: squashRecordDiffs(chunk2.map((e) => e.changes))
3792
+ }))
3793
+ );
3794
+ }
3795
+ var HistoryAccumulator = class {
3796
+ _history = [];
3797
+ _interceptors = /* @__PURE__ */ new Set();
3798
+ /**
3799
+ * Add an interceptor that will be called for each history entry.
3800
+ * Returns a function to remove the interceptor.
3801
+ */
3802
+ addInterceptor(fn) {
3803
+ this._interceptors.add(fn);
3804
+ return () => {
3805
+ this._interceptors.delete(fn);
3806
+ };
3807
+ }
3808
+ /**
3809
+ * Add a history entry to the accumulator.
3810
+ * Calls all registered interceptors with the entry.
3811
+ */
3812
+ add(entry) {
3813
+ this._history.push(entry);
3814
+ for (const interceptor of this._interceptors) {
3815
+ interceptor(entry);
3816
+ }
3817
+ }
3818
+ /**
3819
+ * Flush all accumulated history entries, squashing adjacent entries from the same source.
3820
+ * Clears the internal history buffer.
3821
+ */
3822
+ flush() {
3823
+ const history = squashHistoryEntries(this._history);
3824
+ this._history = [];
3825
+ return history;
3826
+ }
3827
+ /**
3828
+ * Clear all accumulated history entries without flushing.
3829
+ */
3830
+ clear() {
3831
+ this._history = [];
3832
+ }
3833
+ /**
3834
+ * Check if there are any accumulated history entries.
3835
+ */
3836
+ hasChanges() {
3837
+ return this._history.length > 0;
3838
+ }
3839
+ };
3840
+ function createComputedCache(name, derive, opts) {
3841
+ const cache = new WeakCache();
3842
+ return {
3843
+ get(context, id) {
3844
+ const computedCache = cache.get(context, () => {
3845
+ const store = context instanceof Store ? context : context.store;
3846
+ return store.createComputedCache(name, (record) => derive(context, record), opts);
3847
+ });
3848
+ return computedCache.get(id);
3849
+ }
3850
+ };
3851
+ }
3852
+ function upgradeSchema(schema) {
3853
+ if (schema.schemaVersion > 2 || schema.schemaVersion < 1) return Result.err("Bad schema version");
3854
+ if (schema.schemaVersion === 2) return Result.ok(schema);
3855
+ const result = {
3856
+ schemaVersion: 2,
3857
+ sequences: {
3858
+ "com.draw.store": schema.storeVersion
3859
+ }
3860
+ };
3861
+ for (const [typeName, recordVersion] of Object.entries(schema.recordVersions)) {
3862
+ result.sequences[`com.draw.${typeName}`] = recordVersion.version;
3863
+ if ("subTypeKey" in recordVersion) {
3864
+ for (const [subType, version] of Object.entries(recordVersion.subTypeVersions)) {
3865
+ result.sequences[`com.draw.${typeName}.${subType}`] = version;
3866
+ }
3867
+ }
3868
+ }
3869
+ return Result.ok(result);
3870
+ }
3871
+ var StoreSchema = class _StoreSchema {
3872
+ constructor(types, options) {
3873
+ this.types = types;
3874
+ this.options = options;
3875
+ for (const m of options.migrations ?? []) {
3876
+ assert(!this.migrations[m.sequenceId], `Duplicate migration sequenceId ${m.sequenceId}`);
3877
+ validateMigrations(m);
3878
+ this.migrations[m.sequenceId] = m;
3879
+ }
3880
+ const allMigrations = Object.values(this.migrations).flatMap((m) => m.sequence);
3881
+ this.sortedMigrations = sortMigrations(allMigrations);
3882
+ for (const migration of this.sortedMigrations) {
3883
+ if (!migration.dependsOn?.length) continue;
3884
+ for (const dep of migration.dependsOn) {
3885
+ const depMigration = allMigrations.find((m) => m.id === dep);
3886
+ assert(depMigration, `Migration '${migration.id}' depends on missing migration '${dep}'`);
3887
+ }
3888
+ }
3889
+ }
3890
+ types;
3891
+ options;
3892
+ /**
3893
+ * Creates a new StoreSchema with the given record types and options.
3894
+ *
3895
+ * This static factory method is the recommended way to create a StoreSchema.
3896
+ * It ensures type safety while providing a clean API for schema definition.
3897
+ *
3898
+ * @param types - Object mapping type names to their RecordType definitions
3899
+ * @param options - Optional configuration for migrations, validation, and integrity checking
3900
+ * @returns A new StoreSchema instance
3901
+ *
3902
+ * @example
3903
+ * ```ts
3904
+ * const Book = createRecordType<Book>('book', { scope: 'document' })
3905
+ * const Author = createRecordType<Author>('author', { scope: 'document' })
3906
+ *
3907
+ * const schema = StoreSchema.create(
3908
+ * {
3909
+ * book: Book,
3910
+ * author: Author
3911
+ * },
3912
+ * {
3913
+ * migrations: [bookMigrations],
3914
+ * onValidationFailure: (failure) => failure.record
3915
+ * }
3916
+ * )
3917
+ * ```
3918
+ *
3919
+ * @public
3920
+ */
3921
+ static create(types, options) {
3922
+ return new _StoreSchema(types, options ?? {});
3923
+ }
3924
+ migrations = {};
3925
+ sortedMigrations;
3926
+ migrationCache = /* @__PURE__ */ new WeakMap();
3927
+ /**
3928
+ * Validates a record using its corresponding RecordType validator.
3929
+ *
3930
+ * This method ensures that records conform to their type definitions before
3931
+ * being stored. If validation fails and an onValidationFailure handler is
3932
+ * provided, it will be called to potentially recover from the error.
3933
+ *
3934
+ * @param store - The store instance where validation is occurring
3935
+ * @param record - The record to validate
3936
+ * @param phase - The lifecycle phase where validation is happening
3937
+ * @param recordBefore - The previous version of the record (for updates)
3938
+ * @returns The validated record, potentially modified by validation failure handler
3939
+ *
3940
+ * @example
3941
+ * ```ts
3942
+ * try {
3943
+ * const validatedBook = schema.validateRecord(
3944
+ * store,
3945
+ * { id: 'book:1', typeName: 'book', title: '', author: 'Jane Doe' },
3946
+ * 'createRecord',
3947
+ * null
3948
+ * )
3949
+ * } catch (error) {
3950
+ * console.error('Record validation failed:', error)
3951
+ * }
3952
+ * ```
3953
+ *
3954
+ * @public
3955
+ */
3956
+ validateRecord(store, record, phase, recordBefore) {
3957
+ try {
3958
+ const recordType = getOwnProperty(this.types, record.typeName);
3959
+ if (!recordType) {
3960
+ throw new Error(`Missing definition for record type ${record.typeName}`);
3961
+ }
3962
+ return recordType.validate(record, recordBefore ?? void 0);
3963
+ } catch (error) {
3964
+ if (this.options.onValidationFailure) {
3965
+ return this.options.onValidationFailure({
3966
+ store,
3967
+ record,
3968
+ phase,
3969
+ recordBefore,
3970
+ error
3971
+ });
3972
+ } else {
3973
+ throw error;
3974
+ }
3975
+ }
3976
+ }
3977
+ /**
3978
+ * Gets all migrations that need to be applied to upgrade from a persisted schema
3979
+ * to the current schema version.
3980
+ *
3981
+ * This method compares the persisted schema with the current schema and determines
3982
+ * which migrations need to be applied to bring the data up to date. It handles
3983
+ * both regular migrations and retroactive migrations, and caches results for
3984
+ * performance.
3985
+ *
3986
+ * @param persistedSchema - The schema version that was previously persisted
3987
+ * @returns A Result containing the list of migrations to apply, or an error message
3988
+ *
3989
+ * @example
3990
+ * ```ts
3991
+ * const persistedSchema = {
3992
+ * schemaVersion: 2,
3993
+ * sequences: { 'com.draw.book': 1, 'com.draw.author': 0 }
3994
+ * }
3995
+ *
3996
+ * const migrationsResult = schema.getMigrationsSince(persistedSchema)
3997
+ * if (migrationsResult.ok) {
3998
+ * console.log('Migrations to apply:', migrationsResult.value.length)
3999
+ * // Apply each migration to bring data up to date
4000
+ * }
4001
+ * ```
4002
+ *
4003
+ * @public
4004
+ */
4005
+ getMigrationsSince(persistedSchema) {
4006
+ const cached = this.migrationCache.get(persistedSchema);
4007
+ if (cached) {
4008
+ return cached;
4009
+ }
4010
+ const upgradeResult = upgradeSchema(persistedSchema);
4011
+ if (!upgradeResult.ok) {
4012
+ this.migrationCache.set(persistedSchema, upgradeResult);
4013
+ return upgradeResult;
4014
+ }
4015
+ const schema = upgradeResult.value;
4016
+ const sequenceIdsToInclude = new Set(
4017
+ // start with any shared sequences
4018
+ Object.keys(schema.sequences).filter((sequenceId) => this.migrations[sequenceId])
4019
+ );
4020
+ for (const sequenceId in this.migrations) {
4021
+ if (schema.sequences[sequenceId] === void 0 && this.migrations[sequenceId].retroactive) {
4022
+ sequenceIdsToInclude.add(sequenceId);
4023
+ }
4024
+ }
4025
+ if (sequenceIdsToInclude.size === 0) {
4026
+ const result2 = Result.ok([]);
4027
+ this.migrationCache.set(persistedSchema, result2);
4028
+ return result2;
4029
+ }
4030
+ const allMigrationsToInclude = /* @__PURE__ */ new Set();
4031
+ for (const sequenceId of sequenceIdsToInclude) {
4032
+ const theirVersion = schema.sequences[sequenceId];
4033
+ if (typeof theirVersion !== "number" && this.migrations[sequenceId].retroactive || theirVersion === 0) {
4034
+ for (const migration of this.migrations[sequenceId].sequence) {
4035
+ allMigrationsToInclude.add(migration.id);
4036
+ }
4037
+ continue;
4038
+ }
4039
+ const theirVersionId = `${sequenceId}/${theirVersion}`;
4040
+ const idx = this.migrations[sequenceId].sequence.findIndex((m) => m.id === theirVersionId);
4041
+ if (idx === -1) {
4042
+ const result2 = Result.err("Incompatible schema?");
4043
+ this.migrationCache.set(persistedSchema, result2);
4044
+ return result2;
4045
+ }
4046
+ for (const migration of this.migrations[sequenceId].sequence.slice(idx + 1)) {
4047
+ allMigrationsToInclude.add(migration.id);
4048
+ }
4049
+ }
4050
+ const result = Result.ok(
4051
+ this.sortedMigrations.filter(({ id }) => allMigrationsToInclude.has(id))
4052
+ );
4053
+ this.migrationCache.set(persistedSchema, result);
4054
+ return result;
4055
+ }
4056
+ /**
4057
+ * Migrates a single persisted record to match the current schema version.
4058
+ *
4059
+ * This method applies the necessary migrations to transform a record from an
4060
+ * older (or newer) schema version to the current version. It supports both
4061
+ * forward ('up') and backward ('down') migrations.
4062
+ *
4063
+ * @param record - The record to migrate
4064
+ * @param persistedSchema - The schema version the record was persisted with
4065
+ * @param direction - Direction to migrate ('up' for newer, 'down' for older)
4066
+ * @returns A MigrationResult containing the migrated record or an error
4067
+ *
4068
+ * @example
4069
+ * ```ts
4070
+ * const oldRecord = { id: 'book:1', typeName: 'book', title: 'Old Title', publishDate: '2020-01-01' }
4071
+ * const oldSchema = { schemaVersion: 2, sequences: { 'com.draw.book': 1 } }
4072
+ *
4073
+ * const result = schema.migratePersistedRecord(oldRecord, oldSchema, 'up')
4074
+ * if (result.type === 'success') {
4075
+ * console.log('Migrated record:', result.value)
4076
+ * // Record now has publishedYear instead of publishDate
4077
+ * } else {
4078
+ * console.error('Migration failed:', result.reason)
4079
+ * }
4080
+ * ```
4081
+ *
4082
+ * @public
4083
+ */
4084
+ migratePersistedRecord(record, persistedSchema, direction = "up") {
4085
+ const migrations = this.getMigrationsSince(persistedSchema);
4086
+ if (!migrations.ok) {
4087
+ console.error("Error migrating record", migrations.error);
4088
+ return { type: "error", reason: MigrationFailureReason.MigrationError };
4089
+ }
4090
+ let migrationsToApply = migrations.value;
4091
+ if (migrationsToApply.length === 0) {
4092
+ return { type: "success", value: record };
4093
+ }
4094
+ if (!migrationsToApply.every((m) => m.scope === "record")) {
4095
+ return {
4096
+ type: "error",
4097
+ reason: direction === "down" ? MigrationFailureReason.TargetVersionTooOld : MigrationFailureReason.TargetVersionTooNew
4098
+ };
4099
+ }
4100
+ if (direction === "down") {
4101
+ if (!migrationsToApply.every((m) => m.scope === "record" && m.down)) {
4102
+ return {
4103
+ type: "error",
4104
+ reason: MigrationFailureReason.TargetVersionTooOld
4105
+ };
4106
+ }
4107
+ migrationsToApply = migrationsToApply.slice().reverse();
4108
+ }
4109
+ record = structuredClone(record);
4110
+ try {
4111
+ for (const migration of migrationsToApply) {
4112
+ if (migration.scope === "store") throw new Error(
4113
+ /* won't happen, just for TS */
4114
+ );
4115
+ if (migration.scope === "storage") throw new Error(
4116
+ /* won't happen, just for TS */
4117
+ );
4118
+ const shouldApply = migration.filter ? migration.filter(record) : true;
4119
+ if (!shouldApply) continue;
4120
+ const result = migration[direction](record);
4121
+ if (result) {
4122
+ record = structuredClone(result);
4123
+ }
4124
+ }
4125
+ } catch (e) {
4126
+ console.error("Error migrating record", e);
4127
+ return { type: "error", reason: MigrationFailureReason.MigrationError };
4128
+ }
4129
+ return { type: "success", value: record };
4130
+ }
4131
+ migrateStorage(storage) {
4132
+ const schema = storage.getSchema();
4133
+ assert(schema, "Schema is missing.");
4134
+ const migrations = this.getMigrationsSince(schema);
4135
+ if (!migrations.ok) {
4136
+ console.error("Error migrating store", migrations.error);
4137
+ throw new Error(migrations.error);
4138
+ }
4139
+ const migrationsToApply = migrations.value;
4140
+ if (migrationsToApply.length === 0) {
4141
+ return;
4142
+ }
4143
+ storage.setSchema(this.serialize());
4144
+ for (const migration of migrationsToApply) {
4145
+ if (migration.scope === "record") {
4146
+ const updates = [];
4147
+ for (const [id, state] of storage.entries()) {
4148
+ const shouldApply = migration.filter ? migration.filter(state) : true;
4149
+ if (!shouldApply) continue;
4150
+ const record = structuredClone(state);
4151
+ const result = migration.up(record) ?? record;
4152
+ if (!isEqual(result, state)) {
4153
+ updates.push([id, result]);
4154
+ }
4155
+ }
4156
+ for (const [id, record] of updates) {
4157
+ storage.set(id, record);
4158
+ }
4159
+ } else if (migration.scope === "store") {
4160
+ const prevStore = Object.fromEntries(storage.entries());
4161
+ let nextStore = structuredClone(prevStore);
4162
+ nextStore = migration.up(nextStore) ?? nextStore;
4163
+ for (const [id, state] of Object.entries(nextStore)) {
4164
+ if (!state) continue;
4165
+ if (!isEqual(state, prevStore[id])) {
4166
+ storage.set(id, state);
4167
+ }
4168
+ }
4169
+ for (const id of Object.keys(prevStore)) {
4170
+ if (!nextStore[id]) {
4171
+ storage.delete(id);
4172
+ }
4173
+ }
4174
+ } else if (migration.scope === "storage") {
4175
+ migration.up(storage);
4176
+ } else {
4177
+ exhaustiveSwitchError(migration);
4178
+ }
4179
+ }
4180
+ for (const [id, state] of storage.entries()) {
4181
+ if (this.getType(state.typeName).scope !== "document") {
4182
+ storage.delete(id);
4183
+ }
4184
+ }
4185
+ }
4186
+ /**
4187
+ * Migrates an entire store snapshot to match the current schema version.
4188
+ *
4189
+ * This method applies all necessary migrations to bring a persisted store
4190
+ * snapshot up to the current schema version. It handles both record-level
4191
+ * and store-level migrations, and can optionally mutate the input store
4192
+ * for performance.
4193
+ *
4194
+ * @param snapshot - The store snapshot containing data and schema information
4195
+ * @param opts - Options controlling migration behavior
4196
+ * - mutateInputStore - Whether to modify the input store directly (default: false)
4197
+ * @returns A MigrationResult containing the migrated store or an error
4198
+ *
4199
+ * @example
4200
+ * ```ts
4201
+ * const snapshot = {
4202
+ * schema: { schemaVersion: 2, sequences: { 'com.draw.book': 1 } },
4203
+ * store: {
4204
+ * 'book:1': { id: 'book:1', typeName: 'book', title: 'Old Book', publishDate: '2020-01-01' }
4205
+ * }
4206
+ * }
4207
+ *
4208
+ * const result = schema.migrateStoreSnapshot(snapshot)
4209
+ * if (result.type === 'success') {
4210
+ * console.log('Migrated store:', result.value)
4211
+ * // All records are now at current schema version
4212
+ * }
4213
+ * ```
4214
+ *
4215
+ * @public
4216
+ */
4217
+ migrateStoreSnapshot(snapshot, opts) {
4218
+ const migrations = this.getMigrationsSince(snapshot.schema);
4219
+ if (!migrations.ok) {
4220
+ console.error("Error migrating store", migrations.error);
4221
+ return { type: "error", reason: MigrationFailureReason.MigrationError };
4222
+ }
4223
+ const migrationsToApply = migrations.value;
4224
+ if (migrationsToApply.length === 0) {
4225
+ return { type: "success", value: snapshot.store };
4226
+ }
4227
+ const store = Object.assign(
4228
+ new Map(objectMapEntries(snapshot.store).map(devFreeze)),
4229
+ {
4230
+ getSchema: () => snapshot.schema,
4231
+ setSchema: (_) => {
4232
+ }
4233
+ }
4234
+ );
4235
+ try {
4236
+ this.migrateStorage(store);
4237
+ if (opts?.mutateInputStore) {
4238
+ for (const [id, record] of store.entries()) {
4239
+ snapshot.store[id] = record;
4240
+ }
4241
+ for (const id of Object.keys(snapshot.store)) {
4242
+ if (!store.has(id)) {
4243
+ delete snapshot.store[id];
4244
+ }
4245
+ }
4246
+ return { type: "success", value: snapshot.store };
4247
+ } else {
4248
+ return {
4249
+ type: "success",
4250
+ value: Object.fromEntries(store.entries())
4251
+ };
4252
+ }
4253
+ } catch (e) {
4254
+ console.error("Error migrating store", e);
4255
+ return { type: "error", reason: MigrationFailureReason.MigrationError };
4256
+ }
4257
+ }
4258
+ /**
4259
+ * Creates an integrity checker function for the given store.
4260
+ *
4261
+ * This method calls the createIntegrityChecker option if provided, allowing
4262
+ * custom integrity checking logic to be set up for the store. The integrity
4263
+ * checker is used to validate store consistency and catch data corruption.
4264
+ *
4265
+ * @param store - The store instance to create an integrity checker for
4266
+ * @returns An integrity checker function, or undefined if none is configured
4267
+ *
4268
+ * @internal
4269
+ */
4270
+ createIntegrityChecker(store) {
4271
+ return this.options.createIntegrityChecker?.(store) ?? void 0;
4272
+ }
4273
+ /**
4274
+ * Serializes the current schema to a SerializedSchemaV2 format.
4275
+ *
4276
+ * This method creates a serialized representation of the current schema,
4277
+ * capturing the latest version number for each migration sequence.
4278
+ * The result can be persisted and later used to determine what migrations
4279
+ * need to be applied when loading data.
4280
+ *
4281
+ * @returns A SerializedSchemaV2 object representing the current schema state
4282
+ *
4283
+ * @example
4284
+ * ```ts
4285
+ * const serialized = schema.serialize()
4286
+ * console.log(serialized)
4287
+ * // {
4288
+ * // schemaVersion: 2,
4289
+ * // sequences: {
4290
+ * // 'com.draw.book': 3,
4291
+ * // 'com.draw.author': 2
4292
+ * // }
4293
+ * // }
4294
+ *
4295
+ * // Store this with your data for future migrations
4296
+ * localStorage.setItem('schema', JSON.stringify(serialized))
4297
+ * ```
4298
+ *
4299
+ * @public
4300
+ */
4301
+ serialize() {
4302
+ return {
4303
+ schemaVersion: 2,
4304
+ sequences: Object.fromEntries(
4305
+ Object.values(this.migrations).map(({ sequenceId, sequence }) => [
4306
+ sequenceId,
4307
+ sequence.length ? parseMigrationId(sequence.at(-1).id).version : 0
4308
+ ])
4309
+ )
4310
+ };
4311
+ }
4312
+ /**
4313
+ * Serializes a schema representing the earliest possible version.
4314
+ *
4315
+ * This method creates a serialized schema where all migration sequences
4316
+ * are set to version 0, representing the state before any migrations
4317
+ * have been applied. This is used in specific legacy scenarios.
4318
+ *
4319
+ * @returns A SerializedSchema with all sequences set to version 0
4320
+ *
4321
+ * @deprecated This is only here for legacy reasons, don't use it unless you have david's blessing!
4322
+ * @internal
4323
+ */
4324
+ serializeEarliestVersion() {
4325
+ return {
4326
+ schemaVersion: 2,
4327
+ sequences: Object.fromEntries(
4328
+ Object.values(this.migrations).map(({ sequenceId }) => [sequenceId, 0])
4329
+ )
4330
+ };
4331
+ }
4332
+ /**
4333
+ * Gets the RecordType definition for a given type name.
4334
+ *
4335
+ * This method retrieves the RecordType associated with the specified
4336
+ * type name, which contains the record's validation, creation, and
4337
+ * other behavioral logic.
4338
+ *
4339
+ * @param typeName - The name of the record type to retrieve
4340
+ * @returns The RecordType definition for the specified type
4341
+ *
4342
+ * @throws Will throw an error if the record type does not exist
4343
+ *
4344
+ * @internal
4345
+ */
4346
+ getType(typeName) {
4347
+ const type = getOwnProperty(this.types, typeName);
4348
+ assert(type, "record type does not exists");
4349
+ return type;
4350
+ }
4351
+ };
4352
+
4353
+ // src/index.ts
4354
+ registerDrawLibraryVersion(
4355
+ "@ibodr/store",
4356
+ "0.0.0",
4357
+ "esm"
4358
+ );
4359
+ /*!
4360
+ * This file was lovingly and delicately extracted from Immutable.js
4361
+ * MIT License: https://github.com/immutable-js/immutable-js/blob/main/LICENSE
4362
+ * Copyright (c) 2014-present, Lee Byron and other contributors.
4363
+ */
4364
+
4365
+ export { AtomMap, AtomSet, IncrementalSetConstructor, MigrationFailureReason, RecordType, Store, StoreQueries, StoreSchema, StoreSideEffects, assertIdType, createComputedCache, createEmptyRecordsDiff, createMigrationIds, createMigrationSequence, createRecordMigrationSequence, createRecordType, devFreeze, isRecordsDiffEmpty, parseMigrationId, reverseRecordsDiff, squashRecordDiffs, squashRecordDiffsMutable };
4366
+ //# sourceMappingURL=index.mjs.map
4367
+ //# sourceMappingURL=index.mjs.map