rivetkit 2.0.2 → 2.0.3

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 (253) hide show
  1. package/dist/schemas/actor-persist/v1.ts +228 -0
  2. package/dist/schemas/client-protocol/v1.ts +429 -0
  3. package/dist/schemas/file-system-driver/v1.ts +102 -0
  4. package/dist/tsup/actor/errors.cjs +69 -0
  5. package/dist/tsup/actor/errors.cjs.map +1 -0
  6. package/dist/tsup/actor/errors.d.cts +143 -0
  7. package/dist/tsup/actor/errors.d.ts +143 -0
  8. package/dist/tsup/actor/errors.js +69 -0
  9. package/dist/tsup/actor/errors.js.map +1 -0
  10. package/dist/tsup/chunk-2CRLFV6Z.cjs +202 -0
  11. package/dist/tsup/chunk-2CRLFV6Z.cjs.map +1 -0
  12. package/dist/tsup/chunk-3H7O2A7I.js +525 -0
  13. package/dist/tsup/chunk-3H7O2A7I.js.map +1 -0
  14. package/dist/tsup/chunk-42I3OZ3Q.js +15 -0
  15. package/dist/tsup/chunk-42I3OZ3Q.js.map +1 -0
  16. package/dist/tsup/chunk-4NSUQZ2H.js +1790 -0
  17. package/dist/tsup/chunk-4NSUQZ2H.js.map +1 -0
  18. package/dist/tsup/chunk-6PDXBYI5.js +132 -0
  19. package/dist/tsup/chunk-6PDXBYI5.js.map +1 -0
  20. package/dist/tsup/chunk-6WKQDDUD.cjs +1790 -0
  21. package/dist/tsup/chunk-6WKQDDUD.cjs.map +1 -0
  22. package/dist/tsup/chunk-CTBOSFUH.cjs +116 -0
  23. package/dist/tsup/chunk-CTBOSFUH.cjs.map +1 -0
  24. package/dist/tsup/chunk-EGVZZFE2.js +2857 -0
  25. package/dist/tsup/chunk-EGVZZFE2.js.map +1 -0
  26. package/dist/tsup/chunk-FCCPJNMA.cjs +132 -0
  27. package/dist/tsup/chunk-FCCPJNMA.cjs.map +1 -0
  28. package/dist/tsup/chunk-FLMTTN27.js +244 -0
  29. package/dist/tsup/chunk-FLMTTN27.js.map +1 -0
  30. package/dist/tsup/chunk-GIR3AFFI.cjs +315 -0
  31. package/dist/tsup/chunk-GIR3AFFI.cjs.map +1 -0
  32. package/dist/tsup/chunk-INGJP237.js +315 -0
  33. package/dist/tsup/chunk-INGJP237.js.map +1 -0
  34. package/dist/tsup/chunk-KJCJLKRM.js +116 -0
  35. package/dist/tsup/chunk-KJCJLKRM.js.map +1 -0
  36. package/dist/tsup/chunk-KUPQZYUQ.cjs +15 -0
  37. package/dist/tsup/chunk-KUPQZYUQ.cjs.map +1 -0
  38. package/dist/tsup/chunk-O2MBYIXO.cjs +2857 -0
  39. package/dist/tsup/chunk-O2MBYIXO.cjs.map +1 -0
  40. package/dist/tsup/chunk-OGAPU3UG.cjs +525 -0
  41. package/dist/tsup/chunk-OGAPU3UG.cjs.map +1 -0
  42. package/dist/tsup/chunk-OV6AYD4S.js +4406 -0
  43. package/dist/tsup/chunk-OV6AYD4S.js.map +1 -0
  44. package/dist/tsup/chunk-PO4VLDWA.js +47 -0
  45. package/dist/tsup/chunk-PO4VLDWA.js.map +1 -0
  46. package/dist/tsup/chunk-R2OPSKIV.cjs +244 -0
  47. package/dist/tsup/chunk-R2OPSKIV.cjs.map +1 -0
  48. package/dist/tsup/chunk-TZJKSBUQ.cjs +47 -0
  49. package/dist/tsup/chunk-TZJKSBUQ.cjs.map +1 -0
  50. package/dist/tsup/chunk-UBUC5C3G.cjs +189 -0
  51. package/dist/tsup/chunk-UBUC5C3G.cjs.map +1 -0
  52. package/dist/tsup/chunk-UIM22YJL.cjs +4406 -0
  53. package/dist/tsup/chunk-UIM22YJL.cjs.map +1 -0
  54. package/dist/tsup/chunk-URVFQMYI.cjs +230 -0
  55. package/dist/tsup/chunk-URVFQMYI.cjs.map +1 -0
  56. package/dist/tsup/chunk-UVUPOS46.js +230 -0
  57. package/dist/tsup/chunk-UVUPOS46.js.map +1 -0
  58. package/dist/tsup/chunk-VRRHBNJC.js +189 -0
  59. package/dist/tsup/chunk-VRRHBNJC.js.map +1 -0
  60. package/dist/tsup/chunk-XFSS33EQ.js +202 -0
  61. package/dist/tsup/chunk-XFSS33EQ.js.map +1 -0
  62. package/dist/tsup/client/mod.cjs +32 -0
  63. package/dist/tsup/client/mod.cjs.map +1 -0
  64. package/dist/tsup/client/mod.d.cts +26 -0
  65. package/dist/tsup/client/mod.d.ts +26 -0
  66. package/dist/tsup/client/mod.js +32 -0
  67. package/dist/tsup/client/mod.js.map +1 -0
  68. package/dist/tsup/common/log.cjs +13 -0
  69. package/dist/tsup/common/log.cjs.map +1 -0
  70. package/dist/tsup/common/log.d.cts +20 -0
  71. package/dist/tsup/common/log.d.ts +20 -0
  72. package/dist/tsup/common/log.js +13 -0
  73. package/dist/tsup/common/log.js.map +1 -0
  74. package/dist/tsup/common/websocket.cjs +10 -0
  75. package/dist/tsup/common/websocket.cjs.map +1 -0
  76. package/dist/tsup/common/websocket.d.cts +3 -0
  77. package/dist/tsup/common/websocket.d.ts +3 -0
  78. package/dist/tsup/common/websocket.js +10 -0
  79. package/dist/tsup/common/websocket.js.map +1 -0
  80. package/dist/tsup/common-CpqORuCq.d.cts +218 -0
  81. package/dist/tsup/common-CpqORuCq.d.ts +218 -0
  82. package/dist/tsup/connection-BR_Ve4ku.d.cts +2117 -0
  83. package/dist/tsup/connection-BwUMoe6n.d.ts +2117 -0
  84. package/dist/tsup/driver-helpers/mod.cjs +33 -0
  85. package/dist/tsup/driver-helpers/mod.cjs.map +1 -0
  86. package/dist/tsup/driver-helpers/mod.d.cts +18 -0
  87. package/dist/tsup/driver-helpers/mod.d.ts +18 -0
  88. package/dist/tsup/driver-helpers/mod.js +33 -0
  89. package/dist/tsup/driver-helpers/mod.js.map +1 -0
  90. package/dist/tsup/driver-test-suite/mod.cjs +4619 -0
  91. package/dist/tsup/driver-test-suite/mod.cjs.map +1 -0
  92. package/dist/tsup/driver-test-suite/mod.d.cts +57 -0
  93. package/dist/tsup/driver-test-suite/mod.d.ts +57 -0
  94. package/dist/tsup/driver-test-suite/mod.js +4619 -0
  95. package/dist/tsup/driver-test-suite/mod.js.map +1 -0
  96. package/dist/tsup/inspector/mod.cjs +53 -0
  97. package/dist/tsup/inspector/mod.cjs.map +1 -0
  98. package/dist/tsup/inspector/mod.d.cts +408 -0
  99. package/dist/tsup/inspector/mod.d.ts +408 -0
  100. package/dist/tsup/inspector/mod.js +53 -0
  101. package/dist/tsup/inspector/mod.js.map +1 -0
  102. package/dist/tsup/mod.cjs +73 -0
  103. package/dist/tsup/mod.cjs.map +1 -0
  104. package/dist/tsup/mod.d.cts +100 -0
  105. package/dist/tsup/mod.d.ts +100 -0
  106. package/dist/tsup/mod.js +73 -0
  107. package/dist/tsup/mod.js.map +1 -0
  108. package/dist/tsup/router-endpoints-AYkXG8Tl.d.cts +66 -0
  109. package/dist/tsup/router-endpoints-DAbqVFx2.d.ts +66 -0
  110. package/dist/tsup/test/mod.cjs +21 -0
  111. package/dist/tsup/test/mod.cjs.map +1 -0
  112. package/dist/tsup/test/mod.d.cts +27 -0
  113. package/dist/tsup/test/mod.d.ts +27 -0
  114. package/dist/tsup/test/mod.js +21 -0
  115. package/dist/tsup/test/mod.js.map +1 -0
  116. package/dist/tsup/utils-CT0cv4jd.d.cts +17 -0
  117. package/dist/tsup/utils-CT0cv4jd.d.ts +17 -0
  118. package/dist/tsup/utils.cjs +26 -0
  119. package/dist/tsup/utils.cjs.map +1 -0
  120. package/dist/tsup/utils.d.cts +36 -0
  121. package/dist/tsup/utils.d.ts +36 -0
  122. package/dist/tsup/utils.js +26 -0
  123. package/dist/tsup/utils.js.map +1 -0
  124. package/package.json +208 -5
  125. package/src/actor/action.ts +182 -0
  126. package/src/actor/config.ts +765 -0
  127. package/src/actor/connection.ts +260 -0
  128. package/src/actor/context.ts +171 -0
  129. package/src/actor/database.ts +23 -0
  130. package/src/actor/definition.ts +86 -0
  131. package/src/actor/driver.ts +84 -0
  132. package/src/actor/errors.ts +360 -0
  133. package/src/actor/generic-conn-driver.ts +234 -0
  134. package/src/actor/instance.ts +1800 -0
  135. package/src/actor/log.ts +15 -0
  136. package/src/actor/mod.ts +113 -0
  137. package/src/actor/persisted.ts +42 -0
  138. package/src/actor/protocol/old.ts +281 -0
  139. package/src/actor/protocol/serde.ts +131 -0
  140. package/src/actor/router-endpoints.ts +685 -0
  141. package/src/actor/router.ts +263 -0
  142. package/src/actor/schedule.ts +17 -0
  143. package/src/actor/unstable-react.ts +110 -0
  144. package/src/actor/utils.ts +98 -0
  145. package/src/client/actor-common.ts +30 -0
  146. package/src/client/actor-conn.ts +804 -0
  147. package/src/client/actor-handle.ts +208 -0
  148. package/src/client/client.ts +623 -0
  149. package/src/client/errors.ts +41 -0
  150. package/src/client/http-client-driver.ts +326 -0
  151. package/src/client/log.ts +7 -0
  152. package/src/client/mod.ts +56 -0
  153. package/src/client/raw-utils.ts +92 -0
  154. package/src/client/test.ts +44 -0
  155. package/src/client/utils.ts +150 -0
  156. package/src/common/eventsource-interface.ts +47 -0
  157. package/src/common/eventsource.ts +80 -0
  158. package/src/common/fake-event-source.ts +266 -0
  159. package/src/common/inline-websocket-adapter2.ts +445 -0
  160. package/src/common/log-levels.ts +27 -0
  161. package/src/common/log.ts +139 -0
  162. package/src/common/logfmt.ts +228 -0
  163. package/src/common/network.ts +2 -0
  164. package/src/common/router.ts +87 -0
  165. package/src/common/utils.ts +322 -0
  166. package/src/common/versioned-data.ts +95 -0
  167. package/src/common/websocket-interface.ts +49 -0
  168. package/src/common/websocket.ts +43 -0
  169. package/src/driver-helpers/mod.ts +22 -0
  170. package/src/driver-helpers/utils.ts +17 -0
  171. package/src/driver-test-suite/log.ts +7 -0
  172. package/src/driver-test-suite/mod.ts +213 -0
  173. package/src/driver-test-suite/test-inline-client-driver.ts +402 -0
  174. package/src/driver-test-suite/tests/action-features.ts +136 -0
  175. package/src/driver-test-suite/tests/actor-auth.ts +591 -0
  176. package/src/driver-test-suite/tests/actor-conn-state.ts +249 -0
  177. package/src/driver-test-suite/tests/actor-conn.ts +349 -0
  178. package/src/driver-test-suite/tests/actor-driver.ts +25 -0
  179. package/src/driver-test-suite/tests/actor-error-handling.ts +158 -0
  180. package/src/driver-test-suite/tests/actor-handle.ts +259 -0
  181. package/src/driver-test-suite/tests/actor-inline-client.ts +152 -0
  182. package/src/driver-test-suite/tests/actor-inspector.ts +570 -0
  183. package/src/driver-test-suite/tests/actor-metadata.ts +116 -0
  184. package/src/driver-test-suite/tests/actor-onstatechange.ts +95 -0
  185. package/src/driver-test-suite/tests/actor-schedule.ts +108 -0
  186. package/src/driver-test-suite/tests/actor-sleep.ts +413 -0
  187. package/src/driver-test-suite/tests/actor-state.ts +54 -0
  188. package/src/driver-test-suite/tests/actor-vars.ts +93 -0
  189. package/src/driver-test-suite/tests/manager-driver.ts +365 -0
  190. package/src/driver-test-suite/tests/raw-http-direct-registry.ts +226 -0
  191. package/src/driver-test-suite/tests/raw-http-request-properties.ts +414 -0
  192. package/src/driver-test-suite/tests/raw-http.ts +347 -0
  193. package/src/driver-test-suite/tests/raw-websocket-direct-registry.ts +392 -0
  194. package/src/driver-test-suite/tests/raw-websocket.ts +484 -0
  195. package/src/driver-test-suite/tests/request-access.ts +244 -0
  196. package/src/driver-test-suite/utils.ts +68 -0
  197. package/src/drivers/default.ts +31 -0
  198. package/src/drivers/engine/actor-driver.ts +360 -0
  199. package/src/drivers/engine/api-endpoints.ts +128 -0
  200. package/src/drivers/engine/api-utils.ts +70 -0
  201. package/src/drivers/engine/config.ts +39 -0
  202. package/src/drivers/engine/keys.test.ts +266 -0
  203. package/src/drivers/engine/keys.ts +89 -0
  204. package/src/drivers/engine/kv.ts +3 -0
  205. package/src/drivers/engine/log.ts +7 -0
  206. package/src/drivers/engine/manager-driver.ts +391 -0
  207. package/src/drivers/engine/mod.ts +36 -0
  208. package/src/drivers/engine/ws-proxy.ts +170 -0
  209. package/src/drivers/file-system/actor.ts +91 -0
  210. package/src/drivers/file-system/global-state.ts +673 -0
  211. package/src/drivers/file-system/log.ts +7 -0
  212. package/src/drivers/file-system/manager.ts +306 -0
  213. package/src/drivers/file-system/mod.ts +48 -0
  214. package/src/drivers/file-system/utils.ts +109 -0
  215. package/src/globals.d.ts +6 -0
  216. package/src/inline-client-driver/log.ts +7 -0
  217. package/src/inline-client-driver/mod.ts +385 -0
  218. package/src/inspector/actor.ts +298 -0
  219. package/src/inspector/config.ts +83 -0
  220. package/src/inspector/log.ts +5 -0
  221. package/src/inspector/manager.ts +86 -0
  222. package/src/inspector/mod.ts +2 -0
  223. package/src/inspector/protocol/actor.ts +10 -0
  224. package/src/inspector/protocol/common.ts +196 -0
  225. package/src/inspector/protocol/manager.ts +10 -0
  226. package/src/inspector/protocol/mod.ts +2 -0
  227. package/src/inspector/utils.ts +76 -0
  228. package/src/manager/auth.ts +121 -0
  229. package/src/manager/driver.ts +80 -0
  230. package/src/manager/hono-websocket-adapter.ts +333 -0
  231. package/src/manager/log.ts +7 -0
  232. package/src/manager/mod.ts +2 -0
  233. package/src/manager/protocol/mod.ts +24 -0
  234. package/src/manager/protocol/query.ts +89 -0
  235. package/src/manager/router.ts +1792 -0
  236. package/src/mod.ts +20 -0
  237. package/src/registry/config.ts +32 -0
  238. package/src/registry/log.ts +7 -0
  239. package/src/registry/mod.ts +124 -0
  240. package/src/registry/run-config.ts +54 -0
  241. package/src/registry/serve.ts +53 -0
  242. package/src/schemas/actor-persist/mod.ts +1 -0
  243. package/src/schemas/actor-persist/versioned.ts +25 -0
  244. package/src/schemas/client-protocol/mod.ts +1 -0
  245. package/src/schemas/client-protocol/versioned.ts +63 -0
  246. package/src/schemas/file-system-driver/mod.ts +1 -0
  247. package/src/schemas/file-system-driver/versioned.ts +28 -0
  248. package/src/serde.ts +84 -0
  249. package/src/test/config.ts +16 -0
  250. package/src/test/log.ts +7 -0
  251. package/src/test/mod.ts +153 -0
  252. package/src/utils.ts +172 -0
  253. package/README.md +0 -13
