@tldraw/store 4.1.0-canary.9f9255bd7a83 → 4.1.0-canary.a6e63b3bbde6

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
@@ -1,20 +1,240 @@
1
- // let editor: Editor
2
- // beforeEach(() => {
3
- // editor = new Editor({
4
- // shapeUtils: [],
5
- // tools: [],
6
- // store: createTLStore({ shapeUtils: [] }),
7
- // getContainer: () => document.body,
8
- // })
9
- // })
10
-
11
- describe('Side effect manager', () => {
12
- it.todo('Registers an onBeforeCreate handler')
13
- it.todo('Registers an onAfterCreate handler')
14
- it.todo('Registers an onBeforeChange handler')
15
- it.todo('Registers an onAfterChange handler')
16
- it.todo('Registers an onBeforeDelete handler')
17
- it.todo('Registers an onAfterDelete handler')
18
- it.todo('Registers a batch start handler')
19
- it.todo('Registers a batch complete handler')
1
+ import { BaseRecord, RecordId } from './BaseRecord'
2
+ import { createRecordType } from './RecordType'
3
+ import { Store } from './Store'
4
+ import { StoreSchema } from './StoreSchema'
5
+
6
+ interface TestRecord extends BaseRecord<'test', RecordId<TestRecord>> {
7
+ name: string
8
+ value: number
9
+ }
10
+
11
+ const TestRecord = createRecordType<TestRecord>('test', {
12
+ validator: {
13
+ validate: (record) => record as TestRecord,
14
+ },
15
+ scope: 'document',
16
+ }).withDefaultProperties(() => ({
17
+ name: '',
18
+ value: 0,
19
+ }))
20
+
21
+ describe('StoreSideEffects', () => {
22
+ let store: Store<TestRecord>
23
+
24
+ beforeEach(() => {
25
+ const schema = StoreSchema.create<TestRecord>({ test: TestRecord })
26
+ store = new Store({ schema, props: {} })
27
+ })
28
+
29
+ describe('beforeCreate handlers', () => {
30
+ it('calls beforeCreate handlers and allows transformation', () => {
31
+ let handlerCalled = false
32
+ store.sideEffects.registerBeforeCreateHandler('test', (record, source) => {
33
+ handlerCalled = true
34
+ expect(source).toBe('user')
35
+ return { ...record, name: 'transformed' }
36
+ })
37
+
38
+ const record = TestRecord.create({ name: 'original', value: 42 })
39
+ store.put([record])
40
+
41
+ expect(handlerCalled).toBe(true)
42
+ expect(store.get(record.id)!.name).toBe('transformed')
43
+ })
44
+
45
+ it('chains multiple beforeCreate handlers', () => {
46
+ store.sideEffects.registerBeforeCreateHandler('test', (record) => ({
47
+ ...record,
48
+ value: record.value + 10,
49
+ }))
50
+ store.sideEffects.registerBeforeCreateHandler('test', (record) => ({
51
+ ...record,
52
+ value: record.value * 2,
53
+ }))
54
+
55
+ const record = TestRecord.create({ name: 'test', value: 5 })
56
+ store.put([record])
57
+
58
+ // First handler: 5 + 10 = 15, Second handler: 15 * 2 = 30
59
+ expect(store.get(record.id)!.value).toBe(30)
60
+ })
61
+ })
62
+
63
+ describe('afterCreate handlers', () => {
64
+ it('calls afterCreate handlers after record creation', () => {
65
+ let handlerCalled = false
66
+ let capturedRecord: TestRecord | null = null
67
+ store.sideEffects.registerAfterCreateHandler('test', (record, source) => {
68
+ handlerCalled = true
69
+ capturedRecord = record
70
+ expect(source).toBe('user')
71
+ })
72
+
73
+ const record = TestRecord.create({ name: 'test', value: 42 })
74
+ store.put([record])
75
+
76
+ expect(handlerCalled).toBe(true)
77
+ expect(capturedRecord).toEqual(record)
78
+ })
79
+ })
80
+
81
+ describe('beforeChange handlers', () => {
82
+ it('allows transformation of changes', () => {
83
+ const record = TestRecord.create({ name: 'test', value: 42 })
84
+ store.put([record])
85
+
86
+ store.sideEffects.registerBeforeChangeHandler('test', (prev, next) => ({
87
+ ...next,
88
+ name: next.name + '_transformed',
89
+ }))
90
+
91
+ store.update(record.id, (r) => ({ ...r, name: 'updated' }))
92
+
93
+ expect(store.get(record.id)!.name).toBe('updated_transformed')
94
+ })
95
+
96
+ it('can prevent changes by returning previous record', () => {
97
+ const record = TestRecord.create({ name: 'test', value: 42 })
98
+ store.put([record])
99
+
100
+ store.sideEffects.registerBeforeChangeHandler('test', (prev, next) => {
101
+ if (next.value > 100) return prev
102
+ return next
103
+ })
104
+
105
+ store.update(record.id, (r) => ({ ...r, value: 200 }))
106
+
107
+ expect(store.get(record.id)!.value).toBe(42) // unchanged
108
+ })
109
+ })
110
+
111
+ describe('afterChange handlers', () => {
112
+ it('calls afterChange handlers with previous and current records', () => {
113
+ const record = TestRecord.create({ name: 'test', value: 42 })
114
+ store.put([record])
115
+
116
+ let handlerCalled = false
117
+ let capturedPrev: TestRecord | null = null
118
+ let capturedNext: TestRecord | null = null
119
+
120
+ store.sideEffects.registerAfterChangeHandler('test', (prev, next, source) => {
121
+ handlerCalled = true
122
+ capturedPrev = prev
123
+ capturedNext = next
124
+ expect(source).toBe('user')
125
+ })
126
+
127
+ store.update(record.id, (r) => ({ ...r, value: 100 }))
128
+
129
+ expect(handlerCalled).toBe(true)
130
+ expect(capturedPrev!.value).toBe(42)
131
+ expect(capturedNext!.value).toBe(100)
132
+ })
133
+ })
134
+
135
+ describe('beforeDelete handlers', () => {
136
+ it('can prevent deletion by returning false', () => {
137
+ const record = TestRecord.create({ name: 'protected', value: 42 })
138
+ store.put([record])
139
+
140
+ store.sideEffects.registerBeforeDeleteHandler('test', (record) => {
141
+ if (record.name === 'protected') return false
142
+ return
143
+ })
144
+
145
+ store.remove([record.id])
146
+
147
+ expect(store.get(record.id)).toBeDefined() // still exists
148
+ })
149
+
150
+ it('allows deletion when handler returns void', () => {
151
+ const record = TestRecord.create({ name: 'deletable', value: 42 })
152
+ store.put([record])
153
+
154
+ let handlerCalled = false
155
+ store.sideEffects.registerBeforeDeleteHandler('test', (_record) => {
156
+ handlerCalled = true
157
+ return // void return allows deletion
158
+ })
159
+
160
+ store.remove([record.id])
161
+
162
+ expect(handlerCalled).toBe(true)
163
+ expect(store.get(record.id)).toBeUndefined()
164
+ })
165
+ })
166
+
167
+ describe('afterDelete handlers', () => {
168
+ it('calls afterDelete handlers with deleted record', () => {
169
+ const record = TestRecord.create({ name: 'test', value: 42 })
170
+ store.put([record])
171
+
172
+ let handlerCalled = false
173
+ let capturedRecord: TestRecord | null = null
174
+
175
+ store.sideEffects.registerAfterDeleteHandler('test', (record, source) => {
176
+ handlerCalled = true
177
+ capturedRecord = record
178
+ expect(source).toBe('user')
179
+ })
180
+
181
+ store.remove([record.id])
182
+
183
+ expect(handlerCalled).toBe(true)
184
+ expect(capturedRecord).toEqual(record)
185
+ })
186
+ })
187
+
188
+ describe('operationComplete handlers', () => {
189
+ it('calls handler after operation completes', () => {
190
+ let handlerCalled = false
191
+ let capturedSource: 'user' | 'remote' | null = null
192
+
193
+ store.sideEffects.registerOperationCompleteHandler((source) => {
194
+ handlerCalled = true
195
+ capturedSource = source
196
+ })
197
+
198
+ const record = TestRecord.create({ name: 'test', value: 42 })
199
+ store.put([record])
200
+
201
+ expect(handlerCalled).toBe(true)
202
+ expect(capturedSource).toBe('user')
203
+ })
204
+ })
205
+
206
+ describe('handler cleanup', () => {
207
+ it('removes handlers when cleanup function is called', () => {
208
+ let handlerCalled = false
209
+ const cleanup = store.sideEffects.registerBeforeCreateHandler('test', (record) => {
210
+ handlerCalled = true
211
+ return { ...record, name: 'transformed' }
212
+ })
213
+
214
+ cleanup()
215
+
216
+ const record = TestRecord.create({ name: 'original', value: 42 })
217
+ store.put([record])
218
+
219
+ expect(handlerCalled).toBe(false)
220
+ expect(store.get(record.id)!.name).toBe('original')
221
+ })
222
+ })
223
+
224
+ describe('source parameter handling', () => {
225
+ it('passes correct source to handlers for remote changes', () => {
226
+ let capturedSource: 'user' | 'remote' | null = null
227
+
228
+ store.sideEffects.registerAfterCreateHandler('test', (_, source) => {
229
+ capturedSource = source
230
+ })
231
+
232
+ const record = TestRecord.create({ name: 'test', value: 42 })
233
+ store.mergeRemoteChanges(() => {
234
+ store.put([record])
235
+ })
236
+
237
+ expect(capturedSource).toBe('remote')
238
+ })
239
+ })
20
240
  })
@@ -1,51 +1,211 @@
1
1
  import { UnknownRecord } from './BaseRecord'
2
2
  import { Store } from './Store'
3
3
 
4
- /** @public */
4
+ /**
5
+ * Handler function called before a record is created in the store.
6
+ * The handler receives the record to be created and can return a modified version.
7
+ * Use this to validate, transform, or modify records before they are added to the store.
8
+ *
9
+ * @param record - The record about to be created
10
+ * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization
11
+ * @returns The record to actually create (may be modified)
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const handler: StoreBeforeCreateHandler<MyRecord> = (record, source) => {
16
+ * // Ensure all user-created records have a timestamp
17
+ * if (source === 'user' && !record.createdAt) {
18
+ * return { ...record, createdAt: Date.now() }
19
+ * }
20
+ * return record
21
+ * }
22
+ * ```
23
+ *
24
+ * @public
25
+ */
5
26
  export type StoreBeforeCreateHandler<R extends UnknownRecord> = (
6
27
  record: R,
7
28
  source: 'remote' | 'user'
8
29
  ) => R
9
- /** @public */
30
+ /**
31
+ * Handler function called after a record has been successfully created in the store.
32
+ * Use this for side effects that should happen after record creation, such as updating
33
+ * related records or triggering notifications.
34
+ *
35
+ * @param record - The record that was created
36
+ * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const handler: StoreAfterCreateHandler<BookRecord> = (book, source) => {
41
+ * if (source === 'user') {
42
+ * console.log(`New book added: ${book.title}`)
43
+ * updateAuthorBookCount(book.authorId)
44
+ * }
45
+ * }
46
+ * ```
47
+ *
48
+ * @public
49
+ */
10
50
  export type StoreAfterCreateHandler<R extends UnknownRecord> = (
11
51
  record: R,
12
52
  source: 'remote' | 'user'
13
53
  ) => void
14
- /** @public */
54
+ /**
55
+ * Handler function called before a record is updated in the store.
56
+ * The handler receives the current and new versions of the record and can return
57
+ * a modified version or the original to prevent the change.
58
+ *
59
+ * @param prev - The current version of the record in the store
60
+ * @param next - The proposed new version of the record
61
+ * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization
62
+ * @returns The record version to actually store (may be modified or the original to block change)
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * const handler: StoreBeforeChangeHandler<ShapeRecord> = (prev, next, source) => {
67
+ * // Prevent shapes from being moved outside the canvas bounds
68
+ * if (next.x < 0 || next.y < 0) {
69
+ * return prev // Block the change
70
+ * }
71
+ * return next
72
+ * }
73
+ * ```
74
+ *
75
+ * @public
76
+ */
15
77
  export type StoreBeforeChangeHandler<R extends UnknownRecord> = (
16
78
  prev: R,
17
79
  next: R,
18
80
  source: 'remote' | 'user'
19
81
  ) => R
20
- /** @public */
82
+ /**
83
+ * Handler function called after a record has been successfully updated in the store.
84
+ * Use this for side effects that should happen after record changes, such as
85
+ * updating related records or maintaining consistency constraints.
86
+ *
87
+ * @param prev - The previous version of the record
88
+ * @param next - The new version of the record that was stored
89
+ * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * const handler: StoreAfterChangeHandler<ShapeRecord> = (prev, next, source) => {
94
+ * // Update connected arrows when a shape moves
95
+ * if (prev.x !== next.x || prev.y !== next.y) {
96
+ * updateConnectedArrows(next.id)
97
+ * }
98
+ * }
99
+ * ```
100
+ *
101
+ * @public
102
+ */
21
103
  export type StoreAfterChangeHandler<R extends UnknownRecord> = (
22
104
  prev: R,
23
105
  next: R,
24
106
  source: 'remote' | 'user'
25
107
  ) => void
26
- /** @public */
108
+ /**
109
+ * Handler function called before a record is deleted from the store.
110
+ * The handler can return `false` to prevent the deletion from occurring.
111
+ *
112
+ * @param record - The record about to be deleted
113
+ * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization
114
+ * @returns `false` to prevent deletion, `void` or any other value to allow it
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * const handler: StoreBeforeDeleteHandler<BookRecord> = (book, source) => {
119
+ * // Prevent deletion of books that are currently checked out
120
+ * if (book.isCheckedOut) {
121
+ * console.warn('Cannot delete checked out book')
122
+ * return false
123
+ * }
124
+ * // Allow deletion for other books
125
+ * }
126
+ * ```
127
+ *
128
+ * @public
129
+ */
27
130
  export type StoreBeforeDeleteHandler<R extends UnknownRecord> = (
28
131
  record: R,
29
132
  source: 'remote' | 'user'
30
133
  ) => void | false
31
- /** @public */
134
+ /**
135
+ * Handler function called after a record has been successfully deleted from the store.
136
+ * Use this for cleanup operations and maintaining referential integrity.
137
+ *
138
+ * @param record - The record that was deleted
139
+ * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * const handler: StoreAfterDeleteHandler<ShapeRecord> = (shape, source) => {
144
+ * // Clean up arrows that were connected to this shape
145
+ * const connectedArrows = findArrowsConnectedTo(shape.id)
146
+ * store.remove(connectedArrows.map(arrow => arrow.id))
147
+ * }
148
+ * ```
149
+ *
150
+ * @public
151
+ */
32
152
  export type StoreAfterDeleteHandler<R extends UnknownRecord> = (
33
153
  record: R,
34
154
  source: 'remote' | 'user'
35
155
  ) => void
36
156
 
37
- /** @public */
157
+ /**
158
+ * Handler function called when a store operation (atomic transaction) completes.
159
+ * This is useful for performing actions after a batch of changes has been applied,
160
+ * such as triggering saves or sending notifications.
161
+ *
162
+ * @param source - Whether the operation originated from 'user' interaction or 'remote' synchronization
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * const handler: StoreOperationCompleteHandler = (source) => {
167
+ * if (source === 'user') {
168
+ * // Auto-save after user operations complete
169
+ * saveStoreSnapshot()
170
+ * }
171
+ * }
172
+ * ```
173
+ *
174
+ * @public
175
+ */
38
176
  export type StoreOperationCompleteHandler = (source: 'remote' | 'user') => void
39
177
 
40
178
  /**
41
179
  * The side effect manager (aka a "correct state enforcer") is responsible
42
- * for making sure that the editor's state is always correct. This includes
180
+ * for making sure that the store's state is always correct and consistent. This includes
43
181
  * things like: deleting a shape if its parent is deleted; unbinding
44
- * arrows when their binding target is deleted; etc.
182
+ * arrows when their binding target is deleted; maintaining referential integrity; etc.
183
+ *
184
+ * Side effects are organized into lifecycle hooks that run before and after
185
+ * record operations (create, change, delete), allowing you to validate data,
186
+ * transform records, and maintain business rules.
187
+ *
188
+ * @example
189
+ * ```ts
190
+ * const sideEffects = new StoreSideEffects(store)
191
+ *
192
+ * // Ensure arrows are deleted when their target shape is deleted
193
+ * sideEffects.registerAfterDeleteHandler('shape', (shape) => {
194
+ * const arrows = store.query.records('arrow', () => ({
195
+ * toId: { eq: shape.id }
196
+ * })).get()
197
+ * store.remove(arrows.map(arrow => arrow.id))
198
+ * })
199
+ * ```
45
200
  *
46
201
  * @public
47
202
  */
48
203
  export class StoreSideEffects<R extends UnknownRecord> {
204
+ /**
205
+ * Creates a new side effects manager for the given store.
206
+ *
207
+ * store - The store instance to manage side effects for
208
+ */
49
209
  constructor(private readonly store: Store<R>) {}
50
210
 
51
211
  private _beforeCreateHandlers: { [K in string]?: StoreBeforeCreateHandler<any>[] } = {}
@@ -57,16 +217,36 @@ export class StoreSideEffects<R extends UnknownRecord> {
57
217
  private _operationCompleteHandlers: StoreOperationCompleteHandler[] = []
58
218
 
59
219
  private _isEnabled = true
60
- /** @internal */
220
+ /**
221
+ * Checks whether side effects are currently enabled.
222
+ * When disabled, all side effect handlers are bypassed.
223
+ *
224
+ * @returns `true` if side effects are enabled, `false` otherwise
225
+ * @internal
226
+ */
61
227
  isEnabled() {
62
228
  return this._isEnabled
63
229
  }
64
- /** @internal */
230
+ /**
231
+ * Enables or disables side effects processing.
232
+ * When disabled, no side effect handlers will be called.
233
+ *
234
+ * @param enabled - Whether to enable or disable side effects
235
+ * @internal
236
+ */
65
237
  setIsEnabled(enabled: boolean) {
66
238
  this._isEnabled = enabled
67
239
  }
68
240
 
69
- /** @internal */
241
+ /**
242
+ * Processes all registered 'before create' handlers for a record.
243
+ * Handlers are called in registration order and can transform the record.
244
+ *
245
+ * @param record - The record about to be created
246
+ * @param source - Whether the change originated from 'user' or 'remote'
247
+ * @returns The potentially modified record to actually create
248
+ * @internal
249
+ */
70
250
  handleBeforeCreate(record: R, source: 'remote' | 'user') {
71
251
  if (!this._isEnabled) return record
72
252
 
@@ -82,7 +262,14 @@ export class StoreSideEffects<R extends UnknownRecord> {
82
262
  return record
83
263
  }
84
264
 
85
- /** @internal */
265
+ /**
266
+ * Processes all registered 'after create' handlers for a record.
267
+ * Handlers are called in registration order after the record is created.
268
+ *
269
+ * @param record - The record that was created
270
+ * @param source - Whether the change originated from 'user' or 'remote'
271
+ * @internal
272
+ */
86
273
  handleAfterCreate(record: R, source: 'remote' | 'user') {
87
274
  if (!this._isEnabled) return
88
275
 
@@ -94,7 +281,16 @@ export class StoreSideEffects<R extends UnknownRecord> {
94
281
  }
95
282
  }
96
283
 
97
- /** @internal */
284
+ /**
285
+ * Processes all registered 'before change' handlers for a record.
286
+ * Handlers are called in registration order and can modify or block the change.
287
+ *
288
+ * @param prev - The current version of the record
289
+ * @param next - The proposed new version of the record
290
+ * @param source - Whether the change originated from 'user' or 'remote'
291
+ * @returns The potentially modified record to actually store
292
+ * @internal
293
+ */
98
294
  handleBeforeChange(prev: R, next: R, source: 'remote' | 'user') {
99
295
  if (!this._isEnabled) return next
100
296
 
@@ -110,7 +306,15 @@ export class StoreSideEffects<R extends UnknownRecord> {
110
306
  return next
111
307
  }
112
308
 
113
- /** @internal */
309
+ /**
310
+ * Processes all registered 'after change' handlers for a record.
311
+ * Handlers are called in registration order after the record is updated.
312
+ *
313
+ * @param prev - The previous version of the record
314
+ * @param next - The new version of the record that was stored
315
+ * @param source - Whether the change originated from 'user' or 'remote'
316
+ * @internal
317
+ */
114
318
  handleAfterChange(prev: R, next: R, source: 'remote' | 'user') {
115
319
  if (!this._isEnabled) return
116
320
 
@@ -122,7 +326,15 @@ export class StoreSideEffects<R extends UnknownRecord> {
122
326
  }
123
327
  }
124
328
 
125
- /** @internal */
329
+ /**
330
+ * Processes all registered 'before delete' handlers for a record.
331
+ * If any handler returns `false`, the deletion is prevented.
332
+ *
333
+ * @param record - The record about to be deleted
334
+ * @param source - Whether the change originated from 'user' or 'remote'
335
+ * @returns `true` to allow deletion, `false` to prevent it
336
+ * @internal
337
+ */
126
338
  handleBeforeDelete(record: R, source: 'remote' | 'user') {
127
339
  if (!this._isEnabled) return true
128
340
 
@@ -137,7 +349,14 @@ export class StoreSideEffects<R extends UnknownRecord> {
137
349
  return true
138
350
  }
139
351
 
140
- /** @internal */
352
+ /**
353
+ * Processes all registered 'after delete' handlers for a record.
354
+ * Handlers are called in registration order after the record is deleted.
355
+ *
356
+ * @param record - The record that was deleted
357
+ * @param source - Whether the change originated from 'user' or 'remote'
358
+ * @internal
359
+ */
141
360
  handleAfterDelete(record: R, source: 'remote' | 'user') {
142
361
  if (!this._isEnabled) return
143
362
 
@@ -149,7 +368,13 @@ export class StoreSideEffects<R extends UnknownRecord> {
149
368
  }
150
369
  }
151
370
 
152
- /** @internal */
371
+ /**
372
+ * Processes all registered operation complete handlers.
373
+ * Called after an atomic store operation finishes.
374
+ *
375
+ * @param source - Whether the operation originated from 'user' or 'remote'
376
+ * @internal
377
+ */
153
378
  handleOperationComplete(source: 'remote' | 'user') {
154
379
  if (!this._isEnabled) return
155
380
 
@@ -159,7 +384,29 @@ export class StoreSideEffects<R extends UnknownRecord> {
159
384
  }
160
385
 
161
386
  /**
162
- * Internal helper for registering a bunch of side effects at once and keeping them organized.
387
+ * Internal helper for registering multiple side effect handlers at once and keeping them organized.
388
+ * This provides a convenient way to register handlers for multiple record types and lifecycle events
389
+ * in a single call, returning a single cleanup function.
390
+ *
391
+ * @param handlersByType - An object mapping record type names to their respective handlers
392
+ * @returns A function that removes all registered handlers when called
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * const cleanup = sideEffects.register({
397
+ * shape: {
398
+ * afterDelete: (shape) => console.log('Shape deleted:', shape.id),
399
+ * beforeChange: (prev, next) => ({ ...next, lastModified: Date.now() })
400
+ * },
401
+ * arrow: {
402
+ * afterCreate: (arrow) => updateConnectedShapes(arrow)
403
+ * }
404
+ * })
405
+ *
406
+ * // Later, remove all handlers
407
+ * cleanup()
408
+ * ```
409
+ *
163
410
  * @internal
164
411
  */
165
412
  register(handlersByType: {