rivetkit 2.3.0-rc.8 → 2.3.0

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 (221) hide show
  1. package/dist/browser/client.d.ts +481 -74
  2. package/dist/browser/client.js +174 -148
  3. package/dist/browser/client.js.map +1 -1
  4. package/dist/browser/inspector/client.js +47 -18
  5. package/dist/browser/inspector/client.js.map +1 -1
  6. package/dist/tsup/actor/errors.cjs +2 -2
  7. package/dist/tsup/actor/errors.d.cts +1 -1
  8. package/dist/tsup/actor/errors.d.ts +1 -1
  9. package/dist/tsup/actor/errors.js +1 -1
  10. package/dist/tsup/agent-os/index.cjs +2160 -2086
  11. package/dist/tsup/agent-os/index.cjs.map +1 -1
  12. package/dist/tsup/agent-os/index.d.cts +479 -73
  13. package/dist/tsup/agent-os/index.d.ts +479 -73
  14. package/dist/tsup/agent-os/index.js +2160 -2086
  15. package/dist/tsup/agent-os/index.js.map +1 -1
  16. package/dist/tsup/{chunk-KY3CERZR.js → chunk-2OTRTA3J.js} +7 -21
  17. package/dist/tsup/chunk-2OTRTA3J.js.map +1 -0
  18. package/dist/tsup/{chunk-HGW6PBWR.cjs → chunk-3677IIOV.cjs} +11 -25
  19. package/dist/tsup/chunk-3677IIOV.cjs.map +1 -0
  20. package/dist/tsup/{chunk-OT7FF6GB.cjs → chunk-47HHIEXH.cjs} +24 -9
  21. package/dist/tsup/chunk-47HHIEXH.cjs.map +1 -0
  22. package/dist/tsup/{chunk-EMFKMVJR.js → chunk-4JDSFJS5.js} +69 -58
  23. package/dist/tsup/chunk-4JDSFJS5.js.map +1 -0
  24. package/dist/tsup/{chunk-7HLFSAJP.cjs → chunk-7QKCIVAY.cjs} +225 -214
  25. package/dist/tsup/chunk-7QKCIVAY.cjs.map +1 -0
  26. package/dist/tsup/{chunk-AWTPTUQ7.cjs → chunk-B6VUNZUD.cjs} +10 -10
  27. package/dist/tsup/{chunk-AWTPTUQ7.cjs.map → chunk-B6VUNZUD.cjs.map} +1 -1
  28. package/dist/tsup/{chunk-D3T3ZBSY.js → chunk-BEI24WTI.js} +2 -2
  29. package/dist/tsup/{chunk-TMLOKTRB.js → chunk-BRP62GZC.js} +1 -1
  30. package/dist/tsup/chunk-BRP62GZC.js.map +1 -0
  31. package/dist/tsup/{chunk-D5G75T7J.js → chunk-DPIMKYNB.js} +61 -2
  32. package/dist/tsup/chunk-DPIMKYNB.js.map +1 -0
  33. package/dist/tsup/{chunk-BATTOVHF.cjs → chunk-DXXJPH55.cjs} +40 -13
  34. package/dist/tsup/chunk-DXXJPH55.cjs.map +1 -0
  35. package/dist/tsup/{chunk-3YY5S6TV.js → chunk-HXUEHHJF.js} +2 -2
  36. package/dist/tsup/chunk-HXUEHHJF.js.map +1 -0
  37. package/dist/tsup/{chunk-4BPKKZJO.cjs → chunk-I4LN3FNT.cjs} +10 -10
  38. package/dist/tsup/chunk-I4LN3FNT.cjs.map +1 -0
  39. package/dist/tsup/{chunk-PCBNKI2J.js → chunk-JZ7TWV65.js} +1 -1
  40. package/dist/tsup/chunk-JZ7TWV65.js.map +1 -0
  41. package/dist/tsup/{chunk-63WNTDRC.cjs → chunk-KORQB2IR.cjs} +1 -1
  42. package/dist/tsup/{chunk-63WNTDRC.cjs.map → chunk-KORQB2IR.cjs.map} +1 -1
  43. package/dist/tsup/{chunk-6TQSSJ4F.cjs → chunk-LVTBW2RE.cjs} +3 -3
  44. package/dist/tsup/{chunk-6TQSSJ4F.cjs.map → chunk-LVTBW2RE.cjs.map} +1 -1
  45. package/dist/tsup/{chunk-4JU3IPG2.js → chunk-MEHBWPLJ.js} +6 -6
  46. package/dist/tsup/chunk-MEHBWPLJ.js.map +1 -0
  47. package/dist/tsup/{chunk-SRNOPUC6.cjs → chunk-NIY3RSPX.cjs} +62 -3
  48. package/dist/tsup/chunk-NIY3RSPX.cjs.map +1 -0
  49. package/dist/tsup/{chunk-UZXQEGVJ.js → chunk-P2GNQ4RN.js} +4 -4
  50. package/dist/tsup/{chunk-UZXQEGVJ.js.map → chunk-P2GNQ4RN.js.map} +1 -1
  51. package/dist/tsup/{chunk-VUGENVIK.js → chunk-UMZVD6DQ.js} +22 -7
  52. package/dist/tsup/chunk-UMZVD6DQ.js.map +1 -0
  53. package/dist/tsup/{chunk-LD5YASJU.cjs → chunk-VE2X4KMG.cjs} +2 -2
  54. package/dist/tsup/{chunk-LD5YASJU.cjs.map → chunk-VE2X4KMG.cjs.map} +1 -1
  55. package/dist/tsup/{chunk-GBG63SUG.js → chunk-VTTFNQQI.js} +32 -5
  56. package/dist/tsup/chunk-VTTFNQQI.js.map +1 -0
  57. package/dist/tsup/{chunk-2NDZ7JCR.cjs → chunk-ZA7FLHKH.cjs} +1 -1
  58. package/dist/tsup/chunk-ZA7FLHKH.cjs.map +1 -0
  59. package/dist/tsup/client/mod.cjs +9 -9
  60. package/dist/tsup/client/mod.d.cts +5 -5
  61. package/dist/tsup/client/mod.d.ts +5 -5
  62. package/dist/tsup/client/mod.js +8 -8
  63. package/dist/tsup/common/log.cjs +3 -3
  64. package/dist/tsup/common/log.js +2 -2
  65. package/dist/tsup/common/websocket.cjs +4 -4
  66. package/dist/tsup/common/websocket.js +3 -3
  67. package/dist/tsup/{config-Ak1lv4gF.d.ts → config-BxWAw3iH.d.ts} +512 -27
  68. package/dist/tsup/{config-DU_xj4qZ.d.cts → config-CZQQ-mso.d.cts} +512 -27
  69. package/dist/tsup/{config-CxjGYf4K.d.ts → config-D49x8NpL.d.cts} +1 -2
  70. package/dist/tsup/{config-CxjGYf4K.d.cts → config-D49x8NpL.d.ts} +1 -2
  71. package/dist/tsup/{context-DAAp4Lpg.d.ts → context-Bw7xq8w3.d.cts} +8 -8
  72. package/dist/tsup/{context-Dt_L55q8.d.cts → context-D8QA76sV.d.ts} +8 -8
  73. package/dist/tsup/db/drizzle.cjs +3 -3
  74. package/dist/tsup/db/drizzle.d.cts +1 -1
  75. package/dist/tsup/db/drizzle.d.ts +1 -1
  76. package/dist/tsup/db/drizzle.js +1 -1
  77. package/dist/tsup/db/mod.cjs +2 -2
  78. package/dist/tsup/db/mod.d.cts +2 -2
  79. package/dist/tsup/db/mod.d.ts +2 -2
  80. package/dist/tsup/db/mod.js +1 -1
  81. package/dist/tsup/dynamic/mod.cjs +24 -0
  82. package/dist/tsup/dynamic/mod.cjs.map +1 -0
  83. package/dist/tsup/dynamic/mod.d.cts +37 -0
  84. package/dist/tsup/dynamic/mod.d.ts +37 -0
  85. package/dist/tsup/dynamic/mod.js +24 -0
  86. package/dist/tsup/dynamic/mod.js.map +1 -0
  87. package/dist/tsup/inspector/mod.cjs +6 -6
  88. package/dist/tsup/inspector/mod.js +5 -5
  89. package/dist/tsup/inspector-tab/mod.cjs +173 -0
  90. package/dist/tsup/inspector-tab/mod.cjs.map +1 -0
  91. package/dist/tsup/inspector-tab/mod.d.cts +250 -0
  92. package/dist/tsup/inspector-tab/mod.d.ts +250 -0
  93. package/dist/tsup/inspector-tab/mod.js +173 -0
  94. package/dist/tsup/inspector-tab/mod.js.map +1 -0
  95. package/dist/tsup/mod.cjs +615 -348
  96. package/dist/tsup/mod.cjs.map +1 -1
  97. package/dist/tsup/mod.d.cts +5 -5
  98. package/dist/tsup/mod.d.ts +5 -5
  99. package/dist/tsup/mod.js +511 -244
  100. package/dist/tsup/mod.js.map +1 -1
  101. package/dist/tsup/test/mod.cjs +21 -18
  102. package/dist/tsup/test/mod.cjs.map +1 -1
  103. package/dist/tsup/test/mod.d.cts +4 -4
  104. package/dist/tsup/test/mod.d.ts +4 -4
  105. package/dist/tsup/test/mod.js +18 -15
  106. package/dist/tsup/test/mod.js.map +1 -1
  107. package/dist/tsup/{utils-DVekpm4I.d.cts → utils-DQosb24I.d.cts} +1 -1
  108. package/dist/tsup/{utils-DVekpm4I.d.ts → utils-DQosb24I.d.ts} +1 -1
  109. package/dist/tsup/utils.cjs +3 -3
  110. package/dist/tsup/utils.d.cts +1 -1
  111. package/dist/tsup/utils.d.ts +1 -1
  112. package/dist/tsup/utils.js +2 -2
  113. package/dist/tsup/workflow/mod.cjs +279 -279
  114. package/dist/tsup/workflow/mod.cjs.map +1 -1
  115. package/dist/tsup/workflow/mod.d.cts +6 -6
  116. package/dist/tsup/workflow/mod.d.ts +6 -6
  117. package/dist/tsup/workflow/mod.js +380 -380
  118. package/dist/tsup/workflow/mod.js.map +1 -1
  119. package/package.json +29 -9
  120. package/src/actor/config.ts +156 -51
  121. package/src/actor/contexts/index.ts +7 -2
  122. package/src/actor/definition.ts +17 -19
  123. package/src/actor/driver.ts +3 -3
  124. package/src/actor/errors.ts +8 -2
  125. package/src/actor/instance/mod.ts +26 -34
  126. package/src/actor/keys.ts +1 -1
  127. package/src/actor/mod.ts +22 -20
  128. package/src/actor/schema.ts +2 -2
  129. package/src/agent-os/actor/index.ts +38 -18
  130. package/src/agent-os/actor/preview.ts +1 -2
  131. package/src/agent-os/config.ts +1 -1
  132. package/src/agent-os/fs/database-vfs.ts +1 -1
  133. package/src/agent-os/index.ts +16 -15
  134. package/src/client/actor-common.ts +87 -54
  135. package/src/client/actor-conn.ts +11 -11
  136. package/src/client/actor-handle.ts +69 -52
  137. package/src/client/actor-query.ts +5 -5
  138. package/src/client/errors.ts +1 -1
  139. package/src/client/lifecycle-errors.ts +2 -4
  140. package/src/client/query.ts +1 -1
  141. package/src/client/queue.ts +8 -4
  142. package/src/client/raw-utils.ts +8 -6
  143. package/src/client/resolve-gateway-target.ts +1 -1
  144. package/src/client/utils.ts +2 -6
  145. package/src/common/actor-websocket.ts +3 -1
  146. package/src/common/bare/actor-persist/v1.ts +205 -163
  147. package/src/common/bare/actor-persist/v2.ts +265 -213
  148. package/src/common/bare/actor-persist/v3.ts +176 -172
  149. package/src/common/bare/actor-persist/v4.ts +254 -253
  150. package/src/common/bare/transport/v1.ts +659 -543
  151. package/src/common/client-protocol-versioned.ts +66 -64
  152. package/src/common/database/config.ts +2 -8
  153. package/src/common/database/native-database.ts +1 -1
  154. package/src/common/database/shared.ts +1 -0
  155. package/src/common/encoding.ts +13 -17
  156. package/src/common/engine.ts +28 -1
  157. package/src/common/eventsource.ts +1 -1
  158. package/src/common/inline-websocket-adapter.ts +3 -2
  159. package/src/common/router.ts +13 -17
  160. package/src/common/utils.ts +1 -2
  161. package/src/common/websocket-interface.ts +1 -1
  162. package/src/db/mod.ts +1 -1
  163. package/src/devtools-loader/index.ts +4 -7
  164. package/src/devtools-loader/serve-devtools.ts +26 -0
  165. package/src/drivers/engine/actor-driver.ts +48 -46
  166. package/src/dynamic/instance.ts +32 -0
  167. package/src/dynamic/internal.ts +50 -0
  168. package/src/dynamic/isolate-runtime.ts +66 -0
  169. package/src/dynamic/mod.ts +32 -0
  170. package/src/engine-client/actor-http-client.ts +3 -3
  171. package/src/engine-client/actor-websocket-client.ts +5 -5
  172. package/src/engine-client/api-endpoints.ts +51 -2
  173. package/src/engine-client/api-utils.ts +2 -2
  174. package/src/engine-client/driver.ts +1 -1
  175. package/src/engine-client/mod.ts +5 -3
  176. package/src/engine-client/ws-proxy.ts +9 -4
  177. package/src/inspector/client.browser.ts +5 -11
  178. package/src/inspector/mod.ts +1 -3
  179. package/src/inspector-tab/mod.ts +315 -0
  180. package/src/registry/config/envoy.ts +1 -2
  181. package/src/registry/config/index.ts +40 -16
  182. package/src/registry/index.ts +226 -83
  183. package/src/registry/napi-runtime.ts +46 -12
  184. package/src/registry/native-validation.ts +10 -12
  185. package/src/registry/native.ts +307 -164
  186. package/src/registry/process-metrics.ts +90 -23
  187. package/src/registry/runtime.ts +53 -6
  188. package/src/registry/wasm-runtime.ts +30 -3
  189. package/src/serde.ts +1 -1
  190. package/src/serverless/configure.ts +18 -7
  191. package/src/test/mod.ts +11 -8
  192. package/src/utils/endpoint-parser.ts +1 -1
  193. package/src/utils/env-vars.ts +6 -0
  194. package/src/utils/router.ts +1 -1
  195. package/src/utils/serve.ts +4 -5
  196. package/src/utils.ts +1 -2
  197. package/src/workflow/context.ts +30 -29
  198. package/src/workflow/driver.ts +4 -6
  199. package/src/workflow/inspector.ts +2 -2
  200. package/src/workflow/mod.ts +15 -17
  201. package/dist/tsup/chunk-2NDZ7JCR.cjs.map +0 -1
  202. package/dist/tsup/chunk-3YY5S6TV.js.map +0 -1
  203. package/dist/tsup/chunk-4BPKKZJO.cjs.map +0 -1
  204. package/dist/tsup/chunk-4JU3IPG2.js.map +0 -1
  205. package/dist/tsup/chunk-7HLFSAJP.cjs.map +0 -1
  206. package/dist/tsup/chunk-BATTOVHF.cjs.map +0 -1
  207. package/dist/tsup/chunk-D5G75T7J.js.map +0 -1
  208. package/dist/tsup/chunk-EMFKMVJR.js.map +0 -1
  209. package/dist/tsup/chunk-GBG63SUG.js.map +0 -1
  210. package/dist/tsup/chunk-HGW6PBWR.cjs.map +0 -1
  211. package/dist/tsup/chunk-KY3CERZR.js.map +0 -1
  212. package/dist/tsup/chunk-OT7FF6GB.cjs.map +0 -1
  213. package/dist/tsup/chunk-PCBNKI2J.js.map +0 -1
  214. package/dist/tsup/chunk-SRNOPUC6.cjs.map +0 -1
  215. package/dist/tsup/chunk-TMLOKTRB.js.map +0 -1
  216. package/dist/tsup/chunk-VUGENVIK.js.map +0 -1
  217. package/dist/tsup/process-metrics-NW754INA.js +0 -118
  218. package/dist/tsup/process-metrics-NW754INA.js.map +0 -1
  219. package/dist/tsup/process-metrics-TYAGKCEJ.cjs +0 -118
  220. package/dist/tsup/process-metrics-TYAGKCEJ.cjs.map +0 -1
  221. /package/dist/tsup/{chunk-D3T3ZBSY.js.map → chunk-BEI24WTI.js.map} +0 -0
