@tldraw/editor 5.5.4 → 5.6.0-canary.0091c91994d6
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 +166 -1
- package/dist-cjs/index.js +7 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +18 -5
- package/dist-cjs/lib/TldrawEditor.js.map +3 -3
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +57 -15
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
- package/dist-cjs/lib/config/TLI18n.js +64 -0
- package/dist-cjs/lib/config/TLI18n.js.map +7 -0
- package/dist-cjs/lib/editor/Editor.js +44 -16
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +10 -3
- package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +1 -1
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +19 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +3 -5
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +7 -1
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +28 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +3 -0
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/shared/resizeBox.js +21 -16
- package/dist-cjs/lib/editor/shapes/shared/resizeBox.js.map +2 -2
- package/dist-cjs/lib/hooks/useCursor.js +12 -8
- package/dist-cjs/lib/hooks/useCursor.js.map +2 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js +28 -3
- package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
- package/dist-cjs/lib/license/LicenseManager.js +59 -0
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +12 -5
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +166 -1
- package/dist-esm/index.mjs +13 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +18 -5
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +61 -16
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
- package/dist-esm/lib/config/TLI18n.mjs +44 -0
- package/dist-esm/lib/config/TLI18n.mjs.map +7 -0
- package/dist-esm/lib/editor/Editor.mjs +46 -17
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +10 -3
- package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +1 -1
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +20 -3
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +3 -5
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +7 -1
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +28 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +3 -0
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/shared/resizeBox.mjs +21 -16
- package/dist-esm/lib/editor/shapes/shared/resizeBox.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCursor.mjs +12 -8
- package/dist-esm/lib/hooks/useCursor.mjs.map +2 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs +28 -3
- package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseManager.mjs +59 -0
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +13 -6
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +17 -19
- package/src/index.ts +14 -2
- package/src/lib/TldrawEditor.tsx +44 -4
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +66 -18
- package/src/lib/config/TLI18n.test.ts +84 -0
- package/src/lib/config/TLI18n.ts +172 -0
- package/src/lib/editor/Editor.ts +90 -33
- package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +19 -3
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +17 -0
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +2 -2
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +1 -0
- package/src/lib/editor/managers/FontManager/FontManager.ts +26 -3
- package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +32 -0
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +4 -6
- package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +16 -7
- package/src/lib/editor/shapes/ShapeUtil.ts +30 -0
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +4 -0
- package/src/lib/editor/shapes/shared/resizeBox.ts +21 -16
- package/src/lib/hooks/useCursor.ts +14 -7
- package/src/lib/hooks/useDocumentEvents.ts +51 -3
- package/src/lib/license/LicenseManager.test.ts +156 -0
- package/src/lib/license/LicenseManager.ts +83 -0
- package/src/lib/test/useCursor.test.tsx +36 -0
- package/src/lib/test/useDocumentEvents.test.tsx +153 -22
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +73 -0
- package/src/lib/utils/sync/TLLocalSyncClient.ts +24 -8
- package/src/version.ts +3 -3
|
@@ -5,6 +5,34 @@ import { Editor } from '../editor/Editor'
|
|
|
5
5
|
import { TLKeyboardEventInfo } from '../editor/types/event-types'
|
|
6
6
|
import { TL_CONTAINER_CLASS, TldrawEditor } from '../TldrawEditor'
|
|
7
7
|
|
|
8
|
+
async function renderEditor() {
|
|
9
|
+
let editor!: Editor
|
|
10
|
+
const store = createTLStore({ shapeUtils: [], bindingUtils: [] })
|
|
11
|
+
await act(async () => {
|
|
12
|
+
render(<TldrawEditor store={store} autoFocus onMount={(e) => void (editor = e)} />)
|
|
13
|
+
})
|
|
14
|
+
const container = document.querySelector<HTMLElement>(`.${TL_CONTAINER_CLASS}`)!
|
|
15
|
+
return { editor, container }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function keyDown(container: HTMLElement, init: KeyboardEventInit) {
|
|
19
|
+
act(() => {
|
|
20
|
+
container.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, ...init }))
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function keyUp(container: HTMLElement, init: KeyboardEventInit) {
|
|
25
|
+
act(() => {
|
|
26
|
+
container.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, ...init }))
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function blurWindow() {
|
|
31
|
+
act(() => {
|
|
32
|
+
window.dispatchEvent(new Event('blur'))
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
8
36
|
describe('useDocumentEvents drop handling', () => {
|
|
9
37
|
// The container's native drop listener used to stop propagation before the event reached
|
|
10
38
|
// React's root, so React onDrop handlers inside the canvas never fired.
|
|
@@ -33,28 +61,6 @@ describe('useDocumentEvents drop handling', () => {
|
|
|
33
61
|
})
|
|
34
62
|
|
|
35
63
|
describe('useDocumentEvents window blur', () => {
|
|
36
|
-
async function renderEditor() {
|
|
37
|
-
let editor!: Editor
|
|
38
|
-
const store = createTLStore({ shapeUtils: [], bindingUtils: [] })
|
|
39
|
-
await act(async () => {
|
|
40
|
-
render(<TldrawEditor store={store} autoFocus onMount={(e) => void (editor = e)} />)
|
|
41
|
-
})
|
|
42
|
-
const container = document.querySelector<HTMLElement>(`.${TL_CONTAINER_CLASS}`)!
|
|
43
|
-
return { editor, container }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function keyDown(container: HTMLElement, init: KeyboardEventInit) {
|
|
47
|
-
act(() => {
|
|
48
|
-
container.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, ...init }))
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function blurWindow() {
|
|
53
|
-
act(() => {
|
|
54
|
-
window.dispatchEvent(new Event('blur'))
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
|
|
58
64
|
afterEach(() => {
|
|
59
65
|
cleanup()
|
|
60
66
|
})
|
|
@@ -105,3 +111,128 @@ describe('useDocumentEvents window blur', () => {
|
|
|
105
111
|
expect(onEvent).not.toHaveBeenCalled()
|
|
106
112
|
})
|
|
107
113
|
})
|
|
114
|
+
|
|
115
|
+
// Browsers report `metaKey` as still true on the keyup of Meta itself.
|
|
116
|
+
const metaKeyUp: KeyboardEventInit = { key: 'Meta', code: 'MetaLeft', metaKey: true }
|
|
117
|
+
|
|
118
|
+
describe('useDocumentEvents meta release', () => {
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
cleanup()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// macOS never delivers the keyup of a non-modifier key pressed while Meta is held, so the
|
|
124
|
+
// arrow from a Cmd+Arrow shortcut stayed held and the next plain arrow nudged diagonally.
|
|
125
|
+
it('releases keys whose keyup macOS swallowed while Meta was held', async () => {
|
|
126
|
+
const { editor, container } = await renderEditor()
|
|
127
|
+
|
|
128
|
+
keyDown(container, { key: 'Meta', code: 'MetaLeft', metaKey: true })
|
|
129
|
+
keyDown(container, { key: 'ArrowUp', code: 'ArrowUp', metaKey: true })
|
|
130
|
+
expect([...editor.inputs.keys]).toEqual(['MetaLeft', 'ArrowUp'])
|
|
131
|
+
|
|
132
|
+
// no keyup for ArrowUp ever arrives
|
|
133
|
+
keyUp(container, metaKeyUp)
|
|
134
|
+
expect([...editor.inputs.keys]).toEqual([])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// A key held from before Meta went down still gets its own keyup, so releasing it here
|
|
138
|
+
// would drop a key the user is still holding.
|
|
139
|
+
it('leaves keys held from before Meta went down', async () => {
|
|
140
|
+
const { editor, container } = await renderEditor()
|
|
141
|
+
|
|
142
|
+
keyDown(container, { key: ' ', code: 'Space' })
|
|
143
|
+
keyDown(container, { key: 'Meta', code: 'MetaLeft', metaKey: true })
|
|
144
|
+
keyUp(container, metaKeyUp)
|
|
145
|
+
|
|
146
|
+
expect([...editor.inputs.keys]).toEqual(['Space'])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// That key keeps auto-repeating, and once Cmd is down those repeats carry `metaKey`.
|
|
150
|
+
// Counting them would release a key that is still physically down - and a repeat never
|
|
151
|
+
// puts it back, since the editor treats key_repeat as a noop.
|
|
152
|
+
it('leaves a key that was already repeating when Meta went down', async () => {
|
|
153
|
+
const { editor, container } = await renderEditor()
|
|
154
|
+
|
|
155
|
+
keyDown(container, { key: 'ArrowUp', code: 'ArrowUp' })
|
|
156
|
+
keyDown(container, { key: 'Meta', code: 'MetaLeft', metaKey: true })
|
|
157
|
+
keyDown(container, { key: 'ArrowUp', code: 'ArrowUp', metaKey: true, repeat: true })
|
|
158
|
+
keyUp(container, metaKeyUp)
|
|
159
|
+
|
|
160
|
+
expect([...editor.inputs.keys]).toEqual(['ArrowUp'])
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// Idle.onKeyUp re-enters shape editing on Enter, so replaying a key_up here stole the
|
|
164
|
+
// focus that cmd+Enter's a11y action had just put on the style toolbar.
|
|
165
|
+
it('does not fire tool key_up handlers for the keys it releases', async () => {
|
|
166
|
+
const { editor, container } = await renderEditor()
|
|
167
|
+
const keyUps: TLKeyboardEventInfo[] = []
|
|
168
|
+
editor.on('event', (info) => {
|
|
169
|
+
if (info.type === 'keyboard' && info.name === 'key_up') keyUps.push(info)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
keyDown(container, { key: 'Meta', code: 'MetaLeft', metaKey: true })
|
|
173
|
+
keyDown(container, { key: 'Enter', code: 'Enter', metaKey: true })
|
|
174
|
+
keyUp(container, metaKeyUp)
|
|
175
|
+
|
|
176
|
+
expect([...editor.inputs.keys]).toEqual([])
|
|
177
|
+
expect(keyUps.map((info) => info.code)).toEqual(['MetaLeft'])
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// A still-held modifier keeps its own keyup, and reporting it as up would start the
|
|
181
|
+
// editor's 150ms release: `ShiftLeft` would leave `inputs.keys` and shrink the nudge step.
|
|
182
|
+
it('leaves a still-held modifier alone', async () => {
|
|
183
|
+
const { editor, container } = await renderEditor()
|
|
184
|
+
|
|
185
|
+
keyDown(container, { key: 'Shift', code: 'ShiftLeft', shiftKey: true })
|
|
186
|
+
keyDown(container, { key: 'Meta', code: 'MetaLeft', shiftKey: true, metaKey: true })
|
|
187
|
+
keyDown(container, { key: 'ArrowUp', code: 'ArrowUp', shiftKey: true, metaKey: true })
|
|
188
|
+
|
|
189
|
+
keyUp(container, { ...metaKeyUp, shiftKey: true })
|
|
190
|
+
expect([...editor.inputs.keys]).toEqual(['ShiftLeft'])
|
|
191
|
+
|
|
192
|
+
await act(async () => {
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
194
|
+
})
|
|
195
|
+
expect([...editor.inputs.keys]).toEqual(['ShiftLeft'])
|
|
196
|
+
expect(editor.inputs.getShiftKey()).toBe(true)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// Letting go of Cmd while still holding the arrow: the arrow is dropped as owed a keyup,
|
|
200
|
+
// but it is genuinely down, and its repeats say so. Without this, nudging stopped dead
|
|
201
|
+
// mid-gesture and stayed dead for as long as the key was held.
|
|
202
|
+
it('takes back a dropped key that is still repeating', async () => {
|
|
203
|
+
const { editor, container } = await renderEditor()
|
|
204
|
+
|
|
205
|
+
keyDown(container, { key: 'Meta', code: 'MetaLeft', metaKey: true })
|
|
206
|
+
keyDown(container, { key: 'ArrowUp', code: 'ArrowUp', metaKey: true })
|
|
207
|
+
keyUp(container, metaKeyUp)
|
|
208
|
+
expect([...editor.inputs.keys]).toEqual([])
|
|
209
|
+
|
|
210
|
+
keyDown(container, { key: 'ArrowUp', code: 'ArrowUp', repeat: true })
|
|
211
|
+
expect([...editor.inputs.keys]).toEqual(['ArrowUp'])
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('does not disturb keys held without Meta', async () => {
|
|
215
|
+
const { editor, container } = await renderEditor()
|
|
216
|
+
|
|
217
|
+
keyDown(container, { key: 'ArrowUp', code: 'ArrowUp' })
|
|
218
|
+
keyUp(container, { key: 'ArrowUp', code: 'ArrowUp' })
|
|
219
|
+
keyDown(container, { key: 'ArrowRight', code: 'ArrowRight' })
|
|
220
|
+
|
|
221
|
+
expect([...editor.inputs.keys]).toEqual(['ArrowRight'])
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// Browsers report `metaKey: true` on Meta's own keyup, which left `getMetaKey()` true
|
|
225
|
+
// until some later event happened to report it false.
|
|
226
|
+
it('releases meta itself, though its keyup reports metaKey as still down', async () => {
|
|
227
|
+
const { editor, container } = await renderEditor()
|
|
228
|
+
|
|
229
|
+
keyDown(container, { key: 'Meta', code: 'MetaLeft', metaKey: true })
|
|
230
|
+
expect(editor.inputs.getMetaKey()).toBe(true)
|
|
231
|
+
|
|
232
|
+
keyUp(container, metaKeyUp)
|
|
233
|
+
await act(async () => {
|
|
234
|
+
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
235
|
+
})
|
|
236
|
+
expect(editor.inputs.getMetaKey()).toBe(false)
|
|
237
|
+
})
|
|
238
|
+
})
|
|
@@ -484,3 +484,76 @@ test('a buffered newer schema reports a load error without announcing a successf
|
|
|
484
484
|
client.close()
|
|
485
485
|
expect(client.db.storeSnapshot).not.toHaveBeenCalled()
|
|
486
486
|
})
|
|
487
|
+
|
|
488
|
+
test('a client created on the same key while a closed client is still flushing waits for that flush', async () => {
|
|
489
|
+
const inFlightWrite = promiseWithResolve<void>()
|
|
490
|
+
const { client, tick } = testClient()
|
|
491
|
+
client.db.storeSnapshot.mockImplementationOnce(() => inFlightWrite)
|
|
492
|
+
await tick()
|
|
493
|
+
client.store.put([PageRecordType.create({ name: 'first', index: 'a0' as IndexKey })])
|
|
494
|
+
await tick()
|
|
495
|
+
expect(client.db.storeSnapshot).toHaveBeenCalledTimes(1) // still in flight
|
|
496
|
+
client.store.put([PageRecordType.create({ name: 'during write', index: 'a1' as IndexKey })])
|
|
497
|
+
client.close()
|
|
498
|
+
|
|
499
|
+
// remount on the same key while the old client is still flushing
|
|
500
|
+
const loadSpy = vi.spyOn(LocalIndexedDb.prototype, 'load')
|
|
501
|
+
const next = testClient()
|
|
502
|
+
await tick()
|
|
503
|
+
// reading now would miss the queued edit, then erase it on the first write
|
|
504
|
+
expect(loadSpy).not.toHaveBeenCalled()
|
|
505
|
+
expect(next.onLoad).not.toHaveBeenCalled()
|
|
506
|
+
|
|
507
|
+
inFlightWrite.resolve()
|
|
508
|
+
await tick()
|
|
509
|
+
expect(client.db.storeChanges).toHaveBeenCalledTimes(1)
|
|
510
|
+
for (let i = 0; i < 20; i++) await Promise.resolve()
|
|
511
|
+
await next.tick()
|
|
512
|
+
expect(loadSpy).toHaveBeenCalledTimes(1)
|
|
513
|
+
expect(next.onLoad).toHaveBeenCalledTimes(1)
|
|
514
|
+
loadSpy.mockRestore()
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
test('two clients closing on the same key flush in order', async () => {
|
|
518
|
+
const writes: string[] = []
|
|
519
|
+
const inFlightWrite = promiseWithResolve<void>()
|
|
520
|
+
|
|
521
|
+
const a = testClient()
|
|
522
|
+
const b = testClient()
|
|
523
|
+
await a.tick()
|
|
524
|
+
await b.tick()
|
|
525
|
+
// hold a's first write in flight
|
|
526
|
+
a.client.db.storeSnapshot.mockImplementation(() => {
|
|
527
|
+
writes.push('a')
|
|
528
|
+
return inFlightWrite
|
|
529
|
+
})
|
|
530
|
+
a.client.db.storeChanges.mockImplementation(() => {
|
|
531
|
+
writes.push('a')
|
|
532
|
+
return Promise.resolve()
|
|
533
|
+
})
|
|
534
|
+
b.client.db.storeSnapshot.mockImplementation(() => {
|
|
535
|
+
writes.push('b')
|
|
536
|
+
return Promise.resolve()
|
|
537
|
+
})
|
|
538
|
+
b.client.db.storeChanges.mockImplementation(() => {
|
|
539
|
+
writes.push('b')
|
|
540
|
+
return Promise.resolve()
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
a.client.store.put([PageRecordType.create({ name: 'a1', index: 'a0' as IndexKey })])
|
|
544
|
+
await a.tick()
|
|
545
|
+
expect(writes).toEqual(['a']) // in flight
|
|
546
|
+
a.client.store.put([PageRecordType.create({ name: 'a2', index: 'a1' as IndexKey })])
|
|
547
|
+
a.client.close()
|
|
548
|
+
|
|
549
|
+
b.client.store.put([PageRecordType.create({ name: 'b1', index: 'a2' as IndexKey })])
|
|
550
|
+
b.client.close()
|
|
551
|
+
await b.tick()
|
|
552
|
+
// b's snapshot landing first would lose a's queued changes
|
|
553
|
+
expect(writes).toEqual(['a'])
|
|
554
|
+
|
|
555
|
+
inFlightWrite.resolve()
|
|
556
|
+
await a.tick()
|
|
557
|
+
await b.tick()
|
|
558
|
+
expect(writes).toEqual(['a', 'a', 'b'])
|
|
559
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Signal, transact } from '@tldraw/state'
|
|
2
2
|
import { RecordsDiff, SerializedSchema, UnknownRecord, squashRecordDiffs } from '@tldraw/store'
|
|
3
3
|
import { TLStore } from '@tldraw/tlschema'
|
|
4
|
-
import { assert } from '@tldraw/utils'
|
|
4
|
+
import { assert, noop } from '@tldraw/utils'
|
|
5
5
|
import {
|
|
6
6
|
TAB_ID,
|
|
7
7
|
TLSessionStateSnapshot,
|
|
@@ -64,6 +64,9 @@ export class BroadcastChannelMock {
|
|
|
64
64
|
|
|
65
65
|
const BC = typeof BroadcastChannel === 'undefined' ? BroadcastChannelMock : BroadcastChannel
|
|
66
66
|
|
|
67
|
+
// Flushes still running for closed clients, by key. See connect() and close().
|
|
68
|
+
const pendingFlushes = new Map<string, Promise<void>>()
|
|
69
|
+
|
|
67
70
|
/** @internal */
|
|
68
71
|
export class TLLocalSyncClient {
|
|
69
72
|
private disposables = new Set<() => void>()
|
|
@@ -238,6 +241,11 @@ export class TLLocalSyncClient {
|
|
|
238
241
|
this.channel.close()
|
|
239
242
|
})
|
|
240
243
|
|
|
244
|
+
// our first write is a full snapshot, so it would erase whatever a client that just closed
|
|
245
|
+
// on this key is still flushing
|
|
246
|
+
await pendingFlushes.get(this.persistenceKey)
|
|
247
|
+
if (this.didDispose) return
|
|
248
|
+
|
|
241
249
|
try {
|
|
242
250
|
data = await this.db.load({ sessionId: this.sessionId })
|
|
243
251
|
} catch (error: any) {
|
|
@@ -313,8 +321,6 @@ export class TLLocalSyncClient {
|
|
|
313
321
|
if (this.didDispose) return
|
|
314
322
|
this.debug('closing')
|
|
315
323
|
this.didDispose = true
|
|
316
|
-
// Store listeners normally run on the next frame; collect the last edits before removal.
|
|
317
|
-
this.store._flushHistory()
|
|
318
324
|
this.disposables.forEach((d) => d())
|
|
319
325
|
if (this.scheduledPersistTimeout) {
|
|
320
326
|
clearTimeout(this.scheduledPersistTimeout)
|
|
@@ -323,7 +329,18 @@ export class TLLocalSyncClient {
|
|
|
323
329
|
if (typeof window !== 'undefined' && (window as any).tlsync === this) {
|
|
324
330
|
delete (window as any).tlsync
|
|
325
331
|
}
|
|
326
|
-
|
|
332
|
+
// flush now when the key is idle: close() runs during teardown, where a deferred write may
|
|
333
|
+
// never run. wait when it isn't, or our snapshot lands first and gets overwritten.
|
|
334
|
+
// connect() awaits this, so it must never reject
|
|
335
|
+
const previous = pendingFlushes.get(this.persistenceKey)
|
|
336
|
+
const flush = (previous ? previous.then(() => this.flushAndCloseDb()) : this.flushAndCloseDb())
|
|
337
|
+
.catch(noop)
|
|
338
|
+
.then(() => {
|
|
339
|
+
if (pendingFlushes.get(this.persistenceKey) === flush) {
|
|
340
|
+
pendingFlushes.delete(this.persistenceKey)
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
pendingFlushes.set(this.persistenceKey, flush)
|
|
327
344
|
}
|
|
328
345
|
|
|
329
346
|
/**
|
|
@@ -332,17 +349,16 @@ export class TLLocalSyncClient {
|
|
|
332
349
|
* store), and only when something is queued: shouldDoFullDBWrite stays true until the first
|
|
333
350
|
* persist, so an unconditional flush would rewrite the whole document on every no-op close.
|
|
334
351
|
*/
|
|
335
|
-
private flushAndCloseDb() {
|
|
352
|
+
private flushAndCloseDb(): Promise<void> {
|
|
336
353
|
// A persist in flight can't be joined (persistIfNeeded bails while one is running) and,
|
|
337
354
|
// now that we're disposed, won't schedule a follow-up. Edits queued during that write
|
|
338
355
|
// would be lost if the db closed now, so wait for it and flush again.
|
|
339
356
|
if (this.persistPromise) {
|
|
340
|
-
this.persistPromise.then(() => this.flushAndCloseDb())
|
|
341
|
-
return
|
|
357
|
+
return this.persistPromise.then(() => this.flushAndCloseDb())
|
|
342
358
|
}
|
|
343
359
|
if (this.didLoad && this.diffQueue.length > 0) this.persistIfNeeded()
|
|
344
360
|
// the db waits for the transaction the flush just opened before actually closing
|
|
345
|
-
this.db.close()
|
|
361
|
+
return this.db.close()
|
|
346
362
|
}
|
|
347
363
|
|
|
348
364
|
private isPersisting = false
|
package/src/version.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// This file is automatically generated by internal/scripts/refresh-assets.ts.
|
|
2
2
|
// Do not edit manually. Or do, I'm a comment, not a cop.
|
|
3
3
|
|
|
4
|
-
export const version = '5.
|
|
4
|
+
export const version = '5.6.0-canary.0091c91994d6'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2026-05-06T16:28:18.473Z',
|
|
7
|
-
minor: '2026-
|
|
8
|
-
patch: '2026-10-
|
|
7
|
+
minor: '2026-10-28T11:15:20.344Z',
|
|
8
|
+
patch: '2026-10-28T11:15:20.344Z',
|
|
9
9
|
}
|