rivetkit 2.0.24-rc.1 → 2.0.25-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.
Files changed (231) hide show
  1. package/dist/schemas/actor-persist/v1.ts +6 -0
  2. package/dist/schemas/actor-persist/v2.ts +9 -3
  3. package/dist/schemas/actor-persist/v3.ts +280 -0
  4. package/dist/schemas/client-protocol/v1.ts +6 -0
  5. package/dist/schemas/client-protocol/v2.ts +438 -0
  6. package/dist/schemas/file-system-driver/v1.ts +6 -0
  7. package/dist/schemas/file-system-driver/v2.ts +142 -0
  8. package/dist/tsup/actor/errors.cjs +2 -4
  9. package/dist/tsup/actor/errors.cjs.map +1 -1
  10. package/dist/tsup/actor/errors.d.cts +7 -10
  11. package/dist/tsup/actor/errors.d.ts +7 -10
  12. package/dist/tsup/actor/errors.js +9 -11
  13. package/dist/tsup/{actor-router-consts-B3Lu87yJ.d.cts → actor-router-consts-DzI2szci.d.cts} +5 -9
  14. package/dist/tsup/{actor-router-consts-B3Lu87yJ.d.ts → actor-router-consts-DzI2szci.d.ts} +5 -9
  15. package/dist/tsup/{chunk-ZTH3KYFH.cjs → chunk-3FG5OJ3G.cjs} +3 -3
  16. package/dist/tsup/{chunk-ZTH3KYFH.cjs.map → chunk-3FG5OJ3G.cjs.map} +1 -1
  17. package/dist/tsup/{chunk-BLK27ES3.js → chunk-6JN6W6G3.js} +44 -56
  18. package/dist/tsup/chunk-6JN6W6G3.js.map +1 -0
  19. package/dist/tsup/chunk-7IBNNGQ2.js +514 -0
  20. package/dist/tsup/chunk-7IBNNGQ2.js.map +1 -0
  21. package/dist/tsup/{chunk-36JJ4IQB.cjs → chunk-AZATXPR4.cjs} +4 -8
  22. package/dist/tsup/chunk-AZATXPR4.cjs.map +1 -0
  23. package/dist/tsup/chunk-B7MENRD5.cjs +5694 -0
  24. package/dist/tsup/chunk-B7MENRD5.cjs.map +1 -0
  25. package/dist/tsup/{chunk-BOMZS2TJ.js → chunk-BBVFDEYD.js} +9 -9
  26. package/dist/tsup/chunk-BBVFDEYD.js.map +1 -0
  27. package/dist/tsup/{chunk-KSRXX3Z4.cjs → chunk-D6762AOA.cjs} +20 -25
  28. package/dist/tsup/chunk-D6762AOA.cjs.map +1 -0
  29. package/dist/tsup/{chunk-2JYPS5YM.cjs → chunk-E63WZNMR.cjs} +6 -6
  30. package/dist/tsup/chunk-E63WZNMR.cjs.map +1 -0
  31. package/dist/tsup/{chunk-YBG6R7LX.js → chunk-EDGN4OC7.js} +3 -7
  32. package/dist/tsup/chunk-EDGN4OC7.js.map +1 -0
  33. package/dist/tsup/{chunk-BYMKMOBS.js → chunk-FLOQ3UWM.js} +1844 -1681
  34. package/dist/tsup/chunk-FLOQ3UWM.js.map +1 -0
  35. package/dist/tsup/{chunk-7L65NNWP.cjs → chunk-H7GV5DIW.cjs} +187 -185
  36. package/dist/tsup/chunk-H7GV5DIW.cjs.map +1 -0
  37. package/dist/tsup/{chunk-227FEWMB.js → chunk-HZYZ7JSF.js} +3322 -2251
  38. package/dist/tsup/chunk-HZYZ7JSF.js.map +1 -0
  39. package/dist/tsup/{chunk-FX7TWFQR.js → chunk-IDJK7ILQ.js} +2 -6
  40. package/dist/tsup/chunk-IDJK7ILQ.js.map +1 -0
  41. package/dist/tsup/{chunk-VHGY7PU5.cjs → chunk-ILFXA4AL.cjs} +1900 -1737
  42. package/dist/tsup/chunk-ILFXA4AL.cjs.map +1 -0
  43. package/dist/tsup/chunk-MV6M3FDL.cjs +514 -0
  44. package/dist/tsup/chunk-MV6M3FDL.cjs.map +1 -0
  45. package/dist/tsup/{chunk-PLUN2NQT.js → chunk-NWBKMCWC.js} +189 -187
  46. package/dist/tsup/chunk-NWBKMCWC.js.map +1 -0
  47. package/dist/tsup/{chunk-CD33GT6Z.js → chunk-QIHBDXTO.js} +2 -2
  48. package/dist/tsup/{chunk-G64QUEDJ.js → chunk-W6RDS6NW.js} +23 -28
  49. package/dist/tsup/chunk-W6RDS6NW.js.map +1 -0
  50. package/dist/tsup/{chunk-INNFK746.cjs → chunk-WQU4M4ZC.cjs} +10 -14
  51. package/dist/tsup/chunk-WQU4M4ZC.cjs.map +1 -0
  52. package/dist/tsup/{chunk-SHVX2QUR.cjs → chunk-XKZA47XS.cjs} +17 -17
  53. package/dist/tsup/chunk-XKZA47XS.cjs.map +1 -0
  54. package/dist/tsup/{chunk-HHFKKVLR.cjs → chunk-YHWIOWVA.cjs} +45 -57
  55. package/dist/tsup/chunk-YHWIOWVA.cjs.map +1 -0
  56. package/dist/tsup/{chunk-YBHYXIP6.js → chunk-YVL6IRUM.js} +3 -3
  57. package/dist/tsup/chunk-YVL6IRUM.js.map +1 -0
  58. package/dist/tsup/client/mod.cjs +9 -9
  59. package/dist/tsup/client/mod.d.cts +5 -7
  60. package/dist/tsup/client/mod.d.ts +5 -7
  61. package/dist/tsup/client/mod.js +8 -8
  62. package/dist/tsup/common/log.cjs +3 -3
  63. package/dist/tsup/common/log.js +2 -2
  64. package/dist/tsup/common/websocket.cjs +4 -4
  65. package/dist/tsup/common/websocket.js +3 -3
  66. package/dist/tsup/{conn-B3Vhbgnd.d.ts → config-BRDYDraU.d.cts} +1119 -1047
  67. package/dist/tsup/{conn-DJWL3nGx.d.cts → config-Bo-blHpJ.d.ts} +1119 -1047
  68. package/dist/tsup/driver-helpers/mod.cjs +5 -13
  69. package/dist/tsup/driver-helpers/mod.cjs.map +1 -1
  70. package/dist/tsup/driver-helpers/mod.d.cts +11 -9
  71. package/dist/tsup/driver-helpers/mod.d.ts +11 -9
  72. package/dist/tsup/driver-helpers/mod.js +14 -22
  73. package/dist/tsup/driver-test-suite/mod.cjs +474 -303
  74. package/dist/tsup/driver-test-suite/mod.cjs.map +1 -1
  75. package/dist/tsup/driver-test-suite/mod.d.cts +6 -9
  76. package/dist/tsup/driver-test-suite/mod.d.ts +6 -9
  77. package/dist/tsup/driver-test-suite/mod.js +1085 -914
  78. package/dist/tsup/driver-test-suite/mod.js.map +1 -1
  79. package/dist/tsup/inspector/mod.cjs +6 -6
  80. package/dist/tsup/inspector/mod.d.cts +5 -7
  81. package/dist/tsup/inspector/mod.d.ts +5 -7
  82. package/dist/tsup/inspector/mod.js +5 -5
  83. package/dist/tsup/mod.cjs +10 -16
  84. package/dist/tsup/mod.cjs.map +1 -1
  85. package/dist/tsup/mod.d.cts +23 -25
  86. package/dist/tsup/mod.d.ts +23 -25
  87. package/dist/tsup/mod.js +17 -23
  88. package/dist/tsup/test/mod.cjs +11 -11
  89. package/dist/tsup/test/mod.d.cts +4 -6
  90. package/dist/tsup/test/mod.d.ts +4 -6
  91. package/dist/tsup/test/mod.js +10 -10
  92. package/dist/tsup/utils.cjs +3 -5
  93. package/dist/tsup/utils.cjs.map +1 -1
  94. package/dist/tsup/utils.d.cts +1 -2
  95. package/dist/tsup/utils.d.ts +1 -2
  96. package/dist/tsup/utils.js +2 -4
  97. package/package.json +13 -6
  98. package/src/actor/config.ts +56 -44
  99. package/src/actor/conn/driver.ts +61 -0
  100. package/src/actor/conn/drivers/http.ts +17 -0
  101. package/src/actor/conn/drivers/raw-request.ts +24 -0
  102. package/src/actor/conn/drivers/raw-websocket.ts +65 -0
  103. package/src/actor/conn/drivers/websocket.ts +129 -0
  104. package/src/actor/conn/mod.ts +232 -0
  105. package/src/actor/conn/persisted.ts +81 -0
  106. package/src/actor/conn/state-manager.ts +196 -0
  107. package/src/actor/contexts/action.ts +23 -0
  108. package/src/actor/{context.ts → contexts/actor.ts} +19 -8
  109. package/src/actor/contexts/conn-init.ts +31 -0
  110. package/src/actor/contexts/conn.ts +48 -0
  111. package/src/actor/contexts/create-conn-state.ts +13 -0
  112. package/src/actor/contexts/on-before-connect.ts +13 -0
  113. package/src/actor/contexts/on-connect.ts +22 -0
  114. package/src/actor/contexts/request.ts +48 -0
  115. package/src/actor/contexts/websocket.ts +48 -0
  116. package/src/actor/definition.ts +3 -3
  117. package/src/actor/driver.ts +36 -5
  118. package/src/actor/errors.ts +19 -24
  119. package/src/actor/instance/connection-manager.ts +465 -0
  120. package/src/actor/instance/event-manager.ts +292 -0
  121. package/src/actor/instance/kv.ts +15 -0
  122. package/src/actor/instance/mod.ts +1107 -0
  123. package/src/actor/instance/persisted.ts +67 -0
  124. package/src/actor/instance/schedule-manager.ts +349 -0
  125. package/src/actor/instance/state-manager.ts +502 -0
  126. package/src/actor/mod.ts +13 -16
  127. package/src/actor/protocol/old.ts +131 -43
  128. package/src/actor/protocol/serde.ts +19 -4
  129. package/src/actor/router-endpoints.ts +61 -586
  130. package/src/actor/router-websocket-endpoints.ts +408 -0
  131. package/src/actor/router.ts +63 -197
  132. package/src/actor/schedule.ts +1 -1
  133. package/src/client/actor-conn.ts +183 -249
  134. package/src/client/actor-handle.ts +29 -6
  135. package/src/client/client.ts +0 -4
  136. package/src/client/config.ts +1 -4
  137. package/src/client/mod.ts +0 -1
  138. package/src/client/raw-utils.ts +3 -3
  139. package/src/client/utils.ts +85 -39
  140. package/src/common/actor-router-consts.ts +5 -12
  141. package/src/common/{inline-websocket-adapter2.ts → inline-websocket-adapter.ts} +26 -48
  142. package/src/common/log.ts +1 -1
  143. package/src/common/router.ts +28 -17
  144. package/src/common/utils.ts +2 -0
  145. package/src/driver-helpers/mod.ts +7 -10
  146. package/src/driver-helpers/utils.ts +18 -9
  147. package/src/driver-test-suite/mod.ts +26 -50
  148. package/src/driver-test-suite/test-inline-client-driver.ts +27 -51
  149. package/src/driver-test-suite/tests/actor-conn-hibernation.ts +150 -0
  150. package/src/driver-test-suite/tests/actor-conn-state.ts +1 -4
  151. package/src/driver-test-suite/tests/actor-conn.ts +5 -9
  152. package/src/driver-test-suite/tests/actor-destroy.ts +294 -0
  153. package/src/driver-test-suite/tests/actor-driver.ts +0 -7
  154. package/src/driver-test-suite/tests/actor-handle.ts +12 -12
  155. package/src/driver-test-suite/tests/actor-metadata.ts +1 -1
  156. package/src/driver-test-suite/tests/manager-driver.ts +1 -1
  157. package/src/driver-test-suite/tests/raw-http-direct-registry.ts +8 -8
  158. package/src/driver-test-suite/tests/raw-http-request-properties.ts +6 -5
  159. package/src/driver-test-suite/tests/raw-http.ts +5 -5
  160. package/src/driver-test-suite/tests/raw-websocket-direct-registry.ts +7 -7
  161. package/src/driver-test-suite/tests/request-access.ts +4 -4
  162. package/src/driver-test-suite/utils.ts +6 -10
  163. package/src/drivers/engine/actor-driver.ts +614 -424
  164. package/src/drivers/engine/mod.ts +0 -1
  165. package/src/drivers/file-system/actor.ts +24 -12
  166. package/src/drivers/file-system/global-state.ts +427 -37
  167. package/src/drivers/file-system/manager.ts +71 -83
  168. package/src/drivers/file-system/mod.ts +3 -0
  169. package/src/drivers/file-system/utils.ts +18 -8
  170. package/src/engine-process/mod.ts +38 -38
  171. package/src/inspector/utils.ts +7 -5
  172. package/src/manager/driver.ts +11 -4
  173. package/src/manager/gateway.ts +4 -29
  174. package/src/manager/protocol/mod.ts +0 -2
  175. package/src/manager/protocol/query.ts +0 -4
  176. package/src/manager/router.ts +67 -64
  177. package/src/manager-api/actors.ts +13 -0
  178. package/src/mod.ts +1 -3
  179. package/src/registry/mod.ts +20 -20
  180. package/src/registry/serve.ts +9 -14
  181. package/src/remote-manager-driver/actor-websocket-client.ts +1 -16
  182. package/src/remote-manager-driver/api-endpoints.ts +13 -1
  183. package/src/remote-manager-driver/api-utils.ts +8 -0
  184. package/src/remote-manager-driver/metadata.ts +58 -0
  185. package/src/remote-manager-driver/mod.ts +47 -62
  186. package/src/remote-manager-driver/ws-proxy.ts +1 -1
  187. package/src/schemas/actor-persist/mod.ts +1 -1
  188. package/src/schemas/actor-persist/versioned.ts +56 -31
  189. package/src/schemas/client-protocol/mod.ts +1 -1
  190. package/src/schemas/client-protocol/versioned.ts +41 -21
  191. package/src/schemas/client-protocol-zod/mod.ts +103 -0
  192. package/src/schemas/file-system-driver/mod.ts +1 -1
  193. package/src/schemas/file-system-driver/versioned.ts +42 -19
  194. package/src/serde.ts +33 -11
  195. package/src/test/mod.ts +7 -3
  196. package/src/utils/node.ts +173 -0
  197. package/src/utils.ts +0 -4
  198. package/dist/tsup/chunk-227FEWMB.js.map +0 -1
  199. package/dist/tsup/chunk-2JYPS5YM.cjs.map +0 -1
  200. package/dist/tsup/chunk-36JJ4IQB.cjs.map +0 -1
  201. package/dist/tsup/chunk-7L65NNWP.cjs.map +0 -1
  202. package/dist/tsup/chunk-BLK27ES3.js.map +0 -1
  203. package/dist/tsup/chunk-BOMZS2TJ.js.map +0 -1
  204. package/dist/tsup/chunk-BYMKMOBS.js.map +0 -1
  205. package/dist/tsup/chunk-FX7TWFQR.js.map +0 -1
  206. package/dist/tsup/chunk-G64QUEDJ.js.map +0 -1
  207. package/dist/tsup/chunk-HHFKKVLR.cjs.map +0 -1
  208. package/dist/tsup/chunk-INNFK746.cjs.map +0 -1
  209. package/dist/tsup/chunk-KSRXX3Z4.cjs.map +0 -1
  210. package/dist/tsup/chunk-O44LFKSB.cjs +0 -4623
  211. package/dist/tsup/chunk-O44LFKSB.cjs.map +0 -1
  212. package/dist/tsup/chunk-PLUN2NQT.js.map +0 -1
  213. package/dist/tsup/chunk-S4UJG7ZE.js +0 -1119
  214. package/dist/tsup/chunk-S4UJG7ZE.js.map +0 -1
  215. package/dist/tsup/chunk-SHVX2QUR.cjs.map +0 -1
  216. package/dist/tsup/chunk-VFB23BYZ.cjs +0 -1119
  217. package/dist/tsup/chunk-VFB23BYZ.cjs.map +0 -1
  218. package/dist/tsup/chunk-VHGY7PU5.cjs.map +0 -1
  219. package/dist/tsup/chunk-YBG6R7LX.js.map +0 -1
  220. package/dist/tsup/chunk-YBHYXIP6.js.map +0 -1
  221. package/src/actor/action.ts +0 -178
  222. package/src/actor/conn-drivers.ts +0 -216
  223. package/src/actor/conn-socket.ts +0 -8
  224. package/src/actor/conn.ts +0 -272
  225. package/src/actor/instance.ts +0 -2336
  226. package/src/actor/persisted.ts +0 -49
  227. package/src/actor/unstable-react.ts +0 -110
  228. package/src/driver-test-suite/tests/actor-reconnect.ts +0 -170
  229. package/src/drivers/engine/kv.ts +0 -3
  230. package/src/manager/hono-websocket-adapter.ts +0 -393
  231. /package/dist/tsup/{chunk-CD33GT6Z.js.map → chunk-QIHBDXTO.js.map} +0 -0
