rivetkit 2.0.24-rc.1 → 2.0.24

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 (228) hide show
  1. package/dist/schemas/actor-persist/v2.ts +3 -3
  2. package/dist/schemas/actor-persist/v3.ts +274 -0
  3. package/dist/schemas/client-protocol/v2.ts +432 -0
  4. package/dist/schemas/file-system-driver/v2.ts +136 -0
  5. package/dist/tsup/actor/errors.cjs +2 -4
  6. package/dist/tsup/actor/errors.cjs.map +1 -1
  7. package/dist/tsup/actor/errors.d.cts +7 -10
  8. package/dist/tsup/actor/errors.d.ts +7 -10
  9. package/dist/tsup/actor/errors.js +9 -11
  10. package/dist/tsup/{actor-router-consts-B3Lu87yJ.d.cts → actor-router-consts-DzI2szci.d.cts} +5 -9
  11. package/dist/tsup/{actor-router-consts-B3Lu87yJ.d.ts → actor-router-consts-DzI2szci.d.ts} +5 -9
  12. package/dist/tsup/{chunk-HHFKKVLR.cjs → chunk-3543NCSN.cjs} +45 -57
  13. package/dist/tsup/chunk-3543NCSN.cjs.map +1 -0
  14. package/dist/tsup/chunk-4SHILYS5.cjs +5694 -0
  15. package/dist/tsup/chunk-4SHILYS5.cjs.map +1 -0
  16. package/dist/tsup/{chunk-ZTH3KYFH.cjs → chunk-5BZO5XPS.cjs} +3 -3
  17. package/dist/tsup/{chunk-ZTH3KYFH.cjs.map → chunk-5BZO5XPS.cjs.map} +1 -1
  18. package/dist/tsup/{chunk-PLUN2NQT.js → chunk-BAIGSF64.js} +189 -187
  19. package/dist/tsup/chunk-BAIGSF64.js.map +1 -0
  20. package/dist/tsup/{chunk-SHVX2QUR.cjs → chunk-CHLZBSI2.cjs} +17 -17
  21. package/dist/tsup/chunk-CHLZBSI2.cjs.map +1 -0
  22. package/dist/tsup/chunk-D3SLADUD.cjs +512 -0
  23. package/dist/tsup/chunk-D3SLADUD.cjs.map +1 -0
  24. package/dist/tsup/{chunk-KSRXX3Z4.cjs → chunk-D6762AOA.cjs} +20 -25
  25. package/dist/tsup/chunk-D6762AOA.cjs.map +1 -0
  26. package/dist/tsup/{chunk-7L65NNWP.cjs → chunk-DLK5YCTN.cjs} +187 -185
  27. package/dist/tsup/chunk-DLK5YCTN.cjs.map +1 -0
  28. package/dist/tsup/{chunk-YBG6R7LX.js → chunk-DUJQWGYD.js} +3 -7
  29. package/dist/tsup/chunk-DUJQWGYD.js.map +1 -0
  30. package/dist/tsup/{chunk-CD33GT6Z.js → chunk-EIPANQMF.js} +2 -2
  31. package/dist/tsup/{chunk-2JYPS5YM.cjs → chunk-ESMTDP7G.cjs} +6 -6
  32. package/dist/tsup/chunk-ESMTDP7G.cjs.map +1 -0
  33. package/dist/tsup/{chunk-VHGY7PU5.cjs → chunk-FVAKREFB.cjs} +1900 -1737
  34. package/dist/tsup/chunk-FVAKREFB.cjs.map +1 -0
  35. package/dist/tsup/{chunk-BLK27ES3.js → chunk-I3XT7WOF.js} +44 -56
  36. package/dist/tsup/chunk-I3XT7WOF.js.map +1 -0
  37. package/dist/tsup/{chunk-YBHYXIP6.js → chunk-IMDS5T42.js} +3 -3
  38. package/dist/tsup/chunk-IMDS5T42.js.map +1 -0
  39. package/dist/tsup/{chunk-INNFK746.cjs → chunk-J3HZJF2P.cjs} +10 -14
  40. package/dist/tsup/chunk-J3HZJF2P.cjs.map +1 -0
  41. package/dist/tsup/{chunk-BYMKMOBS.js → chunk-MBBJUHSP.js} +1844 -1681
  42. package/dist/tsup/chunk-MBBJUHSP.js.map +1 -0
  43. package/dist/tsup/{chunk-BOMZS2TJ.js → chunk-MO5CB6MD.js} +9 -9
  44. package/dist/tsup/chunk-MO5CB6MD.js.map +1 -0
  45. package/dist/tsup/chunk-OFOTPKAH.js +512 -0
  46. package/dist/tsup/chunk-OFOTPKAH.js.map +1 -0
  47. package/dist/tsup/{chunk-G64QUEDJ.js → chunk-W6RDS6NW.js} +23 -28
  48. package/dist/tsup/chunk-W6RDS6NW.js.map +1 -0
  49. package/dist/tsup/{chunk-36JJ4IQB.cjs → chunk-YC5DUHPM.cjs} +4 -8
  50. package/dist/tsup/chunk-YC5DUHPM.cjs.map +1 -0
  51. package/dist/tsup/{chunk-FX7TWFQR.js → chunk-YC7YPM2T.js} +2 -6
  52. package/dist/tsup/chunk-YC7YPM2T.js.map +1 -0
  53. package/dist/tsup/{chunk-227FEWMB.js → chunk-ZSPU5R4C.js} +3322 -2251
  54. package/dist/tsup/chunk-ZSPU5R4C.js.map +1 -0
  55. package/dist/tsup/client/mod.cjs +9 -9
  56. package/dist/tsup/client/mod.d.cts +5 -7
  57. package/dist/tsup/client/mod.d.ts +5 -7
  58. package/dist/tsup/client/mod.js +8 -8
  59. package/dist/tsup/common/log.cjs +3 -3
  60. package/dist/tsup/common/log.js +2 -2
  61. package/dist/tsup/common/websocket.cjs +4 -4
  62. package/dist/tsup/common/websocket.js +3 -3
  63. package/dist/tsup/{conn-B3Vhbgnd.d.ts → config-BRDYDraU.d.cts} +1119 -1047
  64. package/dist/tsup/{conn-DJWL3nGx.d.cts → config-Bo-blHpJ.d.ts} +1119 -1047
  65. package/dist/tsup/driver-helpers/mod.cjs +5 -13
  66. package/dist/tsup/driver-helpers/mod.cjs.map +1 -1
  67. package/dist/tsup/driver-helpers/mod.d.cts +11 -9
  68. package/dist/tsup/driver-helpers/mod.d.ts +11 -9
  69. package/dist/tsup/driver-helpers/mod.js +14 -22
  70. package/dist/tsup/driver-test-suite/mod.cjs +474 -303
  71. package/dist/tsup/driver-test-suite/mod.cjs.map +1 -1
  72. package/dist/tsup/driver-test-suite/mod.d.cts +6 -9
  73. package/dist/tsup/driver-test-suite/mod.d.ts +6 -9
  74. package/dist/tsup/driver-test-suite/mod.js +1085 -914
  75. package/dist/tsup/driver-test-suite/mod.js.map +1 -1
  76. package/dist/tsup/inspector/mod.cjs +6 -6
  77. package/dist/tsup/inspector/mod.d.cts +5 -7
  78. package/dist/tsup/inspector/mod.d.ts +5 -7
  79. package/dist/tsup/inspector/mod.js +5 -5
  80. package/dist/tsup/mod.cjs +10 -16
  81. package/dist/tsup/mod.cjs.map +1 -1
  82. package/dist/tsup/mod.d.cts +23 -25
  83. package/dist/tsup/mod.d.ts +23 -25
  84. package/dist/tsup/mod.js +17 -23
  85. package/dist/tsup/test/mod.cjs +11 -11
  86. package/dist/tsup/test/mod.d.cts +4 -6
  87. package/dist/tsup/test/mod.d.ts +4 -6
  88. package/dist/tsup/test/mod.js +10 -10
  89. package/dist/tsup/utils.cjs +3 -5
  90. package/dist/tsup/utils.cjs.map +1 -1
  91. package/dist/tsup/utils.d.cts +1 -2
  92. package/dist/tsup/utils.d.ts +1 -2
  93. package/dist/tsup/utils.js +2 -4
  94. package/package.json +13 -6
  95. package/src/actor/config.ts +56 -44
  96. package/src/actor/conn/driver.ts +61 -0
  97. package/src/actor/conn/drivers/http.ts +17 -0
  98. package/src/actor/conn/drivers/raw-request.ts +24 -0
  99. package/src/actor/conn/drivers/raw-websocket.ts +65 -0
  100. package/src/actor/conn/drivers/websocket.ts +129 -0
  101. package/src/actor/conn/mod.ts +232 -0
  102. package/src/actor/conn/persisted.ts +81 -0
  103. package/src/actor/conn/state-manager.ts +196 -0
  104. package/src/actor/contexts/action.ts +23 -0
  105. package/src/actor/{context.ts → contexts/actor.ts} +19 -8
  106. package/src/actor/contexts/conn-init.ts +31 -0
  107. package/src/actor/contexts/conn.ts +48 -0
  108. package/src/actor/contexts/create-conn-state.ts +13 -0
  109. package/src/actor/contexts/on-before-connect.ts +13 -0
  110. package/src/actor/contexts/on-connect.ts +22 -0
  111. package/src/actor/contexts/request.ts +48 -0
  112. package/src/actor/contexts/websocket.ts +48 -0
  113. package/src/actor/definition.ts +3 -3
  114. package/src/actor/driver.ts +36 -5
  115. package/src/actor/errors.ts +19 -24
  116. package/src/actor/instance/connection-manager.ts +465 -0
  117. package/src/actor/instance/event-manager.ts +292 -0
  118. package/src/actor/instance/kv.ts +15 -0
  119. package/src/actor/instance/mod.ts +1107 -0
  120. package/src/actor/instance/persisted.ts +67 -0
  121. package/src/actor/instance/schedule-manager.ts +349 -0
  122. package/src/actor/instance/state-manager.ts +502 -0
  123. package/src/actor/mod.ts +13 -16
  124. package/src/actor/protocol/old.ts +131 -43
  125. package/src/actor/protocol/serde.ts +19 -4
  126. package/src/actor/router-endpoints.ts +61 -586
  127. package/src/actor/router-websocket-endpoints.ts +408 -0
  128. package/src/actor/router.ts +63 -197
  129. package/src/actor/schedule.ts +1 -1
  130. package/src/client/actor-conn.ts +183 -249
  131. package/src/client/actor-handle.ts +29 -6
  132. package/src/client/client.ts +0 -4
  133. package/src/client/config.ts +1 -4
  134. package/src/client/mod.ts +0 -1
  135. package/src/client/raw-utils.ts +3 -3
  136. package/src/client/utils.ts +85 -39
  137. package/src/common/actor-router-consts.ts +5 -12
  138. package/src/common/{inline-websocket-adapter2.ts → inline-websocket-adapter.ts} +26 -48
  139. package/src/common/log.ts +1 -1
  140. package/src/common/router.ts +28 -17
  141. package/src/common/utils.ts +2 -0
  142. package/src/driver-helpers/mod.ts +7 -10
  143. package/src/driver-helpers/utils.ts +18 -9
  144. package/src/driver-test-suite/mod.ts +26 -50
  145. package/src/driver-test-suite/test-inline-client-driver.ts +27 -51
  146. package/src/driver-test-suite/tests/actor-conn-hibernation.ts +150 -0
  147. package/src/driver-test-suite/tests/actor-conn-state.ts +1 -4
  148. package/src/driver-test-suite/tests/actor-conn.ts +5 -9
  149. package/src/driver-test-suite/tests/actor-destroy.ts +294 -0
  150. package/src/driver-test-suite/tests/actor-driver.ts +0 -7
  151. package/src/driver-test-suite/tests/actor-handle.ts +12 -12
  152. package/src/driver-test-suite/tests/actor-metadata.ts +1 -1
  153. package/src/driver-test-suite/tests/manager-driver.ts +1 -1
  154. package/src/driver-test-suite/tests/raw-http-direct-registry.ts +8 -8
  155. package/src/driver-test-suite/tests/raw-http-request-properties.ts +6 -5
  156. package/src/driver-test-suite/tests/raw-http.ts +5 -5
  157. package/src/driver-test-suite/tests/raw-websocket-direct-registry.ts +7 -7
  158. package/src/driver-test-suite/tests/request-access.ts +4 -4
  159. package/src/driver-test-suite/utils.ts +6 -10
  160. package/src/drivers/engine/actor-driver.ts +614 -424
  161. package/src/drivers/engine/mod.ts +0 -1
  162. package/src/drivers/file-system/actor.ts +24 -12
  163. package/src/drivers/file-system/global-state.ts +427 -37
  164. package/src/drivers/file-system/manager.ts +71 -83
  165. package/src/drivers/file-system/mod.ts +3 -0
  166. package/src/drivers/file-system/utils.ts +18 -8
  167. package/src/engine-process/mod.ts +38 -38
  168. package/src/inspector/utils.ts +7 -5
  169. package/src/manager/driver.ts +11 -4
  170. package/src/manager/gateway.ts +4 -29
  171. package/src/manager/protocol/mod.ts +0 -2
  172. package/src/manager/protocol/query.ts +0 -4
  173. package/src/manager/router.ts +67 -64
  174. package/src/manager-api/actors.ts +13 -0
  175. package/src/mod.ts +1 -3
  176. package/src/registry/mod.ts +20 -20
  177. package/src/registry/serve.ts +9 -14
  178. package/src/remote-manager-driver/actor-websocket-client.ts +1 -16
  179. package/src/remote-manager-driver/api-endpoints.ts +13 -1
  180. package/src/remote-manager-driver/api-utils.ts +8 -0
  181. package/src/remote-manager-driver/metadata.ts +58 -0
  182. package/src/remote-manager-driver/mod.ts +47 -62
  183. package/src/remote-manager-driver/ws-proxy.ts +1 -1
  184. package/src/schemas/actor-persist/mod.ts +1 -1
  185. package/src/schemas/actor-persist/versioned.ts +56 -31
  186. package/src/schemas/client-protocol/mod.ts +1 -1
  187. package/src/schemas/client-protocol/versioned.ts +41 -21
  188. package/src/schemas/client-protocol-zod/mod.ts +103 -0
  189. package/src/schemas/file-system-driver/mod.ts +1 -1
  190. package/src/schemas/file-system-driver/versioned.ts +42 -19
  191. package/src/serde.ts +33 -11
  192. package/src/test/mod.ts +7 -3
  193. package/src/utils/node.ts +173 -0
  194. package/src/utils.ts +0 -4
  195. package/dist/tsup/chunk-227FEWMB.js.map +0 -1
  196. package/dist/tsup/chunk-2JYPS5YM.cjs.map +0 -1
  197. package/dist/tsup/chunk-36JJ4IQB.cjs.map +0 -1
  198. package/dist/tsup/chunk-7L65NNWP.cjs.map +0 -1
  199. package/dist/tsup/chunk-BLK27ES3.js.map +0 -1
  200. package/dist/tsup/chunk-BOMZS2TJ.js.map +0 -1
  201. package/dist/tsup/chunk-BYMKMOBS.js.map +0 -1
  202. package/dist/tsup/chunk-FX7TWFQR.js.map +0 -1
  203. package/dist/tsup/chunk-G64QUEDJ.js.map +0 -1
  204. package/dist/tsup/chunk-HHFKKVLR.cjs.map +0 -1
  205. package/dist/tsup/chunk-INNFK746.cjs.map +0 -1
  206. package/dist/tsup/chunk-KSRXX3Z4.cjs.map +0 -1
  207. package/dist/tsup/chunk-O44LFKSB.cjs +0 -4623
  208. package/dist/tsup/chunk-O44LFKSB.cjs.map +0 -1
  209. package/dist/tsup/chunk-PLUN2NQT.js.map +0 -1
  210. package/dist/tsup/chunk-S4UJG7ZE.js +0 -1119
  211. package/dist/tsup/chunk-S4UJG7ZE.js.map +0 -1
  212. package/dist/tsup/chunk-SHVX2QUR.cjs.map +0 -1
  213. package/dist/tsup/chunk-VFB23BYZ.cjs +0 -1119
  214. package/dist/tsup/chunk-VFB23BYZ.cjs.map +0 -1
  215. package/dist/tsup/chunk-VHGY7PU5.cjs.map +0 -1
  216. package/dist/tsup/chunk-YBG6R7LX.js.map +0 -1
  217. package/dist/tsup/chunk-YBHYXIP6.js.map +0 -1
  218. package/src/actor/action.ts +0 -178
  219. package/src/actor/conn-drivers.ts +0 -216
  220. package/src/actor/conn-socket.ts +0 -8
  221. package/src/actor/conn.ts +0 -272
  222. package/src/actor/instance.ts +0 -2336
  223. package/src/actor/persisted.ts +0 -49
  224. package/src/actor/unstable-react.ts +0 -110
  225. package/src/driver-test-suite/tests/actor-reconnect.ts +0 -170
  226. package/src/drivers/engine/kv.ts +0 -3
  227. package/src/manager/hono-websocket-adapter.ts +0 -393
  228. /package/dist/tsup/{chunk-CD33GT6Z.js.map → chunk-EIPANQMF.js.map} +0 -0
