rivetkit 2.3.0-rc.6 → 2.3.0-rc.8

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 +37 -8
  2. package/dist/browser/client.js +64 -34
  3. package/dist/browser/client.js.map +1 -1
  4. package/dist/browser/inspector/client.js +4 -3
  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 +7 -5
  9. package/dist/tsup/agent-os/index.cjs.map +1 -1
  10. package/dist/tsup/agent-os/index.d.cts +26 -5
  11. package/dist/tsup/agent-os/index.d.ts +26 -5
  12. package/dist/tsup/agent-os/index.js +7 -5
  13. package/dist/tsup/agent-os/index.js.map +1 -1
  14. package/dist/tsup/{chunk-M5C7YNI5.cjs → chunk-4BPKKZJO.cjs} +8 -8
  15. package/dist/tsup/{chunk-M5C7YNI5.cjs.map → chunk-4BPKKZJO.cjs.map} +1 -1
  16. package/dist/tsup/{chunk-EMO6E3PJ.js → chunk-4JU3IPG2.js} +3 -3
  17. package/dist/tsup/{chunk-QAZLM4WT.cjs → chunk-63WNTDRC.cjs} +3 -3
  18. package/dist/tsup/{chunk-QAZLM4WT.cjs.map → chunk-63WNTDRC.cjs.map} +1 -1
  19. package/dist/tsup/{chunk-JALSAX7Z.cjs → chunk-6TQSSJ4F.cjs} +3 -3
  20. package/dist/tsup/{chunk-JALSAX7Z.cjs.map → chunk-6TQSSJ4F.cjs.map} +1 -1
  21. package/dist/tsup/{chunk-ENK7C66G.cjs → chunk-7HLFSAJP.cjs} +169 -193
  22. package/dist/tsup/chunk-7HLFSAJP.cjs.map +1 -0
  23. package/dist/tsup/{chunk-2G64KSZQ.cjs → chunk-AWTPTUQ7.cjs} +10 -10
  24. package/dist/tsup/chunk-AWTPTUQ7.cjs.map +1 -0
  25. package/dist/tsup/{chunk-K5BA2LEO.cjs → chunk-BATTOVHF.cjs} +14 -12
  26. package/dist/tsup/chunk-BATTOVHF.cjs.map +1 -0
  27. package/dist/tsup/{chunk-ZI5QJMKO.js → chunk-D3T3ZBSY.js} +2 -2
  28. package/dist/tsup/{chunk-T6YVRM4K.js → chunk-D5G75T7J.js} +3 -1
  29. package/dist/tsup/chunk-D5G75T7J.js.map +1 -0
  30. package/dist/tsup/{chunk-FLODVLYW.js → chunk-EMFKMVJR.js} +10 -34
  31. package/dist/tsup/chunk-EMFKMVJR.js.map +1 -0
  32. package/dist/tsup/{chunk-6S25NVAP.js → chunk-GBG63SUG.js} +7 -5
  33. package/dist/tsup/chunk-GBG63SUG.js.map +1 -0
  34. package/dist/tsup/{chunk-LIXXFXVR.cjs → chunk-HGW6PBWR.cjs} +137 -9
  35. package/dist/tsup/chunk-HGW6PBWR.cjs.map +1 -0
  36. package/dist/tsup/{chunk-RTC2AZGB.js → chunk-KY3CERZR.js} +132 -4
  37. package/dist/tsup/chunk-KY3CERZR.js.map +1 -0
  38. package/dist/tsup/{chunk-HTR4YLNT.cjs → chunk-OT7FF6GB.cjs} +4 -4
  39. package/dist/tsup/{chunk-HTR4YLNT.cjs.map → chunk-OT7FF6GB.cjs.map} +1 -1
  40. package/dist/tsup/{chunk-WQ4HNA4W.cjs → chunk-SRNOPUC6.cjs} +4 -2
  41. package/dist/tsup/chunk-SRNOPUC6.cjs.map +1 -0
  42. package/dist/tsup/{chunk-KIWH5H3K.js → chunk-TMLOKTRB.js} +3 -3
  43. package/dist/tsup/chunk-TMLOKTRB.js.map +1 -0
  44. package/dist/tsup/{chunk-CAF6JDJE.js → chunk-UZXQEGVJ.js} +4 -4
  45. package/dist/tsup/chunk-UZXQEGVJ.js.map +1 -0
  46. package/dist/tsup/{chunk-DEO7MMWQ.js → chunk-VUGENVIK.js} +2 -2
  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-0Ta55UV0.d.ts → config-Ak1lv4gF.d.ts} +27 -6
  56. package/dist/tsup/{config-Ca8dN4cS.d.cts → config-DU_xj4qZ.d.cts} +27 -6
  57. package/dist/tsup/{context-B_IWbWne.d.ts → context-DAAp4Lpg.d.ts} +1 -1
  58. package/dist/tsup/{context-CUrQ9MHc.d.cts → context-Dt_L55q8.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 +470 -316
  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 +391 -237
  66. package/dist/tsup/mod.js.map +1 -1
  67. package/dist/tsup/process-metrics-NW754INA.js +118 -0
  68. package/dist/tsup/process-metrics-NW754INA.js.map +1 -0
  69. package/dist/tsup/process-metrics-TYAGKCEJ.cjs +118 -0
  70. package/dist/tsup/process-metrics-TYAGKCEJ.cjs.map +1 -0
  71. package/dist/tsup/test/mod.cjs +10 -10
  72. package/dist/tsup/test/mod.d.cts +1 -1
  73. package/dist/tsup/test/mod.d.ts +1 -1
  74. package/dist/tsup/test/mod.js +6 -6
  75. package/dist/tsup/utils.cjs +3 -3
  76. package/dist/tsup/utils.js +2 -2
  77. package/dist/tsup/workflow/mod.cjs +41 -16
  78. package/dist/tsup/workflow/mod.cjs.map +1 -1
  79. package/dist/tsup/workflow/mod.d.cts +3 -3
  80. package/dist/tsup/workflow/mod.d.ts +3 -3
  81. package/dist/tsup/workflow/mod.js +35 -10
  82. package/dist/tsup/workflow/mod.js.map +1 -1
  83. package/package.json +11 -10
  84. package/src/actor/config.ts +3 -0
  85. package/src/actor/errors.ts +2 -2
  86. package/src/agent-os/actor/session.ts +2 -2
  87. package/src/client/actor-conn.ts +6 -34
  88. package/src/client/actor-handle.ts +2 -1
  89. package/src/client/queue.ts +2 -1
  90. package/src/client/utils.ts +0 -1
  91. package/src/common/encoding.ts +243 -5
  92. package/src/common/inline-websocket-adapter.ts +12 -12
  93. package/src/common/log.ts +1 -0
  94. package/src/common/router.ts +2 -2
  95. package/src/common/utils.ts +0 -148
  96. package/src/drivers/engine/actor-driver.ts +11 -11
  97. package/src/engine-client/actor-websocket-client.ts +2 -1
  98. package/src/engine-client/mod.ts +3 -2
  99. package/src/registry/index.ts +46 -109
  100. package/src/registry/napi-runtime.ts +11 -34
  101. package/src/registry/native.ts +193 -104
  102. package/src/registry/process-metrics.ts +183 -0
  103. package/src/registry/runtime.ts +5 -12
  104. package/src/registry/wasm-runtime.ts +2 -13
  105. package/src/registry/write-through-proxy.ts +40 -0
  106. package/src/serde.ts +2 -2
  107. package/src/workflow/context.ts +32 -5
  108. package/src/workflow/inspector.ts +2 -1
  109. package/dist/tsup/chunk-2G64KSZQ.cjs.map +0 -1
  110. package/dist/tsup/chunk-6S25NVAP.js.map +0 -1
  111. package/dist/tsup/chunk-CAF6JDJE.js.map +0 -1
  112. package/dist/tsup/chunk-ENK7C66G.cjs.map +0 -1
  113. package/dist/tsup/chunk-FLODVLYW.js.map +0 -1
  114. package/dist/tsup/chunk-K5BA2LEO.cjs.map +0 -1
  115. package/dist/tsup/chunk-KIWH5H3K.js.map +0 -1
  116. package/dist/tsup/chunk-LIXXFXVR.cjs.map +0 -1
  117. package/dist/tsup/chunk-RTC2AZGB.js.map +0 -1
  118. package/dist/tsup/chunk-T6YVRM4K.js.map +0 -1
  119. package/dist/tsup/chunk-WQ4HNA4W.cjs.map +0 -1
  120. /package/dist/tsup/{chunk-EMO6E3PJ.js.map → chunk-4JU3IPG2.js.map} +0 -0
  121. /package/dist/tsup/{chunk-ZI5QJMKO.js.map → chunk-D3T3ZBSY.js.map} +0 -0
  122. /package/dist/tsup/{chunk-DEO7MMWQ.js.map → chunk-VUGENVIK.js.map} +0 -0
@@ -2,6 +2,7 @@ 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,
5
6
  getRunFunction,
6
7
  getRunInspectorConfig,
7
8
  type WorkflowInspectorConfig,
@@ -33,6 +34,10 @@ import { HEADER_CONN_PARAMS } from "@/common/actor-router-consts";
33
34
  import type { AnyDatabaseProvider } from "@/common/database/config";
34
35
  import { wrapJsNativeDatabase } from "@/common/database/native-database";
35
36
  import { decodeWorkflowHistoryTransport } from "@/common/inspector-transport";
37
+ import {
38
+ assertJsonCompatValue,
39
+ type JsonCompatValue,
40
+ } from "@/common/encoding";
36
41
  import { deconstructError, stringifyError } from "@/common/utils";
37
42
  import type {
38
43
  RivetCloseEvent,
@@ -54,6 +59,7 @@ import {
54
59
  } from "@/serde";
55
60
  import { getEnvUniversal, VERSION } from "@/utils";
56
61
  import { logger } from "./log";
62
+ import { createWriteThroughProxy } from "./write-through-proxy";
57
63
  import { loadNapiRuntime } from "./napi-runtime";
58
64
  import {
59
65
  type NativeValidationConfig,
@@ -337,6 +343,15 @@ function databaseNotConfiguredError(): RivetError {
337
343
  );
338
344
  }
339
345
 
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
+
340
355
  function stateNotEnabledError(): RivetError {
341
356
  return new RivetError(
342
357
  "actor",
@@ -406,7 +421,20 @@ async function cleanupNativeSleepRuntimeState(
406
421
  runtime: CoreRuntime,
407
422
  ctx: ActorContextHandle,
408
423
  ): Promise<void> {
409
- await runtime.actorWaitForTrackedShutdownWork(ctx);
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
+ }
410
438
  await closeNativeDatabaseClient(runtime, ctx);
411
439
  await closeNativeSqlDatabase(runtime, ctx);
412
440
  clearNativeRuntimeState(runtime, ctx);
@@ -583,7 +611,7 @@ function decodeValue<T>(value?: RuntimeBytes | null): T {
583
611
  }
584
612
 
585
613
  function encodeValue(value: unknown): RuntimeBytes {
586
- return encodeCborCompat(value);
614
+ return encodeCborCompat(value as JsonCompatValue);
587
615
  }
588
616
 
589
617
  function unwrapTsfnPayload<T>(error: unknown, payload: T): T {
@@ -1058,54 +1086,6 @@ function decodeArgs(value?: RuntimeBytes | null): unknown[] {
1058
1086
  : [decoded];
1059
1087
  }
1060
1088
 
1061
- function createWriteThroughProxy<T>(
1062
- value: T,
1063
- commit: (next: T) => void,
1064
- beforeChange?: () => void,
1065
- ): T {
1066
- if (!value || typeof value !== "object") {
1067
- return value;
1068
- }
1069
-
1070
- const proxies = new WeakMap<object, object>();
1071
- const wrap = (target: object): object => {
1072
- const cached = proxies.get(target);
1073
- if (cached) {
1074
- return cached;
1075
- }
1076
-
1077
- const proxy = new Proxy(target, {
1078
- get(innerTarget, property, receiver) {
1079
- const result = Reflect.get(innerTarget, property, receiver);
1080
- return result && typeof result === "object"
1081
- ? wrap(result as object)
1082
- : result;
1083
- },
1084
- set(innerTarget, property, nextValue, receiver) {
1085
- beforeChange?.();
1086
- const updated = Reflect.set(
1087
- innerTarget,
1088
- property,
1089
- nextValue,
1090
- receiver,
1091
- );
1092
- commit(value);
1093
- return updated;
1094
- },
1095
- deleteProperty(innerTarget, property) {
1096
- beforeChange?.();
1097
- const updated = Reflect.deleteProperty(innerTarget, property);
1098
- commit(value);
1099
- return updated;
1100
- },
1101
- });
1102
-
1103
- proxies.set(target, proxy);
1104
- return proxy;
1105
- };
1106
-
1107
- return wrap(value as object) as T;
1108
- }
1109
1089
 
1110
1090
  function buildRequest(init: {
1111
1091
  method: string;
@@ -1196,14 +1176,21 @@ class NativeConnAdapter {
1196
1176
  );
1197
1177
  }
1198
1178
 
1179
+ [RAW_STATE_SYMBOL](): unknown {
1180
+ return this.#readState();
1181
+ }
1182
+
1199
1183
  get state(): unknown {
1200
1184
  const nextState = this.#readState();
1201
1185
  return createWriteThroughProxy(nextState, (nextValue) => {
1202
1186
  this.#writeState(nextValue, { writeNative: true });
1187
+ }, (newValue) => {
1188
+ assertJsonCompatValue(newValue);
1203
1189
  });
1204
1190
  }
1205
1191
 
1206
1192
  set state(value: unknown) {
1193
+ assertJsonCompatValue(value);
1207
1194
  this.#writeState(value, { writeNative: true });
1208
1195
  }
1209
1196
 
@@ -2315,6 +2302,90 @@ class TrackedWebSocketHandleAdapter implements UniversalWebSocket {
2315
2302
  }
2316
2303
  }
2317
2304
 
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
+
2318
2389
  export class ActorContextHandleAdapter {
2319
2390
  #runtime: CoreRuntime;
2320
2391
  #ctx: ActorContextHandle;
@@ -2323,9 +2394,9 @@ export class ActorContextHandleAdapter {
2323
2394
  #abortSignalCleanup?: () => void;
2324
2395
  #client?: AnyClient;
2325
2396
  #clientFactory?: () => AnyClient;
2397
+ #connMap?: NativeConnectionMap;
2326
2398
  #databaseProvider?: Exclude<AnyDatabaseProvider, undefined>;
2327
2399
  #db?: unknown;
2328
- #dbProxy?: unknown;
2329
2400
  #dispatchCancelToken?: CancellationTokenHandle;
2330
2401
  #kv?: NativeKvAdapter;
2331
2402
  #queue?: NativeQueueAdapter;
@@ -2388,32 +2459,25 @@ export class ActorContextHandleAdapter {
2388
2459
  throw databaseNotConfiguredError();
2389
2460
  }
2390
2461
 
2391
- if (!this.#dbProxy) {
2392
- this.#dbProxy = new Proxy(
2393
- {},
2394
- {
2395
- get: (_target, property) => {
2396
- if (property === "then") {
2397
- return undefined;
2398
- }
2462
+ if (this.#db) {
2463
+ return this.#db;
2464
+ }
2399
2465
 
2400
- return async (...args: Array<unknown>) => {
2401
- const client = await this.ensureDatabaseClient();
2402
- const value = Reflect.get(
2403
- client as object,
2404
- property,
2405
- );
2406
- if (typeof value !== "function") {
2407
- return value;
2408
- }
2409
- return await value.apply(client, args);
2410
- };
2411
- },
2412
- },
2413
- );
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;
2414
2471
  }
2415
2472
 
2416
- return this.#dbProxy;
2473
+ throw databaseClientNotReadyError();
2474
+ }
2475
+
2476
+ [RAW_STATE_SYMBOL](): unknown {
2477
+ if (!this.#stateEnabled) {
2478
+ throw stateNotEnabledError();
2479
+ }
2480
+ return this.#readState();
2417
2481
  }
2418
2482
 
2419
2483
  get state(): unknown {
@@ -2426,8 +2490,9 @@ export class ActorContextHandleAdapter {
2426
2490
  (nextValue) => {
2427
2491
  this.#writeState(nextValue, { scheduleSave: true });
2428
2492
  },
2429
- () => {
2493
+ (newValue) => {
2430
2494
  this.#assertCanMutateState();
2495
+ assertJsonCompatValue(newValue);
2431
2496
  },
2432
2497
  );
2433
2498
  }
@@ -2437,6 +2502,7 @@ export class ActorContextHandleAdapter {
2437
2502
  throw stateNotEnabledError();
2438
2503
  }
2439
2504
  this.#assertCanMutateState();
2505
+ assertJsonCompatValue(value);
2440
2506
  this.#writeState(value, { scheduleSave: true });
2441
2507
  }
2442
2508
 
@@ -2503,27 +2569,11 @@ export class ActorContextHandleAdapter {
2503
2569
  return callNativeSync(() => this.#runtime.actorRegion(this.#ctx));
2504
2570
  }
2505
2571
 
2506
- get conns(): Map<string, NativeConnAdapter> {
2507
- return new Map(
2508
- callNativeSync(() => this.#runtime.actorConns(this.#ctx)).map(
2509
- (conn) => [
2510
- this.#runtime.connId(conn),
2511
- new NativeConnAdapter(
2512
- this.#runtime,
2513
- conn,
2514
- this.#schemas,
2515
- this.#ctx,
2516
- (connId) =>
2517
- callNativeSync(() =>
2518
- this.#runtime.actorQueueHibernationRemoval(
2519
- this.#ctx,
2520
- connId,
2521
- ),
2522
- ),
2523
- ),
2524
- ],
2525
- ),
2526
- );
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;
2527
2577
  }
2528
2578
 
2529
2579
  get log() {
@@ -2754,22 +2804,41 @@ export class ActorContextHandleAdapter {
2754
2804
  }
2755
2805
 
2756
2806
  keepAwake<T>(promise: Promise<T>): Promise<T> {
2807
+ const startedAt = Date.now();
2808
+ logger().debug({
2809
+ msg: "keepAwake registered",
2810
+ at: startedAt,
2811
+ });
2757
2812
  const trackedPromise = Promise.resolve(promise)
2758
- .catch((error) => {
2759
- logger().warn({
2760
- msg: "keepAwake promise rejected",
2761
- error: stringifyError(error),
2762
- });
2763
- })
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
+ )
2764
2828
  .then(() => null);
2765
2829
  try {
2766
2830
  callNativeSync(() =>
2767
2831
  this.#runtime.actorKeepAwake(this.#ctx, trackedPromise),
2768
2832
  );
2769
2833
  } catch (error) {
2770
- if (!isClosedTaskRegistrationError(error)) {
2771
- throw 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;
2772
2840
  }
2841
+ throw error;
2773
2842
  }
2774
2843
  return promise;
2775
2844
  }
@@ -3502,6 +3571,10 @@ export function buildNativeFactory(
3502
3571
  getNativeWorkflowInspector(ctx) !== undefined,
3503
3572
  });
3504
3573
  } catch (error) {
3574
+ logger().error({
3575
+ msg: "error replaying workflow history",
3576
+ error,
3577
+ });
3505
3578
  return errorResponse(error);
3506
3579
  }
3507
3580
  }
@@ -3681,6 +3754,10 @@ export function buildNativeFactory(
3681
3754
  );
3682
3755
  return jsonResponse({ output });
3683
3756
  } catch (error) {
3757
+ logger().error({
3758
+ msg: "Error handling inspector action request",
3759
+ error,
3760
+ });
3684
3761
  return errorResponse(error);
3685
3762
  }
3686
3763
  }