@@ -0,0 +1,1800 @@
1
+ import * as cbor from "cbor-x";
2
+ import invariant from "invariant";
3
+ import onChange from "on-change";
4
+ import type { ActorKey } from "@/actor/mod";
5
+ import type { Client } from "@/client/client";
6
+ import type { Logger } from "@/common/log";
7
+ import { isCborSerializable, stringifyError } from "@/common/utils";
8
+ import type { UniversalWebSocket } from "@/common/websocket-interface";
9
+ import { ActorInspector } from "@/inspector/actor";
10
+ import type { Registry } from "@/mod";
11
+ import type * as bareSchema from "@/schemas/actor-persist/mod";
12
+ import { PERSISTED_ACTOR_VERSIONED } from "@/schemas/actor-persist/versioned";
13
+ import type * as protocol from "@/schemas/client-protocol/mod";
14
+ import { TO_CLIENT_VERSIONED } from "@/schemas/client-protocol/versioned";
15
+ import { bufferToArrayBuffer, SinglePromiseQueue } from "@/utils";
16
+ import type { ActionContext } from "./action";
17
+ import type { ActorConfig, OnConnectOptions } from "./config";
18
+ import {
19
+ CONNECTION_CHECK_LIVENESS_SYMBOL,
20
+ Conn,
21
+ type ConnectionDriver,
22
+ type ConnId,
23
+ } from "./connection";
24
+ import { ActorContext } from "./context";
25
+ import type { AnyDatabaseProvider, InferDatabaseClient } from "./database";
26
+ import type { ActorDriver, ConnDriver, ConnectionDriversMap } from "./driver";
27
+ import * as errors from "./errors";
28
+ import { instanceLogger, logger } from "./log";
29
+ import type {
30
+ PersistedActor,
31
+ PersistedConn,
32
+ PersistedScheduleEvent,
33
+ } from "./persisted";
34
+ import { processMessage } from "./protocol/old";
35
+ import { CachedSerializer } from "./protocol/serde";
36
+ import { Schedule } from "./schedule";
37
+ import { DeadlineError, deadline } from "./utils";
38
+
39
+ /**
40
+ * Options for the `_saveState` method.
41
+ */
42
+ export interface SaveStateOptions {
43
+ /**
44
+ * Forces the state to be saved immediately. This function will return when the state has saved successfully.
45
+ */
46
+ immediate?: boolean;
47
+ /** Bypass ready check for stopping. */
48
+ allowStoppingState?: boolean;
49
+ }
50
+
51
+ /** Actor type alias with all `any` types. Used for `extends` in classes referencing this actor. */
52
+ export type AnyActorInstance = ActorInstance<
53
+ // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
54
+ any,
55
+ // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
56
+ any,
57
+ // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
58
+ any,
59
+ // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
60
+ any,
61
+ // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
62
+ any,
63
+ // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
64
+ any,
65
+ // biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
66
+ any
67
+ >;
68
+
69
+ export type ExtractActorState<A extends AnyActorInstance> =
70
+ A extends ActorInstance<
71
+ infer State,
72
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
73
+ any,
74
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
75
+ any,
76
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
77
+ any,
78
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
79
+ any,
80
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
81
+ any,
82
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
83
+ any
84
+ >
85
+ ? State
86
+ : never;
87
+
88
+ export type ExtractActorConnParams<A extends AnyActorInstance> =
89
+ A extends ActorInstance<
90
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
91
+ any,
92
+ infer ConnParams,
93
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
94
+ any,
95
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
96
+ any,
97
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
98
+ any,
99
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
100
+ any,
101
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
102
+ any
103
+ >
104
+ ? ConnParams
105
+ : never;
106
+
107
+ export type ExtractActorConnState<A extends AnyActorInstance> =
108
+ A extends ActorInstance<
109
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
110
+ any,
111
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
112
+ any,
113
+ infer ConnState,
114
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
115
+ any,
116
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
117
+ any,
118
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
119
+ any,
120
+ // biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
121
+ any
122
+ >
123
+ ? ConnState
124
+ : never;
125
+
126
+ export class ActorInstance<
127
+ S,
128
+ CP,
129
+ CS,
130
+ V,
131
+ I,
132
+ AD,
133
+ DB extends AnyDatabaseProvider,
134
+ > {
135
+ // Shared actor context for this instance
136
+ actorContext: ActorContext<S, CP, CS, V, I, AD, DB>;
137
+ #sleepCalled = false;
138
+ #stopCalled = false;
139
+
140
+ get isStopping() {
141
+ return this.#stopCalled || this.#sleepCalled;
142
+ }
143
+
144
+ #persistChanged = false;
145
+ #isInOnStateChange = false;
146
+
147
+ /**
148
+ * The proxied state that notifies of changes automatically.
149
+ *
150
+ * Any data that should be stored indefinitely should be held within this object.
151
+ */
152
+ #persist!: PersistedActor<S, CP, CS, I>;
153
+
154
+ /** Raw state without the proxy wrapper */
155
+ #persistRaw!: PersistedActor<S, CP, CS, I>;
156
+
157
+ #persistWriteQueue = new SinglePromiseQueue();
158
+ #alarmWriteQueue = new SinglePromiseQueue();
159
+
160
+ #lastSaveTime = 0;
161
+ #pendingSaveTimeout?: NodeJS.Timeout;
162
+
163
+ #vars?: V;
164
+
165
+ #backgroundPromises: Promise<void>[] = [];
166
+ #abortController = new AbortController();
167
+ #config: ActorConfig<S, CP, CS, V, I, AD, DB>;
168
+ #connectionDrivers!: ConnectionDriversMap;
169
+ #actorDriver!: ActorDriver;
170
+ #inlineClient!: Client<Registry<any>>;
171
+ #actorId!: string;
172
+ #name!: string;
173
+ #key!: ActorKey;
174
+ #region!: string;
175
+ #ready = false;
176
+
177
+ #connections = new Map<ConnId, Conn<S, CP, CS, V, I, AD, DB>>();
178
+ #subscriptionIndex = new Map<string, Set<Conn<S, CP, CS, V, I, AD, DB>>>();
179
+ #checkConnLivenessInterval?: NodeJS.Timeout;
180
+
181
+ #sleepTimeout?: NodeJS.Timeout;
182
+
183
+ // Track active raw requests so sleep logic can account for them
184
+ #activeRawFetchCount = 0;
185
+ #activeRawWebSockets = new Set<UniversalWebSocket>();
186
+
187
+ #schedule!: Schedule;
188
+ #db!: InferDatabaseClient<DB>;
189
+
190
+ #inspector = new ActorInspector(() => {
191
+ return {
192
+ isDbEnabled: async () => {
193
+ return this.#db !== undefined;
194
+ },
195
+ getDb: async () => {
196
+ return this.db;
197
+ },
198
+ isStateEnabled: async () => {
199
+ return this.stateEnabled;
200
+ },
201
+ getState: async () => {
202
+ this.#validateStateEnabled();
203
+
204
+ // Must return from `#persistRaw` in order to not return the `onchange` proxy
205
+ return this.#persistRaw.state as Record<string, any> as unknown;
206
+ },
207
+ getRpcs: async () => {
208
+ return Object.keys(this.#config.actions);
209
+ },
210
+ getConnections: async () => {
211
+ return Array.from(this.#connections.entries()).map(([id, conn]) => ({
212
+ id,
213
+ stateEnabled: conn._stateEnabled,
214
+ params: conn.params as {},
215
+ state: conn._stateEnabled ? conn.state : undefined,
216
+ auth: conn.auth as {},
217
+ }));
218
+ },
219
+ setState: async (state: unknown) => {
220
+ this.#validateStateEnabled();
221
+
222
+ // Must set on `#persist` instead of `#persistRaw` in order to ensure that the `Proxy` is correctly configured
223
+ //
224
+ // We have to use `...` so `on-change` recognizes the changes to `state` (i.e. set #persistChanged` to true). This is because:
225
+ // 1. In `getState`, we returned the value from `persistRaw`, which does not have the Proxy to monitor state changes
226
+ // 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`
227
+ this.#persist.state = { ...(state as S) };
228
+ await this.saveState({ immediate: true });
229
+ },
230
+ };
231
+ });
232
+
233
+ get id() {
234
+ return this.#actorId;
235
+ }
236
+
237
+ get inlineClient(): Client<Registry<any>> {
238
+ return this.#inlineClient;
239
+ }
240
+
241
+ get inspector() {
242
+ return this.#inspector;
243
+ }
244
+
245
+ get #sleepingSupported(): boolean {
246
+ return this.#actorDriver.sleep !== undefined;
247
+ }
248
+
249
+ /**
250
+ * This constructor should never be used directly.
251
+ *
252
+ * Constructed in {@link ActorInstance.start}.
253
+ *
254
+ * @private
255
+ */
256
+ constructor(config: ActorConfig<S, CP, CS, V, I, AD, DB>) {
257
+ this.#config = config;
258
+ this.actorContext = new ActorContext(this);
259
+ }
260
+
261
+ async start(
262
+ connectionDrivers: ConnectionDriversMap,
263
+ actorDriver: ActorDriver,
264
+ inlineClient: Client<Registry<any>>,
265
+ actorId: string,
266
+ name: string,
267
+ key: ActorKey,
268
+ region: string,
269
+ ) {
270
+ this.#connectionDrivers = connectionDrivers;
271
+ this.#actorDriver = actorDriver;
272
+ this.#inlineClient = inlineClient;
273
+ this.#actorId = actorId;
274
+ this.#name = name;
275
+ this.#key = key;
276
+ this.#region = region;
277
+ this.#schedule = new Schedule(this);
278
+
279
+ // Initialize server
280
+ //
281
+ // Store the promise so network requests can await initialization
282
+ await this.#initialize();
283
+
284
+ // TODO: Exit process if this errors
285
+ if (this.#varsEnabled) {
286
+ let vars: V | undefined;
287
+ if ("createVars" in this.#config) {
288
+ const dataOrPromise = this.#config.createVars(
289
+ this.actorContext as unknown as ActorContext<
290
+ undefined,
291
+ undefined,
292
+ undefined,
293
+ undefined,
294
+ undefined,
295
+ undefined,
296
+ any
297
+ >,
298
+ this.#actorDriver.getContext(this.#actorId),
299
+ );
300
+ if (dataOrPromise instanceof Promise) {
301
+ vars = await deadline(
302
+ dataOrPromise,
303
+ this.#config.options.createVarsTimeout,
304
+ );
305
+ } else {
306
+ vars = dataOrPromise;
307
+ }
308
+ } else if ("vars" in this.#config) {
309
+ vars = structuredClone(this.#config.vars);
310
+ } else {
311
+ throw new Error("Could not variables from 'createVars' or 'vars'");
312
+ }
313
+ this.#vars = vars;
314
+ }
315
+
316
+ // TODO: Exit process if this errors
317
+ logger().info("actor starting");
318
+ if (this.#config.onStart) {
319
+ const result = this.#config.onStart(this.actorContext);
320
+ if (result instanceof Promise) {
321
+ await result;
322
+ }
323
+ }
324
+
325
+ // Setup Database
326
+ if ("db" in this.#config && this.#config.db) {
327
+ const client = await this.#config.db.createClient({
328
+ getDatabase: () => actorDriver.getDatabase(this.#actorId),
329
+ });
330
+ logger().info("database migration starting");
331
+ await this.#config.db.onMigrate?.(client);
332
+ logger().info("database migration complete");
333
+ this.#db = client;
334
+ }
335
+
336
+ // Set alarm for next scheduled event if any exist after finishing initiation sequence
337
+ if (this.#persist.scheduledEvents.length > 0) {
338
+ await this.#queueSetAlarm(this.#persist.scheduledEvents[0].timestamp);
339
+ }
340
+
341
+ logger().info("actor ready");
342
+ this.#ready = true;
343
+
344
+ // Must be called after setting `#ready` or else it will not schedule sleep
345
+ this.#resetSleepTimer();
346
+
347
+ // Start conn liveness interval
348
+ //
349
+ // Check for liveness immediately since we may have connections that
350
+ // were in `reconnecting` state when the actor went to sleep that we
351
+ // need to purge.
352
+ //
353
+ // We don't use alarms for connection liveness since alarms require
354
+ // durability & are expensive. Connection liveness is safe to assume
355
+ // it only needs to be ran while the actor is awake and does not need
356
+ // to manually wake the actor. The only case this is not true is if the
357
+ // connection liveness timeout is greater than the actor sleep timeout
358
+ // OR if the actor is manually put to sleep. In this case, the connections
359
+ // will be stuck in a `reconnecting` state until the actor is awaken again.
360
+ this.#checkConnLivenessInterval = setInterval(
361
+ this.#checkConnectionsLiveness.bind(this),
362
+ this.#config.options.connectionLivenessInterval,
363
+ );
364
+ this.#checkConnectionsLiveness();
365
+ }
366
+
367
+ async #scheduleEventInner(newEvent: PersistedScheduleEvent) {
368
+ this.actorContext.log.info("scheduling event", newEvent);
369
+
370
+ // Insert event in to index
371
+ const insertIndex = this.#persist.scheduledEvents.findIndex(
372
+ (x) => x.timestamp > newEvent.timestamp,
373
+ );
374
+ if (insertIndex === -1) {
375
+ this.#persist.scheduledEvents.push(newEvent);
376
+ } else {
377
+ this.#persist.scheduledEvents.splice(insertIndex, 0, newEvent);
378
+ }
379
+
380
+ // Update alarm if:
381
+ // - this is the newest event (i.e. at beginning of array) or
382
+ // - this is the only event (i.e. the only event in the array)
383
+ if (insertIndex === 0 || this.#persist.scheduledEvents.length === 1) {
384
+ this.actorContext.log.info("setting alarm", {
385
+ timestamp: newEvent.timestamp,
386
+ eventCount: this.#persist.scheduledEvents.length,
387
+ });
388
+ await this.#queueSetAlarm(newEvent.timestamp);
389
+ }
390
+ }
391
+
392
+ async _onAlarm() {
393
+ const now = Date.now();
394
+ this.actorContext.log.debug("alarm triggered", {
395
+ now,
396
+ events: this.#persist.scheduledEvents.length,
397
+ });
398
+
399
+ // Update sleep
400
+ //
401
+ // Do this before any async logic
402
+ this.#resetSleepTimer();
403
+
404
+ // Remove events from schedule that we're about to run
405
+ const runIndex = this.#persist.scheduledEvents.findIndex(
406
+ (x) => x.timestamp <= now,
407
+ );
408
+ if (runIndex === -1) {
409
+ // No events are due yet. This will happen if timers fire slightly early.
410
+ // Ensure we reschedule the alarm for the next upcoming event to avoid losing it.
411
+ logger().warn("no events are due yet, time may have broken");
412
+ if (this.#persist.scheduledEvents.length > 0) {
413
+ const nextTs = this.#persist.scheduledEvents[0].timestamp;
414
+ this.actorContext.log.warn(
415
+ "alarm fired early, rescheduling for next event",
416
+ {
417
+ now,
418
+ nextTs,
419
+ delta: nextTs - now,
420
+ },
421
+ );
422
+ await this.#queueSetAlarm(nextTs);
423
+ }
424
+ this.actorContext.log.debug("no events to run", { now });
425
+ return;
426
+ }
427
+ const scheduleEvents = this.#persist.scheduledEvents.splice(
428
+ 0,
429
+ runIndex + 1,
430
+ );
431
+ this.actorContext.log.debug("running events", {
432
+ count: scheduleEvents.length,
433
+ });
434
+
435
+ // Set alarm for next event
436
+ if (this.#persist.scheduledEvents.length > 0) {
437
+ const nextTs = this.#persist.scheduledEvents[0].timestamp;
438
+ this.actorContext.log.info("setting next alarm", {
439
+ nextTs,
440
+ remainingEvents: this.#persist.scheduledEvents.length,
441
+ });
442
+ await this.#queueSetAlarm(nextTs);
443
+ }
444
+
445
+ // Iterate by event key in order to ensure we call the events in order
446
+ for (const event of scheduleEvents) {
447
+ try {
448
+ this.actorContext.log.info("running action for event", {
449
+ event: event.eventId,
450
+ timestamp: event.timestamp,
451
+ action: event.kind.generic.actionName,
452
+ });
453
+
454
+ // Look up function
455
+ const fn: unknown = this.#config.actions[event.kind.generic.actionName];
456
+
457
+ if (!fn)
458
+ throw new Error(
459
+ `Missing action for alarm ${event.kind.generic.actionName}`,
460
+ );
461
+ if (typeof fn !== "function")
462
+ throw new Error(
463
+ `Alarm function lookup for ${event.kind.generic.actionName} returned ${typeof fn}`,
464
+ );
465
+
466
+ // Call function
467
+ try {
468
+ const args = event.kind.generic.args
469
+ ? cbor.decode(new Uint8Array(event.kind.generic.args))
470
+ : [];
471
+ await fn.call(undefined, this.actorContext, ...args);
472
+ } catch (error) {
473
+ this.actorContext.log.error("error while running event", {
474
+ error: stringifyError(error),
475
+ event: event.eventId,
476
+ timestamp: event.timestamp,
477
+ action: event.kind.generic.actionName,
478
+ });
479
+ }
480
+ } catch (error) {
481
+ this.actorContext.log.error("internal error while running event", {
482
+ error: stringifyError(error),
483
+ ...event,
484
+ });
485
+ }
486
+ }
487
+ }
488
+
489
+ async scheduleEvent(
490
+ timestamp: number,
491
+ action: string,
492
+ args: unknown[],
493
+ ): Promise<void> {
494
+ return this.#scheduleEventInner({
495
+ eventId: crypto.randomUUID(),
496
+ timestamp,
497
+ kind: {
498
+ generic: {
499
+ actionName: action,
500
+ args: bufferToArrayBuffer(cbor.encode(args)),
501
+ },
502
+ },
503
+ });
504
+ }
505
+
506
+ get stateEnabled() {
507
+ return "createState" in this.#config || "state" in this.#config;
508
+ }
509
+
510
+ #validateStateEnabled() {
511
+ if (!this.stateEnabled) {
512
+ throw new errors.StateNotEnabled();
513
+ }
514
+ }
515
+
516
+ get #connStateEnabled() {
517
+ return "createConnState" in this.#config || "connState" in this.#config;
518
+ }
519
+
520
+ get #varsEnabled() {
521
+ return "createVars" in this.#config || "vars" in this.#config;
522
+ }
523
+
524
+ #validateVarsEnabled() {
525
+ if (!this.#varsEnabled) {
526
+ throw new errors.VarsNotEnabled();
527
+ }
528
+ }
529
+
530
+ /** Promise used to wait for a save to complete. This is required since you cannot await `#saveStateThrottled`. */
531
+ #onPersistSavedPromise?: PromiseWithResolvers<void>;
532
+
533
+ /** Throttled save state method. Used to write to KV at a reasonable cadence. */
534
+ #savePersistThrottled() {
535
+ const now = Date.now();
536
+ const timeSinceLastSave = now - this.#lastSaveTime;
537
+ const saveInterval = this.#config.options.stateSaveInterval;
538
+
539
+ // If we're within the throttle window and not already scheduled, schedule the next save.
540
+ if (timeSinceLastSave < saveInterval) {
541
+ if (this.#pendingSaveTimeout === undefined) {
542
+ this.#pendingSaveTimeout = setTimeout(() => {
543
+ this.#pendingSaveTimeout = undefined;
544
+ this.#savePersistInner();
545
+ }, saveInterval - timeSinceLastSave);
546
+ }
547
+ } else {
548
+ // If we're outside the throttle window, save immediately
549
+ this.#savePersistInner();
550
+ }
551
+ }
552
+
553
+ /** Saves the state to KV. You probably want to use #saveStateThrottled instead except for a few edge cases. */
554
+ async #savePersistInner() {
555
+ try {
556
+ this.#lastSaveTime = Date.now();
557
+
558
+ if (this.#persistChanged) {
559
+ const finished = this.#persistWriteQueue.enqueue(async () => {
560
+ logger().debug("saving persist");
561
+
562
+ // There might be more changes while we're writing, so we set this
563
+ // before writing to KV in order to avoid a race condition.
564
+ this.#persistChanged = false;
565
+
566
+ // Convert to BARE types and write to KV
567
+ const bareData = this.#convertToBarePersisted(this.#persistRaw);
568
+ await this.#actorDriver.writePersistedData(
569
+ this.#actorId,
570
+ PERSISTED_ACTOR_VERSIONED.serializeWithEmbeddedVersion(bareData),
571
+ );
572
+
573
+ logger().debug("persist saved");
574
+ });
575
+
576
+ await finished;
577
+ }
578
+
579
+ this.#onPersistSavedPromise?.resolve();
580
+ } catch (error) {
581
+ this.#onPersistSavedPromise?.reject(error);
582
+ throw error;
583
+ }
584
+ }
585
+
586
+ async #queueSetAlarm(timestamp: number): Promise<void> {
587
+ await this.#alarmWriteQueue.enqueue(async () => {
588
+ await this.#actorDriver.setAlarm(this, timestamp);
589
+ });
590
+ }
591
+
592
+ /**
593
+ * Creates proxy for `#persist` that handles automatically flagging when state needs to be updated.
594
+ */
595
+ #setPersist(target: PersistedActor<S, CP, CS, I>) {
596
+ // Set raw persist object
597
+ this.#persistRaw = target;
598
+
599
+ // TODO: Only validate this for conn state
600
+ // TODO: Allow disabling in production
601
+ // If this can't be proxied, return raw value
602
+ if (target === null || typeof target !== "object") {
603
+ let invalidPath = "";
604
+ if (
605
+ !isCborSerializable(
606
+ target,
607
+ (path) => {
608
+ invalidPath = path;
609
+ },
610
+ "",
611
+ )
612
+ ) {
613
+ throw new errors.InvalidStateType({ path: invalidPath });
614
+ }
615
+ return target;
616
+ }
617
+
618
+ // Unsubscribe from old state
619
+ if (this.#persist) {
620
+ onChange.unsubscribe(this.#persist);
621
+ }
622
+
623
+ // Listen for changes to the object in order to automatically write state
624
+ this.#persist = onChange(
625
+ target,
626
+ // biome-ignore lint/suspicious/noExplicitAny: Don't know types in proxy
627
+ (path: string, value: any, _previousValue: any, _applyData: any) => {
628
+ if (path !== "state" && !path.startsWith("state.")) {
629
+ return;
630
+ }
631
+
632
+ let invalidPath = "";
633
+ if (
634
+ !isCborSerializable(
635
+ value,
636
+ (invalidPathPart) => {
637
+ invalidPath = invalidPathPart;
638
+ },
639
+ "",
640
+ )
641
+ ) {
642
+ throw new errors.InvalidStateType({
643
+ path: path + (invalidPath ? `.${invalidPath}` : ""),
644
+ });
645
+ }
646
+ this.#persistChanged = true;
647
+
648
+ // Inform the inspector about state changes
649
+ this.inspector.emitter.emit("stateUpdated", this.#persist.state);
650
+
651
+ // Call onStateChange if it exists
652
+ // Skip if we're already inside onStateChange to prevent infinite recursion
653
+ if (
654
+ this.#config.onStateChange &&
655
+ this.#ready &&
656
+ !this.#isInOnStateChange
657
+ ) {
658
+ try {
659
+ this.#isInOnStateChange = true;
660
+ this.#config.onStateChange(
661
+ this.actorContext,
662
+ this.#persistRaw.state,
663
+ );
664
+ } catch (error) {
665
+ logger().error("error in `_onStateChange`", {
666
+ error: stringifyError(error),
667
+ });
668
+ } finally {
669
+ this.#isInOnStateChange = false;
670
+ }
671
+ }
672
+
673
+ // State will be flushed at the end of the action
674
+ },
675
+ { ignoreDetached: true },
676
+ );
677
+ }
678
+
679
+ async #initialize() {
680
+ // Read initial state
681
+ const persistDataBuffer = await this.#actorDriver.readPersistedData(
682
+ this.#actorId,
683
+ );
684
+ invariant(
685
+ persistDataBuffer !== undefined,
686
+ "persist data has not been set, it should be set when initialized",
687
+ );
688
+ const bareData =
689
+ PERSISTED_ACTOR_VERSIONED.deserializeWithEmbeddedVersion(
690
+ persistDataBuffer,
691
+ );
692
+ const persistData = this.#convertFromBarePersisted(bareData);
693
+
694
+ if (persistData.hasInitiated) {
695
+ logger().info("actor restoring", {
696
+ connections: persistData.connections.length,
697
+ });
698
+
699
+ // Set initial state
700
+ this.#setPersist(persistData);
701
+
702
+ // Load connections
703
+ for (const connPersist of this.#persist.connections) {
704
+ // Create connections
705
+ const driver = this.__getConnDriver(connPersist.connDriver);
706
+ const conn = new Conn<S, CP, CS, V, I, AD, DB>(
707
+ this,
708
+ connPersist,
709
+ driver,
710
+ this.#connStateEnabled,
711
+ );
712
+ this.#connections.set(conn.id, conn);
713
+
714
+ // Register event subscriptions
715
+ for (const sub of connPersist.subscriptions) {
716
+ this.#addSubscription(sub.eventName, conn, true);
717
+ }
718
+ }
719
+ } else {
720
+ logger().info("actor creating");
721
+
722
+ // Initialize actor state
723
+ let stateData: unknown;
724
+ if (this.stateEnabled) {
725
+ logger().info("actor state initializing");
726
+
727
+ if ("createState" in this.#config) {
728
+ this.#config.createState;
729
+
730
+ // Convert state to undefined since state is not defined yet here
731
+ stateData = await this.#config.createState(
732
+ this.actorContext as unknown as ActorContext<
733
+ undefined,
734
+ undefined,
735
+ undefined,
736
+ undefined,
737
+ undefined,
738
+ undefined,
739
+ undefined
740
+ >,
741
+ persistData.input!,
742
+ );
743
+ } else if ("state" in this.#config) {
744
+ stateData = structuredClone(this.#config.state);
745
+ } else {
746
+ throw new Error("Both 'createState' or 'state' were not defined");
747
+ }
748
+ } else {
749
+ logger().debug("state not enabled");
750
+ }
751
+
752
+ // Save state and mark as initialized
753
+ persistData.state = stateData as S;
754
+ persistData.hasInitiated = true;
755
+
756
+ // Update state
757
+ logger().debug("writing state");
758
+ const bareData = this.#convertToBarePersisted(persistData);
759
+ await this.#actorDriver.writePersistedData(
760
+ this.#actorId,
761
+ PERSISTED_ACTOR_VERSIONED.serializeWithEmbeddedVersion(bareData),
762
+ );
763
+
764
+ this.#setPersist(persistData);
765
+
766
+ // Notify creation
767
+ if (this.#config.onCreate) {
768
+ await this.#config.onCreate(this.actorContext, persistData.input!);
769
+ }
770
+ }
771
+ }
772
+
773
+ __getConnForId(id: string): Conn<S, CP, CS, V, I, AD, DB> | undefined {
774
+ return this.#connections.get(id);
775
+ }
776
+
777
+ /**
778
+ * Removes a connection and cleans up its resources.
779
+ */
780
+ __removeConn(conn: Conn<S, CP, CS, V, I, AD, DB> | undefined) {
781
+ if (!conn) {
782
+ logger().warn("`conn` does not exist");
783
+ return;
784
+ }
785
+
786
+ // Remove from persist & save immediately
787
+ const connIdx = this.#persist.connections.findIndex(
788
+ (c) => c.connId === conn.id,
789
+ );
790
+ if (connIdx !== -1) {
791
+ this.#persist.connections.splice(connIdx, 1);
792
+ this.saveState({ immediate: true, allowStoppingState: true });
793
+ } else {
794
+ logger().warn("could not find persisted connection to remove", {
795
+ connId: conn.id,
796
+ });
797
+ }
798
+
799
+ // Remove from state
800
+ this.#connections.delete(conn.id);
801
+
802
+ // Remove subscriptions
803
+ for (const eventName of [...conn.subscriptions.values()]) {
804
+ this.#removeSubscription(eventName, conn, true);
805
+ }
806
+
807
+ this.inspector.emitter.emit("connectionUpdated");
808
+ if (this.#config.onDisconnect) {
809
+ try {
810
+ const result = this.#config.onDisconnect(this.actorContext, conn);
811
+ if (result instanceof Promise) {
812
+ // Handle promise but don't await it to prevent blocking
813
+ result.catch((error) => {
814
+ logger().error("error in `onDisconnect`", {
815
+ error: stringifyError(error),
816
+ });
817
+ });
818
+ }
819
+ } catch (error) {
820
+ logger().error("error in `onDisconnect`", {
821
+ error: stringifyError(error),
822
+ });
823
+ }
824
+ }
825
+
826
+ // Update sleep
827
+ this.#resetSleepTimer();
828
+ }
829
+
830
+ async prepareConn(
831
+ // biome-ignore lint/suspicious/noExplicitAny: TypeScript bug with ExtractActorConnParams<this>,
832
+ params: any,
833
+ request?: Request,
834
+ ): Promise<CS> {
835
+ // Authenticate connection
836
+ let connState: CS | undefined;
837
+
838
+ const onBeforeConnectOpts = {
839
+ request,
840
+ } satisfies OnConnectOptions;
841
+
842
+ if (this.#config.onBeforeConnect) {
843
+ await this.#config.onBeforeConnect(
844
+ this.actorContext,
845
+ onBeforeConnectOpts,
846
+ params,
847
+ );
848
+ }
849
+
850
+ if (this.#connStateEnabled) {
851
+ if ("createConnState" in this.#config) {
852
+ const dataOrPromise = this.#config.createConnState(
853
+ this.actorContext as unknown as ActorContext<
854
+ undefined,
855
+ undefined,
856
+ undefined,
857
+ undefined,
858
+ undefined,
859
+ undefined,
860
+ undefined
861
+ >,
862
+ onBeforeConnectOpts,
863
+ params,
864
+ );
865
+ if (dataOrPromise instanceof Promise) {
866
+ connState = await deadline(
867
+ dataOrPromise,
868
+ this.#config.options.createConnStateTimeout,
869
+ );
870
+ } else {
871
+ connState = dataOrPromise;
872
+ }
873
+ } else if ("connState" in this.#config) {
874
+ connState = structuredClone(this.#config.connState);
875
+ } else {
876
+ throw new Error(
877
+ "Could not create connection state from 'createConnState' or 'connState'",
878
+ );
879
+ }
880
+ }
881
+
882
+ return connState as CS;
883
+ }
884
+
885
+ __getConnDriver(driverId: ConnectionDriver): ConnDriver {
886
+ // Get driver
887
+ const driver = this.#connectionDrivers[driverId];
888
+ if (!driver) throw new Error(`No connection driver: ${driverId}`);
889
+ return driver;
890
+ }
891
+
892
+ /**
893
+ * Called after establishing a connection handshake.
894
+ */
895
+ async createConn(
896
+ connectionId: string,
897
+ connectionToken: string,
898
+ params: CP,
899
+ state: CS,
900
+ driverId: ConnectionDriver,
901
+ driverState: unknown,
902
+ authData: unknown,
903
+ ): Promise<Conn<S, CP, CS, V, I, AD, DB>> {
904
+ this.#assertReady();
905
+
906
+ if (this.#connections.has(connectionId)) {
907
+ throw new Error(`Connection already exists: ${connectionId}`);
908
+ }
909
+
910
+ // Create connection
911
+ const driver = this.__getConnDriver(driverId);
912
+ const persist: PersistedConn<CP, CS> = {
913
+ connId: connectionId,
914
+ token: connectionToken,
915
+ connDriver: driverId,
916
+ connDriverState: driverState,
917
+ params: params,
918
+ state: state,
919
+ authData: authData,
920
+ lastSeen: Date.now(),
921
+ subscriptions: [],
922
+ };
923
+ const conn = new Conn<S, CP, CS, V, I, AD, DB>(
924
+ this,
925
+ persist,
926
+ driver,
927
+ this.#connStateEnabled,
928
+ );
929
+ this.#connections.set(conn.id, conn);
930
+
931
+ // Update sleep
932
+ //
933
+ // Do this immediately after adding connection & before any async logic in order to avoid race conditions with sleep timeouts
934
+ this.#resetSleepTimer();
935
+
936
+ // Add to persistence & save immediately
937
+ this.#persist.connections.push(persist);
938
+ this.saveState({ immediate: true });
939
+
940
+ // Handle connection
941
+ if (this.#config.onConnect) {
942
+ try {
943
+ const result = this.#config.onConnect(this.actorContext, conn);
944
+ if (result instanceof Promise) {
945
+ deadline(result, this.#config.options.onConnectTimeout).catch(
946
+ (error) => {
947
+ logger().error("error in `onConnect`, closing socket", {
948
+ error,
949
+ });
950
+ conn?.disconnect("`onConnect` failed");
951
+ },
952
+ );
953
+ }
954
+ } catch (error) {
955
+ logger().error("error in `onConnect`", {
956
+ error: stringifyError(error),
957
+ });
958
+ conn?.disconnect("`onConnect` failed");
959
+ }
960
+ }
961
+
962
+ this.inspector.emitter.emit("connectionUpdated");
963
+
964
+ // Send init message
965
+ conn._sendMessage(
966
+ new CachedSerializer<protocol.ToClient>(
967
+ {
968
+ body: {
969
+ tag: "Init",
970
+ val: {
971
+ actorId: this.id,
972
+ connectionId: conn.id,
973
+ connectionToken: conn._token,
974
+ },
975
+ },
976
+ },
977
+ TO_CLIENT_VERSIONED,
978
+ ),
979
+ );
980
+
981
+ return conn;
982
+ }
983
+
984
+ // MARK: Messages
985
+ async processMessage(
986
+ message: protocol.ToServer,
987
+ conn: Conn<S, CP, CS, V, I, AD, DB>,
988
+ ) {
989
+ await processMessage(message, this, conn, {
990
+ onExecuteAction: async (ctx, name, args) => {
991
+ this.inspector.emitter.emit("eventFired", {
992
+ type: "action",
993
+ name,
994
+ args,
995
+ connId: conn.id,
996
+ });
997
+ return await this.executeAction(ctx, name, args);
998
+ },
999
+ onSubscribe: async (eventName, conn) => {
1000
+ this.inspector.emitter.emit("eventFired", {
1001
+ type: "subscribe",
1002
+ eventName,
1003
+ connId: conn.id,
1004
+ });
1005
+ this.#addSubscription(eventName, conn, false);
1006
+ },
1007
+ onUnsubscribe: async (eventName, conn) => {
1008
+ this.inspector.emitter.emit("eventFired", {
1009
+ type: "unsubscribe",
1010
+ eventName,
1011
+ connId: conn.id,
1012
+ });
1013
+ this.#removeSubscription(eventName, conn, false);
1014
+ },
1015
+ });
1016
+ }
1017
+
1018
+ // MARK: Events
1019
+ #addSubscription(
1020
+ eventName: string,
1021
+ connection: Conn<S, CP, CS, V, I, AD, DB>,
1022
+ fromPersist: boolean,
1023
+ ) {
1024
+ if (connection.subscriptions.has(eventName)) {
1025
+ logger().debug("connection already has subscription", { eventName });
1026
+ return;
1027
+ }
1028
+
1029
+ // Persist subscriptions & save immediately
1030
+ //
1031
+ // Don't update persistence if already restoring from persistence
1032
+ if (!fromPersist) {
1033
+ connection.__persist.subscriptions.push({ eventName: eventName });
1034
+ this.saveState({ immediate: true });
1035
+ }
1036
+
1037
+ // Update subscriptions
1038
+ connection.subscriptions.add(eventName);
1039
+
1040
+ // Update subscription index
1041
+ let subscribers = this.#subscriptionIndex.get(eventName);
1042
+ if (!subscribers) {
1043
+ subscribers = new Set();
1044
+ this.#subscriptionIndex.set(eventName, subscribers);
1045
+ }
1046
+ subscribers.add(connection);
1047
+ }
1048
+
1049
+ #removeSubscription(
1050
+ eventName: string,
1051
+ connection: Conn<S, CP, CS, V, I, AD, DB>,
1052
+ fromRemoveConn: boolean,
1053
+ ) {
1054
+ if (!connection.subscriptions.has(eventName)) {
1055
+ logger().warn("connection does not have subscription", { eventName });
1056
+ return;
1057
+ }
1058
+
1059
+ // Persist subscriptions & save immediately
1060
+ //
1061
+ // Don't update the connection itself if the connection is already being removed
1062
+ if (!fromRemoveConn) {
1063
+ connection.subscriptions.delete(eventName);
1064
+
1065
+ const subIdx = connection.__persist.subscriptions.findIndex(
1066
+ (s) => s.eventName === eventName,
1067
+ );
1068
+ if (subIdx !== -1) {
1069
+ connection.__persist.subscriptions.splice(subIdx, 1);
1070
+ } else {
1071
+ logger().warn("subscription does not exist with name", { eventName });
1072
+ }
1073
+
1074
+ this.saveState({ immediate: true });
1075
+ }
1076
+
1077
+ // Update scriptions index
1078
+ const subscribers = this.#subscriptionIndex.get(eventName);
1079
+ if (subscribers) {
1080
+ subscribers.delete(connection);
1081
+ if (subscribers.size === 0) {
1082
+ this.#subscriptionIndex.delete(eventName);
1083
+ }
1084
+ }
1085
+ }
1086
+
1087
+ #assertReady(allowStoppingState: boolean = false) {
1088
+ if (!this.#ready) throw new errors.InternalError("Actor not ready");
1089
+ if (!allowStoppingState && this.#sleepCalled)
1090
+ throw new errors.InternalError("Actor is going to sleep");
1091
+ if (!allowStoppingState && this.#stopCalled)
1092
+ throw new errors.InternalError("Actor is stopping");
1093
+ }
1094
+
1095
+ /**
1096
+ * Check the liveness of all connections.
1097
+ * Sets up a recurring check based on the configured interval.
1098
+ */
1099
+ #checkConnectionsLiveness() {
1100
+ logger().debug("checking connections liveness");
1101
+
1102
+ for (const conn of this.#connections.values()) {
1103
+ const liveness = conn[CONNECTION_CHECK_LIVENESS_SYMBOL]();
1104
+ if (liveness.status === "connected") {
1105
+ logger().debug("connection is alive", { connId: conn.id });
1106
+ } else {
1107
+ const lastSeen = liveness.lastSeen;
1108
+ const sinceLastSeen = Date.now() - lastSeen;
1109
+ if (sinceLastSeen < this.#config.options.connectionLivenessTimeout) {
1110
+ logger().debug("connection might be alive, will check later", {
1111
+ connId: conn.id,
1112
+ lastSeen,
1113
+ sinceLastSeen,
1114
+ });
1115
+ continue;
1116
+ }
1117
+
1118
+ // Connection is dead, remove it
1119
+ logger().warn("connection is dead, removing", {
1120
+ connId: conn.id,
1121
+ lastSeen,
1122
+ });
1123
+
1124
+ // TODO: Do we need to force disconnect the connection here?
1125
+
1126
+ this.__removeConn(conn);
1127
+ }
1128
+ }
1129
+ }
1130
+
1131
+ /**
1132
+ * Check if the actor is ready to handle requests.
1133
+ */
1134
+ isReady(): boolean {
1135
+ return this.#ready;
1136
+ }
1137
+
1138
+ /**
1139
+ * Execute an action call from a client.
1140
+ *
1141
+ * This method handles:
1142
+ * 1. Validating the action name
1143
+ * 2. Executing the action function
1144
+ * 3. Processing the result through onBeforeActionResponse (if configured)
1145
+ * 4. Handling timeouts and errors
1146
+ * 5. Saving state changes
1147
+ *
1148
+ * @param ctx The action context
1149
+ * @param actionName The name of the action being called
1150
+ * @param args The arguments passed to the action
1151
+ * @returns The result of the action call
1152
+ * @throws {ActionNotFound} If the action doesn't exist
1153
+ * @throws {ActionTimedOut} If the action times out
1154
+ * @internal
1155
+ */
1156
+ async executeAction(
1157
+ ctx: ActionContext<S, CP, CS, V, I, AD, DB>,
1158
+ actionName: string,
1159
+ args: unknown[],
1160
+ ): Promise<unknown> {
1161
+ invariant(this.#ready, "executing action before ready");
1162
+
1163
+ // Prevent calling private or reserved methods
1164
+ if (!(actionName in this.#config.actions)) {
1165
+ logger().warn("action does not exist", { actionName });
1166
+ throw new errors.ActionNotFound(actionName);
1167
+ }
1168
+
1169
+ // Check if the method exists on this object
1170
+ const actionFunction = this.#config.actions[actionName];
1171
+ if (typeof actionFunction !== "function") {
1172
+ logger().warn("action is not a function", {
1173
+ actionName: actionName,
1174
+ type: typeof actionFunction,
1175
+ });
1176
+ throw new errors.ActionNotFound(actionName);
1177
+ }
1178
+
1179
+ // TODO: pass abortable to the action to decide when to abort
1180
+ // TODO: Manually call abortable for better error handling
1181
+ // Call the function on this object with those arguments
1182
+ try {
1183
+ // Log when we start executing the action
1184
+ logger().debug("executing action", { actionName: actionName, args });
1185
+
1186
+ const outputOrPromise = actionFunction.call(undefined, ctx, ...args);
1187
+ let output: unknown;
1188
+ if (outputOrPromise instanceof Promise) {
1189
+ // Log that we're waiting for an async action
1190
+ logger().debug("awaiting async action", { actionName: actionName });
1191
+
1192
+ output = await deadline(
1193
+ outputOrPromise,
1194
+ this.#config.options.actionTimeout,
1195
+ );
1196
+
1197
+ // Log that async action completed
1198
+ logger().debug("async action completed", { actionName: actionName });
1199
+ } else {
1200
+ output = outputOrPromise;
1201
+ }
1202
+
1203
+ // Process the output through onBeforeActionResponse if configured
1204
+ if (this.#config.onBeforeActionResponse) {
1205
+ try {
1206
+ const processedOutput = this.#config.onBeforeActionResponse(
1207
+ this.actorContext,
1208
+ actionName,
1209
+ args,
1210
+ output,
1211
+ );
1212
+ if (processedOutput instanceof Promise) {
1213
+ logger().debug("awaiting onBeforeActionResponse", {
1214
+ actionName: actionName,
1215
+ });
1216
+ output = await processedOutput;
1217
+ logger().debug("onBeforeActionResponse completed", {
1218
+ actionName: actionName,
1219
+ });
1220
+ } else {
1221
+ output = processedOutput;
1222
+ }
1223
+ } catch (error) {
1224
+ logger().error("error in `onBeforeActionResponse`", {
1225
+ error: stringifyError(error),
1226
+ });
1227
+ }
1228
+ }
1229
+
1230
+ // Log the output before returning
1231
+ logger().debug("action completed", {
1232
+ actionName: actionName,
1233
+ outputType: typeof output,
1234
+ isPromise: output instanceof Promise,
1235
+ });
1236
+
1237
+ // This output *might* reference a part of the state (using onChange), but
1238
+ // that's OK since this value always gets serialized and sent over the
1239
+ // network.
1240
+ return output;
1241
+ } catch (error) {
1242
+ if (error instanceof DeadlineError) {
1243
+ throw new errors.ActionTimedOut();
1244
+ }
1245
+ logger().error("action error", {
1246
+ actionName: actionName,
1247
+ error: stringifyError(error),
1248
+ });
1249
+ throw error;
1250
+ } finally {
1251
+ this.#savePersistThrottled();
1252
+ }
1253
+ }
1254
+
1255
+ /**
1256
+ * Returns a list of action methods available on this actor.
1257
+ */
1258
+ get actions(): string[] {
1259
+ return Object.keys(this.#config.actions);
1260
+ }
1261
+
1262
+ /**
1263
+ * Handles raw HTTP requests to the actor.
1264
+ */
1265
+ async handleFetch(request: Request, opts: { auth: AD }): Promise<Response> {
1266
+ this.#assertReady();
1267
+
1268
+ if (!this.#config.onFetch) {
1269
+ throw new errors.FetchHandlerNotDefined();
1270
+ }
1271
+
1272
+ // Track active raw fetch while handler runs
1273
+ this.#activeRawFetchCount++;
1274
+ this.#resetSleepTimer();
1275
+
1276
+ try {
1277
+ const response = await this.#config.onFetch(
1278
+ this.actorContext,
1279
+ request,
1280
+ opts,
1281
+ );
1282
+ if (!response) {
1283
+ throw new errors.InvalidFetchResponse();
1284
+ }
1285
+ return response;
1286
+ } catch (error) {
1287
+ logger().error("onFetch error", {
1288
+ error: stringifyError(error),
1289
+ });
1290
+ throw error;
1291
+ } finally {
1292
+ // Decrement active raw fetch counter and re-evaluate sleep
1293
+ this.#activeRawFetchCount = Math.max(0, this.#activeRawFetchCount - 1);
1294
+ this.#resetSleepTimer();
1295
+ this.#savePersistThrottled();
1296
+ }
1297
+ }
1298
+
1299
+ /**
1300
+ * Handles raw WebSocket connections to the actor.
1301
+ */
1302
+ async handleWebSocket(
1303
+ websocket: UniversalWebSocket,
1304
+ opts: { request: Request; auth: AD },
1305
+ ): Promise<void> {
1306
+ this.#assertReady();
1307
+
1308
+ if (!this.#config.onWebSocket) {
1309
+ throw new errors.InternalError("onWebSocket handler not defined");
1310
+ }
1311
+
1312
+ try {
1313
+ // Set up state tracking to detect changes during WebSocket handling
1314
+ const stateBeforeHandler = this.#persistChanged;
1315
+
1316
+ // Track active websocket until it fully closes
1317
+ this.#activeRawWebSockets.add(websocket);
1318
+ this.#resetSleepTimer();
1319
+
1320
+ // Track socket close
1321
+ const onSocketClosed = () => {
1322
+ // Remove listener and socket from tracking
1323
+ try {
1324
+ websocket.removeEventListener("close", onSocketClosed);
1325
+ websocket.removeEventListener("error", onSocketClosed);
1326
+ } catch {}
1327
+ this.#activeRawWebSockets.delete(websocket);
1328
+ this.#resetSleepTimer();
1329
+ };
1330
+ try {
1331
+ websocket.addEventListener("close", onSocketClosed);
1332
+ websocket.addEventListener("error", onSocketClosed);
1333
+ } catch {}
1334
+
1335
+ // Handle WebSocket
1336
+ await this.#config.onWebSocket(this.actorContext, websocket, opts);
1337
+
1338
+ // If state changed during the handler, save it
1339
+ if (this.#persistChanged && !stateBeforeHandler) {
1340
+ await this.saveState({ immediate: true });
1341
+ }
1342
+ } catch (error) {
1343
+ logger().error("onWebSocket error", {
1344
+ error: stringifyError(error),
1345
+ });
1346
+ throw error;
1347
+ } finally {
1348
+ this.#savePersistThrottled();
1349
+ }
1350
+ }
1351
+
1352
+ // MARK: Lifecycle hooks
1353
+
1354
+ // MARK: Exposed methods
1355
+ /**
1356
+ * Gets the logger instance.
1357
+ */
1358
+ get log(): Logger {
1359
+ return instanceLogger();
1360
+ }
1361
+
1362
+ /**
1363
+ * Gets the name.
1364
+ */
1365
+ get name(): string {
1366
+ return this.#name;
1367
+ }
1368
+
1369
+ /**
1370
+ * Gets the key.
1371
+ */
1372
+ get key(): ActorKey {
1373
+ return this.#key;
1374
+ }
1375
+
1376
+ /**
1377
+ * Gets the region.
1378
+ */
1379
+ get region(): string {
1380
+ return this.#region;
1381
+ }
1382
+
1383
+ /**
1384
+ * Gets the scheduler.
1385
+ */
1386
+ get schedule(): Schedule {
1387
+ return this.#schedule;
1388
+ }
1389
+
1390
+ /**
1391
+ * Gets the map of connections.
1392
+ */
1393
+ get conns(): Map<ConnId, Conn<S, CP, CS, V, I, AD, DB>> {
1394
+ return this.#connections;
1395
+ }
1396
+
1397
+ /**
1398
+ * Gets the current state.
1399
+ *
1400
+ * Changing properties of this value will automatically be persisted.
1401
+ */
1402
+ get state(): S {
1403
+ this.#validateStateEnabled();
1404
+ return this.#persist.state;
1405
+ }
1406
+
1407
+ /**
1408
+ * Gets the database.
1409
+ * @experimental
1410
+ * @throws {DatabaseNotEnabled} If the database is not enabled.
1411
+ */
1412
+ get db(): InferDatabaseClient<DB> {
1413
+ if (!this.#db) {
1414
+ throw new errors.DatabaseNotEnabled();
1415
+ }
1416
+ return this.#db;
1417
+ }
1418
+
1419
+ /**
1420
+ * Sets the current state.
1421
+ *
1422
+ * This property will automatically be persisted.
1423
+ */
1424
+ set state(value: S) {
1425
+ this.#validateStateEnabled();
1426
+ this.#persist.state = value;
1427
+ }
1428
+
1429
+ get vars(): V {
1430
+ this.#validateVarsEnabled();
1431
+ invariant(this.#vars !== undefined, "vars not enabled");
1432
+ return this.#vars;
1433
+ }
1434
+
1435
+ /**
1436
+ * Broadcasts an event to all connected clients.
1437
+ * @param name - The name of the event.
1438
+ * @param args - The arguments to send with the event.
1439
+ */
1440
+ _broadcast<Args extends Array<unknown>>(name: string, ...args: Args) {
1441
+ this.#assertReady();
1442
+
1443
+ this.inspector.emitter.emit("eventFired", {
1444
+ type: "broadcast",
1445
+ eventName: name,
1446
+ args,
1447
+ });
1448
+
1449
+ // Send to all connected clients
1450
+ const subscriptions = this.#subscriptionIndex.get(name);
1451
+ if (!subscriptions) return;
1452
+
1453
+ const toClientSerializer = new CachedSerializer<protocol.ToClient>(
1454
+ {
1455
+ body: {
1456
+ tag: "Event",
1457
+ val: {
1458
+ name,
1459
+ args: bufferToArrayBuffer(cbor.encode(args)),
1460
+ },
1461
+ },
1462
+ },
1463
+ TO_CLIENT_VERSIONED,
1464
+ );
1465
+
1466
+ // Send message to clients
1467
+ for (const connection of subscriptions) {
1468
+ connection._sendMessage(toClientSerializer);
1469
+ }
1470
+ }
1471
+
1472
+ /**
1473
+ * Prevents the actor from sleeping until promise is complete.
1474
+ *
1475
+ * This allows the actor runtime to ensure that a promise completes while
1476
+ * returning from an action request early.
1477
+ *
1478
+ * @param promise - The promise to run in the background.
1479
+ */
1480
+ _waitUntil(promise: Promise<void>) {
1481
+ this.#assertReady();
1482
+
1483
+ // TODO: Should we force save the state?
1484
+ // Add logging to promise and make it non-failable
1485
+ const nonfailablePromise = promise
1486
+ .then(() => {
1487
+ logger().debug("wait until promise complete");
1488
+ })
1489
+ .catch((error) => {
1490
+ logger().error("wait until promise failed", {
1491
+ error: stringifyError(error),
1492
+ });
1493
+ });
1494
+ this.#backgroundPromises.push(nonfailablePromise);
1495
+ }
1496
+
1497
+ /**
1498
+ * Forces the state to get saved.
1499
+ *
1500
+ * This is helpful if running a long task that may fail later or when
1501
+ * running a background job that updates the state.
1502
+ *
1503
+ * @param opts - Options for saving the state.
1504
+ */
1505
+ async saveState(opts: SaveStateOptions) {
1506
+ this.#assertReady(opts.allowStoppingState);
1507
+
1508
+ if (this.#persistChanged) {
1509
+ if (opts.immediate) {
1510
+ // Save immediately
1511
+ await this.#savePersistInner();
1512
+ } else {
1513
+ // Create callback
1514
+ if (!this.#onPersistSavedPromise) {
1515
+ this.#onPersistSavedPromise = Promise.withResolvers();
1516
+ }
1517
+
1518
+ // Save state throttled
1519
+ this.#savePersistThrottled();
1520
+
1521
+ // Wait for save
1522
+ await this.#onPersistSavedPromise.promise;
1523
+ }
1524
+ }
1525
+ }
1526
+
1527
+ // MARK: Sleep
1528
+ /**
1529
+ * Reset timer from the last actor interaction that allows it to be put to sleep.
1530
+ *
1531
+ * This should be called any time a sleep-related event happens:
1532
+ * - Connection opens (will clear timer)
1533
+ * - Connection closes (will schedule timer if there are no open connections)
1534
+ * - Alarm triggers (will reset timer)
1535
+ *
1536
+ * We don't need to call this on events like individual action calls, since there will always be a connection open for these.
1537
+ **/
1538
+ #resetSleepTimer() {
1539
+ if (this.#config.options.noSleep || !this.#sleepingSupported) return;
1540
+
1541
+ const canSleep = this.#canSleep();
1542
+
1543
+ logger().debug("resetting sleep timer", {
1544
+ canSleep,
1545
+ existingTimeout: !!this.#sleepTimeout,
1546
+ });
1547
+
1548
+ if (this.#sleepTimeout) {
1549
+ clearTimeout(this.#sleepTimeout);
1550
+ this.#sleepTimeout = undefined;
1551
+ }
1552
+
1553
+ // Don't set a new timer if already sleeping
1554
+ if (this.#sleepCalled) return;
1555
+
1556
+ if (canSleep) {
1557
+ this.#sleepTimeout = setTimeout(() => {
1558
+ this._sleep().catch((error) => {
1559
+ logger().error("error during sleep", {
1560
+ error: stringifyError(error),
1561
+ });
1562
+ });
1563
+ }, this.#config.options.sleepTimeout);
1564
+ }
1565
+ }
1566
+
1567
+ /** If this actor can be put in a sleeping state. */
1568
+ #canSleep(): boolean {
1569
+ if (!this.#ready) return false;
1570
+
1571
+ // Check for active conns. This will also cover active actions, since all actions have a connection.
1572
+ for (const conn of this.#connections.values()) {
1573
+ if (conn.status === "connected") return false;
1574
+ }
1575
+
1576
+ // Do not sleep if raw fetches are in-flight
1577
+ if (this.#activeRawFetchCount > 0) return false;
1578
+
1579
+ // Do not sleep if there are raw websockets open
1580
+ if (this.#activeRawWebSockets.size > 0) return false;
1581
+
1582
+ return true;
1583
+ }
1584
+
1585
+ /** 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). */
1586
+ async _sleep() {
1587
+ const sleep = this.#actorDriver.sleep?.bind(
1588
+ this.#actorDriver,
1589
+ this.#actorId,
1590
+ );
1591
+ invariant(this.#sleepingSupported, "sleeping not supported");
1592
+ invariant(sleep, "no sleep on driver");
1593
+
1594
+ if (this.#sleepCalled) {
1595
+ logger().warn("already sleeping actor");
1596
+ return;
1597
+ }
1598
+ this.#sleepCalled = true;
1599
+
1600
+ logger().info("actor sleeping");
1601
+
1602
+ // Schedule sleep to happen on the next tick. This allows for any action that calls _sleep to complete.
1603
+ setImmediate(async () => {
1604
+ // The actor driver should call stop when ready to stop
1605
+ //
1606
+ // This will call _stop once Pegboard responds with the new status
1607
+ await sleep();
1608
+ });
1609
+ }
1610
+
1611
+ // MARK: Stop
1612
+ async _stop() {
1613
+ if (this.#stopCalled) {
1614
+ logger().warn("already stopping actor");
1615
+ return;
1616
+ }
1617
+ this.#stopCalled = true;
1618
+
1619
+ logger().info("actor stopping");
1620
+
1621
+ // Abort any listeners waiting for shutdown
1622
+ try {
1623
+ this.#abortController.abort();
1624
+ } catch {}
1625
+
1626
+ // Call onStop lifecycle hook if defined
1627
+ if (this.#config.onStop) {
1628
+ try {
1629
+ logger().debug("calling onStop");
1630
+ const result = this.#config.onStop(this.actorContext);
1631
+ if (result instanceof Promise) {
1632
+ await deadline(result, this.#config.options.onStopTimeout);
1633
+ }
1634
+ logger().debug("onStop completed");
1635
+ } catch (error) {
1636
+ if (error instanceof DeadlineError) {
1637
+ logger().error("onStop timed out");
1638
+ } else {
1639
+ logger().error("error in onStop", {
1640
+ error: stringifyError(error),
1641
+ });
1642
+ }
1643
+ }
1644
+ }
1645
+
1646
+ // Disconnect existing connections
1647
+ const promises: Promise<unknown>[] = [];
1648
+ for (const connection of this.#connections.values()) {
1649
+ promises.push(connection.disconnect());
1650
+
1651
+ // TODO: Figure out how to abort HTTP requests on shutdown
1652
+ }
1653
+
1654
+ // Wait for any background tasks to finish, with timeout
1655
+ await this.#waitBackgroundPromises(this.#config.options.waitUntilTimeout);
1656
+
1657
+ // Clear timeouts
1658
+ if (this.#pendingSaveTimeout) clearTimeout(this.#pendingSaveTimeout);
1659
+ if (this.#sleepTimeout) clearTimeout(this.#sleepTimeout);
1660
+ if (this.#checkConnLivenessInterval)
1661
+ clearInterval(this.#checkConnLivenessInterval);
1662
+
1663
+ // Write state
1664
+ await this.saveState({ immediate: true, allowStoppingState: true });
1665
+
1666
+ // Await all `close` event listeners with 1.5 second timeout
1667
+ const res = Promise.race([
1668
+ Promise.all(promises).then(() => false),
1669
+ new Promise<boolean>((res) =>
1670
+ globalThis.setTimeout(() => res(true), 1500),
1671
+ ),
1672
+ ]);
1673
+
1674
+ if (await res) {
1675
+ logger().warn(
1676
+ "timed out waiting for connections to close, shutting down anyway",
1677
+ );
1678
+ }
1679
+
1680
+ // Wait for queues to finish
1681
+ if (this.#persistWriteQueue.runningDrainLoop)
1682
+ await this.#persistWriteQueue.runningDrainLoop;
1683
+ if (this.#alarmWriteQueue.runningDrainLoop)
1684
+ await this.#alarmWriteQueue.runningDrainLoop;
1685
+ }
1686
+
1687
+ /** Abort signal that fires when the actor is stopping. */
1688
+ get abortSignal(): AbortSignal {
1689
+ return this.#abortController.signal;
1690
+ }
1691
+
1692
+ /** Wait for background waitUntil promises with a timeout. */
1693
+ async #waitBackgroundPromises(timeoutMs: number) {
1694
+ const pending = this.#backgroundPromises;
1695
+ if (pending.length === 0) {
1696
+ logger().debug("no background promises");
1697
+ return;
1698
+ }
1699
+
1700
+ // Race promises with timeout to determine if pending promises settled fast enough
1701
+ const timedOut = await Promise.race([
1702
+ Promise.allSettled(pending).then(() => false),
1703
+ new Promise<true>((resolve) =>
1704
+ setTimeout(() => resolve(true), timeoutMs),
1705
+ ),
1706
+ ]);
1707
+
1708
+ if (timedOut) {
1709
+ logger().error(
1710
+ "timed out waiting for background tasks, background promises may have leaked",
1711
+ {
1712
+ count: pending.length,
1713
+ timeoutMs,
1714
+ },
1715
+ );
1716
+ } else {
1717
+ logger().debug("background promises finished");
1718
+ }
1719
+ }
1720
+
1721
+ // MARK: BARE Conversion Helpers
1722
+ #convertToBarePersisted(
1723
+ persist: PersistedActor<S, CP, CS, I>,
1724
+ ): bareSchema.PersistedActor {
1725
+ return {
1726
+ input:
1727
+ persist.input !== undefined
1728
+ ? bufferToArrayBuffer(cbor.encode(persist.input))
1729
+ : null,
1730
+ hasInitialized: persist.hasInitiated,
1731
+ state: bufferToArrayBuffer(cbor.encode(persist.state)),
1732
+ connections: persist.connections.map((conn) => ({
1733
+ id: conn.connId,
1734
+ token: conn.token,
1735
+ driver: conn.connDriver as string,
1736
+ driverState: bufferToArrayBuffer(
1737
+ cbor.encode(conn.connDriverState || {}),
1738
+ ),
1739
+ parameters: bufferToArrayBuffer(cbor.encode(conn.params || {})),
1740
+ state: bufferToArrayBuffer(cbor.encode(conn.state || {})),
1741
+ auth:
1742
+ conn.authData !== undefined
1743
+ ? bufferToArrayBuffer(cbor.encode(conn.authData))
1744
+ : null,
1745
+ subscriptions: conn.subscriptions.map((sub) => ({
1746
+ eventName: sub.eventName,
1747
+ })),
1748
+ lastSeen: BigInt(conn.lastSeen),
1749
+ })),
1750
+ scheduledEvents: persist.scheduledEvents.map((event) => ({
1751
+ eventId: event.eventId,
1752
+ timestamp: BigInt(event.timestamp),
1753
+ kind: {
1754
+ tag: "GenericPersistedScheduleEvent" as const,
1755
+ val: {
1756
+ action: event.kind.generic.actionName,
1757
+ args: event.kind.generic.args ?? null,
1758
+ },
1759
+ },
1760
+ })),
1761
+ };
1762
+ }
1763
+
1764
+ #convertFromBarePersisted(
1765
+ bareData: bareSchema.PersistedActor,
1766
+ ): PersistedActor<S, CP, CS, I> {
1767
+ return {
1768
+ input: bareData.input
1769
+ ? cbor.decode(new Uint8Array(bareData.input))
1770
+ : undefined,
1771
+ hasInitiated: bareData.hasInitialized,
1772
+ state: cbor.decode(new Uint8Array(bareData.state)),
1773
+ connections: bareData.connections.map((conn) => ({
1774
+ connId: conn.id,
1775
+ token: conn.token,
1776
+ connDriver: conn.driver as ConnectionDriver,
1777
+ connDriverState: cbor.decode(new Uint8Array(conn.driverState)),
1778
+ params: cbor.decode(new Uint8Array(conn.parameters)),
1779
+ state: cbor.decode(new Uint8Array(conn.state)),
1780
+ authData: conn.auth
1781
+ ? cbor.decode(new Uint8Array(conn.auth))
1782
+ : undefined,
1783
+ subscriptions: conn.subscriptions.map((sub) => ({
1784
+ eventName: sub.eventName,
1785
+ })),
1786
+ lastSeen: Number(conn.lastSeen),
1787
+ })),
1788
+ scheduledEvents: bareData.scheduledEvents.map((event) => ({
1789
+ eventId: event.eventId,
1790
+ timestamp: Number(event.timestamp),
1791
+ kind: {
1792
+ generic: {
1793
+ actionName: event.kind.val.action,
1794
+ args: event.kind.val.args,
1795
+ },
1796
+ },
1797
+ })),
1798
+ };
1799
+ }
1800
+ }