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,2336 +0,0 @@
1
- import * as cbor from "cbor-x";
2
- import type { SSEStreamingApi } from "hono/streaming";
3
- import type { WSContext } from "hono/ws";
4
- import invariant from "invariant";
5
- import onChange from "on-change";
6
- import type { ActorKey, Encoding } from "@/actor/mod";
7
- import type { Client } from "@/client/client";
8
- import { getBaseLogger, getIncludeTarget, type Logger } from "@/common/log";
9
- import { isCborSerializable, stringifyError } from "@/common/utils";
10
- import type { UniversalWebSocket } from "@/common/websocket-interface";
11
- import { ActorInspector } from "@/inspector/actor";
12
- import type { Registry } from "@/mod";
13
- import type * as bareSchema from "@/schemas/actor-persist/mod";
14
- import { PERSISTED_ACTOR_VERSIONED } from "@/schemas/actor-persist/versioned";
15
- import type * as protocol from "@/schemas/client-protocol/mod";
16
- import { TO_CLIENT_VERSIONED } from "@/schemas/client-protocol/versioned";
17
- import {
18
- arrayBuffersEqual,
19
- bufferToArrayBuffer,
20
- EXTRA_ERROR_LOG,
21
- getEnvUniversal,
22
- idToStr,
23
- promiseWithResolvers,
24
- SinglePromiseQueue,
25
- } from "@/utils";
26
- import { ActionContext } from "./action";
27
- import type { ActorConfig, OnConnectOptions } from "./config";
28
- import {
29
- Conn,
30
- type ConnId,
31
- generateConnId,
32
- generateConnRequestId,
33
- generateConnToken,
34
- } from "./conn";
35
- import {
36
- CONN_DRIVERS,
37
- type ConnDriver,
38
- ConnDriverKind,
39
- type ConnDriverState,
40
- getConnDriverKindFromState,
41
- } from "./conn-drivers";
42
- import type { ConnSocket } from "./conn-socket";
43
- import { ActorContext } from "./context";
44
- import type { AnyDatabaseProvider, InferDatabaseClient } from "./database";
45
- import type { ActorDriver } from "./driver";
46
- import * as errors from "./errors";
47
- import { serializeActorKey } from "./keys";
48
- import type {
49
- PersistedActor,
50
- PersistedConn,
51
- PersistedHibernatableWebSocket,
52
- PersistedScheduleEvent,
53
- } from "./persisted";
54
- import { processMessage } from "./protocol/old";
55
- import { CachedSerializer } from "./protocol/serde";
56
- import { Schedule } from "./schedule";
57
- import { DeadlineError, deadline, isConnStatePath, isStatePath } from "./utils";
58
-
59
- export const PERSIST_SYMBOL = Symbol("persist");
60
-
61
- /**
62
- * Options for the `_saveState` method.
63
- */
64
- export interface SaveStateOptions {
65
- /**
66
- * Forces the state to be saved immediately. This function will return when the state has saved successfully.
67
- */
68
- immediate?: boolean;
69
- /** Bypass ready check for stopping. */
70
- allowStoppingState?: boolean;
71
- }
72
-
73
- /** Actor type alias with all `any` types. Used for `extends` in classes referencing this actor. */
74
- export type AnyActorInstance = ActorInstance<
75
- // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
76
- any,
77
- // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
78
- any,
79
- // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
80
- any,
81
- // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
82
- any,
83
- // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
84
- any,
85
- // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
86
- any
87
- >;
88
-
89
- export type ExtractActorState<A extends AnyActorInstance> =
90
- A extends ActorInstance<
91
- infer State,
92
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
93
- any,
94
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
95
- any,
96
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
97
- any,
98
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
99
- any,
100
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
101
- any
102
- >
103
- ? State
104
- : never;
105
-
106
- export type ExtractActorConnParams<A extends AnyActorInstance> =
107
- A extends ActorInstance<
108
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
109
- any,
110
- infer ConnParams,
111
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
112
- any,
113
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
114
- any,
115
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
116
- any,
117
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
118
- any
119
- >
120
- ? ConnParams
121
- : never;
122
-
123
- export type ExtractActorConnState<A extends AnyActorInstance> =
124
- A extends ActorInstance<
125
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
126
- any,
127
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
128
- any,
129
- infer ConnState,
130
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
131
- any,
132
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
133
- any,
134
- // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
135
- any
136
- >
137
- ? ConnState
138
- : never;
139
-
140
- enum CanSleep {
141
- Yes,
142
- NotReady,
143
- ActiveConns,
144
- ActiveHonoHttpRequests,
145
- ActiveRawWebSockets,
146
- }
147
-
148
- export class ActorInstance<S, CP, CS, V, I, DB extends AnyDatabaseProvider> {
149
- // Shared actor context for this instance
150
- actorContext: ActorContext<S, CP, CS, V, I, DB>;
151
-
152
- /** Actor log, intended for the user to call */
153
- #log!: Logger;
154
-
155
- /** Runtime log, intended for internal actor logs */
156
- #rLog!: Logger;
157
-
158
- #sleepCalled = false;
159
- #stopCalled = false;
160
-
161
- get isStopping() {
162
- return this.#stopCalled;
163
- }
164
-
165
- #persistChanged = false;
166
- #isInOnStateChange = false;
167
-
168
- /**
169
- * The proxied state that notifies of changes automatically.
170
- *
171
- * Any data that should be stored indefinitely should be held within this object.
172
- */
173
- #persist!: PersistedActor<S, CP, CS, I>;
174
-
175
- get [PERSIST_SYMBOL](): PersistedActor<S, CP, CS, I> {
176
- return this.#persist;
177
- }
178
-
179
- /** Raw state without the proxy wrapper */
180
- #persistRaw!: PersistedActor<S, CP, CS, I>;
181
-
182
- #persistWriteQueue = new SinglePromiseQueue();
183
- #alarmWriteQueue = new SinglePromiseQueue();
184
-
185
- #lastSaveTime = 0;
186
- #pendingSaveTimeout?: NodeJS.Timeout;
187
-
188
- #vars?: V;
189
-
190
- #backgroundPromises: Promise<void>[] = [];
191
- #abortController = new AbortController();
192
- #config: ActorConfig<S, CP, CS, V, I, DB>;
193
- #actorDriver!: ActorDriver;
194
- #inlineClient!: Client<Registry<any>>;
195
- #actorId!: string;
196
- #name!: string;
197
- #key!: ActorKey;
198
- #region!: string;
199
- #ready = false;
200
-
201
- #connections = new Map<ConnId, Conn<S, CP, CS, V, I, DB>>();
202
- #subscriptionIndex = new Map<string, Set<Conn<S, CP, CS, V, I, DB>>>();
203
- #checkConnLivenessInterval?: NodeJS.Timeout;
204
-
205
- #sleepTimeout?: NodeJS.Timeout;
206
-
207
- /**
208
- * Track active HTTP requests through Hono router so sleep logic can
209
- * account for them. Does not include WebSockets.
210
- **/
211
- #activeHonoHttpRequests = 0;
212
- #activeRawWebSockets = new Set<UniversalWebSocket>();
213
-
214
- #schedule!: Schedule;
215
- #db!: InferDatabaseClient<DB>;
216
-
217
- #inspector = new ActorInspector(() => {
218
- return {
219
- isDbEnabled: async () => {
220
- return this.#db !== undefined;
221
- },
222
- getDb: async () => {
223
- return this.db;
224
- },
225
- isStateEnabled: async () => {
226
- return this.stateEnabled;
227
- },
228
- getState: async () => {
229
- this.#validateStateEnabled();
230
-
231
- // Must return from `#persistRaw` in order to not return the `onchange` proxy
232
- return this.#persistRaw.state as Record<string, any> as unknown;
233
- },
234
- getRpcs: async () => {
235
- return Object.keys(this.#config.actions);
236
- },
237
- getConnections: async () => {
238
- return Array.from(this.#connections.entries()).map(
239
- ([id, conn]) => ({
240
- id,
241
- params: conn.params as any,
242
- state: conn.__stateEnabled ? conn.state : undefined,
243
- status: conn.status,
244
- subscriptions: conn.subscriptions.size,
245
- lastSeen: conn.lastSeen,
246
- stateEnabled: conn.__stateEnabled,
247
- isHibernatable: conn.isHibernatable,
248
- hibernatableRequestId: conn.__persist
249
- .hibernatableRequestId
250
- ? idToStr(conn.__persist.hibernatableRequestId)
251
- : undefined,
252
- driver: conn.__driverState
253
- ? getConnDriverKindFromState(conn.__driverState)
254
- : undefined,
255
- }),
256
- );
257
- },
258
- setState: async (state: unknown) => {
259
- this.#validateStateEnabled();
260
-
261
- // Must set on `#persist` instead of `#persistRaw` in order to ensure that the `Proxy` is correctly configured
262
- //
263
- // We have to use `...` so `on-change` recognizes the changes to `state` (i.e. set #persistChanged` to true). This is because:
264
- // 1. In `getState`, we returned the value from `persistRaw`, which does not have the Proxy to monitor state changes
265
- // 2. If we were to assign `state` to `#persist.s`, `on-change` would assume nothing changed since `state` is still === `#persist.s` since we returned a reference in `getState`
266
- this.#persist.state = { ...(state as S) };
267
- await this.saveState({ immediate: true });
268
- },
269
- executeAction: async (name, params) => {
270
- const requestId = generateConnRequestId();
271
- const conn = await this.createConn(
272
- {
273
- requestId: requestId,
274
- hibernatable: false,
275
- driverState: { [ConnDriverKind.HTTP]: {} },
276
- },
277
- undefined,
278
- undefined,
279
- );
280
-
281
- try {
282
- return await this.executeAction(
283
- new ActionContext(this.actorContext, conn),
284
- name,
285
- params || [],
286
- );
287
- } finally {
288
- this.__connDisconnected(conn, true, requestId);
289
- }
290
- },
291
- };
292
- });
293
-
294
- get id() {
295
- return this.#actorId;
296
- }
297
-
298
- get inlineClient(): Client<Registry<any>> {
299
- return this.#inlineClient;
300
- }
301
-
302
- get inspector() {
303
- return this.#inspector;
304
- }
305
-
306
- get #sleepingSupported(): boolean {
307
- return this.#actorDriver.startSleep !== undefined;
308
- }
309
-
310
- /**
311
- * This constructor should never be used directly.
312
- *
313
- * Constructed in {@link ActorInstance.start}.
314
- *
315
- * @private
316
- */
317
- constructor(config: ActorConfig<S, CP, CS, V, I, DB>) {
318
- this.#config = config;
319
- this.actorContext = new ActorContext(this);
320
- }
321
-
322
- async start(
323
- actorDriver: ActorDriver,
324
- inlineClient: Client<Registry<any>>,
325
- actorId: string,
326
- name: string,
327
- key: ActorKey,
328
- region: string,
329
- ) {
330
- const logParams = {
331
- actor: name,
332
- key: serializeActorKey(key),
333
- actorId,
334
- };
335
-
336
- const extraLogParams = actorDriver.getExtraActorLogParams?.();
337
- if (extraLogParams) Object.assign(logParams, extraLogParams);
338
-
339
- this.#log = getBaseLogger().child(
340
- Object.assign(
341
- getIncludeTarget() ? { target: "actor" } : {},
342
- logParams,
343
- ),
344
- );
345
- this.#rLog = getBaseLogger().child(
346
- Object.assign(
347
- getIncludeTarget() ? { target: "actor-runtime" } : {},
348
- logParams,
349
- ),
350
- );
351
-
352
- this.#actorDriver = actorDriver;
353
- this.#inlineClient = inlineClient;
354
- this.#actorId = actorId;
355
- this.#name = name;
356
- this.#key = key;
357
- this.#region = region;
358
- this.#schedule = new Schedule(this);
359
-
360
- // Initialize server
361
- //
362
- // Store the promise so network requests can await initialization
363
- await this.#initialize();
364
-
365
- // TODO: Exit process if this errors
366
- if (this.#varsEnabled) {
367
- let vars: V | undefined;
368
- if ("createVars" in this.#config) {
369
- const dataOrPromise = this.#config.createVars(
370
- this.actorContext as unknown as ActorContext<
371
- undefined,
372
- undefined,
373
- undefined,
374
- undefined,
375
- undefined,
376
- any
377
- >,
378
- this.#actorDriver.getContext(this.#actorId),
379
- );
380
- if (dataOrPromise instanceof Promise) {
381
- vars = await deadline(
382
- dataOrPromise,
383
- this.#config.options.createVarsTimeout,
384
- );
385
- } else {
386
- vars = dataOrPromise;
387
- }
388
- } else if ("vars" in this.#config) {
389
- vars = structuredClone(this.#config.vars);
390
- } else {
391
- throw new Error(
392
- "Could not variables from 'createVars' or 'vars'",
393
- );
394
- }
395
- this.#vars = vars;
396
- }
397
-
398
- // TODO: Exit process if this errors
399
- this.#rLog.info({ msg: "actor starting" });
400
- if (this.#config.onStart) {
401
- const result = this.#config.onStart(this.actorContext);
402
- if (result instanceof Promise) {
403
- await result;
404
- }
405
- }
406
-
407
- // Setup Database
408
- if ("db" in this.#config && this.#config.db) {
409
- const client = await this.#config.db.createClient({
410
- getDatabase: () => actorDriver.getDatabase(this.#actorId),
411
- });
412
- this.#rLog.info({ msg: "database migration starting" });
413
- await this.#config.db.onMigrate?.(client);
414
- this.#rLog.info({ msg: "database migration complete" });
415
- this.#db = client;
416
- }
417
-
418
- // Set alarm for next scheduled event if any exist after finishing initiation sequence
419
- if (this.#persist.scheduledEvents.length > 0) {
420
- await this.#queueSetAlarm(
421
- this.#persist.scheduledEvents[0].timestamp,
422
- );
423
- }
424
-
425
- this.#rLog.info({ msg: "actor ready" });
426
- this.#ready = true;
427
-
428
- // Must be called after setting `#ready` or else it will not schedule sleep
429
- this.#resetSleepTimer();
430
-
431
- // Start conn liveness interval
432
- //
433
- // Check for liveness immediately since we may have connections that
434
- // were in `reconnecting` state when the actor went to sleep that we
435
- // need to purge.
436
- //
437
- // We don't use alarms for connection liveness since alarms require
438
- // durability & are expensive. Connection liveness is safe to assume
439
- // it only needs to be ran while the actor is awake and does not need
440
- // to manually wake the actor. The only case this is not true is if the
441
- // connection liveness timeout is greater than the actor sleep timeout
442
- // OR if the actor is manually put to sleep. In this case, the connections
443
- // will be stuck in a `reconnecting` state until the actor is awaken again.
444
- this.#checkConnLivenessInterval = setInterval(
445
- this.#checkConnectionsLiveness.bind(this),
446
- this.#config.options.connectionLivenessInterval,
447
- );
448
- this.#checkConnectionsLiveness();
449
-
450
- // Trigger any pending alarms
451
- await this._onAlarm();
452
- }
453
-
454
- async #scheduleEventInner(newEvent: PersistedScheduleEvent) {
455
- this.actorContext.log.info({ msg: "scheduling event", ...newEvent });
456
-
457
- // Insert event in to index
458
- const insertIndex = this.#persist.scheduledEvents.findIndex(
459
- (x) => x.timestamp > newEvent.timestamp,
460
- );
461
- if (insertIndex === -1) {
462
- this.#persist.scheduledEvents.push(newEvent);
463
- } else {
464
- this.#persist.scheduledEvents.splice(insertIndex, 0, newEvent);
465
- }
466
-
467
- // Update alarm if:
468
- // - this is the newest event (i.e. at beginning of array) or
469
- // - this is the only event (i.e. the only event in the array)
470
- if (insertIndex === 0 || this.#persist.scheduledEvents.length === 1) {
471
- this.actorContext.log.info({
472
- msg: "setting alarm",
473
- timestamp: newEvent.timestamp,
474
- eventCount: this.#persist.scheduledEvents.length,
475
- });
476
- await this.#queueSetAlarm(newEvent.timestamp);
477
- }
478
- }
479
-
480
- /**
481
- * Triggers any pending alarms.
482
- *
483
- * This method is idempotent. It's called automatically when the actor wakes
484
- * in order to trigger any pending alarms.
485
- */
486
- async _onAlarm() {
487
- const now = Date.now();
488
- this.actorContext.log.debug({
489
- msg: "alarm triggered",
490
- now,
491
- events: this.#persist.scheduledEvents.length,
492
- });
493
-
494
- // Update sleep
495
- //
496
- // Do this before any async logic
497
- this.#resetSleepTimer();
498
-
499
- // Remove events from schedule that we're about to run
500
- const runIndex = this.#persist.scheduledEvents.findIndex(
501
- (x) => x.timestamp <= now,
502
- );
503
- if (runIndex === -1) {
504
- // This method is idempotent, so this will happen in scenarios like `start` and
505
- // no events are pending.
506
- this.#rLog.debug({ msg: "no events are due yet" });
507
- if (this.#persist.scheduledEvents.length > 0) {
508
- const nextTs = this.#persist.scheduledEvents[0].timestamp;
509
- this.actorContext.log.debug({
510
- msg: "alarm fired early, rescheduling for next event",
511
- now,
512
- nextTs,
513
- delta: nextTs - now,
514
- });
515
- await this.#queueSetAlarm(nextTs);
516
- }
517
- this.actorContext.log.debug({ msg: "no events to run", now });
518
- return;
519
- }
520
- const scheduleEvents = this.#persist.scheduledEvents.splice(
521
- 0,
522
- runIndex + 1,
523
- );
524
- this.actorContext.log.debug({
525
- msg: "running events",
526
- count: scheduleEvents.length,
527
- });
528
-
529
- // Set alarm for next event
530
- if (this.#persist.scheduledEvents.length > 0) {
531
- const nextTs = this.#persist.scheduledEvents[0].timestamp;
532
- this.actorContext.log.info({
533
- msg: "setting next alarm",
534
- nextTs,
535
- remainingEvents: this.#persist.scheduledEvents.length,
536
- });
537
- await this.#queueSetAlarm(nextTs);
538
- }
539
-
540
- // Iterate by event key in order to ensure we call the events in order
541
- for (const event of scheduleEvents) {
542
- try {
543
- this.actorContext.log.info({
544
- msg: "running action for event",
545
- event: event.eventId,
546
- timestamp: event.timestamp,
547
- action: event.kind.generic.actionName,
548
- });
549
-
550
- // Look up function
551
- const fn: unknown =
552
- this.#config.actions[event.kind.generic.actionName];
553
-
554
- if (!fn)
555
- throw new Error(
556
- `Missing action for alarm ${event.kind.generic.actionName}`,
557
- );
558
- if (typeof fn !== "function")
559
- throw new Error(
560
- `Alarm function lookup for ${event.kind.generic.actionName} returned ${typeof fn}`,
561
- );
562
-
563
- // Call function
564
- try {
565
- const args = event.kind.generic.args
566
- ? cbor.decode(new Uint8Array(event.kind.generic.args))
567
- : [];
568
- await fn.call(undefined, this.actorContext, ...args);
569
- } catch (error) {
570
- this.actorContext.log.error({
571
- msg: "error while running event",
572
- error: stringifyError(error),
573
- event: event.eventId,
574
- timestamp: event.timestamp,
575
- action: event.kind.generic.actionName,
576
- });
577
- }
578
- } catch (error) {
579
- this.actorContext.log.error({
580
- msg: "internal error while running event",
581
- error: stringifyError(error),
582
- ...event,
583
- });
584
- }
585
- }
586
- }
587
-
588
- async scheduleEvent(
589
- timestamp: number,
590
- action: string,
591
- args: unknown[],
592
- ): Promise<void> {
593
- return this.#scheduleEventInner({
594
- eventId: crypto.randomUUID(),
595
- timestamp,
596
- kind: {
597
- generic: {
598
- actionName: action,
599
- args: bufferToArrayBuffer(cbor.encode(args)),
600
- },
601
- },
602
- });
603
- }
604
-
605
- get stateEnabled() {
606
- return "createState" in this.#config || "state" in this.#config;
607
- }
608
-
609
- #validateStateEnabled() {
610
- if (!this.stateEnabled) {
611
- throw new errors.StateNotEnabled();
612
- }
613
- }
614
-
615
- get connStateEnabled() {
616
- return "createConnState" in this.#config || "connState" in this.#config;
617
- }
618
-
619
- get #varsEnabled() {
620
- return "createVars" in this.#config || "vars" in this.#config;
621
- }
622
-
623
- #validateVarsEnabled() {
624
- if (!this.#varsEnabled) {
625
- throw new errors.VarsNotEnabled();
626
- }
627
- }
628
-
629
- /** Promise used to wait for a save to complete. This is required since you cannot await `#saveStateThrottled`. */
630
- #onPersistSavedPromise?: ReturnType<typeof promiseWithResolvers<void>>;
631
-
632
- /** Throttled save state method. Used to write to KV at a reasonable cadence. */
633
- #savePersistThrottled() {
634
- const now = Date.now();
635
- const timeSinceLastSave = now - this.#lastSaveTime;
636
- const saveInterval = this.#config.options.stateSaveInterval;
637
-
638
- // If we're within the throttle window and not already scheduled, schedule the next save.
639
- if (timeSinceLastSave < saveInterval) {
640
- if (this.#pendingSaveTimeout === undefined) {
641
- this.#pendingSaveTimeout = setTimeout(() => {
642
- this.#pendingSaveTimeout = undefined;
643
- this.#savePersistInner();
644
- }, saveInterval - timeSinceLastSave);
645
- }
646
- } else {
647
- // If we're outside the throttle window, save immediately
648
- this.#savePersistInner();
649
- }
650
- }
651
-
652
- /** Saves the state to KV. You probably want to use #saveStateThrottled instead except for a few edge cases. */
653
- async #savePersistInner() {
654
- try {
655
- this.#lastSaveTime = Date.now();
656
-
657
- if (this.#persistChanged) {
658
- const finished = this.#persistWriteQueue.enqueue(async () => {
659
- this.#rLog.debug({ msg: "saving persist" });
660
-
661
- // There might be more changes while we're writing, so we set this
662
- // before writing to KV in order to avoid a race condition.
663
- this.#persistChanged = false;
664
-
665
- // Convert to BARE types and write to KV
666
- const bareData = this.#convertToBarePersisted(
667
- this.#persistRaw,
668
- );
669
- await this.#actorDriver.writePersistedData(
670
- this.#actorId,
671
- PERSISTED_ACTOR_VERSIONED.serializeWithEmbeddedVersion(
672
- bareData,
673
- ),
674
- );
675
-
676
- this.#rLog.debug({ msg: "persist saved" });
677
- });
678
-
679
- await finished;
680
- }
681
-
682
- this.#onPersistSavedPromise?.resolve();
683
- } catch (error) {
684
- this.#rLog.error({
685
- msg: "error saving persist",
686
- error: stringifyError(error),
687
- });
688
- this.#onPersistSavedPromise?.reject(error);
689
- throw error;
690
- }
691
- }
692
-
693
- async #queueSetAlarm(timestamp: number): Promise<void> {
694
- await this.#alarmWriteQueue.enqueue(async () => {
695
- await this.#actorDriver.setAlarm(this, timestamp);
696
- });
697
- }
698
-
699
- /**
700
- * Creates proxy for `#persist` that handles automatically flagging when state needs to be updated.
701
- */
702
- #setPersist(target: PersistedActor<S, CP, CS, I>) {
703
- // Set raw persist object
704
- this.#persistRaw = target;
705
-
706
- // TODO: Allow disabling in production
707
- // If this can't be proxied, return raw value
708
- if (target === null || typeof target !== "object") {
709
- let invalidPath = "";
710
- if (
711
- !isCborSerializable(
712
- target,
713
- (path) => {
714
- invalidPath = path;
715
- },
716
- "",
717
- )
718
- ) {
719
- throw new errors.InvalidStateType({ path: invalidPath });
720
- }
721
- return target;
722
- }
723
-
724
- // Unsubscribe from old state
725
- if (this.#persist) {
726
- onChange.unsubscribe(this.#persist);
727
- }
728
-
729
- // Listen for changes to the object in order to automatically write state
730
- this.#persist = onChange(
731
- target,
732
- // biome-ignore lint/suspicious/noExplicitAny: Don't know types in proxy
733
- (
734
- path: string,
735
- value: any,
736
- _previousValue: any,
737
- _applyData: any,
738
- ) => {
739
- const actorStatePath = isStatePath(path);
740
- const connStatePath = isConnStatePath(path);
741
-
742
- // Validate CBOR serializability for state changes
743
- if (actorStatePath || connStatePath) {
744
- let invalidPath = "";
745
- if (
746
- !isCborSerializable(
747
- value,
748
- (invalidPathPart) => {
749
- invalidPath = invalidPathPart;
750
- },
751
- "",
752
- )
753
- ) {
754
- throw new errors.InvalidStateType({
755
- path: path + (invalidPath ? `.${invalidPath}` : ""),
756
- });
757
- }
758
- }
759
-
760
- this.#rLog.debug({
761
- msg: "onChange triggered, setting persistChanged=true",
762
- path,
763
- });
764
- this.#persistChanged = true;
765
-
766
- // Inform the inspector about state changes (only for state path)
767
- if (actorStatePath) {
768
- this.inspector.emitter.emit(
769
- "stateUpdated",
770
- this.#persist.state,
771
- );
772
- }
773
-
774
- // Call onStateChange if it exists
775
- //
776
- // Skip if we're already inside onStateChange to prevent infinite recursion
777
- if (
778
- actorStatePath &&
779
- this.#config.onStateChange &&
780
- this.#ready &&
781
- !this.#isInOnStateChange
782
- ) {
783
- try {
784
- this.#isInOnStateChange = true;
785
- this.#config.onStateChange(
786
- this.actorContext,
787
- this.#persistRaw.state,
788
- );
789
- } catch (error) {
790
- this.#rLog.error({
791
- msg: "error in `_onStateChange`",
792
- error: stringifyError(error),
793
- });
794
- } finally {
795
- this.#isInOnStateChange = false;
796
- }
797
- }
798
-
799
- // State will be flushed at the end of the action
800
- },
801
- { ignoreDetached: true },
802
- );
803
- }
804
-
805
- async #initialize() {
806
- // Read initial state
807
- const persistDataBuffer = await this.#actorDriver.readPersistedData(
808
- this.#actorId,
809
- );
810
- invariant(
811
- persistDataBuffer !== undefined,
812
- "persist data has not been set, it should be set when initialized",
813
- );
814
- const bareData =
815
- PERSISTED_ACTOR_VERSIONED.deserializeWithEmbeddedVersion(
816
- persistDataBuffer,
817
- );
818
- const persistData = this.#convertFromBarePersisted(bareData);
819
-
820
- if (persistData.hasInitiated) {
821
- this.#rLog.info({
822
- msg: "actor restoring",
823
- connections: persistData.connections.length,
824
- hibernatableWebSockets:
825
- persistData.hibernatableWebSocket.length,
826
- });
827
-
828
- // Set initial state
829
- this.#setPersist(persistData);
830
-
831
- // Load connections
832
- for (const connPersist of this.#persist.connections) {
833
- // Create connections
834
- const conn = new Conn<S, CP, CS, V, I, DB>(this, connPersist);
835
- this.#connections.set(conn.id, conn);
836
-
837
- // Register event subscriptions
838
- for (const sub of connPersist.subscriptions) {
839
- this.#addSubscription(sub.eventName, conn, true);
840
- }
841
- }
842
- } else {
843
- this.#rLog.info({ msg: "actor creating" });
844
-
845
- // Initialize actor state
846
- let stateData: unknown;
847
- if (this.stateEnabled) {
848
- this.#rLog.info({ msg: "actor state initializing" });
849
-
850
- if ("createState" in this.#config) {
851
- this.#config.createState;
852
-
853
- // Convert state to undefined since state is not defined yet here
854
- stateData = await this.#config.createState(
855
- this.actorContext as unknown as ActorContext<
856
- undefined,
857
- undefined,
858
- undefined,
859
- undefined,
860
- undefined,
861
- undefined
862
- >,
863
- persistData.input!,
864
- );
865
- } else if ("state" in this.#config) {
866
- stateData = structuredClone(this.#config.state);
867
- } else {
868
- throw new Error(
869
- "Both 'createState' or 'state' were not defined",
870
- );
871
- }
872
- } else {
873
- this.#rLog.debug({ msg: "state not enabled" });
874
- }
875
-
876
- // Save state and mark as initialized
877
- persistData.state = stateData as S;
878
- persistData.hasInitiated = true;
879
-
880
- // Update state
881
- this.#rLog.debug({ msg: "writing state" });
882
- const bareData = this.#convertToBarePersisted(persistData);
883
- await this.#actorDriver.writePersistedData(
884
- this.#actorId,
885
- PERSISTED_ACTOR_VERSIONED.serializeWithEmbeddedVersion(
886
- bareData,
887
- ),
888
- );
889
-
890
- this.#setPersist(persistData);
891
-
892
- // Notify creation
893
- if (this.#config.onCreate) {
894
- await this.#config.onCreate(
895
- this.actorContext,
896
- persistData.input!,
897
- );
898
- }
899
- }
900
- }
901
-
902
- __getConnForId(id: string): Conn<S, CP, CS, V, I, DB> | undefined {
903
- return this.#connections.get(id);
904
- }
905
-
906
- /**
907
- * Call when conn is disconnected. Used by transports.
908
- *
909
- * If a clean diconnect, will be removed immediately.
910
- *
911
- * If not a clean disconnect, will keep the connection alive for a given interval to wait for reconnect.
912
- */
913
- __connDisconnected(
914
- conn: Conn<S, CP, CS, V, I, DB>,
915
- wasClean: boolean,
916
- requestId: string,
917
- ) {
918
- // If socket ID is provided, check if it matches the current socket ID
919
- // If it doesn't match, this is a stale disconnect event from an old socket
920
- if (
921
- requestId &&
922
- conn.__socket &&
923
- requestId !== conn.__socket.requestId
924
- ) {
925
- this.#rLog.debug({
926
- msg: "ignoring stale disconnect event",
927
- connId: conn.id,
928
- eventRequestId: requestId,
929
- currentRequestId: conn.__socket.requestId,
930
- });
931
- return;
932
- }
933
-
934
- if (wasClean) {
935
- // Disconnected cleanly, remove the conn
936
-
937
- this.#removeConn(conn);
938
- } else {
939
- // Disconnected uncleanly, allow reconnection
940
-
941
- if (!conn.__driverState) {
942
- this.rLog.warn("called conn disconnected without driver state");
943
- }
944
-
945
- // Update last seen so we know when to clean it up
946
- conn.__persist.lastSeen = Date.now();
947
-
948
- // Remove socket
949
- conn.__socket = undefined;
950
-
951
- // Update sleep
952
- this.#resetSleepTimer();
953
- }
954
- }
955
-
956
- /**
957
- * Removes a connection and cleans up its resources.
958
- */
959
- #removeConn(conn: Conn<S, CP, CS, V, I, DB>) {
960
- // Remove from persist & save immediately
961
- const connIdx = this.#persist.connections.findIndex(
962
- (c) => c.connId === conn.id,
963
- );
964
- if (connIdx !== -1) {
965
- this.#persist.connections.splice(connIdx, 1);
966
- this.saveState({ immediate: true, allowStoppingState: true });
967
- } else {
968
- this.#rLog.warn({
969
- msg: "could not find persisted connection to remove",
970
- connId: conn.id,
971
- });
972
- }
973
-
974
- // Remove from state
975
- this.#connections.delete(conn.id);
976
- this.#rLog.debug({ msg: "removed conn", connId: conn.id });
977
-
978
- // Remove subscriptions
979
- for (const eventName of [...conn.subscriptions.values()]) {
980
- this.#removeSubscription(eventName, conn, true);
981
- }
982
-
983
- this.inspector.emitter.emit("connectionUpdated");
984
- if (this.#config.onDisconnect) {
985
- try {
986
- const result = this.#config.onDisconnect(
987
- this.actorContext,
988
- conn,
989
- );
990
- if (result instanceof Promise) {
991
- // Handle promise but don't await it to prevent blocking
992
- result.catch((error) => {
993
- this.#rLog.error({
994
- msg: "error in `onDisconnect`",
995
- error: stringifyError(error),
996
- });
997
- });
998
- }
999
- } catch (error) {
1000
- this.#rLog.error({
1001
- msg: "error in `onDisconnect`",
1002
- error: stringifyError(error),
1003
- });
1004
- }
1005
- }
1006
-
1007
- // Update sleep
1008
- this.#resetSleepTimer();
1009
- }
1010
-
1011
- /**
1012
- * Called to create a new connection or reconnect an existing one.
1013
- */
1014
- async createConn(
1015
- socket: ConnSocket,
1016
- // biome-ignore lint/suspicious/noExplicitAny: TypeScript bug with ExtractActorConnParams<this>,
1017
- params: any,
1018
- request?: Request,
1019
- connectionId?: string,
1020
- connectionToken?: string,
1021
- ): Promise<Conn<S, CP, CS, V, I, DB>> {
1022
- this.#assertReady();
1023
-
1024
- // Check for hibernatable websocket reconnection
1025
- if (socket.requestIdBuf && socket.hibernatable) {
1026
- this.rLog.debug({
1027
- msg: "checking for hibernatable websocket connection",
1028
- requestId: socket.requestId,
1029
- existingConnectionsCount: this.#connections.size,
1030
- });
1031
-
1032
- // Find existing connection with matching hibernatableRequestId
1033
- const existingConn = Array.from(this.#connections.values()).find(
1034
- (conn) =>
1035
- conn.__persist.hibernatableRequestId &&
1036
- arrayBuffersEqual(
1037
- conn.__persist.hibernatableRequestId,
1038
- socket.requestIdBuf!,
1039
- ),
1040
- );
1041
-
1042
- if (existingConn) {
1043
- this.rLog.debug({
1044
- msg: "reconnecting hibernatable websocket connection",
1045
- connectionId: existingConn.id,
1046
- requestId: socket.requestId,
1047
- });
1048
-
1049
- // If there's an existing driver state, clean it up without marking as clean disconnect
1050
- if (existingConn.__driverState) {
1051
- this.#rLog.warn({
1052
- msg: "found existing driver state on hibernatable websocket",
1053
- connectionId: existingConn.id,
1054
- requestId: socket.requestId,
1055
- });
1056
- const driverKind = getConnDriverKindFromState(
1057
- existingConn.__driverState,
1058
- );
1059
- const driver = CONN_DRIVERS[driverKind];
1060
- if (driver.disconnect) {
1061
- // Call driver disconnect to clean up directly. Don't use Conn.disconnect since that will remove the connection entirely.
1062
- driver.disconnect(
1063
- this,
1064
- existingConn,
1065
- (existingConn.__driverState as any)[driverKind],
1066
- "Reconnecting hibernatable websocket with new driver state",
1067
- );
1068
- }
1069
- }
1070
-
1071
- // Update with new driver state
1072
- existingConn.__socket = socket;
1073
- existingConn.__persist.lastSeen = Date.now();
1074
-
1075
- // Update sleep timer since connection is now active
1076
- this.#resetSleepTimer();
1077
-
1078
- this.inspector.emitter.emit("connectionUpdated");
1079
-
1080
- // We don't need to send a new init message since this is a
1081
- // hibernated request that has already been initialized
1082
-
1083
- return existingConn;
1084
- } else {
1085
- this.rLog.debug({
1086
- msg: "no existing hibernatable connection found, creating new connection",
1087
- requestId: socket.requestId,
1088
- });
1089
- }
1090
- }
1091
-
1092
- // If connection ID and token are provided, try to reconnect
1093
- if (connectionId && connectionToken) {
1094
- this.rLog.debug({
1095
- msg: "checking for existing connection",
1096
- connectionId,
1097
- });
1098
- const existingConn = this.#connections.get(connectionId);
1099
- if (existingConn && existingConn._token === connectionToken) {
1100
- // This is a valid reconnection
1101
- this.rLog.debug({
1102
- msg: "reconnecting existing connection",
1103
- connectionId,
1104
- });
1105
-
1106
- // If there's an existing driver state, clean it up without marking as clean disconnect
1107
- if (existingConn.__driverState) {
1108
- const driverKind = getConnDriverKindFromState(
1109
- existingConn.__driverState,
1110
- );
1111
- const driver = CONN_DRIVERS[driverKind];
1112
- if (driver.disconnect) {
1113
- // Call driver disconnect to clean up directly. Don't use Conn.disconnect since that will remove the connection entirely.
1114
- driver.disconnect(
1115
- this,
1116
- existingConn,
1117
- (existingConn.__driverState as any)[driverKind],
1118
- "Reconnecting with new driver state",
1119
- );
1120
- }
1121
- }
1122
-
1123
- // Update with new driver state
1124
- existingConn.__socket = socket;
1125
- existingConn.__persist.lastSeen = Date.now();
1126
-
1127
- // Update sleep timer since connection is now active
1128
- this.#resetSleepTimer();
1129
-
1130
- this.inspector.emitter.emit("connectionUpdated");
1131
-
1132
- // Send init message for reconnection
1133
- existingConn._sendMessage(
1134
- new CachedSerializer<protocol.ToClient>(
1135
- {
1136
- body: {
1137
- tag: "Init",
1138
- val: {
1139
- actorId: this.id,
1140
- connectionId: existingConn.id,
1141
- connectionToken: existingConn._token,
1142
- },
1143
- },
1144
- },
1145
- TO_CLIENT_VERSIONED,
1146
- ),
1147
- );
1148
-
1149
- return existingConn;
1150
- } else {
1151
- this.rLog.debug({
1152
- msg: "connection not found or token mismatch, creating new connection",
1153
- connectionId,
1154
- });
1155
- }
1156
- }
1157
-
1158
- // Generate new connection ID and token if not provided or if reconnection failed
1159
- const newConnId = generateConnId();
1160
- const newConnToken = generateConnToken();
1161
-
1162
- if (this.#connections.has(newConnId)) {
1163
- throw new Error(`Connection already exists: ${newConnId}`);
1164
- }
1165
-
1166
- // Prepare connection state
1167
- let connState: CS | undefined;
1168
-
1169
- const onBeforeConnectOpts = {
1170
- request,
1171
- } satisfies OnConnectOptions;
1172
-
1173
- if (this.#config.onBeforeConnect) {
1174
- await this.#config.onBeforeConnect(
1175
- this.actorContext,
1176
- onBeforeConnectOpts,
1177
- params,
1178
- );
1179
- }
1180
-
1181
- if (this.connStateEnabled) {
1182
- if ("createConnState" in this.#config) {
1183
- const dataOrPromise = this.#config.createConnState(
1184
- this.actorContext as unknown as ActorContext<
1185
- undefined,
1186
- undefined,
1187
- undefined,
1188
- undefined,
1189
- undefined,
1190
- undefined
1191
- >,
1192
- onBeforeConnectOpts,
1193
- params,
1194
- );
1195
- if (dataOrPromise instanceof Promise) {
1196
- connState = await deadline(
1197
- dataOrPromise,
1198
- this.#config.options.createConnStateTimeout,
1199
- );
1200
- } else {
1201
- connState = dataOrPromise;
1202
- }
1203
- } else if ("connState" in this.#config) {
1204
- connState = structuredClone(this.#config.connState);
1205
- } else {
1206
- throw new Error(
1207
- "Could not create connection state from 'createConnState' or 'connState'",
1208
- );
1209
- }
1210
- }
1211
-
1212
- // Create connection
1213
- const persist: PersistedConn<CP, CS> = {
1214
- connId: newConnId,
1215
- token: newConnToken,
1216
- params: params,
1217
- state: connState as CS,
1218
- lastSeen: Date.now(),
1219
- subscriptions: [],
1220
- };
1221
-
1222
- // Check if this connection is for a hibernatable websocket
1223
- if (socket.requestIdBuf) {
1224
- const isHibernatable =
1225
- this.#persist.hibernatableWebSocket.findIndex((ws) =>
1226
- arrayBuffersEqual(ws.requestId, socket.requestIdBuf!),
1227
- ) !== -1;
1228
-
1229
- if (isHibernatable) {
1230
- persist.hibernatableRequestId = socket.requestIdBuf;
1231
- }
1232
- }
1233
-
1234
- const conn = new Conn<S, CP, CS, V, I, DB>(this, persist);
1235
- conn.__socket = socket;
1236
- this.#connections.set(conn.id, conn);
1237
-
1238
- // Update sleep
1239
- //
1240
- // Do this immediately after adding connection & before any async logic in order to avoid race conditions with sleep timeouts
1241
- this.#resetSleepTimer();
1242
-
1243
- // Add to persistence & save immediately
1244
- this.#persist.connections.push(persist);
1245
- this.saveState({ immediate: true });
1246
-
1247
- // Handle connection
1248
- if (this.#config.onConnect) {
1249
- try {
1250
- const result = this.#config.onConnect(this.actorContext, conn);
1251
- if (result instanceof Promise) {
1252
- deadline(
1253
- result,
1254
- this.#config.options.onConnectTimeout,
1255
- ).catch((error) => {
1256
- this.#rLog.error({
1257
- msg: "error in `onConnect`, closing socket",
1258
- error,
1259
- });
1260
- conn?.disconnect("`onConnect` failed");
1261
- });
1262
- }
1263
- } catch (error) {
1264
- this.#rLog.error({
1265
- msg: "error in `onConnect`",
1266
- error: stringifyError(error),
1267
- });
1268
- conn?.disconnect("`onConnect` failed");
1269
- }
1270
- }
1271
-
1272
- this.inspector.emitter.emit("connectionUpdated");
1273
-
1274
- // Send init message
1275
- conn._sendMessage(
1276
- new CachedSerializer<protocol.ToClient>(
1277
- {
1278
- body: {
1279
- tag: "Init",
1280
- val: {
1281
- actorId: this.id,
1282
- connectionId: conn.id,
1283
- connectionToken: conn._token,
1284
- },
1285
- },
1286
- },
1287
- TO_CLIENT_VERSIONED,
1288
- ),
1289
- );
1290
-
1291
- return conn;
1292
- }
1293
-
1294
- // MARK: Messages
1295
- async processMessage(
1296
- message: protocol.ToServer,
1297
- conn: Conn<S, CP, CS, V, I, DB>,
1298
- ) {
1299
- await processMessage(message, this, conn, {
1300
- onExecuteAction: async (ctx, name, args) => {
1301
- this.inspector.emitter.emit("eventFired", {
1302
- type: "action",
1303
- name,
1304
- args,
1305
- connId: conn.id,
1306
- });
1307
- return await this.executeAction(ctx, name, args);
1308
- },
1309
- onSubscribe: async (eventName, conn) => {
1310
- this.inspector.emitter.emit("eventFired", {
1311
- type: "subscribe",
1312
- eventName,
1313
- connId: conn.id,
1314
- });
1315
- this.#addSubscription(eventName, conn, false);
1316
- },
1317
- onUnsubscribe: async (eventName, conn) => {
1318
- this.inspector.emitter.emit("eventFired", {
1319
- type: "unsubscribe",
1320
- eventName,
1321
- connId: conn.id,
1322
- });
1323
- this.#removeSubscription(eventName, conn, false);
1324
- },
1325
- });
1326
- }
1327
-
1328
- // MARK: Events
1329
- #addSubscription(
1330
- eventName: string,
1331
- connection: Conn<S, CP, CS, V, I, DB>,
1332
- fromPersist: boolean,
1333
- ) {
1334
- if (connection.subscriptions.has(eventName)) {
1335
- this.#rLog.debug({
1336
- msg: "connection already has subscription",
1337
- eventName,
1338
- });
1339
- return;
1340
- }
1341
-
1342
- // Persist subscriptions & save immediately
1343
- //
1344
- // Don't update persistence if already restoring from persistence
1345
- if (!fromPersist) {
1346
- connection.__persist.subscriptions.push({ eventName: eventName });
1347
- this.saveState({ immediate: true });
1348
- }
1349
-
1350
- // Update subscriptions
1351
- connection.subscriptions.add(eventName);
1352
-
1353
- // Update subscription index
1354
- let subscribers = this.#subscriptionIndex.get(eventName);
1355
- if (!subscribers) {
1356
- subscribers = new Set();
1357
- this.#subscriptionIndex.set(eventName, subscribers);
1358
- }
1359
- subscribers.add(connection);
1360
- }
1361
-
1362
- #removeSubscription(
1363
- eventName: string,
1364
- connection: Conn<S, CP, CS, V, I, DB>,
1365
- fromRemoveConn: boolean,
1366
- ) {
1367
- if (!connection.subscriptions.has(eventName)) {
1368
- this.#rLog.warn({
1369
- msg: "connection does not have subscription",
1370
- eventName,
1371
- });
1372
- return;
1373
- }
1374
-
1375
- // Persist subscriptions & save immediately
1376
- //
1377
- // Don't update the connection itself if the connection is already being removed
1378
- if (!fromRemoveConn) {
1379
- connection.subscriptions.delete(eventName);
1380
-
1381
- const subIdx = connection.__persist.subscriptions.findIndex(
1382
- (s) => s.eventName === eventName,
1383
- );
1384
- if (subIdx !== -1) {
1385
- connection.__persist.subscriptions.splice(subIdx, 1);
1386
- } else {
1387
- this.#rLog.warn({
1388
- msg: "subscription does not exist with name",
1389
- eventName,
1390
- });
1391
- }
1392
-
1393
- this.saveState({ immediate: true });
1394
- }
1395
-
1396
- // Update scriptions index
1397
- const subscribers = this.#subscriptionIndex.get(eventName);
1398
- if (subscribers) {
1399
- subscribers.delete(connection);
1400
- if (subscribers.size === 0) {
1401
- this.#subscriptionIndex.delete(eventName);
1402
- }
1403
- }
1404
- }
1405
-
1406
- #assertReady(allowStoppingState: boolean = false) {
1407
- if (!this.#ready) throw new errors.InternalError("Actor not ready");
1408
- if (!allowStoppingState && this.#stopCalled)
1409
- throw new errors.InternalError("Actor is stopping");
1410
- }
1411
-
1412
- /**
1413
- * Check the liveness of all connections.
1414
- * Sets up a recurring check based on the configured interval.
1415
- */
1416
- #checkConnectionsLiveness() {
1417
- this.#rLog.debug({ msg: "checking connections liveness" });
1418
-
1419
- let connected = 0;
1420
- let reconnecting = 0;
1421
- let removed = 0;
1422
- for (const conn of this.#connections.values()) {
1423
- if (conn.__status === "connected") {
1424
- connected += 1;
1425
- this.#rLog.debug({
1426
- msg: "connection is alive",
1427
- connId: conn.id,
1428
- });
1429
- } else {
1430
- reconnecting += 1;
1431
-
1432
- const lastSeen = conn.__persist.lastSeen;
1433
- const sinceLastSeen = Date.now() - lastSeen;
1434
- if (
1435
- sinceLastSeen <
1436
- this.#config.options.connectionLivenessTimeout
1437
- ) {
1438
- this.#rLog.debug({
1439
- msg: "connection might be alive, will check later",
1440
- connId: conn.id,
1441
- lastSeen,
1442
- sinceLastSeen,
1443
- });
1444
- continue;
1445
- }
1446
-
1447
- // Connection is dead, remove it
1448
- this.#rLog.info({
1449
- msg: "connection is dead, removing",
1450
- connId: conn.id,
1451
- lastSeen,
1452
- });
1453
-
1454
- // Assume that the connection is dead here, no need to disconnect anything
1455
- removed += 1;
1456
- this.#removeConn(conn);
1457
- }
1458
- }
1459
-
1460
- this.#rLog.debug({
1461
- msg: "checked connection liveness",
1462
- total: connected + reconnecting,
1463
- connected,
1464
- reconnecting,
1465
- removed,
1466
- });
1467
- }
1468
-
1469
- /**
1470
- * Check if the actor is ready to handle requests.
1471
- */
1472
- isReady(): boolean {
1473
- return this.#ready;
1474
- }
1475
-
1476
- /**
1477
- * Execute an action call from a client.
1478
- *
1479
- * This method handles:
1480
- * 1. Validating the action name
1481
- * 2. Executing the action function
1482
- * 3. Processing the result through onBeforeActionResponse (if configured)
1483
- * 4. Handling timeouts and errors
1484
- * 5. Saving state changes
1485
- *
1486
- * @param ctx The action context
1487
- * @param actionName The name of the action being called
1488
- * @param args The arguments passed to the action
1489
- * @returns The result of the action call
1490
- * @throws {ActionNotFound} If the action doesn't exist
1491
- * @throws {ActionTimedOut} If the action times out
1492
- * @internal
1493
- */
1494
- async executeAction(
1495
- ctx: ActionContext<S, CP, CS, V, I, DB>,
1496
- actionName: string,
1497
- args: unknown[],
1498
- ): Promise<unknown> {
1499
- invariant(this.#ready, "executing action before ready");
1500
-
1501
- // Prevent calling private or reserved methods
1502
- if (!(actionName in this.#config.actions)) {
1503
- this.#rLog.warn({ msg: "action does not exist", actionName });
1504
- throw new errors.ActionNotFound(actionName);
1505
- }
1506
-
1507
- // Check if the method exists on this object
1508
- const actionFunction = this.#config.actions[actionName];
1509
- if (typeof actionFunction !== "function") {
1510
- this.#rLog.warn({
1511
- msg: "action is not a function",
1512
- actionName: actionName,
1513
- type: typeof actionFunction,
1514
- });
1515
- throw new errors.ActionNotFound(actionName);
1516
- }
1517
-
1518
- // TODO: pass abortable to the action to decide when to abort
1519
- // TODO: Manually call abortable for better error handling
1520
- // Call the function on this object with those arguments
1521
- try {
1522
- // Log when we start executing the action
1523
- this.#rLog.debug({
1524
- msg: "executing action",
1525
- actionName: actionName,
1526
- args,
1527
- });
1528
-
1529
- const outputOrPromise = actionFunction.call(
1530
- undefined,
1531
- ctx,
1532
- ...args,
1533
- );
1534
- let output: unknown;
1535
- if (outputOrPromise instanceof Promise) {
1536
- // Log that we're waiting for an async action
1537
- this.#rLog.debug({
1538
- msg: "awaiting async action",
1539
- actionName: actionName,
1540
- });
1541
-
1542
- output = await deadline(
1543
- outputOrPromise,
1544
- this.#config.options.actionTimeout,
1545
- );
1546
-
1547
- // Log that async action completed
1548
- this.#rLog.debug({
1549
- msg: "async action completed",
1550
- actionName: actionName,
1551
- });
1552
- } else {
1553
- output = outputOrPromise;
1554
- }
1555
-
1556
- // Process the output through onBeforeActionResponse if configured
1557
- if (this.#config.onBeforeActionResponse) {
1558
- try {
1559
- const processedOutput = this.#config.onBeforeActionResponse(
1560
- this.actorContext,
1561
- actionName,
1562
- args,
1563
- output,
1564
- );
1565
- if (processedOutput instanceof Promise) {
1566
- this.#rLog.debug({
1567
- msg: "awaiting onBeforeActionResponse",
1568
- actionName: actionName,
1569
- });
1570
- output = await processedOutput;
1571
- this.#rLog.debug({
1572
- msg: "onBeforeActionResponse completed",
1573
- actionName: actionName,
1574
- });
1575
- } else {
1576
- output = processedOutput;
1577
- }
1578
- } catch (error) {
1579
- this.#rLog.error({
1580
- msg: "error in `onBeforeActionResponse`",
1581
- error: stringifyError(error),
1582
- });
1583
- }
1584
- }
1585
-
1586
- // Log the output before returning
1587
- this.#rLog.debug({
1588
- msg: "action completed",
1589
- actionName: actionName,
1590
- outputType: typeof output,
1591
- isPromise: output instanceof Promise,
1592
- });
1593
-
1594
- // This output *might* reference a part of the state (using onChange), but
1595
- // that's OK since this value always gets serialized and sent over the
1596
- // network.
1597
- return output;
1598
- } catch (error) {
1599
- if (error instanceof DeadlineError) {
1600
- throw new errors.ActionTimedOut();
1601
- }
1602
- this.#rLog.error({
1603
- msg: "action error",
1604
- actionName: actionName,
1605
- error: stringifyError(error),
1606
- });
1607
- throw error;
1608
- } finally {
1609
- this.#savePersistThrottled();
1610
- }
1611
- }
1612
-
1613
- /**
1614
- * Returns a list of action methods available on this actor.
1615
- */
1616
- get actions(): string[] {
1617
- return Object.keys(this.#config.actions);
1618
- }
1619
-
1620
- /**
1621
- * Handles raw HTTP requests to the actor.
1622
- */
1623
- async handleFetch(
1624
- request: Request,
1625
- opts: Record<never, never>,
1626
- ): Promise<Response> {
1627
- this.#assertReady();
1628
-
1629
- if (!this.#config.onFetch) {
1630
- throw new errors.FetchHandlerNotDefined();
1631
- }
1632
-
1633
- try {
1634
- const response = await this.#config.onFetch(
1635
- this.actorContext,
1636
- request,
1637
- opts,
1638
- );
1639
- if (!response) {
1640
- throw new errors.InvalidFetchResponse();
1641
- }
1642
- return response;
1643
- } catch (error) {
1644
- this.#rLog.error({
1645
- msg: "onFetch error",
1646
- error: stringifyError(error),
1647
- });
1648
- throw error;
1649
- } finally {
1650
- this.#savePersistThrottled();
1651
- }
1652
- }
1653
-
1654
- /**
1655
- * Handles raw WebSocket connections to the actor.
1656
- */
1657
- async handleWebSocket(
1658
- websocket: UniversalWebSocket,
1659
- opts: { request: Request },
1660
- ): Promise<void> {
1661
- this.#assertReady();
1662
-
1663
- if (!this.#config.onWebSocket) {
1664
- throw new errors.InternalError("onWebSocket handler not defined");
1665
- }
1666
-
1667
- try {
1668
- // Set up state tracking to detect changes during WebSocket handling
1669
- const stateBeforeHandler = this.#persistChanged;
1670
-
1671
- // Track active websocket until it fully closes
1672
- this.#activeRawWebSockets.add(websocket);
1673
- this.#resetSleepTimer();
1674
-
1675
- // Track hibernatable WebSockets
1676
- let rivetRequestId: ArrayBuffer | undefined;
1677
- let persistedHibernatableWebSocket:
1678
- | PersistedHibernatableWebSocket
1679
- | undefined;
1680
-
1681
- const onSocketOpened = (event: any) => {
1682
- rivetRequestId = event?.rivetRequestId;
1683
-
1684
- // Find hibernatable WS
1685
- if (rivetRequestId) {
1686
- const rivetRequestIdLocal = rivetRequestId;
1687
- persistedHibernatableWebSocket =
1688
- this.#persist.hibernatableWebSocket.find((ws) =>
1689
- arrayBuffersEqual(
1690
- ws.requestId,
1691
- rivetRequestIdLocal,
1692
- ),
1693
- );
1694
-
1695
- if (persistedHibernatableWebSocket) {
1696
- persistedHibernatableWebSocket.lastSeenTimestamp =
1697
- BigInt(Date.now());
1698
- }
1699
- }
1700
-
1701
- this.#rLog.debug({
1702
- msg: "actor instance onSocketOpened",
1703
- rivetRequestId,
1704
- isHibernatable: !!persistedHibernatableWebSocket,
1705
- hibernationMsgIndex:
1706
- persistedHibernatableWebSocket?.msgIndex,
1707
- });
1708
- };
1709
-
1710
- const onSocketMessage = (event: any) => {
1711
- // Update state of hibernatable WS
1712
- if (persistedHibernatableWebSocket) {
1713
- persistedHibernatableWebSocket.lastSeenTimestamp = BigInt(
1714
- Date.now(),
1715
- );
1716
- persistedHibernatableWebSocket.msgIndex = BigInt(
1717
- event.rivetMessageIndex,
1718
- );
1719
- }
1720
-
1721
- this.#rLog.debug({
1722
- msg: "actor instance onSocketMessage",
1723
- rivetRequestId,
1724
- isHibernatable: !!persistedHibernatableWebSocket,
1725
- hibernationMsgIndex:
1726
- persistedHibernatableWebSocket?.msgIndex,
1727
- });
1728
- };
1729
-
1730
- const onSocketClosed = (_event: any) => {
1731
- // Remove hibernatable WS
1732
- if (rivetRequestId) {
1733
- const rivetRequestIdLocal = rivetRequestId;
1734
- const wsIndex =
1735
- this.#persist.hibernatableWebSocket.findIndex((ws) =>
1736
- arrayBuffersEqual(
1737
- ws.requestId,
1738
- rivetRequestIdLocal,
1739
- ),
1740
- );
1741
-
1742
- const removed = this.#persist.hibernatableWebSocket.splice(
1743
- wsIndex,
1744
- 1,
1745
- );
1746
- if (removed.length > 0) {
1747
- this.#rLog.debug({
1748
- msg: "removed hibernatable websocket",
1749
- rivetRequestId,
1750
- hibernationMsgIndex:
1751
- persistedHibernatableWebSocket?.msgIndex,
1752
- });
1753
- } else {
1754
- this.#rLog.warn({
1755
- msg: "could not find hibernatable websocket to remove",
1756
- rivetRequestId,
1757
- hibernationMsgIndex:
1758
- persistedHibernatableWebSocket?.msgIndex,
1759
- });
1760
- }
1761
- }
1762
-
1763
- this.#rLog.debug({
1764
- msg: "actor instance onSocketMessage",
1765
- rivetRequestId,
1766
- isHibernatable: !!persistedHibernatableWebSocket,
1767
- hibernatableWebSocketCount:
1768
- this.#persist.hibernatableWebSocket.length,
1769
- });
1770
-
1771
- // Remove listener and socket from tracking
1772
- try {
1773
- websocket.removeEventListener("open", onSocketOpened);
1774
- websocket.removeEventListener("message", onSocketMessage);
1775
- websocket.removeEventListener("close", onSocketClosed);
1776
- websocket.removeEventListener("error", onSocketClosed);
1777
- } catch {}
1778
- this.#activeRawWebSockets.delete(websocket);
1779
- this.#resetSleepTimer();
1780
- };
1781
-
1782
- try {
1783
- websocket.addEventListener("open", onSocketOpened);
1784
- websocket.addEventListener("message", onSocketMessage);
1785
- websocket.addEventListener("close", onSocketClosed);
1786
- websocket.addEventListener("error", onSocketClosed);
1787
- } catch {}
1788
-
1789
- // Handle WebSocket
1790
- await this.#config.onWebSocket(this.actorContext, websocket, opts);
1791
-
1792
- // If state changed during the handler, save it
1793
- if (this.#persistChanged && !stateBeforeHandler) {
1794
- await this.saveState({ immediate: true });
1795
- }
1796
- } catch (error) {
1797
- this.#rLog.error({
1798
- msg: "onWebSocket error",
1799
- error: stringifyError(error),
1800
- });
1801
- throw error;
1802
- } finally {
1803
- this.#savePersistThrottled();
1804
- }
1805
- }
1806
-
1807
- // MARK: Lifecycle hooks
1808
-
1809
- // MARK: Exposed methods
1810
- get log(): Logger {
1811
- invariant(this.#log, "log not configured");
1812
- return this.#log;
1813
- }
1814
-
1815
- get rLog(): Logger {
1816
- invariant(this.#rLog, "log not configured");
1817
- return this.#rLog;
1818
- }
1819
-
1820
- /**
1821
- * Gets the name.
1822
- */
1823
- get name(): string {
1824
- return this.#name;
1825
- }
1826
-
1827
- /**
1828
- * Gets the key.
1829
- */
1830
- get key(): ActorKey {
1831
- return this.#key;
1832
- }
1833
-
1834
- /**
1835
- * Gets the region.
1836
- */
1837
- get region(): string {
1838
- return this.#region;
1839
- }
1840
-
1841
- /**
1842
- * Gets the scheduler.
1843
- */
1844
- get schedule(): Schedule {
1845
- return this.#schedule;
1846
- }
1847
-
1848
- /**
1849
- * Gets the map of connections.
1850
- */
1851
- get conns(): Map<ConnId, Conn<S, CP, CS, V, I, DB>> {
1852
- return this.#connections;
1853
- }
1854
-
1855
- /**
1856
- * Gets the current state.
1857
- *
1858
- * Changing properties of this value will automatically be persisted.
1859
- */
1860
- get state(): S {
1861
- this.#validateStateEnabled();
1862
- return this.#persist.state;
1863
- }
1864
-
1865
- /**
1866
- * Gets the database.
1867
- * @experimental
1868
- * @throws {DatabaseNotEnabled} If the database is not enabled.
1869
- */
1870
- get db(): InferDatabaseClient<DB> {
1871
- if (!this.#db) {
1872
- throw new errors.DatabaseNotEnabled();
1873
- }
1874
- return this.#db;
1875
- }
1876
-
1877
- /**
1878
- * Sets the current state.
1879
- *
1880
- * This property will automatically be persisted.
1881
- */
1882
- set state(value: S) {
1883
- this.#validateStateEnabled();
1884
- this.#persist.state = value;
1885
- }
1886
-
1887
- get vars(): V {
1888
- this.#validateVarsEnabled();
1889
- invariant(this.#vars !== undefined, "vars not enabled");
1890
- return this.#vars;
1891
- }
1892
-
1893
- /**
1894
- * Broadcasts an event to all connected clients.
1895
- * @param name - The name of the event.
1896
- * @param args - The arguments to send with the event.
1897
- */
1898
- _broadcast<Args extends Array<unknown>>(name: string, ...args: Args) {
1899
- this.#assertReady();
1900
-
1901
- this.inspector.emitter.emit("eventFired", {
1902
- type: "broadcast",
1903
- eventName: name,
1904
- args,
1905
- });
1906
-
1907
- // Send to all connected clients
1908
- const subscriptions = this.#subscriptionIndex.get(name);
1909
- if (!subscriptions) return;
1910
-
1911
- const toClientSerializer = new CachedSerializer<protocol.ToClient>(
1912
- {
1913
- body: {
1914
- tag: "Event",
1915
- val: {
1916
- name,
1917
- args: bufferToArrayBuffer(cbor.encode(args)),
1918
- },
1919
- },
1920
- },
1921
- TO_CLIENT_VERSIONED,
1922
- );
1923
-
1924
- // Send message to clients
1925
- for (const connection of subscriptions) {
1926
- connection._sendMessage(toClientSerializer);
1927
- }
1928
- }
1929
-
1930
- /**
1931
- * Prevents the actor from sleeping until promise is complete.
1932
- *
1933
- * This allows the actor runtime to ensure that a promise completes while
1934
- * returning from an action request early.
1935
- *
1936
- * @param promise - The promise to run in the background.
1937
- */
1938
- _waitUntil(promise: Promise<void>) {
1939
- this.#assertReady();
1940
-
1941
- // TODO: Should we force save the state?
1942
- // Add logging to promise and make it non-failable
1943
- const nonfailablePromise = promise
1944
- .then(() => {
1945
- this.#rLog.debug({ msg: "wait until promise complete" });
1946
- })
1947
- .catch((error) => {
1948
- this.#rLog.error({
1949
- msg: "wait until promise failed",
1950
- error: stringifyError(error),
1951
- });
1952
- });
1953
- this.#backgroundPromises.push(nonfailablePromise);
1954
- }
1955
-
1956
- /**
1957
- * Forces the state to get saved.
1958
- *
1959
- * This is helpful if running a long task that may fail later or when
1960
- * running a background job that updates the state.
1961
- *
1962
- * @param opts - Options for saving the state.
1963
- */
1964
- async saveState(opts: SaveStateOptions) {
1965
- this.#assertReady(opts.allowStoppingState);
1966
-
1967
- this.#rLog.debug({
1968
- msg: "saveState called",
1969
- persistChanged: this.#persistChanged,
1970
- allowStoppingState: opts.allowStoppingState,
1971
- immediate: opts.immediate,
1972
- });
1973
-
1974
- if (this.#persistChanged) {
1975
- if (opts.immediate) {
1976
- // Save immediately
1977
- await this.#savePersistInner();
1978
- } else {
1979
- // Create callback
1980
- if (!this.#onPersistSavedPromise) {
1981
- this.#onPersistSavedPromise = promiseWithResolvers();
1982
- }
1983
-
1984
- // Save state throttled
1985
- this.#savePersistThrottled();
1986
-
1987
- // Wait for save
1988
- await this.#onPersistSavedPromise.promise;
1989
- }
1990
- }
1991
- }
1992
-
1993
- /**
1994
- * Called by router middleware when an HTTP request begins.
1995
- */
1996
- __beginHonoHttpRequest() {
1997
- this.#activeHonoHttpRequests++;
1998
- this.#resetSleepTimer();
1999
- }
2000
-
2001
- /**
2002
- * Called by router middleware when an HTTP request ends.
2003
- */
2004
- __endHonoHttpRequest() {
2005
- this.#activeHonoHttpRequests--;
2006
- if (this.#activeHonoHttpRequests < 0) {
2007
- this.#activeHonoHttpRequests = 0;
2008
- this.#rLog.warn({
2009
- msg: "active hono requests went below 0, this is a RivetKit bug",
2010
- ...EXTRA_ERROR_LOG,
2011
- });
2012
- }
2013
- this.#resetSleepTimer();
2014
- }
2015
-
2016
- // MARK: Sleep
2017
- /**
2018
- * Reset timer from the last actor interaction that allows it to be put to sleep.
2019
- *
2020
- * This should be called any time a sleep-related event happens:
2021
- * - Connection opens (will clear timer)
2022
- * - Connection closes (will schedule timer if there are no open connections)
2023
- * - Alarm triggers (will reset timer)
2024
- *
2025
- * We don't need to call this on events like individual action calls, since there will always be a connection open for these.
2026
- **/
2027
- #resetSleepTimer() {
2028
- if (this.#config.options.noSleep || !this.#sleepingSupported) return;
2029
-
2030
- // Don't sleep if already stopping
2031
- if (this.#stopCalled) return;
2032
-
2033
- const canSleep = this.#canSleep();
2034
-
2035
- this.#rLog.debug({
2036
- msg: "resetting sleep timer",
2037
- canSleep: CanSleep[canSleep],
2038
- existingTimeout: !!this.#sleepTimeout,
2039
- timeout: this.#config.options.sleepTimeout,
2040
- });
2041
-
2042
- if (this.#sleepTimeout) {
2043
- clearTimeout(this.#sleepTimeout);
2044
- this.#sleepTimeout = undefined;
2045
- }
2046
-
2047
- // Don't set a new timer if already sleeping
2048
- if (this.#sleepCalled) return;
2049
-
2050
- if (canSleep === CanSleep.Yes) {
2051
- this.#sleepTimeout = setTimeout(() => {
2052
- this._startSleep();
2053
- }, this.#config.options.sleepTimeout);
2054
- }
2055
- }
2056
-
2057
- /** If this actor can be put in a sleeping state. */
2058
- #canSleep(): CanSleep {
2059
- if (!this.#ready) return CanSleep.NotReady;
2060
-
2061
- // Do not sleep if Hono HTTP requests are in-flight
2062
- if (this.#activeHonoHttpRequests > 0)
2063
- return CanSleep.ActiveHonoHttpRequests;
2064
-
2065
- // TODO: When WS hibernation is ready, update this to only count non-hibernatable websockets
2066
- // Do not sleep if there are raw websockets open
2067
- if (this.#activeRawWebSockets.size > 0)
2068
- return CanSleep.ActiveRawWebSockets;
2069
-
2070
- // Check for active conns. This will also cover active actions, since all actions have a connection.
2071
- for (const conn of this.#connections.values()) {
2072
- // TODO: Enable this when hibernation is implemented. We're waiting on support for Guard to not auto-wake the actor if it sleeps.
2073
- // if (conn.status === "connected" && !conn.isHibernatable)
2074
- // return false;
2075
-
2076
- if (conn.status === "connected") return CanSleep.ActiveConns;
2077
- }
2078
-
2079
- return CanSleep.Yes;
2080
- }
2081
-
2082
- /**
2083
- * Puts an actor to sleep. This should just start the sleep sequence, most shutdown logic should be in _stop (which is called by the ActorDriver when sleeping).
2084
- *
2085
- * For the engine, this will:
2086
- * 1. Publish EventActorIntent with ActorIntentSleep (via driver.startSleep)
2087
- * 2. Engine runner will wait for CommandStopActor
2088
- * 3. Engine runner will call _onStop and wait for it to finish
2089
- * 4. Engine runner will publish EventActorStateUpdate with ActorStateSTop
2090
- **/
2091
- _startSleep() {
2092
- if (this.#stopCalled) {
2093
- this.#rLog.debug({
2094
- msg: "cannot call _startSleep if actor already stopping",
2095
- });
2096
- return;
2097
- }
2098
-
2099
- // IMPORTANT: #sleepCalled should have no effect on the actor's
2100
- // behavior aside from preventing calling _startSleep twice. Wait for
2101
- // `_onStop` before putting in a stopping state.
2102
- if (this.#sleepCalled) {
2103
- this.#rLog.warn({
2104
- msg: "cannot call _startSleep twice, actor already sleeping",
2105
- });
2106
- return;
2107
- }
2108
- this.#sleepCalled = true;
2109
-
2110
- // NOTE: Publishes ActorIntentSleep
2111
- const sleep = this.#actorDriver.startSleep?.bind(
2112
- this.#actorDriver,
2113
- this.#actorId,
2114
- );
2115
- invariant(this.#sleepingSupported, "sleeping not supported");
2116
- invariant(sleep, "no sleep on driver");
2117
-
2118
- this.#rLog.info({ msg: "actor sleeping" });
2119
-
2120
- // Schedule sleep to happen on the next tick. This allows for any action that calls _sleep to complete.
2121
- setImmediate(() => {
2122
- // The actor driver should call stop when ready to stop
2123
- //
2124
- // This will call _stop once Pegboard responds with the new status
2125
- sleep();
2126
- });
2127
- }
2128
-
2129
- // MARK: Stop
2130
- /**
2131
- * For the engine:
2132
- * 1. Engine runner receives CommandStopActor
2133
- * 2. Engine runner calls _onStop and waits for it to finish
2134
- * 3. Engine runner publishes EventActorStateUpdate with ActorStateSTop
2135
- */
2136
- async _onStop() {
2137
- if (this.#stopCalled) {
2138
- this.#rLog.warn({ msg: "already stopping actor" });
2139
- return;
2140
- }
2141
- this.#stopCalled = true;
2142
-
2143
- this.#rLog.info({ msg: "actor stopping" });
2144
-
2145
- if (this.#sleepTimeout) {
2146
- clearTimeout(this.#sleepTimeout);
2147
- this.#sleepTimeout = undefined;
2148
- }
2149
-
2150
- // Abort any listeners waiting for shutdown
2151
- try {
2152
- this.#abortController.abort();
2153
- } catch {}
2154
-
2155
- // Call onStop lifecycle hook if defined
2156
- if (this.#config.onStop) {
2157
- try {
2158
- this.#rLog.debug({ msg: "calling onStop" });
2159
- const result = this.#config.onStop(this.actorContext);
2160
- if (result instanceof Promise) {
2161
- await deadline(result, this.#config.options.onStopTimeout);
2162
- }
2163
- this.#rLog.debug({ msg: "onStop completed" });
2164
- } catch (error) {
2165
- if (error instanceof DeadlineError) {
2166
- this.#rLog.error({ msg: "onStop timed out" });
2167
- } else {
2168
- this.#rLog.error({
2169
- msg: "error in onStop",
2170
- error: stringifyError(error),
2171
- });
2172
- }
2173
- }
2174
- }
2175
-
2176
- const promises: Promise<unknown>[] = [];
2177
-
2178
- // Disconnect existing non-hibernatable connections
2179
- for (const connection of this.#connections.values()) {
2180
- if (!connection.isHibernatable) {
2181
- this.#rLog.debug({
2182
- msg: "disconnecting non-hibernatable connection on actor stop",
2183
- connId: connection.id,
2184
- });
2185
- promises.push(connection.disconnect());
2186
- }
2187
-
2188
- // TODO: Figure out how to abort HTTP requests on shutdown. This
2189
- // might already be handled by the engine runner tunnel shutdown.
2190
- }
2191
-
2192
- // Wait for any background tasks to finish, with timeout
2193
- await this.#waitBackgroundPromises(
2194
- this.#config.options.waitUntilTimeout,
2195
- );
2196
-
2197
- // Clear timeouts
2198
- if (this.#pendingSaveTimeout) clearTimeout(this.#pendingSaveTimeout);
2199
- if (this.#checkConnLivenessInterval)
2200
- clearInterval(this.#checkConnLivenessInterval);
2201
-
2202
- // Write state
2203
- await this.saveState({ immediate: true, allowStoppingState: true });
2204
-
2205
- // Await all `close` event listeners with 1.5 second timeout
2206
- const res = Promise.race([
2207
- Promise.all(promises).then(() => false),
2208
- new Promise<boolean>((res) =>
2209
- globalThis.setTimeout(() => res(true), 1500),
2210
- ),
2211
- ]);
2212
-
2213
- if (await res) {
2214
- this.#rLog.warn({
2215
- msg: "timed out waiting for connections to close, shutting down anyway",
2216
- });
2217
- }
2218
-
2219
- // Wait for queues to finish
2220
- if (this.#persistWriteQueue.runningDrainLoop)
2221
- await this.#persistWriteQueue.runningDrainLoop;
2222
- if (this.#alarmWriteQueue.runningDrainLoop)
2223
- await this.#alarmWriteQueue.runningDrainLoop;
2224
- }
2225
-
2226
- /** Abort signal that fires when the actor is stopping. */
2227
- get abortSignal(): AbortSignal {
2228
- return this.#abortController.signal;
2229
- }
2230
-
2231
- /** Wait for background waitUntil promises with a timeout. */
2232
- async #waitBackgroundPromises(timeoutMs: number) {
2233
- const pending = this.#backgroundPromises;
2234
- if (pending.length === 0) {
2235
- this.#rLog.debug({ msg: "no background promises" });
2236
- return;
2237
- }
2238
-
2239
- // Race promises with timeout to determine if pending promises settled fast enough
2240
- const timedOut = await Promise.race([
2241
- Promise.allSettled(pending).then(() => false),
2242
- new Promise<true>((resolve) =>
2243
- setTimeout(() => resolve(true), timeoutMs),
2244
- ),
2245
- ]);
2246
-
2247
- if (timedOut) {
2248
- this.#rLog.error({
2249
- msg: "timed out waiting for background tasks, background promises may have leaked",
2250
- count: pending.length,
2251
- timeoutMs,
2252
- });
2253
- } else {
2254
- this.#rLog.debug({ msg: "background promises finished" });
2255
- }
2256
- }
2257
-
2258
- // MARK: BARE Conversion Helpers
2259
- #convertToBarePersisted(
2260
- persist: PersistedActor<S, CP, CS, I>,
2261
- ): bareSchema.PersistedActor {
2262
- return {
2263
- input:
2264
- persist.input !== undefined
2265
- ? bufferToArrayBuffer(cbor.encode(persist.input))
2266
- : null,
2267
- hasInitialized: persist.hasInitiated,
2268
- state: bufferToArrayBuffer(cbor.encode(persist.state)),
2269
- connections: persist.connections.map((conn) => ({
2270
- id: conn.connId,
2271
- token: conn.token,
2272
- parameters: bufferToArrayBuffer(cbor.encode(conn.params || {})),
2273
- state: bufferToArrayBuffer(cbor.encode(conn.state || {})),
2274
- subscriptions: conn.subscriptions.map((sub) => ({
2275
- eventName: sub.eventName,
2276
- })),
2277
- lastSeen: BigInt(conn.lastSeen),
2278
- hibernatableRequestId: conn.hibernatableRequestId ?? null,
2279
- })),
2280
- scheduledEvents: persist.scheduledEvents.map((event) => ({
2281
- eventId: event.eventId,
2282
- timestamp: BigInt(event.timestamp),
2283
- kind: {
2284
- tag: "GenericPersistedScheduleEvent" as const,
2285
- val: {
2286
- action: event.kind.generic.actionName,
2287
- args: event.kind.generic.args ?? null,
2288
- },
2289
- },
2290
- })),
2291
- hibernatableWebSocket: persist.hibernatableWebSocket.map((ws) => ({
2292
- requestId: ws.requestId,
2293
- lastSeenTimestamp: ws.lastSeenTimestamp,
2294
- msgIndex: ws.msgIndex,
2295
- })),
2296
- };
2297
- }
2298
-
2299
- #convertFromBarePersisted(
2300
- bareData: bareSchema.PersistedActor,
2301
- ): PersistedActor<S, CP, CS, I> {
2302
- return {
2303
- input: bareData.input
2304
- ? cbor.decode(new Uint8Array(bareData.input))
2305
- : undefined,
2306
- hasInitiated: bareData.hasInitialized,
2307
- state: cbor.decode(new Uint8Array(bareData.state)),
2308
- connections: bareData.connections.map((conn) => ({
2309
- connId: conn.id,
2310
- token: conn.token,
2311
- params: cbor.decode(new Uint8Array(conn.parameters)),
2312
- state: cbor.decode(new Uint8Array(conn.state)),
2313
- subscriptions: conn.subscriptions.map((sub) => ({
2314
- eventName: sub.eventName,
2315
- })),
2316
- lastSeen: Number(conn.lastSeen),
2317
- hibernatableRequestId: conn.hibernatableRequestId ?? undefined,
2318
- })),
2319
- scheduledEvents: bareData.scheduledEvents.map((event) => ({
2320
- eventId: event.eventId,
2321
- timestamp: Number(event.timestamp),
2322
- kind: {
2323
- generic: {
2324
- actionName: event.kind.val.action,
2325
- args: event.kind.val.args,
2326
- },
2327
- },
2328
- })),
2329
- hibernatableWebSocket: bareData.hibernatableWebSocket.map((ws) => ({
2330
- requestId: ws.requestId,
2331
- lastSeenTimestamp: ws.lastSeenTimestamp,
2332
- msgIndex: ws.msgIndex,
2333
- })),
2334
- };
2335
- }
2336
- }