@@ -1,18 +1,11 @@
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
1
  import invariant from "invariant";
6
2
  import { lookupInRegistry } from "@/actor/definition";
7
- import { ActorAlreadyExists } from "@/actor/errors";
8
- import type { AnyActorInstance } from "@/actor/instance";
3
+ import { ActorDuplicateKey } from "@/actor/errors";
4
+ import type { AnyActorInstance } from "@/actor/instance/mod";
9
5
  import type { ActorKey } from "@/actor/mod";
10
6
  import { generateRandomString } from "@/actor/utils";
11
7
  import type { AnyClient } from "@/client/client";
12
- import {
13
- type ActorDriver,
14
- serializeEmptyPersistData,
15
- } from "@/driver-helpers/mod";
8
+ import { type ActorDriver, getInitialActorKvState } from "@/driver-helpers/mod";
16
9
  import type { RegistryConfig } from "@/registry/config";
17
10
  import type { RunnerConfig } from "@/registry/run-config";
18
11
  import type * as schema from "@/schemas/file-system-driver/mod";
@@ -21,12 +14,19 @@ import {
21
14
  ACTOR_STATE_VERSIONED,
22
15
  } from "@/schemas/file-system-driver/versioned";
23
16
  import {
17
+ arrayBuffersEqual,
24
18
  bufferToArrayBuffer,
25
19
  type LongTimeoutHandle,
26
20
  promiseWithResolvers,
27
21
  setLongTimeout,
28
22
  stringifyError,
29
23
  } from "@/utils";
