@tldraw/store 4.1.0-canary.af5f4bce7236 → 4.1.0-canary.b1f18f73aceb

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 +1880 -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 +1880 -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 +144 -9
  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
@@ -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 ids given a named set of version numbers and a sequence id.
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
- * @public
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
- /** @internal */
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
- /** @public */
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
- /** @public */
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
- /** @public */
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
- /** @public */
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
- /** @public */
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
- /** @public */
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
- /** @public */
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
- /** @internal */
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
- /** @public */
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
- /** @public */
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
+ })
@@ -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 combine.
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
- * @param prev - The previous set
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> = {}