@tldraw/sync-core 5.5.0-canary.8e8b2aa09b36 → 5.5.0-canary.8ec56b5e7aff

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 CHANGED
@@ -61,7 +61,7 @@ var import_TLSyncRoom = require("./lib/TLSyncRoom");
61
61
  var import_TLSyncStorage = require("./lib/TLSyncStorage");
62
62
  (0, import_utils.registerTldrawLibraryVersion)(
63
63
  "@tldraw/sync-core",
64
- "5.5.0-canary.8e8b2aa09b36",
64
+ "5.5.0-canary.8ec56b5e7aff",
65
65
  "cjs"
66
66
  );
67
67
  //# sourceMappingURL=index.js.map
@@ -36,7 +36,7 @@ import {
36
36
  } from "./lib/TLSyncStorage.mjs";
37
37
  registerTldrawLibraryVersion(
38
38
  "@tldraw/sync-core",
39
- "5.5.0-canary.8e8b2aa09b36",
39
+ "5.5.0-canary.8ec56b5e7aff",
40
40
  "esm"
41
41
  );
42
42
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/sync-core",
3
3
  "description": "tldraw infinite canvas SDK (multiplayer sync).",
4
- "version": "5.5.0-canary.8e8b2aa09b36",
4
+ "version": "5.5.0-canary.8ec56b5e7aff",
5
5
  "author": {
6
6
  "name": "tldraw GB Ltd.",
7
7
  "email": "hello@tldraw.com"
@@ -56,17 +56,17 @@
56
56
  "@types/uuid-readable": "^0.0.3",
57
57
  "react": "^19.2.1",
58
58
  "react-dom": "^19.2.1",
59
- "tldraw": "5.5.0-canary.8e8b2aa09b36",
59
+ "tldraw": "5.5.0-canary.8ec56b5e7aff",
60
60
  "typescript": "^7.0.2",
61
61
  "uuid-by-string": "^4.0.0",
62
62
  "uuid-readable": "^0.0.2",
63
- "vitest": "^4.1.7"
63
+ "vitest": "^5.0.0"
64
64
  },
65
65
  "dependencies": {
66
- "@tldraw/state": "5.5.0-canary.8e8b2aa09b36",
67
- "@tldraw/store": "5.5.0-canary.8e8b2aa09b36",
68
- "@tldraw/tlschema": "5.5.0-canary.8e8b2aa09b36",
69
- "@tldraw/utils": "5.5.0-canary.8e8b2aa09b36",
66
+ "@tldraw/state": "5.5.0-canary.8ec56b5e7aff",
67
+ "@tldraw/store": "5.5.0-canary.8ec56b5e7aff",
68
+ "@tldraw/tlschema": "5.5.0-canary.8ec56b5e7aff",
69
+ "@tldraw/utils": "5.5.0-canary.8ec56b5e7aff",
70
70
  "nanoevents": "^7.0.1",
71
71
  "ws": "^8.18.0"
72
72
  },
@@ -73,24 +73,38 @@ describe('ClientWebSocketAdapter', () => {
73
73
  connectedServerSocket = socket
74
74
  })
75
75
 
76
+ // Bound per test on an OS-assigned port. A fixed port raced against the previous test's
77
+ // server, which closes asynchronously, and intermittently failed with EADDRINUSE.
78
+ let port: number
79
+
76
80
  let consoleWarnSpy: ReturnType<typeof vi.spyOn>