24
+ import {
25
+ getNodeCrypto,
26
+ getNodeFs,
27
+ getNodeFsSync,
28
+ getNodePath,
29
+ } from "@/utils/node";
30
30
  import { logger } from "./log";
31
31
  import {
32
32
  ensureDirectoryExists,
@@ -36,10 +36,19 @@ import {
36
36
 
37
37
  // Actor handler to track running instances
38
38
 
39
+ enum ActorLifecycleState {
40
+ NONEXISTENT, // Entry exists but actor not yet created
41
+ AWAKE, // Actor is running normally
42
+ STARTING_SLEEP, // Actor is being put to sleep
43
+ STARTING_DESTROY, // Actor is being destroyed
44
+ DESTROYED, // Actor was destroyed, should not be recreated
45
+ }
46
+
39
47
  interface ActorEntry {
40
48
  id: string;
41
49
 
42
50
  state?: schema.ActorState;
51
+
43
52
  /** Promise for loading the actor state. */
44
53
  loadPromise?: Promise<ActorEntry>;
45
54
 
@@ -54,8 +63,12 @@ interface ActorEntry {
54
63
  /** Resolver for pending write operations that need to be notified when any write completes */
55
64
  pendingWriteResolver?: PromiseWithResolvers<void>;
56
65
 
57
- /** If the actor has been removed by destroy or sleep. */
58
- removed: boolean;
66
+ lifecycleState: ActorLifecycleState;
67
+
68
+ // TODO: This might make sense to move in to actorstate, but we have a
69
+ // single reader/writer so it's not an issue
70
+ /** Generation of this actor when creating/destroying. */
71
+ generation: string;
59
72
  }
60
73
 
61
74
  /**
@@ -68,7 +81,12 @@ export class FileSystemGlobalState {
68
81
  #alarmsDir: string;
69
82
 
70
83
  #persist: boolean;
84
+
85
+ // IMPORTANT: Never delete from this map. Doing so will result in race
86
+ // conditions since the actor generation will cease to be tracked
87
+ // correctly. Always increment generation if a new actor is created.
71
88
  #actors = new Map<string, ActorEntry>();
89
+
72
90
  #actorCountOnStartup: number = 0;
73
91
 
74
92
  #runnerParams?: {
@@ -92,7 +110,8 @@ export class FileSystemGlobalState {
92
110
 
93
111
  constructor(persist: boolean = true, customPath?: string) {
94
112
  this.#persist = persist;
95
- this.#storagePath = persist ? getStoragePath(customPath) : "/tmp";
113
+ this.#storagePath = persist ? (customPath ?? getStoragePath()) : "/tmp";
114
+ const path = getNodePath();
96
115
  this.#stateDir = path.join(this.#storagePath, "state");
97
116
  this.#dbsDir = path.join(this.#storagePath, "databases");
98
117
  this.#alarmsDir = path.join(this.#storagePath, "alarms");
@@ -104,6 +123,7 @@ export class FileSystemGlobalState {
104
123
  ensureDirectoryExistsSync(this.#alarmsDir);
105
124
 
106
125
  try {
126
+ const fsSync = getNodeFsSync();
107
127
  const actorIds = fsSync.readdirSync(this.#stateDir);
108
128
  this.#actorCountOnStartup = actorIds.length;
109
129
  } catch (error) {
@@ -131,15 +151,15 @@ export class FileSystemGlobalState {
131
151
  }
132
152
 
133
153
  getActorStatePath(actorId: string): string {
134
- return path.join(this.#stateDir, actorId);
154
+ return getNodePath().join(this.#stateDir, actorId);
135
155
  }
136
156
 
137
157
  getActorDbPath(actorId: string): string {
138
- return path.join(this.#dbsDir, `${actorId}.db`);
158
+ return getNodePath().join(this.#dbsDir, `${actorId}.db`);
139
159
  }
140
160
 
141
161
  getActorAlarmPath(actorId: string): string {
142
- return path.join(this.#alarmsDir, actorId);
162
+ return getNodePath().join(this.#alarmsDir, actorId);
143
163
  }
144
164
 
145
165
  async *getActorsIterator(params: {
@@ -148,6 +168,7 @@ export class FileSystemGlobalState {
148
168
  let actorIds = Array.from(this.#actors.keys()).sort();
149
169
 
150
170
  // Check if state directory exists first
171
+ const fsSync = getNodeFsSync();
151
172
  if (fsSync.existsSync(this.#stateDir)) {
152
173
  actorIds = fsSync
153
174
  .readdirSync(this.#stateDir)
@@ -191,7 +212,8 @@ export class FileSystemGlobalState {
191
212
 
192
213
  entry = {
193
214
  id: actorId,
194
- removed: false,
215
+ lifecycleState: ActorLifecycleState.NONEXISTENT,
216
+ generation: crypto.randomUUID(),
195
217
  };
196
218
  this.#actors.set(actorId, entry);
197
219
  return entry;
@@ -208,21 +230,44 @@ export class FileSystemGlobalState {
208
230
  ): Promise<ActorEntry> {
209
231
  // TODO: Does not check if actor already exists on fs
210
232
 
211
- if (this.#actors.has(actorId)) {
212
- throw new ActorAlreadyExists(name, key);
233
+ const entry = this.#upsertEntry(actorId);
234
+
235
+ // Check if actor already exists (has state or is being stopped)
236
+ if (entry.state) {
237
+ throw new ActorDuplicateKey(name, key);
238
+ }
239
+ if (this.isActorStopping(actorId)) {
240
+ throw new Error(`Actor ${actorId} is stopping`);
213
241
  }
214
242
 
215
- const entry = this.#upsertEntry(actorId);
243
+ // If actor was destroyed, reset to NONEXISTENT and increment generation
244
+ if (entry.lifecycleState === ActorLifecycleState.DESTROYED) {
245
+ entry.lifecycleState = ActorLifecycleState.NONEXISTENT;
246
+ entry.generation = crypto.randomUUID();
247
+ }
248
+
249
+ // Initialize storage
250
+ const kvStorage: schema.ActorKvEntry[] = [];
251
+ const initialKvState = getInitialActorKvState(input);
252
+ for (const [key, value] of initialKvState) {
253
+ kvStorage.push({
254
+ key: bufferToArrayBuffer(key),
255
+ value: bufferToArrayBuffer(value),
256
+ });
257
+ }
258
+
259
+ // Initialize metadata
216
260
  entry.state = {
217
261
  actorId,
218
262
  name,
219
263
  key,
220
264
  createdAt: BigInt(Date.now()),
221
- persistedData: bufferToArrayBuffer(
222
- serializeEmptyPersistData(input),
223
- ),
265
+ kvStorage,
224
266
  };
225
- await this.writeActor(actorId, entry.state);
267
+ entry.lifecycleState = ActorLifecycleState.AWAKE;
268
+
269
+ await this.writeActor(actorId, entry.generation, entry.state);
270
+
226
271
  return entry;
227
272
  }
228
273
 
@@ -232,6 +277,11 @@ export class FileSystemGlobalState {
232
277
  async loadActor(actorId: string): Promise<ActorEntry> {
233
278
  const entry = this.#upsertEntry(actorId);
234
279
 
280
+ // Check if destroyed - don't load from disk
281
+ if (entry.lifecycleState === ActorLifecycleState.DESTROYED) {
282
+ return entry;
283
+ }
284
+
235
285
  // Check if already loaded
236
286
  if (entry.state) {
237
287
  return entry;
@@ -258,6 +308,7 @@ export class FileSystemGlobalState {
258
308
 
259
309
  // Read & parse file
260
310
  try {
311
+ const fs = getNodeFs();
261
312
  const stateData = await fs.readFile(stateFilePath);
262
313
 
263
314
  // Cache the loaded state in handler
@@ -292,16 +343,34 @@ export class FileSystemGlobalState {
292
343
 
293
344
  // If no state for this actor, then create & write state
294
345
  if (!entry.state) {
346
+ if (this.isActorStopping(actorId)) {
347
+ throw new Error(`Actor ${actorId} stopping`);
348
+ }
349
+
350
+ // If actor was destroyed, reset to NONEXISTENT and increment generation
351
+ if (entry.lifecycleState === ActorLifecycleState.DESTROYED) {
352
+ entry.lifecycleState = ActorLifecycleState.NONEXISTENT;
353
+ entry.generation = crypto.randomUUID();
354
+ }
355
+
356
+ // Initialize kvStorage with the initial persist data
357
+ const kvStorage: schema.ActorKvEntry[] = [];
358
+ const initialKvState = getInitialActorKvState(input);
359
+ for (const [key, value] of initialKvState) {
360
+ kvStorage.push({
361
+ key: bufferToArrayBuffer(key),
362
+ value: bufferToArrayBuffer(value),
363
+ });
364
+ }
365
+
295
366
  entry.state = {
296
367
  actorId,
297
368
  name,
298
369
  key: key as readonly string[],
299
370
  createdAt: BigInt(Date.now()),
300
- persistedData: bufferToArrayBuffer(
301
- serializeEmptyPersistData(input),
302
- ),
371
+ kvStorage,
303
372
  };
304
- await this.writeActor(actorId, entry.state);
373
+ await this.writeActor(actorId, entry.generation, entry.state);
305
374
  }
306
375
  return entry;
307
376
  }
@@ -312,29 +381,128 @@ export class FileSystemGlobalState {
312
381
  "cannot sleep actor with memory driver, must use file system driver",
313
382
  );
314
383
 
315
- const actor = this.#actors.get(actorId);
384
+ // Get the actor. We upsert it even though we're about to destroy it so we have a lock on flagging `destroying` as true.
385
+ const actor = this.#upsertEntry(actorId);
316
386
  invariant(actor, `tried to sleep ${actorId}, does not exist`);
317
387
 
388
+ // Check if already destroying
389
+ if (this.isActorStopping(actorId)) {
390
+ return;
391
+ }
392
+ actor.lifecycleState = ActorLifecycleState.STARTING_SLEEP;
393
+
318
394
  // Wait for actor to fully start before stopping it to avoid race conditions
319
395
  if (actor.loadPromise) await actor.loadPromise.catch();
320
396
  if (actor.startPromise?.promise)
321
397
  await actor.startPromise.promise.catch();
322
398
 
323
- // Mark as removed
324
- actor.removed = true;
325
-
326
399
  // Stop actor
327
400
  invariant(actor.actor, "actor should be loaded");
328
- await actor.actor._onStop();
401
+ await actor.actor.onStop("sleep");
329
402
 
330
403
  // Remove from map after stop is complete
331
404
  this.#actors.delete(actorId);
332
405
  }
333
406
 
407
+ async destroyActor(actorId: string) {
408
+ // Get the actor. We upsert it even though we're about to destroy it so we have a lock on flagging `destroying` as true.
409
+ const actor = this.#upsertEntry(actorId);
410
+
411
+ // If actor is loaded, stop it first
412
+ // Check if already destroying
413
+ if (this.isActorStopping(actorId)) {
414
+ return;
415
+ }
416
+ actor.lifecycleState = ActorLifecycleState.STARTING_DESTROY;
417
+
418
+ // Wait for actor to fully start before stopping it to avoid race conditions
419
+ if (actor.loadPromise) await actor.loadPromise.catch();
420
+ if (actor.startPromise?.promise)
421
+ await actor.startPromise.promise.catch();
422
+
423
+ // Stop actor if it's running
424
+ if (actor.actor) {
425
+ await actor.actor.onStop("destroy");
426
+ }
427
+
428
+ // Clear alarm timeout if exists
429
+ if (actor.alarmTimeout) {
430
+ actor.alarmTimeout.abort();
431
+ }
432
+
433
+ // Delete persisted files if using file system driver
434
+ if (this.#persist) {
435
+ const fs = getNodeFs();
436
+
437
+ // Delete all actor files in parallel
438
+ await Promise.all([
439
+ // Delete actor state file
440
+ (async () => {
441
+ try {
442
+ await fs.unlink(this.getActorStatePath(actorId));
443
+ } catch (err: any) {
444
+ if (err?.code !== "ENOENT") {
445
+ logger().error({
446
+ msg: "failed to delete actor state file",
447
+ actorId,
448
+ error: stringifyError(err),
449
+ });
450
+ }
451
+ }
452
+ })(),
453
+ // Delete actor database file
454
+ (async () => {
455
+ try {
456
+ await fs.unlink(this.getActorDbPath(actorId));
457
+ } catch (err: any) {
458
+ if (err?.code !== "ENOENT") {
459
+ logger().error({
460
+ msg: "failed to delete actor database file",
461
+ actorId,
462
+ error: stringifyError(err),
463
+ });
464
+ }
465
+ }
466
+ })(),
467
+ // Delete actor alarm file
468
+ (async () => {
469
+ try {
470
+ await fs.unlink(this.getActorAlarmPath(actorId));
471
+ } catch (err: any) {
472
+ if (err?.code !== "ENOENT") {
473
+ logger().error({
474
+ msg: "failed to delete actor alarm file",
475
+ actorId,
476
+ error: stringifyError(err),
477
+ });
478
+ }
479
+ }
480
+ })(),
481
+ ]);
482
+ }
483
+
484
+ // Reset the entry
485
+ //
486
+ // Do not remove entry in order to avoid race condition with
487
+ // destroying. Next actor creation will increment the generation.
488
+ actor.state = undefined;
489
+ actor.loadPromise = undefined;
490
+ actor.actor = undefined;
491
+ actor.startPromise = undefined;
492
+ actor.alarmTimeout = undefined;
493
+ actor.alarmTimeout = undefined;
494
+ actor.pendingWriteResolver = undefined;
495
+ actor.lifecycleState = ActorLifecycleState.DESTROYED;
496
+ }
497
+
334
498
  /**
335
499
  * Save actor state to disk.
336
500
  */
337
- async writeActor(actorId: string, state: schema.ActorState): Promise<void> {
501
+ async writeActor(
502
+ actorId: string,
503
+ generation: string,
504
+ state: schema.ActorState,
505
+ ): Promise<void> {
338
506
  if (!this.#persist) {
339
507
  return;
340
508
  }
@@ -342,18 +510,49 @@ export class FileSystemGlobalState {
342
510
  const entry = this.#actors.get(actorId);
343
511
  invariant(entry, "actor entry does not exist");
344
512
 
345
- await this.#performWrite(actorId, state);
513
+ await this.#performWrite(actorId, generation, state);
514
+ }
515
+
516
+ isGenerationCurrentAndNotDestroyed(
517
+ actorId: string,
518
+ generation: string,
519
+ ): boolean {
520
+ const entry = this.#upsertEntry(actorId);
521
+ if (!entry) return false;
522
+ return (
523
+ entry.generation === generation &&
524
+ entry.lifecycleState !== ActorLifecycleState.STARTING_DESTROY
525
+ );
526
+ }
527
+
528
+ isActorStopping(actorId: string) {
529
+ const entry = this.#upsertEntry(actorId);
530
+ if (!entry) return false;
531
+ return (
532
+ entry.lifecycleState === ActorLifecycleState.STARTING_SLEEP ||
533
+ entry.lifecycleState === ActorLifecycleState.STARTING_DESTROY
534
+ );
346
535
  }
347
536
 
348
537
  async setActorAlarm(actorId: string, timestamp: number) {
349
538
  const entry = this.#actors.get(actorId);
350
539
  invariant(entry, "actor entry does not exist");
351
540
 
541
+ // Track generation of the actor when the write started to detect
542
+ // destroy/create race condition
543
+ const writeGeneration = entry.generation;
544
+ if (this.isActorStopping(actorId)) {
545
+ logger().info("skipping set alarm since actor stopping");
546
+ return;
547
+ }
548
+
352
549
  // Persist alarm to disk
353
550
  if (this.#persist) {
354
551
  const alarmPath = this.getActorAlarmPath(actorId);
552
+ const crypto = getNodeCrypto();
355
553
  const tempPath = `${alarmPath}.tmp.${crypto.randomUUID()}`;
356
554
  try {
555
+ const path = getNodePath();
357
556
  await ensureDirectoryExists(path.dirname(alarmPath));
358
557
  const alarmData: schema.ActorAlarm = {
359
558
  actorId,
@@ -363,10 +562,25 @@ export class FileSystemGlobalState {
363
562
  ACTOR_ALARM_VERSIONED.serializeWithEmbeddedVersion(
364
563
  alarmData,
365
564
  );
565
+ const fs = getNodeFs();
366
566
  await fs.writeFile(tempPath, data);
567
+
568
+ if (
569
+ !this.isGenerationCurrentAndNotDestroyed(
570
+ actorId,
571
+ writeGeneration,
572
+ )
573
+ ) {
574
+ logger().debug(
575
+ "skipping writing alarm since actor destroying or new generation",
576
+ );
577
+ return;
578
+ }
579
+
367
580
  await fs.rename(tempPath, alarmPath);
368
581
  } catch (error) {
369
582
  try {
583
+ const fs = getNodeFs();
370
584
  await fs.unlink(tempPath);
371
585
  } catch {}
372
586
  logger().error({
@@ -387,14 +601,17 @@ export class FileSystemGlobalState {
387
601
  */
388
602
  async #performWrite(
389
603
  actorId: string,
604
+ generation: string,
390
605
  state: schema.ActorState,
391
606
  ): Promise<void> {
392
607
  const dataPath = this.getActorStatePath(actorId);
393
608
  // Generate unique temp filename to prevent any race conditions
609
+ const crypto = getNodeCrypto();
394
610
  const tempPath = `${dataPath}.tmp.${crypto.randomUUID()}`;
395
611
 
396
612
  try {
397
613
  // Create directory if needed
614
+ const path = getNodePath();
398
615
  await ensureDirectoryExists(path.dirname(dataPath));
399
616
 
400
617
  // Convert to BARE types for serialization
@@ -403,17 +620,27 @@ export class FileSystemGlobalState {
403
620
  name: state.name,
404
621
  key: state.key,
405
622
  createdAt: state.createdAt,
406
- persistedData: state.persistedData,
623
+ kvStorage: state.kvStorage,
407
624
  };
408
625
 
409
626
  // Perform atomic write
410
627
  const serializedState =
411
628
  ACTOR_STATE_VERSIONED.serializeWithEmbeddedVersion(bareState);
629
+ const fs = getNodeFs();
412
630
  await fs.writeFile(tempPath, serializedState);
631
+
632
+ if (!this.isGenerationCurrentAndNotDestroyed(actorId, generation)) {
633
+ logger().debug(
634
+ "skipping writing alarm since actor destroying or new generation",
635
+ );
636
+ return;
637
+ }
638
+
413
639
  await fs.rename(tempPath, dataPath);
414
640
  } catch (error) {
415
641
  // Cleanup temp file on error
416
642
  try {
643
+ const fs = getNodeFs();
417
644
  await fs.unlink(tempPath);
418
645
  } catch {
419
646
  // Ignore cleanup errors
@@ -548,12 +775,14 @@ export class FileSystemGlobalState {
548
775
  */
549
776
  #loadAlarmsSync(): void {
550
777
  try {
778
+ const fsSync = getNodeFsSync();
551
779
  const files = fsSync.existsSync(this.#alarmsDir)
552
780
  ? fsSync.readdirSync(this.#alarmsDir)
553
781
  : [];
554
782
  for (const file of files) {
555
783
  // Skip temp files
556
784
  if (file.includes(".tmp.")) continue;
785
+ const path = getNodePath();
557
786
  const fullPath = path.join(this.#alarmsDir, file);
558
787
  try {
559
788
  const buf = fsSync.readFileSync(fullPath);
@@ -622,6 +851,7 @@ export class FileSystemGlobalState {
622
851
  // On trigger: remove persisted alarm file
623
852
  if (this.#persist) {
624
853
  try {
854
+ const fs = getNodeFs();
625
855
  await fs.unlink(this.getActorAlarmPath(actorId));
626
856
  } catch (err: any) {
627
857
  if (err?.code !== "ENOENT") {
@@ -656,7 +886,7 @@ export class FileSystemGlobalState {
656
886
  }
657
887
 
658
888
  invariant(loaded.actor, "actor should be loaded after wake");
659
- await loaded.actor._onAlarm();
889
+ await loaded.actor.onAlarm();
660
890
  } catch (err) {
661
891
  logger().error({
662
892
  msg: "failed to handle alarm",
@@ -668,6 +898,8 @@ export class FileSystemGlobalState {
668
898
  }
669
899
 
670
900
  getOrCreateInspectorAccessToken(): string {
901
+ const path = getNodePath();
902
+ const fsSync = getNodeFsSync();
671
903
  const tokenPath = path.join(this.#storagePath, "inspector-token");
672
904
  if (fsSync.existsSync(tokenPath)) {
673
905
  return fsSync.readFileSync(tokenPath, "utf-8");
@@ -683,6 +915,7 @@ export class FileSystemGlobalState {
683
915
  */
684
916
  #cleanupTempFilesSync(): void {
685
917
  try {
918
+ const fsSync = getNodeFsSync();
686
919
  const files = fsSync.readdirSync(this.#stateDir);
687
920
  const tempFiles = files.filter((f) => f.includes(".tmp."));
688
921
 
@@ -690,6 +923,7 @@ export class FileSystemGlobalState {
690
923
 
691
924
  for (const tempFile of tempFiles) {
692
925
  try {
926
+ const path = getNodePath();
693
927
  const fullPath = path.join(this.#stateDir, tempFile);
694
928
  const stat = fsSync.statSync(fullPath);
695
929
 
@@ -716,4 +950,160 @@ export class FileSystemGlobalState {
716
950
  });
717
951
  }
718
952
  }
953
+
954
+ /**
955
+ * Batch put KV entries for an actor.
956
+ */
957
+ async kvBatchPut(
958
+ actorId: string,
959
+ entries: [Uint8Array, Uint8Array][],
960
+ ): Promise<void> {
961
+ const entry = await this.loadActor(actorId);
962
+ if (!entry.state) {
963
+ if (this.isActorStopping(actorId)) {
964
+ return;
965
+ } else {
966
+ throw new Error(`Actor ${actorId} state not loaded`);
967
+ }
968
+ }
969
+
970
+ // Create a mutable copy of kvStorage
971
+ const newKvStorage = [...entry.state.kvStorage];
972
+
973
+ // Update kvStorage with new entries
974
+ for (const [key, value] of entries) {
975
+ // Find existing entry with the same key
976
+ const existingIndex = newKvStorage.findIndex((e) =>
977
+ arrayBuffersEqual(e.key, bufferToArrayBuffer(key)),
978
+ );
979
+
980
+ if (existingIndex >= 0) {
981
+ // Replace existing entry with new one
982
+ newKvStorage[existingIndex] = {
983
+ key: bufferToArrayBuffer(key),
984
+ value: bufferToArrayBuffer(value),
985
+ };
986
+ } else {
987
+ // Add new entry
988
+ newKvStorage.push({
989
+ key: bufferToArrayBuffer(key),
990
+ value: bufferToArrayBuffer(value),
991
+ });
992
+ }
993
+ }
994
+
995
+ // Update state with new kvStorage
996
+ entry.state = {
997
+ ...entry.state,
998
+ kvStorage: newKvStorage,
999
+ };
1000
+
1001
+ // Save state to disk
1002
+ await this.writeActor(actorId, entry.generation, entry.state);
1003
+ }
1004
+
1005
+ /**
1006
+ * Batch get KV entries for an actor.
1007
+ */
1008
+ async kvBatchGet(
1009
+ actorId: string,
1010
+ keys: Uint8Array[],
1011
+ ): Promise<(Uint8Array | null)[]> {
1012
+ const entry = await this.loadActor(actorId);
1013
+ if (!entry.state) {
1014
+ if (this.isActorStopping(actorId)) {
1015
+ throw new Error(`Actor ${actorId} is stopping`);
1016
+ } else {
1017
+ throw new Error(`Actor ${actorId} state not loaded`);
1018
+ }
1019
+ }
1020
+
1021
+ const results: (Uint8Array | null)[] = [];
1022
+ for (const key of keys) {
1023
+ // Find entry with the same key
1024
+ const foundEntry = entry.state.kvStorage.find((e) =>
1025
+ arrayBuffersEqual(e.key, bufferToArrayBuffer(key)),
1026
+ );
1027
+
1028
+ if (foundEntry) {
1029
+ results.push(new Uint8Array(foundEntry.value));
1030
+ } else {
1031
+ results.push(null);
1032
+ }
1033
+ }
1034
+ return results;
1035
+ }
1036
+
1037
+ /**
1038
+ * Batch delete KV entries for an actor.
1039
+ */
1040
+ async kvBatchDelete(actorId: string, keys: Uint8Array[]): Promise<void> {
1041
+ const entry = await this.loadActor(actorId);
1042
+ if (!entry.state) {
1043
+ if (this.isActorStopping(actorId)) {
1044
+ return;
1045
+ } else {
1046
+ throw new Error(`Actor ${actorId} state not loaded`);
1047
+ }
1048
+ }
1049
+
1050
+ // Create a mutable copy of kvStorage
1051
+ const newKvStorage = [...entry.state.kvStorage];
1052
+
1053
+ // Delete entries from kvStorage
1054
+ for (const key of keys) {
1055
+ const indexToDelete = newKvStorage.findIndex((e) =>
1056
+ arrayBuffersEqual(e.key, bufferToArrayBuffer(key)),
1057
+ );
1058
+
1059
+ if (indexToDelete >= 0) {
1060
+ newKvStorage.splice(indexToDelete, 1);
1061
+ }
1062
+ }
1063
+
1064
+ // Update state with new kvStorage
1065
+ entry.state = {
1066
+ ...entry.state,
1067
+ kvStorage: newKvStorage,
1068
+ };
1069
+
1070
+ // Save state to disk
1071
+ await this.writeActor(actorId, entry.generation, entry.state);
1072
+ }
1073
+
1074
+ /**
1075
+ * List KV entries with a given prefix for an actor.
1076
+ */
1077
+ async kvListPrefix(
1078
+ actorId: string,
1079
+ prefix: Uint8Array,
1080
+ ): Promise<[Uint8Array, Uint8Array][]> {
1081
+ const entry = await this.loadActor(actorId);
1082
+ if (!entry.state) {
1083
+ if (this.isActorStopping(actorId)) {
1084
+ throw new Error(`Actor ${actorId} is destroying`);
1085
+ } else {
1086
+ throw new Error(`Actor ${actorId} state not loaded`);
1087
+ }
1088
+ }
1089
+
1090
+ const results: [Uint8Array, Uint8Array][] = [];
1091
+ for (const kvEntry of entry.state.kvStorage) {
1092
+ const keyBytes = new Uint8Array(kvEntry.key);
1093
+ // Check if key starts with prefix
1094
+ if (keyBytes.length >= prefix.length) {
1095
+ let hasPrefix = true;
1096
+ for (let i = 0; i < prefix.length; i++) {
1097
+ if (keyBytes[i] !== prefix[i]) {
1098
+ hasPrefix = false;
1099
+ break;
1100
+ }
1101
+ }
1102
+ if (hasPrefix) {
1103
+ results.push([keyBytes, new Uint8Array(kvEntry.value)]);
1104
+ }
1105
+ }
1106
+ }
1107
+ return results;
1108
+ }
719
1109
  }