@tldraw/store 5.2.0-next.e2b8d10bf10e → 5.2.0
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/DOCS.md +790 -0
- package/README.md +9 -1
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/ImmutableMap.js +0 -13
- package/dist-cjs/lib/ImmutableMap.js.map +2 -2
- package/dist-cjs/lib/RecordType.js +1 -1
- package/dist-cjs/lib/RecordType.js.map +2 -2
- package/dist-cjs/lib/RecordsDiff.js +26 -14
- package/dist-cjs/lib/RecordsDiff.js.map +2 -2
- package/dist-cjs/lib/Store.js +6 -5
- package/dist-cjs/lib/Store.js.map +2 -2
- package/dist-cjs/lib/StoreSchema.js +1 -1
- package/dist-cjs/lib/StoreSchema.js.map +2 -2
- package/dist-cjs/lib/executeQuery.js +1 -1
- package/dist-cjs/lib/executeQuery.js.map +2 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/ImmutableMap.mjs +0 -13
- package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
- package/dist-esm/lib/RecordType.mjs +1 -1
- package/dist-esm/lib/RecordType.mjs.map +2 -2
- package/dist-esm/lib/RecordsDiff.mjs +26 -14
- package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
- package/dist-esm/lib/Store.mjs +7 -6
- package/dist-esm/lib/Store.mjs.map +2 -2
- package/dist-esm/lib/StoreSchema.mjs +1 -1
- package/dist-esm/lib/StoreSchema.mjs.map +2 -2
- package/dist-esm/lib/executeQuery.mjs +1 -1
- package/dist-esm/lib/executeQuery.mjs.map +2 -2
- package/package.json +9 -5
- package/src/lib/{test/AtomMap.test.ts → AtomMap.test.ts} +77 -111
- package/src/lib/AtomSet.test.ts +116 -0
- package/src/lib/BaseRecord.test.ts +10 -22
- package/src/lib/ImmutableMap.test.ts +114 -71
- package/src/lib/ImmutableMap.ts +1 -0
- package/src/lib/IncrementalSetConstructor.test.ts +76 -81
- package/src/lib/RecordType.test.ts +216 -0
- package/src/lib/RecordType.ts +1 -1
- package/src/lib/RecordsDiff.test.ts +112 -106
- package/src/lib/RecordsDiff.ts +43 -18
- package/src/lib/Store.test.ts +570 -630
- package/src/lib/Store.ts +9 -10
- package/src/lib/StoreListeners.test.ts +462 -0
- package/src/lib/StoreQueries.test.ts +586 -434
- package/src/lib/StoreSchema.test.ts +1012 -174
- package/src/lib/StoreSchema.ts +1 -1
- package/src/lib/StoreSideEffects.test.ts +546 -158
- package/src/lib/devFreeze.test.ts +94 -124
- package/src/lib/executeQuery.test.ts +77 -31
- package/src/lib/executeQuery.ts +3 -1
- package/src/lib/migrate.test.ts +273 -296
- package/src/lib/setUtils.test.ts +38 -79
- package/src/lib/test/createMigrations.test.ts +0 -75
- package/src/lib/test/dependsOn.test.ts +0 -166
- package/src/lib/test/getMigrationsSince.test.ts +0 -121
- package/src/lib/test/migrate.test.ts +0 -118
- package/src/lib/test/migratePersistedRecord.test.ts +0 -265
- package/src/lib/test/migrationCaching.test.ts +0 -209
- package/src/lib/test/recordStore.test.ts +0 -1567
- package/src/lib/test/recordStoreQueries.test.ts +0 -814
- package/src/lib/test/recordType.test.ts +0 -19
- package/src/lib/test/sortMigrations.test.ts +0 -83
- package/src/lib/test/upgradeSchema.test.ts +0 -80
- package/src/lib/test/validate.test.ts +0 -178
- package/src/lib/test/validateMigrations.test.ts +0 -165
|
@@ -1,1567 +0,0 @@
|
|
|
1
|
-
import { Computed, react, RESET_VALUE, transact } from '@tldraw/state'
|
|
2
|
-
import { vi } from 'vitest'
|
|
3
|
-
import { BaseRecord, RecordId } from '../BaseRecord'
|
|
4
|
-
import { createMigrationSequence } from '../migrate'
|
|
5
|
-
import { RecordsDiff, reverseRecordsDiff } from '../RecordsDiff'
|
|
6
|
-
import { createRecordType } from '../RecordType'
|
|
7
|
-
import { CollectionDiff, HistoryEntry, Store } from '../Store'
|
|
8
|
-
import { StoreSchema } from '../StoreSchema'
|
|
9
|
-
|
|
10
|
-
interface Book extends BaseRecord<'book', RecordId<Book>> {
|
|
11
|
-
title: string
|
|
12
|
-
author: RecordId<Author>
|
|
13
|
-
numPages: number
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const Book = createRecordType<Book>('book', {
|
|
17
|
-
validator: { validate: (book) => book as Book },
|
|
18
|
-
scope: 'document',
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
interface Author extends BaseRecord<'author', RecordId<Author>> {
|
|
22
|
-
name: string
|
|
23
|
-
isPseudonym: boolean
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const Author = createRecordType<Author>('author', {
|
|
27
|
-
validator: { validate: (author) => author as Author },
|
|
28
|
-
scope: 'document',
|
|
29
|
-
}).withDefaultProperties(() => ({
|
|
30
|
-
isPseudonym: false,
|
|
31
|
-
}))
|
|
32
|
-
|
|
33
|
-
interface Visit extends BaseRecord<'visit', RecordId<Visit>> {
|
|
34
|
-
visitorName: string
|
|
35
|
-
booksInBasket: RecordId<Book>[]
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const Visit = createRecordType<Visit>('visit', {
|
|
39
|
-
validator: { validate: (visit) => visit as Visit },
|
|
40
|
-
scope: 'session',
|
|
41
|
-
}).withDefaultProperties(() => ({
|
|
42
|
-
visitorName: 'Anonymous',
|
|
43
|
-
booksInBasket: [],
|
|
44
|
-
}))
|
|
45
|
-
|
|
46
|
-
type LibraryType = Book | Author | Visit
|
|
47
|
-
|
|
48
|
-
describe('Store', () => {
|
|
49
|
-
let store: Store<Book | Author | Visit>
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
store = new Store({
|
|
52
|
-
props: {},
|
|
53
|
-
schema: StoreSchema.create<LibraryType>({
|
|
54
|
-
book: Book,
|
|
55
|
-
author: Author,
|
|
56
|
-
visit: Visit,
|
|
57
|
-
}),
|
|
58
|
-
})
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('allows records to be added', () => {
|
|
62
|
-
store.put([Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') })])
|
|
63
|
-
expect(store.query.records('author').get()).toEqual([
|
|
64
|
-
{ id: 'author:tolkein', typeName: 'author', name: 'J.R.R Tolkein', isPseudonym: false },
|
|
65
|
-
])
|
|
66
|
-
|
|
67
|
-
store.put([
|
|
68
|
-
{
|
|
69
|
-
id: Book.createId('the-hobbit'),
|
|
70
|
-
typeName: 'book',
|
|
71
|
-
title: 'The Hobbit',
|
|
72
|
-
numPages: 423,
|
|
73
|
-
author: Author.createId('tolkein'),
|
|
74
|
-
},
|
|
75
|
-
])
|
|
76
|
-
|
|
77
|
-
expect(store.query.records('book').get()).toEqual([
|
|
78
|
-
{
|
|
79
|
-
id: 'book:the-hobbit',
|
|
80
|
-
typeName: 'book',
|
|
81
|
-
title: 'The Hobbit',
|
|
82
|
-
numPages: 423,
|
|
83
|
-
author: 'author:tolkein',
|
|
84
|
-
},
|
|
85
|
-
])
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
describe('with history', () => {
|
|
89
|
-
let authorHistory: Computed<number, RecordsDiff<Author>>
|
|
90
|
-
let lastDiff: RecordsDiff<Author>[] | typeof RESET_VALUE = 'undefined' as any
|
|
91
|
-
beforeEach(() => {
|
|
92
|
-
authorHistory = store.query.filterHistory('author')
|
|
93
|
-
react('', (lastReactedEpoch) => {
|
|
94
|
-
lastDiff = authorHistory.getDiffSince(lastReactedEpoch)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
expect(lastDiff!).toBe(RESET_VALUE)
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('allows listening to the change history', () => {
|
|
101
|
-
store.put([Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') })])
|
|
102
|
-
|
|
103
|
-
expect(lastDiff!).toMatchInlineSnapshot(`
|
|
104
|
-
[
|
|
105
|
-
{
|
|
106
|
-
"added": {
|
|
107
|
-
"author:tolkein": {
|
|
108
|
-
"id": "author:tolkein",
|
|
109
|
-
"isPseudonym": false,
|
|
110
|
-
"name": "J.R.R Tolkein",
|
|
111
|
-
"typeName": "author",
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
"removed": {},
|
|
115
|
-
"updated": {},
|
|
116
|
-
},
|
|
117
|
-
]
|
|
118
|
-
`)
|
|
119
|
-
|
|
120
|
-
store.update(Author.createId('tolkein'), (r) => ({ ...r, name: 'Jimmy Tolks' }))
|
|
121
|
-
|
|
122
|
-
expect(lastDiff!).toMatchInlineSnapshot(`
|
|
123
|
-
[
|
|
124
|
-
{
|
|
125
|
-
"added": {},
|
|
126
|
-
"removed": {},
|
|
127
|
-
"updated": {
|
|
128
|
-
"author:tolkein": [
|
|
129
|
-
{
|
|
130
|
-
"id": "author:tolkein",
|
|
131
|
-
"isPseudonym": false,
|
|
132
|
-
"name": "J.R.R Tolkein",
|
|
133
|
-
"typeName": "author",
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
"id": "author:tolkein",
|
|
137
|
-
"isPseudonym": false,
|
|
138
|
-
"name": "Jimmy Tolks",
|
|
139
|
-
"typeName": "author",
|
|
140
|
-
},
|
|
141
|
-
],
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
]
|
|
145
|
-
`)
|
|
146
|
-
|
|
147
|
-
store.remove([Author.createId('tolkein')])
|
|
148
|
-
|
|
149
|
-
expect(lastDiff!).toMatchInlineSnapshot(`
|
|
150
|
-
[
|
|
151
|
-
{
|
|
152
|
-
"added": {},
|
|
153
|
-
"removed": {
|
|
154
|
-
"author:tolkein": {
|
|
155
|
-
"id": "author:tolkein",
|
|
156
|
-
"isPseudonym": false,
|
|
157
|
-
"name": "Jimmy Tolks",
|
|
158
|
-
"typeName": "author",
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
"updated": {},
|
|
162
|
-
},
|
|
163
|
-
]
|
|
164
|
-
`)
|
|
165
|
-
|
|
166
|
-
transact(() => {
|
|
167
|
-
store.put([
|
|
168
|
-
Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') }),
|
|
169
|
-
Author.create({ name: 'David Foster Wallace', id: Author.createId('dfw') }),
|
|
170
|
-
Author.create({ name: 'Cynan Jones', id: Author.createId('cj') }),
|
|
171
|
-
])
|
|
172
|
-
|
|
173
|
-
store.update(Author.createId('tolkein'), (r) => ({ ...r, name: 'Jimmy Tolks' }))
|
|
174
|
-
store.update(Author.createId('cj'), (r) => ({ ...r, name: 'Carter, Jimmy' }))
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
expect(lastDiff!).toMatchInlineSnapshot(`
|
|
178
|
-
[
|
|
179
|
-
{
|
|
180
|
-
"added": {
|
|
181
|
-
"author:cj": {
|
|
182
|
-
"id": "author:cj",
|
|
183
|
-
"isPseudonym": false,
|
|
184
|
-
"name": "Carter, Jimmy",
|
|
185
|
-
"typeName": "author",
|
|
186
|
-
},
|
|
187
|
-
"author:dfw": {
|
|
188
|
-
"id": "author:dfw",
|
|
189
|
-
"isPseudonym": false,
|
|
190
|
-
"name": "David Foster Wallace",
|
|
191
|
-
"typeName": "author",
|
|
192
|
-
},
|
|
193
|
-
"author:tolkein": {
|
|
194
|
-
"id": "author:tolkein",
|
|
195
|
-
"isPseudonym": false,
|
|
196
|
-
"name": "Jimmy Tolks",
|
|
197
|
-
"typeName": "author",
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
"removed": {},
|
|
201
|
-
"updated": {},
|
|
202
|
-
},
|
|
203
|
-
]
|
|
204
|
-
`)
|
|
205
|
-
})
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
it('allows adding onAfterChange callbacks that see the final state of the world', () => {
|
|
209
|
-
/* ADDING */
|
|
210
|
-
const onAfterCreate = vi.fn((current) => {
|
|
211
|
-
expect(current).toEqual(
|
|
212
|
-
Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') })
|
|
213
|
-
)
|
|
214
|
-
expect([...store.query.ids('author').get()]).toEqual([Author.createId('tolkein')])
|
|
215
|
-
})
|
|
216
|
-
store.sideEffects.registerAfterCreateHandler('author', onAfterCreate)
|
|
217
|
-
store.put([Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') })])
|
|
218
|
-
|
|
219
|
-
expect(onAfterCreate).toHaveBeenCalledTimes(1)
|
|
220
|
-
|
|
221
|
-
/* UPDATING */
|
|
222
|
-
const onAfterChange = vi.fn((prev, current) => {
|
|
223
|
-
expect(prev.name).toBe('J.R.R Tolkein')
|
|
224
|
-
expect(current.name).toBe('Butch Cassidy')
|
|
225
|
-
|
|
226
|
-
expect(store.get(Author.createId('tolkein'))!.name).toBe('Butch Cassidy')
|
|
227
|
-
})
|
|
228
|
-
store.sideEffects.registerAfterChangeHandler('author', onAfterChange)
|
|
229
|
-
|
|
230
|
-
store.update(Author.createId('tolkein'), (r) => ({ ...r, name: 'Butch Cassidy' }))
|
|
231
|
-
|
|
232
|
-
expect(onAfterChange).toHaveBeenCalledTimes(1)
|
|
233
|
-
|
|
234
|
-
/* REMOVING */
|
|
235
|
-
const onAfterDelete = vi.fn((prev) => {
|
|
236
|
-
if (prev.typeName === 'author') {
|
|
237
|
-
expect(prev.name).toBe('Butch Cassidy')
|
|
238
|
-
}
|
|
239
|
-
})
|
|
240
|
-
store.sideEffects.registerAfterDeleteHandler('author', onAfterDelete)
|
|
241
|
-
|
|
242
|
-
store.remove([Author.createId('tolkein')])
|
|
243
|
-
|
|
244
|
-
expect(onAfterDelete).toHaveBeenCalledTimes(1)
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
it('allows finding and filtering records with a predicate', () => {
|
|
248
|
-
store.put([
|
|
249
|
-
Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') }),
|
|
250
|
-
Author.create({ name: 'James McAvoy', id: Author.createId('mcavoy') }),
|
|
251
|
-
Author.create({ name: 'Butch Cassidy', id: Author.createId('cassidy') }),
|
|
252
|
-
Author.create({ name: 'Cynan Jones', id: Author.createId('cj') }),
|
|
253
|
-
Author.create({ name: 'David Foster Wallace', id: Author.createId('dfw') }),
|
|
254
|
-
])
|
|
255
|
-
const Js = store.query
|
|
256
|
-
.records('author')
|
|
257
|
-
.get()
|
|
258
|
-
.filter((r) => r.name.startsWith('J'))
|
|
259
|
-
expect(Js.map((j) => j.name).sort()).toEqual(['J.R.R Tolkein', 'James McAvoy'])
|
|
260
|
-
|
|
261
|
-
const david = store.query
|
|
262
|
-
.records('author')
|
|
263
|
-
.get()
|
|
264
|
-
.find((r) => r.name.startsWith('David'))
|
|
265
|
-
expect(david?.name).toBe('David Foster Wallace')
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
it('allows keeping track of the ids of a particular type', () => {
|
|
269
|
-
let lastIdDiff: CollectionDiff<RecordId<Author>>[] | RESET_VALUE = []
|
|
270
|
-
|
|
271
|
-
const authorIds = store.query.ids('author')
|
|
272
|
-
|
|
273
|
-
react('', (lastReactedEpoch) => {
|
|
274
|
-
lastIdDiff = authorIds.getDiffSince(lastReactedEpoch)
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
expect(lastIdDiff).toBe(RESET_VALUE)
|
|
278
|
-
|
|
279
|
-
store.put([Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') })])
|
|
280
|
-
|
|
281
|
-
expect(lastIdDiff).toMatchInlineSnapshot(`
|
|
282
|
-
[
|
|
283
|
-
{
|
|
284
|
-
"added": Set {
|
|
285
|
-
"author:tolkein",
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
]
|
|
289
|
-
`)
|
|
290
|
-
|
|
291
|
-
transact(() => {
|
|
292
|
-
store.put([Author.create({ name: 'James McAvoy', id: Author.createId('mcavoy') })])
|
|
293
|
-
store.put([Author.create({ name: 'Butch Cassidy', id: Author.createId('cassidy') })])
|
|
294
|
-
store.remove([Author.createId('tolkein')])
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
expect(lastIdDiff).toMatchInlineSnapshot(`
|
|
298
|
-
[
|
|
299
|
-
{
|
|
300
|
-
"added": Set {
|
|
301
|
-
"author:mcavoy",
|
|
302
|
-
"author:cassidy",
|
|
303
|
-
},
|
|
304
|
-
"removed": Set {
|
|
305
|
-
"author:tolkein",
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
]
|
|
309
|
-
`)
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
it('supports listening for changes to the whole store', async () => {
|
|
313
|
-
const listener = vi.fn()
|
|
314
|
-
store.listen(listener)
|
|
315
|
-
|
|
316
|
-
transact(() => {
|
|
317
|
-
store.put([
|
|
318
|
-
Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') }),
|
|
319
|
-
Author.create({ name: 'James McAvoy', id: Author.createId('mcavoy') }),
|
|
320
|
-
Author.create({ name: 'Butch Cassidy', id: Author.createId('cassidy') }),
|
|
321
|
-
Book.create({
|
|
322
|
-
title: 'The Hobbit',
|
|
323
|
-
id: Book.createId('hobbit'),
|
|
324
|
-
author: Author.createId('tolkein'),
|
|
325
|
-
numPages: 300,
|
|
326
|
-
}),
|
|
327
|
-
])
|
|
328
|
-
store.put([
|
|
329
|
-
Book.create({
|
|
330
|
-
title: 'The Lord of the Rings',
|
|
331
|
-
id: Book.createId('lotr'),
|
|
332
|
-
author: Author.createId('tolkein'),
|
|
333
|
-
numPages: 1000,
|
|
334
|
-
}),
|
|
335
|
-
])
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
await new Promise((resolve) => requestAnimationFrame(resolve))
|
|
339
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
340
|
-
expect(listener.mock.lastCall?.[0]).toMatchInlineSnapshot(`
|
|
341
|
-
{
|
|
342
|
-
"changes": {
|
|
343
|
-
"added": {
|
|
344
|
-
"author:cassidy": {
|
|
345
|
-
"id": "author:cassidy",
|
|
346
|
-
"isPseudonym": false,
|
|
347
|
-
"name": "Butch Cassidy",
|
|
348
|
-
"typeName": "author",
|
|
349
|
-
},
|
|
350
|
-
"author:mcavoy": {
|
|
351
|
-
"id": "author:mcavoy",
|
|
352
|
-
"isPseudonym": false,
|
|
353
|
-
"name": "James McAvoy",
|
|
354
|
-
"typeName": "author",
|
|
355
|
-
},
|
|
356
|
-
"author:tolkein": {
|
|
357
|
-
"id": "author:tolkein",
|
|
358
|
-
"isPseudonym": false,
|
|
359
|
-
"name": "J.R.R Tolkein",
|
|
360
|
-
"typeName": "author",
|
|
361
|
-
},
|
|
362
|
-
"book:hobbit": {
|
|
363
|
-
"author": "author:tolkein",
|
|
364
|
-
"id": "book:hobbit",
|
|
365
|
-
"numPages": 300,
|
|
366
|
-
"title": "The Hobbit",
|
|
367
|
-
"typeName": "book",
|
|
368
|
-
},
|
|
369
|
-
"book:lotr": {
|
|
370
|
-
"author": "author:tolkein",
|
|
371
|
-
"id": "book:lotr",
|
|
372
|
-
"numPages": 1000,
|
|
373
|
-
"title": "The Lord of the Rings",
|
|
374
|
-
"typeName": "book",
|
|
375
|
-
},
|
|
376
|
-
},
|
|
377
|
-
"removed": {},
|
|
378
|
-
"updated": {},
|
|
379
|
-
},
|
|
380
|
-
"source": "user",
|
|
381
|
-
}
|
|
382
|
-
`)
|
|
383
|
-
|
|
384
|
-
transact(() => {
|
|
385
|
-
store.update(Author.createId('tolkein'), (author) => ({
|
|
386
|
-
...author,
|
|
387
|
-
name: 'Jimmy Tolks',
|
|
388
|
-
}))
|
|
389
|
-
store.update(Book.createId('lotr'), (book) => ({ ...book, numPages: 42 }))
|
|
390
|
-
})
|
|
391
|
-
|
|
392
|
-
await new Promise((resolve) => requestAnimationFrame(resolve))
|
|
393
|
-
expect(listener).toHaveBeenCalledTimes(2)
|
|
394
|
-
|
|
395
|
-
expect(listener.mock.lastCall?.[0]).toMatchInlineSnapshot(`
|
|
396
|
-
{
|
|
397
|
-
"changes": {
|
|
398
|
-
"added": {},
|
|
399
|
-
"removed": {},
|
|
400
|
-
"updated": {
|
|
401
|
-
"author:tolkein": [
|
|
402
|
-
{
|
|
403
|
-
"id": "author:tolkein",
|
|
404
|
-
"isPseudonym": false,
|
|
405
|
-
"name": "J.R.R Tolkein",
|
|
406
|
-
"typeName": "author",
|
|
407
|
-
},
|
|
408
|
-
{
|
|
409
|
-
"id": "author:tolkein",
|
|
410
|
-
"isPseudonym": false,
|
|
411
|
-
"name": "Jimmy Tolks",
|
|
412
|
-
"typeName": "author",
|
|
413
|
-
},
|
|
414
|
-
],
|
|
415
|
-
"book:lotr": [
|
|
416
|
-
{
|
|
417
|
-
"author": "author:tolkein",
|
|
418
|
-
"id": "book:lotr",
|
|
419
|
-
"numPages": 1000,
|
|
420
|
-
"title": "The Lord of the Rings",
|
|
421
|
-
"typeName": "book",
|
|
422
|
-
},
|
|
423
|
-
{
|
|
424
|
-
"author": "author:tolkein",
|
|
425
|
-
"id": "book:lotr",
|
|
426
|
-
"numPages": 42,
|
|
427
|
-
"title": "The Lord of the Rings",
|
|
428
|
-
"typeName": "book",
|
|
429
|
-
},
|
|
430
|
-
],
|
|
431
|
-
},
|
|
432
|
-
},
|
|
433
|
-
"source": "user",
|
|
434
|
-
}
|
|
435
|
-
`)
|
|
436
|
-
|
|
437
|
-
transact(() => {
|
|
438
|
-
store.update(Author.createId('mcavoy'), (author) => ({
|
|
439
|
-
...author,
|
|
440
|
-
name: 'Sookie Houseboat',
|
|
441
|
-
}))
|
|
442
|
-
store.remove([Book.createId('lotr')])
|
|
443
|
-
})
|
|
444
|
-
|
|
445
|
-
await new Promise((resolve) => requestAnimationFrame(resolve))
|
|
446
|
-
expect(listener).toHaveBeenCalledTimes(3)
|
|
447
|
-
|
|
448
|
-
expect(listener.mock.lastCall?.[0]).toMatchInlineSnapshot(`
|
|
449
|
-
{
|
|
450
|
-
"changes": {
|
|
451
|
-
"added": {},
|
|
452
|
-
"removed": {
|
|
453
|
-
"book:lotr": {
|
|
454
|
-
"author": "author:tolkein",
|
|
455
|
-
"id": "book:lotr",
|
|
456
|
-
"numPages": 42,
|
|
457
|
-
"title": "The Lord of the Rings",
|
|
458
|
-
"typeName": "book",
|
|
459
|
-
},
|
|
460
|
-
},
|
|
461
|
-
"updated": {
|
|
462
|
-
"author:mcavoy": [
|
|
463
|
-
{
|
|
464
|
-
"id": "author:mcavoy",
|
|
465
|
-
"isPseudonym": false,
|
|
466
|
-
"name": "James McAvoy",
|
|
467
|
-
"typeName": "author",
|
|
468
|
-
},
|
|
469
|
-
{
|
|
470
|
-
"id": "author:mcavoy",
|
|
471
|
-
"isPseudonym": false,
|
|
472
|
-
"name": "Sookie Houseboat",
|
|
473
|
-
"typeName": "author",
|
|
474
|
-
},
|
|
475
|
-
],
|
|
476
|
-
},
|
|
477
|
-
},
|
|
478
|
-
"source": "user",
|
|
479
|
-
}
|
|
480
|
-
`)
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
it('supports filtering history by scope', () => {
|
|
484
|
-
const listener = vi.fn()
|
|
485
|
-
store.listen(listener, {
|
|
486
|
-
scope: 'session',
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
store.put([
|
|
490
|
-
Author.create({ name: 'J.R.R Tolkien', id: Author.createId('tolkien') }),
|
|
491
|
-
Book.create({
|
|
492
|
-
title: 'The Hobbit',
|
|
493
|
-
id: Book.createId('hobbit'),
|
|
494
|
-
author: Author.createId('tolkien'),
|
|
495
|
-
numPages: 300,
|
|
496
|
-
}),
|
|
497
|
-
])
|
|
498
|
-
|
|
499
|
-
expect(listener).toHaveBeenCalledTimes(0)
|
|
500
|
-
|
|
501
|
-
store.put([
|
|
502
|
-
Author.create({ name: 'J.D. Salinger', id: Author.createId('salinger') }),
|
|
503
|
-
Visit.create({ id: Visit.createId('jimmy'), visitorName: 'Jimmy Beans' }),
|
|
504
|
-
])
|
|
505
|
-
|
|
506
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
507
|
-
|
|
508
|
-
expect(listener.mock.calls[0][0].changes).toMatchInlineSnapshot(`
|
|
509
|
-
{
|
|
510
|
-
"added": {
|
|
511
|
-
"visit:jimmy": {
|
|
512
|
-
"booksInBasket": [],
|
|
513
|
-
"id": "visit:jimmy",
|
|
514
|
-
"typeName": "visit",
|
|
515
|
-
"visitorName": "Jimmy Beans",
|
|
516
|
-
},
|
|
517
|
-
},
|
|
518
|
-
"removed": {},
|
|
519
|
-
"updated": {},
|
|
520
|
-
}
|
|
521
|
-
`)
|
|
522
|
-
})
|
|
523
|
-
|
|
524
|
-
it('supports filtering history by scope (2)', () => {
|
|
525
|
-
const listener = vi.fn()
|
|
526
|
-
store.listen(listener, {
|
|
527
|
-
scope: 'document',
|
|
528
|
-
})
|
|
529
|
-
|
|
530
|
-
store.put([
|
|
531
|
-
Author.create({ name: 'J.D. Salinger', id: Author.createId('salinger') }),
|
|
532
|
-
Visit.create({ id: Visit.createId('jimmy'), visitorName: 'Jimmy Beans' }),
|
|
533
|
-
])
|
|
534
|
-
|
|
535
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
536
|
-
|
|
537
|
-
expect(listener.mock.calls[0][0].changes).toMatchInlineSnapshot(`
|
|
538
|
-
{
|
|
539
|
-
"added": {
|
|
540
|
-
"author:salinger": {
|
|
541
|
-
"id": "author:salinger",
|
|
542
|
-
"isPseudonym": false,
|
|
543
|
-
"name": "J.D. Salinger",
|
|
544
|
-
"typeName": "author",
|
|
545
|
-
},
|
|
546
|
-
},
|
|
547
|
-
"removed": {},
|
|
548
|
-
"updated": {},
|
|
549
|
-
}
|
|
550
|
-
`)
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
it('supports filtering history by source', () => {
|
|
554
|
-
const listener = vi.fn()
|
|
555
|
-
store.listen(listener, {
|
|
556
|
-
source: 'remote',
|
|
557
|
-
})
|
|
558
|
-
|
|
559
|
-
store.put([
|
|
560
|
-
Author.create({ name: 'J.D. Salinger', id: Author.createId('salinger') }),
|
|
561
|
-
Visit.create({ id: Visit.createId('jimmy'), visitorName: 'Jimmy Beans' }),
|
|
562
|
-
])
|
|
563
|
-
|
|
564
|
-
expect(listener).toHaveBeenCalledTimes(0)
|
|
565
|
-
|
|
566
|
-
store.mergeRemoteChanges(() => {
|
|
567
|
-
store.put([
|
|
568
|
-
Author.create({ name: 'J.R.R Tolkien', id: Author.createId('tolkien') }),
|
|
569
|
-
Book.create({
|
|
570
|
-
title: 'The Hobbit',
|
|
571
|
-
id: Book.createId('hobbit'),
|
|
572
|
-
author: Author.createId('tolkien'),
|
|
573
|
-
numPages: 300,
|
|
574
|
-
}),
|
|
575
|
-
])
|
|
576
|
-
})
|
|
577
|
-
|
|
578
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
579
|
-
|
|
580
|
-
expect(listener.mock.calls[0][0].changes).toMatchInlineSnapshot(`
|
|
581
|
-
{
|
|
582
|
-
"added": {
|
|
583
|
-
"author:tolkien": {
|
|
584
|
-
"id": "author:tolkien",
|
|
585
|
-
"isPseudonym": false,
|
|
586
|
-
"name": "J.R.R Tolkien",
|
|
587
|
-
"typeName": "author",
|
|
588
|
-
},
|
|
589
|
-
"book:hobbit": {
|
|
590
|
-
"author": "author:tolkien",
|
|
591
|
-
"id": "book:hobbit",
|
|
592
|
-
"numPages": 300,
|
|
593
|
-
"title": "The Hobbit",
|
|
594
|
-
"typeName": "book",
|
|
595
|
-
},
|
|
596
|
-
},
|
|
597
|
-
"removed": {},
|
|
598
|
-
"updated": {},
|
|
599
|
-
}
|
|
600
|
-
`)
|
|
601
|
-
})
|
|
602
|
-
|
|
603
|
-
it('supports filtering history by source (user)', () => {
|
|
604
|
-
const listener = vi.fn()
|
|
605
|
-
store.listen(listener, {
|
|
606
|
-
source: 'user',
|
|
607
|
-
})
|
|
608
|
-
|
|
609
|
-
store.mergeRemoteChanges(() => {
|
|
610
|
-
store.put([
|
|
611
|
-
Author.create({ name: 'J.R.R Tolkien', id: Author.createId('tolkien') }),
|
|
612
|
-
Book.create({
|
|
613
|
-
title: 'The Hobbit',
|
|
614
|
-
id: Book.createId('hobbit'),
|
|
615
|
-
author: Author.createId('tolkien'),
|
|
616
|
-
numPages: 300,
|
|
617
|
-
}),
|
|
618
|
-
])
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
expect(listener).toHaveBeenCalledTimes(0)
|
|
622
|
-
|
|
623
|
-
store.put([
|
|
624
|
-
Author.create({ name: 'J.D. Salinger', id: Author.createId('salinger') }),
|
|
625
|
-
Visit.create({ id: Visit.createId('jimmy'), visitorName: 'Jimmy Beans' }),
|
|
626
|
-
])
|
|
627
|
-
|
|
628
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
629
|
-
|
|
630
|
-
expect(listener.mock.calls[0][0].changes).toMatchInlineSnapshot(`
|
|
631
|
-
{
|
|
632
|
-
"added": {
|
|
633
|
-
"author:salinger": {
|
|
634
|
-
"id": "author:salinger",
|
|
635
|
-
"isPseudonym": false,
|
|
636
|
-
"name": "J.D. Salinger",
|
|
637
|
-
"typeName": "author",
|
|
638
|
-
},
|
|
639
|
-
"visit:jimmy": {
|
|
640
|
-
"booksInBasket": [],
|
|
641
|
-
"id": "visit:jimmy",
|
|
642
|
-
"typeName": "visit",
|
|
643
|
-
"visitorName": "Jimmy Beans",
|
|
644
|
-
},
|
|
645
|
-
},
|
|
646
|
-
"removed": {},
|
|
647
|
-
"updated": {},
|
|
648
|
-
}
|
|
649
|
-
`)
|
|
650
|
-
})
|
|
651
|
-
|
|
652
|
-
it('does not keep global history if no listeners are attached', () => {
|
|
653
|
-
store.put([Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') })])
|
|
654
|
-
expect((store as any).historyAccumulator._history).toHaveLength(0)
|
|
655
|
-
})
|
|
656
|
-
|
|
657
|
-
it('flushes history before attaching listeners', async () => {
|
|
658
|
-
try {
|
|
659
|
-
// @ts-expect-error
|
|
660
|
-
globalThis.__FORCE_RAF_IN_TESTS__ = true
|
|
661
|
-
store.put([Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') })])
|
|
662
|
-
const firstListener = vi.fn()
|
|
663
|
-
store.listen(firstListener)
|
|
664
|
-
expect(firstListener).toHaveBeenCalledTimes(0)
|
|
665
|
-
|
|
666
|
-
store.put([Author.create({ name: 'Chips McCoy', id: Author.createId('chips') })])
|
|
667
|
-
|
|
668
|
-
expect(firstListener).toHaveBeenCalledTimes(0)
|
|
669
|
-
|
|
670
|
-
const secondListener = vi.fn()
|
|
671
|
-
|
|
672
|
-
store.listen(secondListener)
|
|
673
|
-
|
|
674
|
-
expect(firstListener).toHaveBeenCalledTimes(1)
|
|
675
|
-
expect(secondListener).toHaveBeenCalledTimes(0)
|
|
676
|
-
|
|
677
|
-
await new Promise((resolve) => requestAnimationFrame(resolve))
|
|
678
|
-
|
|
679
|
-
expect(firstListener).toHaveBeenCalledTimes(1)
|
|
680
|
-
expect(secondListener).toHaveBeenCalledTimes(0)
|
|
681
|
-
} finally {
|
|
682
|
-
// @ts-expect-error
|
|
683
|
-
globalThis.__FORCE_RAF_IN_TESTS__ = false
|
|
684
|
-
}
|
|
685
|
-
})
|
|
686
|
-
|
|
687
|
-
it('does not overwrite default properties with undefined', () => {
|
|
688
|
-
const tolkein = Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') })
|
|
689
|
-
expect(tolkein.isPseudonym).toBe(false)
|
|
690
|
-
|
|
691
|
-
const harkaway = Author.create({
|
|
692
|
-
name: 'Nick Harkaway',
|
|
693
|
-
id: Author.createId('harkaway'),
|
|
694
|
-
isPseudonym: true,
|
|
695
|
-
})
|
|
696
|
-
expect(harkaway.isPseudonym).toBe(true)
|
|
697
|
-
|
|
698
|
-
const burns = Author.create({
|
|
699
|
-
name: 'Anna Burns',
|
|
700
|
-
id: Author.createId('burns'),
|
|
701
|
-
isPseudonym: undefined,
|
|
702
|
-
})
|
|
703
|
-
|
|
704
|
-
expect(burns.isPseudonym).toBe(false)
|
|
705
|
-
})
|
|
706
|
-
|
|
707
|
-
it('allows changed to be merged without triggering listeners', () => {
|
|
708
|
-
const id = Author.createId('tolkein')
|
|
709
|
-
store.put([Author.create({ name: 'J.R.R Tolkein', id })])
|
|
710
|
-
|
|
711
|
-
const listener = vi.fn()
|
|
712
|
-
store.listen(listener)
|
|
713
|
-
|
|
714
|
-
// Return the exact same value that came in
|
|
715
|
-
store.update(id, (author) => author)
|
|
716
|
-
|
|
717
|
-
expect(listener).not.toHaveBeenCalled()
|
|
718
|
-
})
|
|
719
|
-
|
|
720
|
-
it('tells listeners the source of the changes so they can decide if they want to run or not', async () => {
|
|
721
|
-
const listener = vi.fn()
|
|
722
|
-
store.listen(listener)
|
|
723
|
-
|
|
724
|
-
store.put([Author.create({ name: 'Jimmy Beans', id: Author.createId('jimmy') })])
|
|
725
|
-
|
|
726
|
-
await new Promise((resolve) => requestAnimationFrame(resolve))
|
|
727
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
728
|
-
expect(listener.mock.calls[0][0].source).toBe('user')
|
|
729
|
-
|
|
730
|
-
store.mergeRemoteChanges(() => {
|
|
731
|
-
store.put([Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') })])
|
|
732
|
-
store.put([
|
|
733
|
-
Book.create({
|
|
734
|
-
title: 'The Hobbit',
|
|
735
|
-
id: Book.createId('hobbit'),
|
|
736
|
-
author: Author.createId('tolkein'),
|
|
737
|
-
numPages: 300,
|
|
738
|
-
}),
|
|
739
|
-
])
|
|
740
|
-
})
|
|
741
|
-
|
|
742
|
-
await new Promise((resolve) => requestAnimationFrame(resolve))
|
|
743
|
-
expect(listener).toHaveBeenCalledTimes(2)
|
|
744
|
-
expect(listener.mock.calls[1][0].source).toBe('remote')
|
|
745
|
-
|
|
746
|
-
store.put([Author.create({ name: 'Steve Ok', id: Author.createId('stever') })])
|
|
747
|
-
|
|
748
|
-
await new Promise((resolve) => requestAnimationFrame(resolve))
|
|
749
|
-
expect(listener).toHaveBeenCalledTimes(3)
|
|
750
|
-
expect(listener.mock.calls[2][0].source).toBe('user')
|
|
751
|
-
})
|
|
752
|
-
})
|
|
753
|
-
|
|
754
|
-
describe('snapshots', () => {
|
|
755
|
-
let store: Store<Book | Author>
|
|
756
|
-
|
|
757
|
-
beforeEach(() => {
|
|
758
|
-
store = new Store({
|
|
759
|
-
props: {},
|
|
760
|
-
schema: StoreSchema.create<Book | Author>({
|
|
761
|
-
book: Book,
|
|
762
|
-
author: Author,
|
|
763
|
-
}),
|
|
764
|
-
})
|
|
765
|
-
|
|
766
|
-
transact(() => {
|
|
767
|
-
store.put([
|
|
768
|
-
Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') }),
|
|
769
|
-
Author.create({ name: 'James McAvoy', id: Author.createId('mcavoy') }),
|
|
770
|
-
Author.create({ name: 'Butch Cassidy', id: Author.createId('cassidy') }),
|
|
771
|
-
Book.create({
|
|
772
|
-
title: 'The Hobbit',
|
|
773
|
-
id: Book.createId('hobbit'),
|
|
774
|
-
author: Author.createId('tolkein'),
|
|
775
|
-
numPages: 300,
|
|
776
|
-
}),
|
|
777
|
-
])
|
|
778
|
-
store.put([
|
|
779
|
-
Book.create({
|
|
780
|
-
title: 'The Lord of the Rings',
|
|
781
|
-
id: Book.createId('lotr'),
|
|
782
|
-
author: Author.createId('tolkein'),
|
|
783
|
-
numPages: 1000,
|
|
784
|
-
}),
|
|
785
|
-
])
|
|
786
|
-
})
|
|
787
|
-
})
|
|
788
|
-
|
|
789
|
-
it('creates and loads a snapshot', () => {
|
|
790
|
-
const serializedStore1 = store.serialize('all')
|
|
791
|
-
const serializedSchema1 = store.schema.serialize()
|
|
792
|
-
|
|
793
|
-
const snapshot1 = store.getStoreSnapshot()
|
|
794
|
-
|
|
795
|
-
const store2 = new Store({
|
|
796
|
-
props: {},
|
|
797
|
-
schema: StoreSchema.create<Book | Author>({
|
|
798
|
-
book: Book,
|
|
799
|
-
author: Author,
|
|
800
|
-
}),
|
|
801
|
-
})
|
|
802
|
-
|
|
803
|
-
store2.loadStoreSnapshot(snapshot1)
|
|
804
|
-
|
|
805
|
-
const serializedStore2 = store2.serialize('all')
|
|
806
|
-
const serializedSchema2 = store2.schema.serialize()
|
|
807
|
-
const snapshot2 = store2.getStoreSnapshot()
|
|
808
|
-
|
|
809
|
-
expect(serializedStore1).toEqual(serializedStore2)
|
|
810
|
-
expect(serializedSchema1).toEqual(serializedSchema2)
|
|
811
|
-
expect(snapshot1).toEqual(snapshot2)
|
|
812
|
-
})
|
|
813
|
-
|
|
814
|
-
it('throws errors when loading a snapshot with a different schema', () => {
|
|
815
|
-
const snapshot1 = store.getStoreSnapshot()
|
|
816
|
-
|
|
817
|
-
const store2 = new Store({
|
|
818
|
-
props: {},
|
|
819
|
-
schema: StoreSchema.create<Book>({
|
|
820
|
-
book: Book,
|
|
821
|
-
// no author
|
|
822
|
-
}),
|
|
823
|
-
})
|
|
824
|
-
|
|
825
|
-
expect(() => {
|
|
826
|
-
// @ts-expect-error
|
|
827
|
-
store2.loadStoreSnapshot(snapshot1)
|
|
828
|
-
}).toThrowErrorMatchingInlineSnapshot(`[Error: Missing definition for record type author]`)
|
|
829
|
-
})
|
|
830
|
-
|
|
831
|
-
it('throws errors when loading a snapshot with a different schema', () => {
|
|
832
|
-
const snapshot1 = store.getStoreSnapshot()
|
|
833
|
-
|
|
834
|
-
const store2 = new Store({
|
|
835
|
-
props: {},
|
|
836
|
-
schema: StoreSchema.create<Book>({
|
|
837
|
-
book: Book,
|
|
838
|
-
}),
|
|
839
|
-
})
|
|
840
|
-
|
|
841
|
-
expect(() => {
|
|
842
|
-
store2.loadStoreSnapshot(snapshot1 as any)
|
|
843
|
-
}).toThrowErrorMatchingInlineSnapshot(`[Error: Missing definition for record type author]`)
|
|
844
|
-
})
|
|
845
|
-
|
|
846
|
-
it('migrates the snapshot', () => {
|
|
847
|
-
const snapshot1 = store.getStoreSnapshot()
|
|
848
|
-
const up = vi.fn((s: any) => {
|
|
849
|
-
s['book:lotr'].numPages = 42
|
|
850
|
-
})
|
|
851
|
-
|
|
852
|
-
expect((snapshot1.store as any)['book:lotr'].numPages).toBe(1000)
|
|
853
|
-
|
|
854
|
-
const store2 = new Store({
|
|
855
|
-
props: {},
|
|
856
|
-
schema: StoreSchema.create<Book | Author>(
|
|
857
|
-
{
|
|
858
|
-
book: Book,
|
|
859
|
-
author: Author,
|
|
860
|
-
},
|
|
861
|
-
{
|
|
862
|
-
migrations: [
|
|
863
|
-
createMigrationSequence({
|
|
864
|
-
sequenceId: 'com.tldraw',
|
|
865
|
-
retroactive: true,
|
|
866
|
-
sequence: [
|
|
867
|
-
{
|
|
868
|
-
id: `com.tldraw/1`,
|
|
869
|
-
scope: 'store',
|
|
870
|
-
up,
|
|
871
|
-
},
|
|
872
|
-
],
|
|
873
|
-
}),
|
|
874
|
-
],
|
|
875
|
-
}
|
|
876
|
-
),
|
|
877
|
-
})
|
|
878
|
-
|
|
879
|
-
expect(() => {
|
|
880
|
-
store2.loadStoreSnapshot(snapshot1)
|
|
881
|
-
}).not.toThrow()
|
|
882
|
-
|
|
883
|
-
expect(up).toHaveBeenCalledTimes(1)
|
|
884
|
-
expect(store2.get(Book.createId('lotr'))!.numPages).toBe(42)
|
|
885
|
-
})
|
|
886
|
-
|
|
887
|
-
it('migrates the snapshot with storage scope', () => {
|
|
888
|
-
const snapshot1 = store.getStoreSnapshot()
|
|
889
|
-
const up = vi.fn((storage: any) => {
|
|
890
|
-
const book = storage.get('book:lotr')
|
|
891
|
-
storage.set('book:lotr', { ...book, numPages: 42 })
|
|
892
|
-
})
|
|
893
|
-
|
|
894
|
-
expect((snapshot1.store as any)['book:lotr'].numPages).toBe(1000)
|
|
895
|
-
|
|
896
|
-
const store2 = new Store({
|
|
897
|
-
props: {},
|
|
898
|
-
schema: StoreSchema.create<Book | Author>(
|
|
899
|
-
{
|
|
900
|
-
book: Book,
|
|
901
|
-
author: Author,
|
|
902
|
-
},
|
|
903
|
-
{
|
|
904
|
-
migrations: [
|
|
905
|
-
createMigrationSequence({
|
|
906
|
-
sequenceId: 'com.tldraw',
|
|
907
|
-
retroactive: true,
|
|
908
|
-
sequence: [
|
|
909
|
-
{
|
|
910
|
-
id: `com.tldraw/1`,
|
|
911
|
-
scope: 'storage',
|
|
912
|
-
up,
|
|
913
|
-
},
|
|
914
|
-
],
|
|
915
|
-
}),
|
|
916
|
-
],
|
|
917
|
-
}
|
|
918
|
-
),
|
|
919
|
-
})
|
|
920
|
-
|
|
921
|
-
expect(() => {
|
|
922
|
-
store2.loadStoreSnapshot(snapshot1)
|
|
923
|
-
}).not.toThrow()
|
|
924
|
-
|
|
925
|
-
expect(up).toHaveBeenCalledTimes(1)
|
|
926
|
-
expect(store2.get(Book.createId('lotr'))!.numPages).toBe(42)
|
|
927
|
-
})
|
|
928
|
-
|
|
929
|
-
it('storage scope migration can delete records', () => {
|
|
930
|
-
const snapshot1 = store.getStoreSnapshot()
|
|
931
|
-
const up = vi.fn((storage: any) => {
|
|
932
|
-
storage.delete('author:mcavoy')
|
|
933
|
-
})
|
|
934
|
-
|
|
935
|
-
expect((snapshot1.store as any)['author:mcavoy']).toBeDefined()
|
|
936
|
-
|
|
937
|
-
const store2 = new Store({
|
|
938
|
-
props: {},
|
|
939
|
-
schema: StoreSchema.create<Book | Author>(
|
|
940
|
-
{
|
|
941
|
-
book: Book,
|
|
942
|
-
author: Author,
|
|
943
|
-
},
|
|
944
|
-
{
|
|
945
|
-
migrations: [
|
|
946
|
-
createMigrationSequence({
|
|
947
|
-
sequenceId: 'com.tldraw',
|
|
948
|
-
retroactive: true,
|
|
949
|
-
sequence: [
|
|
950
|
-
{
|
|
951
|
-
id: `com.tldraw/1`,
|
|
952
|
-
scope: 'storage',
|
|
953
|
-
up,
|
|
954
|
-
},
|
|
955
|
-
],
|
|
956
|
-
}),
|
|
957
|
-
],
|
|
958
|
-
}
|
|
959
|
-
),
|
|
960
|
-
})
|
|
961
|
-
|
|
962
|
-
expect(() => {
|
|
963
|
-
store2.loadStoreSnapshot(snapshot1)
|
|
964
|
-
}).not.toThrow()
|
|
965
|
-
|
|
966
|
-
expect(up).toHaveBeenCalledTimes(1)
|
|
967
|
-
expect(store2.get(Author.createId('mcavoy'))).toBeUndefined()
|
|
968
|
-
})
|
|
969
|
-
|
|
970
|
-
it('storage scope migration can iterate records', () => {
|
|
971
|
-
const snapshot1 = store.getStoreSnapshot()
|
|
972
|
-
const up = vi.fn((storage: any) => {
|
|
973
|
-
for (const [id, record] of storage.entries()) {
|
|
974
|
-
if (record.typeName === 'book') {
|
|
975
|
-
storage.set(id, { ...record, numPages: record.numPages + 100 })
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
})
|
|
979
|
-
|
|
980
|
-
expect((snapshot1.store as any)['book:lotr'].numPages).toBe(1000)
|
|
981
|
-
expect((snapshot1.store as any)['book:hobbit'].numPages).toBe(300)
|
|
982
|
-
|
|
983
|
-
const store2 = new Store({
|
|
984
|
-
props: {},
|
|
985
|
-
schema: StoreSchema.create<Book | Author>(
|
|
986
|
-
{
|
|
987
|
-
book: Book,
|
|
988
|
-
author: Author,
|
|
989
|
-
},
|
|
990
|
-
{
|
|
991
|
-
migrations: [
|
|
992
|
-
createMigrationSequence({
|
|
993
|
-
sequenceId: 'com.tldraw',
|
|
994
|
-
retroactive: true,
|
|
995
|
-
sequence: [
|
|
996
|
-
{
|
|
997
|
-
id: `com.tldraw/1`,
|
|
998
|
-
scope: 'storage',
|
|
999
|
-
up,
|
|
1000
|
-
},
|
|
1001
|
-
],
|
|
1002
|
-
}),
|
|
1003
|
-
],
|
|
1004
|
-
}
|
|
1005
|
-
),
|
|
1006
|
-
})
|
|
1007
|
-
|
|
1008
|
-
expect(() => {
|
|
1009
|
-
store2.loadStoreSnapshot(snapshot1)
|
|
1010
|
-
}).not.toThrow()
|
|
1011
|
-
|
|
1012
|
-
expect(up).toHaveBeenCalledTimes(1)
|
|
1013
|
-
expect(store2.get(Book.createId('lotr'))!.numPages).toBe(1100)
|
|
1014
|
-
expect(store2.get(Book.createId('hobbit'))!.numPages).toBe(400)
|
|
1015
|
-
})
|
|
1016
|
-
|
|
1017
|
-
it('storage scope migration can use values() and keys()', () => {
|
|
1018
|
-
const snapshot1 = store.getStoreSnapshot()
|
|
1019
|
-
const keysCollected: string[] = []
|
|
1020
|
-
const valuesCollected: any[] = []
|
|
1021
|
-
|
|
1022
|
-
const up = vi.fn((storage: any) => {
|
|
1023
|
-
for (const key of storage.keys()) {
|
|
1024
|
-
keysCollected.push(key)
|
|
1025
|
-
}
|
|
1026
|
-
for (const value of storage.values()) {
|
|
1027
|
-
valuesCollected.push(value)
|
|
1028
|
-
}
|
|
1029
|
-
})
|
|
1030
|
-
|
|
1031
|
-
const store2 = new Store({
|
|
1032
|
-
props: {},
|
|
1033
|
-
schema: StoreSchema.create<Book | Author>(
|
|
1034
|
-
{
|
|
1035
|
-
book: Book,
|
|
1036
|
-
author: Author,
|
|
1037
|
-
},
|
|
1038
|
-
{
|
|
1039
|
-
migrations: [
|
|
1040
|
-
createMigrationSequence({
|
|
1041
|
-
sequenceId: 'com.tldraw',
|
|
1042
|
-
retroactive: true,
|
|
1043
|
-
sequence: [
|
|
1044
|
-
{
|
|
1045
|
-
id: `com.tldraw/1`,
|
|
1046
|
-
scope: 'storage',
|
|
1047
|
-
up,
|
|
1048
|
-
},
|
|
1049
|
-
],
|
|
1050
|
-
}),
|
|
1051
|
-
],
|
|
1052
|
-
}
|
|
1053
|
-
),
|
|
1054
|
-
})
|
|
1055
|
-
|
|
1056
|
-
expect(() => {
|
|
1057
|
-
store2.loadStoreSnapshot(snapshot1)
|
|
1058
|
-
}).not.toThrow()
|
|
1059
|
-
|
|
1060
|
-
expect(up).toHaveBeenCalledTimes(1)
|
|
1061
|
-
expect(keysCollected).toContain('book:lotr')
|
|
1062
|
-
expect(keysCollected).toContain('book:hobbit')
|
|
1063
|
-
expect(keysCollected).toContain('author:tolkein')
|
|
1064
|
-
expect(keysCollected).toContain('author:mcavoy')
|
|
1065
|
-
expect(keysCollected).toContain('author:cassidy')
|
|
1066
|
-
expect(valuesCollected.length).toBe(5)
|
|
1067
|
-
})
|
|
1068
|
-
})
|
|
1069
|
-
|
|
1070
|
-
describe('diffs', () => {
|
|
1071
|
-
let store: Store<LibraryType>
|
|
1072
|
-
const authorId = Author.createId('tolkein')
|
|
1073
|
-
const bookId = Book.createId('hobbit')
|
|
1074
|
-
|
|
1075
|
-
beforeEach(() => {
|
|
1076
|
-
store = new Store({
|
|
1077
|
-
props: {},
|
|
1078
|
-
schema: StoreSchema.create<LibraryType>({
|
|
1079
|
-
book: Book,
|
|
1080
|
-
author: Author,
|
|
1081
|
-
visit: Visit,
|
|
1082
|
-
}),
|
|
1083
|
-
})
|
|
1084
|
-
})
|
|
1085
|
-
|
|
1086
|
-
it('produces diffs from `extractingChanges`', () => {
|
|
1087
|
-
expect(
|
|
1088
|
-
store.extractingChanges(() => {
|
|
1089
|
-
store.put([Author.create({ name: 'J.R.R Tolkein', id: authorId })])
|
|
1090
|
-
store.put([
|
|
1091
|
-
Book.create({ title: 'The Hobbit', id: bookId, author: authorId, numPages: 300 }),
|
|
1092
|
-
])
|
|
1093
|
-
})
|
|
1094
|
-
).toMatchInlineSnapshot(`
|
|
1095
|
-
{
|
|
1096
|
-
"added": {
|
|
1097
|
-
"author:tolkein": {
|
|
1098
|
-
"id": "author:tolkein",
|
|
1099
|
-
"isPseudonym": false,
|
|
1100
|
-
"name": "J.R.R Tolkein",
|
|
1101
|
-
"typeName": "author",
|
|
1102
|
-
},
|
|
1103
|
-
"book:hobbit": {
|
|
1104
|
-
"author": "author:tolkein",
|
|
1105
|
-
"id": "book:hobbit",
|
|
1106
|
-
"numPages": 300,
|
|
1107
|
-
"title": "The Hobbit",
|
|
1108
|
-
"typeName": "book",
|
|
1109
|
-
},
|
|
1110
|
-
},
|
|
1111
|
-
"removed": {},
|
|
1112
|
-
"updated": {},
|
|
1113
|
-
}
|
|
1114
|
-
`)
|
|
1115
|
-
|
|
1116
|
-
expect(
|
|
1117
|
-
store.extractingChanges(() => {
|
|
1118
|
-
store.remove([authorId])
|
|
1119
|
-
store.update(bookId, (book) => ({ ...book, title: 'The Hobbit: There and Back Again' }))
|
|
1120
|
-
})
|
|
1121
|
-
).toMatchInlineSnapshot(`
|
|
1122
|
-
{
|
|
1123
|
-
"added": {},
|
|
1124
|
-
"removed": {
|
|
1125
|
-
"author:tolkein": {
|
|
1126
|
-
"id": "author:tolkein",
|
|
1127
|
-
"isPseudonym": false,
|
|
1128
|
-
"name": "J.R.R Tolkein",
|
|
1129
|
-
"typeName": "author",
|
|
1130
|
-
},
|
|
1131
|
-
},
|
|
1132
|
-
"updated": {
|
|
1133
|
-
"book:hobbit": [
|
|
1134
|
-
{
|
|
1135
|
-
"author": "author:tolkein",
|
|
1136
|
-
"id": "book:hobbit",
|
|
1137
|
-
"numPages": 300,
|
|
1138
|
-
"title": "The Hobbit",
|
|
1139
|
-
"typeName": "book",
|
|
1140
|
-
},
|
|
1141
|
-
{
|
|
1142
|
-
"author": "author:tolkein",
|
|
1143
|
-
"id": "book:hobbit",
|
|
1144
|
-
"numPages": 300,
|
|
1145
|
-
"title": "The Hobbit: There and Back Again",
|
|
1146
|
-
"typeName": "book",
|
|
1147
|
-
},
|
|
1148
|
-
],
|
|
1149
|
-
},
|
|
1150
|
-
}
|
|
1151
|
-
`)
|
|
1152
|
-
})
|
|
1153
|
-
it('produces diffs from `addHistoryInterceptor`', () => {
|
|
1154
|
-
const diffs: any[] = []
|
|
1155
|
-
const interceptor = vi.fn((diff) => diffs.push(diff))
|
|
1156
|
-
store.addHistoryInterceptor(interceptor)
|
|
1157
|
-
|
|
1158
|
-
store.put([
|
|
1159
|
-
Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') }),
|
|
1160
|
-
Book.create({ title: 'The Hobbit', id: bookId, author: authorId, numPages: 300 }),
|
|
1161
|
-
])
|
|
1162
|
-
expect(interceptor).toHaveBeenCalledTimes(1)
|
|
1163
|
-
|
|
1164
|
-
store.extractingChanges(() => {
|
|
1165
|
-
store.remove([authorId])
|
|
1166
|
-
|
|
1167
|
-
store.update(bookId, (book) => ({ ...book, title: 'The Hobbit: There and Back Again' }))
|
|
1168
|
-
})
|
|
1169
|
-
expect(interceptor).toHaveBeenCalledTimes(3)
|
|
1170
|
-
|
|
1171
|
-
expect(diffs).toMatchInlineSnapshot(`
|
|
1172
|
-
[
|
|
1173
|
-
{
|
|
1174
|
-
"changes": {
|
|
1175
|
-
"added": {
|
|
1176
|
-
"author:tolkein": {
|
|
1177
|
-
"id": "author:tolkein",
|
|
1178
|
-
"isPseudonym": false,
|
|
1179
|
-
"name": "J.R.R Tolkein",
|
|
1180
|
-
"typeName": "author",
|
|
1181
|
-
},
|
|
1182
|
-
"book:hobbit": {
|
|
1183
|
-
"author": "author:tolkein",
|
|
1184
|
-
"id": "book:hobbit",
|
|
1185
|
-
"numPages": 300,
|
|
1186
|
-
"title": "The Hobbit",
|
|
1187
|
-
"typeName": "book",
|
|
1188
|
-
},
|
|
1189
|
-
},
|
|
1190
|
-
"removed": {},
|
|
1191
|
-
"updated": {},
|
|
1192
|
-
},
|
|
1193
|
-
"source": "user",
|
|
1194
|
-
},
|
|
1195
|
-
{
|
|
1196
|
-
"changes": {
|
|
1197
|
-
"added": {},
|
|
1198
|
-
"removed": {
|
|
1199
|
-
"author:tolkein": {
|
|
1200
|
-
"id": "author:tolkein",
|
|
1201
|
-
"isPseudonym": false,
|
|
1202
|
-
"name": "J.R.R Tolkein",
|
|
1203
|
-
"typeName": "author",
|
|
1204
|
-
},
|
|
1205
|
-
},
|
|
1206
|
-
"updated": {},
|
|
1207
|
-
},
|
|
1208
|
-
"source": "user",
|
|
1209
|
-
},
|
|
1210
|
-
{
|
|
1211
|
-
"changes": {
|
|
1212
|
-
"added": {},
|
|
1213
|
-
"removed": {},
|
|
1214
|
-
"updated": {
|
|
1215
|
-
"book:hobbit": [
|
|
1216
|
-
{
|
|
1217
|
-
"author": "author:tolkein",
|
|
1218
|
-
"id": "book:hobbit",
|
|
1219
|
-
"numPages": 300,
|
|
1220
|
-
"title": "The Hobbit",
|
|
1221
|
-
"typeName": "book",
|
|
1222
|
-
},
|
|
1223
|
-
{
|
|
1224
|
-
"author": "author:tolkein",
|
|
1225
|
-
"id": "book:hobbit",
|
|
1226
|
-
"numPages": 300,
|
|
1227
|
-
"title": "The Hobbit: There and Back Again",
|
|
1228
|
-
"typeName": "book",
|
|
1229
|
-
},
|
|
1230
|
-
],
|
|
1231
|
-
},
|
|
1232
|
-
},
|
|
1233
|
-
"source": "user",
|
|
1234
|
-
},
|
|
1235
|
-
]
|
|
1236
|
-
`)
|
|
1237
|
-
})
|
|
1238
|
-
|
|
1239
|
-
it('can apply and invert diffs', () => {
|
|
1240
|
-
store.put([
|
|
1241
|
-
Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') }),
|
|
1242
|
-
Book.create({ title: 'The Hobbit', id: bookId, author: authorId, numPages: 300 }),
|
|
1243
|
-
])
|
|
1244
|
-
|
|
1245
|
-
const checkpoint1 = store.getStoreSnapshot()
|
|
1246
|
-
|
|
1247
|
-
const forwardsDiff = store.extractingChanges(() => {
|
|
1248
|
-
store.remove([authorId])
|
|
1249
|
-
store.update(bookId, (book) => ({ ...book, title: 'The Hobbit: There and Back Again' }))
|
|
1250
|
-
})
|
|
1251
|
-
|
|
1252
|
-
const checkpoint2 = store.getStoreSnapshot()
|
|
1253
|
-
|
|
1254
|
-
store.applyDiff(reverseRecordsDiff(forwardsDiff))
|
|
1255
|
-
expect(store.getStoreSnapshot()).toEqual(checkpoint1)
|
|
1256
|
-
|
|
1257
|
-
store.applyDiff(forwardsDiff)
|
|
1258
|
-
expect(store.getStoreSnapshot()).toEqual(checkpoint2)
|
|
1259
|
-
})
|
|
1260
|
-
})
|
|
1261
|
-
|
|
1262
|
-
describe('callbacks', () => {
|
|
1263
|
-
let store: Store<Book>
|
|
1264
|
-
let callbacks: any[] = []
|
|
1265
|
-
|
|
1266
|
-
const book1Id = Book.createId('darkness')
|
|
1267
|
-
const book1 = Book.create({
|
|
1268
|
-
title: 'the left hand of darkness',
|
|
1269
|
-
id: book1Id,
|
|
1270
|
-
author: Author.createId('ursula'),
|
|
1271
|
-
numPages: 1,
|
|
1272
|
-
})
|
|
1273
|
-
const book2Id = Book.createId('dispossessed')
|
|
1274
|
-
const book2 = Book.create({
|
|
1275
|
-
title: 'the dispossessed',
|
|
1276
|
-
id: book2Id,
|
|
1277
|
-
author: Author.createId('ursula'),
|
|
1278
|
-
numPages: 1,
|
|
1279
|
-
})
|
|
1280
|
-
|
|
1281
|
-
let onAfterCreate: ReturnType<typeof vi.fn>
|
|
1282
|
-
let onAfterChange: ReturnType<typeof vi.fn>
|
|
1283
|
-
let onAfterDelete: ReturnType<typeof vi.fn>
|
|
1284
|
-
|
|
1285
|
-
let onBeforeCreate: ReturnType<typeof vi.fn>
|
|
1286
|
-
let onBeforeChange: ReturnType<typeof vi.fn>
|
|
1287
|
-
let onBeforeDelete: ReturnType<typeof vi.fn>
|
|
1288
|
-
|
|
1289
|
-
let onOperationComplete: ReturnType<typeof vi.fn>
|
|
1290
|
-
|
|
1291
|
-
beforeEach(() => {
|
|
1292
|
-
store = new Store({
|
|
1293
|
-
props: {},
|
|
1294
|
-
schema: StoreSchema.create<Book>({
|
|
1295
|
-
book: Book,
|
|
1296
|
-
}),
|
|
1297
|
-
})
|
|
1298
|
-
|
|
1299
|
-
onAfterCreate = vi.fn((record) => callbacks.push({ type: 'create', record }))
|
|
1300
|
-
onAfterChange = vi.fn((from, to) => callbacks.push({ type: 'change', from, to }))
|
|
1301
|
-
onAfterDelete = vi.fn((record) => callbacks.push({ type: 'delete', record }))
|
|
1302
|
-
|
|
1303
|
-
onBeforeCreate = vi.fn((record) => record)
|
|
1304
|
-
onBeforeChange = vi.fn((_from, to) => to)
|
|
1305
|
-
onBeforeDelete = vi.fn((_record) => {})
|
|
1306
|
-
|
|
1307
|
-
onOperationComplete = vi.fn(() => callbacks.push({ type: 'complete' }))
|
|
1308
|
-
callbacks = []
|
|
1309
|
-
|
|
1310
|
-
store.sideEffects.registerAfterCreateHandler('book', onAfterCreate)
|
|
1311
|
-
store.sideEffects.registerAfterChangeHandler('book', onAfterChange)
|
|
1312
|
-
store.sideEffects.registerAfterDeleteHandler('book', onAfterDelete)
|
|
1313
|
-
|
|
1314
|
-
store.sideEffects.registerBeforeCreateHandler('book', onBeforeCreate)
|
|
1315
|
-
store.sideEffects.registerBeforeChangeHandler('book', onBeforeChange)
|
|
1316
|
-
store.sideEffects.registerBeforeDeleteHandler('book', onBeforeDelete)
|
|
1317
|
-
|
|
1318
|
-
store.sideEffects.registerOperationCompleteHandler(onOperationComplete)
|
|
1319
|
-
})
|
|
1320
|
-
|
|
1321
|
-
it('fires callbacks at the end of an `atomic` op', () => {
|
|
1322
|
-
store.atomic(() => {
|
|
1323
|
-
expect(callbacks).toHaveLength(0)
|
|
1324
|
-
|
|
1325
|
-
store.put([book1, book2])
|
|
1326
|
-
|
|
1327
|
-
expect(callbacks).toHaveLength(0)
|
|
1328
|
-
})
|
|
1329
|
-
|
|
1330
|
-
expect(callbacks).toMatchObject([
|
|
1331
|
-
{ type: 'create', record: { id: book1Id } },
|
|
1332
|
-
{ type: 'create', record: { id: book2Id } },
|
|
1333
|
-
{ type: 'complete' },
|
|
1334
|
-
])
|
|
1335
|
-
})
|
|
1336
|
-
|
|
1337
|
-
it('doesnt fire callback for a record created then deleted', () => {
|
|
1338
|
-
store.atomic(() => {
|
|
1339
|
-
store.put([book1])
|
|
1340
|
-
store.remove([book1Id])
|
|
1341
|
-
})
|
|
1342
|
-
expect(callbacks).toMatchObject([{ type: 'complete' }])
|
|
1343
|
-
})
|
|
1344
|
-
|
|
1345
|
-
it('bails out if too many callbacks are fired', () => {
|
|
1346
|
-
let limit = 10
|
|
1347
|
-
onAfterCreate.mockImplementation((record: any) => {
|
|
1348
|
-
if (record.numPages < limit) {
|
|
1349
|
-
store.put([{ ...record, numPages: record.numPages + 1 }])
|
|
1350
|
-
}
|
|
1351
|
-
})
|
|
1352
|
-
onAfterChange.mockImplementation((from: any, to: any) => {
|
|
1353
|
-
if (to.numPages < limit) {
|
|
1354
|
-
store.put([{ ...to, numPages: to.numPages + 1 }])
|
|
1355
|
-
}
|
|
1356
|
-
})
|
|
1357
|
-
|
|
1358
|
-
// this should be fine:
|
|
1359
|
-
store.put([book1])
|
|
1360
|
-
expect(store.get(book1Id)!.numPages).toBe(limit)
|
|
1361
|
-
|
|
1362
|
-
// if we increase the limit thought, it should crash:
|
|
1363
|
-
limit = 10000
|
|
1364
|
-
store.clear()
|
|
1365
|
-
expect(() => {
|
|
1366
|
-
store.put([book2])
|
|
1367
|
-
}).toThrowErrorMatchingInlineSnapshot(
|
|
1368
|
-
`[Error: Maximum store update depth exceeded, bailing out]`
|
|
1369
|
-
)
|
|
1370
|
-
})
|
|
1371
|
-
|
|
1372
|
-
it('keeps firing operation complete callbacks until all are cleared', () => {
|
|
1373
|
-
// steps:
|
|
1374
|
-
// 0, 1, 2: after change increment pages
|
|
1375
|
-
// 3: after change, do nothing
|
|
1376
|
-
// 4: operation complete, increment pages by 1000
|
|
1377
|
-
// 5, 6: after change increment pages
|
|
1378
|
-
// 7: after change, do nothing
|
|
1379
|
-
// 8: operation complete, do nothing
|
|
1380
|
-
// 9: done!
|
|
1381
|
-
let step = 0
|
|
1382
|
-
|
|
1383
|
-
store.put([book1])
|
|
1384
|
-
|
|
1385
|
-
onAfterChange.mockImplementation((prev: any, next: any) => {
|
|
1386
|
-
if ([0, 1, 2, 5, 6].includes(step)) {
|
|
1387
|
-
step++
|
|
1388
|
-
store.put([{ ...next, numPages: next.numPages + 1 }])
|
|
1389
|
-
} else if ([3, 7].includes(step)) {
|
|
1390
|
-
step++
|
|
1391
|
-
} else {
|
|
1392
|
-
throw new Error(`Wrong step: ${step}`)
|
|
1393
|
-
}
|
|
1394
|
-
})
|
|
1395
|
-
|
|
1396
|
-
onOperationComplete.mockImplementation(() => {
|
|
1397
|
-
if (step === 4) {
|
|
1398
|
-
step++
|
|
1399
|
-
const book = store.get(book1Id)!
|
|
1400
|
-
store.put([{ ...book, numPages: book.numPages + 1000 }])
|
|
1401
|
-
} else if (step === 8) {
|
|
1402
|
-
step++
|
|
1403
|
-
} else {
|
|
1404
|
-
throw new Error(`Wrong step: ${step}`)
|
|
1405
|
-
}
|
|
1406
|
-
})
|
|
1407
|
-
|
|
1408
|
-
store.put([{ ...book1, numPages: 2 }])
|
|
1409
|
-
|
|
1410
|
-
expect(store.get(book1Id)!.numPages).toBe(1007)
|
|
1411
|
-
expect(step).toBe(9)
|
|
1412
|
-
})
|
|
1413
|
-
|
|
1414
|
-
test('fired during mergeRemoteChanges are flushed at the end so that they end up receiving remote source but outputting user source changes', () => {
|
|
1415
|
-
const diffs: HistoryEntry<Book>[] = []
|
|
1416
|
-
store.listen((entry) => {
|
|
1417
|
-
diffs.push(entry)
|
|
1418
|
-
})
|
|
1419
|
-
|
|
1420
|
-
const firstOrderEffectSources: string[] = []
|
|
1421
|
-
store.sideEffects.registerAfterCreateHandler('book', (record, source) => {
|
|
1422
|
-
firstOrderEffectSources.push(source)
|
|
1423
|
-
if (record.title.startsWith('Harry Potter')) {
|
|
1424
|
-
store.put([
|
|
1425
|
-
{
|
|
1426
|
-
...record,
|
|
1427
|
-
title: record.title + ' is a really great book fr fr',
|
|
1428
|
-
},
|
|
1429
|
-
])
|
|
1430
|
-
}
|
|
1431
|
-
})
|
|
1432
|
-
|
|
1433
|
-
const secondOrderEffectSources: string[] = []
|
|
1434
|
-
store.sideEffects.registerAfterChangeHandler('book', (from, to, source) => {
|
|
1435
|
-
secondOrderEffectSources.push(source)
|
|
1436
|
-
})
|
|
1437
|
-
|
|
1438
|
-
store.mergeRemoteChanges(() => {
|
|
1439
|
-
store.put([
|
|
1440
|
-
{
|
|
1441
|
-
...book1,
|
|
1442
|
-
title: "Harry Potter and the Philosopher's Stone",
|
|
1443
|
-
},
|
|
1444
|
-
])
|
|
1445
|
-
})
|
|
1446
|
-
|
|
1447
|
-
expect(firstOrderEffectSources).toMatchInlineSnapshot(`
|
|
1448
|
-
[
|
|
1449
|
-
"remote",
|
|
1450
|
-
]
|
|
1451
|
-
`)
|
|
1452
|
-
|
|
1453
|
-
// recursive changes are always user
|
|
1454
|
-
expect(secondOrderEffectSources).toMatchInlineSnapshot(`
|
|
1455
|
-
[
|
|
1456
|
-
"user",
|
|
1457
|
-
]
|
|
1458
|
-
`)
|
|
1459
|
-
|
|
1460
|
-
expect(diffs).toMatchInlineSnapshot(`
|
|
1461
|
-
[
|
|
1462
|
-
{
|
|
1463
|
-
"changes": {
|
|
1464
|
-
"added": {
|
|
1465
|
-
"book:darkness": {
|
|
1466
|
-
"author": "author:ursula",
|
|
1467
|
-
"id": "book:darkness",
|
|
1468
|
-
"numPages": 1,
|
|
1469
|
-
"title": "Harry Potter and the Philosopher's Stone",
|
|
1470
|
-
"typeName": "book",
|
|
1471
|
-
},
|
|
1472
|
-
},
|
|
1473
|
-
"removed": {},
|
|
1474
|
-
"updated": {},
|
|
1475
|
-
},
|
|
1476
|
-
"source": "remote",
|
|
1477
|
-
},
|
|
1478
|
-
{
|
|
1479
|
-
"changes": {
|
|
1480
|
-
"added": {},
|
|
1481
|
-
"removed": {},
|
|
1482
|
-
"updated": {
|
|
1483
|
-
"book:darkness": [
|
|
1484
|
-
{
|
|
1485
|
-
"author": "author:ursula",
|
|
1486
|
-
"id": "book:darkness",
|
|
1487
|
-
"numPages": 1,
|
|
1488
|
-
"title": "Harry Potter and the Philosopher's Stone",
|
|
1489
|
-
"typeName": "book",
|
|
1490
|
-
},
|
|
1491
|
-
{
|
|
1492
|
-
"author": "author:ursula",
|
|
1493
|
-
"id": "book:darkness",
|
|
1494
|
-
"numPages": 1,
|
|
1495
|
-
"title": "Harry Potter and the Philosopher's Stone is a really great book fr fr",
|
|
1496
|
-
"typeName": "book",
|
|
1497
|
-
},
|
|
1498
|
-
],
|
|
1499
|
-
},
|
|
1500
|
-
},
|
|
1501
|
-
"source": "user",
|
|
1502
|
-
},
|
|
1503
|
-
]
|
|
1504
|
-
`)
|
|
1505
|
-
})
|
|
1506
|
-
|
|
1507
|
-
test('noop changes do not fire with store.atomic', () => {
|
|
1508
|
-
const book1A = book1
|
|
1509
|
-
const book1B = {
|
|
1510
|
-
...book1,
|
|
1511
|
-
title: book1.title + ' is a really great book fr fr',
|
|
1512
|
-
}
|
|
1513
|
-
const book1C = structuredClone(book1A)
|
|
1514
|
-
store.put([book1A])
|
|
1515
|
-
|
|
1516
|
-
store.atomic(() => {
|
|
1517
|
-
store.put([book1B])
|
|
1518
|
-
store.put([book1C])
|
|
1519
|
-
})
|
|
1520
|
-
expect(onAfterChange).toHaveBeenCalledTimes(0)
|
|
1521
|
-
|
|
1522
|
-
store.atomic(() => {
|
|
1523
|
-
store.put([book1B])
|
|
1524
|
-
store.put([book1C])
|
|
1525
|
-
store.put([book1B])
|
|
1526
|
-
})
|
|
1527
|
-
|
|
1528
|
-
expect(onAfterChange).toHaveBeenCalledTimes(1)
|
|
1529
|
-
})
|
|
1530
|
-
|
|
1531
|
-
test('an atomic block with callbacks enabled can be overridden with an atomic block with callbacks disabled which causes the beforeCallbacks only to not run', () => {
|
|
1532
|
-
store.atomic(() => {
|
|
1533
|
-
store.put([book1])
|
|
1534
|
-
store.atomic(() => {
|
|
1535
|
-
store.put([book2])
|
|
1536
|
-
}, false)
|
|
1537
|
-
})
|
|
1538
|
-
|
|
1539
|
-
expect(onBeforeCreate).toHaveBeenCalledTimes(1)
|
|
1540
|
-
expect(onAfterCreate).toHaveBeenCalledTimes(2)
|
|
1541
|
-
|
|
1542
|
-
store.atomic(() => {
|
|
1543
|
-
store.update(book1Id, (book) => ({
|
|
1544
|
-
...book,
|
|
1545
|
-
numPages: book.numPages + 1,
|
|
1546
|
-
}))
|
|
1547
|
-
store.atomic(() => {
|
|
1548
|
-
store.update(book2Id, (book) => ({
|
|
1549
|
-
...book,
|
|
1550
|
-
numPages: book.numPages + 1,
|
|
1551
|
-
}))
|
|
1552
|
-
}, false)
|
|
1553
|
-
})
|
|
1554
|
-
|
|
1555
|
-
expect(onBeforeChange).toHaveBeenCalledTimes(1)
|
|
1556
|
-
expect(onAfterChange).toHaveBeenCalledTimes(2)
|
|
1557
|
-
|
|
1558
|
-
store.atomic(() => {
|
|
1559
|
-
store.remove([book1Id])
|
|
1560
|
-
store.atomic(() => {
|
|
1561
|
-
store.remove([book2Id])
|
|
1562
|
-
}, false)
|
|
1563
|
-
})
|
|
1564
|
-
expect(onBeforeDelete).toHaveBeenCalledTimes(1)
|
|
1565
|
-
expect(onAfterDelete).toHaveBeenCalledTimes(2)
|
|
1566
|
-
})
|
|
1567
|
-
})
|