@tldraw/store 4.1.0-canary.5b2a01989756 → 4.1.0-canary.5d5610599458
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 +1884 -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 +1884 -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 +145 -10
- 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/migrate.ts
CHANGED
|
@@ -23,8 +23,32 @@ function squashDependsOn(sequence: Array<Migration | StandaloneDependsOn>): Migr
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Creates a migration sequence.
|
|
26
|
+
* Creates a migration sequence that defines how to transform data as your schema evolves.
|
|
27
|
+
*
|
|
28
|
+
* A migration sequence contains a series of migrations that are applied in order to transform
|
|
29
|
+
* data from older versions to newer versions. Each migration is identified by a unique ID
|
|
30
|
+
* and can operate at either the record level (transforming individual records) or store level
|
|
31
|
+
* (transforming the entire store structure).
|
|
32
|
+
*
|
|
27
33
|
* See the [migration guide](https://tldraw.dev/docs/persistence#Migrations) for more info on how to use this API.
|
|
34
|
+
* @param options - Configuration for the migration sequence
|
|
35
|
+
* - sequenceId - Unique identifier for this migration sequence (e.g., 'com.myapp.book')
|
|
36
|
+
* - sequence - Array of migrations or dependency declarations to include in the sequence
|
|
37
|
+
* - retroactive - Whether migrations should apply to snapshots created before this sequence was added (defaults to true)
|
|
38
|
+
* @returns A validated migration sequence that can be included in a store schema
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const bookMigrations = createMigrationSequence({
|
|
42
|
+
* sequenceId: 'com.myapp.book',
|
|
43
|
+
* sequence: [
|
|
44
|
+
* {
|
|
45
|
+
* id: 'com.myapp.book/1',
|
|
46
|
+
* scope: 'record',
|
|
47
|
+
* up: (record) => ({ ...record, newField: 'default' })
|
|
48
|
+
* }
|
|
49
|
+
* ]
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
28
52
|
* @public
|
|
29
53
|
*/
|
|
30
54
|
export function createMigrationSequence({
|
|
@@ -46,10 +70,29 @@ export function createMigrationSequence({
|
|
|
46
70
|
}
|
|
47
71
|
|
|
48
72
|
/**
|
|
49
|
-
* Creates a named set of migration
|
|
73
|
+
* Creates a named set of migration IDs from version numbers and a sequence ID.
|
|
74
|
+
*
|
|
75
|
+
* This utility function helps generate properly formatted migration IDs that follow
|
|
76
|
+
* the required `sequenceId/version` pattern. It takes a sequence ID and a record
|
|
77
|
+
* of named versions, returning migration IDs that can be used in migration definitions.
|
|
50
78
|
*
|
|
51
79
|
* See the [migration guide](https://tldraw.dev/docs/persistence#Migrations) for more info on how to use this API.
|
|
52
|
-
* @
|
|
80
|
+
* @param sequenceId - The sequence identifier (e.g., 'com.myapp.book')
|
|
81
|
+
* @param versions - Record mapping version names to numbers
|
|
82
|
+
* @returns Record mapping version names to properly formatted migration IDs
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* const migrationIds = createMigrationIds('com.myapp.book', {
|
|
86
|
+
* addGenre: 1,
|
|
87
|
+
* addPublisher: 2,
|
|
88
|
+
* removeOldField: 3
|
|
89
|
+
* })
|
|
90
|
+
* // Result: {
|
|
91
|
+
* // addGenre: 'com.myapp.book/1',
|
|
92
|
+
* // addPublisher: 'com.myapp.book/2',
|
|
93
|
+
* // removeOldField: 'com.myapp.book/3'
|
|
94
|
+
* // }
|
|
95
|
+
* ```
|
|
53
96
|
* @public
|
|
54
97
|
*/
|
|
55
98
|
export function createMigrationIds<
|
|
@@ -61,7 +104,22 @@ export function createMigrationIds<
|
|
|
61
104
|
) as any
|
|
62
105
|
}
|
|
63
106
|
|
|
64
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Creates a migration sequence specifically for record-level migrations.
|
|
109
|
+
*
|
|
110
|
+
* This is a convenience function that creates a migration sequence where all migrations
|
|
111
|
+
* operate at the record scope and are automatically filtered to apply only to records
|
|
112
|
+
* of a specific type. Each migration in the sequence will be enhanced with the record
|
|
113
|
+
* scope and appropriate filtering logic.
|
|
114
|
+
* @param opts - Configuration for the record migration sequence
|
|
115
|
+
* - recordType - The record type name these migrations should apply to
|
|
116
|
+
* - filter - Optional additional filter function to determine which records to migrate
|
|
117
|
+
* - retroactive - Whether migrations should apply to snapshots created before this sequence was added
|
|
118
|
+
* - sequenceId - Unique identifier for this migration sequence
|
|
119
|
+
* - sequence - Array of record migration definitions (scope will be added automatically)
|
|
120
|
+
* @returns A migration sequence configured for record-level operations
|
|
121
|
+
* @internal
|
|
122
|
+
*/
|
|
65
123
|
export function createRecordMigrationSequence(opts: {
|
|
66
124
|
recordType: string
|
|
67
125
|
filter?(record: UnknownRecord): boolean
|
|
@@ -88,7 +146,14 @@ export function createRecordMigrationSequence(opts: {
|
|
|
88
146
|
})
|
|
89
147
|
}
|
|
90
148
|
|
|
91
|
-
/**
|
|
149
|
+
/**
|
|
150
|
+
* Legacy migration interface for backward compatibility.
|
|
151
|
+
*
|
|
152
|
+
* This interface represents the old migration format that included both `up` and `down`
|
|
153
|
+
* transformation functions. While still supported, new code should use the `Migration`
|
|
154
|
+
* type which provides more flexibility and better integration with the current system.
|
|
155
|
+
* @public
|
|
156
|
+
*/
|
|
92
157
|
export interface LegacyMigration<Before = any, After = any> {
|
|
93
158
|
// eslint-disable-next-line @typescript-eslint/method-signature-style
|
|
94
159
|
up: (oldState: Before) => After
|
|
@@ -96,15 +161,40 @@ export interface LegacyMigration<Before = any, After = any> {
|
|
|
96
161
|
down: (newState: After) => Before
|
|
97
162
|
}
|
|
98
163
|
|
|
99
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* Unique identifier for a migration in the format `sequenceId/version`.
|
|
166
|
+
*
|
|
167
|
+
* Migration IDs follow a specific pattern where the sequence ID identifies the migration
|
|
168
|
+
* sequence and the version number indicates the order within that sequence. For example:
|
|
169
|
+
* 'com.myapp.book/1', 'com.myapp.book/2', etc.
|
|
170
|
+
* @public
|
|
171
|
+
*/
|
|
100
172
|
export type MigrationId = `${string}/${number}`
|
|
101
173
|
|
|
102
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Declares dependencies for migrations without being a migration itself.
|
|
176
|
+
*
|
|
177
|
+
* This interface allows you to specify that future migrations in a sequence depend on
|
|
178
|
+
* migrations from other sequences, without defining an actual migration transformation.
|
|
179
|
+
* It's used to establish cross-sequence dependencies in the migration graph.
|
|
180
|
+
* @public
|
|
181
|
+
*/
|
|
103
182
|
export interface StandaloneDependsOn {
|
|
104
183
|
readonly dependsOn: readonly MigrationId[]
|
|
105
184
|
}
|
|
106
185
|
|
|
107
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* Defines a single migration that transforms data from one schema version to another.
|
|
188
|
+
*
|
|
189
|
+
* A migration can operate at two different scopes:
|
|
190
|
+
* - `record`: Transforms individual records, with optional filtering to target specific records
|
|
191
|
+
* - `store`: Transforms the entire serialized store structure
|
|
192
|
+
*
|
|
193
|
+
* Each migration has a unique ID and can declare dependencies on other migrations that must
|
|
194
|
+
* be applied first. The `up` function performs the forward transformation, while the optional
|
|
195
|
+
* `down` function can reverse the migration if needed.
|
|
196
|
+
* @public
|
|
197
|
+
*/
|
|
108
198
|
export type Migration = {
|
|
109
199
|
readonly id: MigrationId
|
|
110
200
|
readonly dependsOn?: readonly MigrationId[] | undefined
|
|
@@ -131,20 +221,43 @@ export type Migration = {
|
|
|
131
221
|
}
|
|
132
222
|
)
|
|
133
223
|
|
|
134
|
-
/**
|
|
224
|
+
/**
|
|
225
|
+
* Base interface for legacy migration information.
|
|
226
|
+
*
|
|
227
|
+
* Contains the basic structure used by the legacy migration system, including version
|
|
228
|
+
* range information and the migration functions indexed by version number. This is
|
|
229
|
+
* maintained for backward compatibility with older migration definitions.
|
|
230
|
+
* @public
|
|
231
|
+
*/
|
|
135
232
|
export interface LegacyBaseMigrationsInfo {
|
|
136
233
|
firstVersion: number
|
|
137
234
|
currentVersion: number
|
|
138
235
|
migrators: { [version: number]: LegacyMigration }
|
|
139
236
|
}
|
|
140
237
|
|
|
141
|
-
/**
|
|
238
|
+
/**
|
|
239
|
+
* Legacy migration configuration with support for sub-type migrations.
|
|
240
|
+
*
|
|
241
|
+
* This interface extends the base legacy migration info to support migrations that
|
|
242
|
+
* vary based on a sub-type key within records. This allows different migration paths
|
|
243
|
+
* for different variants of the same record type, which was useful in older migration
|
|
244
|
+
* systems but is now handled more elegantly by the current Migration system.
|
|
245
|
+
* @public
|
|
246
|
+
*/
|
|
142
247
|
export interface LegacyMigrations extends LegacyBaseMigrationsInfo {
|
|
143
248
|
subTypeKey?: string
|
|
144
249
|
subTypeMigrations?: Record<string, LegacyBaseMigrationsInfo>
|
|
145
250
|
}
|
|
146
251
|
|
|
147
|
-
/**
|
|
252
|
+
/**
|
|
253
|
+
* A complete sequence of migrations that can be applied to transform data.
|
|
254
|
+
*
|
|
255
|
+
* A migration sequence represents a series of ordered migrations that belong together,
|
|
256
|
+
* typically for a specific part of your schema. The sequence includes metadata about
|
|
257
|
+
* whether it should be applied retroactively to existing data and contains the actual
|
|
258
|
+
* migration definitions in execution order.
|
|
259
|
+
* @public
|
|
260
|
+
*/
|
|
148
261
|
export interface MigrationSequence {
|
|
149
262
|
sequenceId: string
|
|
150
263
|
/**
|
|
@@ -180,6 +293,17 @@ export interface MigrationSequence {
|
|
|
180
293
|
*
|
|
181
294
|
* @param migrations - Array of migrations to sort
|
|
182
295
|
* @returns Sorted array of migrations in execution order
|
|
296
|
+
* @throws Assertion error if circular dependencies are detected
|
|
297
|
+
* @example
|
|
298
|
+
* ```ts
|
|
299
|
+
* const sorted = sortMigrations([
|
|
300
|
+
* { id: 'app/2', scope: 'record', up: (r) => r },
|
|
301
|
+
* { id: 'app/1', scope: 'record', up: (r) => r },
|
|
302
|
+
* { id: 'lib/1', scope: 'record', up: (r) => r, dependsOn: ['app/1'] }
|
|
303
|
+
* ])
|
|
304
|
+
* // Result: [app/1, app/2, lib/1] (respects both sequence and explicit deps)
|
|
305
|
+
* ```
|
|
306
|
+
* @public
|
|
183
307
|
*/
|
|
184
308
|
export function sortMigrations(migrations: Migration[]): Migration[] {
|
|
185
309
|
if (migrations.length === 0) return []
|
|
@@ -285,7 +409,21 @@ export function sortMigrations(migrations: Migration[]): Migration[] {
|
|
|
285
409
|
return result
|
|
286
410
|
}
|
|
287
411
|
|
|
288
|
-
/**
|
|
412
|
+
/**
|
|
413
|
+
* Parses a migration ID to extract the sequence ID and version number.
|
|
414
|
+
*
|
|
415
|
+
* Migration IDs follow the format `sequenceId/version`, and this function splits
|
|
416
|
+
* them into their component parts. This is used internally for sorting migrations
|
|
417
|
+
* and understanding their relationships.
|
|
418
|
+
* @param id - The migration ID to parse
|
|
419
|
+
* @returns Object containing the sequence ID and numeric version
|
|
420
|
+
* @example
|
|
421
|
+
* ```ts
|
|
422
|
+
* const { sequenceId, version } = parseMigrationId('com.myapp.book/5')
|
|
423
|
+
* // sequenceId: 'com.myapp.book', version: 5
|
|
424
|
+
* ```
|
|
425
|
+
* @internal
|
|
426
|
+
*/
|
|
289
427
|
export function parseMigrationId(id: MigrationId): { sequenceId: string; version: number } {
|
|
290
428
|
const [sequenceId, version] = id.split('/')
|
|
291
429
|
return { sequenceId, version: parseInt(version) }
|
|
@@ -302,6 +440,26 @@ function validateMigrationId(id: string, expectedSequenceId?: string) {
|
|
|
302
440
|
assert(id.match(/^(.*?)\/(0|[1-9]\d*)$/), `Invalid migration id: '${id}'`)
|
|
303
441
|
}
|
|
304
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Validates that a migration sequence is correctly structured.
|
|
445
|
+
*
|
|
446
|
+
* Performs several validation checks to ensure the migration sequence is valid:
|
|
447
|
+
* - Sequence ID doesn't contain invalid characters
|
|
448
|
+
* - All migration IDs belong to the expected sequence
|
|
449
|
+
* - Migration versions start at 1 and increment by 1
|
|
450
|
+
* - Migration IDs follow the correct format
|
|
451
|
+
* @param migrations - The migration sequence to validate
|
|
452
|
+
* @throws Assertion error if any validation checks fail
|
|
453
|
+
* @example
|
|
454
|
+
* ```ts
|
|
455
|
+
* const sequence = createMigrationSequence({
|
|
456
|
+
* sequenceId: 'com.myapp.book',
|
|
457
|
+
* sequence: [{ id: 'com.myapp.book/1', scope: 'record', up: (r) => r }]
|
|
458
|
+
* })
|
|
459
|
+
* validateMigrations(sequence) // Passes validation
|
|
460
|
+
* ```
|
|
461
|
+
* @public
|
|
462
|
+
*/
|
|
305
463
|
export function validateMigrations(migrations: MigrationSequence) {
|
|
306
464
|
assert(
|
|
307
465
|
!migrations.sequenceId.includes('/'),
|
|
@@ -331,12 +489,27 @@ export function validateMigrations(migrations: MigrationSequence) {
|
|
|
331
489
|
}
|
|
332
490
|
}
|
|
333
491
|
|
|
334
|
-
/**
|
|
492
|
+
/**
|
|
493
|
+
* Result type returned by migration operations.
|
|
494
|
+
*
|
|
495
|
+
* Migration operations can either succeed and return the transformed value,
|
|
496
|
+
* or fail with a specific reason. This discriminated union type allows for
|
|
497
|
+
* safe handling of both success and error cases when applying migrations.
|
|
498
|
+
* @public
|
|
499
|
+
*/
|
|
335
500
|
export type MigrationResult<T> =
|
|
336
501
|
| { type: 'success'; value: T }
|
|
337
502
|
| { type: 'error'; reason: MigrationFailureReason }
|
|
338
503
|
|
|
339
|
-
/**
|
|
504
|
+
/**
|
|
505
|
+
* Enumeration of possible reasons why a migration might fail.
|
|
506
|
+
*
|
|
507
|
+
* These reasons help identify what went wrong during migration processing,
|
|
508
|
+
* allowing applications to handle different failure scenarios appropriately.
|
|
509
|
+
* Common failures include incompatible data formats, unknown record types,
|
|
510
|
+
* and version mismatches between the data and available migrations.
|
|
511
|
+
* @public
|
|
512
|
+
*/
|
|
340
513
|
export enum MigrationFailureReason {
|
|
341
514
|
IncompatibleSubtype = 'incompatible-subtype',
|
|
342
515
|
UnknownType = 'unknown-type',
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { diffSets, intersectSets } from './setUtils'
|
|
3
|
+
|
|
4
|
+
describe('intersectSets', () => {
|
|
5
|
+
it('should return intersection of multiple sets', () => {
|
|
6
|
+
const set1 = new Set([1, 2, 3, 4])
|
|
7
|
+
const set2 = new Set([2, 3, 4, 5])
|
|
8
|
+
const set3 = new Set([3, 4, 5, 6])
|
|
9
|
+
|
|
10
|
+
const result = intersectSets([set1, set2, set3])
|
|
11
|
+
|
|
12
|
+
expect(Array.from(result).sort()).toEqual([3, 4])
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should return empty set when no sets provided', () => {
|
|
16
|
+
const result = intersectSets([])
|
|
17
|
+
expect(result.size).toBe(0)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should return empty set when no common elements exist', () => {
|
|
21
|
+
const set1 = new Set([1, 2, 3])
|
|
22
|
+
const set2 = new Set([4, 5, 6])
|
|
23
|
+
|
|
24
|
+
const result = intersectSets([set1, set2])
|
|
25
|
+
|
|
26
|
+
expect(result.size).toBe(0)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should return empty set when any set is empty', () => {
|
|
30
|
+
const set1 = new Set([1, 2, 3])
|
|
31
|
+
const set2 = new Set<number>()
|
|
32
|
+
const set3 = new Set([2, 3, 4])
|
|
33
|
+
|
|
34
|
+
const result = intersectSets([set1, set2, set3])
|
|
35
|
+
|
|
36
|
+
expect(result.size).toBe(0)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should return copy of single set', () => {
|
|
40
|
+
const set1 = new Set([1, 2, 3])
|
|
41
|
+
const result = intersectSets([set1])
|
|
42
|
+
|
|
43
|
+
expect(result).not.toBe(set1)
|
|
44
|
+
expect(Array.from(result).sort()).toEqual([1, 2, 3])
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('diffSets', () => {
|
|
49
|
+
it('should detect added elements', () => {
|
|
50
|
+
const prev = new Set(['a', 'b'])
|
|
51
|
+
const next = new Set(['a', 'b', 'c'])
|
|
52
|
+
|
|
53
|
+
const result = diffSets(prev, next)
|
|
54
|
+
|
|
55
|
+
expect(result?.added).toBeDefined()
|
|
56
|
+
expect(result?.removed).toBeUndefined()
|
|
57
|
+
expect(Array.from(result!.added!)).toEqual(['c'])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should detect removed elements', () => {
|
|
61
|
+
const prev = new Set(['a', 'b', 'c'])
|
|
62
|
+
const next = new Set(['a', 'b'])
|
|
63
|
+
|
|
64
|
+
const result = diffSets(prev, next)
|
|
65
|
+
|
|
66
|
+
expect(result?.removed).toBeDefined()
|
|
67
|
+
expect(result?.added).toBeUndefined()
|
|
68
|
+
expect(Array.from(result!.removed!)).toEqual(['c'])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should detect both added and removed elements', () => {
|
|
72
|
+
const prev = new Set(['a', 'b'])
|
|
73
|
+
const next = new Set(['b', 'c'])
|
|
74
|
+
|
|
75
|
+
const result = diffSets(prev, next)
|
|
76
|
+
|
|
77
|
+
expect(result?.added).toBeDefined()
|
|
78
|
+
expect(result?.removed).toBeDefined()
|
|
79
|
+
expect(Array.from(result!.added!)).toEqual(['c'])
|
|
80
|
+
expect(Array.from(result!.removed!)).toEqual(['a'])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should return undefined when sets are identical', () => {
|
|
84
|
+
const prev = new Set(['a', 'b', 'c'])
|
|
85
|
+
const next = new Set(['a', 'b', 'c'])
|
|
86
|
+
|
|
87
|
+
const result = diffSets(prev, next)
|
|
88
|
+
|
|
89
|
+
expect(result).toBeUndefined()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should handle object references correctly', () => {
|
|
93
|
+
const obj1 = { id: 1 }
|
|
94
|
+
const obj2 = { id: 2 }
|
|
95
|
+
const obj3 = { id: 3 }
|
|
96
|
+
|
|
97
|
+
const prev = new Set([obj1, obj2])
|
|
98
|
+
const next = new Set([obj2, obj3])
|
|
99
|
+
|
|
100
|
+
const result = diffSets(prev, next)
|
|
101
|
+
|
|
102
|
+
expect(result?.added?.has(obj3)).toBe(true)
|
|
103
|
+
expect(result?.removed?.has(obj1)).toBe(true)
|
|
104
|
+
})
|
|
105
|
+
})
|
package/src/lib/setUtils.ts
CHANGED
|
@@ -2,8 +2,27 @@ import { CollectionDiff } from './Store'
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Combine multiple sets into a single set containing only the common elements of all sets.
|
|
5
|
+
* Returns the intersection of all provided sets - elements that exist in every set.
|
|
6
|
+
* If no sets are provided, returns an empty set.
|
|
5
7
|
*
|
|
6
|
-
* @param sets - The sets to
|
|
8
|
+
* @param sets - The sets to intersect. Can be an empty array.
|
|
9
|
+
* @returns A new set containing only elements that exist in all input sets
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const set1 = new Set([1, 2, 3])
|
|
14
|
+
* const set2 = new Set([2, 3, 4])
|
|
15
|
+
* const set3 = new Set([3, 4, 5])
|
|
16
|
+
*
|
|
17
|
+
* const intersection = intersectSets([set1, set2, set3])
|
|
18
|
+
* console.log(intersection) // Set {3}
|
|
19
|
+
*
|
|
20
|
+
* // Empty array returns empty set
|
|
21
|
+
* const empty = intersectSets([])
|
|
22
|
+
* console.log(empty) // Set {}
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
7
26
|
*/
|
|
8
27
|
export function intersectSets<T>(sets: Set<T>[]) {
|
|
9
28
|
if (sets.length === 0) return new Set<T>()
|
|
@@ -21,10 +40,31 @@ export function intersectSets<T>(sets: Set<T>[]) {
|
|
|
21
40
|
}
|
|
22
41
|
|
|
23
42
|
/**
|
|
24
|
-
* Calculates a diff between two sets.
|
|
43
|
+
* Calculates a diff between two sets, identifying which elements were added or removed.
|
|
44
|
+
* Returns undefined if the sets are identical (no changes detected).
|
|
45
|
+
*
|
|
46
|
+
* @param prev - The previous set to compare from
|
|
47
|
+
* @param next - The next set to compare to
|
|
48
|
+
* @returns A CollectionDiff object with `added` and/or `removed` sets, or undefined if no changes
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* const prev = new Set(['a', 'b', 'c'])
|
|
53
|
+
* const next = new Set(['b', 'c', 'd'])
|
|
54
|
+
*
|
|
55
|
+
* const diff = diffSets(prev, next)
|
|
56
|
+
* console.log(diff)
|
|
57
|
+
* // {
|
|
58
|
+
* // added: Set {'d'},
|
|
59
|
+
* // removed: Set {'a'}
|
|
60
|
+
* // }
|
|
61
|
+
*
|
|
62
|
+
* // No changes returns undefined
|
|
63
|
+
* const same = diffSets(prev, prev)
|
|
64
|
+
* console.log(same) // undefined
|
|
65
|
+
* ```
|
|
25
66
|
*
|
|
26
|
-
* @
|
|
27
|
-
* @param next - The next set
|
|
67
|
+
* @public
|
|
28
68
|
*/
|
|
29
69
|
export function diffSets<T>(prev: Set<T>, next: Set<T>): CollectionDiff<T> | undefined {
|
|
30
70
|
const result: CollectionDiff<T> = {}
|