@rvncom/socket-bun-engine 1.0.0
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/LICENSE +22 -0
- package/README.md +148 -0
- package/dist/cors.d.ts +11 -0
- package/dist/cors.js +83 -0
- package/dist/event-emitter.d.ts +105 -0
- package/dist/event-emitter.js +129 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/parser.d.ts +14 -0
- package/dist/parser.js +73 -0
- package/dist/server.d.ts +146 -0
- package/dist/server.js +348 -0
- package/dist/socket.d.ts +123 -0
- package/dist/socket.js +281 -0
- package/dist/transport.d.ts +64 -0
- package/dist/transport.js +59 -0
- package/dist/transports/polling.d.ts +32 -0
- package/dist/transports/polling.js +113 -0
- package/dist/transports/websocket.d.ts +16 -0
- package/dist/transports/websocket.js +38 -0
- package/dist/util.d.ts +1 -0
- package/dist/util.js +4 -0
- package/package.json +36 -0
package/dist/socket.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { EventEmitter } from "./event-emitter";
|
|
2
|
+
import {} from "./parser";
|
|
3
|
+
import { Transport, TransportError } from "./transport";
|
|
4
|
+
import {} from "./server";
|
|
5
|
+
import { debuglog } from "node:util";
|
|
6
|
+
const debug = debuglog("engine.io:socket");
|
|
7
|
+
const FAST_UPGRADE_INTERVAL_MS = 100;
|
|
8
|
+
export class Socket extends EventEmitter {
|
|
9
|
+
id;
|
|
10
|
+
readyState = "opening";
|
|
11
|
+
transport;
|
|
12
|
+
request;
|
|
13
|
+
opts;
|
|
14
|
+
upgradeState = "not_upgraded";
|
|
15
|
+
writeBuffer = [];
|
|
16
|
+
/*
|
|
17
|
+
* Note: using a single timer for all sockets seems to result in a higher CPU consumption than using one timer for each socket
|
|
18
|
+
*/
|
|
19
|
+
pingIntervalTimer;
|
|
20
|
+
pingTimeoutTimer;
|
|
21
|
+
constructor(id, opts, transport, req) {
|
|
22
|
+
super();
|
|
23
|
+
this.id = id;
|
|
24
|
+
this.opts = opts;
|
|
25
|
+
this.transport = transport;
|
|
26
|
+
this.bindTransport(transport);
|
|
27
|
+
this.request = req;
|
|
28
|
+
this.onOpen();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Called upon transport considered open.
|
|
32
|
+
*
|
|
33
|
+
* @private
|
|
34
|
+
*/
|
|
35
|
+
onOpen() {
|
|
36
|
+
this.readyState = "open";
|
|
37
|
+
this.sendPacket("open", JSON.stringify({
|
|
38
|
+
sid: this.id,
|
|
39
|
+
upgrades: this.transport.upgradesTo,
|
|
40
|
+
pingInterval: this.opts.pingInterval,
|
|
41
|
+
pingTimeout: this.opts.pingTimeout,
|
|
42
|
+
maxPayload: this.opts.maxHttpBufferSize,
|
|
43
|
+
}));
|
|
44
|
+
this.emitReserved("open");
|
|
45
|
+
this.schedulePing();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Called upon transport packet.
|
|
49
|
+
*
|
|
50
|
+
* @param packet
|
|
51
|
+
* @private
|
|
52
|
+
*/
|
|
53
|
+
onPacket(packet) {
|
|
54
|
+
if (this.readyState !== "open") {
|
|
55
|
+
debug("packet received with closed socket");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
debug(`received packet ${packet.type}`);
|
|
59
|
+
this.emitReserved("packet", packet);
|
|
60
|
+
switch (packet.type) {
|
|
61
|
+
case "pong":
|
|
62
|
+
debug("got pong");
|
|
63
|
+
clearTimeout(this.pingTimeoutTimer);
|
|
64
|
+
this.schedulePing();
|
|
65
|
+
this.emitReserved("heartbeat");
|
|
66
|
+
break;
|
|
67
|
+
case "message":
|
|
68
|
+
this.emitReserved("data", packet.data);
|
|
69
|
+
break;
|
|
70
|
+
case "error":
|
|
71
|
+
default:
|
|
72
|
+
this.onClose("parse error");
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Called upon transport error.
|
|
78
|
+
*
|
|
79
|
+
* @param err
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
onError(err) {
|
|
83
|
+
debug(`transport error: ${err.message}`);
|
|
84
|
+
this.onClose("transport error");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Pings client every `pingInterval` and expects response
|
|
88
|
+
* within `pingTimeout` or closes connection.
|
|
89
|
+
*
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
schedulePing() {
|
|
93
|
+
if (this.pingTimeoutTimer) {
|
|
94
|
+
this.pingIntervalTimer?.refresh();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.pingIntervalTimer = setTimeout(() => {
|
|
98
|
+
debug(`writing ping packet - expecting pong within ${this.opts.pingTimeout} ms`);
|
|
99
|
+
this.sendPacket("ping");
|
|
100
|
+
this.resetPingTimeout();
|
|
101
|
+
}, this.opts.pingInterval);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Resets ping timeout.
|
|
105
|
+
*
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
resetPingTimeout() {
|
|
109
|
+
clearTimeout(this.pingTimeoutTimer);
|
|
110
|
+
this.pingTimeoutTimer = setTimeout(() => {
|
|
111
|
+
this.onClose("ping timeout");
|
|
112
|
+
}, this.opts.pingTimeout);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Attaches handlers for the given transport.
|
|
116
|
+
*
|
|
117
|
+
* @param transport
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
bindTransport(transport) {
|
|
121
|
+
this.transport = transport;
|
|
122
|
+
this.transport.once("error", (err) => this.onError(err));
|
|
123
|
+
this.transport.on("packet", (packet) => this.onPacket(packet));
|
|
124
|
+
this.transport.on("drain", () => this.flush());
|
|
125
|
+
this.transport.on("close", () => this.onClose("transport close"));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Upgrades socket to the given transport
|
|
129
|
+
*
|
|
130
|
+
* @param transport
|
|
131
|
+
* @private
|
|
132
|
+
*/
|
|
133
|
+
/* private */ _maybeUpgrade(transport) {
|
|
134
|
+
if (this.upgradeState === "upgrading") {
|
|
135
|
+
debug("transport has already been trying to upgrade");
|
|
136
|
+
return transport.close();
|
|
137
|
+
}
|
|
138
|
+
else if (this.upgradeState === "upgraded") {
|
|
139
|
+
debug("transport has already been upgraded");
|
|
140
|
+
return transport.close();
|
|
141
|
+
}
|
|
142
|
+
debug("upgrading existing transport");
|
|
143
|
+
this.upgradeState = "upgrading";
|
|
144
|
+
const timeoutId = setTimeout(() => {
|
|
145
|
+
debug("client did not complete upgrade - closing transport");
|
|
146
|
+
transport.close();
|
|
147
|
+
}, this.opts.upgradeTimeout);
|
|
148
|
+
let fastUpgradeTimerId;
|
|
149
|
+
transport.on("close", () => {
|
|
150
|
+
clearInterval(fastUpgradeTimerId);
|
|
151
|
+
transport.off();
|
|
152
|
+
});
|
|
153
|
+
// we need to make sure that no packets gets lost during the upgrade, so the client does not cancel the HTTP
|
|
154
|
+
// long-polling request itself, instead the server sends a "noop" packet to cleanly end any ongoing polling request
|
|
155
|
+
const sendNoopPacket = () => {
|
|
156
|
+
if (this.transport.name === "polling" && this.transport.writable) {
|
|
157
|
+
debug("writing a noop packet to polling for fast upgrade");
|
|
158
|
+
this.transport.send([{ type: "noop" }]);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
transport.on("packet", (packet) => {
|
|
162
|
+
if (packet.type === "ping" && packet.data === "probe") {
|
|
163
|
+
debug("got probe ping packet, sending pong");
|
|
164
|
+
transport.send([{ type: "pong", data: "probe" }]);
|
|
165
|
+
sendNoopPacket();
|
|
166
|
+
fastUpgradeTimerId = setInterval(sendNoopPacket, FAST_UPGRADE_INTERVAL_MS);
|
|
167
|
+
this.emitReserved("upgrading", transport);
|
|
168
|
+
}
|
|
169
|
+
else if (packet.type === "upgrade" && this.readyState !== "closed") {
|
|
170
|
+
debug("got upgrade packet - upgrading");
|
|
171
|
+
this.upgradeState = "upgraded";
|
|
172
|
+
clearTimeout(timeoutId);
|
|
173
|
+
clearInterval(fastUpgradeTimerId);
|
|
174
|
+
transport.off();
|
|
175
|
+
this.closeTransport();
|
|
176
|
+
this.bindTransport(transport);
|
|
177
|
+
this.emitReserved("upgrade", transport);
|
|
178
|
+
this.flush();
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
debug("invalid upgrade packet");
|
|
182
|
+
clearTimeout(timeoutId);
|
|
183
|
+
transport.close();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Called upon transport considered closed.
|
|
189
|
+
*
|
|
190
|
+
* @param reason
|
|
191
|
+
* @private
|
|
192
|
+
*/
|
|
193
|
+
onClose(reason) {
|
|
194
|
+
if (this.readyState === "closed") {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
debug(`socket closed due to ${reason}`);
|
|
198
|
+
this.readyState = "closed";
|
|
199
|
+
clearTimeout(this.pingIntervalTimer);
|
|
200
|
+
clearTimeout(this.pingTimeoutTimer);
|
|
201
|
+
this.closeTransport();
|
|
202
|
+
this.emitReserved("close", reason);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Sends a "message" packet.
|
|
206
|
+
*
|
|
207
|
+
* @param data
|
|
208
|
+
*/
|
|
209
|
+
write(data) {
|
|
210
|
+
this.sendPacket("message", data);
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Sends a packet.
|
|
215
|
+
*
|
|
216
|
+
* @param type
|
|
217
|
+
* @param data
|
|
218
|
+
* @private
|
|
219
|
+
*/
|
|
220
|
+
sendPacket(type, data) {
|
|
221
|
+
if (["closing", "closed"].includes(this.readyState)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
debug(`sending packet ${type} (${data})`);
|
|
225
|
+
const packet = {
|
|
226
|
+
type,
|
|
227
|
+
data,
|
|
228
|
+
};
|
|
229
|
+
this.emitReserved("packetCreate", packet);
|
|
230
|
+
this.writeBuffer.push(packet);
|
|
231
|
+
this.flush();
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Attempts to flush the packets buffer.
|
|
235
|
+
*
|
|
236
|
+
* @private
|
|
237
|
+
*/
|
|
238
|
+
flush() {
|
|
239
|
+
const shouldFlush = this.readyState !== "closed" &&
|
|
240
|
+
this.transport.writable &&
|
|
241
|
+
this.writeBuffer.length > 0;
|
|
242
|
+
if (!shouldFlush) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
debug(`[socket] flushing buffer with ${this.writeBuffer.length} packet(s) to transport`);
|
|
246
|
+
this.emitReserved("flush", this.writeBuffer);
|
|
247
|
+
const buffer = this.writeBuffer;
|
|
248
|
+
this.writeBuffer = [];
|
|
249
|
+
this.transport.send(buffer);
|
|
250
|
+
this.emitReserved("drain");
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Closes the socket and underlying transport.
|
|
254
|
+
*/
|
|
255
|
+
close() {
|
|
256
|
+
if (this.readyState !== "open") {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
this.readyState = "closing";
|
|
260
|
+
const close = () => {
|
|
261
|
+
this.closeTransport();
|
|
262
|
+
this.onClose("forced close");
|
|
263
|
+
};
|
|
264
|
+
if (this.writeBuffer.length) {
|
|
265
|
+
debug(`buffer not empty, waiting for the drain event`);
|
|
266
|
+
this.once("drain", close);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
close();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Closes the underlying transport.
|
|
274
|
+
*
|
|
275
|
+
* @private
|
|
276
|
+
*/
|
|
277
|
+
closeTransport() {
|
|
278
|
+
this.transport.off();
|
|
279
|
+
this.transport.close();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { EventEmitter } from "./event-emitter";
|
|
2
|
+
import { type Packet } from "./parser";
|
|
3
|
+
import { type ServerOptions } from "./server";
|
|
4
|
+
interface TransportEvents {
|
|
5
|
+
packet: (packet: Packet) => void;
|
|
6
|
+
error: (error: TransportError) => void;
|
|
7
|
+
drain: () => void;
|
|
8
|
+
close: () => void;
|
|
9
|
+
}
|
|
10
|
+
type ReadyState = "open" | "closing" | "closed";
|
|
11
|
+
export declare abstract class Transport extends EventEmitter<Record<never, never>, Record<never, never>, TransportEvents> {
|
|
12
|
+
writable: boolean;
|
|
13
|
+
protected readyState: ReadyState;
|
|
14
|
+
protected readonly opts: ServerOptions;
|
|
15
|
+
constructor(opts: ServerOptions);
|
|
16
|
+
/**
|
|
17
|
+
* The name of the transport
|
|
18
|
+
*/
|
|
19
|
+
abstract get name(): string;
|
|
20
|
+
/**
|
|
21
|
+
* The list of transports to upgrade to
|
|
22
|
+
*/
|
|
23
|
+
abstract get upgradesTo(): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Writes an array of packets.
|
|
26
|
+
*
|
|
27
|
+
* @param packets
|
|
28
|
+
*/
|
|
29
|
+
abstract send(packets: Packet[]): void;
|
|
30
|
+
/**
|
|
31
|
+
* Closes the transport.
|
|
32
|
+
*
|
|
33
|
+
* @protected
|
|
34
|
+
*/
|
|
35
|
+
protected abstract doClose(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Manually closes the transport.
|
|
38
|
+
*/
|
|
39
|
+
close(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Called when the transport encounters a fatal error.
|
|
42
|
+
*
|
|
43
|
+
* @param message
|
|
44
|
+
* @protected
|
|
45
|
+
*/
|
|
46
|
+
protected onError(message: string): void;
|
|
47
|
+
/**
|
|
48
|
+
* Called with a parsed packet from the data stream.
|
|
49
|
+
*
|
|
50
|
+
* @param packet
|
|
51
|
+
* @protected
|
|
52
|
+
*/
|
|
53
|
+
protected onPacket(packet: Packet): void;
|
|
54
|
+
/**
|
|
55
|
+
* Called upon transport close.
|
|
56
|
+
*
|
|
57
|
+
* @protected
|
|
58
|
+
*/
|
|
59
|
+
protected onClose(): void;
|
|
60
|
+
}
|
|
61
|
+
export declare class TransportError extends Error {
|
|
62
|
+
readonly type = "TransportError";
|
|
63
|
+
}
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { EventEmitter } from "./event-emitter";
|
|
2
|
+
import {} from "./parser";
|
|
3
|
+
import {} from "./server";
|
|
4
|
+
import { debuglog } from "node:util";
|
|
5
|
+
const debug = debuglog("engine.io:transport");
|
|
6
|
+
export class Transport extends EventEmitter {
|
|
7
|
+
writable = false;
|
|
8
|
+
readyState = "open";
|
|
9
|
+
opts;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
super();
|
|
12
|
+
this.opts = opts;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Manually closes the transport.
|
|
16
|
+
*/
|
|
17
|
+
close() {
|
|
18
|
+
if (["closing", "closed"].includes(this.readyState)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
debug("closing transport");
|
|
22
|
+
this.readyState = "closing";
|
|
23
|
+
this.doClose();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Called when the transport encounters a fatal error.
|
|
27
|
+
*
|
|
28
|
+
* @param message
|
|
29
|
+
* @protected
|
|
30
|
+
*/
|
|
31
|
+
onError(message) {
|
|
32
|
+
this.emitReserved("error", new TransportError(message));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Called with a parsed packet from the data stream.
|
|
36
|
+
*
|
|
37
|
+
* @param packet
|
|
38
|
+
* @protected
|
|
39
|
+
*/
|
|
40
|
+
onPacket(packet) {
|
|
41
|
+
if (packet.type === "close") {
|
|
42
|
+
debug("received 'close' packet");
|
|
43
|
+
return this.doClose();
|
|
44
|
+
}
|
|
45
|
+
this.emitReserved("packet", packet);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Called upon transport close.
|
|
49
|
+
*
|
|
50
|
+
* @protected
|
|
51
|
+
*/
|
|
52
|
+
onClose() {
|
|
53
|
+
this.readyState = "closed";
|
|
54
|
+
this.emitReserved("close");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export class TransportError extends Error {
|
|
58
|
+
type = "TransportError";
|
|
59
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Transport } from "../transport";
|
|
2
|
+
import { type Packet } from "../parser";
|
|
3
|
+
export declare class Polling extends Transport {
|
|
4
|
+
private pollingPromise?;
|
|
5
|
+
get name(): string;
|
|
6
|
+
get upgradesTo(): string[];
|
|
7
|
+
onRequest(req: Request, responseHeaders: Headers): Promise<Response>;
|
|
8
|
+
/**
|
|
9
|
+
* The client sends a long-polling request awaiting the server to send data.
|
|
10
|
+
*
|
|
11
|
+
* @param req
|
|
12
|
+
* @param responseHeaders
|
|
13
|
+
* @private
|
|
14
|
+
*/
|
|
15
|
+
private onPollRequest;
|
|
16
|
+
/**
|
|
17
|
+
* The client sends a request with data.
|
|
18
|
+
*
|
|
19
|
+
* @param req
|
|
20
|
+
* @param responseHeaders
|
|
21
|
+
*/
|
|
22
|
+
private onDataRequest;
|
|
23
|
+
send(packets: Packet[]): void;
|
|
24
|
+
/**
|
|
25
|
+
* Writes data as response to long-polling request
|
|
26
|
+
*
|
|
27
|
+
* @param data
|
|
28
|
+
* @private
|
|
29
|
+
*/
|
|
30
|
+
private write;
|
|
31
|
+
protected doClose(): void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Transport } from "../transport";
|
|
2
|
+
import { Parser } from "../parser";
|
|
3
|
+
import { debuglog } from "node:util";
|
|
4
|
+
const debug = debuglog("engine.io:polling");
|
|
5
|
+
export class Polling extends Transport {
|
|
6
|
+
pollingPromise;
|
|
7
|
+
get name() {
|
|
8
|
+
return "polling";
|
|
9
|
+
}
|
|
10
|
+
get upgradesTo() {
|
|
11
|
+
return ["websocket"];
|
|
12
|
+
}
|
|
13
|
+
onRequest(req, responseHeaders) {
|
|
14
|
+
if (req.method === "GET") {
|
|
15
|
+
return this.onPollRequest(req, responseHeaders);
|
|
16
|
+
}
|
|
17
|
+
else if (req.method === "POST") {
|
|
18
|
+
return this.onDataRequest(req, responseHeaders);
|
|
19
|
+
}
|
|
20
|
+
return Promise.resolve(new Response(null, { status: 400, headers: responseHeaders }));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The client sends a long-polling request awaiting the server to send data.
|
|
24
|
+
*
|
|
25
|
+
* @param req
|
|
26
|
+
* @param responseHeaders
|
|
27
|
+
* @private
|
|
28
|
+
*/
|
|
29
|
+
onPollRequest(req, responseHeaders) {
|
|
30
|
+
if (this.pollingPromise) {
|
|
31
|
+
debug("request overlap");
|
|
32
|
+
this.onError("overlap from client");
|
|
33
|
+
return Promise.resolve(new Response(null, { status: 400, headers: responseHeaders }));
|
|
34
|
+
}
|
|
35
|
+
debug("new polling request");
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
this.pollingPromise = { resolve, reject, responseHeaders };
|
|
38
|
+
req.signal.addEventListener("abort", () => {
|
|
39
|
+
if (this.pollingPromise) {
|
|
40
|
+
this.pollingPromise = undefined;
|
|
41
|
+
this.writable = false;
|
|
42
|
+
}
|
|
43
|
+
this.onError("polling request aborted");
|
|
44
|
+
});
|
|
45
|
+
debug("transport is now writable");
|
|
46
|
+
this.writable = true;
|
|
47
|
+
this.emitReserved("drain");
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The client sends a request with data.
|
|
52
|
+
*
|
|
53
|
+
* @param req
|
|
54
|
+
* @param responseHeaders
|
|
55
|
+
*/
|
|
56
|
+
async onDataRequest(req, responseHeaders) {
|
|
57
|
+
debug("new data request");
|
|
58
|
+
req.signal.addEventListener("abort", () => {
|
|
59
|
+
this.onError("data request aborted");
|
|
60
|
+
});
|
|
61
|
+
const contentLength = req.headers.get("content-length");
|
|
62
|
+
if (contentLength &&
|
|
63
|
+
parseInt(contentLength, 10) > this.opts.maxHttpBufferSize) {
|
|
64
|
+
this.onError("payload too large");
|
|
65
|
+
return new Response(null, { status: 413, headers: responseHeaders });
|
|
66
|
+
}
|
|
67
|
+
const data = await req.text();
|
|
68
|
+
if (data.length > this.opts.maxHttpBufferSize) {
|
|
69
|
+
this.onError("payload too large");
|
|
70
|
+
return Promise.resolve(new Response(null, { status: 413, headers: responseHeaders }));
|
|
71
|
+
}
|
|
72
|
+
const packets = Parser.decodePayload(data);
|
|
73
|
+
debug(`decoded ${packets.length} packet(s)`);
|
|
74
|
+
for (const packet of packets) {
|
|
75
|
+
this.onPacket(packet);
|
|
76
|
+
}
|
|
77
|
+
return Promise.resolve(new Response("ok", {
|
|
78
|
+
status: 200,
|
|
79
|
+
headers: responseHeaders,
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
send(packets) {
|
|
83
|
+
this.writable = false;
|
|
84
|
+
this.write(Parser.encodePayload(packets));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Writes data as response to long-polling request
|
|
88
|
+
*
|
|
89
|
+
* @param data
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
write(data) {
|
|
93
|
+
debug(`writing ${data}`);
|
|
94
|
+
if (!this.pollingPromise) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const headers = this.pollingPromise.responseHeaders;
|
|
98
|
+
headers.set("Content-Type", "text/plain; charset=UTF-8");
|
|
99
|
+
this.pollingPromise.resolve(new Response(data, {
|
|
100
|
+
status: 200,
|
|
101
|
+
headers,
|
|
102
|
+
}));
|
|
103
|
+
this.pollingPromise = undefined;
|
|
104
|
+
}
|
|
105
|
+
doClose() {
|
|
106
|
+
if (this.writable) {
|
|
107
|
+
debug("transport writable - closing right away");
|
|
108
|
+
// if we have received a "close" packet from the client, then we can just send a "noop" packet back
|
|
109
|
+
this.send([{ type: this.readyState === "closing" ? "close" : "noop" }]);
|
|
110
|
+
}
|
|
111
|
+
this.onClose();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Transport } from "../transport";
|
|
2
|
+
import { type Packet, type RawData } from "../parser";
|
|
3
|
+
export type WebSocketData = {
|
|
4
|
+
transport: WS;
|
|
5
|
+
};
|
|
6
|
+
export type BunWebSocket = Bun.ServerWebSocket<WebSocketData>;
|
|
7
|
+
export declare class WS extends Transport {
|
|
8
|
+
private socket?;
|
|
9
|
+
get name(): string;
|
|
10
|
+
get upgradesTo(): string[];
|
|
11
|
+
send(packets: Packet[]): void;
|
|
12
|
+
protected doClose(): void;
|
|
13
|
+
onOpen(socket: BunWebSocket): void;
|
|
14
|
+
onMessage(message: RawData): void;
|
|
15
|
+
onCloseEvent(_code: number, _message: string): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Transport } from "../transport";
|
|
2
|
+
import { Parser } from "../parser";
|
|
3
|
+
import { debuglog } from "node:util";
|
|
4
|
+
const debug = debuglog("engine.io:websocket");
|
|
5
|
+
export class WS extends Transport {
|
|
6
|
+
socket;
|
|
7
|
+
get name() {
|
|
8
|
+
return "websocket";
|
|
9
|
+
}
|
|
10
|
+
get upgradesTo() {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
send(packets) {
|
|
14
|
+
for (const packet of packets) {
|
|
15
|
+
const data = Parser.encodePacket(packet, true);
|
|
16
|
+
if (this.writable && this.socket?.readyState === WebSocket.OPEN) {
|
|
17
|
+
// TODO use ws.cork() once https://github.com/oven-sh/bun/issues/21588 is resolved
|
|
18
|
+
this.socket.send(data);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
doClose() {
|
|
23
|
+
this.socket?.close();
|
|
24
|
+
}
|
|
25
|
+
onOpen(socket) {
|
|
26
|
+
debug("on open");
|
|
27
|
+
this.socket = socket;
|
|
28
|
+
this.writable = true;
|
|
29
|
+
}
|
|
30
|
+
onMessage(message) {
|
|
31
|
+
debug("on message");
|
|
32
|
+
this.onPacket(Parser.decodePacket(message));
|
|
33
|
+
}
|
|
34
|
+
onCloseEvent(_code, _message) {
|
|
35
|
+
debug("on close");
|
|
36
|
+
this.onClose();
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateId(): string;
|
package/dist/util.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rvncom/socket-bun-engine",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Engine.IO server implementation for Bun runtime",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"bun": ">=1.0.0"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"compile": "rm -rf ./dist && tsc",
|
|
22
|
+
"lint": "oxlint",
|
|
23
|
+
"format:check": "prettier --check \"lib/**/*.ts\" \"test/**/*.ts\"",
|
|
24
|
+
"format:fix": "prettier --write \"lib/**/*.ts\" \"test/**/*.ts\""
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/bun": "^1.3.0",
|
|
28
|
+
"oxlint": "latest",
|
|
29
|
+
"prettier": "^3.6.2",
|
|
30
|
+
"socket.io": "^4.8.1"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"typescript": "^5"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT"
|
|
36
|
+
}
|