@tldraw/store 4.1.0-canary.8ac9e489017b → 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
@@ -19,7 +19,31 @@ import {
19
19
  validateMigrations,
20
20
  } from './migrate'
21
21
 
22
- /** @public */
22
+ /**
23
+ * Version 1 format for serialized store schema information.
24
+ *
25
+ * This is the legacy format used before schema version 2. Version 1 schemas
26
+ * separate store-level versioning from record-level versioning, and support
27
+ * subtypes for complex record types like shapes.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const schemaV1: SerializedSchemaV1 = {
32
+ * schemaVersion: 1,
33
+ * storeVersion: 2,
34
+ * recordVersions: {
35
+ * book: { version: 3 },
36
+ * shape: {
37
+ * version: 2,
38
+ * subTypeVersions: { rectangle: 1, circle: 2 },
39
+ * subTypeKey: 'type'
40
+ * }
41
+ * }
42
+ * }
43
+ * ```
44
+ *
45
+ * @public
46
+ */
23
47
  export interface SerializedSchemaV1 {
24
48
  /** Schema version is the version for this type you're looking at right now */
25
49
  schemaVersion: 1
@@ -43,7 +67,28 @@ export interface SerializedSchemaV1 {
43
67
  >
44
68
  }
45
69
 
46
- /** @public */
70
+ /**
71
+ * Version 2 format for serialized store schema information.
72
+ *
73
+ * This is the current format that uses a unified sequence-based approach
74
+ * for tracking versions across all migration sequences. Each sequence ID
75
+ * maps to the latest version number for that sequence.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * const schemaV2: SerializedSchemaV2 = {
80
+ * schemaVersion: 2,
81
+ * sequences: {
82
+ * 'com.tldraw.store': 3,
83
+ * 'com.tldraw.book': 2,
84
+ * 'com.tldraw.shape': 4,
85
+ * 'com.tldraw.shape.rectangle': 1
86
+ * }
87
+ * }
88
+ * ```
89
+ *
90
+ * @public
91
+ */
47
92
  export interface SerializedSchemaV2 {
48
93
  schemaVersion: 2
49
94
  sequences: {
@@ -51,9 +96,58 @@ export interface SerializedSchemaV2 {
51
96
  }
52
97
  }
53
98
 
54
- /** @public */
99
+ /**
100
+ * Union type representing all supported serialized schema formats.
101
+ *
102
+ * This type allows the store to handle both legacy (V1) and current (V2)
103
+ * schema formats during deserialization and migration.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * function handleSchema(schema: SerializedSchema) {
108
+ * if (schema.schemaVersion === 1) {
109
+ * // Handle V1 format
110
+ * console.log('Store version:', schema.storeVersion)
111
+ * } else {
112
+ * // Handle V2 format
113
+ * console.log('Sequences:', schema.sequences)
114
+ * }
115
+ * }
116
+ * ```
117
+ *
118
+ * @public
119
+ */
55
120
  export type SerializedSchema = SerializedSchemaV1 | SerializedSchemaV2
56
121
 
122
+ /**
123
+ * Upgrades a serialized schema from version 1 to version 2 format.
124
+ *
125
+ * Version 1 schemas use separate `storeVersion` and `recordVersions` fields,
126
+ * while version 2 schemas use a unified `sequences` object with sequence IDs.
127
+ *
128
+ * @param schema - The serialized schema to upgrade
129
+ * @returns A Result containing the upgraded schema or an error message
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * const v1Schema = {
134
+ * schemaVersion: 1,
135
+ * storeVersion: 1,
136
+ * recordVersions: {
137
+ * book: { version: 2 },
138
+ * author: { version: 1, subTypeVersions: { fiction: 1 }, subTypeKey: 'genre' }
139
+ * }
140
+ * }
141
+ *
142
+ * const result = upgradeSchema(v1Schema)
143
+ * if (result.ok) {
144
+ * console.log(result.value.sequences)
145
+ * // { 'com.tldraw.store': 1, 'com.tldraw.book': 2, 'com.tldraw.author': 1, 'com.tldraw.author.fiction': 1 }
146
+ * }
147
+ * ```
148
+ *
149
+ * @public
150
+ */
57
151
  export function upgradeSchema(schema: SerializedSchema): Result<SerializedSchemaV2, string> {
58
152
  if (schema.schemaVersion > 2 || schema.schemaVersion < 1) return Result.err('Bad schema version')
59
153
  if (schema.schemaVersion === 2) return Result.ok(schema as SerializedSchemaV2)
@@ -75,7 +169,32 @@ export function upgradeSchema(schema: SerializedSchema): Result<SerializedSchema
75
169
  return Result.ok(result)
76
170
  }
77
171
 
78
- /** @public */
172
+ /**
173
+ * Information about a record validation failure that occurred in the store.
174
+ *
175
+ * This interface provides context about validation errors, including the failed
176
+ * record, the store state, and the operation phase where the failure occurred.
177
+ * It's used by validation failure handlers to implement recovery strategies.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * const schema = StoreSchema.create(
182
+ * { book: Book },
183
+ * {
184
+ * onValidationFailure: (failure: StoreValidationFailure<Book>) => {
185
+ * console.error(`Validation failed during ${failure.phase}:`, failure.error)
186
+ * console.log('Failed record:', failure.record)
187
+ * console.log('Previous record:', failure.recordBefore)
188
+ *
189
+ * // Return a corrected version of the record
190
+ * return { ...failure.record, title: failure.record.title || 'Untitled' }
191
+ * }
192
+ * }
193
+ * )
194
+ * ```
195
+ *
196
+ * @public
197
+ */
79
198
  export interface StoreValidationFailure<R extends UnknownRecord> {
80
199
  error: unknown
81
200
  store: Store<R>
@@ -84,7 +203,30 @@ export interface StoreValidationFailure<R extends UnknownRecord> {
84
203
  recordBefore: R | null
85
204
  }
86
205
 
87
- /** @public */
206
+ /**
207
+ * Configuration options for creating a StoreSchema.
208
+ *
209
+ * These options control migration behavior, validation error handling,
210
+ * and integrity checking for the store schema.
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * const options: StoreSchemaOptions<MyRecord, MyProps> = {
215
+ * migrations: [bookMigrations, authorMigrations],
216
+ * onValidationFailure: (failure) => {
217
+ * // Log the error and return a corrected record
218
+ * console.error('Validation failed:', failure.error)
219
+ * return sanitizeRecord(failure.record)
220
+ * },
221
+ * createIntegrityChecker: (store) => {
222
+ * // Set up integrity checking logic
223
+ * return setupIntegrityChecks(store)
224
+ * }
225
+ * }
226
+ * ```
227
+ *
228
+ * @public
229
+ */
88
230
  export interface StoreSchemaOptions<R extends UnknownRecord, P> {
89
231
  migrations?: MigrationSequence[]
90
232
  /** @public */
@@ -93,8 +235,68 @@ export interface StoreSchemaOptions<R extends UnknownRecord, P> {
93
235
  createIntegrityChecker?(store: Store<R, P>): void
94
236
  }
95
237
 
96
- /** @public */
238
+ /**
239
+ * Manages the schema definition, validation, and migration system for a Store.
240
+ *
241
+ * StoreSchema coordinates record types, handles data migrations between schema
242
+ * versions, validates records, and provides the foundational structure for
243
+ * reactive stores. It acts as the central authority for data consistency
244
+ * and evolution within the store system.
245
+ *
246
+ * @example
247
+ * ```ts
248
+ * // Define record types
249
+ * const Book = createRecordType<Book>('book', { scope: 'document' })
250
+ * const Author = createRecordType<Author>('author', { scope: 'document' })
251
+ *
252
+ * // Create schema with migrations
253
+ * const schema = StoreSchema.create(
254
+ * { book: Book, author: Author },
255
+ * {
256
+ * migrations: [bookMigrations, authorMigrations],
257
+ * onValidationFailure: (failure) => {
258
+ * console.warn('Validation failed, using default:', failure.error)
259
+ * return failure.record // or return a corrected version
260
+ * }
261
+ * }
262
+ * )
263
+ *
264
+ * // Use with store
265
+ * const store = new Store({ schema })
266
+ * ```
267
+ *
268
+ * @public
269
+ */
97
270
  export class StoreSchema<R extends UnknownRecord, P = unknown> {
271
+ /**
272
+ * Creates a new StoreSchema with the given record types and options.
273
+ *
274
+ * This static factory method is the recommended way to create a StoreSchema.
275
+ * It ensures type safety while providing a clean API for schema definition.
276
+ *
277
+ * @param types - Object mapping type names to their RecordType definitions
278
+ * @param options - Optional configuration for migrations, validation, and integrity checking
279
+ * @returns A new StoreSchema instance
280
+ *
281
+ * @example
282
+ * ```ts
283
+ * const Book = createRecordType<Book>('book', { scope: 'document' })
284
+ * const Author = createRecordType<Author>('author', { scope: 'document' })
285
+ *
286
+ * const schema = StoreSchema.create(
287
+ * {
288
+ * book: Book,
289
+ * author: Author
290
+ * },
291
+ * {
292
+ * migrations: [bookMigrations],
293
+ * onValidationFailure: (failure) => failure.record
294
+ * }
295
+ * )
296
+ * ```
297
+ *
298
+ * @public
299
+ */
98
300
  static create<R extends UnknownRecord, P = unknown>(
99
301
  // HACK: making this param work with RecordType is an enormous pain
100
302
  // let's just settle for making sure each typeName has a corresponding RecordType
@@ -132,6 +334,35 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
132
334
  }
133
335
  }
134
336
 
337
+ /**
338
+ * Validates a record using its corresponding RecordType validator.
339
+ *
340
+ * This method ensures that records conform to their type definitions before
341
+ * being stored. If validation fails and an onValidationFailure handler is
342
+ * provided, it will be called to potentially recover from the error.
343
+ *
344
+ * @param store - The store instance where validation is occurring
345
+ * @param record - The record to validate
346
+ * @param phase - The lifecycle phase where validation is happening
347
+ * @param recordBefore - The previous version of the record (for updates)
348
+ * @returns The validated record, potentially modified by validation failure handler
349
+ *
350
+ * @example
351
+ * ```ts
352
+ * try {
353
+ * const validatedBook = schema.validateRecord(
354
+ * store,
355
+ * { id: 'book:1', typeName: 'book', title: '', author: 'Jane Doe' },
356
+ * 'createRecord',
357
+ * null
358
+ * )
359
+ * } catch (error) {
360
+ * console.error('Record validation failed:', error)
361
+ * }
362
+ * ```
363
+ *
364
+ * @public
365
+ */
135
366
  validateRecord(
136
367
  store: Store<R>,
137
368
  record: R,
@@ -159,6 +390,34 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
159
390
  }
160
391
  }
161
392
 
393
+ /**
394
+ * Gets all migrations that need to be applied to upgrade from a persisted schema
395
+ * to the current schema version.
396
+ *
397
+ * This method compares the persisted schema with the current schema and determines
398
+ * which migrations need to be applied to bring the data up to date. It handles
399
+ * both regular migrations and retroactive migrations, and caches results for
400
+ * performance.
401
+ *
402
+ * @param persistedSchema - The schema version that was previously persisted
403
+ * @returns A Result containing the list of migrations to apply, or an error message
404
+ *
405
+ * @example
406
+ * ```ts
407
+ * const persistedSchema = {
408
+ * schemaVersion: 2,
409
+ * sequences: { 'com.tldraw.book': 1, 'com.tldraw.author': 0 }
410
+ * }
411
+ *
412
+ * const migrationsResult = schema.getMigrationsSince(persistedSchema)
413
+ * if (migrationsResult.ok) {
414
+ * console.log('Migrations to apply:', migrationsResult.value.length)
415
+ * // Apply each migration to bring data up to date
416
+ * }
417
+ * ```
418
+ *
419
+ * @public
420
+ */
162
421
  public getMigrationsSince(persistedSchema: SerializedSchema): Result<Migration[], string> {
163
422
  // Check cache first
164
423
  const cached = this.migrationCache.get(persistedSchema)
@@ -227,6 +486,34 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
227
486
  return result
228
487
  }
229
488
 
489
+ /**
490
+ * Migrates a single persisted record to match the current schema version.
491
+ *
492
+ * This method applies the necessary migrations to transform a record from an
493
+ * older (or newer) schema version to the current version. It supports both
494
+ * forward ('up') and backward ('down') migrations.
495
+ *
496
+ * @param record - The record to migrate
497
+ * @param persistedSchema - The schema version the record was persisted with
498
+ * @param direction - Direction to migrate ('up' for newer, 'down' for older)
499
+ * @returns A MigrationResult containing the migrated record or an error
500
+ *
501
+ * @example
502
+ * ```ts
503
+ * const oldRecord = { id: 'book:1', typeName: 'book', title: 'Old Title', publishDate: '2020-01-01' }
504
+ * const oldSchema = { schemaVersion: 2, sequences: { 'com.tldraw.book': 1 } }
505
+ *
506
+ * const result = schema.migratePersistedRecord(oldRecord, oldSchema, 'up')
507
+ * if (result.type === 'success') {
508
+ * console.log('Migrated record:', result.value)
509
+ * // Record now has publishedYear instead of publishDate
510
+ * } else {
511
+ * console.error('Migration failed:', result.reason)
512
+ * }
513
+ * ```
514
+ *
515
+ * @public
516
+ */
230
517
  migratePersistedRecord(
231
518
  record: R,
232
519
  persistedSchema: SerializedSchema,
@@ -282,6 +569,37 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
282
569
  return { type: 'success', value: record }
283
570
  }
284
571
 
572
+ /**
573
+ * Migrates an entire store snapshot to match the current schema version.
574
+ *
575
+ * This method applies all necessary migrations to bring a persisted store
576
+ * snapshot up to the current schema version. It handles both record-level
577
+ * and store-level migrations, and can optionally mutate the input store
578
+ * for performance.
579
+ *
580
+ * @param snapshot - The store snapshot containing data and schema information
581
+ * @param opts - Options controlling migration behavior
582
+ * - mutateInputStore - Whether to modify the input store directly (default: false)
583
+ * @returns A MigrationResult containing the migrated store or an error
584
+ *
585
+ * @example
586
+ * ```ts
587
+ * const snapshot = {
588
+ * schema: { schemaVersion: 2, sequences: { 'com.tldraw.book': 1 } },
589
+ * store: {
590
+ * 'book:1': { id: 'book:1', typeName: 'book', title: 'Old Book', publishDate: '2020-01-01' }
591
+ * }
592
+ * }
593
+ *
594
+ * const result = schema.migrateStoreSnapshot(snapshot)
595
+ * if (result.type === 'success') {
596
+ * console.log('Migrated store:', result.value)
597
+ * // All records are now at current schema version
598
+ * }
599
+ * ```
600
+ *
601
+ * @public
602
+ */
285
603
  migrateStoreSnapshot(
286
604
  snapshot: StoreSnapshot<R>,
287
605
  opts?: { mutateInputStore?: boolean }
@@ -330,11 +648,50 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
330
648
  return { type: 'success', value: store }
331
649
  }
332
650
 
333
- /** @internal */
651
+ /**
652
+ * Creates an integrity checker function for the given store.
653
+ *
654
+ * This method calls the createIntegrityChecker option if provided, allowing
655
+ * custom integrity checking logic to be set up for the store. The integrity
656
+ * checker is used to validate store consistency and catch data corruption.
657
+ *
658
+ * @param store - The store instance to create an integrity checker for
659
+ * @returns An integrity checker function, or undefined if none is configured
660
+ *
661
+ * @internal
662
+ */
334
663
  createIntegrityChecker(store: Store<R, P>): (() => void) | undefined {
335
664
  return this.options.createIntegrityChecker?.(store) ?? undefined
336
665
  }
337
666
 
667
+ /**
668
+ * Serializes the current schema to a SerializedSchemaV2 format.
669
+ *
670
+ * This method creates a serialized representation of the current schema,
671
+ * capturing the latest version number for each migration sequence.
672
+ * The result can be persisted and later used to determine what migrations
673
+ * need to be applied when loading data.
674
+ *
675
+ * @returns A SerializedSchemaV2 object representing the current schema state
676
+ *
677
+ * @example
678
+ * ```ts
679
+ * const serialized = schema.serialize()
680
+ * console.log(serialized)
681
+ * // {
682
+ * // schemaVersion: 2,
683
+ * // sequences: {
684
+ * // 'com.tldraw.book': 3,
685
+ * // 'com.tldraw.author': 2
686
+ * // }
687
+ * // }
688
+ *
689
+ * // Store this with your data for future migrations
690
+ * localStorage.setItem('schema', JSON.stringify(serialized))
691
+ * ```
692
+ *
693
+ * @public
694
+ */
338
695
  serialize(): SerializedSchemaV2 {
339
696
  return {
340
697
  schemaVersion: 2,
@@ -348,6 +705,14 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
348
705
  }
349
706
 
350
707
  /**
708
+ * Serializes a schema representing the earliest possible version.
709
+ *
710
+ * This method creates a serialized schema where all migration sequences
711
+ * are set to version 0, representing the state before any migrations
712
+ * have been applied. This is used in specific legacy scenarios.
713
+ *
714
+ * @returns A SerializedSchema with all sequences set to version 0
715
+ *
351
716
  * @deprecated This is only here for legacy reasons, don't use it unless you have david's blessing!
352
717
  * @internal
353
718
  */
@@ -360,7 +725,20 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
360
725
  }
361
726
  }
362
727
 
363
- /** @internal */
728
+ /**
729
+ * Gets the RecordType definition for a given type name.
730
+ *
731
+ * This method retrieves the RecordType associated with the specified
732
+ * type name, which contains the record's validation, creation, and
733
+ * other behavioral logic.
734
+ *
735
+ * @param typeName - The name of the record type to retrieve
736
+ * @returns The RecordType definition for the specified type
737
+ *
738
+ * @throws Will throw an error if the record type does not exist
739
+ *
740
+ * @internal
741
+ */
364
742
  getType(typeName: string) {
365
743
  const type = getOwnProperty(this.types, typeName)
366
744
  assert(type, 'record type does not exists')