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,686 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as fsSync from "node:fs";
3
+ import * as fs from "node:fs/promises";
4
+ import * as path from "node:path";
5
+ import invariant from "invariant";
6
+ import { lookupInRegistry } from "@/actor/definition";
7
+ import { ActorAlreadyExists } from "@/actor/errors";
8
+ import {
9
+ createGenericConnDrivers,
10
+ GenericConnGlobalState,
11
+ } from "@/actor/generic-conn-driver";
12
+ import type { AnyActorInstance } from "@/actor/instance";
13
+ import type { ActorKey } from "@/actor/mod";
14
+ import { generateRandomString } from "@/actor/utils";
15
+ import type { AnyClient } from "@/client/client";
16
+ import {
17
+ type ActorDriver,
18
+ serializeEmptyPersistData,
19
+ } from "@/driver-helpers/mod";
20
+ import type { RegistryConfig } from "@/registry/config";
21
+ import type { RunConfig } from "@/registry/run-config";
22
+ import type * as schema from "@/schemas/file-system-driver/mod";
23
+ import {
24
+ ACTOR_ALARM_VERSIONED,
25
+ ACTOR_STATE_VERSIONED,
26
+ } from "@/schemas/file-system-driver/versioned";
27
+ import {
28
+ bufferToArrayBuffer,
29
+ type LongTimeoutHandle,
30
+ SinglePromiseQueue,
31
+ setLongTimeout,
32
+ stringifyError,
33
+ } from "@/utils";
34
+ import { logger } from "./log";
35
+ import {
36
+ ensureDirectoryExists,
37
+ ensureDirectoryExistsSync,
38
+ getStoragePath,
39
+ } from "./utils";
40
+
41
+ // Actor handler to track running instances
42
+
43
+ interface ActorEntry {
44
+ id: string;
45
+
46
+ state?: schema.ActorState;
47
+ /** Promise for loading the actor state. */
48
+ loadPromise?: Promise<ActorEntry>;
49
+
50
+ actor?: AnyActorInstance;
51
+ /** Promise for starting the actor. */
52
+ startPromise?: PromiseWithResolvers<void>;
53
+
54
+ genericConnGlobalState: GenericConnGlobalState;
55
+
56
+ alarmTimeout?: LongTimeoutHandle;
57
+ /** The timestamp currently scheduled for this actor's alarm (ms since epoch). */
58
+ alarmTimestamp?: number;
59
+
60
+ /** Resolver for pending write operations that need to be notified when any write completes */
61
+ pendingWriteResolver?: PromiseWithResolvers<void>;
62
+
63
+ /** If the actor has been removed by destroy or sleep. */
64
+ removed: boolean;
65
+ }
66
+
67
+ /**
68
+ * Global state for the file system driver
69
+ */
70
+ export class FileSystemGlobalState {
71
+ #storagePath: string;
72
+ #stateDir: string;
73
+ #dbsDir: string;
74
+ #alarmsDir: string;
75
+
76
+ #persist: boolean;
77
+ #actors = new Map<string, ActorEntry>();
78
+ #actorCountOnStartup: number = 0;
79
+
80
+ #runnerParams?: {
81
+ registryConfig: RegistryConfig;
82
+ runConfig: RunConfig;
83
+ inlineClient: AnyClient;
84
+ actorDriver: ActorDriver;
85
+ };
86
+
87
+ get persist(): boolean {
88
+ return this.#persist;
89
+ }
90
+
91
+ get storagePath() {
92
+ return this.#storagePath;
93
+ }
94
+
95
+ get actorCountOnStartup() {
96
+ return this.#actorCountOnStartup;
97
+ }
98
+
99
+ constructor(persist: boolean = true, customPath?: string) {
100
+ this.#persist = persist;
101
+ this.#storagePath = persist ? getStoragePath(customPath) : "/tmp";
102
+ this.#stateDir = path.join(this.#storagePath, "state");
103
+ this.#dbsDir = path.join(this.#storagePath, "databases");
104
+ this.#alarmsDir = path.join(this.#storagePath, "alarms");
105
+
106
+ if (this.#persist) {
107
+ // Ensure storage directories exist synchronously during initialization
108
+ ensureDirectoryExistsSync(this.#stateDir);
109
+ ensureDirectoryExistsSync(this.#dbsDir);
110
+ ensureDirectoryExistsSync(this.#alarmsDir);
111
+
112
+ try {
113
+ const actorIds = fsSync.readdirSync(this.#stateDir);
114
+ this.#actorCountOnStartup = actorIds.length;
115
+ } catch (error) {
116
+ logger().error({ msg: "failed to count actors", error });
117
+ }
118
+
119
+ logger().debug({
120
+ msg: "file system driver ready",
121
+ dir: this.#storagePath,
122
+ actorCount: this.#actorCountOnStartup,
123
+ });
124
+
125
+ // Cleanup stale temp files on startup
126
+ try {
127
+ this.#cleanupTempFilesSync();
128
+ } catch (err) {
129
+ logger().error({ msg: "failed to cleanup temp files", error: err });
130
+ }
131
+ } else {
132
+ logger().debug({ msg: "memory driver ready" });
133
+ }
134
+ }
135
+
136
+ getActorStatePath(actorId: string): string {
137
+ return path.join(this.#stateDir, actorId);
138
+ }
139
+
140
+ getActorDbPath(actorId: string): string {
141
+ return path.join(this.#dbsDir, `${actorId}.db`);
142
+ }
143
+
144
+ getActorAlarmPath(actorId: string): string {
145
+ return path.join(this.#alarmsDir, actorId);
146
+ }
147
+
148
+ async *getActorsIterator(params: {
149
+ cursor?: string;
150
+ }): AsyncGenerator<schema.ActorState> {
151
+ let actorIds = Array.from(this.#actors.keys()).sort();
152
+
153
+ // Check if state directory exists first
154
+ if (fsSync.existsSync(this.#stateDir)) {
155
+ actorIds = fsSync
156
+ .readdirSync(this.#stateDir)
157
+ .filter((id) => !id.includes(".tmp"))
158
+ .sort();
159
+ }
160
+
161
+ const startIndex = params.cursor ? actorIds.indexOf(params.cursor) + 1 : 0;
162
+
163
+ for (let i = startIndex; i < actorIds.length; i++) {
164
+ const actorId = actorIds[i];
165
+ if (!actorId) {
166
+ continue;
167
+ }
168
+
169
+ try {
170
+ const state = await this.loadActorStateOrError(actorId);
171
+ yield state;
172
+ } catch (error) {
173
+ logger().error({ msg: "failed to load actor state", actorId, error });
174
+ }
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Ensures an entry exists for this actor.
180
+ *
181
+ * Used for #createActor and #loadActor.
182
+ */
183
+ #upsertEntry(actorId: string): ActorEntry {
184
+ let entry = this.#actors.get(actorId);
185
+ if (entry) {
186
+ return entry;
187
+ }
188
+
189
+ entry = {
190
+ id: actorId,
191
+ genericConnGlobalState: new GenericConnGlobalState(),
192
+ removed: false,
193
+ };
194
+ this.#actors.set(actorId, entry);
195
+ return entry;
196
+ }
197
+
198
+ /**
199
+ * Creates a new actor and writes to file system.
200
+ */
201
+ async createActor(
202
+ actorId: string,
203
+ name: string,
204
+ key: ActorKey,
205
+ input: unknown | undefined,
206
+ ): Promise<ActorEntry> {
207
+ // TODO: Does not check if actor already exists on fs
208
+
209
+ if (this.#actors.has(actorId)) {
210
+ throw new ActorAlreadyExists(name, key);
211
+ }
212
+
213
+ const entry = this.#upsertEntry(actorId);
214
+ entry.state = {
215
+ actorId,
216
+ name,
217
+ key,
218
+ createdAt: BigInt(Date.now()),
219
+ persistedData: bufferToArrayBuffer(serializeEmptyPersistData(input)),
220
+ };
221
+ await this.writeActor(actorId, entry.state);
222
+ return entry;
223
+ }
224
+
225
+ /**
226
+ * Loads the actor from disk or returns the existing actor entry. This will return an entry even if the actor does not actually exist.
227
+ */
228
+ async loadActor(actorId: string): Promise<ActorEntry> {
229
+ const entry = this.#upsertEntry(actorId);
230
+
231
+ // Check if already loaded
232
+ if (entry.state) {
233
+ return entry;
234
+ }
235
+
236
+ // If not persisted, then don't load from FS
237
+ if (!this.#persist) {
238
+ return entry;
239
+ }
240
+
241
+ // If state is currently being loaded, wait for it
242
+ if (entry.loadPromise) {
243
+ await entry.loadPromise;
244
+ return entry;
245
+ }
246
+
247
+ // Start loading state
248
+ entry.loadPromise = this.loadActorState(entry);
249
+ return entry.loadPromise;
250
+ }
251
+
252
+ private async loadActorState(entry: ActorEntry) {
253
+ const stateFilePath = this.getActorStatePath(entry.id);
254
+
255
+ // Read & parse file
256
+ try {
257
+ const stateData = await fs.readFile(stateFilePath);
258
+
259
+ // Cache the loaded state in handler
260
+ entry.state = ACTOR_STATE_VERSIONED.deserializeWithEmbeddedVersion(
261
+ new Uint8Array(stateData),
262
+ );
263
+
264
+ return entry;
265
+ } catch (innerError: any) {
266
+ // File does not exist, meaning the actor does not exist
267
+ if (innerError.code === "ENOENT") {
268
+ entry.loadPromise = undefined;
269
+ return entry;
270
+ }
271
+
272
+ // For other errors, throw
273
+ const error = new Error(`Failed to load actor state: ${innerError}`);
274
+ throw error;
275
+ }
276
+ }
277
+
278
+ async loadOrCreateActor(
279
+ actorId: string,
280
+ name: string,
281
+ key: ActorKey,
282
+ input: unknown | undefined,
283
+ ): Promise<ActorEntry> {
284
+ // Attempt to load actor
285
+ const entry = await this.loadActor(actorId);
286
+
287
+ // If no state for this actor, then create & write state
288
+ if (!entry.state) {
289
+ entry.state = {
290
+ actorId,
291
+ name,
292
+ key: key as readonly string[],
293
+ createdAt: BigInt(Date.now()),
294
+ persistedData: bufferToArrayBuffer(serializeEmptyPersistData(input)),
295
+ };
296
+ await this.writeActor(actorId, entry.state);
297
+ }
298
+ return entry;
299
+ }
300
+
301
+ async sleepActor(actorId: string) {
302
+ invariant(
303
+ this.#persist,
304
+ "cannot sleep actor with memory driver, must use file system driver",
305
+ );
306
+
307
+ const actor = this.#actors.get(actorId);
308
+ invariant(actor, `tried to sleep ${actorId}, does not exist`);
309
+
310
+ // Wait for actor to fully start before stopping it to avoid race conditions
311
+ if (actor.loadPromise) await actor.loadPromise.catch();
312
+ if (actor.startPromise?.promise) await actor.startPromise.promise.catch();
313
+
314
+ // Mark as removed
315
+ actor.removed = true;
316
+
317
+ // Stop actor
318
+ invariant(actor.actor, "actor should be loaded");
319
+ await actor.actor._stop();
320
+
321
+ // Remove from map after stop is complete
322
+ this.#actors.delete(actorId);
323
+ }
324
+
325
+ /**
326
+ * Save actor state to disk.
327
+ */
328
+ async writeActor(actorId: string, state: schema.ActorState): Promise<void> {
329
+ if (!this.#persist) {
330
+ return;
331
+ }
332
+
333
+ const entry = this.#actors.get(actorId);
334
+ invariant(entry, "actor entry does not exist");
335
+
336
+ await this.#performWrite(actorId, state);
337
+ }
338
+
339
+ async setActorAlarm(actorId: string, timestamp: number) {
340
+ const entry = this.#actors.get(actorId);
341
+ invariant(entry, "actor entry does not exist");
342
+
343
+ // Persist alarm to disk
344
+ if (this.#persist) {
345
+ const alarmPath = this.getActorAlarmPath(actorId);
346
+ const tempPath = `${alarmPath}.tmp.${crypto.randomUUID()}`;
347
+ try {
348
+ await ensureDirectoryExists(path.dirname(alarmPath));
349
+ const alarmData: schema.ActorAlarm = {
350
+ actorId,
351
+ timestamp: BigInt(timestamp),
352
+ };
353
+ const data =
354
+ ACTOR_ALARM_VERSIONED.serializeWithEmbeddedVersion(alarmData);
355
+ await fs.writeFile(tempPath, data);
356
+ await fs.rename(tempPath, alarmPath);
357
+ } catch (error) {
358
+ try {
359
+ await fs.unlink(tempPath);
360
+ } catch {}
361
+ logger().error({ msg: "failed to write alarm", actorId, error });
362
+ throw new Error(`Failed to write alarm: ${error}`);
363
+ }
364
+ }
365
+
366
+ // Schedule timeout
367
+ this.#scheduleAlarmTimeout(actorId, timestamp);
368
+ }
369
+
370
+ /**
371
+ * Perform the actual write operation with atomic writes
372
+ */
373
+ async #performWrite(
374
+ actorId: string,
375
+ state: schema.ActorState,
376
+ ): Promise<void> {
377
+ const dataPath = this.getActorStatePath(actorId);
378
+ // Generate unique temp filename to prevent any race conditions
379
+ const tempPath = `${dataPath}.tmp.${crypto.randomUUID()}`;
380
+
381
+ try {
382
+ // Create directory if needed
383
+ await ensureDirectoryExists(path.dirname(dataPath));
384
+
385
+ // Convert to BARE types for serialization
386
+ const bareState: schema.ActorState = {
387
+ actorId: state.actorId,
388
+ name: state.name,
389
+ key: state.key,
390
+ createdAt: state.createdAt,
391
+ persistedData: state.persistedData,
392
+ };
393
+
394
+ // Perform atomic write
395
+ const serializedState =
396
+ ACTOR_STATE_VERSIONED.serializeWithEmbeddedVersion(bareState);
397
+ await fs.writeFile(tempPath, serializedState);
398
+ await fs.rename(tempPath, dataPath);
399
+ } catch (error) {
400
+ // Cleanup temp file on error
401
+ try {
402
+ await fs.unlink(tempPath);
403
+ } catch {
404
+ // Ignore cleanup errors
405
+ }
406
+ logger().error({ msg: "failed to save actor state", actorId, error });
407
+ throw new Error(`Failed to save actor state: ${error}`);
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Call this method after the actor driver has been initiated.
413
+ *
414
+ * This will trigger all initial alarms from the file system.
415
+ *
416
+ * This needs to be sync since DriverConfig.actor is sync
417
+ */
418
+ onRunnerStart(
419
+ registryConfig: RegistryConfig,
420
+ runConfig: RunConfig,
421
+ inlineClient: AnyClient,
422
+ actorDriver: ActorDriver,
423
+ ) {
424
+ if (this.#runnerParams) {
425
+ return;
426
+ }
427
+
428
+ // Save runner params for future use
429
+ this.#runnerParams = {
430
+ registryConfig,
431
+ runConfig,
432
+ inlineClient,
433
+ actorDriver,
434
+ };
435
+
436
+ // Load alarms from disk and schedule timeouts
437
+ try {
438
+ this.#loadAlarmsSync();
439
+ } catch (err) {
440
+ logger().error({ msg: "failed to load alarms on startup", error: err });
441
+ }
442
+ }
443
+
444
+ async startActor(
445
+ registryConfig: RegistryConfig,
446
+ runConfig: RunConfig,
447
+ inlineClient: AnyClient,
448
+ actorDriver: ActorDriver,
449
+ actorId: string,
450
+ ): Promise<AnyActorInstance> {
451
+ // Get the actor metadata
452
+ const entry = await this.loadActor(actorId);
453
+ if (!entry.state) {
454
+ throw new Error(`Actor does exist and cannot be started: ${actorId}`);
455
+ }
456
+
457
+ // Actor already starting
458
+ if (entry.startPromise) {
459
+ await entry.startPromise.promise;
460
+ invariant(entry.actor, "actor should have loaded");
461
+ return entry.actor;
462
+ }
463
+
464
+ // Actor already loaded
465
+ if (entry.actor) {
466
+ return entry.actor;
467
+ }
468
+
469
+ // Create start promise
470
+ entry.startPromise = Promise.withResolvers();
471
+
472
+ try {
473
+ // Create actor
474
+ const definition = lookupInRegistry(registryConfig, entry.state.name);
475
+ entry.actor = definition.instantiate();
476
+
477
+ // Start actor
478
+ const connDrivers = createGenericConnDrivers(
479
+ entry.genericConnGlobalState,
480
+ );
481
+ await entry.actor.start(
482
+ connDrivers,
483
+ actorDriver,
484
+ inlineClient,
485
+ actorId,
486
+ entry.state.name,
487
+ entry.state.key as string[],
488
+ "unknown",
489
+ );
490
+
491
+ // Finish
492
+ entry.startPromise.resolve();
493
+ entry.startPromise = undefined;
494
+
495
+ return entry.actor;
496
+ } catch (innerError) {
497
+ const error = new Error(
498
+ `Failed to start actor ${actorId}: ${innerError}`,
499
+ { cause: innerError },
500
+ );
501
+ entry.startPromise?.reject(error);
502
+ entry.startPromise = undefined;
503
+ throw error;
504
+ }
505
+ }
506
+
507
+ async loadActorStateOrError(actorId: string): Promise<schema.ActorState> {
508
+ const state = (await this.loadActor(actorId)).state;
509
+ if (!state) throw new Error(`Actor does not exist: ${actorId}`);
510
+ return state;
511
+ }
512
+
513
+ getActorOrError(actorId: string): ActorEntry {
514
+ const entry = this.#actors.get(actorId);
515
+ if (!entry) throw new Error(`No entry for actor: ${actorId}`);
516
+ return entry;
517
+ }
518
+
519
+ async createDatabase(actorId: string): Promise<string | undefined> {
520
+ return this.getActorDbPath(actorId);
521
+ }
522
+
523
+ /**
524
+ * Load all persisted alarms from disk and schedule their timers.
525
+ */
526
+ #loadAlarmsSync(): void {
527
+ try {
528
+ const files = fsSync.existsSync(this.#alarmsDir)
529
+ ? fsSync.readdirSync(this.#alarmsDir)
530
+ : [];
531
+ for (const file of files) {
532
+ // Skip temp files
533
+ if (file.includes(".tmp.")) continue;
534
+ const fullPath = path.join(this.#alarmsDir, file);
535
+ try {
536
+ const buf = fsSync.readFileSync(fullPath);
537
+ const alarmData =
538
+ ACTOR_ALARM_VERSIONED.deserializeWithEmbeddedVersion(
539
+ new Uint8Array(buf),
540
+ );
541
+ const timestamp = Number(alarmData.timestamp);
542
+ if (Number.isFinite(timestamp)) {
543
+ this.#scheduleAlarmTimeout(alarmData.actorId, timestamp);
544
+ } else {
545
+ logger().debug({ msg: "invalid alarm file contents", file });
546
+ }
547
+ } catch (err) {
548
+ logger().error({
549
+ msg: "failed to read alarm file",
550
+ file,
551
+ error: stringifyError(err),
552
+ });
553
+ }
554
+ }
555
+ } catch (err) {
556
+ logger().error({ msg: "failed to list alarms directory", error: err });
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Schedule an alarm timer for an actor without writing to disk.
562
+ */
563
+ #scheduleAlarmTimeout(actorId: string, timestamp: number) {
564
+ const entry = this.#upsertEntry(actorId);
565
+
566
+ // If there's already an earlier alarm scheduled, do not override it.
567
+ if (
568
+ entry.alarmTimestamp !== undefined &&
569
+ timestamp >= entry.alarmTimestamp
570
+ ) {
571
+ logger().debug({
572
+ msg: "skipping alarm schedule (later than existing)",
573
+ actorId,
574
+ timestamp,
575
+ current: entry.alarmTimestamp,
576
+ });
577
+ return;
578
+ }
579
+
580
+ logger().debug({ msg: "scheduling alarm", actorId, timestamp });
581
+
582
+ // Cancel existing timeout and update the current scheduled timestamp
583
+ entry.alarmTimeout?.abort();
584
+ entry.alarmTimestamp = timestamp;
585
+
586
+ const delay = Math.max(0, timestamp - Date.now());
587
+ entry.alarmTimeout = setLongTimeout(async () => {
588
+ // Clear currently scheduled timestamp as this alarm is firing now
589
+ entry.alarmTimestamp = undefined;
590
+ // On trigger: remove persisted alarm file
591
+ if (this.#persist) {
592
+ try {
593
+ await fs.unlink(this.getActorAlarmPath(actorId));
594
+ } catch (err: any) {
595
+ if (err?.code !== "ENOENT") {
596
+ logger().debug({
597
+ msg: "failed to remove alarm file",
598
+ actorId,
599
+ error: stringifyError(err),
600
+ });
601
+ }
602
+ }
603
+ }
604
+
605
+ try {
606
+ logger().debug({ msg: "triggering alarm", actorId, timestamp });
607
+
608
+ // Ensure actor state exists and start actor if needed
609
+ const loaded = await this.loadActor(actorId);
610
+ if (!loaded.state) throw new Error(`Actor does not exist: ${actorId}`);
611
+
612
+ // Start actor if not already running
613
+ const runnerParams = this.#runnerParams;
614
+ invariant(runnerParams, "missing runner params");
615
+ if (!loaded.actor) {
616
+ await this.startActor(
617
+ runnerParams.registryConfig,
618
+ runnerParams.runConfig,
619
+ runnerParams.inlineClient,
620
+ runnerParams.actorDriver,
621
+ actorId,
622
+ );
623
+ }
624
+
625
+ invariant(loaded.actor, "actor should be loaded after wake");
626
+ await loaded.actor._onAlarm();
627
+ } catch (err) {
628
+ logger().error({
629
+ msg: "failed to handle alarm",
630
+ actorId,
631
+ error: stringifyError(err),
632
+ });
633
+ }
634
+ }, delay);
635
+ }
636
+
637
+ getOrCreateInspectorAccessToken(): string {
638
+ const tokenPath = path.join(this.#storagePath, "inspector-token");
639
+ if (fsSync.existsSync(tokenPath)) {
640
+ return fsSync.readFileSync(tokenPath, "utf-8");
641
+ }
642
+
643
+ const newToken = generateRandomString();
644
+ fsSync.writeFileSync(tokenPath, newToken);
645
+ return newToken;
646
+ }
647
+
648
+ /**
649
+ * Cleanup stale temp files on startup (synchronous)
650
+ */
651
+ #cleanupTempFilesSync(): void {
652
+ try {
653
+ const files = fsSync.readdirSync(this.#stateDir);
654
+ const tempFiles = files.filter((f) => f.includes(".tmp."));
655
+
656
+ const oneHourAgo = Date.now() - 3600000; // 1 hour in ms
657
+
658
+ for (const tempFile of tempFiles) {
659
+ try {
660
+ const fullPath = path.join(this.#stateDir, tempFile);
661
+ const stat = fsSync.statSync(fullPath);
662
+
663
+ // Remove if older than 1 hour
664
+ if (stat.mtimeMs < oneHourAgo) {
665
+ fsSync.unlinkSync(fullPath);
666
+ logger().info({
667
+ msg: "cleaned up stale temp file",
668
+ file: tempFile,
669
+ });
670
+ }
671
+ } catch (err) {
672
+ logger().debug({
673
+ msg: "failed to cleanup temp file",
674
+ file: tempFile,
675
+ error: err,
676
+ });
677
+ }
678
+ }
679
+ } catch (err) {
680
+ logger().error({
681
+ msg: "failed to read actors directory for cleanup",
682
+ error: err,
683
+ });
684
+ }
685
+ }
686
+ }
@@ -0,0 +1,5 @@
1
+ import { getLogger } from "@/common/log";
2
+
3
+ export function logger() {
4
+ return getLogger("driver-fs");
5
+ }