diff --git a/rivetkit-typescript/packages/rivetkit-napi/index.d.ts b/rivetkit-typescript/packages/rivetkit-napi/index.d.ts index 5dff314187..24c4b59901 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/index.d.ts +++ b/rivetkit-typescript/packages/rivetkit-napi/index.d.ts @@ -50,6 +50,7 @@ export interface JsActionDefinition { export interface JsActorConfig { name?: string icon?: string + hasDatabase?: boolean canHibernateWebsocket?: boolean stateSaveIntervalMs?: number createStateTimeoutMs?: number diff --git a/rivetkit-typescript/packages/rivetkit-napi/src/actor_context.rs b/rivetkit-typescript/packages/rivetkit-napi/src/actor_context.rs index eda56282a4..fdc2919d3f 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/src/actor_context.rs +++ b/rivetkit-typescript/packages/rivetkit-napi/src/actor_context.rs @@ -766,7 +766,14 @@ impl ActorContextShared { *self.abort_token.lock() = None; *self.run_restart.lock() = None; *self.task_sender.lock() = None; - *self.runtime_state.lock() = None; + // napi Ref::unref requires an Env; this function runs on tokio workers + // with no Env available. Dropping without unref panics a debug_assert in + // napi-rs and silently leaks the napi reference slot in release. Forget + // instead so debug matches release behavior. Leak is bounded to one + // JsObject per actor wake cycle until the process exits. + if let Some(old) = self.runtime_state.lock().take() { + std::mem::forget(old); + } *self.end_reason.lock() = None; *self.websocket_callback_regions.lock() = BTreeMap::new(); self.next_websocket_callback_region_id @@ -774,6 +781,15 @@ impl ActorContextShared { } } +impl Drop for ActorContextShared { + fn drop(&mut self) { + // Same Env-less drop problem as reset_runtime_state. See comment there. + if let Some(old) = self.runtime_state.lock().take() { + std::mem::forget(old); + } + } +} + fn actor_context_shared(actor_id: &str) -> Arc { ACTOR_CONTEXT_SHARED.retain_sync(|_, shared| shared.strong_count() > 0); diff --git a/rivetkit-typescript/packages/rivetkit-napi/src/actor_factory.rs b/rivetkit-typescript/packages/rivetkit-napi/src/actor_factory.rs index 0a77ff5249..5e8ee71fab 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/src/actor_factory.rs +++ b/rivetkit-typescript/packages/rivetkit-napi/src/actor_factory.rs @@ -65,6 +65,7 @@ pub struct JsActionDefinition { pub struct JsActorConfig { pub name: Option, pub icon: Option, + pub has_database: Option, pub can_hibernate_websocket: Option, pub state_save_interval_ms: Option, pub create_state_timeout_ms: Option, @@ -883,7 +884,6 @@ fn build_action_payload(env: &Env, payload: ActionPayload) -> napi::Result object.set("conn", ConnHandle::new(conn))?, None => object.set("conn", env.get_null()?)?, } - object.set("name", payload.name)?; object.set("args", Buffer::from(payload.args))?; match payload.cancel_token { Some(cancel_token) => object.set("cancelToken", CancellationToken::new(cancel_token))?, @@ -929,7 +929,7 @@ fn build_serialize_state_payload( ) -> napi::Result> { let mut object = env.create_object()?; object.set("ctx", ActorContext::new(payload.ctx))?; - object.set("reason", payload.reason)?; + object.set("reason", env.create_string_from_std(payload.reason)?)?; Ok(vec![object.into_unknown()]) } @@ -1035,6 +1035,7 @@ impl From for ActorConfigInput { Self { name: value.name, icon: value.icon, + has_database: value.has_database, can_hibernate_websocket: value.can_hibernate_websocket, state_save_interval_ms: value.state_save_interval_ms, create_vars_timeout_ms: value.create_vars_timeout_ms, diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/native.ts b/rivetkit-typescript/packages/rivetkit/src/registry/native.ts index 06152a19e2..fcc20b73f3 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/native.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/native.ts @@ -118,7 +118,7 @@ type NativeWebSocketWithEvents = NativeWebSocket & { }; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); -type SerializeStateReason = "save" | "inspector" | "sleep" | "destroy"; +type SerializeStateReason = "save" | "inspector"; type NativeOnStateChangeHandler = ( ctx: NativeActorContextAdapter, state: unknown, @@ -3046,6 +3046,7 @@ function buildActorConfig( return { name: options.name as string | undefined, icon: options.icon as string | undefined, + hasDatabase: config.db !== undefined, canHibernateWebsocket: typeof canHibernate === "boolean" ? canHibernate : undefined, stateSaveIntervalMs: options.stateSaveInterval as number | undefined,