rivetkit 2.0.2 → 2.0.4

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