@tldraw/store 5.2.0-canary.fb2bc78695c2 → 5.2.0-canary.fb3b3fb8ade6

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 (64) hide show
  1. package/DOCS.md +790 -0
  2. package/README.md +9 -1
  3. package/dist-cjs/index.js +1 -1
  4. package/dist-cjs/lib/ImmutableMap.js +0 -13
  5. package/dist-cjs/lib/ImmutableMap.js.map +2 -2
  6. package/dist-cjs/lib/RecordType.js +1 -1
  7. package/dist-cjs/lib/RecordType.js.map +2 -2
  8. package/dist-cjs/lib/RecordsDiff.js +26 -14
  9. package/dist-cjs/lib/RecordsDiff.js.map +2 -2
  10. package/dist-cjs/lib/Store.js +6 -5
  11. package/dist-cjs/lib/Store.js.map +2 -2
  12. package/dist-cjs/lib/StoreSchema.js +1 -1
  13. package/dist-cjs/lib/StoreSchema.js.map +2 -2
  14. package/dist-cjs/lib/executeQuery.js +1 -1
  15. package/dist-cjs/lib/executeQuery.js.map +2 -2
  16. package/dist-esm/index.mjs +1 -1
  17. package/dist-esm/lib/ImmutableMap.mjs +0 -13
  18. package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
  19. package/dist-esm/lib/RecordType.mjs +1 -1
  20. package/dist-esm/lib/RecordType.mjs.map +2 -2
  21. package/dist-esm/lib/RecordsDiff.mjs +26 -14
  22. package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
  23. package/dist-esm/lib/Store.mjs +7 -6
  24. package/dist-esm/lib/Store.mjs.map +2 -2
  25. package/dist-esm/lib/StoreSchema.mjs +1 -1
  26. package/dist-esm/lib/StoreSchema.mjs.map +2 -2
  27. package/dist-esm/lib/executeQuery.mjs +1 -1
  28. package/dist-esm/lib/executeQuery.mjs.map +2 -2
  29. package/package.json +6 -5
  30. package/src/lib/{test/AtomMap.test.ts → AtomMap.test.ts} +77 -111
  31. package/src/lib/AtomSet.test.ts +116 -0
  32. package/src/lib/BaseRecord.test.ts +10 -22
  33. package/src/lib/ImmutableMap.test.ts +114 -71
  34. package/src/lib/ImmutableMap.ts +1 -0
  35. package/src/lib/IncrementalSetConstructor.test.ts +76 -81
  36. package/src/lib/RecordType.test.ts +216 -0
  37. package/src/lib/RecordType.ts +1 -1
  38. package/src/lib/RecordsDiff.test.ts +112 -106
  39. package/src/lib/RecordsDiff.ts +43 -18
  40. package/src/lib/Store.test.ts +570 -630
  41. package/src/lib/Store.ts +9 -10
  42. package/src/lib/StoreListeners.test.ts +462 -0
  43. package/src/lib/StoreQueries.test.ts +586 -434
  44. package/src/lib/StoreSchema.test.ts +1012 -174
  45. package/src/lib/StoreSchema.ts +1 -1
  46. package/src/lib/StoreSideEffects.test.ts +546 -158
  47. package/src/lib/devFreeze.test.ts +94 -124
  48. package/src/lib/executeQuery.test.ts +77 -31
  49. package/src/lib/executeQuery.ts +3 -1
  50. package/src/lib/migrate.test.ts +273 -296
  51. package/src/lib/setUtils.test.ts +38 -79
  52. package/src/lib/test/createMigrations.test.ts +0 -75
  53. package/src/lib/test/dependsOn.test.ts +0 -166
  54. package/src/lib/test/getMigrationsSince.test.ts +0 -121
  55. package/src/lib/test/migrate.test.ts +0 -118
  56. package/src/lib/test/migratePersistedRecord.test.ts +0 -265
  57. package/src/lib/test/migrationCaching.test.ts +0 -209
  58. package/src/lib/test/recordStore.test.ts +0 -1567
  59. package/src/lib/test/recordStoreQueries.test.ts +0 -814
  60. package/src/lib/test/recordType.test.ts +0 -19
  61. package/src/lib/test/sortMigrations.test.ts +0 -83
  62. package/src/lib/test/upgradeSchema.test.ts +0 -80
  63. package/src/lib/test/validate.test.ts +0 -178
  64. package/src/lib/test/validateMigrations.test.ts +0 -165
