docs-internal/engine/rivetkit-core-internals.md
Internal wiring reference for rivetkit-rust/packages/rivetkit-core/. These are facts about the current implementation. For the principles that govern how new code is added, see the root CLAUDE.md layer + fail-by-default sections. For state-mutation semantics, see docs-internal/engine/rivetkit-core-state-management.md.
Actor subsystems are composed into ActorContextInner, not separate managers.
ActorContextInner. Behavior sits in actor/queue.rs impl ActorContext blocks. Do not reintroduce Arc<QueueInner> or a public Queue re-export.ActorContextInner. Behavior sits in actor/connection.rs impl ActorContext blocks. Do not reintroduce Arc<ConnectionManagerInner> or a public ConnectionManager re-export.ActorContextInner. Behavior sits in actor/state.rs impl ActorContext blocks. Do not reintroduce Arc<ActorStateInner> or a public ActorState re-export.ActorContextInner. Behavior sits in actor/schedule.rs impl ActorContext blocks. Do not reintroduce Arc<ScheduleInner> or a public Schedule re-export.ActorContext::broadcast. Do not reintroduce a separate EventBroadcaster subsystem.Values are serialized with a vbare-compatible 2-byte little-endian embedded version prefix before the BARE body, matching the TypeScript serializeWithEmbeddedVersion(...) format.
| Key | Contents |
|---|---|
[1] | PersistedActor snapshot (matches TypeScript KEYS.PERSIST_DATA) |
[2] + conn_id | Hibernatable websocket connection payload, TypeScript v4 BARE field order |
[5, 1, 1] | Queue metadata |
[5, 1, 2] + u64be(id) | Queue messages (FIFO prefix scan) |
[6] | LAST_PUSHED_ALARM_KEY — Option<i64> last pushed driver alarm |
Preload handling is tri-state for each prefix:
[1]: no bundle falls back to KV, requested-but-absent means fresh actor defaults, present decodes the persisted actor.[2] + conn_id: consumed from preload when PreloadedKv.requested_prefixes includes [2]; fall back to kv.list_prefix([2]) only when that prefix is absent.[5, 1, 1] + [5, 1, 2]: consumed from preload when requested; fall back to KV only when absent.request_save uses RequestSaveOpts { immediate, max_wait_ms }. NAPI callers use ctx.requestSave({ immediate, maxWaitMs }). Do not use a boolean requestSave or requestSaveWithin.ActorContext::request_save(...) + ActorEvent::SerializeState { reason: Save, .. }.ActorContext::save_state(Vec<StateDelta>) because Sleep/Destroy replies are unit-only. Direct durability must still clear pending save-request flags after a successful write.request_save / save_state(Vec<StateDelta>). Do not reintroduce set_state / mutate_state.ActorState through a single helper, then immediately kick save_state(immediate = true) and resync the envoy alarm to the earliest event.on_state_change callbacks fail with actor/state_mutation_reentrant. Use vars or another non-state side channel for callback-run counters.ActorContext::inspector_attach() returning an InspectorAttachGuard plus subscribe_inspector(). Hold the guard for the websocket lifetime so ActorTask can debounce SerializeState { reason: Inspector, .. } off request-save hooks.ActorContext. Queue-specific callbacks carry the current size; connection updates read the context connection count so unconfigured inspectors stay cheap no-ops.Schedule alarm sync is guarded by dirty_since_push. Fresh schedules start dirty, mutations set dirty, and unchanged shutdown syncs must not re-push identical envoy alarms.Option<i64> at actor KV key [6]. Startup loads it with PERSIST_DATA_KEY and skips identical future alarm pushes.on_request errors become HTTP 500 responses; on_websocket errors become logged 1011 closes. ConnHandle and WebSocket wrappers surface explicit configuration errors through internal try_* helpers.ActorEvent::Action dispatch uses conn: None for alarm-originated work and Some(ConnHandle) for real client connections. Do not synthesize placeholder connections for scheduled actions.ActorContext sleep state. Queue waits, scheduled internal work, disconnect callbacks, and websocket callbacks report activity through ActorContext hooks so the idle timer stays accurate.onDisconnect work runs inside ActorContext::with_disconnect_callback(...) so pending_disconnect_count gates sleep until the async callback finishes.ActorContexts with ActorContext::build(...) so state, queue, and connection managers inherit the actor config before lifecycle startup runs. ActorContext::build(...) must seed owned queue, connection, and sleep config storage from its ActorConfig; do not initialize those fields with ActorConfig::default().actor_instances: SccHashMap<String, ActorInstanceState>. Use entry_async for Active/Stopping transitions.RegistryDispatcher::handle_fetch owns framework HTTP routes /metrics, /inspector/*, /action/*, and /queue/*. TypeScript NAPI callbacks keep action/queue schema validation and queue canPublish.onRequest HTTP fetches bypass maxIncomingMessageSize / maxOutgoingMessageSize. Those message-size guards apply only to /action/* and /queue/* framework routes, not unmatched user onRequest paths.metadata for JSON/CBOR responses so missing metadata stays undefined. Only explicit metadata null serializes as null.PersistedActor into ActorContext before factory creation.has_initialized immediately.ready before the driver hook.run in a detached panic-catching task.started.started after the driver hook completes.Two-phase:
SleepGrace fires onSleep immediately and keeps dispatch/save timers live.SleepFinalize gates dispatch, suspends alarms, and runs teardown.Sleep grace fires the actor abort signal on entry and waits for the run handler to exit before finalize.
Finalize:
run task.ActorContext sleep state for the idle window and shutdown-task drains.ActorContext::wait_for_on_state_change_idle(...) before sending final save events so async onStateChange work cannot race durability.sleep_grace_period budget for the destroy phase.wait_for_on_state_change_idle(...) before final saves.Persistence order:
sleep_grace_period_overridden distinguishes an explicit sleep_grace_period from runtime override defaults.EnvoyCallbacks::on_actor_stop_with_completion. The default implementation preserves the old immediate on_actor_stop behavior by auto-completing the stop handle after the callback returns.EnvoyHandle lookups for live actor state read the shared SharedContext.actors mirror keyed by actor id/generation. Blocking back through the envoy task can panic on current-thread Tokio runtimes.futures::future::BoxFuture<'static, ...> plus the shared actor::callbacks::Request and Response wrappers so config and HTTP parsing helpers stay in core for future runtimes.ActorTask test hooks (install_shutdown_cleanup_hook, lifecycle-event/reply hooks) must be actor-scoped and serialized in tests. Parallel cargo test runs will otherwise cross-wire unrelated actors.rivetkit) interopCtx<A> stays a stateless wrapper over rivetkit-core::ActorContext. Actor state lives in the user receive loop. There is no typed vars field. CBOR encode/decode stays at wrapper method boundaries like broadcast and ConnCtx.Ctx<A>::client() builds and caches rivetkit-client from core Envoy client accessors. Keep actor-to-actor client construction in the wrapper, not core.Start<A> wrappers rehydrate each ActorStart.hibernated state blob back onto the ConnHandle before exposing ConnCtx, or conn.state() stops matching the wake snapshot.rivetkit-rust/packages/rivetkit/src/persist.rs owns typed actor-state StateDelta builders. SerializeState/Sleep/Destroy in src/event.rs stay thin reply helpers that reuse those builders instead of open-coding persistence bytes per wrapper.