@@ -2,9 +2,9 @@ import { VirtualWebSocket } from "@rivetkit/virtual-websocket";
2
2
  import {
3
3
  ACTOR_CONTEXT_INTERNAL_SYMBOL,
4
4
  CONN_STATE_MANAGER_SYMBOL,
5
- RAW_STATE_SYMBOL,
6
5
  getRunFunction,
7
6
  getRunInspectorConfig,
7
+ RAW_STATE_SYMBOL,
8
8
  type WorkflowInspectorConfig,
9
9
  } from "@/actor/config";
10
10
  import type { AnyActorDefinition } from "@/actor/definition";
@@ -33,11 +33,8 @@ import { convertRegistryConfigToClientConfig } from "@/client/config";
33
33
  import { HEADER_CONN_PARAMS } from "@/common/actor-router-consts";
34
34
  import type { AnyDatabaseProvider } from "@/common/database/config";
35
35
  import { wrapJsNativeDatabase } from "@/common/database/native-database";
36
+ import { assertJsonCompatValue, type JsonCompatValue } from "@/common/encoding";
36
37
  import { decodeWorkflowHistoryTransport } from "@/common/inspector-transport";
37
- import {
38
- assertJsonCompatValue,
39
- type JsonCompatValue,
40
- } from "@/common/encoding";
41
38
  import { deconstructError, stringifyError } from "@/common/utils";
