cojson-transport-ws 0.8.3 → 0.8.6

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.
@@ -3,20 +3,14 @@ import {
3
3
  BUFFER_LIMIT,
4
4
  BUFFER_LIMIT_POLLING_INTERVAL,
5
5
  createWebSocketPeer,
6
+ CreateWebSocketPeerOpts,
6
7
  } from "../index.js";
7
- import { AnyWebSocket, PingMsg } from "../types.js";
8
+ import { AnyWebSocket } from "../types.js";
8
9
  import { SyncMessage } from "cojson";
9
10
  import { Channel } from "cojson/src/streamUtils.ts";
11
+ import { MAX_OUTGOING_MESSAGES_CHUNK_BYTES } from "../BatchedOutgoingMessages.js";
10
12
 
11
- const g: typeof globalThis & {
12
- jazzPings?: {
13
- received: number;
14
- sent: number;
15
- dc: string;
16
- }[];
17
- } = globalThis;
18
-
19
- function setup() {
13
+ function setup(opts: Partial<CreateWebSocketPeerOpts> = {}) {
20
14
  const listeners = new Map<string, (event: MessageEvent) => void>();
21
15
 
22
16
  const mockWebSocket = {
@@ -32,11 +26,17 @@ function setup() {
32
26
  id: "test-peer",
33
27
  websocket: mockWebSocket,
34
28
  role: "client",
29
+ batchingByDefault: true,
30
+ ...opts,
35
31
  });
36
32
 
37
33
  return { mockWebSocket, peer, listeners };
38
34
  }
39
35
 
36
+ function serializeMessages(messages: SyncMessage[]) {
37
+ return messages.map((msg) => JSON.stringify(msg)).join("\n");
38
+ }
39
+
40
40
  describe("createWebSocketPeer", () => {
41
41
  test("should create a peer with correct properties", () => {
42
42
  const { peer } = setup();
@@ -48,30 +48,6 @@ describe("createWebSocketPeer", () => {
48
48
  expect(peer).toHaveProperty("crashOnClose", false);
49
49
  });
50
50
 
51
- test("should handle ping messages", async () => {
52
- const { listeners } = setup();
53
-
54
- const pingMessage: PingMsg = {
55
- type: "ping",
56
- time: Date.now(),
57
- dc: "test-dc",
58
- };
59
- const messageEvent = new MessageEvent("message", {
60
- data: JSON.stringify(pingMessage),
61
- });
62
-
63
- const messageHandler = listeners.get("message");
64
-
65
- messageHandler?.(messageEvent);
66
-
67
- // Check if the ping was recorded in the global jazzPings array
68
- expect(g.jazzPings?.length).toBeGreaterThan(0);
69
- expect(g.jazzPings?.at(-1)).toMatchObject({
70
- sent: pingMessage.time,
71
- dc: pingMessage.dc,
72
- });
73
- });
74
-
75
51
  test("should handle disconnection", async () => {
76
52
  expect.assertions(1);
77
53
 
@@ -120,11 +96,12 @@ describe("createWebSocketPeer", () => {
120
96
  };
121
97
  const promise = peer.outgoing.push(testMessage);
122
98
 
123
- await new Promise<void>(queueMicrotask);
99
+ await waitFor(() => {
100
+ expect(mockWebSocket.send).toHaveBeenCalledWith(
101
+ JSON.stringify(testMessage),
102
+ );
103
+ });
124
104
 
125
- expect(mockWebSocket.send).toHaveBeenCalledWith(
126
- JSON.stringify(testMessage),
127
- );
128
105
  await expect(promise).resolves.toBeUndefined();
129
106
  });
130
107
 
@@ -141,6 +118,7 @@ describe("createWebSocketPeer", () => {
141
118
  header: false,
142
119
  sessions: {},
143
120
  };
121
+
144
122
  const message2: SyncMessage = {
145
123
  action: "content",
146
124
  id: "co_zlow",
@@ -149,72 +127,337 @@ describe("createWebSocketPeer", () => {
149
127
  };
150
128
 
151
129
  void peer.outgoing.push(message1);
130
+
131
+ await waitFor(() => {
132
+ expect(mockWebSocket.send).toHaveBeenCalled();
133
+ });
134
+
135
+ expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify(message1));
136
+
137
+ mockWebSocket.send.mockClear();
152
138
  void peer.outgoing.push(message2);
153
139
 
154
- await new Promise<void>(queueMicrotask);
140
+ await new Promise<void>((resolve) => setTimeout(resolve, 100))
155
141
 
156
- expect(mockWebSocket.send).toHaveBeenNthCalledWith(
157
- 1,
158
- JSON.stringify(message1),
159
- );
160
- expect(mockWebSocket.send).not.toHaveBeenNthCalledWith(
161
- 2,
162
- JSON.stringify(message2),
163
- );
142
+ expect(mockWebSocket.send).not.toHaveBeenCalled();
164
143
  });
165
144
 
166
- test("should wait for the buffer to be under BUFFER_LIMIT before sending more messages", async () => {
167
- vi.useFakeTimers();
168
- const { peer, mockWebSocket } = setup();
145
+ test("should close the websocket connection", () => {
146
+ const { mockWebSocket, peer } = setup();
169
147
 
170
- mockWebSocket.send.mockImplementation(() => {
171
- mockWebSocket.bufferedAmount = BUFFER_LIMIT + 1;
148
+ peer.outgoing.close();
149
+
150
+ expect(mockWebSocket.close).toHaveBeenCalled();
151
+ });
152
+
153
+ describe("batchingByDefault = true", () => {
154
+ test("should batch outgoing messages", async () => {
155
+ const { peer, mockWebSocket } = setup();
156
+
157
+ mockWebSocket.send.mockImplementation(() => {
158
+ mockWebSocket.readyState = 0;
159
+ });
160
+
161
+ const message1: SyncMessage = {
162
+ action: "known",
163
+ id: "co_ztest",
164
+ header: false,
165
+ sessions: {},
166
+ };
167
+
168
+ const message2: SyncMessage = {
169
+ action: "content",
170
+ id: "co_zlow",
171
+ new: {},
172
+ priority: 1,
173
+ };
174
+
175
+ void peer.outgoing.push(message1);
176
+ void peer.outgoing.push(message2);
177
+
178
+ await waitFor(() => {
179
+ expect(mockWebSocket.send).toHaveBeenCalled();
180
+ });
181
+
182
+ expect(mockWebSocket.send).toHaveBeenCalledWith(
183
+ [message1, message2]
184
+ .map((msg) => JSON.stringify(msg))
185
+ .join("\n"),
186
+ );
172
187
  });
173
188
 
174
- const message1: SyncMessage = {
175
- action: "known",
176
- id: "co_ztest",
177
- header: false,
178
- sessions: {},
179
- };
180
- const message2: SyncMessage = {
181
- action: "content",
182
- id: "co_zlow",
183
- new: {},
184
- priority: 1,
185
- };
189
+ test("should send all the pending messages when the websocket is closed", async () => {
190
+ const { peer, mockWebSocket } = setup();
191
+
192
+ const message1: SyncMessage = {
193
+ action: "known",
194
+ id: "co_ztest",
195
+ header: false,
196
+ sessions: {},
197
+ };
198
+
199
+ const message2: SyncMessage = {
200
+ action: "content",
201
+ id: "co_zlow",
202
+ new: {},
203
+ priority: 1,
204
+ };
205
+
206
+ void peer.outgoing.push(message1);
207
+ void peer.outgoing.push(message2);
208
+
209
+ peer.outgoing.close();
210
+
211
+ expect(mockWebSocket.send).toHaveBeenCalledWith(
212
+ [message1, message2]
213
+ .map((msg) => JSON.stringify(msg))
214
+ .join("\n"),
215
+ );
216
+ });
186
217
 
187
- void peer.outgoing.push(message1);
188
- void peer.outgoing.push(message2);
218
+ test("should limit the chunk size to MAX_OUTGOING_MESSAGES_CHUNK_SIZE", async () => {
219
+ const { peer, mockWebSocket } = setup();
220
+
221
+ const message1: SyncMessage = {
222
+ action: "known",
223
+ id: "co_ztest",
224
+ header: false,
225
+ sessions: {},
226
+ };
227
+ const message2: SyncMessage = {
228
+ action: "content",
229
+ id: "co_zlow",
230
+ new: {},
231
+ priority: 1,
232
+ };
233
+
234
+ const stream: SyncMessage[] = [];
235
+
236
+ while (serializeMessages(stream.concat(message1)).length < MAX_OUTGOING_MESSAGES_CHUNK_BYTES) {
237
+ stream.push(message1);
238
+ void peer.outgoing.push(message1);
239
+ }
240
+
241
+ void peer.outgoing.push(message2);
242
+
243
+ await waitFor(() => {
244
+ expect(mockWebSocket.send).toHaveBeenCalledTimes(2);
245
+ });
246
+
247
+ expect(mockWebSocket.send).toHaveBeenCalledWith(
248
+ serializeMessages(stream),
249
+ );
250
+
251
+ expect(mockWebSocket.send).toHaveBeenNthCalledWith(
252
+ 2,
253
+ JSON.stringify(message2),
254
+ );
255
+ });
189
256
 
190
- await new Promise<void>(queueMicrotask);
257
+ test("should wait for the buffer to be under BUFFER_LIMIT before sending more messages", async () => {
258
+ vi.useFakeTimers();
259
+ const { peer, mockWebSocket } = setup();
191
260
 
192
- expect(mockWebSocket.send).toHaveBeenNthCalledWith(
193
- 1,
194
- JSON.stringify(message1),
195
- );
196
- expect(mockWebSocket.send).not.toHaveBeenNthCalledWith(
197
- 2,
198
- JSON.stringify(message2),
199
- );
261
+ mockWebSocket.send.mockImplementation(() => {
262
+ mockWebSocket.bufferedAmount = BUFFER_LIMIT + 1;
263
+ });
200
264
 
201
- mockWebSocket.bufferedAmount = 0;
265
+ const message1: SyncMessage = {
266
+ action: "known",
267
+ id: "co_ztest",
268
+ header: false,
269
+ sessions: {},
270
+ };
271
+ const message2: SyncMessage = {
272
+ action: "content",
273
+ id: "co_zlow",
274
+ new: {},
275
+ priority: 1,
276
+ };
202
277
 
203
- await vi.advanceTimersByTimeAsync(BUFFER_LIMIT_POLLING_INTERVAL + 1);
278
+ const stream: SyncMessage[] = [];
204
279
 
205
- expect(mockWebSocket.send).toHaveBeenNthCalledWith(
206
- 2,
207
- JSON.stringify(message2),
208
- );
280
+ while (serializeMessages(stream.concat(message1)).length < MAX_OUTGOING_MESSAGES_CHUNK_BYTES) {
281
+ stream.push(message1);
282
+ void peer.outgoing.push(message1);
283
+ }
209
284
 
210
- vi.useRealTimers();
285
+ void peer.outgoing.push(message2);
286
+
287
+ await vi.advanceTimersByTimeAsync(100);
288
+
289
+ expect(mockWebSocket.send).toHaveBeenCalledWith(
290
+ serializeMessages(stream),
291
+ );
292
+
293
+ mockWebSocket.bufferedAmount = 0;
294
+
295
+ await vi.advanceTimersByTimeAsync(
296
+ BUFFER_LIMIT_POLLING_INTERVAL + 1,
297
+ );
298
+
299
+ expect(mockWebSocket.send).toHaveBeenNthCalledWith(
300
+ 2,
301
+ JSON.stringify(message2),
302
+ );
303
+
304
+ vi.useRealTimers();
305
+ });
211
306
  });
212
307
 
213
- test("should close the websocket connection", () => {
214
- const { mockWebSocket, peer } = setup();
308
+ describe("batchingByDefault = false", () => {
309
+ test("should not batch outgoing messages", async () => {
310
+ const { peer, mockWebSocket } = setup({ batchingByDefault: false });
311
+
312
+ const message1: SyncMessage = {
313
+ action: "known",
314
+ id: "co_ztest",
315
+ header: false,
316
+ sessions: {},
317
+ };
318
+
319
+ const message2: SyncMessage = {
320
+ action: "content",
321
+ id: "co_zlow",
322
+ new: {},
323
+ priority: 1,
324
+ };
325
+
326
+ void peer.outgoing.push(message1);
327
+ void peer.outgoing.push(message2);
328
+
329
+ await waitFor(() => {
330
+ expect(mockWebSocket.send).toHaveBeenCalled();
331
+ });
332
+
333
+ expect(mockWebSocket.send).toHaveBeenNthCalledWith(
334
+ 1,
335
+ JSON.stringify(message1),
336
+ );
337
+ expect(mockWebSocket.send).toHaveBeenNthCalledWith(
338
+ 2,
339
+ JSON.stringify(message2),
340
+ );
341
+ });
215
342
 
216
- peer.outgoing.close();
343
+ test("should start batching outgoing messages when reiceving a batched message", async () => {
344
+ const { peer, mockWebSocket, listeners } = setup({
345
+ batchingByDefault: false,
346
+ });
347
+
348
+ const message1: SyncMessage = {
349
+ action: "known",
350
+ id: "co_ztest",
351
+ header: false,
352
+ sessions: {},
353
+ };
354
+
355
+ const messageHandler = listeners.get("message");
356
+
357
+ messageHandler?.(
358
+ new MessageEvent("message", {
359
+ data: Array.from(
360
+ { length: 5 },
361
+ () => message1,
362
+ )
363
+ .map((msg) => JSON.stringify(msg))
364
+ .join("\n"),
365
+ }),
366
+ );
367
+
368
+
369
+ const message2: SyncMessage = {
370
+ action: "content",
371
+ id: "co_zlow",
372
+ new: {},
373
+ priority: 1,
374
+ };
375
+
376
+ void peer.outgoing.push(message1);
377
+ void peer.outgoing.push(message2);
378
+
379
+ await waitFor(() => {
380
+ expect(mockWebSocket.send).toHaveBeenCalled();
381
+ });
382
+
383
+ expect(mockWebSocket.send).toHaveBeenCalledWith(
384
+ [message1, message2]
385
+ .map((msg) => JSON.stringify(msg))
386
+ .join("\n"),
387
+ );
388
+ });
217
389
 
218
- expect(mockWebSocket.close).toHaveBeenCalled();
390
+ test("should not start batching outgoing messages when reiceving non-batched message", async () => {
391
+ const { peer, mockWebSocket, listeners } = setup({
392
+ batchingByDefault: false,
393
+ });
394
+
395
+ const message1: SyncMessage = {
396
+ action: "known",
397
+ id: "co_ztest",
398
+ header: false,
399
+ sessions: {},
400
+ };
401
+
402
+ const messageHandler = listeners.get("message");
403
+
404
+ messageHandler?.(
405
+ new MessageEvent("message", {
406
+ data: JSON.stringify(message1),
407
+ }),
408
+ );
409
+
410
+
411
+ const message2: SyncMessage = {
412
+ action: "content",
413
+ id: "co_zlow",
414
+ new: {},
415
+ priority: 1,
416
+ };
417
+
418
+ void peer.outgoing.push(message1);
419
+ void peer.outgoing.push(message2);
420
+
421
+ await waitFor(() => {
422
+ expect(mockWebSocket.send).toHaveBeenCalled();
423
+ });
424
+
425
+ expect(mockWebSocket.send).toHaveBeenNthCalledWith(
426
+ 1,
427
+ JSON.stringify(message1),
428
+ );
429
+ expect(mockWebSocket.send).toHaveBeenNthCalledWith(
430
+ 2,
431
+ JSON.stringify(message2),
432
+ );
433
+ });
219
434
  });
220
435
  });
436
+
437
+ function waitFor(callback: () => boolean | void) {
438
+ return new Promise<void>((resolve, reject) => {
439
+ const checkPassed = () => {
440
+ try {
441
+ return { ok: callback(), error: null };
442
+ } catch (error) {
443
+ return { ok: false, error };
444
+ }
445
+ };
446
+
447
+ let retries = 0;
448
+
449
+ const interval = setInterval(() => {
450
+ const { ok, error } = checkPassed();
451
+
452
+ if (ok !== false) {
453
+ clearInterval(interval);
454
+ resolve();
455
+ }
456
+
457
+ if (++retries > 10) {
458
+ clearInterval(interval);
459
+ reject(error);
460
+ }
461
+ }, 100);
462
+ });
463
+ }