@rivetkit/engine-runner 2.0.4-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/LICENSE +203 -0
- package/dist/mod.cjs +2813 -0
- package/dist/mod.cjs.map +1 -0
- package/dist/mod.d.cts +298 -0
- package/dist/mod.d.ts +298 -0
- package/dist/mod.js +2813 -0
- package/dist/mod.js.map +1 -0
- package/package.json +43 -0
- package/src/actor.ts +210 -0
- package/src/log.ts +11 -0
- package/src/mod.ts +1862 -0
- package/src/stringify.ts +353 -0
- package/src/tunnel.ts +1169 -0
- package/src/utils.ts +175 -0
- package/src/websocket-tunnel-adapter.ts +201 -0
- package/src/websocket.ts +43 -0
package/src/tunnel.ts
ADDED
|
@@ -0,0 +1,1169 @@
|
|
|
1
|
+
import type * as protocol from "@rivetkit/engine-runner-protocol";
|
|
2
|
+
import type {
|
|
3
|
+
GatewayId,
|
|
4
|
+
MessageId,
|
|
5
|
+
RequestId,
|
|
6
|
+
} from "@rivetkit/engine-runner-protocol";
|
|
7
|
+
import type { Logger } from "pino";
|
|
8
|
+
import {
|
|
9
|
+
parse as uuidparse,
|
|
10
|
+
stringify as uuidstringify,
|
|
11
|
+
v4 as uuidv4,
|
|
12
|
+
} from "uuid";
|
|
13
|
+
import { type Runner, type RunnerActor, RunnerShutdownError } from "./mod";
|
|
14
|
+
import {
|
|
15
|
+
stringifyToClientTunnelMessageKind,
|
|
16
|
+
stringifyToServerTunnelMessageKind,
|
|
17
|
+
} from "./stringify";
|
|
18
|
+
import { arraysEqual, idToStr, stringifyError, unreachable } from "./utils";
|
|
19
|
+
import {
|
|
20
|
+
HIBERNATABLE_SYMBOL,
|
|
21
|
+
WebSocketTunnelAdapter,
|
|
22
|
+
} from "./websocket-tunnel-adapter";
|
|
23
|
+
|
|
24
|
+
export interface PendingRequest {
|
|
25
|
+
resolve: (response: Response) => void;
|
|
26
|
+
reject: (error: Error) => void;
|
|
27
|
+
streamController?: ReadableStreamDefaultController<Uint8Array>;
|
|
28
|
+
actorId?: string;
|
|
29
|
+
gatewayId?: GatewayId;
|
|
30
|
+
requestId?: RequestId;
|
|
31
|
+
clientMessageIndex: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface HibernatingWebSocketMetadata {
|
|
35
|
+
gatewayId: GatewayId;
|
|
36
|
+
requestId: RequestId;
|
|
37
|
+
clientMessageIndex: number;
|
|
38
|
+
serverMessageIndex: number;
|
|
39
|
+
|
|
40
|
+
path: string;
|
|
41
|
+
headers: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class Tunnel {
|
|
45
|
+
#runner: Runner;
|
|
46
|
+
|
|
47
|
+
/** Maps request IDs to actor IDs for lookup */
|
|
48
|
+
#requestToActor: Array<{
|
|
49
|
+
gatewayId: GatewayId;
|
|
50
|
+
requestId: RequestId;
|
|
51
|
+
actorId: string;
|
|
52
|
+
}> = [];
|
|
53
|
+
|
|
54
|
+
/** Buffer for messages when not connected */
|
|
55
|
+
#bufferedMessages: Array<{
|
|
56
|
+
gatewayId: GatewayId;
|
|
57
|
+
requestId: RequestId;
|
|
58
|
+
messageKind: protocol.ToServerTunnelMessageKind;
|
|
59
|
+
}> = [];
|
|
60
|
+
|
|
61
|
+
get log(): Logger | undefined {
|
|
62
|
+
return this.#runner.log;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
constructor(runner: Runner) {
|
|
66
|
+
this.#runner = runner;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
start(): void {
|
|
70
|
+
// No-op - kept for compatibility
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
resendBufferedEvents(): void {
|
|
74
|
+
if (this.#bufferedMessages.length === 0) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.log?.info({
|
|
79
|
+
msg: "resending buffered tunnel messages",
|
|
80
|
+
count: this.#bufferedMessages.length,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const messages = this.#bufferedMessages;
|
|
84
|
+
this.#bufferedMessages = [];
|
|
85
|
+
|
|
86
|
+
for (const { gatewayId, requestId, messageKind } of messages) {
|
|
87
|
+
this.#sendMessage(gatewayId, requestId, messageKind);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
shutdown() {
|
|
92
|
+
// NOTE: Pegboard WS already closed at this point, cannot send
|
|
93
|
+
// anything. All teardown logic is handled by pegboard-runner.
|
|
94
|
+
|
|
95
|
+
// Reject all pending requests and close all WebSockets for all actors
|
|
96
|
+
// RunnerShutdownError will be explicitly ignored
|
|
97
|
+
for (const [_actorId, actor] of this.#runner.actors) {
|
|
98
|
+
// Reject all pending requests for this actor
|
|
99
|
+
for (const entry of actor.pendingRequests) {
|
|
100
|
+
entry.request.reject(new RunnerShutdownError());
|
|
101
|
+
}
|
|
102
|
+
actor.pendingRequests = [];
|
|
103
|
+
|
|
104
|
+
// Close all WebSockets for this actor
|
|
105
|
+
// The WebSocket close event with retry is automatically sent when the
|
|
106
|
+
// runner WS closes, so we only need to notify the client that the WS
|
|
107
|
+
// closed:
|
|
108
|
+
// https://github.com/rivet-dev/rivet/blob/00d4f6a22da178a6f8115e5db50d96c6f8387c2e/engine/packages/pegboard-runner/src/lib.rs#L157
|
|
109
|
+
for (const entry of actor.webSockets) {
|
|
110
|
+
// Only close non-hibernatable websockets to prevent sending
|
|
111
|
+
// unnecessary close messages for websockets that will be hibernated
|
|
112
|
+
if (!entry.ws[HIBERNATABLE_SYMBOL]) {
|
|
113
|
+
entry.ws._closeWithoutCallback(1000, "ws.tunnel_shutdown");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
actor.webSockets = [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Clear the request-to-actor mapping
|
|
120
|
+
this.#requestToActor = [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async restoreHibernatingRequests(
|
|
124
|
+
actorId: string,
|
|
125
|
+
metaEntries: HibernatingWebSocketMetadata[],
|
|
126
|
+
) {
|
|
127
|
+
const actor = this.#runner.getActor(actorId);
|
|
128
|
+
if (!actor) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Actor ${actorId} not found for restoring hibernating requests`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (actor.hibernationRestored) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Actor ${actorId} already restored hibernating requests`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.log?.debug({
|
|
141
|
+
msg: "restoring hibernating requests",
|
|
142
|
+
actorId,
|
|
143
|
+
requests: actor.hibernatingRequests.length,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Track all background operations
|
|
147
|
+
const backgroundOperations: Promise<void>[] = [];
|
|
148
|
+
|
|
149
|
+
// Process connected WebSockets
|
|
150
|
+
let connectedButNotLoadedCount = 0;
|
|
151
|
+
let restoredCount = 0;
|
|
152
|
+
for (const { gatewayId, requestId } of actor.hibernatingRequests) {
|
|
153
|
+
const requestIdStr = idToStr(requestId);
|
|
154
|
+
const meta = metaEntries.find(
|
|
155
|
+
(entry) =>
|
|
156
|
+
arraysEqual(entry.gatewayId, gatewayId) &&
|
|
157
|
+
arraysEqual(entry.requestId, requestId),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (!meta) {
|
|
161
|
+
// Connected but not loaded (not persisted) - close it
|
|
162
|
+
//
|
|
163
|
+
// This may happen if the metadata was not successfully persisted
|
|
164
|
+
this.log?.warn({
|
|
165
|
+
msg: "closing websocket that is not persisted",
|
|
166
|
+
requestId: requestIdStr,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
170
|
+
tag: "ToServerWebSocketClose",
|
|
171
|
+
val: {
|
|
172
|
+
code: 1000,
|
|
173
|
+
reason: "ws.meta_not_found_during_restore",
|
|
174
|
+
hibernate: false,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
connectedButNotLoadedCount++;
|
|
179
|
+
} else {
|
|
180
|
+
// Both connected and persisted - restore it
|
|
181
|
+
const request = buildRequestForWebSocket(
|
|
182
|
+
meta.path,
|
|
183
|
+
meta.headers,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// This will call `runner.config.websocket` under the hood to
|
|
187
|
+
// attach the event listeners to the WebSocket.
|
|
188
|
+
// Track this operation to ensure it completes
|
|
189
|
+
const restoreOperation = this.#createWebSocket(
|
|
190
|
+
actorId,
|
|
191
|
+
gatewayId,
|
|
192
|
+
requestId,
|
|
193
|
+
requestIdStr,
|
|
194
|
+
meta.serverMessageIndex,
|
|
195
|
+
true,
|
|
196
|
+
true,
|
|
197
|
+
request,
|
|
198
|
+
meta.path,
|
|
199
|
+
meta.headers,
|
|
200
|
+
false,
|
|
201
|
+
)
|
|
202
|
+
.then(() => {
|
|
203
|
+
// Create a PendingRequest entry to track the message index
|
|
204
|
+
const actor = this.#runner.getActor(actorId);
|
|
205
|
+
if (actor) {
|
|
206
|
+
actor.createPendingRequest(
|
|
207
|
+
gatewayId,
|
|
208
|
+
requestId,
|
|
209
|
+
meta.clientMessageIndex,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.log?.info({
|
|
214
|
+
msg: "connection successfully restored",
|
|
215
|
+
actorId,
|
|
216
|
+
requestId: requestIdStr,
|
|
217
|
+
});
|
|
218
|
+
})
|
|
219
|
+
.catch((err) => {
|
|
220
|
+
this.log?.error({
|
|
221
|
+
msg: "error creating websocket during restore",
|
|
222
|
+
requestId: requestIdStr,
|
|
223
|
+
error: stringifyError(err),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Close the WebSocket on error
|
|
227
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
228
|
+
tag: "ToServerWebSocketClose",
|
|
229
|
+
val: {
|
|
230
|
+
code: 1011,
|
|
231
|
+
reason: "ws.restore_error",
|
|
232
|
+
hibernate: false,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
backgroundOperations.push(restoreOperation);
|
|
238
|
+
restoredCount++;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Process loaded but not connected (stale) - remove them
|
|
243
|
+
let loadedButNotConnectedCount = 0;
|
|
244
|
+
for (const meta of metaEntries) {
|
|
245
|
+
const requestIdStr = idToStr(meta.requestId);
|
|
246
|
+
const isConnected = actor.hibernatingRequests.some(
|
|
247
|
+
(req) =>
|
|
248
|
+
arraysEqual(req.gatewayId, meta.gatewayId) &&
|
|
249
|
+
arraysEqual(req.requestId, meta.requestId),
|
|
250
|
+
);
|
|
251
|
+
if (!isConnected) {
|
|
252
|
+
this.log?.warn({
|
|
253
|
+
msg: "removing stale persisted websocket",
|
|
254
|
+
requestId: requestIdStr,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const request = buildRequestForWebSocket(
|
|
258
|
+
meta.path,
|
|
259
|
+
meta.headers,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Create adapter to register user's event listeners.
|
|
263
|
+
// Pass engineAlreadyClosed=true so close callback won't send tunnel message.
|
|
264
|
+
// Track this operation to ensure it completes
|
|
265
|
+
const cleanupOperation = this.#createWebSocket(
|
|
266
|
+
actorId,
|
|
267
|
+
meta.gatewayId,
|
|
268
|
+
meta.requestId,
|
|
269
|
+
requestIdStr,
|
|
270
|
+
meta.serverMessageIndex,
|
|
271
|
+
true,
|
|
272
|
+
true,
|
|
273
|
+
request,
|
|
274
|
+
meta.path,
|
|
275
|
+
meta.headers,
|
|
276
|
+
true,
|
|
277
|
+
)
|
|
278
|
+
.then((adapter) => {
|
|
279
|
+
// Close the adapter normally - this will fire user's close event handler
|
|
280
|
+
// (which should clean up persistence) and trigger the close callback
|
|
281
|
+
// (which will clean up maps but skip sending tunnel message)
|
|
282
|
+
adapter.close(1000, "ws.stale_metadata");
|
|
283
|
+
})
|
|
284
|
+
.catch((err) => {
|
|
285
|
+
this.log?.error({
|
|
286
|
+
msg: "error creating stale websocket during restore",
|
|
287
|
+
requestId: requestIdStr,
|
|
288
|
+
error: stringifyError(err),
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
backgroundOperations.push(cleanupOperation);
|
|
293
|
+
loadedButNotConnectedCount++;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Wait for all background operations to complete before finishing
|
|
298
|
+
await Promise.allSettled(backgroundOperations);
|
|
299
|
+
|
|
300
|
+
// Mark restoration as complete
|
|
301
|
+
actor.hibernationRestored = true;
|
|
302
|
+
|
|
303
|
+
this.log?.info({
|
|
304
|
+
msg: "restored hibernatable websockets",
|
|
305
|
+
actorId,
|
|
306
|
+
restoredCount,
|
|
307
|
+
connectedButNotLoadedCount,
|
|
308
|
+
loadedButNotConnectedCount,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Called from WebSocketOpen message and when restoring hibernatable WebSockets.
|
|
314
|
+
*
|
|
315
|
+
* engineAlreadyClosed will be true if this is only being called to trigger
|
|
316
|
+
* the close callback and not to send a close message to the server. This
|
|
317
|
+
* is used specifically to clean up zombie WebSocket connections.
|
|
318
|
+
*/
|
|
319
|
+
async #createWebSocket(
|
|
320
|
+
actorId: string,
|
|
321
|
+
gatewayId: GatewayId,
|
|
322
|
+
requestId: RequestId,
|
|
323
|
+
requestIdStr: string,
|
|
324
|
+
serverMessageIndex: number,
|
|
325
|
+
isHibernatable: boolean,
|
|
326
|
+
isRestoringHibernatable: boolean,
|
|
327
|
+
request: Request,
|
|
328
|
+
path: string,
|
|
329
|
+
headers: Record<string, string>,
|
|
330
|
+
engineAlreadyClosed: boolean,
|
|
331
|
+
): Promise<WebSocketTunnelAdapter> {
|
|
332
|
+
this.log?.debug({
|
|
333
|
+
msg: "createWebSocket creating adapter",
|
|
334
|
+
actorId,
|
|
335
|
+
requestIdStr,
|
|
336
|
+
isHibernatable,
|
|
337
|
+
path,
|
|
338
|
+
});
|
|
339
|
+
// Create WebSocket adapter
|
|
340
|
+
const adapter = new WebSocketTunnelAdapter(
|
|
341
|
+
this,
|
|
342
|
+
actorId,
|
|
343
|
+
requestIdStr,
|
|
344
|
+
serverMessageIndex,
|
|
345
|
+
isHibernatable,
|
|
346
|
+
isRestoringHibernatable,
|
|
347
|
+
request,
|
|
348
|
+
(data: ArrayBuffer | string, isBinary: boolean) => {
|
|
349
|
+
// Send message through tunnel
|
|
350
|
+
const dataBuffer =
|
|
351
|
+
typeof data === "string"
|
|
352
|
+
? (new TextEncoder().encode(data).buffer as ArrayBuffer)
|
|
353
|
+
: data;
|
|
354
|
+
|
|
355
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
356
|
+
tag: "ToServerWebSocketMessage",
|
|
357
|
+
val: {
|
|
358
|
+
data: dataBuffer,
|
|
359
|
+
binary: isBinary,
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
(code?: number, reason?: string) => {
|
|
364
|
+
// Send close through tunnel if engine doesn't already know it's closed
|
|
365
|
+
if (!engineAlreadyClosed) {
|
|
366
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
367
|
+
tag: "ToServerWebSocketClose",
|
|
368
|
+
val: {
|
|
369
|
+
code: code || null,
|
|
370
|
+
reason: reason || null,
|
|
371
|
+
hibernate: false,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Clean up actor tracking
|
|
377
|
+
const actor = this.#runner.getActor(actorId);
|
|
378
|
+
if (actor) {
|
|
379
|
+
actor.deleteWebSocket(gatewayId, requestId);
|
|
380
|
+
actor.deletePendingRequest(gatewayId, requestId);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Clean up request-to-actor mapping
|
|
384
|
+
this.#removeRequestToActor(gatewayId, requestId);
|
|
385
|
+
},
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// Get actor and add websocket to it
|
|
389
|
+
const actor = this.#runner.getActor(actorId);
|
|
390
|
+
if (!actor) {
|
|
391
|
+
throw new Error(`Actor ${actorId} not found`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
actor.setWebSocket(gatewayId, requestId, adapter);
|
|
395
|
+
this.addRequestToActor(gatewayId, requestId, actorId);
|
|
396
|
+
|
|
397
|
+
// Call WebSocket handler. This handler will add event listeners
|
|
398
|
+
// for `open`, etc. Pass the VirtualWebSocket (not the adapter) to the actor.
|
|
399
|
+
await this.#runner.config.websocket(
|
|
400
|
+
this.#runner,
|
|
401
|
+
actorId,
|
|
402
|
+
adapter.websocket,
|
|
403
|
+
gatewayId,
|
|
404
|
+
requestId,
|
|
405
|
+
request,
|
|
406
|
+
path,
|
|
407
|
+
headers,
|
|
408
|
+
isHibernatable,
|
|
409
|
+
isRestoringHibernatable,
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
return adapter;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
addRequestToActor(
|
|
416
|
+
gatewayId: GatewayId,
|
|
417
|
+
requestId: RequestId,
|
|
418
|
+
actorId: string,
|
|
419
|
+
) {
|
|
420
|
+
this.#requestToActor.push({ gatewayId, requestId, actorId });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
#removeRequestToActor(gatewayId: GatewayId, requestId: RequestId) {
|
|
424
|
+
const index = this.#requestToActor.findIndex(
|
|
425
|
+
(entry) =>
|
|
426
|
+
arraysEqual(entry.gatewayId, gatewayId) &&
|
|
427
|
+
arraysEqual(entry.requestId, requestId),
|
|
428
|
+
);
|
|
429
|
+
if (index !== -1) {
|
|
430
|
+
this.#requestToActor.splice(index, 1);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
getRequestActor(
|
|
435
|
+
gatewayId: GatewayId,
|
|
436
|
+
requestId: RequestId,
|
|
437
|
+
): RunnerActor | undefined {
|
|
438
|
+
const entry = this.#requestToActor.find(
|
|
439
|
+
(entry) =>
|
|
440
|
+
arraysEqual(entry.gatewayId, gatewayId) &&
|
|
441
|
+
arraysEqual(entry.requestId, requestId),
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
if (!entry) {
|
|
445
|
+
this.log?.warn({
|
|
446
|
+
msg: "missing requestToActor entry",
|
|
447
|
+
requestId: idToStr(requestId),
|
|
448
|
+
});
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const actor = this.#runner.getActor(entry.actorId);
|
|
453
|
+
if (!actor) {
|
|
454
|
+
this.log?.warn({
|
|
455
|
+
msg: "missing actor for requestToActor lookup",
|
|
456
|
+
requestId: idToStr(requestId),
|
|
457
|
+
actorId: entry.actorId,
|
|
458
|
+
});
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return actor;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async getAndWaitForRequestActor(
|
|
466
|
+
gatewayId: GatewayId,
|
|
467
|
+
requestId: RequestId,
|
|
468
|
+
): Promise<RunnerActor | undefined> {
|
|
469
|
+
const actor = this.getRequestActor(gatewayId, requestId);
|
|
470
|
+
if (!actor) return;
|
|
471
|
+
await actor.actorStartPromise.promise;
|
|
472
|
+
return actor;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
#sendMessage(
|
|
476
|
+
gatewayId: GatewayId,
|
|
477
|
+
requestId: RequestId,
|
|
478
|
+
messageKind: protocol.ToServerTunnelMessageKind,
|
|
479
|
+
) {
|
|
480
|
+
// Buffer message if not connected
|
|
481
|
+
if (!this.#runner.getPegboardWebSocketIfReady()) {
|
|
482
|
+
this.log?.debug({
|
|
483
|
+
msg: "buffering tunnel message, socket not connected to engine",
|
|
484
|
+
requestId: idToStr(requestId),
|
|
485
|
+
message: stringifyToServerTunnelMessageKind(messageKind),
|
|
486
|
+
});
|
|
487
|
+
this.#bufferedMessages.push({ gatewayId, requestId, messageKind });
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Get or initialize message index for this request
|
|
492
|
+
//
|
|
493
|
+
// We don't have to wait for the actor to start since we're not calling
|
|
494
|
+
// any callbacks on the actor
|
|
495
|
+
const gatewayIdStr = idToStr(gatewayId);
|
|
496
|
+
const requestIdStr = idToStr(requestId);
|
|
497
|
+
const actor = this.getRequestActor(gatewayId, requestId);
|
|
498
|
+
if (!actor) {
|
|
499
|
+
this.log?.warn({
|
|
500
|
+
msg: "cannot send tunnel message, actor not found",
|
|
501
|
+
gatewayId: gatewayIdStr,
|
|
502
|
+
requestId: requestIdStr,
|
|
503
|
+
});
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Get message index from pending request
|
|
508
|
+
let clientMessageIndex: number;
|
|
509
|
+
const pending = actor.getPendingRequest(gatewayId, requestId);
|
|
510
|
+
if (pending) {
|
|
511
|
+
clientMessageIndex = pending.clientMessageIndex;
|
|
512
|
+
pending.clientMessageIndex++;
|
|
513
|
+
} else {
|
|
514
|
+
// No pending request
|
|
515
|
+
this.log?.warn({
|
|
516
|
+
msg: "missing pending request for send message, defaulting to message index 0",
|
|
517
|
+
gatewayId: gatewayIdStr,
|
|
518
|
+
requestId: requestIdStr,
|
|
519
|
+
});
|
|
520
|
+
clientMessageIndex = 0;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Build message ID from gatewayId + requestId + messageIndex
|
|
524
|
+
const messageId: protocol.MessageId = {
|
|
525
|
+
gatewayId,
|
|
526
|
+
requestId,
|
|
527
|
+
messageIndex: clientMessageIndex,
|
|
528
|
+
};
|
|
529
|
+
const messageIdStr = `${idToStr(messageId.gatewayId)}-${idToStr(messageId.requestId)}-${messageId.messageIndex}`;
|
|
530
|
+
|
|
531
|
+
this.log?.debug({
|
|
532
|
+
msg: "sending tunnel msg",
|
|
533
|
+
messageId: messageIdStr,
|
|
534
|
+
gatewayId: gatewayIdStr,
|
|
535
|
+
requestId: requestIdStr,
|
|
536
|
+
messageIndex: clientMessageIndex,
|
|
537
|
+
message: stringifyToServerTunnelMessageKind(messageKind),
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Send message
|
|
541
|
+
const message: protocol.ToServer = {
|
|
542
|
+
tag: "ToServerTunnelMessage",
|
|
543
|
+
val: {
|
|
544
|
+
messageId,
|
|
545
|
+
messageKind,
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
this.#runner.__sendToServer(message);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
closeActiveRequests(actor: RunnerActor) {
|
|
552
|
+
const actorId = actor.actorId;
|
|
553
|
+
|
|
554
|
+
// Terminate all requests for this actor. This will no send a
|
|
555
|
+
// ToServerResponse* message since the actor will no longer be loaded.
|
|
556
|
+
// The gateway is responsible for closing the request.
|
|
557
|
+
for (const entry of actor.pendingRequests) {
|
|
558
|
+
entry.request.reject(new Error(`Actor ${actorId} stopped`));
|
|
559
|
+
if (entry.gatewayId && entry.requestId) {
|
|
560
|
+
this.#removeRequestToActor(entry.gatewayId, entry.requestId);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Close all WebSockets. Only send close event to non-HWS. The gateway is
|
|
565
|
+
// responsible for hibernating HWS and closing regular WS.
|
|
566
|
+
for (const entry of actor.webSockets) {
|
|
567
|
+
const isHibernatable = entry.ws[HIBERNATABLE_SYMBOL];
|
|
568
|
+
if (!isHibernatable) {
|
|
569
|
+
entry.ws._closeWithoutCallback(1000, "actor.stopped");
|
|
570
|
+
}
|
|
571
|
+
// Note: request-to-actor mapping is cleaned up in the close callback
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async #fetch(
|
|
576
|
+
actorId: string,
|
|
577
|
+
gatewayId: protocol.GatewayId,
|
|
578
|
+
requestId: protocol.RequestId,
|
|
579
|
+
request: Request,
|
|
580
|
+
): Promise<Response> {
|
|
581
|
+
// Validate actor exists
|
|
582
|
+
if (!this.#runner.hasActor(actorId)) {
|
|
583
|
+
this.log?.warn({
|
|
584
|
+
msg: "ignoring request for unknown actor",
|
|
585
|
+
actorId,
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// NOTE: This is a special response that will cause Guard to retry the request
|
|
589
|
+
//
|
|
590
|
+
// See should_retry_request_inner
|
|
591
|
+
// https://github.com/rivet-dev/rivet/blob/222dae87e3efccaffa2b503de40ecf8afd4e31eb/engine/packages/guard-core/src/proxy_service.rs#L2458
|
|
592
|
+
return new Response("Actor not found", {
|
|
593
|
+
status: 503,
|
|
594
|
+
headers: { "x-rivet-error": "runner.actor_not_found" },
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const fetchHandler = this.#runner.config.fetch(
|
|
599
|
+
this.#runner,
|
|
600
|
+
actorId,
|
|
601
|
+
gatewayId,
|
|
602
|
+
requestId,
|
|
603
|
+
request,
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
if (!fetchHandler) {
|
|
607
|
+
return new Response("Not Implemented", { status: 501 });
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return fetchHandler;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async handleTunnelMessage(message: protocol.ToClientTunnelMessage) {
|
|
614
|
+
// Parse the gateway ID, request ID, and message index from the messageId
|
|
615
|
+
const { gatewayId, requestId, messageIndex } = message.messageId;
|
|
616
|
+
|
|
617
|
+
const gatewayIdStr = idToStr(gatewayId);
|
|
618
|
+
const requestIdStr = idToStr(requestId);
|
|
619
|
+
this.log?.debug({
|
|
620
|
+
msg: "receive tunnel msg",
|
|
621
|
+
gatewayId: gatewayIdStr,
|
|
622
|
+
requestId: requestIdStr,
|
|
623
|
+
messageIndex: message.messageId.messageIndex,
|
|
624
|
+
message: stringifyToClientTunnelMessageKind(message.messageKind),
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
switch (message.messageKind.tag) {
|
|
628
|
+
case "ToClientRequestStart":
|
|
629
|
+
await this.#handleRequestStart(
|
|
630
|
+
gatewayId,
|
|
631
|
+
requestId,
|
|
632
|
+
message.messageKind.val,
|
|
633
|
+
);
|
|
634
|
+
break;
|
|
635
|
+
case "ToClientRequestChunk":
|
|
636
|
+
await this.#handleRequestChunk(
|
|
637
|
+
gatewayId,
|
|
638
|
+
requestId,
|
|
639
|
+
message.messageKind.val,
|
|
640
|
+
);
|
|
641
|
+
break;
|
|
642
|
+
case "ToClientRequestAbort":
|
|
643
|
+
await this.#handleRequestAbort(gatewayId, requestId);
|
|
644
|
+
break;
|
|
645
|
+
case "ToClientWebSocketOpen":
|
|
646
|
+
await this.#handleWebSocketOpen(
|
|
647
|
+
gatewayId,
|
|
648
|
+
requestId,
|
|
649
|
+
message.messageKind.val,
|
|
650
|
+
);
|
|
651
|
+
break;
|
|
652
|
+
case "ToClientWebSocketMessage": {
|
|
653
|
+
await this.#handleWebSocketMessage(
|
|
654
|
+
gatewayId,
|
|
655
|
+
requestId,
|
|
656
|
+
messageIndex,
|
|
657
|
+
message.messageKind.val,
|
|
658
|
+
);
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
case "ToClientWebSocketClose":
|
|
662
|
+
await this.#handleWebSocketClose(
|
|
663
|
+
gatewayId,
|
|
664
|
+
requestId,
|
|
665
|
+
message.messageKind.val,
|
|
666
|
+
);
|
|
667
|
+
break;
|
|
668
|
+
default:
|
|
669
|
+
unreachable(message.messageKind);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async #handleRequestStart(
|
|
674
|
+
gatewayId: GatewayId,
|
|
675
|
+
requestId: RequestId,
|
|
676
|
+
req: protocol.ToClientRequestStart,
|
|
677
|
+
) {
|
|
678
|
+
// Track this request for the actor
|
|
679
|
+
const requestIdStr = idToStr(requestId);
|
|
680
|
+
const actor = await this.#runner.getAndWaitForActor(req.actorId);
|
|
681
|
+
if (!actor) {
|
|
682
|
+
this.log?.warn({
|
|
683
|
+
msg: "actor does not exist in handleRequestStart, request will leak",
|
|
684
|
+
actorId: req.actorId,
|
|
685
|
+
requestId: requestIdStr,
|
|
686
|
+
});
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Add to request-to-actor mapping
|
|
691
|
+
this.addRequestToActor(gatewayId, requestId, req.actorId);
|
|
692
|
+
|
|
693
|
+
try {
|
|
694
|
+
// Convert headers map to Headers object
|
|
695
|
+
const headers = new Headers();
|
|
696
|
+
for (const [key, value] of req.headers) {
|
|
697
|
+
headers.append(key, value);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Create Request object
|
|
701
|
+
const request = new Request(`http://localhost${req.path}`, {
|
|
702
|
+
method: req.method,
|
|
703
|
+
headers,
|
|
704
|
+
body: req.body ? new Uint8Array(req.body) : undefined,
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// Handle streaming request
|
|
708
|
+
if (req.stream) {
|
|
709
|
+
// Create a stream for the request body
|
|
710
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
711
|
+
start: (controller) => {
|
|
712
|
+
// Store controller for chunks
|
|
713
|
+
const existing = actor.getPendingRequest(
|
|
714
|
+
gatewayId,
|
|
715
|
+
requestId,
|
|
716
|
+
);
|
|
717
|
+
if (existing) {
|
|
718
|
+
existing.streamController = controller;
|
|
719
|
+
existing.actorId = req.actorId;
|
|
720
|
+
existing.gatewayId = gatewayId;
|
|
721
|
+
existing.requestId = requestId;
|
|
722
|
+
} else {
|
|
723
|
+
actor.createPendingRequestWithStreamController(
|
|
724
|
+
gatewayId,
|
|
725
|
+
requestId,
|
|
726
|
+
0,
|
|
727
|
+
controller,
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// Create request with streaming body
|
|
734
|
+
const streamingRequest = new Request(request, {
|
|
735
|
+
body: stream,
|
|
736
|
+
duplex: "half",
|
|
737
|
+
} as any);
|
|
738
|
+
|
|
739
|
+
// Call fetch handler with validation
|
|
740
|
+
const response = await this.#fetch(
|
|
741
|
+
req.actorId,
|
|
742
|
+
gatewayId,
|
|
743
|
+
requestId,
|
|
744
|
+
streamingRequest,
|
|
745
|
+
);
|
|
746
|
+
await this.#sendResponse(
|
|
747
|
+
actor.actorId,
|
|
748
|
+
actor.generation,
|
|
749
|
+
gatewayId,
|
|
750
|
+
requestId,
|
|
751
|
+
response,
|
|
752
|
+
);
|
|
753
|
+
} else {
|
|
754
|
+
// Non-streaming request
|
|
755
|
+
// Create a pending request entry to track messageIndex for the response
|
|
756
|
+
actor.createPendingRequest(gatewayId, requestId, 0);
|
|
757
|
+
|
|
758
|
+
const response = await this.#fetch(
|
|
759
|
+
req.actorId,
|
|
760
|
+
gatewayId,
|
|
761
|
+
requestId,
|
|
762
|
+
request,
|
|
763
|
+
);
|
|
764
|
+
await this.#sendResponse(
|
|
765
|
+
actor.actorId,
|
|
766
|
+
actor.generation,
|
|
767
|
+
gatewayId,
|
|
768
|
+
requestId,
|
|
769
|
+
response,
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
} catch (error) {
|
|
773
|
+
if (error instanceof RunnerShutdownError) {
|
|
774
|
+
this.log?.debug({ msg: "catught runner shutdown error" });
|
|
775
|
+
} else {
|
|
776
|
+
this.log?.error({ msg: "error handling request", error });
|
|
777
|
+
this.#sendResponseError(
|
|
778
|
+
actor.actorId,
|
|
779
|
+
actor.generation,
|
|
780
|
+
gatewayId,
|
|
781
|
+
requestId,
|
|
782
|
+
500,
|
|
783
|
+
"Internal Server Error",
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
} finally {
|
|
787
|
+
// Clean up request tracking
|
|
788
|
+
if (this.#runner.hasActor(req.actorId, actor.generation)) {
|
|
789
|
+
actor.deletePendingRequest(gatewayId, requestId);
|
|
790
|
+
this.#removeRequestToActor(gatewayId, requestId);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async #handleRequestChunk(
|
|
796
|
+
gatewayId: GatewayId,
|
|
797
|
+
requestId: RequestId,
|
|
798
|
+
chunk: protocol.ToClientRequestChunk,
|
|
799
|
+
) {
|
|
800
|
+
const actor = await this.getAndWaitForRequestActor(
|
|
801
|
+
gatewayId,
|
|
802
|
+
requestId,
|
|
803
|
+
);
|
|
804
|
+
if (actor) {
|
|
805
|
+
const pending = actor.getPendingRequest(gatewayId, requestId);
|
|
806
|
+
if (pending?.streamController) {
|
|
807
|
+
pending.streamController.enqueue(new Uint8Array(chunk.body));
|
|
808
|
+
if (chunk.finish) {
|
|
809
|
+
pending.streamController.close();
|
|
810
|
+
actor.deletePendingRequest(gatewayId, requestId);
|
|
811
|
+
this.#removeRequestToActor(gatewayId, requestId);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async #handleRequestAbort(gatewayId: GatewayId, requestId: RequestId) {
|
|
818
|
+
const actor = await this.getAndWaitForRequestActor(
|
|
819
|
+
gatewayId,
|
|
820
|
+
requestId,
|
|
821
|
+
);
|
|
822
|
+
if (actor) {
|
|
823
|
+
const pending = actor.getPendingRequest(gatewayId, requestId);
|
|
824
|
+
if (pending?.streamController) {
|
|
825
|
+
pending.streamController.error(new Error("Request aborted"));
|
|
826
|
+
}
|
|
827
|
+
actor.deletePendingRequest(gatewayId, requestId);
|
|
828
|
+
this.#removeRequestToActor(gatewayId, requestId);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async #sendResponse(
|
|
833
|
+
actorId: string,
|
|
834
|
+
generation: number,
|
|
835
|
+
gatewayId: GatewayId,
|
|
836
|
+
requestId: ArrayBuffer,
|
|
837
|
+
response: Response,
|
|
838
|
+
) {
|
|
839
|
+
if (!this.#runner.hasActor(actorId, generation)) {
|
|
840
|
+
this.log?.warn({
|
|
841
|
+
msg: "actor not loaded to send response, assuming gateway has closed request",
|
|
842
|
+
actorId,
|
|
843
|
+
generation,
|
|
844
|
+
requestId,
|
|
845
|
+
});
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Always treat responses as non-streaming for now
|
|
850
|
+
// In the future, we could detect streaming responses based on:
|
|
851
|
+
// - Transfer-Encoding: chunked
|
|
852
|
+
// - Content-Type: text/event-stream
|
|
853
|
+
// - Explicit stream flag from the handler
|
|
854
|
+
|
|
855
|
+
// Read the body first to get the actual content
|
|
856
|
+
const body = response.body ? await response.arrayBuffer() : null;
|
|
857
|
+
|
|
858
|
+
// Convert headers to map and add Content-Length if not present
|
|
859
|
+
const headers = new Map<string, string>();
|
|
860
|
+
response.headers.forEach((value, key) => {
|
|
861
|
+
headers.set(key, value);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// Add Content-Length header if we have a body and it's not already set
|
|
865
|
+
if (body && !headers.has("content-length")) {
|
|
866
|
+
headers.set("content-length", String(body.byteLength));
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Send as non-streaming response if actor has not stopped
|
|
870
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
871
|
+
tag: "ToServerResponseStart",
|
|
872
|
+
val: {
|
|
873
|
+
status: response.status as protocol.u16,
|
|
874
|
+
headers,
|
|
875
|
+
body: body || null,
|
|
876
|
+
stream: false,
|
|
877
|
+
},
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
#sendResponseError(
|
|
882
|
+
actorId: string,
|
|
883
|
+
generation: number,
|
|
884
|
+
gatewayId: GatewayId,
|
|
885
|
+
requestId: ArrayBuffer,
|
|
886
|
+
status: number,
|
|
887
|
+
message: string,
|
|
888
|
+
) {
|
|
889
|
+
if (!this.#runner.hasActor(actorId, generation)) {
|
|
890
|
+
this.log?.warn({
|
|
891
|
+
msg: "actor not loaded to send response, assuming gateway has closed request",
|
|
892
|
+
actorId,
|
|
893
|
+
generation,
|
|
894
|
+
requestId,
|
|
895
|
+
});
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const headers = new Map<string, string>();
|
|
900
|
+
headers.set("content-type", "text/plain");
|
|
901
|
+
|
|
902
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
903
|
+
tag: "ToServerResponseStart",
|
|
904
|
+
val: {
|
|
905
|
+
status: status as protocol.u16,
|
|
906
|
+
headers,
|
|
907
|
+
body: new TextEncoder().encode(message).buffer as ArrayBuffer,
|
|
908
|
+
stream: false,
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async #handleWebSocketOpen(
|
|
914
|
+
gatewayId: GatewayId,
|
|
915
|
+
requestId: RequestId,
|
|
916
|
+
open: protocol.ToClientWebSocketOpen,
|
|
917
|
+
) {
|
|
918
|
+
// NOTE: This method is safe to be async since we will not receive any
|
|
919
|
+
// further WebSocket events until we send a ToServerWebSocketOpen
|
|
920
|
+
// tunnel message. We can do any async logic we need to between those two events.
|
|
921
|
+
//
|
|
922
|
+
// Sending a ToServerWebSocketClose will terminate the WebSocket early.
|
|
923
|
+
|
|
924
|
+
const requestIdStr = idToStr(requestId);
|
|
925
|
+
|
|
926
|
+
// Validate actor exists
|
|
927
|
+
const actor = await this.#runner.getAndWaitForActor(open.actorId);
|
|
928
|
+
if (!actor) {
|
|
929
|
+
this.log?.warn({
|
|
930
|
+
msg: "ignoring websocket for unknown actor",
|
|
931
|
+
actorId: open.actorId,
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
// NOTE: Closing a WebSocket before open is equivalent to a Service
|
|
935
|
+
// Unavailable error and will cause Guard to retry the request
|
|
936
|
+
//
|
|
937
|
+
// See
|
|
938
|
+
// https://github.com/rivet-dev/rivet/blob/222dae87e3efccaffa2b503de40ecf8afd4e31eb/engine/packages/pegboard-gateway/src/lib.rs#L238
|
|
939
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
940
|
+
tag: "ToServerWebSocketClose",
|
|
941
|
+
val: {
|
|
942
|
+
code: 1011,
|
|
943
|
+
reason: "Actor not found",
|
|
944
|
+
hibernate: false,
|
|
945
|
+
},
|
|
946
|
+
});
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Close existing WebSocket if one already exists for this request ID.
|
|
951
|
+
// This should never happen, but prevents any potential duplicate
|
|
952
|
+
// WebSockets from retransmits.
|
|
953
|
+
const existingAdapter = actor.getWebSocket(gatewayId, requestId);
|
|
954
|
+
if (existingAdapter) {
|
|
955
|
+
this.log?.warn({
|
|
956
|
+
msg: "closing existing websocket for duplicate open event for the same request id",
|
|
957
|
+
requestId: requestIdStr,
|
|
958
|
+
});
|
|
959
|
+
// Close without sending a message through the tunnel since the server
|
|
960
|
+
// already knows about the new connection
|
|
961
|
+
existingAdapter._closeWithoutCallback(1000, "ws.duplicate_open");
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Create WebSocket
|
|
965
|
+
try {
|
|
966
|
+
const request = buildRequestForWebSocket(
|
|
967
|
+
open.path,
|
|
968
|
+
Object.fromEntries(open.headers),
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
const canHibernate =
|
|
972
|
+
this.#runner.config.hibernatableWebSocket.canHibernate(
|
|
973
|
+
actor.actorId,
|
|
974
|
+
gatewayId,
|
|
975
|
+
requestId,
|
|
976
|
+
request,
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
// #createWebSocket will call `runner.config.websocket` under the
|
|
980
|
+
// hood to add the event listeners for open, etc. If this handler
|
|
981
|
+
// throws, then the WebSocket will be closed before sending the
|
|
982
|
+
// open event.
|
|
983
|
+
const adapter = await this.#createWebSocket(
|
|
984
|
+
actor.actorId,
|
|
985
|
+
gatewayId,
|
|
986
|
+
requestId,
|
|
987
|
+
requestIdStr,
|
|
988
|
+
0,
|
|
989
|
+
canHibernate,
|
|
990
|
+
false,
|
|
991
|
+
request,
|
|
992
|
+
open.path,
|
|
993
|
+
Object.fromEntries(open.headers),
|
|
994
|
+
false,
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
// Create a PendingRequest entry to track the message index
|
|
998
|
+
actor.createPendingRequest(gatewayId, requestId, 0);
|
|
999
|
+
|
|
1000
|
+
// Open the WebSocket after `config.socket` so (a) the event
|
|
1001
|
+
// handlers can be added and (b) any errors in `config.websocket`
|
|
1002
|
+
// will cause the WebSocket to terminate before the open event.
|
|
1003
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
1004
|
+
tag: "ToServerWebSocketOpen",
|
|
1005
|
+
val: {
|
|
1006
|
+
canHibernate,
|
|
1007
|
+
},
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
// Dispatch open event
|
|
1011
|
+
adapter._handleOpen(requestId);
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
this.log?.error({ msg: "error handling websocket open", error });
|
|
1014
|
+
|
|
1015
|
+
// TODO: Call close event on adapter if needed
|
|
1016
|
+
|
|
1017
|
+
// Send close on error
|
|
1018
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
1019
|
+
tag: "ToServerWebSocketClose",
|
|
1020
|
+
val: {
|
|
1021
|
+
code: 1011,
|
|
1022
|
+
reason: "Server Error",
|
|
1023
|
+
hibernate: false,
|
|
1024
|
+
},
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
// Clean up actor tracking
|
|
1028
|
+
actor.deleteWebSocket(gatewayId, requestId);
|
|
1029
|
+
actor.deletePendingRequest(gatewayId, requestId);
|
|
1030
|
+
this.#removeRequestToActor(gatewayId, requestId);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
async #handleWebSocketMessage(
|
|
1035
|
+
gatewayId: GatewayId,
|
|
1036
|
+
requestId: RequestId,
|
|
1037
|
+
serverMessageIndex: number,
|
|
1038
|
+
msg: protocol.ToClientWebSocketMessage,
|
|
1039
|
+
) {
|
|
1040
|
+
const actor = await this.getAndWaitForRequestActor(
|
|
1041
|
+
gatewayId,
|
|
1042
|
+
requestId,
|
|
1043
|
+
);
|
|
1044
|
+
if (actor) {
|
|
1045
|
+
const adapter = actor.getWebSocket(gatewayId, requestId);
|
|
1046
|
+
if (adapter) {
|
|
1047
|
+
const data = msg.binary
|
|
1048
|
+
? new Uint8Array(msg.data)
|
|
1049
|
+
: new TextDecoder().decode(new Uint8Array(msg.data));
|
|
1050
|
+
|
|
1051
|
+
adapter._handleMessage(
|
|
1052
|
+
requestId,
|
|
1053
|
+
data,
|
|
1054
|
+
serverMessageIndex,
|
|
1055
|
+
msg.binary,
|
|
1056
|
+
);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// TODO: This will never retransmit the socket and the socket will close
|
|
1062
|
+
this.log?.warn({
|
|
1063
|
+
msg: "missing websocket for incoming websocket message, this may indicate the actor stopped before processing a message",
|
|
1064
|
+
requestId,
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
sendHibernatableWebSocketMessageAck(
|
|
1069
|
+
gatewayId: ArrayBuffer,
|
|
1070
|
+
requestId: ArrayBuffer,
|
|
1071
|
+
clientMessageIndex: number,
|
|
1072
|
+
) {
|
|
1073
|
+
const requestIdStr = idToStr(requestId);
|
|
1074
|
+
|
|
1075
|
+
this.log?.debug({
|
|
1076
|
+
msg: "ack ws msg",
|
|
1077
|
+
requestId: requestIdStr,
|
|
1078
|
+
index: clientMessageIndex,
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
if (clientMessageIndex < 0 || clientMessageIndex > 65535)
|
|
1082
|
+
throw new Error("invalid websocket ack index");
|
|
1083
|
+
|
|
1084
|
+
// Get the actor to find the gatewayId
|
|
1085
|
+
//
|
|
1086
|
+
// We don't have to wait for the actor to start since we're not calling
|
|
1087
|
+
// any callbacks on the actor
|
|
1088
|
+
const actor = this.getRequestActor(gatewayId, requestId);
|
|
1089
|
+
if (!actor) {
|
|
1090
|
+
this.log?.warn({
|
|
1091
|
+
msg: "cannot send websocket ack, actor not found",
|
|
1092
|
+
requestId: requestIdStr,
|
|
1093
|
+
});
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Get gatewayId from the pending request
|
|
1098
|
+
const pending = actor.getPendingRequest(gatewayId, requestId);
|
|
1099
|
+
if (!pending?.gatewayId) {
|
|
1100
|
+
this.log?.warn({
|
|
1101
|
+
msg: "cannot send websocket ack, gatewayId not found in pending request",
|
|
1102
|
+
requestId: requestIdStr,
|
|
1103
|
+
});
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Send the ack message
|
|
1108
|
+
this.#sendMessage(pending.gatewayId, requestId, {
|
|
1109
|
+
tag: "ToServerWebSocketMessageAck",
|
|
1110
|
+
val: {
|
|
1111
|
+
index: clientMessageIndex,
|
|
1112
|
+
},
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
async #handleWebSocketClose(
|
|
1117
|
+
gatewayId: GatewayId,
|
|
1118
|
+
requestId: RequestId,
|
|
1119
|
+
close: protocol.ToClientWebSocketClose,
|
|
1120
|
+
) {
|
|
1121
|
+
const actor = await this.getAndWaitForRequestActor(
|
|
1122
|
+
gatewayId,
|
|
1123
|
+
requestId,
|
|
1124
|
+
);
|
|
1125
|
+
if (actor) {
|
|
1126
|
+
const adapter = actor.getWebSocket(gatewayId, requestId);
|
|
1127
|
+
if (adapter) {
|
|
1128
|
+
// We don't need to send a close response
|
|
1129
|
+
adapter._handleClose(
|
|
1130
|
+
requestId,
|
|
1131
|
+
close.code || undefined,
|
|
1132
|
+
close.reason || undefined,
|
|
1133
|
+
);
|
|
1134
|
+
actor.deleteWebSocket(gatewayId, requestId);
|
|
1135
|
+
actor.deletePendingRequest(gatewayId, requestId);
|
|
1136
|
+
this.#removeRequestToActor(gatewayId, requestId);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Builds a request that represents the incoming request for a given WebSocket.
|
|
1144
|
+
*
|
|
1145
|
+
* This request is not a real request and will never be sent. It's used to be passed to the actor to behave like a real incoming request.
|
|
1146
|
+
*/
|
|
1147
|
+
function buildRequestForWebSocket(
|
|
1148
|
+
path: string,
|
|
1149
|
+
headers: Record<string, string>,
|
|
1150
|
+
): Request {
|
|
1151
|
+
// We need to manually ensure the original Upgrade/Connection WS
|
|
1152
|
+
// headers are present
|
|
1153
|
+
const fullHeaders = {
|
|
1154
|
+
...headers,
|
|
1155
|
+
Upgrade: "websocket",
|
|
1156
|
+
Connection: "Upgrade",
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
if (!path.startsWith("/")) {
|
|
1160
|
+
throw new Error("path must start with leading slash");
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const request = new Request(`http://actor${path}`, {
|
|
1164
|
+
method: "GET",
|
|
1165
|
+
headers: fullHeaders,
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
return request;
|
|
1169
|
+
}
|