@rivetkit/engine-runner 2.0.22 → 2.0.24-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 +339 -150
- package/dist/mod.cjs.map +1 -1
- package/dist/mod.d.cts +1 -0
- package/dist/mod.d.ts +1 -0
- package/dist/mod.js +339 -150
- package/dist/mod.js.map +1 -1
- package/package.json +2 -2
- package/src/mod.ts +131 -102
- package/src/stringify.ts +184 -0
- package/src/tunnel.ts +92 -32
- package/src/utils.ts +35 -0
- package/src/websocket-tunnel-adapter.ts +26 -4
package/src/stringify.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type * as protocol from "@rivetkit/engine-runner-protocol";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper function to stringify ArrayBuffer for logging
|
|
5
|
+
*/
|
|
6
|
+
function stringifyArrayBuffer(buffer: ArrayBuffer): string {
|
|
7
|
+
return `ArrayBuffer(${buffer.byteLength})`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper function to stringify bigint for logging
|
|
12
|
+
*/
|
|
13
|
+
function stringifyBigInt(value: bigint): string {
|
|
14
|
+
return `${value}n`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Helper function to stringify Map for logging
|
|
19
|
+
*/
|
|
20
|
+
function stringifyMap(map: ReadonlyMap<string, string>): string {
|
|
21
|
+
const entries = Array.from(map.entries())
|
|
22
|
+
.map(([k, v]) => `"${k}": "${v}"`)
|
|
23
|
+
.join(", ");
|
|
24
|
+
return `Map(${map.size}){${entries}}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Stringify ToServerTunnelMessageKind for logging
|
|
29
|
+
* Handles ArrayBuffers, BigInts, and Maps that can't be JSON.stringified
|
|
30
|
+
*/
|
|
31
|
+
export function stringifyToServerTunnelMessageKind(
|
|
32
|
+
kind: protocol.ToServerTunnelMessageKind,
|
|
33
|
+
): string {
|
|
34
|
+
switch (kind.tag) {
|
|
35
|
+
case "TunnelAck":
|
|
36
|
+
return "TunnelAck";
|
|
37
|
+
case "ToServerResponseStart": {
|
|
38
|
+
const { status, headers, body, stream } = kind.val;
|
|
39
|
+
const bodyStr = body === null ? "null" : stringifyArrayBuffer(body);
|
|
40
|
+
return `ToServerResponseStart{status: ${status}, headers: ${stringifyMap(headers)}, body: ${bodyStr}, stream: ${stream}}`;
|
|
41
|
+
}
|
|
42
|
+
case "ToServerResponseChunk": {
|
|
43
|
+
const { body, finish } = kind.val;
|
|
44
|
+
return `ToServerResponseChunk{body: ${stringifyArrayBuffer(body)}, finish: ${finish}}`;
|
|
45
|
+
}
|
|
46
|
+
case "ToServerResponseAbort":
|
|
47
|
+
return "ToServerResponseAbort";
|
|
48
|
+
case "ToServerWebSocketOpen": {
|
|
49
|
+
const { canHibernate, lastMsgIndex } = kind.val;
|
|
50
|
+
return `ToServerWebSocketOpen{canHibernate: ${canHibernate}, lastMsgIndex: ${stringifyBigInt(lastMsgIndex)}}`;
|
|
51
|
+
}
|
|
52
|
+
case "ToServerWebSocketMessage": {
|
|
53
|
+
const { data, binary } = kind.val;
|
|
54
|
+
return `ToServerWebSocketMessage{data: ${stringifyArrayBuffer(data)}, binary: ${binary}}`;
|
|
55
|
+
}
|
|
56
|
+
case "ToServerWebSocketMessageAck": {
|
|
57
|
+
const { index } = kind.val;
|
|
58
|
+
return `ToServerWebSocketMessageAck{index: ${index}}`;
|
|
59
|
+
}
|
|
60
|
+
case "ToServerWebSocketClose": {
|
|
61
|
+
const { code, reason, retry } = kind.val;
|
|
62
|
+
const codeStr = code === null ? "null" : code.toString();
|
|
63
|
+
const reasonStr = reason === null ? "null" : `"${reason}"`;
|
|
64
|
+
return `ToServerWebSocketClose{code: ${codeStr}, reason: ${reasonStr}, retry: ${retry}}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Stringify ToClientTunnelMessageKind for logging
|
|
71
|
+
* Handles ArrayBuffers, BigInts, and Maps that can't be JSON.stringified
|
|
72
|
+
*/
|
|
73
|
+
export function stringifyToClientTunnelMessageKind(
|
|
74
|
+
kind: protocol.ToClientTunnelMessageKind,
|
|
75
|
+
): string {
|
|
76
|
+
switch (kind.tag) {
|
|
77
|
+
case "TunnelAck":
|
|
78
|
+
return "TunnelAck";
|
|
79
|
+
case "ToClientRequestStart": {
|
|
80
|
+
const { actorId, method, path, headers, body, stream } = kind.val;
|
|
81
|
+
const bodyStr = body === null ? "null" : stringifyArrayBuffer(body);
|
|
82
|
+
return `ToClientRequestStart{actorId: "${actorId}", method: "${method}", path: "${path}", headers: ${stringifyMap(headers)}, body: ${bodyStr}, stream: ${stream}}`;
|
|
83
|
+
}
|
|
84
|
+
case "ToClientRequestChunk": {
|
|
85
|
+
const { body, finish } = kind.val;
|
|
86
|
+
return `ToClientRequestChunk{body: ${stringifyArrayBuffer(body)}, finish: ${finish}}`;
|
|
87
|
+
}
|
|
88
|
+
case "ToClientRequestAbort":
|
|
89
|
+
return "ToClientRequestAbort";
|
|
90
|
+
case "ToClientWebSocketOpen": {
|
|
91
|
+
const { actorId, path, headers } = kind.val;
|
|
92
|
+
return `ToClientWebSocketOpen{actorId: "${actorId}", path: "${path}", headers: ${stringifyMap(headers)}}`;
|
|
93
|
+
}
|
|
94
|
+
case "ToClientWebSocketMessage": {
|
|
95
|
+
const { index, data, binary } = kind.val;
|
|
96
|
+
return `ToClientWebSocketMessage{index: ${index}, data: ${stringifyArrayBuffer(data)}, binary: ${binary}}`;
|
|
97
|
+
}
|
|
98
|
+
case "ToClientWebSocketClose": {
|
|
99
|
+
const { code, reason } = kind.val;
|
|
100
|
+
const codeStr = code === null ? "null" : code.toString();
|
|
101
|
+
const reasonStr = reason === null ? "null" : `"${reason}"`;
|
|
102
|
+
return `ToClientWebSocketClose{code: ${codeStr}, reason: ${reasonStr}}`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Stringify Command for logging
|
|
109
|
+
* Handles ArrayBuffers, BigInts, and Maps that can't be JSON.stringified
|
|
110
|
+
*/
|
|
111
|
+
export function stringifyCommand(command: protocol.Command): string {
|
|
112
|
+
switch (command.tag) {
|
|
113
|
+
case "CommandStartActor": {
|
|
114
|
+
const { actorId, generation, config } = command.val;
|
|
115
|
+
const keyStr = config.key === null ? "null" : `"${config.key}"`;
|
|
116
|
+
const inputStr =
|
|
117
|
+
config.input === null
|
|
118
|
+
? "null"
|
|
119
|
+
: stringifyArrayBuffer(config.input);
|
|
120
|
+
return `CommandStartActor{actorId: "${actorId}", generation: ${generation}, config: {name: "${config.name}", key: ${keyStr}, createTs: ${stringifyBigInt(config.createTs)}, input: ${inputStr}}}`;
|
|
121
|
+
}
|
|
122
|
+
case "CommandStopActor": {
|
|
123
|
+
const { actorId, generation } = command.val;
|
|
124
|
+
return `CommandStopActor{actorId: "${actorId}", generation: ${generation}}`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Stringify CommandWrapper for logging
|
|
131
|
+
* Handles ArrayBuffers, BigInts, and Maps that can't be JSON.stringified
|
|
132
|
+
*/
|
|
133
|
+
export function stringifyCommandWrapper(
|
|
134
|
+
wrapper: protocol.CommandWrapper,
|
|
135
|
+
): string {
|
|
136
|
+
return `CommandWrapper{index: ${stringifyBigInt(wrapper.index)}, inner: ${stringifyCommand(wrapper.inner)}}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Stringify Event for logging
|
|
141
|
+
* Handles ArrayBuffers, BigInts, and Maps that can't be JSON.stringified
|
|
142
|
+
*/
|
|
143
|
+
export function stringifyEvent(event: protocol.Event): string {
|
|
144
|
+
switch (event.tag) {
|
|
145
|
+
case "EventActorIntent": {
|
|
146
|
+
const { actorId, generation, intent } = event.val;
|
|
147
|
+
const intentStr =
|
|
148
|
+
intent.tag === "ActorIntentSleep"
|
|
149
|
+
? "Sleep"
|
|
150
|
+
: intent.tag === "ActorIntentStop"
|
|
151
|
+
? "Stop"
|
|
152
|
+
: "Unknown";
|
|
153
|
+
return `EventActorIntent{actorId: "${actorId}", generation: ${generation}, intent: ${intentStr}}`;
|
|
154
|
+
}
|
|
155
|
+
case "EventActorStateUpdate": {
|
|
156
|
+
const { actorId, generation, state } = event.val;
|
|
157
|
+
let stateStr: string;
|
|
158
|
+
if (state.tag === "ActorStateRunning") {
|
|
159
|
+
stateStr = "Running";
|
|
160
|
+
} else if (state.tag === "ActorStateStopped") {
|
|
161
|
+
const { code, message } = state.val;
|
|
162
|
+
const messageStr = message === null ? "null" : `"${message}"`;
|
|
163
|
+
stateStr = `Stopped{code: ${code}, message: ${messageStr}}`;
|
|
164
|
+
} else {
|
|
165
|
+
stateStr = "Unknown";
|
|
166
|
+
}
|
|
167
|
+
return `EventActorStateUpdate{actorId: "${actorId}", generation: ${generation}, state: ${stateStr}}`;
|
|
168
|
+
}
|
|
169
|
+
case "EventActorSetAlarm": {
|
|
170
|
+
const { actorId, generation, alarmTs } = event.val;
|
|
171
|
+
const alarmTsStr =
|
|
172
|
+
alarmTs === null ? "null" : stringifyBigInt(alarmTs);
|
|
173
|
+
return `EventActorSetAlarm{actorId: "${actorId}", generation: ${generation}, alarmTs: ${alarmTsStr}}`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Stringify EventWrapper for logging
|
|
180
|
+
* Handles ArrayBuffers, BigInts, and Maps that can't be JSON.stringified
|
|
181
|
+
*/
|
|
182
|
+
export function stringifyEventWrapper(wrapper: protocol.EventWrapper): string {
|
|
183
|
+
return `EventWrapper{index: ${stringifyBigInt(wrapper.index)}, inner: ${stringifyEvent(wrapper.inner)}}`;
|
|
184
|
+
}
|
package/src/tunnel.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type * as protocol from "@rivetkit/engine-runner-protocol";
|
|
2
2
|
import type { MessageId, RequestId } from "@rivetkit/engine-runner-protocol";
|
|
3
|
+
import type { Logger } from "pino";
|
|
3
4
|
import { stringify as uuidstringify, v4 as uuidv4 } from "uuid";
|
|
4
5
|
import { logger } from "./log";
|
|
5
6
|
import type { ActorInstance, Runner } from "./mod";
|
|
7
|
+
import {
|
|
8
|
+
stringifyToClientTunnelMessageKind,
|
|
9
|
+
stringifyToServerTunnelMessageKind,
|
|
10
|
+
} from "./stringify";
|
|
6
11
|
import { unreachable } from "./utils";
|
|
7
12
|
import { WebSocketTunnelAdapter } from "./websocket-tunnel-adapter";
|
|
8
13
|
|
|
@@ -22,6 +27,12 @@ interface PendingTunnelMessage {
|
|
|
22
27
|
requestIdStr: string;
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
class RunnerShutdownError extends Error {
|
|
31
|
+
constructor() {
|
|
32
|
+
super("Runner shut down");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
export class Tunnel {
|
|
26
37
|
#runner: Runner;
|
|
27
38
|
|
|
@@ -35,6 +46,10 @@ export class Tunnel {
|
|
|
35
46
|
|
|
36
47
|
#gcInterval?: NodeJS.Timeout;
|
|
37
48
|
|
|
49
|
+
get log(): Logger | undefined {
|
|
50
|
+
return this.#runner.log;
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
constructor(runner: Runner) {
|
|
39
54
|
this.#runner = runner;
|
|
40
55
|
}
|
|
@@ -44,20 +59,34 @@ export class Tunnel {
|
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
shutdown() {
|
|
62
|
+
// NOTE: Pegboard WS already closed at this point, cannot send
|
|
63
|
+
// anything. All teardown logic is handled by pegboard-runner.
|
|
64
|
+
|
|
47
65
|
if (this.#gcInterval) {
|
|
48
66
|
clearInterval(this.#gcInterval);
|
|
49
67
|
this.#gcInterval = undefined;
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
// Reject all pending requests
|
|
71
|
+
//
|
|
72
|
+
// RunnerShutdownError will be explicitly ignored
|
|
53
73
|
for (const [_, request] of this.#actorPendingRequests) {
|
|
54
|
-
request.reject(new
|
|
74
|
+
request.reject(new RunnerShutdownError());
|
|
55
75
|
}
|
|
56
76
|
this.#actorPendingRequests.clear();
|
|
57
77
|
|
|
58
78
|
// Close all WebSockets
|
|
79
|
+
//
|
|
80
|
+
// The WebSocket close event with retry is automatically sent when the
|
|
81
|
+
// runner WS closes, so we only need to notify the client that the WS
|
|
82
|
+
// closed:
|
|
83
|
+
// https://github.com/rivet-dev/rivet/blob/00d4f6a22da178a6f8115e5db50d96c6f8387c2e/engine/packages/pegboard-runner/src/lib.rs#L157
|
|
59
84
|
for (const [_, ws] of this.#actorWebSockets) {
|
|
60
|
-
|
|
85
|
+
// Only close non-hibernatable websockets to prevent sending
|
|
86
|
+
// unnecessary close messages for websockets that will be hibernated
|
|
87
|
+
if (!ws.canHibernate) {
|
|
88
|
+
ws.__closeWithoutCallback(1000, "ws.tunnel_shutdown");
|
|
89
|
+
}
|
|
61
90
|
}
|
|
62
91
|
this.#actorWebSockets.clear();
|
|
63
92
|
}
|
|
@@ -68,9 +97,11 @@ export class Tunnel {
|
|
|
68
97
|
) {
|
|
69
98
|
// TODO: Switch this with runner WS
|
|
70
99
|
if (!this.#runner.__webSocketReady()) {
|
|
71
|
-
|
|
72
|
-
"cannot send tunnel message, socket not connected to engine",
|
|
73
|
-
|
|
100
|
+
this.log?.warn({
|
|
101
|
+
msg: "cannot send tunnel message, socket not connected to engine",
|
|
102
|
+
requestId: idToStr(requestId),
|
|
103
|
+
message: stringifyToServerTunnelMessageKind(messageKind),
|
|
104
|
+
});
|
|
74
105
|
return;
|
|
75
106
|
}
|
|
76
107
|
|
|
@@ -84,11 +115,11 @@ export class Tunnel {
|
|
|
84
115
|
requestIdStr,
|
|
85
116
|
});
|
|
86
117
|
|
|
87
|
-
|
|
118
|
+
this.log?.debug({
|
|
88
119
|
msg: "send tunnel msg",
|
|
89
120
|
requestId: requestIdStr,
|
|
90
121
|
messageId: messageIdStr,
|
|
91
|
-
message: messageKind,
|
|
122
|
+
message: stringifyToServerTunnelMessageKind(messageKind),
|
|
92
123
|
});
|
|
93
124
|
|
|
94
125
|
// Send message
|
|
@@ -117,7 +148,7 @@ export class Tunnel {
|
|
|
117
148
|
},
|
|
118
149
|
};
|
|
119
150
|
|
|
120
|
-
|
|
151
|
+
this.log?.debug({
|
|
121
152
|
msg: "ack tunnel msg",
|
|
122
153
|
requestId: idToStr(requestId),
|
|
123
154
|
messageId: idToStr(messageId),
|
|
@@ -184,7 +215,7 @@ export class Tunnel {
|
|
|
184
215
|
|
|
185
216
|
// Remove timed out messages
|
|
186
217
|
if (messagesToDelete.length > 0) {
|
|
187
|
-
|
|
218
|
+
this.log?.warn({
|
|
188
219
|
msg: "purging unacked tunnel messages, this indicates that the Rivet Engine is disconnected or not responding",
|
|
189
220
|
count: messagesToDelete.length,
|
|
190
221
|
});
|
|
@@ -208,11 +239,11 @@ export class Tunnel {
|
|
|
208
239
|
actor.requests.clear();
|
|
209
240
|
|
|
210
241
|
// Flush acks and close all WebSockets for this actor
|
|
211
|
-
for (const
|
|
212
|
-
const ws = this.#actorWebSockets.get(
|
|
242
|
+
for (const requestIdStr of actor.webSockets) {
|
|
243
|
+
const ws = this.#actorWebSockets.get(requestIdStr);
|
|
213
244
|
if (ws) {
|
|
214
245
|
ws.__closeWithRetry(1000, "Actor stopped");
|
|
215
|
-
this.#actorWebSockets.delete(
|
|
246
|
+
this.#actorWebSockets.delete(requestIdStr);
|
|
216
247
|
}
|
|
217
248
|
}
|
|
218
249
|
actor.webSockets.clear();
|
|
@@ -225,7 +256,7 @@ export class Tunnel {
|
|
|
225
256
|
): Promise<Response> {
|
|
226
257
|
// Validate actor exists
|
|
227
258
|
if (!this.#runner.hasActor(actorId)) {
|
|
228
|
-
|
|
259
|
+
this.log?.warn({
|
|
229
260
|
msg: "ignoring request for unknown actor",
|
|
230
261
|
actorId,
|
|
231
262
|
});
|
|
@@ -257,11 +288,11 @@ export class Tunnel {
|
|
|
257
288
|
async handleTunnelMessage(message: protocol.ToClientTunnelMessage) {
|
|
258
289
|
const requestIdStr = idToStr(message.requestId);
|
|
259
290
|
const messageIdStr = idToStr(message.messageId);
|
|
260
|
-
|
|
291
|
+
this.log?.debug({
|
|
261
292
|
msg: "receive tunnel msg",
|
|
262
293
|
requestId: requestIdStr,
|
|
263
294
|
messageId: messageIdStr,
|
|
264
|
-
message: message.messageKind,
|
|
295
|
+
message: stringifyToClientTunnelMessageKind(message.messageKind),
|
|
265
296
|
});
|
|
266
297
|
|
|
267
298
|
if (message.messageKind.tag === "TunnelAck") {
|
|
@@ -271,7 +302,7 @@ export class Tunnel {
|
|
|
271
302
|
const didDelete =
|
|
272
303
|
this.#pendingTunnelMessages.delete(messageIdStr);
|
|
273
304
|
if (!didDelete) {
|
|
274
|
-
|
|
305
|
+
this.log?.warn({
|
|
275
306
|
msg: "received tunnel ack for nonexistent message",
|
|
276
307
|
requestId: requestIdStr,
|
|
277
308
|
messageId: messageIdStr,
|
|
@@ -402,8 +433,16 @@ export class Tunnel {
|
|
|
402
433
|
await this.#sendResponse(requestId, response);
|
|
403
434
|
}
|
|
404
435
|
} catch (error) {
|
|
405
|
-
|
|
406
|
-
|
|
436
|
+
if (error instanceof RunnerShutdownError) {
|
|
437
|
+
this.log?.debug({ msg: "catught runner shutdown error" });
|
|
438
|
+
} else {
|
|
439
|
+
this.log?.error({ msg: "error handling request", error });
|
|
440
|
+
this.#sendResponseError(
|
|
441
|
+
requestId,
|
|
442
|
+
500,
|
|
443
|
+
"Internal Server Error",
|
|
444
|
+
);
|
|
445
|
+
}
|
|
407
446
|
} finally {
|
|
408
447
|
// Clean up request tracking
|
|
409
448
|
const actor = this.#runner.getActor(req.actorId);
|
|
@@ -493,11 +532,12 @@ export class Tunnel {
|
|
|
493
532
|
requestId: protocol.RequestId,
|
|
494
533
|
open: protocol.ToClientWebSocketOpen,
|
|
495
534
|
) {
|
|
496
|
-
const
|
|
535
|
+
const requestIdStr = idToStr(requestId);
|
|
536
|
+
|
|
497
537
|
// Validate actor exists
|
|
498
538
|
const actor = this.#runner.getActor(open.actorId);
|
|
499
539
|
if (!actor) {
|
|
500
|
-
|
|
540
|
+
this.log?.warn({
|
|
501
541
|
msg: "ignoring websocket for unknown actor",
|
|
502
542
|
actorId: open.actorId,
|
|
503
543
|
});
|
|
@@ -521,7 +561,7 @@ export class Tunnel {
|
|
|
521
561
|
const websocketHandler = this.#runner.config.websocket;
|
|
522
562
|
|
|
523
563
|
if (!websocketHandler) {
|
|
524
|
-
|
|
564
|
+
this.log?.error({
|
|
525
565
|
msg: "no websocket handler configured for tunnel",
|
|
526
566
|
});
|
|
527
567
|
// Send close immediately
|
|
@@ -536,15 +576,33 @@ export class Tunnel {
|
|
|
536
576
|
return;
|
|
537
577
|
}
|
|
538
578
|
|
|
579
|
+
// Close existing WebSocket if one already exists for this request ID.
|
|
580
|
+
// There should always be a close message sent before another open
|
|
581
|
+
// message for the same message ID.
|
|
582
|
+
//
|
|
583
|
+
// This should never occur if all is functioning correctly, but this
|
|
584
|
+
// prevents any edge case that would result in duplicate WebSockets for
|
|
585
|
+
// the same request.
|
|
586
|
+
const existingAdapter = this.#actorWebSockets.get(requestIdStr);
|
|
587
|
+
if (existingAdapter) {
|
|
588
|
+
this.log?.warn({
|
|
589
|
+
msg: "closing existing websocket for duplicate open event for the same request id",
|
|
590
|
+
requestId: requestIdStr,
|
|
591
|
+
});
|
|
592
|
+
// Close without sending a message through the tunnel since the server
|
|
593
|
+
// already knows about the new connection
|
|
594
|
+
existingAdapter.__closeWithoutCallback(1000, "ws.duplicate_open");
|
|
595
|
+
}
|
|
596
|
+
|
|
539
597
|
// Track this WebSocket for the actor
|
|
540
598
|
if (actor) {
|
|
541
|
-
actor.webSockets.add(
|
|
599
|
+
actor.webSockets.add(requestIdStr);
|
|
542
600
|
}
|
|
543
601
|
|
|
544
602
|
try {
|
|
545
603
|
// Create WebSocket adapter
|
|
546
604
|
const adapter = new WebSocketTunnelAdapter(
|
|
547
|
-
|
|
605
|
+
requestIdStr,
|
|
548
606
|
(data: ArrayBuffer | string, isBinary: boolean) => {
|
|
549
607
|
// Send message through tunnel
|
|
550
608
|
const dataBuffer =
|
|
@@ -573,17 +631,17 @@ export class Tunnel {
|
|
|
573
631
|
});
|
|
574
632
|
|
|
575
633
|
// Remove from map
|
|
576
|
-
this.#actorWebSockets.delete(
|
|
634
|
+
this.#actorWebSockets.delete(requestIdStr);
|
|
577
635
|
|
|
578
636
|
// Clean up actor tracking
|
|
579
637
|
if (actor) {
|
|
580
|
-
actor.webSockets.delete(
|
|
638
|
+
actor.webSockets.delete(requestIdStr);
|
|
581
639
|
}
|
|
582
640
|
},
|
|
583
641
|
);
|
|
584
642
|
|
|
585
643
|
// Store adapter
|
|
586
|
-
this.#actorWebSockets.set(
|
|
644
|
+
this.#actorWebSockets.set(requestIdStr, adapter);
|
|
587
645
|
|
|
588
646
|
// Convert headers to map
|
|
589
647
|
//
|
|
@@ -613,6 +671,8 @@ export class Tunnel {
|
|
|
613
671
|
requestId,
|
|
614
672
|
request,
|
|
615
673
|
);
|
|
674
|
+
adapter.canHibernate = hibernationConfig.enabled;
|
|
675
|
+
|
|
616
676
|
this.#sendMessage(requestId, {
|
|
617
677
|
tag: "ToServerWebSocketOpen",
|
|
618
678
|
val: {
|
|
@@ -633,7 +693,7 @@ export class Tunnel {
|
|
|
633
693
|
request,
|
|
634
694
|
);
|
|
635
695
|
} catch (error) {
|
|
636
|
-
|
|
696
|
+
this.log?.error({ msg: "error handling websocket open", error });
|
|
637
697
|
// Send close on error
|
|
638
698
|
this.#sendMessage(requestId, {
|
|
639
699
|
tag: "ToServerWebSocketClose",
|
|
@@ -644,11 +704,11 @@ export class Tunnel {
|
|
|
644
704
|
},
|
|
645
705
|
});
|
|
646
706
|
|
|
647
|
-
this.#actorWebSockets.delete(
|
|
707
|
+
this.#actorWebSockets.delete(requestIdStr);
|
|
648
708
|
|
|
649
709
|
// Clean up actor tracking
|
|
650
710
|
if (actor) {
|
|
651
|
-
actor.webSockets.delete(
|
|
711
|
+
actor.webSockets.delete(requestIdStr);
|
|
652
712
|
}
|
|
653
713
|
}
|
|
654
714
|
}
|
|
@@ -658,8 +718,8 @@ export class Tunnel {
|
|
|
658
718
|
requestId: ArrayBuffer,
|
|
659
719
|
msg: protocol.ToClientWebSocketMessage,
|
|
660
720
|
): Promise<boolean> {
|
|
661
|
-
const
|
|
662
|
-
const adapter = this.#actorWebSockets.get(
|
|
721
|
+
const requestIdStr = idToStr(requestId);
|
|
722
|
+
const adapter = this.#actorWebSockets.get(requestIdStr);
|
|
663
723
|
if (adapter) {
|
|
664
724
|
const data = msg.binary
|
|
665
725
|
? new Uint8Array(msg.data)
|
|
@@ -677,7 +737,7 @@ export class Tunnel {
|
|
|
677
737
|
}
|
|
678
738
|
|
|
679
739
|
__ackWebsocketMessage(requestId: ArrayBuffer, index: number) {
|
|
680
|
-
|
|
740
|
+
this.log?.debug({
|
|
681
741
|
msg: "ack ws msg",
|
|
682
742
|
requestId: idToStr(requestId),
|
|
683
743
|
index,
|
package/src/utils.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { logger } from "./log";
|
|
2
|
+
|
|
1
3
|
export function unreachable(x: never): never {
|
|
2
4
|
throw `Unreachable: ${x}`;
|
|
3
5
|
}
|
|
@@ -29,3 +31,36 @@ export function calculateBackoff(
|
|
|
29
31
|
|
|
30
32
|
return Math.floor(delay);
|
|
31
33
|
}
|
|
34
|
+
|
|
35
|
+
export interface ParsedCloseReason {
|
|
36
|
+
group: string;
|
|
37
|
+
error: string;
|
|
38
|
+
rayId?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parses a WebSocket close reason in the format: {group}.{error} or {group}.{error}#{ray_id}
|
|
43
|
+
*
|
|
44
|
+
* Examples:
|
|
45
|
+
* - "ws.eviction#t1s80so6h3irenp8ymzltfoittcl00"
|
|
46
|
+
* - "ws.client_closed"
|
|
47
|
+
*
|
|
48
|
+
* Returns undefined if the format is invalid
|
|
49
|
+
*/
|
|
50
|
+
export function parseWebSocketCloseReason(
|
|
51
|
+
reason: string,
|
|
52
|
+
): ParsedCloseReason | undefined {
|
|
53
|
+
const [mainPart, rayId] = reason.split("#");
|
|
54
|
+
const [group, error] = mainPart.split(".");
|
|
55
|
+
|
|
56
|
+
if (!group || !error) {
|
|
57
|
+
logger()?.warn({ msg: "failed to parse close reason", reason });
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
group,
|
|
63
|
+
error,
|
|
64
|
+
rayId,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -18,6 +18,7 @@ export class WebSocketTunnelAdapter {
|
|
|
18
18
|
#url = "";
|
|
19
19
|
#sendCallback: (data: ArrayBuffer | string, isBinary: boolean) => void;
|
|
20
20
|
#closeCallback: (code?: number, reason?: string, retry?: boolean) => void;
|
|
21
|
+
#canHibernate: boolean = false;
|
|
21
22
|
|
|
22
23
|
// Event buffering for events fired before listeners are attached
|
|
23
24
|
#bufferedEvents: Array<{
|
|
@@ -73,6 +74,16 @@ export class WebSocketTunnelAdapter {
|
|
|
73
74
|
return this.#url;
|
|
74
75
|
}
|
|
75
76
|
|
|
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
|
+
|
|
76
87
|
get onopen(): ((this: any, ev: any) => any) | null {
|
|
77
88
|
return this.#onopen;
|
|
78
89
|
}
|
|
@@ -190,14 +201,23 @@ export class WebSocketTunnelAdapter {
|
|
|
190
201
|
}
|
|
191
202
|
|
|
192
203
|
close(code?: number, reason?: string): void {
|
|
193
|
-
this.closeInner(code, reason);
|
|
204
|
+
this.closeInner(code, reason, false, true);
|
|
194
205
|
}
|
|
195
206
|
|
|
196
207
|
__closeWithRetry(code?: number, reason?: string): void {
|
|
197
|
-
this.closeInner(code, reason, true);
|
|
208
|
+
this.closeInner(code, reason, true, true);
|
|
198
209
|
}
|
|
199
210
|
|
|
200
|
-
|
|
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 {
|
|
201
221
|
if (
|
|
202
222
|
this.#readyState === 2 || // CLOSING
|
|
203
223
|
this.#readyState === 3 // CLOSED
|
|
@@ -208,7 +228,9 @@ export class WebSocketTunnelAdapter {
|
|
|
208
228
|
this.#readyState = 2; // CLOSING
|
|
209
229
|
|
|
210
230
|
// Send close through tunnel
|
|
211
|
-
|
|
231
|
+
if (callback) {
|
|
232
|
+
this.#closeCallback(code, reason, retry);
|
|
233
|
+
}
|
|
212
234
|
|
|
213
235
|
// Update state and fire event
|
|
214
236
|
this.#readyState = 3; // CLOSED
|