@rivetkit/engine-runner 2.0.22-rc.1 → 2.0.22

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/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 = 1;
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
- this.#sendActorStateUpdate(actorId, actor.generation, "stopped");
182
+ // Close requests after onActorStop so you can send messages over the tunnel
183
+ this.#tunnel?.closeActiveRequests(actor);
169
184
 
170
- this.#config.onActorStop(actorId, actor.generation).catch((err) => {
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 endpoint = this.#config.pegboardEndpoint || this.#config.endpoint;
463
- const wsEndpoint = endpoint
456
+ const wsEndpoint = this.pegboardEndpoint
464
457
  .replace("http://", "ws://")
465
458
  .replace("https://", "wss://");
466
- return `${wsEndpoint}?protocol_version=${PROTOCOL_VERSION}&namespace=${encodeURIComponent(this.#config.namespace)}&runner_key=${encodeURIComponent(this.#config.runnerKey)}`;
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", `rivet_target.runner`];
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: "Connected" });
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
- }, pingInterval);
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.close();
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 = bufferToString(requestId);
80
- this.#pendingTunnelMessages.set(bufferToString(messageId), {
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.close(1000, "Message acknowledgment timeout");
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
- unregisterActor(actor: ActorInstance) {
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
- // Close all WebSockets for this actor
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.close(1000, "Actor stopped");
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(actorId: string, request: Request): Promise<Response> {
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
- return new Response("Actor not found", { status: 404 });
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 msgIdStr = bufferToString(message.messageId);
230
- const pending = this.#pendingTunnelMessages.get(msgIdStr);
269
+ const pending = this.#pendingTunnelMessages.get(messageIdStr);
231
270
  if (pending) {
232
- this.#pendingTunnelMessages.delete(msgIdStr);
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
- await this.#handleWebSocketMessage(
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 = bufferToString(requestId);
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(req.actorId, request);
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 = bufferToString(requestId);
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 = bufferToString(requestId);
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: ArrayBuffer,
493
+ requestId: protocol.RequestId,
430
494
  open: protocol.ToClientWebSocketOpen,
431
495
  ) {
432
- const webSocketId = bufferToString(requestId);
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
- // Send close immediately
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
- // Send open confirmation
517
- this.#sendMessage(requestId, {
518
- tag: "ToServerWebSocketOpen",
519
- val: null,
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.ToServerWebSocketMessage,
575
- ) {
576
- const webSocketId = bufferToString(requestId);
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(data, msg.binary);
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.ToServerWebSocketClose,
700
+ close: protocol.ToClientWebSocketClose,
590
701
  ) {
591
- const webSocketId = bufferToString(requestId);
592
- const adapter = this.#actorWebSockets.get(webSocketId);
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(webSocketId);
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
+ }