cojson-transport-ws 0.15.7 → 0.15.9

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 (39) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +14 -0
  3. package/dist/BatchedOutgoingMessages.d.ts +17 -6
  4. package/dist/BatchedOutgoingMessages.d.ts.map +1 -1
  5. package/dist/BatchedOutgoingMessages.js +77 -14
  6. package/dist/BatchedOutgoingMessages.js.map +1 -1
  7. package/dist/createWebSocketPeer.d.ts +0 -2
  8. package/dist/createWebSocketPeer.d.ts.map +1 -1
  9. package/dist/createWebSocketPeer.js +27 -84
  10. package/dist/createWebSocketPeer.js.map +1 -1
  11. package/dist/tests/createWebSocketPeer.test.js +64 -22
  12. package/dist/tests/createWebSocketPeer.test.js.map +1 -1
  13. package/dist/tests/syncServer.d.ts +1 -0
  14. package/dist/tests/syncServer.d.ts.map +1 -1
  15. package/dist/tests/syncServer.js +1 -0
  16. package/dist/tests/syncServer.js.map +1 -1
  17. package/dist/tests/webSocket.integration.test.d.ts +2 -0
  18. package/dist/tests/webSocket.integration.test.d.ts.map +1 -0
  19. package/dist/tests/{integration.test.js → webSocket.integration.test.js} +28 -2
  20. package/dist/tests/webSocket.integration.test.js.map +1 -0
  21. package/dist/utils.d.ts +8 -0
  22. package/dist/utils.d.ts.map +1 -0
  23. package/dist/utils.js +24 -0
  24. package/dist/utils.js.map +1 -0
  25. package/package.json +2 -2
  26. package/src/BatchedOutgoingMessages.ts +124 -16
  27. package/src/createWebSocketPeer.ts +33 -118
  28. package/src/tests/createWebSocketPeer.test.ts +87 -37
  29. package/src/tests/syncServer.ts +1 -0
  30. package/src/tests/{integration.test.ts → webSocket.integration.test.ts} +37 -2
  31. package/src/utils.ts +30 -0
  32. package/dist/tests/BatchedOutgoingMessages.test.d.ts +0 -2
  33. package/dist/tests/BatchedOutgoingMessages.test.d.ts.map +0 -1
  34. package/dist/tests/BatchedOutgoingMessages.test.js +0 -112
  35. package/dist/tests/BatchedOutgoingMessages.test.js.map +0 -1
  36. package/dist/tests/integration.test.d.ts +0 -2
  37. package/dist/tests/integration.test.d.ts.map +0 -1
  38. package/dist/tests/integration.test.js.map +0 -1
  39. package/src/tests/BatchedOutgoingMessages.test.ts +0 -146
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=webSocket.integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webSocket.integration.test.d.ts","sourceRoot":"","sources":["../../src/tests/webSocket.integration.test.ts"],"names":[],"mappings":""}
@@ -67,7 +67,10 @@ describe("WebSocket Peer Integration", () => {
67
67
  // Verify data reached the server
68
68
  const serverNode = server.localNode;
69
69
  const serverMap = await serverNode.load(map.id);
70
- expect(serverMap.get("testKey")).toBe("testValue");
70
+ if (serverMap === "unavailable") {
71
+ throw new Error("Server map is unavailable");
72
+ }
73
+ expect(serverMap.get("testKey")?.toString()).toBe("testValue");
71
74
  });
72
75
  test("should handle disconnection and cleanup", async () => {
73
76
  const clientAgent = crypto.newRandomAgentSecret();
@@ -113,5 +116,28 @@ describe("WebSocket Peer Integration", () => {
113
116
  });
114
117
  expect(ws.readyState).toBe(WebSocket.CLOSED);
115
118
  });
119
+ test("calling terminate on the server should close the connection", async () => {
120
+ const ws = new WebSocket(syncServerUrl);
121
+ let disconnectCalled = false;
122
+ createWebSocketPeer({
123
+ id: "test-client",
124
+ websocket: ws,
125
+ role: "server",
126
+ onClose: () => {
127
+ disconnectCalled = true;
128
+ },
129
+ });
130
+ await waitFor(() => {
131
+ expect(server.wss.clients.size).toBe(1);
132
+ });
133
+ const peerOnServer = server.localNode.syncManager.getPeers()[0];
134
+ for (const client of server.wss.clients) {
135
+ client.terminate();
136
+ }
137
+ await waitFor(() => {
138
+ expect(disconnectCalled).toBe(true);
139
+ });
140
+ expect(peerOnServer?.closed).toBe(true);
141
+ });
116
142
  });
