@tldraw/store 4.1.0-canary.c62140a07605 → 4.1.0-canary.c9992319dc92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +1884 -153
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/AtomMap.js +241 -1
- package/dist-cjs/lib/AtomMap.js.map +2 -2
- package/dist-cjs/lib/BaseRecord.js.map +2 -2
- package/dist-cjs/lib/ImmutableMap.js +141 -0
- package/dist-cjs/lib/ImmutableMap.js.map +2 -2
- package/dist-cjs/lib/IncrementalSetConstructor.js +45 -5
- package/dist-cjs/lib/IncrementalSetConstructor.js.map +2 -2
- package/dist-cjs/lib/RecordType.js +116 -21
- package/dist-cjs/lib/RecordType.js.map +2 -2
- package/dist-cjs/lib/RecordsDiff.js.map +2 -2
- package/dist-cjs/lib/Store.js +233 -39
- package/dist-cjs/lib/Store.js.map +2 -2
- package/dist-cjs/lib/StoreQueries.js +135 -22
- package/dist-cjs/lib/StoreQueries.js.map +2 -2
- package/dist-cjs/lib/StoreSchema.js +207 -2
- package/dist-cjs/lib/StoreSchema.js.map +2 -2
- package/dist-cjs/lib/StoreSideEffects.js +102 -10
- package/dist-cjs/lib/StoreSideEffects.js.map +2 -2
- package/dist-cjs/lib/executeQuery.js.map +2 -2
- package/dist-cjs/lib/migrate.js.map +2 -2
- package/dist-cjs/lib/setUtils.js.map +2 -2
- package/dist-esm/index.d.mts +1884 -153
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/AtomMap.mjs +241 -1
- package/dist-esm/lib/AtomMap.mjs.map +2 -2
- package/dist-esm/lib/BaseRecord.mjs.map +2 -2
- package/dist-esm/lib/ImmutableMap.mjs +141 -0
- package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
- package/dist-esm/lib/IncrementalSetConstructor.mjs +45 -5
- package/dist-esm/lib/IncrementalSetConstructor.mjs.map +2 -2
- package/dist-esm/lib/RecordType.mjs +116 -21
- package/dist-esm/lib/RecordType.mjs.map +2 -2
- package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
- package/dist-esm/lib/Store.mjs +233 -39
- package/dist-esm/lib/Store.mjs.map +2 -2
- package/dist-esm/lib/StoreQueries.mjs +135 -22
- package/dist-esm/lib/StoreQueries.mjs.map +2 -2
- package/dist-esm/lib/StoreSchema.mjs +207 -2
- package/dist-esm/lib/StoreSchema.mjs.map +2 -2
- package/dist-esm/lib/StoreSideEffects.mjs +102 -10
- package/dist-esm/lib/StoreSideEffects.mjs.map +2 -2
- package/dist-esm/lib/executeQuery.mjs.map +2 -2
- package/dist-esm/lib/migrate.mjs.map +2 -2
- package/dist-esm/lib/setUtils.mjs.map +2 -2
- package/package.json +3 -3
- package/src/lib/AtomMap.ts +241 -1
- package/src/lib/BaseRecord.test.ts +44 -0
- package/src/lib/BaseRecord.ts +118 -4
- package/src/lib/ImmutableMap.test.ts +103 -0
- package/src/lib/ImmutableMap.ts +212 -0
- package/src/lib/IncrementalSetConstructor.test.ts +111 -0
- package/src/lib/IncrementalSetConstructor.ts +63 -6
- package/src/lib/RecordType.ts +149 -25
- package/src/lib/RecordsDiff.test.ts +144 -0
- package/src/lib/RecordsDiff.ts +145 -10
- package/src/lib/Store.test.ts +827 -0
- package/src/lib/Store.ts +533 -67
- package/src/lib/StoreQueries.test.ts +627 -0
- package/src/lib/StoreQueries.ts +194 -27
- package/src/lib/StoreSchema.test.ts +226 -0
- package/src/lib/StoreSchema.ts +386 -8
- package/src/lib/StoreSideEffects.test.ts +239 -19
- package/src/lib/StoreSideEffects.ts +266 -19
- package/src/lib/devFreeze.test.ts +137 -0
- package/src/lib/executeQuery.test.ts +481 -0
- package/src/lib/executeQuery.ts +80 -2
- package/src/lib/migrate.test.ts +400 -0
- package/src/lib/migrate.ts +187 -14
- package/src/lib/setUtils.test.ts +105 -0
- package/src/lib/setUtils.ts +44 -4
|
@@ -1,20 +1,240 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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: {
|