cojson-transport-ws 0.8.13 → 0.8.17
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/CHANGELOG.md +163 -148
- package/dist/BatchedOutgoingMessages.js +3 -3
- package/dist/BatchedOutgoingMessages.js.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/serialization.js.map +1 -1
- package/dist/tests/BatchedOutgoingMessages.test.js +41 -11
- package/dist/tests/BatchedOutgoingMessages.test.js.map +1 -1
- package/dist/tests/createWebSocketPeer.test.js +9 -13
- package/dist/tests/createWebSocketPeer.test.js.map +1 -1
- package/package.json +5 -9
- package/src/BatchedOutgoingMessages.ts +36 -35
- package/src/index.ts +170 -163
- package/src/serialization.ts +25 -23
- package/src/tests/BatchedOutgoingMessages.test.ts +138 -106
- package/src/tests/createWebSocketPeer.test.ts +404 -411
- package/src/types.ts +19 -20
- package/tsconfig.json +2 -2
- package/.eslintrc.cjs +0 -24
- package/.prettierrc.js +0 -9
package/src/index.ts
CHANGED
|
@@ -1,209 +1,216 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
DisconnectedError,
|
|
3
|
+
Peer,
|
|
4
|
+
PingTimeoutError,
|
|
5
|
+
SyncMessage,
|
|
6
|
+
cojsonInternals,
|
|
7
7
|
} from "cojson";
|
|
8
|
-
import { AnyWebSocket } from "./types.js";
|
|
9
8
|
import { BatchedOutgoingMessages } from "./BatchedOutgoingMessages.js";
|
|
10
9
|
import { deserializeMessages } from "./serialization.js";
|
|
10
|
+
import { AnyWebSocket } from "./types.js";
|
|
11
11
|
|
|
12
12
|
export const BUFFER_LIMIT = 100_000;
|
|
13
13
|
export const BUFFER_LIMIT_POLLING_INTERVAL = 10;
|
|
14
14
|
|
|
15
15
|
export type CreateWebSocketPeerOpts = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
id: string;
|
|
17
|
+
websocket: AnyWebSocket;
|
|
18
|
+
role: Peer["role"];
|
|
19
|
+
expectPings?: boolean;
|
|
20
|
+
batchingByDefault?: boolean;
|
|
21
|
+
onClose?: () => void;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
function createPingTimeoutListener(enabled: boolean, callback: () => void) {
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
reset() {},
|
|
28
|
-
clear() {}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let pingTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
33
|
-
|
|
25
|
+
if (!enabled) {
|
|
34
26
|
return {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
pingTimeout = setTimeout(() => {
|
|
38
|
-
callback();
|
|
39
|
-
}, 10_000);
|
|
40
|
-
},
|
|
41
|
-
clear() {
|
|
42
|
-
pingTimeout && clearTimeout(pingTimeout);
|
|
43
|
-
}
|
|
27
|
+
reset() {},
|
|
28
|
+
clear() {},
|
|
44
29
|
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let pingTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
reset() {
|
|
36
|
+
pingTimeout && clearTimeout(pingTimeout);
|
|
37
|
+
pingTimeout = setTimeout(() => {
|
|
38
|
+
callback();
|
|
39
|
+
}, 10_000);
|
|
40
|
+
},
|
|
41
|
+
clear() {
|
|
42
|
+
pingTimeout && clearTimeout(pingTimeout);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function waitForWebSocketOpen(websocket: AnyWebSocket) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
return new Promise<void>((resolve) => {
|
|
49
|
+
if (websocket.readyState === 1) {
|
|
50
|
+
resolve();
|
|
51
|
+
} else {
|
|
52
|
+
websocket.addEventListener("open", resolve, { once: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function createOutgoingMessagesManager(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
function createOutgoingMessagesManager(
|
|
58
|
+
websocket: AnyWebSocket,
|
|
59
|
+
batchingByDefault: boolean,
|
|
60
|
+
) {
|
|
61
|
+
const outgoingMessages = new BatchedOutgoingMessages((messages) => {
|
|
62
|
+
if (websocket.readyState === 1) {
|
|
63
|
+
websocket.send(messages);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
if (websocket.readyState !== 1) {
|
|
68
|
-
await waitForWebSocketOpen(websocket);
|
|
69
|
-
}
|
|
67
|
+
let batchingEnabled = batchingByDefault;
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
await new Promise<void>((resolve) =>
|
|
76
|
-
setTimeout(resolve, BUFFER_LIMIT_POLLING_INTERVAL),
|
|
77
|
-
);
|
|
78
|
-
}
|
|
69
|
+
async function sendMessage(msg: SyncMessage) {
|
|
70
|
+
if (websocket.readyState !== 1) {
|
|
71
|
+
await waitForWebSocketOpen(websocket);
|
|
72
|
+
}
|
|
79
73
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
while (
|
|
75
|
+
websocket.bufferedAmount > BUFFER_LIMIT &&
|
|
76
|
+
websocket.readyState === 1
|
|
77
|
+
) {
|
|
78
|
+
await new Promise<void>((resolve) =>
|
|
79
|
+
setTimeout(resolve, BUFFER_LIMIT_POLLING_INTERVAL),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
} else {
|
|
87
|
-
outgoingMessages.push(msg);
|
|
88
|
-
}
|
|
83
|
+
if (websocket.readyState !== 1) {
|
|
84
|
+
return;
|
|
89
85
|
}
|
|
90
86
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
87
|
+
if (!batchingEnabled) {
|
|
88
|
+
websocket.send(JSON.stringify(msg));
|
|
89
|
+
} else {
|
|
90
|
+
outgoingMessages.push(msg);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
sendMessage,
|
|
96
|
+
setBatchingEnabled(enabled: boolean) {
|
|
97
|
+
batchingEnabled = enabled;
|
|
98
|
+
},
|
|
99
|
+
close() {
|
|
100
|
+
outgoingMessages.close();
|
|
101
|
+
},
|
|
102
|
+
};
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
function createClosedEventEmitter(callback = () => {}) {
|
|
103
|
-
|
|
106
|
+
let disconnected = false;
|
|
104
107
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
return () => {
|
|
109
|
+
if (disconnected) return;
|
|
110
|
+
disconnected = true;
|
|
111
|
+
callback();
|
|
112
|
+
};
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
export function createWebSocketPeer({
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
id,
|
|
117
|
+
websocket,
|
|
118
|
+
role,
|
|
119
|
+
expectPings = true,
|
|
120
|
+
batchingByDefault = true,
|
|
121
|
+
onClose,
|
|
119
122
|
}: CreateWebSocketPeerOpts): Peer {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
const incoming = new cojsonInternals.Channel<
|
|
124
|
+
SyncMessage | DisconnectedError | PingTimeoutError
|
|
125
|
+
>();
|
|
126
|
+
const emitClosedEvent = createClosedEventEmitter(onClose);
|
|
127
|
+
|
|
128
|
+
function handleClose() {
|
|
129
|
+
incoming
|
|
130
|
+
.push("Disconnected")
|
|
131
|
+
.catch((e) => console.error("Error while pushing disconnect msg", e));
|
|
132
|
+
emitClosedEvent();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
websocket.addEventListener("close", handleClose);
|
|
136
|
+
|
|
137
|
+
const pingTimeout = createPingTimeoutListener(expectPings, () => {
|
|
138
|
+
incoming
|
|
139
|
+
.push("PingTimeout")
|
|
140
|
+
.catch((e) => console.error("Error while pushing ping timeout", e));
|
|
141
|
+
emitClosedEvent();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const outgoingMessages = createOutgoingMessagesManager(
|
|
145
|
+
websocket,
|
|
146
|
+
batchingByDefault,
|
|
147
|
+
);
|
|
124
148
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
console.error("Error while pushing disconnect msg", e),
|
|
130
|
-
);
|
|
131
|
-
emitClosedEvent();
|
|
149
|
+
function handleIncomingMsg(event: { data: unknown }) {
|
|
150
|
+
if (event.data === "") {
|
|
151
|
+
console.log("client", id, "sent empty message");
|
|
152
|
+
return;
|
|
132
153
|
}
|
|
133
154
|
|
|
134
|
-
|
|
155
|
+
const result = deserializeMessages(event.data);
|
|
135
156
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
function handleIncomingMsg(event: { data: unknown }) {
|
|
146
|
-
const result = deserializeMessages(event.data);
|
|
147
|
-
|
|
148
|
-
if (!result.ok) {
|
|
149
|
-
console.error(
|
|
150
|
-
"Error while deserializing messages",
|
|
151
|
-
event.data,
|
|
152
|
-
result.error,
|
|
153
|
-
);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
157
|
+
if (!result.ok) {
|
|
158
|
+
console.error(
|
|
159
|
+
"Error while deserializing messages",
|
|
160
|
+
event.data,
|
|
161
|
+
result.error,
|
|
162
|
+
);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
156
165
|
|
|
157
|
-
|
|
166
|
+
const { messages } = result;
|
|
158
167
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
168
|
+
if (messages.length > 1) {
|
|
169
|
+
// If more than one message is received, the other peer supports batching
|
|
170
|
+
outgoingMessages.setBatchingEnabled(true);
|
|
171
|
+
}
|
|
163
172
|
|
|
164
|
-
|
|
173
|
+
pingTimeout.reset();
|
|
165
174
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
+
for (const msg of messages) {
|
|
176
|
+
if (msg && "action" in msg) {
|
|
177
|
+
incoming
|
|
178
|
+
.push(msg)
|
|
179
|
+
.catch((e) => console.error("Error while pushing incoming msg", e));
|
|
180
|
+
}
|
|
175
181
|
}
|
|
182
|
+
}
|
|
176
183
|
|
|
177
|
-
|
|
184
|
+
websocket.addEventListener("message", handleIncomingMsg);
|
|
178
185
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
},
|
|
199
|
-
{ once: true },
|
|
200
|
-
);
|
|
201
|
-
} else if (websocket.readyState == 1) {
|
|
202
|
-
websocket.close();
|
|
203
|
-
}
|
|
186
|
+
return {
|
|
187
|
+
id,
|
|
188
|
+
incoming,
|
|
189
|
+
outgoing: {
|
|
190
|
+
push: outgoingMessages.sendMessage,
|
|
191
|
+
close() {
|
|
192
|
+
console.log("Trying to close", id, websocket.readyState);
|
|
193
|
+
outgoingMessages.close();
|
|
194
|
+
|
|
195
|
+
websocket.removeEventListener("message", handleIncomingMsg);
|
|
196
|
+
websocket.removeEventListener("close", handleClose);
|
|
197
|
+
pingTimeout.clear();
|
|
198
|
+
emitClosedEvent();
|
|
199
|
+
|
|
200
|
+
if (websocket.readyState === 0) {
|
|
201
|
+
websocket.addEventListener(
|
|
202
|
+
"open",
|
|
203
|
+
function handleClose() {
|
|
204
|
+
websocket.close();
|
|
204
205
|
},
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
{ once: true },
|
|
207
|
+
);
|
|
208
|
+
} else if (websocket.readyState == 1) {
|
|
209
|
+
websocket.close();
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
role,
|
|
214
|
+
crashOnClose: false,
|
|
215
|
+
};
|
|
209
216
|
}
|
package/src/serialization.ts
CHANGED
|
@@ -2,30 +2,32 @@ import { SyncMessage } from "cojson";
|
|
|
2
2
|
import { PingMsg } from "./types.js";
|
|
3
3
|
|
|
4
4
|
export function addMessageToBacklog(backlog: string, message: SyncMessage) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
if (!backlog) {
|
|
6
|
+
return JSON.stringify(message);
|
|
7
|
+
}
|
|
8
|
+
return `${backlog}\n${JSON.stringify(message)}`;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function deserializeMessages(messages: unknown)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
export function deserializeMessages(messages: unknown) {
|
|
12
|
+
if (typeof messages !== "string") {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
error: new Error("Expected a string"),
|
|
16
|
+
} as const;
|
|
17
|
+
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
try {
|
|
20
|
+
return {
|
|
21
|
+
ok: true,
|
|
22
|
+
messages: messages.split("\n").map((msg) => JSON.parse(msg)) as
|
|
23
|
+
| SyncMessage[]
|
|
24
|
+
| PingMsg[],
|
|
25
|
+
} as const;
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error("Error while deserializing messages", e);
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
error: e,
|
|
31
|
+
} as const;
|
|
32
|
+
}
|
|
31
33
|
}
|