rivetkit 2.3.11-rc.9 → 2.3.11

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 (152) hide show
  1. package/dist/browser/client.d.ts +96 -14
  2. package/dist/browser/client.js +12 -2
  3. package/dist/browser/client.js.map +1 -1
  4. package/dist/browser/inspector/client.d.ts +139 -139
  5. package/dist/browser/inspector/client.js +12 -2
  6. package/dist/browser/inspector/client.js.map +1 -1
  7. package/dist/tsup/agent-os/index.cjs +146 -76
  8. package/dist/tsup/agent-os/index.cjs.map +1 -1
  9. package/dist/tsup/agent-os/index.d.cts +83 -9
  10. package/dist/tsup/agent-os/index.d.ts +83 -9
  11. package/dist/tsup/agent-os/index.js +146 -76
  12. package/dist/tsup/agent-os/index.js.map +1 -1
  13. package/dist/tsup/{chunk-HOZ3ZLZ2.js → chunk-3ZR542SK.js} +6 -6
  14. package/dist/tsup/chunk-3ZR542SK.js.map +1 -0
  15. package/dist/tsup/{chunk-HNHLBM3Y.cjs → chunk-4KGXFC6M.cjs} +42 -5
  16. package/dist/tsup/chunk-4KGXFC6M.cjs.map +1 -0
  17. package/dist/tsup/chunk-5UYWLCRO.js +14 -0
  18. package/dist/tsup/chunk-5UYWLCRO.js.map +1 -0
  19. package/dist/tsup/{chunk-QWLJCP3X.js → chunk-6W5VGLFT.js} +42 -5
  20. package/dist/tsup/chunk-6W5VGLFT.js.map +1 -0
  21. package/dist/tsup/chunk-6WYATRS4.js +243 -0
  22. package/dist/tsup/chunk-6WYATRS4.js.map +1 -0
  23. package/dist/tsup/chunk-B3W74FBB.cjs +14 -0
  24. package/dist/tsup/chunk-B3W74FBB.cjs.map +1 -0
  25. package/dist/tsup/{chunk-XOOESJY7.js → chunk-FWROC5LA.js} +2 -2
  26. package/dist/tsup/{chunk-V5CVATRU.cjs → chunk-GLIPKKEC.cjs} +7 -7
  27. package/dist/tsup/{chunk-V5CVATRU.cjs.map → chunk-GLIPKKEC.cjs.map} +1 -1
  28. package/dist/tsup/{chunk-T5XVTVQY.cjs → chunk-GN2EUMEW.cjs} +3 -3
  29. package/dist/tsup/{chunk-T5XVTVQY.cjs.map → chunk-GN2EUMEW.cjs.map} +1 -1
  30. package/dist/tsup/{chunk-T56IZ42M.cjs → chunk-GU7ML3KE.cjs} +13 -3
  31. package/dist/tsup/chunk-GU7ML3KE.cjs.map +1 -0
  32. package/dist/tsup/{chunk-IOLZ3ONU.js → chunk-JQ2AZ5FN.js} +3 -3
  33. package/dist/tsup/{chunk-7AFZMFPQ.js → chunk-JTHHCZCZ.js} +10 -1
  34. package/dist/tsup/chunk-JTHHCZCZ.js.map +1 -0
  35. package/dist/tsup/{chunk-UEULFXUI.cjs → chunk-KVFIONKQ.cjs} +11 -2
  36. package/dist/tsup/chunk-KVFIONKQ.cjs.map +1 -0
  37. package/dist/tsup/chunk-LWQFXGKS.cjs +243 -0
  38. package/dist/tsup/chunk-LWQFXGKS.cjs.map +1 -0
  39. package/dist/tsup/{chunk-QVCO6DTZ.js → chunk-MO2SSUOR.js} +8 -7
  40. package/dist/tsup/chunk-MO2SSUOR.js.map +1 -0
  41. package/dist/tsup/{chunk-LZ4TSBIP.js → chunk-OUENKQHB.js} +13 -3
  42. package/dist/tsup/chunk-OUENKQHB.js.map +1 -0
  43. package/dist/tsup/{chunk-6SBPWTXJ.cjs → chunk-PUZY2XCD.cjs} +10 -9
  44. package/dist/tsup/chunk-PUZY2XCD.cjs.map +1 -0
  45. package/dist/tsup/{chunk-C5TORAVP.js → chunk-QFJ24MBU.js} +27 -2
  46. package/dist/tsup/{chunk-C5TORAVP.js.map → chunk-QFJ24MBU.js.map} +1 -1
  47. package/dist/tsup/{chunk-BZWKY6WO.cjs → chunk-RXXDNJXB.cjs} +140 -140
  48. package/dist/tsup/{chunk-BZWKY6WO.cjs.map → chunk-RXXDNJXB.cjs.map} +1 -1
  49. package/dist/tsup/{chunk-IMRDHZM2.cjs → chunk-VBJQIBXI.cjs} +30 -5
  50. package/dist/tsup/chunk-VBJQIBXI.cjs.map +1 -0
  51. package/dist/tsup/client/mod.cjs +6 -6
  52. package/dist/tsup/client/mod.d.cts +4 -3
  53. package/dist/tsup/client/mod.d.ts +4 -3
  54. package/dist/tsup/client/mod.js +5 -5
  55. package/dist/tsup/common/log.cjs +2 -2
  56. package/dist/tsup/common/log.js +1 -1
  57. package/dist/tsup/common/websocket.cjs +3 -3
  58. package/dist/tsup/common/websocket.js +2 -2
  59. package/dist/tsup/{config-CZcjyT5B.d.ts → config-C4OSv0QP.d.ts} +92 -7
  60. package/dist/tsup/{config-rGWrumlO.d.cts → config-DTGoWdkG.d.cts} +92 -7
  61. package/dist/tsup/{config-BxOimoGX.d.cts → config-ey2I2zz5.d.cts} +46 -6
  62. package/dist/tsup/{config-BxOimoGX.d.ts → config-ey2I2zz5.d.ts} +46 -6
  63. package/dist/tsup/{context-B8W8E8nW.d.cts → context-C6cvDNyk.d.cts} +2 -2
  64. package/dist/tsup/{context-_UrDZhEh.d.ts → context-CMygeBxS.d.ts} +2 -2
  65. package/dist/tsup/db/drizzle.cjs +18 -10
  66. package/dist/tsup/db/drizzle.cjs.map +1 -1
  67. package/dist/tsup/db/drizzle.d.cts +5 -6
  68. package/dist/tsup/db/drizzle.d.ts +5 -6
  69. package/dist/tsup/db/drizzle.js +13 -5
  70. package/dist/tsup/db/drizzle.js.map +1 -1
  71. package/dist/tsup/db/mod.cjs +5 -175
  72. package/dist/tsup/db/mod.cjs.map +1 -1
  73. package/dist/tsup/db/mod.d.cts +11 -4
  74. package/dist/tsup/db/mod.d.ts +11 -4
  75. package/dist/tsup/db/mod.js +4 -174
  76. package/dist/tsup/db/mod.js.map +1 -1
  77. package/dist/tsup/dynamic/mod.cjs +2 -2
  78. package/dist/tsup/dynamic/mod.d.cts +3 -2
  79. package/dist/tsup/dynamic/mod.d.ts +3 -2
  80. package/dist/tsup/dynamic/mod.js +1 -1
  81. package/dist/tsup/inspector/mod.cjs +5 -4
  82. package/dist/tsup/inspector/mod.cjs.map +1 -1
  83. package/dist/tsup/inspector/mod.d.cts +12 -146
  84. package/dist/tsup/inspector/mod.d.ts +12 -146
  85. package/dist/tsup/inspector/mod.js +4 -3
  86. package/dist/tsup/inspector/mod.js.map +1 -1
  87. package/dist/tsup/inspector/workflow.cjs +21 -0
  88. package/dist/tsup/inspector/workflow.cjs.map +1 -0
  89. package/dist/tsup/inspector/workflow.d.cts +144 -0
  90. package/dist/tsup/inspector/workflow.d.ts +144 -0
  91. package/dist/tsup/inspector/workflow.js +21 -0
  92. package/dist/tsup/inspector/workflow.js.map +1 -0
  93. package/dist/tsup/inspector-tab/mod.cjs +2 -2
  94. package/dist/tsup/inspector-tab/mod.d.cts +4 -3
  95. package/dist/tsup/inspector-tab/mod.d.ts +4 -3
  96. package/dist/tsup/inspector-tab/mod.js +1 -1
  97. package/dist/tsup/mod.cjs +743 -247
  98. package/dist/tsup/mod.cjs.map +1 -1
  99. package/dist/tsup/mod.d.cts +6 -5
  100. package/dist/tsup/mod.d.ts +6 -5
  101. package/dist/tsup/mod.js +670 -174
  102. package/dist/tsup/mod.js.map +1 -1
  103. package/dist/tsup/test/mod.cjs +7 -7
  104. package/dist/tsup/test/mod.d.cts +3 -2
  105. package/dist/tsup/test/mod.d.ts +3 -2
  106. package/dist/tsup/test/mod.js +5 -5
  107. package/dist/tsup/unstable/migrations.d.cts +1 -1
  108. package/dist/tsup/unstable/migrations.d.ts +1 -1
  109. package/dist/tsup/utils.cjs +2 -2
  110. package/dist/tsup/utils.js +1 -1
  111. package/dist/tsup/workflow/mod.cjs +89 -95
  112. package/dist/tsup/workflow/mod.cjs.map +1 -1
  113. package/dist/tsup/workflow/mod.d.cts +5 -4
  114. package/dist/tsup/workflow/mod.d.ts +5 -4
  115. package/dist/tsup/workflow/mod.js +86 -92
  116. package/dist/tsup/workflow/mod.js.map +1 -1
  117. package/package.json +19 -9
  118. package/src/actor/config.test.ts +65 -1
  119. package/src/actor/config.ts +169 -5
  120. package/src/actor/mod.ts +10 -1
  121. package/src/client/actor-common.ts +5 -14
  122. package/src/common/database/config.ts +50 -3
  123. package/src/common/database/mod.test.ts +142 -4
  124. package/src/common/database/mod.ts +210 -98
  125. package/src/common/database/native-database.ts +42 -3
  126. package/src/common/database/shared.ts +9 -0
  127. package/src/db/drizzle.test.ts +8 -1
  128. package/src/db/drizzle.ts +15 -4
  129. package/src/db/mod.ts +3 -0
  130. package/src/inspector/workflow.ts +61 -0
  131. package/src/registry/napi-runtime.ts +69 -1
  132. package/src/registry/native.ts +474 -161
  133. package/src/registry/run-handler-coordinator.test.ts +252 -0
  134. package/src/registry/run-handler-coordinator.ts +219 -0
  135. package/src/registry/runtime.ts +34 -1
  136. package/src/registry/wasm-runtime.ts +122 -17
  137. package/src/utils/node.ts +44 -28
  138. package/src/workflow/driver.ts +56 -58
  139. package/src/workflow/inspector.ts +10 -15
  140. package/src/workflow/mod.ts +48 -70
  141. package/dist/tsup/chunk-6SBPWTXJ.cjs.map +0 -1
  142. package/dist/tsup/chunk-7AFZMFPQ.js.map +0 -1
  143. package/dist/tsup/chunk-HNHLBM3Y.cjs.map +0 -1
  144. package/dist/tsup/chunk-HOZ3ZLZ2.js.map +0 -1
  145. package/dist/tsup/chunk-IMRDHZM2.cjs.map +0 -1
  146. package/dist/tsup/chunk-LZ4TSBIP.js.map +0 -1
  147. package/dist/tsup/chunk-QVCO6DTZ.js.map +0 -1
  148. package/dist/tsup/chunk-QWLJCP3X.js.map +0 -1
  149. package/dist/tsup/chunk-T56IZ42M.cjs.map +0 -1
  150. package/dist/tsup/chunk-UEULFXUI.cjs.map +0 -1
  151. /package/dist/tsup/{chunk-XOOESJY7.js.map → chunk-FWROC5LA.js.map} +0 -0
  152. /package/dist/tsup/{chunk-IOLZ3ONU.js.map → chunk-JQ2AZ5FN.js.map} +0 -0
