@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.d.ts +3327 -0
- package/dist/index.mjs +4367 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +40 -0
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
|