@@ -1,65 +1,74 @@
1
1
  import type {
2
2
  ActorConfig as EngineActorConfig,
3
3
  RunnerConfig as EngineRunnerConfig,
4
- HibernationConfig,
4
+ HibernatingWebSocketMetadata,
5
5
  } from "@rivetkit/engine-runner";
6
- import { Runner } from "@rivetkit/engine-runner";
6
+ import { idToStr, Runner } from "@rivetkit/engine-runner";
7
7
  import * as cbor from "cbor-x";
8
8
  import type { Context as HonoContext } from "hono";
9
9
  import { streamSSE } from "hono/streaming";
10
- import { WSContext } from "hono/ws";
10
+ import { WSContext, type WSContextInit } from "hono/ws";
11
11
  import invariant from "invariant";
12
+ import { type AnyConn, CONN_STATE_MANAGER_SYMBOL } from "@/actor/conn/mod";
12
13
  import { lookupInRegistry } from "@/actor/definition";
13
- import { PERSIST_SYMBOL } from "@/actor/instance";
14
+ import { KEYS } from "@/actor/instance/kv";
14
15
  import { deserializeActorKey } from "@/actor/keys";
15
- import { EncodingSchema } from "@/actor/protocol/serde";
16
+ import { getValueLength } from "@/actor/protocol/old";
16
17
  import { type ActorRouter, createActorRouter } from "@/actor/router";
