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.
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +14 -0
- package/dist/BatchedOutgoingMessages.js +42 -0
- package/dist/BatchedOutgoingMessages.js.map +1 -0
- package/dist/index.js +37 -18
- package/dist/index.js.map +1 -1
- package/dist/serialization.js +22 -0
- package/dist/serialization.js.map +1 -0
- package/dist/tests/BatchedOutgoingMessages.test.js +82 -0
- package/dist/tests/BatchedOutgoingMessages.test.js.map +1 -0
- package/dist/tests/createWebSocketPeer.test.js +229 -53
- package/dist/tests/createWebSocketPeer.test.js.map +1 -1
- package/package.json +2 -2
- package/src/BatchedOutgoingMessages.ts +47 -0
- package/src/index.ts +63 -35
- package/src/serialization.ts +24 -0
- package/src/tests/BatchedOutgoingMessages.test.ts +114 -0
- package/src/tests/createWebSocketPeer.test.ts +329 -86
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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>(
|
|
140
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 100))
|
|
155
141
|
|
|
156
|
-
expect(mockWebSocket.send).
|
|
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
|
|
167
|
-
|
|
168
|
-
const { peer, mockWebSocket } = setup();
|
|
145
|
+
test("should close the websocket connection", () => {
|
|
146
|
+
const { mockWebSocket, peer } = setup();
|
|
169
147
|
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
278
|
+
const stream: SyncMessage[] = [];
|
|
204
279
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|