rivetkit 2.3.0-rc.8 → 2.3.0-rc.9

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 (122) hide show
  1. package/dist/browser/client.d.ts +8 -37
  2. package/dist/browser/client.js +34 -64
  3. package/dist/browser/client.js.map +1 -1
  4. package/dist/browser/inspector/client.js +3 -4
  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.js +1 -1
  8. package/dist/tsup/agent-os/index.cjs +5 -7
  9. package/dist/tsup/agent-os/index.cjs.map +1 -1
  10. package/dist/tsup/agent-os/index.d.cts +5 -26
  11. package/dist/tsup/agent-os/index.d.ts +5 -26
  12. package/dist/tsup/agent-os/index.js +5 -7
  13. package/dist/tsup/agent-os/index.js.map +1 -1
  14. package/dist/tsup/{chunk-OT7FF6GB.cjs → chunk-4CGA6QJO.cjs} +4 -4
  15. package/dist/tsup/{chunk-OT7FF6GB.cjs.map → chunk-4CGA6QJO.cjs.map} +1 -1
  16. package/dist/tsup/{chunk-4BPKKZJO.cjs → chunk-4WPEZBK4.cjs} +8 -8
  17. package/dist/tsup/{chunk-4BPKKZJO.cjs.map → chunk-4WPEZBK4.cjs.map} +1 -1
  18. package/dist/tsup/{chunk-AWTPTUQ7.cjs → chunk-CPA4Y3RG.cjs} +10 -10
  19. package/dist/tsup/chunk-CPA4Y3RG.cjs.map +1 -0
  20. package/dist/tsup/{chunk-EMFKMVJR.js → chunk-F3Q5BFQ6.js} +34 -10
  21. package/dist/tsup/chunk-F3Q5BFQ6.js.map +1 -0
  22. package/dist/tsup/{chunk-7HLFSAJP.cjs → chunk-GVTOE34S.cjs} +193 -169
  23. package/dist/tsup/chunk-GVTOE34S.cjs.map +1 -0
  24. package/dist/tsup/{chunk-D3T3ZBSY.js → chunk-H37XQU3I.js} +2 -2
  25. package/dist/tsup/{chunk-4JU3IPG2.js → chunk-H7P7WR2Y.js} +3 -3
  26. package/dist/tsup/{chunk-TMLOKTRB.js → chunk-KIWH5H3K.js} +3 -3
  27. package/dist/tsup/chunk-KIWH5H3K.js.map +1 -0
  28. package/dist/tsup/{chunk-VUGENVIK.js → chunk-KJTA3ATT.js} +2 -2
  29. package/dist/tsup/{chunk-6TQSSJ4F.cjs → chunk-MALSPBAF.cjs} +3 -3
  30. package/dist/tsup/{chunk-6TQSSJ4F.cjs.map → chunk-MALSPBAF.cjs.map} +1 -1
  31. package/dist/tsup/{chunk-UZXQEGVJ.js → chunk-MMMEZM5J.js} +4 -4
  32. package/dist/tsup/chunk-MMMEZM5J.js.map +1 -0
  33. package/dist/tsup/{chunk-63WNTDRC.cjs → chunk-QAZLM4WT.cjs} +3 -3
  34. package/dist/tsup/{chunk-63WNTDRC.cjs.map → chunk-QAZLM4WT.cjs.map} +1 -1
  35. package/dist/tsup/{chunk-D5G75T7J.js → chunk-T6YVRM4K.js} +1 -3
  36. package/dist/tsup/chunk-T6YVRM4K.js.map +1 -0
  37. package/dist/tsup/{chunk-HGW6PBWR.cjs → chunk-VJFRBJVQ.cjs} +9 -137
  38. package/dist/tsup/chunk-VJFRBJVQ.cjs.map +1 -0
  39. package/dist/tsup/{chunk-GBG63SUG.js → chunk-VRCIXJRN.js} +5 -7
  40. package/dist/tsup/chunk-VRCIXJRN.js.map +1 -0
  41. package/dist/tsup/{chunk-KY3CERZR.js → chunk-W7EYSYVI.js} +4 -132
  42. package/dist/tsup/chunk-W7EYSYVI.js.map +1 -0
  43. package/dist/tsup/{chunk-SRNOPUC6.cjs → chunk-WQ4HNA4W.cjs} +2 -4
  44. package/dist/tsup/chunk-WQ4HNA4W.cjs.map +1 -0
  45. package/dist/tsup/{chunk-BATTOVHF.cjs → chunk-Y5NSCZA2.cjs} +12 -14
  46. package/dist/tsup/chunk-Y5NSCZA2.cjs.map +1 -0
  47. package/dist/tsup/client/mod.cjs +7 -7
  48. package/dist/tsup/client/mod.d.cts +2 -2
  49. package/dist/tsup/client/mod.d.ts +2 -2
  50. package/dist/tsup/client/mod.js +6 -6
  51. package/dist/tsup/common/log.cjs +3 -3
  52. package/dist/tsup/common/log.js +2 -2
  53. package/dist/tsup/common/websocket.cjs +4 -4
  54. package/dist/tsup/common/websocket.js +3 -3
  55. package/dist/tsup/{config-Ak1lv4gF.d.ts → config-0Ta55UV0.d.ts} +6 -27
  56. package/dist/tsup/{config-DU_xj4qZ.d.cts → config-Ca8dN4cS.d.cts} +6 -27
  57. package/dist/tsup/{context-DAAp4Lpg.d.ts → context-B_IWbWne.d.ts} +1 -1
  58. package/dist/tsup/{context-Dt_L55q8.d.cts → context-CUrQ9MHc.d.cts} +1 -1
  59. package/dist/tsup/inspector/mod.cjs +6 -6
  60. package/dist/tsup/inspector/mod.js +5 -5
  61. package/dist/tsup/mod.cjs +355 -482
  62. package/dist/tsup/mod.cjs.map +1 -1
  63. package/dist/tsup/mod.d.cts +3 -3
  64. package/dist/tsup/mod.d.ts +3 -3
  65. package/dist/tsup/mod.js +276 -403
  66. package/dist/tsup/mod.js.map +1 -1
  67. package/dist/tsup/test/mod.cjs +10 -10
  68. package/dist/tsup/test/mod.d.cts +1 -1
  69. package/dist/tsup/test/mod.d.ts +1 -1
  70. package/dist/tsup/test/mod.js +6 -6
  71. package/dist/tsup/utils.cjs +3 -3
  72. package/dist/tsup/utils.js +2 -2
  73. package/dist/tsup/workflow/mod.cjs +16 -41
  74. package/dist/tsup/workflow/mod.cjs.map +1 -1
  75. package/dist/tsup/workflow/mod.d.cts +3 -3
  76. package/dist/tsup/workflow/mod.d.ts +3 -3
  77. package/dist/tsup/workflow/mod.js +10 -35
  78. package/dist/tsup/workflow/mod.js.map +1 -1
  79. package/package.json +10 -11
  80. package/src/actor/config.ts +0 -3
  81. package/src/actor/errors.ts +2 -2
  82. package/src/agent-os/actor/session.ts +2 -2
  83. package/src/client/actor-conn.ts +34 -6
  84. package/src/client/actor-handle.ts +1 -2
  85. package/src/client/queue.ts +1 -2
  86. package/src/client/utils.ts +1 -0
  87. package/src/common/encoding.ts +5 -243
  88. package/src/common/inline-websocket-adapter.ts +12 -12
  89. package/src/common/log.ts +0 -1
  90. package/src/common/router.ts +2 -2
  91. package/src/common/utils.ts +148 -0
  92. package/src/drivers/engine/actor-driver.ts +11 -11
  93. package/src/engine-client/actor-websocket-client.ts +1 -2
  94. package/src/engine-client/mod.ts +2 -3
  95. package/src/registry/index.ts +109 -46
  96. package/src/registry/napi-runtime.ts +34 -11
  97. package/src/registry/native.ts +162 -205
  98. package/src/registry/runtime.ts +12 -5
  99. package/src/registry/wasm-runtime.ts +13 -2
  100. package/src/serde.ts +2 -2
  101. package/src/workflow/context.ts +5 -32
  102. package/src/workflow/inspector.ts +1 -2
  103. package/dist/tsup/chunk-7HLFSAJP.cjs.map +0 -1
  104. package/dist/tsup/chunk-AWTPTUQ7.cjs.map +0 -1
  105. package/dist/tsup/chunk-BATTOVHF.cjs.map +0 -1
  106. package/dist/tsup/chunk-D5G75T7J.js.map +0 -1
  107. package/dist/tsup/chunk-EMFKMVJR.js.map +0 -1
  108. package/dist/tsup/chunk-GBG63SUG.js.map +0 -1
  109. package/dist/tsup/chunk-HGW6PBWR.cjs.map +0 -1
  110. package/dist/tsup/chunk-KY3CERZR.js.map +0 -1
  111. package/dist/tsup/chunk-SRNOPUC6.cjs.map +0 -1
  112. package/dist/tsup/chunk-TMLOKTRB.js.map +0 -1
  113. package/dist/tsup/chunk-UZXQEGVJ.js.map +0 -1
  114. package/dist/tsup/process-metrics-NW754INA.js +0 -118
  115. package/dist/tsup/process-metrics-NW754INA.js.map +0 -1
  116. package/dist/tsup/process-metrics-TYAGKCEJ.cjs +0 -118
  117. package/dist/tsup/process-metrics-TYAGKCEJ.cjs.map +0 -1
  118. package/src/registry/process-metrics.ts +0 -183
  119. package/src/registry/write-through-proxy.ts +0 -40
  120. /package/dist/tsup/{chunk-D3T3ZBSY.js.map → chunk-H37XQU3I.js.map} +0 -0
  121. /package/dist/tsup/{chunk-4JU3IPG2.js.map → chunk-H7P7WR2Y.js.map} +0 -0
  122. /package/dist/tsup/{chunk-VUGENVIK.js.map → chunk-KJTA3ATT.js.map} +0 -0