17
18
  import {
18
- handleRawWebSocketHandler,
19
- handleWebSocketConnect,
19
+ parseWebSocketProtocols,
20
+ routeWebSocket,
20
21
  truncateRawWebSocketPathPrefix,
21
- } from "@/actor/router-endpoints";
22
+ type UpgradeWebSocketArgs,
23
+ } from "@/actor/router-websocket-endpoints";
22
24
  import type { Client } from "@/client/client";
23
25
  import {
24
- PATH_CONNECT_WEBSOCKET,
25
- PATH_RAW_WEBSOCKET_PREFIX,
26
- WS_PROTOCOL_CONN_PARAMS,
27
- WS_PROTOCOL_ENCODING,
28
- WS_PROTOCOL_TOKEN,
26
+ PATH_CONNECT,
27
+ PATH_INSPECTOR_CONNECT,
28
+ PATH_WEBSOCKET_BASE,
29
+ PATH_WEBSOCKET_PREFIX,
29
30
  } from "@/common/actor-router-consts";
30
- import type { UpgradeWebSocketArgs } from "@/common/inline-websocket-adapter2";
31
31
  import { getLogger } from "@/common/log";
32
32
  import type {
33
- RivetEvent,
34
33
  RivetMessageEvent,
35
34
  UniversalWebSocket,
36
35
  } from "@/common/websocket-interface";
37
36
  import {
38
37
  type ActorDriver,
39
38
  type AnyActorInstance,
39
+ getInitialActorKvState,
40
40
  type ManagerDriver,
41
- serializeEmptyPersistData,
42
41
  } from "@/driver-helpers/mod";
43
42
  import { buildActorNames, type RegistryConfig } from "@/registry/config";
44
43
  import type { RunnerConfig } from "@/registry/run-config";
45
44
  import { getEndpoint } from "@/remote-manager-driver/api-utils";
46
45
  import {
47
- arrayBuffersEqual,
48
- idToStr,
49
46
  type LongTimeoutHandle,
50
47
  promiseWithResolvers,
51
48
  setLongTimeout,
52
49
  stringifyError,
53
50
  } from "@/utils";
54
- import { KEYS } from "./kv";
55
51
  import { logger } from "./log";
56
52
 
57
53
  const RUNNER_SSE_PING_INTERVAL = 1000;
58
54
 
55
+ // Message ack deadline is 30s on the gateway, but we will ack more frequently
56
+ // in order to minimize the message buffer size on the gateway and to give
57
+ // generous breathing room for the timeout.
58
+ //
59
+ // See engine/packages/pegboard-gateway/src/shared_state.rs
60
+ // (HWS_MESSAGE_ACK_TIMEOUT)
61
+ const CONN_MESSAGE_ACK_DEADLINE = 5_000;
62
+
63
+ // Force saveState when cumulative message size reaches this threshold (0.5 MB)
64
+ //
65
+ // See engine/packages/pegboard-gateway/src/shared_state.rs
66
+ // (HWS_MAX_PENDING_MSGS_SIZE_PER_REQ)
67
+ const CONN_BUFFERED_MESSAGE_SIZE_THRESHOLD = 500_000;
68
+
59
69
  interface ActorHandler {
60
70
  actor?: AnyActorInstance;
61
71
  actorStartPromise?: ReturnType<typeof promiseWithResolvers<void>>;
62
- persistedData?: Uint8Array;
63
72
  }
64
73
 
65
74
  export type DriverContext = {};
