@rivetkit/engine-runner 2.0.21 → 2.0.22-rc.2
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 +19 -19
- package/dist/mod.cjs +180 -93
- package/dist/mod.cjs.map +1 -1
- package/dist/mod.d.cts +11 -4
- package/dist/mod.d.ts +11 -4
- package/dist/mod.js +180 -93
- package/dist/mod.js.map +1 -1
- package/package.json +2 -2
- package/src/mod.ts +49 -40
- package/src/tunnel.ts +160 -49
- package/src/websocket-tunnel-adapter.ts +33 -9
- package/.turbo/turbo-check-types.log +0 -4
package/src/mod.ts
CHANGED
|
@@ -8,7 +8,8 @@ import { importWebSocket } from "./websocket.js";
|
|
|
8
8
|
import type { WebSocketTunnelAdapter } from "./websocket-tunnel-adapter";
|
|
9
9
|
|
|
10
10
|
const KV_EXPIRE: number = 30_000;
|
|
11
|
-
const PROTOCOL_VERSION: number =
|
|
11
|
+
const PROTOCOL_VERSION: number = 2;
|
|
12
|
+
const RUNNER_PING_INTERVAL = 3_000;
|
|
12
13
|
|
|
13
14
|
/** Warn once the backlog significantly exceeds the server's ack batch size. */
|
|
14
15
|
const EVENT_BACKLOG_WARN_THRESHOLD = 10_000;
|
|
@@ -43,17 +44,19 @@ export interface RunnerConfig {
|
|
|
43
44
|
prepopulateActorNames: Record<string, { metadata: Record<string, any> }>;
|
|
44
45
|
metadata?: Record<string, any>;
|
|
45
46
|
onConnected: () => void;
|
|
46
|
-
onDisconnected: () => void;
|
|
47
|
+
onDisconnected: (code: number, reason: string) => void;
|
|
47
48
|
onShutdown: () => void;
|
|
48
49
|
fetch: (
|
|
49
50
|
runner: Runner,
|
|
50
51
|
actorId: string,
|
|
52
|
+
requestId: protocol.RequestId,
|
|
51
53
|
request: Request,
|
|
52
54
|
) => Promise<Response>;
|
|
53
55
|
websocket?: (
|
|
54
56
|
runner: Runner,
|
|
55
57
|
actorId: string,
|
|
56
58
|
ws: any,
|
|
59
|
+
requestId: protocol.RequestId,
|
|
57
60
|
request: Request,
|
|
58
61
|
) => Promise<void>;
|
|
59
62
|
onActorStart: (
|
|
@@ -62,9 +65,19 @@ export interface RunnerConfig {
|
|
|
62
65
|
config: ActorConfig,
|
|
63
66
|
) => Promise<void>;
|
|
64
67
|
onActorStop: (actorId: string, generation: number) => Promise<void>;
|
|
68
|
+
getActorHibernationConfig: (
|
|
69
|
+
actorId: string,
|
|
70
|
+
requestId: ArrayBuffer,
|
|
71
|
+
request: Request,
|
|
72
|
+
) => HibernationConfig;
|
|
65
73
|
noAutoShutdown?: boolean;
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
export interface HibernationConfig {
|
|
77
|
+
enabled: boolean;
|
|
78
|
+
lastMsgIndex: number | undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
68
81
|
export interface KvListOptions {
|
|
69
82
|
reverse?: boolean;
|
|
70
83
|
limit?: number;
|
|
@@ -155,26 +168,21 @@ export class Runner {
|
|
|
155
168
|
const actor = this.#removeActor(actorId, generation);
|
|
156
169
|
if (!actor) return;
|
|
157
170
|
|
|
158
|
-
// Unregister actor from tunnel
|
|
159
|
-
this.#tunnel?.unregisterActor(actor);
|
|
160
|
-
|
|
161
171
|
// If onActorStop times out, Pegboard will handle this timeout with ACTOR_STOP_THRESHOLD_DURATION_MS
|
|
172
|
+
//
|
|
173
|
+
// If we receive a request while onActorStop is running, a Service
|
|
174
|
+
// Unavailable error will be returned to Guard and the request will be
|
|
175
|
+
// retried
|
|
162
176
|
try {
|
|
163
177
|
await this.#config.onActorStop(actorId, actor.generation);
|
|
164
178
|
} catch (err) {
|
|
165
179
|
console.error(`Error in onActorStop for actor ${actorId}:`, err);
|
|
166
180
|
}
|
|
167
181
|
|
|
168
|
-
|
|
182
|
+
// Close requests after onActorStop so you can send messages over the tunnel
|
|
183
|
+
this.#tunnel?.closeActiveRequests(actor);
|
|
169
184
|
|
|
170
|
-
this.#
|
|
171
|
-
logger()?.error({
|
|
172
|
-
msg: "error in onactorstop for actor",
|
|
173
|
-
runnerId: this.runnerId,
|
|
174
|
-
actorId,
|
|
175
|
-
err,
|
|
176
|
-
});
|
|
177
|
-
});
|
|
185
|
+
this.#sendActorStateUpdate(actorId, actor.generation, "stopped");
|
|
178
186
|
}
|
|
179
187
|
|
|
180
188
|
#stopAllActors() {
|
|
@@ -221,6 +229,7 @@ export class Runner {
|
|
|
221
229
|
);
|
|
222
230
|
}
|
|
223
231
|
|
|
232
|
+
// IMPORTANT: Make sure to call stopActiveRequests if calling #removeActor
|
|
224
233
|
#removeActor(
|
|
225
234
|
actorId: string,
|
|
226
235
|
generation?: number,
|
|
@@ -246,24 +255,6 @@ export class Runner {
|
|
|
246
255
|
|
|
247
256
|
this.#actors.delete(actorId);
|
|
248
257
|
|
|
249
|
-
// Close all WebSocket connections for this actor
|
|
250
|
-
const actorWebSockets = this.#actorWebSockets.get(actorId);
|
|
251
|
-
if (actorWebSockets) {
|
|
252
|
-
for (const ws of actorWebSockets) {
|
|
253
|
-
try {
|
|
254
|
-
ws.close(1000, "Actor stopped");
|
|
255
|
-
} catch (err) {
|
|
256
|
-
logger()?.error({
|
|
257
|
-
msg: "error closing websocket for actor",
|
|
258
|
-
runnerId: this.runnerId,
|
|
259
|
-
actorId,
|
|
260
|
-
err,
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
this.#actorWebSockets.delete(actorId);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
258
|
return actor;
|
|
268
259
|
}
|
|
269
260
|
|
|
@@ -458,17 +449,24 @@ export class Runner {
|
|
|
458
449
|
}
|
|
459
450
|
|
|
460
451
|
// MARK: Networking
|
|
452
|
+
get pegboardEndpoint() {
|
|
453
|
+
return this.#config.pegboardEndpoint || this.#config.endpoint;
|
|
454
|
+
}
|
|
461
455
|
get pegboardUrl() {
|
|
462
|
-
const
|
|
463
|
-
const wsEndpoint = endpoint
|
|
456
|
+
const wsEndpoint = this.pegboardEndpoint
|
|
464
457
|
.replace("http://", "ws://")
|
|
465
458
|
.replace("https://", "wss://");
|
|
466
|
-
|
|
459
|
+
|
|
460
|
+
// Ensure the endpoint ends with /runners/connect
|
|
461
|
+
const baseUrl = wsEndpoint.endsWith("/")
|
|
462
|
+
? wsEndpoint.slice(0, -1)
|
|
463
|
+
: wsEndpoint;
|
|
464
|
+
return `${baseUrl}/runners/connect?protocol_version=${PROTOCOL_VERSION}&namespace=${encodeURIComponent(this.#config.namespace)}&runner_key=${encodeURIComponent(this.#config.runnerKey)}`;
|
|
467
465
|
}
|
|
468
466
|
|
|
469
467
|
// MARK: Runner protocol
|
|
470
468
|
async #openPegboardWebSocket() {
|
|
471
|
-
const protocols = ["rivet"
|
|
469
|
+
const protocols = ["rivet"];
|
|
472
470
|
if (this.config.token)
|
|
473
471
|
protocols.push(`rivet_token.${this.config.token}`);
|
|
474
472
|
|
|
@@ -476,8 +474,16 @@ export class Runner {
|
|
|
476
474
|
const ws = new WS(this.pegboardUrl, protocols) as any as WebSocket;
|
|
477
475
|
this.#pegboardWebSocket = ws;
|
|
478
476
|
|
|
477
|
+
logger()?.info({
|
|
478
|
+
msg: "connecting",
|
|
479
|
+
endpoint: this.pegboardEndpoint,
|
|
480
|
+
namespace: this.#config.namespace,
|
|
481
|
+
runnerKey: this.#config.runnerKey,
|
|
482
|
+
hasToken: !!this.config.token,
|
|
483
|
+
});
|
|
484
|
+
|
|
479
485
|
ws.addEventListener("open", () => {
|
|
480
|
-
logger()?.info({ msg: "
|
|
486
|
+
logger()?.info({ msg: "connected" });
|
|
481
487
|
|
|
482
488
|
// Reset reconnect attempt counter on successful connection
|
|
483
489
|
this.#reconnectAttempt = 0;
|
|
@@ -523,7 +529,6 @@ export class Runner {
|
|
|
523
529
|
this.#processUnsentKvRequests();
|
|
524
530
|
|
|
525
531
|
// Start ping interval
|
|
526
|
-
const pingInterval = 1000;
|
|
527
532
|
const pingLoop = setInterval(() => {
|
|
528
533
|
if (ws.readyState === 1) {
|
|
529
534
|
this.__sendToServer({
|
|
@@ -539,7 +544,7 @@ export class Runner {
|
|
|
539
544
|
runnerId: this.runnerId,
|
|
540
545
|
});
|
|
541
546
|
}
|
|
542
|
-
},
|
|
547
|
+
}, RUNNER_PING_INTERVAL);
|
|
543
548
|
this.#pingLoop = pingLoop;
|
|
544
549
|
|
|
545
550
|
// Start command acknowledgment interval (5 minutes)
|
|
@@ -652,7 +657,7 @@ export class Runner {
|
|
|
652
657
|
reason: ev.reason.toString(),
|
|
653
658
|
});
|
|
654
659
|
|
|
655
|
-
this.#config.onDisconnected();
|
|
660
|
+
this.#config.onDisconnected(ev.code, ev.reason);
|
|
656
661
|
|
|
657
662
|
if (ev.reason.toString().startsWith("ws.eviction")) {
|
|
658
663
|
logger()?.info({
|
|
@@ -1390,6 +1395,10 @@ export class Runner {
|
|
|
1390
1395
|
}
|
|
1391
1396
|
}
|
|
1392
1397
|
|
|
1398
|
+
sendWebsocketMessageAck(requestId: ArrayBuffer, index: number) {
|
|
1399
|
+
this.#tunnel?.__ackWebsocketMessage(requestId, index);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1393
1402
|
getServerlessInitPacket(): string | undefined {
|
|
1394
1403
|
if (!this.runnerId) return undefined;
|
|
1395
1404
|
|
package/src/tunnel.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type * as protocol from "@rivetkit/engine-runner-protocol";
|
|
2
2
|
import type { MessageId, RequestId } from "@rivetkit/engine-runner-protocol";
|
|
3
|
-
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
import { stringify as uuidstringify, v4 as uuidv4 } from "uuid";
|
|
4
4
|
import { logger } from "./log";
|
|
5
5
|
import type { ActorInstance, Runner } from "./mod";
|
|
6
6
|
import { unreachable } from "./utils";
|
|
@@ -8,6 +8,7 @@ import { WebSocketTunnelAdapter } from "./websocket-tunnel-adapter";
|
|
|
8
8
|
|
|
9
9
|
const GC_INTERVAL = 60000; // 60 seconds
|
|
10
10
|
const MESSAGE_ACK_TIMEOUT = 5000; // 5 seconds
|
|
11
|
+
const WEBSOCKET_STATE_PERSIST_TIMEOUT = 30000; // 30 seconds
|
|
11
12
|
|
|
12
13
|
interface PendingRequest {
|
|
13
14
|
resolve: (response: Response) => void;
|
|
@@ -56,7 +57,7 @@ export class Tunnel {
|
|
|
56
57
|
|
|
57
58
|
// Close all WebSockets
|
|
58
59
|
for (const [_, ws] of this.#actorWebSockets) {
|
|
59
|
-
ws.
|
|
60
|
+
ws.__closeWithRetry();
|
|
60
61
|
}
|
|
61
62
|
this.#actorWebSockets.clear();
|
|
62
63
|
}
|
|
@@ -76,12 +77,20 @@ export class Tunnel {
|
|
|
76
77
|
// Build message
|
|
77
78
|
const messageId = generateUuidBuffer();
|
|
78
79
|
|
|
79
|
-
const requestIdStr =
|
|
80
|
-
|
|
80
|
+
const requestIdStr = idToStr(requestId);
|
|
81
|
+
const messageIdStr = idToStr(messageId);
|
|
82
|
+
this.#pendingTunnelMessages.set(messageIdStr, {
|
|
81
83
|
sentAt: Date.now(),
|
|
82
84
|
requestIdStr,
|
|
83
85
|
});
|
|
84
86
|
|
|
87
|
+
logger()?.debug({
|
|
88
|
+
msg: "send tunnel msg",
|
|
89
|
+
requestId: requestIdStr,
|
|
90
|
+
messageId: messageIdStr,
|
|
91
|
+
message: messageKind,
|
|
92
|
+
});
|
|
93
|
+
|
|
85
94
|
// Send message
|
|
86
95
|
const message: protocol.ToServer = {
|
|
87
96
|
tag: "ToServerTunnelMessage",
|
|
@@ -108,6 +117,12 @@ export class Tunnel {
|
|
|
108
117
|
},
|
|
109
118
|
};
|
|
110
119
|
|
|
120
|
+
logger()?.debug({
|
|
121
|
+
msg: "ack tunnel msg",
|
|
122
|
+
requestId: idToStr(requestId),
|
|
123
|
+
messageId: idToStr(messageId),
|
|
124
|
+
});
|
|
125
|
+
|
|
111
126
|
this.#runner.__sendToServer(message);
|
|
112
127
|
}
|
|
113
128
|
|
|
@@ -156,7 +171,10 @@ export class Tunnel {
|
|
|
156
171
|
const webSocket = this.#actorWebSockets.get(requestIdStr);
|
|
157
172
|
if (webSocket) {
|
|
158
173
|
// Close the WebSocket connection
|
|
159
|
-
webSocket.
|
|
174
|
+
webSocket.__closeWithRetry(
|
|
175
|
+
1000,
|
|
176
|
+
"Message acknowledgment timeout",
|
|
177
|
+
);
|
|
160
178
|
|
|
161
179
|
// Clean up from actorWebSockets map
|
|
162
180
|
this.#actorWebSockets.delete(requestIdStr);
|
|
@@ -176,7 +194,7 @@ export class Tunnel {
|
|
|
176
194
|
}
|
|
177
195
|
}
|
|
178
196
|
|
|
179
|
-
|
|
197
|
+
closeActiveRequests(actor: ActorInstance) {
|
|
180
198
|
const actorId = actor.actorId;
|
|
181
199
|
|
|
182
200
|
// Terminate all requests for this actor
|
|
@@ -189,30 +207,43 @@ export class Tunnel {
|
|
|
189
207
|
}
|
|
190
208
|
actor.requests.clear();
|
|
191
209
|
|
|
192
|
-
//
|
|
210
|
+
// Flush acks and close all WebSockets for this actor
|
|
193
211
|
for (const webSocketId of actor.webSockets) {
|
|
194
212
|
const ws = this.#actorWebSockets.get(webSocketId);
|
|
195
213
|
if (ws) {
|
|
196
|
-
ws.
|
|
214
|
+
ws.__closeWithRetry(1000, "Actor stopped");
|
|
197
215
|
this.#actorWebSockets.delete(webSocketId);
|
|
198
216
|
}
|
|
199
217
|
}
|
|
200
218
|
actor.webSockets.clear();
|
|
201
219
|
}
|
|
202
220
|
|
|
203
|
-
async #fetch(
|
|
221
|
+
async #fetch(
|
|
222
|
+
actorId: string,
|
|
223
|
+
requestId: protocol.RequestId,
|
|
224
|
+
request: Request,
|
|
225
|
+
): Promise<Response> {
|
|
204
226
|
// Validate actor exists
|
|
205
227
|
if (!this.#runner.hasActor(actorId)) {
|
|
206
228
|
logger()?.warn({
|
|
207
229
|
msg: "ignoring request for unknown actor",
|
|
208
230
|
actorId,
|
|
209
231
|
});
|
|
210
|
-
|
|
232
|
+
|
|
233
|
+
// NOTE: This is a special response that will cause Guard to retry the request
|
|
234
|
+
//
|
|
235
|
+
// See should_retry_request_inner
|
|
236
|
+
// https://github.com/rivet-dev/rivet/blob/222dae87e3efccaffa2b503de40ecf8afd4e31eb/engine/packages/guard-core/src/proxy_service.rs#L2458
|
|
237
|
+
return new Response("Actor not found", {
|
|
238
|
+
status: 503,
|
|
239
|
+
headers: { "x-rivet-error": "runner.actor_not_found" },
|
|
240
|
+
});
|
|
211
241
|
}
|
|
212
242
|
|
|
213
243
|
const fetchHandler = this.#runner.config.fetch(
|
|
214
244
|
this.#runner,
|
|
215
245
|
actorId,
|
|
246
|
+
requestId,
|
|
216
247
|
request,
|
|
217
248
|
);
|
|
218
249
|
|
|
@@ -224,44 +255,72 @@ export class Tunnel {
|
|
|
224
255
|
}
|
|
225
256
|
|
|
226
257
|
async handleTunnelMessage(message: protocol.ToClientTunnelMessage) {
|
|
258
|
+
const requestIdStr = idToStr(message.requestId);
|
|
259
|
+
const messageIdStr = idToStr(message.messageId);
|
|
260
|
+
logger()?.debug({
|
|
261
|
+
msg: "receive tunnel msg",
|
|
262
|
+
requestId: requestIdStr,
|
|
263
|
+
messageId: messageIdStr,
|
|
264
|
+
message: message.messageKind,
|
|
265
|
+
});
|
|
266
|
+
|
|
227
267
|
if (message.messageKind.tag === "TunnelAck") {
|
|
228
268
|
// Mark pending message as acknowledged and remove it
|
|
229
|
-
const
|
|
230
|
-
const pending = this.#pendingTunnelMessages.get(msgIdStr);
|
|
269
|
+
const pending = this.#pendingTunnelMessages.get(messageIdStr);
|
|
231
270
|
if (pending) {
|
|
232
|
-
|
|
271
|
+
const didDelete =
|
|
272
|
+
this.#pendingTunnelMessages.delete(messageIdStr);
|
|
273
|
+
if (!didDelete) {
|
|
274
|
+
logger()?.warn({
|
|
275
|
+
msg: "received tunnel ack for nonexistent message",
|
|
276
|
+
requestId: requestIdStr,
|
|
277
|
+
messageId: messageIdStr,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
233
280
|
}
|
|
234
281
|
} else {
|
|
235
|
-
this.#sendAck(message.requestId, message.messageId);
|
|
236
282
|
switch (message.messageKind.tag) {
|
|
237
283
|
case "ToClientRequestStart":
|
|
284
|
+
this.#sendAck(message.requestId, message.messageId);
|
|
285
|
+
|
|
238
286
|
await this.#handleRequestStart(
|
|
239
287
|
message.requestId,
|
|
240
288
|
message.messageKind.val,
|
|
241
289
|
);
|
|
242
290
|
break;
|
|
243
291
|
case "ToClientRequestChunk":
|
|
292
|
+
this.#sendAck(message.requestId, message.messageId);
|
|
293
|
+
|
|
244
294
|
await this.#handleRequestChunk(
|
|
245
295
|
message.requestId,
|
|
246
296
|
message.messageKind.val,
|
|
247
297
|
);
|
|
248
298
|
break;
|
|
249
299
|
case "ToClientRequestAbort":
|
|
300
|
+
this.#sendAck(message.requestId, message.messageId);
|
|
301
|
+
|
|
250
302
|
await this.#handleRequestAbort(message.requestId);
|
|
251
303
|
break;
|
|
252
304
|
case "ToClientWebSocketOpen":
|
|
305
|
+
this.#sendAck(message.requestId, message.messageId);
|
|
306
|
+
|
|
253
307
|
await this.#handleWebSocketOpen(
|
|
254
308
|
message.requestId,
|
|
255
309
|
message.messageKind.val,
|
|
256
310
|
);
|
|
257
311
|
break;
|
|
258
|
-
case "ToClientWebSocketMessage":
|
|
259
|
-
|
|
312
|
+
case "ToClientWebSocketMessage": {
|
|
313
|
+
this.#sendAck(message.requestId, message.messageId);
|
|
314
|
+
|
|
315
|
+
const _unhandled = await this.#handleWebSocketMessage(
|
|
260
316
|
message.requestId,
|
|
261
317
|
message.messageKind.val,
|
|
262
318
|
);
|
|
263
319
|
break;
|
|
320
|
+
}
|
|
264
321
|
case "ToClientWebSocketClose":
|
|
322
|
+
this.#sendAck(message.requestId, message.messageId);
|
|
323
|
+
|
|
265
324
|
await this.#handleWebSocketClose(
|
|
266
325
|
message.requestId,
|
|
267
326
|
message.messageKind.val,
|
|
@@ -278,7 +337,7 @@ export class Tunnel {
|
|
|
278
337
|
req: protocol.ToClientRequestStart,
|
|
279
338
|
) {
|
|
280
339
|
// Track this request for the actor
|
|
281
|
-
const requestIdStr =
|
|
340
|
+
const requestIdStr = idToStr(requestId);
|
|
282
341
|
const actor = this.#runner.getActor(req.actorId);
|
|
283
342
|
if (actor) {
|
|
284
343
|
actor.requests.add(requestIdStr);
|
|
@@ -329,12 +388,17 @@ export class Tunnel {
|
|
|
329
388
|
// Call fetch handler with validation
|
|
330
389
|
const response = await this.#fetch(
|
|
331
390
|
req.actorId,
|
|
391
|
+
requestId,
|
|
332
392
|
streamingRequest,
|
|
333
393
|
);
|
|
334
394
|
await this.#sendResponse(requestId, response);
|
|
335
395
|
} else {
|
|
336
396
|
// Non-streaming request
|
|
337
|
-
const response = await this.#fetch(
|
|
397
|
+
const response = await this.#fetch(
|
|
398
|
+
req.actorId,
|
|
399
|
+
requestId,
|
|
400
|
+
request,
|
|
401
|
+
);
|
|
338
402
|
await this.#sendResponse(requestId, response);
|
|
339
403
|
}
|
|
340
404
|
} catch (error) {
|
|
@@ -353,7 +417,7 @@ export class Tunnel {
|
|
|
353
417
|
requestId: ArrayBuffer,
|
|
354
418
|
chunk: protocol.ToClientRequestChunk,
|
|
355
419
|
) {
|
|
356
|
-
const requestIdStr =
|
|
420
|
+
const requestIdStr = idToStr(requestId);
|
|
357
421
|
const pending = this.#actorPendingRequests.get(requestIdStr);
|
|
358
422
|
if (pending?.streamController) {
|
|
359
423
|
pending.streamController.enqueue(new Uint8Array(chunk.body));
|
|
@@ -365,7 +429,7 @@ export class Tunnel {
|
|
|
365
429
|
}
|
|
366
430
|
|
|
367
431
|
async #handleRequestAbort(requestId: ArrayBuffer) {
|
|
368
|
-
const requestIdStr =
|
|
432
|
+
const requestIdStr = idToStr(requestId);
|
|
369
433
|
const pending = this.#actorPendingRequests.get(requestIdStr);
|
|
370
434
|
if (pending?.streamController) {
|
|
371
435
|
pending.streamController.error(new Error("Request aborted"));
|
|
@@ -426,10 +490,10 @@ export class Tunnel {
|
|
|
426
490
|
}
|
|
427
491
|
|
|
428
492
|
async #handleWebSocketOpen(
|
|
429
|
-
requestId:
|
|
493
|
+
requestId: protocol.RequestId,
|
|
430
494
|
open: protocol.ToClientWebSocketOpen,
|
|
431
495
|
) {
|
|
432
|
-
const webSocketId =
|
|
496
|
+
const webSocketId = idToStr(requestId);
|
|
433
497
|
// Validate actor exists
|
|
434
498
|
const actor = this.#runner.getActor(open.actorId);
|
|
435
499
|
if (!actor) {
|
|
@@ -437,12 +501,18 @@ export class Tunnel {
|
|
|
437
501
|
msg: "ignoring websocket for unknown actor",
|
|
438
502
|
actorId: open.actorId,
|
|
439
503
|
});
|
|
440
|
-
|
|
504
|
+
|
|
505
|
+
// NOTE: Closing a WebSocket before open is equivalent to a Service
|
|
506
|
+
// Unavailable error and will cause Guard to retry the request
|
|
507
|
+
//
|
|
508
|
+
// See
|
|
509
|
+
// https://github.com/rivet-dev/rivet/blob/222dae87e3efccaffa2b503de40ecf8afd4e31eb/engine/packages/pegboard-gateway/src/lib.rs#L238
|
|
441
510
|
this.#sendMessage(requestId, {
|
|
442
511
|
tag: "ToServerWebSocketClose",
|
|
443
512
|
val: {
|
|
444
513
|
code: 1011,
|
|
445
514
|
reason: "Actor not found",
|
|
515
|
+
retry: false,
|
|
446
516
|
},
|
|
447
517
|
});
|
|
448
518
|
return;
|
|
@@ -460,6 +530,7 @@ export class Tunnel {
|
|
|
460
530
|
val: {
|
|
461
531
|
code: 1011,
|
|
462
532
|
reason: "Not Implemented",
|
|
533
|
+
retry: false,
|
|
463
534
|
},
|
|
464
535
|
});
|
|
465
536
|
return;
|
|
@@ -490,13 +561,14 @@ export class Tunnel {
|
|
|
490
561
|
},
|
|
491
562
|
});
|
|
492
563
|
},
|
|
493
|
-
(code?: number, reason?: string) => {
|
|
564
|
+
(code?: number, reason?: string, retry: boolean = false) => {
|
|
494
565
|
// Send close through tunnel
|
|
495
566
|
this.#sendMessage(requestId, {
|
|
496
567
|
tag: "ToServerWebSocketClose",
|
|
497
568
|
val: {
|
|
498
569
|
code: code || null,
|
|
499
570
|
reason: reason || null,
|
|
571
|
+
retry,
|
|
500
572
|
},
|
|
501
573
|
});
|
|
502
574
|
|
|
@@ -513,17 +585,10 @@ export class Tunnel {
|
|
|
513
585
|
// Store adapter
|
|
514
586
|
this.#actorWebSockets.set(webSocketId, adapter);
|
|
515
587
|
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
// Notify adapter that connection is open
|
|
523
|
-
adapter._handleOpen();
|
|
524
|
-
|
|
525
|
-
// Create a minimal request object for the websocket handler
|
|
526
|
-
// Include original headers from the open message
|
|
588
|
+
// Convert headers to map
|
|
589
|
+
//
|
|
590
|
+
// We need to manually ensure the original Upgrade/Connection WS
|
|
591
|
+
// headers are present
|
|
527
592
|
const headerInit: Record<string, string> = {};
|
|
528
593
|
if (open.headers) {
|
|
529
594
|
for (const [k, v] of open.headers as ReadonlyMap<
|
|
@@ -533,7 +598,6 @@ export class Tunnel {
|
|
|
533
598
|
headerInit[k] = v;
|
|
534
599
|
}
|
|
535
600
|
}
|
|
536
|
-
// Ensure websocket upgrade headers are present
|
|
537
601
|
headerInit["Upgrade"] = "websocket";
|
|
538
602
|
headerInit["Connection"] = "Upgrade";
|
|
539
603
|
|
|
@@ -542,11 +606,30 @@ export class Tunnel {
|
|
|
542
606
|
headers: headerInit,
|
|
543
607
|
});
|
|
544
608
|
|
|
609
|
+
// Send open confirmation
|
|
610
|
+
const hibernationConfig =
|
|
611
|
+
this.#runner.config.getActorHibernationConfig(
|
|
612
|
+
actor.actorId,
|
|
613
|
+
requestId,
|
|
614
|
+
request,
|
|
615
|
+
);
|
|
616
|
+
this.#sendMessage(requestId, {
|
|
617
|
+
tag: "ToServerWebSocketOpen",
|
|
618
|
+
val: {
|
|
619
|
+
canHibernate: hibernationConfig.enabled,
|
|
620
|
+
lastMsgIndex: BigInt(hibernationConfig.lastMsgIndex ?? -1),
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// Notify adapter that connection is open
|
|
625
|
+
adapter._handleOpen(requestId);
|
|
626
|
+
|
|
545
627
|
// Call websocket handler
|
|
546
628
|
await websocketHandler(
|
|
547
629
|
this.#runner,
|
|
548
630
|
open.actorId,
|
|
549
631
|
adapter,
|
|
632
|
+
requestId,
|
|
550
633
|
request,
|
|
551
634
|
);
|
|
552
635
|
} catch (error) {
|
|
@@ -557,6 +640,7 @@ export class Tunnel {
|
|
|
557
640
|
val: {
|
|
558
641
|
code: 1011,
|
|
559
642
|
reason: "Server Error",
|
|
643
|
+
retry: false,
|
|
560
644
|
},
|
|
561
645
|
});
|
|
562
646
|
|
|
@@ -569,45 +653,72 @@ export class Tunnel {
|
|
|
569
653
|
}
|
|
570
654
|
}
|
|
571
655
|
|
|
656
|
+
/// Returns false if the message was sent off
|
|
572
657
|
async #handleWebSocketMessage(
|
|
573
658
|
requestId: ArrayBuffer,
|
|
574
|
-
msg: protocol.
|
|
575
|
-
) {
|
|
576
|
-
const webSocketId =
|
|
659
|
+
msg: protocol.ToClientWebSocketMessage,
|
|
660
|
+
): Promise<boolean> {
|
|
661
|
+
const webSocketId = idToStr(requestId);
|
|
577
662
|
const adapter = this.#actorWebSockets.get(webSocketId);
|
|
578
663
|
if (adapter) {
|
|
579
664
|
const data = msg.binary
|
|
580
665
|
? new Uint8Array(msg.data)
|
|
581
666
|
: new TextDecoder().decode(new Uint8Array(msg.data));
|
|
582
667
|
|
|
583
|
-
adapter._handleMessage(
|
|
668
|
+
return adapter._handleMessage(
|
|
669
|
+
requestId,
|
|
670
|
+
data,
|
|
671
|
+
msg.index,
|
|
672
|
+
msg.binary,
|
|
673
|
+
);
|
|
674
|
+
} else {
|
|
675
|
+
return true;
|
|
584
676
|
}
|
|
585
677
|
}
|
|
586
678
|
|
|
679
|
+
__ackWebsocketMessage(requestId: ArrayBuffer, index: number) {
|
|
680
|
+
logger()?.debug({
|
|
681
|
+
msg: "ack ws msg",
|
|
682
|
+
requestId: idToStr(requestId),
|
|
683
|
+
index,
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
if (index < 0 || index > 65535)
|
|
687
|
+
throw new Error("invalid websocket ack index");
|
|
688
|
+
|
|
689
|
+
// Send the ack message
|
|
690
|
+
this.#sendMessage(requestId, {
|
|
691
|
+
tag: "ToServerWebSocketMessageAck",
|
|
692
|
+
val: {
|
|
693
|
+
index,
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
587
698
|
async #handleWebSocketClose(
|
|
588
699
|
requestId: ArrayBuffer,
|
|
589
|
-
close: protocol.
|
|
700
|
+
close: protocol.ToClientWebSocketClose,
|
|
590
701
|
) {
|
|
591
|
-
const
|
|
592
|
-
const adapter = this.#actorWebSockets.get(
|
|
702
|
+
const requestIdStr = idToStr(requestId);
|
|
703
|
+
const adapter = this.#actorWebSockets.get(requestIdStr);
|
|
593
704
|
if (adapter) {
|
|
594
705
|
adapter._handleClose(
|
|
706
|
+
requestId,
|
|
595
707
|
close.code || undefined,
|
|
596
708
|
close.reason || undefined,
|
|
597
709
|
);
|
|
598
|
-
this.#actorWebSockets.delete(
|
|
710
|
+
this.#actorWebSockets.delete(requestIdStr);
|
|
599
711
|
}
|
|
600
712
|
}
|
|
601
713
|
}
|
|
602
714
|
|
|
603
|
-
/** Converts a buffer to a string. Used for storing strings in a lookup map. */
|
|
604
|
-
function bufferToString(buffer: ArrayBuffer): string {
|
|
605
|
-
return Buffer.from(buffer).toString("base64");
|
|
606
|
-
}
|
|
607
|
-
|
|
608
715
|
/** Generates a UUID as bytes. */
|
|
609
716
|
function generateUuidBuffer(): ArrayBuffer {
|
|
610
717
|
const buffer = new Uint8Array(16);
|
|
611
718
|
uuidv4(undefined, buffer);
|
|
612
719
|
return buffer.buffer;
|
|
613
720
|
}
|
|
721
|
+
|
|
722
|
+
function idToStr(id: ArrayBuffer): string {
|
|
723
|
+
return uuidstringify(new Uint8Array(id));
|
|
724
|
+
}
|