@@ -3695,6 +3772,10 @@ export function buildNativeFactory(
3695
3772
  { status: 404 },
3696
3773
  );
3697
3774
  } catch (error) {
3775
+ logger().error({
3776
+ msg: "Error handling inspector request",
3777
+ error,
3778
+ });
3698
3779
  return errorResponse(error);
3699
3780
  } finally {
3700
3781
  await actorCtx.dispose();
@@ -4605,6 +4686,14 @@ export async function buildConfiguredRegistry(config: RegistryConfig): Promise<{
4605
4686
  serveConfig: RuntimeServeConfig;
4606
4687
  }> {
4607
4688
  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
+ }
4608
4697
  return buildRegistryWithRuntime(
4609
4698
  normalizeRuntimeConfig(config, runtime),
4610
4699
  runtime,
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Node.js runtime health metrics.
3
+ *
4
+ * Collects JS-internal data (event loop lag, GC, heap, libuv handles,
5
+ * event loop utilization, CPU) using Node built-ins (`node:perf_hooks`,
6
+ * `process`, `node:v8`, `PerformanceObserver`) and pushes them across NAPI
7
+ * into Rust-side prometheus collectors registered with
8
+ * `rivet_metrics::REGISTRY` so they appear on the existing `/metrics`
9
+ * endpoint.
10
+ *
11
+ * All data collection happens here in TypeScript. The NAPI bridge is pure
12
+ * type marshalling and the Rust side only registers + stores the metrics.
13
+ */
14
+ import { monitorEventLoopDelay, performance, PerformanceObserver } from "node:perf_hooks";
15
+ import { getHeapStatistics } from "node:v8";
16
+ import * as napi from "@rivetkit/rivetkit-napi";
17
+
18
+ // Some napi process-metrics symbols may be missing on older native binaries
19
+ // (the auto-generated index.js destructures them as `undefined` if the
20
+ // underlying `.node` was built before they were added). Guard each call so
21
+ // the metrics collection runs as a no-op instead of throwing
22
+ // `TypeError: napi.jsXxx is not a function` on every interval tick.
23
+ function callIfFn<T extends unknown[]>(
24
+ fn: ((...args: T) => void) | undefined,
25
+ ...args: T
26
+ ): void {
27
+ if (typeof fn === "function") {
28
+ fn(...args);
29
+ }
30
+ }
31
+
32
+ const SCRAPE_INTERVAL_MS = 5_000;
33
+ const HEARTBEAT_INTERVAL_MS = 100;
34
+ const EVENTLOOP_DELAY_RESOLUTION_MS = 20;
35
+ const NS_PER_SECOND = 1e9;
36
+ const US_PER_SECOND = 1e6;
37
+
38
+ // V8 GC kind bitfield from Node's perf_hooks documentation. A `gc` performance
39
+ // entry's `kind` field is one of these values.
40
+ const GC_KIND_NAMES: Record<number, string> = {
41
+ 1: "minor",
42
+ 2: "major",
43
+ 4: "incremental",
44
+ 8: "weakcb",
45
+ };
46
+
47
+ interface ProcessMetricsState {
48
+ scrapeInterval: NodeJS.Timeout;
49
+ heartbeatInterval: NodeJS.Timeout;
50
+ gcObserver: PerformanceObserver;
51
+ eventLoopHistogram: ReturnType<typeof monitorEventLoopDelay>;
52
+ lastCpuUsage: NodeJS.CpuUsage;
53
+ lastEventLoopUtilization: ReturnType<typeof performance.eventLoopUtilization>;
54
+ }
55
+
56
+ let state: ProcessMetricsState | undefined;
57
+
58
+ export function startProcessMetrics(): void {
59
+ if (state) {
60
+ return;
61
+ }
62
+
63
+ const eventLoopHistogram = monitorEventLoopDelay({
64
+ resolution: EVENTLOOP_DELAY_RESOLUTION_MS,
65
+ });
66
+ eventLoopHistogram.enable();
67
+
68
+ const gcObserver = new PerformanceObserver((list) => {
69
+ for (const entry of list.getEntries()) {
70
+ const kind =
71
+ (entry as PerformanceEntry & { detail?: { kind?: number }; kind?: number }).detail
72
+ ?.kind ??
73
+ (entry as PerformanceEntry & { kind?: number }).kind;
74
+ if (typeof kind !== "number") continue;
75
+ const kindName = GC_KIND_NAMES[kind];
76
+ if (!kindName) continue;
77
+ // `entry.duration` is in milliseconds; convert to seconds.
78
+ callIfFn(napi.jsObserveGcDuration, kindName, entry.duration / 1000);
79
+ }
80
+ });
81
+ gcObserver.observe({ entryTypes: ["gc"], buffered: false });
82
+
83
+ const lastCpuUsage = process.cpuUsage();
84
+ const lastEventLoopUtilization = performance.eventLoopUtilization();
85
+
86
+ const heartbeatInterval = setInterval(() => {
87
+ callIfFn(napi.jsSetEventloopHeartbeatTsMs, Date.now());
88
+ }, HEARTBEAT_INTERVAL_MS);
89
+ heartbeatInterval.unref();
90
+
91
+ const scrapeInterval = setInterval(() => {
92
+ try {
93
+ collectAndPush();
94
+ } catch {
95
+ // Collection errors must never bring down the process; metrics
96
+ // are best-effort.
97
+ }
98
+ }, SCRAPE_INTERVAL_MS);
99
+ scrapeInterval.unref();
100
+
101
+ state = {
102
+ scrapeInterval,
103
+ heartbeatInterval,
104
+ gcObserver,
105
+ eventLoopHistogram,
106
+ lastCpuUsage,
107
+ lastEventLoopUtilization,
108
+ };
109
+
110
+ // Emit one snapshot immediately so freshly-scraped instances have data.
111
+ callIfFn(napi.jsSetEventloopHeartbeatTsMs, Date.now());
112
+ try {
113
+ collectAndPush();
114
+ } catch {
115
+ // As above; best-effort.
116
+ }
117
+ }
118
+
119
+ export function stopProcessMetrics(): void {
120
+ if (!state) {
121
+ return;
122
+ }
123
+ clearInterval(state.scrapeInterval);
124
+ clearInterval(state.heartbeatInterval);
125
+ state.gcObserver.disconnect();
126
+ state.eventLoopHistogram.disable();
127
+ state = undefined;
128
+ }
129
+
130
+ function collectAndPush(): void {
131
+ if (!state) return;
132
+
133
+ // Event loop delay quantiles. `monitorEventLoopDelay()` reports values in
134
+ // nanoseconds; convert to seconds. Reset after reading so the next window
135
+ // reflects only the new interval.
136
+ const hist = state.eventLoopHistogram;
137
+ callIfFn(napi.jsSetEventloopLagQuantile, "p50", hist.percentile(50) / NS_PER_SECOND);
138
+ callIfFn(napi.jsSetEventloopLagQuantile, "p90", hist.percentile(90) / NS_PER_SECOND);
139
+ callIfFn(napi.jsSetEventloopLagQuantile, "p99", hist.percentile(99) / NS_PER_SECOND);
140
+ callIfFn(napi.jsSetEventloopLagQuantile, "max", hist.max / NS_PER_SECOND);
141
+ hist.reset();
142
+
143
+ // Event loop utilization delta over the scrape window.
144
+ const nextElu = performance.eventLoopUtilization();
145
+ const eluDelta = performance.eventLoopUtilization(nextElu, state.lastEventLoopUtilization);
146
+ state.lastEventLoopUtilization = nextElu;
147
+ callIfFn(napi.jsSetEventloopUtilization, eluDelta.utilization);
148
+
149
+ // CPU usage delta. `process.cpuUsage()` returns microseconds.
150
+ const nextCpu = process.cpuUsage();
151
+ const userDeltaUs = nextCpu.user - state.lastCpuUsage.user;
152
+ const systemDeltaUs = nextCpu.system - state.lastCpuUsage.system;
153
+ state.lastCpuUsage = nextCpu;
154
+ if (userDeltaUs > 0) {
155
+ callIfFn(napi.jsAddProcessCpuSeconds, "user", userDeltaUs / US_PER_SECOND);
156
+ }
157
+ if (systemDeltaUs > 0) {
158
+ callIfFn(napi.jsAddProcessCpuSeconds, "system", systemDeltaUs / US_PER_SECOND);
159
+ }
160
+
161
+ // Memory + heap.
162
+ const mem = process.memoryUsage();
163
+ callIfFn(napi.jsSetProcessResidentMemoryBytes, mem.rss);
164
+ callIfFn(napi.jsSetHeapBytes, "used", mem.heapUsed);
165
+ callIfFn(napi.jsSetHeapBytes, "total", mem.heapTotal);
166
+ const heapLimit = getHeapStatistics().heap_size_limit;
167
+ callIfFn(napi.jsSetHeapBytes, "limit", heapLimit);
168
+
169
+ // libuv active handles + requests. These are unstable Node internals
170
+ // guarded behind underscore-prefixed names; if a future Node release
171
+ // removes them the try/catch above keeps the rest of the collection
172
+ // alive.
173
+ const proc = process as unknown as {
174
+ _getActiveHandles?: () => unknown[];
175
+ _getActiveRequests?: () => unknown[];
176
+ };
177
+ if (typeof proc._getActiveHandles === "function") {
178
+ callIfFn(napi.jsSetActiveHandles, proc._getActiveHandles().length);
179
+ }
180
+ if (typeof proc._getActiveRequests === "function") {
181
+ callIfFn(napi.jsSetActiveRequests, proc._getActiveRequests().length);
182
+ }
183
+ }
@@ -257,10 +257,9 @@ export interface RuntimeServerlessResponseHead {
257
257
  headers: Record<string, string>;
258
258
  }
259
259
 
260
- export interface RuntimeRegistryRouteResponse {
261
- status: number;
262
- headers: Record<string, string>;
263
- body: RuntimeBytes;
260
+ export interface RuntimeRegistryDiagnostics {
261
+ mode: string;
262
+ envoyActiveActorCount?: number | null;
264
263
  }
265
264
 
266
265
  export type RuntimeServerlessStreamEvent =
@@ -320,15 +319,9 @@ export interface CoreRuntime {
320
319
  cancelToken: CancellationTokenHandle,
321
320
  config: RuntimeServeConfig,
322
321
  ): Promise<RuntimeServerlessResponseHead>;
323
- registryHealth?(
324
- registry: RegistryHandle,
325
- ): Promise<RuntimeRegistryRouteResponse>;
326
- registryMetadata?(
327
- registry: RegistryHandle,
328
- ): Promise<RuntimeRegistryRouteResponse>;
329
- registryMetrics?(
322
+ registryDiagnostics?(
330
323
  registry: RegistryHandle,
331
- ): Promise<RuntimeRegistryRouteResponse>;
324
+ ): Promise<RuntimeRegistryDiagnostics>;
332
325
  createActorFactory(
333
326
  callbacks: object,
334
327
  config?: RuntimeActorConfig | undefined | null,
@@ -33,7 +33,6 @@ import type {
33
33
  RuntimeQueueTryNextBatchOptions,
34
34
  RuntimeQueueWaitOptions,
35
35
  RuntimeRequestSaveOpts,
36
- RuntimeRegistryRouteResponse,
37
36
  RuntimeServeConfig,
38
37
  RuntimeServerlessRequest,
39
38
  RuntimeServerlessResponseHead,
@@ -271,18 +270,8 @@ export class WasmCoreRuntime implements CoreRuntime {
271
270
  await callWasm(() => asWasmRegistry(registry).shutdown());
272
271
  }
273
272
 
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
- };
273
+ async registryDiagnostics(): Promise<{ mode: string; envoyActiveActorCount: null }> {
274
+ return { mode: "wasm", envoyActiveActorCount: null };
286
275
  }
287
276
 
288
277
  async handleServerlessRequest(