@@ -79,12 +88,32 @@ export class EngineActorDriver implements ActorDriver {
79
88
  #runnerStopped: PromiseWithResolvers<undefined> = promiseWithResolvers();
80
89
  #isRunnerStopped: boolean = false;
81
90
 
82
- // WebSocket message acknowledgment debouncing
83
- #wsAckQueue: Map<
91
+ // HACK: Track actor stop intent locally since the runner protocol doesn't
92
+ // pass the stop reason to onActorStop. This will be fixed when the runner
93
+ // protocol is updated to send the intent directly (see RVT-5284)
94
+ #actorStopIntent: Map<string, "sleep" | "destroy"> = new Map();
95
+
96
+ // Map of conn IDs to message index waiting to be persisted before sending
97
+ // an ack
98
+ //
99
+ // serverMessageIndex is updated and pendingAck is flagged in needed in
100
+ // onBeforePersistConnect, then the HWS ack message is sent in
101
+ // onAfterPersistConn. This allows us to track what's about to be written
102
+ // to storage to prevent race conditions with the serverMessageIndex being
103
+ // updated while writing the existing state.
104
+ //
105
+ // bufferedMessageSize tracks the total bytes received since last persist
106
+ // to force a saveState when threshold is reached. This is the amount of
107
+ // data currently buffered on the gateway.
108
+ #hwsMessageIndex = new Map<
84
109
  string,
85
- { requestIdBuf: ArrayBuffer; messageIndex: number }
86
- > = new Map();
87
- #wsAckFlushInterval?: NodeJS.Timeout;
110
+ {
111
+ serverMessageIndex: number;
112
+ bufferedMessageSize: number;
113
+ pendingAckFromMessageIndex: boolean;
114
+ pendingAckFromBufferSize: boolean;
115
+ }
116
+ >();
88
117
 
89
118
  constructor(
90
119
  registryConfig: RegistryConfig,
@@ -133,159 +162,12 @@ export class EngineActorDriver implements ActorDriver {
133
162
  },
134
163
  fetch: this.#runnerFetch.bind(this),
135
164
  websocket: this.#runnerWebSocket.bind(this),
165
+ hibernatableWebSocket: {
166
+ canHibernate: this.#hwsCanHibernate.bind(this),
167
+ },
136
168
  onActorStart: this.#runnerOnActorStart.bind(this),
137
169
  onActorStop: this.#runnerOnActorStop.bind(this),
138
170
  logger: getLogger("engine-runner"),
139
- getActorHibernationConfig: (
140
- actorId: string,
141
- requestId: ArrayBuffer,
142
- request: Request,
143
- ): HibernationConfig => {
144
- const url = new URL(request.url);
145
- const path = url.pathname;
146
-
147
- // Get actor instance from runner to access actor name
148
- const actorInstance = this.#runner.getActor(actorId);
149
- if (!actorInstance) {
150
- logger().warn({
151
- msg: "actor not found in getActorHibernationConfig",
152
- actorId,
153
- });
154
- return { enabled: false, lastMsgIndex: undefined };
155
- }
156
-
157
- // Load actor handler to access persisted data
158
- const handler = this.#actors.get(actorId);
159
- if (!handler) {
160
- logger().warn({
161
- msg: "actor handler not found in getActorHibernationConfig",
162
- actorId,
163
- });
164
- return { enabled: false, lastMsgIndex: undefined };
165
- }
166
- if (!handler.actor) {
167
- logger().warn({
168
- msg: "actor not found in getActorHibernationConfig",
169
- actorId,
170
- });
171
- return { enabled: false, lastMsgIndex: undefined };
172
- }
173
-
174
- // Check for existing WS
175
- const hibernatableArray =
176
- handler.actor[PERSIST_SYMBOL].hibernatableWebSocket;
177
- logger().debug({
178
- msg: "checking hibernatable websockets",
179
- requestId: idToStr(requestId),
180
- existingHibernatableWebSockets: hibernatableArray.length,
181
- });
182
- const existingWs = hibernatableArray.find((ws) =>
183
- arrayBuffersEqual(ws.requestId, requestId),
184
- );
185
-
186
- // Determine configuration for new WS
187
- let hibernationConfig: HibernationConfig;
188
- if (existingWs) {
189
- logger().debug({
190
- msg: "found existing hibernatable websocket",
191
- requestId: idToStr(requestId),
192
- lastMsgIndex: existingWs.msgIndex,
193
- });
194
- hibernationConfig = {
195
- enabled: true,
196
- lastMsgIndex: Number(existingWs.msgIndex),
197
- };
198
- } else {
199
- logger().debug({
200
- msg: "no existing hibernatable websocket found",
201
- requestId: idToStr(requestId),
202
- });
203
- if (path === PATH_CONNECT_WEBSOCKET) {
204
- hibernationConfig = {
205
- enabled: true,
206
- lastMsgIndex: undefined,
207
- };
208
- } else if (path.startsWith(PATH_RAW_WEBSOCKET_PREFIX)) {
209
- // Find actor config
210
- const definition = lookupInRegistry(
211
- this.#registryConfig,
212
- actorInstance.config.name,
213
- );
214
-
215
- // Check if can hibernate
216
- const canHibernatWebSocket =
217
- definition.config.options?.canHibernatWebSocket;
218
- if (canHibernatWebSocket === true) {
219
- hibernationConfig = {
220
- enabled: true,
221
- lastMsgIndex: undefined,
222
- };
223
- } else if (typeof canHibernatWebSocket === "function") {
224
- try {
225
- // Truncate the path to match the behavior on onRawWebSocket
226
- const newPath = truncateRawWebSocketPathPrefix(
227
- url.pathname,
228
- );
229
- const truncatedRequest = new Request(
230
- `http://actor${newPath}`,
231
- request,
232
- );
233
-
234
- const canHibernate =
235
- canHibernatWebSocket(truncatedRequest);
236
- hibernationConfig = {
237
- enabled: canHibernate,
238
- lastMsgIndex: undefined,
239
- };
240
- } catch (error) {
241
- logger().error({
242
- msg: "error calling canHibernatWebSocket",
243
- error,
244
- });
245
- hibernationConfig = {
246
- enabled: false,
247
- lastMsgIndex: undefined,
248
- };
249
- }
250
- } else {
251
- hibernationConfig = {
252
- enabled: false,
253
- lastMsgIndex: undefined,
254
- };
255
- }
256
- } else {
257
- logger().warn({
258
- msg: "unexpected path for getActorHibernationConfig",
259
- path,
260
- });
261
- hibernationConfig = {
262
- enabled: false,
263
- lastMsgIndex: undefined,
264
- };
265
- }
266
- }
267
-
268
- // Save or update hibernatable WebSocket
269
- if (existingWs) {
270
- logger().debug({
271
- msg: "updated existing hibernatable websocket timestamp",
272
- requestId: idToStr(requestId),
273
- });
274
- existingWs.lastSeenTimestamp = BigInt(Date.now());
275
- } else {
276
- logger().debug({
277
- msg: "created new hibernatable websocket entry",
278
- requestId: idToStr(requestId),
279
- });
280
- handler.actor[PERSIST_SYMBOL].hibernatableWebSocket.push({
281
- requestId,
282
- lastSeenTimestamp: BigInt(Date.now()),
283
- msgIndex: -1n,
284
- });
285
- }
286
-
287
- return hibernationConfig;
288
- },
289
171
  };
290
172
 
291
173
  // Create and start runner
@@ -297,15 +179,10 @@ export class EngineActorDriver implements ActorDriver {
297
179
  namespace: runConfig.namespace,
298
180
  runnerName: runConfig.runnerName,
299
181
  });
182
+ }
300
183
 
