feat(effect): Effect SDK design#5244
Conversation
Runner.start now reads the collected RegistryEntry list, builds the
underlying rivetkit `use` map by calling actor({ actions, options })
per registered actor, then setup({ use, endpoint, token, namespace })
.start(). Action handlers are stub-throwing placeholders until Step 4
opens per-instance scopes and routes dispatch through a global
ManagedRuntime. RunnerShape is now a real `{ mode }` marker so
Runner.of({ mode: "start" }) can construct without phantom-symbol
gymnastics.
Smoke-tested: examples/effect prints the RivetKit serverful welcome
banner with `Actors: 1` and stays running.
…start Runner.start now snapshots the current Effect context, then builds rivetkit actor() definitions that dispatch through it. Per actor: - onWake creates a fresh Scope.make() and runs entry.buildHandlers with that scope provided. The resulting Handlers bag plus the scope is stored on a side Map<actorId, ActorInstance>. - Each action callback decodes the incoming payload via Schema.decodeUnknownEffect(payloadSchema), runs the matching handler effect, and encodes the result via Schema.encodeUnknownEffect(successSchema). - onSleep closes the scope (firing user Effect.addFinalizer cleanup) and drops the map entry. Schema services for the slice's pure-data schemas reduce to never, so Effect.runPromiseWith(context) is enough — handler-specific service injection (and typed-error / defect wire encoding) are follow-ups. Smoke test: examples/effect boots clean against the remote engine endpoint.
… engine - main.ts now uses Registry.layer() with no endpoint so the example resolves through env (RIVET_RUN_ENGINE=1 spawns the local engine) instead of pointing at api.rivet.dev. - start/dev scripts bake in RIVET_RUN_ENGINE=1 so `pnpm start` runs the full stack out of the box. Repo contributors running off cargo build still need RIVET_ENGINE_BINARY pointing at their target/debug/rivet-engine. - Add a `pnpm client` smoke test in src/client.ts that drives Counter.Increment + GetCount via a plain rivetkit client. The parked Effect-Client preview block stays at the bottom for the next slice. Verified end-to-end: GetCount 0 -> Increment(5)=5 -> Increment(3)=8 -> GetCount 8. Counter.Increment(20) triggers CounterOverflowError server-side; it crosses the wire as a generic RivetError defect (typed-error encoding via action.errorSchema is the next slice).
…mple Pure TypeScript example with no UI framework: actor in src/index.ts and a rivetkit/client script in src/client.ts. Registry self-spawns the engine via startEngine: true. Includes vitest coverage via setupTest.
Add a per-instance Address ({ actorId, name, key }) carrying both
addressing modes (engine-assigned actorId and the user-facing
name+key pair) and a CurrentAddress Context.Service that holds it.
Runner.start's onWake now reads c.actorId/name/key and provides
CurrentAddress alongside Scope.Scope when running buildHandlers,
mirroring effect-cluster's Entity.CurrentAddress pattern.
Actor.toLayer's RX excludes CurrentAddress so consumers don't leak
the service into the resulting layer's R channel.
Counter example uses it to log waking/sleeping with the address;
verified end-to-end via examples/effect.
…rror
Client.layer wraps a single rivetkit createClient transport behind a
narrow callAction surface. Counter.client yields a typed accessor whose
handle methods encode payloads, dispatch through the transport, and
decode success / typed errors from the wire.
Typed errors round-trip end-to-end: the server-side action wrapper
encodes failures via action.errorSchema and throws a rivetkit UserError
carrying the encoded shape as metadata (with the typed error's _tag as
the wire code). The client decodes that metadata back into the original
error class, falling through to RivetError when the schema doesn't match.
Verified against the example: Effect.catchTag("CounterOverflowError")
fires with the original instance (e.limit accessible). Raw-transport
diagnostics moved to client-raw.ts under a pnpm client:raw script.
…INE_BINARY configuration
Schema.TaggedErrorClass's `message` field is special-cased onto the underlying Error instance, so it surfaces both server-side (rivetkit core's dispatch_action warning gains a real description) and client-side (catchTag handler reads `e.message` alongside `e.limit`). Replaces the previous empty-message fallback that left the wire UserError with `""` as its message.
Restore the persisted state, durable messages, typed events, KV/DB services, queued message loop, and client `send` / `subscribe` calls in `examples/effect/src/` as commented-out code with notes that they are not yet implemented in the v1 SDK. Keeps the intended public contract visible so it can be re-enabled once each feature lands.
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.
# Conflicts: # rivetkit-rust/packages/rivetkit-core/tests/context.rs # rivetkit-rust/packages/rivetkit-core/tests/modules/action_dispatch_error.rs # rivetkit-rust/packages/rivetkit-core/tests/schedule.rs # rivetkit-rust/packages/rivetkit-core/tests/sqlite.rs # rivetkit-rust/packages/rivetkit-core/tests/task.rs
…-design # Conflicts: # pnpm-lock.yaml
|
🚅 Deployed to the rivet-pr-5244 environment in rivet-frontend
|
Code Review: feat(effect): Effect SDK designOverviewThis PR introduces The design is well-thought-out and idiomatic Effect. The action/actor/state separation is clean, and the tracing/logging integration is solid. A few issues are worth addressing before merge. Issues1. Breaking change in trace attributes (existing code)
This is a semantic versioning breaking change — any dashboards, alert rules, or OTEL queries filtering on 2. Massive code duplication in
|
Effect SDK design work authored by @IGassmann.