cojson-transport-ws 0.8.13 → 0.8.16
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 +155 -148
- package/dist/BatchedOutgoingMessages.js +3 -3
- package/dist/BatchedOutgoingMessages.js.map +1 -1
- package/dist/index.js +2 -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 +168 -166
- 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,211 @@
|
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
+
);
|
|
148
|
+
|
|
149
|
+
function handleIncomingMsg(event: { data: unknown }) {
|
|
150
|
+
const result = deserializeMessages(event.data);
|
|
151
|
+
|
|
152
|
+
if (!result.ok) {
|
|
153
|
+
console.error(
|
|
154
|
+
"Error while deserializing messages",
|
|
155
|
+
event.data,
|
|
156
|
+
result.error,
|
|
157
|
+
);
|
|
158
|
+
return;
|
|
132
159
|
}
|
|
133
160
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const pingTimeout = createPingTimeoutListener(expectPings, () => {
|
|
137
|
-
incoming
|
|
138
|
-
.push("PingTimeout")
|
|
139
|
-
.catch((e) => console.error("Error while pushing ping timeout", e));
|
|
140
|
-
emitClosedEvent();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
const outgoingMessages = createOutgoingMessagesManager(websocket, batchingByDefault);
|
|
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
|
-
}
|
|
156
|
-
|
|
157
|
-
const { messages } = result;
|
|
161
|
+
const { messages } = result;
|
|
158
162
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
if (messages.length > 1) {
|
|
164
|
+
// If more than one message is received, the other peer supports batching
|
|
165
|
+
outgoingMessages.setBatchingEnabled(true);
|
|
166
|
+
}
|
|
163
167
|
|
|
164
|
-
|
|
168
|
+
pingTimeout.reset();
|
|
165
169
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
170
|
+
for (const msg of messages) {
|
|
171
|
+
if (msg && "action" in msg) {
|
|
172
|
+
incoming
|
|
173
|
+
.push(msg)
|
|
174
|
+
.catch((e) => console.error("Error while pushing incoming msg", e));
|
|
175
|
+
}
|
|
175
176
|
}
|
|
177
|
+
}
|
|
176
178
|
|
|
177
|
-
|
|
179
|
+
websocket.addEventListener("message", handleIncomingMsg);
|
|
178
180
|
|
|
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
|
-
}
|
|
181
|
+
return {
|
|
182
|
+
id,
|
|
183
|
+
incoming,
|
|
184
|
+
outgoing: {
|
|
185
|
+
push: outgoingMessages.sendMessage,
|
|
186
|
+
close() {
|
|
187
|
+
console.log("Trying to close", id, websocket.readyState);
|
|
188
|
+
outgoingMessages.close();
|
|
189
|
+
|
|
190
|
+
websocket.removeEventListener("message", handleIncomingMsg);
|
|
191
|
+
websocket.removeEventListener("close", handleClose);
|
|
192
|
+
pingTimeout.clear();
|
|
193
|
+
emitClosedEvent();
|
|
194
|
+
|
|
195
|
+
if (websocket.readyState === 0) {
|
|
196
|
+
websocket.addEventListener(
|
|
197
|
+
"open",
|
|
198
|
+
function handleClose() {
|
|
199
|
+
websocket.close();
|
|
204
200
|
},
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
201
|
+
{ once: true },
|
|
202
|
+
);
|
|
203
|
+
} else if (websocket.readyState == 1) {
|
|
204
|
+
websocket.close();
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
role,
|
|
209
|
+
crashOnClose: false,
|
|
210
|
+
};
|
|
209
211
|
}
|
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
|
}
|