301
- // Start WebSocket ack flush interval
302
- //
303
- // Decreasing this reduces the amount of buffered messages on the
304
- // gateway
305
- //
306
- // Gateway timeout configured to 30s
307
- // https://github.com/rivet-dev/rivet/blob/222dae87e3efccaffa2b503de40ecf8afd4e31eb/engine/packages/pegboard-gateway/src/shared_state.rs#L17
308
- this.#wsAckFlushInterval = setInterval(() => this.#flushWsAcks(), 1000);
184
+ getExtraActorLogParams(): Record<string, string> {
185
+ return { runnerId: this.#runner.runnerId ?? "-" };
309
186
  }
310
187
 
311
188
  async #loadActorHandler(actorId: string): Promise<ActorHandler> {
@@ -318,52 +195,10 @@ export class EngineActorDriver implements ActorDriver {
318
195
  return handler;
319
196
  }
320
197
 
321
- async loadActor(actorId: string): Promise<AnyActorInstance> {
322
- const handler = await this.#loadActorHandler(actorId);
323
- if (!handler.actor) throw new Error(`Actor ${actorId} failed to load`);
324
- return handler.actor;
325
- }
326
-
327
- #flushWsAcks(): void {
328
- if (this.#wsAckQueue.size === 0) return;
329
-
330
- for (const {
331
- requestIdBuf: requestId,
332
- messageIndex: index,
333
- } of this.#wsAckQueue.values()) {
334
- this.#runner.sendWebsocketMessageAck(requestId, index);
335
- }
336
-
337
- this.#wsAckQueue.clear();
338
- }
339
-
340
198
  getContext(actorId: string): DriverContext {
341
199
  return {};
342
200
  }
343
201
 
344
- async readPersistedData(actorId: string): Promise<Uint8Array | undefined> {
345
- const handler = this.#actors.get(actorId);
346
- if (!handler) throw new Error(`Actor ${actorId} not loaded`);
347
-
348
- // This was loaded during actor startup
349
- return handler.persistedData;
350
- }
351
-
352
- async writePersistedData(actorId: string, data: Uint8Array): Promise<void> {
353
- const handler = this.#actors.get(actorId);
354
- if (!handler) throw new Error(`Actor ${actorId} not loaded`);
355
-
356
- handler.persistedData = data;
357
-
358
- logger().debug({
359
- msg: "writing persisted data for actor",
360
- actorId,
361
- dataSize: data.byteLength,
362
- });
363
-
364
- await this.#runner.kvPut(actorId, [[KEYS.PERSIST_DATA, data]]);
365
- }
366
-
367
202
  async setAlarm(actor: AnyActorInstance, timestamp: number): Promise<void> {
368
203
  // Clear prev timeout
369
204
  if (this.#alarmTimeout) {
@@ -374,7 +209,7 @@ export class EngineActorDriver implements ActorDriver {
374
209
  // Set alarm
375
210
  const delay = Math.max(0, timestamp - Date.now());
376
211
  this.#alarmTimeout = setLongTimeout(() => {
377
- actor._onAlarm();
212
+ actor.onAlarm();
378
213
  this.#alarmTimeout = undefined;
379
214
  }, delay);
380
215
 
@@ -385,7 +220,7 @@ export class EngineActorDriver implements ActorDriver {
385
220
  // Instead, it just wakes the actor on the alarm (if not
386
221
  // already awake).
387
222
  //
388
- // _onAlarm is automatically called on `ActorInstance.start` when waking
223
+ // onAlarm is automatically called on `ActorInstance.start` when waking
389
224
  // again.
390
225
  this.#runner.setAlarm(actor.id, timestamp);
391
226
  }
@@ -394,7 +229,172 @@ export class EngineActorDriver implements ActorDriver {
394
229
  return undefined;
395
230
  }
396
231
 
397
- // Runner lifecycle callbacks
232
+ // MARK: - Batch KV operations
233
+ async kvBatchPut(
234
+ actorId: string,
235
+ entries: [Uint8Array, Uint8Array][],
236
+ ): Promise<void> {
237
+ await this.#runner.kvPut(actorId, entries);
238
+ }
239
+
240
+ async kvBatchGet(
241
+ actorId: string,
242
+ keys: Uint8Array[],
243
+ ): Promise<(Uint8Array | null)[]> {
244
+ return await this.#runner.kvGet(actorId, keys);
245
+ }
246
+
247
+ async kvBatchDelete(actorId: string, keys: Uint8Array[]): Promise<void> {
248
+ await this.#runner.kvDelete(actorId, keys);
249
+ }
250
+
251
+ async kvList(actorId: string): Promise<Uint8Array[]> {
252
+ const entries = await this.#runner.kvListPrefix(
253
+ actorId,
254
+ new Uint8Array(),
255
+ );
256
+ const keys = entries.map(([key]) => key);
257
+ logger().info({
258
+ msg: "kvList called",
259
+ actorId,
260
+ keysCount: keys.length,
261
+ keys: keys.map((k) => new TextDecoder().decode(k)),
262
+ });
263
+ return keys;
264
+ }
265
+
266
+ async kvListPrefix(
267
+ actorId: string,
268
+ prefix: Uint8Array,
269
+ ): Promise<[Uint8Array, Uint8Array][]> {
270
+ const result = await this.#runner.kvListPrefix(actorId, prefix);
271
+ logger().info({
272
+ msg: "kvListPrefix called",
273
+ actorId,
274
+ prefixStr: new TextDecoder().decode(prefix),
275
+ entriesCount: result.length,
276
+ keys: result.map(([key]) => new TextDecoder().decode(key)),
277
+ });
278
+ return result;
279
+ }
280
+
281
+ // MARK: - Actor Lifecycle
282
+ async loadActor(actorId: string): Promise<AnyActorInstance> {
283
+ const handler = await this.#loadActorHandler(actorId);
284
+ if (!handler.actor) throw new Error(`Actor ${actorId} failed to load`);
285
+ return handler.actor;
286
+ }
287
+
288
+ startSleep(actorId: string) {
289
+ // HACK: Track intent for onActorStop (see RVT-5284)
290
+ this.#actorStopIntent.set(actorId, "sleep");
291
+ this.#runner.sleepActor(actorId);
292
+ }
293
+
294
+ startDestroy(actorId: string) {
295
+ // HACK: Track intent for onActorStop (see RVT-5284)
296
+ this.#actorStopIntent.set(actorId, "destroy");
297
+ this.#runner.stopActor(actorId);
298
+ }
299
+
300
+ async shutdownRunner(immediate: boolean): Promise<void> {
301
+ logger().info({ msg: "stopping engine actor driver", immediate });
302
+
303
+ // TODO: We need to update the runner to have a draining state so:
304
+ // 1. Send ToServerDraining
305
+ // - This causes Pegboard to stop allocating actors to this runner
306
+ // 2. Pegboard sends ToClientStopActor for all actors on this runner which handles the graceful migration of each actor independently
307
+ // 3. Send ToServerStopping once all actors have successfully stopped
308
+ //
309
+ // What's happening right now is:
310
+ // 1. All actors enter stopped state
311
+ // 2. Actors still respond to requests because only RivetKit knows it's
312
+ // stopping, this causes all requests to issue errors that the actor is
313
+ // stopping. (This will NOT return a 503 bc the runner has no idea the
314
+ // actors are stopping.)
315
+ // 3. Once the last actor stops, then the runner finally stops + actors
316
+ // reschedule
317
+ //
318
+ // This means that:
319
+ // - All actors on this runner are bricked until the slowest onStop finishes
320
+ // - Guard will not gracefully handle requests bc it's not receiving a 503
321
+ // - Actors can still be scheduled to this runner while the other
322
+ // actors are stopping, meaning that those actors will NOT get onStop
323
+ // and will potentiall corrupt their state
324
+ //
325
+ // HACK: Stop all actors to allow state to be saved
326
+ // NOTE: onStop is only supposed to be called by the runner, we're
327
+ // abusing it here
328
+ logger().debug({
329
+ msg: "stopping all actors before shutdown",
330
+ actorCount: this.#actors.size,
331
+ });
332
+ const stopPromises: Promise<void>[] = [];
333
+ for (const [_actorId, handler] of this.#actors.entries()) {
334
+ if (handler.actor) {
335
+ stopPromises.push(
336
+ handler.actor.onStop("sleep").catch((err) => {
337
+ handler.actor?.rLog.error({
338
+ msg: "onStop errored",
339
+ error: stringifyError(err),
340
+ });
341
+ }),
342
+ );
343
+ }
344
+ }
345
+ await Promise.all(stopPromises);
346
+ logger().debug({ msg: "all actors stopped" });
347
+
348
+ await this.#runner.shutdown(immediate);
349
+ }
350
+
351
+ async serverlessHandleStart(c: HonoContext): Promise<Response> {
352
+ return streamSSE(c, async (stream) => {
353
+ // NOTE: onAbort does not work reliably
354
+ stream.onAbort(() => {});
355
+ c.req.raw.signal.addEventListener("abort", () => {
356
+ logger().debug("SSE aborted, shutting down runner");
357
+
358
+ // We cannot assume that the request will always be closed gracefully by Rivet. We always proceed with a graceful shutdown in case the request was terminated for any other reason.
359
+ //
360
+ // If we did not use a graceful shutdown, the runner would
361
+ this.shutdownRunner(false);
362
+ });
363
+
364
+ await this.#runnerStarted.promise;
365
+
366
+ // Runner id should be set if the runner started
367
+ const payload = this.#runner.getServerlessInitPacket();
368
+ invariant(payload, "runnerId not set");
369
+ await stream.writeSSE({ data: payload });
370
+
371
+ // Send ping every second to keep the connection alive
372
+ while (true) {
373
+ if (this.#isRunnerStopped) {
374
+ logger().debug({
375
+ msg: "runner is stopped",
376
+ });
377
+ break;
378
+ }
379
+
380
+ if (stream.closed || stream.aborted) {
381
+ logger().debug({
382
+ msg: "runner sse stream closed",
383
+ closed: stream.closed,
384
+ aborted: stream.aborted,
385
+ });
386
+ break;
387
+ }
388
+
389
+ await stream.writeSSE({ event: "ping", data: "" });
390
+ await stream.sleep(RUNNER_SSE_PING_INTERVAL);
391
+ }
392
+
393
+ // Wait for the runner to stop if the SSE stream aborted early for any reason
394
+ await this.#runnerStopped.promise;
395
+ });
396
+ }
397
+
398
398
  async #runnerOnActorStart(
399
399
  actorId: string,
400
400
  generation: number,
@@ -422,32 +422,33 @@ export class EngineActorDriver implements ActorDriver {
422
422
  // create the same handler simultaneously.
423
423
  handler = {
424
424
  actorStartPromise: promiseWithResolvers(),
425
- persistedData: undefined,
426
425
  };
427
426
  this.#actors.set(actorId, handler);
427
+ }
428
428
 
429
- // Load persisted data from storage
430
- const [persistedValue] = await this.#runner.kvGet(actorId, [
431
- KEYS.PERSIST_DATA,
432
- ]);
433
-
434
- handler.persistedData =
435
- persistedValue !== null
436
- ? persistedValue
437
- : serializeEmptyPersistData(input);
429
+ const name = actorConfig.name as string;
430
+ invariant(actorConfig.key, "actor should have a key");
431
+ const key = deserializeActorKey(actorConfig.key);
438
432
 
433
+ // Initialize storage
434
+ const [persistDataBuffer] = await this.#runner.kvGet(actorId, [
435
+ KEYS.PERSIST_DATA,
436
+ ]);
437
+ if (persistDataBuffer === null) {
438
+ const initialKvState = getInitialActorKvState(input);
439
+ await this.#runner.kvPut(actorId, initialKvState);
440
+ logger().debug({
441
+ msg: "initialized persist data for new actor",
442
+ actorId,
443
+ });
444
+ } else {
439
445
  logger().debug({
440
- msg: "loaded persisted data for actor",
446
+ msg: "found existing persist data for actor",
441
447
  actorId,
442
- dataSize: handler.persistedData?.byteLength,
443
- wasInStorage: persistedValue !== null,
448
+ dataSize: persistDataBuffer.byteLength,
444
449
  });
445
450
  }
446
451
 
447
- const name = actorConfig.name as string;
448
- invariant(actorConfig.key, "actor should have a key");
449
- const key = deserializeActorKey(actorConfig.key);
450
-
451
452
  // Create actor instance
452
453
  const definition = lookupInRegistry(
453
454
  this.#registryConfig,
@@ -465,10 +466,6 @@ export class EngineActorDriver implements ActorDriver {
465
466
  "unknown", // TODO: Add regions
466
467
  );
467
468
 
468
- // Resolve promise if waiting
469
- handler.actorStartPromise?.resolve();
470
- handler.actorStartPromise = undefined;
471
-
472
469
  logger().debug({ msg: "runner actor started", actorId, name, key });
473
470
  }
474
471
 
