@tldraw/sync-core 5.4.2 → 5.5.0-canary.0241f11f2e99
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.js +1 -1
- package/dist-cjs/lib/ClientWebSocketAdapter.js +24 -12
- package/dist-cjs/lib/ClientWebSocketAdapter.js.map +2 -2
- package/dist-cjs/lib/SQLiteSyncStorage.js +15 -7
- package/dist-cjs/lib/SQLiteSyncStorage.js.map +2 -2
- package/dist-cjs/lib/TLSocketRoom.js +4 -7
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +30 -3
- package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
- package/dist-cjs/lib/chunk.js +10 -1
- package/dist-cjs/lib/chunk.js.map +2 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/ClientWebSocketAdapter.mjs +24 -12
- package/dist-esm/lib/ClientWebSocketAdapter.mjs.map +2 -2
- package/dist-esm/lib/SQLiteSyncStorage.mjs +15 -7
- package/dist-esm/lib/SQLiteSyncStorage.mjs.map +2 -2
- package/dist-esm/lib/TLSocketRoom.mjs +4 -7
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +30 -3
- package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
- package/dist-esm/lib/chunk.mjs +10 -1
- package/dist-esm/lib/chunk.mjs.map +2 -2
- package/package.json +6 -6
- package/src/lib/ClientWebSocketAdapter.test.ts +122 -2
- package/src/lib/ClientWebSocketAdapter.ts +30 -14
- package/src/lib/SQLiteSyncStorage.test.ts +60 -1
- package/src/lib/SQLiteSyncStorage.ts +22 -14
- package/src/lib/TLSocketRoom.ts +8 -7
- package/src/lib/TLSyncRoom.ts +39 -3
- package/src/lib/chunk.test.ts +34 -0
- package/src/lib/chunk.ts +20 -1
- package/src/test/TLSocketRoom.test.ts +39 -0
- package/src/test/TLSyncRoom.test.ts +105 -0
- package/src/test/objectStore.test.ts +35 -0
|
@@ -1949,6 +1949,111 @@ describe('25. Messaging and broadcast (RB)', () => {
|
|
|
1949
1949
|
expect(socketB.sendMessage).not.toHaveBeenCalled()
|
|
1950
1950
|
})
|
|
1951
1951
|
|
|
1952
|
+
it('[RB3] a debounced flush into a socket that closed meanwhile cancels the session instead of sending', () => {
|
|
1953
|
+
vi.useFakeTimers()
|
|
1954
|
+
const { room, socketB } = setupTwoSessions()
|
|
1955
|
+
const newPage = makePage('page_3', 'v1')
|
|
1956
|
+
|
|
1957
|
+
room.handleMessage('a', {
|
|
1958
|
+
type: 'push',
|
|
1959
|
+
clientClock: 1,
|
|
1960
|
+
diff: { [newPage.id]: ['put', newPage] },
|
|
1961
|
+
} as TLPushRequest<TLRecord>)
|
|
1962
|
+
room.handleMessage('a', {
|
|
1963
|
+
type: 'push',
|
|
1964
|
+
clientClock: 2,
|
|
1965
|
+
diff: { [newPage.id]: ['patch', { name: ['put', 'v2'] }] },
|
|
1966
|
+
} as TLPushRequest<TLRecord>)
|
|
1967
|
+
expect(socketB.sendMessage).toHaveBeenCalledTimes(1)
|
|
1968
|
+
|
|
1969
|
+
// the socket closes while the second message sits in the debounce buffer
|
|
1970
|
+
socketB.isOpen = false
|
|
1971
|
+
vi.advanceTimersByTime(DATA_MESSAGE_DEBOUNCE_INTERVAL + 1)
|
|
1972
|
+
|
|
1973
|
+
expect(socketB.sendMessage).toHaveBeenCalledTimes(1)
|
|
1974
|
+
expect(room.sessions.get('b')?.state).toBe(RoomSessionState.AwaitingRemoval)
|
|
1975
|
+
})
|
|
1976
|
+
|
|
1977
|
+
it('[RC7] close() drops pending debounced flushes so nothing is sent into closed sockets afterwards', () => {
|
|
1978
|
+
vi.useFakeTimers()
|
|
1979
|
+
const { room, socketB } = setupTwoSessions()
|
|
1980
|
+
const newPage = makePage('page_3', 'v1')
|
|
1981
|
+
|
|
1982
|
+
room.handleMessage('a', {
|
|
1983
|
+
type: 'push',
|
|
1984
|
+
clientClock: 1,
|
|
1985
|
+
diff: { [newPage.id]: ['put', newPage] },
|
|
1986
|
+
} as TLPushRequest<TLRecord>)
|
|
1987
|
+
room.handleMessage('a', {
|
|
1988
|
+
type: 'push',
|
|
1989
|
+
clientClock: 2,
|
|
1990
|
+
diff: { [newPage.id]: ['patch', { name: ['put', 'v2'] }] },
|
|
1991
|
+
} as TLPushRequest<TLRecord>)
|
|
1992
|
+
expect(socketB.sendMessage).toHaveBeenCalledTimes(1)
|
|
1993
|
+
|
|
1994
|
+
room.close()
|
|
1995
|
+
expect(room.isClosed()).toBe(true)
|
|
1996
|
+
vi.advanceTimersByTime(DATA_MESSAGE_DEBOUNCE_INTERVAL + 1)
|
|
1997
|
+
|
|
1998
|
+
expect(socketB.sendMessage).toHaveBeenCalledTimes(1)
|
|
1999
|
+
expect(socketB.close).toHaveBeenCalled()
|
|
2000
|
+
})
|
|
2001
|
+
|
|
2002
|
+
it('[RC7] close() closes every socket even when one of them throws on close', () => {
|
|
2003
|
+
const { room, socketA, socketB } = setupTwoSessions()
|
|
2004
|
+
socketA.close.mockImplementationOnce(() => {
|
|
2005
|
+
throw new Error('already closing')
|
|
2006
|
+
})
|
|
2007
|
+
|
|
2008
|
+
expect(() => room.close()).not.toThrow()
|
|
2009
|
+
expect(room.isClosed()).toBe(true)
|
|
2010
|
+
expect(socketB.close).toHaveBeenCalled()
|
|
2011
|
+
})
|
|
2012
|
+
|
|
2013
|
+
it('[RC7] close() forgets its sessions, and late socket close events emit no lifecycle events', () => {
|
|
2014
|
+
vi.useFakeTimers()
|
|
2015
|
+
const { room, socketA } = setupTwoSessions()
|
|
2016
|
+
const removed = vi.fn()
|
|
2017
|
+
const becameEmpty = vi.fn()
|
|
2018
|
+
room.events.on('session_removed', removed)
|
|
2019
|
+
room.events.on('room_became_empty', becameEmpty)
|
|
2020
|
+
|
|
2021
|
+
// b's socket closes before close(), a's closes after: both must stay silent
|
|
2022
|
+
room.handleClose('b')
|
|
2023
|
+
room.close()
|
|
2024
|
+
expect(room.sessions.size).toBe(0)
|
|
2025
|
+
room.handleClose('a')
|
|
2026
|
+
|
|
2027
|
+
vi.advanceTimersByTime(SESSION_REMOVAL_WAIT_TIME + 2001)
|
|
2028
|
+
expect(removed).not.toHaveBeenCalled()
|
|
2029
|
+
expect(becameEmpty).not.toHaveBeenCalled()
|
|
2030
|
+
expect(socketA.sendMessage).not.toHaveBeenCalled()
|
|
2031
|
+
})
|
|
2032
|
+
|
|
2033
|
+
it('[RC7] sessions added after close() are rejected and their sockets closed', () => {
|
|
2034
|
+
const { room } = makeRoom()
|
|
2035
|
+
room.close()
|
|
2036
|
+
|
|
2037
|
+
const late = makeSocket()
|
|
2038
|
+
room.handleNewSession({ sessionId: 'late', socket: late, meta: undefined, isReadonly: false })
|
|
2039
|
+
expect(late.close).toHaveBeenCalled()
|
|
2040
|
+
|
|
2041
|
+
const resumed = makeSocket()
|
|
2042
|
+
room.handleResumedSession({
|
|
2043
|
+
sessionId: 'resumed',
|
|
2044
|
+
socket: resumed,
|
|
2045
|
+
meta: undefined,
|
|
2046
|
+
isReadonly: false,
|
|
2047
|
+
serializedSchema: room.serializedSchema,
|
|
2048
|
+
presenceId: null,
|
|
2049
|
+
presenceRecord: null,
|
|
2050
|
+
requiresLegacyRejection: false,
|
|
2051
|
+
supportsStringAppend: true,
|
|
2052
|
+
})
|
|
2053
|
+
expect(resumed.close).toHaveBeenCalled()
|
|
2054
|
+
expect(room.sessions.size).toBe(0)
|
|
2055
|
+
})
|
|
2056
|
+
|
|
1952
2057
|
it('[RB4] a per-session migration failure during broadcast rejects only the affected session', () => {
|
|
1953
2058
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
1954
2059
|
try {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
TLSocketServerSentEvent,
|
|
9
9
|
getTlsyncProtocolVersion,
|
|
10
10
|
} from '../lib/protocol'
|
|
11
|
+
import { TLSocketRoom } from '../lib/TLSocketRoom'
|
|
11
12
|
import { TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason } from '../lib/TLSyncClient'
|
|
12
13
|
import { RoomSnapshot, TLRoomSocket, TLSyncRoom } from '../lib/TLSyncRoom'
|
|
13
14
|
|
|
@@ -354,6 +355,40 @@ describe('object store lane', () => {
|
|
|
354
355
|
const connectMsg = late.__messages.find((m: any) => m.type === 'connect') as any
|
|
355
356
|
expect(Object.keys(connectMsg.diff).sort()).toEqual([doc.id, note.id].sort())
|
|
356
357
|
})
|
|
358
|
+
|
|
359
|
+
it('[US1][US3] updateStore sees and can delete object-lane records', async () => {
|
|
360
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
361
|
+
const socketRoom = new TLSocketRoom<R, void>({
|
|
362
|
+
schema,
|
|
363
|
+
storage: new InMemorySyncStorage<R>({
|
|
364
|
+
snapshot: { documents: [], clock: 0, documentClock: 0, schema: schema.serialize() },
|
|
365
|
+
objectTypes: ['note'],
|
|
366
|
+
}),
|
|
367
|
+
objectTypes: ['note'],
|
|
368
|
+
})
|
|
369
|
+
disposables.push(() => socketRoom.close())
|
|
370
|
+
const doc = Doc.create({ title: 'canvas' })
|
|
371
|
+
const note = Note.create({ text: 'annotation' })
|
|
372
|
+
socketRoom.storage.transaction((txn) => {
|
|
373
|
+
txn.set(doc.id, doc)
|
|
374
|
+
txn.set(note.id, note)
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
// the update context reads both lanes, not just the document snapshot
|
|
378
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
379
|
+
await socketRoom.updateStore((store) => {
|
|
380
|
+
expect(store.get(note.id)).toEqual(note)
|
|
381
|
+
expect(
|
|
382
|
+
store
|
|
383
|
+
.getAll()
|
|
384
|
+
.map((r) => r.id)
|
|
385
|
+
.sort()
|
|
386
|
+
).toEqual([doc.id, note.id].sort())
|
|
387
|
+
store.delete(note.id)
|
|
388
|
+
})
|
|
389
|
+
expect(socketRoom.getRecord(note.id)).toBeUndefined()
|
|
390
|
+
expect(socketRoom.getRecord(doc.id)).toEqual(doc)
|
|
391
|
+
})
|
|
357
392
|
})
|
|
358
393
|
|
|
359
394
|
describe('connect message', () => {
|