@tldraw/store 5.2.0-next.ee0fa4d6244f → 5.2.1
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
|
@@ -9,136 +9,142 @@ import {
|
|
|
9
9
|
squashRecordDiffsMutable,
|
|
10
10
|
} from './RecordsDiff'
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
|
|
12
|
+
// Tests for SPEC.md §4 (RecordsDiff).
|
|
13
|
+
// Rule IDs like [D3] in test names refer to that document.
|
|
14
|
+
|
|
15
|
+
interface Book extends BaseRecord<'book', RecordId<Book>> {
|
|
14
16
|
title: string
|
|
15
|
-
author: string
|
|
16
|
-
pages: number
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
id: id as RecordId<TestBook>,
|
|
23
|
-
typeName: 'book',
|
|
24
|
-
title,
|
|
25
|
-
author,
|
|
26
|
-
pages,
|
|
27
|
-
}
|
|
19
|
+
function book(id: string, title: string): Book {
|
|
20
|
+
return { id: id as RecordId<Book>, typeName: 'book', title }
|
|
28
21
|
}
|
|
29
22
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
added: added as any,
|
|
38
|
-
updated: updated as any,
|
|
39
|
-
removed: removed as any,
|
|
40
|
-
}
|
|
23
|
+
function diff(
|
|
24
|
+
added: Record<string, Book> = {},
|
|
25
|
+
updated: Record<string, [Book, Book]> = {},
|
|
26
|
+
removed: Record<string, Book> = {}
|
|
27
|
+
): RecordsDiff<Book> {
|
|
28
|
+
return { added, updated, removed } as RecordsDiff<Book>
|
|
41
29
|
}
|
|
42
30
|
|
|
43
|
-
describe('
|
|
44
|
-
it('
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
describe('empty diffs (D)', () => {
|
|
32
|
+
it('[D1] createEmptyRecordsDiff returns a diff with empty collections', () => {
|
|
33
|
+
expect(createEmptyRecordsDiff()).toEqual({ added: {}, updated: {}, removed: {} })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('[D1] isRecordsDiffEmpty is true exactly when all three collections are empty', () => {
|
|
37
|
+
expect(isRecordsDiffEmpty(createEmptyRecordsDiff())).toBe(true)
|
|
38
|
+
|
|
39
|
+
expect(isRecordsDiffEmpty(diff({ 'book:1': book('book:1', 'a') }))).toBe(false)
|
|
40
|
+
expect(
|
|
41
|
+
isRecordsDiffEmpty(diff({}, { 'book:1': [book('book:1', 'a'), book('book:1', 'b')] }))
|
|
42
|
+
).toBe(false)
|
|
43
|
+
expect(isRecordsDiffEmpty(diff({}, {}, { 'book:1': book('book:1', 'a') }))).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
describe('reversing diffs (D)', () => {
|
|
48
|
+
it('[D2] swaps added and removed and reverses updated pairs', () => {
|
|
49
|
+
const added = book('book:1', 'Added')
|
|
50
|
+
const from = book('book:2', 'Old')
|
|
51
|
+
const to = book('book:2', 'New')
|
|
52
|
+
const removed = book('book:3', 'Removed')
|
|
50
53
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
expect(isRecordsDiffEmpty(updatedDiff)).toBe(false)
|
|
54
|
+
const reversed = reverseRecordsDiff(
|
|
55
|
+
diff({ 'book:1': added }, { 'book:2': [from, to] }, { 'book:3': removed })
|
|
56
|
+
)
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
{},
|
|
58
|
-
{},
|
|
59
|
-
{ 'book:1': createBook('book:1', 'Deleted', 'Author') }
|
|
58
|
+
expect(reversed).toEqual(
|
|
59
|
+
diff({ 'book:3': removed }, { 'book:2': [to, from] }, { 'book:1': added })
|
|
60
60
|
)
|
|
61
|
-
expect(isRecordsDiffEmpty(removedDiff)).toBe(false)
|
|
62
61
|
})
|
|
63
62
|
})
|
|
64
63
|
|
|
65
|
-
describe('
|
|
66
|
-
it('
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const originalDiff = createDiff<TestBook>(
|
|
73
|
-
{ 'book:1': addedBook },
|
|
74
|
-
{ 'book:2': [oldUpdatedBook, newUpdatedBook] },
|
|
75
|
-
{ 'book:3': removedBook }
|
|
64
|
+
describe('squashing diffs (D)', () => {
|
|
65
|
+
it('[D3] add then update becomes an add with the final value', () => {
|
|
66
|
+
const v1 = book('book:1', 'v1')
|
|
67
|
+
const v2 = book('book:1', 'v2')
|
|
68
|
+
expect(squashRecordDiffs([diff({ 'book:1': v1 }), diff({}, { 'book:1': [v1, v2] })])).toEqual(
|
|
69
|
+
diff({ 'book:1': v2 })
|
|
76
70
|
)
|
|
71
|
+
})
|
|
77
72
|
|
|
78
|
-
|
|
73
|
+
it('[D3] add then remove cancels out', () => {
|
|
74
|
+
const b = book('book:1', 'a')
|
|
75
|
+
expect(squashRecordDiffs([diff({ 'book:1': b }), diff({}, {}, { 'book:1': b })])).toEqual(
|
|
76
|
+
createEmptyRecordsDiff()
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
it('[D3] update then update collapses from the original from to the final to', () => {
|
|
81
|
+
const v1 = book('book:1', 'v1')
|
|
82
|
+
const v2 = book('book:1', 'v2')
|
|
83
|
+
const v3 = book('book:1', 'v3')
|
|
84
|
+
expect(
|
|
85
|
+
squashRecordDiffs([diff({}, { 'book:1': [v1, v2] }), diff({}, { 'book:1': [v2, v3] })])
|
|
86
|
+
).toEqual(diff({}, { 'book:1': [v1, v3] }))
|
|
86
87
|
})
|
|
87
|
-
})
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const diff1 = createDiff<TestBook>({ 'book:1': initialBook })
|
|
96
|
-
const diff2 = createDiff<TestBook>({}, { 'book:1': [initialBook, updatedBook] })
|
|
97
|
-
const result1 = squashRecordDiffs([diff1, diff2])
|
|
98
|
-
expect(result1).toEqual(createDiff<TestBook>({ 'book:1': updatedBook }))
|
|
99
|
-
|
|
100
|
-
// Add then remove cancels out
|
|
101
|
-
const book = createBook('book:1', 'Test Book', 'Author')
|
|
102
|
-
const diff3 = createDiff<TestBook>({ 'book:1': book })
|
|
103
|
-
const diff4 = createDiff<TestBook>({}, {}, { 'book:1': book })
|
|
104
|
-
const result2 = squashRecordDiffs([diff3, diff4])
|
|
105
|
-
expect(result2).toEqual(createEmptyRecordsDiff<TestBook>())
|
|
106
|
-
|
|
107
|
-
// Remove then add becomes update
|
|
108
|
-
const originalBook = createBook('book:1', 'Original', 'Author')
|
|
109
|
-
const newBook = createBook('book:1', 'New', 'Author')
|
|
110
|
-
const diff5 = createDiff<TestBook>({}, {}, { 'book:1': originalBook })
|
|
111
|
-
const diff6 = createDiff<TestBook>({ 'book:1': newBook })
|
|
112
|
-
const result3 = squashRecordDiffs([diff5, diff6])
|
|
113
|
-
expect(result3).toEqual(createDiff<TestBook>({}, { 'book:1': [originalBook, newBook] }))
|
|
114
|
-
|
|
115
|
-
// Chain updates together
|
|
116
|
-
const v1 = createBook('book:1', 'Version 1', 'Author')
|
|
117
|
-
const v2 = createBook('book:1', 'Version 2', 'Author')
|
|
118
|
-
const v3 = createBook('book:1', 'Version 3', 'Author')
|
|
119
|
-
const diff7 = createDiff<TestBook>({}, { 'book:1': [v1, v2] })
|
|
120
|
-
const diff8 = createDiff<TestBook>({}, { 'book:1': [v2, v3] })
|
|
121
|
-
const result4 = squashRecordDiffs([diff7, diff8])
|
|
122
|
-
expect(result4).toEqual(createDiff<TestBook>({}, { 'book:1': [v1, v3] }))
|
|
89
|
+
it('[D3] update then remove becomes a remove of the original from', () => {
|
|
90
|
+
const v1 = book('book:1', 'v1')
|
|
91
|
+
const v2 = book('book:1', 'v2')
|
|
92
|
+
expect(
|
|
93
|
+
squashRecordDiffs([diff({}, { 'book:1': [v1, v2] }), diff({}, {}, { 'book:1': v2 })])
|
|
94
|
+
).toEqual(diff({}, {}, { 'book:1': v1 }))
|
|
123
95
|
})
|
|
124
|
-
})
|
|
125
96
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
97
|
+
it('[D3] remove then add becomes an update', () => {
|
|
98
|
+
const original = book('book:1', 'Original')
|
|
99
|
+
const replacement = book('book:1', 'New')
|
|
100
|
+
expect(
|
|
101
|
+
squashRecordDiffs([diff({}, {}, { 'book:1': original }), diff({ 'book:1': replacement })])
|
|
102
|
+
).toEqual(diff({}, { 'book:1': [original, replacement] }))
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('[D3] remove then add of the identical object cancels out', () => {
|
|
106
|
+
const same = book('book:1', 'Same')
|
|
107
|
+
expect(squashRecordDiffs([diff({}, {}, { 'book:1': same }), diff({ 'book:1': same })])).toEqual(
|
|
108
|
+
createEmptyRecordsDiff()
|
|
109
|
+
)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('[D4] does not mutate its inputs by default', () => {
|
|
113
|
+
const v1 = book('book:1', 'v1')
|
|
114
|
+
const v2 = book('book:1', 'v2')
|
|
115
|
+
const first = diff({ 'book:1': v1 })
|
|
116
|
+
const second = diff({}, { 'book:1': [v1, v2] })
|
|
117
|
+
|
|
118
|
+
squashRecordDiffs([first, second])
|
|
119
|
+
|
|
120
|
+
expect(first).toEqual(diff({ 'book:1': v1 }))
|
|
121
|
+
expect(second).toEqual(diff({}, { 'book:1': [v1, v2] }))
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('[D4] mutateFirstDiff applies the result onto the first diff in place', () => {
|
|
125
|
+
const v1 = book('book:1', 'v1')
|
|
126
|
+
const v2 = book('book:1', 'v2')
|
|
127
|
+
const first = diff({ 'book:1': v1 })
|
|
128
|
+
|
|
129
|
+
const result = squashRecordDiffs([first, diff({}, { 'book:1': [v1, v2] })], {
|
|
130
|
+
mutateFirstDiff: true,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
expect(result).toBe(first)
|
|
134
|
+
expect(first).toEqual(diff({ 'book:1': v2 }))
|
|
135
|
+
})
|
|
131
136
|
|
|
132
|
-
|
|
133
|
-
const
|
|
137
|
+
it('[D4] squashRecordDiffsMutable agrees with squashRecordDiffs', () => {
|
|
138
|
+
const added = book('book:1', 'Book 1')
|
|
139
|
+
const from = book('book:2', 'Old')
|
|
140
|
+
const to = book('book:2', 'New')
|
|
134
141
|
|
|
135
|
-
|
|
136
|
-
const immutableResult = squashRecordDiffs([diff1, diff2])
|
|
142
|
+
const diffs = [diff({ 'book:1': added }), diff({}, { 'book:2': [from, to] })]
|
|
137
143
|
|
|
138
|
-
|
|
139
|
-
const mutableTarget = createEmptyRecordsDiff<
|
|
140
|
-
squashRecordDiffsMutable(mutableTarget,
|
|
144
|
+
const immutableResult = squashRecordDiffs(diffs)
|
|
145
|
+
const mutableTarget = createEmptyRecordsDiff<Book>()
|
|
146
|
+
squashRecordDiffsMutable(mutableTarget, diffs)
|
|
141
147
|
|
|
142
|
-
expect(
|
|
148
|
+
expect(mutableTarget).toEqual(immutableResult)
|
|
143
149
|
})
|
|
144
150
|
})
|
package/src/lib/RecordsDiff.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { objectMapEntries } from '@tldraw/utils'
|
|
2
1
|
import { IdOf, UnknownRecord } from './BaseRecord'
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -107,11 +106,16 @@ export function reverseRecordsDiff(diff: RecordsDiff<any>) {
|
|
|
107
106
|
* @public
|
|
108
107
|
*/
|
|
109
108
|
export function isRecordsDiffEmpty<T extends UnknownRecord>(diff: RecordsDiff<T>) {
|
|
110
|
-
return (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
return !hasAnyKey(diff.added) && !hasAnyKey(diff.updated) && !hasAnyKey(diff.removed)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Cheaper than Object.keys().length, but note that for-in still pays an O(N)
|
|
113
|
+
// key-collection prologue on dictionary-mode objects (which diffs become once keys
|
|
114
|
+
// are deleted from them) — avoid calling this on large diffs in per-frame hot paths.
|
|
115
|
+
/** @internal */
|
|
116
|
+
export function hasAnyKey(obj: object) {
|
|
117
|
+
for (const _ in obj) return true
|
|
118
|
+
return false
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
/**
|
|
@@ -203,9 +207,22 @@ export function squashRecordDiffsMutable<T extends UnknownRecord>(
|
|
|
203
207
|
target: RecordsDiff<T>,
|
|
204
208
|
diffs: RecordsDiff<T>[]
|
|
205
209
|
): void {
|
|
210
|
+
// This runs on every history interceptor call — e.g. once per input tick while
|
|
211
|
+
// resizing N shapes, with N entries in diff.updated — so the updated loop must not
|
|
212
|
+
// allocate per entry. We use for-in instead of Object.entries, mutate the target's
|
|
213
|
+
// existing [from, to] tuples in place, and skip delete calls against collections we
|
|
214
|
+
// know are empty. In-place tuple mutation is safe because the target exclusively
|
|
215
|
+
// owns its updated tuples: they are always created here (never shared with a source
|
|
216
|
+
// diff), and sources are never mutated.
|
|
206
217
|
for (const diff of diffs) {
|
|
207
|
-
|
|
208
|
-
|
|
218
|
+
// target.removed can only lose entries before the removed loop below, so a
|
|
219
|
+
// stale `true` is harmless (extra no-op deletes).
|
|
220
|
+
const targetHasRemoved = hasAnyKey(target.removed)
|
|
221
|
+
|
|
222
|
+
for (const _id in diff.added) {
|
|
223
|
+
const id = _id as IdOf<T>
|
|
224
|
+
const value = diff.added[id]
|
|
225
|
+
if (targetHasRemoved && target.removed[id]) {
|
|
209
226
|
const original = target.removed[id]
|
|
210
227
|
delete target.removed[id]
|
|
211
228
|
if (original !== value) {
|
|
@@ -216,24 +233,32 @@ export function squashRecordDiffsMutable<T extends UnknownRecord>(
|
|
|
216
233
|
}
|
|
217
234
|
}
|
|
218
235
|
|
|
219
|
-
|
|
220
|
-
|
|
236
|
+
// the added loop above may have inserted into target.added
|
|
237
|
+
const targetHasAdded = hasAnyKey(target.added)
|
|
238
|
+
|
|
239
|
+
for (const _id in diff.updated) {
|
|
240
|
+
const id = _id as IdOf<T>
|
|
241
|
+
const to = diff.updated[id][1]
|
|
242
|
+
if (targetHasAdded && target.added[id]) {
|
|
221
243
|
target.added[id] = to
|
|
222
244
|
delete target.updated[id]
|
|
223
|
-
delete target.removed[id]
|
|
245
|
+
if (targetHasRemoved) delete target.removed[id]
|
|
224
246
|
continue
|
|
225
247
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
248
|
+
const existing = target.updated[id]
|
|
249
|
+
if (existing) {
|
|
250
|
+
existing[1] = to
|
|
251
|
+
if (targetHasRemoved) delete target.removed[id]
|
|
229
252
|
continue
|
|
230
253
|
}
|
|
231
254
|
|
|
232
|
-
target
|
|
233
|
-
|
|
255
|
+
// copy the tuple so the target owns it and can mutate it in place later
|
|
256
|
+
target.updated[id] = [diff.updated[id][0], to]
|
|
257
|
+
if (targetHasRemoved) delete target.removed[id]
|
|
234
258
|
}
|
|
235
259
|
|
|
236
|
-
for (const
|
|
260
|
+
for (const _id in diff.removed) {
|
|
261
|
+
const id = _id as IdOf<T>
|
|
237
262
|
// the same record was added in this diff sequence, just drop it
|
|
238
263
|
if (target.added[id]) {
|
|
239
264
|
delete target.added[id]
|
|
@@ -241,7 +266,7 @@ export function squashRecordDiffsMutable<T extends UnknownRecord>(
|
|
|
241
266
|
target.removed[id] = target.updated[id][0]
|
|
242
267
|
delete target.updated[id]
|
|
243
268
|
} else {
|
|
244
|
-
target.removed[id] =
|
|
269
|
+
target.removed[id] = diff.removed[id]
|
|
245
270
|
}
|
|
246
271
|
}
|
|
247
272
|
}
|