@@ -15,6 +15,8 @@ import {
15
15
  disposeRunInspector,
16
16
  getRunFunction,
17
17
  getRunInspectorConfig,
18
+ getRunInspectorKind,
19
+ hasRunInspectorConfig,
18
20
  RAW_STATE_SYMBOL,
19
21
  type ScheduledEventInfo,
20
22
  type WorkflowInspectorConfig,
@@ -44,7 +46,15 @@ import {
44
46
  } from "@/client/client";
45
47
  import { convertRegistryConfigToClientConfig } from "@/client/config";
46
48
  import { HEADER_CONN_PARAMS } from "@/common/actor-router-consts";
47
- import type { AnyDatabaseProvider } from "@/common/database/config";
49
+ import type {
50
+ AnyDatabaseProvider,
51
+ SqliteProfilingOptions,
52
+ } from "@/common/database/config";
53
+ import {
54
+ bindNativeStateTransactionContext,
55
+ db as defaultDb,
56
+ registerNativeStateTransactionOpener,
57
+ } from "@/common/database/mod";
48
58
  import { wrapJsNativeDatabase } from "@/common/database/native-database";
49
59
  import { assertJsonCompatValue, type JsonCompatValue } from "@/common/encoding";
50
60
  import { isResponseLike, type ResponseLike } from "@/common/fetch-like";
@@ -83,6 +93,7 @@ import {
83
93
  validateQueueBody,
84
94
  validateQueueComplete,
85
95
  } from "./native-validation";
96
+ import { RunHandlerCoordinator } from "./run-handler-coordinator";
86
97
  import type {
87
98
  ActorContextHandle,
88
99
  ActorFactoryHandle,
@@ -129,10 +140,14 @@ type NativeOnStateChangeHandler = (
129
140
  ) => void | Promise<void>;
130
141
  type NativePersistConnState = {
131
142
  state: unknown;
132
- // Memoized deep write-through proxy and the state object it wraps. Rebuilt
133
- // only when the underlying state object identity changes.
134
- stateProxy?: unknown;
135
- stateProxyTarget?: unknown;
143
+ };
144
+
145
+ type NativeStateTransactionScope = {
146
+ actorContext: ActorContextHandleAdapter;
147
+ actorStateBaseline: unknown;
148
+ connectionStateBaselines: Map<string, Uint8Array>;
149
+ committed: boolean;
150
+ release: () => void;
136
151
  };
137
152
 
138
153
  const defaultRuntimeLoaders: RuntimeLoaders = {
@@ -278,14 +293,14 @@ type NativePersistActorState = {
278
293
  state: unknown;
279
294
  isInOnStateChange: boolean;
280
295
  connStates: Map<string, NativePersistConnState>;
281
- // Memoized deep write-through proxy and the state object it wraps. Rebuilt
282
- // only when the underlying state object identity changes.
283
- stateProxy?: unknown;
284
- stateProxyTarget?: unknown;
285
296
  // Set when a coalesced save and onStateChange flush is pending for the
286
297
  // current event loop tick.
287
298
  saveScheduled?: boolean;
288
299
  pendingSaveHandle?: ReturnType<typeof setImmediate>;
300
+ activeStateTransactionOwner?: symbol;
301
+ pendingStateTransactionOwners?: Set<symbol>;
302
+ stateTransactionTail?: Promise<void>;
303
+ stateTransactionSaveDeferred?: boolean;
289
304
  };
290
305
  type NativeDestroyGate = {
291
306
  destroyCompletion?: Promise<void>;
@@ -371,6 +386,15 @@ function stateMutationReentrantError(): RivetError {
371
386
  );
372
387
  }
373
388
 
389
+ function stateTransactionConflictError(): RivetError {
390
+ return new RivetError(
391
+ "actor",
392
+ "state_transaction_conflict",
393
+ "State cannot be mutated by another action while an experimental state-aware database transaction is active.",
394
+ { public: true, statusCode: 409 },
395
+ );
396
+ }
397
+
374
398
  function databaseNotConfiguredError(): RivetError {
375
399
  return new RivetError(
376
400
  "actor",
@@ -388,6 +412,14 @@ function databaseClientNotReadyError(): RivetError {
388
412
  { public: true },
389
413
  );
390
414
  }
415
+ function runHandlerNotConfiguredError(): RivetError {
416
+ return new RivetError(
417
+ "actor",
418
+ "run_handler_unavailable",
419
+ "Cannot schedule a run wake because this actor does not define a run handler.",
420
+ { public: true, statusCode: 409 },
421
+ );
422
+ }
391
423
 
392
424
  function stateNotEnabledError(): RivetError {
393
425
  return new RivetError(
@@ -540,10 +572,11 @@ function getOrCreateNativeSqlDatabase(
540
572
  execute: (sql, params) => runtime.actorSqlExecute(ctx, sql, params),
541
573
  executeBatch: (statements) =>
542
574
  runtime.actorSqlExecuteBatch(ctx, statements),
543
- beginTransaction: async (timeoutMs) => {
575
+ beginTransaction: async (timeoutMs, name) => {
544
576
  const transaction = await runtime.actorSqlBeginTransaction(
545
577
  ctx,
546
578
  timeoutMs,
579
+ name,
547
580
  );
548
581
  return {
549
582
  exec: (sql) =>
@@ -559,6 +592,40 @@ function getOrCreateNativeSqlDatabase(
559
592
  runtime.actorSqlTransactionRollback(transaction),
560
593
  };
561
594
  },
595
+ beginStateTransaction: async (timeoutMs, context) => {
596
+ const scope = context as NativeStateTransactionScope | undefined;
597
+ if (!scope) {
598
+ throw new Error(
599
+ "actor state transaction context is not configured",
600
+ );
601
+ }
602
+ const transaction = await runtime.actorBeginStateTransaction(
603
+ ctx,
604
+ timeoutMs,
605
+ );
606
+ return {
607
+ exec: async () => {
608
+ throw new Error(
609
+ "actor state transactions only support single-statement execute calls",
610
+ );
611
+ },
612
+ execute: (sql, params) =>
613
+ runtime.actorStateTransactionExecute(
614
+ transaction,
615
+ sql,
616
+ params,
617
+ ),
618
+ commit: async () => {
619
+ await runtime.actorStateTransactionCommit(
620
+ transaction,
621
+ scope.actorContext.serializeForTick("save"),
622
+ );
623
+ scope.committed = true;
624
+ },
625
+ rollback: () =>
626
+ runtime.actorStateTransactionRollback(transaction),
627
+ };
628
+ },
562
629
  query: (sql, params) => runtime.actorSqlQuery(ctx, sql, params),
563
630
  run: (sql, params) => runtime.actorSqlRun(ctx, sql, params),
564
631
  metrics: () => runtime.actorSqlMetrics(ctx),
@@ -1234,6 +1301,9 @@ class NativeConnAdapter {
1234
1301
  #schemas: NativeValidationConfig;
1235
1302
  #ctx?: ActorContextHandle;
1236
1303
  #queueHibernationRemoval?: (connId: string) => void;
1304
+ #assertCanMutateState?: () => void;
1305
+ #stateProxy?: unknown;
1306
+ #stateProxyTarget?: unknown;
1237
1307
 
1238
1308
  constructor(
1239
1309
  runtime: CoreRuntime,
@@ -1241,12 +1311,14 @@ class NativeConnAdapter {
1241
1311
  schemas: NativeValidationConfig = {},
1242
1312
  ctx?: ActorContextHandle,
1243
1313
  queueHibernationRemoval?: (connId: string) => void,
1314
+ assertCanMutateState?: () => void,
1244
1315
  ) {
1245
1316
  this.#runtime = runtime;
1246
1317
  this.#conn = conn;
1247
1318
  this.#schemas = schemas;
1248
1319
  this.#ctx = ctx;
1249
1320
  this.#queueHibernationRemoval = queueHibernationRemoval;
1321
+ this.#assertCanMutateState = assertCanMutateState;
1250
1322
  (
1251
1323
  this as NativeConnAdapter & {
1252
1324
  [CONN_STATE_MANAGER_SYMBOL]?: unknown;
@@ -1285,34 +1357,38 @@ class NativeConnAdapter {
1285
1357
  // allocates fresh on-change caches and rewraps the whole tree. Memoize
1286
1358
  // the proxy keyed on the underlying state object so repeated reads and
1287
1359
  // deep read cascades reuse a single proxy.
1288
- const connState = getNativeConnPersistState(
1289
- this.#runtime,
1290
- this.#ctx,
1291
- this.#conn,
1292
- );
1293
1360
  if (
1294
- connState.stateProxy === undefined ||
1295
- connState.stateProxyTarget !== nextState
1361
+ this.#stateProxy === undefined ||
1362
+ this.#stateProxyTarget !== nextState
1296
1363
  ) {
1297
- connState.stateProxyTarget = nextState;
1298
- connState.stateProxy = this.#createStateProxy(nextState);
1364
+ this.#stateProxyTarget = nextState;
1365
+ this.#stateProxy = this.#createStateProxy(nextState);
1299
1366
  }
1300
- return connState.stateProxy;
1367
+ return this.#stateProxy;
1301
1368
  }
1302
1369
 
1303
1370
  #createStateProxy(state: unknown): unknown {
1371
+ const assertCurrentState = () => {
1372
+ this.#assertCanMutateState?.();
1373
+ if (this.#ctx && this.#readState() !== state) {
1374
+ throw stateTransactionConflictError();
1375
+ }
1376
+ };
1304
1377
  return createWriteThroughProxy(
1305
1378
  state,
1306
1379
  (nextValue) => {
1380
+ assertCurrentState();
1307
1381
  this.#writeState(nextValue, { writeNative: true });
1308
1382
  },
1309
1383
  (newValue) => {
1384
+ assertCurrentState();
1310
1385
  assertJsonCompatValue(newValue);
1311
1386
  },
1312
1387
  );
1313
1388
  }
1314
1389
 
1315
1390
  set state(value: unknown) {
1391
+ this.#assertCanMutateState?.();
1316
1392
  const nextValue = unwrapWriteThroughProxy(value);
1317
1393
  assertJsonCompatValue(nextValue);
1318
1394
  this.#writeState(nextValue, { writeNative: true });
@@ -1734,8 +1810,9 @@ function wrapQueueMessage(
1734
1810
  schemas: NativeValidationConfig["queues"],
1735
1811
  ) {
1736
1812
  const name = callNativeSync(() => message.name());
1813
+ const id = callNativeSync(() => message.id());
1737
1814
  return {
1738
- id: Number(callNativeSync(() => message.id())),
1815
+ id: id <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(id) : id,
1739
1816
  name,
1740
1817
  body: validateQueueBody(
1741
1818
  schemas,
@@ -1919,6 +1996,32 @@ class NativeQueueAdapter {
1919
1996
  cleanup?.();
1920
1997
  }
1921
1998
  }
1999
+ async waitForAvailable(
2000
+ names?: readonly string[],
2001
+ options?: { timeout?: number; signal?: AbortSignal },
2002
+ ): Promise<void> {
2003
+ await this.waitForNamesAvailable(names ?? [], options);
2004
+ }
2005
+
2006
+ async complete(
2007
+ message: { id: number | bigint; name: string },
2008
+ response?: unknown,
2009
+ ): Promise<void> {
2010
+ const validatedResponse = validateQueueComplete(
2011
+ this.#schemas,
2012
+ message.name,
2013
+ response,
2014
+ );
2015
+ await callNative(() =>
2016
+ this.#runtime.actorQueueCompletePersisted(
2017
+ this.#ctx,
2018
+ BigInt(message.id),
2019
+ message.name,
2020
+ encodeValue(validatedResponse),
2021
+ ),
2022
+ );
2023
+ this.#pendingCompletableMessageIds.delete(message.id.toString());
2024
+ }
1922
2025
 
1923
2026
  async enqueueAndWait(
1924
2027
  name: string,
@@ -2498,15 +2601,18 @@ class NativeConnectionMap implements ReadonlyMap<string, NativeConnAdapter> {
2498
2601
  #runtime: CoreRuntime;
2499
2602
  #ctx: ActorContextHandle;
2500
2603
  #schemas: NativeValidationConfig;
2604
+ #assertCanMutateState: () => void;
2501
2605
 
2502
2606
  constructor(
2503
2607
  runtime: CoreRuntime,
2504
2608
  ctx: ActorContextHandle,
2505
2609
  schemas: NativeValidationConfig,
2610
+ assertCanMutateState: () => void,
2506
2611
  ) {
2507
2612
  this.#runtime = runtime;
2508
2613
  this.#ctx = ctx;
2509
2614
  this.#schemas = schemas;
2615
+ this.#assertCanMutateState = assertCanMutateState;
2510
2616
  }
2511
2617
 
2512
2618
  #connToAdapter(conn: ConnHandle): NativeConnAdapter {
@@ -2522,6 +2628,7 @@ class NativeConnectionMap implements ReadonlyMap<string, NativeConnAdapter> {
2522
2628
  connId,
2523
2629
  ),
2524
2630
  ),
2631
+ this.#assertCanMutateState,
2525
2632
  );
2526
2633
  }
2527
2634
 
@@ -2609,10 +2716,13 @@ export class ActorContextHandleAdapter {
2609
2716
  #queue?: NativeQueueAdapter;
2610
2717
  #request?: Request;
2611
2718
  #schedule?: NativeScheduleAdapter;
2612
- #sql?: ReturnType<typeof wrapJsNativeDatabase>;
2613
- #runHandlerActiveProvider?: () => boolean;
2719
+ #run?: { setWakeAt(timestamp: number | null): Promise<void> };
2720
+ #runHandlerConfigured: boolean;
2614
2721
  #onStateChange?: NativeOnStateChangeHandler;
2615
2722
  #stateEnabled: boolean;
2723
+ #stateProxy?: unknown;
2724
+ #stateProxyTarget?: unknown;
2725
+ readonly #stateTransactionOwner = Symbol("actor-state-transaction-owner");
2616
2726
 
2617
2727
  constructor(
2618
2728
  runtime: CoreRuntime,
@@ -2622,16 +2732,16 @@ export class ActorContextHandleAdapter {
2622
2732
  databaseProvider?: AnyDatabaseProvider,
2623
2733
  request?: Request,
2624
2734
  stateEnabled = true,
2625
- runHandlerActiveProvider?: () => boolean,
2626
2735
  onStateChange?: NativeOnStateChangeHandler,
2627
2736
  dispatchCancelToken?: CancellationTokenHandle,
2737
+ runHandlerConfigured = false,
2628
2738
  ) {
2629
2739
  this.#runtime = runtime;
2630
2740
  this.#ctx = ctx;
2631
2741
  this.#clientFactory = clientFactory;
2632
2742
  this.#schemas = schemas;
2633
2743
  this.#dispatchCancelToken = dispatchCancelToken;
2634
- this.#runHandlerActiveProvider = runHandlerActiveProvider;
2744
+ this.#runHandlerConfigured = runHandlerConfigured;
2635
2745
  this.#onStateChange = onStateChange;
2636
2746
  this.#stateEnabled = stateEnabled;
2637
2747
  if (databaseProvider) {
@@ -2654,13 +2764,6 @@ export class ActorContextHandleAdapter {
2654
2764
  return this.#kv;
2655
2765
  }
2656
2766
 
2657
- get sql() {
2658
- if (!this.#sql) {
2659
- this.#sql = getOrCreateNativeSqlDatabase(this.#runtime, this.#ctx);
2660
- }
2661
- return this.#sql;
2662
- }
2663
-
2664
2767
  async actorRuntimeSocket(): Promise<{ path: string }> {
2665
2768
  return await callNative(() =>
2666
2769
  this.#runtime.actorRuntimeSocketProvision(this.#ctx),
@@ -2679,7 +2782,7 @@ export class ActorContextHandleAdapter {
2679
2782
  const runtimeState = getNativeRuntimeState(this.#runtime, this.#ctx);
2680
2783
  const cachedClient = runtimeState.databaseClient;
2681
2784
  if (cachedClient) {
2682
- this.#db = cachedClient.client;
2785
+ this.#db = this.#bindDatabaseClient(cachedClient.client);
2683
2786
  return this.#db;
2684
2787
  }
2685
2788
 
@@ -2697,29 +2800,35 @@ export class ActorContextHandleAdapter {
2697
2800
  if (!this.#stateEnabled) {
2698
2801
  throw stateNotEnabledError();
2699
2802
  }
2700
- const actorState = getNativePersistState(this.#runtime, this.#ctx);
2701
2803
  const nextState = this.#readState();
2702
2804
  // Reading `c.state` rebuilds the deep write-through proxy, which
2703
2805
  // allocates fresh on-change caches and rewraps the whole tree. Memoize
2704
2806
  // the proxy keyed on the underlying state object so repeated reads and
2705
2807
  // deep read cascades reuse a single proxy.
2706
2808
  if (
2707
- actorState.stateProxy === undefined ||
2708
- actorState.stateProxyTarget !== nextState
2809
+ this.#stateProxy === undefined ||
2810
+ this.#stateProxyTarget !== nextState
2709
2811
  ) {
2710
- actorState.stateProxyTarget = nextState;
2711
- actorState.stateProxy = createWriteThroughProxy(
2812
+ const assertCurrentState = () => {
2813
+ this.#assertCanMutateState();
2814
+ if (this.#readState() !== nextState) {
2815
+ throw stateTransactionConflictError();
2816
+ }
2817
+ };
2818
+ this.#stateProxyTarget = nextState;
2819
+ this.#stateProxy = createWriteThroughProxy(
2712
2820
  nextState,
2713
2821
  (nextValue) => {
2822
+ assertCurrentState();
2714
2823
  this.#writeState(nextValue, { scheduleSave: true });
2715
2824
  },
2716
2825
  (newValue) => {
2717
- this.#assertCanMutateState();
2826
+ assertCurrentState();
2718
2827
  assertJsonCompatValue(newValue);
2719
2828
  },
2720
2829
  );
2721
2830
  }
2722
- return actorState.stateProxy;
2831
+ return this.#stateProxy;
2723
2832
  }
2724
2833
 
2725
2834
  set state(value: unknown) {
@@ -2769,6 +2878,30 @@ export class ActorContextHandleAdapter {
2769
2878
  return this.#queue;
2770
2879
  }
2771
2880
 
2881
+ get run() {
2882
+ if (!this.#run) {
2883
+ this.#run = {
2884
+ setWakeAt: async (timestamp: number | null) => {
2885
+ if (timestamp !== null && !this.#runHandlerConfigured) {
2886
+ throw runHandlerNotConfiguredError();
2887
+ }
2888
+ if (
2889
+ timestamp !== null &&
2890
+ (!Number.isSafeInteger(timestamp) || timestamp < 0)
2891
+ ) {
2892
+ throw new TypeError(
2893
+ "Run wake timestamp must be a non-negative safe integer or null",
2894
+ );
2895
+ }
2896
+ await callNative(() =>
2897
+ this.#runtime.actorSetRunWakeAt(this.#ctx, timestamp),
2898
+ );
2899
+ },
2900
+ };
2901
+ }
2902
+ return this.#run;
2903
+ }
2904
+
2772
2905
  get schedule(): NativeScheduleAdapter {
2773
2906
  if (!this.#schedule) {
2774
2907
  this.#schedule = new NativeScheduleAdapter(
@@ -2810,6 +2943,7 @@ export class ActorContextHandleAdapter {
2810
2943
  this.#runtime,
2811
2944
  this.#ctx,
2812
2945
  this.#schemas,
2946
+ () => this.#assertCanMutateState(),
2813
2947
  );
2814
2948
  }
2815
2949
  return this.#connMap;
@@ -2890,12 +3024,12 @@ export class ActorContextHandleAdapter {
2890
3024
  const runtimeState = getNativeRuntimeState(this.#runtime, this.#ctx);
2891
3025
  const cachedClient = runtimeState.databaseClient;
2892
3026
  if (cachedClient) {
2893
- this.#db = cachedClient.client;
3027
+ this.#db = this.#bindDatabaseClient(cachedClient.client);
2894
3028
  return this.#db;
2895
3029
  }
2896
3030
 
2897
3031
  const actorId = this.actorId;
2898
- const client = await this.#databaseProvider.createClient({
3032
+ const createdClient = await this.#databaseProvider.createClient({
2899
3033
  actorId,
2900
3034
  kv: {
2901
3035
  batchPut: async (entries) => {
@@ -2916,21 +3050,147 @@ export class ActorContextHandleAdapter {
2916
3050
  log: {
2917
3051
  debug: (obj) => logger().debug(obj),
2918
3052
  },
2919
- nativeDatabaseProvider: {
2920
- open: async (requestedActorId) => {
2921
- void requestedActorId;
2922
- return getOrCreateNativeSqlDatabase(
3053
+ nativeDatabaseProvider: registerNativeStateTransactionOpener(
3054
+ {
3055
+ open: async (requestedActorId) => {
3056
+ void requestedActorId;
3057
+ return getOrCreateNativeSqlDatabase(
3058
+ this.#runtime,
3059
+ this.#ctx,
3060
+ );
3061
+ },
3062
+ },
3063
+ async (timeoutMs, context) =>
3064
+ await getOrCreateNativeSqlDatabase(
2923
3065
  this.#runtime,
2924
3066
  this.#ctx,
2925
- );
2926
- },
2927
- },
3067
+ ).beginStateTransaction(timeoutMs, context),
3068
+ ),
2928
3069
  });
2929
3070
  runtimeState.databaseClient = {
2930
- client,
3071
+ client: createdClient,
2931
3072
  };
2932
- this.#db = client;
2933
- return client;
3073
+ this.#db = this.#bindDatabaseClient(createdClient);
3074
+ return this.#db;
3075
+ }
3076
+
3077
+ #bindDatabaseClient(client: unknown): unknown {
3078
+ return bindNativeStateTransactionContext(client, {
3079
+ enter: async () => await this.#enterStateTransaction(),
3080
+ exit: (scope) =>
3081
+ this.#exitStateTransaction(
3082
+ scope as NativeStateTransactionScope,
3083
+ ),
3084
+ });
3085
+ }
3086
+
3087
+ async #enterStateTransaction(): Promise<NativeStateTransactionScope> {
3088
+ const actorState = getNativePersistState(this.#runtime, this.#ctx);
3089
+ let pendingOwners = actorState.pendingStateTransactionOwners;
3090
+ if (!pendingOwners) {
3091
+ pendingOwners = new Set();
3092
+ actorState.pendingStateTransactionOwners = pendingOwners;
3093
+ }
3094
+ if (
3095
+ actorState.activeStateTransactionOwner ===
3096
+ this.#stateTransactionOwner ||
3097
+ pendingOwners.has(this.#stateTransactionOwner)
3098
+ ) {
3099
+ throw stateTransactionConflictError();
3100
+ }
3101
+ pendingOwners.add(this.#stateTransactionOwner);
3102
+ const predecessor =
3103
+ actorState.stateTransactionTail ?? Promise.resolve();
3104
+ let release!: () => void;
3105
+ const hold = new Promise<void>((resolve) => {
3106
+ release = resolve;
3107
+ });
3108
+ actorState.stateTransactionTail = predecessor.then(
3109
+ async () => await hold,
3110
+ );
3111
+ try {
3112
+ await predecessor;
3113
+ pendingOwners.delete(this.#stateTransactionOwner);
3114
+ actorState.activeStateTransactionOwner =
3115
+ this.#stateTransactionOwner;
3116
+ return {
3117
+ actorContext: this,
3118
+ actorStateBaseline: this.#stateEnabled
3119
+ ? structuredClone(this.#readState())
3120
+ : undefined,
3121
+ connectionStateBaselines: new Map(
3122
+ callNativeSync(() =>
3123
+ this.#runtime.actorConns(this.#ctx),
3124
+ ).map((conn) => [
3125
+ callNativeSync(() => this.#runtime.connId(conn)),
3126
+ new Uint8Array(
3127
+ callNativeSync(() => this.#runtime.connState(conn)),
3128
+ ),
3129
+ ]),
3130
+ ),
3131
+ committed: false,
3132
+ release,
3133
+ };
3134
+ } catch (error) {
3135
+ pendingOwners.delete(this.#stateTransactionOwner);
3136
+ if (
3137
+ actorState.activeStateTransactionOwner ===
3138
+ this.#stateTransactionOwner
3139
+ ) {
3140
+ actorState.activeStateTransactionOwner = undefined;
3141
+ }
3142
+ release();
3143
+ throw error;
3144
+ }
3145
+ }
3146
+
3147
+ #exitStateTransaction(scope: NativeStateTransactionScope): void {
3148
+ const actorState = getNativePersistState(this.#runtime, this.#ctx);
3149
+ try {
3150
+ if (!scope.committed) {
3151
+ this.#restoreStateTransactionBaseline(scope);
3152
+ }
3153
+ } finally {
3154
+ if (
3155
+ actorState.activeStateTransactionOwner ===
3156
+ this.#stateTransactionOwner
3157
+ ) {
3158
+ actorState.activeStateTransactionOwner = undefined;
3159
+ }
3160
+ if (actorState.stateTransactionSaveDeferred) {
3161
+ actorState.stateTransactionSaveDeferred = false;
3162
+ this.#scheduleSave();
3163
+ }
3164
+ scope.release();
3165
+ }
3166
+ }
3167
+
3168
+ #restoreStateTransactionBaseline(scope: NativeStateTransactionScope): void {
3169
+ const actorState = getNativePersistState(this.#runtime, this.#ctx);
3170
+ if (actorState.pendingSaveHandle !== undefined) {
3171
+ clearImmediate(actorState.pendingSaveHandle);
3172
+ }
3173
+ actorState.pendingSaveHandle = undefined;
3174
+ actorState.saveScheduled = false;
3175
+
3176
+ if (this.#stateEnabled) {
3177
+ actorState.state = structuredClone(scope.actorStateBaseline);
3178
+ }
3179
+ for (const conn of callNativeSync(() =>
3180
+ this.#runtime.actorConns(this.#ctx),
3181
+ )) {
3182
+ const connId = callNativeSync(() => this.#runtime.connId(conn));
3183
+ const baseline = scope.connectionStateBaselines.get(connId);
3184
+ if (baseline === undefined) {
3185
+ continue;
3186
+ }
3187
+ callNativeSync(() => this.#runtime.connSetState(conn, baseline));
3188
+ const cached = actorState.connStates.get(connId);
3189
+ if (cached) {
3190
+ cached.state = decodeValue(baseline);
3191
+ }
3192
+ }
3193
+ this.#scheduleSave();
2934
3194
  }
2935
3195
 
2936
3196
  async prepare(): Promise<void> {
@@ -2953,7 +3213,6 @@ export class ActorContextHandleAdapter {
2953
3213
 
2954
3214
  async closeDatabase(): Promise<void> {
2955
3215
  this.#db = undefined;
2956
- this.#sql = undefined;
2957
3216
  await closeNativeDatabaseClient(this.#runtime, this.#ctx);
2958
3217
  await closeNativeSqlDatabase(this.#runtime, this.#ctx);
2959
3218
  }
@@ -2978,6 +3237,7 @@ export class ActorContextHandleAdapter {
2978
3237
  maxWait?: number;
2979
3238
  }): Promise<void> {
2980
3239
  if (opts?.immediate) {
3240
+ this.#assertCanAwaitStateSave();
2981
3241
  await callNative(() =>
2982
3242
  this.#runtime.actorRequestSaveAndWait(this.#ctx, {
2983
3243
  immediate: true,
@@ -3003,11 +3263,22 @@ export class ActorContextHandleAdapter {
3003
3263
  async saveStateAndWorkflowBatch(
3004
3264
  writes: Array<{ key: Uint8Array; value: Uint8Array }>,
3005
3265
  ): Promise<void> {
3266
+ this.#assertCanAwaitStateSave();
3006
3267
  await callNative(() =>
3007
3268
  this.#runtime.actorSaveStateAndWorkflowBatch(this.#ctx, writes),
3008
3269
  );
3009
3270
  }
3010
3271
 
3272
+ #assertCanAwaitStateSave(): void {
3273
+ const actorState = getNativePersistState(this.#runtime, this.#ctx);
3274
+ if (
3275
+ actorState.activeStateTransactionOwner ===
3276
+ this.#stateTransactionOwner
3277
+ ) {
3278
+ throw stateTransactionConflictError();
3279
+ }
3280
+ }
3281
+
3011
3282
  serializeForTick(reason: SerializeStateReason): RuntimeStateDeltaPayload {
3012
3283
  void reason;
3013
3284
  const actorState = getNativePersistState(this.#runtime, this.#ctx);
@@ -3071,10 +3342,6 @@ export class ActorContextHandleAdapter {
3071
3342
  return promise;
3072
3343
  }
3073
3344
 
3074
- runHandlerActive(): boolean {
3075
- return this.#runHandlerActiveProvider?.() ?? false;
3076
- }
3077
-
3078
3345
  internalKeepAwake<T>(run: Promise<T> | (() => Promise<T>)): Promise<T> {
3079
3346
  const promise = typeof run === "function" ? run() : run;
3080
3347
  // Track only completion, swallowing the outcome. The real result/error
@@ -3175,7 +3442,6 @@ export class ActorContextHandleAdapter {
3175
3442
  // down so the request-save and onStateChange always run.
3176
3443
  this.#flushStateChange();
3177
3444
  this.#abortSignalCleanup?.();
3178
- this.#sql = undefined;
3179
3445
  }
3180
3446
 
3181
3447
  #createActorAbortSignal(): AbortSignal {
@@ -3222,6 +3488,19 @@ export class ActorContextHandleAdapter {
3222
3488
  if (actorState.isInOnStateChange) {
3223
3489
  throw stateMutationReentrantError();
3224
3490
  }
3491
+ if (
3492
+ actorState.activeStateTransactionOwner !==
3493
+ this.#stateTransactionOwner &&
3494
+ (actorState.activeStateTransactionOwner !== undefined ||
3495
+ (actorState.pendingStateTransactionOwners?.size ?? 0) > 0)
3496
+ ) {
3497
+ throw stateTransactionConflictError();
3498
+ }
3499
+ }
3500
+
3501
+ /** @internal */
3502
+ assertCanMutateState(): void {
3503
+ this.#assertCanMutateState();
3225
3504
  }
3226
3505
 
3227
3506
  // Coalesce the request-save and onStateChange work to once per event loop
@@ -3245,6 +3524,15 @@ export class ActorContextHandleAdapter {
3245
3524
  if (!actorState.saveScheduled) {
3246
3525
  return;
3247
3526
  }
3527
+ if (actorState.activeStateTransactionOwner !== undefined) {
3528
+ actorState.saveScheduled = false;
3529
+ if (actorState.pendingSaveHandle !== undefined) {
3530
+ clearImmediate(actorState.pendingSaveHandle);
3531
+ actorState.pendingSaveHandle = undefined;
3532
+ }
3533
+ actorState.stateTransactionSaveDeferred = true;
3534
+ return;
3535
+ }
3248
3536
  actorState.saveScheduled = false;
3249
3537
  if (actorState.pendingSaveHandle !== undefined) {
3250
3538
  clearImmediate(actorState.pendingSaveHandle);
@@ -3446,10 +3734,6 @@ class NativeWorkflowRuntimeAdapter {
3446
3734
  };
3447
3735
  }
3448
3736
 
3449
- isRunHandlerActive(): boolean {
3450
- return this.#ctx.runHandlerActive();
3451
- }
3452
-
3453
3737
  async restartRunHandler(): Promise<void> {
3454
3738
  await this.#ctx.restartRunHandler();
3455
3739
  }
@@ -3497,28 +3781,33 @@ function withConnContext(
3497
3781
  stateEnabled = true,
3498
3782
  onStateChange?: NativeOnStateChangeHandler,
3499
3783
  dispatchCancelToken?: CancellationTokenHandle,
3784
+ runHandlerConfigured = false,
3500
3785
  ) {
3501
- return Object.assign(
3502
- new ActorContextHandleAdapter(
3786
+ const actorContext = new ActorContextHandleAdapter(
3787
+ runtime,
3788
+ ctx,
3789
+ clientFactory,
3790
+ schemas,
3791
+ databaseProvider,
3792
+ request,
3793
+ stateEnabled,
3794
+ onStateChange,
3795
+ dispatchCancelToken,
3796
+ runHandlerConfigured,
3797
+ );
3798
+ return Object.assign(actorContext, {
3799
+ conn: new NativeConnAdapter(
3503
3800
  runtime,
3504
- ctx,
3505
- clientFactory,
3801
+ conn,
3506
3802
  schemas,
3507
- databaseProvider,
3508
- request,
3509
- stateEnabled,
3510
- undefined,
3511
- onStateChange,
3512
- dispatchCancelToken,
3513
- ),
3514
- {
3515
- conn: new NativeConnAdapter(runtime, conn, schemas, ctx, (connId) =>
3803
+ ctx,
3804
+ (connId) =>
3516
3805
  callNativeSync(() =>
3517
3806
  runtime.actorQueueHibernationRemoval(ctx, connId),
3518
3807
  ),
3519
- ),
3520
- },
3521
- );
3808
+ () => actorContext.assertCanMutateState(),
3809
+ ),
3810
+ });
3522
3811
  }
3523
3812
 
3524
3813
  function buildActorConfig(
@@ -3531,12 +3820,16 @@ function buildActorConfig(
3531
3820
  const canHibernate = options.canHibernateWebSocket;
3532
3821
  const usesRemoteSqlite =
3533
3822
  sqliteBackendForConfig(registryConfig) === "remote";
3823
+ const sqliteProfiling = (
3824
+ config.db as { sqliteProfiling?: SqliteProfilingOptions } | undefined
3825
+ )?.sqliteProfiling;
3534
3826
 
3535
3827
  return {
3536
3828
  name: options.name as string | undefined,
3537
3829
  icon: options.icon as string | undefined,
3538
- hasDatabase: config.db !== undefined || usesRemoteSqlite,
3830
+ hasDatabase: true,
3539
3831
  remoteSqlite: usesRemoteSqlite,
3832
+ sqliteProfiling,
3540
3833
  enableActorRuntimeSocket: options.enableActorRuntimeSocket === true,
3541
3834
  hasState:
3542
3835
  config.state !== undefined ||
@@ -3681,7 +3974,10 @@ export function buildNativeFactory(
3681
3974
  definition: AnyActorDefinition,
3682
3975
  ): ActorFactoryHandle {
3683
3976
  const config = definition.config as Record<string, any>;
3684
- const databaseProvider = config.db as AnyDatabaseProvider;
3977
+ const databaseProvider = (config.db ?? defaultDb()) as Exclude<
3978
+ AnyDatabaseProvider,
3979
+ undefined
3980
+ >;
3685
3981
  const actionHandlers = flattenActionHandlers(config.actions);
3686
3982
  const schemaConfig: NativeValidationConfig = {
3687
3983
  actionInputSchemas: flattenActionInputSchemas(
@@ -3699,16 +3995,21 @@ export function buildNativeFactory(
3699
3995
  ),
3700
3996
  { encoding: "bare" },
3701
3997
  );
3702
- const nativeRunHandlerActiveByActorId = new Map<string, boolean>();
3703
- const isNativeRunHandlerActive = (ctx: ActorContextHandle) =>
3704
- nativeRunHandlerActiveByActorId.get(
3705
- callNativeSync(() => runtime.actorId(ctx)),
3706
- ) ?? false;
3707
- const getNativeWorkflowInspector = (ctx: ActorContextHandle) =>
3708
- getRunInspectorConfig(
3709
- config.run,
3710
- callNativeSync(() => runtime.actorId(ctx)),
3711
- )?.workflow as NativeWorkflowInspectorConfig | undefined;
3998
+ const run = getRunFunction(config.run);
3999
+ const runHandlerCoordinator =
4000
+ getRunInspectorKind(config.run) === "workflow"
4001
+ ? new RunHandlerCoordinator(config.run)
4002
+ : undefined;
4003
+ const getNativeWorkflowInspector = (ctx: ActorContextHandle) => {
4004
+ const actorId = callNativeSync(() => runtime.actorId(ctx));
4005
+ const restart = () =>
4006
+ callNativeSync(() => runtime.actorRestartRunHandler(ctx));
4007
+ return (runHandlerCoordinator?.getInspector(actorId, restart)
4008
+ ?.workflow ??
4009
+ getRunInspectorConfig(config.run, actorId)?.workflow) as
4010
+ | NativeWorkflowInspectorConfig
4011
+ | undefined;
4012
+ };
3712
4013
  const onStateChange =
3713
4014
  typeof config.onStateChange === "function"
3714
4015
  ? (actorCtx: ActorContextHandleAdapter, nextState: unknown) => {
@@ -3741,9 +4042,9 @@ export function buildNativeFactory(
3741
4042
  databaseProvider,
3742
4043
  request,
3743
4044
  stateEnabled,
3744
- () => isNativeRunHandlerActive(ctx),
3745
4045
  onStateChange,
3746
4046
  cancelToken,
4047
+ run !== undefined,
3747
4048
  );
3748
4049
  const makeConnCtx = (
3749
4050
  ctx: ActorContextHandle,
@@ -3762,6 +4063,7 @@ export function buildNativeFactory(
3762
4063
  stateEnabled,
3763
4064
  onStateChange,
3764
4065
  cancelToken,
4066
+ run !== undefined,
3765
4067
  );
3766
4068
  const maybeHandleNativeInspectorRequest = async (
3767
4069
  ctx: ActorContextHandle,
@@ -3830,6 +4132,7 @@ export function buildNativeFactory(
3830
4132
  const workflowState = async () =>
3831
4133
  (await getNativeWorkflowInspector(ctx)?.getState?.()) ?? null;
3832
4134
  const actorCtx = makeActorCtx(ctx, jsRequest);
4135
+ const sql = getOrCreateNativeSqlDatabase(runtime, ctx);
3833
4136
  try {
3834
4137
  if (
3835
4138
  url.pathname === "/inspector/state" &&
@@ -4001,7 +4304,7 @@ export function buildNativeFactory(
4001
4304
  url.pathname === "/inspector/database/schema" &&
4002
4305
  jsRequest.method === "GET"
4003
4306
  ) {
4004
- const db = actorCtx.sql;
4307
+ const db = sql;
4005
4308
  const tables = queryRows(
4006
4309
  await db.query(
4007
4310
  "SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__drizzle_%' ORDER BY name",
@@ -4055,7 +4358,7 @@ export function buildNativeFactory(
4055
4358
  );
4056
4359
  const quoted = `"${table.replace(/"/g, '""')}"`;
4057
4360
  const rows = queryRows(
4058
- await actorCtx.sql.query(
4361
+ await sql.query(
4059
4362
  `SELECT * FROM ${quoted} LIMIT ? OFFSET ?`,
4060
4363
  [
4061
4364
  Math.max(0, Math.min(limit, 500)),
@@ -4098,15 +4401,11 @@ export function buildNativeFactory(
4098
4401
  const bindings = normalizeSqlitePropertyBindings(
4099
4402
  body.properties as Record<string, unknown>,
4100
4403
  );
4101
- const rows = queryRows(
4102
- await actorCtx.sql.query(body.sql, bindings),
4103
- );
4404
+ const rows = queryRows(await sql.query(body.sql, bindings));
4104
4405
  return jsonResponse({ rows: jsonSafe(rows) });
4105
4406
  }
4106
4407
  const args = Array.isArray(body.args) ? body.args : [];
4107
- const rows = queryRows(
4108
- await actorCtx.sql.query(body.sql, args),
4109
- );
4408
+ const rows = queryRows(await sql.query(body.sql, args));
4110
4409
  return jsonResponse({ rows: jsonSafe(rows) });
4111
4410
  }
4112
4411
  if (
@@ -4135,7 +4434,7 @@ export function buildNativeFactory(
4135
4434
  rpcs: Object.keys(actionHandlers).sort(),
4136
4435
  queueSize: inspectorSnapshot.queueSize,
4137
4436
  isStateEnabled: stateEnabled,
4138
- isDatabaseEnabled: databaseProvider !== undefined,
4437
+ isDatabaseEnabled: true,
4139
4438
  isWorkflowEnabled:
4140
4439
  getNativeWorkflowInspector(ctx) !== undefined,
4141
4440
  workflowState: await workflowState(),
@@ -4391,6 +4690,7 @@ export function buildNativeFactory(
4391
4690
  async (error: unknown, payload: { ctx: ActorContextHandle }) => {
4392
4691
  const { ctx } = unwrapTsfnPayload(error, payload);
4393
4692
  const actorCtx = makeActorCtx(ctx);
4693
+ const actorId = callNativeSync(() => runtime.actorId(ctx));
4394
4694
  // TODO: Move this save hook into cleanupNativeSleepRuntimeState
4395
4695
  // so immediate and deferred sleep cleanup share one save-state
4396
4696
  // path instead of passing a callback through cleanup.
@@ -4421,6 +4721,8 @@ export function buildNativeFactory(
4421
4721
  saveActorState,
4422
4722
  );
4423
4723
  } finally {
4724
+ runHandlerCoordinator?.destroy(actorId);
4725
+ disposeRunInspector(config.run, actorId);
4424
4726
  await actorCtx.dispose();
4425
4727
  }
4426
4728
  }
@@ -4430,16 +4732,16 @@ export function buildNativeFactory(
4430
4732
  async (error: unknown, payload: { ctx: ActorContextHandle }) => {
4431
4733
  const { ctx } = unwrapTsfnPayload(error, payload);
4432
4734
  const actorCtx = makeActorCtx(ctx);
4735
+ const actorId = callNativeSync(() => runtime.actorId(ctx));
4736
+ // Close run control before user cleanup so replay cannot race actor
4737
+ // destruction. Recreating this actor id receives a fresh controller.
4738
+ runHandlerCoordinator?.destroy(actorId);
4739
+ disposeRunInspector(config.run, actorId);
4433
4740
  try {
4434
4741
  if (typeof config.onDestroy === "function") {
4435
4742
  await config.onDestroy(actorCtx);
4436
4743
  }
4437
4744
  } finally {
4438
- const actorId = callNativeSync(() => runtime.actorId(ctx));
4439
- // Release actorId-keyed state so it does not accumulate per
4440
- // destroyed actor.
4441
- nativeRunHandlerActiveByActorId.delete(actorId);
4442
- disposeRunInspector(config.run, actorId);
4443
4745
  resolveNativeDestroy(runtime, ctx);
4444
4746
  await actorCtx.closeDatabase();
4445
4747
  clearNativeRuntimeState(runtime, ctx);
@@ -4520,6 +4822,7 @@ export function buildNativeFactory(
4520
4822
  connId,
4521
4823
  ),
4522
4824
  ),
4825
+ () => actorCtx.assertCanMutateState(),
4523
4826
  );
4524
4827
  try {
4525
4828
  const nextConnState = hasStaticConnState
@@ -4575,6 +4878,7 @@ export function buildNativeFactory(
4575
4878
  connId,
4576
4879
  ),
4577
4880
  ),
4881
+ () => actorCtx.assertCanMutateState(),
4578
4882
  );
4579
4883
  try {
4580
4884
  await config.onConnect(
@@ -4618,6 +4922,7 @@ export function buildNativeFactory(
4618
4922
  connId,
4619
4923
  ),
4620
4924
  ),
4925
+ () => actorCtx.assertCanMutateState(),
4621
4926
  ),
4622
4927
  );
4623
4928
  }
@@ -4826,7 +5131,6 @@ export function buildNativeFactory(
4826
5131
  )
4827
5132
  : undefined,
4828
5133
  run: (() => {
4829
- const run = getRunFunction(config.run);
4830
5134
  if (!run) {
4831
5135
  return undefined;
4832
5136
  }
@@ -4838,67 +5142,75 @@ export function buildNativeFactory(
4838
5142
  ) => {
4839
5143
  const { ctx } = unwrapTsfnPayload(error, payload);
4840
5144
  const actorId = callNativeSync(() => runtime.actorId(ctx));
4841
- const actorCtx = makeActorCtx(ctx);
4842
- nativeRunHandlerActiveByActorId.set(actorId, true);
4843
- try {
4844
- await run(actorCtx);
4845
- } finally {
4846
- // Delete rather than set(false): an absent entry already
4847
- // reads as inactive, and deleting keeps this map bounded
4848
- // to currently-running handlers instead of accumulating an
4849
- // entry per actor id forever.
4850
- nativeRunHandlerActiveByActorId.delete(actorId);
4851
- await actorCtx.dispose();
5145
+ const executeRun = async () => {
5146
+ const actorCtx = makeActorCtx(ctx);
5147
+ try {
5148
+ await run(actorCtx);
5149
+ } finally {
5150
+ await actorCtx.dispose();
5151
+ }
5152
+ };
5153
+ if (runHandlerCoordinator) {
5154
+ const outcome = await runHandlerCoordinator.run(
5155
+ actorId,
5156
+ () =>
5157
+ callNativeSync(() =>
5158
+ runtime.actorRestartRunHandler(ctx),
5159
+ ),
5160
+ executeRun,
5161
+ );
5162
+ // Only a start suppressed behind a failed replay must preserve a
5163
+ // consumed durable wake. Closed actors must remain closed.
5164
+ return outcome !== "suppressed";
4852
5165
  }
5166
+ await executeRun();
5167
+ return true;
4853
5168
  },
4854
5169
  );
4855
5170
  })(),
4856
- getWorkflowHistory:
4857
- getRunInspectorConfig(config.run) !== undefined
4858
- ? wrapNativeCallback(
4859
- async (
4860
- error: unknown,
4861
- payload: { ctx: ActorContextHandle },
4862
- ) => {
4863
- const { ctx } = unwrapTsfnPayload(error, payload);
4864
- const history =
4865
- getNativeWorkflowInspector(ctx)?.getHistory();
4866
- return history == null
4867
- ? undefined
4868
- : toUint8Array(history);
5171
+ getWorkflowHistory: hasRunInspectorConfig(config.run)
5172
+ ? wrapNativeCallback(
5173
+ async (
5174
+ error: unknown,
5175
+ payload: { ctx: ActorContextHandle },
5176
+ ) => {
5177
+ const { ctx } = unwrapTsfnPayload(error, payload);
5178
+ const history =
5179
+ getNativeWorkflowInspector(ctx)?.getHistory();
5180
+ return history == null
5181
+ ? undefined
5182
+ : toUint8Array(history);
5183
+ },
5184
+ )
5185
+ : undefined,
5186
+ replayWorkflow: hasRunInspectorConfig(config.run)
5187
+ ? wrapNativeCallback(
5188
+ async (
5189
+ error: unknown,
5190
+ payload: {
5191
+ ctx: ActorContextHandle;
5192
+ entryId?: string;
4869
5193
  },
4870
- )
4871
- : undefined,
4872
- replayWorkflow:
4873
- getRunInspectorConfig(config.run) !== undefined
4874
- ? wrapNativeCallback(
4875
- async (
4876
- error: unknown,
4877
- payload: {
4878
- ctx: ActorContextHandle;
4879
- entryId?: string;
4880
- },
4881
- ) => {
4882
- const { ctx, entryId } = unwrapTsfnPayload(
4883
- error,
4884
- payload,
4885
- );
4886
- const workflowInspector =
4887
- getNativeWorkflowInspector(ctx);
4888
- if (!workflowInspector?.replayFromStep) {
4889
- return undefined;
4890
- }
5194
+ ) => {
5195
+ const { ctx, entryId } = unwrapTsfnPayload(
5196
+ error,
5197
+ payload,
5198
+ );
5199
+ const workflowInspector =
5200
+ getNativeWorkflowInspector(ctx);
5201
+ if (!workflowInspector?.replayFromStep) {
5202
+ return undefined;
5203
+ }
4891
5204
 
4892
- const history =
4893
- (await workflowInspector.replayFromStep(
4894
- entryId,
4895
- )) ?? null;
4896
- return history == null
4897
- ? undefined
4898
- : toUint8Array(history);
4899
- },
4900
- )
4901
- : undefined,
5205
+ const history =
5206
+ (await workflowInspector.replayFromStep(entryId)) ??
5207
+ null;
5208
+ return history == null
5209
+ ? undefined
5210
+ : toUint8Array(history);
5211
+ },
5212
+ )
5213
+ : undefined,
4902
5214
  actions: Object.fromEntries(
4903
5215
  Object.entries(actionHandlers).map(([name, handler]) => [
4904
5216
  name,
@@ -4980,6 +5292,7 @@ export function buildNativeFactory(
4980
5292
  stateEnabled,
4981
5293
  onStateChange,
4982
5294
  cancelToken,
5295
+ run !== undefined,
4983
5296
  );
4984
5297
  try {
4985
5298
  if (