@rivetkit/engine-runner 25.7.1-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/src/tunnel.ts ADDED
@@ -0,0 +1,841 @@
1
+ import WebSocket from "ws";
2
+ import * as tunnel from "@rivetkit/engine-tunnel-protocol";
3
+ import { WebSocketTunnelAdapter } from "./websocket-tunnel-adapter";
4
+ import { calculateBackoff } from "./utils";
5
+ import type { Runner, ActorInstance } from "./mod";
6
+ import { v4 as uuidv4 } from "uuid";
7
+ import { logger } from "./log";
8
+
9
+ const GC_INTERVAL = 60000; // 60 seconds
10
+ const MESSAGE_ACK_TIMEOUT = 5000; // 5 seconds
11
+
12
+ interface PendingRequest {
13
+ resolve: (response: Response) => void;
14
+ reject: (error: Error) => void;
15
+ streamController?: ReadableStreamDefaultController<Uint8Array>;
16
+ actorId?: string;
17
+ }
18
+
19
+ interface TunnelCallbacks {
20
+ onConnected(): void;
21
+ onDisconnected(): void;
22
+ }
23
+
24
+ interface PendingMessage {
25
+ sentAt: number;
26
+ requestIdStr: string;
27
+ }
28
+
29
+ export class Tunnel {
30
+ #pegboardTunnelUrl: string;
31
+
32
+ #runner: Runner;
33
+
34
+ #tunnelWs?: WebSocket;
35
+ #shutdown = false;
36
+ #reconnectTimeout?: NodeJS.Timeout;
37
+ #reconnectAttempt = 0;
38
+
39
+ #actorPendingRequests: Map<string, PendingRequest> = new Map();
40
+ #actorWebSockets: Map<string, WebSocketTunnelAdapter> = new Map();
41
+
42
+ #pendingMessages: Map<string, PendingMessage> = new Map();
43
+ #gcInterval?: NodeJS.Timeout;
44
+
45
+ #callbacks: TunnelCallbacks;
46
+
47
+ constructor(
48
+ runner: Runner,
49
+ pegboardTunnelUrl: string,
50
+ callbacks: TunnelCallbacks,
51
+ ) {
52
+ this.#pegboardTunnelUrl = pegboardTunnelUrl;
53
+ this.#runner = runner;
54
+ this.#callbacks = callbacks;
55
+ }
56
+
57
+ start(): void {
58
+ if (this.#tunnelWs?.readyState === WebSocket.OPEN) {
59
+ return;
60
+ }
61
+
62
+ this.#connect();
63
+ this.#startGarbageCollector();
64
+ }
65
+
66
+ shutdown() {
67
+ this.#shutdown = true;
68
+
69
+ if (this.#reconnectTimeout) {
70
+ clearTimeout(this.#reconnectTimeout);
71
+ this.#reconnectTimeout = undefined;
72
+ }
73
+
74
+ if (this.#gcInterval) {
75
+ clearInterval(this.#gcInterval);
76
+ this.#gcInterval = undefined;
77
+ }
78
+
79
+ if (this.#tunnelWs) {
80
+ this.#tunnelWs.close();
81
+ this.#tunnelWs = undefined;
82
+ }
83
+
84
+ // TODO: Should we use unregisterActor instead
85
+
86
+ // Reject all pending requests
87
+ for (const [_, request] of this.#actorPendingRequests) {
88
+ request.reject(new Error("Tunnel shutting down"));
89
+ }
90
+ this.#actorPendingRequests.clear();
91
+
92
+ // Close all WebSockets
93
+ for (const [_, ws] of this.#actorWebSockets) {
94
+ ws.close();
95
+ }
96
+ this.#actorWebSockets.clear();
97
+ }
98
+
99
+ #sendMessage(requestId: tunnel.RequestId, messageKind: tunnel.MessageKind) {
100
+ if (!this.#tunnelWs || this.#tunnelWs.readyState !== WebSocket.OPEN) {
101
+ console.warn("Cannot send tunnel message, WebSocket not connected");
102
+ return;
103
+ }
104
+
105
+ // Build message
106
+ const messageId = generateUuidBuffer();
107
+
108
+ const requestIdStr = bufferToString(requestId);
109
+ this.#pendingMessages.set(bufferToString(messageId), {
110
+ sentAt: Date.now(),
111
+ requestIdStr,
112
+ });
113
+
114
+ // Send message
115
+ const message: tunnel.RunnerMessage = {
116
+ requestId,
117
+ messageId,
118
+ messageKind,
119
+ };
120
+
121
+ const encoded = tunnel.encodeRunnerMessage(message);
122
+ this.#tunnelWs.send(encoded);
123
+ }
124
+
125
+ #sendAck(requestId: tunnel.RequestId, messageId: tunnel.MessageId) {
126
+ if (!this.#tunnelWs || this.#tunnelWs.readyState !== WebSocket.OPEN) {
127
+ return;
128
+ }
129
+
130
+ const message: tunnel.RunnerMessage = {
131
+ requestId,
132
+ messageId,
133
+ messageKind: { tag: "Ack", val: null },
134
+ };
135
+
136
+ const encoded = tunnel.encodeRunnerMessage(message);
137
+ this.#tunnelWs.send(encoded);
138
+ }
139
+
140
+ #startGarbageCollector() {
141
+ if (this.#gcInterval) {
142
+ clearInterval(this.#gcInterval);
143
+ }
144
+
145
+ this.#gcInterval = setInterval(() => {
146
+ this.#gc();
147
+ }, GC_INTERVAL);
148
+ }
149
+
150
+ #gc() {
151
+ const now = Date.now();
152
+ const messagesToDelete: string[] = [];
153
+
154
+ for (const [messageId, pendingMessage] of this.#pendingMessages) {
155
+ // Check if message is older than timeout
156
+ if (now - pendingMessage.sentAt > MESSAGE_ACK_TIMEOUT) {
157
+ messagesToDelete.push(messageId);
158
+
159
+ const requestIdStr = pendingMessage.requestIdStr;
160
+
161
+ // Check if this is an HTTP request
162
+ const pendingRequest =
163
+ this.#actorPendingRequests.get(requestIdStr);
164
+ if (pendingRequest) {
165
+ // Reject the pending HTTP request
166
+ pendingRequest.reject(
167
+ new Error("Message acknowledgment timeout"),
168
+ );
169
+
170
+ // Close stream controller if it exists
171
+ if (pendingRequest.streamController) {
172
+ pendingRequest.streamController.error(
173
+ new Error("Message acknowledgment timeout"),
174
+ );
175
+ }
176
+
177
+ // Clean up from actorPendingRequests map
178
+ this.#actorPendingRequests.delete(requestIdStr);
179
+ }
180
+
181
+ // Check if this is a WebSocket
182
+ const webSocket = this.#actorWebSockets.get(requestIdStr);
183
+ if (webSocket) {
184
+ // Close the WebSocket connection
185
+ webSocket.close(1000, "Message acknowledgment timeout");
186
+
187
+ // Clean up from actorWebSockets map
188
+ this.#actorWebSockets.delete(requestIdStr);
189
+ }
190
+ }
191
+ }
192
+
193
+ // Remove timed out messages
194
+ for (const messageId of messagesToDelete) {
195
+ this.#pendingMessages.delete(messageId);
196
+ console.warn(`Purged unacked message: ${messageId}`);
197
+ }
198
+ }
199
+
200
+ unregisterActor(actor: ActorInstance) {
201
+ const actorId = actor.actorId;
202
+
203
+ // Terminate all requests for this actor
204
+ for (const requestId of actor.requests) {
205
+ const pending = this.#actorPendingRequests.get(requestId);
206
+ if (pending) {
207
+ pending.reject(new Error(`Actor ${actorId} stopped`));
208
+ this.#actorPendingRequests.delete(requestId);
209
+ }
210
+ }
211
+ actor.requests.clear();
212
+
213
+ // Close all WebSockets for this actor
214
+ for (const webSocketId of actor.webSockets) {
215
+ const ws = this.#actorWebSockets.get(webSocketId);
216
+ if (ws) {
217
+ ws.close(1000, "Actor stopped");
218
+ this.#actorWebSockets.delete(webSocketId);
219
+ }
220
+ }
221
+ actor.webSockets.clear();
222
+ }
223
+
224
+ async #fetch(actorId: string, request: Request): Promise<Response> {
225
+ // Validate actor exists
226
+ if (!this.#runner.hasActor(actorId)) {
227
+ logger()?.warn({
228
+ msg: "ignoring request for unknown actor",
229
+ actorId,
230
+ });
231
+ return new Response("Actor not found", { status: 404 });
232
+ }
233
+
234
+ const fetchHandler = this.#runner.config.fetch(actorId, request);
235
+
236
+ if (!fetchHandler) {
237
+ return new Response("Not Implemented", { status: 501 });
238
+ }
239
+
240
+ return fetchHandler;
241
+ }
242
+
243
+ #connect() {
244
+ if (this.#shutdown) return;
245
+
246
+ try {
247
+ this.#tunnelWs = new WebSocket(this.#pegboardTunnelUrl, {
248
+ headers: {
249
+ "x-rivet-target": "tunnel",
250
+ },
251
+ });
252
+
253
+ this.#tunnelWs.binaryType = "arraybuffer";
254
+
255
+ this.#tunnelWs.addEventListener("open", () => {
256
+ this.#reconnectAttempt = 0;
257
+
258
+ if (this.#reconnectTimeout) {
259
+ clearTimeout(this.#reconnectTimeout);
260
+ this.#reconnectTimeout = undefined;
261
+ }
262
+
263
+ this.#callbacks.onConnected();
264
+ });
265
+
266
+ this.#tunnelWs.addEventListener("message", async (event) => {
267
+ try {
268
+ await this.#handleMessage(event.data as ArrayBuffer);
269
+ } catch (error) {
270
+ logger()?.error({
271
+ msg: "error handling tunnel message",
272
+ error,
273
+ });
274
+ }
275
+ });
276
+
277
+ this.#tunnelWs.addEventListener("error", (event) => {
278
+ logger()?.error({ msg: "tunnel websocket error", event });
279
+ });
280
+
281
+ this.#tunnelWs.addEventListener("close", () => {
282
+ this.#callbacks.onDisconnected();
283
+
284
+ if (!this.#shutdown) {
285
+ this.#scheduleReconnect();
286
+ }
287
+ });
288
+ } catch (error) {
289
+ logger()?.error({ msg: "failed to connect tunnel", error });
290
+ if (!this.#shutdown) {
291
+ this.#scheduleReconnect();
292
+ }
293
+ }
294
+ }
295
+
296
+ #scheduleReconnect() {
297
+ if (this.#shutdown) return;
298
+
299
+ const delay = calculateBackoff(this.#reconnectAttempt, {
300
+ initialDelay: 1000,
301
+ maxDelay: 30000,
302
+ multiplier: 2,
303
+ jitter: true,
304
+ });
305
+
306
+ this.#reconnectAttempt++;
307
+
308
+ this.#reconnectTimeout = setTimeout(() => {
309
+ this.#connect();
310
+ }, delay);
311
+ }
312
+
313
+ async #handleMessage(data: ArrayBuffer) {
314
+ const message = tunnel.decodeRunnerMessage(new Uint8Array(data));
315
+
316
+ if (message.messageKind.tag === "Ack") {
317
+ // Mark pending message as acknowledged and remove it
318
+ const msgIdStr = bufferToString(message.messageId);
319
+ const pending = this.#pendingMessages.get(msgIdStr);
320
+ if (pending) {
321
+ this.#pendingMessages.delete(msgIdStr);
322
+ }
323
+ } else {
324
+ this.#sendAck(message.requestId, message.messageId);
325
+ switch (message.messageKind.tag) {
326
+ case "ToServerRequestStart":
327
+ await this.#handleRequestStart(
328
+ message.requestId,
329
+ message.messageKind.val,
330
+ );
331
+ break;
332
+ case "ToServerRequestChunk":
333
+ await this.#handleRequestChunk(
334
+ message.requestId,
335
+ message.messageKind.val,
336
+ );
337
+ break;
338
+ case "ToServerRequestAbort":
339
+ await this.#handleRequestAbort(message.requestId);
340
+ break;
341
+ case "ToServerWebSocketOpen":
342
+ await this.#handleWebSocketOpen(
343
+ message.requestId,
344
+ message.messageKind.val,
345
+ );
346
+ break;
347
+ case "ToServerWebSocketMessage":
348
+ await this.#handleWebSocketMessage(
349
+ message.requestId,
350
+ message.messageKind.val,
351
+ );
352
+ break;
353
+ case "ToServerWebSocketClose":
354
+ await this.#handleWebSocketClose(
355
+ message.requestId,
356
+ message.messageKind.val,
357
+ );
358
+ break;
359
+ case "ToClientResponseStart":
360
+ this.#handleResponseStart(
361
+ message.requestId,
362
+ message.messageKind.val,
363
+ );
364
+ break;
365
+ case "ToClientResponseChunk":
366
+ this.#handleResponseChunk(
367
+ message.requestId,
368
+ message.messageKind.val,
369
+ );
370
+ break;
371
+ case "ToClientResponseAbort":
372
+ this.#handleResponseAbort(message.requestId);
373
+ break;
374
+ case "ToClientWebSocketOpen":
375
+ this.#handleWebSocketOpenResponse(
376
+ message.requestId,
377
+ message.messageKind.val,
378
+ );
379
+ break;
380
+ case "ToClientWebSocketMessage":
381
+ this.#handleWebSocketMessageResponse(
382
+ message.requestId,
383
+ message.messageKind.val,
384
+ );
385
+ break;
386
+ case "ToClientWebSocketClose":
387
+ this.#handleWebSocketCloseResponse(
388
+ message.requestId,
389
+ message.messageKind.val,
390
+ );
391
+ break;
392
+ }
393
+ }
394
+ }
395
+
396
+ async #handleRequestStart(
397
+ requestId: ArrayBuffer,
398
+ req: tunnel.ToServerRequestStart,
399
+ ) {
400
+ // Track this request for the actor
401
+ const requestIdStr = bufferToString(requestId);
402
+ const actor = this.#runner.getActor(req.actorId);
403
+ if (actor) {
404
+ actor.requests.add(requestIdStr);
405
+ }
406
+
407
+ try {
408
+ // Convert headers map to Headers object
409
+ const headers = new Headers();
410
+ for (const [key, value] of req.headers) {
411
+ headers.append(key, value);
412
+ }
413
+
414
+ // Create Request object
415
+ const request = new Request(`http://localhost${req.path}`, {
416
+ method: req.method,
417
+ headers,
418
+ body: req.body ? new Uint8Array(req.body) : undefined,
419
+ });
420
+
421
+ // Handle streaming request
422
+ if (req.stream) {
423
+ // Create a stream for the request body
424
+ const stream = new ReadableStream<Uint8Array>({
425
+ start: (controller) => {
426
+ // Store controller for chunks
427
+ const existing =
428
+ this.#actorPendingRequests.get(requestIdStr);
429
+ if (existing) {
430
+ existing.streamController = controller;
431
+ existing.actorId = req.actorId;
432
+ } else {
433
+ this.#actorPendingRequests.set(requestIdStr, {
434
+ resolve: () => {},
435
+ reject: () => {},
436
+ streamController: controller,
437
+ actorId: req.actorId,
438
+ });
439
+ }
440
+ },
441
+ });
442
+
443
+ // Create request with streaming body
444
+ const streamingRequest = new Request(request, {
445
+ body: stream,
446
+ duplex: "half",
447
+ } as any);
448
+
449
+ // Call fetch handler with validation
450
+ const response = await this.#fetch(
451
+ req.actorId,
452
+ streamingRequest,
453
+ );
454
+ await this.#sendResponse(requestId, response);
455
+ } else {
456
+ // Non-streaming request
457
+ const response = await this.#fetch(req.actorId, request);
458
+ await this.#sendResponse(requestId, response);
459
+ }
460
+ } catch (error) {
461
+ logger()?.error({ msg: "error handling request", error });
462
+ this.#sendResponseError(requestId, 500, "Internal Server Error");
463
+ } finally {
464
+ // Clean up request tracking
465
+ const actor = this.#runner.getActor(req.actorId);
466
+ if (actor) {
467
+ actor.requests.delete(requestIdStr);
468
+ }
469
+ }
470
+ }
471
+
472
+ async #handleRequestChunk(
473
+ requestId: ArrayBuffer,
474
+ chunk: tunnel.ToServerRequestChunk,
475
+ ) {
476
+ const requestIdStr = bufferToString(requestId);
477
+ const pending = this.#actorPendingRequests.get(requestIdStr);
478
+ if (pending?.streamController) {
479
+ pending.streamController.enqueue(new Uint8Array(chunk.body));
480
+ if (chunk.finish) {
481
+ pending.streamController.close();
482
+ this.#actorPendingRequests.delete(requestIdStr);
483
+ }
484
+ }
485
+ }
486
+
487
+ async #handleRequestAbort(requestId: ArrayBuffer) {
488
+ const requestIdStr = bufferToString(requestId);
489
+ const pending = this.#actorPendingRequests.get(requestIdStr);
490
+ if (pending?.streamController) {
491
+ pending.streamController.error(new Error("Request aborted"));
492
+ }
493
+ this.#actorPendingRequests.delete(requestIdStr);
494
+ }
495
+
496
+ async #sendResponse(requestId: ArrayBuffer, response: Response) {
497
+ // Always treat responses as non-streaming for now
498
+ // In the future, we could detect streaming responses based on:
499
+ // - Transfer-Encoding: chunked
500
+ // - Content-Type: text/event-stream
501
+ // - Explicit stream flag from the handler
502
+
503
+ // Read the body first to get the actual content
504
+ const body = response.body ? await response.arrayBuffer() : null;
505
+
506
+ // Convert headers to map and add Content-Length if not present
507
+ const headers = new Map<string, string>();
508
+ response.headers.forEach((value, key) => {
509
+ headers.set(key, value);
510
+ });
511
+
512
+ // Add Content-Length header if we have a body and it's not already set
513
+ if (body && !headers.has("content-length")) {
514
+ headers.set("content-length", String(body.byteLength));
515
+ }
516
+
517
+ // Send as non-streaming response
518
+ this.#sendMessage(requestId, {
519
+ tag: "ToClientResponseStart",
520
+ val: {
521
+ status: response.status as tunnel.u16,
522
+ headers,
523
+ body: body || null,
524
+ stream: false,
525
+ },
526
+ });
527
+ }
528
+
529
+ #sendResponseError(
530
+ requestId: ArrayBuffer,
531
+ status: number,
532
+ message: string,
533
+ ) {
534
+ const headers = new Map<string, string>();
535
+ headers.set("content-type", "text/plain");
536
+
537
+ this.#sendMessage(requestId, {
538
+ tag: "ToClientResponseStart",
539
+ val: {
540
+ status: status as tunnel.u16,
541
+ headers,
542
+ body: new TextEncoder().encode(message).buffer as ArrayBuffer,
543
+ stream: false,
544
+ },
545
+ });
546
+ }
547
+
548
+ async #handleWebSocketOpen(
549
+ requestId: ArrayBuffer,
550
+ open: tunnel.ToServerWebSocketOpen,
551
+ ) {
552
+ const webSocketId = bufferToString(requestId);
553
+ // Validate actor exists
554
+ const actor = this.#runner.getActor(open.actorId);
555
+ if (!actor) {
556
+ logger()?.warn({
557
+ msg: "ignoring websocket for unknown actor",
558
+ actorId: open.actorId,
559
+ });
560
+ // Send close immediately
561
+ this.#sendMessage(requestId, {
562
+ tag: "ToClientWebSocketClose",
563
+ val: {
564
+ code: 1011,
565
+ reason: "Actor not found",
566
+ },
567
+ });
568
+ return;
569
+ }
570
+
571
+ const websocketHandler = this.#runner.config.websocket;
572
+
573
+ if (!websocketHandler) {
574
+ console.error("No websocket handler configured for tunnel");
575
+ logger()?.error({
576
+ msg: "no websocket handler configured for tunnel",
577
+ });
578
+ // Send close immediately
579
+ this.#sendMessage(requestId, {
580
+ tag: "ToClientWebSocketClose",
581
+ val: {
582
+ code: 1011,
583
+ reason: "Not Implemented",
584
+ },
585
+ });
586
+ return;
587
+ }
588
+
589
+ // Track this WebSocket for the actor
590
+ if (actor) {
591
+ actor.webSockets.add(webSocketId);
592
+ }
593
+
594
+ try {
595
+ // Create WebSocket adapter
596
+ const adapter = new WebSocketTunnelAdapter(
597
+ webSocketId,
598
+ (data: ArrayBuffer | string, isBinary: boolean) => {
599
+ // Send message through tunnel
600
+ const dataBuffer =
601
+ typeof data === "string"
602
+ ? (new TextEncoder().encode(data)
603
+ .buffer as ArrayBuffer)
604
+ : data;
605
+
606
+ this.#sendMessage(requestId, {
607
+ tag: "ToClientWebSocketMessage",
608
+ val: {
609
+ data: dataBuffer,
610
+ binary: isBinary,
611
+ },
612
+ });
613
+ },
614
+ (code?: number, reason?: string) => {
615
+ // Send close through tunnel
616
+ this.#sendMessage(requestId, {
617
+ tag: "ToClientWebSocketClose",
618
+ val: {
619
+ code: code || null,
620
+ reason: reason || null,
621
+ },
622
+ });
623
+
624
+ // Remove from map
625
+ this.#actorWebSockets.delete(webSocketId);
626
+
627
+ // Clean up actor tracking
628
+ if (actor) {
629
+ actor.webSockets.delete(webSocketId);
630
+ }
631
+ },
632
+ );
633
+
634
+ // Store adapter
635
+ this.#actorWebSockets.set(webSocketId, adapter);
636
+
637
+ // Send open confirmation
638
+ this.#sendMessage(requestId, {
639
+ tag: "ToClientWebSocketOpen",
640
+ val: null,
641
+ });
642
+
643
+ // Notify adapter that connection is open
644
+ adapter._handleOpen();
645
+
646
+ // Create a minimal request object for the websocket handler
647
+ // Include original headers from the open message
648
+ const headerInit: Record<string, string> = {};
649
+ if (open.headers) {
650
+ for (const [k, v] of open.headers as ReadonlyMap<
651
+ string,
652
+ string
653
+ >) {
654
+ headerInit[k] = v;
655
+ }
656
+ }
657
+ // Ensure websocket upgrade headers are present
658
+ headerInit["Upgrade"] = "websocket";
659
+ headerInit["Connection"] = "Upgrade";
660
+
661
+ const request = new Request(`http://localhost${open.path}`, {
662
+ method: "GET",
663
+ headers: headerInit,
664
+ });
665
+
666
+ // Call websocket handler
667
+ await websocketHandler(open.actorId, adapter, request);
668
+ } catch (error) {
669
+ logger()?.error({ msg: "error handling websocket open", error });
670
+ // Send close on error
671
+ this.#sendMessage(requestId, {
672
+ tag: "ToClientWebSocketClose",
673
+ val: {
674
+ code: 1011,
675
+ reason: "Server Error",
676
+ },
677
+ });
678
+
679
+ this.#actorWebSockets.delete(webSocketId);
680
+
681
+ // Clean up actor tracking
682
+ if (actor) {
683
+ actor.webSockets.delete(webSocketId);
684
+ }
685
+ }
686
+ }
687
+
688
+ async #handleWebSocketMessage(
689
+ requestId: ArrayBuffer,
690
+ msg: tunnel.ToServerWebSocketMessage,
691
+ ) {
692
+ const webSocketId = bufferToString(requestId);
693
+ const adapter = this.#actorWebSockets.get(webSocketId);
694
+ if (adapter) {
695
+ const data = msg.binary
696
+ ? new Uint8Array(msg.data)
697
+ : new TextDecoder().decode(new Uint8Array(msg.data));
698
+
699
+ adapter._handleMessage(data, msg.binary);
700
+ }
701
+ }
702
+
703
+ async #handleWebSocketClose(
704
+ requestId: ArrayBuffer,
705
+ close: tunnel.ToServerWebSocketClose,
706
+ ) {
707
+ const webSocketId = bufferToString(requestId);
708
+ const adapter = this.#actorWebSockets.get(webSocketId);
709
+ if (adapter) {
710
+ adapter._handleClose(
711
+ close.code || undefined,
712
+ close.reason || undefined,
713
+ );
714
+ this.#actorWebSockets.delete(webSocketId);
715
+ }
716
+ }
717
+
718
+ #handleResponseStart(
719
+ requestId: ArrayBuffer,
720
+ resp: tunnel.ToClientResponseStart,
721
+ ) {
722
+ const requestIdStr = bufferToString(requestId);
723
+ const pending = this.#actorPendingRequests.get(requestIdStr);
724
+ if (!pending) {
725
+ logger()?.warn({
726
+ msg: "received response for unknown request",
727
+ requestId: requestIdStr,
728
+ });
729
+ return;
730
+ }
731
+
732
+ // Convert headers map to Headers object
733
+ const headers = new Headers();
734
+ for (const [key, value] of resp.headers) {
735
+ headers.append(key, value);
736
+ }
737
+
738
+ if (resp.stream) {
739
+ // Create streaming response
740
+ const stream = new ReadableStream<Uint8Array>({
741
+ start: (controller) => {
742
+ pending.streamController = controller;
743
+ },
744
+ });
745
+
746
+ const response = new Response(stream, {
747
+ status: resp.status,
748
+ headers,
749
+ });
750
+
751
+ pending.resolve(response);
752
+ } else {
753
+ // Non-streaming response
754
+ const body = resp.body ? new Uint8Array(resp.body) : null;
755
+ const response = new Response(body, {
756
+ status: resp.status,
757
+ headers,
758
+ });
759
+
760
+ pending.resolve(response);
761
+ this.#actorPendingRequests.delete(requestIdStr);
762
+ }
763
+ }
764
+
765
+ #handleResponseChunk(
766
+ requestId: ArrayBuffer,
767
+ chunk: tunnel.ToClientResponseChunk,
768
+ ) {
769
+ const requestIdStr = bufferToString(requestId);
770
+ const pending = this.#actorPendingRequests.get(requestIdStr);
771
+ if (pending?.streamController) {
772
+ pending.streamController.enqueue(new Uint8Array(chunk.body));
773
+ if (chunk.finish) {
774
+ pending.streamController.close();
775
+ this.#actorPendingRequests.delete(requestIdStr);
776
+ }
777
+ }
778
+ }
779
+
780
+ #handleResponseAbort(requestId: ArrayBuffer) {
781
+ const requestIdStr = bufferToString(requestId);
782
+ const pending = this.#actorPendingRequests.get(requestIdStr);
783
+ if (pending?.streamController) {
784
+ pending.streamController.error(new Error("Response aborted"));
785
+ }
786
+ this.#actorPendingRequests.delete(requestIdStr);
787
+ }
788
+
789
+ #handleWebSocketOpenResponse(
790
+ requestId: ArrayBuffer,
791
+ open: tunnel.ToClientWebSocketOpen,
792
+ ) {
793
+ const webSocketId = bufferToString(requestId);
794
+ const adapter = this.#actorWebSockets.get(webSocketId);
795
+ if (adapter) {
796
+ adapter._handleOpen();
797
+ }
798
+ }
799
+
800
+ #handleWebSocketMessageResponse(
801
+ requestId: ArrayBuffer,
802
+ msg: tunnel.ToClientWebSocketMessage,
803
+ ) {
804
+ const webSocketId = bufferToString(requestId);
805
+ const adapter = this.#actorWebSockets.get(webSocketId);
806
+ if (adapter) {
807
+ const data = msg.binary
808
+ ? new Uint8Array(msg.data)
809
+ : new TextDecoder().decode(new Uint8Array(msg.data));
810
+
811
+ adapter._handleMessage(data, msg.binary);
812
+ }
813
+ }
814
+
815
+ #handleWebSocketCloseResponse(
816
+ requestId: ArrayBuffer,
817
+ close: tunnel.ToClientWebSocketClose,
818
+ ) {
819
+ const webSocketId = bufferToString(requestId);
820
+ const adapter = this.#actorWebSockets.get(webSocketId);
821
+ if (adapter) {
822
+ adapter._handleClose(
823
+ close.code || undefined,
824
+ close.reason || undefined,
825
+ );
826
+ this.#actorWebSockets.delete(webSocketId);
827
+ }
828
+ }
829
+ }
830
+
831
+ /** Converts a buffer to a string. Used for storing strings in a lookup map. */
832
+ function bufferToString(buffer: ArrayBuffer): string {
833
+ return Buffer.from(buffer).toString("base64");
834
+ }
835
+
836
+ /** Generates a UUID as bytes. */
837
+ function generateUuidBuffer(): ArrayBuffer {
838
+ const buffer = new Uint8Array(16);
839
+ uuidv4(undefined, buffer);
840
+ return buffer.buffer;
841
+ }