@@ -478,25 +475,37 @@ export class EngineActorDriver implements ActorDriver {
478
475
  ): Promise<void> {
479
476
  logger().debug({ msg: "runner actor stopping", actorId, generation });
480
477
 
478
+ // HACK: Retrieve the stop intent we tracked locally (see RVT-5284)
479
+ // Default to "sleep" if no intent was recorded (e.g., if the runner
480
+ // initiated the stop)
481
+ //
482
+ // TODO: This will not work if the actor is destroyed from the API
483
+ // correctly. Currently, it will use the sleep intent, but it's
484
+ // actually a destroy intent.
485
+ const reason = this.#actorStopIntent.get(actorId) ?? "sleep";
486
+ this.#actorStopIntent.delete(actorId);
487
+
481
488
  const handler = this.#actors.get(actorId);
482
489
  if (handler?.actor) {
483
490
  try {
484
- await handler.actor._onStop();
491
+ await handler.actor.onStop(reason);
485
492
  } catch (err) {
486
493
  logger().error({
487
- msg: "error in _onStop, proceeding with removing actor",
494
+ msg: "error in onStop, proceeding with removing actor",
488
495
  err: stringifyError(err),
489
496
  });
490
497
  }
491
498
  this.#actors.delete(actorId);
492
499
  }
493
500
 
494
- logger().debug({ msg: "runner actor stopped", actorId });
501
+ logger().debug({ msg: "runner actor stopped", actorId, reason });
495
502
  }
496
503
 
504
+ // MARK: - Runner Networking
497
505
  async #runnerFetch(
498
506
  _runner: Runner,
499
507
  actorId: string,
508
+ _gatewayIdBuf: ArrayBuffer,
500
509
  _requestIdBuf: ArrayBuffer,
501
510
  request: Request,