@@ -2,7 +2,6 @@ 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,
8
7
  type WorkflowInspectorConfig,
@@ -34,10 +33,6 @@ import { HEADER_CONN_PARAMS } from "@/common/actor-router-consts";
34
33
  import type { AnyDatabaseProvider } from "@/common/database/config";
35
34
  import { wrapJsNativeDatabase } from "@/common/database/native-database";
36
35
  import { decodeWorkflowHistoryTransport } from "@/common/inspector-transport";
37
- import {
38
- assertJsonCompatValue,
39
- type JsonCompatValue,
40
- } from "@/common/encoding";
41
36
  import { deconstructError, stringifyError } from "@/common/utils";
42
37
  import type {
43
38
  RivetCloseEvent,
@@ -59,7 +54,6 @@ import {
59
54
  } from "@/serde";
60
55
  import { getEnvUniversal, VERSION } from "@/utils";
61
56
  import { logger } from "./log";
62
- import { createWriteThroughProxy } from "./write-through-proxy";
63
57
  import { loadNapiRuntime } from "./napi-runtime";
64
58
  import {
65
59
  type NativeValidationConfig,
@@ -249,6 +243,14 @@ type NativePersistActorState = {
249
243
  state: unknown;
250
244
  isInOnStateChange: boolean;
251
245
  connStates: Map<string, NativePersistConnState>;
246
+ // Memoized deep write-through proxy and the state object it wraps. Rebuilt
247
+ // only when the underlying state object identity changes.
248
+ stateProxy?: unknown;
249
+ stateProxyTarget?: unknown;
250
+ // Set when a coalesced save and onStateChange flush is pending for the
251
+ // current event loop tick.
252
+ saveScheduled?: boolean;
253
+ pendingSaveHandle?: ReturnType<typeof setImmediate>;
252
254
  };
253
255
  type NativeDestroyGate = {
254
256
  destroyCompletion?: Promise<void>;
@@ -343,15 +345,6 @@ function databaseNotConfiguredError(): RivetError {
343
345
  );
344
346
  }
345
347
 
346
- function databaseClientNotReadyError(): RivetError {
347
- return new RivetError(
348
- "actor",
349
- "database_client_not_ready",
350
- "actor database client was not initialized before user code ran. this is an internal lifecycle error; the migration callback should have pre-warmed the client. file an issue if you can reproduce.",
351
- { public: true },
352
- );
353
- }
354
-
355
348
  function stateNotEnabledError(): RivetError {
356
349
  return new RivetError(
357
350
  "actor",
@@ -421,20 +414,7 @@ async function cleanupNativeSleepRuntimeState(
421
414
  runtime: CoreRuntime,
422
415
  ctx: ActorContextHandle,
423
416
  ): Promise<void> {
424
- const waitStarted = Date.now();
425
- 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
- });
437
- }
417
+ await runtime.actorWaitForTrackedShutdownWork(ctx);
438
418
  await closeNativeDatabaseClient(runtime, ctx);
439
419
  await closeNativeSqlDatabase(runtime, ctx);
440
420
  clearNativeRuntimeState(runtime, ctx);
@@ -611,7 +591,7 @@ function decodeValue<T>(value?: RuntimeBytes | null): T {
611
591
  }
612
592
 
613
593
  function encodeValue(value: unknown): RuntimeBytes {
614
- return encodeCborCompat(value as JsonCompatValue);
594
+ return encodeCborCompat(value);
615
595
  }
616
596
 
617
597
  function unwrapTsfnPayload<T>(error: unknown, payload: T): T {
@@ -1086,6 +1066,54 @@ function decodeArgs(value?: RuntimeBytes | null): unknown[] {
1086
1066
  : [decoded];
1087
1067
  }
1088
1068
 
1069
+ function createWriteThroughProxy<T>(
1070
+ value: T,
1071
+ commit: (next: T) => void,
1072
+ beforeChange?: () => void,
1073
+ ): T {
1074
+ if (!value || typeof value !== "object") {
1075
+ return value;
1076
+ }
1077
+
1078
+ const proxies = new WeakMap<object, object>();
1079
+ const wrap = (target: object): object => {
1080
+ const cached = proxies.get(target);
1081
+ if (cached) {
1082
+ return cached;
1083
+ }
1084
+
1085
+ const proxy = new Proxy(target, {
1086
+ get(innerTarget, property, receiver) {
1087
+ const result = Reflect.get(innerTarget, property, receiver);
1088
+ return result && typeof result === "object"
1089
+ ? wrap(result as object)
1090
+ : result;
1091
+ },
1092
+ set(innerTarget, property, nextValue, receiver) {
1093
+ beforeChange?.();
1094
+ const updated = Reflect.set(
1095
+ innerTarget,
1096
+ property,
1097
+ nextValue,
1098
+ receiver,
1099
+ );
1100
+ commit(value);
1101
+ return updated;
1102
+ },
1103
+ deleteProperty(innerTarget, property) {
1104
+ beforeChange?.();
1105
+ const updated = Reflect.deleteProperty(innerTarget, property);
1106
+ commit(value);
1107
+ return updated;
1108
+ },
1109
+ });
1110
+
1111
+ proxies.set(target, proxy);
1112
+ return proxy;
1113
+ };
1114
+
1115
+ return wrap(value as object) as T;
1116
+ }
1089
1117
 
1090
1118
  function buildRequest(init: {
1091
1119
  method: string;
@@ -1176,21 +1204,14 @@ class NativeConnAdapter {
1176
1204
  );
1177
1205
  }
1178
1206
 
1179
- [RAW_STATE_SYMBOL](): unknown {
1180
- return this.#readState();
1181
- }
1182
-
1183
1207
  get state(): unknown {
1184
1208
  const nextState = this.#readState();
1185
1209
  return createWriteThroughProxy(nextState, (nextValue) => {
1186
1210
  this.#writeState(nextValue, { writeNative: true });
1187
- }, (newValue) => {
1188
- assertJsonCompatValue(newValue);
1189
1211
  });
1190
1212
  }
1191
1213
 
1192
1214
  set state(value: unknown) {
1193
- assertJsonCompatValue(value);
1194
1215
  this.#writeState(value, { writeNative: true });
1195
1216
  }
1196
1217
 
@@ -2302,90 +2323,6 @@ class TrackedWebSocketHandleAdapter implements UniversalWebSocket {
2302
2323
  }
2303
2324
  }
2304
2325
 
2305
- class NativeConnectionMap implements ReadonlyMap<string, NativeConnAdapter> {
2306
- #runtime: CoreRuntime;
2307
- #ctx: ActorContextHandle;
2308
- #schemas: NativeValidationConfig;
2309
-
2310
- constructor(
2311
- runtime: CoreRuntime,
2312
- ctx: ActorContextHandle,
2313
- schemas: NativeValidationConfig,
2314
- ) {
2315
- this.#runtime = runtime;
2316
- this.#ctx = ctx;
2317
- this.#schemas = schemas;
2318
- }
2319
-
2320
- #connToAdapter(conn: ConnHandle): NativeConnAdapter {
2321
- return new NativeConnAdapter(
2322
- this.#runtime,
2323
- conn,
2324
- this.#schemas,
2325
- this.#ctx,
2326
- (connId) =>
2327
- callNativeSync(() =>
2328
- this.#runtime.actorQueueHibernationRemoval(
2329
- this.#ctx,
2330
- connId,
2331
- ),
2332
- ),
2333
- );
2334
- }
2335
-
2336
- get size(): number {
2337
- return callNativeSync(() => this.#runtime.actorConns(this.#ctx)).length;
2338
- }
2339
-
2340
- get(key: string): NativeConnAdapter | undefined {
2341
- const conns = callNativeSync(() => this.#runtime.actorConns(this.#ctx));
2342
- const conn = conns.find(
2343
- (c) => this.#runtime.connId(c) === key,
2344
- );
2345
- if (!conn) return undefined;
2346
- return this.#connToAdapter(conn);
2347
- }
2348
-
2349
- has(key: string): boolean {
2350
- const conns = callNativeSync(() => this.#runtime.actorConns(this.#ctx));
2351
- return conns.some((c) => this.#runtime.connId(c) === key);
2352
- }
2353
-
2354
- keys(): MapIterator<string> {
2355
- const conns = callNativeSync(() => this.#runtime.actorConns(this.#ctx));
2356
- return conns.map((c) => this.#runtime.connId(c))[Symbol.iterator]() satisfies MapIterator<string>;
2357
- }
2358
-
2359
- values(): MapIterator<NativeConnAdapter> {
2360
- const conns = callNativeSync(() => this.#runtime.actorConns(this.#ctx));
2361
- return conns.map((c) => this.#connToAdapter(c))[Symbol.iterator]() satisfies MapIterator<NativeConnAdapter>;
2362
- }
2363
-
2364
- entries(): MapIterator<[string, NativeConnAdapter]> {
2365
- 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]>;
2369
- }
2370
-
2371
- forEach(
2372
- callback: (value: NativeConnAdapter, key: string, map: ReadonlyMap<string, NativeConnAdapter>) => void,
2373
- thisArg?: unknown,
2374
- ): void {
2375
- const conns = callNativeSync(() => this.#runtime.actorConns(this.#ctx));
2376
- for (const conn of conns) {
2377
- const id = this.#runtime.connId(conn);
2378
- callback.call(thisArg, this.#connToAdapter(conn), id, this);
2379
- }
2380
- }
2381
-
2382
- [Symbol.iterator](): MapIterator<[string, NativeConnAdapter]> {
2383
- return this.entries();
2384
- }
2385
-
2386
- readonly [Symbol.toStringTag] = "NativeConnectionMap";
2387
- }
2388
-
2389
2326
  export class ActorContextHandleAdapter {
2390
2327
  #runtime: CoreRuntime;
2391
2328
  #ctx: ActorContextHandle;
@@ -2394,9 +2331,9 @@ export class ActorContextHandleAdapter {
2394
2331
  #abortSignalCleanup?: () => void;
2395
2332
  #client?: AnyClient;
2396
2333
  #clientFactory?: () => AnyClient;
2397
- #connMap?: NativeConnectionMap;
2398
2334
  #databaseProvider?: Exclude<AnyDatabaseProvider, undefined>;
2399
2335
  #db?: unknown;
2336
+ #dbProxy?: unknown;
2400
2337
  #dispatchCancelToken?: CancellationTokenHandle;
2401
2338
  #kv?: NativeKvAdapter;
2402
2339
  #queue?: NativeQueueAdapter;
@@ -2459,42 +2396,60 @@ export class ActorContextHandleAdapter {
2459
2396
  throw databaseNotConfiguredError();
2460
2397
  }
2461
2398
 
2462
- if (this.#db) {
2463
- return this.#db;
2464
- }
2399
+ if (!this.#dbProxy) {
2400
+ this.#dbProxy = new Proxy(
2401
+ {},
2402
+ {
2403
+ get: (_target, property) => {
2404
+ if (property === "then") {
2405
+ return undefined;
2406
+ }
2465
2407
 
2466
- const runtimeState = getNativeRuntimeState(this.#runtime, this.#ctx);
2467
- const cachedClient = runtimeState.databaseClient;
2468
- if (cachedClient) {
2469
- this.#db = cachedClient.client;
2470
- return this.#db;
2408
+ return async (...args: Array<unknown>) => {
2409
+ const client = await this.ensureDatabaseClient();
2410
+ const value = Reflect.get(
2411
+ client as object,
2412
+ property,
2413
+ );
2414
+ if (typeof value !== "function") {
2415
+ return value;
2416
+ }
2417
+ return await value.apply(client, args);
2418
+ };
2419
+ },
2420
+ },
2421
+ );
2471
2422
  }
2472
2423
 
2473
- throw databaseClientNotReadyError();
2474
- }
2475
-
2476
- [RAW_STATE_SYMBOL](): unknown {
2477
- if (!this.#stateEnabled) {
2478
- throw stateNotEnabledError();
2479
- }
2480
- return this.#readState();
2424
+ return this.#dbProxy;
2481
2425
  }
2482
2426
 
2483
2427
  get state(): unknown {
2484
2428
  if (!this.#stateEnabled) {
2485
2429
  throw stateNotEnabledError();
2486
2430
  }
2431
+ const actorState = getNativePersistState(this.#runtime, this.#ctx);
2487
2432
  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
- );
2433
+ // Reading `c.state` rebuilds the deep write-through proxy, which
2434
+ // allocates fresh on-change caches and rewraps the whole tree. Memoize
2435
+ // the proxy keyed on the underlying state object so repeated reads and
2436
+ // deep read cascades reuse a single proxy.
2437
+ if (
2438
+ actorState.stateProxy === undefined ||
2439
+ actorState.stateProxyTarget !== nextState
2440
+ ) {
2441
+ actorState.stateProxyTarget = nextState;
2442
+ actorState.stateProxy = createWriteThroughProxy(
2443
+ nextState,
2444
+ (nextValue) => {
2445
+ this.#writeState(nextValue, { scheduleSave: true });
2446
+ },
2447
+ () => {
2448
+ this.#assertCanMutateState();
2449
+ },
2450
+ );
2451
+ }
2452
+ return actorState.stateProxy;
2498
2453
  }
2499
2454
 
2500
2455
  set state(value: unknown) {
@@ -2502,7 +2457,6 @@ export class ActorContextHandleAdapter {
2502
2457
  throw stateNotEnabledError();
2503
2458
  }
2504
2459
  this.#assertCanMutateState();
2505
- assertJsonCompatValue(value);
2506
2460
  this.#writeState(value, { scheduleSave: true });
2507
2461
  }
2508
2462
 
@@ -2569,11 +2523,27 @@ export class ActorContextHandleAdapter {
2569
2523
  return callNativeSync(() => this.#runtime.actorRegion(this.#ctx));
2570
2524
  }
2571
2525
 
2572
- get conns(): ReadonlyMap<string, NativeConnAdapter> {
2573
- if (!this.#connMap) {
2574
- this.#connMap = new NativeConnectionMap(this.#runtime, this.#ctx, this.#schemas);
2575
- }
2576
- return this.#connMap;
2526
+ get conns(): Map<string, NativeConnAdapter> {
2527
+ return new Map(
2528
+ callNativeSync(() => this.#runtime.actorConns(this.#ctx)).map(
2529
+ (conn) => [
2530
+ this.#runtime.connId(conn),
2531
+ new NativeConnAdapter(
2532
+ this.#runtime,
2533
+ conn,
2534
+ this.#schemas,
2535
+ this.#ctx,
2536
+ (connId) =>
2537
+ callNativeSync(() =>
2538
+ this.#runtime.actorQueueHibernationRemoval(
2539
+ this.#ctx,
2540
+ connId,
2541
+ ),
2542
+ ),
2543
+ ),
2544
+ ],
2545
+ ),
2546
+ );
2577
2547
  }
2578
2548
 
2579
2549
  get log() {
@@ -2804,41 +2774,22 @@ export class ActorContextHandleAdapter {
2804
2774
  }
2805
2775
 
2806
2776
  keepAwake<T>(promise: Promise<T>): Promise<T> {
2807
- const startedAt = Date.now();
2808
- logger().debug({
2809
- msg: "keepAwake registered",
2810
- at: startedAt,
2811
- });
2812
2777
  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
- )
2778
+ .catch((error) => {
2779
+ logger().warn({
2780
+ msg: "keepAwake promise rejected",
2781
+ error: stringifyError(error),
2782
+ });
2783
+ })
2828
2784
  .then(() => null);
2829
2785
  try {
2830
2786
  callNativeSync(() =>
2831
2787
  this.#runtime.actorKeepAwake(this.#ctx, trackedPromise),
2832
2788
  );
2833
2789
  } 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;
2790
+ if (!isClosedTaskRegistrationError(error)) {
2791
+ throw error;
2840
2792
  }
2841
- throw error;
2842
2793
  }
2843
2794
  return promise;
2844
2795
  }
@@ -2934,6 +2885,9 @@ export class ActorContextHandleAdapter {
2934
2885
  }
2935
2886
 
2936
2887
  async dispose(): Promise<void> {
2888
+ // Flush any save coalesced for this tick before the context is torn
2889
+ // down so the request-save and onStateChange always run.
2890
+ this.#flushStateChange();
2937
2891
  this.#abortSignalCleanup?.();
2938
2892
  this.#sql = undefined;
2939
2893
  }
@@ -2969,13 +2923,12 @@ export class ActorContextHandleAdapter {
2969
2923
  scheduleSave: boolean;
2970
2924
  },
2971
2925
  ): void {
2972
- encodeValue(value);
2973
2926
  const actorState = getNativePersistState(this.#runtime, this.#ctx);
2974
2927
  actorState.state = value;
2975
2928
  if (!options.scheduleSave) {
2976
2929
  return;
2977
2930
  }
2978
- this.#handleStateChange();
2931
+ this.#scheduleSave();
2979
2932
  }
2980
2933
 
2981
2934
  #assertCanMutateState(): void {
@@ -2985,9 +2938,33 @@ export class ActorContextHandleAdapter {
2985
2938
  }
2986
2939
  }
2987
2940
 
2988
- #handleStateChange(): void {
2941
+ // Coalesce the request-save and onStateChange work to once per event loop
2942
+ // tick. A synchronous burst of mutations (for example
2943
+ // `Object.assign(c.state, ...)`) would otherwise cross the NAPI boundary and
2944
+ // run onStateChange once per field, re-serializing the whole state each time
2945
+ // and pinning the event loop on large state.
2946
+ #scheduleSave(): void {
2947
+ const actorState = getNativePersistState(this.#runtime, this.#ctx);
2948
+ if (actorState.saveScheduled) {
2949
+ return;
2950
+ }
2951
+ actorState.saveScheduled = true;
2952
+ actorState.pendingSaveHandle = setImmediate(() => {
2953
+ this.#flushStateChange();
2954
+ });
2955
+ }
2956
+
2957
+ #flushStateChange(): void {
2989
2958
  const actorState = getNativePersistState(this.#runtime, this.#ctx);
2990
- encodeValue(actorState.state);
2959
+ if (!actorState.saveScheduled) {
2960
+ return;
2961
+ }
2962
+ actorState.saveScheduled = false;
2963
+ if (actorState.pendingSaveHandle !== undefined) {
2964
+ clearImmediate(actorState.pendingSaveHandle);
2965
+ actorState.pendingSaveHandle = undefined;
2966
+ }
2967
+
2991
2968
  callNativeSync(() =>
2992
2969
  this.#runtime.actorRequestSave(this.#ctx, { immediate: false }),
2993
2970
  );
@@ -3571,10 +3548,6 @@ export function buildNativeFactory(
3571
3548
  getNativeWorkflowInspector(ctx) !== undefined,
3572
3549
  });
3573
3550
  } catch (error) {
3574
- logger().error({
3575
- msg: "error replaying workflow history",
3576
- error,
3577
- });
3578
3551
  return errorResponse(error);
3579
3552
  }
3580
3553
  }
@@ -3754,10 +3727,6 @@ export function buildNativeFactory(
3754
3727
  );
3755
3728
  return jsonResponse({ output });
3756
3729
  } catch (error) {
3757
- logger().error({
3758
- msg: "Error handling inspector action request",
3759
- error,
3760
- });
3761
3730
  return errorResponse(error);
3762
3731
  }
3763
3732
  }
@@ -3772,10 +3741,6 @@ export function buildNativeFactory(
3772
3741
  { status: 404 },
3773
3742
  );
3774
3743
  } catch (error) {
3775
- logger().error({
3776
- msg: "Error handling inspector request",
3777
- error,
3778
- });
3779
3744
  return errorResponse(error);
3780
3745
  } finally {
3781
3746
  await actorCtx.dispose();
@@ -4686,14 +4651,6 @@ export async function buildConfiguredRegistry(config: RegistryConfig): Promise<{
4686
4651
  serveConfig: RuntimeServeConfig;
4687
4652
  }> {
4688
4653
  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
4654
  return buildRegistryWithRuntime(
4698
4655
  normalizeRuntimeConfig(config, runtime),
4699
4656
  runtime,
@@ -257,9 +257,10 @@ export interface RuntimeServerlessResponseHead {
257
257
  headers: Record<string, string>;
258
258
  }
259
259
 
260
- export interface RuntimeRegistryDiagnostics {
261
- mode: string;
262
- envoyActiveActorCount?: number | null;
260
+ export interface RuntimeRegistryRouteResponse {
261
+ status: number;
262
+ headers: Record<string, string>;
263
+ body: RuntimeBytes;
263
264
  }
264
265
 
265
266
  export type RuntimeServerlessStreamEvent =
@@ -319,9 +320,15 @@ export interface CoreRuntime {
319
320
  cancelToken: CancellationTokenHandle,
320
321
  config: RuntimeServeConfig,
321
322
  ): Promise<RuntimeServerlessResponseHead>;
322
- registryDiagnostics?(
323
+ registryHealth?(
324
+ registry: RegistryHandle,
325
+ ): Promise<RuntimeRegistryRouteResponse>;
326
+ registryMetadata?(
327
+ registry: RegistryHandle,
328
+ ): Promise<RuntimeRegistryRouteResponse>;
329
+ registryMetrics?(
323
330
  registry: RegistryHandle,
324
- ): Promise<RuntimeRegistryDiagnostics>;
331
+ ): Promise<RuntimeRegistryRouteResponse>;
325
332
  createActorFactory(
326
333
  callbacks: object,
327
334
  config?: RuntimeActorConfig | undefined | null,
@@ -33,6 +33,7 @@ import type {
33
33
  RuntimeQueueTryNextBatchOptions,
34
34
  RuntimeQueueWaitOptions,
35
35
  RuntimeRequestSaveOpts,
36
+ RuntimeRegistryRouteResponse,
36
37
  RuntimeServeConfig,
37
38
  RuntimeServerlessRequest,
38
39
  RuntimeServerlessResponseHead,
@@ -270,8 +271,18 @@ export class WasmCoreRuntime implements CoreRuntime {
270
271
  await callWasm(() => asWasmRegistry(registry).shutdown());
271
272
  }
272
273
 
273
- async registryDiagnostics(): Promise<{ mode: string; envoyActiveActorCount: null }> {
274
- return { mode: "wasm", envoyActiveActorCount: null };
274
+ async registryHealth(): Promise<RuntimeRegistryRouteResponse> {
275
+ return {
276
+ status: 200,
277
+ headers: { "content-type": "application/json" },
278
+ body: new TextEncoder().encode(
279
+ JSON.stringify({
280
+ status: "ok",
281
+ runtime: "rivetkit",
282
+ version: "wasm",
283
+ }),
284
+ ),
285
+ };
275
286
  }
276
287
 
277
288
  async handleServerlessRequest(
package/src/serde.ts CHANGED
@@ -3,7 +3,7 @@ import invariant from "invariant";
3
3
  import type { VersionedDataHandler } from "vbare";
4
4
  import type { z } from "zod/v4";
5
5
  import { assertUnreachable } from "@/common/utils";
6
- import type { JsonCompatValue, Encoding } from "@/common/encoding";
6
+ import type { Encoding } from "@/common/encoding";
7
7
  import {
8
8
  encodeJsonCompatValue,
9
9
  jsonParseCompat,
@@ -46,7 +46,7 @@ export function contentTypeForEncoding(encoding: Encoding): string {
46
46
  }
47
47
  }
48
48
 
49
- export function encodeCborCompat(value: JsonCompatValue): Uint8Array {
49
+ export function encodeCborCompat(value: unknown): Uint8Array {
50
50
  return cbor.encode(encodeJsonCompatValue(value));
51
51
  }
52
52
 
@@ -1,5 +1,5 @@
1
1
  // @ts-nocheck
2
- import { RAW_STATE_SYMBOL, type RunContext } from "@/actor/config";
2
+ import type { RunContext } from "@/actor/config";
3
3
  import type {
4
4
  QueueFilterName,
5
5
  QueueNextBatchOptions,
@@ -247,14 +247,14 @@ export class ActorWorkflowContext<
247
247
  }
248
248
  return await this.#wrapActive(() =>
249
249
  this.#inner.step(nameOrConfig, () =>
250
- this.#withActorAccessAndStateRollback(run),
250
+ this.#withActorAccess(run),
251
251
  ),
252
252
  );
253
253
  }
254
254
  const stepConfig = nameOrConfig as StepConfig<T>;
255
255
  const config: StepConfig<T> = {
256
256
  ...stepConfig,
257
- run: () => this.#withActorAccessAndStateRollback(stepConfig.run),
257
+ run: () => this.#withActorAccess(stepConfig.run),
258
258
  };
259
259
  return await this.#wrapActive(() => this.#inner.step(config));
260
260
  }
@@ -271,14 +271,14 @@ export class ActorWorkflowContext<
271
271
  }
272
272
  return await this.#wrapActive(() =>
273
273
  this.#inner.tryStep(nameOrConfig, () =>
274
- this.#withActorAccessAndStateRollback(run),
274
+ this.#withActorAccess(run),
275
275
  ),
276
276
  );
277
277
  }
278
278
  const stepConfig = nameOrConfig as TryStepConfig<T>;
279
279
  const config: TryStepConfig<T> = {
280
280
  ...stepConfig,
281
- run: () => this.#withActorAccessAndStateRollback(stepConfig.run),
281
+ run: () => this.#withActorAccess(stepConfig.run),
282
282
  };
283
283
  return await this.#wrapActive(() => this.#inner.tryStep(config));
284
284
  }
@@ -612,33 +612,6 @@ export class ActorWorkflowContext<
612
612
  }
613
613
  }
614
614
 
615
- async #withActorAccessAndStateRollback<T>(
616
- run: () => Promise<T>,
617
- ): Promise<T> {
618
- let stateSnapshot: { state: TState } | null = null;
619
- try {
620
- stateSnapshot = { state: this.#runCtx[RAW_STATE_SYMBOL]() };
621
- } catch (error) {
622
- this.#runCtx.log.debug({
623
- msg: "failed to get state, likely due to being stateless workflow",
624
- error,
625
- });
626
- }
627
- if (stateSnapshot) {
628
- stateSnapshot.state = structuredClone(stateSnapshot.state);
629
- }
630
- const varsSnapshot = structuredClone(this.#runCtx.vars);
631
- try {
632
- return await this.#withActorAccess(run);
633
- } catch (error) {
634
- if (stateSnapshot) {
635
- this.#runCtx.state = stateSnapshot.state;
636
- }
637
- this.#runCtx.vars = varsSnapshot;
638
- throw error;
639
- }
640
- }
641
-
642
615
  #ensureActorAccess(feature: string): void {
643
616
  if (!this.#allowActorAccess) {
644
617
  this.#guardViolation = true;