package/DOCS.md ADDED
@@ -0,0 +1,790 @@
1
+ # @tldraw/store
2
+
3
+ A reactive record storage library built on `@tldraw/state` that provides type-safe, event-driven database functionality for managing collections of records. You can think of it as a reactive, in-memory database that automatically tracks changes and provides powerful querying capabilities while maintaining excellent performance and type safety.
4
+
5
+ > Important: This documentation assumes you have a basic understanding of reactive programming concepts and TypeScript. The store is built on top of `@tldraw/state` signals, so familiarity with atoms and computed values will be helpful.
6
+
7
+ ## 1. Introduction
8
+
9
+ The `@tldraw/store` manages collections of typed **records** - immutable objects that represent your application's data. Unlike traditional databases, every piece of data in the store is reactive, meaning your application will automatically update when the underlying data changes.
10
+
11
+ The store provides:
12
+
13
+ - **Reactive storage** - Data changes automatically trigger updates throughout your application
14
+ - **Type safety** - Full TypeScript support with compile-time validation
15
+ - **Change tracking** - Complete history of all modifications with undo/redo support
16
+ - **Migrations** - Seamless schema evolution as your application grows
17
+ - **Side effects** - Lifecycle hooks for implementing business logic
18
+ - **Queries** - Reactive indexes and filtering for efficient data access
19
+
20
+ ## 2. Core Concepts
21
+
22
+ ### Records: The Foundation
23
+
24
+ **Records** are immutable data objects that extend the `BaseRecord` interface. Every record has an `id` and a `typeName` that identifies its type:
25
+
26
+ ```ts
27
+ import { BaseRecord, RecordId } from '@tldraw/store'
28
+
29
+ interface Book extends BaseRecord<'book', RecordId<Book>> {
30
+ title: string
31
+ author: string
32
+ publishedYear: number
33
+ inStock: boolean
34
+ }
35
+ ```
36
+
37
+ ### Record Types: Factories for Records
38
+
39
+ A **RecordType** is a factory that creates and manages records of a specific type. You define how records are created, validated, and what their default properties are:
40
+
41
+ ```ts
42
+ import { createRecordType } from '@tldraw/store'
43
+
44
+ const Book = createRecordType<Book>('book', {
45
+ validator: bookValidator, // Optional validation function
46
+ scope: 'document', // Persistence behavior
47
+ }).withDefaultProperties(() => ({
48
+ inStock: true,
49
+ publishedYear: new Date().getFullYear(),
50
+ }))
51
+ ```
52
+
53
+ // Create a new book
54
+
55
+ ## 2. Core Concepts
56
+
57
+ ### Records: The Foundation
58
+
59
+ **Records** are immutable data objects that extend the `BaseRecord` interface. Every record has an `id` and a `typeName` that identifies its type:
60
+
61
+ ```ts
62
+ import { BaseRecord, RecordId } from '@tldraw/store'
63
+
64
+ interface Author extends BaseRecord<'author', RecordId<Author>> {
65
+ name: string
66
+ }
67
+
68
+ interface Book extends BaseRecord<'book', RecordId<Book>> {
69
+ title: string
70
+ authorId: RecordId<Author>
71
+ publishedYear: number
72
+ inStock: boolean
73
+ }
74
+ ```
75
+
76
+ ### Record Types: Factories for Records
77
+
78
+ A **RecordType** is a factory that creates and manages records of a specific type. You define how records are created, validated, and what their default properties are:
79
+
80
+ ```ts
81
+ import { createRecordType } from '@tldraw/store'
82
+
83
+ const Author = createRecordType<Author>('author', {
84
+ scope: 'document',
85
+ })
86
+
87
+ const Book = createRecordType<Book>('book', {
88
+ scope: 'document', // Persistence behavior
89
+ }).withDefaultProperties(() => ({
90
+ inStock: true,
91
+ publishedYear: new Date().getFullYear(),
92
+ }))
93
+
94
+ // Create a new author and book
95
+ const orwell = Author.create({ name: 'George Orwell' })
96
+ const book = Book.create({
97
+ title: '1984',
98
+ authorId: orwell.id,
99
+ })
100
+ // Results in: { id: 'book:abc123', typeName: 'book', title: '1984', authorId: 'author:xyz789', publishedYear: 2025, inStock: true }
101
+ ```
102
+
103
+ ## 3. Basic Usage
104
+
105
+ ### Creating and Storing Records
106
+
107
+ You add records to the store using the `put` method:
108
+
109
+ ```ts
110
+ // Create some authors and books
111
+ const orwell = Author.create({ name: 'George Orwell' })
112
+ const huxley = Author.create({ name: 'Aldous Huxley' })
113
+
114
+ const books = [
115
+ Book.create({ title: '1984', authorId: orwell.id }),
116
+ Book.create({ title: 'Animal Farm', authorId: orwell.id }),
117
+ Book.create({ title: 'Brave New World', authorId: huxley.id }),
118
+ ]
119
+
120
+ // Add authors and books to the store
121
+ store.put([orwell, huxley, ...books])
122
+ ```
123
+
124
+ > Tip: The `put` method handles both creating new records and updating existing ones. If a record with the same ID already exists, it will be updated.
125
+
126
+ ### Reading Records
127
+
128
+ You can read individual records or access all records of a type:
129
+
130
+ ```ts
131
+ // Get a specific book by ID
132
+ const book = store.get(books[0].id)
133
+ console.log(book?.title) // "1984"
134
+
135
+ // Get all records in the store
136
+ const allRecords = store.allRecords()
137
+
138
+ // Check if a record exists
139
+ const hasBook = store.has(books[0].id) // true
140
+ ```
141
+
142
+ ### Updating Records
143
+
144
+ Use the `update` method to modify existing records:
145
+
146
+ ```ts
147
+ // Update a book's stock status
148
+ store.update(book.id, (currentBook) => ({
149
+ ...currentBook,
150
+ inStock: false,
151
+ }))
152
+ ```
153
+
154
+ The update function receives the current record and returns a new record with your changes applied.
155
+
156
+ ### Removing Records
157
+
158
+ Remove records using their IDs:
159
+
160
+ ```ts
161
+ // Remove a single book
162
+ store.remove([book.id])
163
+
164
+ // Remove multiple books
165
+ store.remove([book1.id, book2.id, book3.id])
166
+ ```
167
+
168
+ ## 4. Reactive Queries and Indexing
169
+
170
+ ### Understanding Reactive Indexes
171
+
172
+ The store automatically maintains **reactive indexes** that allow efficient querying of your data. These indexes update automatically when records change:
173
+
174
+ ```ts
175
+ // Create an index by author
176
+ // (assuming `orwell` is an Author record we've created)
177
+ const booksByAuthor = store.query.index('book', 'authorId')
178
+
179
+ // Get all books by George Orwell
180
+ const orwellBooks = booksByAuthor.get().get(orwell.id)
181
+ console.log(orwellBooks) // Set<RecordId<Book>>
182
+ ```
183
+
184
+ The index returns a `Map` where keys are property values and values are `Set`s of record IDs that have that property value.
185
+
186
+ ### Reactive Queries with Computed Values
187
+
188
+ Combine indexes with computed values to create reactive queries:
189
+
190
+ ```ts
191
+ import { computed } from '@tldraw/state'
192
+
193
+ // Create a reactive query for in-stock books
194
+ const inStockBooks = store.query.records('book', () => ({
195
+ inStock: { eq: true },
196
+ }))
197
+
198
+ // Then, group them by author in a second computed value
199
+ const inStockBooksByAuthor = computed('inStockBooksByAuthor', () => {
200
+ const results = new Map<RecordId<Author>, Book[]>()
201
+ for (const book of inStockBooks.get()) {
202
+ const authorBooks = results.get(book.authorId) || []
203
+ authorBooks.push(book)
204
+ results.set(book.authorId, authorBooks)
205
+ }
206
+ return results
207
+ })
208
+
209
+ // The computed value automatically updates when in-stock books change
210
+ console.log(inStockBooksByAuthor.get()) // Map<RecordId<Author>, Book[]>
211
+ ```
212
+
213
+ ### The Store: Your Reactive Database
214
+
215
+ The **Store** is the central container that manages all your records. It provides reactive access to data and automatically tracks changes:
216
+
217
+ ```ts
218
+ import { Store, StoreSchema } from '@tldraw/store'
219
+
220
+ // Create a schema that defines all your record types
221
+ const schema = StoreSchema.create({
222
+ book: Book,
223
+ // ... other record types
224
+ })
225
+
226
+ // Create the store
227
+ const store = new Store({
228
+ schema,
229
+ props: {}, // Custom properties for your application
230
+ })
231
+ ```
232
+
233
+ ## 3. Basic Usage
234
+
235
+ ### Creating and Storing Records
236
+
237
+ You add records to the store using the `put` method:
238
+
239
+ ```ts
240
+ // Create some books
241
+ const books = [
242
+ Book.create({ title: '1984', author: 'George Orwell' }),
243
+ Book.create({ title: 'Animal Farm', author: 'George Orwell' }),
244
+ Book.create({ title: 'Brave New World', author: 'Aldous Huxley' }),
245
+ ]
246
+
247
+ // Add them to the store
248
+ store.put(books)
249
+ ```
250
+
251
+ > Tip: The `put` method handles both creating new records and updating existing ones. If a record with the same ID already exists, it will be updated.
252
+
253
+ ### Reading Records
254
+
255
+ You can read individual records or access all records of a type:
256
+
257
+ ```ts
258
+ // Get a specific book by ID
259
+ const book = store.get(books[0].id)
260
+ console.log(book?.title) // "1984"
261
+
262
+ // Get all records in the store
263
+ const allRecords = store.allRecords()
264
+
265
+ // Check if a record exists
266
+ const hasBook = store.has(books[0].id) // true
267
+ ```
268
+
269
+ ### Updating Records
270
+
271
+ Use the `update` method to modify existing records:
272
+
273
+ ```ts
274
+ // Update a book's stock status
275
+ store.update(book.id, (currentBook) => ({
276
+ ...currentBook,
277
+ inStock: false,
278
+ }))
279
+ ```
280
+
281
+ The update function receives the current record and returns a new record with your changes applied.
282
+
283
+ ### Removing Records
284
+
285
+ Remove records using their IDs:
286
+
287
+ ```ts
288
+ // Remove a single book
289
+ store.remove([book.id])
290
+
291
+ // Remove multiple books
292
+ store.remove([book1.id, book2.id, book3.id])
293
+ ```
294
+
295
+ ## 4. Reactive Queries and Indexing
296
+
297
+ ### Understanding Reactive Indexes
298
+
299
+ The store automatically maintains **reactive indexes** that allow efficient querying of your data. These indexes update automatically when records change:
300
+
301
+ ```ts
302
+ // Create an index by author
303
+ const booksByAuthor = store.query.index('book', 'author')
304
+
305
+ // Get all books by George Orwell
306
+ const orwellBooks = booksByAuthor.get().get('George Orwell')
307
+ console.log(orwellBooks) // Set<RecordId<Book>>
308
+ ```
309
+
310
+ The index returns a `Map` where keys are property values and values are `Set`s of record IDs that have that property value.
311
+
312
+ ### Reactive Queries with Computed Values
313
+
314
+ Combine indexes with computed values to create reactive queries:
315
+
316
+ ```ts
317
+ import { computed } from '@tldraw/state'
318
+
319
+ // Create a reactive query for books in stock by author
320
+ const inStockBooksByAuthor = computed('inStockBooksByAuthor', () => {
321
+ const results = new Map<string, Book[]>()
322
+
323
+ // Get all books
324
+ for (const book of store.allRecords()) {
325
+ if (book.typeName === 'book' && book.inStock) {
326
+ const authorBooks = results.get(book.author) || []
327
+ authorBooks.push(book)
328
+ results.set(book.author, authorBooks)
329
+ }
330
+ }
331
+
332
+ return results
333
+ })
334
+
335
+ // The computed value automatically updates when books change
336
+ console.log(inStockBooksByAuthor.get()) // Map<string, Book[]>
337
+ ```
338
+
339
+ ### Filtering History for Specific Record Types
340
+
341
+ You can create reactive computations that track changes to specific record types:
342
+
343
+ ```ts
344
+ import { react } from '@tldraw/state'
345
+
346
+ // Get a reactive history computation for books only
347
+ const bookHistory = store.query.filterHistory('book')
348
+
349
+ // React to book changes
350
+ const dispose = react('book-changes', () => {
351
+ const currentEpoch = bookHistory.get()
352
+ console.log('Book history updated, current epoch:', currentEpoch)
353
+
354
+ // You can get the actual changes using getDiffSince if needed
355
+ // const changes = bookHistory.getDiffSince(previousEpoch)
356
+ })
357
+ ```
358
+
359
+ > Tip: The `filterHistory` method returns a `Computed` that tracks changes to records of a specific type. Use it with `react()` from `@tldraw/state` to respond to changes.
360
+
361
+ ## 5. Record Scopes and Persistence
362
+
363
+ ### Understanding Record Scopes
364
+
365
+ Records have different **scopes** that determine how they're persisted and synchronized:
366
+
367
+ ```ts
368
+ const DocumentRecord = createRecordType<DocumentData>('document', {
369
+ scope: 'document', // Persisted and synced across instances
370
+ })
371
+
372
+ const SessionRecord = createRecordType<SessionData>('session', {
373
+ scope: 'session', // Per-instance, may be persisted but not synced
374
+ })
375
+
376
+ const PresenceRecord = createRecordType<PresenceData>('presence', {
377
+ scope: 'presence', // Per-instance, synced but not persisted (like cursors)
378
+ })
379
+ ```
380
+
381
+ - **`document`** - Permanent data that should be saved and shared
382
+ - **`session`** - Per-instance data that might be saved locally
383
+ - **`presence`** - Temporary data that's shared but not saved
384
+
385
+ ### Serialization and Snapshots
386
+
387
+ You can serialize the store's data for persistence:
388
+
389
+ ```ts
390
+ // Get a snapshot of all document records
391
+ const snapshot = store.getStoreSnapshot('document')
392
+
393
+ // Save it somewhere
394
+ localStorage.setItem('myApp', JSON.stringify(snapshot))
395
+
396
+ // Later, restore the data
397
+ const saved = JSON.parse(localStorage.getItem('myApp'))
398
+ store.loadStoreSnapshot(saved)
399
+ ```
400
+
401
+ > Note: The store automatically handles migrations when loading snapshots from older versions of your schema.
402
+
403
+ ## 6. Side Effects and Business Logic
404
+
405
+ ### Understanding Side Effects
406
+
407
+ **Side effects** are hooks that let you implement business logic in response to record changes. They run automatically when records are created, updated, or deleted:
408
+
409
+ ```ts
410
+ // React when books are created
411
+ store.sideEffects.registerAfterCreateHandler('book', (book, source) => {
412
+ console.log(`New book added: ${book.title}`)
413
+
414
+ // Update author statistics
415
+ updateAuthorBookCount(book.authorId, 1)
416
+ })
417
+
418
+ // Validate before updates
419
+ store.sideEffects.registerBeforeChangeHandler('book', (prev, next, source) => {
420
+ // Ensure price never goes negative
421
+ if (next.price < 0) {
422
+ return { ...next, price: 0 }
423
+ }
424
+ return next
425
+ })
426
+
427
+ // Clean up when books are deleted
428
+ store.sideEffects.registerAfterDeleteHandler('book', (book, source) => {
429
+ console.log(`Book removed: ${book.title}`)
430
+ updateAuthorBookCount(book.authorId, -1)
431
+ })
432
+ ```
433
+
434
+ ### Change Sources
435
+
436
+ Side effects receive a `source` parameter that tells you where the change originated:
437
+
438
+ - `'user'` - Changes from your application logic
439
+ - `'remote'` - Changes from synchronization or external sources
440
+
441
+ ```ts
442
+ store.sideEffects.registerAfterCreateHandler('book', (book, source) => {
443
+ if (source === 'user') {
444
+ // Only send notifications for local changes
445
+ notifyUser(`You added "${book.title}" to your library`)
446
+ }
447
+ })
448
+ ```
449
+
450
+ ### Before Handlers: Validation and Transformation
451
+
452
+ **Before handlers** run before changes are applied and can validate or transform the data:
453
+
454
+ ```ts
455
+ // Prevent deletion of books that are checked out
456
+ store.sideEffects.registerBeforeDeleteHandler('book', (book, source) => {
457
+ if (book.checkedOut) {
458
+ // Return false to prevent the deletion
459
+ return false
460
+ }
461
+ })
462
+
463
+ // Transform data before storing
464
+ store.sideEffects.registerBeforeCreateHandler('book', (book, source) => {
465
+ // Always store titles in title case
466
+ return {
467
+ ...book,
468
+ title: toTitleCase(book.title),
469
+ }
470
+ })
471
+ ```
472
+
473
+ ## 7. Schema Evolution with Migrations
474
+
475
+ ### Creating Migration Sequences
476
+
477
+ As your application evolves, you'll need to update your data structure. **Migrations** handle this automatically:
478
+
479
+ ```ts
480
+ import { createMigrationSequence } from '@tldraw/store'
481
+
482
+ const bookMigrations = createMigrationSequence({
483
+ sequenceId: 'com.myapp.book',
484
+ sequence: [
485
+ // Migration 1: Add publishedYear field
486
+ {
487
+ id: 'com.myapp.book/add-published-year',
488
+ scope: 'record',
489
+ up: (record: any) => {
490
+ // Convert publishDate string to publishedYear number
491
+ record.publishedYear = new Date(record.publishDate).getFullYear()
492
+ delete record.publishDate
493
+ return record
494
+ },
495
+ down: (record: any) => {
496
+ // Reverse the migration if needed
497
+ record.publishDate = new Date(record.publishedYear, 0, 1).toISOString()
498
+ delete record.publishedYear
499
+ return record
500
+ },
501
+ },
502
+
503
+ // Migration 2: Add genre field with default
504
+ {
505
+ id: 'com.myapp.book/add-genre',
506
+ scope: 'record',
507
+ up: (record: any) => {
508
+ record.genre = record.genre || 'Fiction'
509
+ return record
510
+ },
511
+ down: (record: any) => {
512
+ delete record.genre
513
+ return record
514
+ },
515
+ },
516
+ ],
517
+ })
518
+
519
+ // Include migrations in your schema
520
+ const schema = StoreSchema.create(
521
+ {
522
+ book: Book,
523
+ },
524
+ {
525
+ migrations: [bookMigrations],
526
+ }
527
+ )
528
+ ```
529
+
530
+ ### Migration Safety
531
+
532
+ The store automatically applies migrations when loading data from older versions:
533
+
534
+ ```ts
535
+ // This snapshot might be from an older version
536
+ const oldSnapshot = JSON.parse(localStorage.getItem('myApp'))
537
+
538
+ // Migrations run automatically during load
539
+ store.loadStoreSnapshot(oldSnapshot)
540
+ // All records are now up to date with current schema
541
+ ```
542
+
543
+ > Tip: Always test your migrations thoroughly with real data before deploying to production.
544
+
545
+ ## 8. Advanced Topics
546
+
547
+ ### Transactional Operations
548
+
549
+ The store automatically ensures that related operations happen atomically. When you perform multiple operations in response to user actions or side effects, they are automatically grouped together for consistency and performance.
550
+
551
+ ### Custom Computed Caches
552
+
553
+ Create **computed caches** for expensive derivations that should be memoized per record:
554
+
555
+ ```ts
556
+ const expensiveBookData = store.createComputedCache('expensiveBookData', (book: Book) => {
557
+ // This expensive computation is cached per book
558
+ return performExpensiveAnalysis(book)
559
+ })
560
+
561
+ // Access cached data
562
+ const analysis = expensiveBookData.get(book.id)
563
+ ```
564
+
565
+ The cache automatically updates when the underlying record changes and cleans up when records are deleted.
566
+
567
+ ### Extracting Changes
568
+
569
+ You can capture changes that occur within a function:
570
+
571
+ ```ts
572
+ const changes = store.extractingChanges(() => {
573
+ // Make various changes
574
+ store.put([newBook])
575
+ store.update(book.id, (b) => ({ ...b, title: 'New Title' }))
576
+ })
577
+
578
+ // `changes` contains a diff of what was modified
579
+ console.log(changes) // { added: {...}, updated: {...}, removed: {...} }
580
+ ```
581
+
582
+ This is useful for implementing undo/redo systems or understanding what changed during complex operations.
583
+
584
+ ## 9. Debugging
585
+
586
+ The store provides several tools for understanding what's happening in your application.
587
+
588
+ ### Store Listeners
589
+
590
+ Add listeners to react to changes in your store:
591
+
592
+ ```ts
593
+ // Listen to all changes
594
+ const removeListener = store.listen((entry) => {
595
+ console.log('Changes occurred:', entry.changes)
596
+ console.log('Source:', entry.source) // 'user' or 'remote'
597
+ })
598
+
599
+ // Listen only to document changes from user actions
600
+ const removeDocumentListener = store.listen(
601
+ (entry) => {
602
+ console.log('User made document changes:', entry.changes)
603
+ },
604
+ {
605
+ source: 'user',
606
+ scope: 'document',
607
+ }
608
+ )
609
+ ```
610
+
611
+ ### Understanding Record Validation
612
+
613
+ The store validates all records when they're created or updated. Validation errors provide detailed information:
614
+
615
+ ```ts
616
+ try {
617
+ store.put([invalidBook])
618
+ } catch (error) {
619
+ console.log('Validation failed:', error.message)
620
+ // Check your record's structure and validator function
621
+ }
622
+ ```
623
+
624
+ > Tip: Validation runs in development mode to help catch errors early. Make sure your validators are efficient since they run on every change.
625
+
626
+ ## 10. Integration
627
+
628
+ ### Framework Integration
629
+
630
+ The store is framework-agnostic but integrates well with React through `@tldraw/state-react`:
631
+
632
+ ```ts
633
+ import { track } from '@tldraw/state-react'
634
+
635
+ const BookList = track(() => {
636
+ const books = store.allRecords().filter(r => r.typeName === 'book')
637
+
638
+ return (
639
+ <ul>
640
+ {books.map(book => (
641
+ <li key={book.id}>{book.title} by {book.author}</li>
642
+ ))}
643
+ </ul>
644
+ )
645
+ })
646
+ ```
647
+
648
+ The `track` function automatically subscribes the component to relevant store changes.
649
+
650
+ ### Persistence Strategies
651
+
652
+ Implement different persistence strategies based on your needs:
653
+
654
+ ```ts
655
+ // Auto-save on every change
656
+ store.listen((entry) => {
657
+ if (entry.source === 'user') {
658
+ const snapshot = store.getStoreSnapshot()
659
+ saveToDatabase(snapshot)
660
+ }
661
+ })
662
+
663
+ // Batch saves every few seconds
664
+ let saveTimeout: NodeJS.Timeout
665
+ store.listen(() => {
666
+ clearTimeout(saveTimeout)
667
+ saveTimeout = setTimeout(() => {
668
+ const snapshot = store.getStoreSnapshot()
669
+ saveToDatabase(snapshot)
670
+ }, 5000)
671
+ })
672
+ ```
673
+
674
+ ### Synchronization
675
+
676
+ For multi-user applications, use the store with `@tldraw/sync`:
677
+
678
+ ```ts
679
+ // Merge remote changes
680
+ store.mergeRemoteChanges(() => {
681
+ // Apply changes from other users
682
+ store.put(remoteRecords)
683
+ })
684
+ ```
685
+
686
+ Changes merged this way are marked with `source: 'remote'` so your side effects can handle them appropriately.
687
+
688
+ ## 11. Performance Considerations
689
+
690
+ ### Memory Management
691
+
692
+ The store uses several strategies to maintain good performance:
693
+
694
+ - **Lazy atom creation** - Atoms are only created when records are accessed reactively
695
+ - **Automatic cleanup** - Atoms are cleaned up when records are deleted
696
+ - **Efficient indexes** - Reactive indexes use incremental updates rather than full rebuilds
697
+ - **Change batching** - Multiple changes are batched together before notifying listeners
698
+
699
+ ### Query Optimization
700
+
701
+ For best performance with large datasets:
702
+
703
+ ```ts
704
+ // Use indexes for common queries
705
+ const booksByGenre = store.query.index('book', 'genre')
706
+ const sciFiBooks = booksByGenre.get().get('Science Fiction')
707
+
708
+ // Avoid reactive access in hot paths
709
+ const book = store.unsafeGetWithoutCapture(bookId) // No reactive subscription
710
+
711
+ // Use computed caches for expensive derivations
712
+ const expensiveData = store.createComputedCache('expensive', computeExpensiveData)
713
+ ```
714
+
715
+ ### Side Effect Performance
716
+
717
+ Keep side effects fast since they run synchronously:
718
+
719
+ ```ts
720
+ // Good: Fast side effect
721
+ store.sideEffects.registerAfterCreateHandler('book', (book) => {
722
+ updateQuickStats(book)
723
+ })
724
+
725
+ // Better: Async work in background
726
+ store.sideEffects.registerAfterCreateHandler('book', (book) => {
727
+ queueAsyncWork(book) // Handle async work separately
728
+ })
729
+ ```
730
+
731
+ ## 12. Common Patterns
732
+
733
+ ### Repository Pattern
734
+
735
+ Create typed repositories for cleaner APIs:
736
+
737
+ ```ts
738
+ class BookRepository {
739
+ constructor(private store: Store<Book>) {}
740
+
741
+ findByAuthor(author: string): Book[] {
742
+ return this.store.allRecords().filter((book) => book.author === author)
743
+ }
744
+
745
+ getInStock(): Book[] {
746
+ return this.store.allRecords().filter((book) => book.inStock)
747
+ }
748
+
749
+ create(data: Omit<Book, 'id' | 'typeName'>): Book {
750
+ const book = Book.create(data)
751
+ this.store.put([book])
752
+ return book
753
+ }
754
+ }
755
+ ```
756
+
757
+ ### State Machines with Records
758
+
759
+ Use records to represent state machine states:
760
+
761
+ ```ts
762
+ interface OrderState extends BaseRecord<'orderState', RecordId<OrderState>> {
763
+ orderId: string
764
+ status: 'pending' | 'confirmed' | 'shipped' | 'delivered'
765
+ createdAt: number
766
+ }
767
+
768
+ const OrderState = createRecordType<OrderState>('orderState', { scope: 'document' })
769
+
770
+ // Transition states with side effects
771
+ store.sideEffects.registerAfterChangeHandler('orderState', (prev, next) => {
772
+ if (prev.status !== next.status) {
773
+ handleStatusChange(prev.status, next.status, next.orderId)
774
+ }
775
+ })
776
+ ```
777
+
778
+ ### Computed Relationships
779
+
780
+ Model relationships between records using efficient queries:
781
+
782
+ ```ts
783
+ // Get all books by a specific author
784
+ const authorId = 'author:123'
785
+ const authorBooks = store.query.records('book', () => ({
786
+ authorId: { eq: authorId },
787
+ }))
788
+ ```
789
+
790
+ The store provides a powerful, reactive foundation for managing your application's data. By understanding these patterns and concepts, you can build applications that automatically stay in sync as data changes, while maintaining excellent performance and type safety throughout.