502
511
  ): Promise<Response> {
@@ -513,243 +522,424 @@ export class EngineActorDriver implements ActorDriver {
513
522
  _runner: Runner,
514
523
  actorId: string,
515
524
  websocketRaw: any,
525
+ gatewayIdBuf: ArrayBuffer,
516
526
  requestIdBuf: ArrayBuffer,
517
527
  request: Request,
528
+ requestPath: string,
529
+ requestHeaders: Record<string, string>,
530
+ isHibernatable: boolean,
531
+ isRestoringHibernatable: boolean,
518
532
  ): Promise<void> {
519
533
  const websocket = websocketRaw as UniversalWebSocket;
520
- const requestId = idToStr(requestIdBuf);
521
534
 
522
- logger().debug({ msg: "runner websocket", actorId, url: request.url });
535
+ // Add a unique ID to track this WebSocket object
536
+ const wsUniqueId = `ws_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
537
+ (websocket as any).__rivet_ws_id = wsUniqueId;
523
538
 
524
- const url = new URL(request.url);
539
+ logger().debug({
540
+ msg: "runner websocket",
541
+ actorId,
542
+ url: request.url,
543
+ isRestoringHibernatable,
544
+ websocketObjectId: websocketRaw
545
+ ? Object.prototype.toString.call(websocketRaw)
546
+ : "null",
547
+ websocketType: websocketRaw?.constructor?.name,
548
+ wsUniqueId,
549
+ websocketProps: websocketRaw
550
+ ? Object.keys(websocketRaw).join(", ")
551
+ : "null",
552
+ });
525
553
 
526
554
  // Parse configuration from Sec-WebSocket-Protocol header (optional for path-based routing)
527
555
  const protocols = request.headers.get("sec-websocket-protocol");
528
-
529
- let encodingRaw: string | undefined;
530
- let connParamsRaw: string | undefined;
531
-
532
- if (protocols) {
533
- const protocolList = protocols.split(",").map((p) => p.trim());
534
- for (const protocol of protocolList) {
535
- if (protocol.startsWith(WS_PROTOCOL_ENCODING)) {
536
- encodingRaw = protocol.substring(
537
- WS_PROTOCOL_ENCODING.length,
538
- );
539
- } else if (protocol.startsWith(WS_PROTOCOL_CONN_PARAMS)) {
540
- connParamsRaw = decodeURIComponent(
541
- protocol.substring(WS_PROTOCOL_CONN_PARAMS.length),
542
- );
543
- }
544
- }
545
- }
546
-
547
- const encoding = EncodingSchema.parse(encodingRaw);
548
- const connParams = connParamsRaw
549
- ? JSON.parse(connParamsRaw)
550
- : undefined;
556
+ const { encoding, connParams } = parseWebSocketProtocols(protocols);
551
557
 
552
558
  // Fetch WS handler
553
559
  //
554
560
  // We store the promise since we need to add WebSocket event listeners immediately that will wait for the promise to resolve
555
- let wsHandlerPromise: Promise<UpgradeWebSocketArgs>;
556
- if (url.pathname === PATH_CONNECT_WEBSOCKET) {
557
- wsHandlerPromise = handleWebSocketConnect(
561
+ let wsHandler: UpgradeWebSocketArgs;
562
+ try {
563
+ wsHandler = await routeWebSocket(
558
564
  request,
565
+ requestPath,
566
+ requestHeaders,
559
567
  this.#runConfig,
560
568
  this,
561
569
  actorId,
562
570
  encoding,
563
571
  connParams,
564
- requestId,
565
- requestIdBuf,
566
- // Extract connId and connToken from protocols if needed
567
- undefined,
568
- undefined,
569
- );
570
- } else if (url.pathname.startsWith(PATH_RAW_WEBSOCKET_PREFIX)) {
571
- wsHandlerPromise = handleRawWebSocketHandler(
572
- request,
573
- url.pathname + url.search,
574
- this,
575
- actorId,
572
+ gatewayIdBuf,
576
573
  requestIdBuf,
574
+ isHibernatable,
575
+ isRestoringHibernatable,
577
576
  );
578
- } else {
579
- throw new Error(`Unreachable path: ${url.pathname}`);
577
+ } catch (err) {
578
+ logger().error({ msg: "building websocket handlers errored", err });
579
+ websocketRaw.close(1011, "ws.route_error");
580
+ return;
580
581
  }
581
582
 
582
- // TODO: Add close
583
-
584
583
  // Connect the Hono WS hook to the adapter
584
+ //
585
+ // We need to assign to `raw` in order for WSContext to expose it on
586
+ // `ws.raw`
587
+ (websocket as WSContextInit).raw = websocket;
585
588
  const wsContext = new WSContext(websocket);
586
589
 
587
- wsHandlerPromise.catch((err) => {
588
- logger().error({ msg: "building websocket handlers errored", err });
589
- wsContext.close(1011, `${err}`);
590
+ // Get connection and actor from wsHandler (may be undefined for inspector endpoint)
591
+ const conn = wsHandler.conn;
592
+ const actor = wsHandler.actor;
593
+ const connStateManager = conn?.[CONN_STATE_MANAGER_SYMBOL];
594
+
595
+ // Bind event listeners to Hono WebSocket handlers
596
+ //
597
+ // We update the HWS data after calling handlers in order to ensure
598
+ // that the handler ran successfully. By doing this, we ensure at least
599
+ // once delivery of events to the event handlers.
600
+
601
+ // Log when attaching event listeners
602
+ logger().debug({
603
+ msg: "attaching websocket event listeners",
604
+ actorId,
605
+ connId: conn?.id,
606
+ wsUniqueId: (websocket as any).__rivet_ws_id,
607
+ isRestoringHibernatable,
608
+ websocketType: websocket?.constructor?.name,
590
609
  });
591
610
 
592
- if (websocket.readyState === 1) {
593
- wsHandlerPromise.then((x) =>
594
- x.onOpen?.(new Event("open"), wsContext),
595
- );
596
- } else {
597
- websocket.addEventListener("open", (event) => {
598
- wsHandlerPromise.then((x) => x.onOpen?.(event, wsContext));
599
- });
611
+ if (isRestoringHibernatable) {
612
+ wsHandler.onRestore?.(wsContext);
600
613
  }
601
614
 
615
+ websocket.addEventListener("open", (event) => {
616
+ wsHandler.onOpen(event, wsContext);
617
+ });
618
+
602
619
  websocket.addEventListener("message", (event: RivetMessageEvent) => {
603
- wsHandlerPromise.then((x) => x.onMessage?.(event, wsContext));
620
+ logger().debug({
621
+ msg: "websocket message event listener triggered",
622
+ connId: conn?.id,
623
+ actorId: actor?.id,
624
+ messageIndex: event.rivetMessageIndex,
625
+ hasWsHandler: !!wsHandler,
626
+ hasOnMessage: !!wsHandler?.onMessage,
627
+ actorIsStopping: actor?.isStopping,
628
+ websocketType: websocket?.constructor?.name,
629
+ wsUniqueId: (websocket as any).__rivet_ws_id,
630
+ eventTargetWsId: (event.target as any)?.__rivet_ws_id,
631
+ });
604
632
 
605
- invariant(event.rivetRequestId, "missing rivetRequestId");
606
- invariant(event.rivetMessageIndex, "missing rivetMessageIndex");
633
+ // Check if actor is stopping - if so, don't process new messages.
634
+ // These messages will be reprocessed when the actor wakes up from hibernation.
635
+ // TODO: This will never retransmit the socket and the socket will close
636
+ if (actor?.isStopping) {
637
+ logger().debug({
638
+ msg: "ignoring ws message, actor is stopping",
639
+ connId: conn?.id,
640
+ actorId: actor?.id,
641
+ messageIndex: event.rivetMessageIndex,
642
+ });
643
+ return;
644
+ }
607
645
 
608
- // Track only the highest seen message index per request
609
- // Convert ArrayBuffer to string for Map key
610
- const currentEntry = this.#wsAckQueue.get(requestId);
611
- if (currentEntry) {
612
- if (event.rivetMessageIndex > currentEntry.messageIndex) {
613
- currentEntry.messageIndex = event.rivetMessageIndex;
646
+ // Process message
647
+ logger().debug({
648
+ msg: "calling wsHandler.onMessage",
649
+ connId: conn?.id,
650
+ messageIndex: event.rivetMessageIndex,
651
+ });
652
+ wsHandler.onMessage(event, wsContext);
653
+
654
+ // Persist message index for hibernatable connections
655
+ const hibernate = connStateManager?.hibernatableData;
656
+
657
+ if (hibernate && conn && actor) {
658
+ invariant(
659
+ typeof event.rivetMessageIndex === "number",
660
+ "missing event.rivetMessageIndex",
661
+ );
662
+
663
+ // Persist message index
664
+ const previousMsgIndex = hibernate.serverMessageIndex;
665
+ hibernate.serverMessageIndex = event.rivetMessageIndex;
666
+ logger().info({
667
+ msg: "persisting message index",
668
+ connId: conn.id,
669
+ previousMsgIndex,
670
+ newMsgIndex: event.rivetMessageIndex,
671
+ });
672
+
673
+ // Calculate message size and track cumulative size
674
+ const entry = this.#hwsMessageIndex.get(conn.id);
675
+ if (entry) {
676
+ // Track message length
677
+ const messageLength = getValueLength(event.data);
678
+ entry.bufferedMessageSize += messageLength;
679
+
680
+ if (
681
+ entry.bufferedMessageSize >=
682
+ CONN_BUFFERED_MESSAGE_SIZE_THRESHOLD
683
+ ) {
684
+ // Reset buffered message size immeidatley (instead
685
+ // of waiting for onAfterPersistConn) since we may
686
+ // receive more messages before onAfterPersistConn
687
+ // is called, which would called saveState
688
+ // immediate multiple times
689
+ entry.bufferedMessageSize = 0;
690
+ entry.pendingAckFromBufferSize = true;
691
+
692
+ // Save state immediately if approaching buffer threshold
693
+ actor.stateManager.saveState({
694
+ immediate: true,
695
+ });
696
+ } else {
697
+ // Save message index. The maxWait is set to the ack deadline
698
+ // since we ack the message immediately after persisting the index.
699
+ // If cumulative size exceeds threshold, force immediate persist.
700
+ //
701
+ // This will call EngineActorDriver.onAfterPersistConn after
702
+ // persist to send the ack to the gateway.
703
+ actor.stateManager.saveState({
704
+ maxWait: CONN_MESSAGE_ACK_DEADLINE,
705
+ });
706
+ }
614
707
  } else {
615
- logger().warn({
616
- msg: "received lower index than ack queue for message",
617
- requestId,
618
- queuedMessageIndex: currentEntry,
619
- eventMessageIndex: event.rivetMessageIndex,
708
+ // Fallback if entry missing
709
+ actor.stateManager.saveState({
710
+ maxWait: CONN_MESSAGE_ACK_DEADLINE,
620
711
  });
621
712
  }
622
- } else {
623
- this.#wsAckQueue.set(requestId, {
624
- requestIdBuf,
625
- messageIndex: event.rivetMessageIndex,
626
- });
627
713
  }
628
714
  });
629
715
 
630
716
  websocket.addEventListener("close", (event) => {
631
- // Flush any pending acks before closing
632
- this.#flushWsAcks();
633
- wsHandlerPromise.then((x) => x.onClose?.(event, wsContext));
717
+ wsHandler.onClose(event, wsContext);
718
+
719
+ // NOTE: Persisted connection is removed when `conn.disconnect`
720
+ // is called by the WebSocket route
634
721
  });
635
722
 
636
723
  websocket.addEventListener("error", (event) => {
637
- wsHandlerPromise.then((x) => x.onError?.(event, wsContext));
724
+ wsHandler.onError(event, wsContext);
638
725
  });
639
- }
640
726
 
641
- startSleep(actorId: string) {
642
- this.#runner.sleepActor(actorId);
727
+ // Log event listener attachment for restored connections
728
+ if (isRestoringHibernatable) {
729
+ logger().info({
730
+ msg: "event listeners attached to restored websocket",
731
+ actorId,
732
+ connId: conn?.id,
733
+ gatewayId: idToStr(gatewayIdBuf),
734
+ requestId: idToStr(requestIdBuf),
735
+ websocketType: websocket?.constructor?.name,
736
+ hasMessageListener: !!websocket.addEventListener,
737
+ });
738
+ }
643
739
  }
644
740
 
645
- async shutdownRunner(immediate: boolean): Promise<void> {
646
- logger().info({ msg: "stopping engine actor driver", immediate });
741
+ // MARK: - Hibernating WebSockets
742
+ #hwsCanHibernate(
743
+ actorId: string,
744
+ gatewayId: ArrayBuffer,
745
+ requestId: ArrayBuffer,
746
+ request: Request,
747
+ ): boolean {
748
+ const url = new URL(request.url);
749
+ const path = url.pathname;
647
750
 
648
- // TODO: We need to update the runner to have a draining state so:
649
- // 1. Send ToServerDraining
650
- // - This causes Pegboard to stop allocating actors to this runner
651
- // 2. Pegboard sends ToClientStopActor for all actors on this runner which handles the graceful migration of each actor independently
652
- // 3. Send ToServerStopping once all actors have successfully stopped
653
- //
654
- // What's happening right now is:
655
- // 1. All actors enter stopped state
656
- // 2. Actors still respond to requests because only RivetKit knows it's
657
- // stopping, this causes all requests to issue errors that the actor is
658
- // stopping. (This will NOT return a 503 bc the runner has no idea the
659
- // actors are stopping.)
660
- // 3. Once the last actor stops, then the runner finally stops + actors
661
- // reschedule
662
- //
663
- // This means that:
664
- // - All actors on this runner are bricked until the slowest _onStop finishes
665
- // - Guard will not gracefully handle requests bc it's not receiving a 503
666
- // - Actors can still be scheduled to this runner while the other
667
- // actors are stopping, meaning that those actors will NOT get _onStop
668
- // and will potentiall corrupt their state
669
- //
670
- // HACK: Stop all actors to allow state to be saved
671
- // NOTE: _onStop is only supposed to be called by the runner, we're
672
- // abusing it here
751
+ // Get actor instance from runner to access actor name
752
+ const actorInstance = this.#runner.getActor(actorId);
753
+ if (!actorInstance) {
754
+ logger().warn({
755
+ msg: "actor not found in #hwsCanHibernate",
756
+ actorId,
757
+ });
758
+ return false;
759
+ }
760
+
761
+ // Load actor handler to access persisted data
762
+ const handler = this.#actors.get(actorId);
763
+ if (!handler) {
764
+ logger().warn({
765
+ msg: "actor handler not found in #hwsCanHibernate",
766
+ actorId,
767
+ });
768
+ return false;
769
+ }
770
+ if (!handler.actor) {
771
+ logger().warn({
772
+ msg: "actor not found in #hwsCanHibernate",
773
+ actorId,
774
+ });
775
+ return false;
776
+ }
777
+
778
+ // Determine configuration for new WS
673
779
  logger().debug({
674
- msg: "stopping all actors before shutdown",
675
- actorCount: this.#actors.size,
780
+ msg: "no existing hibernatable websocket found",
781
+ gatewayId: idToStr(gatewayId),
782
+ requestId: idToStr(requestId),
676
783
  });
677
- const stopPromises: Promise<void>[] = [];
678
- for (const [_actorId, handler] of this.#actors.entries()) {
679
- if (handler.actor) {
680
- stopPromises.push(
681
- handler.actor._onStop().catch((err) => {
682
- handler.actor?.rLog.error({
683
- msg: "_onStop errored",
684
- error: stringifyError(err),
685
- });
686
- }),
687
- );
784
+ if (path === PATH_CONNECT) {
785
+ return true;
786
+ } else if (
787
+ path === PATH_WEBSOCKET_BASE ||
788
+ path.startsWith(PATH_WEBSOCKET_PREFIX)
789
+ ) {
790
+ // Find actor config
791
+ const definition = lookupInRegistry(
792
+ this.#registryConfig,
793
+ actorInstance.config.name,
794
+ );
795
+
796
+ // Check if can hibernate
797
+ const canHibernateWebSocket =
798
+ definition.config.options?.canHibernateWebSocket;
799
+ if (canHibernateWebSocket === true) {
800
+ return true;
801
+ } else if (typeof canHibernateWebSocket === "function") {
802
+ try {
803
+ // Truncate the path to match the behavior on onRawWebSocket
804
+ const newPath = truncateRawWebSocketPathPrefix(
805
+ url.pathname,
806
+ );
807
+ const truncatedRequest = new Request(
808
+ `http://actor${newPath}`,
809
+ request,
810
+ );
811
+
812
+ const canHibernate =
813
+ canHibernateWebSocket(truncatedRequest);
814
+ return canHibernate;
815
+ } catch (error) {
816
+ logger().error({
817
+ msg: "error calling canHibernateWebSocket",
818
+ error,
819
+ });
820
+ return false;
821
+ }
822
+ } else {
823
+ return false;
688
824
  }