117
- //# sourceMappingURL=integration.test.js.map
143
+ //# sourceMappingURL=webSocket.integration.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webSocket.integration.test.js","sourceRoot":"","sources":["../../src/tests/webSocket.integration.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { AnyWebSocket } from "./types.js";
2
+ export declare const BUFFER_LIMIT = 100000;
3
+ export declare const BUFFER_LIMIT_POLLING_INTERVAL = 10;
4
+ export declare function isWebSocketOpen(websocket: AnyWebSocket): boolean;
5
+ export declare function hasWebSocketTooMuchBufferedData(websocket: AnyWebSocket): boolean;
6
+ export declare function waitForWebSocketOpen(websocket: AnyWebSocket): Promise<void>;
7
+ export declare function waitForWebSocketBufferedAmount(websocket: AnyWebSocket): Promise<void>;
8
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,eAAO,MAAM,YAAY,SAAU,CAAC;AACpC,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAEhD,wBAAgB,eAAe,CAAC,SAAS,EAAE,YAAY,WAEtD;AAED,wBAAgB,+BAA+B,CAAC,SAAS,EAAE,YAAY,WAEtE;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,YAAY,iBAQ3D;AAED,wBAAsB,8BAA8B,CAAC,SAAS,EAAE,YAAY,iBAM3E"}
package/dist/utils.js ADDED
@@ -0,0 +1,24 @@
1
+ export const BUFFER_LIMIT = 100000;
2
+ export const BUFFER_LIMIT_POLLING_INTERVAL = 10;
3
+ export function isWebSocketOpen(websocket) {
4
+ return websocket.readyState === 1;
5
+ }
6
+ export function hasWebSocketTooMuchBufferedData(websocket) {
7
+ return websocket.bufferedAmount > BUFFER_LIMIT && isWebSocketOpen(websocket);
8
+ }
9
+ export function waitForWebSocketOpen(websocket) {
10
+ return new Promise((resolve) => {
11
+ if (websocket.readyState === 1) {
12
+ resolve();
13
+ }
14
+ else {
15
+ websocket.addEventListener("open", () => resolve(), { once: true });
16
+ }
17
+ });
18
+ }
19
+ export async function waitForWebSocketBufferedAmount(websocket) {
20
+ while (hasWebSocketTooMuchBufferedData(websocket)) {
21
+ await new Promise((resolve) => setTimeout(resolve, BUFFER_LIMIT_POLLING_INTERVAL));
22
+ }
23
+ }
24
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,YAAY,GAAG,MAAO,CAAC;AACpC,MAAM,CAAC,MAAM,6BAA6B,GAAG,EAAE,CAAC;AAEhD,MAAM,UAAU,eAAe,CAAC,SAAuB;IACrD,OAAO,SAAS,CAAC,UAAU,KAAK,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,+BAA+B,CAAC,SAAuB;IACrE,OAAO,SAAS,CAAC,cAAc,GAAG,YAAY,IAAI,eAAe,CAAC,SAAS,CAAC,CAAC;AAC/E,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,SAAuB;IAC1D,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QACnC,IAAI,SAAS,CAAC,UAAU,KAAK,CAAC,EAAE,CAAC;YAC/B,OAAO,EAAE,CAAC;QACZ,CAAC;aAAM,CAAC;YACN,SAAS,CAAC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACtE,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAAC,SAAuB;IAC1E,OAAO,+BAA+B,CAAC,SAAS,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAClC,UAAU,CAAC,OAAO,EAAE,6BAA6B,CAAC,CACnD,CAAC;IACJ,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "cojson-transport-ws",
3
3
  "type": "module",
4
- "version": "0.15.7",
4
+ "version": "0.15.9",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
- "cojson": "0.15.7"
9
+ "cojson": "0.15.9"
10
10
  },
