rivetkit 2.0.2 → 2.0.3

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