825
+ } else if (path === PATH_INSPECTOR_CONNECT) {
826
+ return false;
827
+ } else {
828
+ logger().warn({
829
+ msg: "unexpected path for getActorHibernationConfig",
830
+ path,
831
+ });
832
+ return false;
689
833
  }
690
- await Promise.all(stopPromises);
691
- logger().debug({ msg: "all actors stopped" });
834
+ }
692
835
 
693
- // Clear the ack flush interval
694
- if (this.#wsAckFlushInterval) {
695
- clearInterval(this.#wsAckFlushInterval);
696
- this.#wsAckFlushInterval = undefined;
697
- }
836
+ async #hwsLoadAll(
837
+ actorId: string,
838
+ ): Promise<HibernatingWebSocketMetadata[]> {
839
+ const actor = await this.loadActor(actorId);
840
+ return actor.conns
841
+ .values()
842
+ .map((conn) => {
843
+ const connStateManager = conn[CONN_STATE_MANAGER_SYMBOL];
844
+ const hibernatable = connStateManager.hibernatableData;
845
+ if (!hibernatable) return undefined;
846
+ return {
847
+ gatewayId: hibernatable.gatewayId,
848
+ requestId: hibernatable.requestId,
849
+ serverMessageIndex: hibernatable.serverMessageIndex,
850
+ clientMessageIndex: hibernatable.clientMessageIndex,
851
+ path: hibernatable.requestPath,
852
+ headers: hibernatable.requestHeaders,
853
+ } satisfies HibernatingWebSocketMetadata;
854
+ })
855
+ .filter((x) => x !== undefined)
856
+ .toArray();
857
+ }
698
858
 
699
- // Flush any remaining acks
700
- this.#flushWsAcks();
859
+ async onBeforeActorStart(actor: AnyActorInstance): Promise<void> {
860
+ // Resolve promise if waiting
861
+ const handler = this.#actors.get(actor.id);
862
+ invariant(handler, "missing actor handler in onBeforeActorReady");
863
+ handler.actorStartPromise?.resolve();
864
+ handler.actorStartPromise = undefined;
701
865
 
702
- await this.#runner.shutdown(immediate);
866
+ // Restore hibernating requests
867
+ const metaEntries = await this.#hwsLoadAll(actor.id);
868
+ await this.#runner.restoreHibernatingRequests(actor.id, metaEntries);
703
869
  }
704
870
 
705
- async serverlessHandleStart(c: HonoContext): Promise<Response> {
706
- return streamSSE(c, async (stream) => {
707
- // NOTE: onAbort does not work reliably
708
- stream.onAbort(() => {});
709
- c.req.raw.signal.addEventListener("abort", () => {
710
- logger().debug("SSE aborted, shutting down runner");
871
+ onCreateConn(conn: AnyConn) {
872
+ const hibernatable = conn[CONN_STATE_MANAGER_SYMBOL].hibernatableData;
873
+ if (!hibernatable) return;
711
874
 
712
- // We cannot assume that the request will always be closed gracefully by Rivet. We always proceed with a graceful shutdown in case the request was terminated for any other reason.
713
- //
714
- // If we did not use a graceful shutdown, the runner would
715
- this.shutdownRunner(false);
716
- });
875
+ this.#hwsMessageIndex.set(conn.id, {
876
+ serverMessageIndex: hibernatable.serverMessageIndex,
877
+ bufferedMessageSize: 0,
878
+ pendingAckFromMessageIndex: false,
879
+ pendingAckFromBufferSize: false,
880
+ });
717
881
 
718
- await this.#runnerStarted.promise;
882
+ logger().debug({
883
+ msg: "created #hwsMessageIndex entry",
884
+ connId: conn.id,
885
+ serverMessageIndex: hibernatable.serverMessageIndex,
886
+ });
887
+ }
719
888
 
720
- // Runner id should be set if the runner started
721
- const payload = this.#runner.getServerlessInitPacket();
722
- invariant(payload, "runnerId not set");
723
- await stream.writeSSE({ data: payload });
889
+ onDestroyConn(conn: AnyConn) {
890
+ this.#hwsMessageIndex.delete(conn.id);
724
891
 
725
- // Send ping every second to keep the connection alive
726
- while (true) {
727
- if (this.#isRunnerStopped) {
728
- logger().debug({
729
- msg: "runner is stopped",
730
- });
731
- break;
732
- }
892
+ logger().debug({
893
+ msg: "removed #hwsMessageIndex entry",
894
+ connId: conn.id,
895
+ });
896
+ }
733
897
 
734
- if (stream.closed || stream.aborted) {
735
- logger().debug({
736
- msg: "runner sse stream closed",
737
- closed: stream.closed,
738
- aborted: stream.aborted,
739
- });
740
- break;
741
- }
898
+ onBeforePersistConn(conn: AnyConn) {
899
+ const stateManager = conn[CONN_STATE_MANAGER_SYMBOL];
900
+ const hibernatable = stateManager.hibernatableDataOrError();
742
901
 
743
- await stream.writeSSE({ event: "ping", data: "" });
744
- await stream.sleep(RUNNER_SSE_PING_INTERVAL);
745
- }
902
+ const entry = this.#hwsMessageIndex.get(conn.id);
903
+ if (!entry) {
904
+ logger().warn({
905
+ msg: "missing EngineActorDriver.#hwsMessageIndex entry for conn",
906
+ connId: conn.id,
907
+ });
908
+ return;
909
+ }
746
910
 
747
- // Wait for the runner to stop if the SSE stream aborted early for any reason
748
- await this.#runnerStopped.promise;
749
- });
911
+ // There is a newer message index
912
+ entry.pendingAckFromMessageIndex =
913
+ hibernatable.serverMessageIndex > entry.serverMessageIndex;
914
+ entry.serverMessageIndex = hibernatable.serverMessageIndex;
750
915
  }
751
916
 
752
- getExtraActorLogParams(): Record<string, string> {
753
- return { runnerId: this.#runner.runnerId ?? "-" };
917
+ onAfterPersistConn(conn: AnyConn) {
918
+ const stateManager = conn[CONN_STATE_MANAGER_SYMBOL];
919
+ const hibernatable = stateManager.hibernatableDataOrError();
920
+
921
+ const entry = this.#hwsMessageIndex.get(conn.id);
922
+ if (!entry) {
923
+ logger().warn({
924
+ msg: "missing EngineActorDriver.#hwsMessageIndex entry for conn",
925
+ connId: conn.id,
926
+ });
927
+ return;
928
+ }
929
+
930
+ // Ack entry
931
+ if (
932
+ entry.pendingAckFromMessageIndex ||
933
+ entry.pendingAckFromBufferSize
934
+ ) {
935
+ this.#runner.sendHibernatableWebSocketMessageAck(
936
+ hibernatable.gatewayId,
937
+ hibernatable.requestId,
938
+ entry.serverMessageIndex,
939
+ );
940
+ entry.pendingAckFromMessageIndex = false;
941
+ entry.pendingAckFromBufferSize = false;
942
+ entry.bufferedMessageSize = 0;
943
+ }
754
944
  }
755
945
  }