77
- beforeEach(() => {
81
+ beforeEach(async () => {
78
82
  consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
79
- adapter = new ClientWebSocketAdapter(() => 'ws://localhost:2233')
80
- wsServer = new WebSocketServer({ port: 2233 })
83
+ wsServer = new WebSocketServer({ port: 0 })
84
+ // Attach before the adapter exists: the server is already listening by the time the
85
+ // adapter is constructed, so a handler added afterwards can miss the first connection.
81
86
  wsServer.on('connection', connectMock as any)
87
+ await new Promise<void>((resolve, reject) => {
88
+ wsServer.once('listening', resolve)
89
+ wsServer.once('error', reject)
90
+ })
91
+ port = (wsServer.address() as { port: number }).port
92
+ adapter = new ClientWebSocketAdapter(() => `ws://localhost:${port}`)
82
93
  })
83
94
 
84
95
  afterEach(() => {
85
96
  consoleWarnSpy.mockRestore()
86
97
  adapter.close()
98
+ // Not awaited: the next test binds its own OS-assigned port, so a server still closing
99
+ // here can't collide with it. Awaiting would deliver in-flight messages to the socket
100
+ // adapter.close() just orphaned.
87
101
  wsServer.close()
88
102
  connectMock.mockClear()
89
103
  })
90
104
 
91
105
  describe('connection and initial state (CW1, CW2)', () => {
92
106
  it('[CW2] starts with connectionStatus offline while the internal status is initial', () => {
93
- const newAdapter = new ClientWebSocketAdapter(() => 'ws://localhost:2233')
107
+ const newAdapter = new ClientWebSocketAdapter(() => `ws://localhost:${port}`)
94
108
  try {
95
109
  expect(newAdapter._connectionStatus.get()).toBe('initial')
96
110
  expect(newAdapter.connectionStatus).toBe('offline')
@@ -113,7 +127,7 @@ describe('ClientWebSocketAdapter', () => {
113
127
  let uriCallCount = 0
114
128
  const dynamicAdapter = new ClientWebSocketAdapter(() => {
115
129
  uriCallCount++
116
- return `ws://localhost:2233?attempt=${uriCallCount}`
130
+ return `ws://localhost:${port}?attempt=${uriCallCount}`
117
131
  })
118
132
  try {
119
133
  await waitFor(() => dynamicAdapter._ws?.readyState === WebSocket.OPEN)
@@ -143,7 +157,7 @@ describe('ClientWebSocketAdapter', () => {
143
157
  // no socket can be created until the promise resolves
144
158
  expect(asyncAdapter._ws).toBeNull()
145
159
 
146
- resolveUri!('ws://localhost:2233')
160
+ resolveUri!(`ws://localhost:${port}`)
147
161
 
148
162
  await waitFor(() => asyncAdapter._ws?.readyState === WebSocket.OPEN)
149
163
  expect(asyncAdapter.connectionStatus).toBe('online')
@@ -301,7 +315,7 @@ describe('ClientWebSocketAdapter', () => {
301
315
 
302
316
  describe('URI conversion (CW1)', () => {
303
317
  it('[CW1] converts http URIs to ws and connects', async () => {
304
- const httpAdapter = new ClientWebSocketAdapter(() => 'http://localhost:2233')
318
+ const httpAdapter = new ClientWebSocketAdapter(() => `http://localhost:${port}`)
305
319
  try {
306
320
  await waitFor(() => httpAdapter._ws?.readyState === WebSocket.OPEN)
307
321
  expect(httpAdapter._ws!.url).toMatch(/^ws:\/\//)
@@ -312,7 +326,7 @@ describe('ClientWebSocketAdapter', () => {
312
326
  })
313
327
 
314
328
  it('[CW1] converts https URIs to wss', async () => {
315
- const httpsAdapter = new ClientWebSocketAdapter(() => 'https://localhost:2233')
329
+ const httpsAdapter = new ClientWebSocketAdapter(() => `https://localhost:${port}`)
316
330
  try {
317
331
  await waitFor(() => httpsAdapter._ws !== null)
318
332
  expect(httpsAdapter._ws!.url).toMatch(/^wss:\/\//)
@@ -529,7 +543,7 @@ describe('ClientWebSocketAdapter', () => {
529
543
  })
530
544
 
531
545
  it('[CW9][RM5] closing immediately does not start another getUri call', async () => {
532
- const getUri = vi.fn(() => 'ws://localhost:2233')
546
+ const getUri = vi.fn(() => `ws://localhost:${port}`)
533
547
  const testAdapter = new ClientWebSocketAdapter(getUri)
534
548
  testAdapter.close()
535
549
  const callsAtClose = getUri.mock.calls.length
@@ -544,7 +558,7 @@ describe('ClientWebSocketAdapter', () => {
544
558
  let uriCallCount = 0
545
559
  const testAdapter = new ClientWebSocketAdapter(() => {
546
560
  uriCallCount++
547
- return 'ws://localhost:2233'
561
+ return `ws://localhost:${port}`
548
562
  })
549
563
  const onStatusChange = vi.fn()
550
564
  testAdapter.onStatusChange(onStatusChange)
@@ -606,17 +620,26 @@ describe('ReconnectManager', () => {
606
620
  connectedServerSocket = socket
607
621
  })
608
622
 
623
+ // See the note on the other suite's port: bound per test on an OS-assigned port.
624
+ let port: number
625
+
609
626
  let consoleWarnSpy: ReturnType<typeof vi.spyOn>
610
- beforeEach(() => {
627
+ beforeEach(async () => {
611
628
  consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
612
- adapter = new ClientWebSocketAdapter(() => 'ws://localhost:2234')
613
- wsServer = new WebSocketServer({ port: 2234 })
629
+ wsServer = new WebSocketServer({ port: 0 })
614
630
  wsServer.on('connection', connectMock as any)
631
+ await new Promise<void>((resolve, reject) => {
632
+ wsServer.once('listening', resolve)
633
+ wsServer.once('error', reject)
634
+ })
635
+ port = (wsServer.address() as { port: number }).port
636
+ adapter = new ClientWebSocketAdapter(() => `ws://localhost:${port}`)
615
637
  })
616
638
 
617
639
  afterEach(() => {
618
640
  consoleWarnSpy.mockRestore()
619
641
  adapter.close()
642
+ // Not awaited - see the note on the other suite's teardown.
620
643
  wsServer.close()
621
644
  connectMock.mockClear()
622
645
  })
@@ -631,7 +654,7 @@ describe('ReconnectManager', () => {
631
654
  })
632
655
 
633
656
  it('[RM1] reconnection delays back off exponentially, bounded by the active delays', () => {
634
- const testAdapter = new ClientWebSocketAdapter(() => 'ws://localhost:2234')
657
+ const testAdapter = new ClientWebSocketAdapter(() => `ws://localhost:${port}`)
635
658
  try {
636
659
  const manager = testAdapter._reconnectManager
637
660
  // make every disconnected() call take the "attempt now" branch
@@ -656,7 +679,7 @@ describe('ReconnectManager', () => {
656
679
 
657
680
  it('[RM1] uses the inactive delay bounds when the tab is hidden', () => {
658
681
  const hiddenMock = vi.spyOn(document, 'hidden', 'get').mockReturnValue(true)
659
- const testAdapter = new ClientWebSocketAdapter(() => 'ws://localhost:2234')
682
+ const testAdapter = new ClientWebSocketAdapter(() => `ws://localhost:${port}`)
660
683
  try {
661
684
  const manager = testAdapter._reconnectManager
662
685
  ;(manager as any).lastAttemptStart = Date.now() - 1_000_000
@@ -828,7 +851,7 @@ describe('ReconnectManager', () => {
828
851
  if (failure === 'throws') throw error
829
852
  return Promise.reject(error)
830
853
  }
831
- return 'ws://localhost:2234'
854
+ return `ws://localhost:${port}`
832
855
  })
833
856
  try {
834
857
  await waitFor(() => testAdapter._ws?.readyState === WebSocket.OPEN)
@@ -843,7 +866,7 @@ describe('ReconnectManager', () => {
843
866
  )
844
867
 
845
868
  it('[RM5] close cancels timers and removes the reconnect event listeners', () => {
846
- const testAdapter = new ClientWebSocketAdapter(() => 'ws://localhost:2234')
869
+ const testAdapter = new ClientWebSocketAdapter(() => `ws://localhost:${port}`)
847
870
  const manager = testAdapter._reconnectManager
848
871
 
849
872
  testAdapter.close()
@@ -862,6 +885,10 @@ describe('ReconnectManager', () => {
862
885
  })
863
886
 
864
887
  describe('URI failure boundaries', () => {
888
+ // These tests never open a real socket - the adapter is closed first, or the socket is a
889
+ // mock - so the URI only has to parse. No server, hence no port to bind.
890
+ const UNUSED_URI = 'ws://localhost:1'
891
+
865
892
  it.each(['resolves', 'rejects'])('ignores getUri that %s after close', async (outcome) => {
866
893
  const uri = promiseWithResolve<string>()
867
894
  const getUri = vi.fn(() => uri)
@@ -872,7 +899,7 @@ describe('URI failure boundaries', () => {
872
899
  try {
873
900
  await Promise.resolve()
874
901
  testAdapter.close()
875
- if (outcome === 'resolves') uri.resolve('ws://localhost:2234')
902
+ if (outcome === 'resolves') uri.resolve(UNUSED_URI)
876
903
  else uri.reject(new Error('token endpoint unavailable'))
877
904
  await vi.advanceTimersByTimeAsync(INACTIVE_MAX_DELAY)
878
905
 
@@ -890,7 +917,7 @@ describe('URI failure boundaries', () => {
890
917
  it.each([
891
918
  { uri: 'not a URL', openSocket: false, error: 'Invalid URL' },
892
919
  {
893
- uri: 'ws://localhost:2234',
920
+ uri: UNUSED_URI,
894
921
  openSocket: true,
895
922
  error: 'There should be no connection attempts while already connected',
896
923
  },