cojson-transport-ws 0.19.22 → 0.20.1
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 +1 -1
- package/CHANGELOG.md +18 -0
- package/dist/BatchedOutgoingMessages.d.ts +2 -0
- package/dist/BatchedOutgoingMessages.d.ts.map +1 -1
- package/dist/BatchedOutgoingMessages.js +18 -15
- package/dist/BatchedOutgoingMessages.js.map +1 -1
- package/dist/tests/createWebSocketPeer.test.js +158 -77
- package/dist/tests/createWebSocketPeer.test.js.map +1 -1
- package/package.json +2 -2
- package/src/BatchedOutgoingMessages.ts +23 -17
- package/src/tests/createWebSocketPeer.test.ts +213 -108
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# cojson-transport-nodejs-ws
|
|
2
2
|
|
|
3
|
+
## 0.20.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [03195eb]
|
|
8
|
+
- cojson@0.20.1
|
|
9
|
+
|
|
10
|
+
## 0.20.0
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [6b9368a]
|
|
15
|
+
- Updated dependencies [89332d5]
|
|
16
|
+
- Updated dependencies [f562a1f]
|
|
17
|
+
- Updated dependencies [b5ada4d]
|
|
18
|
+
- Updated dependencies [8934d8a]
|
|
19
|
+
- cojson@0.20.0
|
|
20
|
+
|
|
3
21
|
## 0.19.22
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
@@ -23,6 +23,8 @@ export declare class BatchedOutgoingMessages implements CojsonInternalTypes.Outg
|
|
|
23
23
|
push(msg: SyncMessage | DisconnectedError): void;
|
|
24
24
|
private processQueue;
|
|
25
25
|
private processMessage;
|
|
26
|
+
private serializeMessage;
|
|
27
|
+
private appendMessage;
|
|
26
28
|
private sendMessagesInBulk;
|
|
27
29
|
drain(): void;
|
|
28
30
|
setBatching(enabled: boolean): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BatchedOutgoingMessages.d.ts","sourceRoot":"","sources":["../src/BatchedOutgoingMessages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAsB,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EACL,KAAK,mBAAmB,EAIzB,MAAM,QAAQ,CAAC;AAChB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAW/C,qBAAa,uBACX,YAAW,mBAAmB,CAAC,mBAAmB;IAShD,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,QAAQ;IAEhB;;OAEG;IACH,OAAO,CAAC,IAAI,CAAC;IAbf,OAAO,CAAC,OAAO,
|
|
1
|
+
{"version":3,"file":"BatchedOutgoingMessages.d.ts","sourceRoot":"","sources":["../src/BatchedOutgoingMessages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAsB,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EACL,KAAK,mBAAmB,EAIzB,MAAM,QAAQ,CAAC;AAChB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAW/C,qBAAa,uBACX,YAAW,mBAAmB,CAAC,mBAAmB;IAShD,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,QAAQ;IAEhB;;OAEG;IACH,OAAO,CAAC,IAAI,CAAC;IAbf,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,KAAK,CAA4B;IACzC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,kBAAkB,CAAC;gBAGjB,SAAS,EAAE,YAAY,EACvB,QAAQ,EAAE,OAAO,EACzB,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC;IACtB;;OAEG;IACK,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,YAAA,EAC9C,KAAK,CAAC,EAAE,KAAK;IAsBf,IAAI,CAAC,GAAG,EAAE,WAAW,GAAG,iBAAiB;YAyB3B,YAAY;IA+B1B,OAAO,CAAC,cAAc;IA+BtB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,kBAAkB;IAO1B,KAAK;IAIL,WAAW,CAAC,OAAO,EAAE,OAAO;IAI5B,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI;IAI5B,KAAK;CAqBN"}
|
|
@@ -11,7 +11,7 @@ export class BatchedOutgoingMessages {
|
|
|
11
11
|
this.websocket = websocket;
|
|
12
12
|
this.batching = batching;
|
|
13
13
|
this.meta = meta;
|
|
14
|
-
this.backlog =
|
|
14
|
+
this.backlog = [];
|
|
15
15
|
this.processing = false;
|
|
16
16
|
this.closed = false;
|
|
17
17
|
this.closeListeners = new Set();
|
|
@@ -35,6 +35,11 @@ export class BatchedOutgoingMessages {
|
|
|
35
35
|
if (this.processing) {
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
+
if (isWebSocketOpen(this.websocket) &&
|
|
39
|
+
!hasWebSocketTooMuchBufferedData(this.websocket)) {
|
|
40
|
+
this.processMessage(msg, true);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
38
43
|
this.processQueue().catch((e) => {
|
|
39
44
|
logger.error("Error while processing sendMessage queue", { err: e });
|
|
40
45
|
});
|
|
@@ -42,9 +47,6 @@ export class BatchedOutgoingMessages {
|
|
|
42
47
|
async processQueue() {
|
|
43
48
|
const { websocket } = this;
|
|
44
49
|
this.processing = true;
|
|
45
|
-
// Delay the initiation of the queue processing to accumulate messages
|
|
46
|
-
// before sending them, in order to do prioritization and batching
|
|
47
|
-
await new Promise((resolve) => setTimeout(resolve, WEBSOCKET_CONFIG.OUTGOING_MESSAGES_CHUNK_DELAY));
|
|
48
50
|
let msg = this.queue.pull();
|
|
49
51
|
while (msg) {
|
|
50
52
|
if (this.closed) {
|
|
@@ -64,12 +66,12 @@ export class BatchedOutgoingMessages {
|
|
|
64
66
|
this.sendMessagesInBulk();
|
|
65
67
|
this.processing = false;
|
|
66
68
|
}
|
|
67
|
-
processMessage(msg) {
|
|
69
|
+
processMessage(msg, skipBatching = false) {
|
|
68
70
|
if (msg.action === "content") {
|
|
69
71
|
this.egressBytesCounter.add(getContentMessageSize(msg), this.meta);
|
|
70
72
|
}
|
|
71
|
-
const stringifiedMsg =
|
|
72
|
-
if (!this.batching) {
|
|
73
|
+
const stringifiedMsg = this.serializeMessage(msg);
|
|
74
|
+
if (!this.batching || skipBatching) {
|
|
73
75
|
this.websocket.send(stringifiedMsg);
|
|
74
76
|
return;
|
|
75
77
|
}
|
|
@@ -80,21 +82,22 @@ export class BatchedOutgoingMessages {
|
|
|
80
82
|
newBacklogSize > WEBSOCKET_CONFIG.MAX_OUTGOING_MESSAGES_CHUNK_BYTES) {
|
|
81
83
|
this.sendMessagesInBulk();
|
|
82
84
|
}
|
|
83
|
-
|
|
84
|
-
this.backlog += `\n${stringifiedMsg}`;
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
this.backlog = stringifiedMsg;
|
|
88
|
-
}
|
|
85
|
+
this.appendMessage(stringifiedMsg);
|
|
89
86
|
// If message itself exceeds the chunk size, send it immediately
|
|
90
87
|
if (msgSize >= WEBSOCKET_CONFIG.MAX_OUTGOING_MESSAGES_CHUNK_BYTES) {
|
|
91
88
|
this.sendMessagesInBulk();
|
|
92
89
|
}
|
|
93
90
|
}
|
|
91
|
+
serializeMessage(msg) {
|
|
92
|
+
return JSON.stringify(msg);
|
|
93
|
+
}
|
|
94
|
+
appendMessage(msg) {
|
|
95
|
+
this.backlog.push(msg);
|
|
96
|
+
}
|
|
94
97
|
sendMessagesInBulk() {
|
|
95
98
|
if (this.backlog.length > 0 && isWebSocketOpen(this.websocket)) {
|
|
96
|
-
this.websocket.send(this.backlog);
|
|
97
|
-
this.backlog =
|
|
99
|
+
this.websocket.send(this.backlog.join("\n"));
|
|
100
|
+
this.backlog.length = 0;
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
drain() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BatchedOutgoingMessages.js","sourceRoot":"","sources":["../src/BatchedOutgoingMessages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAGpE,OAAO,EAEL,yBAAyB,EACzB,eAAe,EACf,MAAM,GACP,MAAM,QAAQ,CAAC;AAEhB,OAAO,EACL,+BAA+B,EAC/B,eAAe,EACf,8BAA8B,EAC9B,oBAAoB,GACrB,MAAM,YAAY,CAAC;AAEpB,MAAM,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,GAClE,eAAe,CAAC;AAElB,MAAM,OAAO,uBAAuB;IASlC,YACU,SAAuB,EACvB,QAAiB,EACzB,QAAsB;IACtB;;OAEG;IACK,IAAsC,EAC9C,KAAa;QAPL,cAAS,GAAT,SAAS,CAAc;QACvB,aAAQ,GAAR,QAAQ,CAAS;QAKjB,SAAI,GAAJ,IAAI,CAAkC;QAbxC,YAAO,
|
|
1
|
+
{"version":3,"file":"BatchedOutgoingMessages.js","sourceRoot":"","sources":["../src/BatchedOutgoingMessages.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,36 +3,65 @@ import { afterEach, describe, expect, test, vi } from "vitest";
|
|
|
3
3
|
import { createWebSocketPeer, } from "../createWebSocketPeer.js";
|
|
4
4
|
import { BUFFER_LIMIT, BUFFER_LIMIT_POLLING_INTERVAL } from "../utils.js";
|
|
5
5
|
import { createTestMetricReader, tearDownTestMetricReader } from "./utils.js";
|
|
6
|
-
const { CO_VALUE_PRIORITY, WEBSOCKET_CONFIG
|
|
6
|
+
const { CO_VALUE_PRIORITY, WEBSOCKET_CONFIG } = cojsonInternals;
|
|
7
7
|
const { MAX_OUTGOING_MESSAGES_CHUNK_BYTES } = WEBSOCKET_CONFIG;
|
|
8
8
|
function setup(opts = {}) {
|
|
9
|
+
const { initialReadyState = 1, ...peerOpts } = opts;
|
|
9
10
|
const listeners = new Map();
|
|
10
11
|
const mockWebSocket = {
|
|
11
|
-
readyState:
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
readyState: initialReadyState,
|
|
13
|
+
bufferedAmount: 0,
|
|
14
|
+
addEventListener: vi
|
|
15
|
+
.fn()
|
|
16
|
+
.mockImplementation((type, callback, options) => {
|
|
17
|
+
if (!listeners.has(type)) {
|
|
18
|
+
listeners.set(type, new Set());
|
|
19
|
+
}
|
|
20
|
+
listeners.get(type).add({ callback, once: options?.once });
|
|
14
21
|
}),
|
|
15
|
-
removeEventListener: vi
|
|
16
|
-
|
|
22
|
+
removeEventListener: vi
|
|
23
|
+
.fn()
|
|
24
|
+
.mockImplementation((type, callback) => {
|
|
25
|
+
const set = listeners.get(type);
|
|
26
|
+
if (set) {
|
|
27
|
+
for (const entry of set) {
|
|
28
|
+
if (entry.callback === callback) {
|
|
29
|
+
set.delete(entry);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
17
34
|
}),
|
|
18
35
|
close: vi.fn(),
|
|
19
36
|
send: vi.fn(),
|
|
20
37
|
};
|
|
38
|
+
const triggerEvent = (type, event) => {
|
|
39
|
+
const set = listeners.get(type);
|
|
40
|
+
if (set) {
|
|
41
|
+
const toRemove = [];
|
|
42
|
+
for (const entry of set) {
|
|
43
|
+
entry.callback(event ?? new MessageEvent(type));
|
|
44
|
+
if (entry.once) {
|
|
45
|
+
toRemove.push(entry);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const entry of toRemove) {
|
|
49
|
+
set.delete(entry);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
21
53
|
const peer = createWebSocketPeer({
|
|
22
54
|
id: "test-peer",
|
|
23
55
|
websocket: mockWebSocket,
|
|
24
56
|
role: "client",
|
|
25
57
|
batchingByDefault: true,
|
|
26
|
-
...
|
|
58
|
+
...peerOpts,
|
|
27
59
|
});
|
|
28
|
-
return { mockWebSocket, peer, listeners };
|
|
60
|
+
return { mockWebSocket, peer, listeners, triggerEvent };
|
|
29
61
|
}
|
|
30
62
|
function serializeMessages(messages) {
|
|
31
63
|
return messages.map((msg) => JSON.stringify(msg)).join("\n");
|
|
32
64
|
}
|
|
33
|
-
afterEach(() => {
|
|
34
|
-
setOutgoingMessagesChunkDelay(5);
|
|
35
|
-
});
|
|
36
65
|
describe("createWebSocketPeer", () => {
|
|
37
66
|
test("should create a peer with correct properties", () => {
|
|
38
67
|
const { peer } = setup();
|
|
@@ -42,20 +71,18 @@ describe("createWebSocketPeer", () => {
|
|
|
42
71
|
expect(peer).toHaveProperty("role", "client");
|
|
43
72
|
});
|
|
44
73
|
test("should handle disconnection", async () => {
|
|
45
|
-
const {
|
|
74
|
+
const { triggerEvent, peer } = setup();
|
|
46
75
|
const onMessageSpy = vi.fn();
|
|
47
76
|
peer.incoming.onMessage(onMessageSpy);
|
|
48
|
-
|
|
49
|
-
closeHandler?.(new MessageEvent("close"));
|
|
77
|
+
triggerEvent("close");
|
|
50
78
|
expect(onMessageSpy).toHaveBeenCalledWith("Disconnected");
|
|
51
79
|
});
|
|
52
80
|
test("should handle ping timeout", async () => {
|
|
53
81
|
vi.useFakeTimers();
|
|
54
|
-
const {
|
|
82
|
+
const { triggerEvent, peer } = setup();
|
|
55
83
|
const onMessageSpy = vi.fn();
|
|
56
84
|
peer.incoming.onMessage(onMessageSpy);
|
|
57
|
-
|
|
58
|
-
messageHandler?.(new MessageEvent("message", { data: "{}" }));
|
|
85
|
+
triggerEvent("message", new MessageEvent("message", { data: "{}" }));
|
|
59
86
|
await vi.advanceTimersByTimeAsync(10000);
|
|
60
87
|
expect(onMessageSpy).toHaveBeenCalledWith("Disconnected");
|
|
61
88
|
vi.useRealTimers();
|
|
@@ -107,8 +134,7 @@ describe("createWebSocketPeer", () => {
|
|
|
107
134
|
});
|
|
108
135
|
test("should call onSuccess handler after receiving first message", () => {
|
|
109
136
|
const onSuccess = vi.fn();
|
|
110
|
-
const {
|
|
111
|
-
const messageHandler = listeners.get("message");
|
|
137
|
+
const { triggerEvent } = setup({ onSuccess });
|
|
112
138
|
const message = {
|
|
113
139
|
action: "known",
|
|
114
140
|
id: "co_ztest",
|
|
@@ -116,17 +142,16 @@ describe("createWebSocketPeer", () => {
|
|
|
116
142
|
sessions: {},
|
|
117
143
|
};
|
|
118
144
|
// First message should trigger onSuccess
|
|
119
|
-
|
|
145
|
+
triggerEvent("message", new MessageEvent("message", { data: JSON.stringify(message) }));
|
|
120
146
|
expect(onSuccess).toHaveBeenCalledTimes(1);
|
|
121
147
|
// Subsequent messages should not trigger onSuccess again
|
|
122
|
-
|
|
148
|
+
triggerEvent("message", new MessageEvent("message", { data: JSON.stringify(message) }));
|
|
123
149
|
expect(onSuccess).toHaveBeenCalledTimes(1);
|
|
124
150
|
});
|
|
125
151
|
describe("batchingByDefault = true", () => {
|
|
126
|
-
test("should batch outgoing messages", async () => {
|
|
127
|
-
const { peer, mockWebSocket } = setup(
|
|
128
|
-
|
|
129
|
-
mockWebSocket.readyState = 0;
|
|
152
|
+
test("should batch outgoing messages when socket is not ready", async () => {
|
|
153
|
+
const { peer, mockWebSocket, triggerEvent } = setup({
|
|
154
|
+
initialReadyState: 0,
|
|
130
155
|
});
|
|
131
156
|
const message1 = {
|
|
132
157
|
action: "known",
|
|
@@ -142,40 +167,75 @@ describe("createWebSocketPeer", () => {
|
|
|
142
167
|
};
|
|
143
168
|
void peer.outgoing.push(message1);
|
|
144
169
|
void peer.outgoing.push(message2);
|
|
170
|
+
// Simulate socket becoming ready
|
|
171
|
+
mockWebSocket.readyState = 1;
|
|
172
|
+
triggerEvent("open");
|
|
145
173
|
await waitFor(() => {
|
|
146
174
|
expect(mockWebSocket.send).toHaveBeenCalled();
|
|
147
175
|
});
|
|
148
176
|
expect(mockWebSocket.send).toHaveBeenCalledWith([message1, message2].map((msg) => JSON.stringify(msg)).join("\n"));
|
|
149
177
|
});
|
|
150
|
-
test("should
|
|
178
|
+
test("should send messages immediately when socket is ready", async () => {
|
|
151
179
|
const { peer, mockWebSocket } = setup();
|
|
152
|
-
mockWebSocket.send.mockImplementation(() => {
|
|
153
|
-
mockWebSocket.readyState = 0;
|
|
154
|
-
});
|
|
155
180
|
const message1 = {
|
|
181
|
+
action: "known",
|
|
182
|
+
id: "co_ztest",
|
|
183
|
+
header: false,
|
|
184
|
+
sessions: {},
|
|
185
|
+
};
|
|
186
|
+
const message2 = {
|
|
187
|
+
action: "content",
|
|
188
|
+
id: "co_zlow",
|
|
189
|
+
new: {},
|
|
190
|
+
priority: 6,
|
|
191
|
+
};
|
|
192
|
+
void peer.outgoing.push(message1);
|
|
193
|
+
await waitFor(() => {
|
|
194
|
+
expect(mockWebSocket.send).toHaveBeenCalledTimes(1);
|
|
195
|
+
});
|
|
196
|
+
expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify(message1));
|
|
197
|
+
void peer.outgoing.push(message2);
|
|
198
|
+
await waitFor(() => {
|
|
199
|
+
expect(mockWebSocket.send).toHaveBeenCalledTimes(2);
|
|
200
|
+
});
|
|
201
|
+
expect(mockWebSocket.send).toHaveBeenNthCalledWith(2, JSON.stringify(message2));
|
|
202
|
+
});
|
|
203
|
+
test("should sort remaining queued messages by priority after first message", async () => {
|
|
204
|
+
const { peer, mockWebSocket, triggerEvent } = setup({
|
|
205
|
+
initialReadyState: 0,
|
|
206
|
+
});
|
|
207
|
+
const lowPriority = {
|
|
156
208
|
action: "content",
|
|
157
209
|
id: "co_zlow",
|
|
158
210
|
new: {},
|
|
159
211
|
priority: CO_VALUE_PRIORITY.LOW,
|
|
160
212
|
};
|
|
161
|
-
const
|
|
213
|
+
const highPriority = {
|
|
162
214
|
action: "content",
|
|
163
215
|
id: "co_zhigh",
|
|
164
216
|
new: {},
|
|
165
217
|
priority: CO_VALUE_PRIORITY.HIGH,
|
|
166
218
|
};
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
void peer.outgoing.push(
|
|
219
|
+
// First message is pulled immediately before socket check,
|
|
220
|
+
// so it will be first regardless of priority
|
|
221
|
+
void peer.outgoing.push(lowPriority);
|
|
222
|
+
// Subsequent messages are queued and sorted by priority
|
|
223
|
+
void peer.outgoing.push(lowPriority);
|
|
224
|
+
void peer.outgoing.push(highPriority);
|
|
225
|
+
// Simulate socket becoming ready
|
|
226
|
+
mockWebSocket.readyState = 1;
|
|
227
|
+
triggerEvent("open");
|
|
170
228
|
await waitFor(() => {
|
|
171
229
|
expect(mockWebSocket.send).toHaveBeenCalled();
|
|
172
230
|
});
|
|
173
|
-
|
|
231
|
+
// First message (lowPriority) comes first as it was pulled before waiting,
|
|
232
|
+
// then remaining messages are sorted: highPriority before lowPriority
|
|
233
|
+
expect(mockWebSocket.send).toHaveBeenCalledWith([lowPriority, highPriority, lowPriority]
|
|
174
234
|
.map((msg) => JSON.stringify(msg))
|
|
175
235
|
.join("\n"));
|
|
176
236
|
});
|
|
177
|
-
test("should send
|
|
178
|
-
const { peer, mockWebSocket } = setup();
|
|
237
|
+
test("should send remaining queued messages when close is called", async () => {
|
|
238
|
+
const { peer, mockWebSocket } = setup({ initialReadyState: 0 });
|
|
179
239
|
const message1 = {
|
|
180
240
|
action: "known",
|
|
181
241
|
id: "co_ztest",
|
|
@@ -188,13 +248,28 @@ describe("createWebSocketPeer", () => {
|
|
|
188
248
|
new: {},
|
|
189
249
|
priority: 6,
|
|
190
250
|
};
|
|
251
|
+
const message3 = {
|
|
252
|
+
action: "content",
|
|
253
|
+
id: "co_zmedium",
|
|
254
|
+
new: {},
|
|
255
|
+
priority: 3,
|
|
256
|
+
};
|
|
191
257
|
void peer.outgoing.push(message1);
|
|
192
258
|
void peer.outgoing.push(message2);
|
|
259
|
+
void peer.outgoing.push(message3);
|
|
260
|
+
// Set socket to open before close to allow sending
|
|
261
|
+
mockWebSocket.readyState = 1;
|
|
193
262
|
peer.outgoing.close();
|
|
194
|
-
|
|
263
|
+
// First message was already pulled by processQueue (waiting for socket),
|
|
264
|
+
// close() processes and sends remaining messages from queue sorted by priority
|
|
265
|
+
expect(mockWebSocket.send).toHaveBeenCalledWith([message3, message2].map((msg) => JSON.stringify(msg)).join("\n"));
|
|
195
266
|
});
|
|
196
267
|
test("should limit the chunk size to MAX_OUTGOING_MESSAGES_CHUNK_SIZE", async () => {
|
|
268
|
+
// This test verifies chunking works when socket is already ready
|
|
197
269
|
const { peer, mockWebSocket } = setup();
|
|
270
|
+
mockWebSocket.send.mockImplementation((value) => {
|
|
271
|
+
mockWebSocket.bufferedAmount += value.length;
|
|
272
|
+
});
|
|
198
273
|
const message1 = {
|
|
199
274
|
action: "known",
|
|
200
275
|
id: "co_ztest",
|
|
@@ -207,21 +282,22 @@ describe("createWebSocketPeer", () => {
|
|
|
207
282
|
new: {},
|
|
208
283
|
priority: 6,
|
|
209
284
|
};
|
|
210
|
-
|
|
211
|
-
while (
|
|
212
|
-
|
|
213
|
-
stream.push(message1);
|
|
214
|
-
void peer.outgoing.push(message1);
|
|
285
|
+
// Fill up the buffer
|
|
286
|
+
while (mockWebSocket.bufferedAmount < BUFFER_LIMIT) {
|
|
287
|
+
peer.outgoing.push(message1);
|
|
215
288
|
}
|
|
289
|
+
mockWebSocket.send.mockClear();
|
|
216
290
|
void peer.outgoing.push(message2);
|
|
291
|
+
expect(mockWebSocket.send).not.toHaveBeenCalled();
|
|
292
|
+
// Reset the buffer, make it look like we have sent the messages
|
|
293
|
+
mockWebSocket.bufferedAmount = 0;
|
|
217
294
|
await waitFor(() => {
|
|
218
|
-
expect(mockWebSocket.send).
|
|
295
|
+
expect(mockWebSocket.send).toHaveBeenCalled();
|
|
219
296
|
});
|
|
220
|
-
expect(mockWebSocket.send).toHaveBeenCalledWith(serializeMessages(stream));
|
|
221
|
-
expect(mockWebSocket.send).toHaveBeenNthCalledWith(2, JSON.stringify(message2));
|
|
222
297
|
});
|
|
223
298
|
test("should send accumulated messages before a large message", async () => {
|
|
224
299
|
const { peer, mockWebSocket } = setup();
|
|
300
|
+
mockWebSocket.bufferedAmount = BUFFER_LIMIT + 1;
|
|
225
301
|
const smallMessage = {
|
|
226
302
|
action: "known",
|
|
227
303
|
id: "co_z_small",
|
|
@@ -239,6 +315,7 @@ describe("createWebSocketPeer", () => {
|
|
|
239
315
|
};
|
|
240
316
|
void peer.outgoing.push(smallMessage);
|
|
241
317
|
void peer.outgoing.push(largeMessage);
|
|
318
|
+
mockWebSocket.bufferedAmount = 0;
|
|
242
319
|
await waitFor(() => {
|
|
243
320
|
expect(mockWebSocket.send).toHaveBeenCalledTimes(2);
|
|
244
321
|
});
|
|
@@ -249,9 +326,6 @@ describe("createWebSocketPeer", () => {
|
|
|
249
326
|
test("should wait for the buffer to be under BUFFER_LIMIT before sending more messages", async () => {
|
|
250
327
|
vi.useFakeTimers();
|
|
251
328
|
const { peer, mockWebSocket } = setup();
|
|
252
|
-
mockWebSocket.send.mockImplementation(() => {
|
|
253
|
-
mockWebSocket.bufferedAmount = BUFFER_LIMIT + 1;
|
|
254
|
-
});
|
|
255
329
|
const message1 = {
|
|
256
330
|
action: "known",
|
|
257
331
|
id: "co_ztest",
|
|
@@ -264,18 +338,18 @@ describe("createWebSocketPeer", () => {
|
|
|
264
338
|
new: {},
|
|
265
339
|
priority: 6,
|
|
266
340
|
};
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
stream.push(message1);
|
|
271
|
-
void peer.outgoing.push(message1);
|
|
272
|
-
}
|
|
341
|
+
// Start with buffer full so messages go through the queue
|
|
342
|
+
mockWebSocket.bufferedAmount = BUFFER_LIMIT + 1;
|
|
343
|
+
void peer.outgoing.push(message1);
|
|
273
344
|
void peer.outgoing.push(message2);
|
|
274
|
-
await vi.advanceTimersByTimeAsync(
|
|
275
|
-
|
|
345
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
346
|
+
// No messages sent yet because buffer is full
|
|
347
|
+
expect(mockWebSocket.send).not.toHaveBeenCalled();
|
|
348
|
+
// Clear the buffer
|
|
276
349
|
mockWebSocket.bufferedAmount = 0;
|
|
277
350
|
await vi.advanceTimersByTimeAsync(BUFFER_LIMIT_POLLING_INTERVAL + 1);
|
|
278
|
-
|
|
351
|
+
// Both messages are batched together
|
|
352
|
+
expect(mockWebSocket.send).toHaveBeenCalledWith([message1, message2].map((msg) => JSON.stringify(msg)).join("\n"));
|
|
279
353
|
vi.useRealTimers();
|
|
280
354
|
});
|
|
281
355
|
});
|
|
@@ -295,16 +369,20 @@ describe("createWebSocketPeer", () => {
|
|
|
295
369
|
priority: 6,
|
|
296
370
|
};
|
|
297
371
|
void peer.outgoing.push(message1);
|
|
372
|
+
await waitFor(() => {
|
|
373
|
+
expect(mockWebSocket.send).toHaveBeenCalledTimes(1);
|
|
374
|
+
});
|
|
298
375
|
void peer.outgoing.push(message2);
|
|
299
376
|
await waitFor(() => {
|
|
300
|
-
expect(mockWebSocket.send).
|
|
377
|
+
expect(mockWebSocket.send).toHaveBeenCalledTimes(2);
|
|
301
378
|
});
|
|
302
379
|
expect(mockWebSocket.send).toHaveBeenNthCalledWith(1, JSON.stringify(message1));
|
|
303
380
|
expect(mockWebSocket.send).toHaveBeenNthCalledWith(2, JSON.stringify(message2));
|
|
304
381
|
});
|
|
305
|
-
test("should start batching outgoing messages when
|
|
306
|
-
const { peer, mockWebSocket,
|
|
382
|
+
test("should start batching outgoing messages when receiving a batched message", async () => {
|
|
383
|
+
const { peer, mockWebSocket, triggerEvent } = setup({
|
|
307
384
|
batchingByDefault: false,
|
|
385
|
+
initialReadyState: 0,
|
|
308
386
|
});
|
|
309
387
|
const message1 = {
|
|
310
388
|
action: "known",
|
|
@@ -312,8 +390,7 @@ describe("createWebSocketPeer", () => {
|
|
|
312
390
|
header: false,
|
|
313
391
|
sessions: {},
|
|
314
392
|
};
|
|
315
|
-
|
|
316
|
-
messageHandler?.(new MessageEvent("message", {
|
|
393
|
+
triggerEvent("message", new MessageEvent("message", {
|
|
317
394
|
data: Array.from({ length: 5 }, () => message1)
|
|
318
395
|
.map((msg) => JSON.stringify(msg))
|
|
319
396
|
.join("\n"),
|
|
@@ -326,13 +403,16 @@ describe("createWebSocketPeer", () => {
|
|
|
326
403
|
};
|
|
327
404
|
void peer.outgoing.push(message1);
|
|
328
405
|
void peer.outgoing.push(message2);
|
|
406
|
+
// Simulate socket becoming ready
|
|
407
|
+
mockWebSocket.readyState = 1;
|
|
408
|
+
triggerEvent("open");
|
|
329
409
|
await waitFor(() => {
|
|
330
410
|
expect(mockWebSocket.send).toHaveBeenCalled();
|
|
331
411
|
});
|
|
332
412
|
expect(mockWebSocket.send).toHaveBeenCalledWith([message1, message2].map((msg) => JSON.stringify(msg)).join("\n"));
|
|
333
413
|
});
|
|
334
|
-
test("should not start batching outgoing messages when
|
|
335
|
-
const { peer, mockWebSocket,
|
|
414
|
+
test("should not start batching outgoing messages when receiving non-batched message", async () => {
|
|
415
|
+
const { peer, mockWebSocket, triggerEvent } = setup({
|
|
336
416
|
batchingByDefault: false,
|
|
337
417
|
});
|
|
338
418
|
const message1 = {
|
|
@@ -341,8 +421,7 @@ describe("createWebSocketPeer", () => {
|
|
|
341
421
|
header: false,
|
|
342
422
|
sessions: {},
|
|
343
423
|
};
|
|
344
|
-
|
|
345
|
-
messageHandler?.(new MessageEvent("message", {
|
|
424
|
+
triggerEvent("message", new MessageEvent("message", {
|
|
346
425
|
data: JSON.stringify(message1),
|
|
347
426
|
}));
|
|
348
427
|
const message2 = {
|
|
@@ -352,9 +431,12 @@ describe("createWebSocketPeer", () => {
|
|
|
352
431
|
priority: 6,
|
|
353
432
|
};
|
|
354
433
|
void peer.outgoing.push(message1);
|
|
434
|
+
await waitFor(() => {
|
|
435
|
+
expect(mockWebSocket.send).toHaveBeenCalledTimes(1);
|
|
436
|
+
});
|
|
355
437
|
void peer.outgoing.push(message2);
|
|
356
438
|
await waitFor(() => {
|
|
357
|
-
expect(mockWebSocket.send).
|
|
439
|
+
expect(mockWebSocket.send).toHaveBeenCalledTimes(2);
|
|
358
440
|
});
|
|
359
441
|
expect(mockWebSocket.send).toHaveBeenNthCalledWith(1, JSON.stringify(message1));
|
|
360
442
|
expect(mockWebSocket.send).toHaveBeenNthCalledWith(2, JSON.stringify(message2));
|
|
@@ -376,12 +458,11 @@ describe("createWebSocketPeer", () => {
|
|
|
376
458
|
});
|
|
377
459
|
test("should correctly measure incoming ingress", async () => {
|
|
378
460
|
const metricReader = createTestMetricReader();
|
|
379
|
-
const {
|
|
461
|
+
const { triggerEvent } = setup({
|
|
380
462
|
meta: { label: "value" },
|
|
381
463
|
});
|
|
382
|
-
const messageHandler = listeners.get("message");
|
|
383
464
|
const encryptedChanges = "Hello, world!";
|
|
384
|
-
|
|
465
|
+
triggerEvent("message", new MessageEvent("message", {
|
|
385
466
|
data: JSON.stringify({
|
|
386
467
|
action: "content",
|
|
387
468
|
new: {
|
|
@@ -403,7 +484,7 @@ describe("createWebSocketPeer", () => {
|
|
|
403
484
|
label: "value",
|
|
404
485
|
})).toBe(encryptedChanges.length);
|
|
405
486
|
const trustingChanges = "Jazz is great!";
|
|
406
|
-
|
|
487
|
+
triggerEvent("message", new MessageEvent("message", {
|
|
407
488
|
data: JSON.stringify({
|
|
408
489
|
action: "content",
|
|
409
490
|
new: {
|
|
@@ -423,9 +504,8 @@ describe("createWebSocketPeer", () => {
|
|
|
423
504
|
})).toBe(encryptedChanges.length + trustingChanges.length);
|
|
424
505
|
});
|
|
425
506
|
test("should drain the outgoing queue on websocket close so pulled equals pushed", async () => {
|
|
426
|
-
setOutgoingMessagesChunkDelay(500);
|
|
427
507
|
const metricReader = createTestMetricReader();
|
|
428
|
-
const { peer,
|
|
508
|
+
const { peer, triggerEvent } = setup({ initialReadyState: 0 });
|
|
429
509
|
const high = {
|
|
430
510
|
action: "content",
|
|
431
511
|
id: "co_zhigh",
|
|
@@ -459,10 +539,12 @@ describe("createWebSocketPeer", () => {
|
|
|
459
539
|
priority: CO_VALUE_PRIORITY.LOW,
|
|
460
540
|
peerRole: "client",
|
|
461
541
|
})).toBe(1);
|
|
542
|
+
// First message is already pulled by processQueue (waiting for socket open),
|
|
543
|
+
// so pulled count for that priority is already 1
|
|
462
544
|
expect(await metricReader.getMetricValue("jazz.messagequeue.outgoing.pulled", {
|
|
463
545
|
priority: CO_VALUE_PRIORITY.HIGH,
|
|
464
546
|
peerRole: "client",
|
|
465
|
-
})).toBe(
|
|
547
|
+
})).toBe(1);
|
|
466
548
|
expect(await metricReader.getMetricValue("jazz.messagequeue.outgoing.pulled", {
|
|
467
549
|
priority: CO_VALUE_PRIORITY.MEDIUM,
|
|
468
550
|
peerRole: "client",
|
|
@@ -471,8 +553,8 @@ describe("createWebSocketPeer", () => {
|
|
|
471
553
|
priority: CO_VALUE_PRIORITY.LOW,
|
|
472
554
|
peerRole: "client",
|
|
473
555
|
})).toBe(0);
|
|
474
|
-
|
|
475
|
-
|
|
556
|
+
triggerEvent("close");
|
|
557
|
+
// After close, drain() is called which pulls all remaining messages
|
|
476
558
|
expect(await metricReader.getMetricValue("jazz.messagequeue.outgoing.pulled", {
|
|
477
559
|
priority: CO_VALUE_PRIORITY.HIGH,
|
|
478
560
|
peerRole: "client",
|
|
@@ -485,7 +567,6 @@ describe("createWebSocketPeer", () => {
|
|
|
485
567
|
priority: CO_VALUE_PRIORITY.LOW,
|
|
486
568
|
peerRole: "client",
|
|
487
569
|
})).toBe(1);
|
|
488
|
-
vi.useRealTimers();
|
|
489
570
|
});
|
|
490
571
|
});
|
|
491
572
|
});
|