@tldraw/store 4.1.0-canary.af5f4bce7236 → 4.1.0-canary.b34d5b101192
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-cjs/index.d.ts +1880 -153
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/AtomMap.js +241 -1
- package/dist-cjs/lib/AtomMap.js.map +2 -2
- package/dist-cjs/lib/BaseRecord.js.map +2 -2
- package/dist-cjs/lib/ImmutableMap.js +141 -0
- package/dist-cjs/lib/ImmutableMap.js.map +2 -2
- package/dist-cjs/lib/IncrementalSetConstructor.js +45 -5
- package/dist-cjs/lib/IncrementalSetConstructor.js.map +2 -2
- package/dist-cjs/lib/RecordType.js +116 -21
- package/dist-cjs/lib/RecordType.js.map +2 -2
- package/dist-cjs/lib/RecordsDiff.js.map +2 -2
- package/dist-cjs/lib/Store.js +233 -39
- package/dist-cjs/lib/Store.js.map +2 -2
- package/dist-cjs/lib/StoreQueries.js +135 -22
- package/dist-cjs/lib/StoreQueries.js.map +2 -2
- package/dist-cjs/lib/StoreSchema.js +207 -2
- package/dist-cjs/lib/StoreSchema.js.map +2 -2
- package/dist-cjs/lib/StoreSideEffects.js +102 -10
- package/dist-cjs/lib/StoreSideEffects.js.map +2 -2
- package/dist-cjs/lib/executeQuery.js.map +2 -2
- package/dist-cjs/lib/migrate.js.map +2 -2
- package/dist-cjs/lib/setUtils.js.map +2 -2
- package/dist-esm/index.d.mts +1880 -153
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/AtomMap.mjs +241 -1
- package/dist-esm/lib/AtomMap.mjs.map +2 -2
- package/dist-esm/lib/BaseRecord.mjs.map +2 -2
- package/dist-esm/lib/ImmutableMap.mjs +141 -0
- package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
- package/dist-esm/lib/IncrementalSetConstructor.mjs +45 -5
- package/dist-esm/lib/IncrementalSetConstructor.mjs.map +2 -2
- package/dist-esm/lib/RecordType.mjs +116 -21
- package/dist-esm/lib/RecordType.mjs.map +2 -2
- package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
- package/dist-esm/lib/Store.mjs +233 -39
- package/dist-esm/lib/Store.mjs.map +2 -2
- package/dist-esm/lib/StoreQueries.mjs +135 -22
- package/dist-esm/lib/StoreQueries.mjs.map +2 -2
- package/dist-esm/lib/StoreSchema.mjs +207 -2
- package/dist-esm/lib/StoreSchema.mjs.map +2 -2
- package/dist-esm/lib/StoreSideEffects.mjs +102 -10
- package/dist-esm/lib/StoreSideEffects.mjs.map +2 -2
- package/dist-esm/lib/executeQuery.mjs.map +2 -2
- package/dist-esm/lib/migrate.mjs.map +2 -2
- package/dist-esm/lib/setUtils.mjs.map +2 -2
- package/package.json +3 -3
- package/src/lib/AtomMap.ts +241 -1
- package/src/lib/BaseRecord.test.ts +44 -0
- package/src/lib/BaseRecord.ts +118 -4
- package/src/lib/ImmutableMap.test.ts +103 -0
- package/src/lib/ImmutableMap.ts +212 -0
- package/src/lib/IncrementalSetConstructor.test.ts +111 -0
- package/src/lib/IncrementalSetConstructor.ts +63 -6
- package/src/lib/RecordType.ts +149 -25
- package/src/lib/RecordsDiff.test.ts +144 -0
- package/src/lib/RecordsDiff.ts +144 -9
- package/src/lib/Store.test.ts +827 -0
- package/src/lib/Store.ts +533 -67
- package/src/lib/StoreQueries.test.ts +627 -0
- package/src/lib/StoreQueries.ts +194 -27
- package/src/lib/StoreSchema.test.ts +226 -0
- package/src/lib/StoreSchema.ts +386 -8
- package/src/lib/StoreSideEffects.test.ts +239 -19
- package/src/lib/StoreSideEffects.ts +266 -19
- package/src/lib/devFreeze.test.ts +137 -0
- package/src/lib/executeQuery.test.ts +481 -0
- package/src/lib/executeQuery.ts +80 -2
- package/src/lib/migrate.test.ts +400 -0
- package/src/lib/migrate.ts +187 -14
- package/src/lib/setUtils.test.ts +105 -0
- package/src/lib/setUtils.ts +44 -4
package/src/lib/ImmutableMap.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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) {
|