@tldraw/store 4.1.0-canary.8e597b345c40 → 4.1.0-canary.95d46c96eb30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist-cjs/index.d.ts +1884 -153
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/AtomMap.js +241 -1
  4. package/dist-cjs/lib/AtomMap.js.map +2 -2
  5. package/dist-cjs/lib/BaseRecord.js.map +2 -2
  6. package/dist-cjs/lib/ImmutableMap.js +141 -0
  7. package/dist-cjs/lib/ImmutableMap.js.map +2 -2
  8. package/dist-cjs/lib/IncrementalSetConstructor.js +45 -5
  9. package/dist-cjs/lib/IncrementalSetConstructor.js.map +2 -2
  10. package/dist-cjs/lib/RecordType.js +116 -21
  11. package/dist-cjs/lib/RecordType.js.map +2 -2
  12. package/dist-cjs/lib/RecordsDiff.js.map +2 -2
  13. package/dist-cjs/lib/Store.js +233 -39
  14. package/dist-cjs/lib/Store.js.map +2 -2
  15. package/dist-cjs/lib/StoreQueries.js +135 -22
  16. package/dist-cjs/lib/StoreQueries.js.map +2 -2
  17. package/dist-cjs/lib/StoreSchema.js +207 -2
  18. package/dist-cjs/lib/StoreSchema.js.map +2 -2
  19. package/dist-cjs/lib/StoreSideEffects.js +102 -10
  20. package/dist-cjs/lib/StoreSideEffects.js.map +2 -2
  21. package/dist-cjs/lib/executeQuery.js.map +2 -2
  22. package/dist-cjs/lib/migrate.js.map +2 -2
  23. package/dist-cjs/lib/setUtils.js.map +2 -2
  24. package/dist-esm/index.d.mts +1884 -153
  25. package/dist-esm/index.mjs +1 -1
  26. package/dist-esm/lib/AtomMap.mjs +241 -1
  27. package/dist-esm/lib/AtomMap.mjs.map +2 -2
  28. package/dist-esm/lib/BaseRecord.mjs.map +2 -2
  29. package/dist-esm/lib/ImmutableMap.mjs +141 -0
  30. package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
  31. package/dist-esm/lib/IncrementalSetConstructor.mjs +45 -5
  32. package/dist-esm/lib/IncrementalSetConstructor.mjs.map +2 -2
  33. package/dist-esm/lib/RecordType.mjs +116 -21
  34. package/dist-esm/lib/RecordType.mjs.map +2 -2
  35. package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
  36. package/dist-esm/lib/Store.mjs +233 -39
  37. package/dist-esm/lib/Store.mjs.map +2 -2
  38. package/dist-esm/lib/StoreQueries.mjs +135 -22
  39. package/dist-esm/lib/StoreQueries.mjs.map +2 -2
  40. package/dist-esm/lib/StoreSchema.mjs +207 -2
  41. package/dist-esm/lib/StoreSchema.mjs.map +2 -2
  42. package/dist-esm/lib/StoreSideEffects.mjs +102 -10
  43. package/dist-esm/lib/StoreSideEffects.mjs.map +2 -2
  44. package/dist-esm/lib/executeQuery.mjs.map +2 -2
  45. package/dist-esm/lib/migrate.mjs.map +2 -2
  46. package/dist-esm/lib/setUtils.mjs.map +2 -2
  47. package/package.json +3 -3
  48. package/src/lib/AtomMap.ts +241 -1
  49. package/src/lib/BaseRecord.test.ts +44 -0
  50. package/src/lib/BaseRecord.ts +118 -4
  51. package/src/lib/ImmutableMap.test.ts +103 -0
  52. package/src/lib/ImmutableMap.ts +212 -0
  53. package/src/lib/IncrementalSetConstructor.test.ts +111 -0
  54. package/src/lib/IncrementalSetConstructor.ts +63 -6
  55. package/src/lib/RecordType.ts +149 -25
  56. package/src/lib/RecordsDiff.test.ts +144 -0
  57. package/src/lib/RecordsDiff.ts +145 -10
  58. package/src/lib/Store.test.ts +827 -0
  59. package/src/lib/Store.ts +533 -67
  60. package/src/lib/StoreQueries.test.ts +627 -0
  61. package/src/lib/StoreQueries.ts +194 -27
  62. package/src/lib/StoreSchema.test.ts +226 -0
  63. package/src/lib/StoreSchema.ts +386 -8
  64. package/src/lib/StoreSideEffects.test.ts +239 -19
  65. package/src/lib/StoreSideEffects.ts +266 -19
  66. package/src/lib/devFreeze.test.ts +137 -0
  67. package/src/lib/executeQuery.test.ts +481 -0
  68. package/src/lib/executeQuery.ts +80 -2
  69. package/src/lib/migrate.test.ts +400 -0
  70. package/src/lib/migrate.ts +187 -14
  71. package/src/lib/setUtils.test.ts +105 -0
  72. package/src/lib/setUtils.ts +44 -4