42
39
  import type {
43
40
  RivetCloseEvent,
@@ -52,14 +49,9 @@ import type {
52
49
  RuntimeKind,
53
50
  SqliteBackend,
54
51
  } from "@/registry/config";
55
- import {
56
- decodeCborCompat,
57
- decodeCborJsonCompat,
58
- encodeCborCompat,
59
- } from "@/serde";
52
+ import { decodeCborCompat, encodeCborCompat } from "@/serde";
60
53
  import { getEnvUniversal, VERSION } from "@/utils";
61
54
  import { logger } from "./log";
62
- import { createWriteThroughProxy } from "./write-through-proxy";
63
55
  import { loadNapiRuntime } from "./napi-runtime";
64
56
  import {
65
57
  type NativeValidationConfig,
@@ -79,12 +71,16 @@ import type {
79
71
  RuntimeActorConfig,
80
72
  RuntimeBytes,
81
73
  RuntimeHttpResponse,
74
+ RuntimeInspectorTabEntry,
82
75
  RuntimeQueueMessage,
83
76
  RuntimeServeConfig,
84
77
  RuntimeStateDeltaPayload,
85
78
  WebSocketHandle,
86
79
  } from "./runtime";
87
80
  import { loadWasmRuntime } from "./wasm-runtime";
81
+ import nodeFs from "node:fs";
82
+ import nodePath from "node:path";
83
+ import { createWriteThroughProxy } from "./write-through-proxy";
88
84
 
89
85
  const textEncoder = new TextEncoder();
90
86
  const textDecoder = new TextDecoder();
@@ -249,6 +245,14 @@ type NativePersistActorState = {
249
245
  state: unknown;
250
246
  isInOnStateChange: boolean;
251
247
  connStates: Map<string, NativePersistConnState>;
248
+ // Memoized deep write-through proxy and the state object it wraps. Rebuilt
249
+ // only when the underlying state object identity changes.
250
+ stateProxy?: unknown;
251
+ stateProxyTarget?: unknown;
252
+ // Set when a coalesced save and onStateChange flush is pending for the
253
+ // current event loop tick.
254
+ saveScheduled?: boolean;
255
+ pendingSaveHandle?: ReturnType<typeof setImmediate>;
252
256
  };
253
257
  type NativeDestroyGate = {
254
258
  destroyCompletion?: Promise<void>;
@@ -420,21 +424,33 @@ function clearNativeRuntimeState(
420
424
  async function cleanupNativeSleepRuntimeState(
421
425
  runtime: CoreRuntime,
422
426
  ctx: ActorContextHandle,
427
+ afterTrackedWorkDrained?: () => Promise<void>,
423
428
  ): Promise<void> {
424
- const waitStarted = Date.now();
429
+ // The bounded wait gives shutdown work one grace-period chance to finish.
430
+ // Drained means all tracked shutdown work completed before the deadline, so
431
+ // we can save final state and clear runtime state immediately. If it did not
432
+ // drain, close database handles now, then defer the final save and clear until
433
+ // the tracked work finishes without a deadline.
425
434
  const drained = await runtime.actorWaitForTrackedShutdownWork(ctx);
426
- const waitMs = Date.now() - waitStarted;
427
- if (drained) {
428
- logger().debug({
429
- msg: "sleep cleanup: tracked shutdown work drained",
430
- waitMs,
431
- });
432
- } else {
433
- logger().warn({
434
- msg: "sleep cleanup: shutdown deadline reached before tracked work drained; closing DB anyway",
435
- waitMs,
436
- });
435
+ if (!drained) {
436
+ await closeNativeDatabaseClient(runtime, ctx);
437
+ await closeNativeSqlDatabase(runtime, ctx);
438
+ void runtime
439
+ .actorWaitForTrackedShutdownWorkUnbounded(ctx)
440
+ .then(async () => {
441
+ await afterTrackedWorkDrained?.();
442
+ clearNativeRuntimeState(runtime, ctx);
443
+ })
444
+ .catch((error) => {
445
+ logger().warn({
446
+ msg: "deferred native sleep cleanup failed",
447
+ error: stringifyError(error),
448
+ });
449
+ });
450
+ return;
437
451
  }
452
+
453
+ await afterTrackedWorkDrained?.();
438
454
  await closeNativeDatabaseClient(runtime, ctx);
439
455
  await closeNativeSqlDatabase(runtime, ctx);
440
456
  clearNativeRuntimeState(runtime, ctx);
@@ -607,13 +623,21 @@ function decodeValue<T>(value?: RuntimeBytes | null): T {
607
623
  return undefined as T;
608
624
  }
609
625
 
610
- return decodeCborJsonCompat(value);
626
+ return decodeCborCompat(value);
611
627
  }
612
628
 
613
629
  function encodeValue(value: unknown): RuntimeBytes {
614
630
  return encodeCborCompat(value as JsonCompatValue);
615
631
  }
616
632
 
633
+ function normalizeArgs(value: unknown): unknown[] {
634
+ return Array.isArray(value)
635
+ ? value
636
+ : value === undefined || value === null
637
+ ? []
638
+ : [value];
639
+ }
640
+
617
641
  function unwrapTsfnPayload<T>(error: unknown, payload: T): T {
618
642
  if (error !== null && error !== undefined) {
619
643
  throw error;
@@ -944,6 +968,7 @@ function serializeWorkflowEntryKind(
944
968
  }
945
969
  }
946
970
 
971
+ // TODO: Switch inspector routes to CBOR encoding
947
972
  function serializeWorkflowHistoryForJson(data: ArrayBuffer | null): {
948
973
  nameRegistry: string[];
949
974
  entries: Array<{
@@ -977,7 +1002,7 @@ function serializeWorkflowHistoryForJson(data: ArrayBuffer | null): {
977
1002
 
978
1003
  const history = decodeWorkflowHistoryTransport(data);
979
1004
 
980
- return {
1005
+ return jsonSafe({
981
1006
  nameRegistry: [...history.nameRegistry],
982
1007
  entries: history.entries.map((entry) => ({
983
1008
  id: entry.id,
@@ -1007,7 +1032,7 @@ function serializeWorkflowHistoryForJson(data: ArrayBuffer | null): {
1007
1032
  ],
1008
1033
  ),
1009
1034
  ),
1010
- };
1035
+ });
1011
1036
  }
1012
1037
 
1013
1038
  function toHttpJsonCompatible<T>(value: T): T {
@@ -1079,14 +1104,9 @@ function wrapNativeCallback<Args extends Array<unknown>, Result>(
1079
1104
 
1080
1105
  function decodeArgs(value?: RuntimeBytes | null): unknown[] {
1081
1106
  const decoded = decodeValue<unknown>(value);
1082
- return Array.isArray(decoded)
1083
- ? decoded
1084
- : decoded === undefined
1085
- ? []
1086
- : [decoded];
1107
+ return normalizeArgs(decoded);
1087
1108
  }
1088
1109
 
1089
-
1090
1110
  function buildRequest(init: {
1091
1111
  method: string;
1092
1112
  uri: string;
@@ -1182,11 +1202,15 @@ class NativeConnAdapter {
1182
1202
 
1183
1203
  get state(): unknown {
1184
1204
  const nextState = this.#readState();
1185
- return createWriteThroughProxy(nextState, (nextValue) => {
1186
- this.#writeState(nextValue, { writeNative: true });
1187
- }, (newValue) => {
1188
- assertJsonCompatValue(newValue);
1189
- });
1205
+ return createWriteThroughProxy(
1206
+ nextState,
1207
+ (nextValue) => {
1208
+ this.#writeState(nextValue, { writeNative: true });
1209
+ },
1210
+ (newValue) => {
1211
+ assertJsonCompatValue(newValue);
1212
+ },
1213
+ );
1190
1214
  }
1191
1215
 
1192
1216
  set state(value: unknown) {
@@ -1662,7 +1686,12 @@ class NativeQueueAdapter {
1662
1686
  signal?: AbortSignal;
1663
1687
  },
1664
1688
  ) {
1665
- if (!options?.signal) {
1689
+ const { token, cleanup } = await createCancellationTokenHandle(
1690
+ this.#runtime,
1691
+ options?.signal,
1692
+ );
1693
+
1694
+ try {
1666
1695
  await callNative(() =>
1667
1696
  this.#runtime.actorQueueWaitForNamesAvailable(
1668
1697
  this.#ctx,
@@ -1670,57 +1699,11 @@ class NativeQueueAdapter {
1670
1699
  {
1671
1700
  timeoutMs: options?.timeout,
1672
1701
  },
1702
+ token,
1673
1703
  ),
1674
1704
  );
1675
- return;
1676
- }
1677
-
1678
- const deadline =
1679
- options.timeout === undefined
1680
- ? undefined
1681
- : Date.now() + options.timeout;
1682
-
1683
- for (;;) {
1684
- if (options.signal.aborted) {
1685
- throw actorAbortedError();
1686
- }
1687
-
1688
- const remainingTimeout =
1689
- deadline === undefined
1690
- ? undefined
1691
- : Math.max(0, deadline - Date.now());
1692
- const sliceTimeout =
1693
- remainingTimeout === undefined
1694
- ? 100
1695
- : Math.min(remainingTimeout, 100);
1696
-
1697
- try {
1698
- await callNative(() =>
1699
- this.#runtime.actorQueueWaitForNamesAvailable(
1700
- this.#ctx,
1701
- [...names],
1702
- {
1703
- timeoutMs: sliceTimeout,
1704
- },
1705
- ),
1706
- );
1707
- return;
1708
- } catch (error) {
1709
- if (
1710
- (error as { group?: string; code?: string }).group ===
1711
- "queue" &&
1712
- (error as { group?: string; code?: string }).code ===
1713
- "timed_out"
1714
- ) {
1715
- if (
1716
- remainingTimeout === undefined ||
1717
- remainingTimeout > 100
1718
- ) {
1719
- continue;
1720
- }
1721
- }
1722
- throw error;
1723
- }
1705
+ } finally {
1706
+ cleanup?.();
1724
1707
  }
1725
1708
  }
1726
1709
 
@@ -2339,9 +2322,7 @@ class NativeConnectionMap implements ReadonlyMap<string, NativeConnAdapter> {
2339
2322
 
2340
2323
  get(key: string): NativeConnAdapter | undefined {
2341
2324
  const conns = callNativeSync(() => this.#runtime.actorConns(this.#ctx));
2342
- const conn = conns.find(
2343
- (c) => this.#runtime.connId(c) === key,
2344
- );
2325
+ const conn = conns.find((c) => this.#runtime.connId(c) === key);
2345
2326
  if (!conn) return undefined;
2346
2327
  return this.#connToAdapter(conn);
2347
2328
  }
@@ -2353,23 +2334,39 @@ class NativeConnectionMap implements ReadonlyMap<string, NativeConnAdapter> {
2353
2334
 
2354
2335
  keys(): MapIterator<string> {
2355
2336
  const conns = callNativeSync(() => this.#runtime.actorConns(this.#ctx));
2356
- return conns.map((c) => this.#runtime.connId(c))[Symbol.iterator]() satisfies MapIterator<string>;
2337
+ return conns
2338
+ .map((c) => this.#runtime.connId(c))
2339
+ [Symbol.iterator]() satisfies MapIterator<string>;
2357
2340
  }
2358
2341
 
2359
2342
  values(): MapIterator<NativeConnAdapter> {
2360
2343
  const conns = callNativeSync(() => this.#runtime.actorConns(this.#ctx));
2361
- return conns.map((c) => this.#connToAdapter(c))[Symbol.iterator]() satisfies MapIterator<NativeConnAdapter>;
2344
+ return conns
2345
+ .map((c) => this.#connToAdapter(c))
2346
+ [Symbol.iterator]() satisfies MapIterator<NativeConnAdapter>;
2362
2347
  }
2363
2348
 
2364
2349
  entries(): MapIterator<[string, NativeConnAdapter]> {
2365
2350
  const conns = callNativeSync(() => this.#runtime.actorConns(this.#ctx));
2366
- return conns.map(
2367
- (c) => [this.#runtime.connId(c), this.#connToAdapter(c)] as [string, NativeConnAdapter],
2368
- )[Symbol.iterator]() satisfies MapIterator<[string, NativeConnAdapter]>;
2351
+ return conns
2352
+ .map(
2353
+ (c) =>
2354
+ [this.#runtime.connId(c), this.#connToAdapter(c)] as [
2355
+ string,
2356
+ NativeConnAdapter,
2357
+ ],
2358
+ )
2359
+ [Symbol.iterator]() satisfies MapIterator<
2360
+ [string, NativeConnAdapter]
2361
+ >;
2369
2362
  }
2370
2363
 
2371
2364
  forEach(
2372
- callback: (value: NativeConnAdapter, key: string, map: ReadonlyMap<string, NativeConnAdapter>) => void,
2365
+ callback: (
2366
+ value: NativeConnAdapter,
2367
+ key: string,
2368
+ map: ReadonlyMap<string, NativeConnAdapter>,
2369
+ ) => void,
2373
2370
  thisArg?: unknown,
2374
2371
  ): void {
2375
2372
  const conns = callNativeSync(() => this.#runtime.actorConns(this.#ctx));
@@ -2484,17 +2481,29 @@ export class ActorContextHandleAdapter {
2484
2481
  if (!this.#stateEnabled) {
2485
2482
  throw stateNotEnabledError();
2486
2483
  }
2484
+ const actorState = getNativePersistState(this.#runtime, this.#ctx);
2487
2485
  const nextState = this.#readState();
2488
- return createWriteThroughProxy(
2489
- nextState,
2490
- (nextValue) => {
2491
- this.#writeState(nextValue, { scheduleSave: true });
2492
- },
2493
- (newValue) => {
2494
- this.#assertCanMutateState();
2495
- assertJsonCompatValue(newValue);
2496
- },
2497
- );
2486
+ // Reading `c.state` rebuilds the deep write-through proxy, which
2487
+ // allocates fresh on-change caches and rewraps the whole tree. Memoize
2488
+ // the proxy keyed on the underlying state object so repeated reads and
2489
+ // deep read cascades reuse a single proxy.
2490
+ if (
2491
+ actorState.stateProxy === undefined ||
2492
+ actorState.stateProxyTarget !== nextState
2493
+ ) {
2494
+ actorState.stateProxyTarget = nextState;
2495
+ actorState.stateProxy = createWriteThroughProxy(
2496
+ nextState,
2497
+ (nextValue) => {
2498
+ this.#writeState(nextValue, { scheduleSave: true });
2499
+ },
2500
+ (newValue) => {
2501
+ this.#assertCanMutateState();
2502
+ assertJsonCompatValue(newValue);
2503
+ },
2504
+ );
2505
+ }
2506
+ return actorState.stateProxy;
2498
2507
  }
2499
2508
 
2500
2509
  set state(value: unknown) {
@@ -2571,7 +2580,11 @@ export class ActorContextHandleAdapter {
2571
2580
 
2572
2581
  get conns(): ReadonlyMap<string, NativeConnAdapter> {
2573
2582
  if (!this.#connMap) {
2574
- this.#connMap = new NativeConnectionMap(this.#runtime, this.#ctx, this.#schemas);
2583
+ this.#connMap = new NativeConnectionMap(
2584
+ this.#runtime,
2585
+ this.#ctx,
2586
+ this.#schemas,
2587
+ );
2575
2588
  }
2576
2589
  return this.#connMap;
2577
2590
  }
@@ -2804,41 +2817,22 @@ export class ActorContextHandleAdapter {
2804
2817
  }
2805
2818
 
2806
2819
  keepAwake<T>(promise: Promise<T>): Promise<T> {
2807
- const startedAt = Date.now();
2808
- logger().debug({
2809
- msg: "keepAwake registered",
2810
- at: startedAt,
2811
- });
2812
2820
  const trackedPromise = Promise.resolve(promise)
2813
- .then(
2814
- () => {
2815
- logger().debug({
2816
- msg: "keepAwake promise resolved",
2817
- durationMs: Date.now() - startedAt,
2818
- });
2819
- },
2820
- (error) => {
2821
- logger().warn({
2822
- msg: "keepAwake promise rejected",
2823
- durationMs: Date.now() - startedAt,
2824
- error: stringifyError(error),
2825
- });
2826
- },
2827
- )
2821
+ .catch((error) => {
2822
+ logger().warn({
2823
+ msg: "keepAwake promise rejected",
2824
+ error: stringifyError(error),
2825
+ });
2826
+ })
2828
2827
  .then(() => null);
2829
2828
  try {
2830
2829
  callNativeSync(() =>
2831
2830
  this.#runtime.actorKeepAwake(this.#ctx, trackedPromise),
2832
2831
  );
2833
2832
  } catch (error) {
2834
- if (isClosedTaskRegistrationError(error)) {
2835
- logger().warn({
2836
- msg: "keepAwake registration dropped (teardown already started); promise will not delay grace",
2837
- error: stringifyError(error),
2838
- });
2839
- return promise;
2833
+ if (!isClosedTaskRegistrationError(error)) {
2834
+ throw error;
2840
2835
  }
2841
- throw error;
2842
2836
  }
2843
2837
  return promise;
2844
2838
  }
@@ -2911,6 +2905,7 @@ export class ActorContextHandleAdapter {
2911
2905
  }
2912
2906
 
2913
2907
  sleep(): void {
2908
+ this.#flushStateChange();
2914
2909
  callNativeSync(() => this.#runtime.actorSleep(this.#ctx));
2915
2910
  }
2916
2911
 
@@ -2934,6 +2929,9 @@ export class ActorContextHandleAdapter {
2934
2929
  }
2935
2930
 
2936
2931
  async dispose(): Promise<void> {
2932
+ // Flush any save coalesced for this tick before the context is torn
2933
+ // down so the request-save and onStateChange always run.
2934
+ this.#flushStateChange();
2937
2935
  this.#abortSignalCleanup?.();
2938
2936
  this.#sql = undefined;
2939
2937
  }
@@ -2969,13 +2967,12 @@ export class ActorContextHandleAdapter {
2969
2967
  scheduleSave: boolean;
2970
2968
  },
2971
2969
  ): void {
2972
- encodeValue(value);
2973
2970
  const actorState = getNativePersistState(this.#runtime, this.#ctx);
2974
2971
  actorState.state = value;
2975
2972
  if (!options.scheduleSave) {
2976
2973
  return;
2977
2974
  }
2978
- this.#handleStateChange();
2975
+ this.#scheduleSave();
2979
2976
  }
2980
2977
 
2981
2978
  #assertCanMutateState(): void {
@@ -2985,9 +2982,33 @@ export class ActorContextHandleAdapter {
2985
2982
  }
2986
2983
  }
2987
2984
 
2988
- #handleStateChange(): void {
2985
+ // Coalesce the request-save and onStateChange work to once per event loop
2986
+ // tick. A synchronous burst of mutations (for example
2987
+ // `Object.assign(c.state, ...)`) would otherwise cross the NAPI boundary and
2988
+ // run onStateChange once per field, re-serializing the whole state each time
2989
+ // and pinning the event loop on large state.
2990
+ #scheduleSave(): void {
2989
2991
  const actorState = getNativePersistState(this.#runtime, this.#ctx);
2990
- encodeValue(actorState.state);
2992
+ if (actorState.saveScheduled) {
2993
+ return;
2994
+ }
2995
+ actorState.saveScheduled = true;
2996
+ actorState.pendingSaveHandle = setImmediate(() => {
2997
+ this.#flushStateChange();
2998
+ });
2999
+ }
3000
+
3001
+ #flushStateChange(): void {
3002
+ const actorState = getNativePersistState(this.#runtime, this.#ctx);
3003
+ if (!actorState.saveScheduled) {
3004
+ return;
3005
+ }
3006
+ actorState.saveScheduled = false;
3007
+ if (actorState.pendingSaveHandle !== undefined) {
3008
+ clearImmediate(actorState.pendingSaveHandle);
3009
+ actorState.pendingSaveHandle = undefined;
3010
+ }
3011
+
2991
3012
  callNativeSync(() =>
2992
3013
  this.#runtime.actorRequestSave(this.#ctx, { immediate: false }),
2993
3014
  );
@@ -3309,9 +3330,87 @@ function buildActorConfig(
3309
3330
  actions: Object.keys((config.actions ?? {}) as Record<string, unknown>)
3310
3331
  .sort()
3311
3332
  .map((name) => ({ name })),
3333
+ inspectorTabs: buildInspectorTabs(config.inspector),
3312
3334
  };
3313
3335
  }
3314
3336
 
3337
+ function buildInspectorTabs(
3338
+ inspector: unknown,
3339
+ ): Array<RuntimeInspectorTabEntry> | undefined {
3340
+ if (!inspector || typeof inspector !== "object") return undefined;
3341
+ const tabs = (inspector as { tabs?: unknown }).tabs;
3342
+ if (!Array.isArray(tabs) || tabs.length === 0) return undefined;
3343
+ return tabs.map((raw) => {
3344
+ const entry = raw as {
3345
+ id: string;
3346
+ label?: string;
3347
+ source?: string;
3348
+ icon?: string;
3349
+ hidden?: boolean;
3350
+ };
3351
+ if (entry.hidden === true) {
3352
+ return { id: entry.id, hidden: true };
3353
+ }
3354
+ // Resolve the author's source path against the current working
3355
+ // directory so the Rust runtime gets an absolute path. The author
3356
+ // runs the actor process from their project root by convention.
3357
+ const resolved =
3358
+ entry.source !== undefined
3359
+ ? nodePath.resolve(entry.source)
3360
+ : undefined;
3361
+ if (resolved !== undefined) {
3362
+ validateInspectorTabSource(entry.id, resolved);
3363
+ }
3364
+ return {
3365
+ id: entry.id,
3366
+ label: entry.label,
3367
+ icon: entry.icon,
3368
+ source: resolved,
3369
+ };
3370
+ });
3371
+ }
3372
+
3373
+ function validateInspectorTabSource(tabId: string, resolved: string): void {
3374
+ // Catch obviously dangerous misconfigurations at registry construction
3375
+ // rather than silently exposing the wrong subtree over the unauthenticated
3376
+ // `/inspector/custom-tabs/<id>/*` route. Fail loudly so misconfigured
3377
+ // actors never start.
3378
+ if (resolved === nodePath.parse(resolved).root) {
3379
+ throw new Error(
3380
+ `inspector.tabs[id="${tabId}"].source resolves to the filesystem root (${resolved}). ` +
3381
+ "Point it at the tab's own static-asset directory instead.",
3382
+ );
3383
+ }
3384
+ let stat: import("node:fs").Stats;
3385
+ try {
3386
+ stat = nodeFs.statSync(resolved);
3387
+ } catch (err) {
3388
+ const code = (err as NodeJS.ErrnoException)?.code;
3389
+ if (code === "ENOENT") {
3390
+ throw new Error(
3391
+ `inspector.tabs[id="${tabId}"].source (${resolved}) does not exist.`,
3392
+ );
3393
+ }
3394
+ if (code === "EACCES") {
3395
+ throw new Error(
3396
+ `inspector.tabs[id="${tabId}"].source (${resolved}) is not readable (EACCES).`,
3397
+ );
3398
+ }
3399
+ throw new Error(
3400
+ `inspector.tabs[id="${tabId}"].source (${resolved}) could not be stat'd: ${
3401
+ (err as Error)?.message ?? err
3402
+ }`,
3403
+ );
3404
+ }
3405
+ if (!stat.isDirectory()) {
3406
+ throw new Error(
3407
+ `inspector.tabs[id="${tabId}"].source (${resolved}) must be a directory, got ${
3408
+ stat.isFile() ? "file" : "non-directory"
3409
+ }.`,
3410
+ );
3411
+ }
3412
+ }
3413
+
3315
3414
  export function buildNativeFactory(
3316
3415
  runtime: CoreRuntime,
3317
3416
  registryConfig: RegistryConfig,
@@ -3742,14 +3841,38 @@ export function buildNativeFactory(
3742
3841
  404,
3743
3842
  );
3744
3843
  }
3745
- const body = (await jsRequest.json()) as { args?: unknown[] };
3844
+ const body = (await jsRequest.json()) as {
3845
+ args?: unknown;
3846
+ properties?: unknown;
3847
+ };
3848
+ if (body.args !== undefined && body.properties !== undefined) {
3849
+ return jsonResponse(
3850
+ { error: "use either args or properties, not both" },
3851
+ { status: 400 },
3852
+ );
3853
+ }
3854
+ if (
3855
+ body.properties !== undefined &&
3856
+ (body.properties === null ||
3857
+ typeof body.properties !== "object" ||
3858
+ Array.isArray(body.properties))
3859
+ ) {
3860
+ return jsonResponse(
3861
+ { error: "properties must be an object" },
3862
+ { status: 400 },
3863
+ );
3864
+ }
3865
+ const args =
3866
+ body.properties !== undefined
3867
+ ? [body.properties]
3868
+ : normalizeArgs(body.args);
3746
3869
  try {
3747
3870
  const output = await action(
3748
3871
  actorCtx,
3749
3872
  ...validateActionArgs(
3750
3873
  schemaConfig.actionInputSchemas,
3751
3874
  actionName,
3752
- body.args ?? [],
3875
+ args,
3753
3876
  ),
3754
3877
  );
3755
3878
  return jsonResponse({ output });
@@ -3948,26 +4071,35 @@ export function buildNativeFactory(
3948
4071
  async (error: unknown, payload: { ctx: ActorContextHandle }) => {
3949
4072
  const { ctx } = unwrapTsfnPayload(error, payload);
3950
4073
  const actorCtx = makeActorCtx(ctx);
4074
+ // TODO: Move this save hook into cleanupNativeSleepRuntimeState
4075
+ // so immediate and deferred sleep cleanup share one save-state
4076
+ // path instead of passing a callback through cleanup.
4077
+ const saveActorState = async () => {
4078
+ if (runtime.kind === "wasm") {
4079
+ // Wasm cannot use the native context save helper here because
4080
+ // the runtime owns the serialized state handoff.
4081
+ await runtime.actorSaveState(
4082
+ ctx,
4083
+ actorCtx.serializeForTick("save"),
4084
+ );
4085
+ } else {
4086
+ await actorCtx.saveState({
4087
+ immediate: true,
4088
+ });
4089
+ }
4090
+ };
3951
4091
  try {
3952
4092
  if (onSleep) {
3953
- try {
3954
- await onSleep(actorCtx);
3955
- } finally {
3956
- if (runtime.kind === "wasm") {
3957
- // Wasm cannot use the native context save helper here because
3958
- // the runtime owns the serialized state handoff.
3959
- await runtime.actorSaveState(
3960
- ctx,
3961
- actorCtx.serializeForTick("save"),
3962
- );
3963
- } else {
3964
- await actorCtx.saveState({ immediate: true });
3965
- }
3966
- }
4093
+ await onSleep(actorCtx);
3967
4094
  }
4095
+ await saveActorState();
3968
4096
  } finally {
3969
4097
  try {
3970
- await cleanupNativeSleepRuntimeState(runtime, ctx);
4098
+ await cleanupNativeSleepRuntimeState(
4099
+ runtime,
4100
+ ctx,
4101
+ saveActorState,
4102
+ );
3971
4103
  } finally {
3972
4104
  await actorCtx.dispose();
3973
4105
  }
@@ -4624,10 +4756,29 @@ export async function buildServeConfig(
4624
4756
  serverlessMaxStartPayloadBytes: config.serverless.maxStartPayloadBytes,
4625
4757
  };
4626
4758
 
4627
- if (config.startEngine) {
4759
+ // Always best-effort resolve the npm-installed engine binary and hand its
4760
+ // path to the core. The core alone decides whether to actually spawn a local
4761
+ // engine (its `should_manage_engine`, based on the endpoint + spawn mode), so
4762
+ // JS must not duplicate that decision here. Only JS knows the npm
4763
+ // `node_modules` layout, so it resolves the path; if no binary is available
4764
+ // (remote-only install, unsupported platform, optional deps skipped), leave
4765
+ // it unset and let the core report `engine.binary_unavailable` if it actually
4766
+ // needs one.
4767
+ try {
4628
4768
  const { getEnginePath } = await loadEngineCli();
4629
4769
  serveConfig.engineBinaryPath = getEnginePath();
4770
+ } catch (error) {
4771
+ // The npm-installed engine binary could not be resolved. The core still
4772
+ // decides whether it needs to spawn a local engine; if it does, it will
4773
+ // fail with engine.binary_unavailable (auto-download is off in the napi
4774
+ // runtime). Warn so the cause is actionable.
4775
+ logger().warn({
4776
+ msg: "could not resolve a local engine binary; if a local engine must be spawned it will fail with engine.binary_unavailable — set RIVET_ENGINE_BINARY_PATH or install the @rivetkit/engine-cli platform package",
4777
+ error: stringifyError(error),
4778
+ });
4630
4779
  }
4780
+ serveConfig.engineHost = config.engineHost;
4781
+ serveConfig.enginePort = config.enginePort;
4631
4782
  if (config.test?.enabled) {
4632
4783
  serveConfig.inspectorTestToken =
4633
4784
  getEnvUniversal("_RIVET_TEST_INSPECTOR_TOKEN") ?? "token";
@@ -4686,14 +4837,6 @@ export async function buildConfiguredRegistry(config: RegistryConfig): Promise<{
4686
4837
  serveConfig: RuntimeServeConfig;
4687
4838
  }> {
4688
4839
  const runtime = await loadConfiguredRuntime(config);
4689
- if (runtime.kind === "napi") {
4690
- // Start Node.js runtime health metrics collection (event loop lag,
4691
- // GC, heap, CPU, libuv handles). Only available on the native NAPI
4692
- // runtime; wasm/edge hosts do not expose perf_hooks/v8 the same
4693
- // way and have no Rust-side prometheus collectors loaded.
4694
- const { startProcessMetrics } = await import("./process-metrics");
4695
- startProcessMetrics();
4696
- }
4697
4840
  return buildRegistryWithRuntime(
4698
4841
  normalizeRuntimeConfig(config, runtime),
4699
4842
  runtime,