@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.
Files changed (34) hide show
  1. package/dist-cjs/index.js +1 -1
  2. package/dist-cjs/lib/ClientWebSocketAdapter.js +24 -12
  3. package/dist-cjs/lib/ClientWebSocketAdapter.js.map +2 -2
  4. package/dist-cjs/lib/SQLiteSyncStorage.js +15 -7
  5. package/dist-cjs/lib/SQLiteSyncStorage.js.map +2 -2
  6. package/dist-cjs/lib/TLSocketRoom.js +4 -7
  7. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  8. package/dist-cjs/lib/TLSyncRoom.js +30 -3
  9. package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
  10. package/dist-cjs/lib/chunk.js +10 -1
  11. package/dist-cjs/lib/chunk.js.map +2 -2
  12. package/dist-esm/index.mjs +1 -1
  13. package/dist-esm/lib/ClientWebSocketAdapter.mjs +24 -12
  14. package/dist-esm/lib/ClientWebSocketAdapter.mjs.map +2 -2
  15. package/dist-esm/lib/SQLiteSyncStorage.mjs +15 -7
  16. package/dist-esm/lib/SQLiteSyncStorage.mjs.map +2 -2
  17. package/dist-esm/lib/TLSocketRoom.mjs +4 -7
  18. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  19. package/dist-esm/lib/TLSyncRoom.mjs +30 -3
  20. package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
  21. package/dist-esm/lib/chunk.mjs +10 -1
  22. package/dist-esm/lib/chunk.mjs.map +2 -2
  23. package/package.json +6 -6
  24. package/src/lib/ClientWebSocketAdapter.test.ts +122 -2
  25. package/src/lib/ClientWebSocketAdapter.ts +30 -14
  26. package/src/lib/SQLiteSyncStorage.test.ts +60 -1
  27. package/src/lib/SQLiteSyncStorage.ts +22 -14
  28. package/src/lib/TLSocketRoom.ts +8 -7
  29. package/src/lib/TLSyncRoom.ts +39 -3
  30. package/src/lib/chunk.test.ts +34 -0
  31. package/src/lib/chunk.ts +20 -1
  32. package/src/test/TLSocketRoom.test.ts +39 -0
  33. package/src/test/TLSyncRoom.test.ts +105 -0
  34. 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', () => {