11
11
  "devDependencies": {
12
12
  "typescript": "5.6.2",
@@ -1,21 +1,106 @@
1
- import type { SyncMessage } from "cojson";
1
+ import type { DisconnectedError, SyncMessage } from "cojson";
2
+ import type { Peer } from "cojson";
3
+ import {
4
+ type CojsonInternalTypes,
5
+ PriorityBasedMessageQueue,
6
+ cojsonInternals,
7
+ logger,
8
+ } from "cojson";
2
9
  import { addMessageToBacklog } from "./serialization.js";
10
+ import type { AnyWebSocket } from "./types.js";
11
+ import {
12
+ hasWebSocketTooMuchBufferedData,
13
+ isWebSocketOpen,
14
+ waitForWebSocketBufferedAmount,
15
+ waitForWebSocketOpen,
16
+ } from "./utils.js";
17
+
18
+ const { CO_VALUE_PRIORITY } = cojsonInternals;
3
19
 
4
20
  export const MAX_OUTGOING_MESSAGES_CHUNK_BYTES = 25_000;
5
21
 
6
- export class BatchedOutgoingMessages {
22
+ export class BatchedOutgoingMessages
23
+ implements CojsonInternalTypes.OutgoingPeerChannel
24
+ {
7
25
  private backlog = "";
8
- private timeout: ReturnType<typeof setTimeout> | null = null;
26
+ private queue: PriorityBasedMessageQueue;
27
+ private processing = false;
28
+ private closed = false;
9
29
 
10
- constructor(private send: (messages: string) => void) {}
30
+ constructor(
31
+ private websocket: AnyWebSocket,
32
+ private batching: boolean,
33
+ peerRole: Peer["role"],
34
+ ) {
35
+ this.queue = new PriorityBasedMessageQueue(
36
+ CO_VALUE_PRIORITY.HIGH,
37
+ "outgoing",
38
+ {
39
+ peerRole: peerRole,
40
+ },
41
+ );
42
+ }
11
43
 
12
- push(msg: SyncMessage) {
13
- const payload = addMessageToBacklog(this.backlog, msg);
44
+ push(msg: SyncMessage | DisconnectedError) {
45
+ if (msg === "Disconnected") {
46
+ this.close();
47
+ return;
48
+ }
49
+
50
+ this.queue.push(msg);
14
51
 
15
- if (this.timeout) {
16
- clearTimeout(this.timeout);
52
+ if (this.processing) {
53
+ return;
17
54
  }
18
55
 
56
+ this.processQueue().catch((e) => {
57
+ logger.error("Error while processing sendMessage queue", { err: e });
58
+ });
59
+ }
60
+
61
+ private async processQueue() {
62
+ const { websocket } = this;
63
+
64
+ this.processing = true;
65
+
66
+ // Delay the initiation of the queue processing to accumulate messages
67
+ // before sending them, in order to do prioritization and batching
68
+ await new Promise<void>((resolve) => setTimeout(resolve, 5));
69
+
70
+ let msg = this.queue.pull();
71
+
72
+ while (msg) {
73
+ if (this.closed) {
74
+ return;
75
+ }
76
+
77
+ if (!isWebSocketOpen(websocket)) {
78
+ await waitForWebSocketOpen(websocket);
79
+ }
80
+
81
+ if (hasWebSocketTooMuchBufferedData(websocket)) {
82
+ await waitForWebSocketBufferedAmount(websocket);
83
+ }
84
+
85
+ if (isWebSocketOpen(websocket)) {
86
+ this.processMessage(msg);
87
+
88
+ msg = this.queue.pull();
89
+ }
90
+ }
91
+
92
+ this.sendMessagesInBulk();
93
+ this.processing = false;
94
+ }
95
+
96
+ processMessage(msg: SyncMessage) {
97
+ if (!this.batching) {
98
+ this.websocket.send(JSON.stringify(msg));
99
+ return;
100
+ }
101
+
102
+ const payload = addMessageToBacklog(this.backlog, msg);
103
+
19
104
  const maxChunkSizeReached =
20
105
  payload.length >= MAX_OUTGOING_MESSAGES_CHUNK_BYTES;
21
106
  const backlogExists = this.backlog.length > 0;
@@ -23,26 +108,49 @@ export class BatchedOutgoingMessages {
23
108
  if (maxChunkSizeReached && backlogExists) {
24
109
  this.sendMessagesInBulk();
25
110
  this.backlog = addMessageToBacklog("", msg);
26
- this.timeout = setTimeout(() => {
27
- this.sendMessagesInBulk();
28
- }, 0);
29
111
  } else if (maxChunkSizeReached) {
30
112
  this.backlog = payload;
31
113
  this.sendMessagesInBulk();
32
114
  } else {
33
115
  this.backlog = payload;
34
- this.timeout = setTimeout(() => {
35
- this.sendMessagesInBulk();
36
- }, 0);
37
116
  }
38
117
  }
39
118
 
40
119
  sendMessagesInBulk() {
41
- this.send(this.backlog);
42
- this.backlog = "";
120
+ if (this.backlog.length > 0 && isWebSocketOpen(this.websocket)) {
121
+ this.websocket.send(this.backlog);
122
+ this.backlog = "";
123
+ }
124
+ }
125
+
126
+ setBatching(enabled: boolean) {
127
+ this.batching = enabled;
128
+ }
129
+
130
+ private closeListeners = new Set<() => void>();
131
+ onClose(callback: () => void) {
132
+ this.closeListeners.add(callback);
43
133
  }
44
134
 
45
135
  close() {
136
+ if (this.closed) {
137
+ return;
138
+ }
139
+
140
+ let msg = this.queue.pull();
141
+
142
+ while (msg) {
143
+ this.processMessage(msg);
144
+ msg = this.queue.pull();
145
+ }
146
+
147
+ this.closed = true;
46
148
  this.sendMessagesInBulk();
149
+
150
+ for (const listener of this.closeListeners) {
151
+ listener();
152
+ }
153
+
154
+ this.closeListeners.clear();
47
155
  }
48
156
  }
@@ -1,17 +1,9 @@
1
- import {
2
- type DisconnectedError,
3
- type Peer,
4
- type PingTimeoutError,
5
- type SyncMessage,
6
- cojsonInternals,
7
- logger,
8
- } from "cojson";
1
+ import { type Peer, type SyncMessage, cojsonInternals, logger } from "cojson";
9
2
  import { BatchedOutgoingMessages } from "./BatchedOutgoingMessages.js";
10
3
  import { deserializeMessages } from "./serialization.js";
11
4
  import type { AnyWebSocket } from "./types.js";
12
5
 
13
- export const BUFFER_LIMIT = 100_000;
14
- export const BUFFER_LIMIT_POLLING_INTERVAL = 10;
6
+ const { ConnectedPeerChannel } = cojsonInternals;
15
7
 
16
8
  export type CreateWebSocketPeerOpts = {
17
9
  id: string;
@@ -52,70 +44,6 @@ function createPingTimeoutListener(
52
44
  };
53
45
  }
54
46
 
55
- function waitForWebSocketOpen(websocket: AnyWebSocket) {
56
- return new Promise<void>((resolve) => {
57
- if (websocket.readyState === 1) {
58
- resolve();
59
- } else {
60
- websocket.addEventListener("open", () => resolve(), { once: true });
61
- }
62
- });
63
- }
64
-
65
- function createOutgoingMessagesManager(
66
- websocket: AnyWebSocket,
67
- batchingByDefault: boolean,
68
- ) {
69
- let closed = false;
70
- const outgoingMessages = new BatchedOutgoingMessages((messages) => {
71
- if (websocket.readyState === 1) {
72
- websocket.send(messages);
73
- }
74
- });
75
-
76
- let batchingEnabled = batchingByDefault;
77
-
78
- async function sendMessage(msg: SyncMessage) {
79
- if (closed) {
80
- return Promise.reject(new Error("WebSocket closed"));
81
- }
82
-
83
- if (websocket.readyState !== 1) {
84
- await waitForWebSocketOpen(websocket);
85
- }
86
-
87
- while (
88
- websocket.bufferedAmount > BUFFER_LIMIT &&
89
- websocket.readyState === 1
90
- ) {
91
- await new Promise<void>((resolve) =>
92
- setTimeout(resolve, BUFFER_LIMIT_POLLING_INTERVAL),
93
- );
94
- }
95
-
96
- if (websocket.readyState !== 1) {
97
- return;
98
- }
99
-
100
- if (!batchingEnabled) {
101
- websocket.send(JSON.stringify(msg));
102
- } else {
103
- outgoingMessages.push(msg);
104
- }
105
- }
106
-
107
- return {
108
- sendMessage,
109
- setBatchingEnabled(enabled: boolean) {
110
- batchingEnabled = enabled;
111
- },
112
- close() {
113
- closed = true;
114
- outgoingMessages.close();
115
- },
116
- };
117
- }
118
-
119
47
  function createClosedEventEmitter(callback = () => {}) {
120
48
  let disconnected = false;
121
49
 
@@ -137,17 +65,11 @@ export function createWebSocketPeer({
137
65
  onSuccess,
138
66
  onClose,
139
67
  }: CreateWebSocketPeerOpts): Peer {
140
- const incoming = new cojsonInternals.Channel<
141
- SyncMessage | DisconnectedError | PingTimeoutError
142
- >();
68
+ const incoming = new ConnectedPeerChannel();
143
69
  const emitClosedEvent = createClosedEventEmitter(onClose);
144
70
 
145
71
  function handleClose() {
146
- incoming
147
- .push("Disconnected")
148
- .catch((e) =>
149
- logger.error("Error while pushing disconnect msg", { err: e }),
150
- );
72
+ incoming.push("Disconnected");
151
73
  emitClosedEvent();
152
74
  }
153
75
 
@@ -166,18 +88,19 @@ export function createWebSocketPeer({
166
88
  expectPings,
167
89
  pingTimeout,
168
90
  () => {
169
- incoming
170
- .push("PingTimeout")
171
- .catch((e) =>
172
- logger.error("Error while pushing ping timeout", { err: e }),
173
- );
91
+ incoming.push("Disconnected");
92
+ logger.error("Ping timeout from peer", {
93
+ peerId: id,
94
+ peerRole: role,
95
+ });
174
96
  emitClosedEvent();
175
97
  },
176
98
  );
177
99
 
178
- const outgoingMessages = createOutgoingMessagesManager(
100
+ const outgoing = new BatchedOutgoingMessages(
179
101
  websocket,
180
102
  batchingByDefault,
103
+ role,
181
104
  );
182
105
  let isFirstMessage = true;
183
106
 
@@ -206,50 +129,42 @@ export function createWebSocketPeer({
206
129
 
207
130
  if (messages.length > 1) {
208
131
  // If more than one message is received, the other peer supports batching
209
- outgoingMessages.setBatchingEnabled(true);
132
+ outgoing.setBatching(true);
210
133
  }
211
134
 
212
135
  for (const msg of messages) {
213
136
  if (msg && "action" in msg) {
214
- incoming
215
- .push(msg)
216
- .catch((e) =>
217
- logger.error("Error while pushing incoming msg", { err: e }),
218
- );
137
+ incoming.push(msg);
219
138
  }
220
139
  }
221
140
  }
222
141
 
223
142
  websocket.addEventListener("message", handleIncomingMsg);
224
143
 
144
+ outgoing.onClose(() => {
145
+ websocket.removeEventListener("message", handleIncomingMsg);
146
+ websocket.removeEventListener("close", handleClose);
147
+ pingTimeoutListener.clear();
148
+ emitClosedEvent();
149
+
150
+ if (websocket.readyState === 0) {
151
+ websocket.addEventListener(
152
+ "open",
153
+ function handleClose() {
154
+ websocket.close();
155
+ },
156
+ { once: true },
157
+ );
158
+ } else if (websocket.readyState === 1) {
159
+ websocket.close();
160
+ }
161
+ });
162
+
225
163
  return {
226
164
  id,
227
165
  incoming,
228
- outgoing: {
229
- push: outgoingMessages.sendMessage,
230
- close() {
231
- outgoingMessages.close();
232
-
233
- websocket.removeEventListener("message", handleIncomingMsg);
234
- websocket.removeEventListener("close", handleClose);
235
- pingTimeoutListener.clear();
236
- emitClosedEvent();
237
-
238
- if (websocket.readyState === 0) {
239
- websocket.addEventListener(
240
- "open",
241
- function handleClose() {
242
- websocket.close();
243
- },
244
- { once: true },
245
- );
246
- } else if (websocket.readyState === 1) {
247
- websocket.close();
248
- }
249
- },
250
- },
166
+ outgoing,
251
167
  role,
252
- crashOnClose: false,
253
168
  deletePeerStateOnClose,
254
169
  };
255
170
  }