@@ -190,6 +190,33 @@ const is = Object.is
190
190
 
191
191
  class OwnerID {}
192
192
 
193
+ /**
194
+ * A persistent immutable map implementation based on a Hash Array Mapped Trie (HAMT) data structure.
195
+ * Provides efficient operations for creating, reading, updating, and deleting key-value pairs while
196
+ * maintaining structural sharing to minimize memory usage and maximize performance.
197
+ *
198
+ * This implementation is extracted and adapted from Immutable.js, optimized for tldraw's store needs.
199
+ * All operations return new instances rather than modifying existing ones, ensuring immutability.
200
+ *
201
+ * @public
202
+ * @example
203
+ * ```ts
204
+ * // Create a new map
205
+ * const map = new ImmutableMap([
206
+ * ['key1', 'value1'],
207
+ * ['key2', 'value2']
208
+ * ])
209
+ *
210
+ * // Add or update values
211
+ * const updated = map.set('key3', 'value3')
212
+ *
213
+ * // Get values
214
+ * const value = map.get('key1') // 'value1'
215
+ *
216
+ * // Delete values
217
+ * const smaller = map.delete('key1')
218
+ * ```
219
+ */
193
220
  export class ImmutableMap<K, V> {
194
221
  // @pragma Construction
195
222
  // @ts-ignore
@@ -203,6 +230,22 @@ export class ImmutableMap<K, V> {
203
230
  // @ts-ignore
204
231
  __altered: boolean
205
232
 
233
+ /**
234
+ * Creates a new ImmutableMap instance.
235
+ *
236
+ * @param value - An iterable of key-value pairs to populate the map, or null/undefined for an empty map
237
+ * @example
238
+ * ```ts
239
+ * // Create from array of pairs
240
+ * const map1 = new ImmutableMap([['a', 1], ['b', 2]])
241
+ *
242
+ * // Create empty map
243
+ * const map2 = new ImmutableMap()
244
+ *
245
+ * // Create from another map
246
+ * const map3 = new ImmutableMap(map1)
247
+ * ```
248
+ */
206
249
  constructor(value?: Iterable<[K, V]> | null | undefined) {
207
250
  // @ts-ignore
208
251
  return value === undefined || value === null
@@ -216,19 +259,83 @@ export class ImmutableMap<K, V> {
216
259
  })
217
260
  }
218
261
 
262
+ /**
263
+ * Gets the value associated with the specified key.
264
+ *
265
+ * @param k - The key to look up
266
+ * @returns The value associated with the key, or undefined if not found
267
+ * @example
268
+ * ```ts
269
+ * const map = new ImmutableMap([['key1', 'value1']])
270
+ * console.log(map.get('key1')) // 'value1'
271
+ * console.log(map.get('missing')) // undefined
272
+ * ```
273
+ */
219
274
  get(k: K): V | undefined
275
+ /**
276
+ * Gets the value associated with the specified key, with a fallback value.
277
+ *
278
+ * @param k - The key to look up
279
+ * @param notSetValue - The value to return if the key is not found
280
+ * @returns The value associated with the key, or the fallback value if not found
281
+ * @example
282
+ * ```ts
283
+ * const map = new ImmutableMap([['key1', 'value1']])
284
+ * console.log(map.get('key1', 'default')) // 'value1'
285
+ * console.log(map.get('missing', 'default')) // 'default'
286
+ * ```
287
+ */
220
288
  get(k: K, notSetValue?: V): V {
221
289
  return this._root ? this._root.get(0, undefined as any, k, notSetValue)! : notSetValue!
222
290
  }
223
291
 
292
+ /**
293
+ * Returns a new ImmutableMap with the specified key-value pair added or updated.
294
+ * If the key already exists, its value is replaced. Otherwise, a new entry is created.
295
+ *
296
+ * @param k - The key to set
297
+ * @param v - The value to associate with the key
298
+ * @returns A new ImmutableMap with the key-value pair set
299
+ * @example
300
+ * ```ts
301
+ * const map = new ImmutableMap([['a', 1]])
302
+ * const updated = map.set('b', 2) // New map with both 'a' and 'b'
303
+ * const replaced = map.set('a', 10) // New map with 'a' updated to 10
304
+ * ```
305
+ */
224
306
  set(k: K, v: V) {
225
307
  return updateMap(this, k, v)
226
308
  }
227
309
 
310
+ /**
311
+ * Returns a new ImmutableMap with the specified key removed.
312
+ * If the key doesn't exist, returns the same map instance.
313
+ *
314
+ * @param k - The key to remove
315
+ * @returns A new ImmutableMap with the key removed, or the same instance if key not found
316
+ * @example
317
+ * ```ts
318
+ * const map = new ImmutableMap([['a', 1], ['b', 2]])
319
+ * const smaller = map.delete('a') // New map with only 'b'
320
+ * const same = map.delete('missing') // Returns original map
321
+ * ```
322
+ */
228
323
  delete(k: K) {
229
324
  return updateMap(this, k, NOT_SET as any)
230
325
  }
231
326
 
327
+ /**
328
+ * Returns a new ImmutableMap with all specified keys removed.
329
+ * This is more efficient than calling delete() multiple times.
330
+ *
331
+ * @param keys - An iterable of keys to remove
332
+ * @returns A new ImmutableMap with all specified keys removed
333
+ * @example
334
+ * ```ts
335
+ * const map = new ImmutableMap([['a', 1], ['b', 2], ['c', 3]])
336
+ * const smaller = map.deleteAll(['a', 'c']) // New map with only 'b'
337
+ * ```
338
+ */
232
339
  deleteAll(keys: Iterable<K>) {
233
340
  return this.withMutations((map) => {
234
341
  for (const key of keys) {
@@ -252,32 +359,105 @@ export class ImmutableMap<K, V> {
252
359
  return makeMap(this.size, this._root, ownerID, this.__hash)
253
360
  }
254
361
 
362
+ /**
363
+ * Applies multiple mutations efficiently by creating a mutable copy,
364
+ * applying all changes, then returning an immutable result.
365
+ * This is more efficient than chaining multiple set/delete operations.
366
+ *
367
+ * @param fn - Function that receives a mutable copy and applies changes
368
+ * @returns A new ImmutableMap with all mutations applied, or the same instance if no changes
369
+ * @example
370
+ * ```ts
371
+ * const map = new ImmutableMap([['a', 1]])
372
+ * const updated = map.withMutations(mutable => {
373
+ * mutable.set('b', 2)
374
+ * mutable.set('c', 3)
375
+ * mutable.delete('a')
376
+ * }) // Efficiently applies all changes at once
377
+ * ```
378
+ */
255
379
  withMutations(fn: (mutable: this) => void): this {
256
380
  const mutable = this.asMutable()
257
381
  fn(mutable)
258
382
  return mutable.wasAltered() ? mutable.__ensureOwner(this.__ownerID) : this
259
383
  }
260
384
 
385
+ /**
386
+ * Checks if this map instance has been altered during a mutation operation.
387
+ * This is used internally to optimize mutations.
388
+ *
389
+ * @returns True if the map was altered, false otherwise
390
+ * @internal
391
+ */
261
392
  wasAltered() {
262
393
  return this.__altered
263
394
  }
264
395
 
396
+ /**
397
+ * Returns a mutable copy of this map that can be efficiently modified.
398
+ * Multiple changes to the mutable copy are batched together.
399
+ *
400
+ * @returns A mutable copy of this map
401
+ * @internal
402
+ */
265
403
  asMutable() {
266
404
  return this.__ownerID ? this : this.__ensureOwner(new OwnerID())
267
405
  }
268
406
 
407
+ /**
408
+ * Makes the map iterable, yielding key-value pairs.
409
+ *
410
+ * @returns An iterator over [key, value] pairs
411
+ * @example
412
+ * ```ts
413
+ * const map = new ImmutableMap([['a', 1], ['b', 2]])
414
+ * for (const [key, value] of map) {
415
+ * console.log(key, value) // 'a' 1, then 'b' 2
416
+ * }
417
+ * ```
418
+ */
269
419
  [Symbol.iterator](): Iterator<[K, V]> {
270
420
  return this.entries()[Symbol.iterator]()
271
421
  }
272
422
 
423
+ /**
424
+ * Returns an iterable of key-value pairs.
425
+ *
426
+ * @returns An iterable over [key, value] pairs
427
+ * @example
428
+ * ```ts
429
+ * const map = new ImmutableMap([['a', 1], ['b', 2]])
430
+ * const entries = Array.from(map.entries()) // [['a', 1], ['b', 2]]
431
+ * ```
432
+ */
273
433
  entries(): Iterable<[K, V]> {
274
434
  return new MapIterator(this, ITERATE_ENTRIES, false)
275
435
  }
276
436
 
437
+ /**
438
+ * Returns an iterable of keys.
439
+ *
440
+ * @returns An iterable over keys
441
+ * @example
442
+ * ```ts
443
+ * const map = new ImmutableMap([['a', 1], ['b', 2]])
444
+ * const keys = Array.from(map.keys()) // ['a', 'b']
445
+ * ```
446
+ */
277
447
  keys(): Iterable<K> {
278
448
  return new MapIterator(this, ITERATE_KEYS, false)
279
449
  }
280
450
 
451
+ /**
452
+ * Returns an iterable of values.
453
+ *
454
+ * @returns An iterable over values
455
+ * @example
456
+ * ```ts
457
+ * const map = new ImmutableMap([['a', 1], ['b', 2]])
458
+ * const values = Array.from(map.values()) // [1, 2]
459
+ * ```
460
+ */
281
461
  values(): Iterable<V> {
282
462
  return new MapIterator(this, ITERATE_VALUES, false)
283
463
  }
@@ -757,6 +937,19 @@ function iteratorValue<K, V>(
757
937
  return iteratorResult
758
938
  }
759
939
 
940
+ /**
941
+ * Creates a completed iterator result object indicating iteration is finished.
942
+ * Used internally by map iterators to signal the end of iteration.
943
+ *
944
+ * @returns An IteratorResult object with done set to true and value as undefined
945
+ * @public
946
+ * @example
947
+ * ```ts
948
+ * // Used internally by iterators
949
+ * const result = iteratorDone()
950
+ * console.log(result) // { value: undefined, done: true }
951
+ * ```
952
+ */
760
953
  export function iteratorDone() {
761
954
  return { value: undefined, done: true }
762
955
  }
@@ -772,6 +965,25 @@ function makeMap<K, V>(size: number, root?: MapNode<K, V>, ownerID?: OwnerID, ha
772
965
  }
773
966
 
774
967
  let EMPTY_MAP: ImmutableMap<unknown, unknown>
968
+ /**
969
+ * Returns a singleton empty ImmutableMap instance.
970
+ * This function is optimized to return the same empty map instance for all calls,
971
+ * saving memory when working with many empty maps.
972
+ *
973
+ * @returns An empty ImmutableMap instance
974
+ * @public
975
+ * @example
976
+ * ```ts
977
+ * // Get an empty map
978
+ * const empty = emptyMap<string, number>()
979
+ * console.log(empty.size) // 0
980
+ *
981
+ * // All empty maps are the same instance
982
+ * const empty1 = emptyMap()
983
+ * const empty2 = emptyMap()
984
+ * console.log(empty1 === empty2) // true
985
+ * ```
986
+ */
775
987
  export function emptyMap<K, V>(): ImmutableMap<K, V> {
776
988
  return (EMPTY_MAP as any) || (EMPTY_MAP = makeMap(0))
777
989
  }
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { IncrementalSetConstructor } from './IncrementalSetConstructor'
3
+
4
+ describe('IncrementalSetConstructor', () => {
5
+ describe('core functionality', () => {
6
+ it('should return undefined when no net changes occur', () => {
7
+ const originalSet = new Set(['a', 'b', 'c'])
8
+ const constructor = new IncrementalSetConstructor(originalSet)
9
+
10
+ constructor.add('d')
11
+ constructor.remove('d')
12
+
13
+ const result = constructor.get()
14
+ expect(result).toBeUndefined()
15
+ })
16
+
17
+ it('should return correct result when items are added', () => {
18
+ const originalSet = new Set(['a', 'b'])
19
+ const constructor = new IncrementalSetConstructor(originalSet)
20
+
21
+ constructor.add('c')
22
+ constructor.add('d')
23
+
24
+ const result = constructor.get()
25
+ expect(result).toBeDefined()
26
+ expect(result!.value).toEqual(new Set(['a', 'b', 'c', 'd']))
27
+ expect(result!.diff.added).toEqual(new Set(['c', 'd']))
28
+ expect(result!.diff.removed).toBeUndefined()
29
+ })
30
+
31
+ it('should return correct result when items are removed', () => {
32
+ const originalSet = new Set(['a', 'b', 'c', 'd'])
33
+ const constructor = new IncrementalSetConstructor(originalSet)
34
+
35
+ constructor.remove('c')
36
+ constructor.remove('d')
37
+
38
+ const result = constructor.get()
39
+ expect(result).toBeDefined()
40
+ expect(result!.value).toEqual(new Set(['a', 'b']))
41
+ expect(result!.diff.removed).toEqual(new Set(['c', 'd']))
42
+ expect(result!.diff.added).toBeUndefined()
43
+ })
44
+
45
+ it('should handle mixed add and remove operations correctly', () => {
46
+ const originalSet = new Set(['a', 'b', 'c'])
47
+ const constructor = new IncrementalSetConstructor(originalSet)
48
+
49
+ constructor.remove('a')
50
+ constructor.add('d')
51
+ constructor.add('e')
52
+
53
+ const result = constructor.get()
54
+ expect(result).toBeDefined()
55
+ expect(result!.value).toEqual(new Set(['b', 'c', 'd', 'e']))
56
+ expect(result!.diff.added).toEqual(new Set(['d', 'e']))
57
+ expect(result!.diff.removed).toEqual(new Set(['a']))
58
+ })
59
+
60
+ it('should handle adding existing items as no-op', () => {
61
+ const originalSet = new Set(['a', 'b', 'c'])
62
+ const constructor = new IncrementalSetConstructor(originalSet)
63
+
64
+ constructor.add('a')
65
+ constructor.add('b')
66
+
67
+ const result = constructor.get()
68
+ expect(result).toBeUndefined()
69
+ })
70
+
71
+ it('should handle complex restore and cancel scenarios', () => {
72
+ const originalSet = new Set(['a', 'b', 'c'])
73
+ const constructor = new IncrementalSetConstructor(originalSet)
74
+
75
+ constructor.remove('a')
76
+ constructor.remove('b')
77
+ constructor.add('d')
78
+ constructor.add('a') // Restore one removed item
79
+
80
+ const result = constructor.get()
81
+ expect(result!.value).toEqual(new Set(['a', 'c', 'd']))
82
+ expect(result!.diff.added).toEqual(new Set(['d']))
83
+ expect(result!.diff.removed).toEqual(new Set(['b']))
84
+ })
85
+
86
+ it('should handle removing non-existent items as no-op', () => {
87
+ const originalSet = new Set(['a', 'b'])
88
+ const constructor = new IncrementalSetConstructor(originalSet)
89
+
90
+ constructor.remove('c')
91
+ constructor.remove('d')
92
+
93
+ const result = constructor.get()
94
+ expect(result).toBeUndefined()
95
+ })
96
+
97
+ it('should remove recently added items correctly', () => {
98
+ const originalSet = new Set(['a', 'b'])
99
+ const constructor = new IncrementalSetConstructor(originalSet)
100
+
101
+ constructor.add('c')
102
+ constructor.add('d')
103
+ constructor.remove('c') // Remove recently added item
104
+
105
+ const result = constructor.get()
106
+ expect(result!.value).toEqual(new Set(['a', 'b', 'd']))
107
+ expect(result!.diff.added).toEqual(new Set(['d']))
108
+ expect(result!.diff.removed).toBeUndefined()
109
+ })
110
+ })
111
+ })
@@ -1,7 +1,24 @@
1
1
  import { CollectionDiff } from './Store'
2
2
 
3
3
  /**
4
- * A class that can be used to incrementally construct a set of records.
4
+ * A utility class for incrementally building a set while tracking changes. This class allows
5
+ * you to add and remove items from a set while maintaining a diff of what was added and
6
+ * removed from the original set. It's optimized for cases where you need to track changes
7
+ * to a set over time and get both the final result and the change delta.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const originalSet = new Set(['a', 'b', 'c'])
12
+ * const constructor = new IncrementalSetConstructor(originalSet)
13
+ *
14
+ * constructor.add('d') // Add new item
15
+ * constructor.remove('b') // Remove existing item
16
+ * constructor.add('a') // Re-add removed item (no-op since already present)
17
+ *
18
+ * const result = constructor.get()
19
+ * // result.value contains Set(['a', 'c', 'd'])
20
+ * // result.diff contains { added: Set(['d']), removed: Set(['b']) }
21
+ * ```
5
22
  *
6
23
  * @internal
7
24
  */
@@ -31,7 +48,23 @@ export class IncrementalSetConstructor<T> {
31
48
  ) {}
32
49
 
33
50
  /**
34
- * Get the next value of the set.
51
+ * Gets the result of the incremental set construction if any changes were made.
52
+ * Returns undefined if no additions or removals occurred.
53
+ *
54
+ * @returns An object containing the final set value and the diff of changes,
55
+ * or undefined if no changes were made
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const constructor = new IncrementalSetConstructor(new Set(['a', 'b']))
60
+ * constructor.add('c')
61
+ *
62
+ * const result = constructor.get()
63
+ * // result = {
64
+ * // value: Set(['a', 'b', 'c']),
65
+ * // diff: { added: Set(['c']) }
66
+ * // }
67
+ * ```
35
68
  *
36
69
  * @public
37
70
  */
@@ -65,9 +98,21 @@ export class IncrementalSetConstructor<T> {
65
98
  }
66
99
 
67
100
  /**
68
- * Add an item to the set.
101
+ * Adds an item to the set. If the item was already present in the original set
102
+ * and was previously removed during this construction, it will be restored.
103
+ * If the item is already present and wasn't removed, this is a no-op.
104
+ *
105
+ * @param item - The item to add to the set
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const constructor = new IncrementalSetConstructor(new Set(['a', 'b']))
110
+ * constructor.add('c') // Adds new item
111
+ * constructor.add('a') // No-op, already present
112
+ * constructor.remove('b')
113
+ * constructor.add('b') // Restores previously removed item
114
+ * ```
69
115
  *
70
- * @param item - The item to add.
71
116
  * @public
72
117
  */
73
118
  add(item: T) {
@@ -108,9 +153,21 @@ export class IncrementalSetConstructor<T> {
108
153
  }
109
154
 
110
155
  /**
111
- * Remove an item from the set.
156
+ * Removes an item from the set. If the item wasn't present in the original set
157
+ * and was added during this construction, it will be removed from the added diff.
158
+ * If the item is not present at all, this is a no-op.
159
+ *
160
+ * @param item - The item to remove from the set
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * const constructor = new IncrementalSetConstructor(new Set(['a', 'b']))
165
+ * constructor.remove('a') // Removes existing item
166
+ * constructor.remove('c') // No-op, not present
167
+ * constructor.add('d')
168
+ * constructor.remove('d') // Removes recently added item
169
+ * ```
112
170
  *
113
- * @param item - The item to remove.
114
171
  * @public
115
172
  */
116
173
  remove(item: T) {