Skip to content

Commit 8a13950

Browse files
committed
feat: [DT-033] - [F32] Move actor-keyed module-level maps off process globals in native.ts
1 parent e7b0a53 commit 8a13950

8 files changed

Lines changed: 170 additions & 87 deletions

File tree

rivetkit-typescript/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ The log name matches the key in `ActorMetrics.startup`. Internal phases use `per
8282
- On this branch, the native TS actor/conn persistence glue still lives in `packages/rivetkit/src/registry/native.ts`; PRD references to split `state-manager.ts` or `connection-manager.ts` files may be stale, so land equivalent behavior in `registry/native.ts` unless those modules reappear first.
8383
- Public TS actor `onWake` maps to the native callback bag's `onWake`; `onBeforeActorStart` is an internal driver/NAPI startup hook, not public actor config.
8484
- Static actor `state` values in `packages/rivetkit/src/registry/native.ts` must be `structuredClone(...)`d per actor instance; reusing the literal leaks mutations across different keyed actors.
85+
- JS-only native actor caches in `packages/rivetkit/src/registry/native.ts` should live on `ActorContext.runtimeState()`, not on actorId-keyed module globals. Same-key recreates must get a fresh bag.
8586
- Every `NativeConnAdapter` construction path in `packages/rivetkit/src/registry/native.ts` must keep the `CONN_STATE_MANAGER_SYMBOL` hookup; hibernatable conn mutations rely on core `ConnHandle::set_state` dirty tracking to request persistence.
8687
- Durable native actor saves in `packages/rivetkit/src/registry/native.ts` must use `ctx.requestSaveAndWait({ immediate: true })`; state bytes are collected only through the `serializeState` callback.
8788
- Opaque user payloads that must preserve JS `undefined` across Rust JSON/CBOR bridges should go through `encodeCborCompat` / `decodeCborCompat`; do not use those helpers on structural JSON envelopes where omitted fields must stay omitted.

rivetkit-typescript/packages/rivetkit-napi/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ export declare class ActorContext {
212212
broadcast(name: string, args: Buffer): void
213213
waitUntil(promise: Promise<any>): Promise<void>
214214
registerTask(promise: Promise<any>): void
215+
runtimeState(): object
215216
}
216217
export declare class NapiActorFactory {
217218
constructor(callbacks: object, config?: JsActorConfig | undefined | null)

rivetkit-typescript/packages/rivetkit-napi/src/actor_context.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use napi::bindgen_prelude::{Buffer, Promise};
1313
use napi::threadsafe_function::{
1414
ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode,
1515
};
16-
use napi::{Env, JsFunction, JsObject};
16+
use napi::{Env, JsFunction, JsObject, Ref};
1717
use napi_derive::napi;
1818
use parking_lot::Mutex;
1919
use rivetkit_core::types::ActorKeySegment;
@@ -57,6 +57,7 @@ struct ActorContextShared {
5757
abort_token: Mutex<Option<CoreCancellationToken>>,
5858
run_restart: Mutex<Option<RunRestartHook>>,
5959
task_sender: Mutex<Option<UnboundedSender<RegisteredTask>>>,
60+
runtime_state: Mutex<Option<Ref<()>>>,
6061
end_reason: Mutex<Option<EndReason>>,
6162
websocket_callback_regions: Mutex<BTreeMap<u32, WebSocketCallbackRegion>>,
6263
next_websocket_callback_region_id: AtomicU32,
@@ -630,6 +631,11 @@ impl ActorContext {
630631
}))
631632
.map_err(napi_anyhow_error)
632633
}
634+
635+
#[napi]
636+
pub fn runtime_state(&self, env: Env) -> napi::Result<JsObject> {
637+
self.shared.runtime_state(env)
638+
}
633639
}
634640

635641
impl Drop for ActorContext {
@@ -692,6 +698,18 @@ impl ActorContextShared {
692698
self.run_restart.lock().is_some()
693699
}
694700

701+
fn runtime_state(&self, env: Env) -> napi::Result<JsObject> {
702+
let mut runtime_state = self.runtime_state.lock();
703+
if let Some(reference) = runtime_state.as_ref() {
704+
return env.get_reference_value(reference);
705+
}
706+
707+
let reference = env.create_reference(env.create_object()?)?;
708+
let state = env.get_reference_value(&reference)?;
709+
*runtime_state = Some(reference);
710+
Ok(state)
711+
}
712+
695713
#[allow(dead_code)]
696714
fn set_end_reason(&self, reason: EndReason) {
697715
*self.end_reason.lock() = Some(reason);
@@ -748,6 +766,7 @@ impl ActorContextShared {
748766
*self.abort_token.lock() = None;
749767
*self.run_restart.lock() = None;
750768
*self.task_sender.lock() = None;
769+
*self.runtime_state.lock() = None;
751770
*self.end_reason.lock() = None;
752771
*self.websocket_callback_regions.lock() = BTreeMap::new();
753772
self.next_websocket_callback_region_id

rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/destroy.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const destroyObserver = actor({
1919

2020
export const destroyActor = actor({
2121
state: { value: 0, key: "" },
22+
createVars: () => ({ ephemeral: "fresh" }),
2223
queues: {
2324
values: queue<number>(),
2425
},
@@ -60,6 +61,13 @@ export const destroyActor = actor({
6061
getValue: (c) => {
6162
return c.state.value;
6263
},
64+
setEphemeral: (c, value: string) => {
65+
c.vars.ephemeral = value;
66+
return c.vars.ephemeral;
67+
},
68+
getEphemeral: (c) => {
69+
return c.vars.ephemeral;
70+
},
6371
receiveValue: async (c) => {
6472
const message = await c.queue.next({
6573
names: ["values"],

0 commit comments

Comments
 (0)