feat: @rivetkit/effect SDK#4703
Draft
IGassmann wants to merge 117 commits intorivet-dev:mainfrom
Draft
Conversation
3f26ccf to
9c7bc2a
Compare
08bda7b to
671b6de
Compare
Add examples/effect with the proposed API surface for @rivetkit/effect. This is a design-only example for feedback, not a working implementation.
Move action schemas inline into Actor.make instead of separate Action.make declarations. Reduces boilerplate and eliminates the name-sync problem across three declaration sites.
Remove the Exit-matching create/destroy pattern from the wake scope finalizer. Create and destroy are separate lifecycle events that don't map to scope exit signals.
Replace hand-rolled `Options`, `EngineOptions`, and `ClientOptions` declarations with `Pick`s of the canonical rivetkit input types (`GlobalActorOptionsInput`, `RegistryConfigInput`, `ClientConfigInput`) to prevent drift, and use `WakeContextOf`/`SleepContextOf` for the wake/sleep callback parameters instead of structural sub-shapes.
Derive `ActorAddress` from `Rivetkit.ActorContext` instead of hand-rolling the field types, and reuse `ActorKeyParam` for `ClientShape.callAction.params.key` in place of the duplicated `string | ReadonlyArray<string>` shape.
…odes from `Runner`
`Runner.test` is the Effect-Cluster-`TestRunner.layer` analogue: one `Layer.effectContext` that boots the rivetkit registry in test mode, auto-spawns the engine when no endpoint is configured, and provides `Runner | Client` so consumers wire the test runtime in a single `Layer.provideMerge`. Adds a `Runner.test.ts` exercising the wire path against a Counter actor (success, in-wake state, typed-error round-trip), plus a `globalSetup` that wipes orphaned engine state so repeated `pnpm test` invocations are deterministic.
Override RIVET_LOG_LEVEL=ERROR in the package vitest config so the spawned rivet-engine and runtime stop flooding the terminal with DEBUG output during test runs.
…actor Extends the e2e Counter fixture with EchoDate, Tags (custom CSV transform), Greet, and WakeGreeting actions plus a Greeter user service yielded both in the wake scope and inside an action handler. Adds a minimal Pinger actor used solely for the multi-actor registration test. Fills in the four TODO test cases and adds `registers and serves multiple actors`.
Runner.test now reads the resolved endpoint from rivetkitRegistry's parsed config and passes it to createClient when the user didn't supply one. Suppresses the rivetkit "No endpoint provided" warning that fired on every test run when the engine was auto-spawned.
Adds an end-to-end test asserting that an action's payload, success, and error all resolve their schema decoding/encoding services on both sides of the wire. Drives a `Multiplier`-dependent `ScaledNumber` schema through a new `Scale` action on the existing `Counter` actor.
Adds a `FailingActor` whose `toLayer` build dies, registers it with the test runner, and asserts that calling an action on it surfaces a `RivetError` to the client.
Captures today's behavior when a client calls an actor whose `*Live` layer was never provided: the engine logs the precise `not_registered` reason but flattens it on the wire to `guard/service_unavailable`, the same generic code a transient engine outage surfaces as. Locking the mapping in a test forces a review if/when the engine starts emitting a distinct code so the SDK can decode it into a typed `ActorNotFound`.
Action calls now ride a wire-side `ActionMeta` envelope (`args[1]`)
that carries the caller's span IDs. The client-side wrapper opens a
child span via `Effect.fn`, the server-side wrapper reattaches it as
`Tracer.externalSpan` parent so handlers and any nested user spans
join the same trace. Span names follow OTel RPC conventions
(`${actor}/${action}` + `kind` + `rpc.system.name` / `rpc.method`).
… quieter test output
Refactor sleep teardown to use deterministic post-condition polling with `TestClock.withLive` instead of relying on real-time delays, ensuring consistent test outcomes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces
@rivetkit/effect, an Effect-based SDK for Rivet Actors.What's here
examples/effect/— proposed API surfacerivetkit-typescript/packages/effect/—@rivetkit/effectpackageLooking for feedback on the API's design.
Benefits
throw new UserError("...", { code: "..." })). Callers catchunknown. The Effect SDK puts errors in theEchannel:Effect<number, CounterOverflowError>. The client sees the exact union of possible errors at compile time.(c, amount: number)) are TypeScript-only. Types are erased at runtime. A malicious or drifted-version client can send anything. The Effect SDK validates wire data withSchema.decodeUnknownbefore it reaches handler code.Counter.clientdepends onClientin theRchannel. If you forgetLayer.provide, you get a compile error naming the missing runtime dependency. The current SDK'stypeof registrygives type-safe method calls, but doesn't track the Effect runtime dependency.setupTestbut swapping individual services requires the test framework's internals.ScopeandaddFinalizerguarantee cleanup runs even on errors or interruption. The current SDK has lifecycle hooks (onSleep,onDestroy) but they're not compositional. You can't nest scopes or attach finalizers from within action handlers.Schema.Datedecodes an ISO string into aDate,Schema.BigIntdecodes a string into aBigInt. The current SDK has no equivalent — you get whatever JSON gives you.c.queue.iter()). The Effect SDK exposes durable messages as an EffectQueue.Dequeue, giving you batching, concurrent consumers via forked fibers, and composability withStream,Schedule, and other Effect primitives out of the box.${actor}/${action}name,kind: "client" | "server",rpc.system.name/rpc.methodattributes), and user-defined sub-spans inside handlers nest correctly under the SDK's server span.Tasks
Schema.encode/decodeat the state persistence boundaryCounter.EventsAPIActor.DbAPIRunner.serve(), ...@rivetkit/effectpnpm testinherits prior runs' dead-envoy actor bindings and stalls on the engine's lost-envoy threshold"Open Questions
State initialization
state: { count: 0 })createState(c, input))getOrCreate(key, { createWithInput: { start: 10 } })equivalentState persistence semantics
SubscriptionRefchanges persisted? On everyRef.update, or batched?stateSaveInterval(current SDK batches writes at a configurable interval)?Configuration and timeouts
noSleep,maxQueueSize,maxQueueMessageSizelive?Actor.makeoptions,toLayeroptions, or registry-level config?Connections
createConnState(c, params))c.conn.state)c.conns)onBeforeConnect,onConnect,onDisconnect)Scheduling
c.schedule.after(delayMs, "actionName", ...args))Lifecycle hooks
onCreate,onDestroyonBeforeConnect,onConnect,onDisconnectonStateChange,onBeforeActionResponseActor self-awareness and control
c.key)c.destroy())Low-level handlers
onRequest)onWebSocket)Client ergonomics
counter.increment(5)) vs single-object payload (counter.increment({ amount: 5 }))Access control for events and queues
canSubscribeon events andcanPublishon queues — guards that receive the connection context and return a boolean to allow/deny a specific client. How should these map to the Effect design?Mixing Effect and plain RivetKit actors
Registry.layer?Registry.fromPlain({ chatRoom })could wrap plain actor configs into a Layer for composition, without requiring schema conversionFuture Improvements
Counter.clientprojection with a per-actor service model (Counter.ClientplusCounter.clientLayer). This would make application effects depend on the exact actor clients they use instead of the sharedClient, improving dependency readability, enabling selective test fakes per actor, supporting per-actor endpoint/token/project overrides through layers, and allowing higher-level services to mock or swap one actor client without replacing the entire transport.http_requestwithparent: None), rivetkit's actor instance tree (rooted atactor.action.<name>via@rivetkit/traces, gated byRIVET_EXPERIMENTAL_OTEL), and the Effect SDK tree (the only one that crosses the wire today, viaActionMeta.trace+Tracer.externalSpan). Joining them would require: (1) the engine'sapi-builder/guard-coremiddleware to extracttraceparentand replaceparent: None, plus forwarding context across the engine→envoy hop; (2) rivetkit'sexecuteActionto read the wire trace context (fromActionMetaortraceparent) and parentactor.action.<name>to it; (3) the Effect SDK to either reparent its server span under rivetkit's actionSpan via a context tag, or migrate to@effect/opentelemetryso both sides emit through one OTel pipeline with standard W3C propagation. End state: a single trace from caller through engine through actor host through handler sub-spans.