@rivetkit/engine-runner 2.0.24-rc.1 → 2.0.25-rc.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 +10 -10
- package/dist/mod.cjs +1460 -812
- package/dist/mod.cjs.map +1 -1
- package/dist/mod.d.cts +263 -17
- package/dist/mod.d.ts +263 -17
- package/dist/mod.js +1454 -806
- package/dist/mod.js.map +1 -1
- package/package.json +2 -2
- package/src/actor.ts +196 -0
- package/src/mod.ts +409 -177
- package/src/stringify.ts +182 -12
- package/src/tunnel.ts +822 -428
- package/src/utils.ts +93 -0
- package/src/websocket-tunnel-adapter.ts +340 -357
- package/tests/utils.test.ts +194 -0
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// WebSocket-like adapter for tunneled connections
|
|
2
2
|
// Implements a subset of the WebSocket interface for compatibility with runner code
|
|
3
3
|
|
|
4
|
+
import type { Logger } from "pino";
|
|
4
5
|
import { logger } from "./log";
|
|
6
|
+
import type { Tunnel } from "./tunnel";
|
|
7
|
+
import { wrappingAddU16, wrappingLteU16, wrappingSubU16 } from "./utils";
|
|
8
|
+
|
|
9
|
+
export const HIBERNATABLE_SYMBOL = Symbol("hibernatable");
|
|
5
10
|
|
|
6
11
|
export class WebSocketTunnelAdapter {
|
|
7
|
-
|
|
12
|
+
// MARK: - WebSocket Compat Variables
|
|
8
13
|
#readyState: number = 0; // CONNECTING
|
|
9
14
|
#eventListeners: Map<string, Set<(event: any) => void>> = new Map();
|
|
10
15
|
#onopen: ((this: any, ev: any) => any) | null = null;
|
|
@@ -16,38 +21,340 @@ export class WebSocketTunnelAdapter {
|
|
|
16
21
|
#extensions = "";
|
|
17
22
|
#protocol = "";
|
|
18
23
|
#url = "";
|
|
24
|
+
|
|
25
|
+
// mARK: - Internal State
|
|
26
|
+
#tunnel: Tunnel;
|
|
27
|
+
#actorId: string;
|
|
28
|
+
#requestId: string;
|
|
29
|
+
#hibernatable: boolean;
|
|
30
|
+
#serverMessageIndex: number;
|
|
31
|
+
|
|
32
|
+
get [HIBERNATABLE_SYMBOL](): boolean {
|
|
33
|
+
return this.#hibernatable;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Called when sending a message from this WebSocket.
|
|
38
|
+
*
|
|
39
|
+
* Used to send a tunnel message from Tunnel.
|
|
40
|
+
*/
|
|
19
41
|
#sendCallback: (data: ArrayBuffer | string, isBinary: boolean) => void;
|
|
20
|
-
#closeCallback: (code?: number, reason?: string, retry?: boolean) => void;
|
|
21
|
-
#canHibernate: boolean = false;
|
|
22
42
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Called when closing this WebSocket.
|
|
45
|
+
*
|
|
46
|
+
* Used to send a tunnel message from Tunnel
|
|
47
|
+
*/
|
|
48
|
+
#closeCallback: (
|
|
49
|
+
code?: number,
|
|
50
|
+
reason?: string,
|
|
51
|
+
hibernate?: boolean,
|
|
52
|
+
) => void;
|
|
53
|
+
|
|
54
|
+
get #log(): Logger | undefined {
|
|
55
|
+
return this.#tunnel.log;
|
|
56
|
+
}
|
|
28
57
|
|
|
29
58
|
constructor(
|
|
30
|
-
|
|
59
|
+
tunnel: Tunnel,
|
|
60
|
+
actorId: string,
|
|
61
|
+
requestId: string,
|
|
62
|
+
serverMessageIndex: number,
|
|
63
|
+
hibernatable: boolean,
|
|
64
|
+
isRestoringHibernatable: boolean,
|
|
65
|
+
/** @experimental */
|
|
66
|
+
public readonly request: Request,
|
|
31
67
|
sendCallback: (data: ArrayBuffer | string, isBinary: boolean) => void,
|
|
32
|
-
closeCallback: (
|
|
33
|
-
code?: number,
|
|
34
|
-
reason?: string,
|
|
35
|
-
retry?: boolean,
|
|
36
|
-
) => void,
|
|
68
|
+
closeCallback: (code?: number, reason?: string) => void,
|
|
37
69
|
) {
|
|
38
|
-
this.#
|
|
70
|
+
this.#tunnel = tunnel;
|
|
71
|
+
this.#actorId = actorId;
|
|
72
|
+
this.#requestId = requestId;
|
|
73
|
+
this.#hibernatable = hibernatable;
|
|
74
|
+
this.#serverMessageIndex = serverMessageIndex;
|
|
39
75
|
this.#sendCallback = sendCallback;
|
|
40
76
|
this.#closeCallback = closeCallback;
|
|
41
|
-
}
|
|
42
77
|
|
|
43
|
-
|
|
44
|
-
|
|
78
|
+
// For restored WebSockets, immediately set to OPEN state
|
|
79
|
+
if (isRestoringHibernatable) {
|
|
80
|
+
this.#log?.debug({
|
|
81
|
+
msg: "setting WebSocket to OPEN state for restored connection",
|
|
82
|
+
actorId: this.#actorId,
|
|
83
|
+
requestId: this.#requestId,
|
|
84
|
+
hibernatable: this.#hibernatable,
|
|
85
|
+
});
|
|
86
|
+
this.#readyState = 1; // OPEN
|
|
87
|
+
}
|
|
45
88
|
}
|
|
46
89
|
|
|
90
|
+
// MARK: - Lifecycle
|
|
47
91
|
get bufferedAmount(): number {
|
|
48
92
|
return this.#bufferedAmount;
|
|
49
93
|
}
|
|
50
94
|
|
|
95
|
+
_handleOpen(requestId: ArrayBuffer): void {
|
|
96
|
+
if (this.#readyState !== 0) {
|
|
97
|
+
// CONNECTING
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.#readyState = 1; // OPEN
|
|
102
|
+
|
|
103
|
+
const event = {
|
|
104
|
+
type: "open",
|
|
105
|
+
rivetRequestId: requestId,
|
|
106
|
+
target: this,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
this.#fireEvent("open", event);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_handleMessage(
|
|
113
|
+
requestId: ArrayBuffer,
|
|
114
|
+
data: string | Uint8Array,
|
|
115
|
+
serverMessageIndex: number,
|
|
116
|
+
isBinary: boolean,
|
|
117
|
+
): boolean {
|
|
118
|
+
if (this.#readyState !== 1) {
|
|
119
|
+
this.#log?.warn({
|
|
120
|
+
msg: "WebSocket message ignored - not in OPEN state",
|
|
121
|
+
requestId: this.#requestId,
|
|
122
|
+
actorId: this.#actorId,
|
|
123
|
+
currentReadyState: this.#readyState,
|
|
124
|
+
expectedReadyState: 1,
|
|
125
|
+
serverMessageIndex: serverMessageIndex,
|
|
126
|
+
hibernatable: this.#hibernatable,
|
|
127
|
+
});
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Validate message index
|
|
132
|
+
if (this.#hibernatable) {
|
|
133
|
+
const previousIndex = this.#serverMessageIndex;
|
|
134
|
+
|
|
135
|
+
// Ignore duplicate old messages
|
|
136
|
+
//
|
|
137
|
+
// This should only happen if something goes wrong
|
|
138
|
+
// between persisting the previous index and acking the
|
|
139
|
+
// message index to the gateway. If the ack is never
|
|
140
|
+
// received by the gateway (due to a crash or network
|
|
141
|
+
// issue), the gateway will resend all messages from
|
|
142
|
+
// the last ack on reconnect.
|
|
143
|
+
if (wrappingLteU16(serverMessageIndex, previousIndex)) {
|
|
144
|
+
this.#log?.info({
|
|
145
|
+
msg: "received duplicate hibernating websocket message, this indicates the actor failed to ack the message index before restarting",
|
|
146
|
+
requestId,
|
|
147
|
+
actorId: this.#actorId,
|
|
148
|
+
previousIndex,
|
|
149
|
+
expectedIndex: wrappingAddU16(previousIndex, 1),
|
|
150
|
+
receivedIndex: serverMessageIndex,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Close message if skipped message in sequence
|
|
157
|
+
//
|
|
158
|
+
// There is no scenario where this should ever happen
|
|
159
|
+
const expectedIndex = wrappingAddU16(previousIndex, 1);
|
|
160
|
+
if (serverMessageIndex !== expectedIndex) {
|
|
161
|
+
const closeReason = "ws.message_index_skip";
|
|
162
|
+
|
|
163
|
+
this.#log?.warn({
|
|
164
|
+
msg: "hibernatable websocket message index out of sequence, closing connection",
|
|
165
|
+
requestId,
|
|
166
|
+
actorId: this.#actorId,
|
|
167
|
+
previousIndex,
|
|
168
|
+
expectedIndex,
|
|
169
|
+
receivedIndex: serverMessageIndex,
|
|
170
|
+
closeReason,
|
|
171
|
+
gap: wrappingSubU16(
|
|
172
|
+
wrappingSubU16(serverMessageIndex, previousIndex),
|
|
173
|
+
1,
|
|
174
|
+
),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Close the WebSocket and skip processing
|
|
178
|
+
this.close(1008, closeReason);
|
|
179
|
+
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Update to the next index
|
|
184
|
+
this.#serverMessageIndex = serverMessageIndex;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Dispatch event
|
|
188
|
+
let messageData: any;
|
|
189
|
+
if (isBinary) {
|
|
190
|
+
// Handle binary data based on binaryType
|
|
191
|
+
if (this.#binaryType === "nodebuffer") {
|
|
192
|
+
// Convert to Buffer for Node.js compatibility
|
|
193
|
+
messageData = Buffer.from(data as Uint8Array);
|
|
194
|
+
} else if (this.#binaryType === "arraybuffer") {
|
|
195
|
+
// Convert to ArrayBuffer
|
|
196
|
+
if (data instanceof Uint8Array) {
|
|
197
|
+
messageData = data.buffer.slice(
|
|
198
|
+
data.byteOffset,
|
|
199
|
+
data.byteOffset + data.byteLength,
|
|
200
|
+
);
|
|
201
|
+
} else {
|
|
202
|
+
messageData = data;
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
// Blob type - not commonly used in Node.js
|
|
206
|
+
throw new Error(
|
|
207
|
+
"Blob binaryType not supported in tunnel adapter",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
messageData = data;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const event = {
|
|
215
|
+
type: "message",
|
|
216
|
+
data: messageData,
|
|
217
|
+
rivetRequestId: requestId,
|
|
218
|
+
rivetMessageIndex: serverMessageIndex,
|
|
219
|
+
target: this,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
this.#fireEvent("message", event);
|
|
223
|
+
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_handleClose(
|
|
228
|
+
_requestId: ArrayBuffer,
|
|
229
|
+
code?: number,
|
|
230
|
+
reason?: string,
|
|
231
|
+
): void {
|
|
232
|
+
this.#closeInner(code, reason, true);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
_handleError(error: Error): void {
|
|
236
|
+
const event = {
|
|
237
|
+
type: "error",
|
|
238
|
+
target: this,
|
|
239
|
+
error,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
this.#fireEvent("error", event);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_closeWithoutCallback(code?: number, reason?: string): void {
|
|
246
|
+
this.#closeInner(code, reason, false);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#fireEvent(type: string, event: any): void {
|
|
250
|
+
// Call all registered event listeners
|
|
251
|
+
const listeners = this.#eventListeners.get(type);
|
|
252
|
+
|
|
253
|
+
if (listeners && listeners.size > 0) {
|
|
254
|
+
for (const listener of listeners) {
|
|
255
|
+
try {
|
|
256
|
+
listener.call(this, event);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
logger()?.error({
|
|
259
|
+
msg: "error in websocket event listener",
|
|
260
|
+
error,
|
|
261
|
+
type,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Call the onX property if set
|
|
268
|
+
switch (type) {
|
|
269
|
+
case "open":
|
|
270
|
+
if (this.#onopen) {
|
|
271
|
+
try {
|
|
272
|
+
this.#onopen.call(this, event);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
logger()?.error({
|
|
275
|
+
msg: "error in onopen handler",
|
|
276
|
+
error,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
break;
|
|
281
|
+
case "close":
|
|
282
|
+
if (this.#onclose) {
|
|
283
|
+
try {
|
|
284
|
+
this.#onclose.call(this, event);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
logger()?.error({
|
|
287
|
+
msg: "error in onclose handler",
|
|
288
|
+
error,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
case "error":
|
|
294
|
+
if (this.#onerror) {
|
|
295
|
+
try {
|
|
296
|
+
this.#onerror.call(this, event);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
logger()?.error({
|
|
299
|
+
msg: "error in onerror handler",
|
|
300
|
+
error,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
case "message":
|
|
306
|
+
if (this.#onmessage) {
|
|
307
|
+
try {
|
|
308
|
+
this.#onmessage.call(this, event);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
logger()?.error({
|
|
311
|
+
msg: "error in onmessage handler",
|
|
312
|
+
error,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#closeInner(
|
|
321
|
+
code: number | undefined,
|
|
322
|
+
reason: string | undefined,
|
|
323
|
+
callback: boolean,
|
|
324
|
+
): void {
|
|
325
|
+
if (
|
|
326
|
+
this.#readyState === 2 || // CLOSING
|
|
327
|
+
this.#readyState === 3 // CLOSED
|
|
328
|
+
) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
this.#readyState = 2; // CLOSING
|
|
333
|
+
|
|
334
|
+
// Send close through tunnel
|
|
335
|
+
if (callback) {
|
|
336
|
+
this.#closeCallback(code, reason);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Update state and fire event
|
|
340
|
+
this.#readyState = 3; // CLOSED
|
|
341
|
+
|
|
342
|
+
const closeEvent = {
|
|
343
|
+
wasClean: true,
|
|
344
|
+
code: code || 1000,
|
|
345
|
+
reason: reason || "",
|
|
346
|
+
type: "close",
|
|
347
|
+
target: this,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
this.#fireEvent("close", closeEvent);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// MARK: - WebSocket Compatible API
|
|
354
|
+
get readyState(): number {
|
|
355
|
+
return this.#readyState;
|
|
356
|
+
}
|
|
357
|
+
|
|
51
358
|
get binaryType(): string {
|
|
52
359
|
return this.#binaryType;
|
|
53
360
|
}
|
|
@@ -74,26 +381,12 @@ export class WebSocketTunnelAdapter {
|
|
|
74
381
|
return this.#url;
|
|
75
382
|
}
|
|
76
383
|
|
|
77
|
-
/** @experimental */
|
|
78
|
-
get canHibernate(): boolean {
|
|
79
|
-
return this.#canHibernate;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** @experimental */
|
|
83
|
-
set canHibernate(value: boolean) {
|
|
84
|
-
this.#canHibernate = value;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
384
|
get onopen(): ((this: any, ev: any) => any) | null {
|
|
88
385
|
return this.#onopen;
|
|
89
386
|
}
|
|
90
387
|
|
|
91
388
|
set onopen(value: ((this: any, ev: any) => any) | null) {
|
|
92
389
|
this.#onopen = value;
|
|
93
|
-
// Flush any buffered open events when onopen is set
|
|
94
|
-
if (value) {
|
|
95
|
-
this.#flushBufferedEvents("open");
|
|
96
|
-
}
|
|
97
390
|
}
|
|
98
391
|
|
|
99
392
|
get onclose(): ((this: any, ev: any) => any) | null {
|
|
@@ -102,10 +395,6 @@ export class WebSocketTunnelAdapter {
|
|
|
102
395
|
|
|
103
396
|
set onclose(value: ((this: any, ev: any) => any) | null) {
|
|
104
397
|
this.#onclose = value;
|
|
105
|
-
// Flush any buffered close events when onclose is set
|
|
106
|
-
if (value) {
|
|
107
|
-
this.#flushBufferedEvents("close");
|
|
108
|
-
}
|
|
109
398
|
}
|
|
110
399
|
|
|
111
400
|
get onerror(): ((this: any, ev: any) => any) | null {
|
|
@@ -114,10 +403,6 @@ export class WebSocketTunnelAdapter {
|
|
|
114
403
|
|
|
115
404
|
set onerror(value: ((this: any, ev: any) => any) | null) {
|
|
116
405
|
this.#onerror = value;
|
|
117
|
-
// Flush any buffered error events when onerror is set
|
|
118
|
-
if (value) {
|
|
119
|
-
this.#flushBufferedEvents("error");
|
|
120
|
-
}
|
|
121
406
|
}
|
|
122
407
|
|
|
123
408
|
get onmessage(): ((this: any, ev: any) => any) | null {
|
|
@@ -126,16 +411,21 @@ export class WebSocketTunnelAdapter {
|
|
|
126
411
|
|
|
127
412
|
set onmessage(value: ((this: any, ev: any) => any) | null) {
|
|
128
413
|
this.#onmessage = value;
|
|
129
|
-
// Flush any buffered message events when onmessage is set
|
|
130
|
-
if (value) {
|
|
131
|
-
this.#flushBufferedEvents("message");
|
|
132
|
-
}
|
|
133
414
|
}
|
|
134
415
|
|
|
135
416
|
send(data: string | ArrayBuffer | ArrayBufferView | Blob | Buffer): void {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
417
|
+
// Handle different ready states
|
|
418
|
+
if (this.#readyState === 0) {
|
|
419
|
+
// CONNECTING
|
|
420
|
+
throw new DOMException(
|
|
421
|
+
"WebSocket is still in CONNECTING state",
|
|
422
|
+
"InvalidStateError",
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (this.#readyState === 2 || this.#readyState === 3) {
|
|
427
|
+
// CLOSING or CLOSED - silently ignore
|
|
428
|
+
return;
|
|
139
429
|
}
|
|
140
430
|
|
|
141
431
|
let isBinary = false;
|
|
@@ -201,49 +491,7 @@ export class WebSocketTunnelAdapter {
|
|
|
201
491
|
}
|
|
202
492
|
|
|
203
493
|
close(code?: number, reason?: string): void {
|
|
204
|
-
this
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
__closeWithRetry(code?: number, reason?: string): void {
|
|
208
|
-
this.closeInner(code, reason, true, true);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
__closeWithoutCallback(code?: number, reason?: string): void {
|
|
212
|
-
this.closeInner(code, reason, false, false);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
closeInner(
|
|
216
|
-
code: number | undefined,
|
|
217
|
-
reason: string | undefined,
|
|
218
|
-
retry: boolean,
|
|
219
|
-
callback: boolean,
|
|
220
|
-
): void {
|
|
221
|
-
if (
|
|
222
|
-
this.#readyState === 2 || // CLOSING
|
|
223
|
-
this.#readyState === 3 // CLOSED
|
|
224
|
-
) {
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
this.#readyState = 2; // CLOSING
|
|
229
|
-
|
|
230
|
-
// Send close through tunnel
|
|
231
|
-
if (callback) {
|
|
232
|
-
this.#closeCallback(code, reason, retry);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Update state and fire event
|
|
236
|
-
this.#readyState = 3; // CLOSED
|
|
237
|
-
|
|
238
|
-
const closeEvent = {
|
|
239
|
-
wasClean: true,
|
|
240
|
-
code: code || 1000,
|
|
241
|
-
reason: reason || "",
|
|
242
|
-
type: "close",
|
|
243
|
-
target: this,
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
this.#fireEvent("close", closeEvent);
|
|
494
|
+
this.#closeInner(code, reason, true);
|
|
247
495
|
}
|
|
248
496
|
|
|
249
497
|
addEventListener(
|
|
@@ -258,9 +506,6 @@ export class WebSocketTunnelAdapter {
|
|
|
258
506
|
this.#eventListeners.set(type, listeners);
|
|
259
507
|
}
|
|
260
508
|
listeners.add(listener);
|
|
261
|
-
|
|
262
|
-
// Flush any buffered events for this type
|
|
263
|
-
this.#flushBufferedEvents(type);
|
|
264
509
|
}
|
|
265
510
|
}
|
|
266
511
|
|
|
@@ -278,278 +523,15 @@ export class WebSocketTunnelAdapter {
|
|
|
278
523
|
}
|
|
279
524
|
|
|
280
525
|
dispatchEvent(event: any): boolean {
|
|
281
|
-
//
|
|
526
|
+
// TODO:
|
|
282
527
|
return true;
|
|
283
528
|
}
|
|
284
529
|
|
|
285
|
-
#fireEvent(type: string, event: any): void {
|
|
286
|
-
// Call all registered event listeners
|
|
287
|
-
const listeners = this.#eventListeners.get(type);
|
|
288
|
-
let hasListeners = false;
|
|
289
|
-
|
|
290
|
-
if (listeners && listeners.size > 0) {
|
|
291
|
-
hasListeners = true;
|
|
292
|
-
for (const listener of listeners) {
|
|
293
|
-
try {
|
|
294
|
-
listener.call(this, event);
|
|
295
|
-
} catch (error) {
|
|
296
|
-
logger()?.error({
|
|
297
|
-
msg: "error in websocket event listener",
|
|
298
|
-
error,
|
|
299
|
-
type,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Call the onX property if set
|
|
306
|
-
switch (type) {
|
|
307
|
-
case "open":
|
|
308
|
-
if (this.#onopen) {
|
|
309
|
-
hasListeners = true;
|
|
310
|
-
try {
|
|
311
|
-
this.#onopen.call(this, event);
|
|
312
|
-
} catch (error) {
|
|
313
|
-
logger()?.error({
|
|
314
|
-
msg: "error in onopen handler",
|
|
315
|
-
error,
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
break;
|
|
320
|
-
case "close":
|
|
321
|
-
if (this.#onclose) {
|
|
322
|
-
hasListeners = true;
|
|
323
|
-
try {
|
|
324
|
-
this.#onclose.call(this, event);
|
|
325
|
-
} catch (error) {
|
|
326
|
-
logger()?.error({
|
|
327
|
-
msg: "error in onclose handler",
|
|
328
|
-
error,
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
break;
|
|
333
|
-
case "error":
|
|
334
|
-
if (this.#onerror) {
|
|
335
|
-
hasListeners = true;
|
|
336
|
-
try {
|
|
337
|
-
this.#onerror.call(this, event);
|
|
338
|
-
} catch (error) {
|
|
339
|
-
logger()?.error({
|
|
340
|
-
msg: "error in onerror handler",
|
|
341
|
-
error,
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
break;
|
|
346
|
-
case "message":
|
|
347
|
-
if (this.#onmessage) {
|
|
348
|
-
hasListeners = true;
|
|
349
|
-
try {
|
|
350
|
-
this.#onmessage.call(this, event);
|
|
351
|
-
} catch (error) {
|
|
352
|
-
logger()?.error({
|
|
353
|
-
msg: "error in onmessage handler",
|
|
354
|
-
error,
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Buffer the event if no listeners are registered
|
|
362
|
-
if (!hasListeners) {
|
|
363
|
-
this.#bufferedEvents.push({ type, event });
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
#flushBufferedEvents(type: string): void {
|
|
368
|
-
const eventsToFlush = this.#bufferedEvents.filter(
|
|
369
|
-
(buffered) => buffered.type === type,
|
|
370
|
-
);
|
|
371
|
-
this.#bufferedEvents = this.#bufferedEvents.filter(
|
|
372
|
-
(buffered) => buffered.type !== type,
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
for (const { event } of eventsToFlush) {
|
|
376
|
-
// Re-fire the event, which will now have listeners
|
|
377
|
-
const listeners = this.#eventListeners.get(type);
|
|
378
|
-
if (listeners) {
|
|
379
|
-
for (const listener of listeners) {
|
|
380
|
-
try {
|
|
381
|
-
listener.call(this, event);
|
|
382
|
-
} catch (error) {
|
|
383
|
-
logger()?.error({
|
|
384
|
-
msg: "error in websocket event listener",
|
|
385
|
-
error,
|
|
386
|
-
type,
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Also call the onX handler if it exists
|
|
393
|
-
switch (type) {
|
|
394
|
-
case "open":
|
|
395
|
-
if (this.#onopen) {
|
|
396
|
-
try {
|
|
397
|
-
this.#onopen.call(this, event);
|
|
398
|
-
} catch (error) {
|
|
399
|
-
logger()?.error({
|
|
400
|
-
msg: "error in onopen handler",
|
|
401
|
-
error,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
break;
|
|
406
|
-
case "close":
|
|
407
|
-
if (this.#onclose) {
|
|
408
|
-
try {
|
|
409
|
-
this.#onclose.call(this, event);
|
|
410
|
-
} catch (error) {
|
|
411
|
-
logger()?.error({
|
|
412
|
-
msg: "error in onclose handler",
|
|
413
|
-
error,
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
break;
|
|
418
|
-
case "error":
|
|
419
|
-
if (this.#onerror) {
|
|
420
|
-
try {
|
|
421
|
-
this.#onerror.call(this, event);
|
|
422
|
-
} catch (error) {
|
|
423
|
-
logger()?.error({
|
|
424
|
-
msg: "error in onerror handler",
|
|
425
|
-
error,
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
break;
|
|
430
|
-
case "message":
|
|
431
|
-
if (this.#onmessage) {
|
|
432
|
-
try {
|
|
433
|
-
this.#onmessage.call(this, event);
|
|
434
|
-
} catch (error) {
|
|
435
|
-
logger()?.error({
|
|
436
|
-
msg: "error in onmessage handler",
|
|
437
|
-
error,
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
break;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Internal methods called by the Tunnel class
|
|
447
|
-
_handleOpen(requestId: ArrayBuffer): void {
|
|
448
|
-
if (this.#readyState !== 0) {
|
|
449
|
-
// CONNECTING
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
this.#readyState = 1; // OPEN
|
|
454
|
-
|
|
455
|
-
const event = {
|
|
456
|
-
type: "open",
|
|
457
|
-
rivetRequestId: requestId,
|
|
458
|
-
target: this,
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
this.#fireEvent("open", event);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/// Returns false if the message was sent off.
|
|
465
|
-
_handleMessage(
|
|
466
|
-
requestId: ArrayBuffer,
|
|
467
|
-
data: string | Uint8Array,
|
|
468
|
-
index: number,
|
|
469
|
-
isBinary: boolean,
|
|
470
|
-
): boolean {
|
|
471
|
-
if (this.#readyState !== 1) {
|
|
472
|
-
// OPEN
|
|
473
|
-
return true;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
let messageData: any;
|
|
477
|
-
|
|
478
|
-
if (isBinary) {
|
|
479
|
-
// Handle binary data based on binaryType
|
|
480
|
-
if (this.#binaryType === "nodebuffer") {
|
|
481
|
-
// Convert to Buffer for Node.js compatibility
|
|
482
|
-
messageData = Buffer.from(data as Uint8Array);
|
|
483
|
-
} else if (this.#binaryType === "arraybuffer") {
|
|
484
|
-
// Convert to ArrayBuffer
|
|
485
|
-
if (data instanceof Uint8Array) {
|
|
486
|
-
messageData = data.buffer.slice(
|
|
487
|
-
data.byteOffset,
|
|
488
|
-
data.byteOffset + data.byteLength,
|
|
489
|
-
);
|
|
490
|
-
} else {
|
|
491
|
-
messageData = data;
|
|
492
|
-
}
|
|
493
|
-
} else {
|
|
494
|
-
// Blob type - not commonly used in Node.js
|
|
495
|
-
throw new Error(
|
|
496
|
-
"Blob binaryType not supported in tunnel adapter",
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
} else {
|
|
500
|
-
messageData = data;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const event = {
|
|
504
|
-
type: "message",
|
|
505
|
-
data: messageData,
|
|
506
|
-
rivetRequestId: requestId,
|
|
507
|
-
rivetMessageIndex: index,
|
|
508
|
-
target: this,
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
this.#fireEvent("message", event);
|
|
512
|
-
|
|
513
|
-
return false;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
_handleClose(requestId: ArrayBuffer, code?: number, reason?: string): void {
|
|
517
|
-
if (this.#readyState === 3) {
|
|
518
|
-
// CLOSED
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
this.#readyState = 3; // CLOSED
|
|
523
|
-
|
|
524
|
-
const event = {
|
|
525
|
-
type: "close",
|
|
526
|
-
wasClean: true,
|
|
527
|
-
code: code || 1000,
|
|
528
|
-
reason: reason || "",
|
|
529
|
-
rivetRequestId: requestId,
|
|
530
|
-
target: this,
|
|
531
|
-
};
|
|
532
|
-
|
|
533
|
-
this.#fireEvent("close", event);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
_handleError(error: Error): void {
|
|
537
|
-
const event = {
|
|
538
|
-
type: "error",
|
|
539
|
-
target: this,
|
|
540
|
-
error,
|
|
541
|
-
};
|
|
542
|
-
|
|
543
|
-
this.#fireEvent("error", event);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// WebSocket constants for compatibility
|
|
547
530
|
static readonly CONNECTING = 0;
|
|
548
531
|
static readonly OPEN = 1;
|
|
549
532
|
static readonly CLOSING = 2;
|
|
550
533
|
static readonly CLOSED = 3;
|
|
551
534
|
|
|
552
|
-
// Instance constants
|
|
553
535
|
readonly CONNECTING = 0;
|
|
554
536
|
readonly OPEN = 1;
|
|
555
537
|
readonly CLOSING = 2;
|
|
@@ -566,6 +548,7 @@ export class WebSocketTunnelAdapter {
|
|
|
566
548
|
if (cb) cb(new Error("Pong not supported in tunnel adapter"));
|
|
567
549
|
}
|
|
568
550
|
|
|
551
|
+
/** @experimental */
|
|
569
552
|
terminate(): void {
|
|
570
553
|
// Immediate close without close frame
|
|
571
554
|
this.#readyState = 3; // CLOSED
|