Skip to content

Commit e0c30f6

Browse files
committed
fix(rivetkit-napi): plumb ctx through serializeState TSF and stop panicking on runtime_state Ref drop
1 parent 5366cce commit e0c30f6

4 files changed

Lines changed: 23 additions & 4 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface JsActionDefinition {
5050
export interface JsActorConfig {
5151
name?: string
5252
icon?: string
53+
hasDatabase?: boolean
5354
canHibernateWebsocket?: boolean
5455
stateSaveIntervalMs?: number
5556
createStateTimeoutMs?: number

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -766,14 +766,30 @@ impl ActorContextShared {
766766
*self.abort_token.lock() = None;
767767
*self.run_restart.lock() = None;
768768
*self.task_sender.lock() = None;
769-
*self.runtime_state.lock() = None;
769+
// napi Ref::unref requires an Env; this function runs on tokio workers
770+
// with no Env available. Dropping without unref panics a debug_assert in
771+
// napi-rs and silently leaks the napi reference slot in release. Forget
772+
// instead so debug matches release behavior. Leak is bounded to one
773+
// JsObject per actor wake cycle until the process exits.
774+
if let Some(old) = self.runtime_state.lock().take() {
775+
std::mem::forget(old);
776+
}
770777
*self.end_reason.lock() = None;
771778
*self.websocket_callback_regions.lock() = BTreeMap::new();
772779
self.next_websocket_callback_region_id
773780
.store(0, Ordering::SeqCst);
774781
}
775782
}
776783

784+
impl Drop for ActorContextShared {
785+
fn drop(&mut self) {
786+
// Same Env-less drop problem as reset_runtime_state. See comment there.
787+
if let Some(old) = self.runtime_state.lock().take() {
788+
std::mem::forget(old);
789+
}
790+
}
791+
}
792+
777793
fn actor_context_shared(actor_id: &str) -> Arc<ActorContextShared> {
778794
ACTOR_CONTEXT_SHARED.retain_sync(|_, shared| shared.strong_count() > 0);
779795

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ pub struct JsActionDefinition {
6565
pub struct JsActorConfig {
6666
pub name: Option<String>,
6767
pub icon: Option<String>,
68+
pub has_database: Option<bool>,
6869
pub can_hibernate_websocket: Option<bool>,
6970
pub state_save_interval_ms: Option<u32>,
7071
pub create_state_timeout_ms: Option<u32>,
@@ -883,7 +884,6 @@ fn build_action_payload(env: &Env, payload: ActionPayload) -> napi::Result<Vec<n
883884
Some(conn) => object.set("conn", ConnHandle::new(conn))?,
884885
None => object.set("conn", env.get_null()?)?,
885886
}
886-
object.set("name", payload.name)?;
887887
object.set("args", Buffer::from(payload.args))?;
888888
match payload.cancel_token {
889889
Some(cancel_token) => object.set("cancelToken", CancellationToken::new(cancel_token))?,
@@ -929,7 +929,7 @@ fn build_serialize_state_payload(
929929
) -> napi::Result<Vec<napi::JsUnknown>> {
930930
let mut object = env.create_object()?;
931931
object.set("ctx", ActorContext::new(payload.ctx))?;
932-
object.set("reason", payload.reason)?;
932+
object.set("reason", env.create_string_from_std(payload.reason)?)?;
933933
Ok(vec![object.into_unknown()])
934934
}
935935

@@ -1035,6 +1035,7 @@ impl From<JsActorConfig> for ActorConfigInput {
10351035
Self {
10361036
name: value.name,
10371037
icon: value.icon,
1038+
has_database: value.has_database,
10381039
can_hibernate_websocket: value.can_hibernate_websocket,
10391040
state_save_interval_ms: value.state_save_interval_ms,
10401041
create_vars_timeout_ms: value.create_vars_timeout_ms,

rivetkit-typescript/packages/rivetkit/src/registry/native.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ type NativeWebSocketWithEvents = NativeWebSocket & {
118118
};
119119
const textEncoder = new TextEncoder();
120120
const textDecoder = new TextDecoder();
121-
type SerializeStateReason = "save" | "inspector" | "sleep" | "destroy";
121+
type SerializeStateReason = "save" | "inspector";
122122
type NativeOnStateChangeHandler = (
123123
ctx: NativeActorContextAdapter,
124124
state: unknown,
@@ -3046,6 +3046,7 @@ function buildActorConfig(
30463046
return {
30473047
name: options.name as string | undefined,
30483048
icon: options.icon as string | undefined,
3049+
hasDatabase: config.db !== undefined,
30493050
canHibernateWebsocket:
30503051
typeof canHibernate === "boolean" ? canHibernate : undefined,
30513052
stateSaveIntervalMs: options.stateSaveInterval as number | undefined,

0 commit comments

Comments
 (0)