Skip to content

chore: move ActorDefinition and ActorInstance to interfaces#4331

Open
NathanFlurry wants to merge 1 commit intofeat-dynamic-actorsfrom
chore-move-actordefinition-and
Open

chore: move ActorDefinition and ActorInstance to interfaces#4331
NathanFlurry wants to merge 1 commit intofeat-dynamic-actorsfrom
chore-move-actordefinition-and

Conversation

@NathanFlurry
Copy link
Member

Description

Please include a summary of the changes and the related issue. Please also include relevant motivation and context.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

@NathanFlurry
Copy link
Member Author

This change is part of the following stack:

Change managed by git-spice.

@railway-app
Copy link

railway-app bot commented Feb 28, 2026

🚅 Deployed to the rivet-pr-4331 environment in rivet-frontend

Service Status Web Updated (UTC)
frontend-inspector 😴 Sleeping (View Logs) Web Feb 28, 2026 at 8:16 am
frontend-cloud ❌ Build Failed (View Logs) Web Feb 28, 2026 at 8:06 am
website ❌ Build Failed (View Logs) Web Feb 28, 2026 at 8:06 am
ladle ❌ Build Failed (View Logs) Web Feb 28, 2026 at 8:04 am
mcp-hub ✅ Success (View Logs) Web Feb 28, 2026 at 8:04 am

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 28, 2026

More templates

@rivetkit/cloudflare-workers

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/cloudflare-workers@4331

@rivetkit/framework-base

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/framework-base@4331

@rivetkit/next-js

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/next-js@4331

@rivetkit/react

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/react@4331

rivetkit

pnpm add https://pkg.pr.new/rivet-dev/rivet/rivetkit@4331

@rivetkit/sql-loader

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/sql-loader@4331

@rivetkit/sqlite-vfs

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/sqlite-vfs@4331

@rivetkit/traces

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/traces@4331

@rivetkit/workflow-engine

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/workflow-engine@4331

@rivetkit/virtual-websocket

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/virtual-websocket@4331

@rivetkit/engine-runner

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/engine-runner@4331

@rivetkit/engine-runner-protocol

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/engine-runner-protocol@4331

commit: 3982e4f

@claude
Copy link

claude bot commented Feb 28, 2026

Code Review: chore: move ActorDefinition and ActorInstance to interfaces

This is a well-structured architectural refactor. Introducing a minimal ActorInstance interface that both StaticActorInstance and DynamicActorInstance implement is sound and enables the engine to handle dynamic actors alongside static ones without leaking internal implementation details. The approach is largely correct and coherent. A few concerns below.


Architecture and Design

DynamicActorDefinition.instantiate() throws at runtime. The method is required by the ActorDefinition interface contract but deliberately throws:

instantiate(): ActorInstance<...> {
    throw new Error(
        "dynamic actor definitions are instantiated by the runtime driver",
    );
}

This violates the Liskov Substitution Principle. The guards in actor-driver.ts and global-state.ts correctly check isDynamicActorDefinition before calling instantiate(), but nothing at the type level prevents a future code path from calling instantiate() on an AnyActorDefinition and hitting this unexpected throw. Consider either narrowing the return type signature or splitting DynamicActorDefinition away from the ActorDefinition interface entirely.

Three copies of loadStaticActor. The same guard helper appears independently in router-endpoints.ts, router-websocket-endpoints.ts, and router.ts. All three call actorDriver.loadActor, assert the result is a StaticActorInstance, and throw if not. Additionally, error types are inconsistent: router-endpoints.ts throws errors.InternalError (the proper domain error) while the other two throw a plain new Error(...). This should be unified into a shared utility with consistent error handling.

ExtractActorState, ExtractActorConnParams, ExtractActorConnState now hardcode StaticActorInstance. These utility types only return non-never for StaticActorInstance. If the intent is that extraction only makes sense for static actors, the type constraint should be A extends AnyStaticActorInstance rather than the misleading A extends AnyActorInstance.


Potential Bugs

Race condition in #runnerDynamicWebSocket message flush. The flush runs:

proxyToActorWs.addEventListener("open", () => {
    actorWebSocketReady = true;
    void flushPendingMessages();
});

flushPendingMessages is not awaited, and the outer websocket.addEventListener("message", ...) handler also calls runtime.forwardIncomingWebSocketMessage without coordination. If messages arrive while flushPendingMessages is draining the queue (after actorWebSocketReady = true but before clearing all pending entries), a concurrent incoming message will also try to forward immediately. This can cause messages to be delivered out of order.

Silent discard of messages to a stopping actor. In the engine WebSocket handler, messages to a stopping dynamic actor are silently dropped with return. For hibernatable WebSocket paths, the ACK is never sent for the dropped message. If the gateway waits on an ACK before delivering the next message, this will stall the connection without a clean close. It would be safer to close the WebSocket explicitly when discarding due to a stopping actor.

dynamicGetHibernatingWebSocketsEnvelope silently drops connections when symbol lookup fails. The isolate code accesses conn[CONN_STATE_MANAGER_SYMBOL]. If the isolate's require("rivetkit") resolves a different symbol than the one used internally (e.g., due to module caching or version mismatch), the lookup returns undefined with no warning, silently omitting connections from the hibernating snapshot.


Code Quality

(session.websocket as any).triggerMessage(...) casts. The as any casts suggest the VirtualWebSocket type accessible from host-runtime.ts may not yet have the updated triggerMessage signature (with the new rivetMessageIndex parameter). If the shared package type is now properly typed, these casts should be removed.

DynamicActorConfigInput may be missing noSleep. The old dynamicActor implementation set noSleep: true on the host-side placeholder actor. The new DynamicActorConfigInput only exposes GlobalActorOptionsInput, and noSleep lives in InstanceActorOptionsBaseSchema, so it cannot be passed through. Worth confirming this was intentionally dropped rather than accidentally omitted.

Inconsistent indentation in several new files. There are a few spots with misaligned tab depths in mod.ts (around db.createClient), host-runtime.ts (in #sendWebSocketMessage and openWebSocket array arguments), and driver-engine-dynamic.test.ts (the describe callback body). These look like carry-over formatting artifacts.


Security

No new attack surface is introduced. The hibernation metadata serialization (base64 of ArrayBuffer across the isolate boundary) is handled correctly, including the byteOffset fix when slicing from a Node.js Buffer.


Test Coverage

The new driver-engine-dynamic.test.ts covers the main happy paths well (URL-loaded source, action invocation, KV, WebSocket ping/pong, sleep/wake, alarms). A few gaps worth noting:

  • No test for the hibernatable WebSocket path (isHibernatable: true / isRestoringHibernatable: true flow).
  • No test for the error path when the dynamic runtime fails to start (verifying cleanup from #dynamicRuntimes).
  • No test verifying that messages buffered while actorWebSocketReady = false are flushed in order.
  • The readWebSocketJson helper has a hardcoded 5-second timeout with no per-test override, which may be fragile on slow CI for the sleep/alarm tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant