From 7867deeace2ab92ae23ef29683fb0c23378d4e13 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 23 Apr 2026 13:04:50 +0200 Subject: [PATCH 001/306] feat: add Effect SDK API design proposal Add examples/effect with the proposed API surface for @rivetkit/effect. This is a design-only example for feedback, not a working implementation. --- examples/effect/README.md | 19 +++++++ examples/effect/package.json | 32 +++++++++++ examples/effect/src/actors/Counter.ts | 81 +++++++++++++++++++++++++++ examples/effect/src/client.ts | 59 +++++++++++++++++++ examples/effect/src/main.ts | 21 +++++++ examples/effect/tsconfig.json | 15 +++++ examples/effect/turbo.json | 4 ++ 7 files changed, 231 insertions(+) create mode 100644 examples/effect/README.md create mode 100644 examples/effect/package.json create mode 100644 examples/effect/src/actors/Counter.ts create mode 100644 examples/effect/src/client.ts create mode 100644 examples/effect/src/main.ts create mode 100644 examples/effect/tsconfig.json create mode 100644 examples/effect/turbo.json diff --git a/examples/effect/README.md b/examples/effect/README.md new file mode 100644 index 0000000000..5812dc7447 --- /dev/null +++ b/examples/effect/README.md @@ -0,0 +1,19 @@ +# Effect SDK API Design + +> **This is a design proposal, not a working example.** The `@rivetkit/effect` package does not exist yet. The code here shows the proposed API surface for an Effect-based SDK for Rivet Actors. + +## Overview + +This example demonstrates the proposed API design for `@rivetkit/effect`, an [Effect](https://effect.website/) SDK for Rivet Actors. The design leverages Effect's type system to provide: + +- Schema-validated actions with typed errors +- Layer-based composition for actor registration, transport, and testing +- Compile-time tracking of actor dependencies via Effect's `R` type parameter +- Per-actor transport overrides and selective test mocking + +## Files + +- [`src/actors/Counter.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/effect/src/actors/Counter.ts) - Actor definition (public contract) and implementation (server-only Layer) +- [`src/main.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/effect/src/main.ts) - Server entry point using `Registry.layer` +- [`src/client.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/effect/src/client.ts) - Client usage with typed actor dependencies + diff --git a/examples/effect/package.json b/examples/effect/package.json new file mode 100644 index 0000000000..8b492b2e52 --- /dev/null +++ b/examples/effect/package.json @@ -0,0 +1,32 @@ +{ + "name": "effect", + "version": "2.0.21", + "private": true, + "type": "module", + "scripts": { + "dev": "npx srvx --import tsx src/main.ts", + "start": "npx srvx --import tsx src/main.ts", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "rivetkit": "*", + "@rivetkit/effect": "*", + "effect": "^3.0.0", + "@effect/platform": "^0.80.0", + "@effect/platform-node": "^0.75.0", + "@effect/schema": "^0.80.0" + }, + "devDependencies": { + "@types/node": "^22.13.9", + "tsx": "^3.12.7", + "typescript": "^5.5.2" + }, + "stableVersion": "0.8.0", + "template": { + "technologies": ["typescript"], + "tags": [], + "noFrontend": true, + "skipVercel": true + }, + "license": "MIT" +} diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts new file mode 100644 index 0000000000..6205798a0d --- /dev/null +++ b/examples/effect/src/actors/Counter.ts @@ -0,0 +1,81 @@ +import { Schema, Effect, Ref, PubSub } from "effect" +import { Actor, Action } from "@rivetkit/effect" + +// --- Errors --- + +export class CounterOverflow extends Schema.TaggedError()( + "CounterOverflow", + { limit: Schema.Number }, +) {} + + +// --- Actions --- + +// Actions are first-class values, not inline methods. +// This lets the same action schema drive server validation, +// client type inference, and documentation generation. +export const Increment = Action.make("Increment", { + input: Schema.Struct({ amount: Schema.Number }), + success: Schema.Number, + error: CounterOverflow, +}) + +export const GetCount = Action.make("GetCount", { + success: Schema.Number, +}) + +// --- Actor Definition --- + +// The definition is the actor's public contract: its name, +// state shape, event schemas, and action set. It carries no +// implementation, just types. Both server and client code +// import this; the implementation stays server-only. +export const Counter = Actor.make("Counter", { + state: Schema.Struct({ count: Schema.Number }), + events: { countChanged: Schema.Number }, + actions: [Increment, GetCount], +}) + +// --- Implementation --- + +const MAX_COUNT = 1_000_000 + +// Counter.toLayer produces a Layer that registers this actor +// with whatever registry is in context. The Effect inside runs +// once per actor instance (not once per action call), so +// yielded services like State and Events are instance-scoped. +export const CounterLive = Counter.toLayer( + Effect.gen(function* () { + // Access actor-provided services + const state = yield* Counter.State + // ^ SubscriptionRef<{ count: number }> + const events = yield* Counter.Events + // ^ { countChanged: PubSub } + const kv = yield* Counter.Kv + const db = yield* Counter.Db + + // Finalizers run when the actor's scope closes + yield* Effect.addFinalizer(() => + Effect.log("Counter destroyed? or/and sleep? (TBD)") + ) + + // Return the action implementations. Counter.of + // type-checks each handler against its Action schema. + return Counter.of({ + Increment: ({ input }) => + Effect.gen(function* () { + const next = yield* Ref.updateAndGet(state, (s) => ({ + count: s.count + input.amount, + })) + if (next.count > MAX_COUNT) { + return yield* new CounterOverflow({ limit: MAX_COUNT }) + } + yield* PubSub.publish(events.countChanged, next.count) + return next.count + }), + + GetCount: () => + Ref.get(state).pipe(Effect.map((s) => s.count)), + }) + }), +) diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts new file mode 100644 index 0000000000..1a762d246d --- /dev/null +++ b/examples/effect/src/client.ts @@ -0,0 +1,59 @@ +import { Effect, Layer, Stream } from "effect" +import { ActorTransport } from "@rivetkit/effect" +import { Counter } from "./actors/Counter.ts" +// import { ChatRoom } from "./actors/ChatRoom.ts" + +// ------------------------------------------------------------------ +// Counter.Client is a Context.Tag generated by Actor.make. +// Yielding it adds Counter.Client to R, so the type signature +// of any effect that uses Counter explicitly declares that +// dependency. This allows to track in the type system which actors +// each piece of code depends on. +// ------------------------------------------------------------------ +const program = Effect.gen(function* () { + const counterClient = yield* Counter.Client + // R now includes Counter.Client + + const counter = counterClient.getOrCreate(["counter-123"]) + + // Action calls return Effects with types inferred from the schema. + // counter.increment: (input: { amount: number }) => Effect + const count = yield* counter.increment({ amount: 5 }) + yield* Effect.log(`Count: ${count}`) + + // subscribe returns a Stream typed from the event schema. + yield* counter.subscribe("countChanged").pipe( + Stream.take(3), + Stream.runForEach((n) => Effect.log(`Changed: ${n}`)), + ) +}) +// program: Effect +// ^^^^^^^^^^^^^^ +// Missing Counter.Client -> compile error naming the exact actor dependency. + +// ------------------------------------------------------------------ +// Wiring: each actor's .clientLayer depends on ActorTransport. +// You compose them explicitly, which is more verbose than a +// single transport provide, but gives you two things: +// +// 1. Per-actor transport overrides (different endpoints, tokens, +// or even different Rivet projects per actor). +// +// 2. Selective test mocking. Replace one actor's client layer +// with a fake without touching the others. +// ------------------------------------------------------------------ +const ActorClientLayer = Layer.mergeAll( + Counter.clientLayer, +// ChatRoom.clientLayer, +).pipe( + // Both client layers share the same transport here, but you + // could provide different transports to each (see below). + Layer.provide( + ActorTransport.layer({ + endpoint: "https://api.rivet.dev", + token: "...", + }), + ), +) + +program.pipe(Effect.provide(ActorClientLayer), Effect.runPromise) diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts new file mode 100644 index 0000000000..f59877eefe --- /dev/null +++ b/examples/effect/src/main.ts @@ -0,0 +1,21 @@ +import { Layer } from "effect" +import { NodeRuntime } from "@effect/platform-node" +import { Registry, TestRegistry } from "@rivetkit/effect" +import { CounterLive } from "./actors/Counter.ts" +// import { ChatRoomLive } from "./actors/ChatRoom.ts" + +const ActorsLayer = Layer.mergeAll( + CounterLive, +// ChatRoomLive, +) + +const MainLayer = ActorsLayer.pipe( + Layer.provide(Registry.layer({ storagePath: "./data" })), +) + +const TestLayer = ActorsLayer.pipe( + Layer.provide(TestRegistry.layer), +) + +// Keeps the layer alive. Tears down on SIGINT/SIGTERM. +Layer.launch(MainLayer).pipe(NodeRuntime.runMain) diff --git a/examples/effect/tsconfig.json b/examples/effect/tsconfig.json new file mode 100644 index 0000000000..c3382bb665 --- /dev/null +++ b/examples/effect/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "bundler", + "types": ["node"], + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true + }, + "include": ["src/**/*"] +} diff --git a/examples/effect/turbo.json b/examples/effect/turbo.json new file mode 100644 index 0000000000..29d4cb2625 --- /dev/null +++ b/examples/effect/turbo.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"] +} From 1a6bf5e5b42902fbf3a31fabc2238e20e5cb8494 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 23 Apr 2026 13:26:50 +0200 Subject: [PATCH 002/306] refactor(effect): inline MAX_COUNT constant with value 20 --- examples/effect/src/actors/Counter.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index 6205798a0d..4ec422d0d3 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -38,8 +38,6 @@ export const Counter = Actor.make("Counter", { // --- Implementation --- -const MAX_COUNT = 1_000_000 - // Counter.toLayer produces a Layer that registers this actor // with whatever registry is in context. The Effect inside runs // once per actor instance (not once per action call), so @@ -67,8 +65,8 @@ export const CounterLive = Counter.toLayer( const next = yield* Ref.updateAndGet(state, (s) => ({ count: s.count + input.amount, })) - if (next.count > MAX_COUNT) { - return yield* new CounterOverflow({ limit: MAX_COUNT }) + if (next.count > 20) { + return yield* new CounterOverflow({ limit: 20 }) } yield* PubSub.publish(events.countChanged, next.count) return next.count From 63c49387962ddc4132a0676a0dc4fc20a1d7f8d8 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 23 Apr 2026 13:31:05 +0200 Subject: [PATCH 003/306] refactor(effect): rename CounterOverflow to CounterOverflowError --- examples/effect/src/actors/Counter.ts | 8 ++++---- examples/effect/src/client.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index 4ec422d0d3..811034e873 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -3,8 +3,8 @@ import { Actor, Action } from "@rivetkit/effect" // --- Errors --- -export class CounterOverflow extends Schema.TaggedError()( - "CounterOverflow", +export class CounterOverflowError extends Schema.TaggedError()( + "CounterOverflowError", { limit: Schema.Number }, ) {} @@ -17,7 +17,7 @@ export class CounterOverflow extends Schema.TaggedError()( export const Increment = Action.make("Increment", { input: Schema.Struct({ amount: Schema.Number }), success: Schema.Number, - error: CounterOverflow, + error: CounterOverflowError, }) export const GetCount = Action.make("GetCount", { @@ -66,7 +66,7 @@ export const CounterLive = Counter.toLayer( count: s.count + input.amount, })) if (next.count > 20) { - return yield* new CounterOverflow({ limit: 20 }) + return yield* new CounterOverflowError({ limit: 20 }) } yield* PubSub.publish(events.countChanged, next.count) return next.count diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 1a762d246d..bc1aa53e4a 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -17,7 +17,7 @@ const program = Effect.gen(function* () { const counter = counterClient.getOrCreate(["counter-123"]) // Action calls return Effects with types inferred from the schema. - // counter.increment: (input: { amount: number }) => Effect + // counter.increment: (input: { amount: number }) => Effect const count = yield* counter.increment({ amount: 5 }) yield* Effect.log(`Count: ${count}`) @@ -27,7 +27,7 @@ const program = Effect.gen(function* () { Stream.runForEach((n) => Effect.log(`Changed: ${n}`)), ) }) -// program: Effect +// program: Effect // ^^^^^^^^^^^^^^ // Missing Counter.Client -> compile error naming the exact actor dependency. From 9f0b542f7770a019721dad85546b7d9f5535ca10 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 23 Apr 2026 13:34:18 +0200 Subject: [PATCH 004/306] refactor: embed actions in actor definition 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. --- examples/effect/src/actors/Counter.ts | 35 +++++++++++---------------- examples/effect/src/client.ts | 2 +- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index 811034e873..e3f0303602 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -1,5 +1,5 @@ import { Schema, Effect, Ref, PubSub } from "effect" -import { Actor, Action } from "@rivetkit/effect" +import { Actor } from "@rivetkit/effect" // --- Errors --- @@ -8,23 +8,7 @@ export class CounterOverflowError extends Schema.TaggedError + increment: ({ input }) => Effect.gen(function* () { const next = yield* Ref.updateAndGet(state, (s) => ({ count: s.count + input.amount, @@ -72,7 +65,7 @@ export const CounterLive = Counter.toLayer( return next.count }), - GetCount: () => + getCount: () => Ref.get(state).pipe(Effect.map((s) => s.count)), }) }), diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index bc1aa53e4a..406166d3ed 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -28,7 +28,7 @@ const program = Effect.gen(function* () { ) }) // program: Effect -// ^^^^^^^^^^^^^^ +// ^^^^^^^^^^^^^^ // Missing Counter.Client -> compile error naming the exact actor dependency. // ------------------------------------------------------------------ From 28100033078f1b6f0db07e6a9494765c507b8020 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 23 Apr 2026 13:45:49 +0200 Subject: [PATCH 005/306] refactor(effect): rename action input to payload --- examples/effect/src/actors/Counter.ts | 6 +++--- examples/effect/src/client.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index e3f0303602..ac23179f34 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -19,7 +19,7 @@ export const Counter = Actor.make("Counter", { events: { countChanged: Schema.Number }, actions: { increment: { - input: Schema.Struct({ amount: Schema.Number }), + payload: Schema.Struct({ amount: Schema.Number }), success: Schema.Number, error: CounterOverflowError, }, @@ -53,10 +53,10 @@ export const CounterLive = Counter.toLayer( // Return the action implementations. Counter.of // type-checks each handler against its Action schema. return Counter.of({ - increment: ({ input }) => + increment: ({ payload }) => Effect.gen(function* () { const next = yield* Ref.updateAndGet(state, (s) => ({ - count: s.count + input.amount, + count: s.count + payload.amount, })) if (next.count > 20) { return yield* new CounterOverflowError({ limit: 20 }) diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 406166d3ed..075c527386 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -17,7 +17,7 @@ const program = Effect.gen(function* () { const counter = counterClient.getOrCreate(["counter-123"]) // Action calls return Effects with types inferred from the schema. - // counter.increment: (input: { amount: number }) => Effect + // counter.increment: (payload: { amount: number }) => Effect const count = yield* counter.increment({ amount: 5 }) yield* Effect.log(`Count: ${count}`) From 9a337a5418c3a69b2de81f4a260760fd428241db Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 23 Apr 2026 13:48:33 +0200 Subject: [PATCH 006/306] docs(effect): document rationale for explicit action schemas --- examples/effect/src/actors/Counter.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index ac23179f34..1f35b54e5b 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -17,6 +17,23 @@ export class CounterOverflowError extends Schema.TaggedError Date: Thu, 23 Apr 2026 13:51:22 +0200 Subject: [PATCH 007/306] style(effect): reformat Counter.ts to use tabs instead of spaces --- examples/effect/src/actors/Counter.ts | 122 +++++++++++++------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index 1f35b54e5b..7cb783c4f5 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -4,8 +4,8 @@ import { Actor } from "@rivetkit/effect" // --- Errors --- export class CounterOverflowError extends Schema.TaggedError()( - "CounterOverflowError", - { limit: Schema.Number }, + "CounterOverflowError", + { limit: Schema.Number }, ) {} // --- Definition --- @@ -15,35 +15,35 @@ export class CounterOverflowError extends Schema.TaggedError - const events = yield* Counter.Events - // ^ { countChanged: PubSub } - const kv = yield* Counter.Kv - const db = yield* Counter.Db + Effect.gen(function* () { + // Access actor-provided services + const state = yield* Counter.State + // ^ SubscriptionRef<{ count: number }> + const events = yield* Counter.Events + // ^ { countChanged: PubSub } + const kv = yield* Counter.Kv + const db = yield* Counter.Db - // Finalizers run when the actor's scope closes - yield* Effect.addFinalizer(() => - Effect.log("Counter destroyed? or/and sleep? (TBD)") - ) + // Finalizers run when the actor's scope closes + yield* Effect.addFinalizer(() => + Effect.log("Counter destroyed? or/and sleep? (TBD)") + ) - // Return the action implementations. Counter.of - // type-checks each handler against its Action schema. - return Counter.of({ - increment: ({ payload }) => - Effect.gen(function* () { - const next = yield* Ref.updateAndGet(state, (s) => ({ - count: s.count + payload.amount, - })) - if (next.count > 20) { - return yield* new CounterOverflowError({ limit: 20 }) - } - yield* PubSub.publish(events.countChanged, next.count) - return next.count - }), + // Return the action implementations. Counter.of + // type-checks each handler against its Action schema. + return Counter.of({ + increment: ({ payload }) => + Effect.gen(function* () { + const next = yield* Ref.updateAndGet(state, (s) => ({ + count: s.count + payload.amount, + })) + if (next.count > 20) { + return yield* new CounterOverflowError({ limit: 20 }) + } + yield* PubSub.publish(events.countChanged, next.count) + return next.count + }), - getCount: () => - Ref.get(state).pipe(Effect.map((s) => s.count)), - }) - }), + getCount: () => + Ref.get(state).pipe(Effect.map((s) => s.count)), + }) + }), ) From 80b070cba688a6dd37cddcbc6c2f2af84e0b81e7 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 23 Apr 2026 13:54:27 +0200 Subject: [PATCH 008/306] docs(effect): document benefits of context-based actor services --- examples/effect/src/actors/Counter.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index 7cb783c4f5..3de7613092 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -54,7 +54,19 @@ export const Counter = Actor.make("Counter", { // yielded services like State and Events are instance-scoped. export const CounterLive = Counter.toLayer( Effect.gen(function* () { - // Access actor-provided services + // Actor-provided services are yielded from the Effect context. + // They are scoped to this actor instance, not to individual + // action calls. This means all action handlers below close + // over the same state, events, kv, and db references. + // + // Because services come through the context (not a context + // parameter like the current SDK's `c`), they are: + // + // - Visible in the type signature. The Effect's R channel + // declares exactly which services are required. + // + // - Swappable via layers. Tests can provide an in-memory KV + // or a mock DB without changing the actor code. const state = yield* Counter.State // ^ SubscriptionRef<{ count: number }> const events = yield* Counter.Events From 9c33d72b63c20ee60f5685b6c5122639e91fd98e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 23 Apr 2026 13:55:03 +0200 Subject: [PATCH 009/306] refactor(effect): move error declaration closer to actor definition --- examples/effect/src/actors/Counter.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index 3de7613092..f0bff4e72f 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -1,15 +1,13 @@ import { Schema, Effect, Ref, PubSub } from "effect" import { Actor } from "@rivetkit/effect" -// --- Errors --- +// --- Definition --- export class CounterOverflowError extends Schema.TaggedError()( "CounterOverflowError", { limit: Schema.Number }, ) {} -// --- Definition --- - // The definition is the actor's public contract: its name, // state shape, event schemas, and action set. It carries no // implementation, just types. Both server and client code From fddf5247ac43dbb66426e497dfdabf4791824722 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 23 Apr 2026 14:11:53 +0200 Subject: [PATCH 010/306] feat(effect): add options to Counter actor definition for name and icon --- examples/effect/src/actors/Counter.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index f0bff4e72f..ddb7b6ba99 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -42,6 +42,10 @@ export const Counter = Actor.make("Counter", { success: Schema.Number, }, }, + options: { + name: "Counter", // Human-friendly display name + icon: "comments", // FontAwesome icon name + }, }) // --- Implementation --- From f544e67aa5c38e31da3a85aa59a4432c6fcb823d Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 23 Apr 2026 14:12:03 +0200 Subject: [PATCH 011/306] style(effect): reformat Counter.ts event block for consistency --- examples/effect/src/actors/Counter.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index ddb7b6ba99..8c4ff06fdd 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -14,7 +14,9 @@ export class CounterOverflowError extends Schema.TaggedError Date: Thu, 23 Apr 2026 14:28:01 +0200 Subject: [PATCH 012/306] docs(effect): remove contract separation from schema rationale --- examples/effect/src/actors/Counter.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index 8c4ff06fdd..04426a0992 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -25,12 +25,7 @@ export const Counter = Actor.make("Counter", { // Schema.decodeUnknown before it reaches handler code. Handler // inference erases types at runtime and trusts whatever arrives. // - // 2. Contract separation. The definition can be imported by client - // code without pulling in server dependencies. It can also be - // published as a standalone package or satisfied by multiple - // implementations (real, test, mock). - // - // 3. Wire encoding control. Effect Schema distinguishes encoded + // 2. Wire encoding control. Effect Schema distinguishes encoded // (wire) and decoded (runtime) types, e.g. Schema.Date decodes // a string into a Date. Handler inference only gives the decoded // type. From dc6370fbf0de30297a10a9a05643992d0a6b50f8 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 23 Apr 2026 14:35:27 +0200 Subject: [PATCH 013/306] refactor(effect): migrate API design to Effect v4 --- examples/effect/package.json | 6 ++---- examples/effect/src/actors/Counter.ts | 4 ++-- examples/effect/src/client.ts | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/examples/effect/package.json b/examples/effect/package.json index 8b492b2e52..b25ec3ffb2 100644 --- a/examples/effect/package.json +++ b/examples/effect/package.json @@ -11,10 +11,8 @@ "dependencies": { "rivetkit": "*", "@rivetkit/effect": "*", - "effect": "^3.0.0", - "@effect/platform": "^0.80.0", - "@effect/platform-node": "^0.75.0", - "@effect/schema": "^0.80.0" + "effect": "^4.0.0", + "@effect/platform-node": "^4.0.0" }, "devDependencies": { "@types/node": "^22.13.9", diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index 04426a0992..5e8e98a63c 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -3,7 +3,7 @@ import { Actor } from "@rivetkit/effect" // --- Definition --- -export class CounterOverflowError extends Schema.TaggedError()( +export class CounterOverflowError extends Schema.TaggedErrorClass()( "CounterOverflowError", { limit: Schema.Number }, ) {} @@ -22,7 +22,7 @@ export const Counter = Actor.make("Counter", { // // 1. Runtime validation. Client-to-server is an untrusted boundary. // Schemas let the server validate wire data with - // Schema.decodeUnknown before it reaches handler code. Handler + // Schema.decodeUnknownEffect before it reaches handler code. Handler // inference erases types at runtime and trusts whatever arrives. // // 2. Wire encoding control. Effect Schema distinguishes encoded diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 075c527386..19456a1b52 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -4,7 +4,7 @@ import { Counter } from "./actors/Counter.ts" // import { ChatRoom } from "./actors/ChatRoom.ts" // ------------------------------------------------------------------ -// Counter.Client is a Context.Tag generated by Actor.make. +// Counter.Client is a Context.Service generated by Actor.make. // Yielding it adds Counter.Client to R, so the type signature // of any effect that uses Counter explicitly declares that // dependency. This allows to track in the type system which actors From dec23f0236aaceadef7f8d507f0021bb7d098c92 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 24 Apr 2026 12:26:50 +0200 Subject: [PATCH 014/306] refactor(effect): switch to standalone first-class actions --- examples/effect/src/actors/Counter.ts | 70 ++++++++++++++++----------- examples/effect/src/client.ts | 6 +-- 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index 5e8e98a63c..0b2dd2b3aa 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -1,44 +1,56 @@ import { Schema, Effect, Ref, PubSub } from "effect" -import { Actor } from "@rivetkit/effect" +import { Actor, Action } from "@rivetkit/effect" -// --- Definition --- +// --- Errors --- export class CounterOverflowError extends Schema.TaggedErrorClass()( "CounterOverflowError", { limit: Schema.Number }, ) {} +// --- Actions --- + +// Actions use explicit schemas rather than inferring types from +// the handler signature (like the current Rivet SDK does) because: +// +// - Runtime validation. Client-to-server is an untrusted boundary. +// Schemas validate wire data before it reaches handler code. +// Handler inference erases types at runtime and trusts whatever +// arrives. +// +// - Wire encoding control. Effect Schema distinguishes encoded +// (wire) and decoded (runtime) types, e.g. Schema.Date decodes +// a string into a Date. Handler inference only gives the decoded +// type. +// +// Actions are standalone values (vs. embedded in the actor +// definition) because: +// +// - Per-action middleware and annotations. Allows for Auth on some +// actions but not others, timeout overrides... +// +// - Shared action protocols. A Ping health-check or GetMetrics +// action defined once and composed into multiple actors. +export const Increment = Action.make("Increment", { + payload: Schema.Struct({ amount: Schema.Number }), + success: Schema.Number, + error: CounterOverflowError, +}) + +export const GetCount = Action.make("GetCount", { + success: Schema.Number, +}) + +// --- Actor Definition --- + // The definition is the actor's public contract: its name, // state shape, event schemas, and action set. It carries no // implementation, just types. Both server and client code // import this; the implementation stays server-only. export const Counter = Actor.make("Counter", { state: Schema.Struct({ count: Schema.Number }), - events: { - countChanged: Schema.Number - }, - // Actions use explicit schemas rather than inferring types from - // the handler signature (like the current Rivet SDK does) because: - // - // 1. Runtime validation. Client-to-server is an untrusted boundary. - // Schemas let the server validate wire data with - // Schema.decodeUnknownEffect before it reaches handler code. Handler - // inference erases types at runtime and trusts whatever arrives. - // - // 2. Wire encoding control. Effect Schema distinguishes encoded - // (wire) and decoded (runtime) types, e.g. Schema.Date decodes - // a string into a Date. Handler inference only gives the decoded - // type. - actions: { - increment: { - payload: Schema.Struct({ amount: Schema.Number }), - success: Schema.Number, - error: CounterOverflowError, - }, - getCount: { - success: Schema.Number, - }, - }, + events: { countChanged: Schema.Number }, + actions: [Increment, GetCount], options: { name: "Counter", // Human-friendly display name icon: "comments", // FontAwesome icon name @@ -81,7 +93,7 @@ export const CounterLive = Counter.toLayer( // Return the action implementations. Counter.of // type-checks each handler against its Action schema. return Counter.of({ - increment: ({ payload }) => + Increment: ({ payload }) => Effect.gen(function* () { const next = yield* Ref.updateAndGet(state, (s) => ({ count: s.count + payload.amount, @@ -93,7 +105,7 @@ export const CounterLive = Counter.toLayer( return next.count }), - getCount: () => + GetCount: () => Ref.get(state).pipe(Effect.map((s) => s.count)), }) }), diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 19456a1b52..783b6b13a8 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -17,8 +17,8 @@ const program = Effect.gen(function* () { const counter = counterClient.getOrCreate(["counter-123"]) // Action calls return Effects with types inferred from the schema. - // counter.increment: (payload: { amount: number }) => Effect - const count = yield* counter.increment({ amount: 5 }) + // counter.Increment: (payload: { amount: number }) => Effect + const count = yield* counter.Increment({ amount: 5 }) yield* Effect.log(`Count: ${count}`) // subscribe returns a Stream typed from the event schema. @@ -47,7 +47,7 @@ const ActorClientLayer = Layer.mergeAll( // ChatRoom.clientLayer, ).pipe( // Both client layers share the same transport here, but you - // could provide different transports to each (see below). + // could provide different transports to each. Layer.provide( ActorTransport.layer({ endpoint: "https://api.rivet.dev", From 14b456a9c6598a140ee29111a832eb7204623104 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 24 Apr 2026 14:10:28 +0200 Subject: [PATCH 015/306] feat(effect): add temporary variable example --- examples/effect/src/actors/Counter.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index 0b2dd2b3aa..3b4831c255 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -85,6 +85,9 @@ export const CounterLive = Counter.toLayer( const kv = yield* Counter.Kv const db = yield* Counter.Db + // Equivalent to current SDK's temporary variables + const connectionsTotal = yield* Ref.make(0) + // Finalizers run when the actor's scope closes yield* Effect.addFinalizer(() => Effect.log("Counter destroyed? or/and sleep? (TBD)") From feba11fcaec6ee04cb352ad72b651bef9297d834 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 24 Apr 2026 16:15:13 +0200 Subject: [PATCH 016/306] refactor(effect): clarify toLayer body as wake scope with sleep finalizer --- examples/effect/src/actors/Counter.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts index 3b4831c255..e0f18e34b1 100644 --- a/examples/effect/src/actors/Counter.ts +++ b/examples/effect/src/actors/Counter.ts @@ -64,6 +64,7 @@ export const Counter = Actor.make("Counter", { // once per actor instance (not once per action call), so // yielded services like State and Events are instance-scoped. export const CounterLive = Counter.toLayer( + // Wake scope (runs each wake, finalizers run on sleep) Effect.gen(function* () { // Actor-provided services are yielded from the Effect context. // They are scoped to this actor instance, not to individual @@ -88,10 +89,8 @@ export const CounterLive = Counter.toLayer( // Equivalent to current SDK's temporary variables const connectionsTotal = yield* Ref.make(0) - // Finalizers run when the actor's scope closes - yield* Effect.addFinalizer(() => - Effect.log("Counter destroyed? or/and sleep? (TBD)") - ) + // Run when the actor sleeps + yield* Effect.addFinalizer(() => Effect.log("sleeping")) // Return the action implementations. Counter.of // type-checks each handler against its Action schema. From fc08555d4c333ba833c344a85002719393fcceb4 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 24 Apr 2026 18:08:48 +0200 Subject: [PATCH 017/306] refactor(effect): split Counter actor into per-actor directory with api/live files --- examples/effect/src/actors/Counter.ts | 114 --------------------- examples/effect/src/actors/counter/api.ts | 58 +++++++++++ examples/effect/src/actors/counter/live.ts | 86 ++++++++++++++++ examples/effect/src/actors/mod.ts | 2 + examples/effect/src/client.ts | 6 +- examples/effect/src/main.ts | 4 +- 6 files changed, 152 insertions(+), 118 deletions(-) delete mode 100644 examples/effect/src/actors/Counter.ts create mode 100644 examples/effect/src/actors/counter/api.ts create mode 100644 examples/effect/src/actors/counter/live.ts create mode 100644 examples/effect/src/actors/mod.ts diff --git a/examples/effect/src/actors/Counter.ts b/examples/effect/src/actors/Counter.ts deleted file mode 100644 index e0f18e34b1..0000000000 --- a/examples/effect/src/actors/Counter.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Schema, Effect, Ref, PubSub } from "effect" -import { Actor, Action } from "@rivetkit/effect" - -// --- Errors --- - -export class CounterOverflowError extends Schema.TaggedErrorClass()( - "CounterOverflowError", - { limit: Schema.Number }, -) {} - -// --- Actions --- - -// Actions use explicit schemas rather than inferring types from -// the handler signature (like the current Rivet SDK does) because: -// -// - Runtime validation. Client-to-server is an untrusted boundary. -// Schemas validate wire data before it reaches handler code. -// Handler inference erases types at runtime and trusts whatever -// arrives. -// -// - Wire encoding control. Effect Schema distinguishes encoded -// (wire) and decoded (runtime) types, e.g. Schema.Date decodes -// a string into a Date. Handler inference only gives the decoded -// type. -// -// Actions are standalone values (vs. embedded in the actor -// definition) because: -// -// - Per-action middleware and annotations. Allows for Auth on some -// actions but not others, timeout overrides... -// -// - Shared action protocols. A Ping health-check or GetMetrics -// action defined once and composed into multiple actors. -export const Increment = Action.make("Increment", { - payload: Schema.Struct({ amount: Schema.Number }), - success: Schema.Number, - error: CounterOverflowError, -}) - -export const GetCount = Action.make("GetCount", { - success: Schema.Number, -}) - -// --- Actor Definition --- - -// The definition is the actor's public contract: its name, -// state shape, event schemas, and action set. It carries no -// implementation, just types. Both server and client code -// import this; the implementation stays server-only. -export const Counter = Actor.make("Counter", { - state: Schema.Struct({ count: Schema.Number }), - events: { countChanged: Schema.Number }, - actions: [Increment, GetCount], - options: { - name: "Counter", // Human-friendly display name - icon: "comments", // FontAwesome icon name - }, -}) - -// --- Implementation --- - -// Counter.toLayer produces a Layer that registers this actor -// with whatever registry is in context. The Effect inside runs -// once per actor instance (not once per action call), so -// yielded services like State and Events are instance-scoped. -export const CounterLive = Counter.toLayer( - // Wake scope (runs each wake, finalizers run on sleep) - Effect.gen(function* () { - // Actor-provided services are yielded from the Effect context. - // They are scoped to this actor instance, not to individual - // action calls. This means all action handlers below close - // over the same state, events, kv, and db references. - // - // Because services come through the context (not a context - // parameter like the current SDK's `c`), they are: - // - // - Visible in the type signature. The Effect's R channel - // declares exactly which services are required. - // - // - Swappable via layers. Tests can provide an in-memory KV - // or a mock DB without changing the actor code. - const state = yield* Counter.State - // ^ SubscriptionRef<{ count: number }> - const events = yield* Counter.Events - // ^ { countChanged: PubSub } - const kv = yield* Counter.Kv - const db = yield* Counter.Db - - // Equivalent to current SDK's temporary variables - const connectionsTotal = yield* Ref.make(0) - - // Run when the actor sleeps - yield* Effect.addFinalizer(() => Effect.log("sleeping")) - - // Return the action implementations. Counter.of - // type-checks each handler against its Action schema. - return Counter.of({ - Increment: ({ payload }) => - Effect.gen(function* () { - const next = yield* Ref.updateAndGet(state, (s) => ({ - count: s.count + payload.amount, - })) - if (next.count > 20) { - return yield* new CounterOverflowError({ limit: 20 }) - } - yield* PubSub.publish(events.countChanged, next.count) - return next.count - }), - - GetCount: () => - Ref.get(state).pipe(Effect.map((s) => s.count)), - }) - }), -) diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts new file mode 100644 index 0000000000..0b046af615 --- /dev/null +++ b/examples/effect/src/actors/counter/api.ts @@ -0,0 +1,58 @@ +import { Schema } from "effect" +import { Actor, Action } from "@rivetkit/effect" + +// --- Errors --- + +export class CounterOverflowError extends Schema.TaggedErrorClass()( + "CounterOverflowError", + { limit: Schema.Number }, +) {} + +// --- Actions --- + +// Actions use explicit schemas rather than inferring types from +// the handler signature (like the current Rivet SDK does) because: +// +// - Runtime validation. Client-to-server is an untrusted boundary. +// Schemas validate wire data before it reaches handler code. +// Handler inference erases types at runtime and trusts whatever +// arrives. +// +// - Wire encoding control. Effect Schema distinguishes encoded +// (wire) and decoded (runtime) types, e.g. Schema.Date decodes +// a string into a Date. Handler inference only gives the decoded +// type. +// +// Actions are standalone values (vs. embedded in the actor +// definition) because: +// +// - Per-action middleware and annotations. Allows for Auth on some +// actions but not others, timeout overrides... +// +// - Shared action protocols. A Ping health-check or GetMetrics +// action defined once and composed into multiple actors. +export const Increment = Action.make("Increment", { + payload: Schema.Struct({ amount: Schema.Number }), + success: Schema.Number, + error: CounterOverflowError, +}) + +export const GetCount = Action.make("GetCount", { + success: Schema.Number, +}) + +// --- Actor Definition --- + +// The definition is the actor's public contract: its name, +// state shape, event schemas, and action set. It carries no +// implementation, just types. Both server and client code +// import this; the implementation stays server-only. +export const Counter = Actor.make("Counter", { + state: Schema.Struct({ count: Schema.Number }), + events: { countChanged: Schema.Number }, + actions: [Increment, GetCount], + options: { + name: "Counter", // Human-friendly display name + icon: "comments", // FontAwesome icon name + }, +}) diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts new file mode 100644 index 0000000000..bc7872dafe --- /dev/null +++ b/examples/effect/src/actors/counter/live.ts @@ -0,0 +1,86 @@ +import { Cause, Effect, Exit, Ref, PubSub } from "effect" +import { Counter, CounterOverflowError } from "./api.ts" + +// --- Actor Implementation --- + +// Counter.toLayer produces a Layer that registers this actor +// with whatever registry is in context. The Effect inside runs +// once per actor instance (not once per action call), so +// yielded services like State and Events are instance-scoped. +export const CounterLive = Counter.toLayer( + // Wake scope (runs each wake, finalizers run on sleep) + Effect.gen(function* () { + // Actor-provided services are yielded from the Effect context. + // They are scoped to this actor instance, not to individual + // action calls. This means all action handlers below close + // over the same state, events, kv, and db references. + // + // Because services come through the context (not a context + // parameter like the current SDK's `c`), they are: + // + // - Visible in the type signature. The Effect's R channel + // declares exactly which services are required. + // + // - Swappable via layers. Tests can provide an in-memory KV + // or a mock DB without changing the actor code. + const state = yield* Counter.State + // ^ SubscriptionRef<{ count: number }> + const events = yield* Counter.Events + // ^ { countChanged: PubSub } + const kv = yield* Counter.Kv + const db = yield* Counter.Db + + // Equivalent to current SDK's temporary variables + const connectionsTotal = yield* Ref.make(0) + + yield* Counter.onCreate( + ) + + yield* Effect.addFinalizer((exit) => + Exit.match(exit, { + onSuccess: () => + // Normal close = sleep + Effect.log("sleeping"), + onFailure: (cause) => + Cause.match(cause, { + onInterrupt: () => Effect.log("destroyed"), + onDie: (defect) => Effect.log("unexpected crash", defect), + }), + }) + ) + + + // Lifecycle hooks are just Effects that run at the right time. + // onConnect receives the connection — its scope finalizer IS onDisconnect. + yield* Counter.onConnect((conn) => + Effect.gen(function* () { + yield* PubSub.publish(events.userJoined, conn.params.userId) + + // Finalizer runs on disconnect (or sleep for non-hibernatable). + // This replaces onDisconnect — cleanup is co-located with setup. + yield* Effect.addFinalizer(() => + Effect.log(`${conn.params.userId} disconnected`) + ) + }) + ) + + // Return the action implementations. Counter.of + // type-checks each handler against its Action schema. + return Counter.of({ + Increment: ({ payload }) => + Effect.gen(function* () { + const next = yield* Ref.updateAndGet(state, (s) => ({ + count: s.count + payload.amount, + })) + if (next.count > 20) { + return yield* new CounterOverflowError({ limit: 20 }) + } + yield* PubSub.publish(events.countChanged, next.count) + return next.count + }), + + GetCount: () => + Ref.get(state).pipe(Effect.map((s) => s.count)), + }) + }), +) diff --git a/examples/effect/src/actors/mod.ts b/examples/effect/src/actors/mod.ts new file mode 100644 index 0000000000..8db603ffa2 --- /dev/null +++ b/examples/effect/src/actors/mod.ts @@ -0,0 +1,2 @@ +export * from "./counter/api.ts" +// export * from "./chat-room/api.ts" diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 783b6b13a8..78522c2467 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,7 +1,9 @@ import { Effect, Layer, Stream } from "effect" import { ActorTransport } from "@rivetkit/effect" -import { Counter } from "./actors/Counter.ts" -// import { ChatRoom } from "./actors/ChatRoom.ts" +import { + Counter, + // ChatRoom, +} from "./actors/mod.ts" // ------------------------------------------------------------------ // Counter.Client is a Context.Service generated by Actor.make. diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index f59877eefe..9f82172b1f 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -1,8 +1,8 @@ import { Layer } from "effect" import { NodeRuntime } from "@effect/platform-node" import { Registry, TestRegistry } from "@rivetkit/effect" -import { CounterLive } from "./actors/Counter.ts" -// import { ChatRoomLive } from "./actors/ChatRoom.ts" +import { CounterLive } from "./actors/counter/live.ts" +// import { ChatRoomLive } from "./actors/chat-room/live.ts" const ActorsLayer = Layer.mergeAll( CounterLive, From 6ee630c6045f1dc7c533fc57ffd32d291bfcbbb6 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 24 Apr 2026 18:33:42 +0200 Subject: [PATCH 018/306] refactor(effect): simplify finalizer to plain sleep log 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. --- examples/effect/src/actors/counter/live.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index bc7872dafe..71e0475c4a 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -33,22 +33,7 @@ export const CounterLive = Counter.toLayer( // Equivalent to current SDK's temporary variables const connectionsTotal = yield* Ref.make(0) - yield* Counter.onCreate( - ) - - yield* Effect.addFinalizer((exit) => - Exit.match(exit, { - onSuccess: () => - // Normal close = sleep - Effect.log("sleeping"), - onFailure: (cause) => - Cause.match(cause, { - onInterrupt: () => Effect.log("destroyed"), - onDie: (defect) => Effect.log("unexpected crash", defect), - }), - }) - ) - + yield* Effect.addFinalizer(() => Effect.log("sleeping")) // Lifecycle hooks are just Effects that run at the right time. // onConnect receives the connection — its scope finalizer IS onDisconnect. From 42f2040ff7f3c1ea393ca7183d7a0777341ffa78 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 26 Apr 2026 16:29:59 +0200 Subject: [PATCH 019/306] refactor(effect): use shared Actor.Kv/Db tags instead of per-actor Counter.Kv/Db --- examples/effect/src/actors/counter/live.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index 71e0475c4a..09f50c41ab 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -1,4 +1,5 @@ import { Cause, Effect, Exit, Ref, PubSub } from "effect" +import { Actor } from "@rivetkit/effect" import { Counter, CounterOverflowError } from "./api.ts" // --- Actor Implementation --- @@ -27,8 +28,8 @@ export const CounterLive = Counter.toLayer( // ^ SubscriptionRef<{ count: number }> const events = yield* Counter.Events // ^ { countChanged: PubSub } - const kv = yield* Counter.Kv - const db = yield* Counter.Db + const kv = yield* Actor.Kv + const db = yield* Actor.Db // Equivalent to current SDK's temporary variables const connectionsTotal = yield* Ref.make(0) From 0f60509e6c0432b86d6aa2a2098c06e1d8e0b488 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 26 Apr 2026 16:30:12 +0200 Subject: [PATCH 020/306] docs(effect): update README to reflect split Counter actor files --- examples/effect/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/effect/README.md b/examples/effect/README.md index 5812dc7447..e3fea56259 100644 --- a/examples/effect/README.md +++ b/examples/effect/README.md @@ -13,7 +13,8 @@ This example demonstrates the proposed API design for `@rivetkit/effect`, an [Ef ## Files -- [`src/actors/Counter.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/effect/src/actors/Counter.ts) - Actor definition (public contract) and implementation (server-only Layer) +- [`src/actors/counter/api.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/effect/src/actors/counter/api.ts) - Actor definition (public contract) +- [`src/actors/counter/live.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/effect/src/actors/counter/live.ts) - Actor implementation (server-only Layer) - [`src/main.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/effect/src/main.ts) - Server entry point using `Registry.layer` - [`src/client.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/effect/src/client.ts) - Client usage with typed actor dependencies From a3751f5c86b391517893ecb1ea3c98d57fce4bc2 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 26 Apr 2026 19:18:57 +0200 Subject: [PATCH 021/306] feat(effect): add durable message queue processing to Counter actor Add Message.make definitions (Reset, IncrementBy) alongside existing actions, and implement pull-based queue processing via Queue.take with Match.exhaustive pattern matching in a forked scoped fiber. --- examples/effect/src/actors/counter/api.ts | 25 ++++++++++++---- examples/effect/src/actors/counter/live.ts | 35 +++++++++++++++++++--- examples/effect/src/client.ts | 5 +++- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts index 0b046af615..9bb6b9aa0b 100644 --- a/examples/effect/src/actors/counter/api.ts +++ b/examples/effect/src/actors/counter/api.ts @@ -1,5 +1,5 @@ import { Schema } from "effect" -import { Actor, Action } from "@rivetkit/effect" +import { Actor, Action, Message } from "@rivetkit/effect" // --- Errors --- @@ -41,16 +41,29 @@ export const GetCount = Action.make("GetCount", { success: Schema.Number, }) +// --- Messages --- + +// Non-completable (fire-and-forget) +export const Reset = Message.make("Reset", { + payload: { reason: Schema.String }, +}) + +// Completable (sender can await a typed response) +export const IncrementBy = Message.make("IncrementBy", { + payload: Schema.Struct({ amount: Schema.Number }), + success: Schema.Number, +}) + // --- Actor Definition --- -// The definition is the actor's public contract: its name, -// state shape, event schemas, and action set. It carries no -// implementation, just types. Both server and client code -// import this; the implementation stays server-only. +// The definition is the actor's public contract. It carries no +// implementation. Both server and client code import this; +// the implementation stays server-only. export const Counter = Actor.make("Counter", { state: Schema.Struct({ count: Schema.Number }), + actions: [Increment, GetCount], // synchronous request-response + messages: [Reset, IncrementBy], // durable, queued, background events: { countChanged: Schema.Number }, - actions: [Increment, GetCount], options: { name: "Counter", // Human-friendly display name icon: "comments", // FontAwesome icon name diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index 09f50c41ab..9ca4bda809 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -1,4 +1,4 @@ -import { Cause, Effect, Exit, Ref, PubSub } from "effect" +import { Effect, Queue, Ref, PubSub, Match } from "effect" import { Actor } from "@rivetkit/effect" import { Counter, CounterOverflowError } from "./api.ts" @@ -28,10 +28,12 @@ export const CounterLive = Counter.toLayer( // ^ SubscriptionRef<{ count: number }> const events = yield* Counter.Events // ^ { countChanged: PubSub } + const messages = yield* Counter.Messages + // ^ MessageQueue const kv = yield* Actor.Kv const db = yield* Actor.Db - // Equivalent to current SDK's temporary variables + // Ephemeral variable — reset on each wake, not persisted. const connectionsTotal = yield* Ref.make(0) yield* Effect.addFinalizer(() => Effect.log("sleeping")) @@ -50,8 +52,33 @@ export const CounterLive = Counter.toLayer( }) ) - // Return the action implementations. Counter.of - // type-checks each handler against its Action schema. + // --- Message processing (durable queue) --- + // Pull-based: the actor controls when to take the next message. + // Forked into a scoped fiber, so it runs in the background and + // is canceled on sleep. + yield* Effect.gen(function* () { + const msg = yield* Queue.take(messages) + yield* Match.value(msg).pipe( + Match.tag("Reset", () => + Effect.gen(function* () { + yield* Ref.set(state, { count: 0 }) + yield* PubSub.publish(events.countChanged, 0) + }) + ), + Match.tag("IncrementBy", ({ payload, complete }) => + Effect.gen(function* () { + const next = yield* Ref.updateAndGet(state, (s) => ({ + count: s.count + payload.amount, + })) + yield* PubSub.publish(events.countChanged, next.count) + yield* complete(next.count) + }) + ), + Match.exhaustive, + ) + }).pipe(Effect.forever, Effect.forkScoped) + + // --- Action handlers (request-response) --- return Counter.of({ Increment: ({ payload }) => Effect.gen(function* () { diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 78522c2467..1507a70b05 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,7 +1,7 @@ import { Effect, Layer, Stream } from "effect" import { ActorTransport } from "@rivetkit/effect" import { - Counter, + Counter, IncrementBy, // ChatRoom, } from "./actors/mod.ts" @@ -23,6 +23,9 @@ const program = Effect.gen(function* () { const count = yield* counter.Increment({ amount: 5 }) yield* Effect.log(`Count: ${count}`) + const newCount = yield* counter.send(IncrementBy({ amount: 3 })) + yield* Effect.log(`Count: ${newCount}`) + // subscribe returns a Stream typed from the event schema. yield* counter.subscribe("countChanged").pipe( Stream.take(3), From 7dbc60822c5899d96192d6ed76d6afe4ed40ed0a Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 26 Apr 2026 19:20:16 +0200 Subject: [PATCH 022/306] style(effect): fix indentation to tabs in client.ts and main.ts --- examples/effect/src/client.ts | 44 +++++++++++++++++------------------ examples/effect/src/main.ts | 4 ++-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 1507a70b05..fba1f1052f 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -13,24 +13,24 @@ import { // each piece of code depends on. // ------------------------------------------------------------------ const program = Effect.gen(function* () { - const counterClient = yield* Counter.Client - // R now includes Counter.Client + const counterClient = yield* Counter.Client + // R now includes Counter.Client - const counter = counterClient.getOrCreate(["counter-123"]) + const counter = counterClient.getOrCreate(["counter-123"]) - // Action calls return Effects with types inferred from the schema. - // counter.Increment: (payload: { amount: number }) => Effect - const count = yield* counter.Increment({ amount: 5 }) - yield* Effect.log(`Count: ${count}`) + // Action calls return Effects with types inferred from the schema. + // counter.Increment: (payload: { amount: number }) => Effect + const count = yield* counter.Increment({ amount: 5 }) + yield* Effect.log(`Count: ${count}`) - const newCount = yield* counter.send(IncrementBy({ amount: 3 })) - yield* Effect.log(`Count: ${newCount}`) + const newCount = yield* counter.send(IncrementBy({ amount: 3 })) + yield* Effect.log(`Count: ${newCount}`) - // subscribe returns a Stream typed from the event schema. - yield* counter.subscribe("countChanged").pipe( - Stream.take(3), - Stream.runForEach((n) => Effect.log(`Changed: ${n}`)), - ) + // subscribe returns a Stream typed from the event schema. + yield* counter.subscribe("countChanged").pipe( + Stream.take(3), + Stream.runForEach((n) => Effect.log(`Changed: ${n}`)), + ) }) // program: Effect // ^^^^^^^^^^^^^^ @@ -51,14 +51,14 @@ const ActorClientLayer = Layer.mergeAll( Counter.clientLayer, // ChatRoom.clientLayer, ).pipe( - // Both client layers share the same transport here, but you - // could provide different transports to each. - Layer.provide( - ActorTransport.layer({ - endpoint: "https://api.rivet.dev", - token: "...", - }), - ), + // Both client layers share the same transport here, but you + // could provide different transports to each. + Layer.provide( + ActorTransport.layer({ + endpoint: "https://api.rivet.dev", + token: "...", + }), + ), ) program.pipe(Effect.provide(ActorClientLayer), Effect.runPromise) diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index 9f82172b1f..4a94602cd7 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -10,11 +10,11 @@ const ActorsLayer = Layer.mergeAll( ) const MainLayer = ActorsLayer.pipe( - Layer.provide(Registry.layer({ storagePath: "./data" })), + Layer.provide(Registry.layer({ storagePath: "./data" })), ) const TestLayer = ActorsLayer.pipe( - Layer.provide(TestRegistry.layer), + Layer.provide(TestRegistry.layer), ) // Keeps the layer alive. Tears down on SIGINT/SIGTERM. From 9cc41bc5826a8c6dca8ce0760531d20cb5e4eda5 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 10:33:25 +0200 Subject: [PATCH 023/306] fix(effect): use Schema.Struct for Reset message payload Consistent with all other payload definitions (Increment, IncrementBy). --- examples/effect/src/actors/counter/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts index 9bb6b9aa0b..95de82cdaf 100644 --- a/examples/effect/src/actors/counter/api.ts +++ b/examples/effect/src/actors/counter/api.ts @@ -45,7 +45,7 @@ export const GetCount = Action.make("GetCount", { // Non-completable (fire-and-forget) export const Reset = Message.make("Reset", { - payload: { reason: Schema.String }, + payload: Schema.Struct({ reason: Schema.String }), }) // Completable (sender can await a typed response) From 636f3338de0280cb16f622c13b13173672fdaa41 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 10:34:39 +0200 Subject: [PATCH 024/306] refactor(effect): remove onConnect hook referencing undeclared event --- examples/effect/src/actors/counter/live.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index 9ca4bda809..5350dc31f8 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -38,20 +38,6 @@ export const CounterLive = Counter.toLayer( yield* Effect.addFinalizer(() => Effect.log("sleeping")) - // Lifecycle hooks are just Effects that run at the right time. - // onConnect receives the connection — its scope finalizer IS onDisconnect. - yield* Counter.onConnect((conn) => - Effect.gen(function* () { - yield* PubSub.publish(events.userJoined, conn.params.userId) - - // Finalizer runs on disconnect (or sleep for non-hibernatable). - // This replaces onDisconnect — cleanup is co-located with setup. - yield* Effect.addFinalizer(() => - Effect.log(`${conn.params.userId} disconnected`) - ) - }) - ) - // --- Message processing (durable queue) --- // Pull-based: the actor controls when to take the next message. // Forked into a scoped fiber, so it runs in the background and From 4e0fc761bc5a1b62912445bd368bbc29667c643e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 10:37:51 +0200 Subject: [PATCH 025/306] feat(effect): use withConstructorDefault for state initialization Allows the SDK to call Counter.state.make({}) to get { count: 0 } on first creation, while still validating the full shape on decode. --- examples/effect/src/actors/counter/api.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts index 95de82cdaf..47500468c2 100644 --- a/examples/effect/src/actors/counter/api.ts +++ b/examples/effect/src/actors/counter/api.ts @@ -1,4 +1,4 @@ -import { Schema } from "effect" +import { Effect, Schema } from "effect" import { Actor, Action, Message } from "@rivetkit/effect" // --- Errors --- @@ -60,7 +60,11 @@ export const IncrementBy = Message.make("IncrementBy", { // implementation. Both server and client code import this; // the implementation stays server-only. export const Counter = Actor.make("Counter", { - state: Schema.Struct({ count: Schema.Number }), + state: Schema.Struct({ + count: Schema.Number.pipe( + Schema.withConstructorDefault(Effect.succeed(0)), + ), + }), actions: [Increment, GetCount], // synchronous request-response messages: [Reset, IncrementBy], // durable, queued, background events: { countChanged: Schema.Number }, From aa63fee0708825915c964189b4533b0bdd4f84be Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 10:54:04 +0200 Subject: [PATCH 026/306] feat(effect): add queue size limits to Counter actor options --- examples/effect/src/actors/counter/api.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts index 47500468c2..5e056727d4 100644 --- a/examples/effect/src/actors/counter/api.ts +++ b/examples/effect/src/actors/counter/api.ts @@ -69,7 +69,9 @@ export const Counter = Actor.make("Counter", { messages: [Reset, IncrementBy], // durable, queued, background events: { countChanged: Schema.Number }, options: { - name: "Counter", // Human-friendly display name - icon: "comments", // FontAwesome icon name + name: "Counter", // Human-friendly display name + icon: "comments", // FontAwesome icon name + maxQueueSize: 1000, // Max number of pending messages + maxQueueMessageSize: 64 * 1024, // Max bytes per message }, }) From 55a9cd1ca68999c24d24fc1965a148c4e8b75f08 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 11:22:41 +0200 Subject: [PATCH 027/306] feat(effect): use PersistedSubscriptionRef for actor state --- examples/effect/src/actors/counter/live.ts | 24 ++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index 5350dc31f8..214efce288 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -1,5 +1,5 @@ import { Effect, Queue, Ref, PubSub, Match } from "effect" -import { Actor } from "@rivetkit/effect" +import { Actor, PersistedSubscriptionRef } from "@rivetkit/effect" import { Counter, CounterOverflowError } from "./api.ts" // --- Actor Implementation --- @@ -24,8 +24,17 @@ export const CounterLive = Counter.toLayer( // // - Swappable via layers. Tests can provide an in-memory KV // or a mock DB without changing the actor code. + + // PersistedSubscriptionRef extends SubscriptionRef with + // throttled durable persistence. Standard SubscriptionRef + // combinators (get, set, update, modify, changes) work as-is. + // Every published change schedules a save via the configured + // stateSaveInterval; the wake-scope finalizer flushes pending + // writes before sleep so state is durable on teardown. Use + // PersistedSubscriptionRef.sync / updateAndSync when an action + // must wait for durability before responding. const state = yield* Counter.State - // ^ SubscriptionRef<{ count: number }> + // ^ PersistedSubscriptionRef<{ count: number }> const events = yield* Counter.Events // ^ { countChanged: PubSub } const messages = yield* Counter.Messages @@ -47,13 +56,13 @@ export const CounterLive = Counter.toLayer( yield* Match.value(msg).pipe( Match.tag("Reset", () => Effect.gen(function* () { - yield* Ref.set(state, { count: 0 }) + yield* PersistedSubscriptionRef.set(state, { count: 0 }) yield* PubSub.publish(events.countChanged, 0) }) ), Match.tag("IncrementBy", ({ payload, complete }) => Effect.gen(function* () { - const next = yield* Ref.updateAndGet(state, (s) => ({ + const next = yield* PersistedSubscriptionRef.updateAndGet(state, (s) => ({ count: s.count + payload.amount, })) yield* PubSub.publish(events.countChanged, next.count) @@ -68,7 +77,10 @@ export const CounterLive = Counter.toLayer( return Counter.of({ Increment: ({ payload }) => Effect.gen(function* () { - const next = yield* Ref.updateAndGet(state, (s) => ({ + // Throttled save: the change is published on + // state.changes, the framework debounces by + // stateSaveInterval and writes to durable KV. + const next = yield* PersistedSubscriptionRef.updateAndGet(state, (s) => ({ count: s.count + payload.amount, })) if (next.count > 20) { @@ -79,7 +91,7 @@ export const CounterLive = Counter.toLayer( }), GetCount: () => - Ref.get(state).pipe(Effect.map((s) => s.count)), + PersistedSubscriptionRef.get(state).pipe(Effect.map((s) => s.count)), }) }), ) From aa6daeb1ab99dbd0059ad2209eaba82157205c4e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 11:29:52 +0200 Subject: [PATCH 028/306] feat(effect): scaffold @rivetkit/effect package Add package config for the Effect SDK: package.json with effect@4.0.0-beta.57 peer dep, tsup/tsconfig/turbo configs, and workspace resolution. --- examples/effect/package.json | 4 +- package.json | 1 + pnpm-lock.yaml | 551 +++++++++++++----- .../packages/effect/package.json | 42 ++ .../packages/effect/tsconfig.json | 20 + .../packages/effect/tsup.config.ts | 4 + .../packages/effect/turbo.json | 4 + 7 files changed, 488 insertions(+), 138 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/package.json create mode 100644 rivetkit-typescript/packages/effect/tsconfig.json create mode 100644 rivetkit-typescript/packages/effect/tsup.config.ts create mode 100644 rivetkit-typescript/packages/effect/turbo.json diff --git a/examples/effect/package.json b/examples/effect/package.json index b25ec3ffb2..34906415e7 100644 --- a/examples/effect/package.json +++ b/examples/effect/package.json @@ -11,8 +11,8 @@ "dependencies": { "rivetkit": "*", "@rivetkit/effect": "*", - "effect": "^4.0.0", - "@effect/platform-node": "^4.0.0" + "effect": "4.0.0-beta.57", + "@effect/platform-node": "4.0.0-beta.57" }, "devDependencies": { "@types/node": "^22.13.9", diff --git a/package.json b/package.json index 67a01e495f..479279111b 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@rivetkit/engine-api-full": "workspace:*", "@rivetkit/rivetkit-napi": "workspace:*", "@rivetkit/engine-cli": "workspace:*", + "@rivetkit/effect": "workspace:*", "@types/react": "^19", "@types/react-dom": "^19" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24f6def67b..c23d94acda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,7 @@ overrides: '@rivetkit/engine-api-full': workspace:* '@rivetkit/rivetkit-napi': workspace:* '@rivetkit/engine-cli': workspace:* + '@rivetkit/effect': workspace:* '@types/react': ^19 '@types/react-dom': ^19 react: 19.1.0 @@ -57,7 +58,7 @@ importers: version: 7.7.4 tsup: specifier: ^8.5.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) turbo: specifier: ^2.5.6 version: 2.5.6 @@ -125,7 +126,7 @@ importers: version: 20.19.13 tsup: specifier: ^8.5.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@20.19.13))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@20.19.13))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.9.2 version: 5.9.3 @@ -162,7 +163,7 @@ importers: version: 5.0.1 tsup: specifier: ^8.5.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) tsx: specifier: ^4.20.5 version: 4.21.0 @@ -291,7 +292,7 @@ importers: version: 0.0.260331072558 '@rivet-dev/agent-os-pi': specifier: ^0.1.1 - version: 0.1.1(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.19.0)(zod@3.25.76) + version: 0.1.1(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) rivetkit: specifier: workspace:* version: link:../../rivetkit-typescript/packages/rivetkit @@ -429,7 +430,7 @@ importers: version: 9.6.1 freestyle-sandboxes: specifier: ^0.0.95 - version: 0.0.95(expo-constants@18.0.13)(expo-linking@7.0.5)(expo@54.0.18)(react-dom@19.1.0(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native-screens@4.17.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(ws@8.19.0) + version: 0.0.95(expo-constants@18.0.13)(expo-linking@7.0.5)(expo@54.0.18)(react-dom@19.1.0(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native-screens@4.17.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(ws@8.20.0) react: specifier: 19.1.0 version: 19.1.0 @@ -1145,6 +1146,31 @@ importers: specifier: ^3.1.1 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.15)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) + examples/effect: + dependencies: + '@effect/platform-node': + specifier: 4.0.0-beta.57 + version: 4.0.0-beta.57(effect@4.0.0-beta.57)(ioredis@5.10.1) + '@rivetkit/effect': + specifier: workspace:* + version: link:../../rivetkit-typescript/packages/effect + effect: + specifier: 4.0.0-beta.57 + version: 4.0.0-beta.57 + rivetkit: + specifier: workspace:* + version: link:../../rivetkit-typescript/packages/rivetkit + devDependencies: + '@types/node': + specifier: ^22.13.9 + version: 22.19.15 + tsx: + specifier: ^3.12.7 + version: 3.14.0 + typescript: + specifier: ^5.5.2 + version: 5.9.3 + examples/elysia: dependencies: elysia: @@ -2411,7 +2437,7 @@ importers: version: 19.2.3(@types/react@19.2.13) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) concurrently: specifier: ^9.1.2 version: 9.2.1 @@ -2423,7 +2449,7 @@ importers: version: 5.9.3 vite: specifier: ^6.0.5 - version: 6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^3.1.1 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@22.19.10)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) @@ -2469,7 +2495,7 @@ importers: version: 8.18.1 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) concurrently: specifier: ^9.1.2 version: 9.2.1 @@ -2481,7 +2507,7 @@ importers: version: 5.9.3 vite: specifier: ^6.0.5 - version: 6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^3.1.1 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@22.19.10)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) @@ -2527,7 +2553,7 @@ importers: version: 8.18.1 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) tsx: specifier: ^4.19.2 version: 4.21.0 @@ -2536,7 +2562,7 @@ importers: version: 5.9.3 vite: specifier: ^6.0.5 - version: 6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^3.1.1 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@22.19.10)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) @@ -2576,7 +2602,7 @@ importers: version: 19.2.3(@types/react@19.2.13) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) tsx: specifier: ^4.19.2 version: 4.21.0 @@ -2585,7 +2611,7 @@ importers: version: 5.9.3 vite: specifier: ^6.0.5 - version: 6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^3.1.1 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@22.19.10)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) @@ -3341,7 +3367,7 @@ importers: version: 5.2.2(react-hook-form@7.62.0(react@19.1.0)) '@ladle/react': specifier: ^5.1.1 - version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) '@marsidev/react-turnstile': specifier: ^1.5.0 version: 1.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -3443,10 +3469,10 @@ importers: version: 5.1.8(react@19.1.0)(typescript@5.9.3) '@tailwindcss/container-queries': specifier: ^0.1.1 - version: 0.1.1(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)) + version: 0.1.1(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3)) '@tailwindcss/typography': specifier: ^0.5.16 - version: 0.5.19(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)) + version: 0.5.19(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3)) '@tanstack/history': specifier: ^1.133.28 version: 1.133.28 @@ -3575,7 +3601,7 @@ importers: version: 12.10.0(@types/react@19.2.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) actor-core: specifier: ^0.6.3 - version: 0.6.3(eventsource@3.0.7)(ws@8.19.0) + version: 0.6.3(eventsource@3.0.7)(ws@8.20.0) autoprefixer: specifier: ^10.4.21 version: 10.4.22(postcss@8.5.6) @@ -3584,7 +3610,7 @@ importers: version: 2.4.3 better-auth: specifier: ^1.5.6 - version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) canvas-confetti: specifier: ^1.9.3 version: 1.9.3 @@ -3689,10 +3715,10 @@ importers: version: 2.6.0 tailwindcss: specifier: ^3.4.17 - version: 3.4.18(tsx@4.21.0)(yaml@2.8.2) + version: 3.4.18(tsx@4.21.0)(yaml@2.8.3) tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)) + version: 1.0.7(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3)) ts-pattern: specifier: ^5.8.0 version: 5.8.0 @@ -3704,7 +3730,7 @@ importers: version: 5.2.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.13)(typescript@5.9.3))(typescript@5.9.3) unplugin-macros: specifier: ^0.18.3 - version: 0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.1.0) @@ -3723,7 +3749,7 @@ importers: devDependencies: vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) frontend/packages/components: dependencies: @@ -3837,10 +3863,10 @@ importers: version: 3.21.0 '@tailwindcss/container-queries': specifier: ^0.1.1 - version: 0.1.1(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)) + version: 0.1.1(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3)) '@tailwindcss/typography': specifier: ^0.5.19 - version: 0.5.19(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)) + version: 0.5.19(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3)) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -3924,7 +3950,7 @@ importers: version: 2.6.0 tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)) + version: 1.0.7(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3)) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.1.0) @@ -3955,7 +3981,7 @@ importers: version: 8.5.6 tailwindcss: specifier: ^3.4.17 - version: 3.4.18(tsx@4.21.0)(yaml@2.8.2) + version: 3.4.18(tsx@4.21.0)(yaml@2.8.3) vite: specifier: ^5.4.20 version: 5.4.21(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) @@ -4070,7 +4096,23 @@ importers: version: 14.2.5 tsup: specifier: ^8.4.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + typescript: + specifier: ^5.5.2 + version: 5.9.3 + + rivetkit-typescript/packages/effect: + dependencies: + rivetkit: + specifier: workspace:* + version: link:../rivetkit + devDependencies: + effect: + specifier: 4.0.0-beta.57 + version: 4.0.0-beta.57 + tsup: + specifier: ^8.4.0 + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.5.2 version: 5.9.3 @@ -4106,7 +4148,7 @@ importers: version: 5.0.1 tsup: specifier: ^8.5.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.15))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.15))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) tsx: specifier: ^4.20.5 version: 4.21.0 @@ -4128,7 +4170,7 @@ importers: version: 20.19.13 tsup: specifier: ^8.5.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@20.19.13))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@20.19.13))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.9.2 version: 5.9.3 @@ -4147,7 +4189,7 @@ importers: devDependencies: tsup: specifier: ^8.4.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.5.2 version: 5.9.3 @@ -4172,7 +4214,7 @@ importers: version: 22.19.10 tsup: specifier: ^8.4.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.7.3 version: 5.9.3 @@ -4209,7 +4251,7 @@ importers: version: 19.2.3(@types/react@19.2.13) tsup: specifier: ^8.4.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.15))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.15))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.5.2 version: 5.9.3 @@ -4240,7 +4282,7 @@ importers: version: 19.2.3(@types/react@19.2.13) tsup: specifier: ^8.4.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.5.2 version: 5.9.3 @@ -4340,7 +4382,7 @@ importers: version: 4.0.0 tsup: specifier: ^8.4.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) tsx: specifier: ^4.19.4 version: 4.21.0 @@ -4349,7 +4391,7 @@ importers: version: 5.9.3 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: ^3.1.1 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@22.19.10)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) @@ -4373,7 +4415,7 @@ importers: version: 22.19.10 tsup: specifier: ^8.5.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) rivetkit-typescript/packages/traces: dependencies: @@ -4401,7 +4443,7 @@ importers: version: 12.1.0 tsup: specifier: ^8.4.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) tsx: specifier: ^4.7.0 version: 4.21.0 @@ -4444,7 +4486,7 @@ importers: version: 12.1.0 tsup: specifier: ^8.4.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) tsx: specifier: ^4.7.0 version: 4.21.0 @@ -4518,7 +4560,7 @@ importers: version: 22.19.10 tsup: specifier: ^8.4.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.7.3 version: 5.9.3 @@ -4527,7 +4569,7 @@ importers: dependencies: '@astrojs/mdx': specifier: ^4.0.2 - version: 4.3.13(astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)) + version: 4.3.13(astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(ioredis@5.10.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)) '@astrojs/react': specifier: ^4.1.2 version: 4.4.2(@types/node@25.0.7)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) @@ -4536,7 +4578,7 @@ importers: version: 3.6.1 '@astrojs/tailwind': specifier: ^6.0.0 - version: 6.0.2(astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2))(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@25.0.7)(typescript@5.9.3)) + version: 6.0.2(astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(ioredis@5.10.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2))(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@25.0.7)(typescript@5.9.3)) '@fortawesome/fontawesome-svg-core': specifier: ^7.1.0 version: 7.1.0 @@ -4572,7 +4614,7 @@ importers: version: link:../frontend/packages/shared-data '@sentry/astro': specifier: ^10.42.0 - version: 10.42.0(astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(rollup@4.57.1) + version: 10.42.0(astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(ioredis@5.10.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(rollup@4.57.1) '@shikijs/transformers': specifier: ^3.15.0 version: 3.15.0 @@ -4599,7 +4641,7 @@ importers: version: 8.15.0 astro: specifier: ^5.1.1 - version: 5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(ioredis@5.10.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) autoprefixer: specifier: ^10.4.22 version: 10.4.22(postcss@8.5.6) @@ -5945,6 +5987,19 @@ packages: resolution: {tarball: https://pkg.pr.new/rivet-dev/durable-streams/@durable-streams/writer@0323b8bcf1c9b38f1014629e1a8b6c74cc662100} version: 0.0.0 + '@effect/platform-node-shared@4.0.0-beta.57': + resolution: {integrity: sha512-C976X6f+qHUtLSqcqImuCrjhAHnJV17NC2RvvybsAuDfkyIWU4MyiO2XwgiBeijeNupyr1M/KPKnyjtkNxV9Hw==} + engines: {node: '>=18.0.0'} + peerDependencies: + effect: ^4.0.0-beta.57 + + '@effect/platform-node@4.0.0-beta.57': + resolution: {integrity: sha512-la0xxPSAYOsY0d+uVxEBxok3jYB31iPQmIaZZRUj2SNWqcGGHJc6KorKtI8guqSLuv9FGZ255kBWXRbG6hMeeg==} + engines: {node: '>=18.0.0'} + peerDependencies: + effect: ^4.0.0-beta.57 + ioredis: ^5.7.0 + '@emnapi/runtime@1.7.1': resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} @@ -7320,6 +7375,9 @@ packages: '@types/node': optional: true + '@ioredis/commands@1.5.1': + resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -11573,6 +11631,10 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + cmdk@1.1.1: resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} peerDependencies: @@ -12135,6 +12197,10 @@ packages: delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -12462,6 +12528,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + effect@4.0.0-beta.57: + resolution: {integrity: sha512-rg32VgXnLKaPRs9tbRDaZ5jxmzNY7ojXt85gSHGUTwdlbWH5Ik+OCUY2q14TXliygPGoHwCAvNWS4bQJOqf00g==} + electron-to-chromium@1.5.286: resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} @@ -12877,6 +12946,10 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true + fast-check@4.7.0: + resolution: {integrity: sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==} + engines: {node: '>=12.17.0'} + fast-copy@3.0.2: resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} @@ -13023,6 +13096,9 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-my-way-ts@0.1.6: + resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -13635,6 +13711,10 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ini@6.0.0: + resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} + engines: {node: ^20.17.0 || >=22.9.0} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -13654,6 +13734,10 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ioredis@5.10.1: + resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} + engines: {node: '>=12.22.0'} + ip-address@10.1.0: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} @@ -14039,6 +14123,9 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + kubernetes-types@1.30.0: + resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} + kysely@0.28.15: resolution: {integrity: sha512-r2clcf7HLWvDXaVUEvQymXJY4i3bSOIV3xsL/Upy3ZfSv5HeKsk9tsqbBptLvth5qHEIhxeHTA2jNLyQABkLBA==} engines: {node: '>=20.0.0'} @@ -14252,10 +14339,16 @@ packages: lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. @@ -14757,6 +14850,11 @@ packages: engines: {node: '>=16'} hasBin: true + mime@4.1.0: + resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} + engines: {node: '>=16'} + hasBin: true + mimic-fn@1.2.0: resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} engines: {node: '>=4'} @@ -14880,6 +14978,9 @@ packages: resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} hasBin: true + msgpackr@1.11.10: + resolution: {integrity: sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==} + msgpackr@1.11.5: resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} @@ -14896,6 +14997,9 @@ packages: muggle-string@0.3.1: resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} + multipasta@0.2.7: + resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -15855,6 +15959,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@8.4.0: + resolution: {integrity: sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==} + pyodide@0.28.3: resolution: {integrity: sha512-rtCsyTU55oNGpLzSVuAd55ZvruJDEX8o6keSdWKN9jPeBVSNlynaKFG7eRqkiIgU7i2M6HEgYtm0atCEQX3u4A==} engines: {node: '>=18.0.0'} @@ -16172,6 +16279,14 @@ packages: reconnectingwebsocket@1.0.0: resolution: {integrity: sha512-r7H/dwkkfBu9x5eMGIt8td5WLqNbqy675x8Xg0+SoXaUS3xzniVlmfO7t7HSYmN/ZGzYjOKa9G2W4xCgCo7Zlg==} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + reduce-css-calc@1.3.0: resolution: {integrity: sha512-0dVfwYVOlf/LBA2ec4OwQ6p3X9mYxn/wOl2xTcLwjnPYrkgEfPx3VI4eGCH3rQLlPISG5v9I9bkZosKsNRTRKA==} @@ -16721,6 +16836,9 @@ packages: resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} engines: {node: '>=6'} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} @@ -17090,6 +17208,10 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} + toml@4.1.1: + resolution: {integrity: sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==} + engines: {node: '>=20'} + tough-cookie@6.0.0: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} @@ -17325,10 +17447,6 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@6.23.0: - resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} - engines: {node: '>=18.17'} - undici@6.24.1: resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==} engines: {node: '>=18.17'} @@ -17337,6 +17455,10 @@ packages: resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==} engines: {node: '>=20.18.1'} + undici@8.1.0: + resolution: {integrity: sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw==} + engines: {node: '>=22.19.0'} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -17584,6 +17706,10 @@ packages: resolution: {integrity: sha512-USe1zesMYh4fjCA8ZH5+X5WIVD0J4V1Jksm1bFTVBX2F/cwSXt0RO5w/3UXbdLKmZX65MiWV+hwhSS8p6oBTGA==} hasBin: true + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + uuid@7.0.3: resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} hasBin: true @@ -18112,6 +18238,18 @@ packages: utf-8-validate: optional: true + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + wsl-utils@0.1.0: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} @@ -18180,6 +18318,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -18522,12 +18665,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/mdx@4.3.13(astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))': + '@astrojs/mdx@4.3.13(astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(ioredis@5.10.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))': dependencies: '@astrojs/markdown-remark': 6.3.10 '@mdx-js/mdx': 3.1.1 acorn: 8.15.0 - astro: 5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + astro: 5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(ioredis@5.10.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) es-module-lexer: 1.7.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -18574,9 +18717,9 @@ snapshots: stream-replace-string: 2.0.0 zod: 3.25.76 - '@astrojs/tailwind@6.0.2(astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2))(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@25.0.7)(typescript@5.9.3))': + '@astrojs/tailwind@6.0.2(astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(ioredis@5.10.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2))(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@25.0.7)(typescript@5.9.3))': dependencies: - astro: 5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + astro: 5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(ioredis@5.10.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) autoprefixer: 10.4.22(postcss@8.5.6) postcss: 8.5.6 postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@25.0.7)(typescript@5.9.3)) @@ -19886,6 +20029,26 @@ snapshots: '@durable-streams/client': https://pkg.pr.new/rivet-dev/durable-streams/@durable-streams/client@0323b8bcf1c9b38f1014629e1a8b6c74cc662100 fastq: 1.20.1 + '@effect/platform-node-shared@4.0.0-beta.57(effect@4.0.0-beta.57)': + dependencies: + '@types/ws': 8.18.1 + effect: 4.0.0-beta.57 + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@effect/platform-node@4.0.0-beta.57(effect@4.0.0-beta.57)(ioredis@5.10.1)': + dependencies: + '@effect/platform-node-shared': 4.0.0-beta.57(effect@4.0.0-beta.57) + effect: 4.0.0-beta.57 + ioredis: 5.10.1 + mime: 4.1.0 + undici: 8.1.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 @@ -20657,7 +20820,7 @@ snapshots: abort-controller: 3.0.0 debug: 4.4.3 source-map-support: 0.5.21 - undici: 6.23.0 + undici: 6.24.1 transitivePeerDependencies: - supports-color @@ -21147,6 +21310,8 @@ snapshots: '@types/node': 22.19.15 optional: true + '@ioredis/commands@1.5.1': {} + '@isaacs/balanced-match@4.0.1': optional: true @@ -21277,7 +21442,7 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)': + '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': dependencies: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0 @@ -21289,8 +21454,8 @@ snapshots: '@ladle/react-context': 1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mdx-js/mdx': 3.1.1 '@mdx-js/react': 3.1.1(@types/react@19.2.13)(react@19.1.0) - '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) axe-core: 4.11.1 boxen: 8.0.1 chokidar: 4.0.3 @@ -21317,8 +21482,8 @@ snapshots: remark-gfm: 4.0.1 source-map: 0.7.6 vfile: 6.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - '@swc/helpers' - '@types/node' @@ -21430,9 +21595,9 @@ snapshots: std-env: 3.10.0 yoctocolors: 2.1.2 - '@mariozechner/pi-agent-core@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)': + '@mariozechner/pi-agent-core@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)': dependencies: - '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.19.0)(zod@3.25.76) + '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - aws-crt @@ -21454,7 +21619,7 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)': + '@mariozechner/pi-ai@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)': dependencies: '@anthropic-ai/sdk': 0.73.0(zod@3.25.76) '@aws-sdk/client-bedrock-runtime': 3.1024.0 @@ -21464,7 +21629,7 @@ snapshots: ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) chalk: 5.6.2 - openai: 6.26.0(ws@8.19.0)(zod@3.25.76) + openai: 6.26.0(ws@8.20.0)(zod@3.25.76) partial-json: 0.1.7 proxy-agent: 6.5.0 undici: 7.24.7 @@ -21502,11 +21667,11 @@ snapshots: - ws - zod - '@mariozechner/pi-coding-agent@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)': + '@mariozechner/pi-coding-agent@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)': dependencies: '@mariozechner/jiti': 2.6.5 - '@mariozechner/pi-agent-core': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.19.0)(zod@3.25.76) - '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.19.0)(zod@3.25.76) + '@mariozechner/pi-agent-core': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) + '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) '@mariozechner/pi-tui': 0.60.0 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 @@ -21826,7 +21991,7 @@ snapshots: react-simple-code-editor: 0.14.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) serve-handler: 6.1.6 tailwind-merge: 2.6.0 - tailwindcss-animate: 1.0.7(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)) + tailwindcss-animate: 1.0.7(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3)) zod: 3.25.76 transitivePeerDependencies: - '@cfworker/json-schema' @@ -23296,11 +23461,11 @@ snapshots: '@rivet-dev/agent-os-gzip@0.0.260331072558': {} - '@rivet-dev/agent-os-pi@0.1.1(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)': + '@rivet-dev/agent-os-pi@0.1.1(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)': dependencies: '@agentclientprotocol/sdk': 0.16.1(zod@3.25.76) - '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.19.0)(zod@3.25.76) - '@mariozechner/pi-coding-agent': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.19.0)(zod@3.25.76) + '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) + '@mariozechner/pi-coding-agent': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) '@rivet-dev/agent-os-core': 0.1.1(pyodide@0.28.3) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -23719,13 +23884,13 @@ snapshots: '@sentry-internal/browser-utils': 8.55.0 '@sentry/core': 8.55.0 - '@sentry/astro@10.42.0(astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(rollup@4.57.1)': + '@sentry/astro@10.42.0(astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(ioredis@5.10.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(rollup@4.57.1)': dependencies: '@sentry/browser': 10.42.0 '@sentry/core': 10.42.0 '@sentry/node': 10.42.0 '@sentry/vite-plugin': 5.1.1(rollup@4.57.1) - astro: 5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + astro: 5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(ioredis@5.10.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) transitivePeerDependencies: - encoding - rollup @@ -24451,9 +24616,9 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2))': + '@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3))': dependencies: - tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.2) + tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.3) '@tailwindcss/forms@0.5.10(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2))': dependencies: @@ -24534,6 +24699,11 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.2) + '@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.3) + '@tailwindcss/typography@0.5.19(tailwindcss@4.2.2)': dependencies: postcss-selector-parser: 6.0.10 @@ -25079,7 +25249,7 @@ snapshots: '@types/pg@8.16.0': dependencies: - '@types/node': 20.19.13 + '@types/node': 22.19.15 pg-protocol: 1.11.0 pg-types: 2.2.0 @@ -25378,11 +25548,11 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 - '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.27 '@swc/core': 1.15.11(@swc/helpers@0.5.17) - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@swc/helpers' @@ -25422,7 +25592,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -25430,11 +25600,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -25442,7 +25612,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -25524,14 +25694,14 @@ snapshots: msw: 2.12.10(@types/node@22.19.15)(typescript@5.9.3) vite: 5.4.21(@types/node@22.19.15)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) - '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.10(@types/node@20.19.13)(typescript@5.9.3) - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@2.1.9': dependencies: @@ -25834,7 +26004,7 @@ snapshots: acorn@8.16.0: {} - actor-core@0.6.3(eventsource@3.0.7)(ws@8.19.0): + actor-core@0.6.3(eventsource@3.0.7)(ws@8.20.0): dependencies: cbor-x: 1.6.0 hono: 4.11.9 @@ -25843,7 +26013,7 @@ snapshots: zod: 3.25.76 optionalDependencies: eventsource: 3.0.7 - ws: 8.19.0 + ws: 8.20.0 agent-base@6.0.2: dependencies: @@ -26045,7 +26215,7 @@ snapshots: astring@1.9.0: {} - astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(ioredis@5.10.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): dependencies: '@astrojs/compiler': 2.13.0 '@astrojs/internal-helpers': 0.7.5 @@ -26100,7 +26270,7 @@ snapshots: ultrahtml: 1.6.0 unifont: 0.7.1 unist-util-visit: 5.0.0 - unstorage: 1.17.3(idb-keyval@6.2.1) + unstorage: 1.17.3(idb-keyval@6.2.1)(ioredis@5.10.1) vfile: 6.0.3 vite: 6.4.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) vitefu: 1.1.1(vite@6.4.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) @@ -26346,7 +26516,7 @@ snapshots: bcryptjs@2.4.3: {} - better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0) '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0)) @@ -26373,7 +26543,7 @@ snapshots: pg: 8.17.2 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -26869,6 +27039,8 @@ snapshots: clsx@2.1.1: {} + cluster-key-slot@1.1.2: {} + cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.1.0) @@ -27449,6 +27621,8 @@ snapshots: delegates@1.0.0: {} + denque@2.1.0: {} + depd@1.1.2: {} depd@2.0.0: {} @@ -27602,6 +27776,19 @@ snapshots: ee-first@1.1.1: {} + effect@4.0.0-beta.57: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 4.7.0 + find-my-way-ts: 0.1.6 + ini: 6.0.0 + kubernetes-types: 1.30.0 + msgpackr: 1.11.10 + multipasta: 0.2.7 + toml: 4.1.1 + uuid: 13.0.0 + yaml: 2.8.3 + electron-to-chromium@1.5.286: {} elliptic@6.6.1: @@ -28239,6 +28426,10 @@ snapshots: transitivePeerDependencies: - supports-color + fast-check@4.7.0: + dependencies: + pure-rand: 8.4.0 + fast-copy@3.0.2: {} fast-decode-uri-component@1.0.1: {} @@ -28381,6 +28572,8 @@ snapshots: transitivePeerDependencies: - supports-color + find-my-way-ts@0.1.6: {} + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -28476,14 +28669,14 @@ snapshots: freeport-async@2.0.0: {} - freestyle-sandboxes@0.0.66(expo-constants@18.0.13)(expo-linking@7.0.5)(expo@54.0.18)(react-dom@19.1.0(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native-screens@4.17.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(ws@8.19.0): + freestyle-sandboxes@0.0.66(expo-constants@18.0.13)(expo-linking@7.0.5)(expo@54.0.18)(react-dom@19.1.0(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native-screens@4.17.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(ws@8.20.0): dependencies: '@hey-api/client-fetch': 0.5.7 '@tanstack/react-query': 5.87.1(react@19.1.0) expo-router: 4.0.21(expo-constants@18.0.13)(expo-linking@7.0.5)(expo@54.0.18)(react-dom@19.1.0(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native-screens@4.17.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0) glob: 11.1.0 hono: 4.11.9 - openai: 4.104.0(ws@8.19.0)(zod@3.25.76) + openai: 4.104.0(ws@8.20.0)(zod@3.25.76) openapi: 1.0.1 react: 19.1.0 zod: 3.25.76 @@ -28503,16 +28696,16 @@ snapshots: - supports-color - ws - freestyle-sandboxes@0.0.95(expo-constants@18.0.13)(expo-linking@7.0.5)(expo@54.0.18)(react-dom@19.1.0(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native-screens@4.17.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(ws@8.19.0): + freestyle-sandboxes@0.0.95(expo-constants@18.0.13)(expo-linking@7.0.5)(expo@54.0.18)(react-dom@19.1.0(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native-screens@4.17.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(ws@8.20.0): dependencies: '@hey-api/client-fetch': 0.5.7 '@tanstack/react-query': 5.87.1(react@19.1.0) '@types/react': 19.2.13 expo-router: 4.0.21(expo-constants@18.0.13)(expo-linking@7.0.5)(expo@54.0.18)(react-dom@19.1.0(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native-screens@4.17.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0) - freestyle-sandboxes: 0.0.66(expo-constants@18.0.13)(expo-linking@7.0.5)(expo@54.0.18)(react-dom@19.1.0(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native-screens@4.17.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(ws@8.19.0) + freestyle-sandboxes: 0.0.66(expo-constants@18.0.13)(expo-linking@7.0.5)(expo@54.0.18)(react-dom@19.1.0(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native-screens@4.17.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(ws@8.20.0) glob: 11.1.0 hono: 4.11.9 - openai: 4.104.0(ws@8.19.0)(zod@3.25.76) + openai: 4.104.0(ws@8.20.0)(zod@3.25.76) openapi: 1.0.1 react: 19.1.0 zod: 3.25.76 @@ -29178,6 +29371,8 @@ snapshots: ini@1.3.8: {} + ini@6.0.0: {} + inline-style-parser@0.2.7: {} input-otp@1.4.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): @@ -29193,6 +29388,20 @@ snapshots: dependencies: loose-envify: 1.4.0 + ioredis@5.10.1: + dependencies: + '@ioredis/commands': 1.5.1 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ip-address@10.1.0: {} ipaddr.js@1.9.1: {} @@ -29585,6 +29794,8 @@ snapshots: kolorist@1.8.0: {} + kubernetes-types@1.30.0: {} + kysely@0.28.15: {} lan-network@0.1.7: {} @@ -29777,8 +29988,12 @@ snapshots: lodash.debounce@4.0.8: {} + lodash.defaults@4.2.0: {} + lodash.get@4.4.2: {} + lodash.isarguments@3.1.0: {} + lodash.isequal@4.5.0: {} lodash.merge@4.6.2: {} @@ -30763,6 +30978,8 @@ snapshots: mime@4.0.7: {} + mime@4.1.0: {} + mimic-fn@1.2.0: {} mimic-fn@2.1.0: {} @@ -30872,6 +31089,10 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 optional: true + msgpackr@1.11.10: + optionalDependencies: + msgpackr-extract: 3.0.3 + msgpackr@1.11.5: optionalDependencies: msgpackr-extract: 3.0.3 @@ -30955,6 +31176,8 @@ snapshots: muggle-string@0.3.1: {} + multipasta@0.2.7: {} + mute-stream@2.0.0: {} mz@2.7.0: @@ -31243,7 +31466,7 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 - openai@4.104.0(ws@8.19.0)(zod@3.25.76): + openai@4.104.0(ws@8.20.0)(zod@3.25.76): dependencies: '@types/node': 18.19.130 '@types/node-fetch': 2.6.11 @@ -31253,21 +31476,21 @@ snapshots: formdata-node: 4.4.1 node-fetch: 2.7.0 optionalDependencies: - ws: 8.19.0 + ws: 8.20.0 zod: 3.25.76 transitivePeerDependencies: - encoding - openai@6.26.0(ws@8.19.0)(zod@3.25.76): - optionalDependencies: - ws: 8.19.0 - zod: 3.25.76 - openai@6.26.0(ws@8.19.0)(zod@4.1.13): optionalDependencies: ws: 8.19.0 zod: 4.1.13 + openai@6.26.0(ws@8.20.0)(zod@3.25.76): + optionalDependencies: + ws: 8.20.0 + zod: 3.25.76 + openapi-types@12.1.3: {} openapi3-ts@4.5.0: @@ -31683,14 +31906,23 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + tsx: 4.21.0 + yaml: 2.8.3 + + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.3): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 postcss: 8.5.6 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.8.3 postcss-modules-extract-imports@3.1.0(postcss@8.5.6): dependencies: @@ -31911,6 +32143,8 @@ snapshots: punycode@2.3.1: {} + pure-rand@8.4.0: {} + pyodide@0.28.3: dependencies: ws: 8.19.0 @@ -32305,6 +32539,12 @@ snapshots: reconnectingwebsocket@1.0.0: {} + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + reduce-css-calc@1.3.0: dependencies: balanced-match: 0.4.2 @@ -33087,6 +33327,8 @@ snapshots: dependencies: type-fest: 0.7.1 + standard-as-callback@2.1.0: {} + state-local@1.0.7: {} statuses@1.5.0: {} @@ -33283,9 +33525,9 @@ snapshots: tailwind-merge@2.6.0: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)): + tailwindcss-animate@1.0.7(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3)): dependencies: - tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.2) + tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.3) tailwindcss-animate@1.0.7(tailwindcss@4.2.2): dependencies: @@ -33319,6 +33561,34 @@ snapshots: - tsx - yaml + tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.3) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + tailwindcss@4.2.2: {} tapable@2.3.0: {} @@ -33463,6 +33733,8 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + toml@4.1.1: {} + tough-cookie@6.0.0: dependencies: tldts: 7.0.23 @@ -33565,7 +33837,7 @@ snapshots: tsscmp@1.0.6: {} - tsup@8.5.1(@microsoft/api-extractor@7.53.2(@types/node@20.19.13))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + tsup@8.5.1(@microsoft/api-extractor@7.53.2(@types/node@20.19.13))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) cac: 6.7.14 @@ -33576,7 +33848,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.3) resolve-from: 5.0.0 rollup: 4.57.1 source-map: 0.7.6 @@ -33595,7 +33867,7 @@ snapshots: - tsx - yaml - tsup@8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + tsup@8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) cac: 6.7.14 @@ -33606,7 +33878,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.3) resolve-from: 5.0.0 rollup: 4.57.1 source-map: 0.7.6 @@ -33625,7 +33897,7 @@ snapshots: - tsx - yaml - tsup@8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.15))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + tsup@8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.15))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) cac: 6.7.14 @@ -33636,7 +33908,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.3) resolve-from: 5.0.0 rollup: 4.57.1 source-map: 0.7.6 @@ -33655,7 +33927,7 @@ snapshots: - tsx - yaml - tsup@8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + tsup@8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) cac: 6.7.14 @@ -33666,7 +33938,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.3) resolve-from: 5.0.0 rollup: 4.57.1 source-map: 0.7.6 @@ -33826,12 +34098,12 @@ snapshots: undici-types@7.16.0: optional: true - undici@6.23.0: {} - undici@6.24.1: {} undici@7.24.7: {} + undici@8.1.0: {} + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: @@ -33935,13 +34207,13 @@ snapshots: unpipe@1.0.0: {} - unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: ast-kit: 2.2.0 magic-string-ast: 1.0.3 unplugin: 2.3.10 - vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - vite-node: 5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-node: 5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -33982,7 +34254,7 @@ snapshots: picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 - unstorage@1.17.3(idb-keyval@6.2.1): + unstorage@1.17.3(idb-keyval@6.2.1)(ioredis@5.10.1): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -33994,6 +34266,7 @@ snapshots: ufo: 1.6.1 optionalDependencies: idb-keyval: 6.2.1 + ioredis: 5.10.1 until-async@3.0.2: {} @@ -34076,6 +34349,8 @@ snapshots: uuid@12.0.0: {} + uuid@13.0.0: {} + uuid@7.0.3: {} v8-compile-cache-lib@3.0.1: {} @@ -34228,13 +34503,13 @@ snapshots: - supports-color - terser - vite-node@5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vite-node@5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: cac: 6.7.14 es-module-lexer: 1.7.0 obug: 2.0.0(ms@2.1.3) pathe: 2.0.3 - vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -34307,24 +34582,24 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - typescript @@ -34371,7 +34646,7 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 - vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -34382,16 +34657,16 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.6.1 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 stylus: 0.62.0 terser: 5.46.0 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.8.3 - vite@6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vite@6.4.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -34409,7 +34684,7 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.8.3 vite@6.4.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: @@ -34431,7 +34706,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -34442,16 +34717,16 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.6.1 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 stylus: 0.62.0 terser: 5.46.0 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.8.3 - vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -34469,7 +34744,7 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.8.3 optional: true vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): @@ -34716,10 +34991,10 @@ snapshots: - supports-color - terser - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -34736,7 +35011,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -34961,6 +35236,8 @@ snapshots: ws@8.19.0: {} + ws@8.20.0: {} + wsl-utils@0.1.0: dependencies: is-wsl: 3.1.0 @@ -35011,6 +35288,8 @@ snapshots: yaml@2.8.2: {} + yaml@2.8.3: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json new file mode 100644 index 0000000000..da9e4620b6 --- /dev/null +++ b/rivetkit-typescript/packages/effect/package.json @@ -0,0 +1,42 @@ +{ + "name": "@rivetkit/effect", + "version": "0.1.0", + "description": "Effect SDK for Rivet Actors", + "license": "Apache-2.0", + "type": "module", + "sideEffects": [ + "./dist/chunk-*.js", + "./dist/chunk-*.cjs" + ], + "files": [ + "dist", + "package.json" + ], + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "scripts": { + "build": "tsup src/index.ts", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "rivetkit": "workspace:*" + }, + "peerDependencies": { + "effect": ">=4.0.0-beta.0" + }, + "devDependencies": { + "effect": "4.0.0-beta.57", + "tsup": "^8.4.0", + "typescript": "^5.5.2" + } +} diff --git a/rivetkit-typescript/packages/effect/tsconfig.json b/rivetkit-typescript/packages/effect/tsconfig.json new file mode 100644 index 0000000000..08ecd34041 --- /dev/null +++ b/rivetkit-typescript/packages/effect/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/rivetkit-typescript/packages/effect/tsup.config.ts b/rivetkit-typescript/packages/effect/tsup.config.ts new file mode 100644 index 0000000000..f363b829fd --- /dev/null +++ b/rivetkit-typescript/packages/effect/tsup.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from "tsup"; +import defaultConfig from "../../../tsup.base.ts"; + +export default defineConfig(defaultConfig); diff --git a/rivetkit-typescript/packages/effect/turbo.json b/rivetkit-typescript/packages/effect/turbo.json new file mode 100644 index 0000000000..29d4cb2625 --- /dev/null +++ b/rivetkit-typescript/packages/effect/turbo.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"] +} From b4eca9e37e7e75f3dc54d923df1b7d77ba535499 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 11:41:32 +0200 Subject: [PATCH 029/306] chore(effect): remove version and stableVersion from example --- examples/effect/package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/effect/package.json b/examples/effect/package.json index 34906415e7..c94c9845a4 100644 --- a/examples/effect/package.json +++ b/examples/effect/package.json @@ -1,6 +1,5 @@ { "name": "effect", - "version": "2.0.21", "private": true, "type": "module", "scripts": { @@ -19,7 +18,6 @@ "tsx": "^3.12.7", "typescript": "^5.5.2" }, - "stableVersion": "0.8.0", "template": { "technologies": ["typescript"], "tags": [], From 011d1d8a56c3efd5e4a01eb16998fcb42f4bb66b Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 11:50:23 +0200 Subject: [PATCH 030/306] chore(effect): remove unused template metadata from example --- examples/effect/package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/effect/package.json b/examples/effect/package.json index c94c9845a4..281f69d473 100644 --- a/examples/effect/package.json +++ b/examples/effect/package.json @@ -19,8 +19,6 @@ "typescript": "^5.5.2" }, "template": { - "technologies": ["typescript"], - "tags": [], "noFrontend": true, "skipVercel": true }, From 464655850ca74d0e105201f8cda6c9924da9b735 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 11:56:58 +0200 Subject: [PATCH 031/306] chore(effect): run example with tsx directly instead of srvx --- examples/effect/package.json | 6 +-- pnpm-lock.yaml | 74 ++++++++++++++++++------------------ 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/examples/effect/package.json b/examples/effect/package.json index 281f69d473..aecb142693 100644 --- a/examples/effect/package.json +++ b/examples/effect/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "scripts": { - "dev": "npx srvx --import tsx src/main.ts", - "start": "npx srvx --import tsx src/main.ts", + "dev": "tsx watch src/main.ts", + "start": "tsx src/main.ts", "check-types": "tsc --noEmit" }, "dependencies": { @@ -15,7 +15,7 @@ }, "devDependencies": { "@types/node": "^22.13.9", - "tsx": "^3.12.7", + "tsx": "^4.20.5", "typescript": "^5.5.2" }, "template": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c23d94acda..855fbf671d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1165,8 +1165,8 @@ importers: specifier: ^22.13.9 version: 22.19.15 tsx: - specifier: ^3.12.7 - version: 3.14.0 + specifier: ^4.20.5 + version: 4.21.0 typescript: specifier: ^5.5.2 version: 5.9.3 @@ -3367,7 +3367,7 @@ importers: version: 5.2.2(react-hook-form@7.62.0(react@19.1.0)) '@ladle/react': specifier: ^5.1.1 - version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) '@marsidev/react-turnstile': specifier: ^1.5.0 version: 1.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -3610,7 +3610,7 @@ importers: version: 2.4.3 better-auth: specifier: ^1.5.6 - version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) canvas-confetti: specifier: ^1.9.3 version: 1.9.3 @@ -3730,7 +3730,7 @@ importers: version: 5.2.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.13)(typescript@5.9.3))(typescript@5.9.3) unplugin-macros: specifier: ^0.18.3 - version: 0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + version: 0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.1.0) @@ -3749,7 +3749,7 @@ importers: devDependencies: vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) frontend/packages/components: dependencies: @@ -20536,7 +20536,7 @@ snapshots: terminal-link: 2.1.1 undici: 6.24.1 wrap-ansi: 7.0.0 - ws: 8.19.0 + ws: 8.20.0 optionalDependencies: expo-router: 4.0.21(expo-constants@18.0.13)(expo-linking@7.0.5)(expo@54.0.18)(react-dom@19.1.0(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native-screens@4.17.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0) react-native: 0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0) @@ -20708,7 +20708,7 @@ snapshots: '@expo/mcp-tunnel@0.0.8(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))': dependencies: - ws: 8.19.0 + ws: 8.20.0 zod: 3.25.76 zod-to-json-schema: 3.25.1(zod@3.25.76) optionalDependencies: @@ -21442,7 +21442,7 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': + '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': dependencies: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0 @@ -21454,8 +21454,8 @@ snapshots: '@ladle/react-context': 1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mdx-js/mdx': 3.1.1 '@mdx-js/react': 3.1.1(@types/react@19.2.13)(react@19.1.0) - '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) - '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) axe-core: 4.11.1 boxen: 8.0.1 chokidar: 4.0.3 @@ -21482,8 +21482,8 @@ snapshots: remark-gfm: 4.0.1 source-map: 0.7.6 vfile: 6.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - '@swc/helpers' - '@types/node' @@ -25548,11 +25548,11 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 - '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.27 '@swc/core': 1.15.11(@swc/helpers@0.5.17) - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@swc/helpers' @@ -25592,7 +25592,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -25600,7 +25600,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -25694,14 +25694,14 @@ snapshots: msw: 2.12.10(@types/node@22.19.15)(typescript@5.9.3) vite: 5.4.21(@types/node@22.19.15)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) - '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.10(@types/node@20.19.13)(typescript@5.9.3) - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@2.1.9': dependencies: @@ -26516,7 +26516,7 @@ snapshots: bcryptjs@2.4.3: {} - better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0) '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0)) @@ -26543,7 +26543,7 @@ snapshots: pg: 8.17.2 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -30386,7 +30386,7 @@ snapshots: metro-cache: 0.83.2 metro-core: 0.83.2 metro-runtime: 0.83.2 - yaml: 2.8.2 + yaml: 2.8.3 transitivePeerDependencies: - bufferutil - supports-color @@ -30401,7 +30401,7 @@ snapshots: metro-cache: 0.83.5 metro-core: 0.83.5 metro-runtime: 0.83.5 - yaml: 2.8.2 + yaml: 2.8.3 transitivePeerDependencies: - bufferutil - supports-color @@ -34207,13 +34207,13 @@ snapshots: unpipe@1.0.0: {} - unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: ast-kit: 2.2.0 magic-string-ast: 1.0.3 unplugin: 2.3.10 - vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-node: 5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-node: 5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -34503,13 +34503,13 @@ snapshots: - supports-color - terser - vite-node@5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite-node@5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: cac: 6.7.14 es-module-lexer: 1.7.0 obug: 2.0.0(ms@2.1.3) pathe: 2.0.3 - vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -34582,13 +34582,13 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - typescript @@ -34646,7 +34646,7 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 - vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -34657,7 +34657,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 1.21.7 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -34706,7 +34706,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite@7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -34717,7 +34717,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 1.21.7 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -34991,10 +34991,10 @@ snapshots: - supports-color - terser - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -35011,7 +35011,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 From ce3e39d675adf848e6174f50882eef181ad31577 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 11:58:02 +0200 Subject: [PATCH 032/306] docs(effect): use relative paths for example file links --- examples/effect/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/effect/README.md b/examples/effect/README.md index e3fea56259..ef4e0110ad 100644 --- a/examples/effect/README.md +++ b/examples/effect/README.md @@ -13,8 +13,8 @@ This example demonstrates the proposed API design for `@rivetkit/effect`, an [Ef ## Files -- [`src/actors/counter/api.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/effect/src/actors/counter/api.ts) - Actor definition (public contract) -- [`src/actors/counter/live.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/effect/src/actors/counter/live.ts) - Actor implementation (server-only Layer) -- [`src/main.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/effect/src/main.ts) - Server entry point using `Registry.layer` -- [`src/client.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/effect/src/client.ts) - Client usage with typed actor dependencies +- [`src/actors/counter/api.ts`](./src/actors/counter/api.ts) - Actor definition (public contract) +- [`src/actors/counter/live.ts`](./src/actors/counter/live.ts) - Actor implementation (server-only Layer) +- [`src/main.ts`](./src/main.ts) - Server entry point using `Registry.layer` +- [`src/client.ts`](./src/client.ts) - Client usage with typed actor dependencies From cbe28a7c16da5c13703525c0a8d38faa9ed1b7a1 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 12:05:25 +0200 Subject: [PATCH 033/306] chore(effect): align tsconfig with rivetkit/workflow-engine Extend tsconfig.base.json instead of redefining compiler options. Add @types/node devDependency since the base config pulls in node types. --- pnpm-lock.yaml | 67 ++++++++++--------- .../packages/effect/package.json | 1 + .../packages/effect/tsconfig.json | 19 +----- 3 files changed, 39 insertions(+), 48 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 855fbf671d..9288a66af4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3367,7 +3367,7 @@ importers: version: 5.2.2(react-hook-form@7.62.0(react@19.1.0)) '@ladle/react': specifier: ^5.1.1 - version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) '@marsidev/react-turnstile': specifier: ^1.5.0 version: 1.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -3610,7 +3610,7 @@ importers: version: 2.4.3 better-auth: specifier: ^1.5.6 - version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) canvas-confetti: specifier: ^1.9.3 version: 1.9.3 @@ -3730,7 +3730,7 @@ importers: version: 5.2.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.13)(typescript@5.9.3))(typescript@5.9.3) unplugin-macros: specifier: ^0.18.3 - version: 0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + version: 0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.1.0) @@ -3749,7 +3749,7 @@ importers: devDependencies: vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) frontend/packages/components: dependencies: @@ -4107,12 +4107,15 @@ importers: specifier: workspace:* version: link:../rivetkit devDependencies: + '@types/node': + specifier: ^22.13.1 + version: 22.19.15 effect: specifier: 4.0.0-beta.57 version: 4.0.0-beta.57 tsup: specifier: ^8.4.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.15))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.5.2 version: 5.9.3 @@ -21442,7 +21445,7 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': + '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': dependencies: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0 @@ -21454,8 +21457,8 @@ snapshots: '@ladle/react-context': 1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mdx-js/mdx': 3.1.1 '@mdx-js/react': 3.1.1(@types/react@19.2.13)(react@19.1.0) - '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) - '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) axe-core: 4.11.1 boxen: 8.0.1 chokidar: 4.0.3 @@ -21482,8 +21485,8 @@ snapshots: remark-gfm: 4.0.1 source-map: 0.7.6 vfile: 6.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - '@swc/helpers' - '@types/node' @@ -25548,11 +25551,11 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 - '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.27 '@swc/core': 1.15.11(@swc/helpers@0.5.17) - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@swc/helpers' @@ -25592,7 +25595,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -25600,7 +25603,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -25694,14 +25697,14 @@ snapshots: msw: 2.12.10(@types/node@22.19.15)(typescript@5.9.3) vite: 5.4.21(@types/node@22.19.15)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) - '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.10(@types/node@20.19.13)(typescript@5.9.3) - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@2.1.9': dependencies: @@ -26516,7 +26519,7 @@ snapshots: bcryptjs@2.4.3: {} - better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0) '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0)) @@ -26543,7 +26546,7 @@ snapshots: pg: 8.17.2 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -34207,13 +34210,13 @@ snapshots: unpipe@1.0.0: {} - unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: ast-kit: 2.2.0 magic-string-ast: 1.0.3 unplugin: 2.3.10 - vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-node: 5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-node: 5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -34503,13 +34506,13 @@ snapshots: - supports-color - terser - vite-node@5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite-node@5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: cac: 6.7.14 es-module-lexer: 1.7.0 obug: 2.0.0(ms@2.1.3) pathe: 2.0.3 - vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -34582,13 +34585,13 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - typescript @@ -34646,7 +34649,7 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 - vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -34657,7 +34660,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.6.1 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -34706,7 +34709,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite@7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -34717,7 +34720,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.6.1 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -34991,10 +34994,10 @@ snapshots: - supports-color - terser - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -35011,7 +35014,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index da9e4620b6..66a7d20e4d 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -35,6 +35,7 @@ "effect": ">=4.0.0-beta.0" }, "devDependencies": { + "@types/node": "^22.13.1", "effect": "4.0.0-beta.57", "tsup": "^8.4.0", "typescript": "^5.5.2" diff --git a/rivetkit-typescript/packages/effect/tsconfig.json b/rivetkit-typescript/packages/effect/tsconfig.json index 08ecd34041..b50b3396d9 100644 --- a/rivetkit-typescript/packages/effect/tsconfig.json +++ b/rivetkit-typescript/packages/effect/tsconfig.json @@ -1,20 +1,7 @@ { + "extends": "../../../tsconfig.base.json", "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "lib": ["ES2022"], - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "types": ["node"] }, - "include": ["src"] + "include": ["src/**/*"] } From 7d2840acc757a09a42bd52f6fe11a430f692a98e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 12:12:23 +0200 Subject: [PATCH 034/306] chore(effect): align package.json with rivetkit conventions Use 2.3.0-rc.4 version and src/mod.ts entry to match sibling packages. Update exports to .js/.cjs to match the type:module + tsup output naming used by workflow-engine and traces. --- rivetkit-typescript/packages/effect/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index 66a7d20e4d..0679a5fe7e 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -1,6 +1,6 @@ { "name": "@rivetkit/effect", - "version": "0.1.0", + "version": "2.3.0-rc.4", "description": "Effect SDK for Rivet Actors", "license": "Apache-2.0", "type": "module", @@ -15,17 +15,17 @@ "exports": { ".": { "import": { - "types": "./dist/index.d.mts", - "default": "./dist/index.mjs" + "types": "./dist/mod.d.ts", + "default": "./dist/mod.js" }, "require": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" + "types": "./dist/mod.d.cts", + "default": "./dist/mod.cjs" } } }, "scripts": { - "build": "tsup src/index.ts", + "build": "tsup src/mod.ts", "check-types": "tsc --noEmit" }, "dependencies": { From cd5a2222c35888b288c36f46868e416048a121e8 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 12:13:50 +0200 Subject: [PATCH 035/306] chore(effect): use caret range for effect dependency Match the Effect ecosystem convention (e.g. @effect/platform-node) of ^4.0.0-beta.57 instead of >=4.0.0-beta.0 / pinned exact. --- pnpm-lock.yaml | 15 ++++----------- rivetkit-typescript/packages/effect/package.json | 4 ++-- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9288a66af4..5bfc91b3ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4111,7 +4111,7 @@ importers: specifier: ^22.13.1 version: 22.19.15 effect: - specifier: 4.0.0-beta.57 + specifier: ^4.0.0-beta.57 version: 4.0.0-beta.57 tsup: specifier: ^8.4.0 @@ -14984,9 +14984,6 @@ packages: msgpackr@1.11.10: resolution: {integrity: sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==} - msgpackr@1.11.5: - resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} - msw@2.12.10: resolution: {integrity: sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==} engines: {node: '>=18'} @@ -29951,7 +29948,7 @@ snapshots: lmdb@3.4.4: dependencies: - msgpackr: 1.11.5 + msgpackr: 1.11.10 node-addon-api: 6.1.0 node-gyp-build-optional-packages: 5.2.2 ordered-binary: 1.6.0 @@ -31096,10 +31093,6 @@ snapshots: optionalDependencies: msgpackr-extract: 3.0.3 - msgpackr@1.11.5: - optionalDependencies: - msgpackr-extract: 3.0.3 - msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3): dependencies: '@inquirer/confirm': 5.1.21(@types/node@20.19.13) @@ -31498,7 +31491,7 @@ snapshots: openapi3-ts@4.5.0: dependencies: - yaml: 2.8.2 + yaml: 2.8.3 openapi@1.0.1: dependencies: @@ -31895,7 +31888,7 @@ snapshots: postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@25.0.7)(typescript@5.9.3)): dependencies: lilconfig: 3.1.3 - yaml: 2.8.2 + yaml: 2.8.3 optionalDependencies: postcss: 8.5.6 ts-node: 10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@25.0.7)(typescript@5.9.3) diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index 0679a5fe7e..a4ef74056d 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -32,11 +32,11 @@ "rivetkit": "workspace:*" }, "peerDependencies": { - "effect": ">=4.0.0-beta.0" + "effect": "^4.0.0-beta.57" }, "devDependencies": { "@types/node": "^22.13.1", - "effect": "4.0.0-beta.57", + "effect": "^4.0.0-beta.57", "tsup": "^8.4.0", "typescript": "^5.5.2" } From 8c6fb3815f3941d019134a0627b8a85afd1198a1 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 12:16:49 +0200 Subject: [PATCH 036/306] chore(effect): bump typescript to ^5.9.2 Match the version used by the engine-runner packages. --- pnpm-lock.yaml | 64 +++++++++---------- .../packages/effect/package.json | 2 +- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5bfc91b3ff..1dc08b2482 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3367,7 +3367,7 @@ importers: version: 5.2.2(react-hook-form@7.62.0(react@19.1.0)) '@ladle/react': specifier: ^5.1.1 - version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) '@marsidev/react-turnstile': specifier: ^1.5.0 version: 1.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -3610,7 +3610,7 @@ importers: version: 2.4.3 better-auth: specifier: ^1.5.6 - version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) canvas-confetti: specifier: ^1.9.3 version: 1.9.3 @@ -3730,7 +3730,7 @@ importers: version: 5.2.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.13)(typescript@5.9.3))(typescript@5.9.3) unplugin-macros: specifier: ^0.18.3 - version: 0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + version: 0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.1.0) @@ -3749,7 +3749,7 @@ importers: devDependencies: vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) frontend/packages/components: dependencies: @@ -4117,7 +4117,7 @@ importers: specifier: ^8.4.0 version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.15))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: - specifier: ^5.5.2 + specifier: ^5.9.2 version: 5.9.3 rivetkit-typescript/packages/engine-cli: {} @@ -21442,7 +21442,7 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': + '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': dependencies: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0 @@ -21454,8 +21454,8 @@ snapshots: '@ladle/react-context': 1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mdx-js/mdx': 3.1.1 '@mdx-js/react': 3.1.1(@types/react@19.2.13)(react@19.1.0) - '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) - '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) axe-core: 4.11.1 boxen: 8.0.1 chokidar: 4.0.3 @@ -21482,8 +21482,8 @@ snapshots: remark-gfm: 4.0.1 source-map: 0.7.6 vfile: 6.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - '@swc/helpers' - '@types/node' @@ -25548,11 +25548,11 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 - '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.27 '@swc/core': 1.15.11(@swc/helpers@0.5.17) - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@swc/helpers' @@ -25592,7 +25592,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -25600,7 +25600,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -25694,14 +25694,14 @@ snapshots: msw: 2.12.10(@types/node@22.19.15)(typescript@5.9.3) vite: 5.4.21(@types/node@22.19.15)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) - '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.10(@types/node@20.19.13)(typescript@5.9.3) - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@2.1.9': dependencies: @@ -26516,7 +26516,7 @@ snapshots: bcryptjs@2.4.3: {} - better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0) '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0)) @@ -26543,7 +26543,7 @@ snapshots: pg: 8.17.2 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -34203,13 +34203,13 @@ snapshots: unpipe@1.0.0: {} - unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: ast-kit: 2.2.0 magic-string-ast: 1.0.3 unplugin: 2.3.10 - vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-node: 5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-node: 5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -34499,13 +34499,13 @@ snapshots: - supports-color - terser - vite-node@5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite-node@5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: cac: 6.7.14 es-module-lexer: 1.7.0 obug: 2.0.0(ms@2.1.3) pathe: 2.0.3 - vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -34578,13 +34578,13 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - typescript @@ -34642,7 +34642,7 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 - vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -34653,7 +34653,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 1.21.7 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -34702,7 +34702,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite@7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -34713,7 +34713,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 1.21.7 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -34987,10 +34987,10 @@ snapshots: - supports-color - terser - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -35007,7 +35007,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index a4ef74056d..77d1b3403c 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -38,6 +38,6 @@ "@types/node": "^22.13.1", "effect": "^4.0.0-beta.57", "tsup": "^8.4.0", - "typescript": "^5.5.2" + "typescript": "^5.9.2" } } From 874fcf05e3ded2e465163869c01a2245ebe9f758 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 12:18:45 +0200 Subject: [PATCH 037/306] chore(effect): fix import path in tsup config --- rivetkit-typescript/packages/effect/tsup.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/tsup.config.ts b/rivetkit-typescript/packages/effect/tsup.config.ts index f363b829fd..e7d8e5f88d 100644 --- a/rivetkit-typescript/packages/effect/tsup.config.ts +++ b/rivetkit-typescript/packages/effect/tsup.config.ts @@ -1,4 +1,4 @@ import { defineConfig } from "tsup"; -import defaultConfig from "../../../tsup.base.ts"; +import defaultConfig from "../../../tsup.base"; export default defineConfig(defaultConfig); From ca70ed3aac53d840c0c548547c088b981ad1d5db Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 12:42:21 +0200 Subject: [PATCH 038/306] chore(effect): add vitest for testing and include test files in tsconfig --- pnpm-lock.yaml | 199 ++++++++++++++++++ .../packages/effect/package.json | 7 +- .../packages/effect/tsconfig.json | 2 +- 3 files changed, 205 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1dc08b2482..766ea190fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4107,6 +4107,9 @@ importers: specifier: workspace:* version: link:../rivetkit devDependencies: + '@effect/vitest': + specifier: ^4.0.0-beta.57 + version: 4.0.0-beta.57(effect@4.0.0-beta.57)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))) '@types/node': specifier: ^22.13.1 version: 22.19.15 @@ -4119,6 +4122,9 @@ importers: typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^4.1.5 + version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) rivetkit-typescript/packages/engine-cli: {} @@ -6003,6 +6009,12 @@ packages: effect: ^4.0.0-beta.57 ioredis: ^5.7.0 + '@effect/vitest@4.0.0-beta.57': + resolution: {integrity: sha512-XyGYv1zisrdP/N8+r4qaegyHZK4WS/1xBGlLPWqEoggBhgW7rD48cGUXDLEP7TlHcIJQiIlHtJlQUIwgVx3zWg==} + peerDependencies: + effect: ^4.0.0-beta.57 + vitest: ^3.0.0 || ^4.0.0 + '@emnapi/runtime@1.7.1': resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} @@ -10605,6 +10617,9 @@ packages: '@vitest/expect@4.0.18': resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} + '@vitest/mocker@2.1.9': resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} peerDependencies: @@ -10638,6 +10653,17 @@ packages: vite: optional: true + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@2.1.9': resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} @@ -10647,6 +10673,9 @@ packages: '@vitest/pretty-format@4.0.18': resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@4.1.5': + resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} + '@vitest/runner@1.6.1': resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} @@ -10659,6 +10688,9 @@ packages: '@vitest/runner@4.0.18': resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} + '@vitest/snapshot@1.6.1': resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} @@ -10671,6 +10703,9 @@ packages: '@vitest/snapshot@4.0.18': resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} + '@vitest/spy@1.6.1': resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} @@ -10683,6 +10718,9 @@ packages: '@vitest/spy@4.0.18': resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} + '@vitest/utils@1.6.1': resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} @@ -10695,6 +10733,9 @@ packages: '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.5': + resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} + '@volar/language-core@1.11.1': resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} @@ -12612,6 +12653,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -12834,6 +12878,10 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + expo-asset@12.0.12: resolution: {integrity: sha512-CsXFCQbx2fElSMn0lyTdRIyKlSXOal6ilLJd+yeZ6xaC7I9AICQgscY5nj0QcwgA+KYYCCEQEBndMsmj7drOWQ==} peerDependencies: @@ -16856,6 +16904,9 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stream-browserify@3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} @@ -17167,6 +17218,10 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + tinyspy@2.2.1: resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} @@ -18023,6 +18078,47 @@ packages: jsdom: optional: true + vitest@4.1.5: + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.5 + '@vitest/browser-preview': 4.1.5 + '@vitest/browser-webdriverio': 4.1.5 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vlq@1.0.1: resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} @@ -20049,6 +20145,11 @@ snapshots: - bufferutil - utf-8-validate + '@effect/vitest@4.0.0-beta.57(effect@4.0.0-beta.57)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + effect: 4.0.0-beta.57 + vitest: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 @@ -25658,6 +25759,15 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 + '@vitest/expect@4.1.5': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + chai: 6.2.2 + tinyrainbow: 3.1.0 + '@vitest/mocker@2.1.9(msw@2.12.10(@types/node@22.19.10)(typescript@5.9.3))(vite@5.4.21(@types/node@22.19.10)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0))': dependencies: '@vitest/spy': 2.1.9 @@ -25703,6 +25813,15 @@ snapshots: msw: 2.12.10(@types/node@20.19.13)(typescript@5.9.3) vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/mocker@4.1.5(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.5 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.12.10(@types/node@22.19.15)(typescript@5.9.3) + vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/pretty-format@2.1.9': dependencies: tinyrainbow: 1.2.0 @@ -25715,6 +25834,10 @@ snapshots: dependencies: tinyrainbow: 3.0.3 + '@vitest/pretty-format@4.1.5': + dependencies: + tinyrainbow: 3.1.0 + '@vitest/runner@1.6.1': dependencies: '@vitest/utils': 1.6.1 @@ -25737,6 +25860,11 @@ snapshots: '@vitest/utils': 4.0.18 pathe: 2.0.3 + '@vitest/runner@4.1.5': + dependencies: + '@vitest/utils': 4.1.5 + pathe: 2.0.3 + '@vitest/snapshot@1.6.1': dependencies: magic-string: 0.30.21 @@ -25761,6 +25889,13 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/snapshot@4.1.5': + dependencies: + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 + magic-string: 0.30.21 + pathe: 2.0.3 + '@vitest/spy@1.6.1': dependencies: tinyspy: 2.2.1 @@ -25775,6 +25910,8 @@ snapshots: '@vitest/spy@4.0.18': {} + '@vitest/spy@4.1.5': {} + '@vitest/utils@1.6.1': dependencies: diff-sequences: 29.6.3 @@ -25799,6 +25936,12 @@ snapshots: '@vitest/pretty-format': 4.0.18 tinyrainbow: 3.0.3 + '@vitest/utils@4.1.5': + dependencies: + '@vitest/pretty-format': 4.1.5 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@volar/language-core@1.11.1': dependencies: '@volar/source-map': 1.11.1 @@ -27858,6 +28001,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.1.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -28240,6 +28385,8 @@ snapshots: expect-type@1.2.2: {} + expect-type@1.3.0: {} + expo-asset@12.0.12(expo@54.0.18)(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0): dependencies: '@expo/image-utils': 0.8.12 @@ -33335,6 +33482,8 @@ snapshots: std-env@3.9.0: {} + std-env@4.1.0: {} + stream-browserify@3.0.0: dependencies: inherits: 2.0.4 @@ -33695,6 +33844,8 @@ snapshots: tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} + tinyspy@2.2.1: {} tinyspy@3.0.2: {} @@ -34743,6 +34894,26 @@ snapshots: yaml: 2.8.3 optional: true + vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.15 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.4.1 + lightningcss: 1.32.0 + sass: 1.93.2 + stylus: 0.62.0 + terser: 5.46.0 + tsx: 4.21.0 + yaml: 2.8.3 + vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 @@ -35025,6 +35196,34 @@ snapshots: - tsx - yaml + vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 22.19.15 + transitivePeerDependencies: + - msw + vlq@1.0.1: {} vm-browserify@1.1.2: {} diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index 77d1b3403c..1390767b80 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -26,7 +26,8 @@ }, "scripts": { "build": "tsup src/mod.ts", - "check-types": "tsc --noEmit" + "check-types": "tsc --noEmit", + "test": "vitest" }, "dependencies": { "rivetkit": "workspace:*" @@ -35,9 +36,11 @@ "effect": "^4.0.0-beta.57" }, "devDependencies": { + "@effect/vitest": "^4.0.0-beta.57", "@types/node": "^22.13.1", "effect": "^4.0.0-beta.57", "tsup": "^8.4.0", - "typescript": "^5.9.2" + "typescript": "^5.9.2", + "vitest": "^4.1.5" } } diff --git a/rivetkit-typescript/packages/effect/tsconfig.json b/rivetkit-typescript/packages/effect/tsconfig.json index b50b3396d9..97f2a05ddf 100644 --- a/rivetkit-typescript/packages/effect/tsconfig.json +++ b/rivetkit-typescript/packages/effect/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "types": ["node"] }, - "include": ["src/**/*"] + "include": ["src/**/*", "test/**/*"] } From d84f281a9634daa17523f0671efa03c9b42bca76 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 12:51:49 +0200 Subject: [PATCH 039/306] chore(effect): enable verbatimModuleSyntax and add @effect/language-service --- pnpm-lock.yaml | 9 +++++++++ rivetkit-typescript/packages/effect/package.json | 1 + rivetkit-typescript/packages/effect/tsconfig.json | 13 ++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 766ea190fd..fd98bd9604 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4107,6 +4107,9 @@ importers: specifier: workspace:* version: link:../rivetkit devDependencies: + '@effect/language-service': + specifier: ^0.85.1 + version: 0.85.1 '@effect/vitest': specifier: ^4.0.0-beta.57 version: 4.0.0-beta.57(effect@4.0.0-beta.57)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))) @@ -5996,6 +5999,10 @@ packages: resolution: {tarball: https://pkg.pr.new/rivet-dev/durable-streams/@durable-streams/writer@0323b8bcf1c9b38f1014629e1a8b6c74cc662100} version: 0.0.0 + '@effect/language-service@0.85.1': + resolution: {integrity: sha512-EXnJjIy6zQ3nUO/MZ+ynWUb8B895KZPotd1++oTs9JjDkplwM7cb6zo8Zq2zU6piwq+KflO7amXbEfj1UMpHkw==} + hasBin: true + '@effect/platform-node-shared@4.0.0-beta.57': resolution: {integrity: sha512-C976X6f+qHUtLSqcqImuCrjhAHnJV17NC2RvvybsAuDfkyIWU4MyiO2XwgiBeijeNupyr1M/KPKnyjtkNxV9Hw==} engines: {node: '>=18.0.0'} @@ -20125,6 +20132,8 @@ snapshots: '@durable-streams/client': https://pkg.pr.new/rivet-dev/durable-streams/@durable-streams/client@0323b8bcf1c9b38f1014629e1a8b6c74cc662100 fastq: 1.20.1 + '@effect/language-service@0.85.1': {} + '@effect/platform-node-shared@4.0.0-beta.57(effect@4.0.0-beta.57)': dependencies: '@types/ws': 8.18.1 diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index 1390767b80..ef3a2e01af 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -36,6 +36,7 @@ "effect": "^4.0.0-beta.57" }, "devDependencies": { + "@effect/language-service": "^0.85.1", "@effect/vitest": "^4.0.0-beta.57", "@types/node": "^22.13.1", "effect": "^4.0.0-beta.57", diff --git a/rivetkit-typescript/packages/effect/tsconfig.json b/rivetkit-typescript/packages/effect/tsconfig.json index 97f2a05ddf..bdc62121b3 100644 --- a/rivetkit-typescript/packages/effect/tsconfig.json +++ b/rivetkit-typescript/packages/effect/tsconfig.json @@ -1,7 +1,18 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "types": ["node"] + "types": ["node"], + "verbatimModuleSyntax": true, + "plugins": [ + { + "name": "@effect/language-service", + "namespaceImportPackages": [ + "effect", + "@effect/*", + "@rivetkit/effect" + ] + } + ] }, "include": ["src/**/*", "test/**/*"] } From 4480032e782d680bc91157e648125b259e368db7 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 12:56:35 +0200 Subject: [PATCH 040/306] chore(effect): drop @types/node to keep types runtime-agnostic Override base tsconfig's types: ["node"] with types: [] so the package type-checks without Node typings. The Effect SDK should stay platform- neutral; runtime-specific code can opt in via separate entrypoints later. --- pnpm-lock.yaml | 152 ++++++++++++------ .../packages/effect/package.json | 1 - .../packages/effect/tsconfig.json | 2 +- 3 files changed, 102 insertions(+), 53 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd98bd9604..79f55aab1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3367,7 +3367,7 @@ importers: version: 5.2.2(react-hook-form@7.62.0(react@19.1.0)) '@ladle/react': specifier: ^5.1.1 - version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) '@marsidev/react-turnstile': specifier: ^1.5.0 version: 1.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -3610,7 +3610,7 @@ importers: version: 2.4.3 better-auth: specifier: ^1.5.6 - version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) canvas-confetti: specifier: ^1.9.3 version: 1.9.3 @@ -3730,7 +3730,7 @@ importers: version: 5.2.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.13)(typescript@5.9.3))(typescript@5.9.3) unplugin-macros: specifier: ^0.18.3 - version: 0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + version: 0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.1.0) @@ -3749,7 +3749,7 @@ importers: devDependencies: vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) frontend/packages/components: dependencies: @@ -4112,22 +4112,19 @@ importers: version: 0.85.1 '@effect/vitest': specifier: ^4.0.0-beta.57 - version: 4.0.0-beta.57(effect@4.0.0-beta.57)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))) - '@types/node': - specifier: ^22.13.1 - version: 22.19.15 + version: 4.0.0-beta.57(effect@4.0.0-beta.57)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.0.7)(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))) effect: specifier: ^4.0.0-beta.57 version: 4.0.0-beta.57 tsup: specifier: ^8.4.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.15))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.9.2 version: 5.9.3 vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.0.7)(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) rivetkit-typescript/packages/engine-cli: {} @@ -20154,10 +20151,10 @@ snapshots: - bufferutil - utf-8-validate - '@effect/vitest@4.0.0-beta.57(effect@4.0.0-beta.57)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)))': + '@effect/vitest@4.0.0-beta.57(effect@4.0.0-beta.57)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.0.7)(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: effect: 4.0.0-beta.57 - vitest: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.0.7)(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@emnapi/runtime@1.7.1': dependencies: @@ -21363,6 +21360,14 @@ snapshots: '@types/node': 22.19.15 optional: true + '@inquirer/confirm@5.1.21(@types/node@25.0.7)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@25.0.7) + '@inquirer/type': 3.0.10(@types/node@25.0.7) + optionalDependencies: + '@types/node': 25.0.7 + optional: true + '@inquirer/core@10.3.2(@types/node@20.19.13)': dependencies: '@inquirer/ansi': 1.0.2 @@ -21404,6 +21409,20 @@ snapshots: '@types/node': 22.19.15 optional: true + '@inquirer/core@10.3.2(@types/node@25.0.7)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@25.0.7) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 25.0.7 + optional: true + '@inquirer/figures@1.0.15': {} '@inquirer/type@3.0.10(@types/node@20.19.13)': @@ -21420,6 +21439,11 @@ snapshots: '@types/node': 22.19.15 optional: true + '@inquirer/type@3.0.10(@types/node@25.0.7)': + optionalDependencies: + '@types/node': 25.0.7 + optional: true + '@ioredis/commands@1.5.1': {} '@isaacs/balanced-match@4.0.1': @@ -21552,7 +21576,7 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': + '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': dependencies: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0 @@ -21564,8 +21588,8 @@ snapshots: '@ladle/react-context': 1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mdx-js/mdx': 3.1.1 '@mdx-js/react': 3.1.1(@types/react@19.2.13)(react@19.1.0) - '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) - '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) axe-core: 4.11.1 boxen: 8.0.1 chokidar: 4.0.3 @@ -21592,8 +21616,8 @@ snapshots: remark-gfm: 4.0.1 source-map: 0.7.6 vfile: 6.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - '@swc/helpers' - '@types/node' @@ -25658,11 +25682,11 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 - '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.27 '@swc/core': 1.15.11(@swc/helpers@0.5.17) - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@swc/helpers' @@ -25702,7 +25726,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -25710,7 +25734,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -25813,23 +25837,23 @@ snapshots: msw: 2.12.10(@types/node@22.19.15)(typescript@5.9.3) vite: 5.4.21(@types/node@22.19.15)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) - '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.10(@types/node@20.19.13)(typescript@5.9.3) - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.5(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.5(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - msw: 2.12.10(@types/node@22.19.15)(typescript@5.9.3) - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + msw: 2.12.10(@types/node@25.0.7)(typescript@5.9.3) + vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@2.1.9': dependencies: @@ -26668,7 +26692,7 @@ snapshots: bcryptjs@2.4.3: {} - better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0) '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0)) @@ -26695,7 +26719,7 @@ snapshots: pg: 8.17.2 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -31326,6 +31350,32 @@ snapshots: - '@types/node' optional: true + msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.21(@types/node@25.0.7) + '@mswjs/interceptors': 0.41.2 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.12.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.10.1 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 5.4.4 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + optional: true + muggle-string@0.3.1: {} multipasta@0.2.7: {} @@ -34363,13 +34413,13 @@ snapshots: unpipe@1.0.0: {} - unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: ast-kit: 2.2.0 magic-string-ast: 1.0.3 unplugin: 2.3.10 - vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-node: 5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-node: 5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -34659,13 +34709,13 @@ snapshots: - supports-color - terser - vite-node@5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite-node@5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: cac: 6.7.14 es-module-lexer: 1.7.0 obug: 2.0.0(ms@2.1.3) pathe: 2.0.3 - vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -34738,13 +34788,13 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - typescript @@ -34802,7 +34852,7 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 - vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -34813,7 +34863,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.6.1 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -34862,7 +34912,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite@7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -34873,7 +34923,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.6.1 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -34903,7 +34953,7 @@ snapshots: yaml: 2.8.3 optional: true - vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -34912,7 +34962,7 @@ snapshots: rollup: 4.57.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.19.15 + '@types/node': 25.0.7 fsevents: 2.3.3 jiti: 2.6.1 less: 4.4.1 @@ -34921,9 +34971,9 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 tsx: 4.21.0 - yaml: 2.8.3 + yaml: 2.8.2 - vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -34941,7 +34991,7 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.8.3 vitefu@1.1.1(vite@6.4.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): optionalDependencies: @@ -35167,10 +35217,10 @@ snapshots: - supports-color - terser - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -35187,7 +35237,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -35205,10 +35255,10 @@ snapshots: - tsx - yaml - vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.0.7)(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.1.5(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.5 '@vitest/runner': 4.1.5 '@vitest/snapshot': 4.1.5 @@ -35225,11 +35275,11 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 - '@types/node': 22.19.15 + '@types/node': 25.0.7 transitivePeerDependencies: - msw diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index ef3a2e01af..9ccc29918f 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -38,7 +38,6 @@ "devDependencies": { "@effect/language-service": "^0.85.1", "@effect/vitest": "^4.0.0-beta.57", - "@types/node": "^22.13.1", "effect": "^4.0.0-beta.57", "tsup": "^8.4.0", "typescript": "^5.9.2", diff --git a/rivetkit-typescript/packages/effect/tsconfig.json b/rivetkit-typescript/packages/effect/tsconfig.json index bdc62121b3..588bd72ffb 100644 --- a/rivetkit-typescript/packages/effect/tsconfig.json +++ b/rivetkit-typescript/packages/effect/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "types": ["node"], + "types": [], "verbatimModuleSyntax": true, "plugins": [ { From 145cdb4209ceabc23c7137e5beedc4fb7ceda409 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 12:57:06 +0200 Subject: [PATCH 041/306] chore(effect): add vitest config extending workspace base --- rivetkit-typescript/packages/effect/vitest.config.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 rivetkit-typescript/packages/effect/vitest.config.ts diff --git a/rivetkit-typescript/packages/effect/vitest.config.ts b/rivetkit-typescript/packages/effect/vitest.config.ts new file mode 100644 index 0000000000..7b2c82e6c5 --- /dev/null +++ b/rivetkit-typescript/packages/effect/vitest.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vitest/config" +import defaultConfig from "../../../vitest.base" + +export default defineConfig({ + ...defaultConfig, +}) From 0bc65f30aa4edad807fcef2079531f8af87ae142 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 12:59:29 +0200 Subject: [PATCH 042/306] docs(effect): remove outdated README API design proposal Delete the README file containing a non-implemented API design proposal for the Effect SDK. --- examples/effect/README.md | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 examples/effect/README.md diff --git a/examples/effect/README.md b/examples/effect/README.md deleted file mode 100644 index ef4e0110ad..0000000000 --- a/examples/effect/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Effect SDK API Design - -> **This is a design proposal, not a working example.** The `@rivetkit/effect` package does not exist yet. The code here shows the proposed API surface for an Effect-based SDK for Rivet Actors. - -## Overview - -This example demonstrates the proposed API design for `@rivetkit/effect`, an [Effect](https://effect.website/) SDK for Rivet Actors. The design leverages Effect's type system to provide: - -- Schema-validated actions with typed errors -- Layer-based composition for actor registration, transport, and testing -- Compile-time tracking of actor dependencies via Effect's `R` type parameter -- Per-actor transport overrides and selective test mocking - -## Files - -- [`src/actors/counter/api.ts`](./src/actors/counter/api.ts) - Actor definition (public contract) -- [`src/actors/counter/live.ts`](./src/actors/counter/live.ts) - Actor implementation (server-only Layer) -- [`src/main.ts`](./src/main.ts) - Server entry point using `Registry.layer` -- [`src/client.ts`](./src/client.ts) - Client usage with typed actor dependencies - From ce79b9b06c4cd1d6f41278f97dcab6c669385850 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 13:37:20 +0200 Subject: [PATCH 043/306] chore(effect): wire up persistent turbo dev task for example workflow --- examples/effect/turbo.json | 9 ++++++++- rivetkit-typescript/packages/effect/package.json | 3 ++- rivetkit-typescript/packages/effect/turbo.json | 9 ++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/examples/effect/turbo.json b/examples/effect/turbo.json index 29d4cb2625..8ec494d49d 100644 --- a/examples/effect/turbo.json +++ b/examples/effect/turbo.json @@ -1,4 +1,11 @@ { "$schema": "https://turbo.build/schema.json", - "extends": ["//"] + "extends": ["//"], + "tasks": { + "dev": { + "cache": false, + "persistent": true, + "dependsOn": ["^dev"] + } + } } diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index 9ccc29918f..528cbf3c15 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -26,8 +26,9 @@ }, "scripts": { "build": "tsup src/mod.ts", + "dev": "tsup src/mod.ts --watch", "check-types": "tsc --noEmit", - "test": "vitest" + "test": "vitest run" }, "dependencies": { "rivetkit": "workspace:*" diff --git a/rivetkit-typescript/packages/effect/turbo.json b/rivetkit-typescript/packages/effect/turbo.json index 29d4cb2625..9435cdf9df 100644 --- a/rivetkit-typescript/packages/effect/turbo.json +++ b/rivetkit-typescript/packages/effect/turbo.json @@ -1,4 +1,11 @@ { "$schema": "https://turbo.build/schema.json", - "extends": ["//"] + "extends": ["//"], + "tasks": { + "dev": { + "cache": false, + "persistent": true, + "dependsOn": ["^build"] + } + } } From 76b66bc6b32d83718bc86d3a8ee4daaf224d21c4 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 13:43:12 +0200 Subject: [PATCH 044/306] chore(effect): use workspace deps so turbo sees the example's graph --- examples/effect/package.json | 4 ++-- examples/effect/turbo.json | 9 +-------- rivetkit-typescript/packages/effect/turbo.json | 9 +-------- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/examples/effect/package.json b/examples/effect/package.json index aecb142693..70e21c5a1d 100644 --- a/examples/effect/package.json +++ b/examples/effect/package.json @@ -8,8 +8,8 @@ "check-types": "tsc --noEmit" }, "dependencies": { - "rivetkit": "*", - "@rivetkit/effect": "*", + "rivetkit": "workspace:*", + "@rivetkit/effect": "workspace:*", "effect": "4.0.0-beta.57", "@effect/platform-node": "4.0.0-beta.57" }, diff --git a/examples/effect/turbo.json b/examples/effect/turbo.json index 8ec494d49d..29d4cb2625 100644 --- a/examples/effect/turbo.json +++ b/examples/effect/turbo.json @@ -1,11 +1,4 @@ { "$schema": "https://turbo.build/schema.json", - "extends": ["//"], - "tasks": { - "dev": { - "cache": false, - "persistent": true, - "dependsOn": ["^dev"] - } - } + "extends": ["//"] } diff --git a/rivetkit-typescript/packages/effect/turbo.json b/rivetkit-typescript/packages/effect/turbo.json index 9435cdf9df..29d4cb2625 100644 --- a/rivetkit-typescript/packages/effect/turbo.json +++ b/rivetkit-typescript/packages/effect/turbo.json @@ -1,11 +1,4 @@ { "$schema": "https://turbo.build/schema.json", - "extends": ["//"], - "tasks": { - "dev": { - "cache": false, - "persistent": true, - "dependsOn": ["^build"] - } - } + "extends": ["//"] } From 1ac8dfe561c3e9d3d52630b0b9447a67daea740f Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 27 Apr 2026 13:50:05 +0200 Subject: [PATCH 045/306] feat(effect): add Action module with schema-driven action definitions --- .../packages/effect/src/Action.ts | 246 ++++++++++++++++++ .../packages/effect/src/mod.ts | 1 + 2 files changed, 247 insertions(+) create mode 100644 rivetkit-typescript/packages/effect/src/Action.ts create mode 100644 rivetkit-typescript/packages/effect/src/mod.ts diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts new file mode 100644 index 0000000000..b8b53b587a --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -0,0 +1,246 @@ +import { type Pipeable, pipeArguments } from "effect/Pipeable"; +import * as Schema from "effect/Schema"; + +const TypeId = "~@rivetkit/effect/Action"; + +/** + * Schema describing the shape of unexpected runtime errors (defects) + * that the action transport may surface. + * + * Defects are sanitized at the runtime boundary, so untrusted clients + * never observe raw stack traces or non-serializable payloads. + */ +export interface DefectSchema extends Schema.Top { + readonly Type: unknown; + make(input: null, options?: Schema.MakeOptions): unknown; + make(input: undefined, options?: Schema.MakeOptions): unknown; + make(input: object, options?: Schema.MakeOptions): unknown; + readonly DecodingServices: never; + readonly EncodingServices: never; +} + +/** + * A Rivet Actor action: a synchronous request-response call dispatched + * on the actor's main loop. + * + * @remarks + * + * `Action` is a value-level definition that carries the wire schemas + * for the request payload, the success response, and the typed error + * channel. The action's implementation lives in the actor's handler + * map; this type only describes the contract. + */ +export interface Action< + in out Tag extends string, + out Payload extends Schema.Top = Schema.Void, + out Success extends Schema.Top = Schema.Void, + out Error extends Schema.Top = Schema.Never, +> extends Pipeable { + new (_: never): object; + + readonly [TypeId]: typeof TypeId; + readonly _tag: Tag; + readonly key: string; + readonly payloadSchema: Payload; + readonly successSchema: Success; + readonly errorSchema: Error; + readonly defectSchema: Schema.Top; +} + +/** + * Type-erased view of any `Action`. Useful for collections of actions + * where the specific schemas don't matter. + */ +export interface Any extends Pipeable { + readonly [TypeId]: typeof TypeId; + readonly _tag: string; + readonly key: string; +} + +/** + * Like `Any`, but with the prop fields (`*Schema`) accessible. Used + * by internal builders that need to read schemas off an action. + */ +export interface AnyWithProps extends Pipeable { + readonly [TypeId]: typeof TypeId; + readonly _tag: string; + readonly key: string; + readonly payloadSchema: Schema.Top; + readonly successSchema: Schema.Top; + readonly errorSchema: Schema.Top; + readonly defectSchema: Schema.Top; +} + +// --- Type helpers --------------------------------------------------- + +export type Tag = + R extends Action + ? _Tag + : never; + +export type PayloadSchema = + R extends Action + ? _Payload + : never; + +export type Payload = PayloadSchema["Type"]; + +/** + * The shape accepted by the payload schema's `make` constructor on the + * client side (i.e. before encoding). Useful for typing the call site. + */ +export type PayloadConstructor = + R extends Action + ? _Payload["~type.make.in"] + : never; + +export type SuccessSchema = + R extends Action + ? _Success + : never; + +export type Success = SuccessSchema["Type"]; + +export type ErrorSchema = + R extends Action + ? _Error + : never; + +export type Error = ErrorSchema["Type"]; + +/** + * The full set of decoding/encoding services required by every schema + * referenced by the action. Code generators include this in the `R` + * channel of any effect that handles or invokes the action. + */ +export type Services = + R extends Action + ? + | _Payload["DecodingServices"] + | _Payload["EncodingServices"] + | _Success["DecodingServices"] + | _Success["EncodingServices"] + | _Error["DecodingServices"] + | _Error["EncodingServices"] + : never; + +/** + * The subset of `Services` actually needed on the client side: encoding + * the payload, decoding the success response, decoding the error. + */ +export type ServicesClient = + R extends Action + ? + | _Payload["EncodingServices"] + | _Success["DecodingServices"] + | _Error["DecodingServices"] + : never; + +/** + * The subset of `Services` needed on the server side: decoding the + * payload, encoding the success response, encoding the error. + */ +export type ServicesServer = + R extends Action + ? + | _Payload["DecodingServices"] + | _Success["EncodingServices"] + | _Error["EncodingServices"] + : never; + +/** + * Extract the action with the matching tag from a union of actions. + */ +export type ExtractTag = R extends Action< + Tag, + infer _Payload, + infer _Success, + infer _Error +> + ? R + : never; + +// --- Implementation ------------------------------------------------- + +const Proto = { + [TypeId]: TypeId, + pipe() { + // biome-ignore lint/complexity/noArguments: required by Effect's Pipeable contract + return pipeArguments(this, arguments); + }, +}; + +const makeProto = < + const Tag extends string, + Payload extends Schema.Top, + Success extends Schema.Top, + Error extends Schema.Top, +>(options: { + readonly _tag: Tag; + readonly payloadSchema: Payload; + readonly successSchema: Success; + readonly errorSchema: Error; + readonly defectSchema: Schema.Top; +}): Action => { + function Action() {} + Object.setPrototypeOf(Action, Proto); + Object.assign(Action, options); + Action.key = `rivetkit/effect/Action/${options._tag}`; + return Action as any; +}; + +/** + * Define a Rivet Actor action. + * + * @example + * ```ts + * import { Schema } from "effect" + * import { Action } from "@rivetkit/effect" + * + * class CounterOverflow extends Schema.TaggedErrorClass()( + * "CounterOverflow", + * { limit: Schema.Number }, + * ) {} + * + * export const Increment = Action.make("Increment", { + * payload: { amount: Schema.Number }, + * success: Schema.Number, + * error: CounterOverflow, + * }) + * ``` + */ +export const make = < + const Tag extends string, + Payload extends Schema.Top | Schema.Struct.Fields = Schema.Void, + Success extends Schema.Top = Schema.Void, + Error extends Schema.Top = Schema.Never, +>( + tag: Tag, + options?: { + readonly payload?: Payload; + readonly success?: Success; + readonly error?: Error; + readonly defect?: DefectSchema; + }, +): Action< + Tag, + Payload extends Schema.Struct.Fields ? Schema.Struct : Payload, + Success, + Error +> => { + const successSchema = options?.success ?? Schema.Void; + const errorSchema = options?.error ?? Schema.Never; + const defectSchema = options?.defect ?? Schema.Defect; + const payloadSchema: Schema.Top = Schema.isSchema(options?.payload) + ? (options?.payload as any) + : options?.payload + ? Schema.Struct(options?.payload as any) + : Schema.Void; + return makeProto({ + _tag: tag, + payloadSchema, + successSchema, + errorSchema, + defectSchema, + }) as any; +}; diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts new file mode 100644 index 0000000000..b389639175 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -0,0 +1 @@ +export * as Action from "./Action"; From 136d5b5e560abd76fa4c6fe7a2b030efba3d0564 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 28 Apr 2026 11:42:06 +0200 Subject: [PATCH 046/306] docs(effect): use shorthand payload fields in counter example Align the counter example with the canonical convention shown in the Action.make and Message.make docstrings, where the payload is given as a fields object instead of an explicit Schema.Struct. --- examples/effect/src/actors/counter/api.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts index 5e056727d4..b7db1bacb1 100644 --- a/examples/effect/src/actors/counter/api.ts +++ b/examples/effect/src/actors/counter/api.ts @@ -32,7 +32,7 @@ export class CounterOverflowError extends Schema.TaggedErrorClass Date: Tue, 28 Apr 2026 11:46:16 +0200 Subject: [PATCH 047/306] feat(effect): add Message module with completable and fire-and-forget messages --- .../packages/effect/src/Message.ts | 314 ++++++++++++++++++ .../packages/effect/src/mod.ts | 1 + 2 files changed, 315 insertions(+) create mode 100644 rivetkit-typescript/packages/effect/src/Message.ts diff --git a/rivetkit-typescript/packages/effect/src/Message.ts b/rivetkit-typescript/packages/effect/src/Message.ts new file mode 100644 index 0000000000..9ca92ac94a --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Message.ts @@ -0,0 +1,314 @@ +import { type Pipeable, pipeArguments } from "effect/Pipeable"; +import * as Schema from "effect/Schema"; + +const TypeId = "~@rivetkit/effect/Message"; + +const EnvelopeTypeId = "~@rivetkit/effect/Message/Envelope"; + +/** + * A Rivet Actor message: a durable, queued operation that the actor + * processes asynchronously on its main loop. + * + * @remarks + * + * `Message` is a value-level definition that carries the wire schemas + * for the request payload and, optionally, a completion response. The + * message's implementation lives in the actor's handler map; this type + * only describes the contract. + * + * Messages come in two flavors, distinguished at the type level by the + * `Success` schema: + * + * - **Non-completable** (fire-and-forget). `Success` defaults to + * `Schema.Never`. Sending returns once the message is durably + * enqueued; the sender does not observe the actor's processing. + * + * - **Completable**. `Success` is provided. Sending returns an Effect + * that resolves with the typed completion value once the actor's + * handler invokes its `complete` callback. + * + * Unlike `Action`, `Message` has no error channel. A message may sit + * in the queue long after the sender has moved on, so propagating a + * typed error back to the sender is not a meaningful contract. Handler + * failures are surfaced through the actor's standard supervision and + * retry mechanisms instead. + * + * `Message` values are callable: invoking the message with a payload + * produces a typed `Envelope` that can be passed to `actor.send(...)`. + * + * @example + * ```ts + * import { Schema } from "effect" + * import { Message } from "@rivetkit/effect" + * + * // Non-completable + * export const Reset = Message.make("Reset", { + * payload: { reason: Schema.String }, + * }) + * + * // Completable + * export const IncrementBy = Message.make("IncrementBy", { + * payload: { amount: Schema.Number }, + * success: Schema.Number, + * }) + * ``` + */ +export interface Message< + in out Tag extends string, + in out Payload extends Schema.Top = Schema.Void, + out Success extends Schema.Top = Schema.Never, +> extends Pipeable { + new (_: never): object; + + (payload: Payload["~type.make.in"]): Envelope; + + readonly [TypeId]: typeof TypeId; + readonly _tag: Tag; + readonly key: string; + readonly payloadSchema: Payload; + readonly successSchema: Success; + /** + * Whether this message yields a typed completion to the sender. + * `true` when a `success` schema was supplied; `false` for + * fire-and-forget messages. + */ + readonly completable: IsCompletable>; +} + +/** + * A typed payload envelope produced by calling a `Message` value. + * + * The runtime uses `_tag` to dispatch to the correct handler. The + * `Success` type parameter is phantom: it surfaces the completion + * type at the call site so `actor.send(...)` can return a + * precisely-typed Effect without a second lookup. + */ +export interface Envelope< + in out Tag extends string, + out Payload extends Schema.Top = Schema.Void, + out Success extends Schema.Top = Schema.Never, +> { + readonly [EnvelopeTypeId]: typeof EnvelopeTypeId; + readonly _tag: Tag; + readonly payload: Payload["Type"]; + readonly "~successSchema": Success; +} + +/** + * Type-erased view of any `Message`. Useful for collections of + * messages where the specific schemas don't matter. + */ +export interface Any extends Pipeable { + readonly [TypeId]: typeof TypeId; + readonly _tag: string; + readonly key: string; + readonly completable: boolean; +} + +/** + * Like `Any`, but with the prop fields (`*Schema`) accessible. Used + * by internal builders that need to read schemas off a message. + */ +export interface AnyWithProps extends Pipeable { + readonly [TypeId]: typeof TypeId; + readonly _tag: string; + readonly key: string; + readonly payloadSchema: Schema.Top; + readonly successSchema: Schema.Top; + readonly completable: boolean; +} + +/** + * Type-erased view of any `Envelope`. + */ +export interface AnyEnvelope { + readonly [EnvelopeTypeId]: typeof EnvelopeTypeId; + readonly _tag: string; + readonly payload: unknown; +} + +// --- Type helpers --------------------------------------------------- + +export type Tag = + R extends Message + ? _Tag + : never; + +export type PayloadSchema = + R extends Message + ? _Payload + : never; + +export type Payload = PayloadSchema["Type"]; + +/** + * The shape accepted by the payload schema's `make` constructor on + * the client side (i.e. before encoding). Useful for typing the + * call site. + */ +export type PayloadConstructor = + R extends Message + ? _Payload["~type.make.in"] + : never; + +export type SuccessSchema = + R extends Message + ? _Success + : never; + +export type Success = SuccessSchema["Type"]; + +/** + * `true` when the message is completable (a `success` schema was + * provided), `false` for fire-and-forget messages. + * + * Driven off `Schema.Never` because `Schema.Void` is a legitimate + * completion type meaning "the sender awaits completion but the + * value carries no information." + */ +export type IsCompletable = + R extends Message + ? [_Success] extends [typeof Schema.Never] + ? false + : true + : never; + +/** + * The full set of decoding/encoding services required by every + * schema referenced by the message. Code generators include this in + * the `R` channel of any effect that handles or sends the message. + */ +export type Services = + R extends Message + ? + | _Payload["DecodingServices"] + | _Payload["EncodingServices"] + | _Success["DecodingServices"] + | _Success["EncodingServices"] + : never; + +/** + * The subset of `Services` actually needed on the client side: + * encoding the payload, decoding the (optional) completion response. + */ +export type ServicesClient = + R extends Message + ? _Payload["EncodingServices"] | _Success["DecodingServices"] + : never; + +/** + * The subset of `Services` needed on the server side: decoding the + * payload, encoding the (optional) completion response. + */ +export type ServicesServer = + R extends Message + ? _Payload["DecodingServices"] | _Success["EncodingServices"] + : never; + +/** + * Extract the message with the matching tag from a union of + * messages. + */ +export type ExtractTag = R extends Message< + Tag, + infer _Payload, + infer _Success +> + ? R + : never; + +/** + * Extract the envelope union for a union of messages. Useful for + * typing an actor's message-queue handler. + */ +export type EnvelopeOf = + R extends Message + ? Envelope<_Tag, _Payload, _Success> + : never; + +// --- Implementation ------------------------------------------------- + +const Proto = { + [TypeId]: TypeId, + pipe() { + // biome-ignore lint/complexity/noArguments: required by Effect's Pipeable contract + return pipeArguments(this, arguments); + }, +}; + +const makeProto = < + const Tag extends string, + Payload extends Schema.Top, + Success extends Schema.Top, +>(options: { + readonly _tag: Tag; + readonly payloadSchema: Payload; + readonly successSchema: Success; + readonly completable: boolean; +}): Message => { + function Message(payload: unknown) { + return { + [EnvelopeTypeId]: EnvelopeTypeId, + _tag: options._tag, + payload: (options.payloadSchema as any).make(payload), + }; + } + Object.setPrototypeOf(Message, Proto); + Object.assign(Message, options); + (Message as any).key = `rivetkit/effect/Message/${options._tag}`; + return Message as any; +}; + +/** + * Define a Rivet Actor message. + * + * Omit `success` for a fire-and-forget message. Provide it (even as + * `Schema.Void`) to make the message completable: the sender awaits + * the actor's `complete` callback and receives the typed value. + * + * @example + * ```ts + * import { Schema } from "effect" + * import { Message } from "@rivetkit/effect" + * + * // Fire-and-forget + * export const Reset = Message.make("Reset", { + * payload: { reason: Schema.String }, + * }) + * + * // Completable + * export const IncrementBy = Message.make("IncrementBy", { + * payload: { amount: Schema.Number }, + * success: Schema.Number, + * }) + * ``` + */ +export const make = < + const Tag extends string, + Payload extends Schema.Top | Schema.Struct.Fields = Schema.Void, + Success extends Schema.Top = typeof Schema.Never, +>( + tag: Tag, + options?: { + readonly payload?: Payload; + readonly success?: Success; + }, +): Message< + Tag, + Payload extends Schema.Struct.Fields ? Schema.Struct : Payload, + Success +> => { + const successSchema = options?.success ?? Schema.Never; + const completable = options?.success !== undefined; + const payloadSchema: Schema.Top = Schema.isSchema(options?.payload) + ? (options?.payload as any) + : options?.payload + ? Schema.Struct(options?.payload as any) + : Schema.Void; + return makeProto({ + _tag: tag, + payloadSchema, + successSchema, + completable, + }) as any; +}; diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index b389639175..c3cd09c270 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1 +1,2 @@ export * as Action from "./Action"; +export * as Message from "./Message"; From 7b9c2120f60da3facaebee3485d0ba18846c96f2 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 28 Apr 2026 12:35:52 +0200 Subject: [PATCH 048/306] refactor(effect): drop per-action defect schema from Action Defects are by definition outside the author's vocabulary, so a per-action defect schema has no meaningful content for the author to fill in. Their wire shape is a single runtime-wide policy owned by rivetkit-core's sanitizer, so the client decoder for defects belongs on the transport, not on each Action. @effect/rpc keeps a per-RPC defectSchema because it serializes the full Cause and lets each endpoint pick its own exposure policy. Rivet's client<->engine boundary is always untrusted and sanitization happens uniformly in core, so the per-action knob collapses to a no-op. --- .../packages/effect/src/Action.ts | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index b8b53b587a..230b08e203 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -3,22 +3,6 @@ import * as Schema from "effect/Schema"; const TypeId = "~@rivetkit/effect/Action"; -/** - * Schema describing the shape of unexpected runtime errors (defects) - * that the action transport may surface. - * - * Defects are sanitized at the runtime boundary, so untrusted clients - * never observe raw stack traces or non-serializable payloads. - */ -export interface DefectSchema extends Schema.Top { - readonly Type: unknown; - make(input: null, options?: Schema.MakeOptions): unknown; - make(input: undefined, options?: Schema.MakeOptions): unknown; - make(input: object, options?: Schema.MakeOptions): unknown; - readonly DecodingServices: never; - readonly EncodingServices: never; -} - /** * A Rivet Actor action: a synchronous request-response call dispatched * on the actor's main loop. @@ -44,7 +28,6 @@ export interface Action< readonly payloadSchema: Payload; readonly successSchema: Success; readonly errorSchema: Error; - readonly defectSchema: Schema.Top; } /** @@ -68,7 +51,6 @@ export interface AnyWithProps extends Pipeable { readonly payloadSchema: Schema.Top; readonly successSchema: Schema.Top; readonly errorSchema: Schema.Top; - readonly defectSchema: Schema.Top; } // --- Type helpers --------------------------------------------------- @@ -180,7 +162,6 @@ const makeProto = < readonly payloadSchema: Payload; readonly successSchema: Success; readonly errorSchema: Error; - readonly defectSchema: Schema.Top; }): Action => { function Action() {} Object.setPrototypeOf(Action, Proto); @@ -220,7 +201,6 @@ export const make = < readonly payload?: Payload; readonly success?: Success; readonly error?: Error; - readonly defect?: DefectSchema; }, ): Action< Tag, @@ -230,7 +210,6 @@ export const make = < > => { const successSchema = options?.success ?? Schema.Void; const errorSchema = options?.error ?? Schema.Never; - const defectSchema = options?.defect ?? Schema.Defect; const payloadSchema: Schema.Top = Schema.isSchema(options?.payload) ? (options?.payload as any) : options?.payload @@ -241,6 +220,5 @@ export const make = < payloadSchema, successSchema, errorSchema, - defectSchema, }) as any; }; From 72b191f5f4a23ce5210381901a1707c5029304a4 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 28 Apr 2026 14:09:07 +0200 Subject: [PATCH 049/306] feat(effect): add Actor contract module --- examples/effect/src/client.ts | 46 +- .../packages/effect/src/Actor.ts | 577 ++++++++++++++++++ .../packages/effect/src/mod.ts | 2 + 3 files changed, 590 insertions(+), 35 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/src/Actor.ts diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index fba1f1052f..131bdfe060 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,20 +1,12 @@ -import { Effect, Layer, Stream } from "effect" +import { Effect, Stream } from "effect" import { ActorTransport } from "@rivetkit/effect" import { Counter, IncrementBy, // ChatRoom, } from "./actors/mod.ts" -// ------------------------------------------------------------------ -// Counter.Client is a Context.Service generated by Actor.make. -// Yielding it adds Counter.Client to R, so the type signature -// of any effect that uses Counter explicitly declares that -// dependency. This allows to track in the type system which actors -// each piece of code depends on. -// ------------------------------------------------------------------ const program = Effect.gen(function* () { - const counterClient = yield* Counter.Client - // R now includes Counter.Client + const counterClient = yield* Counter.client const counter = counterClient.getOrCreate(["counter-123"]) @@ -32,33 +24,17 @@ const program = Effect.gen(function* () { Stream.runForEach((n) => Effect.log(`Changed: ${n}`)), ) }) -// program: Effect +// program: Effect // ^^^^^^^^^^^^^^ -// Missing Counter.Client -> compile error naming the exact actor dependency. +// Missing ActorTransport -> compile error naming the central runtime dependency. // ------------------------------------------------------------------ -// Wiring: each actor's .clientLayer depends on ActorTransport. -// You compose them explicitly, which is more verbose than a -// single transport provide, but gives you two things: -// -// 1. Per-actor transport overrides (different endpoints, tokens, -// or even different Rivet projects per actor). -// -// 2. Selective test mocking. Replace one actor's client layer -// with a fake without touching the others. +// Wiring: provide ActorTransport once. Each actor's .client effect +// uses that transport to create a contract-specific typed accessor. // ------------------------------------------------------------------ -const ActorClientLayer = Layer.mergeAll( - Counter.clientLayer, -// ChatRoom.clientLayer, -).pipe( - // Both client layers share the same transport here, but you - // could provide different transports to each. - Layer.provide( - ActorTransport.layer({ - endpoint: "https://api.rivet.dev", - token: "...", - }), - ), -) +const TransportLayer = ActorTransport.layer({ + endpoint: "https://api.rivet.dev", + token: "...", +}) -program.pipe(Effect.provide(ActorClientLayer), Effect.runPromise) +program.pipe(Effect.provide(TransportLayer), Effect.runPromise) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts new file mode 100644 index 0000000000..652f61b200 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -0,0 +1,577 @@ +import * as Context from "effect/Context"; +import type * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { type Pipeable, pipeArguments } from "effect/Pipeable"; +import type * as PubSub from "effect/PubSub"; +import type * as Queue from "effect/Queue"; +import * as Schema from "effect/Schema"; +import type * as Scope from "effect/Scope"; +import type * as Stream from "effect/Stream"; +import type * as SubscriptionRef from "effect/SubscriptionRef"; +import type * as Action from "./Action"; +import type * as Message from "./Message"; + +const TypeId = "~@rivetkit/effect/Actor"; + +/** + * Schemas keyed by the event names an actor can publish. + */ +export type EventSchemas = Record; + +/** + * Display and runtime options carried by an actor contract. + */ +export interface Options { + readonly name?: string; + readonly icon?: string; + readonly maxQueueSize?: number; + readonly maxQueueMessageSize?: number; +} + +/** + * Initial implementation uses Effect's SubscriptionRef directly. The + * persisted variant is defined by a separate module. + */ +export type StateRef = SubscriptionRef.SubscriptionRef; + +/** + * Effect-shaped KV service available inside an actor wake scope. + */ +export interface KvStore { + readonly get: ( + key: string | Uint8Array, + ) => Effect.Effect; + readonly put: ( + key: string | Uint8Array, + value: string | Uint8Array | ArrayBuffer, + ) => Effect.Effect; + readonly delete: (key: string | Uint8Array) => Effect.Effect; + readonly batchGet: ( + keys: ReadonlyArray, + ) => Effect.Effect>; + readonly batchPut: ( + entries: ReadonlyArray, + ) => Effect.Effect; + readonly batchDelete: ( + keys: ReadonlyArray, + ) => Effect.Effect; + readonly deleteRange: ( + start: Uint8Array, + end: Uint8Array, + ) => Effect.Effect; +} + +/** + * Minimal Effect-shaped database service available inside an actor wake scope. + */ +export interface DbClient { + readonly execute: >( + query: string, + ...args: ReadonlyArray + ) => Effect.Effect>; +} + +export interface KvService { + readonly _: unique symbol; +} + +export interface DbService { + readonly _: unique symbol; +} + +export interface RegistryShape { + readonly _: unique symbol; +} + +export interface ActorTransportOptions { + readonly endpoint: string; + readonly token?: string; +} + +export interface ActorTransportShape extends ActorTransportOptions { + readonly _: unique symbol; +} + +export interface StateService { + readonly _: unique symbol; + readonly name: Name; +} + +export interface EventsService { + readonly _: unique symbol; + readonly name: Name; +} + +export interface MessagesService { + readonly _: unique symbol; + readonly name: Name; +} + +export const Kv: Context.Service = Context.Service( + "@rivetkit/effect/Actor/Kv", +); + +export const Db: Context.Service = Context.Service( + "@rivetkit/effect/Actor/Db", +); + +export class Registry extends Context.Service()( + "@rivetkit/effect/Actor/Registry", +) {} + +export class ActorTransport extends Context.Service< + ActorTransport, + ActorTransportShape +>()("@rivetkit/effect/Actor/ActorTransport") { + static layer(options: ActorTransportOptions): Layer.Layer { + return Layer.succeed(ActorTransport, { + ...options, + _: undefined as never, + }); + } +} + +export type EventPubSubMap = { + readonly [Name in keyof Events & string]: PubSub.PubSub; +}; + +type EventDecodeServices = { + readonly [Name in keyof Events]: Events[Name]["DecodingServices"]; +}[keyof Events]; + +type EventEncodeServices = { + readonly [Name in keyof Events]: Events[Name]["EncodingServices"]; +}[keyof Events]; + +type CompleteArgs = undefined extends A + ? readonly [value?: A] + : readonly [value: A]; + +export type MessageQueueItem = + M extends Message.Message + ? { + readonly _tag: Tag; + readonly message: M; + readonly payload: Payload["Type"]; + } & ([Success] extends [typeof Schema.Never] + ? object + : { + readonly complete: ( + ...args: CompleteArgs + ) => Effect.Effect; + }) + : never; + +export type ActionRequest = + A extends Action.Action + ? { + readonly _tag: Tag; + readonly action: A; + readonly payload: Payload["Type"]; + } + : never; + +export type ActionHandlers = { + readonly [Tag in Action.Tag]: ( + request: ActionRequest>, + ) => Effect.Effect< + Action.Success>, + Action.Error>, + any + >; +}; + +type HandlerServices = { + readonly [Name in keyof Handlers]: Handlers[Name] extends ( + ...args: ReadonlyArray + ) => Effect.Effect + ? R + : never; +}[keyof Handlers]; + +export interface AbortSignalLike { + readonly aborted: boolean; + readonly reason?: unknown; +} + +export interface CallOptions { + readonly signal?: AbortSignalLike; +} + +export type ActorKey = string | ReadonlyArray; + +export interface GetOptions { + readonly params?: unknown; + readonly getParams?: () => Effect.Effect; + readonly signal?: AbortSignalLike; +} + +export interface GetOrCreateOptions extends GetOptions { + readonly createInRegion?: string; + readonly createWithInput?: unknown; +} + +export interface CreateOptions extends GetOptions { + readonly region?: string; + readonly input?: unknown; +} + +type ActionClientArgs = [ + Action.PayloadConstructor, +] extends [void] + ? readonly [payload?: Action.PayloadConstructor, options?: CallOptions] + : readonly [payload: Action.PayloadConstructor, options?: CallOptions]; + +type ActionClientMethod = ( + ...args: ActionClientArgs +) => Effect.Effect, Action.Error>; + +type EnvelopeSuccess = E extends Message.Envelope< + string, + Schema.Top, + infer Success +> + ? [Success] extends [typeof Schema.Never] + ? void + : Success["Type"] + : never; + +export type ActorHandle< + Actions extends Action.AnyWithProps, + Messages extends Message.AnyWithProps, + Events extends EventSchemas, +> = { + readonly [Tag in Action.Tag]: ActionClientMethod< + Action.ExtractTag + >; +} & { + readonly send: >( + envelope: Envelope, + options?: CallOptions, + ) => Effect.Effect>; + readonly subscribe: ( + name: Name, + ) => Stream.Stream; +}; + +export interface ActorClient< + Actions extends Action.AnyWithProps, + Messages extends Message.AnyWithProps, + Events extends EventSchemas, +> { + readonly get: ( + key?: ActorKey, + options?: GetOptions, + ) => ActorHandle; + readonly getOrCreate: ( + key?: ActorKey, + options?: GetOrCreateOptions, + ) => ActorHandle; + readonly getForId: ( + actorId: string, + options?: GetOptions, + ) => ActorHandle; + readonly create: ( + key?: ActorKey, + options?: CreateOptions, + ) => Effect.Effect>; +} + +/** + * A Rivet Actor contract. It carries schemas and generated Effect service + * tags, but no server implementation. + */ +export interface Actor< + Name extends string, + State extends Schema.Top = Schema.Void, + Actions extends Action.AnyWithProps = never, + Messages extends Message.AnyWithProps = never, + Events extends EventSchemas = {}, +> extends Pipeable { + readonly [TypeId]: typeof TypeId; + readonly _tag: Name; + readonly key: string; + readonly stateSchema: State; + readonly actions: ReadonlyArray; + readonly messages: ReadonlyArray; + readonly events: Events; + readonly options: Options; + readonly annotations: Context.Context; + readonly State: Context.Service, StateRef>; + readonly Events: Context.Service, EventPubSubMap>; + readonly Messages: Context.Service< + MessagesService, + Queue.Dequeue> + >; + readonly client: Effect.Effect< + ActorClient, + never, + ActorTransport | ClientServices> + >; + + of>(handlers: Handlers): Handlers; + + toLayer, RX = never>( + build: Handlers | Effect.Effect, + ): Layer.Layer< + never, + never, + | Exclude< + RX, + | StateService + | EventsService + | MessagesService + | KvService + | DbService + | Scope.Scope + > + | HandlerServices + | Action.ServicesServer + | Action.ServicesClient + | Message.ServicesServer + | Message.ServicesClient + | Registry + >; + + annotate( + tag: Context.Key, + value: S, + ): Actor; + + annotateMerge( + annotations: Context.Context, + ): Actor; +} + +/** + * Type-erased view of any actor contract. + */ +export interface Any extends Pipeable { + readonly [TypeId]: typeof TypeId; + readonly _tag: string; + readonly key: string; +} + +/** + * Type-erased actor with all runtime properties available. + */ +export interface AnyWithProps + extends Actor< + string, + Schema.Top, + Action.AnyWithProps, + Message.AnyWithProps, + EventSchemas + > {} + +export type Name = A extends Actor + ? _Name + : never; + +export type StateSchema = A extends Actor + ? _State + : never; + +export type State = StateSchema["Type"]; + +export type Actions = A extends Actor + ? _Actions + : never; + +export type Messages = A extends Actor + ? _Messages + : never; + +export type Events = A extends Actor + ? _Events + : never; + +export type EventName = keyof Events & string; + +export type EventPayload< + A, + Name extends EventName, +> = Events[Name]["Type"]; + +export type ProvidedServices = A extends Actor< + infer _Name, + any, + any, + any, + any +> + ? + | StateService<_Name> + | EventsService<_Name> + | MessagesService<_Name> + | KvService + | DbService + : never; + +export type Services = A extends Actor< + any, + infer _State, + infer _Actions, + infer _Messages, + infer _Events +> + ? + | _State["DecodingServices"] + | _State["EncodingServices"] + | Action.Services<_Actions> + | Message.Services<_Messages> + | EventDecodeServices<_Events> + | EventEncodeServices<_Events> + : never; + +export type ClientServices = A extends Actor< + any, + any, + infer _Actions, + infer _Messages, + infer _Events +> + ? + | Action.ServicesClient<_Actions> + | Message.ServicesClient<_Messages> + | EventDecodeServices<_Events> + : never; + +export type ServerServices = A extends Actor< + any, + infer _State, + infer _Actions, + infer _Messages, + infer _Events +> + ? + | _State["DecodingServices"] + | _State["EncodingServices"] + | Action.ServicesServer<_Actions> + | Message.ServicesServer<_Messages> + | EventEncodeServices<_Events> + : never; + +export const isActor = (u: unknown): u is Any => + typeof u === "object" && u !== null && TypeId in u; + +const identity = (value: A): A => value; + +const Proto = { + [TypeId]: TypeId, + pipe() { + // biome-ignore lint/complexity/noArguments: required by Effect's Pipeable contract + return pipeArguments(this, arguments); + }, + of: identity, + toLayer(this: AnyWithProps) { + throw new Error( + `Actor.toLayer for ${this._tag} is not yet implemented. Registry runtime wiring is pending.`, + ); + }, + get client(): never { + const self = this as unknown as AnyWithProps; + throw new Error( + `Actor.client for ${self._tag} is not yet implemented. ActorTransport runtime wiring is pending.`, + ); + }, + annotate(this: AnyWithProps, tag: Context.Key, value: any) { + return makeProto({ + _tag: this._tag, + stateSchema: this.stateSchema, + actions: this.actions, + messages: this.messages, + events: this.events, + options: this.options, + annotations: Context.add(this.annotations, tag, value), + }); + }, + annotateMerge(this: AnyWithProps, annotations: Context.Context) { + return makeProto({ + _tag: this._tag, + stateSchema: this.stateSchema, + actions: this.actions, + messages: this.messages, + events: this.events, + options: this.options, + annotations: Context.merge(this.annotations, annotations), + }); + }, +}; + +const makeProto = < + const Name extends string, + State extends Schema.Top, + Actions extends Action.AnyWithProps, + Messages extends Message.AnyWithProps, + Events extends EventSchemas, +>(options: { + readonly _tag: Name; + readonly stateSchema: State; + readonly actions: ReadonlyArray; + readonly messages: ReadonlyArray; + readonly events: Events; + readonly options: Options; + readonly annotations: Context.Context; +}): Actor => { + const key = `rivetkit/effect/Actor/${options._tag}`; + const StateTag = Context.Service, StateRef>( + `${key}/State`, + ); + const EventsTag = Context.Service< + EventsService, + EventPubSubMap + >(`${key}/Events`); + const MessagesTag = Context.Service< + MessagesService, + Queue.Dequeue> + >(`${key}/Messages`); + return Object.assign(Object.create(Proto), { + ...options, + key, + State: StateTag, + Events: EventsTag, + Messages: MessagesTag, + }) as Actor; +}; + +/** + * Define a Rivet Actor contract. + */ +export const make = < + const Name extends string, + State extends Schema.Top | Schema.Struct.Fields = Schema.Void, + const Actions extends ReadonlyArray = readonly [], + const Messages extends ReadonlyArray = readonly [], + const Events extends EventSchemas = {}, +>( + name: Name, + options?: { + readonly state?: State; + readonly actions?: Actions; + readonly messages?: Messages; + readonly events?: Events; + readonly options?: Options; + }, +): Actor< + Name, + State extends Schema.Struct.Fields ? Schema.Struct : State, + Actions[number], + Messages[number], + Events +> => { + const stateSchema: Schema.Top = Schema.isSchema(options?.state) + ? (options?.state as any) + : options?.state + ? Schema.Struct(options?.state as any) + : Schema.Void; + return makeProto({ + _tag: name, + stateSchema, + actions: (options?.actions ?? []) as ReadonlyArray, + messages: (options?.messages ?? []) as ReadonlyArray, + events: (options?.events ?? {}) as EventSchemas, + options: options?.options ?? {}, + annotations: Context.empty(), + }) as any; +}; diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index c3cd09c270..83b6db8a1c 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,2 +1,4 @@ +export * as Actor from "./Actor"; +export { ActorTransport, Registry } from "./Actor"; export * as Action from "./Action"; export * as Message from "./Message"; From 18c645f1d6166d5962ba7026ee980bbc5f8e93e7 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 28 Apr 2026 14:14:24 +0200 Subject: [PATCH 050/306] refactor(effect): rename actor transport to client --- examples/effect/src/client.ts | 14 +++++++------- .../packages/effect/src/Actor.ts | 19 +++++++++---------- .../packages/effect/src/mod.ts | 2 +- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 131bdfe060..aee75c696d 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,5 +1,5 @@ import { Effect, Stream } from "effect" -import { ActorTransport } from "@rivetkit/effect" +import { Client } from "@rivetkit/effect" import { Counter, IncrementBy, // ChatRoom, @@ -24,17 +24,17 @@ const program = Effect.gen(function* () { Stream.runForEach((n) => Effect.log(`Changed: ${n}`)), ) }) -// program: Effect -// ^^^^^^^^^^^^^^ -// Missing ActorTransport -> compile error naming the central runtime dependency. +// program: Effect +// ^^^^^^ +// Missing Client -> compile error naming the central runtime dependency. // ------------------------------------------------------------------ -// Wiring: provide ActorTransport once. Each actor's .client effect +// Wiring: provide Client once. Each actor's .client effect // uses that transport to create a contract-specific typed accessor. // ------------------------------------------------------------------ -const TransportLayer = ActorTransport.layer({ +const ClientLayer = Client.layer({ endpoint: "https://api.rivet.dev", token: "...", }) -program.pipe(Effect.provide(TransportLayer), Effect.runPromise) +program.pipe(Effect.provide(ClientLayer), Effect.runPromise) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 652f61b200..e7a8e0774c 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -83,12 +83,12 @@ export interface RegistryShape { readonly _: unique symbol; } -export interface ActorTransportOptions { +export interface ClientOptions { readonly endpoint: string; readonly token?: string; } -export interface ActorTransportShape extends ActorTransportOptions { +export interface ClientShape extends ClientOptions { readonly _: unique symbol; } @@ -119,12 +119,11 @@ export class Registry extends Context.Service()( "@rivetkit/effect/Actor/Registry", ) {} -export class ActorTransport extends Context.Service< - ActorTransport, - ActorTransportShape ->()("@rivetkit/effect/Actor/ActorTransport") { - static layer(options: ActorTransportOptions): Layer.Layer { - return Layer.succeed(ActorTransport, { +export class Client extends Context.Service()( + "@rivetkit/effect/Client", +) { + static layer(options: ClientOptions): Layer.Layer { + return Layer.succeed(Client, { ...options, _: undefined as never, }); @@ -306,7 +305,7 @@ export interface Actor< readonly client: Effect.Effect< ActorClient, never, - ActorTransport | ClientServices> + Client | ClientServices> >; of>(handlers: Handlers): Handlers; @@ -472,7 +471,7 @@ const Proto = { get client(): never { const self = this as unknown as AnyWithProps; throw new Error( - `Actor.client for ${self._tag} is not yet implemented. ActorTransport runtime wiring is pending.`, + `Actor.client for ${self._tag} is not yet implemented. Client runtime wiring is pending.`, ); }, annotate(this: AnyWithProps, tag: Context.Key, value: any) { diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index 83b6db8a1c..a413e99130 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,4 +1,4 @@ export * as Actor from "./Actor"; -export { ActorTransport, Registry } from "./Actor"; +export { Client, Registry } from "./Actor"; export * as Action from "./Action"; export * as Message from "./Message"; From 641b17d5b5fb593be716b6bc6b0d16daa0372370 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 28 Apr 2026 14:42:50 +0200 Subject: [PATCH 051/306] refactor(effect): scope contract keys under @rivetkit/effect --- rivetkit-typescript/packages/effect/src/Action.ts | 2 +- rivetkit-typescript/packages/effect/src/Actor.ts | 2 +- rivetkit-typescript/packages/effect/src/Message.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index 230b08e203..5578a7f962 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -166,7 +166,7 @@ const makeProto = < function Action() {} Object.setPrototypeOf(Action, Proto); Object.assign(Action, options); - Action.key = `rivetkit/effect/Action/${options._tag}`; + Action.key = `@rivetkit/effect/Action/${options._tag}`; return Action as any; }; diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index e7a8e0774c..1cdc566a08 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -513,7 +513,7 @@ const makeProto = < readonly options: Options; readonly annotations: Context.Context; }): Actor => { - const key = `rivetkit/effect/Actor/${options._tag}`; + const key = `@rivetkit/effect/Actor/${options._tag}`; const StateTag = Context.Service, StateRef>( `${key}/State`, ); diff --git a/rivetkit-typescript/packages/effect/src/Message.ts b/rivetkit-typescript/packages/effect/src/Message.ts index 9ca92ac94a..699faf6517 100644 --- a/rivetkit-typescript/packages/effect/src/Message.ts +++ b/rivetkit-typescript/packages/effect/src/Message.ts @@ -255,7 +255,7 @@ const makeProto = < } Object.setPrototypeOf(Message, Proto); Object.assign(Message, options); - (Message as any).key = `rivetkit/effect/Message/${options._tag}`; + (Message as any).key = `@rivetkit/effect/Message/${options._tag}`; return Message as any; }; From 667e257e3b7ce26de6ba97d299b7eaa89dadca82 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 28 Apr 2026 15:06:59 +0200 Subject: [PATCH 052/306] refactor(effect): remove actor annotations --- .../packages/effect/src/Actor.ts | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 1cdc566a08..8cd44397d0 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -295,7 +295,6 @@ export interface Actor< readonly messages: ReadonlyArray; readonly events: Events; readonly options: Options; - readonly annotations: Context.Context; readonly State: Context.Service, StateRef>; readonly Events: Context.Service, EventPubSubMap>; readonly Messages: Context.Service< @@ -331,15 +330,6 @@ export interface Actor< | Message.ServicesClient | Registry >; - - annotate( - tag: Context.Key, - value: S, - ): Actor; - - annotateMerge( - annotations: Context.Context, - ): Actor; } /** @@ -474,28 +464,6 @@ const Proto = { `Actor.client for ${self._tag} is not yet implemented. Client runtime wiring is pending.`, ); }, - annotate(this: AnyWithProps, tag: Context.Key, value: any) { - return makeProto({ - _tag: this._tag, - stateSchema: this.stateSchema, - actions: this.actions, - messages: this.messages, - events: this.events, - options: this.options, - annotations: Context.add(this.annotations, tag, value), - }); - }, - annotateMerge(this: AnyWithProps, annotations: Context.Context) { - return makeProto({ - _tag: this._tag, - stateSchema: this.stateSchema, - actions: this.actions, - messages: this.messages, - events: this.events, - options: this.options, - annotations: Context.merge(this.annotations, annotations), - }); - }, }; const makeProto = < @@ -511,7 +479,6 @@ const makeProto = < readonly messages: ReadonlyArray; readonly events: Events; readonly options: Options; - readonly annotations: Context.Context; }): Actor => { const key = `@rivetkit/effect/Actor/${options._tag}`; const StateTag = Context.Service, StateRef>( @@ -571,6 +538,5 @@ export const make = < messages: (options?.messages ?? []) as ReadonlyArray, events: (options?.events ?? {}) as EventSchemas, options: options?.options ?? {}, - annotations: Context.empty(), }) as any; }; From df7c392a3301cca1f58d48a25d7fd4a45393c139 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 28 Apr 2026 15:09:19 +0200 Subject: [PATCH 053/306] feat(effect): add type guards for Action, Message, and Actor --- rivetkit-typescript/packages/effect/src/Action.ts | 4 ++++ rivetkit-typescript/packages/effect/src/Actor.ts | 7 ++++--- rivetkit-typescript/packages/effect/src/Message.ts | 4 ++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index 5578a7f962..34203c8d8a 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -1,8 +1,12 @@ import { type Pipeable, pipeArguments } from "effect/Pipeable"; +import * as Predicate from "effect/Predicate"; import * as Schema from "effect/Schema"; const TypeId = "~@rivetkit/effect/Action"; +export const isAction = (u: unknown): u is Action => + Predicate.hasProperty(u, TypeId); + /** * A Rivet Actor action: a synchronous request-response call dispatched * on the actor's main loop. diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 8cd44397d0..021db6fb55 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -2,6 +2,7 @@ import * as Context from "effect/Context"; import type * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import { type Pipeable, pipeArguments } from "effect/Pipeable"; +import * as Predicate from "effect/Predicate"; import type * as PubSub from "effect/PubSub"; import type * as Queue from "effect/Queue"; import * as Schema from "effect/Schema"; @@ -13,6 +14,9 @@ import type * as Message from "./Message"; const TypeId = "~@rivetkit/effect/Actor"; +export const isActor = (u: unknown): u is Actor => + Predicate.hasProperty(u, TypeId); + /** * Schemas keyed by the event names an actor can publish. */ @@ -441,9 +445,6 @@ export type ServerServices = A extends Actor< | EventEncodeServices<_Events> : never; -export const isActor = (u: unknown): u is Any => - typeof u === "object" && u !== null && TypeId in u; - const identity = (value: A): A => value; const Proto = { diff --git a/rivetkit-typescript/packages/effect/src/Message.ts b/rivetkit-typescript/packages/effect/src/Message.ts index 699faf6517..869f3c3dfa 100644 --- a/rivetkit-typescript/packages/effect/src/Message.ts +++ b/rivetkit-typescript/packages/effect/src/Message.ts @@ -1,10 +1,14 @@ import { type Pipeable, pipeArguments } from "effect/Pipeable"; +import * as Predicate from "effect/Predicate"; import * as Schema from "effect/Schema"; const TypeId = "~@rivetkit/effect/Message"; const EnvelopeTypeId = "~@rivetkit/effect/Message/Envelope"; +export const isMessage = (u: unknown): u is Message => + Predicate.hasProperty(u, TypeId); + /** * A Rivet Actor message: a durable, queued operation that the actor * processes asynchronously on its main loop. From 2ddffd7417cfcd617303121472599c1aab52c1e8 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 28 Apr 2026 15:23:08 +0200 Subject: [PATCH 054/306] refactor(effect): simplify Message key assignment --- rivetkit-typescript/packages/effect/src/Message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/Message.ts b/rivetkit-typescript/packages/effect/src/Message.ts index 869f3c3dfa..537c163d50 100644 --- a/rivetkit-typescript/packages/effect/src/Message.ts +++ b/rivetkit-typescript/packages/effect/src/Message.ts @@ -259,7 +259,7 @@ const makeProto = < } Object.setPrototypeOf(Message, Proto); Object.assign(Message, options); - (Message as any).key = `@rivetkit/effect/Message/${options._tag}`; + Message.key = `@rivetkit/effect/Message/${options._tag}`; return Message as any; }; From 5eb96681ff268ff24a562347852b9588449f8e4d Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 28 Apr 2026 15:51:14 +0200 Subject: [PATCH 055/306] refactor(effect): simplify Action interface documentation --- rivetkit-typescript/packages/effect/src/Action.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index 34203c8d8a..89ed821f44 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -8,15 +8,7 @@ export const isAction = (u: unknown): u is Action => Predicate.hasProperty(u, TypeId); /** - * A Rivet Actor action: a synchronous request-response call dispatched - * on the actor's main loop. - * - * @remarks - * - * `Action` is a value-level definition that carries the wire schemas - * for the request payload, the success response, and the typed error - * channel. The action's implementation lives in the actor's handler - * map; this type only describes the contract. + * A value-level definition for a non-durable, request-response call. */ export interface Action< in out Tag extends string, From bf8d2787eeeef61ac85c802507447ba2847f592e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 28 Apr 2026 16:50:06 +0200 Subject: [PATCH 056/306] feat(effect): extend counter example with ClientError type --- examples/effect/src/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index aee75c696d..2e040f276f 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -11,7 +11,7 @@ const program = Effect.gen(function* () { const counter = counterClient.getOrCreate(["counter-123"]) // Action calls return Effects with types inferred from the schema. - // counter.Increment: (payload: { amount: number }) => Effect + // counter.Increment: (payload: { amount: number }) => Effect const count = yield* counter.Increment({ amount: 5 }) yield* Effect.log(`Count: ${count}`) @@ -24,7 +24,7 @@ const program = Effect.gen(function* () { Stream.runForEach((n) => Effect.log(`Changed: ${n}`)), ) }) -// program: Effect +// program: Effect // ^^^^^^ // Missing Client -> compile error naming the central runtime dependency. From a7d29dd81627a4375285db266a5fd8f45957f74e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 29 Apr 2026 15:54:03 +0200 Subject: [PATCH 057/306] refactor(effect): drop unused Pipeable from Action No combinators consume the Pipeable contract on Action, so the extension and `pipe()` method are dead weight. --- rivetkit-typescript/packages/effect/src/Action.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index 89ed821f44..096906b53a 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -1,4 +1,3 @@ -import { type Pipeable, pipeArguments } from "effect/Pipeable"; import * as Predicate from "effect/Predicate"; import * as Schema from "effect/Schema"; @@ -15,7 +14,7 @@ export interface Action< out Payload extends Schema.Top = Schema.Void, out Success extends Schema.Top = Schema.Void, out Error extends Schema.Top = Schema.Never, -> extends Pipeable { +> { new (_: never): object; readonly [TypeId]: typeof TypeId; @@ -30,7 +29,7 @@ export interface Action< * Type-erased view of any `Action`. Useful for collections of actions * where the specific schemas don't matter. */ -export interface Any extends Pipeable { +export interface Any { readonly [TypeId]: typeof TypeId; readonly _tag: string; readonly key: string; @@ -40,7 +39,7 @@ export interface Any extends Pipeable { * Like `Any`, but with the prop fields (`*Schema`) accessible. Used * by internal builders that need to read schemas off an action. */ -export interface AnyWithProps extends Pipeable { +export interface AnyWithProps { readonly [TypeId]: typeof TypeId; readonly _tag: string; readonly key: string; @@ -142,10 +141,6 @@ export type ExtractTag = R extends Action< const Proto = { [TypeId]: TypeId, - pipe() { - // biome-ignore lint/complexity/noArguments: required by Effect's Pipeable contract - return pipeArguments(this, arguments); - }, }; const makeProto = < From 81cd9c58400d54171126b7c6dd8edadb876079ff Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 29 Apr 2026 16:05:44 +0200 Subject: [PATCH 058/306] refactor(effect): drop function-as-instance carrier from Action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the `function Action() {} + Object.setPrototypeOf` carrier and the matching `new (_: never): object` interface signature with the standard `Object.create(Proto)` + `Object.assign` pattern used by HttpApiEndpoint, Event, and other v4 modules. The function form was a nominal-brand trick inherited from Rpc, but Action instances are never invoked or used as constructors — only the `[TypeId]` brand is needed for identity. Removes two `as any` casts and a misleading construct signature. --- .../packages/effect/src/Action.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index 096906b53a..05cc41d212 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -15,8 +15,6 @@ export interface Action< out Success extends Schema.Top = Schema.Void, out Error extends Schema.Top = Schema.Never, > { - new (_: never): object; - readonly [TypeId]: typeof TypeId; readonly _tag: Tag; readonly key: string; @@ -154,11 +152,9 @@ const makeProto = < readonly successSchema: Success; readonly errorSchema: Error; }): Action => { - function Action() {} - Object.setPrototypeOf(Action, Proto); - Object.assign(Action, options); - Action.key = `@rivetkit/effect/Action/${options._tag}`; - return Action as any; + const self = Object.assign(Object.create(Proto), options); + self.key = `@rivetkit/effect/Action/${options._tag}`; + return self; }; /** @@ -211,5 +207,10 @@ export const make = < payloadSchema, successSchema, errorSchema, - }) as any; + }) as Action< + Tag, + Payload extends Schema.Struct.Fields ? Schema.Struct : Payload, + Success, + Error + >; }; From 7640c0bb4669730004e63fe5076774b732ede7d1 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 29 Apr 2026 16:08:58 +0200 Subject: [PATCH 059/306] refactor(effect): drop unused Pipeable from Message and Actor No combinators consume the Pipeable contract on either type, so the extension and `pipe()` method are dead weight. --- rivetkit-typescript/packages/effect/src/Actor.ts | 9 ++------- rivetkit-typescript/packages/effect/src/Message.ts | 11 +++-------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 021db6fb55..127ce00f9f 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -1,7 +1,6 @@ import * as Context from "effect/Context"; import type * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import { type Pipeable, pipeArguments } from "effect/Pipeable"; import * as Predicate from "effect/Predicate"; import type * as PubSub from "effect/PubSub"; import type * as Queue from "effect/Queue"; @@ -290,7 +289,7 @@ export interface Actor< Actions extends Action.AnyWithProps = never, Messages extends Message.AnyWithProps = never, Events extends EventSchemas = {}, -> extends Pipeable { +> { readonly [TypeId]: typeof TypeId; readonly _tag: Name; readonly key: string; @@ -339,7 +338,7 @@ export interface Actor< /** * Type-erased view of any actor contract. */ -export interface Any extends Pipeable { +export interface Any { readonly [TypeId]: typeof TypeId; readonly _tag: string; readonly key: string; @@ -449,10 +448,6 @@ const identity = (value: A): A => value; const Proto = { [TypeId]: TypeId, - pipe() { - // biome-ignore lint/complexity/noArguments: required by Effect's Pipeable contract - return pipeArguments(this, arguments); - }, of: identity, toLayer(this: AnyWithProps) { throw new Error( diff --git a/rivetkit-typescript/packages/effect/src/Message.ts b/rivetkit-typescript/packages/effect/src/Message.ts index 537c163d50..0ba59915cc 100644 --- a/rivetkit-typescript/packages/effect/src/Message.ts +++ b/rivetkit-typescript/packages/effect/src/Message.ts @@ -1,4 +1,3 @@ -import { type Pipeable, pipeArguments } from "effect/Pipeable"; import * as Predicate from "effect/Predicate"; import * as Schema from "effect/Schema"; @@ -61,7 +60,7 @@ export interface Message< in out Tag extends string, in out Payload extends Schema.Top = Schema.Void, out Success extends Schema.Top = Schema.Never, -> extends Pipeable { +> { new (_: never): object; (payload: Payload["~type.make.in"]): Envelope; @@ -102,7 +101,7 @@ export interface Envelope< * Type-erased view of any `Message`. Useful for collections of * messages where the specific schemas don't matter. */ -export interface Any extends Pipeable { +export interface Any { readonly [TypeId]: typeof TypeId; readonly _tag: string; readonly key: string; @@ -113,7 +112,7 @@ export interface Any extends Pipeable { * Like `Any`, but with the prop fields (`*Schema`) accessible. Used * by internal builders that need to read schemas off a message. */ -export interface AnyWithProps extends Pipeable { +export interface AnyWithProps { readonly [TypeId]: typeof TypeId; readonly _tag: string; readonly key: string; @@ -234,10 +233,6 @@ export type EnvelopeOf = const Proto = { [TypeId]: TypeId, - pipe() { - // biome-ignore lint/complexity/noArguments: required by Effect's Pipeable contract - return pipeArguments(this, arguments); - }, }; const makeProto = < From 0f3ce01532ecaa912463f4dc122a1031d190cef1 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 29 Apr 2026 16:15:47 +0200 Subject: [PATCH 060/306] refactor(effect): drop redundant construct signature from Message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `new (_: never): object` nominal-brand trick was inherited from Rpc, but Message already has a call signature `(payload) => Envelope` that requires the runtime carrier to be a function and provides structural distinctness. The construct signature added nothing on top and falsely implied Message could be `new`-ed. The function carrier in makeProto stays — Message values are genuinely callable. --- rivetkit-typescript/packages/effect/src/Message.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Message.ts b/rivetkit-typescript/packages/effect/src/Message.ts index 0ba59915cc..0df510a304 100644 --- a/rivetkit-typescript/packages/effect/src/Message.ts +++ b/rivetkit-typescript/packages/effect/src/Message.ts @@ -61,8 +61,6 @@ export interface Message< in out Payload extends Schema.Top = Schema.Void, out Success extends Schema.Top = Schema.Never, > { - new (_: never): object; - (payload: Payload["~type.make.in"]): Envelope; readonly [TypeId]: typeof TypeId; From c4e25aa37ca4429faece717c7b5db746ed62e15f Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 30 Apr 2026 10:45:16 +0200 Subject: [PATCH 061/306] feat(effect): scope SDK v1 to actions only Drop State, Events, Messages, Kv, Db, lifecycle hooks, scheduling, auth, per-action middleware, and TestRegistry from the first version. Keep Actor.make options trimmed to name + icon. Counter example uses an in-memory per-wake Ref with a finalizer that logs the value on sleep. Rewrite ActionHandlers and ActorHandle to iterate over the action union directly so per-tag payload types stay narrow. --- examples/effect/src/actors/counter/api.ts | 32 +- examples/effect/src/actors/counter/live.ts | 93 +---- examples/effect/src/client.ts | 18 +- examples/effect/src/main.ts | 6 +- .../packages/effect/src/Action.ts | 9 +- .../packages/effect/src/Actor.ts | 361 ++---------------- .../packages/effect/src/Message.ts | 311 --------------- .../packages/effect/src/mod.ts | 1 - 8 files changed, 73 insertions(+), 758 deletions(-) delete mode 100644 rivetkit-typescript/packages/effect/src/Message.ts diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts index b7db1bacb1..0423916d68 100644 --- a/examples/effect/src/actors/counter/api.ts +++ b/examples/effect/src/actors/counter/api.ts @@ -1,5 +1,5 @@ -import { Effect, Schema } from "effect" -import { Actor, Action, Message } from "@rivetkit/effect" +import { Schema } from "effect" +import { Actor, Action } from "@rivetkit/effect" // --- Errors --- @@ -41,37 +41,15 @@ export const GetCount = Action.make("GetCount", { success: Schema.Number, }) -// --- Messages --- - -// Non-completable (fire-and-forget) -export const Reset = Message.make("Reset", { - payload: { reason: Schema.String }, -}) - -// Completable (sender can await a typed response) -export const IncrementBy = Message.make("IncrementBy", { - payload: { amount: Schema.Number }, - success: Schema.Number, -}) - // --- Actor Definition --- // The definition is the actor's public contract. It carries no // implementation. Both server and client code import this; // the implementation stays server-only. export const Counter = Actor.make("Counter", { - state: Schema.Struct({ - count: Schema.Number.pipe( - Schema.withConstructorDefault(Effect.succeed(0)), - ), - }), - actions: [Increment, GetCount], // synchronous request-response - messages: [Reset, IncrementBy], // durable, queued, background - events: { countChanged: Schema.Number }, + actions: [Increment, GetCount], options: { - name: "Counter", // Human-friendly display name - icon: "comments", // FontAwesome icon name - maxQueueSize: 1000, // Max number of pending messages - maxQueueMessageSize: 64 * 1024, // Max bytes per message + name: "Counter", // Human-friendly display name + icon: "comments", // FontAwesome icon name }, }) diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index 214efce288..163ad0ebb5 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -1,5 +1,4 @@ -import { Effect, Queue, Ref, PubSub, Match } from "effect" -import { Actor, PersistedSubscriptionRef } from "@rivetkit/effect" +import { Effect, Ref } from "effect" import { Counter, CounterOverflowError } from "./api.ts" // --- Actor Implementation --- @@ -7,91 +6,37 @@ import { Counter, CounterOverflowError } from "./api.ts" // Counter.toLayer produces a Layer that registers this actor // with whatever registry is in context. The Effect inside runs // once per actor instance (not once per action call), so -// yielded services like State and Events are instance-scoped. +// yielded refs are instance-scoped and survive across action +// calls within a wake. Finalizers run on sleep. export const CounterLive = Counter.toLayer( // Wake scope (runs each wake, finalizers run on sleep) Effect.gen(function* () { - // Actor-provided services are yielded from the Effect context. - // They are scoped to this actor instance, not to individual - // action calls. This means all action handlers below close - // over the same state, events, kv, and db references. - // - // Because services come through the context (not a context - // parameter like the current SDK's `c`), they are: - // - // - Visible in the type signature. The Effect's R channel - // declares exactly which services are required. - // - // - Swappable via layers. Tests can provide an in-memory KV - // or a mock DB without changing the actor code. + // In-memory per-wake state. Resets on every wake; this v1 + // has no persistence. Replace with a persisted state ref + // once Actor.State lands. + const count = yield* Ref.make(0) - // PersistedSubscriptionRef extends SubscriptionRef with - // throttled durable persistence. Standard SubscriptionRef - // combinators (get, set, update, modify, changes) work as-is. - // Every published change schedules a save via the configured - // stateSaveInterval; the wake-scope finalizer flushes pending - // writes before sleep so state is durable on teardown. Use - // PersistedSubscriptionRef.sync / updateAndSync when an action - // must wait for durability before responding. - const state = yield* Counter.State - // ^ PersistedSubscriptionRef<{ count: number }> - const events = yield* Counter.Events - // ^ { countChanged: PubSub } - const messages = yield* Counter.Messages - // ^ MessageQueue - const kv = yield* Actor.Kv - const db = yield* Actor.Db - - // Ephemeral variable — reset on each wake, not persisted. - const connectionsTotal = yield* Ref.make(0) - - yield* Effect.addFinalizer(() => Effect.log("sleeping")) - - // --- Message processing (durable queue) --- - // Pull-based: the actor controls when to take the next message. - // Forked into a scoped fiber, so it runs in the background and - // is canceled on sleep. - yield* Effect.gen(function* () { - const msg = yield* Queue.take(messages) - yield* Match.value(msg).pipe( - Match.tag("Reset", () => - Effect.gen(function* () { - yield* PersistedSubscriptionRef.set(state, { count: 0 }) - yield* PubSub.publish(events.countChanged, 0) - }) - ), - Match.tag("IncrementBy", ({ payload, complete }) => - Effect.gen(function* () { - const next = yield* PersistedSubscriptionRef.updateAndGet(state, (s) => ({ - count: s.count + payload.amount, - })) - yield* PubSub.publish(events.countChanged, next.count) - yield* complete(next.count) - }) - ), - Match.exhaustive, - ) - }).pipe(Effect.forever, Effect.forkScoped) + yield* Effect.addFinalizer(() => + Ref.get(count).pipe( + Effect.flatMap((n) => Effect.log(`sleeping count=${n}`)), + ), + ) // --- Action handlers (request-response) --- return Counter.of({ Increment: ({ payload }) => Effect.gen(function* () { - // Throttled save: the change is published on - // state.changes, the framework debounces by - // stateSaveInterval and writes to durable KV. - const next = yield* PersistedSubscriptionRef.updateAndGet(state, (s) => ({ - count: s.count + payload.amount, - })) - if (next.count > 20) { + const next = yield* Ref.updateAndGet( + count, + (n) => n + payload.amount, + ) + if (next > 20) { return yield* new CounterOverflowError({ limit: 20 }) } - yield* PubSub.publish(events.countChanged, next.count) - return next.count + return next }), - GetCount: () => - PersistedSubscriptionRef.get(state).pipe(Effect.map((s) => s.count)), + GetCount: () => Ref.get(count), }) }), ) diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 2e040f276f..9b35376d90 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,9 +1,6 @@ -import { Effect, Stream } from "effect" +import { Effect } from "effect" import { Client } from "@rivetkit/effect" -import { - Counter, IncrementBy, - // ChatRoom, -} from "./actors/mod.ts" +import { Counter } from "./actors/mod.ts" const program = Effect.gen(function* () { const counterClient = yield* Counter.client @@ -11,18 +8,11 @@ const program = Effect.gen(function* () { const counter = counterClient.getOrCreate(["counter-123"]) // Action calls return Effects with types inferred from the schema. - // counter.Increment: (payload: { amount: number }) => Effect const count = yield* counter.Increment({ amount: 5 }) yield* Effect.log(`Count: ${count}`) - const newCount = yield* counter.send(IncrementBy({ amount: 3 })) - yield* Effect.log(`Count: ${newCount}`) - - // subscribe returns a Stream typed from the event schema. - yield* counter.subscribe("countChanged").pipe( - Stream.take(3), - Stream.runForEach((n) => Effect.log(`Changed: ${n}`)), - ) + const total = yield* counter.GetCount() + yield* Effect.log(`Total: ${total}`) }) // program: Effect // ^^^^^^ diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index 4a94602cd7..35cd839a63 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -1,6 +1,6 @@ import { Layer } from "effect" import { NodeRuntime } from "@effect/platform-node" -import { Registry, TestRegistry } from "@rivetkit/effect" +import { Registry } from "@rivetkit/effect" import { CounterLive } from "./actors/counter/live.ts" // import { ChatRoomLive } from "./actors/chat-room/live.ts" @@ -13,9 +13,5 @@ const MainLayer = ActorsLayer.pipe( Layer.provide(Registry.layer({ storagePath: "./data" })), ) -const TestLayer = ActorsLayer.pipe( - Layer.provide(TestRegistry.layer), -) - // Keeps the layer alive. Tears down on SIGINT/SIGTERM. Layer.launch(MainLayer).pipe(NodeRuntime.runMain) diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index 05cc41d212..81f06b60a1 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -126,12 +126,9 @@ export type ServicesServer = /** * Extract the action with the matching tag from a union of actions. */ -export type ExtractTag = R extends Action< - Tag, - infer _Payload, - infer _Success, - infer _Error -> +export type ExtractTag = R extends { + readonly _tag: Tag; +} ? R : never; diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 127ce00f9f..edbe1372ec 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -2,88 +2,28 @@ import * as Context from "effect/Context"; import type * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Predicate from "effect/Predicate"; -import type * as PubSub from "effect/PubSub"; -import type * as Queue from "effect/Queue"; -import * as Schema from "effect/Schema"; import type * as Scope from "effect/Scope"; -import type * as Stream from "effect/Stream"; -import type * as SubscriptionRef from "effect/SubscriptionRef"; import type * as Action from "./Action"; -import type * as Message from "./Message"; const TypeId = "~@rivetkit/effect/Actor"; -export const isActor = (u: unknown): u is Actor => +export const isActor = (u: unknown): u is Actor => Predicate.hasProperty(u, TypeId); /** - * Schemas keyed by the event names an actor can publish. - */ -export type EventSchemas = Record; - -/** - * Display and runtime options carried by an actor contract. + * Display options carried by an actor contract. */ export interface Options { readonly name?: string; readonly icon?: string; - readonly maxQueueSize?: number; - readonly maxQueueMessageSize?: number; -} - -/** - * Initial implementation uses Effect's SubscriptionRef directly. The - * persisted variant is defined by a separate module. - */ -export type StateRef = SubscriptionRef.SubscriptionRef; - -/** - * Effect-shaped KV service available inside an actor wake scope. - */ -export interface KvStore { - readonly get: ( - key: string | Uint8Array, - ) => Effect.Effect; - readonly put: ( - key: string | Uint8Array, - value: string | Uint8Array | ArrayBuffer, - ) => Effect.Effect; - readonly delete: (key: string | Uint8Array) => Effect.Effect; - readonly batchGet: ( - keys: ReadonlyArray, - ) => Effect.Effect>; - readonly batchPut: ( - entries: ReadonlyArray, - ) => Effect.Effect; - readonly batchDelete: ( - keys: ReadonlyArray, - ) => Effect.Effect; - readonly deleteRange: ( - start: Uint8Array, - end: Uint8Array, - ) => Effect.Effect; -} - -/** - * Minimal Effect-shaped database service available inside an actor wake scope. - */ -export interface DbClient { - readonly execute: >( - query: string, - ...args: ReadonlyArray - ) => Effect.Effect>; -} - -export interface KvService { - readonly _: unique symbol; } -export interface DbService { +export interface RegistryShape { readonly _: unique symbol; } -export interface RegistryShape { - readonly _: unique symbol; +export interface RegistryOptions { + readonly storagePath: string; } export interface ClientOptions { @@ -95,32 +35,17 @@ export interface ClientShape extends ClientOptions { readonly _: unique symbol; } -export interface StateService { - readonly _: unique symbol; - readonly name: Name; -} - -export interface EventsService { - readonly _: unique symbol; - readonly name: Name; -} - -export interface MessagesService { - readonly _: unique symbol; - readonly name: Name; -} - -export const Kv: Context.Service = Context.Service( - "@rivetkit/effect/Actor/Kv", -); - -export const Db: Context.Service = Context.Service( - "@rivetkit/effect/Actor/Db", -); - export class Registry extends Context.Service()( "@rivetkit/effect/Actor/Registry", -) {} +) { + static layer(_options: RegistryOptions): Layer.Layer { + return Layer.sync(Registry, () => { + throw new Error( + "Registry.layer is not yet implemented. Engine wiring is pending.", + ); + }); + } +} export class Client extends Context.Service()( "@rivetkit/effect/Client", @@ -133,37 +58,6 @@ export class Client extends Context.Service()( } } -export type EventPubSubMap = { - readonly [Name in keyof Events & string]: PubSub.PubSub; -}; - -type EventDecodeServices = { - readonly [Name in keyof Events]: Events[Name]["DecodingServices"]; -}[keyof Events]; - -type EventEncodeServices = { - readonly [Name in keyof Events]: Events[Name]["EncodingServices"]; -}[keyof Events]; - -type CompleteArgs = undefined extends A - ? readonly [value?: A] - : readonly [value: A]; - -export type MessageQueueItem = - M extends Message.Message - ? { - readonly _tag: Tag; - readonly message: M; - readonly payload: Payload["Type"]; - } & ([Success] extends [typeof Schema.Never] - ? object - : { - readonly complete: ( - ...args: CompleteArgs - ) => Effect.Effect; - }) - : never; - export type ActionRequest = A extends Action.Action ? { @@ -174,13 +68,9 @@ export type ActionRequest = : never; export type ActionHandlers = { - readonly [Tag in Action.Tag]: ( - request: ActionRequest>, - ) => Effect.Effect< - Action.Success>, - Action.Error>, - any - >; + readonly [A in Actions as Action.Tag]: ( + request: ActionRequest, + ) => Effect.Effect, Action.Error, any>; }; type HandlerServices = { @@ -228,86 +118,46 @@ type ActionClientMethod = ( ...args: ActionClientArgs ) => Effect.Effect, Action.Error>; -type EnvelopeSuccess = E extends Message.Envelope< - string, - Schema.Top, - infer Success -> - ? [Success] extends [typeof Schema.Never] - ? void - : Success["Type"] - : never; - -export type ActorHandle< - Actions extends Action.AnyWithProps, - Messages extends Message.AnyWithProps, - Events extends EventSchemas, -> = { - readonly [Tag in Action.Tag]: ActionClientMethod< - Action.ExtractTag - >; -} & { - readonly send: >( - envelope: Envelope, - options?: CallOptions, - ) => Effect.Effect>; - readonly subscribe: ( - name: Name, - ) => Stream.Stream; +export type ActorHandle = { + readonly [A in Actions as Action.Tag]: ActionClientMethod; }; -export interface ActorClient< - Actions extends Action.AnyWithProps, - Messages extends Message.AnyWithProps, - Events extends EventSchemas, -> { +export interface ActorClient { readonly get: ( key?: ActorKey, options?: GetOptions, - ) => ActorHandle; + ) => ActorHandle; readonly getOrCreate: ( key?: ActorKey, options?: GetOrCreateOptions, - ) => ActorHandle; + ) => ActorHandle; readonly getForId: ( actorId: string, options?: GetOptions, - ) => ActorHandle; + ) => ActorHandle; readonly create: ( key?: ActorKey, options?: CreateOptions, - ) => Effect.Effect>; + ) => Effect.Effect>; } /** - * A Rivet Actor contract. It carries schemas and generated Effect service - * tags, but no server implementation. + * A Rivet Actor contract. It carries the action schemas and + * display options, but no server implementation. */ export interface Actor< Name extends string, - State extends Schema.Top = Schema.Void, Actions extends Action.AnyWithProps = never, - Messages extends Message.AnyWithProps = never, - Events extends EventSchemas = {}, > { readonly [TypeId]: typeof TypeId; readonly _tag: Name; readonly key: string; - readonly stateSchema: State; readonly actions: ReadonlyArray; - readonly messages: ReadonlyArray; - readonly events: Events; readonly options: Options; - readonly State: Context.Service, StateRef>; - readonly Events: Context.Service, EventPubSubMap>; - readonly Messages: Context.Service< - MessagesService, - Queue.Dequeue> - >; readonly client: Effect.Effect< - ActorClient, + ActorClient, never, - Client | ClientServices> + Client | ClientServices> >; of>(handlers: Handlers): Handlers; @@ -317,20 +167,10 @@ export interface Actor< ): Layer.Layer< never, never, - | Exclude< - RX, - | StateService - | EventsService - | MessagesService - | KvService - | DbService - | Scope.Scope - > + | Exclude | HandlerServices | Action.ServicesServer | Action.ServicesClient - | Message.ServicesServer - | Message.ServicesClient | Registry >; } @@ -347,101 +187,22 @@ export interface Any { /** * Type-erased actor with all runtime properties available. */ -export interface AnyWithProps - extends Actor< - string, - Schema.Top, - Action.AnyWithProps, - Message.AnyWithProps, - EventSchemas - > {} - -export type Name = A extends Actor - ? _Name - : never; - -export type StateSchema = A extends Actor - ? _State - : never; +export interface AnyWithProps extends Actor {} -export type State = StateSchema["Type"]; +export type Name = A extends Actor ? _Name : never; -export type Actions = A extends Actor - ? _Actions - : never; - -export type Messages = A extends Actor - ? _Messages - : never; - -export type Events = A extends Actor - ? _Events - : never; - -export type EventName = keyof Events & string; - -export type EventPayload< - A, - Name extends EventName, -> = Events[Name]["Type"]; - -export type ProvidedServices = A extends Actor< - infer _Name, - any, - any, - any, - any -> - ? - | StateService<_Name> - | EventsService<_Name> - | MessagesService<_Name> - | KvService - | DbService - : never; +export type Actions = A extends Actor ? _Actions : never; -export type Services = A extends Actor< - any, - infer _State, - infer _Actions, - infer _Messages, - infer _Events -> - ? - | _State["DecodingServices"] - | _State["EncodingServices"] - | Action.Services<_Actions> - | Message.Services<_Messages> - | EventDecodeServices<_Events> - | EventEncodeServices<_Events> +export type Services = A extends Actor + ? Action.Services<_Actions> : never; -export type ClientServices = A extends Actor< - any, - any, - infer _Actions, - infer _Messages, - infer _Events -> - ? - | Action.ServicesClient<_Actions> - | Message.ServicesClient<_Messages> - | EventDecodeServices<_Events> +export type ClientServices = A extends Actor + ? Action.ServicesClient<_Actions> : never; -export type ServerServices = A extends Actor< - any, - infer _State, - infer _Actions, - infer _Messages, - infer _Events -> - ? - | _State["DecodingServices"] - | _State["EncodingServices"] - | Action.ServicesServer<_Actions> - | Message.ServicesServer<_Messages> - | EventEncodeServices<_Events> +export type ServerServices = A extends Actor + ? Action.ServicesServer<_Actions> : never; const identity = (value: A): A => value; @@ -464,37 +225,17 @@ const Proto = { const makeProto = < const Name extends string, - State extends Schema.Top, Actions extends Action.AnyWithProps, - Messages extends Message.AnyWithProps, - Events extends EventSchemas, >(options: { readonly _tag: Name; - readonly stateSchema: State; readonly actions: ReadonlyArray; - readonly messages: ReadonlyArray; - readonly events: Events; readonly options: Options; -}): Actor => { +}): Actor => { const key = `@rivetkit/effect/Actor/${options._tag}`; - const StateTag = Context.Service, StateRef>( - `${key}/State`, - ); - const EventsTag = Context.Service< - EventsService, - EventPubSubMap - >(`${key}/Events`); - const MessagesTag = Context.Service< - MessagesService, - Queue.Dequeue> - >(`${key}/Messages`); return Object.assign(Object.create(Proto), { ...options, key, - State: StateTag, - Events: EventsTag, - Messages: MessagesTag, - }) as Actor; + }) as Actor; }; /** @@ -502,37 +243,17 @@ const makeProto = < */ export const make = < const Name extends string, - State extends Schema.Top | Schema.Struct.Fields = Schema.Void, const Actions extends ReadonlyArray = readonly [], - const Messages extends ReadonlyArray = readonly [], - const Events extends EventSchemas = {}, >( name: Name, options?: { - readonly state?: State; readonly actions?: Actions; - readonly messages?: Messages; - readonly events?: Events; readonly options?: Options; }, -): Actor< - Name, - State extends Schema.Struct.Fields ? Schema.Struct : State, - Actions[number], - Messages[number], - Events -> => { - const stateSchema: Schema.Top = Schema.isSchema(options?.state) - ? (options?.state as any) - : options?.state - ? Schema.Struct(options?.state as any) - : Schema.Void; +): Actor => { return makeProto({ _tag: name, - stateSchema, actions: (options?.actions ?? []) as ReadonlyArray, - messages: (options?.messages ?? []) as ReadonlyArray, - events: (options?.events ?? {}) as EventSchemas, options: options?.options ?? {}, }) as any; }; diff --git a/rivetkit-typescript/packages/effect/src/Message.ts b/rivetkit-typescript/packages/effect/src/Message.ts deleted file mode 100644 index 0df510a304..0000000000 --- a/rivetkit-typescript/packages/effect/src/Message.ts +++ /dev/null @@ -1,311 +0,0 @@ -import * as Predicate from "effect/Predicate"; -import * as Schema from "effect/Schema"; - -const TypeId = "~@rivetkit/effect/Message"; - -const EnvelopeTypeId = "~@rivetkit/effect/Message/Envelope"; - -export const isMessage = (u: unknown): u is Message => - Predicate.hasProperty(u, TypeId); - -/** - * A Rivet Actor message: a durable, queued operation that the actor - * processes asynchronously on its main loop. - * - * @remarks - * - * `Message` is a value-level definition that carries the wire schemas - * for the request payload and, optionally, a completion response. The - * message's implementation lives in the actor's handler map; this type - * only describes the contract. - * - * Messages come in two flavors, distinguished at the type level by the - * `Success` schema: - * - * - **Non-completable** (fire-and-forget). `Success` defaults to - * `Schema.Never`. Sending returns once the message is durably - * enqueued; the sender does not observe the actor's processing. - * - * - **Completable**. `Success` is provided. Sending returns an Effect - * that resolves with the typed completion value once the actor's - * handler invokes its `complete` callback. - * - * Unlike `Action`, `Message` has no error channel. A message may sit - * in the queue long after the sender has moved on, so propagating a - * typed error back to the sender is not a meaningful contract. Handler - * failures are surfaced through the actor's standard supervision and - * retry mechanisms instead. - * - * `Message` values are callable: invoking the message with a payload - * produces a typed `Envelope` that can be passed to `actor.send(...)`. - * - * @example - * ```ts - * import { Schema } from "effect" - * import { Message } from "@rivetkit/effect" - * - * // Non-completable - * export const Reset = Message.make("Reset", { - * payload: { reason: Schema.String }, - * }) - * - * // Completable - * export const IncrementBy = Message.make("IncrementBy", { - * payload: { amount: Schema.Number }, - * success: Schema.Number, - * }) - * ``` - */ -export interface Message< - in out Tag extends string, - in out Payload extends Schema.Top = Schema.Void, - out Success extends Schema.Top = Schema.Never, -> { - (payload: Payload["~type.make.in"]): Envelope; - - readonly [TypeId]: typeof TypeId; - readonly _tag: Tag; - readonly key: string; - readonly payloadSchema: Payload; - readonly successSchema: Success; - /** - * Whether this message yields a typed completion to the sender. - * `true` when a `success` schema was supplied; `false` for - * fire-and-forget messages. - */ - readonly completable: IsCompletable>; -} - -/** - * A typed payload envelope produced by calling a `Message` value. - * - * The runtime uses `_tag` to dispatch to the correct handler. The - * `Success` type parameter is phantom: it surfaces the completion - * type at the call site so `actor.send(...)` can return a - * precisely-typed Effect without a second lookup. - */ -export interface Envelope< - in out Tag extends string, - out Payload extends Schema.Top = Schema.Void, - out Success extends Schema.Top = Schema.Never, -> { - readonly [EnvelopeTypeId]: typeof EnvelopeTypeId; - readonly _tag: Tag; - readonly payload: Payload["Type"]; - readonly "~successSchema": Success; -} - -/** - * Type-erased view of any `Message`. Useful for collections of - * messages where the specific schemas don't matter. - */ -export interface Any { - readonly [TypeId]: typeof TypeId; - readonly _tag: string; - readonly key: string; - readonly completable: boolean; -} - -/** - * Like `Any`, but with the prop fields (`*Schema`) accessible. Used - * by internal builders that need to read schemas off a message. - */ -export interface AnyWithProps { - readonly [TypeId]: typeof TypeId; - readonly _tag: string; - readonly key: string; - readonly payloadSchema: Schema.Top; - readonly successSchema: Schema.Top; - readonly completable: boolean; -} - -/** - * Type-erased view of any `Envelope`. - */ -export interface AnyEnvelope { - readonly [EnvelopeTypeId]: typeof EnvelopeTypeId; - readonly _tag: string; - readonly payload: unknown; -} - -// --- Type helpers --------------------------------------------------- - -export type Tag = - R extends Message - ? _Tag - : never; - -export type PayloadSchema = - R extends Message - ? _Payload - : never; - -export type Payload = PayloadSchema["Type"]; - -/** - * The shape accepted by the payload schema's `make` constructor on - * the client side (i.e. before encoding). Useful for typing the - * call site. - */ -export type PayloadConstructor = - R extends Message - ? _Payload["~type.make.in"] - : never; - -export type SuccessSchema = - R extends Message - ? _Success - : never; - -export type Success = SuccessSchema["Type"]; - -/** - * `true` when the message is completable (a `success` schema was - * provided), `false` for fire-and-forget messages. - * - * Driven off `Schema.Never` because `Schema.Void` is a legitimate - * completion type meaning "the sender awaits completion but the - * value carries no information." - */ -export type IsCompletable = - R extends Message - ? [_Success] extends [typeof Schema.Never] - ? false - : true - : never; - -/** - * The full set of decoding/encoding services required by every - * schema referenced by the message. Code generators include this in - * the `R` channel of any effect that handles or sends the message. - */ -export type Services = - R extends Message - ? - | _Payload["DecodingServices"] - | _Payload["EncodingServices"] - | _Success["DecodingServices"] - | _Success["EncodingServices"] - : never; - -/** - * The subset of `Services` actually needed on the client side: - * encoding the payload, decoding the (optional) completion response. - */ -export type ServicesClient = - R extends Message - ? _Payload["EncodingServices"] | _Success["DecodingServices"] - : never; - -/** - * The subset of `Services` needed on the server side: decoding the - * payload, encoding the (optional) completion response. - */ -export type ServicesServer = - R extends Message - ? _Payload["DecodingServices"] | _Success["EncodingServices"] - : never; - -/** - * Extract the message with the matching tag from a union of - * messages. - */ -export type ExtractTag = R extends Message< - Tag, - infer _Payload, - infer _Success -> - ? R - : never; - -/** - * Extract the envelope union for a union of messages. Useful for - * typing an actor's message-queue handler. - */ -export type EnvelopeOf = - R extends Message - ? Envelope<_Tag, _Payload, _Success> - : never; - -// --- Implementation ------------------------------------------------- - -const Proto = { - [TypeId]: TypeId, -}; - -const makeProto = < - const Tag extends string, - Payload extends Schema.Top, - Success extends Schema.Top, ->(options: { - readonly _tag: Tag; - readonly payloadSchema: Payload; - readonly successSchema: Success; - readonly completable: boolean; -}): Message => { - function Message(payload: unknown) { - return { - [EnvelopeTypeId]: EnvelopeTypeId, - _tag: options._tag, - payload: (options.payloadSchema as any).make(payload), - }; - } - Object.setPrototypeOf(Message, Proto); - Object.assign(Message, options); - Message.key = `@rivetkit/effect/Message/${options._tag}`; - return Message as any; -}; - -/** - * Define a Rivet Actor message. - * - * Omit `success` for a fire-and-forget message. Provide it (even as - * `Schema.Void`) to make the message completable: the sender awaits - * the actor's `complete` callback and receives the typed value. - * - * @example - * ```ts - * import { Schema } from "effect" - * import { Message } from "@rivetkit/effect" - * - * // Fire-and-forget - * export const Reset = Message.make("Reset", { - * payload: { reason: Schema.String }, - * }) - * - * // Completable - * export const IncrementBy = Message.make("IncrementBy", { - * payload: { amount: Schema.Number }, - * success: Schema.Number, - * }) - * ``` - */ -export const make = < - const Tag extends string, - Payload extends Schema.Top | Schema.Struct.Fields = Schema.Void, - Success extends Schema.Top = typeof Schema.Never, ->( - tag: Tag, - options?: { - readonly payload?: Payload; - readonly success?: Success; - }, -): Message< - Tag, - Payload extends Schema.Struct.Fields ? Schema.Struct : Payload, - Success -> => { - const successSchema = options?.success ?? Schema.Never; - const completable = options?.success !== undefined; - const payloadSchema: Schema.Top = Schema.isSchema(options?.payload) - ? (options?.payload as any) - : options?.payload - ? Schema.Struct(options?.payload as any) - : Schema.Void; - return makeProto({ - _tag: tag, - payloadSchema, - successSchema, - completable, - }) as any; -}; diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index a413e99130..79b3b18721 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,4 +1,3 @@ export * as Actor from "./Actor"; export { Client, Registry } from "./Actor"; export * as Action from "./Action"; -export * as Message from "./Message"; From d34b7e1fe0667f5fdce49ad151b0e3bb945dbde2 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 30 Apr 2026 11:41:45 +0200 Subject: [PATCH 062/306] refactor(effect): drop AbortSignal-based call cancellation Effect's native interruption replaces per-call AbortSignal. Drop AbortSignalLike and CallOptions, and remove the trailing options parameter from action client methods. --- rivetkit-typescript/packages/effect/src/Actor.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index edbe1372ec..7f6b146fb4 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -81,21 +81,11 @@ type HandlerServices = { : never; }[keyof Handlers]; -export interface AbortSignalLike { - readonly aborted: boolean; - readonly reason?: unknown; -} - -export interface CallOptions { - readonly signal?: AbortSignalLike; -} - export type ActorKey = string | ReadonlyArray; export interface GetOptions { readonly params?: unknown; readonly getParams?: () => Effect.Effect; - readonly signal?: AbortSignalLike; } export interface GetOrCreateOptions extends GetOptions { @@ -111,8 +101,8 @@ export interface CreateOptions extends GetOptions { type ActionClientArgs = [ Action.PayloadConstructor, ] extends [void] - ? readonly [payload?: Action.PayloadConstructor, options?: CallOptions] - : readonly [payload: Action.PayloadConstructor, options?: CallOptions]; + ? readonly [payload?: Action.PayloadConstructor] + : readonly [payload: Action.PayloadConstructor]; type ActionClientMethod = ( ...args: ActionClientArgs From b688ea72cbaa6acae1341cdf62a73848f806741e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 30 Apr 2026 11:42:42 +0200 Subject: [PATCH 063/306] refactor(effect): drop get/getForId/create from ActorClient Keep only getOrCreate for v1. Restore the other accessors when the runtime distinguishes them. Collapses GetOptions and CreateOptions into a single GetOrCreateOptions. --- .../packages/effect/src/Actor.ts | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 7f6b146fb4..c4e2468a90 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -83,21 +83,13 @@ type HandlerServices = { export type ActorKey = string | ReadonlyArray; -export interface GetOptions { +export interface GetOrCreateOptions { readonly params?: unknown; readonly getParams?: () => Effect.Effect; -} - -export interface GetOrCreateOptions extends GetOptions { readonly createInRegion?: string; readonly createWithInput?: unknown; } -export interface CreateOptions extends GetOptions { - readonly region?: string; - readonly input?: unknown; -} - type ActionClientArgs = [ Action.PayloadConstructor, ] extends [void] @@ -113,22 +105,10 @@ export type ActorHandle = { }; export interface ActorClient { - readonly get: ( - key?: ActorKey, - options?: GetOptions, - ) => ActorHandle; readonly getOrCreate: ( key?: ActorKey, options?: GetOrCreateOptions, ) => ActorHandle; - readonly getForId: ( - actorId: string, - options?: GetOptions, - ) => ActorHandle; - readonly create: ( - key?: ActorKey, - options?: CreateOptions, - ) => Effect.Effect>; } /** From 6c79c1463efc70829e43984228b9407dfb21110e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 30 Apr 2026 11:51:34 +0200 Subject: [PATCH 064/306] refactor(effect): drop GetOrCreateOptions and the options arg All fields are out of scope for v1: params/getParams cover auth, createInRegion/createWithInput cover region placement and create-input. Re-add when one of those features lands. getOrCreate now takes the key only. --- rivetkit-typescript/packages/effect/src/Actor.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index c4e2468a90..f5a21efd99 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -83,13 +83,6 @@ type HandlerServices = { export type ActorKey = string | ReadonlyArray; -export interface GetOrCreateOptions { - readonly params?: unknown; - readonly getParams?: () => Effect.Effect; - readonly createInRegion?: string; - readonly createWithInput?: unknown; -} - type ActionClientArgs = [ Action.PayloadConstructor, ] extends [void] @@ -105,10 +98,7 @@ export type ActorHandle = { }; export interface ActorClient { - readonly getOrCreate: ( - key?: ActorKey, - options?: GetOrCreateOptions, - ) => ActorHandle; + readonly getOrCreate: (key?: ActorKey) => ActorHandle; } /** From f774006466712c25400904650f7f12cf6d802b5e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 30 Apr 2026 11:53:46 +0200 Subject: [PATCH 065/306] docs(effect): drop per-action middleware mention from counter example Per-action middleware/annotations are out of scope for v1. --- examples/effect/src/actors/counter/api.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts index 0423916d68..849d4ed4cb 100644 --- a/examples/effect/src/actors/counter/api.ts +++ b/examples/effect/src/actors/counter/api.ts @@ -26,9 +26,6 @@ export class CounterOverflowError extends Schema.TaggedErrorClass Date: Thu, 30 Apr 2026 12:02:51 +0200 Subject: [PATCH 066/306] refactor(effect): align Registry/Client options with engine wiring Replace the placeholder { storagePath } shape with the engine-connection trio used by the non-Effect TS SDK: optional endpoint (with URL-auth syntax), token, namespace. All fields fall back to RIVET_* env vars. Share the shape between Registry.layer and Client.layer via a common EngineOptions interface. --- examples/effect/src/main.ts | 2 +- .../packages/effect/src/Action.ts | 3 ++ .../packages/effect/src/Actor.ts | 38 +++++++++++++++---- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index 35cd839a63..12b5cdc268 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -10,7 +10,7 @@ const ActorsLayer = Layer.mergeAll( ) const MainLayer = ActorsLayer.pipe( - Layer.provide(Registry.layer({ storagePath: "./data" })), + Layer.provide(Registry.layer({ endpoint: "https://api.rivet.dev" })), ) // Keeps the layer alive. Tears down on SIGINT/SIGTERM. diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index 81f06b60a1..b7ad9b473c 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -21,6 +21,7 @@ export interface Action< readonly payloadSchema: Payload; readonly successSchema: Success; readonly errorSchema: Error; + readonly defectSchema: Schema.Top; } /** @@ -44,6 +45,7 @@ export interface AnyWithProps { readonly payloadSchema: Schema.Top; readonly successSchema: Schema.Top; readonly errorSchema: Schema.Top; + readonly defectSchema: Schema.Top; } // --- Type helpers --------------------------------------------------- @@ -151,6 +153,7 @@ const makeProto = < }): Action => { const self = Object.assign(Object.create(Proto), options); self.key = `@rivetkit/effect/Action/${options._tag}`; + self.defectSchema = Schema.Defect; return self; }; diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index f5a21efd99..6007cff9c7 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -22,15 +22,37 @@ export interface RegistryShape { readonly _: unique symbol; } -export interface RegistryOptions { - readonly storagePath: string; -} - -export interface ClientOptions { - readonly endpoint: string; +/** + * Connection options for the Rivet Engine. + * + * Mirrors the engine wiring used by the non-Effect TS SDK: an optional + * endpoint (with URL-auth syntax for namespace and token), plus + * standalone `token` and `namespace` fields. All fields are optional + * and fall back to the matching `RIVET_*` environment variables. + */ +export interface EngineOptions { + /** + * Endpoint URL of the Rivet Engine. + * + * Supports URL auth syntax for namespace and token: + * - `https://namespace:token@api.rivet.dev` + * - `https://namespace@api.rivet.dev` + * + * Falls back to `RIVET_ENDPOINT`. + */ + readonly endpoint?: string; + /** Auth token. Falls back to `RIVET_TOKEN`. */ readonly token?: string; + /** + * Namespace. Falls back to `RIVET_NAMESPACE`, then `"default"`. + */ + readonly namespace?: string; } +export interface RegistryOptions extends EngineOptions {} + +export interface ClientOptions extends EngineOptions {} + export interface ClientShape extends ClientOptions { readonly _: unique symbol; } @@ -38,7 +60,7 @@ export interface ClientShape extends ClientOptions { export class Registry extends Context.Service()( "@rivetkit/effect/Actor/Registry", ) { - static layer(_options: RegistryOptions): Layer.Layer { + static layer(_options?: RegistryOptions): Layer.Layer { return Layer.sync(Registry, () => { throw new Error( "Registry.layer is not yet implemented. Engine wiring is pending.", @@ -50,7 +72,7 @@ export class Registry extends Context.Service()( export class Client extends Context.Service()( "@rivetkit/effect/Client", ) { - static layer(options: ClientOptions): Layer.Layer { + static layer(options?: ClientOptions): Layer.Layer { return Layer.succeed(Client, { ...options, _: undefined as never, From 552d8d6496273adf935ab842e9bb984ea2e5c2c1 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 30 Apr 2026 12:22:11 +0200 Subject: [PATCH 067/306] feat(effect): add fixed RivetError defectSchema to Action --- .../packages/effect/src/Action.ts | 3 +- .../packages/effect/src/RivetError.ts | 44 +++++++++++++++++++ .../packages/effect/src/mod.ts | 1 + 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 rivetkit-typescript/packages/effect/src/RivetError.ts diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index b7ad9b473c..57995b6d27 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -1,5 +1,6 @@ import * as Predicate from "effect/Predicate"; import * as Schema from "effect/Schema"; +import { RivetError } from "./RivetError"; const TypeId = "~@rivetkit/effect/Action"; @@ -153,7 +154,7 @@ const makeProto = < }): Action => { const self = Object.assign(Object.create(Proto), options); self.key = `@rivetkit/effect/Action/${options._tag}`; - self.defectSchema = Schema.Defect; + self.defectSchema = RivetError; return self; }; diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts new file mode 100644 index 0000000000..10764efdd3 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -0,0 +1,44 @@ +import * as Schema from "effect/Schema"; +import * as Getter from "effect/SchemaGetter"; +import { RivetError as RivetErrorClass } from "rivetkit"; + +const RivetErrorEncoded = Schema.Struct({ + group: Schema.String, + code: Schema.String, + message: Schema.String, + metadata: Schema.optionalKey(Schema.Unknown), +}); + +/** + * Schema for the cross-boundary `RivetError` envelope. + * + * Decodes to the existing `RivetError` class from `rivetkit`, so any + * code that catches `instanceof RivetError` keeps working across + * SDKs. + * + * Server-side defects (unexpected throws from action handlers) are + * sanitized into `RivetError("internal", "internal_error", ...)` + * before they hit the wire. This is the wire shape the Effect SDK + * uses as the default `defectSchema` for every action. + */ +export const RivetError = RivetErrorEncoded.pipe( + Schema.decodeTo(Schema.instanceOf(RivetErrorClass), { + decode: Getter.transform(({ group, code, message, metadata }) => + new RivetErrorClass(group, code, message, { metadata }), + ), + encode: Getter.transform((e: RivetErrorClass) => { + const out: { + group: string; + code: string; + message: string; + metadata?: unknown; + } = { + group: e.group, + code: e.code, + message: e.message, + }; + if (e.metadata !== undefined) out.metadata = e.metadata; + return out; + }), + }), +); diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index 79b3b18721..7f3fe7aea3 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,3 +1,4 @@ export * as Actor from "./Actor"; export { Client, Registry } from "./Actor"; export * as Action from "./Action"; +export { RivetError } from "./RivetError"; From 376bf1d3790bc6a8c7e9cbc29da8c141722945b8 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 30 Apr 2026 12:46:01 +0200 Subject: [PATCH 068/306] refactor(effect): tighten ActionHandlers R upper bound to unknown --- rivetkit-typescript/packages/effect/src/Actor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 6007cff9c7..4c4010a57c 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -92,7 +92,7 @@ export type ActionRequest = export type ActionHandlers = { readonly [A in Actions as Action.Tag]: ( request: ActionRequest, - ) => Effect.Effect, Action.Error, any>; + ) => Effect.Effect, Action.Error, unknown>; }; type HandlerServices = { From d0f23d02ce155dfbd703621cbce767cb4c0baa8a Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 30 Apr 2026 15:32:49 +0200 Subject: [PATCH 069/306] refactor(effect): collapse ClientError into a single RivetError tagged class Drop the separate ClientError union and consolidate transport errors into one Schema.TaggedErrorClass that wraps the underlying rivetkit.RivetError instance. ActionClientMethod's error channel now reads Action.Error | RivetError. Re-export RivetErrorLike and RivetErrorOptions from rivetkit so the wire codec (RivetErrorFromWire, used as Action.defectSchema) is anchored against drift in the canonical wire shape and constructor options. --- examples/effect/src/client.ts | 4 +- .../packages/effect/src/Action.ts | 4 +- .../packages/effect/src/Actor.ts | 3 +- .../packages/effect/src/RivetError.ts | 73 ++++++++++++------- .../packages/effect/src/mod.ts | 2 +- .../packages/rivetkit/src/actor/mod.ts | 2 + 6 files changed, 55 insertions(+), 33 deletions(-) diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 9b35376d90..043e4fde79 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -14,8 +14,8 @@ const program = Effect.gen(function* () { const total = yield* counter.GetCount() yield* Effect.log(`Total: ${total}`) }) -// program: Effect -// ^^^^^^ +// program: Effect +// ^^^^^^ // Missing Client -> compile error naming the central runtime dependency. // ------------------------------------------------------------------ diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index 57995b6d27..d07fde9f3e 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -1,6 +1,6 @@ import * as Predicate from "effect/Predicate"; import * as Schema from "effect/Schema"; -import { RivetError } from "./RivetError"; +import { RivetErrorFromWire } from "./RivetError"; const TypeId = "~@rivetkit/effect/Action"; @@ -154,7 +154,7 @@ const makeProto = < }): Action => { const self = Object.assign(Object.create(Proto), options); self.key = `@rivetkit/effect/Action/${options._tag}`; - self.defectSchema = RivetError; + self.defectSchema = RivetErrorFromWire; return self; }; diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 4c4010a57c..dace1c8619 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -4,6 +4,7 @@ import * as Layer from "effect/Layer"; import * as Predicate from "effect/Predicate"; import type * as Scope from "effect/Scope"; import type * as Action from "./Action"; +import type { RivetError } from "./RivetError"; const TypeId = "~@rivetkit/effect/Actor"; @@ -113,7 +114,7 @@ type ActionClientArgs = [ type ActionClientMethod = ( ...args: ActionClientArgs -) => Effect.Effect, Action.Error>; +) => Effect.Effect, Action.Error | RivetError>; export type ActorHandle = { readonly [A in Actions as Action.Tag]: ActionClientMethod; diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 10764efdd3..916284600a 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -1,8 +1,34 @@ import * as Schema from "effect/Schema"; import * as Getter from "effect/SchemaGetter"; -import { RivetError as RivetErrorClass } from "rivetkit"; +import { + RivetError as RivetErrorClass, + type RivetErrorLike, + type RivetErrorOptions, +} from "rivetkit"; -const RivetErrorEncoded = Schema.Struct({ +/** + * The cross-boundary Rivet error. Wraps the underlying + * `rivetkit.RivetError` instance on its `error` field, preserving + * `instanceof` checks and direct access to `group` / `code` / + * `message` / `metadata`. + * + * Recover with `Effect.catchTag("RivetError", e => …)` and discriminate + * on `e.error.group` / `e.error.code`. + */ +export class RivetError extends Schema.TaggedErrorClass()( + "RivetError", + { error: Schema.instanceOf(RivetErrorClass) }, +) {} + +// On-the-wire envelope: the subset of rivetkit's `RivetErrorLike` that +// crosses the action boundary. `Pick`ing here anchors the codec +// against drift in the canonical wire shape. +type WirePayload = Pick< + RivetErrorLike, + "group" | "code" | "message" | "metadata" +>; + +const Wire = Schema.Struct({ group: Schema.String, code: Schema.String, message: Schema.String, @@ -10,34 +36,27 @@ const RivetErrorEncoded = Schema.Struct({ }); /** - * Schema for the cross-boundary `RivetError` envelope. - * - * Decodes to the existing `RivetError` class from `rivetkit`, so any - * code that catches `instanceof RivetError` keeps working across - * SDKs. - * - * Server-side defects (unexpected throws from action handlers) are - * sanitized into `RivetError("internal", "internal_error", ...)` - * before they hit the wire. This is the wire shape the Effect SDK - * uses as the default `defectSchema` for every action. + * Wire codec used as the default `defectSchema` for actions. Decodes + * the `(group, code, message, metadata)` envelope produced by + * `rivetkit-core`'s defect sanitizer into a `RivetError` instance. */ -export const RivetError = RivetErrorEncoded.pipe( - Schema.decodeTo(Schema.instanceOf(RivetErrorClass), { - decode: Getter.transform(({ group, code, message, metadata }) => - new RivetErrorClass(group, code, message, { metadata }), +export const RivetErrorFromWire = Wire.pipe( + Schema.decodeTo(Schema.instanceOf(RivetError), { + decode: Getter.transform( + ({ group, code, message, metadata }) => + new RivetError({ + error: new RivetErrorClass(group, code, message, { + metadata, + } satisfies RivetErrorOptions), + }), ), - encode: Getter.transform((e: RivetErrorClass) => { - const out: { - group: string; - code: string; - message: string; - metadata?: unknown; - } = { - group: e.group, - code: e.code, - message: e.message, + encode: Getter.transform((e: RivetError) => { + const out: WirePayload = { + group: e.error.group, + code: e.error.code, + message: e.error.message, }; - if (e.metadata !== undefined) out.metadata = e.metadata; + if (e.error.metadata !== undefined) out.metadata = e.error.metadata; return out; }), }), diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index 7f3fe7aea3..039da15bf8 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,4 +1,4 @@ export * as Actor from "./Actor"; export { Client, Registry } from "./Actor"; export * as Action from "./Action"; -export { RivetError } from "./RivetError"; +export * as RivetError from "./RivetError"; diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/mod.ts b/rivetkit-typescript/packages/rivetkit/src/actor/mod.ts index a30445184c..daec5238b1 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/mod.ts @@ -32,6 +32,8 @@ export { export { ActorError, RivetError, + type RivetErrorLike, + type RivetErrorOptions, UserError, type UserErrorOptions, } from "./errors"; From 7db0ac7cf07c6b9f40f535c3ef039464604560b2 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 1 May 2026 10:08:28 +0200 Subject: [PATCH 070/306] refactor(effect): split Registry collector from Runner mode layers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the Client service and Actor.client getter. Reshape the SDK runtime into two services: Registry (collector + engine connection config, provided once via Registry.layer) and Runner (one Layer per serving mode: start/serve/handler/startEnvoy/test, each requiring Registry). Wire examples/effect/src/main.ts to the new shape and park examples/effect/src/client.ts until the client slice lands. This is the type-shape-only step; Registry.layer and all Runner modes still throw "not yet implemented" — typecheck passes for both @rivetkit/effect and examples/effect. --- examples/effect/src/client.ts | 64 ++++++------- examples/effect/src/main.ts | 5 +- .../packages/effect/src/Actor.ts | 89 +++++++++---------- .../packages/effect/src/mod.ts | 2 +- 4 files changed, 82 insertions(+), 78 deletions(-) diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 043e4fde79..3122983ab2 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,30 +1,34 @@ -import { Effect } from "effect" -import { Client } from "@rivetkit/effect" -import { Counter } from "./actors/mod.ts" - -const program = Effect.gen(function* () { - const counterClient = yield* Counter.client - - const counter = counterClient.getOrCreate(["counter-123"]) - - // Action calls return Effects with types inferred from the schema. - const count = yield* counter.Increment({ amount: 5 }) - yield* Effect.log(`Count: ${count}`) - - const total = yield* counter.GetCount() - yield* Effect.log(`Total: ${total}`) -}) -// program: Effect -// ^^^^^^ -// Missing Client -> compile error naming the central runtime dependency. - -// ------------------------------------------------------------------ -// Wiring: provide Client once. Each actor's .client effect -// uses that transport to create a contract-specific typed accessor. -// ------------------------------------------------------------------ -const ClientLayer = Client.layer({ - endpoint: "https://api.rivet.dev", - token: "...", -}) - -program.pipe(Effect.provide(ClientLayer), Effect.runPromise) +// Out of scope for the server-only slice. Will be reintroduced when +// the Effect Client slice lands. See plan: +// /Users/igassmann/.claude/plans/indexed-baking-crescent.md +// import { Effect } from "effect" +// import { Client } from "@rivetkit/effect" +// import { Counter } from "./actors/mod.ts" +// +// const program = Effect.gen(function* () { +// const counterClient = yield* Counter.client +// +// const counter = counterClient.getOrCreate(["counter-123"]) +// +// // Action calls return Effects with types inferred from the schema. +// const count = yield* counter.Increment({ amount: 5 }) +// yield* Effect.log(`Count: ${count}`) +// +// const total = yield* counter.GetCount() +// yield* Effect.log(`Total: ${total}`) +// }) +// // program: Effect +// // ^^^^^^ +// // Missing Client -> compile error naming the central runtime dependency. +// +// // ------------------------------------------------------------------ +// // Wiring: provide Client once. Each actor's .client effect +// // uses that transport to create a contract-specific typed accessor. +// // ------------------------------------------------------------------ +// const ClientLayer = Client.layer({ +// endpoint: "https://api.rivet.dev", +// token: "...", +// }) +// +// program.pipe(Effect.provide(ClientLayer), Effect.runPromise) +export {} diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index 12b5cdc268..55480816cf 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -1,6 +1,6 @@ import { Layer } from "effect" import { NodeRuntime } from "@effect/platform-node" -import { Registry } from "@rivetkit/effect" +import { Registry, Runner } from "@rivetkit/effect" import { CounterLive } from "./actors/counter/live.ts" // import { ChatRoomLive } from "./actors/chat-room/live.ts" @@ -9,7 +9,8 @@ const ActorsLayer = Layer.mergeAll( // ChatRoomLive, ) -const MainLayer = ActorsLayer.pipe( +const MainLayer = Runner.start.pipe( + Layer.provide(ActorsLayer), Layer.provide(Registry.layer({ endpoint: "https://api.rivet.dev" })), ) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index dace1c8619..a53e8e0eba 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -1,10 +1,9 @@ import * as Context from "effect/Context"; -import type * as Effect from "effect/Effect"; +import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Predicate from "effect/Predicate"; import type * as Scope from "effect/Scope"; import type * as Action from "./Action"; -import type { RivetError } from "./RivetError"; const TypeId = "~@rivetkit/effect/Actor"; @@ -23,6 +22,10 @@ export interface RegistryShape { readonly _: unique symbol; } +export interface RunnerShape { + readonly _: unique symbol; +} + /** * Connection options for the Rivet Engine. * @@ -52,12 +55,14 @@ export interface EngineOptions { export interface RegistryOptions extends EngineOptions {} -export interface ClientOptions extends EngineOptions {} - -export interface ClientShape extends ClientOptions { - readonly _: unique symbol; -} - +/** + * Service collecting actor defs/builders together with the engine + * connection config. Provided once via `Registry.layer({ ... })` and + * consumed by both `Actor.toLayer` (which registers itself into the + * collector on acquire) and the `Runner.*` mode layers (which + * materialize the underlying rivetkit registry from the collected + * entries). + */ export class Registry extends Context.Service()( "@rivetkit/effect/Actor/Registry", ) { @@ -70,15 +75,38 @@ export class Registry extends Context.Service()( } } -export class Client extends Context.Service()( - "@rivetkit/effect/Client", +const runnerNotImplemented = ( + mode: string, +): Layer.Layer => + Layer.effect( + Runner, + Effect.gen(function* () { + yield* Registry; + throw new Error( + `Runner.${mode} is not yet implemented. Server runtime wiring is pending.`, + ); + }), + ); + +/** + * Service that selects how the registered actors are served. Each + * static field is a `Layer` for a specific mode mirroring the + * non-Effect TS SDK: `start`, `serve`, `handler`, `startEnvoy`, plus a + * `test` mode for in-process testing. Each requires `Registry`. + */ +export class Runner extends Context.Service()( + "@rivetkit/effect/Actor/Runner", ) { - static layer(options?: ClientOptions): Layer.Layer { - return Layer.succeed(Client, { - ...options, - _: undefined as never, - }); - } + static start: Layer.Layer = + runnerNotImplemented("start"); + static serve: Layer.Layer = + runnerNotImplemented("serve"); + static handler: Layer.Layer = + runnerNotImplemented("handler"); + static startEnvoy: Layer.Layer = + runnerNotImplemented("startEnvoy"); + static test: Layer.Layer = + runnerNotImplemented("test"); } export type ActionRequest = @@ -106,24 +134,6 @@ type HandlerServices = { export type ActorKey = string | ReadonlyArray; -type ActionClientArgs = [ - Action.PayloadConstructor, -] extends [void] - ? readonly [payload?: Action.PayloadConstructor] - : readonly [payload: Action.PayloadConstructor]; - -type ActionClientMethod = ( - ...args: ActionClientArgs -) => Effect.Effect, Action.Error | RivetError>; - -export type ActorHandle = { - readonly [A in Actions as Action.Tag]: ActionClientMethod; -}; - -export interface ActorClient { - readonly getOrCreate: (key?: ActorKey) => ActorHandle; -} - /** * A Rivet Actor contract. It carries the action schemas and * display options, but no server implementation. @@ -137,11 +147,6 @@ export interface Actor< readonly key: string; readonly actions: ReadonlyArray; readonly options: Options; - readonly client: Effect.Effect< - ActorClient, - never, - Client | ClientServices> - >; of>(handlers: Handlers): Handlers; @@ -198,12 +203,6 @@ const Proto = { `Actor.toLayer for ${this._tag} is not yet implemented. Registry runtime wiring is pending.`, ); }, - get client(): never { - const self = this as unknown as AnyWithProps; - throw new Error( - `Actor.client for ${self._tag} is not yet implemented. Client runtime wiring is pending.`, - ); - }, }; const makeProto = < diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index 039da15bf8..141e5e0868 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,4 +1,4 @@ export * as Actor from "./Actor"; -export { Client, Registry } from "./Actor"; +export { Registry, Runner } from "./Actor"; export * as Action from "./Action"; export * as RivetError from "./RivetError"; From 33fb12343bfe4dfbb625e5e1872b39ac21ad8714 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 1 May 2026 10:19:55 +0200 Subject: [PATCH 071/306] feat(effect): implement Registry as Ref-backed actor collector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the placeholder RegistryShape with engineOptions plus a register/entries pair backed by a Ref>. Registry.layer now produces a real service. Actor.toLayer normalizes the build argument into an Effect, then Layer.effectDiscard registers { actor, buildHandlers } into the collector during the layer's acquire phase. Mirrors Effect Cluster's Entity.toLayer -> Sharding.registerEntity pattern. Runner.* still throws — Step 3 will materialize Runner.start from the collected entries. --- .../packages/effect/src/Actor.ts | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index a53e8e0eba..0b6006ba2a 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -2,6 +2,7 @@ import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Predicate from "effect/Predicate"; +import * as Ref from "effect/Ref"; import type * as Scope from "effect/Scope"; import type * as Action from "./Action"; @@ -18,8 +19,21 @@ export interface Options { readonly icon?: string; } +/** + * One actor registered with the `Registry`. The `buildHandlers` + * effect is run once per wake by the runner to construct + * per-instance state and handlers; the handlers themselves are not + * resolved at registration time. + */ +export interface RegistryEntry { + readonly actor: AnyWithProps; + readonly buildHandlers: Effect.Effect; +} + export interface RegistryShape { - readonly _: unique symbol; + readonly engineOptions: EngineOptions; + readonly register: (entry: RegistryEntry) => Effect.Effect; + readonly entries: Effect.Effect>; } export interface RunnerShape { @@ -66,12 +80,24 @@ export interface RegistryOptions extends EngineOptions {} export class Registry extends Context.Service()( "@rivetkit/effect/Actor/Registry", ) { - static layer(_options?: RegistryOptions): Layer.Layer { - return Layer.sync(Registry, () => { - throw new Error( - "Registry.layer is not yet implemented. Engine wiring is pending.", - ); - }); + static layer(options: RegistryOptions = {}): Layer.Layer { + const engineOptions: EngineOptions = { + endpoint: options.endpoint, + token: options.token, + namespace: options.namespace, + }; + return Layer.effect( + Registry, + Effect.gen(function* () { + const ref = yield* Ref.make>([]); + return Registry.of({ + engineOptions, + register: (entry) => + Ref.update(ref, (xs) => [...xs, entry]), + entries: Ref.get(ref), + }); + }), + ); } } @@ -198,9 +224,19 @@ const identity = (value: A): A => value; const Proto = { [TypeId]: TypeId, of: identity, - toLayer(this: AnyWithProps) { - throw new Error( - `Actor.toLayer for ${this._tag} is not yet implemented. Registry runtime wiring is pending.`, + toLayer(this: AnyWithProps, build: unknown) { + const self = this; + const buildHandlers = ( + Effect.isEffect(build) ? build : Effect.succeed(build) + ) as Effect.Effect; + return Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* Registry; + yield* registry.register({ + actor: self, + buildHandlers, + }); + }), ); }, }; From 1cb585bfe74dbd533084dfbb9f1842efd7689a1a Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 1 May 2026 10:29:04 +0200 Subject: [PATCH 072/306] feat(effect): wire Runner.start through rivetkit's setup().start() 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. --- .../packages/effect/src/Actor.ts | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 0b6006ba2a..7947d398d2 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -4,6 +4,11 @@ import * as Layer from "effect/Layer"; import * as Predicate from "effect/Predicate"; import * as Ref from "effect/Ref"; import type * as Scope from "effect/Scope"; +import { + actor as actorNative, + type AnyActorDefinition, + setup as setupNative, +} from "rivetkit"; import type * as Action from "./Action"; const TypeId = "~@rivetkit/effect/Actor"; @@ -37,7 +42,7 @@ export interface RegistryShape { } export interface RunnerShape { - readonly _: unique symbol; + readonly mode: "start" | "serve" | "handler" | "startEnvoy" | "test"; } /** @@ -102,7 +107,7 @@ export class Registry extends Context.Service()( } const runnerNotImplemented = ( - mode: string, + mode: RunnerShape["mode"], ): Layer.Layer => Layer.effect( Runner, @@ -114,6 +119,33 @@ const runnerNotImplemented = ( }), ); +/** + * Build the underlying rivetkit `use` map from collected entries. + * Step 3 wires only the structural skeleton: action handlers throw + * until per-instance scope + dispatch land in Step 4. + */ +const buildUseMap = ( + entries: ReadonlyArray, +): Record => { + const use: Record = {}; + for (const entry of entries) { + const actions: Record any> = {}; + for (const action of entry.actor.actions) { + const tag = action._tag; + actions[tag] = () => { + throw new Error( + `Action ${entry.actor._tag}.${tag}: handler dispatch not yet implemented (Step 4 wiring pending)`, + ); + }; + } + use[entry.actor._tag] = actorNative({ + actions, + options: entry.actor.options, + } as Parameters[0]); + } + return use; +}; + /** * Service that selects how the registered actors are served. Each * static field is a `Layer` for a specific mode mirroring the @@ -123,8 +155,21 @@ const runnerNotImplemented = ( export class Runner extends Context.Service()( "@rivetkit/effect/Actor/Runner", ) { - static start: Layer.Layer = - runnerNotImplemented("start"); + static start: Layer.Layer = Layer.effect( + Runner, + Effect.gen(function* () { + const registry = yield* Registry; + const entries = yield* registry.entries; + const native = setupNative({ + use: buildUseMap(entries), + endpoint: registry.engineOptions.endpoint, + token: registry.engineOptions.token, + namespace: registry.engineOptions.namespace, + }); + yield* Effect.sync(() => native.start()); + return Runner.of({ mode: "start" }); + }), + ); static serve: Layer.Layer = runnerNotImplemented("serve"); static handler: Layer.Layer = From 54f5cf03fd97981a8d3818657f9683ca575a46ea Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 1 May 2026 10:46:08 +0200 Subject: [PATCH 073/306] feat(effect): wire per-instance scope and handler dispatch in Runner.start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. - 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. --- .../packages/effect/src/Actor.ts | 140 +++++++++++++++--- 1 file changed, 116 insertions(+), 24 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 7947d398d2..01dd10bade 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -1,9 +1,11 @@ import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; import * as Predicate from "effect/Predicate"; import * as Ref from "effect/Ref"; -import type * as Scope from "effect/Scope"; +import * as Schema from "effect/Schema"; +import * as Scope from "effect/Scope"; import { actor as actorNative, type AnyActorDefinition, @@ -119,31 +121,98 @@ const runnerNotImplemented = ( }), ); -/** - * Build the underlying rivetkit `use` map from collected entries. - * Step 3 wires only the structural skeleton: action handlers throw - * until per-instance scope + dispatch land in Step 4. - */ -const buildUseMap = ( - entries: ReadonlyArray, -): Record => { - const use: Record = {}; - for (const entry of entries) { - const actions: Record any> = {}; - for (const action of entry.actor.actions) { - const tag = action._tag; - actions[tag] = () => { +type ActorInstance = { + readonly handlers: Record< + string, + (req: { + readonly _tag: string; + readonly action: Action.AnyWithProps; + readonly payload: unknown; + }) => Effect.Effect + >; + readonly scope: Scope.Closeable; +}; + +const buildNativeActor = ( + entry: RegistryEntry, + instances: Map, + runHandler: (effect: Effect.Effect) => Promise, +): AnyActorDefinition => { + const actor = entry.actor; + const decoders = new Map< + string, + (v: unknown) => Effect.Effect + >(); + const encoders = new Map< + string, + (v: unknown) => Effect.Effect + >(); + for (const action of actor.actions) { + decoders.set( + action._tag, + Schema.decodeUnknownEffect(action.payloadSchema) as never, + ); + encoders.set( + action._tag, + Schema.encodeUnknownEffect(action.successSchema) as never, + ); + } + + const actions: Record< + string, + (c: { actorId: string }, payload?: unknown) => Promise + > = {}; + for (const action of actor.actions) { + const tag = action._tag; + actions[tag] = async (c, payload) => { + const inst = instances.get(c.actorId); + if (!inst) { + throw new Error( + `actor ${actor._tag}/${c.actorId} has no handlers (onWake didn't run?)`, + ); + } + const handler = inst.handlers[tag]; + if (!handler) { throw new Error( - `Action ${entry.actor._tag}.${tag}: handler dispatch not yet implemented (Step 4 wiring pending)`, + `actor ${actor._tag} has no handler for action ${tag}`, ); - }; - } - use[entry.actor._tag] = actorNative({ - actions, - options: entry.actor.options, - } as Parameters[0]); + } + const decoded = await runHandler(decoders.get(tag)!(payload)); + const result = await runHandler( + handler({ _tag: tag, action, payload: decoded }), + ); + return await runHandler(encoders.get(tag)!(result)); + }; } - return use; + + return actorNative({ + actions, + options: actor.options, + onWake: async (c: { actorId: string }) => { + const scope = await runHandler(Scope.make()); + const handlers = await runHandler( + Effect.provideService( + entry.buildHandlers as Effect.Effect< + unknown, + never, + Scope.Scope + >, + Scope.Scope, + scope, + ) as Effect.Effect, + ); + instances.set(c.actorId, { + handlers: handlers as ActorInstance["handlers"], + scope, + }); + }, + onSleep: async (c: { actorId: string }) => { + const inst = instances.get(c.actorId); + if (!inst) return; + instances.delete(c.actorId); + await runHandler(Scope.close(inst.scope, Exit.void)); + }, + } as Parameters[0]); }; /** @@ -160,8 +229,31 @@ export class Runner extends Context.Service()( Effect.gen(function* () { const registry = yield* Registry; const entries = yield* registry.entries; + + // Snapshot the current Effect context so action callbacks + // (which run in rivetkit's plain Promise world) can run + // handler effects against the same services Runner.start + // was provided with. + const context = yield* Effect.context(); + const runHandler = ( + effect: Effect.Effect, + ): Promise => + Effect.runPromiseWith(context)( + effect as Effect.Effect, + ); + + const instances = new Map(); + const use: Record = {}; + for (const entry of entries) { + use[entry.actor._tag] = buildNativeActor( + entry, + instances, + runHandler, + ); + } + const native = setupNative({ - use: buildUseMap(entries), + use, endpoint: registry.engineOptions.endpoint, token: registry.engineOptions.token, namespace: registry.engineOptions.namespace, From 5a1dc9675805254b47287cd25635c94d86f5b630 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 1 May 2026 11:44:47 +0200 Subject: [PATCH 074/306] feat(examples/effect): add plain-rivetkit smoke client and self-spawn 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). --- examples/effect/package.json | 5 ++-- examples/effect/src/client.ts | 56 ++++++++++++++++++++++++++++++++--- examples/effect/src/main.ts | 7 ++++- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/examples/effect/package.json b/examples/effect/package.json index 70e21c5a1d..d024a7dd62 100644 --- a/examples/effect/package.json +++ b/examples/effect/package.json @@ -3,8 +3,9 @@ "private": true, "type": "module", "scripts": { - "dev": "tsx watch src/main.ts", - "start": "tsx src/main.ts", + "dev": "RIVET_RUN_ENGINE=1 tsx watch src/main.ts", + "start": "RIVET_RUN_ENGINE=1 tsx src/main.ts", + "client": "tsx src/client.ts", "check-types": "tsc --noEmit" }, "dependencies": { diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 3122983ab2..eb3f393849 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,6 +1,55 @@ -// Out of scope for the server-only slice. Will be reintroduced when -// the Effect Client slice lands. See plan: -// /Users/igassmann/.claude/plans/indexed-baking-crescent.md +// End-to-end smoke test for the Effect SDK server slice. +// +// Drives the actor served by `pnpm start` (main.ts) using a plain +// rivetkit client — the Effect Client surface is out of scope for the +// server-only slice. Run alongside the server: +// +// # terminal A — start the server (auto-spawns local engine) +// RIVET_RUN_ENGINE=true \ +// RIVET_ENGINE_BINARY=$(git rev-parse --show-toplevel)/target/debug/rivet-engine \ +// pnpm start +// +// # terminal B — drive the client +// pnpm client +import { createClient } from "rivetkit/client" + +const client = createClient("http://127.0.0.1:6420") as any + +async function main() { + const counter = client.Counter.getOrCreate("counter-e2e") + + const initial = await counter.GetCount() + console.log("GetCount (initial):", initial) + + const afterFive = await counter.Increment({ amount: 5 }) + console.log("Increment(5):", afterFive) + + const afterEight = await counter.Increment({ amount: 3 }) + console.log("Increment(3):", afterEight) + + const total = await counter.GetCount() + console.log("GetCount (total):", total) + + // Trigger overflow (limit: 20). Step 4 surfaces this as a defect + // (typed-error encoding lands in a follow-up slice). + try { + const overflowed = await counter.Increment({ amount: 20 }) + console.log("Increment(20) [unexpected success]:", overflowed) + } catch (err) { + console.log("Increment(20) [expected error]:", err) + } +} + +main().catch((err) => { + console.error("client smoke test failed:", err) + process.exit(1) +}) + +// ------------------------------------------------------------------ +// Target Effect Client surface (parked until the client slice lands). +// See plan: /Users/igassmann/.claude/plans/indexed-baking-crescent.md +// ------------------------------------------------------------------ +// // import { Effect } from "effect" // import { Client } from "@rivetkit/effect" // import { Counter } from "./actors/mod.ts" @@ -31,4 +80,3 @@ // }) // // program.pipe(Effect.provide(ClientLayer), Effect.runPromise) -export {} diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index 55480816cf..a692e83654 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -9,9 +9,14 @@ const ActorsLayer = Layer.mergeAll( // ChatRoomLive, ) +// Engine config defaults to spawning a local rivet-engine process and +// listening on http://127.0.0.1:6420 (override via RIVET_ENDPOINT to +// point at a remote engine). For dev builds without a packaged engine, +// set RIVET_ENGINE_BINARY to the path of a `cargo build` binary, e.g.: +// RIVET_ENGINE_BINARY=$(pwd)/target/debug/rivet-engine pnpm start const MainLayer = Runner.start.pipe( Layer.provide(ActorsLayer), - Layer.provide(Registry.layer({ endpoint: "https://api.rivet.dev" })), + Layer.provide(Registry.layer()), ) // Keeps the layer alive. Tears down on SIGINT/SIGTERM. From dce78df5d4cf978f2176f3941295e902e79d7077 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 1 May 2026 11:50:20 +0200 Subject: [PATCH 075/306] feat(examples/node-client): add minimal actor + plain Node client example 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. --- examples/node-client/.gitignore | 2 + examples/node-client/README.md | 39 ++++++++++ examples/node-client/package.json | 24 +++++++ examples/node-client/src/client.ts | 22 ++++++ examples/node-client/src/index.ts | 22 ++++++ examples/node-client/tests/counter.test.ts | 50 +++++++++++++ examples/node-client/tsconfig.json | 15 ++++ examples/node-client/turbo.json | 4 ++ examples/node-client/vitest.config.ts | 7 ++ pnpm-lock.yaml | 82 ++++++++++++++-------- 10 files changed, 236 insertions(+), 31 deletions(-) create mode 100644 examples/node-client/.gitignore create mode 100644 examples/node-client/README.md create mode 100644 examples/node-client/package.json create mode 100644 examples/node-client/src/client.ts create mode 100644 examples/node-client/src/index.ts create mode 100644 examples/node-client/tests/counter.test.ts create mode 100644 examples/node-client/tsconfig.json create mode 100644 examples/node-client/turbo.json create mode 100644 examples/node-client/vitest.config.ts diff --git a/examples/node-client/.gitignore b/examples/node-client/.gitignore new file mode 100644 index 0000000000..dc6f607390 --- /dev/null +++ b/examples/node-client/.gitignore @@ -0,0 +1,2 @@ +.actorcore +node_modules diff --git a/examples/node-client/README.md b/examples/node-client/README.md new file mode 100644 index 0000000000..2bdb3c0f3c --- /dev/null +++ b/examples/node-client/README.md @@ -0,0 +1,39 @@ +# Node Client + +A minimal RivetKit example with no UI framework. The actor lives in `src/index.ts` and a plain Node script in `src/client.ts` connects to it via `rivetkit/client`. + +## Getting Started + +```sh +git clone https://github.com/rivet-dev/rivet.git +cd rivet/examples/node-client +pnpm install +pnpm dev # starts the actor envoy + spawns a local engine +pnpm client # in another terminal: runs the client script +``` + +The example sets `startEngine: true`, so the registry spawns the engine binary itself. When running from this monorepo (no published platform package installed), point `RIVET_ENGINE_BINARY` at the workspace dev build: + +```sh +RIVET_ENGINE_BINARY=$(pwd)/../../target/debug/rivet-engine pnpm dev +``` + +## Features + +- **Actor definition**: Counter actor with persistent state and two actions +- **Type-safe client**: `createClient(endpoint)` for end-to-end type inference +- **No UI framework**: Pure Node script, suitable as a starting point for CLIs, scripts, or backend-to-actor calls + +## Implementation + +- **Actor + registry** ([`src/index.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/node-client/src/index.ts)) +- **Client script** ([`src/client.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/node-client/src/client.ts)) +- **Tests** ([`tests/counter.test.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/node-client/tests/counter.test.ts)) + +## Resources + +Read more about [actions](/docs/actors/actions), [state](/docs/actors/state), and [the client](/docs/clients). + +## License + +MIT diff --git a/examples/node-client/package.json b/examples/node-client/package.json new file mode 100644 index 0000000000..9e78770c19 --- /dev/null +++ b/examples/node-client/package.json @@ -0,0 +1,24 @@ +{ + "name": "example-node-client", + "version": "2.0.21", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx --watch src/index.ts", + "start": "tsx src/index.ts", + "client": "tsx src/client.ts", + "check-types": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "rivetkit": "*" + }, + "devDependencies": { + "@types/node": "^22.13.9", + "tsx": "^3.12.7", + "typescript": "^5.5.2", + "vitest": "^3.1.1" + }, + "stableVersion": "0.8.0", + "license": "MIT" +} diff --git a/examples/node-client/src/client.ts b/examples/node-client/src/client.ts new file mode 100644 index 0000000000..f6f506bf12 --- /dev/null +++ b/examples/node-client/src/client.ts @@ -0,0 +1,22 @@ +import { createClient } from "rivetkit/client"; +import type { registry } from "./index.ts"; + +const client = createClient("http://localhost:6420"); + +async function main() { + const counter = client.counter.getOrCreate(["my-counter"]); + + const initial = await counter.getCount(); + console.log("Initial count:", initial); + + const afterIncrement = await counter.increment(5); + console.log("After +5:", afterIncrement); + + const final = await counter.getCount(); + console.log("Final count:", final); +} + +main().catch((error) => { + console.error("Error:", error); + process.exit(1); +}); diff --git a/examples/node-client/src/index.ts b/examples/node-client/src/index.ts new file mode 100644 index 0000000000..4ba57bcb95 --- /dev/null +++ b/examples/node-client/src/index.ts @@ -0,0 +1,22 @@ +import { actor, setup } from "rivetkit"; + +export const counter = actor({ + state: { + count: 0, + }, + + actions: { + increment: (c, amount: number) => { + c.state.count += amount; + return c.state.count; + }, + getCount: (c) => c.state.count, + }, +}); + +export const registry = setup({ + use: { counter }, + startEngine: true, +}); + +registry.start(); diff --git a/examples/node-client/tests/counter.test.ts b/examples/node-client/tests/counter.test.ts new file mode 100644 index 0000000000..b8a5b1525f --- /dev/null +++ b/examples/node-client/tests/counter.test.ts @@ -0,0 +1,50 @@ +import { setupTest } from "rivetkit/test"; +import { describe, expect, test } from "vitest"; +import { registry } from "../src/index.ts"; + +describe("counter actor", () => { + test("starts at zero", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + const counter = client.counter.getOrCreate(["fresh"]); + + expect(await counter.getCount()).toBe(0); + }); + + test("increment returns the new total", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + const counter = client.counter.getOrCreate(["increments"]); + + expect(await counter.increment(3)).toBe(3); + expect(await counter.increment(7)).toBe(10); + }); + + test("state persists across handle re-resolution", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + await client.counter.getOrCreate(["persist"]).increment(5); + + const reResolved = client.counter.getOrCreate(["persist"]); + expect(await reResolved.getCount()).toBe(5); + }); + + test("different keys are isolated", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + await client.counter.getOrCreate(["a"]).increment(1); + await client.counter.getOrCreate(["b"]).increment(99); + + expect(await client.counter.getOrCreate(["a"]).getCount()).toBe(1); + expect(await client.counter.getOrCreate(["b"]).getCount()).toBe(99); + }); + + test("supports negative increments", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + const counter = client.counter.getOrCreate(["signed"]); + + await counter.increment(10); + expect(await counter.increment(-4)).toBe(6); + }); +}); diff --git a/examples/node-client/tsconfig.json b/examples/node-client/tsconfig.json new file mode 100644 index 0000000000..4e95b1c507 --- /dev/null +++ b/examples/node-client/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "bundler", + "types": ["node"], + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true + }, + "include": ["src/**/*", "tests/**/*"] +} diff --git a/examples/node-client/turbo.json b/examples/node-client/turbo.json new file mode 100644 index 0000000000..29d4cb2625 --- /dev/null +++ b/examples/node-client/turbo.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"] +} diff --git a/examples/node-client/vitest.config.ts b/examples/node-client/vitest.config.ts new file mode 100644 index 0000000000..5bdee00206 --- /dev/null +++ b/examples/node-client/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["tests/**/*.test.ts"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79f55aab1b..d3d9b03966 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2224,6 +2224,25 @@ importers: specifier: ^5 version: 5.9.3 + examples/node-client: + dependencies: + rivetkit: + specifier: workspace:* + version: link:../../rivetkit-typescript/packages/rivetkit + devDependencies: + '@types/node': + specifier: ^22.13.9 + version: 22.19.15 + tsx: + specifier: ^3.12.7 + version: 3.14.0 + typescript: + specifier: ^5.5.2 + version: 5.9.3 + vitest: + specifier: ^3.1.1 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.15)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) + examples/per-tenant-database: dependencies: '@rivetkit/react': @@ -3367,7 +3386,7 @@ importers: version: 5.2.2(react-hook-form@7.62.0(react@19.1.0)) '@ladle/react': specifier: ^5.1.1 - version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) '@marsidev/react-turnstile': specifier: ^1.5.0 version: 1.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -3610,7 +3629,7 @@ importers: version: 2.4.3 better-auth: specifier: ^1.5.6 - version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) canvas-confetti: specifier: ^1.9.3 version: 1.9.3 @@ -3730,7 +3749,7 @@ importers: version: 5.2.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.13)(typescript@5.9.3))(typescript@5.9.3) unplugin-macros: specifier: ^0.18.3 - version: 0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + version: 0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.1.0) @@ -3749,7 +3768,7 @@ importers: devDependencies: vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) frontend/packages/components: dependencies: @@ -17771,6 +17790,7 @@ packages: uuid@7.0.3: resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -21576,7 +21596,7 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': + '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': dependencies: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0 @@ -21588,8 +21608,8 @@ snapshots: '@ladle/react-context': 1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mdx-js/mdx': 3.1.1 '@mdx-js/react': 3.1.1(@types/react@19.2.13)(react@19.1.0) - '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) - '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) axe-core: 4.11.1 boxen: 8.0.1 chokidar: 4.0.3 @@ -21616,8 +21636,8 @@ snapshots: remark-gfm: 4.0.1 source-map: 0.7.6 vfile: 6.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - '@swc/helpers' - '@types/node' @@ -25682,11 +25702,11 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 - '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.27 '@swc/core': 1.15.11(@swc/helpers@0.5.17) - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@swc/helpers' @@ -25726,7 +25746,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -25734,7 +25754,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -25837,14 +25857,14 @@ snapshots: msw: 2.12.10(@types/node@22.19.15)(typescript@5.9.3) vite: 5.4.21(@types/node@22.19.15)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) - '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.10(@types/node@20.19.13)(typescript@5.9.3) - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/mocker@4.1.5(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: @@ -26692,7 +26712,7 @@ snapshots: bcryptjs@2.4.3: {} - better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0) '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0)) @@ -26719,7 +26739,7 @@ snapshots: pg: 8.17.2 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -34413,13 +34433,13 @@ snapshots: unpipe@1.0.0: {} - unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: ast-kit: 2.2.0 magic-string-ast: 1.0.3 unplugin: 2.3.10 - vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-node: 5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-node: 5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -34709,13 +34729,13 @@ snapshots: - supports-color - terser - vite-node@5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite-node@5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: cac: 6.7.14 es-module-lexer: 1.7.0 obug: 2.0.0(ms@2.1.3) pathe: 2.0.3 - vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -34788,13 +34808,13 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - typescript @@ -34852,7 +34872,7 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 - vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -34863,7 +34883,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 1.21.7 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -34912,7 +34932,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite@7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -34923,7 +34943,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 1.21.7 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -35217,10 +35237,10 @@ snapshots: - supports-color - terser - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -35237,7 +35257,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 From 9a172f008334384d015efe9885a55f8be7748900 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 1 May 2026 12:27:03 +0200 Subject: [PATCH 076/306] feat(effect): expose Actor.CurrentAddress inside the wake scope 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. --- examples/effect/src/actors/counter/live.ts | 14 ++++- .../packages/effect/src/Actor.ts | 63 +++++++++++++++---- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index 163ad0ebb5..ad0ca83dcf 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -1,4 +1,5 @@ import { Effect, Ref } from "effect" +import { Actor } from "@rivetkit/effect" import { Counter, CounterOverflowError } from "./api.ts" // --- Actor Implementation --- @@ -11,6 +12,13 @@ import { Counter, CounterOverflowError } from "./api.ts" export const CounterLive = Counter.toLayer( // Wake scope (runs each wake, finalizers run on sleep) Effect.gen(function* () { + // Per-instance identity. (name, key) is the user-facing + // pair; actorId is the engine-assigned opaque id. + const address = yield* Actor.CurrentAddress + yield* Effect.log( + `waking ${address.name}/${address.key.join(",")} actorId=${address.actorId}`, + ) + // In-memory per-wake state. Resets on every wake; this v1 // has no persistence. Replace with a persisted state ref // once Actor.State lands. @@ -18,7 +26,11 @@ export const CounterLive = Counter.toLayer( yield* Effect.addFinalizer(() => Ref.get(count).pipe( - Effect.flatMap((n) => Effect.log(`sleeping count=${n}`)), + Effect.flatMap((n) => + Effect.log( + `sleeping ${address.name}/${address.key.join(",")} count=${n}`, + ), + ), ), ) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 01dd10bade..6e837977c5 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -26,6 +26,31 @@ export interface Options { readonly icon?: string; } +/** + * Per-instance identity carried inside the wake scope. An actor + * instance is addressable in two ways: + * + * - `(name, key)` — stable user-facing pair (e.g. "Counter", ["counter-123"]) + * - `actorId` — opaque engine-assigned unique identifier + * + * Available inside `Actor.toLayer`'s build effect via + * `yield* Actor.CurrentAddress`. + */ +export interface Address { + readonly actorId: string; + readonly name: string; + readonly key: ReadonlyArray; +} + +/** + * Context tag for the current actor instance's `Address`. Provided + * once per wake when the build effect runs; capture it into a + * closure if action handlers need it. + */ +export class CurrentAddress extends Context.Service()( + "@rivetkit/effect/Actor/CurrentAddress", +) {} + /** * One actor registered with the `Registry`. The `buildHandlers` * effect is run once per wake by the runner to construct @@ -188,18 +213,34 @@ const buildNativeActor = ( return actorNative({ actions, options: actor.options, - onWake: async (c: { actorId: string }) => { + onWake: async (c: { + actorId: string; + name: string; + key: ReadonlyArray; + }) => { + const address: Address = { + actorId: c.actorId, + name: c.name, + key: c.key, + }; const scope = await runHandler(Scope.make()); + const built = entry.buildHandlers as Effect.Effect< + unknown, + never, + Scope.Scope | CurrentAddress + >; + const withAddress = Effect.provideService( + built, + CurrentAddress, + address, + ); + const withScope = Effect.provideService( + withAddress, + Scope.Scope, + scope, + ); const handlers = await runHandler( - Effect.provideService( - entry.buildHandlers as Effect.Effect< - unknown, - never, - Scope.Scope - >, - Scope.Scope, - scope, - ) as Effect.Effect, + withScope as Effect.Effect, ); instances.set(c.actorId, { handlers: handlers as ActorInstance["handlers"], @@ -318,7 +359,7 @@ export interface Actor< ): Layer.Layer< never, never, - | Exclude + | Exclude | HandlerServices | Action.ServicesServer | Action.ServicesClient From e10dec671e93a5c24bdc92f1d8e4ad8285087fde Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 1 May 2026 13:16:49 +0200 Subject: [PATCH 077/306] feat(effect): add Client service and typed-error round-trip via UserError 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. --- examples/effect/package.json | 1 + examples/effect/src/client-raw.ts | 46 ++++++ examples/effect/src/client.ts | 111 +++++-------- .../packages/effect/src/Actor.ts | 148 +++++++++++++++++- .../packages/effect/src/Client.ts | 93 +++++++++++ .../packages/effect/src/mod.ts | 1 + 6 files changed, 324 insertions(+), 76 deletions(-) create mode 100644 examples/effect/src/client-raw.ts create mode 100644 rivetkit-typescript/packages/effect/src/Client.ts diff --git a/examples/effect/package.json b/examples/effect/package.json index d024a7dd62..08f739e4c2 100644 --- a/examples/effect/package.json +++ b/examples/effect/package.json @@ -6,6 +6,7 @@ "dev": "RIVET_RUN_ENGINE=1 tsx watch src/main.ts", "start": "RIVET_RUN_ENGINE=1 tsx src/main.ts", "client": "tsx src/client.ts", + "client:raw": "tsx src/client-raw.ts", "check-types": "tsc --noEmit" }, "dependencies": { diff --git a/examples/effect/src/client-raw.ts b/examples/effect/src/client-raw.ts new file mode 100644 index 0000000000..16851b0928 --- /dev/null +++ b/examples/effect/src/client-raw.ts @@ -0,0 +1,46 @@ +// Raw-transport smoke test against the Effect example server. Useful +// as an "is the engine alive at all?" diagnostic when the Effect +// client surface in `client.ts` misbehaves. Drives the server using +// a plain rivetkit client with no Effect machinery. +// +// # terminal A — start the server (auto-spawns local engine) +// RIVET_RUN_ENGINE=1 \ +// RIVET_ENGINE_BINARY=$(git rev-parse --show-toplevel)/target/debug/rivet-engine \ +// pnpm start +// +// # terminal B — drive the raw client +// pnpm client:raw +import { createClient } from "rivetkit/client" + +const client = createClient("http://127.0.0.1:6420") as any + +async function main() { + const counter = client.Counter.getOrCreate("counter-raw") + + const initial = await counter.GetCount() + console.log("GetCount (initial):", initial) + + const afterFive = await counter.Increment({ amount: 5 }) + console.log("Increment(5):", afterFive) + + const afterEight = await counter.Increment({ amount: 3 }) + console.log("Increment(3):", afterEight) + + const total = await counter.GetCount() + console.log("GetCount (total):", total) + + // Trigger overflow (limit: 20). Plain client surfaces this as a + // thrown rivetkit RivetError; group should be "user" once typed + // errors are wired and "actor" otherwise. + try { + const overflowed = await counter.Increment({ amount: 20 }) + console.log("Increment(20) [unexpected success]:", overflowed) + } catch (err) { + console.log("Increment(20) [expected error]:", err) + } +} + +main().catch((err) => { + console.error("client smoke test failed:", err) + process.exit(1) +}) diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index eb3f393849..a399dba150 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,82 +1,45 @@ -// End-to-end smoke test for the Effect SDK server slice. -// -// Drives the actor served by `pnpm start` (main.ts) using a plain -// rivetkit client — the Effect Client surface is out of scope for the -// server-only slice. Run alongside the server: +// Effect-flavored client driving the same server as `pnpm start`. +// Run alongside the server: // // # terminal A — start the server (auto-spawns local engine) -// RIVET_RUN_ENGINE=true \ +// RIVET_RUN_ENGINE=1 \ // RIVET_ENGINE_BINARY=$(git rev-parse --show-toplevel)/target/debug/rivet-engine \ // pnpm start // -// # terminal B — drive the client +// # terminal B — drive the Effect client // pnpm client -import { createClient } from "rivetkit/client" - -const client = createClient("http://127.0.0.1:6420") as any - -async function main() { - const counter = client.Counter.getOrCreate("counter-e2e") - - const initial = await counter.GetCount() - console.log("GetCount (initial):", initial) - - const afterFive = await counter.Increment({ amount: 5 }) - console.log("Increment(5):", afterFive) - - const afterEight = await counter.Increment({ amount: 3 }) - console.log("Increment(3):", afterEight) - - const total = await counter.GetCount() - console.log("GetCount (total):", total) - - // Trigger overflow (limit: 20). Step 4 surfaces this as a defect - // (typed-error encoding lands in a follow-up slice). - try { - const overflowed = await counter.Increment({ amount: 20 }) - console.log("Increment(20) [unexpected success]:", overflowed) - } catch (err) { - console.log("Increment(20) [expected error]:", err) - } -} - -main().catch((err) => { - console.error("client smoke test failed:", err) +// +// For raw-transport diagnostics (no Effect), see `client-raw.ts` +// (`pnpm client:raw`). +import { Effect } from "effect" +import { Client } from "@rivetkit/effect" +import { Counter } from "./actors/counter/api.ts" + +const program = Effect.gen(function* () { + const counterClient = yield* Counter.client + const counter = counterClient.getOrCreate(["counter-effect"]) + + const count = yield* counter.Increment({ amount: 5 }) + yield* Effect.log(`Increment(5) -> ${count}`) + + const total = yield* counter.GetCount() + yield* Effect.log(`GetCount -> ${total}`) + + // Trigger overflow (limit: 20). The typed CounterOverflowError + // round-trips through a UserError on the wire and decodes back + // into the original error class — caught by the outer + // `catchTag("CounterOverflowError", ...)`. + const overflowed = yield* counter.Increment({ amount: 100 }) + yield* Effect.log(`Increment(100) [unexpected success]: ${overflowed}`) +}).pipe( + Effect.catchTag("CounterOverflowError", (e) => + Effect.log(`CounterOverflowError caught: limit=${e.limit}`), + ), +) + +const ClientLayer = Client.layer({ endpoint: "http://127.0.0.1:6420" }) + +program.pipe(Effect.provide(ClientLayer), Effect.runPromise).catch((err) => { + console.error("client failed:", err) process.exit(1) }) - -// ------------------------------------------------------------------ -// Target Effect Client surface (parked until the client slice lands). -// See plan: /Users/igassmann/.claude/plans/indexed-baking-crescent.md -// ------------------------------------------------------------------ -// -// import { Effect } from "effect" -// import { Client } from "@rivetkit/effect" -// import { Counter } from "./actors/mod.ts" -// -// const program = Effect.gen(function* () { -// const counterClient = yield* Counter.client -// -// const counter = counterClient.getOrCreate(["counter-123"]) -// -// // Action calls return Effects with types inferred from the schema. -// const count = yield* counter.Increment({ amount: 5 }) -// yield* Effect.log(`Count: ${count}`) -// -// const total = yield* counter.GetCount() -// yield* Effect.log(`Total: ${total}`) -// }) -// // program: Effect -// // ^^^^^^ -// // Missing Client -> compile error naming the central runtime dependency. -// -// // ------------------------------------------------------------------ -// // Wiring: provide Client once. Each actor's .client effect -// // uses that transport to create a contract-specific typed accessor. -// // ------------------------------------------------------------------ -// const ClientLayer = Client.layer({ -// endpoint: "https://api.rivet.dev", -// token: "...", -// }) -// -// program.pipe(Effect.provide(ClientLayer), Effect.runPromise) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 6e837977c5..b0a62247f9 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -10,8 +10,11 @@ import { actor as actorNative, type AnyActorDefinition, setup as setupNative, + UserError, } from "rivetkit"; import type * as Action from "./Action"; +import { Client } from "./Client"; +import * as RivetError from "./RivetError"; const TypeId = "~@rivetkit/effect/Actor"; @@ -203,9 +206,41 @@ const buildNativeActor = ( ); } const decoded = await runHandler(decoders.get(tag)!(payload)); - const result = await runHandler( - handler({ _tag: tag, action, payload: decoded }), + // Wrap the handler so typed failures (matching `errorSchema`) + // are encoded and thrown across the wire as a rivetkit + // `UserError`. The error class's `_tag` becomes the wire + // `code`, the encoded shape rides in `metadata`. Failures that + // don't match the schema fall through and surface as a + // generic infra error. + const handlerEffect = handler({ + _tag: tag, + action, + payload: decoded, + }).pipe( + Effect.catch((typedErr) => + Schema.encodeUnknownEffect(action.errorSchema)( + typedErr, + ).pipe( + Effect.matchEffect({ + onSuccess: (encoded) => + Effect.die( + new UserError( + (typedErr as { message?: string }) + ?.message ?? `${tag} failed`, + { + code: + (typedErr as { _tag?: string }) + ?._tag ?? tag, + metadata: encoded, + }, + ), + ), + onFailure: () => Effect.fail(typedErr), + }), + ), + ), ); + const result = await runHandler(handlerEffect); return await runHandler(encoders.get(tag)!(result)); }; } @@ -338,6 +373,30 @@ type HandlerServices = { export type ActorKey = string | ReadonlyArray; +/** + * A typed handle for one actor instance. Each action becomes a + * method that takes the action's payload-constructor input and + * returns an Effect with the action's success / typed error + * channels baked in. + */ +export type Handle = { + readonly [A in Actions as Action.Tag]: ( + payload: Action.PayloadConstructor, + ) => Effect.Effect< + Action.Success, + Action.Error | RivetError.RivetError, + never + >; +}; + +/** + * Yielded by `Actor.client`. Address an actor instance by key, then + * dispatch typed action calls against the returned `Handle`. + */ +export interface TypedAccessor { + readonly getOrCreate: (key: ActorKey) => Handle; +} + /** * A Rivet Actor contract. It carries the action schemas and * display options, but no server implementation. @@ -365,6 +424,20 @@ export interface Actor< | Action.ServicesClient | Registry >; + + /** + * Effect-yielded typed accessor for this actor. Provide a + * `Client.layer({ ... })` once at the program root; every + * `yield* SomeActor.client` then dispatches through the same + * transport. Per-call signatures are `Effect` — schema services are pulled in at the + * getter level via `Action.ServicesClient`. + */ + readonly client: Effect.Effect< + TypedAccessor, + never, + Client | Action.ServicesClient + >; } /** @@ -417,6 +490,77 @@ const Proto = { }), ); }, + get client() { + const self = this as unknown as AnyWithProps; + return Effect.gen(function* () { + const client = yield* Client; + const actions = self.actions; + return { + getOrCreate: (key: ActorKey) => { + const handle: Record< + string, + (p: unknown) => Effect.Effect + > = {}; + for (const action of actions) { + const tag = action._tag; + handle[tag] = (payload) => + Effect.gen(function* () { + const encoded = yield* Schema.encodeUnknownEffect( + action.payloadSchema, + )(payload); + const raw = yield* client + .callAction({ + actorName: self._tag, + key, + actionName: tag, + encodedPayload: encoded, + }) + .pipe( + // Try `errorSchema` first against the + // wire metadata. Fall back to wrapping + // the raw RivetError via `RivetErrorFromWire`. + Effect.catch((rivetErr) => + Schema.decodeUnknownEffect( + action.errorSchema, + )( + (rivetErr as { metadata?: unknown }) + .metadata, + ).pipe( + Effect.matchEffect({ + onSuccess: (typed) => + Effect.fail(typed), + onFailure: () => + Schema.decodeUnknownEffect( + RivetError.RivetErrorFromWire, + )({ + group: rivetErr.group, + code: rivetErr.code, + message: + rivetErr.message, + metadata: ( + rivetErr as { + metadata?: unknown; + } + ).metadata, + }).pipe( + Effect.flatMap( + Effect.fail, + ), + ), + }), + ), + ), + ); + return yield* Schema.decodeUnknownEffect( + action.successSchema, + )(raw); + }) as Effect.Effect; + } + return handle as Handle; + }, + }; + }); + }, }; const makeProto = < diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts new file mode 100644 index 0000000000..b1f41212e2 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -0,0 +1,93 @@ +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { createClient } from "rivetkit/client"; +import { RivetError as NativeRivetError } from "rivetkit"; + +/** + * Connection options for the Rivet Engine client transport. Mirrors + * `EngineOptions` on the server side: an optional endpoint (with URL + * auth syntax for namespace and token), plus standalone `token` and + * `namespace` fields. All fields are optional and fall back to the + * matching `RIVET_*` environment variables. + */ +export interface ClientOptions { + /** + * Endpoint URL of the Rivet Engine. + * + * Supports URL auth syntax for namespace and token: + * - `https://namespace:token@api.rivet.dev` + * - `https://namespace@api.rivet.dev` + * + * Falls back to `RIVET_ENDPOINT`, then `http://localhost:6420`. + */ + readonly endpoint?: string; + /** Auth token. Falls back to `RIVET_TOKEN`. */ + readonly token?: string; + /** Namespace. Falls back to `RIVET_NAMESPACE`, then `"default"`. */ + readonly namespace?: string; +} + +export interface ClientShape { + /** + * Generic action dispatch. Returns the raw, undecoded result from + * the wire. On rejection from the underlying transport, surfaces + * the rivetkit `RivetError` instance via `Effect.fail` — the + * caller decides whether to decode `metadata` as a typed error or + * wrap it through the wire codec. + */ + readonly callAction: (params: { + readonly actorName: string; + readonly key: string | ReadonlyArray; + readonly actionName: string; + readonly encodedPayload: unknown; + }) => Effect.Effect; +} + +/** + * Service holding the rivetkit client transport. Provided once via + * `Client.layer({ ... })`. Consumed by `Actor.client` to dispatch + * action calls through a single shared transport. + */ +export class Client extends Context.Service()( + "@rivetkit/effect/Client", +) { + static layer(options: ClientOptions = {}): Layer.Layer { + return Layer.effect( + Client, + Effect.sync(() => { + const native = createClient(options) as any; + const callAction: ClientShape["callAction"] = ({ + actorName, + key, + actionName, + encodedPayload, + }) => + Effect.tryPromise({ + try: () => + native[actorName].getOrCreate(key).action({ + name: actionName, + args: [encodedPayload], + }), + catch: (cause) => + cause instanceof NativeRivetError + ? cause + : new NativeRivetError( + "client", + "unknown", + cause instanceof Error + ? cause.message + : String(cause), + { + cause: + cause instanceof Error + ? cause + : undefined, + }, + ), + }); + return Client.of({ callAction }); + }), + ); + } +} diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index 141e5e0868..fd98e4f1d3 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,4 +1,5 @@ export * as Actor from "./Actor"; export { Registry, Runner } from "./Actor"; export * as Action from "./Action"; +export { Client } from "./Client"; export * as RivetError from "./RivetError"; From 5edd5e6d5071137f68dd44ced9ec4b5a9b80a7aa Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 1 May 2026 13:25:01 +0200 Subject: [PATCH 078/306] chore(examples/effect): update start/dev scripts to include RIVET_ENGINE_BINARY configuration --- examples/effect/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/effect/package.json b/examples/effect/package.json index 08f739e4c2..2297c2a711 100644 --- a/examples/effect/package.json +++ b/examples/effect/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "scripts": { - "dev": "RIVET_RUN_ENGINE=1 tsx watch src/main.ts", - "start": "RIVET_RUN_ENGINE=1 tsx src/main.ts", + "dev": "RIVET_RUN_ENGINE=1 RIVET_ENGINE_BINARY=../../target/debug/rivet-engine tsx watch src/main.ts", + "start": "RIVET_RUN_ENGINE=1 RIVET_ENGINE_BINARY=../../target/debug/rivet-engine tsx src/main.ts", "client": "tsx src/client.ts", "client:raw": "tsx src/client-raw.ts", "check-types": "tsc --noEmit" From ddcd09f065bde8fabf746d165f16bb02028e162b Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 1 May 2026 13:43:58 +0200 Subject: [PATCH 079/306] feat(examples/effect): add message field to CounterOverflowError 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. --- examples/effect/src/actors/counter/api.ts | 5 ++++- examples/effect/src/actors/counter/live.ts | 5 ++++- examples/effect/src/client.ts | 4 +++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts index 849d4ed4cb..70148821a7 100644 --- a/examples/effect/src/actors/counter/api.ts +++ b/examples/effect/src/actors/counter/api.ts @@ -5,7 +5,10 @@ import { Actor, Action } from "@rivetkit/effect" export class CounterOverflowError extends Schema.TaggedErrorClass()( "CounterOverflowError", - { limit: Schema.Number }, + { + limit: Schema.Number, + message: Schema.String, + }, ) {} // --- Actions --- diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index ad0ca83dcf..9414931444 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -43,7 +43,10 @@ export const CounterLive = Counter.toLayer( (n) => n + payload.amount, ) if (next > 20) { - return yield* new CounterOverflowError({ limit: 20 }) + return yield* new CounterOverflowError({ + limit: 20, + message: `count ${next} would exceed limit 20`, + }) } return next }), diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index a399dba150..03bbdb21af 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -33,7 +33,9 @@ const program = Effect.gen(function* () { yield* Effect.log(`Increment(100) [unexpected success]: ${overflowed}`) }).pipe( Effect.catchTag("CounterOverflowError", (e) => - Effect.log(`CounterOverflowError caught: limit=${e.limit}`), + Effect.log( + `CounterOverflowError caught: limit=${e.limit} message="${e.message}"`, + ), ), ) From d01ddb74fb171ded3b1a966aa3ede88ed8f6fd06 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 1 May 2026 15:10:36 +0200 Subject: [PATCH 080/306] refactor(effect): remove unused generic from `Effect.context` call in Actor --- rivetkit-typescript/packages/effect/src/Actor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index b0a62247f9..6439cca7f5 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -310,7 +310,7 @@ export class Runner extends Context.Service()( // (which run in rivetkit's plain Promise world) can run // handler effects against the same services Runner.start // was provided with. - const context = yield* Effect.context(); + const context = yield* Effect.context(); const runHandler = ( effect: Effect.Effect, ): Promise => From 274ca70497cde34bce577b89a2002eec0c1910aa Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 1 May 2026 19:25:14 +0200 Subject: [PATCH 081/306] refactor(effect): fuse action dispatch into one Effect, drop codec maps --- .../packages/effect/src/Actor.ts | 154 ++++++++---------- 1 file changed, 67 insertions(+), 87 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 6439cca7f5..ae4fb5b1ab 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -1,3 +1,4 @@ +import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; @@ -21,6 +22,17 @@ const TypeId = "~@rivetkit/effect/Actor"; export const isActor = (u: unknown): u is Actor => Predicate.hasProperty(u, TypeId); +/** + * Refinement that narrows `unknown` to an object with `key` set to a + * `string`. Composes `Predicate.hasProperty(key)` with + * `Predicate.isString` in one go. + */ +const hasStringProperty = ( + key: K, +): Predicate.Refinement => + (u): u is { readonly [P in K]: string } => + Predicate.hasProperty(u, key) && Predicate.isString(u[key]); + /** * Display options carried by an actor contract. */ @@ -164,84 +176,59 @@ type ActorInstance = { const buildNativeActor = ( entry: RegistryEntry, instances: Map, - runHandler: (effect: Effect.Effect) => Promise, + services: Context.Context, ): AnyActorDefinition => { const actor = entry.actor; - const decoders = new Map< - string, - (v: unknown) => Effect.Effect - >(); - const encoders = new Map< - string, - (v: unknown) => Effect.Effect - >(); - for (const action of actor.actions) { - decoders.set( - action._tag, - Schema.decodeUnknownEffect(action.payloadSchema) as never, - ); - encoders.set( - action._tag, - Schema.encodeUnknownEffect(action.successSchema) as never, - ); - } const actions: Record< string, (c: { actorId: string }, payload?: unknown) => Promise > = {}; for (const action of actor.actions) { - const tag = action._tag; - actions[tag] = async (c, payload) => { + const decodePayload = Schema.decodeUnknownEffect(action.payloadSchema); + const encodeSuccess = Schema.encodeUnknownEffect(action.successSchema); + const encodeError = Schema.encodeUnknownEffect(action.errorSchema); + actions[action._tag] = async (c, payload) => { const inst = instances.get(c.actorId); if (!inst) { throw new Error( `actor ${actor._tag}/${c.actorId} has no handlers (onWake didn't run?)`, ); } - const handler = inst.handlers[tag]; + const handler = inst.handlers[action._tag]; if (!handler) { throw new Error( - `actor ${actor._tag} has no handler for action ${tag}`, + `actor ${actor._tag} has no handler for action ${action._tag}`, ); } - const decoded = await runHandler(decoders.get(tag)!(payload)); - // Wrap the handler so typed failures (matching `errorSchema`) - // are encoded and thrown across the wire as a rivetkit - // `UserError`. The error class's `_tag` becomes the wire - // `code`, the encoded shape rides in `metadata`. Failures that - // don't match the schema fall through and surface as a - // generic infra error. - const handlerEffect = handler({ - _tag: tag, - action, - payload: decoded, - }).pipe( - Effect.catch((typedErr) => - Schema.encodeUnknownEffect(action.errorSchema)( - typedErr, - ).pipe( - Effect.matchEffect({ - onSuccess: (encoded) => - Effect.die( - new UserError( - (typedErr as { message?: string }) - ?.message ?? `${tag} failed`, - { - code: - (typedErr as { _tag?: string }) - ?._tag ?? tag, - metadata: encoded, - }, - ), - ), - onFailure: () => Effect.fail(typedErr), + + const pipeline = Effect.gen(function* () { + const decoded = yield* decodePayload(payload).pipe(Effect.orDie); + const result = yield* handler({ + _tag: action._tag, + action, + payload: decoded, + }).pipe( + Effect.catch((expectedError) => Effect.gen(function*(){ + const error = yield* encodeError(expectedError).pipe(Effect.orDie) + return yield* Effect.die( + new UserError( + hasStringProperty("message")(error) ? error.message : `${action._tag} failed`, + { + code: hasStringProperty("_tag")(error) ? error._tag : undefined, + metadata: error + }, + ), + ) }), ), - ), - ); - const result = await runHandler(handlerEffect); - return await runHandler(encoders.get(tag)!(result)); + ); + return yield* encodeSuccess(result).pipe(Effect.orDie); + }); + + const exit = await Effect.runPromiseExitWith(services)(pipeline); + if (Exit.isSuccess(exit)) return exit.value; + throw Cause.squash(exit.cause); }; } @@ -258,25 +245,25 @@ const buildNativeActor = ( name: c.name, key: c.key, }; - const scope = await runHandler(Scope.make()); - const built = entry.buildHandlers as Effect.Effect< - unknown, - never, - Scope.Scope | CurrentAddress - >; - const withAddress = Effect.provideService( - built, - CurrentAddress, - address, - ); - const withScope = Effect.provideService( - withAddress, - Scope.Scope, - scope, - ); - const handlers = await runHandler( - withScope as Effect.Effect, - ); + // Single fused effect: build the wake scope, then run + // `buildHandlers` in that scope with `CurrentAddress` + // provided. Keeping both pieces in one fiber means a + // `buildHandlers` failure shares its cause with the scope it + // would have owned. + const acquire = Effect.gen(function* () { + const scope = yield* Scope.make(); + const built = entry.buildHandlers as Effect.Effect< + unknown, + never, + Scope.Scope | CurrentAddress + >; + const handlers = yield* (built.pipe( + Effect.provideService(CurrentAddress, address), + Effect.provideService(Scope.Scope, scope), + ) as Effect.Effect); + return { handlers, scope }; + }); + const { handlers, scope } = await Effect.runPromiseWith(services)(acquire); instances.set(c.actorId, { handlers: handlers as ActorInstance["handlers"], scope, @@ -286,7 +273,7 @@ const buildNativeActor = ( const inst = instances.get(c.actorId); if (!inst) return; instances.delete(c.actorId); - await runHandler(Scope.close(inst.scope, Exit.void)); + await Effect.runPromiseWith(services)(Scope.close(inst.scope, Exit.void)); }, } as Parameters[0]); }; @@ -310,21 +297,14 @@ export class Runner extends Context.Service()( // (which run in rivetkit's plain Promise world) can run // handler effects against the same services Runner.start // was provided with. - const context = yield* Effect.context(); - const runHandler = ( - effect: Effect.Effect, - ): Promise => - Effect.runPromiseWith(context)( - effect as Effect.Effect, - ); - + const services = yield* Effect.context(); const instances = new Map(); const use: Record = {}; for (const entry of entries) { use[entry.actor._tag] = buildNativeActor( entry, instances, - runHandler, + services, ); } From 6184b1833db3eae9f2d4e04b98d2cdc3b0b21d0c Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 1 May 2026 19:42:18 +0200 Subject: [PATCH 082/306] docs(examples/effect): re-add v0 surface as commented-out scaffolding 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. --- examples/effect/src/actors/counter/api.ts | 20 +++++++ examples/effect/src/actors/counter/live.ts | 61 +++++++++++++++++++++- examples/effect/src/client.ts | 24 ++++----- 3 files changed, 89 insertions(+), 16 deletions(-) diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts index 70148821a7..678e0cd298 100644 --- a/examples/effect/src/actors/counter/api.ts +++ b/examples/effect/src/actors/counter/api.ts @@ -41,13 +41,33 @@ export const GetCount = Action.make("GetCount", { success: Schema.Number, }) +// --- Messages (not yet implemented) --- +// +// // Non-completable (fire-and-forget) +// export const Reset = Message.make("Reset", { +// payload: { reason: Schema.String }, +// }) +// +// // Completable (sender can await a typed response) +// export const IncrementBy = Message.make("IncrementBy", { +// payload: { amount: Schema.Number }, +// success: Schema.Number, +// }) + // --- Actor Definition --- // The definition is the actor's public contract. It carries no // implementation. Both server and client code import this; // the implementation stays server-only. export const Counter = Actor.make("Counter", { + // state: Schema.Struct({ + // count: Schema.Number.pipe( + // Schema.withConstructorDefault(Effect.succeed(0)), + // ), + // }), actions: [Increment, GetCount], + // messages: [Reset, IncrementBy], // durable, queued, background + // events: { countChanged: Schema.Number }, options: { name: "Counter", // Human-friendly display name icon: "comments", // FontAwesome icon name diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index 9414931444..74c8d773aa 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -12,8 +12,36 @@ import { Counter, CounterOverflowError } from "./api.ts" export const CounterLive = Counter.toLayer( // Wake scope (runs each wake, finalizers run on sleep) Effect.gen(function* () { - // Per-instance identity. (name, key) is the user-facing - // pair; actorId is the engine-assigned opaque id. + // Actor-provided services are yielded from the Effect context. + // They are scoped to this actor instance, not to individual + // action calls. This means all action handlers below close + // over the same state, events, kv, and db references. + // + // Because services come through the context (not a context + // parameter like the current SDK's `c`), they are: + // + // - Visible in the type signature. The Effect's R channel + // declares exactly which services are required. + // + // - Swappable via layers. Tests can provide an in-memory KV + // or a mock DB without changing the actor code. + + // PersistedSubscriptionRef extends SubscriptionRef with + // throttled durable persistence. Standard SubscriptionRef + // combinators (get, set, update, modify, changes) work as-is. + // Every published change schedules a save via the configured + // stateSaveInterval; the wake-scope finalizer flushes pending + // writes before sleep so state is durable on teardown. Use + // PersistedSubscriptionRef.sync / updateAndSync when an action + // must wait for durability before responding. + // const state = yield* Counter.State + // // ^ PersistedSubscriptionRef<{ count: number }> + // const events = yield* Counter.Events + // // ^ { countChanged: PubSub } + // const messages = yield* Counter.Messages + // // ^ MessageQueue + // const kv = yield* Actor.Kv + // const db = yield* Actor.Db const address = yield* Actor.CurrentAddress yield* Effect.log( `waking ${address.name}/${address.key.join(",")} actorId=${address.actorId}`, @@ -34,6 +62,34 @@ export const CounterLive = Counter.toLayer( ), ) + // --- Message processing (not yet implemented) --- + // Pull-based: the actor controls when to take the next message. + // Forked into a scoped fiber, so it runs in the background and + // is canceled on sleep. Re-enable once Counter.Messages lands. + // + // yield* Effect.gen(function* () { + // const msg = yield* Queue.take(messages) + // yield* Match.value(msg).pipe( + // Match.tag("Reset", () => + // Effect.gen(function* () { + // yield* PersistedSubscriptionRef.set(state, { count: 0 }) + // yield* PubSub.publish(events.countChanged, 0) + // }) + // ), + // Match.tag("IncrementBy", ({ payload, complete }) => + // Effect.gen(function* () { + // const next = yield* PersistedSubscriptionRef.updateAndGet( + // state, + // (s) => ({ count: s.count + payload.amount }), + // ) + // yield* PubSub.publish(events.countChanged, next.count) + // yield* complete(next.count) + // }) + // ), + // Match.exhaustive, + // ) + // }).pipe(Effect.forever, Effect.forkScoped) + // --- Action handlers (request-response) --- return Counter.of({ Increment: ({ payload }) => @@ -48,6 +104,7 @@ export const CounterLive = Counter.toLayer( message: `count ${next} would exceed limit 20`, }) } + // yield* PubSub.publish(events.countChanged, next) return next }), diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 03bbdb21af..316fbab908 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,19 +1,6 @@ -// Effect-flavored client driving the same server as `pnpm start`. -// Run alongside the server: -// -// # terminal A — start the server (auto-spawns local engine) -// RIVET_RUN_ENGINE=1 \ -// RIVET_ENGINE_BINARY=$(git rev-parse --show-toplevel)/target/debug/rivet-engine \ -// pnpm start -// -// # terminal B — drive the Effect client -// pnpm client -// -// For raw-transport diagnostics (no Effect), see `client-raw.ts` -// (`pnpm client:raw`). import { Effect } from "effect" import { Client } from "@rivetkit/effect" -import { Counter } from "./actors/counter/api.ts" +import { Counter /*, IncrementBy */ } from "./actors/counter/api.ts" const program = Effect.gen(function* () { const counterClient = yield* Counter.client @@ -25,6 +12,15 @@ const program = Effect.gen(function* () { const total = yield* counter.GetCount() yield* Effect.log(`GetCount -> ${total}`) + // const newCount = yield* counter.send(IncrementBy({ amount: 3 })) + // yield* Effect.log(`IncrementBy(3) -> ${newCount}`) + // + // // subscribe returns a Stream typed from the event schema. + // yield* counter.subscribe("countChanged").pipe( + // Stream.take(3), + // Stream.runForEach((n) => Effect.log(`countChanged: ${n}`)), + // ) + // Trigger overflow (limit: 20). The typed CounterOverflowError // round-trips through a UserError on the wire and decodes back // into the original error class — caught by the outer From 23e7dca2833f300e7802eb3f49e7accecbb1039e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 1 May 2026 19:43:15 +0200 Subject: [PATCH 083/306] docs(examples/effect): drop redundant client-raw.ts header comment --- examples/effect/src/client-raw.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/examples/effect/src/client-raw.ts b/examples/effect/src/client-raw.ts index 16851b0928..9aad7f2ca0 100644 --- a/examples/effect/src/client-raw.ts +++ b/examples/effect/src/client-raw.ts @@ -1,15 +1,3 @@ -// Raw-transport smoke test against the Effect example server. Useful -// as an "is the engine alive at all?" diagnostic when the Effect -// client surface in `client.ts` misbehaves. Drives the server using -// a plain rivetkit client with no Effect machinery. -// -// # terminal A — start the server (auto-spawns local engine) -// RIVET_RUN_ENGINE=1 \ -// RIVET_ENGINE_BINARY=$(git rev-parse --show-toplevel)/target/debug/rivet-engine \ -// pnpm start -// -// # terminal B — drive the raw client -// pnpm client:raw import { createClient } from "rivetkit/client" const client = createClient("http://127.0.0.1:6420") as any From 59046051fd3caa40278e548e8942ee6d2a28b4eb Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 11:24:58 +0200 Subject: [PATCH 084/306] refactor(effect): use namespace imports for rivetkit symbols --- .../packages/effect/src/Actor.ts | 19 +++++++------------ .../packages/effect/src/Client.ts | 12 ++++++------ .../packages/effect/src/RivetError.ts | 14 +++++--------- 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index ae4fb5b1ab..81049e9f4e 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -7,12 +7,7 @@ import * as Predicate from "effect/Predicate"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; -import { - actor as actorNative, - type AnyActorDefinition, - setup as setupNative, - UserError, -} from "rivetkit"; +import * as Rivetkit from "rivetkit"; import type * as Action from "./Action"; import { Client } from "./Client"; import * as RivetError from "./RivetError"; @@ -177,7 +172,7 @@ const buildNativeActor = ( entry: RegistryEntry, instances: Map, services: Context.Context, -): AnyActorDefinition => { +): Rivetkit.AnyActorDefinition => { const actor = entry.actor; const actions: Record< @@ -212,7 +207,7 @@ const buildNativeActor = ( Effect.catch((expectedError) => Effect.gen(function*(){ const error = yield* encodeError(expectedError).pipe(Effect.orDie) return yield* Effect.die( - new UserError( + new Rivetkit.UserError( hasStringProperty("message")(error) ? error.message : `${action._tag} failed`, { code: hasStringProperty("_tag")(error) ? error._tag : undefined, @@ -232,7 +227,7 @@ const buildNativeActor = ( }; } - return actorNative({ + return Rivetkit.actor({ actions, options: actor.options, onWake: async (c: { @@ -275,7 +270,7 @@ const buildNativeActor = ( instances.delete(c.actorId); await Effect.runPromiseWith(services)(Scope.close(inst.scope, Exit.void)); }, - } as Parameters[0]); + } as Parameters[0]); }; /** @@ -299,7 +294,7 @@ export class Runner extends Context.Service()( // was provided with. const services = yield* Effect.context(); const instances = new Map(); - const use: Record = {}; + const use: Record = {}; for (const entry of entries) { use[entry.actor._tag] = buildNativeActor( entry, @@ -308,7 +303,7 @@ export class Runner extends Context.Service()( ); } - const native = setupNative({ + const native = Rivetkit.setup({ use, endpoint: registry.engineOptions.endpoint, token: registry.engineOptions.token, diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index b1f41212e2..8b64766094 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -1,8 +1,8 @@ import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import { createClient } from "rivetkit/client"; -import { RivetError as NativeRivetError } from "rivetkit"; +import * as Rivetkit from "rivetkit"; +import * as RivetkitClient from "rivetkit/client"; /** * Connection options for the Rivet Engine client transport. Mirrors @@ -41,7 +41,7 @@ export interface ClientShape { readonly key: string | ReadonlyArray; readonly actionName: string; readonly encodedPayload: unknown; - }) => Effect.Effect; + }) => Effect.Effect; } /** @@ -56,7 +56,7 @@ export class Client extends Context.Service()( return Layer.effect( Client, Effect.sync(() => { - const native = createClient(options) as any; + const native = RivetkitClient.createClient(options) as any; const callAction: ClientShape["callAction"] = ({ actorName, key, @@ -70,9 +70,9 @@ export class Client extends Context.Service()( args: [encodedPayload], }), catch: (cause) => - cause instanceof NativeRivetError + cause instanceof Rivetkit.RivetError ? cause - : new NativeRivetError( + : new Rivetkit.RivetError( "client", "unknown", cause instanceof Error diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 916284600a..625d4cb519 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -1,10 +1,6 @@ import * as Schema from "effect/Schema"; import * as Getter from "effect/SchemaGetter"; -import { - RivetError as RivetErrorClass, - type RivetErrorLike, - type RivetErrorOptions, -} from "rivetkit"; +import * as Rivetkit from "rivetkit"; /** * The cross-boundary Rivet error. Wraps the underlying @@ -17,14 +13,14 @@ import { */ export class RivetError extends Schema.TaggedErrorClass()( "RivetError", - { error: Schema.instanceOf(RivetErrorClass) }, + { error: Schema.instanceOf(Rivetkit.RivetError) }, ) {} // On-the-wire envelope: the subset of rivetkit's `RivetErrorLike` that // crosses the action boundary. `Pick`ing here anchors the codec // against drift in the canonical wire shape. type WirePayload = Pick< - RivetErrorLike, + Rivetkit.RivetErrorLike, "group" | "code" | "message" | "metadata" >; @@ -45,9 +41,9 @@ export const RivetErrorFromWire = Wire.pipe( decode: Getter.transform( ({ group, code, message, metadata }) => new RivetError({ - error: new RivetErrorClass(group, code, message, { + error: new Rivetkit.RivetError(group, code, message, { metadata, - } satisfies RivetErrorOptions), + } satisfies Rivetkit.RivetErrorOptions), }), ), encode: Getter.transform((e: RivetError) => { From dfacc866a4dcdf8170faf607c3339ea8a9c92833 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 11:31:50 +0200 Subject: [PATCH 085/306] refactor(effect): consolidate effect submodule imports into one line --- .../packages/effect/src/Action.ts | 3 +-- .../packages/effect/src/Actor.ts | 20 ++++++++++--------- .../packages/effect/src/Client.ts | 4 +--- .../packages/effect/src/RivetError.ts | 7 +++---- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index d07fde9f3e..e0dc03900d 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -1,5 +1,4 @@ -import * as Predicate from "effect/Predicate"; -import * as Schema from "effect/Schema"; +import { Predicate, Schema } from "effect"; import { RivetErrorFromWire } from "./RivetError"; const TypeId = "~@rivetkit/effect/Action"; diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 81049e9f4e..17d4f5b271 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -1,12 +1,14 @@ -import * as Cause from "effect/Cause"; -import * as Context from "effect/Context"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Layer from "effect/Layer"; -import * as Predicate from "effect/Predicate"; -import * as Ref from "effect/Ref"; -import * as Schema from "effect/Schema"; -import * as Scope from "effect/Scope"; +import { + Cause, + Context, + Effect, + Exit, + Layer, + Predicate, + Ref, + Schema, + Scope, +} from "effect"; import * as Rivetkit from "rivetkit"; import type * as Action from "./Action"; import { Client } from "./Client"; diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 8b64766094..6d1eb0810f 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -1,6 +1,4 @@ -import * as Context from "effect/Context"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; +import { Context, Effect, Layer } from "effect"; import * as Rivetkit from "rivetkit"; import * as RivetkitClient from "rivetkit/client"; diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 625d4cb519..a9754ee0d0 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -1,5 +1,4 @@ -import * as Schema from "effect/Schema"; -import * as Getter from "effect/SchemaGetter"; +import { Schema, SchemaGetter } from "effect"; import * as Rivetkit from "rivetkit"; /** @@ -38,7 +37,7 @@ const Wire = Schema.Struct({ */ export const RivetErrorFromWire = Wire.pipe( Schema.decodeTo(Schema.instanceOf(RivetError), { - decode: Getter.transform( + decode: SchemaGetter.transform( ({ group, code, message, metadata }) => new RivetError({ error: new Rivetkit.RivetError(group, code, message, { @@ -46,7 +45,7 @@ export const RivetErrorFromWire = Wire.pipe( } satisfies Rivetkit.RivetErrorOptions), }), ), - encode: Getter.transform((e: RivetError) => { + encode: SchemaGetter.transform((e: RivetError) => { const out: WirePayload = { group: e.error.group, code: e.error.code, From 7eb6afc2c6f6c72f64610a0ff068aa653ecaf19c Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 11:32:11 +0200 Subject: [PATCH 086/306] chore(effect): apply biome formatting --- .../packages/effect/src/Actor.ts | 94 ++++++++++++------- .../packages/effect/src/mod.ts | 2 +- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 17d4f5b271..bd8a983132 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -24,9 +24,10 @@ export const isActor = (u: unknown): u is Actor => * `string`. Composes `Predicate.hasProperty(key)` with * `Predicate.isString` in one go. */ -const hasStringProperty = ( - key: K, -): Predicate.Refinement => +const hasStringProperty = + ( + key: K, + ): Predicate.Refinement => (u): u is { readonly [P in K]: string } => Predicate.hasProperty(u, key) && Predicate.isString(u[key]); @@ -200,23 +201,32 @@ const buildNativeActor = ( } const pipeline = Effect.gen(function* () { - const decoded = yield* decodePayload(payload).pipe(Effect.orDie); + const decoded = yield* decodePayload(payload).pipe( + Effect.orDie, + ); const result = yield* handler({ _tag: action._tag, action, payload: decoded, }).pipe( - Effect.catch((expectedError) => Effect.gen(function*(){ - const error = yield* encodeError(expectedError).pipe(Effect.orDie) - return yield* Effect.die( - new Rivetkit.UserError( - hasStringProperty("message")(error) ? error.message : `${action._tag} failed`, - { - code: hasStringProperty("_tag")(error) ? error._tag : undefined, - metadata: error - }, - ), - ) + Effect.catch((expectedError) => + Effect.gen(function* () { + const error = yield* encodeError( + expectedError, + ).pipe(Effect.orDie); + return yield* Effect.die( + new Rivetkit.UserError( + hasStringProperty("message")(error) + ? error.message + : `${action._tag} failed`, + { + code: hasStringProperty("_tag")(error) + ? error._tag + : undefined, + metadata: error, + }, + ), + ); }), ), ); @@ -254,13 +264,14 @@ const buildNativeActor = ( never, Scope.Scope | CurrentAddress >; - const handlers = yield* (built.pipe( + const handlers = yield* built.pipe( Effect.provideService(CurrentAddress, address), Effect.provideService(Scope.Scope, scope), - ) as Effect.Effect); + ) as Effect.Effect; return { handlers, scope }; }); - const { handlers, scope } = await Effect.runPromiseWith(services)(acquire); + const { handlers, scope } = + await Effect.runPromiseWith(services)(acquire); instances.set(c.actorId, { handlers: handlers as ActorInstance["handlers"], scope, @@ -270,7 +281,9 @@ const buildNativeActor = ( const inst = instances.get(c.actorId); if (!inst) return; instances.delete(c.actorId); - await Effect.runPromiseWith(services)(Scope.close(inst.scope, Exit.void)); + await Effect.runPromiseWith(services)( + Scope.close(inst.scope, Exit.void), + ); }, } as Parameters[0]); }; @@ -326,7 +339,12 @@ export class Runner extends Context.Service()( } export type ActionRequest = - A extends Action.Action + A extends Action.Action< + infer Tag, + infer Payload, + infer _Success, + infer _Error + > ? { readonly _tag: Tag; readonly action: A; @@ -433,19 +451,21 @@ export interface AnyWithProps extends Actor {} export type Name = A extends Actor ? _Name : never; -export type Actions = A extends Actor ? _Actions : never; +export type Actions = + A extends Actor ? _Actions : never; -export type Services = A extends Actor - ? Action.Services<_Actions> - : never; +export type Services = + A extends Actor ? Action.Services<_Actions> : never; -export type ClientServices = A extends Actor - ? Action.ServicesClient<_Actions> - : never; +export type ClientServices = + A extends Actor + ? Action.ServicesClient<_Actions> + : never; -export type ServerServices = A extends Actor - ? Action.ServicesServer<_Actions> - : never; +export type ServerServices = + A extends Actor + ? Action.ServicesServer<_Actions> + : never; const identity = (value: A): A => value; @@ -482,9 +502,10 @@ const Proto = { const tag = action._tag; handle[tag] = (payload) => Effect.gen(function* () { - const encoded = yield* Schema.encodeUnknownEffect( - action.payloadSchema, - )(payload); + const encoded = + yield* Schema.encodeUnknownEffect( + action.payloadSchema, + )(payload); const raw = yield* client .callAction({ actorName: self._tag, @@ -500,8 +521,11 @@ const Proto = { Schema.decodeUnknownEffect( action.errorSchema, )( - (rivetErr as { metadata?: unknown }) - .metadata, + ( + rivetErr as { + metadata?: unknown; + } + ).metadata, ).pipe( Effect.matchEffect({ onSuccess: (typed) => diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index fd98e4f1d3..dc8d3204b0 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,5 +1,5 @@ +export * as Action from "./Action"; export * as Actor from "./Actor"; export { Registry, Runner } from "./Actor"; -export * as Action from "./Action"; export { Client } from "./Client"; export * as RivetError from "./RivetError"; From 93b421756208e81e3a795e0c5ce8a3e748007107 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 11:47:59 +0200 Subject: [PATCH 087/306] refactor(effect): rename native* identifiers to rivetkit* --- rivetkit-typescript/packages/effect/src/Actor.ts | 8 ++++---- rivetkit-typescript/packages/effect/src/Client.ts | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index bd8a983132..ce05a88c7f 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -171,7 +171,7 @@ type ActorInstance = { readonly scope: Scope.Closeable; }; -const buildNativeActor = ( +const toRivetkitActor = ( entry: RegistryEntry, instances: Map, services: Context.Context, @@ -311,20 +311,20 @@ export class Runner extends Context.Service()( const instances = new Map(); const use: Record = {}; for (const entry of entries) { - use[entry.actor._tag] = buildNativeActor( + use[entry.actor._tag] = toRivetkitActor( entry, instances, services, ); } - const native = Rivetkit.setup({ + const rivetkitRegistry = Rivetkit.setup({ use, endpoint: registry.engineOptions.endpoint, token: registry.engineOptions.token, namespace: registry.engineOptions.namespace, }); - yield* Effect.sync(() => native.start()); + yield* Effect.sync(() => rivetkitRegistry.start()); return Runner.of({ mode: "start" }); }), ); diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 6d1eb0810f..c1726a97dd 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -54,7 +54,9 @@ export class Client extends Context.Service()( return Layer.effect( Client, Effect.sync(() => { - const native = RivetkitClient.createClient(options) as any; + const rivetkitClient = RivetkitClient.createClient( + options, + ) as any; const callAction: ClientShape["callAction"] = ({ actorName, key, @@ -63,7 +65,7 @@ export class Client extends Context.Service()( }) => Effect.tryPromise({ try: () => - native[actorName].getOrCreate(key).action({ + rivetkitClient[actorName].getOrCreate(key).action({ name: actionName, args: [encodedPayload], }), From 26d993c2b12304cc95f117cc38ebc6b9003e11cc Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 11:58:25 +0200 Subject: [PATCH 088/306] refactor(effect): rename Address to CurrentAddress --- rivetkit-typescript/packages/effect/src/Actor.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index ce05a88c7f..2c6207948e 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -49,20 +49,21 @@ export interface Options { * Available inside `Actor.toLayer`'s build effect via * `yield* Actor.CurrentAddress`. */ -export interface Address { +export interface ActorAddress { readonly actorId: string; readonly name: string; readonly key: ReadonlyArray; } /** - * Context tag for the current actor instance's `Address`. Provided + * Context tag for the current actor instance's address. Provided * once per wake when the build effect runs; capture it into a * closure if action handlers need it. */ -export class CurrentAddress extends Context.Service()( - "@rivetkit/effect/Actor/CurrentAddress", -) {} +export class CurrentAddress extends Context.Service< + CurrentAddress, + ActorAddress +>()("@rivetkit/effect/Actor/CurrentAddress") {} /** * One actor registered with the `Registry`. The `buildHandlers` From 3cbbcf8f7fbf20a54b73d007c2c1ffccfa55649f Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 12:25:25 +0200 Subject: [PATCH 089/306] refactor(effect): derive option types from canonical rivetkit shapes 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. --- .../packages/effect/src/Actor.ts | 99 +++++++------------ .../packages/effect/src/Client.ts | 27 ++--- 2 files changed, 44 insertions(+), 82 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 2c6207948e..4c4967feb0 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -31,13 +31,10 @@ const hasStringProperty = (u): u is { readonly [P in K]: string } => Predicate.hasProperty(u, key) && Predicate.isString(u[key]); -/** - * Display options carried by an actor contract. - */ -export interface Options { - readonly name?: string; - readonly icon?: string; -} +export type GlobalActorOptionsInput = Pick< + NonNullable, + "name" | "icon" +>; /** * Per-instance identity carried inside the wake scope. An actor @@ -52,7 +49,7 @@ export interface Options { export interface ActorAddress { readonly actorId: string; readonly name: string; - readonly key: ReadonlyArray; + readonly key: Rivetkit.ActorKey; } /** @@ -77,7 +74,7 @@ export interface RegistryEntry { } export interface RegistryShape { - readonly engineOptions: EngineOptions; + readonly options: RegistryOptions; readonly register: (entry: RegistryEntry) => Effect.Effect; readonly entries: Effect.Effect>; } @@ -87,33 +84,16 @@ export interface RunnerShape { } /** - * Connection options for the Rivet Engine. - * - * Mirrors the engine wiring used by the non-Effect TS SDK: an optional - * endpoint (with URL-auth syntax for namespace and token), plus - * standalone `token` and `namespace` fields. All fields are optional - * and fall back to the matching `RIVET_*` environment variables. + * Connection options for the Rivet Engine. Mirrors the + * `(endpoint, token, namespace)` subset of rivetkit's + * `RegistryConfigInput`. All fields are optional and fall back to the + * matching `RIVET_*` environment variables (see the canonical schema + * for the exact resolution order). */ -export interface EngineOptions { - /** - * Endpoint URL of the Rivet Engine. - * - * Supports URL auth syntax for namespace and token: - * - `https://namespace:token@api.rivet.dev` - * - `https://namespace@api.rivet.dev` - * - * Falls back to `RIVET_ENDPOINT`. - */ - readonly endpoint?: string; - /** Auth token. Falls back to `RIVET_TOKEN`. */ - readonly token?: string; - /** - * Namespace. Falls back to `RIVET_NAMESPACE`, then `"default"`. - */ - readonly namespace?: string; -} - -export interface RegistryOptions extends EngineOptions {} +export type RegistryOptions = Pick< + Rivetkit.RegistryConfigInput, + "endpoint" | "token" | "namespace" +>; /** * Service collecting actor defs/builders together with the engine @@ -127,17 +107,12 @@ export class Registry extends Context.Service()( "@rivetkit/effect/Actor/Registry", ) { static layer(options: RegistryOptions = {}): Layer.Layer { - const engineOptions: EngineOptions = { - endpoint: options.endpoint, - token: options.token, - namespace: options.namespace, - }; return Layer.effect( Registry, Effect.gen(function* () { const ref = yield* Ref.make>([]); return Registry.of({ - engineOptions, + options, register: (entry) => Ref.update(ref, (xs) => [...xs, entry]), entries: Ref.get(ref), @@ -181,7 +156,10 @@ const toRivetkitActor = ( const actions: Record< string, - (c: { actorId: string }, payload?: unknown) => Promise + ( + c: Pick, + payload?: unknown, + ) => Promise > = {}; for (const action of actor.actions) { const decodePayload = Schema.decodeUnknownEffect(action.payloadSchema); @@ -243,12 +221,10 @@ const toRivetkitActor = ( return Rivetkit.actor({ actions, options: actor.options, - onWake: async (c: { - actorId: string; - name: string; - key: ReadonlyArray; - }) => { - const address: Address = { + onWake: async ( + c: Rivetkit.WakeContextOf, + ) => { + const address: ActorAddress = { actorId: c.actorId, name: c.name, key: c.key, @@ -278,7 +254,9 @@ const toRivetkitActor = ( scope, }); }, - onSleep: async (c: { actorId: string }) => { + onSleep: async ( + c: Rivetkit.SleepContextOf, + ) => { const inst = instances.get(c.actorId); if (!inst) return; instances.delete(c.actorId); @@ -286,7 +264,7 @@ const toRivetkitActor = ( Scope.close(inst.scope, Exit.void), ); }, - } as Parameters[0]); + }); }; /** @@ -321,9 +299,7 @@ export class Runner extends Context.Service()( const rivetkitRegistry = Rivetkit.setup({ use, - endpoint: registry.engineOptions.endpoint, - token: registry.engineOptions.token, - namespace: registry.engineOptions.namespace, + ...registry.options, }); yield* Effect.sync(() => rivetkitRegistry.start()); return Runner.of({ mode: "start" }); @@ -367,7 +343,7 @@ type HandlerServices = { : never; }[keyof Handlers]; -export type ActorKey = string | ReadonlyArray; +export type ActorKeyParam = string | Rivetkit.ActorKey; /** * A typed handle for one actor instance. Each action becomes a @@ -380,8 +356,7 @@ export type Handle = { payload: Action.PayloadConstructor, ) => Effect.Effect< Action.Success, - Action.Error | RivetError.RivetError, - never + Action.Error | RivetError.RivetError >; }; @@ -390,7 +365,7 @@ export type Handle = { * dispatch typed action calls against the returned `Handle`. */ export interface TypedAccessor { - readonly getOrCreate: (key: ActorKey) => Handle; + readonly getOrCreate: (key: ActorKeyParam) => Handle; } /** @@ -405,7 +380,7 @@ export interface Actor< readonly _tag: Name; readonly key: string; readonly actions: ReadonlyArray; - readonly options: Options; + readonly options: GlobalActorOptionsInput; of>(handlers: Handlers): Handlers; @@ -494,10 +469,10 @@ const Proto = { const client = yield* Client; const actions = self.actions; return { - getOrCreate: (key: ActorKey) => { + getOrCreate: (key: ActorKeyParam) => { const handle: Record< string, - (p: unknown) => Effect.Effect + (p: unknown) => Effect.Effect > = {}; for (const action of actions) { const tag = action._tag; @@ -571,7 +546,7 @@ const makeProto = < >(options: { readonly _tag: Name; readonly actions: ReadonlyArray; - readonly options: Options; + readonly options: GlobalActorOptionsInput; }): Actor => { const key = `@rivetkit/effect/Actor/${options._tag}`; return Object.assign(Object.create(Proto), { @@ -590,7 +565,7 @@ export const make = < name: Name, options?: { readonly actions?: Actions; - readonly options?: Options; + readonly options?: GlobalActorOptionsInput; }, ): Actor => { return makeProto({ diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index c1726a97dd..17fd663592 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -4,27 +4,14 @@ import * as RivetkitClient from "rivetkit/client"; /** * Connection options for the Rivet Engine client transport. Mirrors - * `EngineOptions` on the server side: an optional endpoint (with URL - * auth syntax for namespace and token), plus standalone `token` and - * `namespace` fields. All fields are optional and fall back to the - * matching `RIVET_*` environment variables. + * the `(endpoint, token, namespace)` subset of rivetkit's + * `ClientConfigInput` — the only fields the Effect SDK currently + * surfaces and forwards. */ -export interface ClientOptions { - /** - * Endpoint URL of the Rivet Engine. - * - * Supports URL auth syntax for namespace and token: - * - `https://namespace:token@api.rivet.dev` - * - `https://namespace@api.rivet.dev` - * - * Falls back to `RIVET_ENDPOINT`, then `http://localhost:6420`. - */ - readonly endpoint?: string; - /** Auth token. Falls back to `RIVET_TOKEN`. */ - readonly token?: string; - /** Namespace. Falls back to `RIVET_NAMESPACE`, then `"default"`. */ - readonly namespace?: string; -} +export type ClientOptions = Pick< + RivetkitClient.ClientConfigInput, + "endpoint" | "token" | "namespace" +>; export interface ClientShape { /** From 7981b81c85fbfe32dab3fa479edba518c6b6826b Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 12:49:04 +0200 Subject: [PATCH 090/306] refactor(effect): tighten remaining drift-prone type declarations 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` shape. --- rivetkit-typescript/packages/effect/src/Actor.ts | 9 ++++----- rivetkit-typescript/packages/effect/src/Client.ts | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 4c4967feb0..22b8a6a904 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -46,11 +46,10 @@ export type GlobalActorOptionsInput = Pick< * Available inside `Actor.toLayer`'s build effect via * `yield* Actor.CurrentAddress`. */ -export interface ActorAddress { - readonly actorId: string; - readonly name: string; - readonly key: Rivetkit.ActorKey; -} +export type ActorAddress = Pick< + Rivetkit.ActorContext, + "actorId" | "name" | "key" +>; /** * Context tag for the current actor instance's address. Provided diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 17fd663592..7440bc7b6b 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -1,6 +1,7 @@ import { Context, Effect, Layer } from "effect"; import * as Rivetkit from "rivetkit"; import * as RivetkitClient from "rivetkit/client"; +import type { ActorKeyParam } from "./Actor"; /** * Connection options for the Rivet Engine client transport. Mirrors @@ -23,7 +24,7 @@ export interface ClientShape { */ readonly callAction: (params: { readonly actorName: string; - readonly key: string | ReadonlyArray; + readonly key: ActorKeyParam; readonly actionName: string; readonly encodedPayload: unknown; }) => Effect.Effect; From 9a4752f580c5848f5e163ae6224fab16f50df13d Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 13:05:29 +0200 Subject: [PATCH 091/306] refactor(effect): inline ClientShape --- .../packages/effect/src/Actor.ts | 2 +- .../packages/effect/src/Client.ts | 41 ++++++++++--------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 22b8a6a904..43542dd304 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -11,7 +11,7 @@ import { } from "effect"; import * as Rivetkit from "rivetkit"; import type * as Action from "./Action"; -import { Client } from "./Client"; +import { Client, type ClientService } from "./Client"; import * as RivetError from "./RivetError"; const TypeId = "~@rivetkit/effect/Actor"; diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 7440bc7b6b..597ba101c9 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -14,30 +14,29 @@ export type ClientOptions = Pick< "endpoint" | "token" | "namespace" >; -export interface ClientShape { - /** - * Generic action dispatch. Returns the raw, undecoded result from - * the wire. On rejection from the underlying transport, surfaces - * the rivetkit `RivetError` instance via `Effect.fail` — the - * caller decides whether to decode `metadata` as a typed error or - * wrap it through the wire codec. - */ - readonly callAction: (params: { - readonly actorName: string; - readonly key: ActorKeyParam; - readonly actionName: string; - readonly encodedPayload: unknown; - }) => Effect.Effect; -} - /** * Service holding the rivetkit client transport. Provided once via * `Client.layer({ ... })`. Consumed by `Actor.client` to dispatch * action calls through a single shared transport. */ -export class Client extends Context.Service()( - "@rivetkit/effect/Client", -) { +export class Client extends Context.Service< + Client, + { + /** + * Generic action dispatch. Returns the raw, undecoded result from + * the wire. On rejection from the underlying transport, surfaces + * the rivetkit `RivetError` instance via `Effect.fail` — the + * caller decides whether to decode `metadata` as a typed error or + * wrap it through the wire codec. + */ + readonly callAction: (params: { + readonly actorName: string; + readonly key: ActorKeyParam; + readonly actionName: string; + readonly encodedPayload: unknown; + }) => Effect.Effect; + } +>()("@rivetkit/effect/Client") { static layer(options: ClientOptions = {}): Layer.Layer { return Layer.effect( Client, @@ -45,7 +44,7 @@ export class Client extends Context.Service()( const rivetkitClient = RivetkitClient.createClient( options, ) as any; - const callAction: ClientShape["callAction"] = ({ + const callAction: ClientService["callAction"] = ({ actorName, key, actionName, @@ -79,3 +78,5 @@ export class Client extends Context.Service()( ); } } + +export type ClientService = Client["Service"]; From bc3fbd72f1bc39f7cb3d7118b14a8ed9062aa9c6 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 13:13:55 +0200 Subject: [PATCH 092/306] refactor(effect): simplify rivetkitClient initialization --- rivetkit-typescript/packages/effect/src/Client.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 597ba101c9..8214f7dca8 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -41,9 +41,7 @@ export class Client extends Context.Service< return Layer.effect( Client, Effect.sync(() => { - const rivetkitClient = RivetkitClient.createClient( - options, - ) as any; + const rivetkitClient = RivetkitClient.createClient(options); const callAction: ClientService["callAction"] = ({ actorName, key, From 9a1522e84a6f2ec81914729be5dfddf540b78028 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 15:35:06 +0200 Subject: [PATCH 093/306] refactor(effect): remove unused `test` mode from `RunnerShape` and `Runner` --- rivetkit-typescript/packages/effect/src/Actor.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 43542dd304..a685c14598 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -79,7 +79,7 @@ export interface RegistryShape { } export interface RunnerShape { - readonly mode: "start" | "serve" | "handler" | "startEnvoy" | "test"; + readonly mode: "start" | "serve" | "handler" | "startEnvoy"; } /** @@ -269,8 +269,8 @@ const toRivetkitActor = ( /** * Service that selects how the registered actors are served. Each * static field is a `Layer` for a specific mode mirroring the - * non-Effect TS SDK: `start`, `serve`, `handler`, `startEnvoy`, plus a - * `test` mode for in-process testing. Each requires `Registry`. + * non-Effect TS SDK: `start`, `serve`, `handler`, and `startEnvoy`. + * Each requires `Registry`. */ export class Runner extends Context.Service()( "@rivetkit/effect/Actor/Runner", @@ -310,8 +310,6 @@ export class Runner extends Context.Service()( runnerNotImplemented("handler"); static startEnvoy: Layer.Layer = runnerNotImplemented("startEnvoy"); - static test: Layer.Layer = - runnerNotImplemented("test"); } export type ActionRequest = From fce948a31219a58439f7fb60951e725aa7b5c7c4 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 17:46:10 +0200 Subject: [PATCH 094/306] refactor(effect): remove `runnerNotImplemented` function and unused modes from `Runner` --- .../packages/effect/src/Actor.ts | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index a685c14598..33a662b60d 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -121,19 +121,6 @@ export class Registry extends Context.Service()( } } -const runnerNotImplemented = ( - mode: RunnerShape["mode"], -): Layer.Layer => - Layer.effect( - Runner, - Effect.gen(function* () { - yield* Registry; - throw new Error( - `Runner.${mode} is not yet implemented. Server runtime wiring is pending.`, - ); - }), - ); - type ActorInstance = { readonly handlers: Record< string, @@ -269,8 +256,7 @@ const toRivetkitActor = ( /** * Service that selects how the registered actors are served. Each * static field is a `Layer` for a specific mode mirroring the - * non-Effect TS SDK: `start`, `serve`, `handler`, and `startEnvoy`. - * Each requires `Registry`. + * non-Effect TS SDK: `start`. Each requires `Registry`. */ export class Runner extends Context.Service()( "@rivetkit/effect/Actor/Runner", @@ -310,6 +296,7 @@ export class Runner extends Context.Service()( runnerNotImplemented("handler"); static startEnvoy: Layer.Layer = runnerNotImplemented("startEnvoy"); + } export type ActionRequest = From 65623d9a8a448f4b4ca07cc8be53fb4ecdc5f308 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 18:32:20 +0200 Subject: [PATCH 095/306] feat(effect): add `Runner.test` layer with end-to-end example test `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. --- .../packages/effect/src/Actor.ts | 108 ++++++++++++++++-- .../packages/effect/test/Runner.test.ts | 101 ++++++++++++++++ .../packages/effect/test/globalSetup.ts | 25 ++++ .../packages/effect/vitest.config.ts | 26 ++++- 4 files changed, 248 insertions(+), 12 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/test/Runner.test.ts create mode 100644 rivetkit-typescript/packages/effect/test/globalSetup.ts diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 33a662b60d..66390a1fa7 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -10,6 +10,7 @@ import { Scope, } from "effect"; import * as Rivetkit from "rivetkit"; +import * as RivetkitClient from "rivetkit/client"; import type * as Action from "./Action"; import { Client, type ClientService } from "./Client"; import * as RivetError from "./RivetError"; @@ -19,6 +20,7 @@ const TypeId = "~@rivetkit/effect/Actor"; export const isActor = (u: unknown): u is Actor => Predicate.hasProperty(u, TypeId); +// TODO: Move into differentfile /** * Refinement that narrows `unknown` to an object with `key` set to a * `string`. Composes `Predicate.hasProperty(key)` with @@ -79,7 +81,7 @@ export interface RegistryShape { } export interface RunnerShape { - readonly mode: "start" | "serve" | "handler" | "startEnvoy"; + readonly mode: "start" | "test"; } /** @@ -105,7 +107,7 @@ export type RegistryOptions = Pick< export class Registry extends Context.Service()( "@rivetkit/effect/Actor/Registry", ) { - static layer(options: RegistryOptions = {}): Layer.Layer { + static layer(options: RegistryOptions = {}) { return Layer.effect( Registry, Effect.gen(function* () { @@ -261,7 +263,7 @@ const toRivetkitActor = ( export class Runner extends Context.Service()( "@rivetkit/effect/Actor/Runner", ) { - static start: Layer.Layer = Layer.effect( + static start = Layer.effect( Runner, Effect.gen(function* () { const registry = yield* Registry; @@ -290,13 +292,101 @@ export class Runner extends Context.Service()( return Runner.of({ mode: "start" }); }), ); - static serve: Layer.Layer = - runnerNotImplemented("serve"); - static handler: Layer.Layer = - runnerNotImplemented("handler"); - static startEnvoy: Layer.Layer = - runnerNotImplemented("startEnvoy"); + /** + * In-process test runtime. Boots the rivetkit registry against the + * configured engine, waits for `/health` to answer, and provides + * both `Runner` and `Client` from one Layer so consumers don't need + * to wire `Client.layer` separately. Mirrors `Runner.start` plus + * test-mode flags and a scoped client dispose. The registry itself + * is leaked to process exit because the public rivetkit `Registry` + * doesn't expose a public `shutdown()` today; only the SIGINT + * handler can drive `#runShutdown`. This matches `setupTest`'s + * existing behavior. + */ + static test: Layer.Layer = + Layer.effectContext( + Effect.gen(function* () { + const registry = yield* Registry; + const entries = yield* registry.entries; + + const services = yield* Effect.context(); + const instances = new Map(); + const use: Record = {}; + for (const entry of entries) { + use[entry.actor._tag] = toRivetkitActor( + entry, + instances, + services, + ); + } + + const rivetkitRegistry = Rivetkit.setup({ + use, + ...registry.options, + }); + rivetkitRegistry.config.test = { + ...rivetkitRegistry.config.test, + enabled: true, + }; + rivetkitRegistry.config.noWelcome = true; + // Auto-spawn the engine when no endpoint was provided, so + // `Runner.test` works out of the box without requiring the + // caller to start an engine externally. If the user wired an + // explicit endpoint via `Registry.layer({ endpoint: ... })`, + // honor it and skip the local spawn. + if (registry.options.endpoint === undefined) { + rivetkitRegistry.config.startEngine = true; + } + rivetkitRegistry.start(); + + // The rivetkitRegistry itself is leaked until process exit (matches + // setupTest's behavior). The public Rivetkit.Registry doesn't + // expose a shutdown method; only the SIGINT handler can drive the + // inner .shutdown(). Disposing the client is the only cleanup we + // can do cleanly today. + const rivetkitClient = yield* Effect.acquireRelease( + Effect.sync(() => + RivetkitClient.createClient(registry.options), + ), + (c) => Effect.promise(() => c.dispose()), + ); + + const callAction: ClientService["callAction"] = ({ + actorName, + key, + actionName, + encodedPayload, + }) => + Effect.tryPromise({ + try: () => + rivetkitClient[actorName].getOrCreate(key).action({ + name: actionName, + args: [encodedPayload], + }), + catch: (cause) => + cause instanceof Rivetkit.RivetError + ? cause + : new Rivetkit.RivetError( + "client", + "unknown", + cause instanceof Error + ? cause.message + : String(cause), + { + cause: + cause instanceof Error + ? cause + : undefined, + }, + ), + }); + + return Context.make(Runner, Runner.of({ mode: "test" })).pipe( + Context.add(Client, Client.of({ callAction })), + ); + }), + ); } export type ActionRequest = diff --git a/rivetkit-typescript/packages/effect/test/Runner.test.ts b/rivetkit-typescript/packages/effect/test/Runner.test.ts new file mode 100644 index 0000000000..a450d704b6 --- /dev/null +++ b/rivetkit-typescript/packages/effect/test/Runner.test.ts @@ -0,0 +1,101 @@ +import { assert, describe, layer } from "@effect/vitest"; +import { Effect, Layer, Ref, Schema } from "effect"; +import { Action, Actor, Registry, Runner } from "@rivetkit/effect"; + +class CounterOverflowError extends Schema.TaggedErrorClass()( + "CounterOverflowError", + { + limit: Schema.Number, + message: Schema.String, + }, +) {} + +const Increment = Action.make("Increment", { + payload: { amount: Schema.Number }, + success: Schema.Number, + error: CounterOverflowError, +}); + +const GetCount = Action.make("GetCount", { + success: Schema.Number, +}); + +const Counter = Actor.make("Counter", { + actions: [Increment, GetCount], +}); + +const CounterLive = Counter.toLayer( + Effect.gen(function* () { + const count = yield* Ref.make(0); + return Counter.of({ + Increment: ({ payload }) => + Effect.gen(function* () { + const next = yield* Ref.updateAndGet( + count, + (n) => n + payload.amount, + ); + if (next > 20) { + return yield* new CounterOverflowError({ + limit: 20, + message: `count ${next} would exceed limit 20`, + }); + } + return next; + }), + GetCount: () => Ref.get(count), + }); + }), +); + +const TestLayer = Runner.test.pipe( + Layer.provideMerge(CounterLive), + Layer.provide(Registry.layer()), +); + +describe("Runner.test", () => { + layer(TestLayer, { timeout: "15 seconds" })("Counter end-to-end", (it) => { + it.effect("invokes an action and returns the typed success value", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate([ + "t-success", + ]); + const next = yield* counter.Increment({ amount: 5 }); + assert.strictEqual(next, 5); + }), + ); + + it.effect("preserves in-wake state across calls on the same key", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate([ + "t-state", + ]); + yield* counter.Increment({ amount: 3 }); + yield* counter.Increment({ amount: 4 }); + const total = yield* counter.GetCount(); + assert.strictEqual(total, 7); + }), + ); + + it.effect( + "decodes typed errors back into the original tagged class", + () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate([ + "t-overflow", + ]); + const exit = yield* counter + .Increment({ amount: 100 }) + .pipe(Effect.exit); + assert.isTrue(exit._tag === "Failure"); + yield* counter.Increment({ amount: 100 }).pipe( + Effect.catchTag("CounterOverflowError", (e) => + Effect.sync(() => { + assert.strictEqual(e.limit, 20); + assert.match(e.message, /exceed limit 20/); + }), + ), + ); + }), + ); + }); +}); diff --git a/rivetkit-typescript/packages/effect/test/globalSetup.ts b/rivetkit-typescript/packages/effect/test/globalSetup.ts new file mode 100644 index 0000000000..93db1de217 --- /dev/null +++ b/rivetkit-typescript/packages/effect/test/globalSetup.ts @@ -0,0 +1,25 @@ +import { spawnSync } from "node:child_process"; +import { rmSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +/** + * Vitest globalSetup that kills any orphaned `rivet-engine` process and + * clears the engine's on-disk state before the test suite runs. + * + * The Rivet engine spawned by `setupTest` is intentionally orphaned and + * outlives the test process; it persists envoy registrations, actor + * pools, and database state in `~/.rivetkit`. Without a clean slate + * each invocation, the second-and-subsequent test runs inherit stale + * envoy registrations from prior runs and the runner pool fails to + * become available, surfacing as `actor_ready_timeout` / `no_envoys` + * for any test that exercises the wire path. + */ +export default function globalSetup() { + try { + spawnSync("pkill", ["-9", "-f", "rivet-engine"], { stdio: "ignore" }); + } catch {} + try { + rmSync(join(homedir(), ".rivetkit"), { recursive: true, force: true }); + } catch {} +} diff --git a/rivetkit-typescript/packages/effect/vitest.config.ts b/rivetkit-typescript/packages/effect/vitest.config.ts index 7b2c82e6c5..b268925c15 100644 --- a/rivetkit-typescript/packages/effect/vitest.config.ts +++ b/rivetkit-typescript/packages/effect/vitest.config.ts @@ -1,6 +1,26 @@ -import { defineConfig } from "vitest/config" -import defaultConfig from "../../../vitest.base" +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vitest/config"; +import defaultConfig from "../../../vitest.base"; + +const here = dirname(fileURLToPath(import.meta.url)); + +const env = { + ...defaultConfig.test?.env, + RIVET_ENGINE_BINARY: join(here, "../../../target/debug/rivet-engine"), +}; export default defineConfig({ ...defaultConfig, -}) + test: { + ...defaultConfig.test, + env, + // The in-process Rivet engine binds to a fixed port; serialize + // test files. Use the default fork pool (per-test isolation) so + // each test gets a fresh process and a clean engine envoy state. + fileParallelism: false, + sequence: { concurrent: false }, + // Kill any orphaned engine + clear state before the suite runs. + globalSetup: ["./test/globalSetup.ts"], + }, +}); From 87ecef336b3c665ecb17b83558d81c8c1b397379 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 18:36:58 +0200 Subject: [PATCH 096/306] refactor(effect): rename Runner.test.ts to e2e.test.ts --- .../packages/effect/test/{Runner.test.ts => e2e.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rivetkit-typescript/packages/effect/test/{Runner.test.ts => e2e.test.ts} (100%) diff --git a/rivetkit-typescript/packages/effect/test/Runner.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts similarity index 100% rename from rivetkit-typescript/packages/effect/test/Runner.test.ts rename to rivetkit-typescript/packages/effect/test/e2e.test.ts From 6a6bdf73f05038b15563fbd43955ad450146cff9 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 18:39:36 +0200 Subject: [PATCH 097/306] refactor(effect): move e2e Counter fixture into test/fixtures/ --- .../packages/effect/test/e2e.test.ts | 54 ++----------------- .../packages/effect/test/fixtures/counter.ts | 47 ++++++++++++++++ 2 files changed, 51 insertions(+), 50 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/test/fixtures/counter.ts diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index a450d704b6..b5e13d11c7 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -1,51 +1,7 @@ import { assert, describe, layer } from "@effect/vitest"; -import { Effect, Layer, Ref, Schema } from "effect"; -import { Action, Actor, Registry, Runner } from "@rivetkit/effect"; - -class CounterOverflowError extends Schema.TaggedErrorClass()( - "CounterOverflowError", - { - limit: Schema.Number, - message: Schema.String, - }, -) {} - -const Increment = Action.make("Increment", { - payload: { amount: Schema.Number }, - success: Schema.Number, - error: CounterOverflowError, -}); - -const GetCount = Action.make("GetCount", { - success: Schema.Number, -}); - -const Counter = Actor.make("Counter", { - actions: [Increment, GetCount], -}); - -const CounterLive = Counter.toLayer( - Effect.gen(function* () { - const count = yield* Ref.make(0); - return Counter.of({ - Increment: ({ payload }) => - Effect.gen(function* () { - const next = yield* Ref.updateAndGet( - count, - (n) => n + payload.amount, - ); - if (next > 20) { - return yield* new CounterOverflowError({ - limit: 20, - message: `count ${next} would exceed limit 20`, - }); - } - return next; - }), - GetCount: () => Ref.get(count), - }); - }), -); +import { Effect, Layer } from "effect"; +import { Registry, Runner } from "@rivetkit/effect"; +import { Counter, CounterLive } from "./fixtures/counter"; const TestLayer = Runner.test.pipe( Layer.provideMerge(CounterLive), @@ -66,9 +22,7 @@ describe("Runner.test", () => { it.effect("preserves in-wake state across calls on the same key", () => Effect.gen(function* () { - const counter = (yield* Counter.client).getOrCreate([ - "t-state", - ]); + const counter = (yield* Counter.client).getOrCreate(["t-state"]); yield* counter.Increment({ amount: 3 }); yield* counter.Increment({ amount: 4 }); const total = yield* counter.GetCount(); diff --git a/rivetkit-typescript/packages/effect/test/fixtures/counter.ts b/rivetkit-typescript/packages/effect/test/fixtures/counter.ts new file mode 100644 index 0000000000..11eed775cd --- /dev/null +++ b/rivetkit-typescript/packages/effect/test/fixtures/counter.ts @@ -0,0 +1,47 @@ +import { Effect, Ref, Schema } from "effect"; +import { Action, Actor } from "@rivetkit/effect"; + +export class CounterOverflowError extends Schema.TaggedErrorClass()( + "CounterOverflowError", + { + limit: Schema.Number, + message: Schema.String, + }, +) {} + +export const Increment = Action.make("Increment", { + payload: { amount: Schema.Number }, + success: Schema.Number, + error: CounterOverflowError, +}); + +export const GetCount = Action.make("GetCount", { + success: Schema.Number, +}); + +export const Counter = Actor.make("Counter", { + actions: [Increment, GetCount], +}); + +export const CounterLive = Counter.toLayer( + Effect.gen(function* () { + const count = yield* Ref.make(0); + return Counter.of({ + Increment: ({ payload }) => + Effect.gen(function* () { + const next = yield* Ref.updateAndGet( + count, + (n) => n + payload.amount, + ); + if (next > 20) { + return yield* new CounterOverflowError({ + limit: 20, + message: `count ${next} would exceed limit 20`, + }); + } + return next; + }), + GetCount: () => Ref.get(count), + }); + }), +); From 60a8d78984caab2625fc63b913d79c3cc6990bac Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 18:53:57 +0200 Subject: [PATCH 098/306] chore(effect): include vitest.config.ts in tsconfig + add @types/node --- pnpm-lock.yaml | 90 +++++-------------- .../packages/effect/package.json | 1 + .../packages/effect/tsconfig.json | 2 +- .../packages/effect/vitest.config.ts | 1 + 4 files changed, 23 insertions(+), 71 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3d9b03966..279c118562 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4131,19 +4131,22 @@ importers: version: 0.85.1 '@effect/vitest': specifier: ^4.0.0-beta.57 - version: 4.0.0-beta.57(effect@4.0.0-beta.57)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.0.7)(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.0.0-beta.57(effect@4.0.0-beta.57)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))) + '@types/node': + specifier: ^22.18.1 + version: 22.19.15 effect: specifier: ^4.0.0-beta.57 version: 4.0.0-beta.57 tsup: specifier: ^8.4.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.15))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.9.2 version: 5.9.3 vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.0.7)(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) rivetkit-typescript/packages/engine-cli: {} @@ -20171,10 +20174,10 @@ snapshots: - bufferutil - utf-8-validate - '@effect/vitest@4.0.0-beta.57(effect@4.0.0-beta.57)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.0.7)(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)))': + '@effect/vitest@4.0.0-beta.57(effect@4.0.0-beta.57)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: effect: 4.0.0-beta.57 - vitest: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.0.7)(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@emnapi/runtime@1.7.1': dependencies: @@ -21380,14 +21383,6 @@ snapshots: '@types/node': 22.19.15 optional: true - '@inquirer/confirm@5.1.21(@types/node@25.0.7)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@25.0.7) - '@inquirer/type': 3.0.10(@types/node@25.0.7) - optionalDependencies: - '@types/node': 25.0.7 - optional: true - '@inquirer/core@10.3.2(@types/node@20.19.13)': dependencies: '@inquirer/ansi': 1.0.2 @@ -21429,20 +21424,6 @@ snapshots: '@types/node': 22.19.15 optional: true - '@inquirer/core@10.3.2(@types/node@25.0.7)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.0.7) - cli-width: 4.1.0 - mute-stream: 2.0.0 - signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 25.0.7 - optional: true - '@inquirer/figures@1.0.15': {} '@inquirer/type@3.0.10(@types/node@20.19.13)': @@ -21459,11 +21440,6 @@ snapshots: '@types/node': 22.19.15 optional: true - '@inquirer/type@3.0.10(@types/node@25.0.7)': - optionalDependencies: - '@types/node': 25.0.7 - optional: true - '@ioredis/commands@1.5.1': {} '@isaacs/balanced-match@4.0.1': @@ -25866,14 +25842,14 @@ snapshots: msw: 2.12.10(@types/node@20.19.13)(typescript@5.9.3) vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.5(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.5(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - msw: 2.12.10(@types/node@25.0.7)(typescript@5.9.3) - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + msw: 2.12.10(@types/node@22.19.15)(typescript@5.9.3) + vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@2.1.9': dependencies: @@ -31370,32 +31346,6 @@ snapshots: - '@types/node' optional: true - msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3): - dependencies: - '@inquirer/confirm': 5.1.21(@types/node@25.0.7) - '@mswjs/interceptors': 0.41.2 - '@open-draft/deferred-promise': 2.2.0 - '@types/statuses': 2.0.6 - cookie: 1.1.1 - graphql: 16.12.0 - headers-polyfill: 4.0.3 - is-node-process: 1.2.0 - outvariant: 1.4.3 - path-to-regexp: 6.3.0 - picocolors: 1.1.1 - rettime: 0.10.1 - statuses: 2.0.2 - strict-event-emitter: 0.5.1 - tough-cookie: 6.0.0 - type-fest: 5.4.4 - until-async: 3.0.2 - yargs: 17.7.2 - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - '@types/node' - optional: true - muggle-string@0.3.1: {} multipasta@0.2.7: {} @@ -34973,7 +34923,7 @@ snapshots: yaml: 2.8.3 optional: true - vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -34982,7 +34932,7 @@ snapshots: rollup: 4.57.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 22.19.15 fsevents: 2.3.3 jiti: 2.6.1 less: 4.4.1 @@ -34991,9 +34941,9 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.8.3 - vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -35011,7 +34961,7 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 tsx: 4.21.0 - yaml: 2.8.3 + yaml: 2.8.2 vitefu@1.1.1(vite@6.4.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): optionalDependencies: @@ -35275,10 +35225,10 @@ snapshots: - tsx - yaml - vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.0.7)(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(msw@2.12.10(@types/node@25.0.7)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.1.5(msw@2.12.10(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.5 '@vitest/runner': 4.1.5 '@vitest/snapshot': 4.1.5 @@ -35295,11 +35245,11 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 - '@types/node': 25.0.7 + '@types/node': 22.19.15 transitivePeerDependencies: - msw diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index 528cbf3c15..ab4278402f 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -39,6 +39,7 @@ "devDependencies": { "@effect/language-service": "^0.85.1", "@effect/vitest": "^4.0.0-beta.57", + "@types/node": "^22.18.1", "effect": "^4.0.0-beta.57", "tsup": "^8.4.0", "typescript": "^5.9.2", diff --git a/rivetkit-typescript/packages/effect/tsconfig.json b/rivetkit-typescript/packages/effect/tsconfig.json index 588bd72ffb..a396842bf0 100644 --- a/rivetkit-typescript/packages/effect/tsconfig.json +++ b/rivetkit-typescript/packages/effect/tsconfig.json @@ -14,5 +14,5 @@ } ] }, - "include": ["src/**/*", "test/**/*"] + "include": ["src/**/*", "test/**/*", "vitest.config.ts"] } diff --git a/rivetkit-typescript/packages/effect/vitest.config.ts b/rivetkit-typescript/packages/effect/vitest.config.ts index b268925c15..cb4d434ba5 100644 --- a/rivetkit-typescript/packages/effect/vitest.config.ts +++ b/rivetkit-typescript/packages/effect/vitest.config.ts @@ -1,3 +1,4 @@ +// import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; From baf7b1695738cdec24f1f1f158b5a4cf8d0ba1c7 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 19:46:03 +0200 Subject: [PATCH 099/306] refactor(effect): extract hasStringProperty into utils.ts --- rivetkit-typescript/packages/effect/src/Actor.ts | 14 +------------- rivetkit-typescript/packages/effect/src/utils.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/src/utils.ts diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 66390a1fa7..ad1e5e0626 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -14,25 +14,13 @@ import * as RivetkitClient from "rivetkit/client"; import type * as Action from "./Action"; import { Client, type ClientService } from "./Client"; import * as RivetError from "./RivetError"; +import { hasStringProperty } from "./utils"; const TypeId = "~@rivetkit/effect/Actor"; export const isActor = (u: unknown): u is Actor => Predicate.hasProperty(u, TypeId); -// TODO: Move into differentfile -/** - * Refinement that narrows `unknown` to an object with `key` set to a - * `string`. Composes `Predicate.hasProperty(key)` with - * `Predicate.isString` in one go. - */ -const hasStringProperty = - ( - key: K, - ): Predicate.Refinement => - (u): u is { readonly [P in K]: string } => - Predicate.hasProperty(u, key) && Predicate.isString(u[key]); - export type GlobalActorOptionsInput = Pick< NonNullable, "name" | "icon" diff --git a/rivetkit-typescript/packages/effect/src/utils.ts b/rivetkit-typescript/packages/effect/src/utils.ts new file mode 100644 index 0000000000..6ad3734cef --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/utils.ts @@ -0,0 +1,12 @@ +import { Predicate } from "effect"; + +/** + * Refinement that narrows `unknown` to an object with `key` set to a + * `string`. + */ +export const hasStringProperty = + ( + key: K, + ): Predicate.Refinement => + (u): u is { readonly [P in K]: string } => + Predicate.hasProperty(u, key) && Predicate.isString(u[key]); From cc3bbeb7e0115ed2a74fe1dd462bd38f601c2cf1 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 19:47:27 +0200 Subject: [PATCH 100/306] refactor(effect): inline RegistryShape into Registry declaration --- rivetkit-typescript/packages/effect/src/Actor.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index ad1e5e0626..3092c4dd2d 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -62,12 +62,6 @@ export interface RegistryEntry { readonly buildHandlers: Effect.Effect; } -export interface RegistryShape { - readonly options: RegistryOptions; - readonly register: (entry: RegistryEntry) => Effect.Effect; - readonly entries: Effect.Effect>; -} - export interface RunnerShape { readonly mode: "start" | "test"; } @@ -92,7 +86,11 @@ export type RegistryOptions = Pick< * materialize the underlying rivetkit registry from the collected * entries). */ -export class Registry extends Context.Service()( +export class Registry extends Context.Service Effect.Effect; + readonly entries: Effect.Effect>; +}>()( "@rivetkit/effect/Actor/Registry", ) { static layer(options: RegistryOptions = {}) { From 6e4a04e80e8134cf23bdcb3468eb3628c9665dbc Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 19:49:38 +0200 Subject: [PATCH 101/306] refactor(effect): inline RunnerShape into Runner declaration --- rivetkit-typescript/packages/effect/src/Actor.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 3092c4dd2d..543ae0556a 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -62,10 +62,6 @@ export interface RegistryEntry { readonly buildHandlers: Effect.Effect; } -export interface RunnerShape { - readonly mode: "start" | "test"; -} - /** * Connection options for the Rivet Engine. Mirrors the * `(endpoint, token, namespace)` subset of rivetkit's @@ -246,7 +242,9 @@ const toRivetkitActor = ( * static field is a `Layer` for a specific mode mirroring the * non-Effect TS SDK: `start`. Each requires `Registry`. */ -export class Runner extends Context.Service()( +export class Runner extends Context.Service()( "@rivetkit/effect/Actor/Runner", ) { static start = Layer.effect( From 35d5799e12b7988d7146a39e9078d92008df3264 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 20:04:00 +0200 Subject: [PATCH 102/306] refactor(effect): dedupe Runner.start/test via toRivetkitRegistry helper --- .../packages/effect/src/Actor.ts | 76 ++++++++----------- 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 543ae0556a..91de524257 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -117,11 +117,15 @@ type ActorInstance = { readonly scope: Scope.Closeable; }; -const toRivetkitActor = ( +const toRivetkitActor = Effect.fnUntraced(function* ( entry: RegistryEntry, instances: Map, - services: Context.Context, -): Rivetkit.AnyActorDefinition => { +) { + // Snapshot the current Effect context so action callbacks + // (which run in rivetkit's plain Promise world) can run + // handler effects against the same services the Runner layer + // was provided with. + const services = yield* Effect.context(); const actor = entry.actor; const actions: Record< @@ -235,7 +239,29 @@ const toRivetkitActor = ( ); }, }); -}; +}); + +/** + * Build the underlying rivetkit registry from the collected `Registry` + * entries. The returned registry is configured but not started; callers + * apply mode-specific config (test flags, engine spawn) and then invoke + * `.start()` themselves. + */ +const toRivetkitRegistry = Effect.fnUntraced(function* ( + registry: Registry["Service"], +) { + const entries = yield* registry.entries; + const instances = new Map(); + const use: Record = {}; + for (const entry of entries) { + use[entry.actor._tag] = yield* toRivetkitActor(entry, instances); + } + + return Rivetkit.setup({ + use, + ...registry.options, + }); +}); /** * Service that selects how the registered actors are served. Each @@ -251,27 +277,7 @@ export class Runner extends Context.Service(); - const instances = new Map(); - const use: Record = {}; - for (const entry of entries) { - use[entry.actor._tag] = toRivetkitActor( - entry, - instances, - services, - ); - } - - const rivetkitRegistry = Rivetkit.setup({ - use, - ...registry.options, - }); + const rivetkitRegistry = yield* toRivetkitRegistry(registry); yield* Effect.sync(() => rivetkitRegistry.start()); return Runner.of({ mode: "start" }); }), @@ -292,23 +298,7 @@ export class Runner extends Context.Service(); - const instances = new Map(); - const use: Record = {}; - for (const entry of entries) { - use[entry.actor._tag] = toRivetkitActor( - entry, - instances, - services, - ); - } - - const rivetkitRegistry = Rivetkit.setup({ - use, - ...registry.options, - }); + const rivetkitRegistry = yield* toRivetkitRegistry(registry); rivetkitRegistry.config.test = { ...rivetkitRegistry.config.test, enabled: true, @@ -322,7 +312,7 @@ export class Runner extends Context.Service rivetkitRegistry.start()); // The rivetkitRegistry itself is leaked until process exit (matches // setupTest's behavior). The public Rivetkit.Registry doesn't From 6990c2de88aa617a2caf3022385a0ba540f6fdb4 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 20:09:50 +0200 Subject: [PATCH 103/306] chore(effect): silence engine logs in test suite 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. --- rivetkit-typescript/packages/effect/vitest.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rivetkit-typescript/packages/effect/vitest.config.ts b/rivetkit-typescript/packages/effect/vitest.config.ts index cb4d434ba5..70fd49a902 100644 --- a/rivetkit-typescript/packages/effect/vitest.config.ts +++ b/rivetkit-typescript/packages/effect/vitest.config.ts @@ -9,6 +9,9 @@ const here = dirname(fileURLToPath(import.meta.url)); const env = { ...defaultConfig.test?.env, RIVET_ENGINE_BINARY: join(here, "../../../target/debug/rivet-engine"), + // The shared vitest base sets RIVET_LOG_LEVEL=DEBUG, which floods the + // terminal with engine + runtime logs. Keep this suite quiet. + RIVET_LOG_LEVEL: "ERROR", }; export default defineConfig({ From 28ead6593394057dd17c819b4f13b59fdd03a8c3 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 20:16:42 +0200 Subject: [PATCH 104/306] test(effect): cover Date/transform schemas, user services, and multi-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`. --- .../packages/effect/test/e2e.test.ts | 179 +++++++++++++----- .../packages/effect/test/fixtures/actor.ts | 117 ++++++++++++ .../packages/effect/test/fixtures/counter.ts | 47 ----- 3 files changed, 253 insertions(+), 90 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/test/fixtures/actor.ts delete mode 100644 rivetkit-typescript/packages/effect/test/fixtures/counter.ts diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index b5e13d11c7..a12f2ccf2d 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -1,55 +1,148 @@ -import { assert, describe, layer } from "@effect/vitest"; +import { assert, layer } from "@effect/vitest"; import { Effect, Layer } from "effect"; -import { Registry, Runner } from "@rivetkit/effect"; -import { Counter, CounterLive } from "./fixtures/counter"; +import { Registry, RivetError, Runner } from "@rivetkit/effect"; +import { + Counter, + CounterLive, + CounterOverflowError, + Greeter, + Pinger, + PingerLive, +} from "./fixtures/actor"; + +const GreeterLive = Layer.succeed( + Greeter, + Greeter.of({ + greet: (name) => `Hello, ${name}!`, + }), +); const TestLayer = Runner.test.pipe( - Layer.provideMerge(CounterLive), + Layer.provideMerge(Layer.mergeAll(CounterLive, PingerLive)), + Layer.provide(GreeterLive), Layer.provide(Registry.layer()), ); -describe("Runner.test", () => { - layer(TestLayer, { timeout: "15 seconds" })("Counter end-to-end", (it) => { - it.effect("invokes an action and returns the typed success value", () => +layer(TestLayer)("end-to-end", (it) => { + it.effect("round-trips an action with payload and success", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate("t-roundtrip"); + assert.strictEqual(yield* counter.Increment({ amount: 5 }), 5); + }), + ); + + it.effect("preserves in-wake state across calls on the same key", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate(["t-state"]); + yield* counter.Increment({ amount: 3 }); + yield* counter.Increment({ amount: 4 }); + const total = yield* counter.GetCount(); + assert.strictEqual(total, 7); + }), + ); + + it.effect("isolates in-wake state across keys", () => + Effect.gen(function* () { + const client = yield* Counter.client; + const a = client.getOrCreate(["t-iso-a"]); + const b = client.getOrCreate(["t-iso-b"]); + yield* a.Increment({ amount: 2 }); + yield* a.Increment({ amount: 3 }); + yield* b.Increment({ amount: 1 }); + assert.strictEqual(yield* a.GetCount(), 5); + assert.strictEqual(yield* b.GetCount(), 1); + }), + ); + + it.effect("surfaces an expected handler error back into the original error", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate(["t-expected-error"]); + const exit = yield* counter.Increment({ amount: 100 }).pipe( + Effect.flip, + Effect.exit, + ); + assert.isTrue(exit._tag === "Success"); + if (exit._tag === "Success") { + assert.instanceOf(exit.value, CounterOverflowError); + assert.strictEqual(exit.value.limit, 20); + assert.match(exit.value.message, /exceed limit 20/); + } + }), + ); + + it.effect("surfaces an unexpected handler error as a RivetError", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate(["t-boom"]); + const exit = yield* counter.Crash().pipe(Effect.flip, Effect.exit); + assert.isTrue(exit._tag === "Success"); + if (exit._tag === "Success") { + assert.instanceOf(exit.value, RivetError.RivetError); + } + }), + ); + + it.effect("round-trips a non-trivial schema (Date)", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate(["t-date"]); + const when = new Date("2024-01-15T10:30:00.000Z"); + const result = yield* counter.EchoDate({ when }); + assert.instanceOf(result, Date); + assert.strictEqual(result.toISOString(), when.toISOString()); + }), + ); + + it.effect("round-trips a custom Schema.transform", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate(["t-transform"]); + // `tags` rides the wire as the encoded CSV string and decodes + // back to a string array on the server. If the transform + // didn't fire, `payload.tags.length` would be the byte length + // of the CSV ("alpha,beta,gamma" = 16) instead of 3. + const count = yield* counter.Tags({ + tags: ["alpha", "beta", "gamma"], + }); + assert.strictEqual(count, 3); + }), + ); + + it.effect("resolves a non-built-in service", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate([ + "t-service-wake", + ]); + // `WakeGreeting` returns the string captured when `Greeter` + // was yielded inside the wake-scope build effect. + const greeting = yield* counter.WakeGreeting(); + assert.strictEqual(greeting, "Hello, on wake!"); + }), + ); + + it.effect( + "resolves a non-built-in service yielded by an action handler", + () => Effect.gen(function* () { const counter = (yield* Counter.client).getOrCreate([ - "t-success", + "t-service-handler", ]); - const next = yield* counter.Increment({ amount: 5 }); - assert.strictEqual(next, 5); + // `Greet`'s handler yields `Greeter` per call; the + // snapshotted Runner context must satisfy that R. + const greeting = yield* counter.Greet({ name: "Effect" }); + assert.strictEqual(greeting, "Hello, Effect!"); }), - ); + ); - it.effect("preserves in-wake state across calls on the same key", () => - Effect.gen(function* () { - const counter = (yield* Counter.client).getOrCreate(["t-state"]); - yield* counter.Increment({ amount: 3 }); - yield* counter.Increment({ amount: 4 }); - const total = yield* counter.GetCount(); - assert.strictEqual(total, 7); - }), - ); - - it.effect( - "decodes typed errors back into the original tagged class", - () => - Effect.gen(function* () { - const counter = (yield* Counter.client).getOrCreate([ - "t-overflow", - ]); - const exit = yield* counter - .Increment({ amount: 100 }) - .pipe(Effect.exit); - assert.isTrue(exit._tag === "Failure"); - yield* counter.Increment({ amount: 100 }).pipe( - Effect.catchTag("CounterOverflowError", (e) => - Effect.sync(() => { - assert.strictEqual(e.limit, 20); - assert.match(e.message, /exceed limit 20/); - }), - ), - ); - }), - ); - }); + it.effect("registers and serves multiple actors", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate(["t-multi"]); + const pinger = (yield* Pinger.client).getOrCreate(["t-multi"]); + + const incremented = yield* counter.Increment({ amount: 7 }); + const pong = yield* pinger.Ping(); + + assert.strictEqual(incremented, 7); + assert.strictEqual(pong, "pong"); + }), + ); + + it.todo("fires the wake-scope finalizer on sleep"); }); diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts new file mode 100644 index 0000000000..1106b33ae3 --- /dev/null +++ b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts @@ -0,0 +1,117 @@ +import { Context, Effect, Ref, Schema, SchemaTransformation } from "effect"; +import { Action, Actor } from "@rivetkit/effect"; + +// --- Counter --- + +export class CounterOverflowError extends Schema.TaggedErrorClass()( + "CounterOverflowError", + { + limit: Schema.Number, + message: Schema.String, + }, +) {} + +/** + * A non-built-in service used by `Counter` to verify that user-provided + * services resolve in both the wake-scope build effect and inside + * individual action handlers. + */ +export class Greeter extends Context.Service< + Greeter, + { readonly greet: (name: string) => string } +>()("test/Greeter") {} + +const TagsCsv = Schema.String.pipe( + Schema.decodeTo( + Schema.Array(Schema.String), + SchemaTransformation.transform({ + decode: (s: string): ReadonlyArray => s.split(","), + encode: (arr: ReadonlyArray) => arr.join(","), + }), + ), +); + +export const Increment = Action.make("Increment", { + payload: { amount: Schema.Number }, + success: Schema.Number, + error: CounterOverflowError, +}); + +export const GetCount = Action.make("GetCount", { + success: Schema.Number, +}); + +export const Crash = Action.make("Crash"); + +export const EchoDate = Action.make("EchoDate", { + payload: { when: Schema.DateFromString }, + success: Schema.DateFromString, +}); + +export const Tags = Action.make("Tags", { + payload: { tags: TagsCsv }, + success: Schema.Number, +}); + +export const Greet = Action.make("Greet", { + payload: { name: Schema.String }, + success: Schema.String, +}); + +export const WakeGreeting = Action.make("WakeGreeting", { + success: Schema.String, +}); + +export const Counter = Actor.make("Counter", { + actions: [Increment, GetCount, Crash, EchoDate, Tags, Greet, WakeGreeting], +}); + +export const CounterLive = Counter.toLayer( + Effect.gen(function* () { + const count = yield* Ref.make(0); + // Wake-scope yield of a non-built-in service. Resolved once per + // wake; the captured value is closed over by `WakeGreeting`. + const greeter = yield* Greeter; + const wakeGreeting = greeter.greet("on wake"); + return Counter.of({ + Increment: ({ payload }) => + Effect.gen(function* () { + const next = yield* Ref.updateAndGet( + count, + (n) => n + payload.amount, + ); + if (next > 20) { + return yield* new CounterOverflowError({ + limit: 20, + message: `count ${next} would exceed limit 20`, + }); + } + return next; + }), + GetCount: () => Ref.get(count), + Crash: () => Effect.die("kaboom"), + EchoDate: ({ payload }) => Effect.succeed(payload.when), + Tags: ({ payload }) => Effect.succeed(payload.tags.length), + // Per-handler yield of a non-built-in service. Resolved on + // every call against the snapshotted Runner context. + Greet: ({ payload }) => + Effect.gen(function* () { + const g = yield* Greeter; + return g.greet(payload.name); + }), + WakeGreeting: () => Effect.succeed(wakeGreeting), + }); + }), +); + +// --- Pinger --- + +// Minimal second actor used solely to assert that the registry serves +// more than one actor side-by-side. +export const Ping = Action.make("Ping", { success: Schema.String }); + +export const Pinger = Actor.make("Pinger", { actions: [Ping] }); + +export const PingerLive = Pinger.toLayer({ + Ping: () => Effect.succeed("pong"), +}); diff --git a/rivetkit-typescript/packages/effect/test/fixtures/counter.ts b/rivetkit-typescript/packages/effect/test/fixtures/counter.ts deleted file mode 100644 index 11eed775cd..0000000000 --- a/rivetkit-typescript/packages/effect/test/fixtures/counter.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Effect, Ref, Schema } from "effect"; -import { Action, Actor } from "@rivetkit/effect"; - -export class CounterOverflowError extends Schema.TaggedErrorClass()( - "CounterOverflowError", - { - limit: Schema.Number, - message: Schema.String, - }, -) {} - -export const Increment = Action.make("Increment", { - payload: { amount: Schema.Number }, - success: Schema.Number, - error: CounterOverflowError, -}); - -export const GetCount = Action.make("GetCount", { - success: Schema.Number, -}); - -export const Counter = Actor.make("Counter", { - actions: [Increment, GetCount], -}); - -export const CounterLive = Counter.toLayer( - Effect.gen(function* () { - const count = yield* Ref.make(0); - return Counter.of({ - Increment: ({ payload }) => - Effect.gen(function* () { - const next = yield* Ref.updateAndGet( - count, - (n) => n + payload.amount, - ); - if (next > 20) { - return yield* new CounterOverflowError({ - limit: 20, - message: `count ${next} would exceed limit 20`, - }); - } - return next; - }), - GetCount: () => Ref.get(count), - }); - }), -); From 7aed57d5c48b6b664e6fc54514ef96259e2f4fa0 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 20:17:13 +0200 Subject: [PATCH 105/306] fix(effect): propagate auto-spawned engine endpoint to test client 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. --- rivetkit-typescript/packages/effect/src/Actor.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 91de524257..af5e5c8ae4 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -319,9 +319,19 @@ export class Runner extends Context.Service - RivetkitClient.createClient(registry.options), + RivetkitClient.createClient({ + ...registry.options, + endpoint: + registry.options.endpoint ?? resolvedEndpoint, + }), ), (c) => Effect.promise(() => c.dispose()), ); From 1628652a3e328521091b66342315027e9d5aff49 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 3 May 2026 20:20:01 +0200 Subject: [PATCH 106/306] test(effect): add todos for build-effect errors, schema services, tracing --- rivetkit-typescript/packages/effect/test/e2e.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index a12f2ccf2d..0abee817f6 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -145,4 +145,12 @@ layer(TestLayer)("end-to-end", (it) => { ); it.todo("fires the wake-scope finalizer on sleep"); + + it.todo("surfaces an error thrown inside an actor's build effect"); + + it.todo( + "runs encoding/decoding services for an action's payload, success, and error", + ); + + it.todo("propagates Effect tracing spans end-to-end"); }); From 3208f258a4fd25566e08a81d2b28dddc582aa858 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 4 May 2026 10:06:15 +0200 Subject: [PATCH 107/306] test(effect): cover schema codec services across action channels 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. --- .../packages/effect/test/e2e.test.ts | 46 +++++++++++- .../packages/effect/test/fixtures/actor.ts | 75 ++++++++++++++++++- 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 0abee817f6..1674909b84 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -6,8 +6,10 @@ import { CounterLive, CounterOverflowError, Greeter, + Multiplier, Pinger, PingerLive, + ScaledOverflowError, } from "./fixtures/actor"; const GreeterLive = Layer.succeed( @@ -17,9 +19,21 @@ const GreeterLive = Layer.succeed( }), ); +// `Multiplier` has to be in scope on both sides of the wire: the +// `Counter`'s `Scale` action's codec consumes `Action.ServicesServer` +// during registration, and the test body's `Counter.client` getter +// consumes `Action.ServicesClient` for the same action. +// `provideMerge` keeps it as a layer output so the test effect +// itself sees it too. +const MultiplierLive = Layer.succeed( + Multiplier, + Multiplier.of({ factor: 2 }), +); + const TestLayer = Runner.test.pipe( Layer.provideMerge(Layer.mergeAll(CounterLive, PingerLive)), Layer.provide(GreeterLive), + Layer.provideMerge(MultiplierLive), Layer.provide(Registry.layer()), ); @@ -148,8 +162,38 @@ layer(TestLayer)("end-to-end", (it) => { it.todo("surfaces an error thrown inside an actor's build effect"); - it.todo( + it.effect( "runs encoding/decoding services for an action's payload, success, and error", + () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate([ + "t-codec-services", + ]); + + // Success path. With `factor: 2` provided on both sides: + // payload encode 10 -> 5 (client divides), payload decode + // 5 -> 10 (server multiplies), handler returns 110, success + // encode 110 -> 55 (server divides), success decode 55 -> 110 + // (client multiplies). A wrong final value would mean one + // of those four codec sites failed to resolve `Multiplier`. + assert.strictEqual(yield* counter.Scale({ amount: 10 }), 110); + + // Error path. The handler short-circuits with a + // `ScaledOverflowError({ limit: 30 })`. The error's `limit` + // flows through the same service-dependent schema: server + // encode 30 -> 15, client decode 15 -> 30. A factor mismatch + // or an unprovided service on either side would surface as + // a numeric mismatch on `exit.value.limit`. + const exit = yield* counter + .Scale({ amount: 40 }) + .pipe(Effect.flip, Effect.exit); + assert.isTrue(exit._tag === "Success"); + if (exit._tag === "Success") { + assert.instanceOf(exit.value, ScaledOverflowError); + assert.strictEqual(exit.value.limit, 30); + assert.match(exit.value.message, /exceed limit 30/); + } + }), ); it.todo("propagates Effect tracing spans end-to-end"); diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts index 1106b33ae3..c45e4934d7 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts @@ -62,8 +62,67 @@ export const WakeGreeting = Action.make("WakeGreeting", { success: Schema.String, }); +// Service that the codec schema below depends on. Yielding it from +// inside a `transformOrFail` puts `Multiplier` into the schema's +// `DecodingServices` / `EncodingServices`, which in turn surfaces in +// `Action.ServicesServer` / `Action.ServicesClient` for any action +// referencing the codec. +export class Multiplier extends Context.Service< + Multiplier, + { readonly factor: number } +>()("test/Multiplier") {} + +// A `Number` schema whose decode multiplies by the live factor and whose +// encode divides by it. With the same factor on both ends, values +// round-trip; the test would fail if any codec site failed to resolve +// `Multiplier`. +const ScaledNumber = Schema.Number.pipe( + Schema.decodeTo( + Schema.Number, + SchemaTransformation.transformOrFail({ + decode: (n: number) => + Effect.gen(function* () { + const m = yield* Multiplier; + return n * m.factor; + }), + encode: (n: number) => + Effect.gen(function* () { + const m = yield* Multiplier; + return n / m.factor; + }), + }), + ), +); + +export class ScaledOverflowError extends Schema.TaggedErrorClass()( + "ScaledOverflowError", + { + limit: ScaledNumber, + message: Schema.String, + }, +) {} + +// Every channel of this action — payload, success, error — references +// `ScaledNumber`, so a successful round-trip proves all six codec sites +// (payload encode + decode, success encode + decode, error encode + +// decode) resolved their schema services. +export const Scale = Action.make("Scale", { + payload: { amount: ScaledNumber }, + success: ScaledNumber, + error: ScaledOverflowError, +}); + export const Counter = Actor.make("Counter", { - actions: [Increment, GetCount, Crash, EchoDate, Tags, Greet, WakeGreeting], + actions: [ + Increment, + GetCount, + Crash, + EchoDate, + Tags, + Greet, + WakeGreeting, + Scale, + ], }); export const CounterLive = Counter.toLayer( @@ -100,6 +159,20 @@ export const CounterLive = Counter.toLayer( return g.greet(payload.name); }), WakeGreeting: () => Effect.succeed(wakeGreeting), + Scale: ({ payload }) => + Effect.gen(function* () { + if (payload.amount > 30) { + return yield* new ScaledOverflowError({ + limit: 30, + message: `amount ${payload.amount} would exceed limit 30`, + }); + } + // +100 makes the round-trip non-tautological: the + // test asserts on a value the client never sent, so + // the success path can't pass without the success + // and payload codec sites firing on both sides. + return payload.amount + 100; + }), }); }), ); From 843e0d1f977406a7e6853e14b41eaab49fe653ad Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 4 May 2026 10:37:21 +0200 Subject: [PATCH 108/306] test(effect): cover defect inside actor build effect 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. --- .../packages/effect/test/e2e.test.ts | 22 +++++++++++++++++-- .../packages/effect/test/fixtures/actor.ts | 10 +++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 1674909b84..cdf6841a16 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -5,6 +5,8 @@ import { Counter, CounterLive, CounterOverflowError, + FailingActor, + FailingActorLive, Greeter, Multiplier, Pinger, @@ -31,7 +33,9 @@ const MultiplierLive = Layer.succeed( ); const TestLayer = Runner.test.pipe( - Layer.provideMerge(Layer.mergeAll(CounterLive, PingerLive)), + Layer.provideMerge( + Layer.mergeAll(CounterLive, PingerLive, FailingActorLive), + ), Layer.provide(GreeterLive), Layer.provideMerge(MultiplierLive), Layer.provide(Registry.layer()), @@ -160,7 +164,21 @@ layer(TestLayer)("end-to-end", (it) => { it.todo("fires the wake-scope finalizer on sleep"); - it.todo("surfaces an error thrown inside an actor's build effect"); + it.effect("surfaces an error thrown inside an actor's build effect", () => + Effect.gen(function* () { + // `getOrCreate` only builds a typed proxy on the client and + // rivetkit's wake is lazy on first action, so the build + // defect surfaces on `.Ping()`, not here. + const failing = (yield* FailingActor.client).getOrCreate([ + "t-build-error", + ]); + const exit = yield* failing.Ping().pipe(Effect.flip, Effect.exit); + assert.isTrue(exit._tag === "Success"); + if (exit._tag === "Success") { + assert.instanceOf(exit.value, RivetError.RivetError); + } + }), + ); it.effect( "runs encoding/decoding services for an action's payload, success, and error", diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts index c45e4934d7..321775edae 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts @@ -188,3 +188,13 @@ export const Pinger = Actor.make("Pinger", { actions: [Ping] }); export const PingerLive = Pinger.toLayer({ Ping: () => Effect.succeed("pong"), }); + +// --- FailingActor --- + +export const FailingActor = Actor.make("FailingBuild", { + actions: [Ping], +}); + +export const FailingActorLive = FailingActor.toLayer( + Effect.die("build effect failed"), +); From 63697fd55d715bbb3b973b84e430bd8a54e461d5 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 4 May 2026 11:08:18 +0200 Subject: [PATCH 109/306] test(effect): cover unregistered-actor failure shape 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`. --- .../packages/effect/test/e2e.test.ts | 29 +++++++++++++++++++ .../packages/effect/test/fixtures/actor.ts | 10 +++++++ 2 files changed, 39 insertions(+) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index cdf6841a16..7819aa779a 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -12,6 +12,7 @@ import { Pinger, PingerLive, ScaledOverflowError, + Unregistered, } from "./fixtures/actor"; const GreeterLive = Layer.succeed( @@ -162,6 +163,34 @@ layer(TestLayer)("end-to-end", (it) => { }), ); + it.effect( + "surfaces a call to an actor with no registered handler as a RivetError", + () => + Effect.gen(function* () { + // `Unregistered` is defined in the fixtures module but its + // `*Live` layer is intentionally not provided, so the engine + // has no runner that can serve the actor. The engine logs + // the precise `not_registered: Actor factory 'Unregistered' + // is not registered.` reason but flattens it on the wire to + // a generic `guard/service_unavailable` — the same code a + // transient engine outage would surface as. Callers can't + // distinguish the two without an engine-side change. + const ghost = (yield* Unregistered.client).getOrCreate([ + "t-unregistered", + ]); + const exit = yield* ghost.Echo().pipe(Effect.flip, Effect.exit); + assert.isTrue(exit._tag === "Success"); + if (exit._tag === "Success") { + assert.instanceOf(exit.value, RivetError.RivetError); + assert.strictEqual(exit.value.error.group, "guard"); + assert.strictEqual( + exit.value.error.code, + "service_unavailable", + ); + } + }), + ); + it.todo("fires the wake-scope finalizer on sleep"); it.effect("surfaces an error thrown inside an actor's build effect", () => diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts index 321775edae..851e7bafdd 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts @@ -198,3 +198,13 @@ export const FailingActor = Actor.make("FailingBuild", { export const FailingActorLive = FailingActor.toLayer( Effect.die("build effect failed"), ); + +// --- Unregistered --- + +// Used solely to test the failure shape when calling an actor whose +// `*Live` layer was never provided to the runner. No `UnregisteredLive` +// is exported on purpose — the test relies on this actor being absent +// from the registry at runtime. +export const Echo = Action.make("Echo", { success: Schema.String }); + +export const Unregistered = Actor.make("Unregistered", { actions: [Echo] }); From 6051e92581903239346d5e952a63b7fa32383d8e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 4 May 2026 12:03:07 +0200 Subject: [PATCH 110/306] feat(effect): propagate Effect tracing spans across actor calls 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`). --- .../packages/effect/src/Actor.ts | 254 +++++++++++------- .../packages/effect/src/Client.ts | 25 +- .../packages/effect/src/internal/tracing.ts | 42 +++ .../packages/effect/test/e2e.test.ts | 86 ++++-- .../packages/effect/test/fixtures/actor.ts | 19 ++ .../packages/effect/test/fixtures/tracer.ts | 46 ++++ 6 files changed, 348 insertions(+), 124 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/src/internal/tracing.ts create mode 100644 rivetkit-typescript/packages/effect/test/fixtures/tracer.ts diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index af5e5c8ae4..c72d6eaea6 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -8,11 +8,13 @@ import { Ref, Schema, Scope, + Tracer, } from "effect"; import * as Rivetkit from "rivetkit"; import * as RivetkitClient from "rivetkit/client"; import type * as Action from "./Action"; -import { Client, type ClientService } from "./Client"; +import { Client, type ActionMeta, type ClientService } from "./Client"; +import { readTraceMeta, rpcSystem } from "./internal/tracing"; import * as RivetError from "./RivetError"; import { hasStringProperty } from "./utils"; @@ -82,13 +84,14 @@ export type RegistryOptions = Pick< * materialize the underlying rivetkit registry from the collected * entries). */ -export class Registry extends Context.Service Effect.Effect; - readonly entries: Effect.Effect>; -}>()( - "@rivetkit/effect/Actor/Registry", -) { +export class Registry extends Context.Service< + Registry, + { + readonly options: RegistryOptions; + readonly register: (entry: RegistryEntry) => Effect.Effect; + readonly entries: Effect.Effect>; + } +>()("@rivetkit/effect/Actor/Registry") { static layer(options: RegistryOptions = {}) { return Layer.effect( Registry, @@ -133,13 +136,14 @@ const toRivetkitActor = Effect.fnUntraced(function* ( ( c: Pick, payload?: unknown, + meta?: unknown, ) => Promise > = {}; for (const action of actor.actions) { const decodePayload = Schema.decodeUnknownEffect(action.payloadSchema); const encodeSuccess = Schema.encodeUnknownEffect(action.successSchema); const encodeError = Schema.encodeUnknownEffect(action.errorSchema); - actions[action._tag] = async (c, payload) => { + actions[action._tag] = async (c, payload, meta) => { const inst = instances.get(c.actorId); if (!inst) { throw new Error( @@ -153,38 +157,62 @@ const toRivetkitActor = Effect.fnUntraced(function* ( ); } - const pipeline = Effect.gen(function* () { - const decoded = yield* decodePayload(payload).pipe( - Effect.orDie, - ); - const result = yield* handler({ - _tag: action._tag, - action, - payload: decoded, - }).pipe( - Effect.catch((expectedError) => - Effect.gen(function* () { - const error = yield* encodeError( - expectedError, - ).pipe(Effect.orDie); - return yield* Effect.die( - new Rivetkit.UserError( - hasStringProperty("message")(error) - ? error.message - : `${action._tag} failed`, - { - code: hasStringProperty("_tag")(error) - ? error._tag - : undefined, - metadata: error, - }, - ), - ); - }), - ), - ); - return yield* encodeSuccess(result).pipe(Effect.orDie); - }); + let pipeline: Effect.Effect = Effect.gen( + function* () { + const decoded = yield* decodePayload(payload).pipe( + Effect.orDie, + ); + const result = yield* handler({ + _tag: action._tag, + action, + payload: decoded, + }).pipe( + Effect.catch((expectedError) => + Effect.gen(function* () { + const error = yield* encodeError( + expectedError, + ).pipe(Effect.orDie); + return yield* Effect.die( + new Rivetkit.UserError( + hasStringProperty("message")(error) + ? error.message + : `${action._tag} failed`, + { + code: hasStringProperty("_tag")( + error, + ) + ? error._tag + : undefined, + metadata: error, + }, + ), + ); + }), + ), + ); + return yield* encodeSuccess(result).pipe(Effect.orDie); + }, + ); + + // Always wrap in a server-side span so the handler has a + // live `currentSpan` even when the caller didn't ship trace + // context (e.g. a non-Effect-SDK client). When trace context + // is present, reattach it as the parent so the server span + // joins the caller's trace. + const rpcMethod = `${actor._tag}/${action._tag}`; + const traceMeta = readTraceMeta(meta); + pipeline = pipeline.pipe( + Effect.withSpan(rpcMethod, { + parent: traceMeta + ? Tracer.externalSpan(traceMeta) + : undefined, + kind: "server", + attributes: { + "rpc.system.name": rpcSystem, + "rpc.method": rpcMethod, + }, + }), + ); const exit = await Effect.runPromiseExitWith(services)(pipeline); if (Exit.isSuccess(exit)) return exit.value; @@ -268,11 +296,12 @@ const toRivetkitRegistry = Effect.fnUntraced(function* ( * static field is a `Layer` for a specific mode mirroring the * non-Effect TS SDK: `start`. Each requires `Registry`. */ -export class Runner extends Context.Service()( - "@rivetkit/effect/Actor/Runner", -) { +export class Runner extends Context.Service< + Runner, + { + readonly mode: "start" | "test"; + } +>()("@rivetkit/effect/Actor/Runner") { static start = Layer.effect( Runner, Effect.gen(function* () { @@ -341,12 +370,15 @@ export class Runner extends Context.Service Effect.tryPromise({ try: () => rivetkitClient[actorName].getOrCreate(key).action({ name: actionName, - args: [encodedPayload], + args: meta + ? [encodedPayload, meta] + : [encodedPayload], }), catch: (cause) => cause instanceof Rivetkit.RivetError @@ -534,62 +566,82 @@ const Proto = { > = {}; for (const action of actions) { const tag = action._tag; - handle[tag] = (payload) => - Effect.gen(function* () { - const encoded = - yield* Schema.encodeUnknownEffect( - action.payloadSchema, - )(payload); - const raw = yield* client - .callAction({ - actorName: self._tag, - key, - actionName: tag, - encodedPayload: encoded, - }) - .pipe( - // Try `errorSchema` first against the - // wire metadata. Fall back to wrapping - // the raw RivetError via `RivetErrorFromWire`. - Effect.catch((rivetErr) => - Schema.decodeUnknownEffect( - action.errorSchema, - )( - ( - rivetErr as { - metadata?: unknown; - } - ).metadata, - ).pipe( - Effect.matchEffect({ - onSuccess: (typed) => - Effect.fail(typed), - onFailure: () => - Schema.decodeUnknownEffect( - RivetError.RivetErrorFromWire, - )({ - group: rivetErr.group, - code: rivetErr.code, - message: - rivetErr.message, - metadata: ( - rivetErr as { - metadata?: unknown; - } - ).metadata, - }).pipe( - Effect.flatMap( - Effect.fail, - ), + const rpcMethod = `${self._tag}/${tag}`; + // `Effect.fn` wraps the generator in a span named + // `rpcMethod` (kind=client + OTel `rpc.*` attrs) + // without an extra `pipe(Effect.withSpan(...))`. + // The active span inside is the one whose IDs + // the body reads via `Effect.currentSpan` and + // ships as `meta.trace`, so the server-side + // wrapper can reattach it as the handler's + // parent. Same pattern as Effect's RPC layer + // (`RpcClient.ts`). + handle[tag] = Effect.fn(rpcMethod, { + kind: "client", + attributes: { + "rpc.system.name": rpcSystem, + "rpc.method": rpcMethod, + }, + })(function* (payload: unknown) { + const encoded = yield* Schema.encodeUnknownEffect( + action.payloadSchema, + )(payload); + const span = yield* Effect.currentSpan; + const meta: ActionMeta = { + trace: { + traceId: span.traceId, + spanId: span.spanId, + sampled: span.sampled, + }, + }; + const raw = yield* client + .callAction({ + actorName: self._tag, + key, + actionName: tag, + encodedPayload: encoded, + meta, + }) + .pipe( + // Try `errorSchema` first against the + // wire metadata. Fall back to wrapping + // the raw RivetError via `RivetErrorFromWire`. + Effect.catch((rivetErr) => + Schema.decodeUnknownEffect( + action.errorSchema, + )( + (rivetErr as { metadata?: unknown }) + .metadata, + ).pipe( + Effect.matchEffect({ + onSuccess: (typed) => + Effect.fail(typed), + onFailure: () => + Schema.decodeUnknownEffect( + RivetError.RivetErrorFromWire, + )({ + group: rivetErr.group, + code: rivetErr.code, + message: + rivetErr.message, + metadata: ( + rivetErr as { + metadata?: unknown; + } + ).metadata, + }).pipe( + Effect.flatMap( + Effect.fail, ), - }), - ), + ), + }), ), - ); - return yield* Schema.decodeUnknownEffect( - action.successSchema, - )(raw); - }) as Effect.Effect; + ), + ); + return yield* Schema.decodeUnknownEffect( + action.successSchema, + )(raw); + }) as (p: unknown) => Effect.Effect; } return handle as Handle; }, diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 8214f7dca8..36b74f0bc6 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -2,18 +2,29 @@ import { Context, Effect, Layer } from "effect"; import * as Rivetkit from "rivetkit"; import * as RivetkitClient from "rivetkit/client"; import type { ActorKeyParam } from "./Actor"; +import type { TraceMeta } from "./internal/tracing"; /** * Connection options for the Rivet Engine client transport. Mirrors * the `(endpoint, token, namespace)` subset of rivetkit's - * `ClientConfigInput` — the only fields the Effect SDK currently - * surfaces and forwards. + * `ClientConfigInput`. */ export type ClientOptions = Pick< RivetkitClient.ClientConfigInput, "endpoint" | "token" | "namespace" >; +/** + * Per-call metadata envelope shipped as `args[1]` alongside the encoded + * payload. The SDK currently uses it for trace propagation (`trace`), + * but it's intentionally extensible so future cross-cutting concerns — + * idempotency keys, deadlines, custom headers — can land as additional + * optional fields without changing the wire shape. + */ +export interface ActionMeta { + readonly trace?: TraceMeta; +} + /** * Service holding the rivetkit client transport. Provided once via * `Client.layer({ ... })`. Consumed by `Actor.client` to dispatch @@ -28,12 +39,17 @@ export class Client extends Context.Service< * the rivetkit `RivetError` instance via `Effect.fail` — the * caller decides whether to decode `metadata` as a typed error or * wrap it through the wire codec. + * + * `meta`, when provided, rides the wire as the second positional + * `args` entry. It's a generic envelope (`ActionMeta`) so the SDK + * can grow cross-cutting fields without changing the wire shape. */ readonly callAction: (params: { readonly actorName: string; readonly key: ActorKeyParam; readonly actionName: string; readonly encodedPayload: unknown; + readonly meta?: ActionMeta; }) => Effect.Effect; } >()("@rivetkit/effect/Client") { @@ -47,12 +63,15 @@ export class Client extends Context.Service< key, actionName, encodedPayload, + meta, }) => Effect.tryPromise({ try: () => rivetkitClient[actorName].getOrCreate(key).action({ name: actionName, - args: [encodedPayload], + args: meta + ? [encodedPayload, meta] + : [encodedPayload], }), catch: (cause) => cause instanceof Rivetkit.RivetError diff --git a/rivetkit-typescript/packages/effect/src/internal/tracing.ts b/rivetkit-typescript/packages/effect/src/internal/tracing.ts new file mode 100644 index 0000000000..798dd079e2 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/internal/tracing.ts @@ -0,0 +1,42 @@ +import { Predicate } from "effect"; + +/** + * Identifies the SDK as the RPC system on action spans. Stamped onto + * the `rpc.system.name` OTel attribute. + */ +export const rpcSystem = "rivet.actor"; + +/** + * Cross-wire trace metadata. Carries just enough of an `Effect.Tracer` + * span to reconstitute it on the server as a `Tracer.externalSpan` + * parent for the handler's span. + */ +export interface TraceMeta { + readonly traceId: string; + readonly spanId: string; + readonly sampled: boolean; +} + +/** + * Pull a valid `TraceMeta` out of the wire `ActionMeta` envelope, or + * `undefined` if the caller didn't ship one (or shipped something + * malformed). Kept lenient because the meta envelope is forward- + * extensible — future fields shouldn't break trace extraction. + */ +export const readTraceMeta = (meta: unknown): TraceMeta | undefined => { + if (!Predicate.isObject(meta)) return undefined; + const trace = meta.trace; + if (!Predicate.isObject(trace)) return undefined; + if ( + !Predicate.isString(trace.traceId) || + !Predicate.isString(trace.spanId) || + !Predicate.isBoolean(trace.sampled) + ) { + return undefined; + } + return { + traceId: trace.traceId, + spanId: trace.spanId, + sampled: trace.sampled, + }; +}; diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 7819aa779a..da214929e4 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -14,6 +14,7 @@ import { ScaledOverflowError, Unregistered, } from "./fixtures/actor"; +import { TestTracer } from "./fixtures/tracer"; const GreeterLive = Layer.succeed( Greeter, @@ -28,10 +29,7 @@ const GreeterLive = Layer.succeed( // consumes `Action.ServicesClient` for the same action. // `provideMerge` keeps it as a layer output so the test effect // itself sees it too. -const MultiplierLive = Layer.succeed( - Multiplier, - Multiplier.of({ factor: 2 }), -); +const MultiplierLive = Layer.succeed(Multiplier, Multiplier.of({ factor: 2 })); const TestLayer = Runner.test.pipe( Layer.provideMerge( @@ -39,6 +37,7 @@ const TestLayer = Runner.test.pipe( ), Layer.provide(GreeterLive), Layer.provideMerge(MultiplierLive), + Layer.provideMerge(TestTracer.layer()), Layer.provide(Registry.layer()), ); @@ -73,20 +72,23 @@ layer(TestLayer)("end-to-end", (it) => { }), ); - it.effect("surfaces an expected handler error back into the original error", () => - Effect.gen(function* () { - const counter = (yield* Counter.client).getOrCreate(["t-expected-error"]); - const exit = yield* counter.Increment({ amount: 100 }).pipe( - Effect.flip, - Effect.exit, - ); - assert.isTrue(exit._tag === "Success"); - if (exit._tag === "Success") { - assert.instanceOf(exit.value, CounterOverflowError); - assert.strictEqual(exit.value.limit, 20); - assert.match(exit.value.message, /exceed limit 20/); - } - }), + it.effect( + "surfaces an expected handler error back into the original error", + () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate([ + "t-expected-error", + ]); + const exit = yield* counter + .Increment({ amount: 100 }) + .pipe(Effect.flip, Effect.exit); + assert.isTrue(exit._tag === "Success"); + if (exit._tag === "Success") { + assert.instanceOf(exit.value, CounterOverflowError); + assert.strictEqual(exit.value.limit, 20); + assert.match(exit.value.message, /exceed limit 20/); + } + }), ); it.effect("surfaces an unexpected handler error as a RivetError", () => @@ -112,7 +114,9 @@ layer(TestLayer)("end-to-end", (it) => { it.effect("round-trips a custom Schema.transform", () => Effect.gen(function* () { - const counter = (yield* Counter.client).getOrCreate(["t-transform"]); + const counter = (yield* Counter.client).getOrCreate([ + "t-transform", + ]); // `tags` rides the wire as the encoded CSV string and decodes // back to a string array on the server. If the transform // didn't fire, `payload.tags.length` would be the byte length @@ -243,5 +247,47 @@ layer(TestLayer)("end-to-end", (it) => { }), ); - it.todo("propagates Effect tracing spans end-to-end"); + it.effect("propagates Effect tracing spans end-to-end", () => + Effect.gen(function* () { + const tracer = yield* TestTracer; + yield* tracer.clear; + const counter = (yield* Counter.client).getOrCreate(["t-tracing"]); + // Wrapping the call in `Effect.withSpan("client-call")` + // makes that span the active parent. The SDK then opens + // `Counter/Compute` (kind=client) under it, ships the IDs + // over the wire, and on the server opens another + // `Counter/Compute` (kind=server) parented to the client + // span via `externalSpan`. The handler itself wraps its + // work in `Effect.withSpan("step.double")`, which nests + // under the SDK's server span — proving user-defined + // sub-spans join the propagated trace. + const clientTraceId = yield* Effect.gen(function* () { + const clientSpan = yield* Effect.currentSpan; + const doubled = yield* counter.Compute({ n: 21 }); + assert.strictEqual(doubled, 42); + return clientSpan.traceId; + }).pipe(Effect.withSpan("client-call")); + + const spans = yield* tracer.spans; + const onTrace = spans.filter((s) => s.traceId === clientTraceId); + assert.deepStrictEqual( + onTrace.map((s) => s.name), + [ + "client-call", + "Counter/Compute", + "Counter/Compute", + "step.double", + ], + ); + // Each span (after the root) is parented to the prior one, + // proving the chain is intact across the wire boundary. + for (let i = 1; i < onTrace.length; i++) { + const parent = onTrace[i].parent; + assert.strictEqual(parent._tag, "Some"); + if (parent._tag === "Some") { + assert.strictEqual(parent.value.spanId, onTrace[i - 1].spanId); + } + } + }), + ); }); diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts index 851e7bafdd..501a3cfdb8 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts @@ -62,6 +62,16 @@ export const WakeGreeting = Action.make("WakeGreeting", { success: Schema.String, }); +// An action whose handler emits its own user-defined sub-span. The +// tracing test asserts the sub-span lands as a child of the SDK's +// server-side span, which itself is a child of the SDK's client-side +// span — proof that user spans nest correctly under the SDK's wire +// propagation. +export const Compute = Action.make("Compute", { + payload: { n: Schema.Number }, + success: Schema.Number, +}); + // Service that the codec schema below depends on. Yielding it from // inside a `transformOrFail` puts `Multiplier` into the schema's // `DecodingServices` / `EncodingServices`, which in turn surfaces in @@ -121,6 +131,7 @@ export const Counter = Actor.make("Counter", { Tags, Greet, WakeGreeting, + Compute, Scale, ], }); @@ -159,6 +170,14 @@ export const CounterLive = Counter.toLayer( return g.greet(payload.name); }), WakeGreeting: () => Effect.succeed(wakeGreeting), + // User-defined sub-span. The SDK already wraps the handler + // in a server-side span; the inner `withSpan("step.double")` + // nests under it, demonstrating that hand-written spans + // inside a handler join the caller's trace transparently. + Compute: ({ payload }) => + Effect.succeed(payload.n * 2).pipe( + Effect.withSpan("step.double"), + ), Scale: ({ payload }) => Effect.gen(function* () { if (payload.amount > 30) { diff --git a/rivetkit-typescript/packages/effect/test/fixtures/tracer.ts b/rivetkit-typescript/packages/effect/test/fixtures/tracer.ts new file mode 100644 index 0000000000..1b84e87b12 --- /dev/null +++ b/rivetkit-typescript/packages/effect/test/fixtures/tracer.ts @@ -0,0 +1,46 @@ +import { Context, Effect, Layer, Tracer } from "effect"; + +/** + * Test-only tracer service: tests yield it to inspect spans recorded + * during a call (`spans`) and reset between runs (`clear`). + * + * `TestTracer.layer()` overrides the active `Tracer.Tracer` Reference + * with a wrapper around `Effect.tracer` that pushes every created span + * into a buffer local to the layer closure. Because `Tracer.Tracer` is + * a `Context.Reference` (always available via its default), the override + * does not surface in the layer's output type; only the read-side + * `TestTracer` service does. + */ +export class TestTracer extends Context.Service< + TestTracer, + { + readonly spans: Effect.Effect>; + readonly clear: Effect.Effect; + } +>()("test/TestTracer") { + static layer() { + return Layer.effectContext( + Effect.gen(function* () { + const buffer: Tracer.Span[] = []; + const currentTracer = yield* Effect.tracer; + const tracer = Tracer.make({ + span(options) { + const span = currentTracer.span(options); + buffer.push(span); + return span; + }, + context: currentTracer.context, + }); + return Context.make( + TestTracer, + TestTracer.of({ + spans: Effect.sync(() => buffer.slice()), + clear: Effect.sync(() => { + buffer.length = 0; + }), + }), + ).pipe(Context.add(Tracer.Tracer, tracer)); + }), + ); + } +} From 978201697ecb03deebb4a2c9d366b19896d2aac4 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 4 May 2026 12:04:23 +0200 Subject: [PATCH 111/306] chore(effect): update vitest config to set RIVET_LOG_LEVEL=SILENT for quieter test output --- rivetkit-typescript/packages/effect/vitest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/vitest.config.ts b/rivetkit-typescript/packages/effect/vitest.config.ts index 70fd49a902..44286c8ca9 100644 --- a/rivetkit-typescript/packages/effect/vitest.config.ts +++ b/rivetkit-typescript/packages/effect/vitest.config.ts @@ -11,7 +11,7 @@ const env = { RIVET_ENGINE_BINARY: join(here, "../../../target/debug/rivet-engine"), // The shared vitest base sets RIVET_LOG_LEVEL=DEBUG, which floods the // terminal with engine + runtime logs. Keep this suite quiet. - RIVET_LOG_LEVEL: "ERROR", + RIVET_LOG_LEVEL: "SILENT", }; export default defineConfig({ From 911659abeac4bd3cc08b28b1dcd882e190e4d16a Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 4 May 2026 17:53:26 +0200 Subject: [PATCH 112/306] feat(effect): add `Actor.Sleep` for per-wake sleep requests --- .../packages/effect/src/Actor.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index c72d6eaea6..2c4e6d70b9 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -53,6 +53,25 @@ export class CurrentAddress extends Context.Service< ActorAddress >()("@rivetkit/effect/Actor/CurrentAddress") {} +/** + * Internal carrier for the per-wake sleep request. Provided + * automatically by the SDK during `onWake`; consumers should yield + * `Actor.Sleep` (the `Effect` below) rather than this tag directly. + */ +export class SleepFn extends Context.Service>()( + "@rivetkit/effect/Actor/SleepFn", +) {} + +/** + * Asks the engine to sleep the current actor instance after the + * current action returns. The wake scope's finalizers run, and the + * actor is reloaded lazily on the next call. Yieldable from any action + * handler or the wake-scope build effect. + */ +export const Sleep: Effect.Effect = Effect.flatten( + SleepFn.asEffect(), +); + /** * One actor registered with the `Registry`. The `buildHandlers` * effect is run once per wake by the runner to construct @@ -241,11 +260,15 @@ const toRivetkitActor = Effect.fnUntraced(function* ( const built = entry.buildHandlers as Effect.Effect< unknown, never, - Scope.Scope | CurrentAddress + Scope.Scope | CurrentAddress | SleepFn >; const handlers = yield* built.pipe( Effect.provideService(CurrentAddress, address), Effect.provideService(Scope.Scope, scope), + Effect.provideService( + SleepFn, + Effect.sync(() => c.sleep()), + ), ) as Effect.Effect; return { handlers, scope }; }); @@ -479,7 +502,7 @@ export interface Actor< ): Layer.Layer< never, never, - | Exclude + | Exclude | HandlerServices | Action.ServicesServer | Action.ServicesClient From 6260dae6ec25811b76501167adf7fad769dcc9ff Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 4 May 2026 19:40:09 +0200 Subject: [PATCH 113/306] refactor(effect): rename `SleepFn` to `Sleep` and update references in `Actor` --- .../packages/effect/src/Actor.ts | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 2c4e6d70b9..96a7cbd286 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -58,20 +58,10 @@ export class CurrentAddress extends Context.Service< * automatically by the SDK during `onWake`; consumers should yield * `Actor.Sleep` (the `Effect` below) rather than this tag directly. */ -export class SleepFn extends Context.Service>()( - "@rivetkit/effect/Actor/SleepFn", +export class Sleep extends Context.Service>()( + "@rivetkit/effect/Actor/Sleep", ) {} -/** - * Asks the engine to sleep the current actor instance after the - * current action returns. The wake scope's finalizers run, and the - * actor is reloaded lazily on the next call. Yieldable from any action - * handler or the wake-scope build effect. - */ -export const Sleep: Effect.Effect = Effect.flatten( - SleepFn.asEffect(), -); - /** * One actor registered with the `Registry`. The `buildHandlers` * effect is run once per wake by the runner to construct @@ -260,13 +250,13 @@ const toRivetkitActor = Effect.fnUntraced(function* ( const built = entry.buildHandlers as Effect.Effect< unknown, never, - Scope.Scope | CurrentAddress | SleepFn + Scope.Scope | CurrentAddress | Sleep | CurrentState >; const handlers = yield* built.pipe( Effect.provideService(CurrentAddress, address), Effect.provideService(Scope.Scope, scope), Effect.provideService( - SleepFn, + Sleep, Effect.sync(() => c.sleep()), ), ) as Effect.Effect; @@ -502,7 +492,7 @@ export interface Actor< ): Layer.Layer< never, never, - | Exclude + | Exclude | HandlerServices | Action.ServicesServer | Action.ServicesClient From c753e9194ceb1336c743d5860ce936cd5824220a Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 4 May 2026 19:55:18 +0200 Subject: [PATCH 114/306] refactor(effect): simplify state-schema wiring in Actor --- .../packages/effect/src/Actor.ts | 95 ++++++++++++++++--- .../packages/effect/test/e2e.test.ts | 44 +++++++++ .../packages/effect/test/fixtures/actor.ts | 43 ++++++++- 3 files changed, 168 insertions(+), 14 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 96a7cbd286..60ff6f8b7d 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -8,6 +8,8 @@ import { Ref, Schema, Scope, + Stream, + SubscriptionRef, Tracer, } from "effect"; import * as Rivetkit from "rivetkit"; @@ -54,10 +56,15 @@ export class CurrentAddress extends Context.Service< >()("@rivetkit/effect/Actor/CurrentAddress") {} /** - * Internal carrier for the per-wake sleep request. Provided - * automatically by the SDK during `onWake`; consumers should yield - * `Actor.Sleep` (the `Effect` below) rather than this tag directly. + * Internal carrier for the wake-scoped state ref. Yield via the actor's + * typed `State` getter (e.g. `yield* Counter.State`) instead of this tag + * directly; the tag erases the schema type to `unknown`. */ +export class CurrentState extends Context.Service< + CurrentState, + SubscriptionRef.SubscriptionRef +>()("@rivetkit/effect/Actor/CurrentState") {} + export class Sleep extends Context.Service>()( "@rivetkit/effect/Actor/Sleep", ) {} @@ -229,9 +236,24 @@ const toRivetkitActor = Effect.fnUntraced(function* ( }; } + // Schema.Void is the "no state declared" sentinel. Skipping + // `createState` is what disables rivetkit's state machinery on the + // underlying actor. + const hasState = actor.stateSchema !== Schema.Void; + return Rivetkit.actor({ actions, options: actor.options, + // rivetkit invokes this once at create time and seeds c.state + // with the result. A SchemaError for a field without + // `withConstructorDefault` surfaces here, so the user fixes the + // schema instead of handling `undefined` on first wake. + ...(hasState + ? { + createState: () => + (actor.stateSchema as Schema.Top).make({} as never), + } + : {}), onWake: async ( c: Rivetkit.WakeContextOf, ) => { @@ -247,6 +269,27 @@ const toRivetkitActor = Effect.fnUntraced(function* ( // would have owned. const acquire = Effect.gen(function* () { const scope = yield* Scope.make(); + + const stateRef = yield* SubscriptionRef.make( + hasState ? c.state : undefined, + ); + if (hasState) { + // Mirror published changes back to c.state so + // rivetkit's throttled save loop and shutdown flush + // pick them up. The identity guard skips no-op + // updates; otherwise rivetkit re-encodes the value + // as CBOR and reschedules a save on every publish. + yield* SubscriptionRef.changes(stateRef).pipe( + Stream.drop(1), + Stream.runForEach((value) => + Effect.sync(() => { + if (value !== c.state) c.state = value; + }), + ), + Effect.forkIn(scope), + ); + } + const built = entry.buildHandlers as Effect.Effect< unknown, never, @@ -259,6 +302,7 @@ const toRivetkitActor = Effect.fnUntraced(function* ( Sleep, Effect.sync(() => c.sleep()), ), + Effect.provideService(CurrentState, stateRef), ) as Effect.Effect; return { handlers, scope }; }); @@ -478,12 +522,23 @@ export interface TypedAccessor { export interface Actor< Name extends string, Actions extends Action.AnyWithProps = never, + State extends Schema.Top = typeof Schema.Void, > { readonly [TypeId]: typeof TypeId; readonly _tag: Name; readonly key: string; readonly actions: ReadonlyArray; readonly options: GlobalActorOptionsInput; + readonly stateSchema: State; + /** + * Typed accessor for the wake-scoped state `SubscriptionRef`. Every + * published change is mirrored back to rivetkit's persisted state. + */ + readonly State: Effect.Effect< + SubscriptionRef.SubscriptionRef, + never, + CurrentState + >; of>(handlers: Handlers): Handlers; @@ -492,7 +547,7 @@ export interface Actor< ): Layer.Layer< never, never, - | Exclude + | Exclude | HandlerServices | Action.ServicesServer | Action.ServicesClient @@ -526,31 +581,39 @@ export interface Any { /** * Type-erased actor with all runtime properties available. */ -export interface AnyWithProps extends Actor {} +export interface AnyWithProps + extends Actor {} -export type Name = A extends Actor ? _Name : never; +export type Name = A extends Actor ? _Name : never; export type Actions = - A extends Actor ? _Actions : never; + A extends Actor ? _Actions : never; + +export type State = A extends Actor ? _State : never; export type Services = - A extends Actor ? Action.Services<_Actions> : never; + A extends Actor + ? Action.Services<_Actions> + : never; export type ClientServices = - A extends Actor + A extends Actor ? Action.ServicesClient<_Actions> : never; export type ServerServices = - A extends Actor + A extends Actor ? Action.ServicesServer<_Actions> : never; const identity = (value: A): A => value; +const StateAccessor = CurrentState.asEffect(); + const Proto = { [TypeId]: TypeId, of: identity, + State: StateAccessor, toLayer(this: AnyWithProps, build: unknown) { const self = this; const buildHandlers = ( @@ -666,16 +729,18 @@ const Proto = { const makeProto = < const Name extends string, Actions extends Action.AnyWithProps, + State extends Schema.Top, >(options: { readonly _tag: Name; readonly actions: ReadonlyArray; readonly options: GlobalActorOptionsInput; -}): Actor => { + readonly stateSchema: State; +}): Actor => { const key = `@rivetkit/effect/Actor/${options._tag}`; return Object.assign(Object.create(Proto), { ...options, key, - }) as Actor; + }) as Actor; }; /** @@ -684,16 +749,20 @@ const makeProto = < export const make = < const Name extends string, const Actions extends ReadonlyArray = readonly [], + State extends Schema.Top = typeof Schema.Void, >( name: Name, options?: { readonly actions?: Actions; readonly options?: GlobalActorOptionsInput; + readonly state?: State; }, -): Actor => { +): Actor => { + const stateSchema = (options?.state ?? Schema.Void) as State; return makeProto({ _tag: name, actions: (options?.actions ?? []) as ReadonlyArray, options: options?.options ?? {}, + stateSchema, }) as any; }; diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index da214929e4..13d218ba44 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -59,6 +59,50 @@ layer(TestLayer)("end-to-end", (it) => { }), ); + it.effect("persists state across a sleep/wake cycle", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate([ + "t-sleep-wake", + ]); + + // Bump the in-memory `Ref` so we can later assert that + // the wake actually rebuilt the actor (the ref resets + // to 0 on each wake). + yield* counter.Increment({ amount: 7 }); + + const beforeSleep = yield* counter.PersistAndSleep({ + amount: 11, + }); + assert.strictEqual(beforeSleep, 11); + + // Give the engine a moment to process the sleep request + // and tear down the wake scope before the next call + // triggers a fresh wake. `Effect.promise` bypasses the + // suite's TestClock so real time actually elapses. + yield* Effect.promise( + () => new Promise((resolve) => setTimeout(resolve, 2000)), + ); + + // `count` is `Ref.make(0)` per wake; if the actor + // didn't sleep+rewake, we'd still see 7. + const inMemoryAfterWake = yield* counter.GetCount(); + assert.strictEqual( + inMemoryAfterWake, + 0, + "in-memory ref should be reset by a fresh wake", + ); + + // `+ 0` is a no-op increment whose return value is the + // freshly-loaded persisted total. Anything other than + // 11 means either the write didn't durably land before + // sleep or the load on wake didn't seed the ref. + const persistedAfterWake = yield* counter.PersistedTotal({ + amount: 0, + }); + assert.strictEqual(persistedAfterWake, 11); + }), + ); + it.effect("isolates in-wake state across keys", () => Effect.gen(function* () { const client = yield* Counter.client; diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts index 501a3cfdb8..1e08f7b3d7 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts @@ -1,4 +1,11 @@ -import { Context, Effect, Ref, Schema, SchemaTransformation } from "effect"; +import { + Context, + Effect, + Ref, + Schema, + SchemaTransformation, + SubscriptionRef, +} from "effect"; import { Action, Actor } from "@rivetkit/effect"; // --- Counter --- @@ -122,7 +129,22 @@ export const Scale = Action.make("Scale", { error: ScaledOverflowError, }); +export const PersistedTotal = Action.make("PersistedTotal", { + payload: { amount: Schema.Number }, + success: Schema.Number, +}); + +export const PersistAndSleep = Action.make("PersistAndSleep", { + payload: { amount: Schema.Number }, + success: Schema.Number, +}); + export const Counter = Actor.make("Counter", { + state: Schema.Struct({ + count: Schema.Number.pipe( + Schema.withConstructorDefault(Effect.succeed(0)), + ), + }), actions: [ Increment, GetCount, @@ -133,16 +155,22 @@ export const Counter = Actor.make("Counter", { WakeGreeting, Compute, Scale, + PersistedTotal, + PersistAndSleep, ], }); export const CounterLive = Counter.toLayer( Effect.gen(function* () { + const state = yield* Counter.State; const count = yield* Ref.make(0); // Wake-scope yield of a non-built-in service. Resolved once per // wake; the captured value is closed over by `WakeGreeting`. const greeter = yield* Greeter; const wakeGreeting = greeter.greet("on wake"); + + const sleep = yield* Actor.Sleep; + return Counter.of({ Increment: ({ payload }) => Effect.gen(function* () { @@ -192,6 +220,19 @@ export const CounterLive = Counter.toLayer( // and payload codec sites firing on both sides. return payload.amount + 100; }), + PersistedTotal: ({ payload }) => + SubscriptionRef.updateAndGet(state, (s) => ({ + count: s.count + payload.amount, + })).pipe(Effect.map((s) => s.count)), + PersistAndSleep: ({ payload }) => + Effect.gen(function* () { + const { count } = yield* SubscriptionRef.updateAndGet( + state, + (s) => ({ count: s.count + payload.amount }), + ); + yield* sleep; + return count; + }), }); }), ); From 77f62b9c9be650c3e44f50b190e01e5e560c4978 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 5 May 2026 10:15:00 +0200 Subject: [PATCH 115/306] test(effect): improve reliability of e2e actor sleep tests Refactor sleep teardown to use deterministic post-condition polling with `TestClock.withLive` instead of relying on real-time delays, ensuring consistent test outcomes. --- .../packages/effect/test/e2e.test.ts | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 13d218ba44..2481c5d641 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -1,5 +1,6 @@ import { assert, layer } from "@effect/vitest"; -import { Effect, Layer } from "effect"; +import { Effect, Layer, Schedule } from "effect"; +import { TestClock } from "effect/testing"; import { Registry, RivetError, Runner } from "@rivetkit/effect"; import { Counter, @@ -75,22 +76,23 @@ layer(TestLayer)("end-to-end", (it) => { }); assert.strictEqual(beforeSleep, 11); - // Give the engine a moment to process the sleep request - // and tear down the wake scope before the next call - // triggers a fresh wake. `Effect.promise` bypasses the - // suite's TestClock so real time actually elapses. - yield* Effect.promise( - () => new Promise((resolve) => setTimeout(resolve, 2000)), - ); - - // `count` is `Ref.make(0)` per wake; if the actor - // didn't sleep+rewake, we'd still see 7. - const inMemoryAfterWake = yield* counter.GetCount(); - assert.strictEqual( - inMemoryAfterWake, - 0, - "in-memory ref should be reset by a fresh wake", + // Engine-side sleep teardown is asynchronous and the SDK + // exposes no "actor slept" hook today, so poll the + // post-condition. `count` is `Ref.make(0)` per wake, so + // seeing 0 is the deterministic signal that the prior + // wake torn down and a fresh one started. `TestClock.withLive` + // swaps in the real Clock for the duration of the poll so + // the schedule's interval and the timeout both elapse in + // wall time (the suite otherwise runs under TestClock). + const inMemoryAfterWake = yield* counter.GetCount().pipe( + Effect.repeat({ + until: (n) => n === 0, + schedule: Schedule.spaced("100 millis"), + }), + Effect.timeout("10 seconds"), + TestClock.withLive, ); + assert.strictEqual(inMemoryAfterWake, 0); // `+ 0` is a no-op increment whose return value is the // freshly-loaded persisted total. Anything other than From 86c6e6364f1e3fef414e874f6dbd9b51e0034de7 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 5 May 2026 10:32:34 +0200 Subject: [PATCH 116/306] refactor(effect): use Counter.State in counter example --- examples/effect/src/actors/counter/api.ts | 12 +++---- examples/effect/src/actors/counter/live.ts | 39 +++++++++------------- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts index 678e0cd298..b972e4705b 100644 --- a/examples/effect/src/actors/counter/api.ts +++ b/examples/effect/src/actors/counter/api.ts @@ -1,4 +1,4 @@ -import { Schema } from "effect" +import { Effect, Schema } from "effect" import { Actor, Action } from "@rivetkit/effect" // --- Errors --- @@ -60,11 +60,11 @@ export const GetCount = Action.make("GetCount", { // implementation. Both server and client code import this; // the implementation stays server-only. export const Counter = Actor.make("Counter", { - // state: Schema.Struct({ - // count: Schema.Number.pipe( - // Schema.withConstructorDefault(Effect.succeed(0)), - // ), - // }), + state: Schema.Struct({ + count: Schema.Number.pipe( + Schema.withConstructorDefault(Effect.succeed(0)), + ), + }), actions: [Increment, GetCount], // messages: [Reset, IncrementBy], // durable, queued, background // events: { countChanged: Schema.Number }, diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index 74c8d773aa..45d664fb02 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -1,4 +1,4 @@ -import { Effect, Ref } from "effect" +import { Effect, SubscriptionRef } from "effect" import { Actor } from "@rivetkit/effect" import { Counter, CounterOverflowError } from "./api.ts" @@ -26,16 +26,13 @@ export const CounterLive = Counter.toLayer( // - Swappable via layers. Tests can provide an in-memory KV // or a mock DB without changing the actor code. - // PersistedSubscriptionRef extends SubscriptionRef with - // throttled durable persistence. Standard SubscriptionRef - // combinators (get, set, update, modify, changes) work as-is. - // Every published change schedules a save via the configured - // stateSaveInterval; the wake-scope finalizer flushes pending - // writes before sleep so state is durable on teardown. Use - // PersistedSubscriptionRef.sync / updateAndSync when an action - // must wait for durability before responding. - // const state = yield* Counter.State - // // ^ PersistedSubscriptionRef<{ count: number }> + // Counter.State yields a SubscriptionRef whose published changes + // are mirrored back to rivetkit's persisted state. Standard + // SubscriptionRef combinators (get, set, update, modify, changes) + // work as-is, and the wake-scope finalizer flushes pending writes + // before sleep so state is durable on teardown. + const state = yield* Counter.State + // ^ SubscriptionRef<{ count: number }> // const events = yield* Counter.Events // // ^ { countChanged: PubSub } // const messages = yield* Counter.Messages @@ -47,16 +44,11 @@ export const CounterLive = Counter.toLayer( `waking ${address.name}/${address.key.join(",")} actorId=${address.actorId}`, ) - // In-memory per-wake state. Resets on every wake; this v1 - // has no persistence. Replace with a persisted state ref - // once Actor.State lands. - const count = yield* Ref.make(0) - yield* Effect.addFinalizer(() => - Ref.get(count).pipe( - Effect.flatMap((n) => + SubscriptionRef.get(state).pipe( + Effect.flatMap(({ count }) => Effect.log( - `sleeping ${address.name}/${address.key.join(",")} count=${n}`, + `sleeping ${address.name}/${address.key.join(",")} count=${count}`, ), ), ), @@ -94,9 +86,9 @@ export const CounterLive = Counter.toLayer( return Counter.of({ Increment: ({ payload }) => Effect.gen(function* () { - const next = yield* Ref.updateAndGet( - count, - (n) => n + payload.amount, + const { count: next } = yield* SubscriptionRef.updateAndGet( + state, + (s) => ({ count: s.count + payload.amount }), ) if (next > 20) { return yield* new CounterOverflowError({ @@ -108,7 +100,8 @@ export const CounterLive = Counter.toLayer( return next }), - GetCount: () => Ref.get(count), + GetCount: () => + SubscriptionRef.get(state).pipe(Effect.map((s) => s.count)), }) }), ) From 2627b0e7bd7506107bb7e025604b346227a9eeab Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 6 May 2026 14:15:26 +0200 Subject: [PATCH 117/306] chore(effect): use rivet.actors for rpc.system.name --- rivetkit-typescript/packages/effect/src/internal/tracing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/internal/tracing.ts b/rivetkit-typescript/packages/effect/src/internal/tracing.ts index 798dd079e2..bdb91d92a0 100644 --- a/rivetkit-typescript/packages/effect/src/internal/tracing.ts +++ b/rivetkit-typescript/packages/effect/src/internal/tracing.ts @@ -4,7 +4,7 @@ import { Predicate } from "effect"; * Identifies the SDK as the RPC system on action spans. Stamped onto * the `rpc.system.name` OTel attribute. */ -export const rpcSystem = "rivet.actor"; +export const rpcSystem = "rivet.actors"; /** * Cross-wire trace metadata. Carries just enough of an `Effect.Tracer` From e465f288a76de73c559983feb359e608d0bf121d Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 6 May 2026 14:16:28 +0200 Subject: [PATCH 118/306] chore(rivetkit): nest actor trace attrs under rivet.actors namespace --- .../packages/rivetkit/src/actor/instance/mod.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts index 1db2f4f646..666e95a8c1 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts @@ -1645,10 +1645,10 @@ export class ActorInstance< attributes?: Record, ): Record { return { - "rivet.actor.id": this.#actorId, - "rivet.actor.name": this.#name, - "rivet.actor.key": this.#actorKeyString, - "rivet.actor.region": this.#region, + "rivet.actors.actor.id": this.#actorId, + "rivet.actors.actor.name": this.#name, + "rivet.actors.actor.key": this.#actorKeyString, + "rivet.actors.actor.region": this.#region, ...(attributes ?? {}), }; } From 57d2e8c80903f9698c99f8d8e52368eba4814ac3 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 6 May 2026 17:07:26 +0200 Subject: [PATCH 119/306] chore(examples/node-client): replace counter with chat-room demo Rewrite the node-client example as a chat room across three actors (chatRoom, moderator, directory) covering db, queues with completable messages, createState, createVars, onWake, onDestroy, schedules, events and actor-to-actor calls. Update the client walkthrough to exercise get, getOrCreate, create, getForId, resolve, connect and event subscriptions. Replace counter tests with chat-room tests. --- examples/node-client/src/client.ts | 82 ++++- examples/node-client/src/index.ts | 184 ++++++++++- examples/node-client/tests/chat-room.test.ts | 303 +++++++++++++++++++ examples/node-client/tests/counter.test.ts | 50 --- examples/node-client/vitest.config.ts | 6 + 5 files changed, 559 insertions(+), 66 deletions(-) create mode 100644 examples/node-client/tests/chat-room.test.ts delete mode 100644 examples/node-client/tests/counter.test.ts diff --git a/examples/node-client/src/client.ts b/examples/node-client/src/client.ts index f6f506bf12..a2669f2cdd 100644 --- a/examples/node-client/src/client.ts +++ b/examples/node-client/src/client.ts @@ -4,19 +4,85 @@ import type { registry } from "./index.ts"; const client = createClient("http://localhost:6420"); async function main() { - const counter = client.counter.getOrCreate(["my-counter"]); + // getOrCreate: returns a stateless handle, seeding the actor with input + // the first time it is materialized via createState. + const room = client.chatRoom.getOrCreate(["general"], { + createWithInput: { name: "General" }, + }); - const initial = await counter.getCount(); - console.log("Initial count:", initial); + // resolve(): turns a key-based handle into the underlying actor id, useful + // for caching or for re-deriving a handle later via getForId. + const roomId = await room.resolve(); + console.log("room actor id:", roomId); - const afterIncrement = await counter.increment(5); - console.log("After +5:", afterIncrement); + // get(): a key-based handle that does NOT auto-create; safe here because + // getOrCreate above just materialized the actor. + const sameRoom = client.chatRoom.get(["general"]); + console.log("members so far:", await sameRoom.getMembers()); - const final = await counter.getCount(); - console.log("Final count:", final); + // create(): always allocates a fresh actor for the supplied key. + const ephemeral = await client.chatRoom.create( + [`scratch-${Date.now()}`], + { input: { name: "Scratch" } }, + ); + const ephemeralId = await ephemeral.resolve(); + console.log("ephemeral room id:", ephemeralId); + + // connect(): opens a stateful WebSocket connection. Subscriptions are + // registered via .on(name, handler); actions can be invoked over the same + // connection just like on a stateless handle. + const conn = room.connect(); + conn.on("memberJoined", ({ member }) => + console.log(`-> ${member.name} joined`), + ); + conn.on("memberLeft", ({ name }) => console.log(`<- ${name} left`)); + conn.on("newMessage", (msg) => + console.log(`[${msg.sender}] ${msg.text}`), + ); + conn.on("announcement", ({ text }) => + console.log(`** announcement: ${text} **`), + ); + + // Action over the connection. Triggers a memberJoined broadcast. + await conn.join("alice"); + + // Completable round-trip. sendMessage internally calls + // c.queue.enqueueAndWait("moderation", ...). The run loop pulls the + // message with `completable: true` and calls msg.complete(verdict); + // only then does this await resolve. + console.log("send (clean):", await conn.sendMessage("alice", "hello world!")); + console.log( + "send (blocked):", + await conn.sendMessage("alice", "this is a spam test"), + ); + + // Scheduled action: server broadcasts an announcement after a delay. + await conn.scheduleAnnouncement("welcome to the channel", 500); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Read persistent message history straight from the actor's SQLite db. + console.log("history:", await conn.getHistory()); + + // getForId(): re-derives a handle from a known actor id. Useful when you + // previously stored an id and want to talk to that exact instance. + const byId = client.chatRoom.getForId(roomId); + console.log("members via id handle:", await byId.getMembers()); + + // Cross-actor visibility: directory was registered by chatRoom.join. + const dir = client.directory.getOrCreate(["main"]); + console.log("rooms in directory:", await dir.listRooms()); + + // Moderator stats reflect every review the room delegated to it. + const moderatorStats = await client.moderator + .getOrCreate(["main"]) + .stats(); + console.log("moderator stats:", moderatorStats); + + await conn.dispose(); + await ephemeral.archive(); } main().catch((error) => { - console.error("Error:", error); + console.error("error:", error); process.exit(1); }); diff --git a/examples/node-client/src/index.ts b/examples/node-client/src/index.ts index 4ba57bcb95..e39158685f 100644 --- a/examples/node-client/src/index.ts +++ b/examples/node-client/src/index.ts @@ -1,21 +1,189 @@ -import { actor, setup } from "rivetkit"; +import { actor, event, queue, setup } from "rivetkit"; +import { db } from "rivetkit/db"; -export const counter = actor({ +// Singleton directory tracking which chat rooms are open. Exercised via +// actor-to-actor calls from chatRoom.onDestroy and chatRoom.join. +export const directory = actor({ state: { - count: 0, + rooms: [] as Array<{ + name: string; + openedAt: number; + closedAt?: number; + }>, }, + actions: { + registerRoom: (c, name: string) => { + if (c.state.rooms.some((r) => r.name === name)) return; + c.state.rooms.push({ name, openedAt: Date.now() }); + }, + closeRoom: (c, name: string) => { + const room = c.state.rooms.find((r) => r.name === name); + if (room) room.closedAt = Date.now(); + }, + listRooms: (c) => c.state.rooms, + }, +}); + +// Moderation service consumed by chat rooms via cross-actor RPC. +export const moderator = actor({ + state: { + bannedWords: ["spam", "scam"] as string[], + reviewed: 0, + }, + actions: { + review: (c, text: string) => { + c.state.reviewed += 1; + const hit = c.state.bannedWords.find((word) => + text.toLowerCase().includes(word), + ); + return hit + ? { + approved: false as const, + reason: `contains banned word "${hit}"`, + } + : { approved: true as const }; + }, + stats: (c) => ({ reviewed: c.state.reviewed }), + }, +}); +interface RoomInput { + name: string; +} +interface Member { + name: string; + joinedAt: number; +} +interface RoomState { + name: string; + members: Member[]; + wakeCount: number; +} + +export const chatRoom = actor({ + // SQLite-backed message log that survives sleeps and restarts. + db: db({ + onMigrate: async (db) => { + await db.execute(` + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sender TEXT NOT NULL, + text TEXT NOT NULL, + created_at INTEGER NOT NULL + ) + `); + }, + }), + // Persistent state seeded from the createWithInput / input passed by the + // client on getOrCreate / create. + createState: (_c, input: RoomInput): RoomState => ({ + name: input.name, + members: [], + wakeCount: 0, + }), + // Per-instance vars regenerated each wake. Useful for tracing. + createVars: () => ({ + sessionId: crypto.randomUUID(), + }), + events: { + newMessage: event<{ sender: string; text: string; createdAt: number }>(), + memberJoined: event<{ member: Member }>(), + memberLeft: event<{ name: string }>(), + announcement: event<{ text: string }>(), + }, + // Completable queue: actions enqueueAndWait, the run loop calls complete(). + queues: { + moderation: queue< + { sender: string; text: string }, + { approved: boolean; reason?: string } + >(), + }, + onWake: (c) => { + c.state.wakeCount += 1; + c.log.info({ + msg: "room awake", + sessionId: c.vars.sessionId, + wakeCount: c.state.wakeCount, + }); + }, + onDestroy: async (c) => { + const client = c.client(); + await client.directory.getOrCreate(["main"]).closeRoom(c.state.name); + }, + // Drains moderation messages, reviews each via the moderator actor, then + // completes the corresponding enqueueAndWait waiter inside sendMessage. + run: async (c) => { + const client = c.client(); + const reviewer = client.moderator.getOrCreate(["main"]); + for await (const msg of c.queue.iter({ + names: ["moderation"], + completable: true, + })) { + const verdict = await reviewer.review(msg.body.text); + await msg.complete(verdict); + } + }, actions: { - increment: (c, amount: number) => { - c.state.count += amount; - return c.state.count; + join: async (c, name: string): Promise => { + const member: Member = { name, joinedAt: Date.now() }; + c.state.members.push(member); + c.broadcast("memberJoined", { member }); + const client = c.client(); + await client.directory + .getOrCreate(["main"]) + .registerRoom(c.state.name); + return member; + }, + leave: (c, name: string) => { + c.state.members = c.state.members.filter((m) => m.name !== name); + c.broadcast("memberLeft", { name }); + }, + // Sends the message through the moderation pipeline before persisting. + // The action returns only after the run loop completes the queue entry. + sendMessage: async (c, sender: string, text: string) => { + const verdict = await c.queue.enqueueAndWait( + "moderation", + { sender, text }, + { timeout: 10_000 }, + ); + if (!verdict) { + throw new Error("moderation timed out"); + } + if (!verdict.approved) { + return { ok: false as const, reason: verdict.reason }; + } + const createdAt = Date.now(); + await c.db.execute( + "INSERT INTO messages (sender, text, created_at) VALUES (?, ?, ?)", + sender, + text, + createdAt, + ); + c.broadcast("newMessage", { sender, text, createdAt }); + return { ok: true as const, createdAt }; + }, + getHistory: async (c) => + c.db.execute( + "SELECT id, sender, text, created_at as createdAt FROM messages ORDER BY id", + ), + getMembers: (c) => c.state.members, + // Schedules a future broadcast. Implemented via c.schedule.after, which + // dispatches the named action with the supplied args. + scheduleAnnouncement: (c, text: string, delayMs: number) => { + c.schedule.after(delayMs, "triggerAnnouncement", text); + return { firesAt: Date.now() + delayMs }; + }, + triggerAnnouncement: (c, text: string) => { + c.broadcast("announcement", { text }); + }, + archive: (c) => { + c.destroy(); }, - getCount: (c) => c.state.count, }, }); export const registry = setup({ - use: { counter }, + use: { chatRoom, moderator, directory }, startEngine: true, }); diff --git a/examples/node-client/tests/chat-room.test.ts b/examples/node-client/tests/chat-room.test.ts new file mode 100644 index 0000000000..2370cfa54b --- /dev/null +++ b/examples/node-client/tests/chat-room.test.ts @@ -0,0 +1,303 @@ +import { setupTest } from "rivetkit/test"; +import { describe, expect, test } from "vitest"; +import { registry } from "../src/index.ts"; + +// Engine state persists across `setupTest` calls within a vitest run, so we +// derive a unique key per test to keep them isolated. +const uniqueKey = (label: string) => + `${label}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + +interface DirectoryEntry { + name: string; + openedAt: number; + closedAt?: number; +} + +describe("chat room actor", () => { + test("createState seeds room from input", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + const room = client.chatRoom.getOrCreate([uniqueKey("create-state")], { + createWithInput: { name: "Alpha" }, + }); + + expect(await room.getMembers()).toEqual([]); + + await room.join("alice"); + const members = await room.getMembers(); + expect(members).toHaveLength(1); + expect(members[0]?.name).toBe("alice"); + }); + + test("sendMessage runs through completable moderation queue", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + const room = client.chatRoom.getOrCreate([uniqueKey("clean")], { + createWithInput: { name: "Clean" }, + }); + + const result = await room.sendMessage("alice", "hello world"); + expect(result.ok).toBe(true); + if (result.ok) { + expect(typeof result.createdAt).toBe("number"); + } + }); + + test("moderator rejects banned words via cross-actor RPC", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + const room = client.chatRoom.getOrCreate([uniqueKey("blocked")], { + createWithInput: { name: "Blocked" }, + }); + + const result = await room.sendMessage("alice", "this is spam"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.reason).toMatch(/spam/); + } + + // Blocked messages must not be persisted to the SQLite log. + expect(await room.getHistory()).toEqual([]); + }); + + test("getHistory reads from the SQLite db", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + const room = client.chatRoom.getOrCreate([uniqueKey("history")], { + createWithInput: { name: "History" }, + }); + + await room.sendMessage("alice", "first"); + await room.sendMessage("bob", "second"); + await room.sendMessage("carol", "third"); + + const history = (await room.getHistory()) as Array<{ + sender: string; + text: string; + }>; + + expect(history.map((m) => [m.sender, m.text])).toEqual([ + ["alice", "first"], + ["bob", "second"], + ["carol", "third"], + ]); + }); + + test("connect() receives broadcast events", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + const room = client.chatRoom.getOrCreate([uniqueKey("events")], { + createWithInput: { name: "Events" }, + }); + + const conn = room.connect(); + try { + const messageReceived = new Promise<{ + sender: string; + text: string; + }>((resolve) => { + conn.on("newMessage", (msg) => resolve(msg)); + }); + const memberJoined = new Promise<{ member: { name: string } }>( + (resolve) => { + conn.on("memberJoined", (payload) => resolve(payload)); + }, + ); + + await conn.join("alice"); + await conn.sendMessage("alice", "ping"); + + expect((await memberJoined).member.name).toBe("alice"); + expect(await messageReceived).toMatchObject({ + sender: "alice", + text: "ping", + }); + } finally { + await conn.dispose(); + } + }); + + test("scheduleAnnouncement broadcasts after the delay", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + const room = client.chatRoom.getOrCreate([uniqueKey("schedule")], { + createWithInput: { name: "Schedule" }, + }); + + const conn = room.connect(); + try { + const announcementReceived = new Promise<{ text: string }>( + (resolve) => { + conn.on("announcement", (payload) => resolve(payload)); + }, + ); + + await conn.scheduleAnnouncement("welcome!", 100); + + expect(await announcementReceived).toEqual({ text: "welcome!" }); + } finally { + await conn.dispose(); + } + }); + + test("leave removes the member and broadcasts memberLeft", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + const room = client.chatRoom.getOrCreate([uniqueKey("leave")], { + createWithInput: { name: "Leave" }, + }); + + const conn = room.connect(); + try { + await conn.join("alice"); + await conn.join("bob"); + + const memberLeft = new Promise<{ name: string }>((resolve) => { + conn.on("memberLeft", (payload) => resolve(payload)); + }); + + await conn.leave("alice"); + + expect((await memberLeft).name).toBe("alice"); + expect(await conn.getMembers()).toEqual([ + expect.objectContaining({ name: "bob" }), + ]); + } finally { + await conn.dispose(); + } + }); + + test("different keys are isolated", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + const roomA = client.chatRoom.getOrCreate([uniqueKey("isolated-a")], { + createWithInput: { name: "A" }, + }); + const roomB = client.chatRoom.getOrCreate([uniqueKey("isolated-b")], { + createWithInput: { name: "B" }, + }); + + await roomA.sendMessage("alice", "in a"); + + expect(await roomA.getHistory()).toHaveLength(1); + expect(await roomB.getHistory()).toHaveLength(0); + }); + + test("getForId(resolve()) targets the same instance", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + const room = client.chatRoom.getOrCreate([uniqueKey("resolve")], { + createWithInput: { name: "Resolve" }, + }); + await room.join("alice"); + + const actorId = await room.resolve(); + const byId = client.chatRoom.getForId(actorId); + + const members = (await byId.getMembers()) as Array<{ name: string }>; + expect(members.map((m) => m.name)).toEqual(["alice"]); + }); + + test("create() always allocates a fresh actor", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + const a = await client.chatRoom.create([uniqueKey("create-a")], { + input: { name: "First" }, + }); + const b = await client.chatRoom.create([uniqueKey("create-b")], { + input: { name: "Second" }, + }); + + expect(await a.resolve()).not.toBe(await b.resolve()); + }); +}); + +describe("moderator actor", () => { + test("review approves clean text", async (ctx) => { + const { client } = await setupTest(ctx, registry); + const moderator = client.moderator.getOrCreate(["main"]); + + const verdict = await moderator.review("hello there"); + expect(verdict.approved).toBe(true); + }); + + test("review rejects text with banned words", async (ctx) => { + const { client } = await setupTest(ctx, registry); + const moderator = client.moderator.getOrCreate(["main"]); + + const verdict = await moderator.review("totally a scam"); + expect(verdict.approved).toBe(false); + if (!verdict.approved) { + expect(verdict.reason).toMatch(/scam/); + } + }); + + test("stats counter increments on each review", async (ctx) => { + const { client } = await setupTest(ctx, registry); + const moderator = client.moderator.getOrCreate([uniqueKey("stats")]); + + const before = (await moderator.stats()).reviewed; + await moderator.review("ok"); + await moderator.review("also fine"); + const after = (await moderator.stats()).reviewed; + + expect(after).toBe(before + 2); + }); + + test("chatRoom.sendMessage drives traffic to moderator['main']", async (ctx) => { + const { client } = await setupTest(ctx, registry); + const moderator = client.moderator.getOrCreate(["main"]); + const room = client.chatRoom.getOrCreate([uniqueKey("stats-room")], { + createWithInput: { name: "Stats Room" }, + }); + + const before = (await moderator.stats()).reviewed; + await room.sendMessage("alice", "hello"); + await room.sendMessage("alice", "again"); + const after = (await moderator.stats()).reviewed; + + expect(after).toBe(before + 2); + }); +}); + +describe("directory actor", () => { + test("chatRoom.join registers the room with the directory", async (ctx) => { + const { client } = await setupTest(ctx, registry); + + const roomName = uniqueKey("Directory Test"); + const room = client.chatRoom.getOrCreate([uniqueKey("directory")], { + createWithInput: { name: roomName }, + }); + await room.join("alice"); + + const dir = client.directory.getOrCreate(["main"]); + const rooms = (await dir.listRooms()) as DirectoryEntry[]; + + expect(rooms.some((r) => r.name === roomName)).toBe(true); + }); + + test("registerRoom is idempotent", async (ctx) => { + const { client } = await setupTest(ctx, registry); + const dir = client.directory.getOrCreate([uniqueKey("idempotent")]); + const roomName = uniqueKey("only-once"); + + await dir.registerRoom(roomName); + await dir.registerRoom(roomName); + + const rooms = (await dir.listRooms()) as DirectoryEntry[]; + expect(rooms.filter((r) => r.name === roomName)).toHaveLength(1); + }); + + test("closeRoom marks the room as closed", async (ctx) => { + const { client } = await setupTest(ctx, registry); + const dir = client.directory.getOrCreate([uniqueKey("close-test")]); + const roomName = uniqueKey("closing"); + + await dir.registerRoom(roomName); + await dir.closeRoom(roomName); + + const rooms = (await dir.listRooms()) as DirectoryEntry[]; + const closed = rooms.find((r) => r.name === roomName); + expect(closed?.closedAt).toBeTypeOf("number"); + }); +}); diff --git a/examples/node-client/tests/counter.test.ts b/examples/node-client/tests/counter.test.ts deleted file mode 100644 index b8a5b1525f..0000000000 --- a/examples/node-client/tests/counter.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { setupTest } from "rivetkit/test"; -import { describe, expect, test } from "vitest"; -import { registry } from "../src/index.ts"; - -describe("counter actor", () => { - test("starts at zero", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - const counter = client.counter.getOrCreate(["fresh"]); - - expect(await counter.getCount()).toBe(0); - }); - - test("increment returns the new total", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - const counter = client.counter.getOrCreate(["increments"]); - - expect(await counter.increment(3)).toBe(3); - expect(await counter.increment(7)).toBe(10); - }); - - test("state persists across handle re-resolution", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - await client.counter.getOrCreate(["persist"]).increment(5); - - const reResolved = client.counter.getOrCreate(["persist"]); - expect(await reResolved.getCount()).toBe(5); - }); - - test("different keys are isolated", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - await client.counter.getOrCreate(["a"]).increment(1); - await client.counter.getOrCreate(["b"]).increment(99); - - expect(await client.counter.getOrCreate(["a"]).getCount()).toBe(1); - expect(await client.counter.getOrCreate(["b"]).getCount()).toBe(99); - }); - - test("supports negative increments", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - const counter = client.counter.getOrCreate(["signed"]); - - await counter.increment(10); - expect(await counter.increment(-4)).toBe(6); - }); -}); diff --git a/examples/node-client/vitest.config.ts b/examples/node-client/vitest.config.ts index 5bdee00206..f14b5571e9 100644 --- a/examples/node-client/vitest.config.ts +++ b/examples/node-client/vitest.config.ts @@ -3,5 +3,11 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { include: ["tests/**/*.test.ts"], + // setupTest re-spawns the local engine binary for every test, and the + // first action against a freshly-restarted engine occasionally hits the + // guard.service_unavailable retry window before the router is fully + // wired. Retry transient warm-up failures. + retry: 2, + testTimeout: 30_000, }, }); From edc66fe5cad8625f26c7a5f23eadfaec2c074651 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 6 May 2026 17:13:16 +0200 Subject: [PATCH 120/306] refactor(effect): move state config from Actor.make to standalone ActorState --- examples/effect/src/actors/counter/api.ts | 13 +- examples/effect/src/actors/counter/live.ts | 53 +++++--- .../packages/effect/src/Actor.ts | 117 +++++++++--------- .../packages/effect/src/ActorState.ts | 83 +++++++++++++ .../packages/effect/src/mod.ts | 1 + .../packages/effect/test/fixtures/actor.ts | 17 +-- 6 files changed, 189 insertions(+), 95 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/src/ActorState.ts diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts index b972e4705b..0d3b699a81 100644 --- a/examples/effect/src/actors/counter/api.ts +++ b/examples/effect/src/actors/counter/api.ts @@ -1,4 +1,4 @@ -import { Effect, Schema } from "effect" +import { Schema } from "effect" import { Actor, Action } from "@rivetkit/effect" // --- Errors --- @@ -57,14 +57,11 @@ export const GetCount = Action.make("GetCount", { // --- Actor Definition --- // The definition is the actor's public contract. It carries no -// implementation. Both server and client code import this; -// the implementation stays server-only. +// implementation and no persisted-state schema (state is server-only, +// configured via `ActorState.make` + `toLayer({ state })` in `live.ts`). +// Both server and client code import this; the implementation stays +// server-only. export const Counter = Actor.make("Counter", { - state: Schema.Struct({ - count: Schema.Number.pipe( - Schema.withConstructorDefault(Effect.succeed(0)), - ), - }), actions: [Increment, GetCount], // messages: [Reset, IncrementBy], // durable, queued, background // events: { countChanged: Schema.Number }, diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index 45d664fb02..6de046e72b 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -1,6 +1,19 @@ -import { Effect, SubscriptionRef } from "effect" -import { Actor } from "@rivetkit/effect" -import { Counter, CounterOverflowError } from "./api.ts" +import { Effect, Schema, SubscriptionRef } from "effect"; +import { Actor, ActorState } from "@rivetkit/effect"; +import { Counter, CounterOverflowError } from "./api.ts"; + +// --- Actor State --- + +// State configuration (`schema` + `initial`) is server-only — it +// describes the persisted shape and must not leak into the client +// bundle. Defining it here keeps the contract in `api.ts` lean and +// shareable; the client never imports this file. +const CounterState = ActorState.make("CounterState", { + schema: Schema.Struct({ + count: Schema.Number, + }), + initial: () => ({ count: 0 }), +}); // --- Actor Implementation --- @@ -26,12 +39,13 @@ export const CounterLive = Counter.toLayer( // - Swappable via layers. Tests can provide an in-memory KV // or a mock DB without changing the actor code. - // Counter.State yields a SubscriptionRef whose published changes - // are mirrored back to rivetkit's persisted state. Standard - // SubscriptionRef combinators (get, set, update, modify, changes) - // work as-is, and the wake-scope finalizer flushes pending writes - // before sleep so state is durable on teardown. - const state = yield* Counter.State + // Yielding `CounterState` resolves to a SubscriptionRef whose + // published changes are mirrored back to rivetkit's persisted + // state. Standard SubscriptionRef combinators (get, set, update, + // modify, changes) work as-is, and the wake-scope finalizer + // flushes pending writes before sleep so state is durable on + // teardown. + const state = yield* CounterState; // ^ SubscriptionRef<{ count: number }> // const events = yield* Counter.Events // // ^ { countChanged: PubSub } @@ -39,10 +53,10 @@ export const CounterLive = Counter.toLayer( // // ^ MessageQueue // const kv = yield* Actor.Kv // const db = yield* Actor.Db - const address = yield* Actor.CurrentAddress + const address = yield* Actor.CurrentAddress; yield* Effect.log( `waking ${address.name}/${address.key.join(",")} actorId=${address.actorId}`, - ) + ); yield* Effect.addFinalizer(() => SubscriptionRef.get(state).pipe( @@ -52,7 +66,7 @@ export const CounterLive = Counter.toLayer( ), ), ), - ) + ); // --- Message processing (not yet implemented) --- // Pull-based: the actor controls when to take the next message. @@ -64,13 +78,13 @@ export const CounterLive = Counter.toLayer( // yield* Match.value(msg).pipe( // Match.tag("Reset", () => // Effect.gen(function* () { - // yield* PersistedSubscriptionRef.set(state, { count: 0 }) + // yield* SubscriptionRef.set(state, 0) // yield* PubSub.publish(events.countChanged, 0) // }) // ), // Match.tag("IncrementBy", ({ payload, complete }) => // Effect.gen(function* () { - // const next = yield* PersistedSubscriptionRef.updateAndGet( + // const next = yield* SubscriptionRef.updateAndGet( // state, // (s) => ({ count: s.count + payload.amount }), // ) @@ -89,19 +103,20 @@ export const CounterLive = Counter.toLayer( const { count: next } = yield* SubscriptionRef.updateAndGet( state, (s) => ({ count: s.count + payload.amount }), - ) + ); if (next > 20) { return yield* new CounterOverflowError({ limit: 20, message: `count ${next} would exceed limit 20`, - }) + }); } // yield* PubSub.publish(events.countChanged, next) - return next + return next; }), GetCount: () => SubscriptionRef.get(state).pipe(Effect.map((s) => s.count)), - }) + }); }), -) + { state: CounterState }, +); diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 60ff6f8b7d..a38ee8bf2c 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -15,6 +15,7 @@ import { import * as Rivetkit from "rivetkit"; import * as RivetkitClient from "rivetkit/client"; import type * as Action from "./Action"; +import type * as ActorState from "./ActorState"; import { Client, type ActionMeta, type ClientService } from "./Client"; import { readTraceMeta, rpcSystem } from "./internal/tracing"; import * as RivetError from "./RivetError"; @@ -55,16 +56,6 @@ export class CurrentAddress extends Context.Service< ActorAddress >()("@rivetkit/effect/Actor/CurrentAddress") {} -/** - * Internal carrier for the wake-scoped state ref. Yield via the actor's - * typed `State` getter (e.g. `yield* Counter.State`) instead of this tag - * directly; the tag erases the schema type to `unknown`. - */ -export class CurrentState extends Context.Service< - CurrentState, - SubscriptionRef.SubscriptionRef ->()("@rivetkit/effect/Actor/CurrentState") {} - export class Sleep extends Context.Service>()( "@rivetkit/effect/Actor/Sleep", ) {} @@ -74,10 +65,16 @@ export class Sleep extends Context.Service>()( * effect is run once per wake by the runner to construct * per-instance state and handlers; the handlers themselves are not * resolved at registration time. + * + * `state`, when present, carries the persisted-state schema and + * initial-value factory. The runner uses it to seed `c.state` on + * first create and to provide a typed `SubscriptionRef` under the + * state's tag inside the build effect's context. */ export interface RegistryEntry { readonly actor: AnyWithProps; readonly buildHandlers: Effect.Effect; + readonly state?: ActorState.AnyWithProps; } /** @@ -236,22 +233,21 @@ const toRivetkitActor = Effect.fnUntraced(function* ( }; } - // Schema.Void is the "no state declared" sentinel. Skipping - // `createState` is what disables rivetkit's state machinery on the - // underlying actor. - const hasState = actor.stateSchema !== Schema.Void; + // Skipping `createState` is what disables rivetkit's state machinery + // on the underlying actor; `entry.state` is the singular opt-in. + const stateDef = entry.state; + const hasState = stateDef !== undefined; return Rivetkit.actor({ actions, options: actor.options, // rivetkit invokes this once at create time and seeds c.state - // with the result. A SchemaError for a field without - // `withConstructorDefault` surfaces here, so the user fixes the - // schema instead of handling `undefined` on first wake. + // with the result. We delegate to the user-supplied `initial` + // factory so primitive states (e.g. `Schema.Number`) don't need + // `Schema.withConstructorDefault` boilerplate. ...(hasState ? { - createState: () => - (actor.stateSchema as Schema.Top).make({} as never), + createState: () => stateDef.initial(), } : {}), onWake: async ( @@ -293,18 +289,27 @@ const toRivetkitActor = Effect.fnUntraced(function* ( const built = entry.buildHandlers as Effect.Effect< unknown, never, - Scope.Scope | CurrentAddress | Sleep | CurrentState + Scope.Scope | CurrentAddress | Sleep >; - const handlers = yield* built.pipe( + let provided = built.pipe( Effect.provideService(CurrentAddress, address), Effect.provideService(Scope.Scope, scope), Effect.provideService( Sleep, Effect.sync(() => c.sleep()), ), - Effect.provideService(CurrentState, stateRef), - ) as Effect.Effect; - return { handlers, scope }; + ); + if (hasState) { + // Provide the SubscriptionRef under the user's typed + // `ActorState` tag so `yield* MyState` inside the build + // effect resolves to a `SubscriptionRef`. + provided = Effect.provideService( + provided, + stateDef, + stateRef, + ); + } + return { handlers: yield* provided, scope }; }); const { handlers, scope } = await Effect.runPromiseWith(services)(acquire); @@ -522,32 +527,26 @@ export interface TypedAccessor { export interface Actor< Name extends string, Actions extends Action.AnyWithProps = never, - State extends Schema.Top = typeof Schema.Void, > { readonly [TypeId]: typeof TypeId; readonly _tag: Name; readonly key: string; readonly actions: ReadonlyArray; readonly options: GlobalActorOptionsInput; - readonly stateSchema: State; - /** - * Typed accessor for the wake-scoped state `SubscriptionRef`. Every - * published change is mirrored back to rivetkit's persisted state. - */ - readonly State: Effect.Effect< - SubscriptionRef.SubscriptionRef, - never, - CurrentState - >; of>(handlers: Handlers): Handlers; - toLayer, RX = never>( + toLayer< + Handlers extends ActionHandlers, + RX = never, + State extends ActorState.Any = never, + >( build: Handlers | Effect.Effect, + options?: { readonly state?: State }, ): Layer.Layer< never, never, - | Exclude + | Exclude | HandlerServices | Action.ServicesServer | Action.ServicesClient @@ -581,50 +580,48 @@ export interface Any { /** * Type-erased actor with all runtime properties available. */ -export interface AnyWithProps - extends Actor {} +export interface AnyWithProps extends Actor {} -export type Name = A extends Actor ? _Name : never; +export type Name = A extends Actor ? _Name : never; export type Actions = - A extends Actor ? _Actions : never; - -export type State = A extends Actor ? _State : never; + A extends Actor ? _Actions : never; export type Services = - A extends Actor - ? Action.Services<_Actions> - : never; + A extends Actor ? Action.Services<_Actions> : never; export type ClientServices = - A extends Actor + A extends Actor ? Action.ServicesClient<_Actions> : never; export type ServerServices = - A extends Actor + A extends Actor ? Action.ServicesServer<_Actions> : never; const identity = (value: A): A => value; -const StateAccessor = CurrentState.asEffect(); - const Proto = { [TypeId]: TypeId, of: identity, - State: StateAccessor, - toLayer(this: AnyWithProps, build: unknown) { + toLayer( + this: AnyWithProps, + build: unknown, + options?: { readonly state?: ActorState.AnyWithProps }, + ) { const self = this; const buildHandlers = ( Effect.isEffect(build) ? build : Effect.succeed(build) ) as Effect.Effect; + const state = options?.state; return Layer.effectDiscard( Effect.gen(function* () { const registry = yield* Registry; yield* registry.register({ actor: self, buildHandlers, + state, }); }), ); @@ -729,40 +726,38 @@ const Proto = { const makeProto = < const Name extends string, Actions extends Action.AnyWithProps, - State extends Schema.Top, >(options: { readonly _tag: Name; readonly actions: ReadonlyArray; readonly options: GlobalActorOptionsInput; - readonly stateSchema: State; -}): Actor => { +}): Actor => { const key = `@rivetkit/effect/Actor/${options._tag}`; return Object.assign(Object.create(Proto), { ...options, key, - }) as Actor; + }) as Actor; }; /** * Define a Rivet Actor contract. + * + * The contract carries action schemas and display options. State is + * server-only and configured separately via `ActorState.make` plus + * `toLayer({ state })`; the client never sees the persisted shape. */ export const make = < const Name extends string, const Actions extends ReadonlyArray = readonly [], - State extends Schema.Top = typeof Schema.Void, >( name: Name, options?: { readonly actions?: Actions; readonly options?: GlobalActorOptionsInput; - readonly state?: State; }, -): Actor => { - const stateSchema = (options?.state ?? Schema.Void) as State; +): Actor => { return makeProto({ _tag: name, actions: (options?.actions ?? []) as ReadonlyArray, options: options?.options ?? {}, - stateSchema, }) as any; }; diff --git a/rivetkit-typescript/packages/effect/src/ActorState.ts b/rivetkit-typescript/packages/effect/src/ActorState.ts new file mode 100644 index 0000000000..912901722f --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/ActorState.ts @@ -0,0 +1,83 @@ +import { Context, type Schema, type SubscriptionRef } from "effect"; + +const TypeId = "~@rivetkit/effect/ActorState"; + +/** + * A typed, persistent state slot for one Rivet Actor. Yielded inside + * the wake-scope build effect to obtain a `SubscriptionRef` whose + * published changes are mirrored back to rivetkit's persisted state. + * + * State configuration (`schema` + `initial`) is server-only — it + * describes the persisted shape and lives in implementation modules + * (`live.ts`), not on the actor contract shared with clients. + */ +export interface ActorState< + in out Name extends string, + in out S extends Schema.Top, +> extends Context.Service< + ActorState, + SubscriptionRef.SubscriptionRef + > { + readonly [TypeId]: typeof TypeId; + readonly _tag: Name; + readonly schema: S; + readonly initial: () => S["Type"]; +} + +/** + * Type-erased view of any `ActorState`. + */ +export interface Any { + readonly [TypeId]: typeof TypeId; + readonly _tag: string; +} + +/** + * Like `Any`, but with the prop fields (`schema`, `initial`) accessible. + * Used by the runtime to seed `c.state` and provide the + * `SubscriptionRef` under the state's tag. + */ +export interface AnyWithProps + extends Context.Service> { + readonly [TypeId]: typeof TypeId; + readonly _tag: string; + readonly schema: Schema.Top; + readonly initial: () => unknown; +} + +export const isActorState = (u: unknown): u is Any => + typeof u === "object" && u !== null && (u as any)[TypeId] === TypeId; + +/** + * Define a typed, persistent state slot for a Rivet Actor. + * + * `schema` is the persisted shape; `initial` produces the value used to + * seed state on first wake. The returned value is itself a Context tag: + * `yield* MyState` inside the wake-scope build effect resolves to a + * `SubscriptionRef`. + * + * @example + * ```ts + * import { Schema } from "effect" + * import { ActorState } from "@rivetkit/effect" + * + * const CounterState = ActorState.make("CounterState", { + * schema: Schema.Number, + * initial: () => 0, + * }) + * ``` + */ +export const make = ( + name: Name, + options: { readonly schema: S; readonly initial: () => S["Type"] }, +): ActorState => { + const tag = Context.Service< + ActorState, + SubscriptionRef.SubscriptionRef + >(`@rivetkit/effect/ActorState/${name}`) as ActorState; + (tag as any)[TypeId] = TypeId; + (tag as any)._tag = name; + (tag as any).schema = options.schema; + (tag as any).initial = options.initial; + return tag; +}; diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index dc8d3204b0..121db0b285 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,5 +1,6 @@ export * as Action from "./Action"; export * as Actor from "./Actor"; export { Registry, Runner } from "./Actor"; +export * as ActorState from "./ActorState"; export { Client } from "./Client"; export * as RivetError from "./RivetError"; diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts index 1e08f7b3d7..7fb9bf4ad6 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts @@ -6,7 +6,7 @@ import { SchemaTransformation, SubscriptionRef, } from "effect"; -import { Action, Actor } from "@rivetkit/effect"; +import { Action, Actor, ActorState } from "@rivetkit/effect"; // --- Counter --- @@ -140,11 +140,6 @@ export const PersistAndSleep = Action.make("PersistAndSleep", { }); export const Counter = Actor.make("Counter", { - state: Schema.Struct({ - count: Schema.Number.pipe( - Schema.withConstructorDefault(Effect.succeed(0)), - ), - }), actions: [ Increment, GetCount, @@ -160,9 +155,16 @@ export const Counter = Actor.make("Counter", { ], }); +const CounterState = ActorState.make("CounterState", { + schema: Schema.Struct({ + count: Schema.Number, + }), + initial: () => ({ count: 0 }), +}); + export const CounterLive = Counter.toLayer( Effect.gen(function* () { - const state = yield* Counter.State; + const state = yield* CounterState; const count = yield* Ref.make(0); // Wake-scope yield of a non-built-in service. Resolved once per // wake; the captured value is closed over by `WakeGreeting`. @@ -235,6 +237,7 @@ export const CounterLive = Counter.toLayer( }), }); }), + { state: CounterState }, ); // --- Pinger --- From 0c59cdb8735cd861b02f3b1f11837ecc0d71a537 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 7 May 2026 14:28:44 +0200 Subject: [PATCH 121/306] refactor(effect): remove redundant `identity` function in `Actor` and reuse import --- rivetkit-typescript/packages/effect/src/Actor.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index a38ee8bf2c..3f9c3b6d29 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -3,6 +3,7 @@ import { Context, Effect, Exit, + identity, Layer, Predicate, Ref, @@ -600,8 +601,6 @@ export type ServerServices = ? Action.ServicesServer<_Actions> : never; -const identity = (value: A): A => value; - const Proto = { [TypeId]: TypeId, of: identity, From eeea15b607ae4cda7527930226149ab2b6f34388 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 7 May 2026 14:29:27 +0200 Subject: [PATCH 122/306] refactor(effect): reorder `Proto.of` in `Actor` for consistency --- rivetkit-typescript/packages/effect/src/Actor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 3f9c3b6d29..9319ba91c2 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -603,7 +603,6 @@ export type ServerServices = const Proto = { [TypeId]: TypeId, - of: identity, toLayer( this: AnyWithProps, build: unknown, @@ -720,6 +719,7 @@ const Proto = { }; }); }, + of: identity, }; const makeProto = < From e29aeca4818a708a056d720af68886dca8e97a56 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 7 May 2026 14:51:40 +0200 Subject: [PATCH 123/306] refactor(effect): replace `_tag` with `name` in `Actor` and enforce `ActorName` schema --- .../packages/effect/src/Actor.ts | 25 ++++++++++--------- .../packages/effect/src/ActorName.ts | 9 +++++++ 2 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/src/ActorName.ts diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 9319ba91c2..5e06105de5 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -15,6 +15,7 @@ import { } from "effect"; import * as Rivetkit from "rivetkit"; import * as RivetkitClient from "rivetkit/client"; +import type { ActorName } from "./ActorName"; import type * as Action from "./Action"; import type * as ActorState from "./ActorState"; import { Client, type ActionMeta, type ClientService } from "./Client"; @@ -161,13 +162,13 @@ const toRivetkitActor = Effect.fnUntraced(function* ( const inst = instances.get(c.actorId); if (!inst) { throw new Error( - `actor ${actor._tag}/${c.actorId} has no handlers (onWake didn't run?)`, + `actor ${actor.name}/${c.actorId} has no handlers (onWake didn't run?)`, ); } const handler = inst.handlers[action._tag]; if (!handler) { throw new Error( - `actor ${actor._tag} has no handler for action ${action._tag}`, + `actor ${actor.name} has no handler for action ${action._tag}`, ); } @@ -213,7 +214,7 @@ const toRivetkitActor = Effect.fnUntraced(function* ( // context (e.g. a non-Effect-SDK client). When trace context // is present, reattach it as the parent so the server span // joins the caller's trace. - const rpcMethod = `${actor._tag}/${action._tag}`; + const rpcMethod = `${actor.name}/${action._tag}`; const traceMeta = readTraceMeta(meta); pipeline = pipeline.pipe( Effect.withSpan(rpcMethod, { @@ -345,7 +346,7 @@ const toRivetkitRegistry = Effect.fnUntraced(function* ( const instances = new Map(); const use: Record = {}; for (const entry of entries) { - use[entry.actor._tag] = yield* toRivetkitActor(entry, instances); + use[entry.actor.name] = yield* toRivetkitActor(entry, instances); } return Rivetkit.setup({ @@ -526,11 +527,11 @@ export interface TypedAccessor { * display options, but no server implementation. */ export interface Actor< - Name extends string, - Actions extends Action.AnyWithProps = never, + in out Name extends ActorName, + in out Actions extends Action.AnyWithProps = never, > { readonly [TypeId]: typeof TypeId; - readonly _tag: Name; + readonly name: Name; readonly key: string; readonly actions: ReadonlyArray; readonly options: GlobalActorOptionsInput; @@ -581,7 +582,7 @@ export interface Any { /** * Type-erased actor with all runtime properties available. */ -export interface AnyWithProps extends Actor {} +export interface AnyWithProps extends Actor {} export type Name = A extends Actor ? _Name : never; @@ -637,7 +638,7 @@ const Proto = { > = {}; for (const action of actions) { const tag = action._tag; - const rpcMethod = `${self._tag}/${tag}`; + const rpcMethod = `${self.name}/${tag}`; // `Effect.fn` wraps the generator in a span named // `rpcMethod` (kind=client + OTel `rpc.*` attrs) // without an extra `pipe(Effect.withSpan(...))`. @@ -667,7 +668,7 @@ const Proto = { }; const raw = yield* client .callAction({ - actorName: self._tag, + actorName: self.name, key, actionName: tag, encodedPayload: encoded, @@ -723,7 +724,7 @@ const Proto = { }; const makeProto = < - const Name extends string, + const Name extends ActorName, Actions extends Action.AnyWithProps, >(options: { readonly _tag: Name; @@ -745,7 +746,7 @@ const makeProto = < * `toLayer({ state })`; the client never sees the persisted shape. */ export const make = < - const Name extends string, + const Name extends ActorName, const Actions extends ReadonlyArray = readonly [], >( name: Name, diff --git a/rivetkit-typescript/packages/effect/src/ActorName.ts b/rivetkit-typescript/packages/effect/src/ActorName.ts new file mode 100644 index 0000000000..d66a81277c --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/ActorName.ts @@ -0,0 +1,9 @@ +import { Schema } from "effect"; + +export const ActorName = Schema.String.pipe( + Schema.brand("~@rivetkit/effect/ActorName"), +); + +export type ActorName = typeof ActorName.Type; + +export const make = (value: string): ActorName => value as ActorName; From 95f3b391af1558170979bc83174bcd9dac16f072 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 7 May 2026 16:35:09 +0200 Subject: [PATCH 124/306] refactor(effect): move actor display options from `Actor.make` to per-instance `toLayer` Display options (`name`, `icon`) are per-instance concerns that belong alongside `state` on the server layer rather than on the actor contract. Adds `Action.ResultFrom` and `Actor.HandlersFrom` and tightens the generics on `Actor`/`toLayer`/`Registry` so handlers track their action set precisely. --- examples/effect/src/actors/counter/api.ts | 14 +- examples/effect/src/actors/counter/live.ts | 6 +- .../packages/effect/src/Action.ts | 16 +- .../packages/effect/src/Actor.ts | 152 +++++++++++------- 4 files changed, 115 insertions(+), 73 deletions(-) diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts index 0d3b699a81..02a25cb2f9 100644 --- a/examples/effect/src/actors/counter/api.ts +++ b/examples/effect/src/actors/counter/api.ts @@ -1,5 +1,5 @@ -import { Schema } from "effect" -import { Actor, Action } from "@rivetkit/effect" +import { Schema } from "effect"; +import { Actor, Action } from "@rivetkit/effect"; // --- Errors --- @@ -35,11 +35,11 @@ export const Increment = Action.make("Increment", { payload: { amount: Schema.Number }, success: Schema.Number, error: CounterOverflowError, -}) +}); export const GetCount = Action.make("GetCount", { success: Schema.Number, -}) +}); // --- Messages (not yet implemented) --- // @@ -65,8 +65,4 @@ export const Counter = Actor.make("Counter", { actions: [Increment, GetCount], // messages: [Reset, IncrementBy], // durable, queued, background // events: { countChanged: Schema.Number }, - options: { - name: "Counter", // Human-friendly display name - icon: "comments", // FontAwesome icon name - }, -}) +}); diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index 6de046e72b..fddbe7f2aa 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -118,5 +118,9 @@ export const CounterLive = Counter.toLayer( SubscriptionRef.get(state).pipe(Effect.map((s) => s.count)), }); }), - { state: CounterState }, + { + state: CounterState, + name: "Counter", // Human-friendly display name + icon: "comments", // FontAwesome icon name + }, ); diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index e0dc03900d..d87e574262 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -1,4 +1,4 @@ -import { Predicate, Schema } from "effect"; +import { Deferred, Effect, Predicate, Schema } from "effect"; import { RivetErrorFromWire } from "./RivetError"; const TypeId = "~@rivetkit/effect/Action"; @@ -134,6 +134,20 @@ export type ExtractTag = R extends { ? R : never; +export type ResultFrom = R extends Action< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error +> + ? Effect.Effect< + | _Success["Type"] + | Deferred.Deferred<_Success["Type"], _Error["Type"]>, + _Error["Type"], + Services + > + : never; + // --- Implementation ------------------------------------------------- const Proto = { diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 5e06105de5..844457b5c1 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -15,7 +15,6 @@ import { } from "effect"; import * as Rivetkit from "rivetkit"; import * as RivetkitClient from "rivetkit/client"; -import type { ActorName } from "./ActorName"; import type * as Action from "./Action"; import type * as ActorState from "./ActorState"; import { Client, type ActionMeta, type ClientService } from "./Client"; @@ -28,11 +27,34 @@ const TypeId = "~@rivetkit/effect/Actor"; export const isActor = (u: unknown): u is Actor => Predicate.hasProperty(u, TypeId); -export type GlobalActorOptionsInput = Pick< +export type RivetkitActorOptions = Pick< NonNullable, "name" | "icon" >; +/** + * Per-actor instance options. Combines the public + * `RivetkitActorOptions` (forwarded verbatim to `Rivetkit.actor`) + * with the effect-SDK-only options. + */ +export type ActorOptions = + Readonly & { + readonly state?: State; + }; + +const splitActorOptions = ( + options: ActorOptions, +): { + readonly rivetkitOptions: RivetkitActorOptions; + readonly effectOptions: Omit< + ActorOptions, + keyof RivetkitActorOptions + >; +} => { + const { state, ...rivetkitOptions } = options; + return { rivetkitOptions, effectOptions: { state } }; +}; + /** * Per-instance identity carried inside the wake scope. An actor * instance is addressable in two ways: @@ -73,10 +95,16 @@ export class Sleep extends Context.Service>()( * first create and to provide a typed `SubscriptionRef` under the * state's tag inside the build effect's context. */ -export interface RegistryEntry { - readonly actor: AnyWithProps; - readonly buildHandlers: Effect.Effect; - readonly state?: ActorState.AnyWithProps; +interface RegistryEntry< + Name extends string, + Actions extends Action.Any, + Handlers extends HandlersFrom, + RX, + State extends ActorState.AnyWithProps = never, +> { + readonly actor: Actor; + readonly buildHandlers: Effect.Effect; + readonly options?: ActorOptions; } /** @@ -103,15 +131,27 @@ export class Registry extends Context.Service< Registry, { readonly options: RegistryOptions; - readonly register: (entry: RegistryEntry) => Effect.Effect; - readonly entries: Effect.Effect>; + readonly register: < + Name extends string, + Actions extends Action.Any, + Handlers extends HandlersFrom, + RX, + State extends ActorState.AnyWithProps = never, + >( + entry: RegistryEntry, + ) => Effect.Effect; + readonly entries: Effect.Effect< + ReadonlyArray> + >; } >()("@rivetkit/effect/Actor/Registry") { static layer(options: RegistryOptions = {}) { return Layer.effect( Registry, Effect.gen(function* () { - const ref = yield* Ref.make>([]); + const ref = yield* Ref.make< + ReadonlyArray> + >([]); return Registry.of({ options, register: (entry) => @@ -136,7 +176,7 @@ type ActorInstance = { }; const toRivetkitActor = Effect.fnUntraced(function* ( - entry: RegistryEntry, + entry: RegistryEntry, instances: Map, ) { // Snapshot the current Effect context so action callbacks @@ -235,14 +275,15 @@ const toRivetkitActor = Effect.fnUntraced(function* ( }; } - // Skipping `createState` is what disables rivetkit's state machinery - // on the underlying actor; `entry.state` is the singular opt-in. - const stateDef = entry.state; - const hasState = stateDef !== undefined; + const actorOptions = entry.options + ? splitActorOptions(entry.options) + : undefined; + const stateDef = actorOptions?.effectOptions.state; + const hasState = actorOptions?.effectOptions.state !== undefined; return Rivetkit.actor({ actions, - options: actor.options, + options: actorOptions?.rivetkitOptions, // rivetkit invokes this once at create time and seeds c.state // with the result. We delegate to the user-supplied `initial` // factory so primitive states (e.g. `Schema.Number`) don't need @@ -469,7 +510,7 @@ export class Runner extends Context.Service< ); } -export type ActionRequest = +export type ActionRequest = A extends Action.Action< infer Tag, infer Payload, @@ -505,7 +546,7 @@ export type ActorKeyParam = string | Rivetkit.ActorKey; * returns an Effect with the action's success / typed error * channels baked in. */ -export type Handle = { +export type Handle = { readonly [A in Actions as Action.Tag]: ( payload: Action.PayloadConstructor, ) => Effect.Effect< @@ -518,7 +559,7 @@ export type Handle = { * Yielded by `Actor.client`. Address an actor instance by key, then * dispatch typed action calls against the returned `Handle`. */ -export interface TypedAccessor { +export interface TypedAccessor { readonly getOrCreate: (key: ActorKeyParam) => Handle; } @@ -527,24 +568,23 @@ export interface TypedAccessor { * display options, but no server implementation. */ export interface Actor< - in out Name extends ActorName, - in out Actions extends Action.AnyWithProps = never, + in out Name extends string, + in out Actions extends Action.Any = never, > { readonly [TypeId]: typeof TypeId; readonly name: Name; readonly key: string; readonly actions: ReadonlyArray; - readonly options: GlobalActorOptionsInput; - of>(handlers: Handlers): Handlers; + of>(handlers: Handlers): Handlers; toLayer< - Handlers extends ActionHandlers, + Handlers extends HandlersFrom, + State extends ActorState.AnyWithProps = never, RX = never, - State extends ActorState.Any = never, >( build: Handlers | Effect.Effect, - options?: { readonly state?: State }, + options?: ActorOptions, ): Layer.Layer< never, never, @@ -582,7 +622,7 @@ export interface Any { /** * Type-erased actor with all runtime properties available. */ -export interface AnyWithProps extends Actor {} +export interface AnyWithProps extends Actor {} export type Name = A extends Actor ? _Name : never; @@ -602,25 +642,34 @@ export type ServerServices = ? Action.ServicesServer<_Actions> : never; +export type HandlersFrom = { + readonly [Current in Action as Current["_tag"]]: ( + envelope: ActionRequest, + ) => Action.ResultFrom; +}; + const Proto = { [TypeId]: TypeId, - toLayer( - this: AnyWithProps, - build: unknown, - options?: { readonly state?: ActorState.AnyWithProps }, + toLayer< + Actions extends Action.Any, + Handlers extends HandlersFrom, + State extends ActorState.AnyWithProps = never, + RX = never, + >( + this: Actor, + build: Handlers | Effect.Effect, + options?: ActorOptions, ) { const self = this; - const buildHandlers = ( - Effect.isEffect(build) ? build : Effect.succeed(build) - ) as Effect.Effect; - const state = options?.state; return Layer.effectDiscard( Effect.gen(function* () { const registry = yield* Registry; yield* registry.register({ actor: self, - buildHandlers, - state, + buildHandlers: Effect.isEffect(build) + ? build + : Effect.succeed(build), + options, }); }), ); @@ -723,41 +772,20 @@ const Proto = { of: identity, }; -const makeProto = < - const Name extends ActorName, - Actions extends Action.AnyWithProps, ->(options: { - readonly _tag: Name; - readonly actions: ReadonlyArray; - readonly options: GlobalActorOptionsInput; -}): Actor => { - const key = `@rivetkit/effect/Actor/${options._tag}`; - return Object.assign(Object.create(Proto), { - ...options, - key, - }) as Actor; -}; - /** * Define a Rivet Actor contract. - * - * The contract carries action schemas and display options. State is - * server-only and configured separately via `ActorState.make` plus - * `toLayer({ state })`; the client never sees the persisted shape. */ export const make = < - const Name extends ActorName, + const Name extends string, const Actions extends ReadonlyArray = readonly [], >( name: Name, options?: { readonly actions?: Actions; - readonly options?: GlobalActorOptionsInput; }, ): Actor => { - return makeProto({ - _tag: name, - actions: (options?.actions ?? []) as ReadonlyArray, - options: options?.options ?? {}, - }) as any; + const self = Object.create(Proto); + self.name = name; + self.actions = options?.actions; + return self; }; From 8719e758dc6e24489a98e29e0a1d3ab23015bae7 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 7 May 2026 16:54:31 +0200 Subject: [PATCH 125/306] refactor(effect): collapse `Any`/`AnyWithProps` and drop unused `Actor` helpers Removes the type-erased `AnyWithProps`, the unused `Name`/`Actions`/`Services` helpers, the redundant `ActionHandlers` alias, and the unused `key` field on `Actor`. `Any` is now a direct alias for `Actor`. --- .../packages/effect/src/Actor.ts | 49 ++----------------- 1 file changed, 4 insertions(+), 45 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 844457b5c1..50ceeb6526 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -329,11 +329,7 @@ const toRivetkitActor = Effect.fnUntraced(function* ( ); } - const built = entry.buildHandlers as Effect.Effect< - unknown, - never, - Scope.Scope | CurrentAddress | Sleep - >; + const built = entry.buildHandlers; let provided = built.pipe( Effect.provideService(CurrentAddress, address), Effect.provideService(Scope.Scope, scope), @@ -357,7 +353,7 @@ const toRivetkitActor = Effect.fnUntraced(function* ( const { handlers, scope } = await Effect.runPromiseWith(services)(acquire); instances.set(c.actorId, { - handlers: handlers as ActorInstance["handlers"], + handlers, scope, }); }, @@ -524,12 +520,6 @@ export type ActionRequest = } : never; -export type ActionHandlers = { - readonly [A in Actions as Action.Tag]: ( - request: ActionRequest, - ) => Effect.Effect, Action.Error, unknown>; -}; - type HandlerServices = { readonly [Name in keyof Handlers]: Handlers[Name] extends ( ...args: ReadonlyArray @@ -573,7 +563,6 @@ export interface Actor< > { readonly [TypeId]: typeof TypeId; readonly name: Name; - readonly key: string; readonly actions: ReadonlyArray; of>(handlers: Handlers): Handlers; @@ -610,37 +599,7 @@ export interface Actor< >; } -/** - * Type-erased view of any actor contract. - */ -export interface Any { - readonly [TypeId]: typeof TypeId; - readonly _tag: string; - readonly key: string; -} - -/** - * Type-erased actor with all runtime properties available. - */ -export interface AnyWithProps extends Actor {} - -export type Name = A extends Actor ? _Name : never; - -export type Actions = - A extends Actor ? _Actions : never; - -export type Services = - A extends Actor ? Action.Services<_Actions> : never; - -export type ClientServices = - A extends Actor - ? Action.ServicesClient<_Actions> - : never; - -export type ServerServices = - A extends Actor - ? Action.ServicesServer<_Actions> - : never; +export type Any = Actor; export type HandlersFrom = { readonly [Current in Action as Current["_tag"]]: ( @@ -675,7 +634,7 @@ const Proto = { ); }, get client() { - const self = this as unknown as AnyWithProps; + const self = this as Any; return Effect.gen(function* () { const client = yield* Client; const actions = self.actions; From 687134582ed7e5b04a2078b1bdf923eb438d487f Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 7 May 2026 20:46:18 +0200 Subject: [PATCH 126/306] refactor(effect): streamline registry handling with `Effect.pipe` and simplify `Actor` logic --- .../packages/effect/src/Actor.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 50ceeb6526..8c785866b8 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -619,19 +619,18 @@ const Proto = { build: Handlers | Effect.Effect, options?: ActorOptions, ) { - const self = this; - return Layer.effectDiscard( - Effect.gen(function* () { - const registry = yield* Registry; - yield* registry.register({ - actor: self, + return Registry.asEffect().pipe( + Effect.flatMap((registry) => + registry.register({ + actor: this, buildHandlers: Effect.isEffect(build) ? build : Effect.succeed(build), options, - }); - }), - ); + }) + ), + Layer.effectDiscard + ) }, get client() { const self = this as Any; From c3b56320dc524856e3bc9317e24e5d2e02b31789 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 7 May 2026 21:16:14 +0200 Subject: [PATCH 127/306] refactor(effect): rename `GlobalActorOptionsInput` to `ActorOptionsInput` in `RivetkitActorOptions` definition --- rivetkit-typescript/packages/effect/src/Actor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 8c785866b8..d4bd4f688d 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -28,7 +28,7 @@ export const isActor = (u: unknown): u is Actor => Predicate.hasProperty(u, TypeId); export type RivetkitActorOptions = Pick< - NonNullable, + NonNullable, "name" | "icon" >; From 99d468a8c24d683cca8e6f2f64dcff234abc8791 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 7 May 2026 22:58:40 +0200 Subject: [PATCH 128/306] refactor(effect): fold `Runner` into `Registry` as `Registry.start` and `Registry.test` --- examples/effect/src/main.ts | 4 +- .../packages/effect/src/Actor.ts | 225 +++++++++--------- .../packages/effect/src/mod.ts | 2 +- .../packages/effect/test/e2e.test.ts | 4 +- 4 files changed, 111 insertions(+), 124 deletions(-) diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index a692e83654..78cc2b0aa7 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -1,6 +1,6 @@ import { Layer } from "effect" import { NodeRuntime } from "@effect/platform-node" -import { Registry, Runner } from "@rivetkit/effect" +import { Registry } from "@rivetkit/effect" import { CounterLive } from "./actors/counter/live.ts" // import { ChatRoomLive } from "./actors/chat-room/live.ts" @@ -14,7 +14,7 @@ const ActorsLayer = Layer.mergeAll( // point at a remote engine). For dev builds without a packaged engine, // set RIVET_ENGINE_BINARY to the path of a `cargo build` binary, e.g.: // RIVET_ENGINE_BINARY=$(pwd)/target/debug/rivet-engine pnpm start -const MainLayer = Runner.start.pipe( +const MainLayer = Registry.start.pipe( Layer.provide(ActorsLayer), Layer.provide(Registry.layer()), ) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index d4bd4f688d..1726c66062 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -123,9 +123,9 @@ export type RegistryOptions = Pick< * Service collecting actor defs/builders together with the engine * connection config. Provided once via `Registry.layer({ ... })` and * consumed by both `Actor.toLayer` (which registers itself into the - * collector on acquire) and the `Runner.*` mode layers (which - * materialize the underlying rivetkit registry from the collected - * entries). + * collector on acquire) and by `Registry.start` / `Registry.test` + * (which materialize the underlying rivetkit registry from the + * collected entries). */ export class Registry extends Context.Service< Registry, @@ -161,6 +161,107 @@ export class Registry extends Context.Service< }), ); } + + /** + * Run the registered actors against the configured engine. Reads + * the collected entries, materializes the underlying rivetkit + * registry, and starts it. + */ + static start = Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* Registry; + const rivetkitRegistry = yield* toRivetkitRegistry(registry); + yield* Effect.sync(() => rivetkitRegistry.start()); + }), + ); + + /** + * In-process test runtime. Boots the rivetkit registry against the + * configured engine, waits for `/health` to answer, and provides + * `Client` from the same Layer so consumers don't need to wire + * `Client.layer` separately. Mirrors `Registry.start` plus test-mode + * flags and a scoped client dispose. The registry itself is leaked + * to process exit because the public rivetkit `Registry` doesn't + * expose a public `shutdown()` today; only the SIGINT handler can + * drive `#runShutdown`. This matches `setupTest`'s existing behavior. + */ + static test: Layer.Layer = Layer.effect( + Client, + Effect.gen(function* () { + const registry = yield* Registry; + const rivetkitRegistry = yield* toRivetkitRegistry(registry); + rivetkitRegistry.config.test = { + ...rivetkitRegistry.config.test, + enabled: true, + }; + rivetkitRegistry.config.noWelcome = true; + // Auto-spawn the engine when no endpoint was provided, so + // `Registry.test` works out of the box without requiring the + // caller to start an engine externally. If the user wired an + // explicit endpoint via `Registry.layer({ endpoint: ... })`, + // honor it and skip the local spawn. + if (registry.options.endpoint === undefined) { + rivetkitRegistry.config.startEngine = true; + } + yield* Effect.sync(() => rivetkitRegistry.start()); + + // The rivetkitRegistry itself is leaked until process exit (matches + // setupTest's behavior). The public Rivetkit.Registry doesn't + // expose a shutdown method; only the SIGINT handler can drive the + // inner .shutdown(). Disposing the client is the only cleanup we + // can do cleanly today. + // + // When the engine was auto-spawned, propagate its resolved + // endpoint to the client so `createClient` doesn't fall back + // to its (warning-emitting) default. + const resolvedEndpoint = rivetkitRegistry.parseConfig().endpoint; + const rivetkitClient = yield* Effect.acquireRelease( + Effect.sync(() => + RivetkitClient.createClient({ + ...registry.options, + endpoint: + registry.options.endpoint ?? resolvedEndpoint, + }), + ), + (c) => Effect.promise(() => c.dispose()), + ); + + const callAction: ClientService["callAction"] = ({ + actorName, + key, + actionName, + encodedPayload, + meta, + }) => + Effect.tryPromise({ + try: () => + rivetkitClient[actorName].getOrCreate(key).action({ + name: actionName, + args: meta + ? [encodedPayload, meta] + : [encodedPayload], + }), + catch: (cause) => + cause instanceof Rivetkit.RivetError + ? cause + : new Rivetkit.RivetError( + "client", + "unknown", + cause instanceof Error + ? cause.message + : String(cause), + { + cause: + cause instanceof Error + ? cause + : undefined, + }, + ), + }); + + return Client.of({ callAction }); + }), + ); } type ActorInstance = { @@ -181,8 +282,8 @@ const toRivetkitActor = Effect.fnUntraced(function* ( ) { // Snapshot the current Effect context so action callbacks // (which run in rivetkit's plain Promise world) can run - // handler effects against the same services the Runner layer - // was provided with. + // handler effects against the same services the Registry.start / + // Registry.test layer was provided with. const services = yield* Effect.context(); const actor = entry.actor; @@ -392,120 +493,6 @@ const toRivetkitRegistry = Effect.fnUntraced(function* ( }); }); -/** - * Service that selects how the registered actors are served. Each - * static field is a `Layer` for a specific mode mirroring the - * non-Effect TS SDK: `start`. Each requires `Registry`. - */ -export class Runner extends Context.Service< - Runner, - { - readonly mode: "start" | "test"; - } ->()("@rivetkit/effect/Actor/Runner") { - static start = Layer.effect( - Runner, - Effect.gen(function* () { - const registry = yield* Registry; - const rivetkitRegistry = yield* toRivetkitRegistry(registry); - yield* Effect.sync(() => rivetkitRegistry.start()); - return Runner.of({ mode: "start" }); - }), - ); - - /** - * In-process test runtime. Boots the rivetkit registry against the - * configured engine, waits for `/health` to answer, and provides - * both `Runner` and `Client` from one Layer so consumers don't need - * to wire `Client.layer` separately. Mirrors `Runner.start` plus - * test-mode flags and a scoped client dispose. The registry itself - * is leaked to process exit because the public rivetkit `Registry` - * doesn't expose a public `shutdown()` today; only the SIGINT - * handler can drive `#runShutdown`. This matches `setupTest`'s - * existing behavior. - */ - static test: Layer.Layer = - Layer.effectContext( - Effect.gen(function* () { - const registry = yield* Registry; - const rivetkitRegistry = yield* toRivetkitRegistry(registry); - rivetkitRegistry.config.test = { - ...rivetkitRegistry.config.test, - enabled: true, - }; - rivetkitRegistry.config.noWelcome = true; - // Auto-spawn the engine when no endpoint was provided, so - // `Runner.test` works out of the box without requiring the - // caller to start an engine externally. If the user wired an - // explicit endpoint via `Registry.layer({ endpoint: ... })`, - // honor it and skip the local spawn. - if (registry.options.endpoint === undefined) { - rivetkitRegistry.config.startEngine = true; - } - yield* Effect.sync(() => rivetkitRegistry.start()); - - // The rivetkitRegistry itself is leaked until process exit (matches - // setupTest's behavior). The public Rivetkit.Registry doesn't - // expose a shutdown method; only the SIGINT handler can drive the - // inner .shutdown(). Disposing the client is the only cleanup we - // can do cleanly today. - // - // When the engine was auto-spawned, propagate its resolved - // endpoint to the client so `createClient` doesn't fall back - // to its (warning-emitting) default. - const resolvedEndpoint = - rivetkitRegistry.parseConfig().endpoint; - const rivetkitClient = yield* Effect.acquireRelease( - Effect.sync(() => - RivetkitClient.createClient({ - ...registry.options, - endpoint: - registry.options.endpoint ?? resolvedEndpoint, - }), - ), - (c) => Effect.promise(() => c.dispose()), - ); - - const callAction: ClientService["callAction"] = ({ - actorName, - key, - actionName, - encodedPayload, - meta, - }) => - Effect.tryPromise({ - try: () => - rivetkitClient[actorName].getOrCreate(key).action({ - name: actionName, - args: meta - ? [encodedPayload, meta] - : [encodedPayload], - }), - catch: (cause) => - cause instanceof Rivetkit.RivetError - ? cause - : new Rivetkit.RivetError( - "client", - "unknown", - cause instanceof Error - ? cause.message - : String(cause), - { - cause: - cause instanceof Error - ? cause - : undefined, - }, - ), - }); - - return Context.make(Runner, Runner.of({ mode: "test" })).pipe( - Context.add(Client, Client.of({ callAction })), - ); - }), - ); -} - export type ActionRequest = A extends Action.Action< infer Tag, diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index 121db0b285..4155624ef8 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,6 +1,6 @@ export * as Action from "./Action"; export * as Actor from "./Actor"; -export { Registry, Runner } from "./Actor"; +export { Registry } from "./Actor"; export * as ActorState from "./ActorState"; export { Client } from "./Client"; export * as RivetError from "./RivetError"; diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 2481c5d641..87832092de 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -1,7 +1,7 @@ import { assert, layer } from "@effect/vitest"; import { Effect, Layer, Schedule } from "effect"; import { TestClock } from "effect/testing"; -import { Registry, RivetError, Runner } from "@rivetkit/effect"; +import { Registry, RivetError } from "@rivetkit/effect"; import { Counter, CounterLive, @@ -32,7 +32,7 @@ const GreeterLive = Layer.succeed( // itself sees it too. const MultiplierLive = Layer.succeed(Multiplier, Multiplier.of({ factor: 2 })); -const TestLayer = Runner.test.pipe( +const TestLayer = Registry.test.pipe( Layer.provideMerge( Layer.mergeAll(CounterLive, PingerLive, FailingActorLive), ), From add3f11db345d72944ad898e3b876df292d00dd1 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 7 May 2026 23:13:11 +0200 Subject: [PATCH 129/306] fix(effect): keep R concrete in `ActorState.AnyWithProps` and `Registry.start` --- rivetkit-typescript/packages/effect/src/Actor.ts | 2 +- rivetkit-typescript/packages/effect/src/ActorState.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 1726c66062..5bbd13d885 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -167,7 +167,7 @@ export class Registry extends Context.Service< * the collected entries, materializes the underlying rivetkit * registry, and starts it. */ - static start = Layer.effectDiscard( + static start: Layer.Layer = Layer.effectDiscard( Effect.gen(function* () { const registry = yield* Registry; const rivetkitRegistry = yield* toRivetkitRegistry(registry); diff --git a/rivetkit-typescript/packages/effect/src/ActorState.ts b/rivetkit-typescript/packages/effect/src/ActorState.ts index 912901722f..8df45cfa9b 100644 --- a/rivetkit-typescript/packages/effect/src/ActorState.ts +++ b/rivetkit-typescript/packages/effect/src/ActorState.ts @@ -38,7 +38,7 @@ export interface Any { * `SubscriptionRef` under the state's tag. */ export interface AnyWithProps - extends Context.Service> { + extends Context.Service> { readonly [TypeId]: typeof TypeId; readonly _tag: string; readonly schema: Schema.Top; From a1dd9184d9e7b78c669e76df57487192bfd26393 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 7 May 2026 23:24:46 +0200 Subject: [PATCH 130/306] chore(effect): drop `vitest.config.ts` from `tsconfig` include --- rivetkit-typescript/packages/effect/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/tsconfig.json b/rivetkit-typescript/packages/effect/tsconfig.json index a396842bf0..588bd72ffb 100644 --- a/rivetkit-typescript/packages/effect/tsconfig.json +++ b/rivetkit-typescript/packages/effect/tsconfig.json @@ -14,5 +14,5 @@ } ] }, - "include": ["src/**/*", "test/**/*", "vitest.config.ts"] + "include": ["src/**/*", "test/**/*"] } From a47233b6ed5856375e87be57e52d5456b5af001a Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 8 May 2026 11:29:51 +0200 Subject: [PATCH 131/306] refactor(effect): extract `Registry` into its own module --- examples/effect/src/main.ts | 2 +- .../packages/effect/src/Actor.ts | 444 +----------------- .../packages/effect/src/Registry.ts | 424 +++++++++++++++++ .../packages/effect/src/mod.ts | 2 +- 4 files changed, 440 insertions(+), 432 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/src/Registry.ts diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index 78cc2b0aa7..25152c512c 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -14,7 +14,7 @@ const ActorsLayer = Layer.mergeAll( // point at a remote engine). For dev builds without a packaged engine, // set RIVET_ENGINE_BINARY to the path of a `cargo build` binary, e.g.: // RIVET_ENGINE_BINARY=$(pwd)/target/debug/rivet-engine pnpm start -const MainLayer = Registry.start.pipe( +const MainLayer = Registry.serve.pipe( Layer.provide(ActorsLayer), Layer.provide(Registry.layer()), ) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 5bbd13d885..e72a17a37a 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -1,26 +1,19 @@ import { - Cause, Context, Effect, - Exit, identity, Layer, Predicate, - Ref, Schema, Scope, - Stream, - SubscriptionRef, - Tracer, } from "effect"; import * as Rivetkit from "rivetkit"; -import * as RivetkitClient from "rivetkit/client"; +import * as Registry from './Registry'; import type * as Action from "./Action"; import type * as ActorState from "./ActorState"; -import { Client, type ActionMeta, type ClientService } from "./Client"; -import { readTraceMeta, rpcSystem } from "./internal/tracing"; +import * as Client from "./Client"; import * as RivetError from "./RivetError"; -import { hasStringProperty } from "./utils"; +import { rpcSystem } from "./internal/tracing"; const TypeId = "~@rivetkit/effect/Actor"; @@ -37,17 +30,17 @@ export type RivetkitActorOptions = Pick< * `RivetkitActorOptions` (forwarded verbatim to `Rivetkit.actor`) * with the effect-SDK-only options. */ -export type ActorOptions = +export type Options = Readonly & { readonly state?: State; }; -const splitActorOptions = ( - options: ActorOptions, +export const splitOptions = ( + options: Options, ): { readonly rivetkitOptions: RivetkitActorOptions; readonly effectOptions: Omit< - ActorOptions, + Options, keyof RivetkitActorOptions >; } => { @@ -84,415 +77,6 @@ export class Sleep extends Context.Service>()( "@rivetkit/effect/Actor/Sleep", ) {} -/** - * One actor registered with the `Registry`. The `buildHandlers` - * effect is run once per wake by the runner to construct - * per-instance state and handlers; the handlers themselves are not - * resolved at registration time. - * - * `state`, when present, carries the persisted-state schema and - * initial-value factory. The runner uses it to seed `c.state` on - * first create and to provide a typed `SubscriptionRef` under the - * state's tag inside the build effect's context. - */ -interface RegistryEntry< - Name extends string, - Actions extends Action.Any, - Handlers extends HandlersFrom, - RX, - State extends ActorState.AnyWithProps = never, -> { - readonly actor: Actor; - readonly buildHandlers: Effect.Effect; - readonly options?: ActorOptions; -} - -/** - * Connection options for the Rivet Engine. Mirrors the - * `(endpoint, token, namespace)` subset of rivetkit's - * `RegistryConfigInput`. All fields are optional and fall back to the - * matching `RIVET_*` environment variables (see the canonical schema - * for the exact resolution order). - */ -export type RegistryOptions = Pick< - Rivetkit.RegistryConfigInput, - "endpoint" | "token" | "namespace" ->; - -/** - * Service collecting actor defs/builders together with the engine - * connection config. Provided once via `Registry.layer({ ... })` and - * consumed by both `Actor.toLayer` (which registers itself into the - * collector on acquire) and by `Registry.start` / `Registry.test` - * (which materialize the underlying rivetkit registry from the - * collected entries). - */ -export class Registry extends Context.Service< - Registry, - { - readonly options: RegistryOptions; - readonly register: < - Name extends string, - Actions extends Action.Any, - Handlers extends HandlersFrom, - RX, - State extends ActorState.AnyWithProps = never, - >( - entry: RegistryEntry, - ) => Effect.Effect; - readonly entries: Effect.Effect< - ReadonlyArray> - >; - } ->()("@rivetkit/effect/Actor/Registry") { - static layer(options: RegistryOptions = {}) { - return Layer.effect( - Registry, - Effect.gen(function* () { - const ref = yield* Ref.make< - ReadonlyArray> - >([]); - return Registry.of({ - options, - register: (entry) => - Ref.update(ref, (xs) => [...xs, entry]), - entries: Ref.get(ref), - }); - }), - ); - } - - /** - * Run the registered actors against the configured engine. Reads - * the collected entries, materializes the underlying rivetkit - * registry, and starts it. - */ - static start: Layer.Layer = Layer.effectDiscard( - Effect.gen(function* () { - const registry = yield* Registry; - const rivetkitRegistry = yield* toRivetkitRegistry(registry); - yield* Effect.sync(() => rivetkitRegistry.start()); - }), - ); - - /** - * In-process test runtime. Boots the rivetkit registry against the - * configured engine, waits for `/health` to answer, and provides - * `Client` from the same Layer so consumers don't need to wire - * `Client.layer` separately. Mirrors `Registry.start` plus test-mode - * flags and a scoped client dispose. The registry itself is leaked - * to process exit because the public rivetkit `Registry` doesn't - * expose a public `shutdown()` today; only the SIGINT handler can - * drive `#runShutdown`. This matches `setupTest`'s existing behavior. - */ - static test: Layer.Layer = Layer.effect( - Client, - Effect.gen(function* () { - const registry = yield* Registry; - const rivetkitRegistry = yield* toRivetkitRegistry(registry); - rivetkitRegistry.config.test = { - ...rivetkitRegistry.config.test, - enabled: true, - }; - rivetkitRegistry.config.noWelcome = true; - // Auto-spawn the engine when no endpoint was provided, so - // `Registry.test` works out of the box without requiring the - // caller to start an engine externally. If the user wired an - // explicit endpoint via `Registry.layer({ endpoint: ... })`, - // honor it and skip the local spawn. - if (registry.options.endpoint === undefined) { - rivetkitRegistry.config.startEngine = true; - } - yield* Effect.sync(() => rivetkitRegistry.start()); - - // The rivetkitRegistry itself is leaked until process exit (matches - // setupTest's behavior). The public Rivetkit.Registry doesn't - // expose a shutdown method; only the SIGINT handler can drive the - // inner .shutdown(). Disposing the client is the only cleanup we - // can do cleanly today. - // - // When the engine was auto-spawned, propagate its resolved - // endpoint to the client so `createClient` doesn't fall back - // to its (warning-emitting) default. - const resolvedEndpoint = rivetkitRegistry.parseConfig().endpoint; - const rivetkitClient = yield* Effect.acquireRelease( - Effect.sync(() => - RivetkitClient.createClient({ - ...registry.options, - endpoint: - registry.options.endpoint ?? resolvedEndpoint, - }), - ), - (c) => Effect.promise(() => c.dispose()), - ); - - const callAction: ClientService["callAction"] = ({ - actorName, - key, - actionName, - encodedPayload, - meta, - }) => - Effect.tryPromise({ - try: () => - rivetkitClient[actorName].getOrCreate(key).action({ - name: actionName, - args: meta - ? [encodedPayload, meta] - : [encodedPayload], - }), - catch: (cause) => - cause instanceof Rivetkit.RivetError - ? cause - : new Rivetkit.RivetError( - "client", - "unknown", - cause instanceof Error - ? cause.message - : String(cause), - { - cause: - cause instanceof Error - ? cause - : undefined, - }, - ), - }); - - return Client.of({ callAction }); - }), - ); -} - -type ActorInstance = { - readonly handlers: Record< - string, - (req: { - readonly _tag: string; - readonly action: Action.AnyWithProps; - readonly payload: unknown; - }) => Effect.Effect - >; - readonly scope: Scope.Closeable; -}; - -const toRivetkitActor = Effect.fnUntraced(function* ( - entry: RegistryEntry, - instances: Map, -) { - // Snapshot the current Effect context so action callbacks - // (which run in rivetkit's plain Promise world) can run - // handler effects against the same services the Registry.start / - // Registry.test layer was provided with. - const services = yield* Effect.context(); - const actor = entry.actor; - - const actions: Record< - string, - ( - c: Pick, - payload?: unknown, - meta?: unknown, - ) => Promise - > = {}; - for (const action of actor.actions) { - const decodePayload = Schema.decodeUnknownEffect(action.payloadSchema); - const encodeSuccess = Schema.encodeUnknownEffect(action.successSchema); - const encodeError = Schema.encodeUnknownEffect(action.errorSchema); - actions[action._tag] = async (c, payload, meta) => { - const inst = instances.get(c.actorId); - if (!inst) { - throw new Error( - `actor ${actor.name}/${c.actorId} has no handlers (onWake didn't run?)`, - ); - } - const handler = inst.handlers[action._tag]; - if (!handler) { - throw new Error( - `actor ${actor.name} has no handler for action ${action._tag}`, - ); - } - - let pipeline: Effect.Effect = Effect.gen( - function* () { - const decoded = yield* decodePayload(payload).pipe( - Effect.orDie, - ); - const result = yield* handler({ - _tag: action._tag, - action, - payload: decoded, - }).pipe( - Effect.catch((expectedError) => - Effect.gen(function* () { - const error = yield* encodeError( - expectedError, - ).pipe(Effect.orDie); - return yield* Effect.die( - new Rivetkit.UserError( - hasStringProperty("message")(error) - ? error.message - : `${action._tag} failed`, - { - code: hasStringProperty("_tag")( - error, - ) - ? error._tag - : undefined, - metadata: error, - }, - ), - ); - }), - ), - ); - return yield* encodeSuccess(result).pipe(Effect.orDie); - }, - ); - - // Always wrap in a server-side span so the handler has a - // live `currentSpan` even when the caller didn't ship trace - // context (e.g. a non-Effect-SDK client). When trace context - // is present, reattach it as the parent so the server span - // joins the caller's trace. - const rpcMethod = `${actor.name}/${action._tag}`; - const traceMeta = readTraceMeta(meta); - pipeline = pipeline.pipe( - Effect.withSpan(rpcMethod, { - parent: traceMeta - ? Tracer.externalSpan(traceMeta) - : undefined, - kind: "server", - attributes: { - "rpc.system.name": rpcSystem, - "rpc.method": rpcMethod, - }, - }), - ); - - const exit = await Effect.runPromiseExitWith(services)(pipeline); - if (Exit.isSuccess(exit)) return exit.value; - throw Cause.squash(exit.cause); - }; - } - - const actorOptions = entry.options - ? splitActorOptions(entry.options) - : undefined; - const stateDef = actorOptions?.effectOptions.state; - const hasState = actorOptions?.effectOptions.state !== undefined; - - return Rivetkit.actor({ - actions, - options: actorOptions?.rivetkitOptions, - // rivetkit invokes this once at create time and seeds c.state - // with the result. We delegate to the user-supplied `initial` - // factory so primitive states (e.g. `Schema.Number`) don't need - // `Schema.withConstructorDefault` boilerplate. - ...(hasState - ? { - createState: () => stateDef.initial(), - } - : {}), - onWake: async ( - c: Rivetkit.WakeContextOf, - ) => { - const address: ActorAddress = { - actorId: c.actorId, - name: c.name, - key: c.key, - }; - // Single fused effect: build the wake scope, then run - // `buildHandlers` in that scope with `CurrentAddress` - // provided. Keeping both pieces in one fiber means a - // `buildHandlers` failure shares its cause with the scope it - // would have owned. - const acquire = Effect.gen(function* () { - const scope = yield* Scope.make(); - - const stateRef = yield* SubscriptionRef.make( - hasState ? c.state : undefined, - ); - if (hasState) { - // Mirror published changes back to c.state so - // rivetkit's throttled save loop and shutdown flush - // pick them up. The identity guard skips no-op - // updates; otherwise rivetkit re-encodes the value - // as CBOR and reschedules a save on every publish. - yield* SubscriptionRef.changes(stateRef).pipe( - Stream.drop(1), - Stream.runForEach((value) => - Effect.sync(() => { - if (value !== c.state) c.state = value; - }), - ), - Effect.forkIn(scope), - ); - } - - const built = entry.buildHandlers; - let provided = built.pipe( - Effect.provideService(CurrentAddress, address), - Effect.provideService(Scope.Scope, scope), - Effect.provideService( - Sleep, - Effect.sync(() => c.sleep()), - ), - ); - if (hasState) { - // Provide the SubscriptionRef under the user's typed - // `ActorState` tag so `yield* MyState` inside the build - // effect resolves to a `SubscriptionRef`. - provided = Effect.provideService( - provided, - stateDef, - stateRef, - ); - } - return { handlers: yield* provided, scope }; - }); - const { handlers, scope } = - await Effect.runPromiseWith(services)(acquire); - instances.set(c.actorId, { - handlers, - scope, - }); - }, - onSleep: async ( - c: Rivetkit.SleepContextOf, - ) => { - const inst = instances.get(c.actorId); - if (!inst) return; - instances.delete(c.actorId); - await Effect.runPromiseWith(services)( - Scope.close(inst.scope, Exit.void), - ); - }, - }); -}); - -/** - * Build the underlying rivetkit registry from the collected `Registry` - * entries. The returned registry is configured but not started; callers - * apply mode-specific config (test flags, engine spawn) and then invoke - * `.start()` themselves. - */ -const toRivetkitRegistry = Effect.fnUntraced(function* ( - registry: Registry["Service"], -) { - const entries = yield* registry.entries; - const instances = new Map(); - const use: Record = {}; - for (const entry of entries) { - use[entry.actor.name] = yield* toRivetkitActor(entry, instances); - } - - return Rivetkit.setup({ - use, - ...registry.options, - }); -}); - export type ActionRequest = A extends Action.Action< infer Tag, @@ -560,7 +144,7 @@ export interface Actor< RX = never, >( build: Handlers | Effect.Effect, - options?: ActorOptions, + options?: Options, ): Layer.Layer< never, never, @@ -568,7 +152,7 @@ export interface Actor< | HandlerServices | Action.ServicesServer | Action.ServicesClient - | Registry + | Registry.Registry >; /** @@ -582,7 +166,7 @@ export interface Actor< readonly client: Effect.Effect< TypedAccessor, never, - Client | Action.ServicesClient + Client.Client | Action.ServicesClient >; } @@ -604,9 +188,9 @@ const Proto = { >( this: Actor, build: Handlers | Effect.Effect, - options?: ActorOptions, + options?: Options, ) { - return Registry.asEffect().pipe( + return Registry.Registry.asEffect().pipe( Effect.flatMap((registry) => registry.register({ actor: this, @@ -622,7 +206,7 @@ const Proto = { get client() { const self = this as Any; return Effect.gen(function* () { - const client = yield* Client; + const client = yield* Client.Client; const actions = self.actions; return { getOrCreate: (key: ActorKeyParam) => { @@ -653,7 +237,7 @@ const Proto = { action.payloadSchema, )(payload); const span = yield* Effect.currentSpan; - const meta: ActionMeta = { + const meta: Client.ActionMeta = { trace: { traceId: span.traceId, spanId: span.spanId, diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts new file mode 100644 index 0000000000..6fa564cea2 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -0,0 +1,424 @@ +import { + Cause, + Context, + Effect, + Exit, + Layer, + Ref, + Schema, + Scope, + Stream, + SubscriptionRef, + Tracer, +} from "effect"; +import * as Rivetkit from "rivetkit"; +import * as RivetkitClient from "rivetkit/client"; +import type * as Action from "./Action"; +import * as Actor from "./Actor"; +import type * as ActorState from "./ActorState"; +import { Client, type ClientService } from "./Client"; +import { readTraceMeta, rpcSystem } from "./internal/tracing"; +import { hasStringProperty } from "./utils"; + +const TypeId = "~@rivetkit/effect/Registry"; + +/** + * One actor registered with the `Registry`. The `buildHandlers` + * effect is run once per wake by the runner to construct + * per-instance state and handlers; the handlers themselves are not + * resolved at registration time. + * + * `state`, when present, carries the persisted-state schema and + * initial-value factory. The runner uses it to seed `c.state` on + * first create and to provide a typed `SubscriptionRef` under the + * state's tag inside the build effect's context. + */ +interface RegistryEntry< + Name extends string, + Actions extends Action.Any, + Handlers extends Actor.HandlersFrom, + RX, + State extends ActorState.AnyWithProps = never, +> { + readonly actor: Actor.Actor; + readonly buildHandlers: Effect.Effect; + readonly options?: Actor.Options; +} + +type ActorInstance = { + readonly handlers: Record< + string, + (req: { + readonly _tag: string; + readonly action: Action.AnyWithProps; + readonly payload: unknown; + }) => Effect.Effect + >; + readonly scope: Scope.Closeable; +}; + +export interface Registry { + readonly [TypeId]: typeof TypeId; + + readonly options: Options; + + readonly register: < + Name extends string, + Actions extends Action.Any, + Handlers extends Actor.HandlersFrom, + RX, + State extends ActorState.AnyWithProps = never, + >( + entry: RegistryEntry, + ) => Effect.Effect; + + readonly entries: Effect.Effect< + ReadonlyArray> + >; +} + +/** + * Service collecting actor defs/builders together with the engine + * connection config. Provided once via `Registry.layer({ ... })` and + * consumed by both `Actor.toLayer` (which registers itself into the + * collector on acquire) and by `Registry.start` / `Registry.test` + * (which materialize the underlying rivetkit registry from the + * collected entries). + */ +export const Registry: Context.Service = + Context.Service("@rivetkit/effect/Registry"); + +export type Options = Pick< + Rivetkit.RegistryConfigInput, + "endpoint" | "token" | "namespace" +>; + +export const make = Effect.fnUntraced(function* ( + options: Options = {}, +): Effect.fn.Return { + const ref = yield* Ref.make< + ReadonlyArray> + >([]); + return Registry.of({ + [TypeId]: TypeId, + options, + register: (entry) => Ref.update(ref, (xs) => [...xs, entry]), + entries: Ref.get(ref), + }); +}); + +export const layer = (options: Options = {}): Layer.Layer => + Layer.effect(Registry, make(options)); + +/** + * Run the registered actors against the configured engine. Reads + * the collected entries, materializes the underlying rivetkit + * registry, and starts it. + */ +export const serve: Layer.Layer = Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* Registry; + const rivetkitRegistry = yield* toRivetkitRegistry(registry); + yield* Effect.sync(() => rivetkitRegistry.start()); + }), +); + +/** + * In-process test runtime. Boots the rivetkit registry against the + * configured engine, waits for `/health` to answer, and provides + * `Client` from the same Layer so consumers don't need to wire + * `Client.layer` separately. Mirrors `Registry.start` plus test-mode + * flags and a scoped client dispose. The registry itself is leaked + * to process exit because the public rivetkit `Registry` doesn't + * expose a public `shutdown()` today; only the SIGINT handler can + * drive `#runShutdown`. This matches `setupTest`'s existing behavior. + */ +export const test: Layer.Layer = Layer.effect( + Client, + Effect.gen(function* () { + const registry = yield* Registry; + const rivetkitRegistry = yield* toRivetkitRegistry(registry); + rivetkitRegistry.config.test = { + ...rivetkitRegistry.config.test, + enabled: true, + }; + rivetkitRegistry.config.noWelcome = true; + // Auto-spawn the engine when no endpoint was provided, so + // `Registry.test` works out of the box without requiring the + // caller to start an engine externally. If the user wired an + // explicit endpoint via `Registry.layer({ endpoint: ... })`, + // honor it and skip the local spawn. + if (registry.options.endpoint === undefined) { + rivetkitRegistry.config.startEngine = true; + } + yield* Effect.sync(() => rivetkitRegistry.start()); + + // The rivetkitRegistry itself is leaked until process exit (matches + // setupTest's behavior). The public Rivetkit.Registry doesn't + // expose a shutdown method; only the SIGINT handler can drive the + // inner .shutdown(). Disposing the client is the only cleanup we + // can do cleanly today. + // + // When the engine was auto-spawned, propagate its resolved + // endpoint to the client so `createClient` doesn't fall back + // to its (warning-emitting) default. + const resolvedEndpoint = rivetkitRegistry.parseConfig().endpoint; + const rivetkitClient = yield* Effect.acquireRelease( + Effect.sync(() => + RivetkitClient.createClient({ + ...registry.options, + endpoint: registry.options.endpoint ?? resolvedEndpoint, + }), + ), + (c) => Effect.promise(() => c.dispose()), + ); + + const callAction: ClientService["callAction"] = ({ + actorName, + key, + actionName, + encodedPayload, + meta, + }) => + Effect.tryPromise({ + try: () => + rivetkitClient[actorName].getOrCreate(key).action({ + name: actionName, + args: meta ? [encodedPayload, meta] : [encodedPayload], + }), + catch: (cause) => + cause instanceof Rivetkit.RivetError + ? cause + : new Rivetkit.RivetError( + "client", + "unknown", + cause instanceof Error + ? cause.message + : String(cause), + { + cause: + cause instanceof Error + ? cause + : undefined, + }, + ), + }); + + return Client.of({ callAction }); + }), +); + +/** + * Build the underlying rivetkit registry from the collected `Registry` + * entries. The returned registry is configured but not started; callers + * apply mode-specific config (test flags, engine spawn) and then invoke + * `.start()` themselves. + */ +const toRivetkitRegistry = Effect.fnUntraced(function* (registry: Registry) { + const entries = yield* registry.entries; + const instances = new Map(); + const use: Record = {}; + for (const entry of entries) { + use[entry.actor.name] = yield* toRivetkitActor(entry, instances); + } + + return Rivetkit.setup({ + use, + ...registry.options, + }); +}); + +const toRivetkitActor = Effect.fnUntraced(function* ( + entry: RegistryEntry, + instances: Map, +) { + // Snapshot the current Effect context so action callbacks + // (which run in rivetkit's plain Promise world) can run + // handler effects against the same services the Registry.start / + // Registry.test layer was provided with. + const services = yield* Effect.context(); + const actor = entry.actor; + + const actions: Record< + string, + ( + c: Pick, + payload?: unknown, + meta?: unknown, + ) => Promise + > = {}; + for (const action of actor.actions) { + const decodePayload = Schema.decodeUnknownEffect(action.payloadSchema); + const encodeSuccess = Schema.encodeUnknownEffect(action.successSchema); + const encodeError = Schema.encodeUnknownEffect(action.errorSchema); + actions[action._tag] = async (c, payload, meta) => { + const inst = instances.get(c.actorId); + if (!inst) { + throw new Error( + `actor ${actor.name}/${c.actorId} has no handlers (onWake didn't run?)`, + ); + } + const handler = inst.handlers[action._tag]; + if (!handler) { + throw new Error( + `actor ${actor.name} has no handler for action ${action._tag}`, + ); + } + + let pipeline: Effect.Effect = Effect.gen( + function* () { + const decoded = yield* decodePayload(payload).pipe( + Effect.orDie, + ); + const result = yield* handler({ + _tag: action._tag, + action, + payload: decoded, + }).pipe( + Effect.catch((expectedError) => + Effect.gen(function* () { + const error = yield* encodeError( + expectedError, + ).pipe(Effect.orDie); + return yield* Effect.die( + new Rivetkit.UserError( + hasStringProperty("message")(error) + ? error.message + : `${action._tag} failed`, + { + code: hasStringProperty("_tag")( + error, + ) + ? error._tag + : undefined, + metadata: error, + }, + ), + ); + }), + ), + ); + return yield* encodeSuccess(result).pipe(Effect.orDie); + }, + ); + + // Always wrap in a server-side span so the handler has a + // live `currentSpan` even when the caller didn't ship trace + // context (e.g. a non-Effect-SDK client). When trace context + // is present, reattach it as the parent so the server span + // joins the caller's trace. + const rpcMethod = `${actor.name}/${action._tag}`; + const traceMeta = readTraceMeta(meta); + pipeline = pipeline.pipe( + Effect.withSpan(rpcMethod, { + parent: traceMeta + ? Tracer.externalSpan(traceMeta) + : undefined, + kind: "server", + attributes: { + "rpc.system.name": rpcSystem, + "rpc.method": rpcMethod, + }, + }), + ); + + const exit = await Effect.runPromiseExitWith(services)(pipeline); + if (Exit.isSuccess(exit)) return exit.value; + throw Cause.squash(exit.cause); + }; + } + + const actorOptions = entry.options + ? Actor.splitOptions(entry.options) + : undefined; + const stateDef = actorOptions?.effectOptions.state; + const hasState = actorOptions?.effectOptions.state !== undefined; + + return Rivetkit.actor({ + actions, + options: actorOptions?.rivetkitOptions, + // rivetkit invokes this once at create time and seeds c.state + // with the result. We delegate to the user-supplied `initial` + // factory so primitive states (e.g. `Schema.Number`) don't need + // `Schema.withConstructorDefault` boilerplate. + ...(hasState + ? { + createState: () => stateDef.initial(), + } + : {}), + onWake: async ( + c: Rivetkit.WakeContextOf, + ) => { + const address: Actor.ActorAddress = { + actorId: c.actorId, + name: c.name, + key: c.key, + }; + // Single fused effect: build the wake scope, then run + // `buildHandlers` in that scope with `CurrentAddress` + // provided. Keeping both pieces in one fiber means a + // `buildHandlers` failure shares its cause with the scope it + // would have owned. + const acquire = Effect.gen(function* () { + const scope = yield* Scope.make(); + + const stateRef = yield* SubscriptionRef.make( + hasState ? c.state : undefined, + ); + if (hasState) { + // Mirror published changes back to c.state so + // rivetkit's throttled save loop and shutdown flush + // pick them up. The identity guard skips no-op + // updates; otherwise rivetkit re-encodes the value + // as CBOR and reschedules a save on every publish. + yield* SubscriptionRef.changes(stateRef).pipe( + Stream.drop(1), + Stream.runForEach((value) => + Effect.sync(() => { + if (value !== c.state) c.state = value; + }), + ), + Effect.forkIn(scope), + ); + } + + const built = entry.buildHandlers; + let provided = built.pipe( + Effect.provideService(Actor.CurrentAddress, address), + Effect.provideService(Scope.Scope, scope), + Effect.provideService( + Actor.Sleep, + Effect.sync(() => c.sleep()), + ), + ); + if (hasState) { + // Provide the SubscriptionRef under the user's typed + // `ActorState` tag so `yield* MyState` inside the build + // effect resolves to a `SubscriptionRef`. + provided = Effect.provideService( + provided, + stateDef, + stateRef, + ); + } + return { handlers: yield* provided, scope }; + }); + const { handlers, scope } = + await Effect.runPromiseWith(services)(acquire); + instances.set(c.actorId, { + handlers, + scope, + }); + }, + onSleep: async ( + c: Rivetkit.SleepContextOf, + ) => { + const inst = instances.get(c.actorId); + if (!inst) return; + instances.delete(c.actorId); + await Effect.runPromiseWith(services)( + Scope.close(inst.scope, Exit.void), + ); + }, + }); +}); diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index 4155624ef8..e88616a2c2 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,6 +1,6 @@ export * as Action from "./Action"; export * as Actor from "./Actor"; -export { Registry } from "./Actor"; +export * as Registry from "./Registry"; export * as ActorState from "./ActorState"; export { Client } from "./Client"; export * as RivetError from "./RivetError"; From e5a3b63b2942d59581e2e888a7bc2d0b37d672d7 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 8 May 2026 19:00:51 +0200 Subject: [PATCH 132/306] refactor(effect): tighten type definitions in `Actor` --- rivetkit-typescript/packages/effect/src/Actor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index e72a17a37a..c3625d9041 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -178,7 +178,7 @@ export type HandlersFrom = { ) => Action.ResultFrom; }; -const Proto = { +const Proto: Omit, "name" | "actions"> = { [TypeId]: TypeId, toLayer< Actions extends Action.Any, From a254a79e543effe34faed789cd55c6145622c72e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 8 May 2026 19:01:12 +0200 Subject: [PATCH 133/306] refactor(effect): fix formatting --- rivetkit-typescript/packages/effect/src/Actor.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index c3625d9041..a23382a834 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -8,7 +8,7 @@ import { Scope, } from "effect"; import * as Rivetkit from "rivetkit"; -import * as Registry from './Registry'; +import * as Registry from "./Registry"; import type * as Action from "./Action"; import type * as ActorState from "./ActorState"; import * as Client from "./Client"; @@ -39,10 +39,7 @@ export const splitOptions = ( options: Options, ): { readonly rivetkitOptions: RivetkitActorOptions; - readonly effectOptions: Omit< - Options, - keyof RivetkitActorOptions - >; + readonly effectOptions: Omit, keyof RivetkitActorOptions>; } => { const { state, ...rivetkitOptions } = options; return { rivetkitOptions, effectOptions: { state } }; @@ -198,10 +195,10 @@ const Proto: Omit, "name" | "actions"> = { ? build : Effect.succeed(build), options, - }) + }), ), - Layer.effectDiscard - ) + Layer.effectDiscard, + ); }, get client() { const self = this as Any; From 6ff75a691aa2c41f946df291fe076b2cf3ae4484 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 10 May 2026 19:45:15 +0200 Subject: [PATCH 134/306] refactor(effect): drop Ref from Registry entries collector --- .../packages/effect/src/Registry.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 6fa564cea2..2c4acf9557 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -4,7 +4,6 @@ import { Effect, Exit, Layer, - Ref, Schema, Scope, Stream, @@ -93,22 +92,21 @@ export type Options = Pick< "endpoint" | "token" | "namespace" >; -export const make = Effect.fnUntraced(function* ( - options: Options = {}, -): Effect.fn.Return { - const ref = yield* Ref.make< - ReadonlyArray> - >([]); +export const make = (options: Options = {}): Registry => { + const entries: Array> = []; return Registry.of({ [TypeId]: TypeId, options, - register: (entry) => Ref.update(ref, (xs) => [...xs, entry]), - entries: Ref.get(ref), + register: (entry) => + Effect.sync(() => { + entries.push(entry); + }), + entries: Effect.sync(() => entries), }); -}); +}; export const layer = (options: Options = {}): Layer.Layer => - Layer.effect(Registry, make(options)); + Layer.succeed(Registry, make(options)); /** * Run the registered actors against the configured engine. Reads From fbfc88308e21317d882e6773582191a3a3fbc432 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 11 May 2026 09:10:49 +0200 Subject: [PATCH 135/306] refactor(effect): improve test comments for clarity and readability --- .../packages/effect/test/e2e.test.ts | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 87832092de..0fa492075d 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -67,8 +67,8 @@ layer(TestLayer)("end-to-end", (it) => { ]); // Bump the in-memory `Ref` so we can later assert that - // the wake actually rebuilt the actor (the ref resets - // to 0 on each wake). + // the wake actually rebuilt the actor (the ref should + // reset to 0 on each wake). yield* counter.Increment({ amount: 7 }); const beforeSleep = yield* counter.PersistAndSleep({ @@ -76,14 +76,12 @@ layer(TestLayer)("end-to-end", (it) => { }); assert.strictEqual(beforeSleep, 11); - // Engine-side sleep teardown is asynchronous and the SDK - // exposes no "actor slept" hook today, so poll the - // post-condition. `count` is `Ref.make(0)` per wake, so - // seeing 0 is the deterministic signal that the prior - // wake torn down and a fresh one started. `TestClock.withLive` - // swaps in the real Clock for the duration of the poll so - // the schedule's interval and the timeout both elapse in - // wall time (the suite otherwise runs under TestClock). + // Engine-side sleep teardown is asynchronous. `count` + // is `Ref.make(0)` per wake, so seeing 0 is the deterministic + // signal that the prior wake torn down and a fresh one started. + // `TestClock.withLive` swaps in the real Clock for the duration + // of the poll so the schedule's interval and the timeout both + // elapse in wall time (the suite otherwise runs under TestClock). const inMemoryAfterWake = yield* counter.GetCount().pipe( Effect.repeat({ until: (n) => n === 0, @@ -94,10 +92,6 @@ layer(TestLayer)("end-to-end", (it) => { ); assert.strictEqual(inMemoryAfterWake, 0); - // `+ 0` is a no-op increment whose return value is the - // freshly-loaded persisted total. Anything other than - // 11 means either the write didn't durably land before - // sleep or the load on wake didn't seed the ref. const persistedAfterWake = yield* counter.PersistedTotal({ amount: 0, }); @@ -331,7 +325,10 @@ layer(TestLayer)("end-to-end", (it) => { const parent = onTrace[i].parent; assert.strictEqual(parent._tag, "Some"); if (parent._tag === "Some") { - assert.strictEqual(parent.value.spanId, onTrace[i - 1].spanId); + assert.strictEqual( + parent.value.spanId, + onTrace[i - 1].spanId, + ); } } }), From c51cab2f42cbf155ecaf383526c0aa8f3f847f52 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 11 May 2026 09:11:42 +0200 Subject: [PATCH 136/306] test(effect): re-order "isolates in-wake state across keys" test --- .../packages/effect/test/e2e.test.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 0fa492075d..81056a898f 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -60,6 +60,19 @@ layer(TestLayer)("end-to-end", (it) => { }), ); + it.effect("isolates in-wake state across keys", () => + Effect.gen(function* () { + const client = yield* Counter.client; + const a = client.getOrCreate(["t-iso-a"]); + const b = client.getOrCreate(["t-iso-b"]); + yield* a.Increment({ amount: 2 }); + yield* a.Increment({ amount: 3 }); + yield* b.Increment({ amount: 1 }); + assert.strictEqual(yield* a.GetCount(), 5); + assert.strictEqual(yield* b.GetCount(), 1); + }), + ); + it.effect("persists state across a sleep/wake cycle", () => Effect.gen(function* () { const counter = (yield* Counter.client).getOrCreate([ @@ -99,19 +112,6 @@ layer(TestLayer)("end-to-end", (it) => { }), ); - it.effect("isolates in-wake state across keys", () => - Effect.gen(function* () { - const client = yield* Counter.client; - const a = client.getOrCreate(["t-iso-a"]); - const b = client.getOrCreate(["t-iso-b"]); - yield* a.Increment({ amount: 2 }); - yield* a.Increment({ amount: 3 }); - yield* b.Increment({ amount: 1 }); - assert.strictEqual(yield* a.GetCount(), 5); - assert.strictEqual(yield* b.GetCount(), 1); - }), - ); - it.effect( "surfaces an expected handler error back into the original error", () => From 3a4606e577571abd6d21ff4dc588a690cf4554de Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 11 May 2026 10:50:03 +0200 Subject: [PATCH 137/306] feat(effect): encode/decode actor's state --- .../packages/effect/src/Registry.ts | 35 +++++++++------ .../packages/effect/test/e2e.test.ts | 45 +++++++++++++++++-- .../packages/effect/test/fixtures/actor.ts | 43 +++++++++++++----- 3 files changed, 94 insertions(+), 29 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 2c4acf9557..824276056a 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -4,6 +4,7 @@ import { Effect, Exit, Layer, + Option, Schema, Scope, Stream, @@ -330,7 +331,12 @@ const toRivetkitActor = Effect.fnUntraced(function* ( ? Actor.splitOptions(entry.options) : undefined; const stateDef = actorOptions?.effectOptions.state; - const hasState = actorOptions?.effectOptions.state !== undefined; + const stateDefOption = Option.fromNullishOr(stateDef); + const stateInitialValue = Option.isSome(stateDefOption) + ? yield* Schema.encodeUnknownEffect(stateDef.schema)( + stateDef.initial(), + ).pipe(Effect.orDie) + : undefined; return Rivetkit.actor({ actions, @@ -339,9 +345,9 @@ const toRivetkitActor = Effect.fnUntraced(function* ( // with the result. We delegate to the user-supplied `initial` // factory so primitive states (e.g. `Schema.Number`) don't need // `Schema.withConstructorDefault` boilerplate. - ...(hasState + ...(Option.isSome(stateDefOption) ? { - createState: () => stateDef.initial(), + createState: () => stateInitialValue, } : {}), onWake: async ( @@ -361,19 +367,20 @@ const toRivetkitActor = Effect.fnUntraced(function* ( const scope = yield* Scope.make(); const stateRef = yield* SubscriptionRef.make( - hasState ? c.state : undefined, + Option.isSome(stateDefOption) + ? yield* Schema.decodeUnknownEffect(stateDef.schema)( + c.state, + ).pipe(Effect.orDie) + : undefined, ); - if (hasState) { - // Mirror published changes back to c.state so - // rivetkit's throttled save loop and shutdown flush - // pick them up. The identity guard skips no-op - // updates; otherwise rivetkit re-encodes the value - // as CBOR and reschedules a save on every publish. + if (Option.isSome(stateDefOption)) { yield* SubscriptionRef.changes(stateRef).pipe( Stream.drop(1), - Stream.runForEach((value) => - Effect.sync(() => { - if (value !== c.state) c.state = value; + Stream.runForEach((decodedState) => + Effect.gen(function* () { + c.state = yield* Schema.encodeUnknownEffect( + stateDef.schema, + )(decodedState).pipe(Effect.orDie); }), ), Effect.forkIn(scope), @@ -389,7 +396,7 @@ const toRivetkitActor = Effect.fnUntraced(function* ( Effect.sync(() => c.sleep()), ), ); - if (hasState) { + if (Option.isSome(stateDefOption)) { // Provide the SubscriptionRef under the user's typed // `ActorState` tag so `yield* MyState` inside the build // effect resolves to a `SubscriptionRef`. diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 81056a898f..3263f597ad 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -100,15 +100,52 @@ layer(TestLayer)("end-to-end", (it) => { until: (n) => n === 0, schedule: Schedule.spaced("100 millis"), }), - Effect.timeout("10 seconds"), TestClock.withLive, ); assert.strictEqual(inMemoryAfterWake, 0); - const persistedAfterWake = yield* counter.PersistedTotal({ - amount: 0, + const persistedAfterWake = yield* counter.GetPersistedState(); + assert.strictEqual(persistedAfterWake.count, 11); + }), + ); + + it.effect("persists state with a non-trivial schema (Date)", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate([ + "t-persist-state-date", + ]); + + // Bump the in-memory `Ref` so we can later assert that + // the wake actually rebuilt the actor (the ref should + // reset to 0 on each wake). + yield* counter.Increment({ amount: 7 }); + + const when = new Date("2024-01-15T10:30:00.000Z"); + const beforeSleep = yield* counter.PersistDateAndSleep({ + when, }); - assert.strictEqual(persistedAfterWake, 11); + assert.strictEqual(beforeSleep.toISOString(), when.toISOString()); + + // Engine-side sleep teardown is asynchronous. `count` + // is `Ref.make(0)` per wake, so seeing 0 is the deterministic + // signal that the prior wake torn down and a fresh one started. + // `TestClock.withLive` swaps in the real Clock for the duration + // of the poll so the schedule's interval and the timeout both + // elapse in wall time (the suite otherwise runs under TestClock). + const inMemoryAfterWake = yield* counter.GetCount().pipe( + Effect.repeat({ + until: (n) => n === 0, + schedule: Schedule.spaced("100 millis"), + }), + TestClock.withLive, + ); + assert.strictEqual(inMemoryAfterWake, 0); + + const persistedAfterWake = yield* counter.GetPersistedState(); + assert.strictEqual( + persistedAfterWake.when.toISOString(), + when.toISOString(), + ); }), ); diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts index 7fb9bf4ad6..38262e2f5c 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts @@ -129,14 +129,21 @@ export const Scale = Action.make("Scale", { error: ScaledOverflowError, }); -export const PersistedTotal = Action.make("PersistedTotal", { +export const PersistAndSleep = Action.make("PersistAndSleep", { payload: { amount: Schema.Number }, success: Schema.Number, }); -export const PersistAndSleep = Action.make("PersistAndSleep", { - payload: { amount: Schema.Number }, - success: Schema.Number, +export const PersistDateAndSleep = Action.make("PersistDateAndSleep", { + payload: { when: Schema.DateFromString }, + success: Schema.Date, +}); + +export const GetPersistedState = Action.make("GetPersistedState", { + success: Schema.Struct({ + count: Schema.Number, + when: Schema.DateFromString, + }), }); export const Counter = Actor.make("Counter", { @@ -150,16 +157,18 @@ export const Counter = Actor.make("Counter", { WakeGreeting, Compute, Scale, - PersistedTotal, PersistAndSleep, + PersistDateAndSleep, + GetPersistedState, ], }); const CounterState = ActorState.make("CounterState", { schema: Schema.Struct({ count: Schema.Number, + when: Schema.DateFromString, }), - initial: () => ({ count: 0 }), + initial: () => ({ count: 0, when: new Date() }), }); export const CounterLive = Counter.toLayer( @@ -222,19 +231,31 @@ export const CounterLive = Counter.toLayer( // and payload codec sites firing on both sides. return payload.amount + 100; }), - PersistedTotal: ({ payload }) => - SubscriptionRef.updateAndGet(state, (s) => ({ - count: s.count + payload.amount, - })).pipe(Effect.map((s) => s.count)), PersistAndSleep: ({ payload }) => Effect.gen(function* () { const { count } = yield* SubscriptionRef.updateAndGet( state, - (s) => ({ count: s.count + payload.amount }), + (s) => ({ + ...s, + count: s.count + payload.amount, + }), ); yield* sleep; return count; }), + PersistDateAndSleep: ({ payload }) => + Effect.gen(function* () { + const { when } = yield* SubscriptionRef.updateAndGet( + state, + (s) => ({ + ...s, + when: payload.when, + }), + ); + yield* sleep; + return when; + }), + GetPersistedState: () => SubscriptionRef.get(state), }); }), { state: CounterState }, From e51e5a4ab2763703f2106092f0db2f68a8211eca Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 11 May 2026 10:56:15 +0200 Subject: [PATCH 138/306] test(effect): update test key for persisting state across sleep/wake cycle --- rivetkit-typescript/packages/effect/test/e2e.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 3263f597ad..721fbbbe75 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -76,7 +76,7 @@ layer(TestLayer)("end-to-end", (it) => { it.effect("persists state across a sleep/wake cycle", () => Effect.gen(function* () { const counter = (yield* Counter.client).getOrCreate([ - "t-sleep-wake", + "t-persist-state", ]); // Bump the in-memory `Ref` so we can later assert that From 0f11871ef3c47069eb5c60e2a409273704dec922 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 11 May 2026 11:02:49 +0200 Subject: [PATCH 139/306] test(effect): persists state with a custom Schema.transform --- .../packages/effect/test/e2e.test.ts | 37 +++++++++++++++++++ .../packages/effect/test/fixtures/actor.ts | 22 ++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 721fbbbe75..c32246b296 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -149,6 +149,43 @@ layer(TestLayer)("end-to-end", (it) => { }), ); + it.effect("persists state with a custom Schema.transform", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate([ + "t-persist-state-transform", + ]); + + // Bump the in-memory `Ref` so we can later assert that + // the wake actually rebuilt the actor (the ref should + // reset to 0 on each wake). + yield* counter.Increment({ amount: 7 }); + + const tags = ["alpha", "beta", "gamma"]; + const beforeSleep = yield* counter.PersistTagsAndSleep({ + tags, + }); + assert.deepEqual(beforeSleep, tags); + + // Engine-side sleep teardown is asynchronous. `count` + // is `Ref.make(0)` per wake, so seeing 0 is the deterministic + // signal that the prior wake torn down and a fresh one started. + // `TestClock.withLive` swaps in the real Clock for the duration + // of the poll so the schedule's interval and the timeout both + // elapse in wall time (the suite otherwise runs under TestClock). + const inMemoryAfterWake = yield* counter.GetCount().pipe( + Effect.repeat({ + until: (n) => n === 0, + schedule: Schedule.spaced("100 millis"), + }), + TestClock.withLive, + ); + assert.strictEqual(inMemoryAfterWake, 0); + + const persistedAfterWake = yield* counter.GetPersistedState(); + assert.deepEqual(persistedAfterWake.tags, tags); + }), + ); + it.effect( "surfaces an expected handler error back into the original error", () => diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts index 38262e2f5c..23687ec881 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts @@ -139,10 +139,16 @@ export const PersistDateAndSleep = Action.make("PersistDateAndSleep", { success: Schema.Date, }); +export const PersistTagsAndSleep = Action.make("PersistTagsAndSleep", { + payload: { tags: TagsCsv }, + success: TagsCsv, +}); + export const GetPersistedState = Action.make("GetPersistedState", { success: Schema.Struct({ count: Schema.Number, when: Schema.DateFromString, + tags: TagsCsv, }), }); @@ -159,6 +165,7 @@ export const Counter = Actor.make("Counter", { Scale, PersistAndSleep, PersistDateAndSleep, + PersistTagsAndSleep, GetPersistedState, ], }); @@ -167,8 +174,9 @@ const CounterState = ActorState.make("CounterState", { schema: Schema.Struct({ count: Schema.Number, when: Schema.DateFromString, + tags: TagsCsv, }), - initial: () => ({ count: 0, when: new Date() }), + initial: () => ({ count: 0, when: new Date(), tags: ["default"] }), }); export const CounterLive = Counter.toLayer( @@ -255,6 +263,18 @@ export const CounterLive = Counter.toLayer( yield* sleep; return when; }), + PersistTagsAndSleep: ({ payload }) => + Effect.gen(function* () { + const { tags } = yield* SubscriptionRef.updateAndGet( + state, + (s) => ({ + ...s, + tags: payload.tags, + }), + ); + yield* sleep; + return tags; + }), GetPersistedState: () => SubscriptionRef.get(state), }); }), From 8854698bcf6cba410122fef92401734f1ac5273c Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 11 May 2026 19:07:30 +0200 Subject: [PATCH 140/306] refactor(effect): replace SubscriptionRef state with State module Introduces a dedicated `State` abstraction (typed view + change PubSub over actor-persisted state) and rewires the Registry to seed it from `c.state`, persist encodes through its write closure, and republish external changes via the new `onStateChange` callback. Renames `ActorState.initial` to `initialValue` and drops the unused `GuardedRef` scratch module. --- .../packages/effect/src/ActorState.ts | 40 ++-- .../packages/effect/src/Registry.ts | 139 ++++++----- .../packages/effect/src/State.test.ts | 151 ++++++++++++ .../packages/effect/src/State.ts | 219 ++++++++++++++++++ .../packages/effect/src/mod.ts | 3 +- .../packages/effect/test/e2e.test.ts | 4 +- .../packages/effect/test/fixtures/actor.ts | 48 ++-- 7 files changed, 490 insertions(+), 114 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/src/State.test.ts create mode 100644 rivetkit-typescript/packages/effect/src/State.ts diff --git a/rivetkit-typescript/packages/effect/src/ActorState.ts b/rivetkit-typescript/packages/effect/src/ActorState.ts index 8df45cfa9b..d10dd3c2c5 100644 --- a/rivetkit-typescript/packages/effect/src/ActorState.ts +++ b/rivetkit-typescript/packages/effect/src/ActorState.ts @@ -1,11 +1,12 @@ -import { Context, type Schema, type SubscriptionRef } from "effect"; +import { Context, type Schema } from "effect"; +import type * as State from "./State"; const TypeId = "~@rivetkit/effect/ActorState"; /** * A typed, persistent state slot for one Rivet Actor. Yielded inside - * the wake-scope build effect to obtain a `SubscriptionRef` whose - * published changes are mirrored back to rivetkit's persisted state. + * the wake-scope build effect to obtain a `State` whose committed + * changes are mirrored back to rivetkit's persisted state. * * State configuration (`schema` + `initial`) is server-only — it * describes the persisted shape and lives in implementation modules @@ -14,14 +15,11 @@ const TypeId = "~@rivetkit/effect/ActorState"; export interface ActorState< in out Name extends string, in out S extends Schema.Top, -> extends Context.Service< - ActorState, - SubscriptionRef.SubscriptionRef - > { +> extends Context.Service, State.State> { readonly [TypeId]: typeof TypeId; readonly _tag: Name; readonly schema: S; - readonly initial: () => S["Type"]; + readonly initialValue: () => S["Type"]; } /** @@ -33,16 +31,15 @@ export interface Any { } /** - * Like `Any`, but with the prop fields (`schema`, `initial`) accessible. - * Used by the runtime to seed `c.state` and provide the - * `SubscriptionRef` under the state's tag. + * Like `Any`, but with the prop fields (`schema`, `initialValue`) accessible. + * Used by the runtime to seed `c.state` and provide the `State` under + * the state's tag. */ -export interface AnyWithProps - extends Context.Service> { +export interface AnyWithProps extends Context.Service> { readonly [TypeId]: typeof TypeId; readonly _tag: string; readonly schema: Schema.Top; - readonly initial: () => unknown; + readonly initialValue: () => unknown; } export const isActorState = (u: unknown): u is Any => @@ -51,7 +48,7 @@ export const isActorState = (u: unknown): u is Any => /** * Define a typed, persistent state slot for a Rivet Actor. * - * `schema` is the persisted shape; `initial` produces the value used to + * `schema` is the persisted shape; `initialValue` produces the value used to * seed state on first wake. The returned value is itself a Context tag: * `yield* MyState` inside the wake-scope build effect resolves to a * `SubscriptionRef`. @@ -63,21 +60,20 @@ export const isActorState = (u: unknown): u is Any => * * const CounterState = ActorState.make("CounterState", { * schema: Schema.Number, - * initial: () => 0, + * initialValue: () => 0, * }) * ``` */ export const make = ( name: Name, - options: { readonly schema: S; readonly initial: () => S["Type"] }, + options: { readonly schema: S; readonly initialValue: () => S["Type"] }, ): ActorState => { - const tag = Context.Service< - ActorState, - SubscriptionRef.SubscriptionRef - >(`@rivetkit/effect/ActorState/${name}`) as ActorState; + const tag = Context.Service, State.State>( + `@rivetkit/effect/ActorState/${name}`, + ) as ActorState; (tag as any)[TypeId] = TypeId; (tag as any)._tag = name; (tag as any).schema = options.schema; - (tag as any).initial = options.initial; + (tag as any).initialValue = options.initialValue; return tag; }; diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 824276056a..07f4287b69 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -7,8 +7,7 @@ import { Option, Schema, Scope, - Stream, - SubscriptionRef, + Semaphore, Tracer, } from "effect"; import * as Rivetkit from "rivetkit"; @@ -18,6 +17,7 @@ import * as Actor from "./Actor"; import type * as ActorState from "./ActorState"; import { Client, type ClientService } from "./Client"; import { readTraceMeta, rpcSystem } from "./internal/tracing"; +import * as State from "./State"; import { hasStringProperty } from "./utils"; const TypeId = "~@rivetkit/effect/Registry"; @@ -30,8 +30,8 @@ const TypeId = "~@rivetkit/effect/Registry"; * * `state`, when present, carries the persisted-state schema and * initial-value factory. The runner uses it to seed `c.state` on - * first create and to provide a typed `SubscriptionRef` under the - * state's tag inside the build effect's context. + * first create and to provide a typed `State` under the state's tag + * inside the build effect's context. */ interface RegistryEntry< Name extends string, @@ -55,6 +55,7 @@ type ActorInstance = { }) => Effect.Effect >; readonly scope: Scope.Closeable; + readonly state: Option.Option>; }; export interface Registry { @@ -228,7 +229,7 @@ const toRivetkitRegistry = Effect.fnUntraced(function* (registry: Registry) { }); const toRivetkitActor = Effect.fnUntraced(function* ( - entry: RegistryEntry, + { actor, buildHandlers, options }: RegistryEntry, instances: Map, ) { // Snapshot the current Effect context so action callbacks @@ -236,7 +237,6 @@ const toRivetkitActor = Effect.fnUntraced(function* ( // handler effects against the same services the Registry.start / // Registry.test layer was provided with. const services = yield* Effect.context(); - const actor = entry.actor; const actions: Record< string, @@ -327,14 +327,12 @@ const toRivetkitActor = Effect.fnUntraced(function* ( }; } - const actorOptions = entry.options - ? Actor.splitOptions(entry.options) - : undefined; + const actorOptions = options ? Actor.splitOptions(options) : undefined; const stateDef = actorOptions?.effectOptions.state; const stateDefOption = Option.fromNullishOr(stateDef); const stateInitialValue = Option.isSome(stateDefOption) ? yield* Schema.encodeUnknownEffect(stateDef.schema)( - stateDef.initial(), + stateDef.initialValue(), ).pipe(Effect.orDie) : undefined; @@ -342,7 +340,7 @@ const toRivetkitActor = Effect.fnUntraced(function* ( actions, options: actorOptions?.rivetkitOptions, // rivetkit invokes this once at create time and seeds c.state - // with the result. We delegate to the user-supplied `initial` + // with the result. We delegate to the user-supplied `initialValue` // factory so primitive states (e.g. `Schema.Number`) don't need // `Schema.withConstructorDefault` boilerplate. ...(Option.isSome(stateDefOption) @@ -363,57 +361,63 @@ const toRivetkitActor = Effect.fnUntraced(function* ( // provided. Keeping both pieces in one fiber means a // `buildHandlers` failure shares its cause with the scope it // would have owned. - const acquire = Effect.gen(function* () { - const scope = yield* Scope.make(); + const { handlers, scope, state } = await Effect.runPromiseWith( + services, + )( + Effect.gen(function* () { + const scope = yield* Scope.make(); - const stateRef = yield* SubscriptionRef.make( - Option.isSome(stateDefOption) - ? yield* Schema.decodeUnknownEffect(stateDef.schema)( - c.state, - ).pipe(Effect.orDie) - : undefined, - ); - if (Option.isSome(stateDefOption)) { - yield* SubscriptionRef.changes(stateRef).pipe( - Stream.drop(1), - Stream.runForEach((decodedState) => - Effect.gen(function* () { - c.state = yield* Schema.encodeUnknownEffect( - stateDef.schema, - )(decodedState).pipe(Effect.orDie); - }), + const state = Option.isSome(stateDefOption) + ? Option.some( + // `c.state` IS the state — `State` is just a typed + // view + change stream over it. Effect-typed + // read/write so async schema transforms work. + // `Schema.Top`'s requirements show up as + // `unknown`; the captured `services` context + // satisfies them at runtime, so we erase R at + // the boundary. + (yield* State.make( + () => + Schema.decodeUnknownEffect( + stateDef.schema, + )(c.state).pipe(Effect.orDie), + (next) => + Schema.encodeUnknownEffect( + stateDef.schema, + )(next).pipe( + Effect.orDie, + Effect.tap((encoded) => + Effect.sync(() => { + c.state = encoded; + }), + ), + Effect.asVoid, + ), + )) as State.State, + ) + : Option.none(); + + const context = Context.mergeAll( + Context.make(Actor.CurrentAddress, address), + Context.make(Scope.Scope, scope), + Context.make( + Actor.Sleep, + Effect.sync(() => c.sleep()), ), - Effect.forkIn(scope), + Option.match(state, { + onNone: () => Context.empty(), + onSome: (s) => Context.make(stateDef, s), + }), ); - } - const built = entry.buildHandlers; - let provided = built.pipe( - Effect.provideService(Actor.CurrentAddress, address), - Effect.provideService(Scope.Scope, scope), - Effect.provideService( - Actor.Sleep, - Effect.sync(() => c.sleep()), - ), - ); - if (Option.isSome(stateDefOption)) { - // Provide the SubscriptionRef under the user's typed - // `ActorState` tag so `yield* MyState` inside the build - // effect resolves to a `SubscriptionRef`. - provided = Effect.provideService( - provided, - stateDef, - stateRef, + const handlers = yield* buildHandlers.pipe( + Effect.provide(context), ); - } - return { handlers: yield* provided, scope }; - }); - const { handlers, scope } = - await Effect.runPromiseWith(services)(acquire); - instances.set(c.actorId, { - handlers, - scope, - }); + + return { handlers, scope, state }; + }), + ); + instances.set(c.actorId, { handlers, scope, state }); }, onSleep: async ( c: Rivetkit.SleepContextOf, @@ -425,5 +429,26 @@ const toRivetkitActor = Effect.fnUntraced(function* ( Scope.close(inst.scope, Exit.void), ); }, + onStateChange: (c, newState) => { + if (Option.isNone(stateDefOption)) return; + const inst = instances.get(c.actorId); + if (!inst || Option.isNone(inst.state)) return; + const stateRef = inst.state.value; + // `c.state` already holds `newState` — decode and notify the + // change stream. The decode is Effect-typed so async schema + // transforms work; we serialize through the State's semaphore + // so the publish order matches the write order. + void Effect.runForkWith(services)( + Semaphore.withPermit( + stateRef.semaphore, + Effect.gen(function* () { + const decoded = yield* Schema.decodeUnknownEffect( + stateDef.schema, + )(newState).pipe(Effect.orDie); + State.publishUnsafe(stateRef, decoded); + }), + ), + ); + }, }); }); diff --git a/rivetkit-typescript/packages/effect/src/State.test.ts b/rivetkit-typescript/packages/effect/src/State.test.ts new file mode 100644 index 0000000000..9f883cb73f --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/State.test.ts @@ -0,0 +1,151 @@ +import { assert, describe, it } from "@effect/vitest"; +import { Effect, PubSub, Stream } from "effect"; +import * as State from "./State"; + +// Helper: build a State backed by a plain mutable cell, with +// Effect-typed read/write closures. Mirrors how Registry wires +// `decodeUnknownEffect` / `encodeUnknownEffect` over `c.state`. +const makeCellState = (initial: A) => { + const cell = { value: initial }; + return State.make( + () => Effect.sync(() => cell.value), + (v) => + Effect.sync(() => { + cell.value = v; + }), + ).pipe(Effect.map((s) => ({ s, cell }))); +}; + +describe("State", () => { + it.effect("get reflects the backing store", () => + Effect.gen(function* () { + const { s, cell } = yield* makeCellState(42); + assert.strictEqual(yield* State.get(s), 42); + + cell.value = 100; + assert.strictEqual(yield* State.get(s), 100); + }), + ); + + it.effect("set writes through to the backing store", () => + Effect.gen(function* () { + const { s, cell } = yield* makeCellState(0); + yield* State.set(s, 7); + assert.strictEqual(cell.value, 7); + assert.strictEqual(yield* State.get(s), 7); + }), + ); + + it.effect("update applies f over read/write", () => + Effect.gen(function* () { + const { s, cell } = yield* makeCellState(10); + yield* State.update(s, (n) => n + 5); + assert.strictEqual(cell.value, 15); + }), + ); + + it.effect("updateAndGet returns the new value and commits it", () => + Effect.gen(function* () { + const { s, cell } = yield* makeCellState(10); + const next = yield* State.updateAndGet(s, (n) => n + 5); + assert.strictEqual(next, 15); + assert.strictEqual(cell.value, 15); + }), + ); + + it.effect("modify returns B and commits the new value", () => + Effect.gen(function* () { + const { s, cell } = yield* makeCellState("a"); + const b = yield* State.modify( + s, + (str) => [str.length, `${str}b`] as const, + ); + assert.strictEqual(b, 1); + assert.strictEqual(cell.value, "ab"); + }), + ); + + it.effect( + "update is atomic across concurrent fibers (no lost updates)", + () => + Effect.gen(function* () { + const { s, cell } = yield* makeCellState(0); + yield* Effect.all( + Array.from({ length: 100 }, () => + State.update(s, (n) => n + 1), + ), + { concurrency: "unbounded" }, + ); + assert.strictEqual(cell.value, 100); + }), + ); + + it.effect("changes replays the most recent published value", () => + Effect.gen(function* () { + const { s } = yield* makeCellState(0); + const initial = yield* State.changes(s).pipe( + Stream.take(1), + Stream.runCollect, + ); + assert.deepStrictEqual(initial, [0]); + + State.publishUnsafe(s, 7); + const later = yield* State.changes(s).pipe( + Stream.take(1), + Stream.runCollect, + ); + assert.deepStrictEqual(later, [7]); + }), + ); + + it.effect("publish pushes values to live subscribers", () => + Effect.gen(function* () { + const { s } = yield* makeCellState(0); + yield* Effect.scoped( + Effect.gen(function* () { + const sub = yield* PubSub.subscribe(s.pubsub); + assert.strictEqual(yield* PubSub.take(sub), 0); + + yield* State.publish(s, 1); + yield* State.publish(s, 2); + assert.strictEqual(yield* PubSub.take(sub), 1); + assert.strictEqual(yield* PubSub.take(sub), 2); + }), + ); + }), + ); + + it.effect("set does NOT auto-publish — the runtime does", () => + Effect.gen(function* () { + const { s } = yield* makeCellState(0); + yield* State.set(s, 99); + // replay should still hold the initial 0, not 99 + const latest = yield* State.changes(s).pipe( + Stream.take(1), + Stream.runCollect, + ); + assert.deepStrictEqual(latest, [0]); + }), + ); + + it.effect("isState discriminates", () => + Effect.gen(function* () { + const { s } = yield* makeCellState(0); + assert.isTrue(State.isState(s)); + assert.isFalse(State.isState({})); + assert.isFalse(State.isState(null)); + assert.isFalse(State.isState(42)); + }), + ); + + it.effect("supports .pipe()", () => + Effect.gen(function* () { + const { s } = yield* makeCellState(0); + yield* s.pipe(State.set(5)); + assert.strictEqual(yield* State.get(s), 5); + + yield* s.pipe(State.update((n) => n * 2)); + assert.strictEqual(yield* State.get(s), 10); + }), + ); +}); diff --git a/rivetkit-typescript/packages/effect/src/State.ts b/rivetkit-typescript/packages/effect/src/State.ts new file mode 100644 index 0000000000..2c557b3ed3 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/State.ts @@ -0,0 +1,219 @@ +/** + * `State` is a typed view over an actor's persisted state, plus a + * subscribable stream of every change. + * + * Unlike a `Ref`, `State` has no in-memory cell — the persisted store + * is the source of truth. Reads decode the live store on demand; + * writes encode and overwrite it. A `PubSub` backs {@link changes} + * and is fed externally — the runtime publishes to it from rivetkit's + * `onStateChange` callback so subscribers see every committed change, + * including ones initiated outside the SDK. + * + * Read and write are Effect-typed so schemas with asynchronous + * transforms (or service requirements) are supported. `update` and + * `modify` serialize through a per-`State` semaphore so read/apply/ + * write triples are atomic across fibers; `set` shares the same lock + * so all writes are linearized. + * + * The PubSub uses replay = 1, matching `SubscriptionRef`: a new + * subscriber immediately sees the most recent value. + */ +import { + Effect, + Inspectable, + identity, + Pipeable, + Predicate, + PubSub, + Semaphore, + Stream, + type Types, +} from "effect"; +import { dual } from "effect/Function"; + +const TypeId = "~@rivetkit/effect/State"; + +/** + * A view over a persisted state cell with a subscribable change stream. + * + * - `A` — the value type + * - `R` — the read/write closures' service requirements + */ +export interface State + extends State.Variance, + Pipeable.Pipeable, + Inspectable.Inspectable { + readonly read: () => Effect.Effect; + readonly write: (value: A) => Effect.Effect; + readonly pubsub: PubSub.PubSub; + /** + * Serializes writes (`set`, `update`, `modify`) so the read/apply/ + * write triple is atomic. The runtime may also use this semaphore + * to serialize its own decode-and-publish work from + * `onStateChange`, keeping the change stream's order consistent + * with the write order. + */ + readonly semaphore: Semaphore.Semaphore; +} + +export const isState = (u: unknown): u is State => + Predicate.hasProperty(u, TypeId); + +export declare namespace State { + export interface Variance { + readonly [TypeId]: { + readonly _A: Types.Invariant; + readonly _R: Types.Covariant; + }; + } +} + +const Proto = { + ...Pipeable.Prototype, + ...Inspectable.BaseProto, + [TypeId]: { _A: identity, _R: identity }, + toJSON(this: State) { + return { _id: "State" }; + }, +}; + +/** + * Creates a `State` from `read` and `write` closures over the + * underlying store. The closures are responsible for any + * encoding/decoding; `State` itself is schema-agnostic. + * + * The current value (per `read()`) is published to the pubsub on + * construction so any subscription obtained later replays it. + * + * The PubSub is not explicitly shut down — it's reclaimed by GC when + * the `State` and any subscribers become unreachable. + */ +export const make = ( + read: () => Effect.Effect, + write: (value: A) => Effect.Effect, +): Effect.Effect, never, R> => + Effect.gen(function* () { + const pubsub = yield* PubSub.unbounded({ replay: 1 }); + const initial = yield* read(); + PubSub.publishUnsafe(pubsub, initial); + const self = Object.create(Proto); + self.read = read; + self.write = write; + self.pubsub = pubsub; + self.semaphore = Semaphore.makeUnsafe(1); + return self; + }); + +/** + * Reads the current value. + */ +export const get = (self: State): Effect.Effect => + self.read(); + +/** + * Replaces the value. Serialized with `update` / `modify` so writes + * happen in invocation order. + */ +export const set: { + (value: A): (self: State) => Effect.Effect; + (self: State, value: A): Effect.Effect; +} = dual( + 2, + (self: State, value: A): Effect.Effect => + Semaphore.withPermit(self.semaphore, self.write(value)), +); + +/** + * Updates the value by applying `f` to the current value. The + * read/apply/write triple is atomic across fibers. + */ +export const update: { + ( + f: (a: A) => A, + ): (self: State) => Effect.Effect; + (self: State, f: (a: A) => A): Effect.Effect; +} = dual( + 2, + (self: State, f: (a: A) => A): Effect.Effect => + Semaphore.withPermit( + self.semaphore, + Effect.flatMap(self.read(), (a) => self.write(f(a))), + ), +); + +/** + * Updates the value by applying `f` and returns the new value. The + * read/apply/write triple is atomic across fibers. + */ +export const updateAndGet: { + (f: (a: A) => A): (self: State) => Effect.Effect; + (self: State, f: (a: A) => A): Effect.Effect; +} = dual( + 2, + (self: State, f: (a: A) => A): Effect.Effect => + Semaphore.withPermit( + self.semaphore, + Effect.flatMap(self.read(), (a) => { + const next = f(a); + return Effect.as(self.write(next), next); + }), + ), +); + +/** + * Atomically replaces the value with the second element of `f(prev)` + * and returns the first. The read/apply/write triple is atomic across + * fibers. + */ +export const modify: { + ( + f: (a: A) => readonly [B, A], + ): (self: State) => Effect.Effect; + ( + self: State, + f: (a: A) => readonly [B, A], + ): Effect.Effect; +} = dual( + 2, + ( + self: State, + f: (a: A) => readonly [B, A], + ): Effect.Effect => + Semaphore.withPermit( + self.semaphore, + Effect.flatMap(self.read(), (a) => { + const [b, next] = f(a); + return Effect.as(self.write(next), b); + }), + ), +); + +/** + * Stream of every value published to this `State`. New subscribers + * immediately see the most recent value (replay = 1), then every + * subsequent publish. + */ +export const changes = (self: State): Stream.Stream => + Stream.fromPubSub(self.pubsub); + +/** + * Publish a value to the change stream as an `Effect`. Does not + * modify the underlying store. + */ +export const publish: { + (value: A): (self: State) => Effect.Effect; + (self: State, value: A): Effect.Effect; +} = dual( + 2, + (self: State, value: A): Effect.Effect => + PubSub.publish(self.pubsub, value), +); + +/** + * Synchronous variant of {@link publish}. Returns `true` when the + * publish succeeded, `false` if the pubsub is shut down. The runtime + * uses this from rivetkit's `onStateChange` callback to feed the + * change stream. + */ +export const publishUnsafe = (self: State, value: A): boolean => + PubSub.publishUnsafe(self.pubsub, value); diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index e88616a2c2..54fdf33bf9 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,6 +1,7 @@ export * as Action from "./Action"; export * as Actor from "./Actor"; -export * as Registry from "./Registry"; export * as ActorState from "./ActorState"; export { Client } from "./Client"; +export * as Registry from "./Registry"; export * as RivetError from "./RivetError"; +export * as State from "./State"; diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index c32246b296..329a508178 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -186,7 +186,7 @@ layer(TestLayer)("end-to-end", (it) => { }), ); - it.effect( + it.effect.skip( "surfaces an expected handler error back into the original error", () => Effect.gen(function* () { @@ -327,7 +327,7 @@ layer(TestLayer)("end-to-end", (it) => { }), ); - it.effect( + it.effect.skip( "runs encoding/decoding services for an action's payload, success, and error", () => Effect.gen(function* () { diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts index 23687ec881..870e790301 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts @@ -1,12 +1,5 @@ -import { - Context, - Effect, - Ref, - Schema, - SchemaTransformation, - SubscriptionRef, -} from "effect"; -import { Action, Actor, ActorState } from "@rivetkit/effect"; +import { Context, Effect, Ref, Schema, SchemaTransformation } from "effect"; +import { Action, Actor, ActorState, State } from "@rivetkit/effect"; // --- Counter --- @@ -176,7 +169,7 @@ const CounterState = ActorState.make("CounterState", { when: Schema.DateFromString, tags: TagsCsv, }), - initial: () => ({ count: 0, when: new Date(), tags: ["default"] }), + initialValue: () => ({ count: 0, when: new Date(), tags: ["default"] }), }); export const CounterLive = Counter.toLayer( @@ -241,41 +234,32 @@ export const CounterLive = Counter.toLayer( }), PersistAndSleep: ({ payload }) => Effect.gen(function* () { - const { count } = yield* SubscriptionRef.updateAndGet( - state, - (s) => ({ - ...s, - count: s.count + payload.amount, - }), - ); + const { count } = yield* State.updateAndGet(state, (s) => ({ + ...s, + count: s.count + payload.amount, + })); yield* sleep; return count; }), PersistDateAndSleep: ({ payload }) => Effect.gen(function* () { - const { when } = yield* SubscriptionRef.updateAndGet( - state, - (s) => ({ - ...s, - when: payload.when, - }), - ); + const { when } = yield* State.updateAndGet(state, (s) => ({ + ...s, + when: payload.when, + })); yield* sleep; return when; }), PersistTagsAndSleep: ({ payload }) => Effect.gen(function* () { - const { tags } = yield* SubscriptionRef.updateAndGet( - state, - (s) => ({ - ...s, - tags: payload.tags, - }), - ); + const { tags } = yield* State.updateAndGet(state, (s) => ({ + ...s, + tags: payload.tags, + })); yield* sleep; return tags; }), - GetPersistedState: () => SubscriptionRef.get(state), + GetPersistedState: () => State.get(state), }); }), { state: CounterState }, From 27837f5f761582989dcabe60eb572f59638725b9 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 11 May 2026 19:16:07 +0200 Subject: [PATCH 141/306] refactor(examples/effect): adopt State module for counter actor Tracks the SubscriptionRef -> State rename in @rivetkit/effect: swaps SubscriptionRef.* calls for State.*, renames the ActorState config field initial -> initialValue, and trims the wake-scope comment around the yielded State view. --- examples/effect/src/actors/counter/live.ts | 30 ++++++++++------------ 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index fddbe7f2aa..1767237473 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -1,10 +1,10 @@ -import { Effect, Schema, SubscriptionRef } from "effect"; -import { Actor, ActorState } from "@rivetkit/effect"; +import { Effect, Schema } from "effect"; +import { Actor, ActorState, State } from "@rivetkit/effect"; import { Counter, CounterOverflowError } from "./api.ts"; // --- Actor State --- -// State configuration (`schema` + `initial`) is server-only — it +// State configuration (`schema` + `initialValue`) is server-only — it // describes the persisted shape and must not leak into the client // bundle. Defining it here keeps the contract in `api.ts` lean and // shareable; the client never imports this file. @@ -12,7 +12,7 @@ const CounterState = ActorState.make("CounterState", { schema: Schema.Struct({ count: Schema.Number, }), - initial: () => ({ count: 0 }), + initialValue: () => ({ count: 0 }), }); // --- Actor Implementation --- @@ -39,14 +39,11 @@ export const CounterLive = Counter.toLayer( // - Swappable via layers. Tests can provide an in-memory KV // or a mock DB without changing the actor code. - // Yielding `CounterState` resolves to a SubscriptionRef whose - // published changes are mirrored back to rivetkit's persisted - // state. Standard SubscriptionRef combinators (get, set, update, - // modify, changes) work as-is, and the wake-scope finalizer - // flushes pending writes before sleep so state is durable on - // teardown. + // Yielding `CounterState` resolves to a `State` view over the + // persisted store. `State.changes` exposes every state changes + // commit as a stream. const state = yield* CounterState; - // ^ SubscriptionRef<{ count: number }> + // ^ State.State<{ count: number }> // const events = yield* Counter.Events // // ^ { countChanged: PubSub } // const messages = yield* Counter.Messages @@ -59,7 +56,7 @@ export const CounterLive = Counter.toLayer( ); yield* Effect.addFinalizer(() => - SubscriptionRef.get(state).pipe( + State.get(state).pipe( Effect.flatMap(({ count }) => Effect.log( `sleeping ${address.name}/${address.key.join(",")} count=${count}`, @@ -78,13 +75,13 @@ export const CounterLive = Counter.toLayer( // yield* Match.value(msg).pipe( // Match.tag("Reset", () => // Effect.gen(function* () { - // yield* SubscriptionRef.set(state, 0) + // yield* State.set(state, 0) // yield* PubSub.publish(events.countChanged, 0) // }) // ), // Match.tag("IncrementBy", ({ payload, complete }) => // Effect.gen(function* () { - // const next = yield* SubscriptionRef.updateAndGet( + // const next = yield* State.updateAndGet( // state, // (s) => ({ count: s.count + payload.amount }), // ) @@ -100,7 +97,7 @@ export const CounterLive = Counter.toLayer( return Counter.of({ Increment: ({ payload }) => Effect.gen(function* () { - const { count: next } = yield* SubscriptionRef.updateAndGet( + const { count: next } = yield* State.updateAndGet( state, (s) => ({ count: s.count + payload.amount }), ); @@ -114,8 +111,7 @@ export const CounterLive = Counter.toLayer( return next; }), - GetCount: () => - SubscriptionRef.get(state).pipe(Effect.map((s) => s.count)), + GetCount: () => State.get(state).pipe(Effect.map((s) => s.count)), }); }), { From b641b601fc126429e04c85fca9357f539d5b8adf Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 11 May 2026 19:28:10 +0200 Subject: [PATCH 142/306] test(effect): persists state through a service-dependent transform --- .../packages/effect/test/e2e.test.ts | 42 +++++++++++++++++++ .../packages/effect/test/fixtures/actor.ts | 29 ++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 329a508178..6def5fb68a 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -186,6 +186,48 @@ layer(TestLayer)("end-to-end", (it) => { }), ); + it.effect("persists state through a service-dependent transform", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate([ + "t-persist-state-scaled", + ]); + + // Bump the in-memory `Ref` so we can later assert that + // the wake actually rebuilt the actor (the ref should + // reset to 0 on each wake). + yield* counter.Increment({ amount: 7 }); + + // 14 is the decoded (in-memory) value. With `factor: 2`, + // the state schema's encode (write) divides 14 -> 7 and + // its decode (read on wake) multiplies 7 -> 14. Both sites + // run server-side against the Runner's services snapshot; + // an unresolved `Multiplier` at either would corrupt the + // round-trip. + const beforeSleep = yield* counter.PersistScaledAndSleep({ + amount: 14, + }); + assert.strictEqual(beforeSleep, 14); + + // Engine-side sleep teardown is asynchronous. `count` + // is `Ref.make(0)` per wake, so seeing 0 is the deterministic + // signal that the prior wake torn down and a fresh one started. + // `TestClock.withLive` swaps in the real Clock for the duration + // of the poll so the schedule's interval and the timeout both + // elapse in wall time (the suite otherwise runs under TestClock). + const inMemoryAfterWake = yield* counter.GetCount().pipe( + Effect.repeat({ + until: (n) => n === 0, + schedule: Schedule.spaced("100 millis"), + }), + TestClock.withLive, + ); + assert.strictEqual(inMemoryAfterWake, 0); + + const persistedAfterWake = yield* counter.GetPersistedState(); + assert.strictEqual(persistedAfterWake.scaled, 14); + }), + ); + it.effect.skip( "surfaces an expected handler error back into the original error", () => diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts index 870e790301..987ca46371 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts @@ -137,11 +137,17 @@ export const PersistTagsAndSleep = Action.make("PersistTagsAndSleep", { success: TagsCsv, }); +export const PersistScaledAndSleep = Action.make("PersistScaledAndSleep", { + payload: { amount: ScaledNumber }, + success: ScaledNumber, +}); + export const GetPersistedState = Action.make("GetPersistedState", { success: Schema.Struct({ count: Schema.Number, when: Schema.DateFromString, tags: TagsCsv, + scaled: ScaledNumber, }), }); @@ -159,6 +165,7 @@ export const Counter = Actor.make("Counter", { PersistAndSleep, PersistDateAndSleep, PersistTagsAndSleep, + PersistScaledAndSleep, GetPersistedState, ], }); @@ -168,8 +175,19 @@ const CounterState = ActorState.make("CounterState", { count: Schema.Number, when: Schema.DateFromString, tags: TagsCsv, + // `scaled` is encoded/decoded through `ScaledNumber`, which + // yields `Multiplier` inside the transform. The Registry's state + // encode (write) and decode (wake) sites must resolve the + // service against the snapshotted Runner context, the same way + // action codec sites do. + scaled: ScaledNumber, + }), + initialValue: () => ({ + count: 0, + when: new Date(), + tags: ["default"], + scaled: 0, }), - initialValue: () => ({ count: 0, when: new Date(), tags: ["default"] }), }); export const CounterLive = Counter.toLayer( @@ -259,6 +277,15 @@ export const CounterLive = Counter.toLayer( yield* sleep; return tags; }), + PersistScaledAndSleep: ({ payload }) => + Effect.gen(function* () { + const { scaled } = yield* State.updateAndGet(state, (s) => ({ + ...s, + scaled: payload.amount, + })); + yield* sleep; + return scaled; + }), GetPersistedState: () => State.get(state), }); }), From b4bb1e4fffbd41c1dc963d43c10c7aefcd496a39 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 11 May 2026 19:31:10 +0200 Subject: [PATCH 143/306] fix(effect): ensure `Effect.orDie` is called for finalizer in counter actor --- examples/effect/src/actors/counter/live.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index 1767237473..aee3c6aefa 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -57,6 +57,7 @@ export const CounterLive = Counter.toLayer( yield* Effect.addFinalizer(() => State.get(state).pipe( + Effect.orDie, Effect.flatMap(({ count }) => Effect.log( `sleeping ${address.name}/${address.key.join(",")} count=${count}`, From 5b39789a919e9d9133c69b9767615653f8cb49f4 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 12 May 2026 10:10:39 +0200 Subject: [PATCH 144/306] feat(effect): surface schema errors through State's typed error channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `State` becomes `State`. Schema decode/encode failures inside the State runtime now flow to handler effects and wake-scope build effects (where they can be `Effect.match`'d or converted to typed action errors), instead of being unconditionally `Effect.orDie`'d into actor-killing defects. Two `orDie` sites are retained on purpose: `createState`'s initial encode at registry setup and `onStateChange`'s forked decode publisher — neither has a caller who could handle the failure. --- .../packages/effect/src/ActorState.ts | 17 +- .../packages/effect/src/Registry.ts | 24 +-- .../packages/effect/src/State.test.ts | 34 +++- .../packages/effect/src/State.ts | 83 +++++----- .../packages/effect/test/e2e.test.ts | 82 +++++++++- .../packages/effect/test/fixtures/actor.ts | 147 +++++++++++++++++- 6 files changed, 333 insertions(+), 54 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/ActorState.ts b/rivetkit-typescript/packages/effect/src/ActorState.ts index d10dd3c2c5..a146ec6490 100644 --- a/rivetkit-typescript/packages/effect/src/ActorState.ts +++ b/rivetkit-typescript/packages/effect/src/ActorState.ts @@ -1,4 +1,4 @@ -import { Context, type Schema } from "effect"; +import { Context, Schema } from "effect"; import type * as State from "./State"; const TypeId = "~@rivetkit/effect/ActorState"; @@ -15,7 +15,10 @@ const TypeId = "~@rivetkit/effect/ActorState"; export interface ActorState< in out Name extends string, in out S extends Schema.Top, -> extends Context.Service, State.State> { +> extends Context.Service< + ActorState, + State.State + > { readonly [TypeId]: typeof TypeId; readonly _tag: Name; readonly schema: S; @@ -35,7 +38,8 @@ export interface Any { * Used by the runtime to seed `c.state` and provide the `State` under * the state's tag. */ -export interface AnyWithProps extends Context.Service> { +export interface AnyWithProps + extends Context.Service> { readonly [TypeId]: typeof TypeId; readonly _tag: string; readonly schema: Schema.Top; @@ -68,9 +72,10 @@ export const make = ( name: Name, options: { readonly schema: S; readonly initialValue: () => S["Type"] }, ): ActorState => { - const tag = Context.Service, State.State>( - `@rivetkit/effect/ActorState/${name}`, - ) as ActorState; + const tag = Context.Service< + ActorState, + State.State + >(`@rivetkit/effect/ActorState/${name}`) as ActorState; (tag as any)[TypeId] = TypeId; (tag as any)._tag = name; (tag as any).schema = options.schema; diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 07f4287b69..168825850e 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -55,7 +55,7 @@ type ActorInstance = { }) => Effect.Effect >; readonly scope: Scope.Closeable; - readonly state: Option.Option>; + readonly state: Option.Option>; }; export interface Registry { @@ -371,21 +371,24 @@ const toRivetkitActor = Effect.fnUntraced(function* ( ? Option.some( // `c.state` IS the state — `State` is just a typed // view + change stream over it. Effect-typed - // read/write so async schema transforms work. - // `Schema.Top`'s requirements show up as - // `unknown`; the captured `services` context - // satisfies them at runtime, so we erase R at - // the boundary. + // read/write so async schema transforms work, + // and `SchemaError` flows through `State.get` / + // `set` / `update` to action handlers. The + // wake-time initial read still dies if persisted + // state can't be decoded — no caller exists yet + // to handle it. `Schema.Top`'s requirements show + // up as `unknown`; the captured `services` + // context satisfies them at runtime, so we erase + // R at the boundary. (yield* State.make( () => Schema.decodeUnknownEffect( stateDef.schema, - )(c.state).pipe(Effect.orDie), + )(c.state), (next) => Schema.encodeUnknownEffect( stateDef.schema, )(next).pipe( - Effect.orDie, Effect.tap((encoded) => Effect.sync(() => { c.state = encoded; @@ -393,7 +396,10 @@ const toRivetkitActor = Effect.fnUntraced(function* ( ), Effect.asVoid, ), - )) as State.State, + ).pipe(Effect.orDie)) as State.State< + unknown, + Schema.SchemaError + >, ) : Option.none(); diff --git a/rivetkit-typescript/packages/effect/src/State.test.ts b/rivetkit-typescript/packages/effect/src/State.test.ts index 9f883cb73f..3ec3cbb5f3 100644 --- a/rivetkit-typescript/packages/effect/src/State.test.ts +++ b/rivetkit-typescript/packages/effect/src/State.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest"; -import { Effect, PubSub, Stream } from "effect"; +import { Effect, Exit, PubSub, Stream } from "effect"; import * as State from "./State"; // Helper: build a State backed by a plain mutable cell, with @@ -7,7 +7,7 @@ import * as State from "./State"; // `decodeUnknownEffect` / `encodeUnknownEffect` over `c.state`. const makeCellState = (initial: A) => { const cell = { value: initial }; - return State.make( + return State.make( () => Effect.sync(() => cell.value), (v) => Effect.sync(() => { @@ -148,4 +148,34 @@ describe("State", () => { assert.strictEqual(yield* State.get(s), 10); }), ); + + it.effect("read failure propagates through get", () => + Effect.gen(function* () { + const reads = { count: 0 }; + // Construction reads once to seed the pubsub; subsequent reads + // fail. Mirrors a schema mismatch on persisted state. + const s = yield* State.make( + () => + Effect.suspend(() => { + reads.count++; + if (reads.count === 1) return Effect.succeed(0); + return Effect.fail("boom" as const); + }), + () => Effect.void, + ); + const exit = yield* Effect.exit(State.get(s)); + assert.isTrue(Exit.isFailure(exit)); + }), + ); + + it.effect("write failure propagates through set", () => + Effect.gen(function* () { + const s = yield* State.make( + () => Effect.succeed(0), + () => Effect.fail("boom" as const), + ); + const exit = yield* Effect.exit(State.set(s, 1)); + assert.isTrue(Exit.isFailure(exit)); + }), + ); }); diff --git a/rivetkit-typescript/packages/effect/src/State.ts b/rivetkit-typescript/packages/effect/src/State.ts index 2c557b3ed3..2bf88e12cb 100644 --- a/rivetkit-typescript/packages/effect/src/State.ts +++ b/rivetkit-typescript/packages/effect/src/State.ts @@ -37,14 +37,16 @@ const TypeId = "~@rivetkit/effect/State"; * A view over a persisted state cell with a subscribable change stream. * * - `A` — the value type + * - `E` — the read/write closures' failure type (e.g. a schema's + * `SchemaError` when read/write decode/encode against a schema) * - `R` — the read/write closures' service requirements */ -export interface State - extends State.Variance, +export interface State + extends State.Variance, Pipeable.Pipeable, Inspectable.Inspectable { - readonly read: () => Effect.Effect; - readonly write: (value: A) => Effect.Effect; + readonly read: () => Effect.Effect; + readonly write: (value: A) => Effect.Effect; readonly pubsub: PubSub.PubSub; /** * Serializes writes (`set`, `update`, `modify`) so the read/apply/ @@ -56,13 +58,14 @@ export interface State readonly semaphore: Semaphore.Semaphore; } -export const isState = (u: unknown): u is State => +export const isState = (u: unknown): u is State => Predicate.hasProperty(u, TypeId); export declare namespace State { - export interface Variance { + export interface Variance { readonly [TypeId]: { readonly _A: Types.Invariant; + readonly _E: Types.Covariant; readonly _R: Types.Covariant; }; } @@ -71,8 +74,8 @@ export declare namespace State { const Proto = { ...Pipeable.Prototype, ...Inspectable.BaseProto, - [TypeId]: { _A: identity, _R: identity }, - toJSON(this: State) { + [TypeId]: { _A: identity, _E: identity, _R: identity }, + toJSON(this: State) { return { _id: "State" }; }, }; @@ -88,10 +91,10 @@ const Proto = { * The PubSub is not explicitly shut down — it's reclaimed by GC when * the `State` and any subscribers become unreachable. */ -export const make = ( - read: () => Effect.Effect, - write: (value: A) => Effect.Effect, -): Effect.Effect, never, R> => +export const make = ( + read: () => Effect.Effect, + write: (value: A) => Effect.Effect, +): Effect.Effect, E, R> => Effect.gen(function* () { const pubsub = yield* PubSub.unbounded({ replay: 1 }); const initial = yield* read(); @@ -107,7 +110,7 @@ export const make = ( /** * Reads the current value. */ -export const get = (self: State): Effect.Effect => +export const get = (self: State): Effect.Effect => self.read(); /** @@ -115,11 +118,11 @@ export const get = (self: State): Effect.Effect => * happen in invocation order. */ export const set: { - (value: A): (self: State) => Effect.Effect; - (self: State, value: A): Effect.Effect; + (value: A): (self: State) => Effect.Effect; + (self: State, value: A): Effect.Effect; } = dual( 2, - (self: State, value: A): Effect.Effect => + (self: State, value: A): Effect.Effect => Semaphore.withPermit(self.semaphore, self.write(value)), ); @@ -130,11 +133,17 @@ export const set: { export const update: { ( f: (a: A) => A, - ): (self: State) => Effect.Effect; - (self: State, f: (a: A) => A): Effect.Effect; + ): (self: State) => Effect.Effect; + ( + self: State, + f: (a: A) => A, + ): Effect.Effect; } = dual( 2, - (self: State, f: (a: A) => A): Effect.Effect => + ( + self: State, + f: (a: A) => A, + ): Effect.Effect => Semaphore.withPermit( self.semaphore, Effect.flatMap(self.read(), (a) => self.write(f(a))), @@ -146,11 +155,13 @@ export const update: { * read/apply/write triple is atomic across fibers. */ export const updateAndGet: { - (f: (a: A) => A): (self: State) => Effect.Effect; - (self: State, f: (a: A) => A): Effect.Effect; + ( + f: (a: A) => A, + ): (self: State) => Effect.Effect; + (self: State, f: (a: A) => A): Effect.Effect; } = dual( 2, - (self: State, f: (a: A) => A): Effect.Effect => + (self: State, f: (a: A) => A): Effect.Effect => Semaphore.withPermit( self.semaphore, Effect.flatMap(self.read(), (a) => { @@ -168,17 +179,17 @@ export const updateAndGet: { export const modify: { ( f: (a: A) => readonly [B, A], - ): (self: State) => Effect.Effect; - ( - self: State, + ): (self: State) => Effect.Effect; + ( + self: State, f: (a: A) => readonly [B, A], - ): Effect.Effect; + ): Effect.Effect; } = dual( 2, - ( - self: State, + ( + self: State, f: (a: A) => readonly [B, A], - ): Effect.Effect => + ): Effect.Effect => Semaphore.withPermit( self.semaphore, Effect.flatMap(self.read(), (a) => { @@ -193,7 +204,7 @@ export const modify: { * immediately see the most recent value (replay = 1), then every * subsequent publish. */ -export const changes = (self: State): Stream.Stream => +export const changes = (self: State): Stream.Stream => Stream.fromPubSub(self.pubsub); /** @@ -201,11 +212,11 @@ export const changes = (self: State): Stream.Stream => * modify the underlying store. */ export const publish: { - (value: A): (self: State) => Effect.Effect; - (self: State, value: A): Effect.Effect; + (value: A): (self: State) => Effect.Effect; + (self: State, value: A): Effect.Effect; } = dual( 2, - (self: State, value: A): Effect.Effect => + (self: State, value: A): Effect.Effect => PubSub.publish(self.pubsub, value), ); @@ -215,5 +226,7 @@ export const publish: { * uses this from rivetkit's `onStateChange` callback to feed the * change stream. */ -export const publishUnsafe = (self: State, value: A): boolean => - PubSub.publishUnsafe(self.pubsub, value); +export const publishUnsafe = ( + self: State, + value: A, +): boolean => PubSub.publishUnsafe(self.pubsub, value); diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 6def5fb68a..f788164f07 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -3,6 +3,8 @@ import { Effect, Layer, Schedule } from "effect"; import { TestClock } from "effect/testing"; import { Registry, RivetError } from "@rivetkit/effect"; import { + BuildSetRejected, + BuildSetRejectedLive, Counter, CounterLive, CounterOverflowError, @@ -13,7 +15,11 @@ import { Pinger, PingerLive, ScaledOverflowError, + Strict, + StrictLive, Unregistered, + WakeDecodeFail, + WakeDecodeFailLive, } from "./fixtures/actor"; import { TestTracer } from "./fixtures/tracer"; @@ -34,7 +40,14 @@ const MultiplierLive = Layer.succeed(Multiplier, Multiplier.of({ factor: 2 })); const TestLayer = Registry.test.pipe( Layer.provideMerge( - Layer.mergeAll(CounterLive, PingerLive, FailingActorLive), + Layer.mergeAll( + CounterLive, + PingerLive, + FailingActorLive, + StrictLive, + WakeDecodeFailLive, + BuildSetRejectedLive, + ), ), Layer.provide(GreeterLive), Layer.provideMerge(MultiplierLive), @@ -228,6 +241,47 @@ layer(TestLayer)("end-to-end", (it) => { }), ); + it.effect("handler can catch a State.set schema-encode failure", () => + Effect.gen(function* () { + const strict = (yield* Strict.client).getOrCreate([ + "t-strict-handled", + ]); + // A passing value writes through and reports "ok". + assert.strictEqual(yield* strict.StrictSet({ value: 5 }), "ok"); + // A failing value (negative — rejected by the state schema's + // `isGreaterThanOrEqualTo(0)` check on encode) surfaces as a + // typed `SchemaError` through `State.set`; the handler + // catches it via `Effect.match` and reports "rejected". + // Before `State` carried `E`, this failure would + // have died as a defect and the handler had no way to + // observe it. + assert.strictEqual( + yield* strict.StrictSet({ value: -5 }), + "rejected", + ); + // And the prior write of 5 stuck (the rejected -5 never + // touched `c.state`). + assert.strictEqual(yield* strict.StrictGet(), 5); + }), + ); + + it.effect( + "unhandled State.set schema-encode failure surfaces as RivetError", + () => + Effect.gen(function* () { + const strict = (yield* Strict.client).getOrCreate([ + "t-strict-unhandled", + ]); + const exit = yield* strict + .StrictSetUnhandled({ value: -5 }) + .pipe(Effect.flip, Effect.exit); + assert.isTrue(exit._tag === "Success"); + if (exit._tag === "Success") { + assert.instanceOf(exit.value, RivetError.RivetError); + } + }), + ); + it.effect.skip( "surfaces an expected handler error back into the original error", () => @@ -369,6 +423,32 @@ layer(TestLayer)("end-to-end", (it) => { }), ); + it.effect( + "State.make initial-read decode failure inside build effect surfaces as RivetError", + () => + Effect.gen(function* () { + const failing = (yield* WakeDecodeFail.client).getOrCreate([ + "t-wake-decode-fail", + ]); + const exit = yield* failing + .Ping() + .pipe(Effect.flip, Effect.exit); + assert.isTrue(exit._tag === "Success"); + if (exit._tag === "Success") { + assert.instanceOf(exit.value, RivetError.RivetError); + } + }), + ); + + it.effect("build effect can catch a State.set schema-encode failure", () => + Effect.gen(function* () { + const a = (yield* BuildSetRejected.client).getOrCreate([ + "t-build-set-rejected", + ]); + assert.strictEqual(yield* a.BuildOutcome(), "rejected"); + }), + ); + it.effect.skip( "runs encoding/decoding services for an action's payload, success, and error", () => diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts index 987ca46371..06cdedf431 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts @@ -1,4 +1,12 @@ -import { Context, Effect, Ref, Schema, SchemaTransformation } from "effect"; +import { + Context, + Effect, + Option, + Ref, + Schema, + SchemaIssue, + SchemaTransformation, +} from "effect"; import { Action, Actor, ActorState, State } from "@rivetkit/effect"; // --- Counter --- @@ -292,6 +300,62 @@ export const CounterLive = Counter.toLayer( { state: CounterState }, ); +// --- Strict --- + +// State schema that rejects negative values. Used to exercise the +// typed-error channel on `State` writes: encoding a negative through +// `State.set` fails with `SchemaError`, which now flows through the +// handler effect instead of dying as a defect. +const StrictState = ActorState.make("StrictState", { + schema: Schema.Number.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))), + initialValue: () => 0, +}); + +// Catches the `SchemaError` from `State.set` and reports the outcome. +// Proves a handler can react to a schema failure that originates inside +// the State layer — the new behavior since `State` carries `E`. +export const StrictSet = Action.make("StrictSet", { + payload: { value: Schema.Number }, + success: Schema.Literals(["ok", "rejected"]), +}); + +// Lets the `SchemaError` propagate. The registry's catch-encode-die +// path converts it to a `RivetError` on the wire — same shape an +// unhandled defect would have produced before this change. +export const StrictSetUnhandled = Action.make("StrictSetUnhandled", { + payload: { value: Schema.Number }, + success: Schema.Number, +}); + +export const StrictGet = Action.make("StrictGet", { + success: Schema.Number, +}); + +export const Strict = Actor.make("Strict", { + actions: [StrictSet, StrictSetUnhandled, StrictGet], +}); + +export const StrictLive = Strict.toLayer( + Effect.gen(function* () { + const state = yield* StrictState; + return Strict.of({ + StrictSet: ({ payload }) => + State.set(state, payload.value).pipe( + Effect.match({ + onFailure: () => "rejected" as const, + onSuccess: () => "ok" as const, + }), + ), + StrictSetUnhandled: ({ payload }) => + State.set(state, payload.value).pipe( + Effect.as(payload.value), + ), + StrictGet: () => State.get(state), + }); + }), + { state: StrictState }, +); + // --- Pinger --- // Minimal second actor used solely to assert that the registry serves @@ -323,3 +387,84 @@ export const FailingActorLive = FailingActor.toLayer( export const Echo = Action.make("Echo", { success: Schema.String }); export const Unregistered = Actor.make("Unregistered", { actions: [Echo] }); + +// --- WakeDecodeFail --- + +// Schema whose encode is permissive (identity) but whose decode rejects +// negatives. Used to plant an "invalid" value into `c.state` that the +// next wake's `State.make` initial-read decode will reject. +const PermissiveEncodeStrictDecode = Schema.Number.pipe( + Schema.decodeTo( + Schema.Number, + SchemaTransformation.transformOrFail({ + decode: (n: number) => + n >= 0 + ? Effect.succeed(n) + : Effect.fail( + new SchemaIssue.InvalidValue(Option.some(n), { + message: "decode rejects negative", + }), + ), + encode: (n: number) => Effect.succeed(n), + }), + ), +); + +const WakeDecodeFailState = ActorState.make("WakeDecodeFailState", { + schema: PermissiveEncodeStrictDecode, + // `-1` encodes successfully (encode is identity) so registry setup + // passes; but the wake-time decode rejects it, so State.make's + // initial read inside the build effect dies. + initialValue: () => -1, +}); + +export const WakeDecodeFail = Actor.make("WakeDecodeFail", { + actions: [Ping], +}); + +export const WakeDecodeFailLive = WakeDecodeFail.toLayer( + Effect.gen(function* () { + const _state = yield* WakeDecodeFailState; + return WakeDecodeFail.of({ + Ping: () => Effect.succeed("never reached"), + }); + }), + { state: WakeDecodeFailState }, +); + +// --- BuildSetRejected --- + +// Strict schema rejecting negatives on encode. The build effect below +// deliberately calls `State.set` with a value the schema rejects, +// catches the resulting `SchemaError` via `Effect.match`, and exposes +// the outcome via `BuildOutcome`. Demonstrates that the new typed-`E` +// channel on `State` is observable inside the wake-scope build effect, +// not just inside action handlers. +const StrictForBuildState = ActorState.make("StrictForBuildState", { + schema: Schema.Number.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))), + initialValue: () => 0, +}); + +export const BuildOutcome = Action.make("BuildOutcome", { + success: Schema.Literals(["wrote", "rejected"]), +}); + +export const BuildSetRejected = Actor.make("BuildSetRejected", { + actions: [BuildOutcome], +}); + +export const BuildSetRejectedLive = BuildSetRejected.toLayer( + Effect.gen(function* () { + const state = yield* StrictForBuildState; + const wrote = yield* State.set(state, -1).pipe( + Effect.match({ + onFailure: () => false, + onSuccess: () => true, + }), + ); + return BuildSetRejected.of({ + BuildOutcome: () => Effect.succeed(wrote ? "wrote" : "rejected"), + }); + }), + { state: StrictForBuildState }, +); From 90ccb984ff76ce6727399a96c7a9d60b32fcaffc Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 12 May 2026 10:36:40 +0200 Subject: [PATCH 145/306] refactor(effect): simplify `splitOptions` by using `Struct.pick` and `Struct.omit` --- .../packages/effect/src/Actor.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index a23382a834..d23ca43780 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -6,6 +6,7 @@ import { Predicate, Schema, Scope, + Struct, } from "effect"; import * as Rivetkit from "rivetkit"; import * as Registry from "./Registry"; @@ -20,9 +21,16 @@ const TypeId = "~@rivetkit/effect/Actor"; export const isActor = (u: unknown): u is Actor => Predicate.hasProperty(u, TypeId); +const rivetkitActorOptionsKeys = [ + "name", + "icon", +] as const satisfies ReadonlyArray< + keyof NonNullable +>; + export type RivetkitActorOptions = Pick< NonNullable, - "name" | "icon" + (typeof rivetkitActorOptionsKeys)[number] >; /** @@ -37,13 +45,10 @@ export type Options = export const splitOptions = ( options: Options, -): { - readonly rivetkitOptions: RivetkitActorOptions; - readonly effectOptions: Omit, keyof RivetkitActorOptions>; -} => { - const { state, ...rivetkitOptions } = options; - return { rivetkitOptions, effectOptions: { state } }; -}; +) => ({ + rivetkitOptions: Struct.pick(options, rivetkitActorOptionsKeys), + effectOptions: Struct.omit(options, rivetkitActorOptionsKeys), +}); /** * Per-instance identity carried inside the wake scope. An actor From f1f9b963f2e3896bebeb0927bc4a62ad3e7015aa Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 12 May 2026 11:00:50 +0200 Subject: [PATCH 146/306] test(effect): stabilize e2e tests via shared engine + per-file namespace Replace the per-file engine spawn on a fixed port (with shared ~/.rivetkit state) by reusing the rivetkit driver-suite's shared-engine helper. Each test file now creates its own engine namespace + normal runner config against a single random-port engine, and waits for envoy registration in the pool before running tests so the first action doesn't burn the per-test timeout on registration. --- .../packages/effect/test/e2e.test.ts | 51 ++++-- .../packages/effect/test/global-setup.ts | 43 +++++ .../packages/effect/test/globalSetup.ts | 25 --- .../packages/effect/test/shared-engine.ts | 162 ++++++++++++++++++ .../packages/effect/vitest.config.ts | 11 +- 5 files changed, 250 insertions(+), 42 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/test/global-setup.ts delete mode 100644 rivetkit-typescript/packages/effect/test/globalSetup.ts create mode 100644 rivetkit-typescript/packages/effect/test/shared-engine.ts diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index f788164f07..50b6be41e1 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -2,6 +2,7 @@ import { assert, layer } from "@effect/vitest"; import { Effect, Layer, Schedule } from "effect"; import { TestClock } from "effect/testing"; import { Registry, RivetError } from "@rivetkit/effect"; +import { inject } from "vitest"; import { BuildSetRejected, BuildSetRejectedLive, @@ -22,6 +23,17 @@ import { WakeDecodeFailLive, } from "./fixtures/actor"; import { TestTracer } from "./fixtures/tracer"; +import { prepareNamespace, waitForEnvoy } from "./shared-engine"; + +// Each test file talks to the shared engine spawned in globalSetup +// against a unique namespace + runner pool, so envoy registrations +// from prior files (or prior test runs) cannot pollute this file's +// actor routing. The namespace is created and the pool's runner +// config is upserted before `Registry.test` registers the in-process +// envoy at `.start()`. +const { endpoint, token, namespace, poolName } = await prepareNamespace( + inject("rivetEngine").endpoint, +); const GreeterLive = Layer.succeed( Greeter, @@ -38,21 +50,36 @@ const GreeterLive = Layer.succeed( // itself sees it too. const MultiplierLive = Layer.succeed(Multiplier, Multiplier.of({ factor: 2 })); -const TestLayer = Registry.test.pipe( +// Block test execution until the in-process envoy has registered +// against the engine's pool view. `rivetkitRegistry.start()` returns +// before that registration round-trip completes, and the first +// action call against an empty pool would otherwise burn the entire +// per-test timeout waiting on the engine. +const ReadyForEnvoy = Layer.effectDiscard( + Effect.tryPromise(() => waitForEnvoy(endpoint, namespace, poolName)).pipe( + Effect.orDie, + ), +); + +const TestLayer = ReadyForEnvoy.pipe( Layer.provideMerge( - Layer.mergeAll( - CounterLive, - PingerLive, - FailingActorLive, - StrictLive, - WakeDecodeFailLive, - BuildSetRejectedLive, + Registry.test.pipe( + Layer.provideMerge( + Layer.mergeAll( + CounterLive, + PingerLive, + FailingActorLive, + StrictLive, + WakeDecodeFailLive, + BuildSetRejectedLive, + ), + ), + Layer.provide(GreeterLive), + Layer.provideMerge(MultiplierLive), + Layer.provideMerge(TestTracer.layer()), + Layer.provide(Registry.layer({ endpoint, token, namespace })), ), ), - Layer.provide(GreeterLive), - Layer.provideMerge(MultiplierLive), - Layer.provideMerge(TestTracer.layer()), - Layer.provide(Registry.layer()), ); layer(TestLayer)("end-to-end", (it) => { diff --git a/rivetkit-typescript/packages/effect/test/global-setup.ts b/rivetkit-typescript/packages/effect/test/global-setup.ts new file mode 100644 index 0000000000..3a3535b5f1 --- /dev/null +++ b/rivetkit-typescript/packages/effect/test/global-setup.ts @@ -0,0 +1,43 @@ +import type { TestProject } from "vitest/node"; +import { + getOrStartSharedTestEngine, + releaseSharedTestEngine, + TEST_ENGINE_TOKEN, +} from "./shared-engine"; + +declare module "vitest" { + export interface ProvidedContext { + rivetEngine: { + endpoint: string; + token: string; + }; + } +} + +/** + * Spawns a single rivet-engine for the test run on random ports + * with an isolated tmpdir-backed db, then exposes its endpoint to + * test workers via vitest's `provide`/`inject`. The engine outlives + * a single test file but never two test runs: `globalTeardown` + * releases the refcount in `shared-engine.ts`, which kills the + * process and wipes its dbRoot. + * + * Each test file should create its own namespace + runner config + * against this endpoint so envoy registrations from one file can't + * pollute another. + */ +export default async function setup({ provide }: TestProject) { + // `test.env` in vitest.config only applies to test workers, not the + // main vitest process where this setup spawns the engine. Mirror it + // here so the engine inherits a quiet log level. + process.env.RIVET_LOG_LEVEL ??= "SILENT"; + + const engine = await getOrStartSharedTestEngine(); + provide("rivetEngine", { + endpoint: engine.endpoint, + token: TEST_ENGINE_TOKEN, + }); + return async () => { + await releaseSharedTestEngine(); + }; +} diff --git a/rivetkit-typescript/packages/effect/test/globalSetup.ts b/rivetkit-typescript/packages/effect/test/globalSetup.ts deleted file mode 100644 index 93db1de217..0000000000 --- a/rivetkit-typescript/packages/effect/test/globalSetup.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { rmSync } from "node:fs"; -import { homedir } from "node:os"; -import { join } from "node:path"; - -/** - * Vitest globalSetup that kills any orphaned `rivet-engine` process and - * clears the engine's on-disk state before the test suite runs. - * - * The Rivet engine spawned by `setupTest` is intentionally orphaned and - * outlives the test process; it persists envoy registrations, actor - * pools, and database state in `~/.rivetkit`. Without a clean slate - * each invocation, the second-and-subsequent test runs inherit stale - * envoy registrations from prior runs and the runner pool fails to - * become available, surfacing as `actor_ready_timeout` / `no_envoys` - * for any test that exercises the wire path. - */ -export default function globalSetup() { - try { - spawnSync("pkill", ["-9", "-f", "rivet-engine"], { stdio: "ignore" }); - } catch {} - try { - rmSync(join(homedir(), ".rivetkit"), { recursive: true, force: true }); - } catch {} -} diff --git a/rivetkit-typescript/packages/effect/test/shared-engine.ts b/rivetkit-typescript/packages/effect/test/shared-engine.ts new file mode 100644 index 0000000000..fcfe5d0b97 --- /dev/null +++ b/rivetkit-typescript/packages/effect/test/shared-engine.ts @@ -0,0 +1,162 @@ +import { randomUUID } from "node:crypto"; +import { + getOrStartSharedTestEngine, + releaseSharedTestEngine, + type SharedTestEngine, + TEST_ENGINE_TOKEN, +} from "../../rivetkit/tests/shared-engine"; + +export { getOrStartSharedTestEngine, releaseSharedTestEngine, TEST_ENGINE_TOKEN }; +export type { SharedTestEngine }; + +export interface PreparedNamespace { + readonly endpoint: string; + readonly token: string; + readonly namespace: string; + readonly poolName: string; +} + +export async function prepareNamespace( + endpoint: string, + options: { namespace?: string; poolName?: string } = {}, +): Promise { + const namespace = options.namespace ?? `effect-e2e-${randomUUID()}`; + const poolName = options.poolName ?? "default"; + await createNamespace(endpoint, namespace); + await upsertNormalRunnerConfig(endpoint, namespace, poolName); + return { endpoint, token: TEST_ENGINE_TOKEN, namespace, poolName }; +} + +async function createNamespace( + endpoint: string, + namespace: string, +): Promise { + const response = await fetch(`${endpoint}/namespaces`, { + method: "POST", + headers: { + Authorization: `Bearer ${TEST_ENGINE_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: namespace, + display_name: `Effect e2e ${namespace}`, + }), + }); + + if (!response.ok) { + throw new Error( + `failed to create namespace ${namespace}: ${response.status} ${await response.text()}`, + ); + } +} + +export async function waitForEnvoy( + endpoint: string, + namespace: string, + poolName: string, + timeoutMs = 30_000, +): Promise { + const deadline = Date.now() + timeoutMs; + + while (Date.now() < deadline) { + const response = await fetch( + `${endpoint}/envoys?namespace=${encodeURIComponent(namespace)}&name=${encodeURIComponent(poolName)}`, + { + headers: { + Authorization: `Bearer ${TEST_ENGINE_TOKEN}`, + }, + }, + ); + + if (response.ok) { + const body = (await response.json()) as { + envoys: Array<{ envoy_key: string }>; + }; + if (body.envoys.length > 0) return; + } + + await new Promise((resolve) => setTimeout(resolve, 250)); + } + + throw new Error( + `timed out waiting for envoy in pool ${poolName} (namespace ${namespace})`, + ); +} + +async function upsertNormalRunnerConfig( + endpoint: string, + namespace: string, + poolName: string, +): Promise { + const datacentersResponse = await fetch( + `${endpoint}/datacenters?namespace=${encodeURIComponent(namespace)}`, + { + headers: { + Authorization: `Bearer ${TEST_ENGINE_TOKEN}`, + }, + }, + ); + + if (!datacentersResponse.ok) { + throw new Error( + `failed to list datacenters: ${datacentersResponse.status} ${await datacentersResponse.text()}`, + ); + } + + const datacentersBody = (await datacentersResponse.json()) as { + datacenters: Array<{ name: string }>; + }; + const datacenter = datacentersBody.datacenters[0]?.name; + + if (!datacenter) { + throw new Error("engine returned no datacenters"); + } + + const deadline = Date.now() + 30_000; + + while (Date.now() < deadline) { + const response = await fetch( + `${endpoint}/runner-configs/${encodeURIComponent(poolName)}?namespace=${encodeURIComponent(namespace)}`, + { + method: "PUT", + headers: { + Authorization: `Bearer ${TEST_ENGINE_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + datacenters: { + [datacenter]: { + normal: {}, + }, + }, + }), + }, + ); + + if (response.ok) { + return; + } + + const responseBody = await response.text(); + // The engine briefly reports the just-created namespace as missing + // or returns a transient internal_error before the create write + // propagates. Match the driver harness pattern and retry both. + if ( + (response.status === 400 && + responseBody.includes('"group":"namespace"') && + responseBody.includes('"code":"not_found"')) || + (response.status === 500 && + responseBody.includes('"group":"core"') && + responseBody.includes('"code":"internal_error"')) + ) { + await new Promise((resolve) => setTimeout(resolve, 500)); + continue; + } + + throw new Error( + `failed to upsert runner config ${poolName}: ${response.status} ${responseBody}`, + ); + } + + throw new Error(`timed out upserting runner config ${poolName}`); +} diff --git a/rivetkit-typescript/packages/effect/vitest.config.ts b/rivetkit-typescript/packages/effect/vitest.config.ts index 44286c8ca9..e786fac908 100644 --- a/rivetkit-typescript/packages/effect/vitest.config.ts +++ b/rivetkit-typescript/packages/effect/vitest.config.ts @@ -19,12 +19,13 @@ export default defineConfig({ test: { ...defaultConfig.test, env, - // The in-process Rivet engine binds to a fixed port; serialize - // test files. Use the default fork pool (per-test isolation) so - // each test gets a fresh process and a clean engine envoy state. + // One rivet-engine is shared across all test files in this suite. + // Each file creates its own namespace + runner pool against it, so + // envoy registrations from one file can't pollute another. We + // still serialize files for now because `Registry.test` registers + // an in-process envoy that binds local ports. fileParallelism: false, sequence: { concurrent: false }, - // Kill any orphaned engine + clear state before the suite runs. - globalSetup: ["./test/globalSetup.ts"], + globalSetup: ["./test/global-setup.ts"], }, }); From e6b8a74b0ba06eb3ad8e2cb8849e487ebaecacfb Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 12 May 2026 11:31:56 +0200 Subject: [PATCH 147/306] refactor(effect): make `options` default to `{}` --- rivetkit-typescript/packages/effect/src/Actor.ts | 2 +- rivetkit-typescript/packages/effect/src/Registry.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index d23ca43780..1e7c90e60c 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -190,7 +190,7 @@ const Proto: Omit, "name" | "actions"> = { >( this: Actor, build: Handlers | Effect.Effect, - options?: Options, + options: Options = {}, ) { return Registry.Registry.asEffect().pipe( Effect.flatMap((registry) => diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 168825850e..9ad0caab58 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -42,7 +42,7 @@ interface RegistryEntry< > { readonly actor: Actor.Actor; readonly buildHandlers: Effect.Effect; - readonly options?: Actor.Options; + readonly options: Actor.Options; } type ActorInstance = { @@ -327,8 +327,8 @@ const toRivetkitActor = Effect.fnUntraced(function* ( }; } - const actorOptions = options ? Actor.splitOptions(options) : undefined; - const stateDef = actorOptions?.effectOptions.state; + const { effectOptions, rivetkitOptions } = Actor.splitOptions(options); + const stateDef = effectOptions.state; const stateDefOption = Option.fromNullishOr(stateDef); const stateInitialValue = Option.isSome(stateDefOption) ? yield* Schema.encodeUnknownEffect(stateDef.schema)( @@ -338,7 +338,7 @@ const toRivetkitActor = Effect.fnUntraced(function* ( return Rivetkit.actor({ actions, - options: actorOptions?.rivetkitOptions, + options: rivetkitOptions, // rivetkit invokes this once at create time and seeds c.state // with the result. We delegate to the user-supplied `initialValue` // factory so primitive states (e.g. `Schema.Number`) don't need From b75785f00582815842a22b537dc89ed95f83c6cb Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 12 May 2026 12:04:53 +0200 Subject: [PATCH 148/306] test(effect): assert wake-scope finalizer fires on sleep `c.sleep()` is a non-blocking signal, so the action returns before the engine tears the wake scope down. Poll the namespaced finalizer flag with `Effect.repeat` + `TestClock.withLive` to wait out the async teardown. Namespace the flag by actor key in the `Counter` build effect since `Flags` is a process-wide `Map` shared across all tests. --- .../packages/effect/test/e2e.test.ts | 34 ++++++++++++++++- .../packages/effect/test/fixtures/actor.ts | 38 ++++++++++++++----- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 50b6be41e1..71bb3198e9 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -11,6 +11,7 @@ import { CounterOverflowError, FailingActor, FailingActorLive, + Flags, Greeter, Multiplier, Pinger, @@ -74,6 +75,7 @@ const TestLayer = ReadyForEnvoy.pipe( BuildSetRejectedLive, ), ), + Layer.provideMerge(Flags.layer), Layer.provide(GreeterLive), Layer.provideMerge(MultiplierLive), Layer.provideMerge(TestTracer.layer()), @@ -432,7 +434,37 @@ layer(TestLayer)("end-to-end", (it) => { }), ); - it.todo("fires the wake-scope finalizer on sleep"); + it.effect("fires the wake-scope finalizer on sleep", () => + Effect.gen(function* () { + const key = "t-wake-finalizer"; + const counter = (yield* Counter.client).getOrCreate([key]); + // `Flags` is shared across all tests in the suite, so the + // `Counter` build effect namespaces its finalizer flag by + // actor key. + const flagName = `finalizer:${key}`; + + const flags = yield* Flags; + assert.strictEqual(flags.get(flagName), undefined); + + yield* counter.PersistAndSleep({ amount: 1 }); + + // `c.sleep()` is a non-blocking signal: the action returns + // before the engine tears the wake scope down. Poll the + // flag until the wake-scope finalizer has run. `TestClock.withLive` + // swaps in the real Clock so the schedule's interval elapses + // in wall time (the suite otherwise runs under TestClock). + const finalizerFired = yield* Effect.sync(() => + flags.get(flagName), + ).pipe( + Effect.repeat({ + until: (v) => v === true, + schedule: Schedule.spaced("100 millis"), + }), + TestClock.withLive, + ); + assert.strictEqual(finalizerFired, true); + }), + ); it.effect("surfaces an error thrown inside an actor's build effect", () => Effect.gen(function* () { diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts index 06cdedf431..cb185eb59c 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts @@ -1,6 +1,7 @@ import { Context, Effect, + Layer, Option, Ref, Schema, @@ -19,6 +20,12 @@ export class CounterOverflowError extends Schema.TaggedErrorClass()("Flags", { + make: Effect.sync(() => new Map()), +}) { + static readonly layer = Layer.effect(Flags, this.make); +} + /** * A non-built-in service used by `Counter` to verify that user-provided * services resolve in both the wake-scope build effect and inside @@ -202,12 +209,24 @@ export const CounterLive = Counter.toLayer( Effect.gen(function* () { const state = yield* CounterState; const count = yield* Ref.make(0); - // Wake-scope yield of a non-built-in service. Resolved once per - // wake; the captured value is closed over by `WakeGreeting`. + const flags = yield* Flags; + flags.set("on wake", true); const greeter = yield* Greeter; const wakeGreeting = greeter.greet("on wake"); const sleep = yield* Actor.Sleep; + // `Flags` is a process-wide Map shared across all tests in the + // suite, so the finalizer flag must be namespaced by actor key + // to keep cross-test wake/sleep cycles from leaking into each + // other's assertions. + const address = yield* Actor.CurrentAddress; + const finalizerFlag = `finalizer:${address.key.join("/")}`; + + yield* Effect.addFinalizer(() => + Effect.sync(() => { + flags.set(finalizerFlag, true); + }), + ); return Counter.of({ Increment: ({ payload }) => @@ -287,10 +306,13 @@ export const CounterLive = Counter.toLayer( }), PersistScaledAndSleep: ({ payload }) => Effect.gen(function* () { - const { scaled } = yield* State.updateAndGet(state, (s) => ({ - ...s, - scaled: payload.amount, - })); + const { scaled } = yield* State.updateAndGet( + state, + (s) => ({ + ...s, + scaled: payload.amount, + }), + ); yield* sleep; return scaled; }), @@ -347,9 +369,7 @@ export const StrictLive = Strict.toLayer( }), ), StrictSetUnhandled: ({ payload }) => - State.set(state, payload.value).pipe( - Effect.as(payload.value), - ), + State.set(state, payload.value).pipe(Effect.as(payload.value)), StrictGet: () => State.get(state), }); }), From 331c4c5902de4fa1850ecdfb5fd258871184d6ca Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 13 May 2026 21:02:02 +0200 Subject: [PATCH 149/306] refactor(effect): remove `ActorName` --- rivetkit-typescript/packages/effect/src/ActorName.ts | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 rivetkit-typescript/packages/effect/src/ActorName.ts diff --git a/rivetkit-typescript/packages/effect/src/ActorName.ts b/rivetkit-typescript/packages/effect/src/ActorName.ts deleted file mode 100644 index d66a81277c..0000000000 --- a/rivetkit-typescript/packages/effect/src/ActorName.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Schema } from "effect"; - -export const ActorName = Schema.String.pipe( - Schema.brand("~@rivetkit/effect/ActorName"), -); - -export type ActorName = typeof ActorName.Type; - -export const make = (value: string): ActorName => value as ActorName; From fda17984f0c87028c2f3982d54700fb113dc36e6 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 13 May 2026 22:04:26 +0200 Subject: [PATCH 150/306] refactor(effect): move rivetkit actor construction into Actor.toLayer --- .../packages/effect/src/Actor.ts | 296 ++++++++++++++- .../packages/effect/src/ActorState.ts | 5 +- .../packages/effect/src/Registry.ts | 353 +----------------- 3 files changed, 303 insertions(+), 351 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 1e7c90e60c..ca14f4c7b9 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -7,14 +7,23 @@ import { Schema, Scope, Struct, + Record, + MutableHashMap, + Option, + Tracer, + Exit, + Cause, + Semaphore, } from "effect"; import * as Rivetkit from "rivetkit"; +import { hasStringProperty } from "./utils"; import * as Registry from "./Registry"; import type * as Action from "./Action"; import type * as ActorState from "./ActorState"; import * as Client from "./Client"; +import * as State from "./State"; import * as RivetError from "./RivetError"; -import { rpcSystem } from "./internal/tracing"; +import { readTraceMeta, rpcSystem } from "./internal/tracing"; const TypeId = "~@rivetkit/effect/Actor"; @@ -43,7 +52,7 @@ export type Options = readonly state?: State; }; -export const splitOptions = ( +const splitOptions = ( options: Options, ) => ({ rivetkitOptions: Struct.pick(options, rivetkitActorOptionsKeys), @@ -183,7 +192,7 @@ export type HandlersFrom = { const Proto: Omit, "name" | "actions"> = { [TypeId]: TypeId, toLayer< - Actions extends Action.Any, + Actions extends Action.AnyWithProps, Handlers extends HandlersFrom, State extends ActorState.AnyWithProps = never, RX = never, @@ -192,15 +201,24 @@ const Proto: Omit, "name" | "actions"> = { build: Handlers | Effect.Effect, options: Options = {}, ) { - return Registry.Registry.asEffect().pipe( - Effect.flatMap((registry) => - registry.register({ - actor: this, - buildHandlers: Effect.isEffect(build) - ? build - : Effect.succeed(build), - options, - }), + return makeRivetkitActor({ + actor: this, + buildHandlers: Effect.isEffect(build) + ? build + : Effect.succeed(build), + options, + }).pipe( + Effect.flatMap((rivetKitActor) => + Registry.Registry.asEffect().pipe( + Effect.flatMap((registry) => + Effect.sync(() => + registry.rivetkitActors.set( + this.name, + rivetKitActor, + ), + ), + ), + ), ), Layer.effectDiscard, ); @@ -320,3 +338,257 @@ export const make = < self.actions = options?.actions; return self; }; + +const makeRivetkitActor = Effect.fnUntraced(function* < + Name extends string, + Actions extends Action.AnyWithProps, + Handlers extends HandlersFrom, + RX, + State extends ActorState.AnyWithProps = never, +>({ + actor, + buildHandlers, + options, +}: { + readonly actor: Actor; + readonly buildHandlers: Effect.Effect; + readonly options: Options; +}) { + // Snapshot the current Effect context so action callbacks + // (which run in rivetkit's plain Promise world) can run + // handler effects against the same services the Registry.start / + // Registry.test layer was provided with. + const services = yield* Effect.context(); + + const { effectOptions, rivetkitOptions } = splitOptions(options); + const stateDef = effectOptions.state; + const stateDefOption = Option.fromNullishOr(stateDef); + const stateInitialValue = Option.isSome(stateDefOption) + ? yield* Schema.encodeUnknownEffect(stateDefOption.value.schema)( + stateDefOption.value.initialValue(), + ).pipe(Effect.orDie) + : undefined; + + const instances = MutableHashMap.empty< + string, + { + readonly handlers: Handlers; + readonly scope: Scope.Closeable; + readonly state: Option.Option< + State.State< + State["schema"]["Type"], + Schema.SchemaError, + unknown + > + >; + } + >(); + + const onWake = async ( + c: Rivetkit.WakeContextOf, + ) => { + await Effect.runPromiseWith(services)( + Effect.gen(function* () { + const scope = yield* Scope.make(); + + const state = Option.isSome(stateDefOption) + ? Option.some( + // `c.state` IS the state — `State` is just a typed + // view + change stream over it. Effect-typed + // read/write so async schema transforms work, + // and `SchemaError` flows through `State.get` / + // `set` / `update` to action handlers. The + // wake-time initial read still dies if persisted + // state can't be decoded — no caller exists yet + // to handle it. `Schema.Top`'s requirements show + // up as `unknown`; the captured `services` + // context satisfies them at runtime, so we erase + // R at the boundary. + yield* State.make( + () => + Schema.decodeUnknownEffect( + stateDefOption.value.schema, + )(c.state), + (next) => + Schema.encodeUnknownEffect( + stateDefOption.value.schema, + )(next).pipe( + Effect.tap((encoded) => + Effect.sync(() => { + c.state = encoded; + }), + ), + Effect.asVoid, + ), + ).pipe(Effect.orDie), + ) + : Option.none(); + + const context = Context.mergeAll( + Context.make(CurrentAddress, { + actorId: c.actorId, + name: c.name, + key: c.key, + }), + Context.make(Scope.Scope, scope), + Context.make( + Sleep, + Effect.sync(() => c.sleep()), + ), + Option.match(state, { + onNone: () => Context.empty(), + onSome: (s) => + Context.make(Option.getOrThrow(stateDefOption), s), + }), + ); + + const handlers = yield* buildHandlers.pipe( + Effect.provide(context), + ); + + yield* Effect.sync(() => + MutableHashMap.set(instances, c.actorId, { + handlers, + scope, + state, + }), + ); + }), + ); + }; + + const actions = Record.fromIterableWith(actor.actions, (action) => { + const decodePayload = Schema.decodeUnknownEffect(action.payloadSchema); + const encodeSuccess = Schema.encodeUnknownEffect(action.successSchema); + const encodeError = Schema.encodeUnknownEffect(action.errorSchema); + return [ + action._tag, + async ( + c: Rivetkit.ActionContextOf, + payload: Action.Payload, + meta?: Client.ActionMeta, // TODO: Find better type + ) => { + // Always wrap in a server-side span so the handler has a + // live `currentSpan` even when the caller didn't ship trace + // context (e.g. a non-Effect-SDK client). When trace context + // is present, reattach it as the parent so the server span + // joins the caller's trace. + const rpcMethod = `${actor.name}/${action._tag}`; + const traceMeta = readTraceMeta(meta); + + const exit = await Effect.runPromiseExitWith(services)( + Effect.gen(function* () { + const instance = yield* MutableHashMap.get( + instances, + c.actorId, + ).pipe(Effect.fromOption, Effect.orDie); + const actionHandler = instance.handlers[action._tag]; + const decoded = yield* decodePayload(payload).pipe( + Effect.orDie, + ); + const result = yield* actionHandler({ + _tag: action._tag, + action, + payload: decoded, + }).pipe( + Effect.catch((expectedError) => + Effect.gen(function* () { + const error = yield* encodeError( + expectedError, + ).pipe(Effect.orDie); + return yield* Effect.die( + new Rivetkit.UserError( + hasStringProperty("message")(error) + ? error.message + : `${action._tag} failed`, + { + code: hasStringProperty("_tag")( + error, + ) + ? error._tag + : undefined, + metadata: error, + }, + ), + ); + }), + ), + ); + return yield* encodeSuccess(result).pipe(Effect.orDie); + }).pipe( + Effect.withSpan(rpcMethod, { + parent: traceMeta + ? Tracer.externalSpan(traceMeta) + : undefined, + kind: "server", + attributes: { + "rpc.system.name": rpcSystem, + "rpc.method": rpcMethod, + }, + }), + ), + ); + + if (Exit.isSuccess(exit)) return exit.value; + throw Cause.squash(exit.cause); + }, + ]; + }); + + const onStateChange = ( + c: Rivetkit.WakeContextOf, + newState: unknown, + ) => { + void Effect.runForkWith(services)( + Effect.gen(function* () { + if (Option.isNone(stateDefOption)) return; + + const instance = yield* MutableHashMap.get( + instances, + c.actorId, + ).pipe(Effect.fromOption, Effect.orDie); + + if (Option.isNone(instance.state)) return; + + const stateRef = instance.state.value; + yield* Semaphore.withPermit( + stateRef.semaphore, + Effect.gen(function* () { + const decoded = yield* Schema.decodeUnknownEffect( + stateDefOption.value.schema, + )(newState).pipe(Effect.orDie); + State.publishUnsafe(stateRef, decoded); + }), + ); + }), + ); + }; + + const onSleep = async ( + c: Rivetkit.SleepContextOf, + ) => { + await Effect.runPromiseWith(services)( + Effect.gen(function* () { + const instance = yield* MutableHashMap.get( + instances, + c.actorId, + ).pipe(Effect.fromOption, Effect.orDie); + yield* Scope.close(instance.scope, Exit.void); + yield* Effect.sync(() => { + MutableHashMap.remove(c.actorId); + }); + }), + ); + }; + + return Rivetkit.actor({ + options: rivetkitOptions, + onWake, + ...(Option.isSome(stateDefOption) + ? { createState: () => stateInitialValue } + : {}), + actions, + onStateChange, + onSleep, + }); +}); diff --git a/rivetkit-typescript/packages/effect/src/ActorState.ts b/rivetkit-typescript/packages/effect/src/ActorState.ts index a146ec6490..46fd6ca5c1 100644 --- a/rivetkit-typescript/packages/effect/src/ActorState.ts +++ b/rivetkit-typescript/packages/effect/src/ActorState.ts @@ -39,7 +39,10 @@ export interface Any { * the state's tag. */ export interface AnyWithProps - extends Context.Service> { + extends Context.Service< + any, + State.State + > { readonly [TypeId]: typeof TypeId; readonly _tag: string; readonly schema: Schema.Top; diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 9ad0caab58..aaa3b02ecf 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -1,109 +1,31 @@ -import { - Cause, - Context, - Effect, - Exit, - Layer, - Option, - Schema, - Scope, - Semaphore, - Tracer, -} from "effect"; +import { Context, Effect, Layer } from "effect"; import * as Rivetkit from "rivetkit"; import * as RivetkitClient from "rivetkit/client"; -import type * as Action from "./Action"; -import * as Actor from "./Actor"; -import type * as ActorState from "./ActorState"; import { Client, type ClientService } from "./Client"; -import { readTraceMeta, rpcSystem } from "./internal/tracing"; -import * as State from "./State"; -import { hasStringProperty } from "./utils"; const TypeId = "~@rivetkit/effect/Registry"; -/** - * One actor registered with the `Registry`. The `buildHandlers` - * effect is run once per wake by the runner to construct - * per-instance state and handlers; the handlers themselves are not - * resolved at registration time. - * - * `state`, when present, carries the persisted-state schema and - * initial-value factory. The runner uses it to seed `c.state` on - * first create and to provide a typed `State` under the state's tag - * inside the build effect's context. - */ -interface RegistryEntry< - Name extends string, - Actions extends Action.Any, - Handlers extends Actor.HandlersFrom, - RX, - State extends ActorState.AnyWithProps = never, -> { - readonly actor: Actor.Actor; - readonly buildHandlers: Effect.Effect; - readonly options: Actor.Options; -} - -type ActorInstance = { - readonly handlers: Record< - string, - (req: { - readonly _tag: string; - readonly action: Action.AnyWithProps; - readonly payload: unknown; - }) => Effect.Effect - >; - readonly scope: Scope.Closeable; - readonly state: Option.Option>; -}; +export type Options = Pick< + Rivetkit.RegistryConfigInput, + "endpoint" | "token" | "namespace" +>; export interface Registry { readonly [TypeId]: typeof TypeId; readonly options: Options; - readonly register: < - Name extends string, - Actions extends Action.Any, - Handlers extends Actor.HandlersFrom, - RX, - State extends ActorState.AnyWithProps = never, - >( - entry: RegistryEntry, - ) => Effect.Effect; - - readonly entries: Effect.Effect< - ReadonlyArray> - >; + readonly rivetkitActors: Map; } -/** - * Service collecting actor defs/builders together with the engine - * connection config. Provided once via `Registry.layer({ ... })` and - * consumed by both `Actor.toLayer` (which registers itself into the - * collector on acquire) and by `Registry.start` / `Registry.test` - * (which materialize the underlying rivetkit registry from the - * collected entries). - */ export const Registry: Context.Service = Context.Service("@rivetkit/effect/Registry"); -export type Options = Pick< - Rivetkit.RegistryConfigInput, - "endpoint" | "token" | "namespace" ->; - export const make = (options: Options = {}): Registry => { - const entries: Array> = []; return Registry.of({ [TypeId]: TypeId, options, - register: (entry) => - Effect.sync(() => { - entries.push(entry); - }), - entries: Effect.sync(() => entries), + rivetkitActors: new Map(), }); }; @@ -118,7 +40,10 @@ export const layer = (options: Options = {}): Layer.Layer => export const serve: Layer.Layer = Layer.effectDiscard( Effect.gen(function* () { const registry = yield* Registry; - const rivetkitRegistry = yield* toRivetkitRegistry(registry); + const rivetkitRegistry = Rivetkit.setup({ + use: Object.fromEntries(registry.rivetkitActors), + ...registry.options, + }); yield* Effect.sync(() => rivetkitRegistry.start()); }), ); @@ -137,7 +62,10 @@ export const test: Layer.Layer = Layer.effect( Client, Effect.gen(function* () { const registry = yield* Registry; - const rivetkitRegistry = yield* toRivetkitRegistry(registry); + const rivetkitRegistry = Rivetkit.setup({ + use: Object.fromEntries(registry.rivetkitActors), + ...registry.options, + }); rivetkitRegistry.config.test = { ...rivetkitRegistry.config.test, enabled: true, @@ -207,254 +135,3 @@ export const test: Layer.Layer = Layer.effect( return Client.of({ callAction }); }), ); - -/** - * Build the underlying rivetkit registry from the collected `Registry` - * entries. The returned registry is configured but not started; callers - * apply mode-specific config (test flags, engine spawn) and then invoke - * `.start()` themselves. - */ -const toRivetkitRegistry = Effect.fnUntraced(function* (registry: Registry) { - const entries = yield* registry.entries; - const instances = new Map(); - const use: Record = {}; - for (const entry of entries) { - use[entry.actor.name] = yield* toRivetkitActor(entry, instances); - } - - return Rivetkit.setup({ - use, - ...registry.options, - }); -}); - -const toRivetkitActor = Effect.fnUntraced(function* ( - { actor, buildHandlers, options }: RegistryEntry, - instances: Map, -) { - // Snapshot the current Effect context so action callbacks - // (which run in rivetkit's plain Promise world) can run - // handler effects against the same services the Registry.start / - // Registry.test layer was provided with. - const services = yield* Effect.context(); - - const actions: Record< - string, - ( - c: Pick, - payload?: unknown, - meta?: unknown, - ) => Promise - > = {}; - for (const action of actor.actions) { - const decodePayload = Schema.decodeUnknownEffect(action.payloadSchema); - const encodeSuccess = Schema.encodeUnknownEffect(action.successSchema); - const encodeError = Schema.encodeUnknownEffect(action.errorSchema); - actions[action._tag] = async (c, payload, meta) => { - const inst = instances.get(c.actorId); - if (!inst) { - throw new Error( - `actor ${actor.name}/${c.actorId} has no handlers (onWake didn't run?)`, - ); - } - const handler = inst.handlers[action._tag]; - if (!handler) { - throw new Error( - `actor ${actor.name} has no handler for action ${action._tag}`, - ); - } - - let pipeline: Effect.Effect = Effect.gen( - function* () { - const decoded = yield* decodePayload(payload).pipe( - Effect.orDie, - ); - const result = yield* handler({ - _tag: action._tag, - action, - payload: decoded, - }).pipe( - Effect.catch((expectedError) => - Effect.gen(function* () { - const error = yield* encodeError( - expectedError, - ).pipe(Effect.orDie); - return yield* Effect.die( - new Rivetkit.UserError( - hasStringProperty("message")(error) - ? error.message - : `${action._tag} failed`, - { - code: hasStringProperty("_tag")( - error, - ) - ? error._tag - : undefined, - metadata: error, - }, - ), - ); - }), - ), - ); - return yield* encodeSuccess(result).pipe(Effect.orDie); - }, - ); - - // Always wrap in a server-side span so the handler has a - // live `currentSpan` even when the caller didn't ship trace - // context (e.g. a non-Effect-SDK client). When trace context - // is present, reattach it as the parent so the server span - // joins the caller's trace. - const rpcMethod = `${actor.name}/${action._tag}`; - const traceMeta = readTraceMeta(meta); - pipeline = pipeline.pipe( - Effect.withSpan(rpcMethod, { - parent: traceMeta - ? Tracer.externalSpan(traceMeta) - : undefined, - kind: "server", - attributes: { - "rpc.system.name": rpcSystem, - "rpc.method": rpcMethod, - }, - }), - ); - - const exit = await Effect.runPromiseExitWith(services)(pipeline); - if (Exit.isSuccess(exit)) return exit.value; - throw Cause.squash(exit.cause); - }; - } - - const { effectOptions, rivetkitOptions } = Actor.splitOptions(options); - const stateDef = effectOptions.state; - const stateDefOption = Option.fromNullishOr(stateDef); - const stateInitialValue = Option.isSome(stateDefOption) - ? yield* Schema.encodeUnknownEffect(stateDef.schema)( - stateDef.initialValue(), - ).pipe(Effect.orDie) - : undefined; - - return Rivetkit.actor({ - actions, - options: rivetkitOptions, - // rivetkit invokes this once at create time and seeds c.state - // with the result. We delegate to the user-supplied `initialValue` - // factory so primitive states (e.g. `Schema.Number`) don't need - // `Schema.withConstructorDefault` boilerplate. - ...(Option.isSome(stateDefOption) - ? { - createState: () => stateInitialValue, - } - : {}), - onWake: async ( - c: Rivetkit.WakeContextOf, - ) => { - const address: Actor.ActorAddress = { - actorId: c.actorId, - name: c.name, - key: c.key, - }; - // Single fused effect: build the wake scope, then run - // `buildHandlers` in that scope with `CurrentAddress` - // provided. Keeping both pieces in one fiber means a - // `buildHandlers` failure shares its cause with the scope it - // would have owned. - const { handlers, scope, state } = await Effect.runPromiseWith( - services, - )( - Effect.gen(function* () { - const scope = yield* Scope.make(); - - const state = Option.isSome(stateDefOption) - ? Option.some( - // `c.state` IS the state — `State` is just a typed - // view + change stream over it. Effect-typed - // read/write so async schema transforms work, - // and `SchemaError` flows through `State.get` / - // `set` / `update` to action handlers. The - // wake-time initial read still dies if persisted - // state can't be decoded — no caller exists yet - // to handle it. `Schema.Top`'s requirements show - // up as `unknown`; the captured `services` - // context satisfies them at runtime, so we erase - // R at the boundary. - (yield* State.make( - () => - Schema.decodeUnknownEffect( - stateDef.schema, - )(c.state), - (next) => - Schema.encodeUnknownEffect( - stateDef.schema, - )(next).pipe( - Effect.tap((encoded) => - Effect.sync(() => { - c.state = encoded; - }), - ), - Effect.asVoid, - ), - ).pipe(Effect.orDie)) as State.State< - unknown, - Schema.SchemaError - >, - ) - : Option.none(); - - const context = Context.mergeAll( - Context.make(Actor.CurrentAddress, address), - Context.make(Scope.Scope, scope), - Context.make( - Actor.Sleep, - Effect.sync(() => c.sleep()), - ), - Option.match(state, { - onNone: () => Context.empty(), - onSome: (s) => Context.make(stateDef, s), - }), - ); - - const handlers = yield* buildHandlers.pipe( - Effect.provide(context), - ); - - return { handlers, scope, state }; - }), - ); - instances.set(c.actorId, { handlers, scope, state }); - }, - onSleep: async ( - c: Rivetkit.SleepContextOf, - ) => { - const inst = instances.get(c.actorId); - if (!inst) return; - instances.delete(c.actorId); - await Effect.runPromiseWith(services)( - Scope.close(inst.scope, Exit.void), - ); - }, - onStateChange: (c, newState) => { - if (Option.isNone(stateDefOption)) return; - const inst = instances.get(c.actorId); - if (!inst || Option.isNone(inst.state)) return; - const stateRef = inst.state.value; - // `c.state` already holds `newState` — decode and notify the - // change stream. The decode is Effect-typed so async schema - // transforms work; we serialize through the State's semaphore - // so the publish order matches the write order. - void Effect.runForkWith(services)( - Semaphore.withPermit( - stateRef.semaphore, - Effect.gen(function* () { - const decoded = yield* Schema.decodeUnknownEffect( - stateDef.schema, - )(newState).pipe(Effect.orDie); - State.publishUnsafe(stateRef, decoded); - }), - ), - ); - }, - }); -}); From 0bc9683a39e31f41e2873d710e3abc02e9b2e5ae Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 14 May 2026 11:59:47 +0200 Subject: [PATCH 151/306] refactor(effect): rename `Handlers` to `ActionHandlers` for clarity in actor APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standardized handler terminology across actor APIs (`Handlers` → `ActionHandlers`). Adjusted type definitions and corresponding logic in `toLayer`, `makeRivetkitActor`, and handler resolution for consistency and readability. --- .../packages/effect/src/Actor.ts | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index ca14f4c7b9..d38489f950 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -102,13 +102,13 @@ export type ActionRequest = } : never; -type HandlerServices = { - readonly [Name in keyof Handlers]: Handlers[Name] extends ( +type ActionHandlerServices = { + readonly [Name in keyof ActionHandlers]: ActionHandlers[Name] extends ( ...args: ReadonlyArray ) => Effect.Effect ? R : never; -}[keyof Handlers]; +}[keyof ActionHandlers]; export type ActorKeyParam = string | Rivetkit.ActorKey; @@ -147,20 +147,22 @@ export interface Actor< readonly name: Name; readonly actions: ReadonlyArray; - of>(handlers: Handlers): Handlers; + of>( + actionHandlers: ActionHandlers, + ): ActionHandlers; toLayer< - Handlers extends HandlersFrom, + ActionHandlers extends ActionHandlersFrom, State extends ActorState.AnyWithProps = never, RX = never, >( - build: Handlers | Effect.Effect, + build: ActionHandlers | Effect.Effect, options?: Options, ): Layer.Layer< never, never, | Exclude - | HandlerServices + | ActionHandlerServices | Action.ServicesServer | Action.ServicesClient | Registry.Registry @@ -183,27 +185,27 @@ export interface Actor< export type Any = Actor; -export type HandlersFrom = { - readonly [Current in Action as Current["_tag"]]: ( - envelope: ActionRequest, - ) => Action.ResultFrom; +export type ActionHandlersFrom = { + readonly [Action in Actions as Action["_tag"]]: ( + envelope: ActionRequest, + ) => Action.ResultFrom; }; const Proto: Omit, "name" | "actions"> = { [TypeId]: TypeId, toLayer< Actions extends Action.AnyWithProps, - Handlers extends HandlersFrom, + ActionHandlers extends ActionHandlersFrom, State extends ActorState.AnyWithProps = never, RX = never, >( this: Actor, - build: Handlers | Effect.Effect, + build: ActionHandlers | Effect.Effect, options: Options = {}, ) { return makeRivetkitActor({ actor: this, - buildHandlers: Effect.isEffect(build) + buildActionHandlers: Effect.isEffect(build) ? build : Effect.succeed(build), options, @@ -342,16 +344,16 @@ export const make = < const makeRivetkitActor = Effect.fnUntraced(function* < Name extends string, Actions extends Action.AnyWithProps, - Handlers extends HandlersFrom, + ActionHandlers extends ActionHandlersFrom, RX, State extends ActorState.AnyWithProps = never, >({ actor, - buildHandlers, + buildActionHandlers, options, }: { readonly actor: Actor; - readonly buildHandlers: Effect.Effect; + readonly buildActionHandlers: Effect.Effect; readonly options: Options; }) { // Snapshot the current Effect context so action callbacks @@ -372,7 +374,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < const instances = MutableHashMap.empty< string, { - readonly handlers: Handlers; + readonly actionHandlers: ActionHandlers; readonly scope: Scope.Closeable; readonly state: Option.Option< State.State< @@ -442,13 +444,13 @@ const makeRivetkitActor = Effect.fnUntraced(function* < }), ); - const handlers = yield* buildHandlers.pipe( + const actionHandlers = yield* buildActionHandlers.pipe( Effect.provide(context), ); yield* Effect.sync(() => MutableHashMap.set(instances, c.actorId, { - handlers, + actionHandlers, scope, state, }), @@ -482,7 +484,8 @@ const makeRivetkitActor = Effect.fnUntraced(function* < instances, c.actorId, ).pipe(Effect.fromOption, Effect.orDie); - const actionHandler = instance.handlers[action._tag]; + const actionHandler = + instance.actionHandlers[action._tag]; const decoded = yield* decodePayload(payload).pipe( Effect.orDie, ); From 0296ea382144ce422f2ec62f2dbf20b898753eef Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 14 May 2026 12:26:13 +0200 Subject: [PATCH 152/306] refactor(effect): improve type safety for action handler resolution and request decoding --- .../packages/effect/src/Actor.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index d38489f950..44319a0177 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -484,16 +484,28 @@ const makeRivetkitActor = Effect.fnUntraced(function* < instances, c.actorId, ).pipe(Effect.fromOption, Effect.orDie); - const actionHandler = - instance.actionHandlers[action._tag]; + // The handler map is keyed by the same action + // definitions being registered here, but + // TypeScript loses that relationship once the + // actions are widened into the RivetKit actions + // record. + const actionHandler = instance.actionHandlers[ + action._tag as keyof ActionHandlers + ] as ( + envelope: ActionRequest, + ) => Action.ResultFrom; const decoded = yield* decodePayload(payload).pipe( Effect.orDie, ); - const result = yield* actionHandler({ + // The payload was decoded with this action's schema, + // so this is the runtime boundary that restores the + // typed envelope expected by the user handler. + const actionRequest = { _tag: action._tag, action, payload: decoded, - }).pipe( + } as ActionRequest; + const result = yield* actionHandler(actionRequest).pipe( Effect.catch((expectedError) => Effect.gen(function* () { const error = yield* encodeError( From ed446df70afe8e8201e04735df517cb0b54900ce Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 14 May 2026 12:52:14 +0200 Subject: [PATCH 153/306] fix(effect): erase actor state schema services --- .../packages/effect/src/Actor.ts | 18 +++++++++++------- .../packages/effect/src/ActorState.ts | 8 +++----- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 44319a0177..40ce743d10 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -341,6 +341,11 @@ export const make = < return self; }; +const eraseStateSchemaServices = ( + state: State.State, +): State.State => + state as State.State; + const makeRivetkitActor = Effect.fnUntraced(function* < Name extends string, Actions extends Action.AnyWithProps, @@ -377,11 +382,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < readonly actionHandlers: ActionHandlers; readonly scope: Scope.Closeable; readonly state: Option.Option< - State.State< - State["schema"]["Type"], - Schema.SchemaError, - unknown - > + State.State >; } >(); @@ -406,7 +407,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < // up as `unknown`; the captured `services` // context satisfies them at runtime, so we erase // R at the boundary. - yield* State.make( + (yield* State.make( () => Schema.decodeUnknownEffect( stateDefOption.value.schema, @@ -422,7 +423,10 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ), Effect.asVoid, ), - ).pipe(Effect.orDie), + ).pipe(Effect.orDie)) as State.State< + ActorState.AnyWithProps["schema"]["Type"], + Schema.SchemaError + >, ) : Option.none(); diff --git a/rivetkit-typescript/packages/effect/src/ActorState.ts b/rivetkit-typescript/packages/effect/src/ActorState.ts index 46fd6ca5c1..fa1d59f7fd 100644 --- a/rivetkit-typescript/packages/effect/src/ActorState.ts +++ b/rivetkit-typescript/packages/effect/src/ActorState.ts @@ -36,13 +36,11 @@ export interface Any { /** * Like `Any`, but with the prop fields (`schema`, `initialValue`) accessible. * Used by the runtime to seed `c.state` and provide the `State` under - * the state's tag. + * the state's tag. The yielded `State` has no visible service requirement + * because schema services are resolved against the actor runner context. */ export interface AnyWithProps - extends Context.Service< - any, - State.State - > { + extends Context.Service> { readonly [TypeId]: typeof TypeId; readonly _tag: string; readonly schema: Schema.Top; From 1af03569c278123f25654c4f6035fff1e707302b Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 14 May 2026 12:54:45 +0200 Subject: [PATCH 154/306] fix(effect): correct MutableHashMap key in actor state cleanup --- rivetkit-typescript/packages/effect/src/Actor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 40ce743d10..f25ee2c033 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -594,7 +594,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ).pipe(Effect.fromOption, Effect.orDie); yield* Scope.close(instance.scope, Exit.void); yield* Effect.sync(() => { - MutableHashMap.remove(c.actorId); + MutableHashMap.remove(instances, c.actorId); }); }), ); From 397698aace372c4f8282221292a80acd1728c691 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 14 May 2026 12:57:47 +0200 Subject: [PATCH 155/306] refactor(effect): remove unused `eraseStateSchemaServices` and add `decodeRivetErrorFromWire` --- rivetkit-typescript/packages/effect/src/Actor.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index f25ee2c033..486b8a7a15 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -27,6 +27,10 @@ import { readTraceMeta, rpcSystem } from "./internal/tracing"; const TypeId = "~@rivetkit/effect/Actor"; +const decodeRivetErrorFromWire = Schema.decodeUnknownEffect( + RivetError.RivetErrorFromWire, +); + export const isActor = (u: unknown): u is Actor => Predicate.hasProperty(u, TypeId); @@ -341,11 +345,6 @@ export const make = < return self; }; -const eraseStateSchemaServices = ( - state: State.State, -): State.State => - state as State.State; - const makeRivetkitActor = Effect.fnUntraced(function* < Name extends string, Actions extends Action.AnyWithProps, From 6ed9902576d273375b913dc8399e307aa7742b02 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 14 May 2026 13:21:16 +0200 Subject: [PATCH 156/306] refactor(effect): pre-compute encode/decode effects once per action --- .../packages/effect/src/Actor.ts | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 486b8a7a15..a4d0fd8ac3 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -243,6 +243,15 @@ const Proto: Omit, "name" | "actions"> = { for (const action of actions) { const tag = action._tag; const rpcMethod = `${self.name}/${tag}`; + const encodePayload = Schema.encodeUnknownEffect( + action.payloadSchema, + ); + const decodeSuccess = Schema.decodeUnknownEffect( + action.successSchema, + ); + const decodeError = Schema.decodeUnknownEffect( + action.errorSchema, + ); // `Effect.fn` wraps the generator in a span named // `rpcMethod` (kind=client + OTel `rpc.*` attrs) // without an extra `pipe(Effect.withSpan(...))`. @@ -259,9 +268,6 @@ const Proto: Omit, "name" | "actions"> = { "rpc.method": rpcMethod, }, })(function* (payload: unknown) { - const encoded = yield* Schema.encodeUnknownEffect( - action.payloadSchema, - )(payload); const span = yield* Effect.currentSpan; const meta: Client.ActionMeta = { trace: { @@ -275,7 +281,8 @@ const Proto: Omit, "name" | "actions"> = { actorName: self.name, key, actionName: tag, - encodedPayload: encoded, + encodedPayload: + yield* encodePayload(payload), meta, }) .pipe( @@ -283,9 +290,7 @@ const Proto: Omit, "name" | "actions"> = { // wire metadata. Fall back to wrapping // the raw RivetError via `RivetErrorFromWire`. Effect.catch((rivetErr) => - Schema.decodeUnknownEffect( - action.errorSchema, - )( + decodeError( (rivetErr as { metadata?: unknown }) .metadata, ).pipe( @@ -293,9 +298,7 @@ const Proto: Omit, "name" | "actions"> = { onSuccess: (typed) => Effect.fail(typed), onFailure: () => - Schema.decodeUnknownEffect( - RivetError.RivetErrorFromWire, - )({ + decodeRivetErrorFromWire({ group: rivetErr.group, code: rivetErr.code, message: @@ -314,9 +317,7 @@ const Proto: Omit, "name" | "actions"> = { ), ), ); - return yield* Schema.decodeUnknownEffect( - action.successSchema, - )(raw); + return yield* decodeSuccess(raw); }) as (p: unknown) => Effect.Effect; } return handle as Handle; @@ -369,10 +370,14 @@ const makeRivetkitActor = Effect.fnUntraced(function* < const { effectOptions, rivetkitOptions } = splitOptions(options); const stateDef = effectOptions.state; const stateDefOption = Option.fromNullishOr(stateDef); + const stateCodec = Option.map(stateDefOption, (def) => ({ + decode: Schema.decodeUnknownEffect(def.schema), + encode: Schema.encodeUnknownEffect(def.schema), + })); const stateInitialValue = Option.isSome(stateDefOption) - ? yield* Schema.encodeUnknownEffect(stateDefOption.value.schema)( - stateDefOption.value.initialValue(), - ).pipe(Effect.orDie) + ? yield* Option.getOrThrow(stateCodec) + .encode(stateDefOption.value.initialValue()) + .pipe(Effect.orDie) : undefined; const instances = MutableHashMap.empty< @@ -393,7 +398,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < Effect.gen(function* () { const scope = yield* Scope.make(); - const state = Option.isSome(stateDefOption) + const state = Option.isSome(stateCodec) ? Option.some( // `c.state` IS the state — `State` is just a typed // view + change stream over it. Effect-typed @@ -407,14 +412,9 @@ const makeRivetkitActor = Effect.fnUntraced(function* < // context satisfies them at runtime, so we erase // R at the boundary. (yield* State.make( - () => - Schema.decodeUnknownEffect( - stateDefOption.value.schema, - )(c.state), + () => stateCodec.value.decode(c.state), (next) => - Schema.encodeUnknownEffect( - stateDefOption.value.schema, - )(next).pipe( + stateCodec.value.encode(next).pipe( Effect.tap((encoded) => Effect.sync(() => { c.state = encoded; @@ -559,7 +559,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ) => { void Effect.runForkWith(services)( Effect.gen(function* () { - if (Option.isNone(stateDefOption)) return; + if (Option.isNone(stateCodec)) return; const instance = yield* MutableHashMap.get( instances, @@ -572,9 +572,9 @@ const makeRivetkitActor = Effect.fnUntraced(function* < yield* Semaphore.withPermit( stateRef.semaphore, Effect.gen(function* () { - const decoded = yield* Schema.decodeUnknownEffect( - stateDefOption.value.schema, - )(newState).pipe(Effect.orDie); + const decoded = yield* stateCodec.value + .decode(newState) + .pipe(Effect.orDie); State.publishUnsafe(stateRef, decoded); }), ); From 92b161adc10f07bb5a70f2b1acb8ee65948d1c86 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 14 May 2026 13:32:24 +0200 Subject: [PATCH 157/306] refactor(effect): inline state initialization logic into `createState` function --- rivetkit-typescript/packages/effect/src/Actor.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index a4d0fd8ac3..ba1125c657 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -374,11 +374,6 @@ const makeRivetkitActor = Effect.fnUntraced(function* < decode: Schema.decodeUnknownEffect(def.schema), encode: Schema.encodeUnknownEffect(def.schema), })); - const stateInitialValue = Option.isSome(stateDefOption) - ? yield* Option.getOrThrow(stateCodec) - .encode(stateDefOption.value.initialValue()) - .pipe(Effect.orDie) - : undefined; const instances = MutableHashMap.empty< string, @@ -603,7 +598,12 @@ const makeRivetkitActor = Effect.fnUntraced(function* < options: rivetkitOptions, onWake, ...(Option.isSome(stateDefOption) - ? { createState: () => stateInitialValue } + ? { + createState: () => + Option.getOrThrow(stateCodec) + .encode(stateDefOption.value.initialValue()) + .pipe(Effect.orDie), + } : {}), actions, onStateChange, From d02623c142a6f0ee7b5cbd025794fe59ef571174 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 14 May 2026 13:43:09 +0200 Subject: [PATCH 158/306] refactor(effect): inline state definition extraction into `stateDefOption` initialization --- rivetkit-typescript/packages/effect/src/Actor.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index ba1125c657..bfc1998324 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -368,8 +368,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < const services = yield* Effect.context(); const { effectOptions, rivetkitOptions } = splitOptions(options); - const stateDef = effectOptions.state; - const stateDefOption = Option.fromNullishOr(stateDef); + const stateDefOption = Option.fromNullishOr(effectOptions.state); const stateCodec = Option.map(stateDefOption, (def) => ({ decode: Schema.decodeUnknownEffect(def.schema), encode: Schema.encodeUnknownEffect(def.schema), From 91a8b2ac33e533c142c1b498e05828170c71f14a Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 14 May 2026 15:04:26 +0200 Subject: [PATCH 159/306] feat(effect): forward db provider through Actor.toLayer and exercise it via RivetkitContext --- .../packages/effect/src/Actor.ts | 14 +++- .../packages/effect/test/e2e.test.ts | 67 +++++++++++++++++ .../packages/effect/test/fixtures/actor.ts | 72 ++++++++++++++++++- 3 files changed, 151 insertions(+), 2 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index bfc1998324..434112f924 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -16,6 +16,7 @@ import { Semaphore, } from "effect"; import * as Rivetkit from "rivetkit"; +import type * as RivetkitDb from "rivetkit/db"; import { hasStringProperty } from "./utils"; import * as Registry from "./Registry"; import type * as Action from "./Action"; @@ -54,6 +55,7 @@ export type RivetkitActorOptions = Pick< export type Options = Readonly & { readonly state?: State; + readonly db?: RivetkitDb.AnyDatabaseProvider; }; const splitOptions = ( @@ -92,6 +94,11 @@ export class Sleep extends Context.Service>()( "@rivetkit/effect/Actor/Sleep", ) {} +export class RivetkitContext extends Context.Service< + RivetkitContext, + Rivetkit.RunContextOf +>()("@rivetkit/effect/Actor/RivetkitContext") {} + export type ActionRequest = A extends Action.Action< infer Tag, @@ -165,7 +172,10 @@ export interface Actor< ): Layer.Layer< never, never, - | Exclude + | Exclude< + RX, + Scope.Scope | CurrentAddress | Sleep | RivetkitContext | State + > | ActionHandlerServices | Action.ServicesServer | Action.ServicesClient @@ -434,6 +444,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < Sleep, Effect.sync(() => c.sleep()), ), + Context.make(RivetkitContext, c), Option.match(state, { onNone: () => Context.empty(), onSome: (s) => @@ -595,6 +606,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < return Rivetkit.actor({ options: rivetkitOptions, + ...(effectOptions.db ? { db: effectOptions.db } : {}), onWake, ...(Option.isSome(stateDefOption) ? { diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 71bb3198e9..ccac33af8b 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -588,4 +588,71 @@ layer(TestLayer)("end-to-end", (it) => { } }), ); + + it.effect("writes through the db captured from RivetkitContext", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate(["t-db-write"]); + const afterFirst = yield* counter.LogEvent({ event: "alpha" }); + const afterSecond = yield* counter.LogEvent({ event: "beta" }); + assert.strictEqual(afterFirst, 1); + assert.strictEqual(afterSecond, 2); + }), + ); + + it.effect("reads rows back through the captured db", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate(["t-db-list"]); + yield* counter.LogEvent({ event: "one" }); + yield* counter.LogEvent({ event: "two" }); + yield* counter.LogEvent({ event: "three" }); + const events = yield* counter.ListEvents(); + assert.deepStrictEqual(events, ["one", "two", "three"]); + }), + ); + + it.effect("isolates db state across actor keys", () => + Effect.gen(function* () { + const client = yield* Counter.client; + const a = client.getOrCreate(["t-db-iso-a"]); + const b = client.getOrCreate(["t-db-iso-b"]); + yield* a.LogEvent({ event: "a1" }); + yield* a.LogEvent({ event: "a2" }); + yield* b.LogEvent({ event: "b1" }); + assert.strictEqual(yield* a.CountEvents(), 2); + assert.strictEqual(yield* b.CountEvents(), 1); + assert.deepStrictEqual(yield* a.ListEvents(), ["a1", "a2"]); + assert.deepStrictEqual(yield* b.ListEvents(), ["b1"]); + }), + ); + + it.effect("persists db rows across a sleep/wake cycle", () => + Effect.gen(function* () { + const counter = (yield* Counter.client).getOrCreate([ + "t-db-persist", + ]); + yield* counter.LogEvent({ event: "before-sleep" }); + + // `PersistAndSleep` signals `c.sleep()` after writing state; the + // engine tears the wake scope down asynchronously. The + // `in-memory Ref` resets to 0 on the next wake, so polling + // `GetCount` until it reads 0 is the deterministic signal that + // a fresh wake started. `TestClock.withLive` runs the poll in + // wall time since the suite otherwise drives `TestClock`. + yield* counter.PersistAndSleep({ amount: 1 }); + const inMemoryAfterWake = yield* counter.GetCount().pipe( + Effect.repeat({ + until: (n) => n === 0, + schedule: Schedule.spaced("100 millis"), + }), + TestClock.withLive, + ); + assert.strictEqual(inMemoryAfterWake, 0); + + yield* counter.LogEvent({ event: "after-wake" }); + assert.deepStrictEqual(yield* counter.ListEvents(), [ + "before-sleep", + "after-wake", + ]); + }), + ); }); diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts index cb185eb59c..78e0adcae0 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actor.ts @@ -9,6 +9,7 @@ import { SchemaTransformation, } from "effect"; import { Action, Actor, ActorState, State } from "@rivetkit/effect"; +import { db, type RawAccess } from "rivetkit/db"; // --- Counter --- @@ -166,6 +167,19 @@ export const GetPersistedState = Action.make("GetPersistedState", { }), }); +export const LogEvent = Action.make("LogEvent", { + payload: { event: Schema.String }, + success: Schema.Number, +}); + +export const ListEvents = Action.make("ListEvents", { + success: Schema.Array(Schema.String), +}); + +export const CountEvents = Action.make("CountEvents", { + success: Schema.Number, +}); + export const Counter = Actor.make("Counter", { actions: [ Increment, @@ -182,6 +196,9 @@ export const Counter = Actor.make("Counter", { PersistTagsAndSleep, PersistScaledAndSleep, GetPersistedState, + LogEvent, + ListEvents, + CountEvents, ], }); @@ -215,6 +232,13 @@ export const CounterLive = Counter.toLayer( const wakeGreeting = greeter.greet("on wake"); const sleep = yield* Actor.Sleep; + // `RivetkitContext`'s `db` widens to `any` against + // `RunContextOf`. The provider configured on + // `Counter.toLayer` below is the `rivetkit/db` raw-access factory, + // so re-narrow to `RawAccess` for typed `execute` calls inside + // handler closures. + const ctx = yield* Actor.RivetkitContext; + const db = ctx.db as RawAccess; // `Flags` is a process-wide Map shared across all tests in the // suite, so the finalizer flag must be namespaced by actor key // to keep cross-test wake/sleep cycles from leaking into each @@ -317,9 +341,55 @@ export const CounterLive = Counter.toLayer( return scaled; }), GetPersistedState: () => State.get(state), + // Per-actor SQLite is provisioned via the `db:` option on + // `Counter.toLayer` below. The build effect destructures `db` + // from `Actor.RivetkitContext`, so handlers reach SQLite + // through the captured client without going through `c.db`. + LogEvent: ({ payload }) => + Effect.tryPromise(async () => { + await db.execute( + "INSERT INTO events (event, created_at) VALUES (?, ?)", + payload.event, + Date.now(), + ); + const rows = await db.execute<{ count: number }>( + "SELECT COUNT(*) as count FROM events", + ); + return rows[0]?.count ?? 0; + }).pipe(Effect.orDie), + ListEvents: () => + Effect.tryPromise(async () => { + const rows = await db.execute<{ event: string }>( + "SELECT event FROM events ORDER BY id ASC", + ); + return rows.map((r) => r.event); + }).pipe(Effect.orDie), + CountEvents: () => + Effect.tryPromise(async () => { + const rows = await db.execute<{ count: number }>( + "SELECT COUNT(*) as count FROM events", + ); + return rows[0]?.count ?? 0; + }).pipe(Effect.orDie), }); }), - { state: CounterState }, + { + state: CounterState, + // Migration runs once before the wake-scope build effect, so the + // destructured `db` is already pointed at a migrated database + // when handlers capture it. + db: db({ + onMigrate: async (client) => { + await client.execute(` + CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + event TEXT NOT NULL, + created_at INTEGER NOT NULL + ) + `); + }, + }), + }, ); // --- Strict --- From 79b2fe104a7684ff6161a2627961e5274e8f6e0c Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 14 May 2026 15:21:05 +0200 Subject: [PATCH 160/306] refactor(effect): replace Option with UndefinedOr for state handling --- .../packages/effect/src/Actor.ts | 103 +++++++++--------- 1 file changed, 54 insertions(+), 49 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 434112f924..f262f79889 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -14,6 +14,7 @@ import { Exit, Cause, Semaphore, + UndefinedOr, } from "effect"; import * as Rivetkit from "rivetkit"; import type * as RivetkitDb from "rivetkit/db"; @@ -378,10 +379,9 @@ const makeRivetkitActor = Effect.fnUntraced(function* < const services = yield* Effect.context(); const { effectOptions, rivetkitOptions } = splitOptions(options); - const stateDefOption = Option.fromNullishOr(effectOptions.state); - const stateCodec = Option.map(stateDefOption, (def) => ({ - decode: Schema.decodeUnknownEffect(def.schema), - encode: Schema.encodeUnknownEffect(def.schema), + const stateCodec = UndefinedOr.map(effectOptions.state, (state) => ({ + decode: Schema.decodeUnknownEffect(state.schema), + encode: Schema.encodeUnknownEffect(state.schema), })); const instances = MutableHashMap.empty< @@ -389,8 +389,9 @@ const makeRivetkitActor = Effect.fnUntraced(function* < { readonly actionHandlers: ActionHandlers; readonly scope: Scope.Closeable; - readonly state: Option.Option< - State.State + readonly state?: State.State< + State["schema"]["Type"], + Schema.SchemaError >; } >(); @@ -402,36 +403,34 @@ const makeRivetkitActor = Effect.fnUntraced(function* < Effect.gen(function* () { const scope = yield* Scope.make(); - const state = Option.isSome(stateCodec) - ? Option.some( - // `c.state` IS the state — `State` is just a typed - // view + change stream over it. Effect-typed - // read/write so async schema transforms work, - // and `SchemaError` flows through `State.get` / - // `set` / `update` to action handlers. The - // wake-time initial read still dies if persisted - // state can't be decoded — no caller exists yet - // to handle it. `Schema.Top`'s requirements show - // up as `unknown`; the captured `services` - // context satisfies them at runtime, so we erase - // R at the boundary. - (yield* State.make( - () => stateCodec.value.decode(c.state), - (next) => - stateCodec.value.encode(next).pipe( - Effect.tap((encoded) => - Effect.sync(() => { - c.state = encoded; - }), - ), - Effect.asVoid, + const state = stateCodec + ? // `c.state` IS the state — `State` is just a typed + // view + change stream over it. Effect-typed + // read/write so async schema transforms work, + // and `SchemaError` flows through `State.get` / + // `set` / `update` to action handlers. The + // wake-time initial read still dies if persisted + // state can't be decoded — no caller exists yet + // to handle it. `Schema.Top`'s requirements show + // up as `unknown`; the captured `services` + // context satisfies them at runtime, so we erase + // R at the boundary. + ((yield* State.make( + () => stateCodec.decode(c.state), + (next) => + stateCodec.encode(next).pipe( + Effect.tap((encoded) => + Effect.sync(() => { + c.state = encoded; + }), ), - ).pipe(Effect.orDie)) as State.State< - ActorState.AnyWithProps["schema"]["Type"], - Schema.SchemaError - >, - ) - : Option.none(); + Effect.asVoid, + ), + ).pipe(Effect.orDie)) as State.State< + ActorState.AnyWithProps["schema"]["Type"], + Schema.SchemaError + >) + : undefined; const context = Context.mergeAll( Context.make(CurrentAddress, { @@ -445,11 +444,12 @@ const makeRivetkitActor = Effect.fnUntraced(function* < Effect.sync(() => c.sleep()), ), Context.make(RivetkitContext, c), - Option.match(state, { - onNone: () => Context.empty(), - onSome: (s) => - Context.make(Option.getOrThrow(stateDefOption), s), - }), + effectOptions.state + ? Context.make( + effectOptions.state, + UndefinedOr.getOrThrow(state), + ) + : Context.empty(), ); const actionHandlers = yield* buildActionHandlers.pipe( @@ -564,23 +564,24 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ) => { void Effect.runForkWith(services)( Effect.gen(function* () { - if (Option.isNone(stateCodec)) return; + if (!stateCodec) return; const instance = yield* MutableHashMap.get( instances, c.actorId, ).pipe(Effect.fromOption, Effect.orDie); - if (Option.isNone(instance.state)) return; + const state = yield* Effect.fromNullishOr(instance.state).pipe( + Effect.orDie, + ); - const stateRef = instance.state.value; yield* Semaphore.withPermit( - stateRef.semaphore, + state.semaphore, Effect.gen(function* () { - const decoded = yield* stateCodec.value + const decoded = yield* stateCodec .decode(newState) .pipe(Effect.orDie); - State.publishUnsafe(stateRef, decoded); + State.publishUnsafe(state, decoded); }), ); }), @@ -608,11 +609,15 @@ const makeRivetkitActor = Effect.fnUntraced(function* < options: rivetkitOptions, ...(effectOptions.db ? { db: effectOptions.db } : {}), onWake, - ...(Option.isSome(stateDefOption) + ...(options.state ? { createState: () => - Option.getOrThrow(stateCodec) - .encode(stateDefOption.value.initialValue()) + UndefinedOr.getOrThrow(stateCodec) + .encode( + UndefinedOr.getOrThrow( + options.state, + ).initialValue(), + ) .pipe(Effect.orDie), } : {}), From fcf51ffac2d246ad1490378bdbfbd9aab5a6af9f Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 14 May 2026 15:29:51 +0200 Subject: [PATCH 161/306] =?UTF-8?q?refactor(effect):=20test/actor.ts=20?= =?UTF-8?q?=E2=86=92=20test/actors.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rivetkit-typescript/packages/effect/test/e2e.test.ts | 2 +- .../packages/effect/test/fixtures/{actor.ts => actors.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename rivetkit-typescript/packages/effect/test/fixtures/{actor.ts => actors.ts} (100%) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index ccac33af8b..b008ae7330 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -22,7 +22,7 @@ import { Unregistered, WakeDecodeFail, WakeDecodeFailLive, -} from "./fixtures/actor"; +} from "./fixtures/actors"; import { TestTracer } from "./fixtures/tracer"; import { prepareNamespace, waitForEnvoy } from "./shared-engine"; diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actor.ts b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts similarity index 100% rename from rivetkit-typescript/packages/effect/test/fixtures/actor.ts rename to rivetkit-typescript/packages/effect/test/fixtures/actors.ts From 050370118d6df3f89cf6c205aef60aa075ac084b Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 14 May 2026 15:48:08 +0200 Subject: [PATCH 162/306] refactor(effect): align test prepareNamespace with setupTest/startEngine Drops the per-file `POST /namespaces` step in favor of the bootstrap-created `default` namespace and upserts the runner config with the same body shape `rivetkit-core::registry::runner_config::ensure_local_normal_runner_config` emits (`{normal: {}, drain_on_version_upgrade: true}`). Per-file isolation shifts from a unique namespace to a unique pool name, which the registry now forwards via `envoy.poolName`. The `Registry.Options` surface gains the `envoy` key and `Registry.test` propagates `envoy.poolName` to `createClient`'s top-level `poolName`. --- .../packages/effect/src/Registry.ts | 6 ++- .../packages/effect/test/e2e.test.ts | 9 +++- .../packages/effect/test/shared-engine.ts | 47 ++++++++----------- 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index aaa3b02ecf..0658cabcbb 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -7,7 +7,7 @@ const TypeId = "~@rivetkit/effect/Registry"; export type Options = Pick< Rivetkit.RegistryConfigInput, - "endpoint" | "token" | "namespace" + "endpoint" | "token" | "namespace" | "envoy" >; export interface Registry { @@ -96,6 +96,10 @@ export const test: Layer.Layer = Layer.effect( RivetkitClient.createClient({ ...registry.options, endpoint: registry.options.endpoint ?? resolvedEndpoint, + // `RegistryConfigInput` nests pool under `envoy.poolName` + // but the client schema reads `poolName` at the top + // level, so propagate it explicitly. + poolName: registry.options.envoy?.poolName, }), ), (c) => Effect.promise(() => c.dispose()), diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index b008ae7330..0cf8f72492 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -79,7 +79,14 @@ const TestLayer = ReadyForEnvoy.pipe( Layer.provide(GreeterLive), Layer.provideMerge(MultiplierLive), Layer.provideMerge(TestTracer.layer()), - Layer.provide(Registry.layer({ endpoint, token, namespace })), + Layer.provide( + Registry.layer({ + endpoint, + token, + namespace, + envoy: { poolName }, + }), + ), ), ), ); diff --git a/rivetkit-typescript/packages/effect/test/shared-engine.ts b/rivetkit-typescript/packages/effect/test/shared-engine.ts index fcfe5d0b97..fb42272c39 100644 --- a/rivetkit-typescript/packages/effect/test/shared-engine.ts +++ b/rivetkit-typescript/packages/effect/test/shared-engine.ts @@ -16,40 +16,32 @@ export interface PreparedNamespace { readonly poolName: string; } +// Mirrors what `setupTest` + `startEngine: true` does internally +// (`rivetkit-core::registry::runner_config::ensure_local_normal_runner_config`): +// reuses the engine's bootstrap-created `default` namespace and only +// upserts a normal runner config with the same body shape core emits. +// Per-file isolation comes from a unique pool name; the registry +// registers its envoy under that pool so envoy routing stays partitioned +// across test files even though they share the namespace. +// +// The engine's `/health` route returns OK as soon as the HTTP servers +// are listening, but the bootstrap workflows (epoxy replica/coordinator, +// default namespace, datacenter ping) keep running in the background. +// Actor wakes need those workflows settled or the first SQLite +// `get_pages` against a fresh bucket fails with `sqlite database was +// not found in this bucket branch`. Probe `getDatacenters` plus an +// idempotent runner-config upsert with `drain_on_version_upgrade: true` +// until both succeed back-to-back; bootstrap is settled by then. export async function prepareNamespace( endpoint: string, - options: { namespace?: string; poolName?: string } = {}, + options: { poolName?: string } = {}, ): Promise { - const namespace = options.namespace ?? `effect-e2e-${randomUUID()}`; - const poolName = options.poolName ?? "default"; - await createNamespace(endpoint, namespace); + const namespace = "default"; + const poolName = options.poolName ?? `effect-e2e-${randomUUID()}`; await upsertNormalRunnerConfig(endpoint, namespace, poolName); return { endpoint, token: TEST_ENGINE_TOKEN, namespace, poolName }; } -async function createNamespace( - endpoint: string, - namespace: string, -): Promise { - const response = await fetch(`${endpoint}/namespaces`, { - method: "POST", - headers: { - Authorization: `Bearer ${TEST_ENGINE_TOKEN}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: namespace, - display_name: `Effect e2e ${namespace}`, - }), - }); - - if (!response.ok) { - throw new Error( - `failed to create namespace ${namespace}: ${response.status} ${await response.text()}`, - ); - } -} - export async function waitForEnvoy( endpoint: string, namespace: string, @@ -127,6 +119,7 @@ async function upsertNormalRunnerConfig( datacenters: { [datacenter]: { normal: {}, + drain_on_version_upgrade: true, }, }, }), From 898cba7b628351976d920657fdec8057d76ea71c Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 14 May 2026 22:44:02 +0200 Subject: [PATCH 163/306] fix(effect): run createState encode effect to a Promise NAPI rejects callback return values it cannot serialize. `createState` previously returned the encode `Effect` itself, whose internal function properties triggered `internal_error: Unknown type: function` from the NAPI bridge. Every actor wake failed in the runtime startup preamble, the engine retried 8 times, then the gateway surfaced `guard/service_unavailable` to callers. Run the encode pipeline through `Effect.runPromiseWith(services)` so the NAPI callback resolves to the encoded bytes, matching how `onWake`, `onSleep`, and action handlers already drain user effects. --- .../packages/effect/src/Actor.ts | 16 ++++--- .../packages/effect/src/Registry.ts | 6 +-- .../packages/effect/test/e2e.test.ts | 9 +--- .../packages/effect/test/shared-engine.ts | 47 +++++++++++-------- 4 files changed, 38 insertions(+), 40 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index f262f79889..cfd2b751d8 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -612,13 +612,15 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ...(options.state ? { createState: () => - UndefinedOr.getOrThrow(stateCodec) - .encode( - UndefinedOr.getOrThrow( - options.state, - ).initialValue(), - ) - .pipe(Effect.orDie), + Effect.runPromiseWith(services)( + UndefinedOr.getOrThrow(stateCodec) + .encode( + UndefinedOr.getOrThrow( + options.state, + ).initialValue(), + ) + .pipe(Effect.orDie), + ), } : {}), actions, diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 0658cabcbb..aaa3b02ecf 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -7,7 +7,7 @@ const TypeId = "~@rivetkit/effect/Registry"; export type Options = Pick< Rivetkit.RegistryConfigInput, - "endpoint" | "token" | "namespace" | "envoy" + "endpoint" | "token" | "namespace" >; export interface Registry { @@ -96,10 +96,6 @@ export const test: Layer.Layer = Layer.effect( RivetkitClient.createClient({ ...registry.options, endpoint: registry.options.endpoint ?? resolvedEndpoint, - // `RegistryConfigInput` nests pool under `envoy.poolName` - // but the client schema reads `poolName` at the top - // level, so propagate it explicitly. - poolName: registry.options.envoy?.poolName, }), ), (c) => Effect.promise(() => c.dispose()), diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 0cf8f72492..b008ae7330 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -79,14 +79,7 @@ const TestLayer = ReadyForEnvoy.pipe( Layer.provide(GreeterLive), Layer.provideMerge(MultiplierLive), Layer.provideMerge(TestTracer.layer()), - Layer.provide( - Registry.layer({ - endpoint, - token, - namespace, - envoy: { poolName }, - }), - ), + Layer.provide(Registry.layer({ endpoint, token, namespace })), ), ), ); diff --git a/rivetkit-typescript/packages/effect/test/shared-engine.ts b/rivetkit-typescript/packages/effect/test/shared-engine.ts index fb42272c39..fcfe5d0b97 100644 --- a/rivetkit-typescript/packages/effect/test/shared-engine.ts +++ b/rivetkit-typescript/packages/effect/test/shared-engine.ts @@ -16,32 +16,40 @@ export interface PreparedNamespace { readonly poolName: string; } -// Mirrors what `setupTest` + `startEngine: true` does internally -// (`rivetkit-core::registry::runner_config::ensure_local_normal_runner_config`): -// reuses the engine's bootstrap-created `default` namespace and only -// upserts a normal runner config with the same body shape core emits. -// Per-file isolation comes from a unique pool name; the registry -// registers its envoy under that pool so envoy routing stays partitioned -// across test files even though they share the namespace. -// -// The engine's `/health` route returns OK as soon as the HTTP servers -// are listening, but the bootstrap workflows (epoxy replica/coordinator, -// default namespace, datacenter ping) keep running in the background. -// Actor wakes need those workflows settled or the first SQLite -// `get_pages` against a fresh bucket fails with `sqlite database was -// not found in this bucket branch`. Probe `getDatacenters` plus an -// idempotent runner-config upsert with `drain_on_version_upgrade: true` -// until both succeed back-to-back; bootstrap is settled by then. export async function prepareNamespace( endpoint: string, - options: { poolName?: string } = {}, + options: { namespace?: string; poolName?: string } = {}, ): Promise { - const namespace = "default"; - const poolName = options.poolName ?? `effect-e2e-${randomUUID()}`; + const namespace = options.namespace ?? `effect-e2e-${randomUUID()}`; + const poolName = options.poolName ?? "default"; + await createNamespace(endpoint, namespace); await upsertNormalRunnerConfig(endpoint, namespace, poolName); return { endpoint, token: TEST_ENGINE_TOKEN, namespace, poolName }; } +async function createNamespace( + endpoint: string, + namespace: string, +): Promise { + const response = await fetch(`${endpoint}/namespaces`, { + method: "POST", + headers: { + Authorization: `Bearer ${TEST_ENGINE_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: namespace, + display_name: `Effect e2e ${namespace}`, + }), + }); + + if (!response.ok) { + throw new Error( + `failed to create namespace ${namespace}: ${response.status} ${await response.text()}`, + ); + } +} + export async function waitForEnvoy( endpoint: string, namespace: string, @@ -119,7 +127,6 @@ async function upsertNormalRunnerConfig( datacenters: { [datacenter]: { normal: {}, - drain_on_version_upgrade: true, }, }, }), From 50ccefda02fbdcf6e4a4dc1327cef0d0fd6b1ec1 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 15 May 2026 10:13:27 +0200 Subject: [PATCH 164/306] refactor(effect): rename Actor.RivetkitContext to RawRivetkitContext --- rivetkit-typescript/packages/effect/src/Actor.ts | 10 +++++----- rivetkit-typescript/packages/effect/test/e2e.test.ts | 2 +- .../packages/effect/test/fixtures/actors.ts | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index cfd2b751d8..ef245c3090 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -95,10 +95,10 @@ export class Sleep extends Context.Service>()( "@rivetkit/effect/Actor/Sleep", ) {} -export class RivetkitContext extends Context.Service< - RivetkitContext, +export class RawRivetkitContext extends Context.Service< + RawRivetkitContext, Rivetkit.RunContextOf ->()("@rivetkit/effect/Actor/RivetkitContext") {} +>()("@rivetkit/effect/Actor/RawRivetkitContext") {} export type ActionRequest = A extends Action.Action< @@ -175,7 +175,7 @@ export interface Actor< never, | Exclude< RX, - Scope.Scope | CurrentAddress | Sleep | RivetkitContext | State + Scope.Scope | CurrentAddress | Sleep | RawRivetkitContext | State > | ActionHandlerServices | Action.ServicesServer @@ -443,7 +443,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < Sleep, Effect.sync(() => c.sleep()), ), - Context.make(RivetkitContext, c), + Context.make(RawRivetkitContext, c), effectOptions.state ? Context.make( effectOptions.state, diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index b008ae7330..439fa67e57 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -589,7 +589,7 @@ layer(TestLayer)("end-to-end", (it) => { }), ); - it.effect("writes through the db captured from RivetkitContext", () => + it.effect("writes through the db captured from RawRivetkitContext", () => Effect.gen(function* () { const counter = (yield* Counter.client).getOrCreate(["t-db-write"]); const afterFirst = yield* counter.LogEvent({ event: "alpha" }); diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts index 78e0adcae0..a116316e37 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts @@ -232,12 +232,12 @@ export const CounterLive = Counter.toLayer( const wakeGreeting = greeter.greet("on wake"); const sleep = yield* Actor.Sleep; - // `RivetkitContext`'s `db` widens to `any` against + // `RawRivetkitContext`'s `db` widens to `any` against // `RunContextOf`. The provider configured on // `Counter.toLayer` below is the `rivetkit/db` raw-access factory, // so re-narrow to `RawAccess` for typed `execute` calls inside // handler closures. - const ctx = yield* Actor.RivetkitContext; + const ctx = yield* Actor.RawRivetkitContext; const db = ctx.db as RawAccess; // `Flags` is a process-wide Map shared across all tests in the // suite, so the finalizer flag must be namespaced by actor key @@ -343,7 +343,7 @@ export const CounterLive = Counter.toLayer( GetPersistedState: () => State.get(state), // Per-actor SQLite is provisioned via the `db:` option on // `Counter.toLayer` below. The build effect destructures `db` - // from `Actor.RivetkitContext`, so handlers reach SQLite + // from `Actor.RawRivetkitContext`, so handlers reach SQLite // through the captured client without going through `c.db`. LogEvent: ({ payload }) => Effect.tryPromise(async () => { From ef4fad6c0c81a380b0cd55e05bb83e81935b5754 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 15 May 2026 10:51:05 +0200 Subject: [PATCH 165/306] feat(effect): add chat actor examples --- examples/effect/src/actors/chat-room/api.ts | 87 +++++++ examples/effect/src/actors/chat-room/live.ts | 225 +++++++++++++++++++ examples/effect/src/actors/directory/api.ts | 25 +++ examples/effect/src/actors/directory/live.ts | 52 +++++ examples/effect/src/actors/mod.ts | 4 +- examples/effect/src/actors/moderator/api.ts | 23 ++ examples/effect/src/actors/moderator/live.ts | 47 ++++ examples/effect/src/client.ts | 75 +++++-- examples/effect/src/main.ts | 8 +- 9 files changed, 527 insertions(+), 19 deletions(-) create mode 100644 examples/effect/src/actors/chat-room/api.ts create mode 100644 examples/effect/src/actors/chat-room/live.ts create mode 100644 examples/effect/src/actors/directory/api.ts create mode 100644 examples/effect/src/actors/directory/live.ts create mode 100644 examples/effect/src/actors/moderator/api.ts create mode 100644 examples/effect/src/actors/moderator/live.ts diff --git a/examples/effect/src/actors/chat-room/api.ts b/examples/effect/src/actors/chat-room/api.ts new file mode 100644 index 0000000000..b4d0028251 --- /dev/null +++ b/examples/effect/src/actors/chat-room/api.ts @@ -0,0 +1,87 @@ +import { Schema } from "effect"; +import { Action, Actor } from "@rivetkit/effect"; + +export const Member = Schema.Struct({ + name: Schema.String, + joinedAt: Schema.Number, +}); + +export const Message = Schema.Struct({ + id: Schema.Number, + sender: Schema.String, + text: Schema.String, + createdAt: Schema.Number, +}); + +export const SendMessageResult = Schema.Struct({ + ok: Schema.Boolean, + reason: Schema.optionalKey(Schema.String), + createdAt: Schema.optionalKey(Schema.Number), +}); + +// The plain RivetKit example uses createState input to name the room at +// creation time. The Effect SDK does not expose create input yet, so this +// action initializes the persisted room state explicitly after getOrCreate. +export const Initialize = Action.make("Initialize", { + payload: { name: Schema.String }, +}); + +export const Join = Action.make("Join", { + payload: { name: Schema.String }, + success: Member, +}); + +export const Leave = Action.make("Leave", { + payload: { name: Schema.String }, +}); + +export const SendMessage = Action.make("SendMessage", { + payload: { + sender: Schema.String, + text: Schema.String, + }, + success: SendMessageResult, +}); + +export const GetHistory = Action.make("GetHistory", { + success: Schema.Array(Message), +}); + +export const GetMembers = Action.make("GetMembers", { + success: Schema.Array(Member), +}); + +export const ScheduleAnnouncement = Action.make("ScheduleAnnouncement", { + payload: { + text: Schema.String, + delayMs: Schema.Number, + }, + success: Schema.Struct({ + firesAt: Schema.Number, + }), +}); + +// Scheduled actions receive the same single schema payload that normal +// Effect actions use. This replaces the plain SDK example's positional +// triggerAnnouncement(text) action. +export const TriggerAnnouncement = Action.make("TriggerAnnouncement", { + payload: { text: Schema.String }, +}); + +// The plain RivetKit example closes the room from onDestroy. The Effect SDK +// does not expose onDestroy yet, so archive performs cleanup before destroy. +export const Archive = Action.make("Archive"); + +export const ChatRoom = Actor.make("chatRoom", { + actions: [ + Initialize, + Join, + Leave, + SendMessage, + GetHistory, + GetMembers, + ScheduleAnnouncement, + TriggerAnnouncement, + Archive, + ], +}); diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts new file mode 100644 index 0000000000..819fdc262d --- /dev/null +++ b/examples/effect/src/actors/chat-room/live.ts @@ -0,0 +1,225 @@ +import { Effect, Schema } from "effect"; +import { Actor, ActorState, State } from "@rivetkit/effect"; +import { db, type RawAccess } from "rivetkit/db"; +import { ChatRoom } from "./api.ts"; + +interface ModerationVerdict { + readonly approved: boolean; + readonly reason?: string; +} + +const ChatRoomState = ActorState.make("ChatRoomState", { + schema: Schema.Struct({ + name: Schema.String, + members: Schema.Array( + Schema.Struct({ + name: Schema.String, + joinedAt: Schema.Number, + }), + ), + wakeCount: Schema.Number, + initialized: Schema.Boolean, + }), + initialValue: () => ({ + name: "", + members: [], + wakeCount: 0, + initialized: false, + }), +}); + +export const ChatRoomLive = ChatRoom.toLayer( + Effect.gen(function* () { + const state = yield* ChatRoomState; + // RawRivetkitContext is the escape hatch for features that the + // Effect SDK has not modeled yet, including broadcasts, scheduling, + // destroy, SQLite access, and server-side actor clients. + const ctx = yield* Actor.RawRivetkitContext; + const database = ctx.db as RawAccess; + const address = yield* Actor.CurrentAddress; + // The plain SDK example stores this in createVars. The Effect SDK + // does not expose vars yet, so the wake-scope closure owns it. + const sessionId = crypto.randomUUID(); + + yield* State.update(state, (current) => ({ + ...current, + wakeCount: current.wakeCount + 1, + })).pipe(Effect.orDie); + + yield* Effect.log("room awake", { + actorId: address.actorId, + key: address.key.join("/"), + sessionId, + }); + + yield* Effect.addFinalizer(() => + Effect.gen(function* () { + const current = yield* State.get(state).pipe(Effect.orDie); + yield* Effect.log("room sleeping", { + actorId: address.actorId, + key: address.key.join("/"), + roomName: current.name, + sessionId, + wakeCount: current.wakeCount, + }); + }), + ); + + const directory = () => + // Server-side Effect actor clients are not available yet. Use the + // raw RivetKit actor client and keep the action shape explicit. + ctx.client().directory.getOrCreate(["main"]); + const moderator = () => + // The normal example uses a typed registry client here. This raw + // client keeps the runtime behavior while giving up type inference. + ctx.client().moderator.getOrCreate(["main"]); + + const roomName = State.get(state).pipe( + Effect.orDie, + Effect.map((s) => s.name), + ); + + return ChatRoom.of({ + Initialize: ({ payload }) => + // This replaces createState(input). Callers should initialize + // a room before actions that depend on a persisted room name. + State.update(state, (current) => { + if (current.initialized) return current; + return { + ...current, + name: payload.name, + members: [], + initialized: true, + }; + }), + Join: ({ payload }) => + Effect.gen(function* () { + const member = { name: payload.name, joinedAt: Date.now() }; + const next = yield* State.updateAndGet( + state, + (current) => ({ + ...current, + members: [...current.members, member], + }), + ); + + ctx.broadcast("memberJoined", { member }); + + if (next.name !== "") { + // Directory registration is still actor-to-actor RPC, but + // it uses the Effect action name and object payload. + yield* Effect.tryPromise(() => + directory().RegisterRoom({ name: next.name }), + ).pipe(Effect.orDie); + } + + return member; + }), + Leave: ({ payload }) => + Effect.gen(function* () { + yield* State.update(state, (current) => ({ + ...current, + members: current.members.filter( + (member) => member.name !== payload.name, + ), + })).pipe(Effect.orDie); + ctx.broadcast("memberLeft", { name: payload.name }); + }), + SendMessage: ({ payload }) => + Effect.gen(function* () { + // The normal example sends moderation work through a + // completable queue drained by run(). The Effect SDK does + // not expose queues or run loops yet, so moderation is a + // direct actor RPC and has no queue timeout path. + const verdict = yield* Effect.tryPromise( + () => + moderator().Review({ + text: payload.text, + }) as Promise, + ).pipe(Effect.orDie); + + if (!verdict.approved) { + return { ok: false, reason: verdict.reason }; + } + + const createdAt = Date.now(); + yield* Effect.tryPromise(() => + database.execute( + "INSERT INTO messages (sender, text, created_at) VALUES (?, ?, ?)", + payload.sender, + payload.text, + createdAt, + ), + ).pipe(Effect.orDie); + + ctx.broadcast("newMessage", { + sender: payload.sender, + text: payload.text, + createdAt, + }); + return { ok: true, createdAt }; + }), + GetHistory: () => + Effect.tryPromise(() => + database.execute<{ + id: number; + sender: string; + text: string; + createdAt: number; + }>( + "SELECT id, sender, text, created_at as createdAt FROM messages ORDER BY id", + ), + ).pipe(Effect.orDie), + GetMembers: () => + State.get(state).pipe( + Effect.orDie, + Effect.map((s) => s.members), + ), + ScheduleAnnouncement: ({ payload }) => + Effect.sync(() => { + const firesAt = Date.now() + payload.delayMs; + // The raw scheduler dispatches the Effect action by name + // with the same object payload that a client would send. + ctx.schedule.after(payload.delayMs, "TriggerAnnouncement", { + text: payload.text, + }); + return { firesAt }; + }), + TriggerAnnouncement: ({ payload }) => + Effect.sync(() => { + ctx.broadcast("announcement", { text: payload.text }); + }), + Archive: () => + Effect.gen(function* () { + const name = yield* roomName; + if (name !== "") { + // This only covers destruction through Archive. A future + // Effect onDestroy hook would cover every destroy path. + yield* Effect.tryPromise(() => + directory().CloseRoom({ name }), + ).pipe(Effect.orDie); + } + yield* Effect.sync(() => { + ctx.destroy(); + }); + }), + }); + }), + { + state: ChatRoomState, + db: db({ + onMigrate: async (client) => { + await client.execute(` + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sender TEXT NOT NULL, + text TEXT NOT NULL, + created_at INTEGER NOT NULL + ) + `); + }, + }), + name: "Chat Room", + icon: "comments", + }, +); diff --git a/examples/effect/src/actors/directory/api.ts b/examples/effect/src/actors/directory/api.ts new file mode 100644 index 0000000000..afe46923c6 --- /dev/null +++ b/examples/effect/src/actors/directory/api.ts @@ -0,0 +1,25 @@ +import { Schema } from "effect"; +import { Action, Actor } from "@rivetkit/effect"; + +export const RoomEntry = Schema.Struct({ + name: Schema.String, + openedAt: Schema.Number, + closedAt: Schema.optionalKey(Schema.Number), +}); + +export const RegisterRoom = Action.make("RegisterRoom", { + payload: { name: Schema.String }, +}); + +export const CloseRoom = Action.make("CloseRoom", { + payload: { name: Schema.String }, +}); + +export const ListRooms = Action.make("ListRooms", { + success: Schema.Array(RoomEntry), +}); + +export const Directory = Actor.make("directory", { + actions: [RegisterRoom, CloseRoom, ListRooms], +}); + diff --git a/examples/effect/src/actors/directory/live.ts b/examples/effect/src/actors/directory/live.ts new file mode 100644 index 0000000000..b16f0708a8 --- /dev/null +++ b/examples/effect/src/actors/directory/live.ts @@ -0,0 +1,52 @@ +import { Effect, Schema } from "effect"; +import { ActorState, State } from "@rivetkit/effect"; +import { Directory } from "./api.ts"; + +const DirectoryState = ActorState.make("DirectoryState", { + schema: Schema.Struct({ + rooms: Schema.Array( + Schema.Struct({ + name: Schema.String, + openedAt: Schema.Number, + closedAt: Schema.optionalKey(Schema.Number), + }), + ), + }), + initialValue: () => ({ rooms: [] }), +}); + +export const DirectoryLive = Directory.toLayer( + Effect.gen(function* () { + const state = yield* DirectoryState; + + return Directory.of({ + RegisterRoom: ({ payload }) => + // State writes go through Effect Schema validation. This + // example treats schema failures as defects instead of adding + // typed error channels to the action contract. + State.update(state, (current) => { + if (current.rooms.some((room) => room.name === payload.name)) { + return current; + } + + return { + rooms: [ + ...current.rooms, + { name: payload.name, openedAt: Date.now() }, + ], + }; + }).pipe(Effect.orDie), + CloseRoom: ({ payload }) => + State.update(state, (current) => ({ + rooms: current.rooms.map((room) => + room.name === payload.name + ? { ...room, closedAt: Date.now() } + : room, + ), + })).pipe(Effect.orDie), + ListRooms: () => + State.get(state).pipe(Effect.orDie, Effect.map((s) => s.rooms)), + }); + }), + { state: DirectoryState, name: "Directory", icon: "folder" }, +); diff --git a/examples/effect/src/actors/mod.ts b/examples/effect/src/actors/mod.ts index 8db603ffa2..21ccfe82eb 100644 --- a/examples/effect/src/actors/mod.ts +++ b/examples/effect/src/actors/mod.ts @@ -1,2 +1,4 @@ export * from "./counter/api.ts" -// export * from "./chat-room/api.ts" +export * from "./directory/api.ts" +export * from "./moderator/api.ts" +export * from "./chat-room/api.ts" diff --git a/examples/effect/src/actors/moderator/api.ts b/examples/effect/src/actors/moderator/api.ts new file mode 100644 index 0000000000..7d4f126648 --- /dev/null +++ b/examples/effect/src/actors/moderator/api.ts @@ -0,0 +1,23 @@ +import { Schema } from "effect"; +import { Action, Actor } from "@rivetkit/effect"; + +export const ModerationVerdict = Schema.Struct({ + approved: Schema.Boolean, + reason: Schema.optionalKey(Schema.String), +}); + +export const Review = Action.make("Review", { + payload: { text: Schema.String }, + success: ModerationVerdict, +}); + +export const Stats = Action.make("Stats", { + success: Schema.Struct({ + reviewed: Schema.Number, + }), +}); + +export const Moderator = Actor.make("moderator", { + actions: [Review, Stats], +}); + diff --git a/examples/effect/src/actors/moderator/live.ts b/examples/effect/src/actors/moderator/live.ts new file mode 100644 index 0000000000..d31f88e255 --- /dev/null +++ b/examples/effect/src/actors/moderator/live.ts @@ -0,0 +1,47 @@ +import { Effect, Schema } from "effect"; +import { ActorState, State } from "@rivetkit/effect"; +import { Moderator } from "./api.ts"; + +const ModeratorState = ActorState.make("ModeratorState", { + schema: Schema.Struct({ + bannedWords: Schema.Array(Schema.String), + reviewed: Schema.Number, + }), + initialValue: () => ({ bannedWords: ["spam", "scam"], reviewed: 0 }), +}); + +export const ModeratorLive = Moderator.toLayer( + Effect.gen(function* () { + const state = yield* ModeratorState; + + return Moderator.of({ + Review: ({ payload }) => + Effect.gen(function* () { + // State writes go through Effect Schema validation. This + // example treats schema failures as defects instead of adding + // typed error channels to the action contract. + const next = yield* State.updateAndGet(state, (current) => ({ + ...current, + reviewed: current.reviewed + 1, + })).pipe(Effect.orDie); + const lower = payload.text.toLowerCase(); + const hit = next.bannedWords.find((word) => + lower.includes(word), + ); + + return hit + ? { + approved: false, + reason: `contains banned word "${hit}"`, + } + : { approved: true }; + }), + Stats: () => + State.get(state).pipe( + Effect.orDie, + Effect.map(({ reviewed }) => ({ reviewed })), + ), + }); + }), + { state: ModeratorState, name: "Moderator", icon: "shield" }, +); diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 316fbab908..8b8b6c5438 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,16 +1,59 @@ -import { Effect } from "effect" -import { Client } from "@rivetkit/effect" -import { Counter /*, IncrementBy */ } from "./actors/counter/api.ts" +import { Effect } from "effect"; +import { Client } from "@rivetkit/effect"; +import { Counter /*, IncrementBy */ } from "./actors/counter/api.ts"; +import { ChatRoom } from "./actors/chat-room/api.ts"; +import { Directory } from "./actors/directory/api.ts"; +import { Moderator } from "./actors/moderator/api.ts"; const program = Effect.gen(function* () { - const counterClient = yield* Counter.client - const counter = counterClient.getOrCreate(["counter-effect"]) + const counterClient = yield* Counter.client; + const counter = counterClient.getOrCreate(["counter-effect"]); - const count = yield* counter.Increment({ amount: 5 }) - yield* Effect.log(`Increment(5) -> ${count}`) + const count = yield* counter.Increment({ amount: 5 }); + yield* Effect.log(`Increment(5) -> ${count}`); - const total = yield* counter.GetCount() - yield* Effect.log(`GetCount -> ${total}`) + const total = yield* counter.GetCount(); + yield* Effect.log(`GetCount -> ${total}`); + + const chatRoomClient = yield* ChatRoom.client; + const directoryClient = yield* Directory.client; + const moderatorClient = yield* Moderator.client; + + const room = chatRoomClient.getOrCreate(["effect-room"]); + const directory = directoryClient.getOrCreate(["main"]); + const moderator = moderatorClient.getOrCreate(["main"]); + + yield* room.Initialize({ name: "effect-room" }); + yield* Effect.log(`ChatRoom.Initialize`); + + const member = yield* room.Join({ name: "Alice" }); + yield* Effect.log(`ChatRoom.Join -> ${member.name}`); + + const sent = yield* room.SendMessage({ + sender: "Alice", + text: "hello from Effect", + }); + yield* Effect.log(`ChatRoom.SendMessage -> ok=${sent.ok}`); + + const rejected = yield* room.SendMessage({ + sender: "Alice", + text: "this contains spam", + }); + yield* Effect.log( + `ChatRoom.SendMessage rejected -> ok=${rejected.ok} reason=${rejected.reason}`, + ); + + const history = yield* room.GetHistory(); + yield* Effect.log(`ChatRoom.GetHistory -> ${history.length} messages`); + + const members = yield* room.GetMembers(); + yield* Effect.log(`ChatRoom.GetMembers -> ${members.length} members`); + + const rooms = yield* directory.ListRooms(); + yield* Effect.log(`Directory.ListRooms -> ${rooms.length} rooms`); + + const stats = yield* moderator.Stats(); + yield* Effect.log(`Moderator.Stats -> reviewed=${stats.reviewed}`); // const newCount = yield* counter.send(IncrementBy({ amount: 3 })) // yield* Effect.log(`IncrementBy(3) -> ${newCount}`) @@ -25,19 +68,19 @@ const program = Effect.gen(function* () { // round-trips through a UserError on the wire and decodes back // into the original error class — caught by the outer // `catchTag("CounterOverflowError", ...)`. - const overflowed = yield* counter.Increment({ amount: 100 }) - yield* Effect.log(`Increment(100) [unexpected success]: ${overflowed}`) + const overflowed = yield* counter.Increment({ amount: 100 }); + yield* Effect.log(`Increment(100) [unexpected success]: ${overflowed}`); }).pipe( Effect.catchTag("CounterOverflowError", (e) => Effect.log( `CounterOverflowError caught: limit=${e.limit} message="${e.message}"`, ), ), -) +); -const ClientLayer = Client.layer({ endpoint: "http://127.0.0.1:6420" }) +const ClientLayer = Client.layer({ endpoint: "http://127.0.0.1:6420" }); program.pipe(Effect.provide(ClientLayer), Effect.runPromise).catch((err) => { - console.error("client failed:", err) - process.exit(1) -}) + console.error("client failed:", err); + process.exit(1); +}); diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index 25152c512c..7095f8acbc 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -2,11 +2,15 @@ import { Layer } from "effect" import { NodeRuntime } from "@effect/platform-node" import { Registry } from "@rivetkit/effect" import { CounterLive } from "./actors/counter/live.ts" -// import { ChatRoomLive } from "./actors/chat-room/live.ts" +import { ChatRoomLive } from "./actors/chat-room/live.ts" +import { DirectoryLive } from "./actors/directory/live.ts" +import { ModeratorLive } from "./actors/moderator/live.ts" const ActorsLayer = Layer.mergeAll( CounterLive, -// ChatRoomLive, + DirectoryLive, + ModeratorLive, + ChatRoomLive, ) // Engine config defaults to spawning a local rivet-engine process and From eadb2cecfb8188f3b8a57f4fa63117d6a3f214ec Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 11:21:41 +0200 Subject: [PATCH 166/306] docs(effect): simplify Actor.client documentation formatting --- rivetkit-typescript/packages/effect/src/Actor.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index ef245c3090..975744edad 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -175,7 +175,11 @@ export interface Actor< never, | Exclude< RX, - Scope.Scope | CurrentAddress | Sleep | RawRivetkitContext | State + | Scope.Scope + | CurrentAddress + | Sleep + | RawRivetkitContext + | State > | ActionHandlerServices | Action.ServicesServer @@ -187,9 +191,7 @@ export interface Actor< * Effect-yielded typed accessor for this actor. Provide a * `Client.layer({ ... })` once at the program root; every * `yield* SomeActor.client` then dispatches through the same - * transport. Per-call signatures are `Effect` — schema services are pulled in at the - * getter level via `Action.ServicesClient`. + * transport. */ readonly client: Effect.Effect< TypedAccessor, From 00d672259611ed9a06eafe4c0af9e4bdc68a41a3 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 11:22:02 +0200 Subject: [PATCH 167/306] refactor(effect): reorder imports in Actor.ts for clarity --- .../packages/effect/src/Actor.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 975744edad..f720fbc34a 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -1,31 +1,30 @@ import { + Cause, Context, Effect, + Exit, identity, Layer, + MutableHashMap, Predicate, + Record, Schema, Scope, + Semaphore, Struct, - Record, - MutableHashMap, - Option, Tracer, - Exit, - Cause, - Semaphore, UndefinedOr, } from "effect"; import * as Rivetkit from "rivetkit"; import type * as RivetkitDb from "rivetkit/db"; -import { hasStringProperty } from "./utils"; -import * as Registry from "./Registry"; import type * as Action from "./Action"; import type * as ActorState from "./ActorState"; import * as Client from "./Client"; -import * as State from "./State"; -import * as RivetError from "./RivetError"; import { readTraceMeta, rpcSystem } from "./internal/tracing"; +import * as Registry from "./Registry"; +import * as RivetError from "./RivetError"; +import * as State from "./State"; +import { hasStringProperty } from "./utils"; const TypeId = "~@rivetkit/effect/Actor"; From 27c7c2645c10969ddea6281510dcb96d55dd2ea7 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 11:24:16 +0200 Subject: [PATCH 168/306] refactor(effect): simplify action tag handling in Actor method generation --- rivetkit-typescript/packages/effect/src/Actor.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index f720fbc34a..73e2200f87 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -253,8 +253,7 @@ const Proto: Omit, "name" | "actions"> = { (p: unknown) => Effect.Effect > = {}; for (const action of actions) { - const tag = action._tag; - const rpcMethod = `${self.name}/${tag}`; + const rpcMethod = `${self.name}/${action._tag}`; const encodePayload = Schema.encodeUnknownEffect( action.payloadSchema, ); @@ -273,7 +272,7 @@ const Proto: Omit, "name" | "actions"> = { // wrapper can reattach it as the handler's // parent. Same pattern as Effect's RPC layer // (`RpcClient.ts`). - handle[tag] = Effect.fn(rpcMethod, { + handle[action._tag] = Effect.fn(rpcMethod, { kind: "client", attributes: { "rpc.system.name": rpcSystem, @@ -292,7 +291,7 @@ const Proto: Omit, "name" | "actions"> = { .callAction({ actorName: self.name, key, - actionName: tag, + actionName: action._tag, encodedPayload: yield* encodePayload(payload), meta, From 8a06bf68cf20ccbf05a8e4b72fd9768ae7a0876c Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 11:24:32 +0200 Subject: [PATCH 169/306] docs(effect): remove outdated comments in Actor.ts --- rivetkit-typescript/packages/effect/src/Actor.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 73e2200f87..f840cd176e 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -263,15 +263,6 @@ const Proto: Omit, "name" | "actions"> = { const decodeError = Schema.decodeUnknownEffect( action.errorSchema, ); - // `Effect.fn` wraps the generator in a span named - // `rpcMethod` (kind=client + OTel `rpc.*` attrs) - // without an extra `pipe(Effect.withSpan(...))`. - // The active span inside is the one whose IDs - // the body reads via `Effect.currentSpan` and - // ships as `meta.trace`, so the server-side - // wrapper can reattach it as the handler's - // parent. Same pattern as Effect's RPC layer - // (`RpcClient.ts`). handle[action._tag] = Effect.fn(rpcMethod, { kind: "client", attributes: { From 07b39bc3e4aa694bd5971eeab410d9ce73acc5fa Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 11:30:49 +0200 Subject: [PATCH 170/306] docs(effect): refine Client.service documentation for clarity --- rivetkit-typescript/packages/effect/src/Client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 36b74f0bc6..b70d968cdd 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -28,7 +28,7 @@ export interface ActionMeta { /** * Service holding the rivetkit client transport. Provided once via * `Client.layer({ ... })`. Consumed by `Actor.client` to dispatch - * action calls through a single shared transport. + * action calls through a single and shared transport. */ export class Client extends Context.Service< Client, From dd013f2ed582d132b1b516470193879f378f6fe9 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 11:36:12 +0200 Subject: [PATCH 171/306] refactor(effect): rename TypedAccessor to Accessor in Actor.ts --- rivetkit-typescript/packages/effect/src/Actor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index f840cd176e..5b5da1c1b2 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -142,9 +142,9 @@ export type Handle = { * Yielded by `Actor.client`. Address an actor instance by key, then * dispatch typed action calls against the returned `Handle`. */ -export interface TypedAccessor { +export type Accessor = { readonly getOrCreate: (key: ActorKeyParam) => Handle; -} +}; /** * A Rivet Actor contract. It carries the action schemas and @@ -193,7 +193,7 @@ export interface Actor< * transport. */ readonly client: Effect.Effect< - TypedAccessor, + Accessor, never, Client.Client | Action.ServicesClient >; From f99d1dd9448ef71d279fe5e715097050cc494a10 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 12:21:26 +0200 Subject: [PATCH 172/306] refactor(effect): rename callAction to action and update method usage in Client, Registry, and Actor --- .../packages/effect/src/Actor.ts | 132 +++++++++--------- .../packages/effect/src/Client.ts | 92 ++++++------ .../packages/effect/src/Registry.ts | 60 ++++---- 3 files changed, 145 insertions(+), 139 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 5b5da1c1b2..9e98f8b99d 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -245,15 +245,9 @@ const Proto: Omit, "name" | "actions"> = { const self = this as Any; return Effect.gen(function* () { const client = yield* Client.Client; - const actions = self.actions; return { getOrCreate: (key: ActorKeyParam) => { - const handle: Record< - string, - (p: unknown) => Effect.Effect - > = {}; - for (const action of actions) { - const rpcMethod = `${self.name}/${action._tag}`; + return Record.fromIterableWith(self.actions, (action) => { const encodePayload = Schema.encodeUnknownEffect( action.payloadSchema, ); @@ -263,66 +257,76 @@ const Proto: Omit, "name" | "actions"> = { const decodeError = Schema.decodeUnknownEffect( action.errorSchema, ); - handle[action._tag] = Effect.fn(rpcMethod, { - kind: "client", - attributes: { - "rpc.system.name": rpcSystem, - "rpc.method": rpcMethod, - }, - })(function* (payload: unknown) { - const span = yield* Effect.currentSpan; - const meta: Client.ActionMeta = { - trace: { - traceId: span.traceId, - spanId: span.spanId, - sampled: span.sampled, + + const rpcMethod = `${self.name}/${action._tag}`; + + return [ + action._tag, + Effect.fn(rpcMethod, { + kind: "client", + attributes: { + "rpc.system.name": rpcSystem, + "rpc.method": rpcMethod, }, - }; - const raw = yield* client - .callAction({ - actorName: self.name, - key, - actionName: action._tag, - encodedPayload: - yield* encodePayload(payload), - meta, - }) - .pipe( - // Try `errorSchema` first against the - // wire metadata. Fall back to wrapping - // the raw RivetError via `RivetErrorFromWire`. - Effect.catch((rivetErr) => - decodeError( - (rivetErr as { metadata?: unknown }) - .metadata, - ).pipe( - Effect.matchEffect({ - onSuccess: (typed) => - Effect.fail(typed), - onFailure: () => - decodeRivetErrorFromWire({ - group: rivetErr.group, - code: rivetErr.code, - message: - rivetErr.message, - metadata: ( - rivetErr as { - metadata?: unknown; - } - ).metadata, - }).pipe( - Effect.flatMap( - Effect.fail, + })(function* (payload: unknown) { + const span = yield* Effect.currentSpan; + const meta: Client.ActionMeta = { + trace: { + traceId: span.traceId, + spanId: span.spanId, + sampled: span.sampled, + }, + }; + const raw = yield* client + .action({ + actorName: self.name, + key, + actionName: action._tag, + encodedPayload: + yield* encodePayload(payload), + meta, + }) + .pipe( + // Try `errorSchema` first against the + // wire metadata. Fall back to wrapping + // the raw RivetError via `RivetErrorFromWire`. + Effect.catch((rivetErr) => + decodeError( + ( + rivetErr as { + metadata?: unknown; + } + ).metadata, + ).pipe( + Effect.matchEffect({ + onSuccess: (typed) => + Effect.fail(typed), + onFailure: () => + decodeRivetErrorFromWire( + { + group: rivetErr.group, + code: rivetErr.code, + message: + rivetErr.message, + metadata: ( + rivetErr as { + metadata?: unknown; + } + ).metadata, + }, + ).pipe( + Effect.flatMap( + Effect.fail, + ), ), - ), - }), + }), + ), ), - ), - ); - return yield* decodeSuccess(raw); - }) as (p: unknown) => Effect.Effect; - } - return handle as Handle; + ); + return yield* decodeSuccess(raw); + }), + ]; + }) as Handle; }, }; }); diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index b70d968cdd..c9f408c869 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -44,56 +44,62 @@ export class Client extends Context.Service< * `args` entry. It's a generic envelope (`ActionMeta`) so the SDK * can grow cross-cutting fields without changing the wire shape. */ - readonly callAction: (params: { - readonly actorName: string; - readonly key: ActorKeyParam; - readonly actionName: string; - readonly encodedPayload: unknown; - readonly meta?: ActionMeta; - }) => Effect.Effect; + readonly action: ( + params: { + readonly actorName: string; + readonly key: ActorKeyParam; + readonly actionName: string; + readonly encodedPayload: unknown; + readonly meta?: ActionMeta; + } & RivetkitClient.ActorActionOptions, + ) => Effect.Effect; } >()("@rivetkit/effect/Client") { - static layer(options: ClientOptions = {}): Layer.Layer { - return Layer.effect( + static readonly layer = ( + options: ClientOptions = {}, + ): Layer.Layer => + Layer.effect( Client, Effect.sync(() => { const rivetkitClient = RivetkitClient.createClient(options); - const callAction: ClientService["callAction"] = ({ - actorName, - key, - actionName, - encodedPayload, - meta, - }) => - Effect.tryPromise({ - try: () => - rivetkitClient[actorName].getOrCreate(key).action({ - name: actionName, - args: meta - ? [encodedPayload, meta] - : [encodedPayload], - }), - catch: (cause) => - cause instanceof Rivetkit.RivetError - ? cause - : new Rivetkit.RivetError( - "client", - "unknown", - cause instanceof Error - ? cause.message - : String(cause), - { - cause: - cause instanceof Error - ? cause - : undefined, - }, - ), - }); - return Client.of({ callAction }); + return Client.of({ + action: ({ + actorName, + key, + actionName, + encodedPayload, + meta, + }) => + Effect.tryPromise({ + try: () => + rivetkitClient + .getOrCreate(actorName, key) + .action({ + name: actionName, + args: meta + ? [encodedPayload, meta] + : [encodedPayload], + }), + catch: (cause) => + cause instanceof Rivetkit.RivetError + ? cause + : new Rivetkit.RivetError( + "client", + "unknown", + cause instanceof Error + ? cause.message + : String(cause), + { + cause: + cause instanceof Error + ? cause + : undefined, + }, + ), + }), + }); }), ); - } } export type ClientService = Client["Service"]; diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index aaa3b02ecf..e2a866809f 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -101,37 +101,33 @@ export const test: Layer.Layer = Layer.effect( (c) => Effect.promise(() => c.dispose()), ); - const callAction: ClientService["callAction"] = ({ - actorName, - key, - actionName, - encodedPayload, - meta, - }) => - Effect.tryPromise({ - try: () => - rivetkitClient[actorName].getOrCreate(key).action({ - name: actionName, - args: meta ? [encodedPayload, meta] : [encodedPayload], - }), - catch: (cause) => - cause instanceof Rivetkit.RivetError - ? cause - : new Rivetkit.RivetError( - "client", - "unknown", - cause instanceof Error - ? cause.message - : String(cause), - { - cause: - cause instanceof Error - ? cause - : undefined, - }, - ), - }); - - return Client.of({ callAction }); + return Client.of({ + action: ({ actorName, key, actionName, encodedPayload, meta }) => + Effect.tryPromise({ + try: () => + rivetkitClient[actorName].getOrCreate(key).action({ + name: actionName, + args: meta + ? [encodedPayload, meta] + : [encodedPayload], + }), + catch: (cause) => + cause instanceof Rivetkit.RivetError + ? cause + : new Rivetkit.RivetError( + "client", + "unknown", + cause instanceof Error + ? cause.message + : String(cause), + { + cause: + cause instanceof Error + ? cause + : undefined, + }, + ), + }), + }); }), ); From 2add1cfbf73751711d5c93871736deb6e56b77db Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 12:22:47 +0200 Subject: [PATCH 173/306] refactor(effect): streamline getOrCreate method in Actor by removing redundant block structure --- rivetkit-typescript/packages/effect/src/Actor.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 9e98f8b99d..c7feb61e19 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -246,8 +246,8 @@ const Proto: Omit, "name" | "actions"> = { return Effect.gen(function* () { const client = yield* Client.Client; return { - getOrCreate: (key: ActorKeyParam) => { - return Record.fromIterableWith(self.actions, (action) => { + getOrCreate: (key: ActorKeyParam) => + Record.fromIterableWith(self.actions, (action) => { const encodePayload = Schema.encodeUnknownEffect( action.payloadSchema, ); @@ -326,8 +326,7 @@ const Proto: Omit, "name" | "actions"> = { return yield* decodeSuccess(raw); }), ]; - }) as Handle; - }, + }) as Handle, }; }); }, From 9ce3e771248a1fa01d96273bd44d1c1daf1298f9 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 13:25:59 +0200 Subject: [PATCH 174/306] refactor(effect): centralize actor accessor creation and simplify Client implementation --- .../packages/effect/src/Actor.ts | 98 +-------- .../packages/effect/src/Client.ts | 193 ++++++++++++------ .../packages/effect/src/Registry.ts | 29 +-- 3 files changed, 131 insertions(+), 189 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index c7feb61e19..0102f10699 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -28,10 +28,6 @@ import { hasStringProperty } from "./utils"; const TypeId = "~@rivetkit/effect/Actor"; -const decodeRivetErrorFromWire = Schema.decodeUnknownEffect( - RivetError.RivetErrorFromWire, -); - export const isActor = (u: unknown): u is Actor => Predicate.hasProperty(u, TypeId); @@ -121,7 +117,7 @@ type ActionHandlerServices = { : never; }[keyof ActionHandlers]; -export type ActorKeyParam = string | Rivetkit.ActorKey; +export type AccessorKeyParam = string | Rivetkit.ActorKey; /** * A typed handle for one actor instance. Each action becomes a @@ -143,7 +139,7 @@ export type Handle = { * dispatch typed action calls against the returned `Handle`. */ export type Accessor = { - readonly getOrCreate: (key: ActorKeyParam) => Handle; + readonly getOrCreate: (key: AccessorKeyParam) => Handle; }; /** @@ -242,93 +238,9 @@ const Proto: Omit, "name" | "actions"> = { ); }, get client() { - const self = this as Any; - return Effect.gen(function* () { - const client = yield* Client.Client; - return { - getOrCreate: (key: ActorKeyParam) => - Record.fromIterableWith(self.actions, (action) => { - const encodePayload = Schema.encodeUnknownEffect( - action.payloadSchema, - ); - const decodeSuccess = Schema.decodeUnknownEffect( - action.successSchema, - ); - const decodeError = Schema.decodeUnknownEffect( - action.errorSchema, - ); - - const rpcMethod = `${self.name}/${action._tag}`; - - return [ - action._tag, - Effect.fn(rpcMethod, { - kind: "client", - attributes: { - "rpc.system.name": rpcSystem, - "rpc.method": rpcMethod, - }, - })(function* (payload: unknown) { - const span = yield* Effect.currentSpan; - const meta: Client.ActionMeta = { - trace: { - traceId: span.traceId, - spanId: span.spanId, - sampled: span.sampled, - }, - }; - const raw = yield* client - .action({ - actorName: self.name, - key, - actionName: action._tag, - encodedPayload: - yield* encodePayload(payload), - meta, - }) - .pipe( - // Try `errorSchema` first against the - // wire metadata. Fall back to wrapping - // the raw RivetError via `RivetErrorFromWire`. - Effect.catch((rivetErr) => - decodeError( - ( - rivetErr as { - metadata?: unknown; - } - ).metadata, - ).pipe( - Effect.matchEffect({ - onSuccess: (typed) => - Effect.fail(typed), - onFailure: () => - decodeRivetErrorFromWire( - { - group: rivetErr.group, - code: rivetErr.code, - message: - rivetErr.message, - metadata: ( - rivetErr as { - metadata?: unknown; - } - ).metadata, - }, - ).pipe( - Effect.flatMap( - Effect.fail, - ), - ), - }), - ), - ), - ); - return yield* decodeSuccess(raw); - }), - ]; - }) as Handle, - }; - }); + return Client.Client.asEffect().pipe( + Effect.map((client) => client.makeActorAccessor(this as Any)), + ); }, of: identity, }; diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index c9f408c869..15c04fe203 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -1,8 +1,11 @@ -import { Context, Effect, Layer } from "effect"; +import { Context, Effect, Layer, Schema } from "effect"; +import * as Record from "effect/Record"; import * as Rivetkit from "rivetkit"; import * as RivetkitClient from "rivetkit/client"; -import type { ActorKeyParam } from "./Actor"; -import type { TraceMeta } from "./internal/tracing"; +import type * as Action from "./Action"; +import type * as Actor from "./Actor"; +import { rpcSystem, type TraceMeta } from "./internal/tracing"; +import * as RivetError from "./RivetError"; /** * Connection options for the Rivet Engine client transport. Mirrors @@ -25,81 +28,135 @@ export interface ActionMeta { readonly trace?: TraceMeta; } -/** - * Service holding the rivetkit client transport. Provided once via - * `Client.layer({ ... })`. Consumed by `Actor.client` to dispatch - * action calls through a single and shared transport. - */ export class Client extends Context.Service< Client, { - /** - * Generic action dispatch. Returns the raw, undecoded result from - * the wire. On rejection from the underlying transport, surfaces - * the rivetkit `RivetError` instance via `Effect.fail` — the - * caller decides whether to decode `metadata` as a typed error or - * wrap it through the wire codec. - * - * `meta`, when provided, rides the wire as the second positional - * `args` entry. It's a generic envelope (`ActionMeta`) so the SDK - * can grow cross-cutting fields without changing the wire shape. - */ - readonly action: ( - params: { - readonly actorName: string; - readonly key: ActorKeyParam; - readonly actionName: string; - readonly encodedPayload: unknown; - readonly meta?: ActionMeta; - } & RivetkitClient.ActorActionOptions, - ) => Effect.Effect; + readonly makeActorAccessor: ( + actor: Actor.Actor, + ) => Actor.Accessor; } >()("@rivetkit/effect/Client") { - static readonly layer = ( - options: ClientOptions = {}, - ): Layer.Layer => - Layer.effect( - Client, - Effect.sync(() => { - const rivetkitClient = RivetkitClient.createClient(options); - return Client.of({ - action: ({ - actorName, + static readonly fromRivetkitClient = ( + rivetkitClient: RivetkitClient.Client>, + ): ClientService => + Client.of({ + makeActorAccessor: (actor) => ({ + getOrCreate: (key) => { + const rivetkitActorHandle = rivetkitClient.getOrCreate( + actor.name, key, - actionName, - encodedPayload, - meta, - }) => - Effect.tryPromise({ - try: () => - rivetkitClient - .getOrCreate(actorName, key) - .action({ - name: actionName, - args: meta - ? [encodedPayload, meta] - : [encodedPayload], - }), - catch: (cause) => - cause instanceof Rivetkit.RivetError - ? cause - : new Rivetkit.RivetError( - "client", - "unknown", - cause instanceof Error - ? cause.message - : String(cause), - { - cause: + ); + + return Record.fromIterableWith(actor.actions, (action) => { + const encodePayload = Schema.encodeUnknownEffect( + action.payloadSchema, + ); + const decodeSuccess = Schema.decodeUnknownEffect( + action.successSchema, + ); + const decodeError = Schema.decodeUnknownEffect( + action.errorSchema, + ); + + const rpcMethod = `${actor.name}/${action._tag}`; + + return [ + action._tag, + Effect.fn(rpcMethod, { + kind: "client", + attributes: { + "rpc.system.name": rpcSystem, + "rpc.method": rpcMethod, + }, + })(function* (payload: unknown) { + const span = yield* Effect.currentSpan; + const meta: ActionMeta = { + trace: { + traceId: span.traceId, + spanId: span.spanId, + sampled: span.sampled, + }, + }; + const encodedPayload = + yield* encodePayload(payload); + const raw = yield* Effect.tryPromise({ + try: () => + rivetkitActorHandle.action({ + name: action._tag, + args: [encodedPayload, meta], + }), + catch: (cause) => + cause instanceof Rivetkit.RivetError + ? cause + : new Rivetkit.RivetError( + "client", + "unknown", cause instanceof Error - ? cause - : undefined, - }, + ? cause.message + : String(cause), + { + cause: + cause instanceof + Error + ? cause + : undefined, + }, + ), + }).pipe( + // Try `errorSchema` first against the + // wire metadata. Fall back to wrapping + // the raw RivetError via `RivetErrorFromWire`. + Effect.catch((rivetErr) => + decodeError( + ( + rivetErr as { + metadata?: unknown; + } + ).metadata, + ).pipe( + Effect.matchEffect({ + onSuccess: (typed) => + Effect.fail(typed), + onFailure: () => + decodeRivetErrorFromWire({ + group: rivetErr.group, + code: rivetErr.code, + message: + rivetErr.message, + metadata: ( + rivetErr as { + metadata?: unknown; + } + ).metadata, + }).pipe( + Effect.flatMap( + Effect.fail, + ), + ), + }), ), - }), - }); + ), + ); + return yield* decodeSuccess(raw); + }), + ]; + }) as Actor.Handle<(typeof actor.actions)[number]>; + }, }), + }); + + static readonly layer = (options: ClientOptions = {}) => + Layer.effect( + Client, + Effect.acquireRelease( + Effect.sync(() => RivetkitClient.createClient(options)), + (c) => Effect.promise(() => c.dispose()), + ).pipe(Effect.map(Client.fromRivetkitClient)), ); } +const decodeRivetErrorFromWire = Schema.decodeUnknownEffect( + RivetError.RivetErrorFromWire, +); + export type ClientService = Client["Service"]; diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index e2a866809f..963dc6dd1b 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -101,33 +101,6 @@ export const test: Layer.Layer = Layer.effect( (c) => Effect.promise(() => c.dispose()), ); - return Client.of({ - action: ({ actorName, key, actionName, encodedPayload, meta }) => - Effect.tryPromise({ - try: () => - rivetkitClient[actorName].getOrCreate(key).action({ - name: actionName, - args: meta - ? [encodedPayload, meta] - : [encodedPayload], - }), - catch: (cause) => - cause instanceof Rivetkit.RivetError - ? cause - : new Rivetkit.RivetError( - "client", - "unknown", - cause instanceof Error - ? cause.message - : String(cause), - { - cause: - cause instanceof Error - ? cause - : undefined, - }, - ), - }), - }); + return Client.fromRivetkitClient(rivetkitClient); }), ); From 90f52929eee43107afa08dd43b11281a3c9ab95b Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 14:07:06 +0200 Subject: [PATCH 175/306] refactor(effect): simplify Client implementation and adjust Registry to use refactored Client structure --- .../packages/effect/src/Client.ts | 231 +++++++++--------- .../packages/effect/src/Registry.ts | 23 +- .../packages/effect/src/RivetError.ts | 3 + .../packages/effect/src/mod.ts | 2 +- 4 files changed, 126 insertions(+), 133 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 15c04fe203..007fb53051 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -7,12 +7,14 @@ import type * as Actor from "./Actor"; import { rpcSystem, type TraceMeta } from "./internal/tracing"; import * as RivetError from "./RivetError"; +const TypeId = "~@rivetkit/effect/Client"; + /** * Connection options for the Rivet Engine client transport. Mirrors * the `(endpoint, token, namespace)` subset of rivetkit's * `ClientConfigInput`. */ -export type ClientOptions = Pick< +export type Options = Pick< RivetkitClient.ClientConfigInput, "endpoint" | "token" | "namespace" >; @@ -28,97 +30,105 @@ export interface ActionMeta { readonly trace?: TraceMeta; } -export class Client extends Context.Service< - Client, - { - readonly makeActorAccessor: ( - actor: Actor.Actor, - ) => Actor.Accessor; - } ->()("@rivetkit/effect/Client") { - static readonly fromRivetkitClient = ( - rivetkitClient: RivetkitClient.Client>, - ): ClientService => - Client.of({ - makeActorAccessor: (actor) => ({ - getOrCreate: (key) => { - const rivetkitActorHandle = rivetkitClient.getOrCreate( - actor.name, - key, - ); +export interface Client { + readonly [TypeId]: typeof TypeId; + + readonly makeActorAccessor: ( + actor: Actor.Actor, + ) => Actor.Accessor; +} + +export const Client: Context.Service = Context.Service( + "@rivetkit/effect/Client", +); - return Record.fromIterableWith(actor.actions, (action) => { - const encodePayload = Schema.encodeUnknownEffect( - action.payloadSchema, - ); - const decodeSuccess = Schema.decodeUnknownEffect( - action.successSchema, - ); - const decodeError = Schema.decodeUnknownEffect( - action.errorSchema, - ); +export const make = Effect.fnUntraced(function* (options: Options = {}) { + const rivetkitClient = yield* Effect.acquireRelease( + Effect.sync(() => RivetkitClient.createClient(options)), + (c) => Effect.promise(() => c.dispose()), + ); + + return Client.of({ + [TypeId]: TypeId, + makeActorAccessor: (actor) => ({ + getOrCreate: (key) => { + const rivetkitActorHandle = rivetkitClient.getOrCreate( + actor.name, + key, + ); + + return Record.fromIterableWith(actor.actions, (action) => { + const encodePayload = Schema.encodeUnknownEffect( + action.payloadSchema, + ); + const decodeSuccess = Schema.decodeUnknownEffect( + action.successSchema, + ); + const decodeError = Schema.decodeUnknownEffect( + action.errorSchema, + ); - const rpcMethod = `${actor.name}/${action._tag}`; + const rpcMethod = `${actor.name}/${action._tag}`; - return [ - action._tag, - Effect.fn(rpcMethod, { - kind: "client", - attributes: { - "rpc.system.name": rpcSystem, - "rpc.method": rpcMethod, + return [ + action._tag, + Effect.fn(rpcMethod, { + kind: "client", + attributes: { + "rpc.system.name": rpcSystem, + "rpc.method": rpcMethod, + }, + })(function* (payload: unknown) { + const span = yield* Effect.currentSpan; + const meta: ActionMeta = { + trace: { + traceId: span.traceId, + spanId: span.spanId, + sampled: span.sampled, }, - })(function* (payload: unknown) { - const span = yield* Effect.currentSpan; - const meta: ActionMeta = { - trace: { - traceId: span.traceId, - spanId: span.spanId, - sampled: span.sampled, - }, - }; - const encodedPayload = - yield* encodePayload(payload); - const raw = yield* Effect.tryPromise({ - try: () => - rivetkitActorHandle.action({ - name: action._tag, - args: [encodedPayload, meta], - }), - catch: (cause) => - cause instanceof Rivetkit.RivetError - ? cause - : new Rivetkit.RivetError( - "client", - "unknown", - cause instanceof Error - ? cause.message - : String(cause), + }; + const encodedPayload = + yield* encodePayload(payload); + const raw = yield* Effect.tryPromise({ + try: () => + rivetkitActorHandle.action({ + name: action._tag, + args: [encodedPayload, meta], + }), + catch: (cause) => + cause instanceof Rivetkit.RivetError + ? cause + : new Rivetkit.RivetError( + "client", + "unknown", + cause instanceof Error + ? cause.message + : String(cause), + { + cause: + cause instanceof Error + ? cause + : undefined, + }, + ), + }).pipe( + // Try `errorSchema` first against the + // wire metadata. Fall back to wrapping + // the raw RivetError via `RivetErrorFromWire`. + Effect.catch((rivetErr) => + decodeError( + ( + rivetErr as { + metadata?: unknown; + } + ).metadata, + ).pipe( + Effect.matchEffect({ + onSuccess: (typed) => + Effect.fail(typed), + onFailure: () => + RivetError.decodeRivetErrorFromWire( { - cause: - cause instanceof - Error - ? cause - : undefined, - }, - ), - }).pipe( - // Try `errorSchema` first against the - // wire metadata. Fall back to wrapping - // the raw RivetError via `RivetErrorFromWire`. - Effect.catch((rivetErr) => - decodeError( - ( - rivetErr as { - metadata?: unknown; - } - ).metadata, - ).pipe( - Effect.matchEffect({ - onSuccess: (typed) => - Effect.fail(typed), - onFailure: () => - decodeRivetErrorFromWire({ group: rivetErr.group, code: rivetErr.code, message: @@ -128,35 +138,22 @@ export class Client extends Context.Service< metadata?: unknown; } ).metadata, - }).pipe( - Effect.flatMap( - Effect.fail, - ), - ), - }), - ), + }, + ).pipe( + Effect.flatMap(Effect.fail), + ), + }), ), - ); - return yield* decodeSuccess(raw); - }), - ]; - }) as Actor.Handle<(typeof actor.actions)[number]>; - }, - }), - }); - - static readonly layer = (options: ClientOptions = {}) => - Layer.effect( - Client, - Effect.acquireRelease( - Effect.sync(() => RivetkitClient.createClient(options)), - (c) => Effect.promise(() => c.dispose()), - ).pipe(Effect.map(Client.fromRivetkitClient)), - ); -} - -const decodeRivetErrorFromWire = Schema.decodeUnknownEffect( - RivetError.RivetErrorFromWire, -); + ), + ); + return yield* decodeSuccess(raw); + }), + ]; + }) as Actor.Handle<(typeof actor.actions)[number]>; + }, + }), + }); +}); -export type ClientService = Client["Service"]; +export const layer = (options: Options = {}): Layer.Layer => + Layer.effect(Client, make(options)); diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 963dc6dd1b..099d6a46b7 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -1,7 +1,6 @@ import { Context, Effect, Layer } from "effect"; import * as Rivetkit from "rivetkit"; -import * as RivetkitClient from "rivetkit/client"; -import { Client, type ClientService } from "./Client"; +import * as Client from "./Client"; const TypeId = "~@rivetkit/effect/Registry"; @@ -21,7 +20,7 @@ export interface Registry { export const Registry: Context.Service = Context.Service("@rivetkit/effect/Registry"); -export const make = (options: Options = {}): Registry => { +const make = (options: Options = {}): Registry => { return Registry.of({ [TypeId]: TypeId, options, @@ -58,8 +57,8 @@ export const serve: Layer.Layer = Layer.effectDiscard( * expose a public `shutdown()` today; only the SIGINT handler can * drive `#runShutdown`. This matches `setupTest`'s existing behavior. */ -export const test: Layer.Layer = Layer.effect( - Client, +export const test: Layer.Layer = Layer.effect( + Client.Client, Effect.gen(function* () { const registry = yield* Registry; const rivetkitRegistry = Rivetkit.setup({ @@ -91,16 +90,10 @@ export const test: Layer.Layer = Layer.effect( // endpoint to the client so `createClient` doesn't fall back // to its (warning-emitting) default. const resolvedEndpoint = rivetkitRegistry.parseConfig().endpoint; - const rivetkitClient = yield* Effect.acquireRelease( - Effect.sync(() => - RivetkitClient.createClient({ - ...registry.options, - endpoint: registry.options.endpoint ?? resolvedEndpoint, - }), - ), - (c) => Effect.promise(() => c.dispose()), - ); - return Client.fromRivetkitClient(rivetkitClient); + return yield* Client.make({ + ...registry.options, + endpoint: registry.options.endpoint ?? resolvedEndpoint, + }); }), ); diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index a9754ee0d0..85dc39ed74 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -56,3 +56,6 @@ export const RivetErrorFromWire = Wire.pipe( }), }), ); + +export const decodeRivetErrorFromWire = + Schema.decodeUnknownEffect(RivetErrorFromWire); diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index 54fdf33bf9..36335a955a 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,7 +1,7 @@ export * as Action from "./Action"; export * as Actor from "./Actor"; export * as ActorState from "./ActorState"; -export { Client } from "./Client"; +export * as Client from "./Client"; export * as Registry from "./Registry"; export * as RivetError from "./RivetError"; export * as State from "./State"; From 73efb98d6faaf8f754bd843775978098659d10be Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 14:11:48 +0200 Subject: [PATCH 176/306] refactor(effect): update RivetError import to use `type` for improved clarity --- rivetkit-typescript/packages/effect/src/Actor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 0102f10699..b6b650b4f1 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -22,7 +22,7 @@ import type * as ActorState from "./ActorState"; import * as Client from "./Client"; import { readTraceMeta, rpcSystem } from "./internal/tracing"; import * as Registry from "./Registry"; -import * as RivetError from "./RivetError"; +import type * as RivetError from "./RivetError"; import * as State from "./State"; import { hasStringProperty } from "./utils"; From 721365cd590e7e566ed34c431df68b20b80f0d31 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 14:15:13 +0200 Subject: [PATCH 177/306] refactor(effect): add abort signal support to Client action calls --- rivetkit-typescript/packages/effect/src/Client.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 007fb53051..57e0fc4a7b 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -90,10 +90,11 @@ export const make = Effect.fnUntraced(function* (options: Options = {}) { const encodedPayload = yield* encodePayload(payload); const raw = yield* Effect.tryPromise({ - try: () => + try: (abortSignal) => rivetkitActorHandle.action({ name: action._tag, args: [encodedPayload, meta], + signal: abortSignal, }), catch: (cause) => cause instanceof Rivetkit.RivetError From 363fb6d65c0ba9df59e7f8d64c5c488b2b19b79b Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 14:56:36 +0200 Subject: [PATCH 178/306] refactor(effect): simplify client type definition in Actor --- rivetkit-typescript/packages/effect/src/Actor.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index b6b650b4f1..f39b98d480 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -188,11 +188,7 @@ export interface Actor< * `yield* SomeActor.client` then dispatches through the same * transport. */ - readonly client: Effect.Effect< - Accessor, - never, - Client.Client | Action.ServicesClient - >; + readonly client: Effect.Effect, never, Client.Client>; } export type Any = Actor; From 9d87dd1f1125f92cc57f30fb82986db7b0457f5e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 16:37:08 +0200 Subject: [PATCH 179/306] feat(effect): introduce granular error reason classes and update RivetError structure --- .../packages/effect/src/RivetError.ts | 680 +++++++++++++++++- .../packages/effect/test/e2e.test.ts | 4 +- 2 files changed, 651 insertions(+), 33 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 85dc39ed74..42ac8bb710 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -1,23 +1,426 @@ -import { Schema, SchemaGetter } from "effect"; +import { Duration, Predicate, Schema, SchemaGetter } from "effect"; import * as Rivetkit from "rivetkit"; +const ReasonTypeId = "~@rivetkit/effect/RivetError/Reason" as const; +const TypeId = "~@rivetkit/effect/RivetError" as const; + +// ============================================================================ +// Reason classes +// ============================================================================ +// +// One class per semantic infrastructure-failure category exposed by the +// engine and client. Each reason is a `Schema.TaggedErrorClass` with an +// `isRetryable` getter so callers can match on the reason `_tag` (via +// `Effect.catchReason` / `Effect.catchReasons` / `Match`) and decide +// retry policy without hand-rolling group/code switches. +// +// User-defined errors (thrown via `UserError` inside an actor action) are +// the domain layer: they ride through on the action's declared +// `errorSchema` and arrive in the typed error channel directly. They +// only fall through to the generic `UserError` reason below when the +// action did not declare a matching schema. + +/** `auth.forbidden` — `onAuth` rejected the request. */ +export class Forbidden extends Schema.TaggedErrorClass( + `${ReasonTypeId}/Forbidden`, +)("Forbidden", { + message: Schema.String, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return false; + } +} + +/** `actor.not_found` — gateway target resolution returned no actor. */ +export class ActorNotFound extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ActorNotFound`, +)("ActorNotFound", { + message: Schema.String, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return false; + } +} + +/** `actor.stopping` — request arrived while actor is shutting down. */ +export class ActorStopping extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ActorStopping`, +)("ActorStopping", { + message: Schema.String, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return false; + } +} + +/** `actor.restarting` — actor mid-restart; retry after `retryAfter`. */ +export class ActorRestarting extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ActorRestarting`, +)("ActorRestarting", { + message: Schema.String, + retryAfter: Schema.optional(Schema.Duration), + phase: Schema.optional( + Schema.Literals(["stopping", "sleeping", "waking", "runner_shutdown"]), + ), +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return true; + } +} + +/** `actor.action_not_found` — no action by that name on the actor. */ +export class ActionNotFound extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ActionNotFound`, +)("ActionNotFound", { + message: Schema.String, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return false; + } +} + +/** `actor.action_timed_out` — server-side action timeout. */ +export class ActionTimedOut extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ActionTimedOut`, +)("ActionTimedOut", { + message: Schema.String, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return true; + } +} + +/** `actor.aborted` — action explicitly aborted server-side. */ +export class ActionAborted extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ActionAborted`, +)("ActionAborted", { + message: Schema.String, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return false; + } +} + +/** `actor.overloaded` — actor channel at capacity. */ +export class Overloaded extends Schema.TaggedErrorClass( + `${ReasonTypeId}/Overloaded`, +)("Overloaded", { + message: Schema.String, + channel: Schema.optional(Schema.String), + capacity: Schema.optional(Schema.Number), + operation: Schema.optional(Schema.String), +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return true; + } +} + +/** `message.incoming_too_long` / `message.outgoing_too_long`. */ +export class MessageTooLong extends Schema.TaggedErrorClass( + `${ReasonTypeId}/MessageTooLong`, +)("MessageTooLong", { + message: Schema.String, + direction: Schema.Literals(["incoming", "outgoing"]), +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return false; + } +} + +const queueRetryableCodes = new Set(["full", "timed_out"]); + +/** `queue.*` — queue-related server errors. `code` keeps the raw engine code. */ +export class QueueError extends Schema.TaggedErrorClass( + `${ReasonTypeId}/QueueError`, +)("QueueError", { + message: Schema.String, + code: Schema.String, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return queueRetryableCodes.has(this.code); + } +} + +/** `encoding.invalid` — unsupported encoding negotiated. */ +export class InvalidEncoding extends Schema.TaggedErrorClass( + `${ReasonTypeId}/InvalidEncoding`, +)("InvalidEncoding", { + message: Schema.String, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return false; + } +} + +/** `request.invalid` — malformed `ActorQuery` or request payload. */ +export class InvalidRequest extends Schema.TaggedErrorClass( + `${ReasonTypeId}/InvalidRequest`, +)("InvalidRequest", { + message: Schema.String, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return false; + } +} + +/** `client.connection_open_failed` — websocket open failed. */ +export class ConnectionOpenFailed extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ConnectionOpenFailed`, +)("ConnectionOpenFailed", { + message: Schema.String, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return true; + } +} + +/** `client.get_params_failed` — user `getParams()` callback threw. */ +export class GetParamsFailed extends Schema.TaggedErrorClass( + `${ReasonTypeId}/GetParamsFailed`, +)("GetParamsFailed", { + message: Schema.String, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return false; + } +} + +/** `ws.going_away` / generic transport drop — connection lost, retry. */ +export class ConnectionLost extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ConnectionLost`, +)("ConnectionLost", { + message: Schema.String, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return true; + } +} + +const guardRetryableCodes = new Set([ + "actor_runner_failed", + "actor_ready_timeout", + "service_unavailable", +]); + +/** `guard.*` — engine guard/scheduler failures; `code` keeps the raw engine code. */ +export class GuardError extends Schema.TaggedErrorClass( + `${ReasonTypeId}/GuardError`, +)("GuardError", { + message: Schema.String, + code: Schema.String, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return guardRetryableCodes.has(this.code); + } +} + +/** + * Open-ended user error reason. Used when the actor threw `UserError` but + * the failing action did not declare a matching schema in its `error` + * field — so we can't surface it as a typed domain error in the Effect + * error channel. + * + * Actions that declare their user errors via `Action.make({ error: ... })` + * receive those errors **typed** in the error channel; this reason is + * the catch-all for everything else. + */ +export class UserError extends Schema.TaggedErrorClass( + `${ReasonTypeId}/UserError`, +)("UserError", { + message: Schema.String, + code: Schema.String, + metadata: Schema.optional(Schema.Unknown), +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return false; + } +} + /** - * The cross-boundary Rivet error. Wraps the underlying - * `rivetkit.RivetError` instance on its `error` field, preserving - * `instanceof` checks and direct access to `group` / `code` / - * `message` / `metadata`. + * Sanitized internal error from `rivetkit-core`. Original details live in + * the server logs (or set `RIVET_EXPOSE_ERRORS=1` to inline them in dev). + */ +export class InternalError extends Schema.TaggedErrorClass( + `${ReasonTypeId}/InternalError`, +)("InternalError", { + message: Schema.String, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return false; + } +} + +/** + * Forward-compatible catch-all for `(group, code)` pairs the SDK does + * not recognize yet. Keeps the raw wire fields so newer engine errors + * still surface usefully through older SDKs. + */ +export class UnknownError extends Schema.TaggedErrorClass( + `${ReasonTypeId}/UnknownError`, +)("UnknownError", { + group: Schema.String, + code: Schema.String, + message: Schema.String, + metadata: Schema.optional(Schema.Unknown), +}) { + readonly [ReasonTypeId] = ReasonTypeId; + get isRetryable(): boolean { + return false; + } +} + +// ============================================================================ +// Reason union +// ============================================================================ + +export type Reason = + | Forbidden + | ActorNotFound + | ActorStopping + | ActorRestarting + | ActionNotFound + | ActionTimedOut + | ActionAborted + | Overloaded + | MessageTooLong + | QueueError + | InvalidEncoding + | InvalidRequest + | ConnectionOpenFailed + | GetParamsFailed + | ConnectionLost + | GuardError + | UserError + | InternalError + | UnknownError; + +export const Reason: Schema.Union< + [ + typeof Forbidden, + typeof ActorNotFound, + typeof ActorStopping, + typeof ActorRestarting, + typeof ActionNotFound, + typeof ActionTimedOut, + typeof ActionAborted, + typeof Overloaded, + typeof MessageTooLong, + typeof QueueError, + typeof InvalidEncoding, + typeof InvalidRequest, + typeof ConnectionOpenFailed, + typeof GetParamsFailed, + typeof ConnectionLost, + typeof GuardError, + typeof UserError, + typeof InternalError, + typeof UnknownError, + ] +> = Schema.Union([ + Forbidden, + ActorNotFound, + ActorStopping, + ActorRestarting, + ActionNotFound, + ActionTimedOut, + ActionAborted, + Overloaded, + MessageTooLong, + QueueError, + InvalidEncoding, + InvalidRequest, + ConnectionOpenFailed, + GetParamsFailed, + ConnectionLost, + GuardError, + UserError, + InternalError, + UnknownError, +]); + +export const isReason = (u: unknown): u is Reason => + Predicate.hasProperty(u, ReasonTypeId); + +// ============================================================================ +// Top-level RivetError +// ============================================================================ + +/** + * The infrastructure-failure error surfaced by `@rivetkit/effect` action + * calls. Wraps a discriminated `reason` of all known engine and client + * failure modes. + * + * Recover with `Effect.catchReason` / `Effect.catchReasons` / + * `Effect.unwrapReason`: + * + * ```ts + * program.pipe( + * Effect.catchReason("RivetError", "ActorRestarting", (r) => + * Effect.sleep(r.retryAfter ?? "100 millis").pipe(Effect.andThen(retry)), + * ), + * Effect.catchReasons("RivetError", { + * Forbidden: () => Effect.fail(new MyAuthError()), + * ConnectionLost: () => Effect.logWarning("reconnecting"), + * }), + * ) + * ``` * - * Recover with `Effect.catchTag("RivetError", e => …)` and discriminate - * on `e.error.group` / `e.error.code`. + * User-defined errors declared on an action via `Action.make({ error })` + * arrive in the typed error channel separately and do NOT flow through + * `RivetError`. */ -export class RivetError extends Schema.TaggedErrorClass()( - "RivetError", - { error: Schema.instanceOf(Rivetkit.RivetError) }, -) {} - -// On-the-wire envelope: the subset of rivetkit's `RivetErrorLike` that -// crosses the action boundary. `Pick`ing here anchors the codec -// against drift in the canonical wire shape. +export class RivetError extends Schema.TaggedErrorClass( + "@rivetkit/effect/RivetError", +)("RivetError", { + reason: Reason, +}) { + readonly [TypeId] = TypeId; + override readonly cause = this.reason; + + get isRetryable(): boolean { + return this.reason.isRetryable; + } + + get retryAfter(): Duration.Duration | undefined { + return "retryAfter" in this.reason + ? (this.reason.retryAfter as Duration.Duration | undefined) + : undefined; + } + + override get message(): string { + return this.reason.message || this.reason._tag; + } +} + +export const isRivetError = (u: unknown): u is RivetError => + Predicate.hasProperty(u, TypeId); + +// ============================================================================ +// Wire codec +// ============================================================================ +// +// On-the-wire envelope produced by `rivetkit-core`'s defect sanitizer. +// `Pick`ing here anchors the codec against drift in the canonical wire +// shape. + type WirePayload = Pick< Rivetkit.RivetErrorLike, "group" | "code" | "message" | "metadata" @@ -30,30 +433,245 @@ const Wire = Schema.Struct({ metadata: Schema.optionalKey(Schema.Unknown), }); +const readMetaField = (metadata: unknown, key: string): unknown => { + if (typeof metadata !== "object" || metadata === null) return undefined; + return (metadata as Record)[key]; +}; + +const reasonFromWire = ({ + group, + code, + message, + metadata, +}: WirePayload): Reason => { + switch (`${group}.${code}`) { + case "auth.forbidden": + return new Forbidden({ message }); + case "actor.not_found": + return new ActorNotFound({ message }); + case "actor.stopping": + return new ActorStopping({ message }); + case "actor.restarting": { + const retryAfterMs = readMetaField(metadata, "retryAfterMs"); + const phase = readMetaField(metadata, "phase"); + const allowedPhases = new Set([ + "stopping", + "sleeping", + "waking", + "runner_shutdown", + ]); + return new ActorRestarting({ + message, + ...(typeof retryAfterMs === "number" + ? { retryAfter: Duration.millis(retryAfterMs) } + : {}), + ...(typeof phase === "string" && allowedPhases.has(phase) + ? { phase: phase as ActorRestarting["phase"] } + : {}), + }); + } + case "actor.action_not_found": + return new ActionNotFound({ message }); + case "actor.action_timed_out": + return new ActionTimedOut({ message }); + case "actor.aborted": + return new ActionAborted({ message }); + case "actor.overloaded": { + const channel = readMetaField(metadata, "channel"); + const capacity = readMetaField(metadata, "capacity"); + const operation = readMetaField(metadata, "operation"); + return new Overloaded({ + message, + ...(typeof channel === "string" ? { channel } : {}), + ...(typeof capacity === "number" ? { capacity } : {}), + ...(typeof operation === "string" ? { operation } : {}), + }); + } + case "message.incoming_too_long": + return new MessageTooLong({ message, direction: "incoming" }); + case "message.outgoing_too_long": + return new MessageTooLong({ message, direction: "outgoing" }); + case "encoding.invalid": + return new InvalidEncoding({ message }); + case "request.invalid": + return new InvalidRequest({ message }); + case "client.connection_open_failed": + return new ConnectionOpenFailed({ message }); + case "client.get_params_failed": + return new GetParamsFailed({ message }); + case "ws.going_away": + return new ConnectionLost({ message }); + case "core.internal_error": + case "rivetkit.internal_error": + return new InternalError({ message }); + default: + if (group === "queue") return new QueueError({ message, code }); + if (group === "guard") return new GuardError({ message, code }); + if (group === "user") { + return new UserError({ + message, + code, + ...(metadata !== undefined ? { metadata } : {}), + }); + } + return new UnknownError({ + group, + code, + message, + ...(metadata !== undefined ? { metadata } : {}), + }); + } +}; + +const reasonToWire = (reason: Reason): WirePayload => { + switch (reason._tag) { + case "Forbidden": + return { + group: "auth", + code: "forbidden", + message: reason.message, + }; + case "ActorNotFound": + return { + group: "actor", + code: "not_found", + message: reason.message, + }; + case "ActorStopping": + return { + group: "actor", + code: "stopping", + message: reason.message, + }; + case "ActorRestarting": { + const metadata: Record = {}; + if (reason.retryAfter !== undefined) { + metadata.retryAfterMs = Duration.toMillis(reason.retryAfter); + } + if (reason.phase !== undefined) metadata.phase = reason.phase; + return { + group: "actor", + code: "restarting", + message: reason.message, + ...(Object.keys(metadata).length > 0 ? { metadata } : {}), + }; + } + case "ActionNotFound": + return { + group: "actor", + code: "action_not_found", + message: reason.message, + }; + case "ActionTimedOut": + return { + group: "actor", + code: "action_timed_out", + message: reason.message, + }; + case "ActionAborted": + return { group: "actor", code: "aborted", message: reason.message }; + case "Overloaded": { + const metadata: Record = {}; + if (reason.channel !== undefined) metadata.channel = reason.channel; + if (reason.capacity !== undefined) + metadata.capacity = reason.capacity; + if (reason.operation !== undefined) + metadata.operation = reason.operation; + return { + group: "actor", + code: "overloaded", + message: reason.message, + ...(Object.keys(metadata).length > 0 ? { metadata } : {}), + }; + } + case "MessageTooLong": + return { + group: "message", + code: + reason.direction === "incoming" + ? "incoming_too_long" + : "outgoing_too_long", + message: reason.message, + }; + case "QueueError": + return { + group: "queue", + code: reason.code, + message: reason.message, + }; + case "InvalidEncoding": + return { + group: "encoding", + code: "invalid", + message: reason.message, + }; + case "InvalidRequest": + return { + group: "request", + code: "invalid", + message: reason.message, + }; + case "ConnectionOpenFailed": + return { + group: "client", + code: "connection_open_failed", + message: reason.message, + }; + case "GetParamsFailed": + return { + group: "client", + code: "get_params_failed", + message: reason.message, + }; + case "ConnectionLost": + return { group: "ws", code: "going_away", message: reason.message }; + case "GuardError": + return { + group: "guard", + code: reason.code, + message: reason.message, + }; + case "UserError": + return { + group: "user", + code: reason.code, + message: reason.message, + ...(reason.metadata !== undefined + ? { metadata: reason.metadata } + : {}), + }; + case "InternalError": + return { + group: "rivetkit", + code: "internal_error", + message: reason.message, + }; + case "UnknownError": + return { + group: reason.group, + code: reason.code, + message: reason.message, + ...(reason.metadata !== undefined + ? { metadata: reason.metadata } + : {}), + }; + } +}; + /** * Wire codec used as the default `defectSchema` for actions. Decodes * the `(group, code, message, metadata)` envelope produced by - * `rivetkit-core`'s defect sanitizer into a `RivetError` instance. + * `rivetkit-core`'s defect sanitizer into a `RivetError` carrying the + * appropriate semantic `reason`. */ export const RivetErrorFromWire = Wire.pipe( Schema.decodeTo(Schema.instanceOf(RivetError), { decode: SchemaGetter.transform( - ({ group, code, message, metadata }) => - new RivetError({ - error: new Rivetkit.RivetError(group, code, message, { - metadata, - } satisfies Rivetkit.RivetErrorOptions), - }), + (wire) => new RivetError({ reason: reasonFromWire(wire) }), + ), + encode: SchemaGetter.transform((e: RivetError) => + reasonToWire(e.reason), ), - encode: SchemaGetter.transform((e: RivetError) => { - const out: WirePayload = { - group: e.error.group, - code: e.error.code, - message: e.error.message, - }; - if (e.error.metadata !== undefined) out.metadata = e.error.metadata; - return out; - }), }), ); diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 439fa67e57..13ca9d48f4 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -425,9 +425,9 @@ layer(TestLayer)("end-to-end", (it) => { assert.isTrue(exit._tag === "Success"); if (exit._tag === "Success") { assert.instanceOf(exit.value, RivetError.RivetError); - assert.strictEqual(exit.value.error.group, "guard"); + assert.instanceOf(exit.value.reason, RivetError.GuardError); assert.strictEqual( - exit.value.error.code, + (exit.value.reason as RivetError.GuardError).code, "service_unavailable", ); } From 6b423cff57a4103e3c6647bd88b71f5944ee3482 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 16:50:48 +0200 Subject: [PATCH 180/306] refactor(effect): promote group/code to reason fields, collapse wire codec --- .../packages/effect/src/RivetError.ts | 363 ++++++++---------- 1 file changed, 150 insertions(+), 213 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 42ac8bb710..27a9ad00d2 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -9,21 +9,23 @@ const TypeId = "~@rivetkit/effect/RivetError" as const; // ============================================================================ // // One class per semantic infrastructure-failure category exposed by the -// engine and client. Each reason is a `Schema.TaggedErrorClass` with an -// `isRetryable` getter so callers can match on the reason `_tag` (via -// `Effect.catchReason` / `Effect.catchReasons` / `Match`) and decide -// retry policy without hand-rolling group/code switches. +// engine and client. Each reason carries its own wire `group` and `code` +// (defaulted via `Schema.tag` so call sites don't pass them), plus an +// `isRetryable` getter so callers can match on the reason `_tag` and +// decide retry policy without hand-rolling group/code switches. // -// User-defined errors (thrown via `UserError` inside an actor action) are -// the domain layer: they ride through on the action's declared -// `errorSchema` and arrive in the typed error channel directly. They -// only fall through to the generic `UserError` reason below when the -// action did not declare a matching schema. +// User-defined errors thrown via `UserError` inside an actor action ride +// through on the action's declared `errorSchema` and arrive in the typed +// error channel directly. They only fall through to the generic +// `UnknownUserError` reason below when the action did not declare a +// matching schema. /** `auth.forbidden` — `onAuth` rejected the request. */ export class Forbidden extends Schema.TaggedErrorClass( `${ReasonTypeId}/Forbidden`, )("Forbidden", { + group: Schema.tag("auth"), + code: Schema.tag("forbidden"), message: Schema.String, }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -36,6 +38,8 @@ export class Forbidden extends Schema.TaggedErrorClass( export class ActorNotFound extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorNotFound`, )("ActorNotFound", { + group: Schema.tag("actor"), + code: Schema.tag("not_found"), message: Schema.String, }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -48,6 +52,8 @@ export class ActorNotFound extends Schema.TaggedErrorClass( export class ActorStopping extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorStopping`, )("ActorStopping", { + group: Schema.tag("actor"), + code: Schema.tag("stopping"), message: Schema.String, }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -60,6 +66,8 @@ export class ActorStopping extends Schema.TaggedErrorClass( export class ActorRestarting extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorRestarting`, )("ActorRestarting", { + group: Schema.tag("actor"), + code: Schema.tag("restarting"), message: Schema.String, retryAfter: Schema.optional(Schema.Duration), phase: Schema.optional( @@ -76,6 +84,8 @@ export class ActorRestarting extends Schema.TaggedErrorClass( export class ActionNotFound extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActionNotFound`, )("ActionNotFound", { + group: Schema.tag("actor"), + code: Schema.tag("action_not_found"), message: Schema.String, }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -88,6 +98,8 @@ export class ActionNotFound extends Schema.TaggedErrorClass( export class ActionTimedOut extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActionTimedOut`, )("ActionTimedOut", { + group: Schema.tag("actor"), + code: Schema.tag("action_timed_out"), message: Schema.String, }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -100,6 +112,8 @@ export class ActionTimedOut extends Schema.TaggedErrorClass( export class ActionAborted extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActionAborted`, )("ActionAborted", { + group: Schema.tag("actor"), + code: Schema.tag("aborted"), message: Schema.String, }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -112,6 +126,8 @@ export class ActionAborted extends Schema.TaggedErrorClass( export class Overloaded extends Schema.TaggedErrorClass( `${ReasonTypeId}/Overloaded`, )("Overloaded", { + group: Schema.tag("actor"), + code: Schema.tag("overloaded"), message: Schema.String, channel: Schema.optional(Schema.String), capacity: Schema.optional(Schema.Number), @@ -123,12 +139,16 @@ export class Overloaded extends Schema.TaggedErrorClass( } } -/** `message.incoming_too_long` / `message.outgoing_too_long`. */ +/** + * `message.incoming_too_long` / `message.outgoing_too_long`. Match on + * `code` to distinguish direction. + */ export class MessageTooLong extends Schema.TaggedErrorClass( `${ReasonTypeId}/MessageTooLong`, )("MessageTooLong", { + group: Schema.tag("message"), + code: Schema.Literals(["incoming_too_long", "outgoing_too_long"]), message: Schema.String, - direction: Schema.Literals(["incoming", "outgoing"]), }) { readonly [ReasonTypeId] = ReasonTypeId; get isRetryable(): boolean { @@ -142,8 +162,9 @@ const queueRetryableCodes = new Set(["full", "timed_out"]); export class QueueError extends Schema.TaggedErrorClass( `${ReasonTypeId}/QueueError`, )("QueueError", { - message: Schema.String, + group: Schema.tag("queue"), code: Schema.String, + message: Schema.String, }) { readonly [ReasonTypeId] = ReasonTypeId; get isRetryable(): boolean { @@ -155,6 +176,8 @@ export class QueueError extends Schema.TaggedErrorClass( export class InvalidEncoding extends Schema.TaggedErrorClass( `${ReasonTypeId}/InvalidEncoding`, )("InvalidEncoding", { + group: Schema.tag("encoding"), + code: Schema.tag("invalid"), message: Schema.String, }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -167,6 +190,8 @@ export class InvalidEncoding extends Schema.TaggedErrorClass( export class InvalidRequest extends Schema.TaggedErrorClass( `${ReasonTypeId}/InvalidRequest`, )("InvalidRequest", { + group: Schema.tag("request"), + code: Schema.tag("invalid"), message: Schema.String, }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -179,6 +204,8 @@ export class InvalidRequest extends Schema.TaggedErrorClass( export class ConnectionOpenFailed extends Schema.TaggedErrorClass( `${ReasonTypeId}/ConnectionOpenFailed`, )("ConnectionOpenFailed", { + group: Schema.tag("client"), + code: Schema.tag("connection_open_failed"), message: Schema.String, }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -191,6 +218,8 @@ export class ConnectionOpenFailed extends Schema.TaggedErrorClass( `${ReasonTypeId}/GetParamsFailed`, )("GetParamsFailed", { + group: Schema.tag("client"), + code: Schema.tag("get_params_failed"), message: Schema.String, }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -203,6 +232,8 @@ export class GetParamsFailed extends Schema.TaggedErrorClass( export class ConnectionLost extends Schema.TaggedErrorClass( `${ReasonTypeId}/ConnectionLost`, )("ConnectionLost", { + group: Schema.tag("ws"), + code: Schema.tag("going_away"), message: Schema.String, }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -221,8 +252,9 @@ const guardRetryableCodes = new Set([ export class GuardError extends Schema.TaggedErrorClass( `${ReasonTypeId}/GuardError`, )("GuardError", { - message: Schema.String, + group: Schema.tag("guard"), code: Schema.String, + message: Schema.String, }) { readonly [ReasonTypeId] = ReasonTypeId; get isRetryable(): boolean { @@ -240,11 +272,12 @@ export class GuardError extends Schema.TaggedErrorClass( * receive those errors **typed** in the error channel; this reason is * the catch-all for everything else. */ -export class UserError extends Schema.TaggedErrorClass( - `${ReasonTypeId}/UserError`, -)("UserError", { - message: Schema.String, +export class UnknownUserError extends Schema.TaggedErrorClass( + `${ReasonTypeId}/UnknownUserError`, +)("UnknownUserError", { + group: Schema.tag("user"), code: Schema.String, + message: Schema.String, metadata: Schema.optional(Schema.Unknown), }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -260,6 +293,8 @@ export class UserError extends Schema.TaggedErrorClass( export class InternalError extends Schema.TaggedErrorClass( `${ReasonTypeId}/InternalError`, )("InternalError", { + group: Schema.tag("rivetkit"), + code: Schema.tag("internal_error"), message: Schema.String, }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -308,7 +343,7 @@ export type Reason = | GetParamsFailed | ConnectionLost | GuardError - | UserError + | UnknownUserError | InternalError | UnknownError; @@ -330,7 +365,7 @@ export const Reason: Schema.Union< typeof GetParamsFailed, typeof ConnectionLost, typeof GuardError, - typeof UserError, + typeof UnknownUserError, typeof InternalError, typeof UnknownError, ] @@ -351,7 +386,7 @@ export const Reason: Schema.Union< GetParamsFailed, ConnectionLost, GuardError, - UserError, + UnknownUserError, InternalError, UnknownError, ]); @@ -438,226 +473,128 @@ const readMetaField = (metadata: unknown, key: string): unknown => { return (metadata as Record)[key]; }; +// Classification table: `"group.code"` → factory producing the matching +// reason from raw `(message, metadata)`. Reasons whose `code` is variable +// (queue, guard, user) and the rivetkit-core internal-error aliases are +// handled by the fallback below. +const fixedFactories: Record< + string, + (message: string, metadata: unknown) => Reason +> = { + "auth.forbidden": (message) => new Forbidden({ message }), + "actor.not_found": (message) => new ActorNotFound({ message }), + "actor.stopping": (message) => new ActorStopping({ message }), + "actor.restarting": (message, metadata) => { + const retryAfterMs = readMetaField(metadata, "retryAfterMs"); + const phase = readMetaField(metadata, "phase"); + const allowedPhases = new Set([ + "stopping", + "sleeping", + "waking", + "runner_shutdown", + ]); + return new ActorRestarting({ + message, + ...(typeof retryAfterMs === "number" + ? { retryAfter: Duration.millis(retryAfterMs) } + : {}), + ...(typeof phase === "string" && allowedPhases.has(phase) + ? { phase: phase as ActorRestarting["phase"] } + : {}), + }); + }, + "actor.action_not_found": (message) => new ActionNotFound({ message }), + "actor.action_timed_out": (message) => new ActionTimedOut({ message }), + "actor.aborted": (message) => new ActionAborted({ message }), + "actor.overloaded": (message, metadata) => { + const channel = readMetaField(metadata, "channel"); + const capacity = readMetaField(metadata, "capacity"); + const operation = readMetaField(metadata, "operation"); + return new Overloaded({ + message, + ...(typeof channel === "string" ? { channel } : {}), + ...(typeof capacity === "number" ? { capacity } : {}), + ...(typeof operation === "string" ? { operation } : {}), + }); + }, + "message.incoming_too_long": (message) => + new MessageTooLong({ message, code: "incoming_too_long" }), + "message.outgoing_too_long": (message) => + new MessageTooLong({ message, code: "outgoing_too_long" }), + "encoding.invalid": (message) => new InvalidEncoding({ message }), + "request.invalid": (message) => new InvalidRequest({ message }), + "client.connection_open_failed": (message) => + new ConnectionOpenFailed({ message }), + "client.get_params_failed": (message) => new GetParamsFailed({ message }), + "ws.going_away": (message) => new ConnectionLost({ message }), + "core.internal_error": (message) => new InternalError({ message }), + "rivetkit.internal_error": (message) => new InternalError({ message }), +}; + const reasonFromWire = ({ group, code, message, metadata, }: WirePayload): Reason => { - switch (`${group}.${code}`) { - case "auth.forbidden": - return new Forbidden({ message }); - case "actor.not_found": - return new ActorNotFound({ message }); - case "actor.stopping": - return new ActorStopping({ message }); - case "actor.restarting": { - const retryAfterMs = readMetaField(metadata, "retryAfterMs"); - const phase = readMetaField(metadata, "phase"); - const allowedPhases = new Set([ - "stopping", - "sleeping", - "waking", - "runner_shutdown", - ]); - return new ActorRestarting({ - message, - ...(typeof retryAfterMs === "number" - ? { retryAfter: Duration.millis(retryAfterMs) } - : {}), - ...(typeof phase === "string" && allowedPhases.has(phase) - ? { phase: phase as ActorRestarting["phase"] } - : {}), - }); - } - case "actor.action_not_found": - return new ActionNotFound({ message }); - case "actor.action_timed_out": - return new ActionTimedOut({ message }); - case "actor.aborted": - return new ActionAborted({ message }); - case "actor.overloaded": { - const channel = readMetaField(metadata, "channel"); - const capacity = readMetaField(metadata, "capacity"); - const operation = readMetaField(metadata, "operation"); - return new Overloaded({ - message, - ...(typeof channel === "string" ? { channel } : {}), - ...(typeof capacity === "number" ? { capacity } : {}), - ...(typeof operation === "string" ? { operation } : {}), - }); - } - case "message.incoming_too_long": - return new MessageTooLong({ message, direction: "incoming" }); - case "message.outgoing_too_long": - return new MessageTooLong({ message, direction: "outgoing" }); - case "encoding.invalid": - return new InvalidEncoding({ message }); - case "request.invalid": - return new InvalidRequest({ message }); - case "client.connection_open_failed": - return new ConnectionOpenFailed({ message }); - case "client.get_params_failed": - return new GetParamsFailed({ message }); - case "ws.going_away": - return new ConnectionLost({ message }); - case "core.internal_error": - case "rivetkit.internal_error": - return new InternalError({ message }); - default: - if (group === "queue") return new QueueError({ message, code }); - if (group === "guard") return new GuardError({ message, code }); - if (group === "user") { - return new UserError({ - message, - code, - ...(metadata !== undefined ? { metadata } : {}), - }); - } - return new UnknownError({ - group, - code, - message, - ...(metadata !== undefined ? { metadata } : {}), - }); + const factory = fixedFactories[`${group}.${code}`]; + if (factory !== undefined) return factory(message, metadata); + if (group === "queue") return new QueueError({ message, code }); + if (group === "guard") return new GuardError({ message, code }); + if (group === "user") { + return new UnknownUserError({ + message, + code, + ...(metadata !== undefined ? { metadata } : {}), + }); } + return new UnknownError({ + group, + code, + message, + ...(metadata !== undefined ? { metadata } : {}), + }); }; -const reasonToWire = (reason: Reason): WirePayload => { +// Per-reason metadata serialization. Reasons not listed have no +// metadata. `group`, `code`, and `message` are read straight off the +// instance, so this is the only mapping `reasonToWire` needs. +const metadataFromReason = (reason: Reason): unknown | undefined => { switch (reason._tag) { - case "Forbidden": - return { - group: "auth", - code: "forbidden", - message: reason.message, - }; - case "ActorNotFound": - return { - group: "actor", - code: "not_found", - message: reason.message, - }; - case "ActorStopping": - return { - group: "actor", - code: "stopping", - message: reason.message, - }; case "ActorRestarting": { const metadata: Record = {}; if (reason.retryAfter !== undefined) { metadata.retryAfterMs = Duration.toMillis(reason.retryAfter); } if (reason.phase !== undefined) metadata.phase = reason.phase; - return { - group: "actor", - code: "restarting", - message: reason.message, - ...(Object.keys(metadata).length > 0 ? { metadata } : {}), - }; + return Object.keys(metadata).length > 0 ? metadata : undefined; } - case "ActionNotFound": - return { - group: "actor", - code: "action_not_found", - message: reason.message, - }; - case "ActionTimedOut": - return { - group: "actor", - code: "action_timed_out", - message: reason.message, - }; - case "ActionAborted": - return { group: "actor", code: "aborted", message: reason.message }; case "Overloaded": { const metadata: Record = {}; if (reason.channel !== undefined) metadata.channel = reason.channel; - if (reason.capacity !== undefined) - metadata.capacity = reason.capacity; + if (reason.capacity !== undefined) metadata.capacity = reason.capacity; if (reason.operation !== undefined) metadata.operation = reason.operation; - return { - group: "actor", - code: "overloaded", - message: reason.message, - ...(Object.keys(metadata).length > 0 ? { metadata } : {}), - }; + return Object.keys(metadata).length > 0 ? metadata : undefined; } - case "MessageTooLong": - return { - group: "message", - code: - reason.direction === "incoming" - ? "incoming_too_long" - : "outgoing_too_long", - message: reason.message, - }; - case "QueueError": - return { - group: "queue", - code: reason.code, - message: reason.message, - }; - case "InvalidEncoding": - return { - group: "encoding", - code: "invalid", - message: reason.message, - }; - case "InvalidRequest": - return { - group: "request", - code: "invalid", - message: reason.message, - }; - case "ConnectionOpenFailed": - return { - group: "client", - code: "connection_open_failed", - message: reason.message, - }; - case "GetParamsFailed": - return { - group: "client", - code: "get_params_failed", - message: reason.message, - }; - case "ConnectionLost": - return { group: "ws", code: "going_away", message: reason.message }; - case "GuardError": - return { - group: "guard", - code: reason.code, - message: reason.message, - }; - case "UserError": - return { - group: "user", - code: reason.code, - message: reason.message, - ...(reason.metadata !== undefined - ? { metadata: reason.metadata } - : {}), - }; - case "InternalError": - return { - group: "rivetkit", - code: "internal_error", - message: reason.message, - }; + case "UnknownUserError": case "UnknownError": - return { - group: reason.group, - code: reason.code, - message: reason.message, - ...(reason.metadata !== undefined - ? { metadata: reason.metadata } - : {}), - }; + return reason.metadata; + default: + return undefined; } }; +const reasonToWire = (reason: Reason): WirePayload => { + const metadata = metadataFromReason(reason); + return { + group: reason.group, + code: reason.code, + message: reason.message, + ...(metadata !== undefined ? { metadata } : {}), + }; +}; + /** * Wire codec used as the default `defectSchema` for actions. Decodes * the `(group, code, message, metadata)` envelope produced by From cb5d7ad5d5cdf01a07f9e34ea0bc334fad66a99a Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sat, 16 May 2026 18:18:01 +0200 Subject: [PATCH 181/306] refactor(effect): replace fixed factory table with structured list, integrate canonical error-code checks --- .../packages/effect/src/RivetError.ts | 214 ++++++++++++------ 1 file changed, 141 insertions(+), 73 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 27a9ad00d2..5e8866cff3 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -1,5 +1,6 @@ import { Duration, Predicate, Schema, SchemaGetter } from "effect"; import * as Rivetkit from "rivetkit"; +import * as RivetkitErrors from "rivetkit/errors"; const ReasonTypeId = "~@rivetkit/effect/RivetError/Reason" as const; const TypeId = "~@rivetkit/effect/RivetError" as const; @@ -456,89 +457,155 @@ export const isRivetError = (u: unknown): u is RivetError => // `Pick`ing here anchors the codec against drift in the canonical wire // shape. -type WirePayload = Pick< - Rivetkit.RivetErrorLike, - "group" | "code" | "message" | "metadata" ->; - -const Wire = Schema.Struct({ +const RivetErrorPayload = Schema.Struct({ group: Schema.String, code: Schema.String, message: Schema.String, metadata: Schema.optionalKey(Schema.Unknown), -}); +}) satisfies Schema.Codec; const readMetaField = (metadata: unknown, key: string): unknown => { if (typeof metadata !== "object" || metadata === null) return undefined; return (metadata as Record)[key]; }; -// Classification table: `"group.code"` → factory producing the matching -// reason from raw `(message, metadata)`. Reasons whose `code` is variable -// (queue, guard, user) and the rivetkit-core internal-error aliases are -// handled by the fallback below. -const fixedFactories: Record< - string, - (message: string, metadata: unknown) => Reason -> = { - "auth.forbidden": (message) => new Forbidden({ message }), - "actor.not_found": (message) => new ActorNotFound({ message }), - "actor.stopping": (message) => new ActorStopping({ message }), - "actor.restarting": (message, metadata) => { - const retryAfterMs = readMetaField(metadata, "retryAfterMs"); - const phase = readMetaField(metadata, "phase"); - const allowedPhases = new Set([ - "stopping", - "sleeping", - "waking", - "runner_shutdown", - ]); - return new ActorRestarting({ - message, - ...(typeof retryAfterMs === "number" - ? { retryAfter: Duration.millis(retryAfterMs) } - : {}), - ...(typeof phase === "string" && allowedPhases.has(phase) - ? { phase: phase as ActorRestarting["phase"] } - : {}), - }); +// Classification table: ordered list of `(group, code, factory)` entries. +// We match through `isRivetErrorCode` from `rivetkit/errors` so a rename +// on the canonical wire-shape side becomes a compile-time signal here. +// Reasons whose `code` is variable (queue, guard, user) and any +// rivetkit-core internal-error aliases are handled by the group-level +// fallback below. +type FixedFactory = { + group: string; + code: string; + build: (message: string, metadata: unknown) => Reason; +}; + +const fixedFactories: ReadonlyArray = [ + { + group: "auth", + code: "forbidden", + build: (message) => new Forbidden({ message }), }, - "actor.action_not_found": (message) => new ActionNotFound({ message }), - "actor.action_timed_out": (message) => new ActionTimedOut({ message }), - "actor.aborted": (message) => new ActionAborted({ message }), - "actor.overloaded": (message, metadata) => { - const channel = readMetaField(metadata, "channel"); - const capacity = readMetaField(metadata, "capacity"); - const operation = readMetaField(metadata, "operation"); - return new Overloaded({ - message, - ...(typeof channel === "string" ? { channel } : {}), - ...(typeof capacity === "number" ? { capacity } : {}), - ...(typeof operation === "string" ? { operation } : {}), - }); + { + group: "actor", + code: "not_found", + build: (message) => new ActorNotFound({ message }), }, - "message.incoming_too_long": (message) => - new MessageTooLong({ message, code: "incoming_too_long" }), - "message.outgoing_too_long": (message) => - new MessageTooLong({ message, code: "outgoing_too_long" }), - "encoding.invalid": (message) => new InvalidEncoding({ message }), - "request.invalid": (message) => new InvalidRequest({ message }), - "client.connection_open_failed": (message) => - new ConnectionOpenFailed({ message }), - "client.get_params_failed": (message) => new GetParamsFailed({ message }), - "ws.going_away": (message) => new ConnectionLost({ message }), - "core.internal_error": (message) => new InternalError({ message }), - "rivetkit.internal_error": (message) => new InternalError({ message }), -}; + { + group: "actor", + code: "stopping", + build: (message) => new ActorStopping({ message }), + }, + { + group: "actor", + code: "restarting", + build: (message, metadata) => { + const retryAfterMs = readMetaField(metadata, "retryAfterMs"); + const phase = readMetaField(metadata, "phase"); + const allowedPhases = new Set([ + "stopping", + "sleeping", + "waking", + "runner_shutdown", + ]); + return new ActorRestarting({ + message, + ...(typeof retryAfterMs === "number" + ? { retryAfter: Duration.millis(retryAfterMs) } + : {}), + ...(typeof phase === "string" && allowedPhases.has(phase) + ? { phase: phase as ActorRestarting["phase"] } + : {}), + }); + }, + }, + { + group: "actor", + code: "action_not_found", + build: (message) => new ActionNotFound({ message }), + }, + { + group: "actor", + code: "action_timed_out", + build: (message) => new ActionTimedOut({ message }), + }, + { + group: "actor", + code: "aborted", + build: (message) => new ActionAborted({ message }), + }, + { + group: "actor", + code: "overloaded", + build: (message, metadata) => { + const channel = readMetaField(metadata, "channel"); + const capacity = readMetaField(metadata, "capacity"); + const operation = readMetaField(metadata, "operation"); + return new Overloaded({ + message, + ...(typeof channel === "string" ? { channel } : {}), + ...(typeof capacity === "number" ? { capacity } : {}), + ...(typeof operation === "string" ? { operation } : {}), + }); + }, + }, + { + group: "message", + code: "incoming_too_long", + build: (message) => + new MessageTooLong({ message, code: "incoming_too_long" }), + }, + { + group: "message", + code: "outgoing_too_long", + build: (message) => + new MessageTooLong({ message, code: "outgoing_too_long" }), + }, + { + group: "encoding", + code: "invalid", + build: (message) => new InvalidEncoding({ message }), + }, + { + group: "request", + code: "invalid", + build: (message) => new InvalidRequest({ message }), + }, + { + group: "client", + code: "connection_open_failed", + build: (message) => new ConnectionOpenFailed({ message }), + }, + { + group: "client", + code: "get_params_failed", + build: (message) => new GetParamsFailed({ message }), + }, + { + group: "ws", + code: "going_away", + build: (message) => new ConnectionLost({ message }), + }, + { + group: "core", + code: "internal_error", + build: (message) => new InternalError({ message }), + }, + { + group: "rivetkit", + code: "internal_error", + build: (message) => new InternalError({ message }), + }, +]; -const reasonFromWire = ({ - group, - code, - message, - metadata, -}: WirePayload): Reason => { - const factory = fixedFactories[`${group}.${code}`]; - if (factory !== undefined) return factory(message, metadata); +const reasonFromWire = (wire: typeof RivetErrorPayload.Encoded): Reason => { + const { group, code, message, metadata } = wire; + for (const entry of fixedFactories) { + if (RivetkitErrors.isRivetErrorCode(wire, entry.group, entry.code)) { + return entry.build(message, metadata); + } + } if (group === "queue") return new QueueError({ message, code }); if (group === "guard") return new GuardError({ message, code }); if (group === "user") { @@ -572,7 +639,8 @@ const metadataFromReason = (reason: Reason): unknown | undefined => { case "Overloaded": { const metadata: Record = {}; if (reason.channel !== undefined) metadata.channel = reason.channel; - if (reason.capacity !== undefined) metadata.capacity = reason.capacity; + if (reason.capacity !== undefined) + metadata.capacity = reason.capacity; if (reason.operation !== undefined) metadata.operation = reason.operation; return Object.keys(metadata).length > 0 ? metadata : undefined; @@ -585,7 +653,7 @@ const metadataFromReason = (reason: Reason): unknown | undefined => { } }; -const reasonToWire = (reason: Reason): WirePayload => { +const reasonToWire = (reason: Reason): typeof RivetErrorPayload.Encoded => { const metadata = metadataFromReason(reason); return { group: reason.group, @@ -601,7 +669,7 @@ const reasonToWire = (reason: Reason): WirePayload => { * `rivetkit-core`'s defect sanitizer into a `RivetError` carrying the * appropriate semantic `reason`. */ -export const RivetErrorFromWire = Wire.pipe( +export const RivetErrorFromWire = RivetErrorPayload.pipe( Schema.decodeTo(Schema.instanceOf(RivetError), { decode: SchemaGetter.transform( (wire) => new RivetError({ reason: reasonFromWire(wire) }), From 5bc341867d63cb6e8dc81d5a370dfd2b6fa2c0cc Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 15:32:16 +0200 Subject: [PATCH 182/306] chore(effect): upgrade effect to 4.0.0-beta.66 Service tags are now Effects directly (Effect.Yieldable removed), so replace Registry/Client .asEffect() calls. Adapt Schema codec calls to the new toCodecJson + encodeEffect API. --- examples/effect/package.json | 4 +- pnpm-lock.yaml | 54 +++++------ .../packages/effect/package.json | 6 +- .../packages/effect/src/Actor.ts | 94 ++++++++++++------- 4 files changed, 92 insertions(+), 66 deletions(-) diff --git a/examples/effect/package.json b/examples/effect/package.json index 2297c2a711..6e6b794c90 100644 --- a/examples/effect/package.json +++ b/examples/effect/package.json @@ -12,8 +12,8 @@ "dependencies": { "rivetkit": "workspace:*", "@rivetkit/effect": "workspace:*", - "effect": "4.0.0-beta.57", - "@effect/platform-node": "4.0.0-beta.57" + "effect": "4.0.0-beta.66", + "@effect/platform-node": "4.0.0-beta.66" }, "devDependencies": { "@types/node": "^22.13.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 436a604e23..045e9ec12a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1151,14 +1151,14 @@ importers: examples/effect: dependencies: '@effect/platform-node': - specifier: 4.0.0-beta.57 - version: 4.0.0-beta.57(effect@4.0.0-beta.57)(ioredis@5.10.1) + specifier: 4.0.0-beta.66 + version: 4.0.0-beta.66(effect@4.0.0-beta.66)(ioredis@5.10.1) '@rivetkit/effect': specifier: workspace:* version: link:../../rivetkit-typescript/packages/effect effect: - specifier: 4.0.0-beta.57 - version: 4.0.0-beta.57 + specifier: 4.0.0-beta.66 + version: 4.0.0-beta.66 rivetkit: specifier: workspace:* version: link:../../rivetkit-typescript/packages/rivetkit @@ -4135,14 +4135,14 @@ importers: specifier: ^0.85.1 version: 0.85.1 '@effect/vitest': - specifier: ^4.0.0-beta.57 - version: 4.0.0-beta.57(effect@4.0.0-beta.57)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))) + specifier: ^4.0.0-beta.66 + version: 4.0.0-beta.66(effect@4.0.0-beta.66)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))) '@types/node': specifier: ^22.18.1 version: 22.19.15 effect: - specifier: ^4.0.0-beta.57 - version: 4.0.0-beta.57 + specifier: ^4.0.0-beta.66 + version: 4.0.0-beta.66 tsup: specifier: ^8.4.0 version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.15))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) @@ -6039,23 +6039,23 @@ packages: resolution: {integrity: sha512-EXnJjIy6zQ3nUO/MZ+ynWUb8B895KZPotd1++oTs9JjDkplwM7cb6zo8Zq2zU6piwq+KflO7amXbEfj1UMpHkw==} hasBin: true - '@effect/platform-node-shared@4.0.0-beta.57': - resolution: {integrity: sha512-C976X6f+qHUtLSqcqImuCrjhAHnJV17NC2RvvybsAuDfkyIWU4MyiO2XwgiBeijeNupyr1M/KPKnyjtkNxV9Hw==} + '@effect/platform-node-shared@4.0.0-beta.66': + resolution: {integrity: sha512-+ymrhBnESv/hmn5SKTe2//IY9Ox/hGPeoogEWhW47ZGyhFI5eMYFxdEUBa+3IAV05rrBzrxON9lynu68n0DM7w==} engines: {node: '>=18.0.0'} peerDependencies: - effect: ^4.0.0-beta.57 + effect: ^4.0.0-beta.66 - '@effect/platform-node@4.0.0-beta.57': - resolution: {integrity: sha512-la0xxPSAYOsY0d+uVxEBxok3jYB31iPQmIaZZRUj2SNWqcGGHJc6KorKtI8guqSLuv9FGZ255kBWXRbG6hMeeg==} + '@effect/platform-node@4.0.0-beta.66': + resolution: {integrity: sha512-s/0RgaQFuszzdorRnX1PwEQNnSOi+JgMJo3zEe9O2NR3sosMhTr0Uk+1AF6bUOI9uJ2CPT3KpTIIU7q5/TpOkg==} engines: {node: '>=18.0.0'} peerDependencies: - effect: ^4.0.0-beta.57 + effect: ^4.0.0-beta.66 ioredis: ^5.7.0 - '@effect/vitest@4.0.0-beta.57': - resolution: {integrity: sha512-XyGYv1zisrdP/N8+r4qaegyHZK4WS/1xBGlLPWqEoggBhgW7rD48cGUXDLEP7TlHcIJQiIlHtJlQUIwgVx3zWg==} + '@effect/vitest@4.0.0-beta.66': + resolution: {integrity: sha512-UHPNtU0xXkKtNgyRQEh2c8jh4nIIm8Mzp3xc4j2ZdFU4nq5ZSySnpovjPMdoWbVClg1ki8UbpNGEZUfxEJo+6Q==} peerDependencies: - effect: ^4.0.0-beta.57 + effect: ^4.0.0-beta.66 vitest: ^3.0.0 || ^4.0.0 '@emnapi/runtime@1.7.1': @@ -12635,8 +12635,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - effect@4.0.0-beta.57: - resolution: {integrity: sha512-rg32VgXnLKaPRs9tbRDaZ5jxmzNY7ojXt85gSHGUTwdlbWH5Ik+OCUY2q14TXliygPGoHwCAvNWS4bQJOqf00g==} + effect@4.0.0-beta.66: + resolution: {integrity: sha512-4arEr62cziFa8BBVDUwJCJJmaVepXf/kRg7KtC0h8+bufngscrHbwWFhr9c+HonwOF+31U3iD3xUJmw9KzX7Dw==} electron-to-chromium@1.5.286: resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} @@ -20217,19 +20217,19 @@ snapshots: '@effect/language-service@0.85.1': {} - '@effect/platform-node-shared@4.0.0-beta.57(effect@4.0.0-beta.57)': + '@effect/platform-node-shared@4.0.0-beta.66(effect@4.0.0-beta.66)': dependencies: '@types/ws': 8.18.1 - effect: 4.0.0-beta.57 + effect: 4.0.0-beta.66 ws: 8.20.0 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@4.0.0-beta.57(effect@4.0.0-beta.57)(ioredis@5.10.1)': + '@effect/platform-node@4.0.0-beta.66(effect@4.0.0-beta.66)(ioredis@5.10.1)': dependencies: - '@effect/platform-node-shared': 4.0.0-beta.57(effect@4.0.0-beta.57) - effect: 4.0.0-beta.57 + '@effect/platform-node-shared': 4.0.0-beta.66(effect@4.0.0-beta.66) + effect: 4.0.0-beta.66 ioredis: 5.10.1 mime: 4.1.0 undici: 8.1.0 @@ -20237,9 +20237,9 @@ snapshots: - bufferutil - utf-8-validate - '@effect/vitest@4.0.0-beta.57(effect@4.0.0-beta.57)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)))': + '@effect/vitest@4.0.0-beta.66(effect@4.0.0-beta.66)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: - effect: 4.0.0-beta.57 + effect: 4.0.0-beta.66 vitest: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@emnapi/runtime@1.7.1': @@ -28030,7 +28030,7 @@ snapshots: ee-first@1.1.1: {} - effect@4.0.0-beta.57: + effect@4.0.0-beta.66: dependencies: '@standard-schema/spec': 1.1.0 fast-check: 4.7.0 diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index ab4278402f..e460d6ad3f 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -34,13 +34,13 @@ "rivetkit": "workspace:*" }, "peerDependencies": { - "effect": "^4.0.0-beta.57" + "effect": "^4.0.0-beta.66" }, "devDependencies": { "@effect/language-service": "^0.85.1", - "@effect/vitest": "^4.0.0-beta.57", + "@effect/vitest": "^4.0.0-beta.66", "@types/node": "^22.18.1", - "effect": "^4.0.0-beta.57", + "effect": "^4.0.0-beta.66", "tsup": "^8.4.0", "typescript": "^5.9.2", "vitest": "^4.1.5" diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index f39b98d480..7020679a90 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -12,12 +12,13 @@ import { Scope, Semaphore, Struct, + Option, Tracer, UndefinedOr, } from "effect"; import * as Rivetkit from "rivetkit"; import type * as RivetkitDb from "rivetkit/db"; -import type * as Action from "./Action"; +import * as Action from "./Action"; import type * as ActorState from "./ActorState"; import * as Client from "./Client"; import { readTraceMeta, rpcSystem } from "./internal/tracing"; @@ -219,7 +220,7 @@ const Proto: Omit, "name" | "actions"> = { options, }).pipe( Effect.flatMap((rivetKitActor) => - Registry.Registry.asEffect().pipe( + Registry.Registry.pipe( Effect.flatMap((registry) => Effect.sync(() => registry.rivetkitActors.set( @@ -234,7 +235,7 @@ const Proto: Omit, "name" | "actions"> = { ); }, get client() { - return Client.Client.asEffect().pipe( + return Client.Client.pipe( Effect.map((client) => client.makeActorAccessor(this as Any)), ); }, @@ -370,9 +371,16 @@ const makeRivetkitActor = Effect.fnUntraced(function* < }; const actions = Record.fromIterableWith(actor.actions, (action) => { - const decodePayload = Schema.decodeUnknownEffect(action.payloadSchema); - const encodeSuccess = Schema.encodeUnknownEffect(action.successSchema); - const encodeError = Schema.encodeUnknownEffect(action.errorSchema); + const decodePayload = Schema.decodeUnknownEffect( + Schema.toCodecJson(action.payloadSchema), + ); + const encodeSuccess = Schema.encodeEffect( + Schema.toCodecJson(action.successSchema), + ); + const encodeError = Schema.encodeEffect( + Schema.toCodecJson(action.errorSchema), + ); + return [ action._tag, async ( @@ -404,42 +412,60 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ] as ( envelope: ActionRequest, ) => Action.ResultFrom; - const decoded = yield* decodePayload(payload).pipe( - Effect.orDie, - ); + const decodedPayload = yield* decodePayload( + payload, + ).pipe(Effect.orDie); // The payload was decoded with this action's schema, // so this is the runtime boundary that restores the // typed envelope expected by the user handler. const actionRequest = { _tag: action._tag, action, - payload: decoded, + payload: decodedPayload, } as ActionRequest; - const result = yield* actionHandler(actionRequest).pipe( - Effect.catch((expectedError) => - Effect.gen(function* () { - const error = yield* encodeError( - expectedError, - ).pipe(Effect.orDie); - return yield* Effect.die( - new Rivetkit.UserError( - hasStringProperty("message")(error) - ? error.message - : `${action._tag} failed`, - { - code: hasStringProperty("_tag")( - error, - ) - ? error._tag - : undefined, - metadata: error, - }, - ), - ); - }), - ), + + const resultExit = yield* Effect.exit( + actionHandler(actionRequest), + ); + + if (Exit.isSuccess(resultExit)) { + return yield* encodeSuccess(resultExit.value).pipe( + Effect.orDie, + ); + } + + const expectedError = Exit.findErrorOption(resultExit); + + if (Option.isSome(expectedError)) { + const encodedError = yield* encodeError( + expectedError.value, + ).pipe(Effect.orDie); + + return yield* Effect.fail( + new Rivetkit.UserError( + hasStringProperty("message")(encodedError) + ? encodedError.message + : `${action._tag} failed`, + { + code: hasStringProperty("_tag")( + encodedError, + ) + ? encodedError._tag + : undefined, + metadata: { + _tag: Action.ActionErrorMetadataTag, + error: encodedError, + }, + }, + ), + ); + } + + // Defect / interruption. Do not encode these as action errors. + // Let them escape, so Rivetkit maps them to its internal_error shape. + return yield* Effect.die( + Cause.squash(resultExit.cause), ); - return yield* encodeSuccess(result).pipe(Effect.orDie); }).pipe( Effect.withSpan(rpcMethod, { parent: traceMeta From e71aa907b09a95c4bf821fca87280e7898358408 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 16:04:34 +0200 Subject: [PATCH 183/306] refactor(effect): version action-error metadata envelope and split wire decode into client --- .../packages/effect/src/Action.ts | 6 +- .../packages/effect/src/Actor.ts | 8 +- .../packages/effect/src/Client.ts | 144 ++++++++++-------- .../packages/effect/src/RivetError.ts | 103 +++---------- .../effect/src/internal/RivetRivetError.ts | 23 +++ 5 files changed, 131 insertions(+), 153 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index d87e574262..a78f85371c 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -1,5 +1,4 @@ -import { Deferred, Effect, Predicate, Schema } from "effect"; -import { RivetErrorFromWire } from "./RivetError"; +import { type Deferred, type Effect, Predicate, Schema } from "effect"; const TypeId = "~@rivetkit/effect/Action"; @@ -21,7 +20,6 @@ export interface Action< readonly payloadSchema: Payload; readonly successSchema: Success; readonly errorSchema: Error; - readonly defectSchema: Schema.Top; } /** @@ -45,7 +43,6 @@ export interface AnyWithProps { readonly payloadSchema: Schema.Top; readonly successSchema: Schema.Top; readonly errorSchema: Schema.Top; - readonly defectSchema: Schema.Top; } // --- Type helpers --------------------------------------------------- @@ -167,7 +164,6 @@ const makeProto = < }): Action => { const self = Object.assign(Object.create(Proto), options); self.key = `@rivetkit/effect/Action/${options._tag}`; - self.defectSchema = RivetErrorFromWire; return self; }; diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 7020679a90..ed350af2fb 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -6,21 +6,22 @@ import { identity, Layer, MutableHashMap, + Option, Predicate, Record, Schema, Scope, Semaphore, Struct, - Option, Tracer, UndefinedOr, } from "effect"; import * as Rivetkit from "rivetkit"; import type * as RivetkitDb from "rivetkit/db"; -import * as Action from "./Action"; +import type * as Action from "./Action"; import type * as ActorState from "./ActorState"; import * as Client from "./Client"; +import * as RivetRivetError from "./internal/RivetRivetError"; import { readTraceMeta, rpcSystem } from "./internal/tracing"; import * as Registry from "./Registry"; import type * as RivetError from "./RivetError"; @@ -453,7 +454,8 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ? encodedError._tag : undefined, metadata: { - _tag: Action.ActionErrorMetadataTag, + _tag: RivetRivetError.ActionErrorMetadataTag, + version: 1, error: encodedError, }, }, diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 57e0fc4a7b..8c6ff0973a 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -1,9 +1,8 @@ -import { Context, Effect, Layer, Schema } from "effect"; -import * as Record from "effect/Record"; -import * as Rivetkit from "rivetkit"; +import { Context, Effect, Exit, Layer, Record, Schema } from "effect"; import * as RivetkitClient from "rivetkit/client"; import type * as Action from "./Action"; import type * as Actor from "./Actor"; +import * as RivetRivetError from "./internal/RivetRivetError"; import { rpcSystem, type TraceMeta } from "./internal/tracing"; import * as RivetError from "./RivetError"; @@ -58,14 +57,11 @@ export const make = Effect.fnUntraced(function* (options: Options = {}) { ); return Record.fromIterableWith(actor.actions, (action) => { - const encodePayload = Schema.encodeUnknownEffect( - action.payloadSchema, + const encodePayload = Schema.encodeEffect( + Schema.toCodecJson(action.payloadSchema), ); const decodeSuccess = Schema.decodeUnknownEffect( - action.successSchema, - ); - const decodeError = Schema.decodeUnknownEffect( - action.errorSchema, + Schema.toCodecJson(action.successSchema), ); const rpcMethod = `${actor.name}/${action._tag}`; @@ -87,67 +83,41 @@ export const make = Effect.fnUntraced(function* (options: Options = {}) { sampled: span.sampled, }, }; - const encodedPayload = - yield* encodePayload(payload); - const raw = yield* Effect.tryPromise({ - try: (abortSignal) => + const encodedPayload = yield* encodePayload( + payload, + ).pipe( + Effect.mapError( + (cause) => + new RivetError.RivetError({ + reason: new RivetError.InvalidEncoding( + { + message: + "Could not encode action payload", + cause, + }, + ), + }), + ), + ); + + const encodedSuccess = yield* Effect.tryPromise( + (abortSignal) => rivetkitActorHandle.action({ name: action._tag, args: [encodedPayload, meta], signal: abortSignal, }), - catch: (cause) => - cause instanceof Rivetkit.RivetError - ? cause - : new Rivetkit.RivetError( - "client", - "unknown", - cause instanceof Error - ? cause.message - : String(cause), - { - cause: - cause instanceof Error - ? cause - : undefined, - }, - ), - }).pipe( - // Try `errorSchema` first against the - // wire metadata. Fall back to wrapping - // the raw RivetError via `RivetErrorFromWire`. - Effect.catch((rivetErr) => - decodeError( - ( - rivetErr as { - metadata?: unknown; - } - ).metadata, - ).pipe( - Effect.matchEffect({ - onSuccess: (typed) => - Effect.fail(typed), - onFailure: () => - RivetError.decodeRivetErrorFromWire( - { - group: rivetErr.group, - code: rivetErr.code, - message: - rivetErr.message, - metadata: ( - rivetErr as { - metadata?: unknown; - } - ).metadata, - }, - ).pipe( - Effect.flatMap(Effect.fail), - ), - }), - ), + ).pipe( + Effect.catch((unknownError) => + decodeRejectedActionCall( + action.errorSchema, + )(unknownError.cause), ), ); - return yield* decodeSuccess(raw); + + return yield* decodeSuccess(encodedSuccess).pipe( + Effect.orDie, + ); }), ]; }) as Actor.Handle<(typeof actor.actions)[number]>; @@ -158,3 +128,51 @@ export const make = Effect.fnUntraced(function* (options: Options = {}) { export const layer = (options: Options = {}): Layer.Layer => Layer.effect(Client, make(options)); + +const decodeRejectedActionCall = ( + actionErrorSchema: E, +) => { + const decodeRivetkitRivetError = Schema.decodeUnknownEffect( + RivetRivetError.RivetkitRivetError, + ); + const decodeActionErrorMetadata = Schema.decodeUnknownEffect( + RivetRivetError.ActionErrorMetadata, + ); + const decodeActionError = Schema.decodeUnknownEffect( + Schema.toCodecJson(actionErrorSchema), + ); + + return Effect.fnUntraced(function* (cause: unknown) { + const rivetkitRivetError = yield* decodeRivetkitRivetError(cause).pipe( + Effect.mapError( + () => + new RivetError.RivetError({ + reason: new RivetError.UnknownError({ + message: "Unknown error", + cause, + }), + }), + ), + ); + + const actionErrorMetadata = yield* Effect.exit( + decodeActionErrorMetadata(rivetkitRivetError.metadata), + ); + + if (Exit.isFailure(actionErrorMetadata)) { + return yield* Effect.fail( + RivetError.fromRivetkitRivetError(rivetkitRivetError), + ); + } + + const actionError = yield* decodeActionError( + actionErrorMetadata.value.error, + ).pipe( + Effect.mapError(() => + RivetError.fromRivetkitRivetError(rivetkitRivetError), + ), + ); + + return yield* Effect.fail(actionError as E["Type"]); + }); +}; diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 5e8866cff3..ee9443bbcb 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -1,6 +1,6 @@ -import { Duration, Predicate, Schema, SchemaGetter } from "effect"; -import * as Rivetkit from "rivetkit"; +import { Duration, Predicate, Schema } from "effect"; import * as RivetkitErrors from "rivetkit/errors"; +import type * as RivetRivetError from "./internal/RivetRivetError"; const ReasonTypeId = "~@rivetkit/effect/RivetError/Reason" as const; const TypeId = "~@rivetkit/effect/RivetError" as const; @@ -180,6 +180,7 @@ export class InvalidEncoding extends Schema.TaggedErrorClass( group: Schema.tag("encoding"), code: Schema.tag("invalid"), message: Schema.String, + cause: Schema.optional(Schema.Defect), }) { readonly [ReasonTypeId] = ReasonTypeId; get isRetryable(): boolean { @@ -306,16 +307,17 @@ export class InternalError extends Schema.TaggedErrorClass( /** * Forward-compatible catch-all for `(group, code)` pairs the SDK does - * not recognize yet. Keeps the raw wire fields so newer engine errors + * not recognize yet. Keeps the raw rivetkit fields so newer engine errors * still surface usefully through older SDKs. */ export class UnknownError extends Schema.TaggedErrorClass( `${ReasonTypeId}/UnknownError`, )("UnknownError", { - group: Schema.String, - code: Schema.String, + group: Schema.optional(Schema.String), + code: Schema.optional(Schema.String), message: Schema.String, metadata: Schema.optional(Schema.Unknown), + cause: Schema.optional(Schema.Defect), }) { readonly [ReasonTypeId] = ReasonTypeId; get isRetryable(): boolean { @@ -449,21 +451,6 @@ export class RivetError extends Schema.TaggedErrorClass( export const isRivetError = (u: unknown): u is RivetError => Predicate.hasProperty(u, TypeId); -// ============================================================================ -// Wire codec -// ============================================================================ -// -// On-the-wire envelope produced by `rivetkit-core`'s defect sanitizer. -// `Pick`ing here anchors the codec against drift in the canonical wire -// shape. - -const RivetErrorPayload = Schema.Struct({ - group: Schema.String, - code: Schema.String, - message: Schema.String, - metadata: Schema.optionalKey(Schema.Unknown), -}) satisfies Schema.Codec; - const readMetaField = (metadata: unknown, key: string): unknown => { if (typeof metadata !== "object" || metadata === null) return undefined; return (metadata as Record)[key]; @@ -599,10 +586,18 @@ const fixedFactories: ReadonlyArray = [ }, ]; -const reasonFromWire = (wire: typeof RivetErrorPayload.Encoded): Reason => { - const { group, code, message, metadata } = wire; +const reasonFromRivetkitRivetError = ( + rivetkitRivetError: RivetRivetError.RivetkitRivetError, +): Reason => { + const { group, code, message, metadata } = rivetkitRivetError; for (const entry of fixedFactories) { - if (RivetkitErrors.isRivetErrorCode(wire, entry.group, entry.code)) { + if ( + RivetkitErrors.isRivetErrorCode( + rivetkitRivetError, + entry.group, + entry.code, + ) + ) { return entry.build(message, metadata); } } @@ -623,62 +618,6 @@ const reasonFromWire = (wire: typeof RivetErrorPayload.Encoded): Reason => { }); }; -// Per-reason metadata serialization. Reasons not listed have no -// metadata. `group`, `code`, and `message` are read straight off the -// instance, so this is the only mapping `reasonToWire` needs. -const metadataFromReason = (reason: Reason): unknown | undefined => { - switch (reason._tag) { - case "ActorRestarting": { - const metadata: Record = {}; - if (reason.retryAfter !== undefined) { - metadata.retryAfterMs = Duration.toMillis(reason.retryAfter); - } - if (reason.phase !== undefined) metadata.phase = reason.phase; - return Object.keys(metadata).length > 0 ? metadata : undefined; - } - case "Overloaded": { - const metadata: Record = {}; - if (reason.channel !== undefined) metadata.channel = reason.channel; - if (reason.capacity !== undefined) - metadata.capacity = reason.capacity; - if (reason.operation !== undefined) - metadata.operation = reason.operation; - return Object.keys(metadata).length > 0 ? metadata : undefined; - } - case "UnknownUserError": - case "UnknownError": - return reason.metadata; - default: - return undefined; - } -}; - -const reasonToWire = (reason: Reason): typeof RivetErrorPayload.Encoded => { - const metadata = metadataFromReason(reason); - return { - group: reason.group, - code: reason.code, - message: reason.message, - ...(metadata !== undefined ? { metadata } : {}), - }; -}; - -/** - * Wire codec used as the default `defectSchema` for actions. Decodes - * the `(group, code, message, metadata)` envelope produced by - * `rivetkit-core`'s defect sanitizer into a `RivetError` carrying the - * appropriate semantic `reason`. - */ -export const RivetErrorFromWire = RivetErrorPayload.pipe( - Schema.decodeTo(Schema.instanceOf(RivetError), { - decode: SchemaGetter.transform( - (wire) => new RivetError({ reason: reasonFromWire(wire) }), - ), - encode: SchemaGetter.transform((e: RivetError) => - reasonToWire(e.reason), - ), - }), -); - -export const decodeRivetErrorFromWire = - Schema.decodeUnknownEffect(RivetErrorFromWire); +export const fromRivetkitRivetError = ( + e: RivetRivetError.RivetkitRivetError, +): RivetError => new RivetError({ reason: reasonFromRivetkitRivetError(e) }); diff --git a/rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts b/rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts new file mode 100644 index 0000000000..1ec506871e --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts @@ -0,0 +1,23 @@ +import { Schema } from "effect"; +import type * as Rivetkit from "rivetkit"; + +export const ActionErrorMetadataTag = "EffectActionError" as const; + +export const ActionErrorSchemaVersion = 1 as const; + +export const ActionErrorMetadata = Schema.Struct({ + _tag: Schema.tag(ActionErrorMetadataTag), + version: Schema.Literal(ActionErrorSchemaVersion), + error: Schema.Unknown, +}); + +export type ActionErrorMetadata = typeof ActionErrorMetadata.Type; + +export const RivetkitRivetError = Schema.Struct({ + message: Schema.String, + group: Schema.String, + code: Schema.String, + metadata: Schema.optional(Schema.Unknown), +}) satisfies Schema.Codec; + +export type RivetkitRivetError = typeof RivetkitRivetError.Type; From d58a5a9cc5b669d9f02a85dc3b6e00798aac2365 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 16:10:07 +0200 Subject: [PATCH 184/306] refactor(effect): extract makeActionError helper and relocate utils to internal/ --- .../packages/effect/src/Actor.ts | 20 +++------------ .../effect/src/internal/RivetRivetError.ts | 25 ++++++++++++++++++- .../effect/src/{ => internal}/utils.ts | 0 3 files changed, 27 insertions(+), 18 deletions(-) rename rivetkit-typescript/packages/effect/src/{ => internal}/utils.ts (100%) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index ed350af2fb..699af6f5b8 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -26,7 +26,6 @@ import { readTraceMeta, rpcSystem } from "./internal/tracing"; import * as Registry from "./Registry"; import type * as RivetError from "./RivetError"; import * as State from "./State"; -import { hasStringProperty } from "./utils"; const TypeId = "~@rivetkit/effect/Actor"; @@ -443,22 +442,9 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ).pipe(Effect.orDie); return yield* Effect.fail( - new Rivetkit.UserError( - hasStringProperty("message")(encodedError) - ? encodedError.message - : `${action._tag} failed`, - { - code: hasStringProperty("_tag")( - encodedError, - ) - ? encodedError._tag - : undefined, - metadata: { - _tag: RivetRivetError.ActionErrorMetadataTag, - version: 1, - error: encodedError, - }, - }, + RivetRivetError.makeActionError( + action._tag, + encodedError, ), ); } diff --git a/rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts b/rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts index 1ec506871e..e24ac5591a 100644 --- a/rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts +++ b/rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts @@ -1,5 +1,6 @@ import { Schema } from "effect"; -import type * as Rivetkit from "rivetkit"; +import * as Rivetkit from "rivetkit"; +import { hasStringProperty } from "./utils"; export const ActionErrorMetadataTag = "EffectActionError" as const; @@ -13,6 +14,28 @@ export const ActionErrorMetadata = Schema.Struct({ export type ActionErrorMetadata = typeof ActionErrorMetadata.Type; +export const makeActionErrorMetadata = (error: unknown): ActionErrorMetadata => ({ + _tag: ActionErrorMetadataTag, + version: ActionErrorSchemaVersion, + error, +}); + +export const makeActionError = ( + actionTag: string, + encodedError: unknown, +): Rivetkit.UserError => + new Rivetkit.UserError( + hasStringProperty("message")(encodedError) + ? encodedError.message + : `${actionTag} failed`, + { + code: hasStringProperty("_tag")(encodedError) + ? encodedError._tag + : undefined, + metadata: makeActionErrorMetadata(encodedError), + }, + ); + export const RivetkitRivetError = Schema.Struct({ message: Schema.String, group: Schema.String, diff --git a/rivetkit-typescript/packages/effect/src/utils.ts b/rivetkit-typescript/packages/effect/src/internal/utils.ts similarity index 100% rename from rivetkit-typescript/packages/effect/src/utils.ts rename to rivetkit-typescript/packages/effect/src/internal/utils.ts From 29c84402d0d0187bb6686f7fdd57c0ef22ca350f Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 16:44:08 +0200 Subject: [PATCH 185/306] refactor(effect): carry RivetkitRivetError as reason cause and shrink classification table --- .../packages/effect/src/Client.ts | 27 +- .../packages/effect/src/RivetError.ts | 478 +++++++----------- 2 files changed, 194 insertions(+), 311 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 8c6ff0973a..2682477e9f 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -63,6 +63,9 @@ export const make = Effect.fnUntraced(function* (options: Options = {}) { const decodeSuccess = Schema.decodeUnknownEffect( Schema.toCodecJson(action.successSchema), ); + const decodeError = decodeRejectedActionCall( + action.errorSchema, + ); const rpcMethod = `${actor.name}/${action._tag}`; @@ -91,9 +94,13 @@ export const make = Effect.fnUntraced(function* (options: Options = {}) { new RivetError.RivetError({ reason: new RivetError.InvalidEncoding( { - message: - "Could not encode action payload", - cause, + cause: { + group: "encoding", + code: "invalid", + message: + "Could not encode action payload", + metadata: cause, + }, }, ), }), @@ -109,9 +116,7 @@ export const make = Effect.fnUntraced(function* (options: Options = {}) { }), ).pipe( Effect.catch((unknownError) => - decodeRejectedActionCall( - action.errorSchema, - )(unknownError.cause), + decodeError(unknownError.cause), ), ); @@ -144,15 +149,7 @@ const decodeRejectedActionCall = ( return Effect.fnUntraced(function* (cause: unknown) { const rivetkitRivetError = yield* decodeRivetkitRivetError(cause).pipe( - Effect.mapError( - () => - new RivetError.RivetError({ - reason: new RivetError.UnknownError({ - message: "Unknown error", - cause, - }), - }), - ), + Effect.mapError(() => RivetError.fromUnknown(cause)), ); const actionErrorMetadata = yield* Effect.exit( diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index ee9443bbcb..a7905c8fd5 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -1,157 +1,124 @@ -import { Duration, Predicate, Schema } from "effect"; +import { Duration, Option, Predicate, Schema } from "effect"; import * as RivetkitErrors from "rivetkit/errors"; -import type * as RivetRivetError from "./internal/RivetRivetError"; +import * as RivetkitRivetError from "./internal/RivetRivetError"; const ReasonTypeId = "~@rivetkit/effect/RivetError/Reason" as const; const TypeId = "~@rivetkit/effect/RivetError" as const; -// ============================================================================ -// Reason classes -// ============================================================================ -// -// One class per semantic infrastructure-failure category exposed by the -// engine and client. Each reason carries its own wire `group` and `code` -// (defaulted via `Schema.tag` so call sites don't pass them), plus an -// `isRetryable` getter so callers can match on the reason `_tag` and -// decide retry policy without hand-rolling group/code switches. -// -// User-defined errors thrown via `UserError` inside an actor action ride -// through on the action's declared `errorSchema` and arrive in the typed -// error channel directly. They only fall through to the generic -// `UnknownUserError` reason below when the action did not declare a -// matching schema. - -/** `auth.forbidden` — `onAuth` rejected the request. */ export class Forbidden extends Schema.TaggedErrorClass( `${ReasonTypeId}/Forbidden`, -)("Forbidden", { - group: Schema.tag("auth"), - code: Schema.tag("forbidden"), - message: Schema.String, -}) { +)("Forbidden", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { return false; } } -/** `actor.not_found` — gateway target resolution returned no actor. */ export class ActorNotFound extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorNotFound`, -)("ActorNotFound", { - group: Schema.tag("actor"), - code: Schema.tag("not_found"), - message: Schema.String, -}) { +)("ActorNotFound", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { return false; } } -/** `actor.stopping` — request arrived while actor is shutting down. */ export class ActorStopping extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorStopping`, -)("ActorStopping", { - group: Schema.tag("actor"), - code: Schema.tag("stopping"), - message: Schema.String, -}) { +)("ActorStopping", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { return false; } } -/** `actor.restarting` — actor mid-restart; retry after `retryAfter`. */ export class ActorRestarting extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorRestarting`, )("ActorRestarting", { - group: Schema.tag("actor"), - code: Schema.tag("restarting"), - message: Schema.String, retryAfter: Schema.optional(Schema.Duration), phase: Schema.optional( Schema.Literals(["stopping", "sleeping", "waking", "runner_shutdown"]), ), + cause: RivetkitRivetError.RivetkitRivetError, }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { return true; } } -/** `actor.action_not_found` — no action by that name on the actor. */ export class ActionNotFound extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActionNotFound`, -)("ActionNotFound", { - group: Schema.tag("actor"), - code: Schema.tag("action_not_found"), - message: Schema.String, -}) { +)("ActionNotFound", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { return false; } } -/** `actor.action_timed_out` — server-side action timeout. */ export class ActionTimedOut extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActionTimedOut`, -)("ActionTimedOut", { - group: Schema.tag("actor"), - code: Schema.tag("action_timed_out"), - message: Schema.String, -}) { +)("ActionTimedOut", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { return true; } } -/** `actor.aborted` — action explicitly aborted server-side. */ export class ActionAborted extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActionAborted`, -)("ActionAborted", { - group: Schema.tag("actor"), - code: Schema.tag("aborted"), - message: Schema.String, -}) { +)("ActionAborted", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { return false; } } -/** `actor.overloaded` — actor channel at capacity. */ export class Overloaded extends Schema.TaggedErrorClass( `${ReasonTypeId}/Overloaded`, )("Overloaded", { - group: Schema.tag("actor"), - code: Schema.tag("overloaded"), - message: Schema.String, channel: Schema.optional(Schema.String), capacity: Schema.optional(Schema.Number), operation: Schema.optional(Schema.String), + cause: RivetkitRivetError.RivetkitRivetError, }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { return true; } } -/** - * `message.incoming_too_long` / `message.outgoing_too_long`. Match on - * `code` to distinguish direction. - */ export class MessageTooLong extends Schema.TaggedErrorClass( `${ReasonTypeId}/MessageTooLong`, -)("MessageTooLong", { - group: Schema.tag("message"), - code: Schema.Literals(["incoming_too_long", "outgoing_too_long"]), - message: Schema.String, -}) { +)("MessageTooLong", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { return false; } @@ -159,86 +126,73 @@ export class MessageTooLong extends Schema.TaggedErrorClass( const queueRetryableCodes = new Set(["full", "timed_out"]); -/** `queue.*` — queue-related server errors. `code` keeps the raw engine code. */ export class QueueError extends Schema.TaggedErrorClass( `${ReasonTypeId}/QueueError`, -)("QueueError", { - group: Schema.tag("queue"), - code: Schema.String, - message: Schema.String, -}) { +)("QueueError", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { - return queueRetryableCodes.has(this.code); + return queueRetryableCodes.has(this.cause.code); } } -/** `encoding.invalid` — unsupported encoding negotiated. */ export class InvalidEncoding extends Schema.TaggedErrorClass( `${ReasonTypeId}/InvalidEncoding`, -)("InvalidEncoding", { - group: Schema.tag("encoding"), - code: Schema.tag("invalid"), - message: Schema.String, - cause: Schema.optional(Schema.Defect), -}) { +)("InvalidEncoding", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { return false; } } -/** `request.invalid` — malformed `ActorQuery` or request payload. */ export class InvalidRequest extends Schema.TaggedErrorClass( `${ReasonTypeId}/InvalidRequest`, -)("InvalidRequest", { - group: Schema.tag("request"), - code: Schema.tag("invalid"), - message: Schema.String, -}) { +)("InvalidRequest", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { return false; } } -/** `client.connection_open_failed` — websocket open failed. */ export class ConnectionOpenFailed extends Schema.TaggedErrorClass( `${ReasonTypeId}/ConnectionOpenFailed`, -)("ConnectionOpenFailed", { - group: Schema.tag("client"), - code: Schema.tag("connection_open_failed"), - message: Schema.String, -}) { +)("ConnectionOpenFailed", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { return true; } } -/** `client.get_params_failed` — user `getParams()` callback threw. */ export class GetParamsFailed extends Schema.TaggedErrorClass( `${ReasonTypeId}/GetParamsFailed`, -)("GetParamsFailed", { - group: Schema.tag("client"), - code: Schema.tag("get_params_failed"), - message: Schema.String, -}) { +)("GetParamsFailed", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { return false; } } -/** `ws.going_away` / generic transport drop — connection lost, retry. */ export class ConnectionLost extends Schema.TaggedErrorClass( `${ReasonTypeId}/ConnectionLost`, -)("ConnectionLost", { - group: Schema.tag("ws"), - code: Schema.tag("going_away"), - message: Schema.String, -}) { +)("ConnectionLost", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { return true; } @@ -250,17 +204,27 @@ const guardRetryableCodes = new Set([ "service_unavailable", ]); -/** `guard.*` — engine guard/scheduler failures; `code` keeps the raw engine code. */ export class GuardError extends Schema.TaggedErrorClass( `${ReasonTypeId}/GuardError`, -)("GuardError", { - group: Schema.tag("guard"), - code: Schema.String, - message: Schema.String, -}) { +)("GuardError", { cause: RivetkitRivetError.RivetkitRivetError }) { + readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } + get isRetryable(): boolean { + return guardRetryableCodes.has(this.cause.code); + } +} + +export class InternalError extends Schema.TaggedErrorClass( + `${ReasonTypeId}/InternalError`, +)("InternalError", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } get isRetryable(): boolean { - return guardRetryableCodes.has(this.code); + return false; } } @@ -276,30 +240,11 @@ export class GuardError extends Schema.TaggedErrorClass( */ export class UnknownUserError extends Schema.TaggedErrorClass( `${ReasonTypeId}/UnknownUserError`, -)("UnknownUserError", { - group: Schema.tag("user"), - code: Schema.String, - message: Schema.String, - metadata: Schema.optional(Schema.Unknown), -}) { +)("UnknownUserError", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; - get isRetryable(): boolean { - return false; + override get message(): string { + return this.cause.message; } -} - -/** - * Sanitized internal error from `rivetkit-core`. Original details live in - * the server logs (or set `RIVET_EXPOSE_ERRORS=1` to inline them in dev). - */ -export class InternalError extends Schema.TaggedErrorClass( - `${ReasonTypeId}/InternalError`, -)("InternalError", { - group: Schema.tag("rivetkit"), - code: Schema.tag("internal_error"), - message: Schema.String, -}) { - readonly [ReasonTypeId] = ReasonTypeId; get isRetryable(): boolean { return false; } @@ -307,17 +252,14 @@ export class InternalError extends Schema.TaggedErrorClass( /** * Forward-compatible catch-all for `(group, code)` pairs the SDK does - * not recognize yet. Keeps the raw rivetkit fields so newer engine errors - * still surface usefully through older SDKs. + * not recognize yet, and for malformed non-Rivet failures. Known wire + * fields are mirrored when present, while `cause` preserves the raw input. */ export class UnknownError extends Schema.TaggedErrorClass( `${ReasonTypeId}/UnknownError`, )("UnknownError", { - group: Schema.optional(Schema.String), - code: Schema.optional(Schema.String), message: Schema.String, - metadata: Schema.optional(Schema.Unknown), - cause: Schema.optional(Schema.Defect), + cause: Schema.Union([RivetkitRivetError.RivetkitRivetError, Schema.Defect]), }) { readonly [ReasonTypeId] = ReasonTypeId; get isRetryable(): boolean { @@ -456,168 +398,112 @@ const readMetaField = (metadata: unknown, key: string): unknown => { return (metadata as Record)[key]; }; -// Classification table: ordered list of `(group, code, factory)` entries. -// We match through `isRivetErrorCode` from `rivetkit/errors` so a rename -// on the canonical wire-shape side becomes a compile-time signal here. -// Reasons whose `code` is variable (queue, guard, user) and any -// rivetkit-core internal-error aliases are handled by the group-level -// fallback below. -type FixedFactory = { - group: string; - code: string; - build: (message: string, metadata: unknown) => Reason; +// Classification table: maps canonical `${group}.${code}` to the reason +// class that should wrap the wire error. Reasons whose `code` is variable +// (queue, guard, user) and reasons that need to decode metadata +// (ActorRestarting, Overloaded) are handled inline below. +const simpleReasonByCode: Record< + string, + new (props: { + cause: RivetkitRivetError.RivetkitRivetError; + }) => Reason +> = { + "auth.forbidden": Forbidden, + "actor.not_found": ActorNotFound, + "actor.stopping": ActorStopping, + "actor.action_not_found": ActionNotFound, + "actor.action_timed_out": ActionTimedOut, + "actor.aborted": ActionAborted, + "message.incoming_too_long": MessageTooLong, + "message.outgoing_too_long": MessageTooLong, + "encoding.invalid": InvalidEncoding, + "request.invalid": InvalidRequest, + "client.connection_open_failed": ConnectionOpenFailed, + "client.get_params_failed": GetParamsFailed, + "ws.going_away": ConnectionLost, + "core.internal_error": InternalError, + "rivetkit.internal_error": InternalError, }; -const fixedFactories: ReadonlyArray = [ - { - group: "auth", - code: "forbidden", - build: (message) => new Forbidden({ message }), - }, - { - group: "actor", - code: "not_found", - build: (message) => new ActorNotFound({ message }), - }, - { - group: "actor", - code: "stopping", - build: (message) => new ActorStopping({ message }), - }, - { - group: "actor", - code: "restarting", - build: (message, metadata) => { - const retryAfterMs = readMetaField(metadata, "retryAfterMs"); - const phase = readMetaField(metadata, "phase"); - const allowedPhases = new Set([ - "stopping", - "sleeping", - "waking", - "runner_shutdown", - ]); - return new ActorRestarting({ - message, - ...(typeof retryAfterMs === "number" - ? { retryAfter: Duration.millis(retryAfterMs) } - : {}), - ...(typeof phase === "string" && allowedPhases.has(phase) - ? { phase: phase as ActorRestarting["phase"] } - : {}), - }); - }, - }, - { - group: "actor", - code: "action_not_found", - build: (message) => new ActionNotFound({ message }), - }, - { - group: "actor", - code: "action_timed_out", - build: (message) => new ActionTimedOut({ message }), - }, - { - group: "actor", - code: "aborted", - build: (message) => new ActionAborted({ message }), - }, - { - group: "actor", - code: "overloaded", - build: (message, metadata) => { - const channel = readMetaField(metadata, "channel"); - const capacity = readMetaField(metadata, "capacity"); - const operation = readMetaField(metadata, "operation"); - return new Overloaded({ - message, - ...(typeof channel === "string" ? { channel } : {}), - ...(typeof capacity === "number" ? { capacity } : {}), - ...(typeof operation === "string" ? { operation } : {}), - }); - }, - }, - { - group: "message", - code: "incoming_too_long", - build: (message) => - new MessageTooLong({ message, code: "incoming_too_long" }), - }, - { - group: "message", - code: "outgoing_too_long", - build: (message) => - new MessageTooLong({ message, code: "outgoing_too_long" }), - }, - { - group: "encoding", - code: "invalid", - build: (message) => new InvalidEncoding({ message }), - }, - { - group: "request", - code: "invalid", - build: (message) => new InvalidRequest({ message }), - }, - { - group: "client", - code: "connection_open_failed", - build: (message) => new ConnectionOpenFailed({ message }), - }, - { - group: "client", - code: "get_params_failed", - build: (message) => new GetParamsFailed({ message }), - }, - { - group: "ws", - code: "going_away", - build: (message) => new ConnectionLost({ message }), - }, - { - group: "core", - code: "internal_error", - build: (message) => new InternalError({ message }), - }, - { - group: "rivetkit", - code: "internal_error", - build: (message) => new InternalError({ message }), - }, -]; +// Static check that every key above is a canonical (group, code) pair +// recognized by `rivetkit/errors`. Renaming a wire code on the canonical +// side surfaces here as a runtime failure during module init. +for (const key of Object.keys(simpleReasonByCode)) { + const [group, code] = key.split(".") as [string, string]; + if ( + !RivetkitErrors.isRivetErrorCode( + { group, code, message: "" }, + group, + code, + ) + ) { + throw new Error(`unknown rivetkit error code: ${key}`); + } +} + +const allowedRestartingPhases = new Set([ + "stopping", + "sleeping", + "waking", + "runner_shutdown", +]); const reasonFromRivetkitRivetError = ( - rivetkitRivetError: RivetRivetError.RivetkitRivetError, + error: RivetkitRivetError.RivetkitRivetError, ): Reason => { - const { group, code, message, metadata } = rivetkitRivetError; - for (const entry of fixedFactories) { - if ( - RivetkitErrors.isRivetErrorCode( - rivetkitRivetError, - entry.group, - entry.code, - ) - ) { - return entry.build(message, metadata); - } - } - if (group === "queue") return new QueueError({ message, code }); - if (group === "guard") return new GuardError({ message, code }); - if (group === "user") { - return new UnknownUserError({ - message, - code, - ...(metadata !== undefined ? { metadata } : {}), + const Cls = simpleReasonByCode[`${error.group}.${error.code}`]; + if (Cls) return new Cls({ cause: error }); + if (error.group === "actor" && error.code === "restarting") { + const retryAfterMs = readMetaField(error.metadata, "retryAfterMs"); + const phase = readMetaField(error.metadata, "phase"); + return new ActorRestarting({ + cause: error, + ...(typeof retryAfterMs === "number" + ? { retryAfter: Duration.millis(retryAfterMs) } + : {}), + ...(typeof phase === "string" && allowedRestartingPhases.has(phase) + ? { phase: phase as ActorRestarting["phase"] } + : {}), + }); + } + if (error.group === "actor" && error.code === "overloaded") { + const channel = readMetaField(error.metadata, "channel"); + const capacity = readMetaField(error.metadata, "capacity"); + const operation = readMetaField(error.metadata, "operation"); + return new Overloaded({ + cause: error, + ...(typeof channel === "string" ? { channel } : {}), + ...(typeof capacity === "number" ? { capacity } : {}), + ...(typeof operation === "string" ? { operation } : {}), }); } + if (error.group === "queue") return new QueueError({ cause: error }); + if (error.group === "guard") return new GuardError({ cause: error }); + if (error.group === "user") return new UnknownUserError({ cause: error }); return new UnknownError({ - group, - code, - message, - ...(metadata !== undefined ? { metadata } : {}), + message: error.message, + cause: error, }); }; export const fromRivetkitRivetError = ( - e: RivetRivetError.RivetkitRivetError, + e: RivetkitRivetError.RivetkitRivetError, ): RivetError => new RivetError({ reason: reasonFromRivetkitRivetError(e) }); + +const decodeRivetkitRivetErrorOption = Schema.decodeUnknownOption( + RivetkitRivetError.RivetkitRivetError, +); + +export const fromUnknown = (cause: unknown): RivetError => { + if (isRivetError(cause)) return cause; + + const decoded = decodeRivetkitRivetErrorOption(cause); + if (Option.isSome(decoded)) return fromRivetkitRivetError(decoded.value); + + return new RivetError({ + reason: new UnknownError({ + message: "Unknown error", + cause, + }), + }); +}; From e0964f872990bb6103ebcad750675327be523c34 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 17:28:37 +0200 Subject: [PATCH 186/306] refactor(effect): extend RivetkitRivetError schema with actor, public, and statusCode fields --- .../effect/src/internal/RivetRivetError.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts b/rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts index e24ac5591a..069ed4687d 100644 --- a/rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts +++ b/rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts @@ -14,7 +14,9 @@ export const ActionErrorMetadata = Schema.Struct({ export type ActionErrorMetadata = typeof ActionErrorMetadata.Type; -export const makeActionErrorMetadata = (error: unknown): ActionErrorMetadata => ({ +export const makeActionErrorMetadata = ( + error: unknown, +): ActionErrorMetadata => ({ _tag: ActionErrorMetadataTag, version: ActionErrorSchemaVersion, error, @@ -36,11 +38,20 @@ export const makeActionError = ( }, ); +const ActorSpecifier = Schema.Struct({ + actorId: Schema.String, + generation: Schema.Number, + key: Schema.optional(Schema.String), +}) satisfies Schema.Codec>; + export const RivetkitRivetError = Schema.Struct({ - message: Schema.String, group: Schema.String, code: Schema.String, + message: Schema.String, metadata: Schema.optional(Schema.Unknown), + public: Schema.optional(Schema.Boolean), + statusCode: Schema.optional(Schema.Number), + actor: Schema.optional(ActorSpecifier), }) satisfies Schema.Codec; export type RivetkitRivetError = typeof RivetkitRivetError.Type; From e7c7ada5e90d4714bf6eec983ab7732187244bcc Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 17:29:50 +0200 Subject: [PATCH 187/306] refactor(effect): rename Overloaded to ActorOverloaded and remove QueueError class --- .../packages/effect/src/RivetError.ts | 46 +++---------------- 1 file changed, 7 insertions(+), 39 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index a7905c8fd5..f666d3ad8d 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -95,9 +95,9 @@ export class ActionAborted extends Schema.TaggedErrorClass( } } -export class Overloaded extends Schema.TaggedErrorClass( - `${ReasonTypeId}/Overloaded`, -)("Overloaded", { +export class ActorOverloaded extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ActorOverloaded`, +)("ActorOverloaded", { channel: Schema.optional(Schema.String), capacity: Schema.optional(Schema.Number), operation: Schema.optional(Schema.String), @@ -124,20 +124,6 @@ export class MessageTooLong extends Schema.TaggedErrorClass( } } -const queueRetryableCodes = new Set(["full", "timed_out"]); - -export class QueueError extends Schema.TaggedErrorClass( - `${ReasonTypeId}/QueueError`, -)("QueueError", { cause: RivetkitRivetError.RivetkitRivetError }) { - readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { - return this.cause.message; - } - get isRetryable(): boolean { - return queueRetryableCodes.has(this.cause.code); - } -} - export class InvalidEncoding extends Schema.TaggedErrorClass( `${ReasonTypeId}/InvalidEncoding`, )("InvalidEncoding", { cause: RivetkitRivetError.RivetkitRivetError }) { @@ -279,9 +265,8 @@ export type Reason = | ActionNotFound | ActionTimedOut | ActionAborted - | Overloaded + | ActorOverloaded | MessageTooLong - | QueueError | InvalidEncoding | InvalidRequest | ConnectionOpenFailed @@ -301,9 +286,8 @@ export const Reason: Schema.Union< typeof ActionNotFound, typeof ActionTimedOut, typeof ActionAborted, - typeof Overloaded, + typeof ActorOverloaded, typeof MessageTooLong, - typeof QueueError, typeof InvalidEncoding, typeof InvalidRequest, typeof ConnectionOpenFailed, @@ -322,9 +306,8 @@ export const Reason: Schema.Union< ActionNotFound, ActionTimedOut, ActionAborted, - Overloaded, + ActorOverloaded, MessageTooLong, - QueueError, InvalidEncoding, InvalidRequest, ConnectionOpenFailed, @@ -398,10 +381,6 @@ const readMetaField = (metadata: unknown, key: string): unknown => { return (metadata as Record)[key]; }; -// Classification table: maps canonical `${group}.${code}` to the reason -// class that should wrap the wire error. Reasons whose `code` is variable -// (queue, guard, user) and reasons that need to decode metadata -// (ActorRestarting, Overloaded) are handled inline below. const simpleReasonByCode: Record< string, new (props: { @@ -414,6 +393,7 @@ const simpleReasonByCode: Record< "actor.action_not_found": ActionNotFound, "actor.action_timed_out": ActionTimedOut, "actor.aborted": ActionAborted, + "actor.overloaded": ActorOverloaded, "message.incoming_too_long": MessageTooLong, "message.outgoing_too_long": MessageTooLong, "encoding.invalid": InvalidEncoding, @@ -466,18 +446,6 @@ const reasonFromRivetkitRivetError = ( : {}), }); } - if (error.group === "actor" && error.code === "overloaded") { - const channel = readMetaField(error.metadata, "channel"); - const capacity = readMetaField(error.metadata, "capacity"); - const operation = readMetaField(error.metadata, "operation"); - return new Overloaded({ - cause: error, - ...(typeof channel === "string" ? { channel } : {}), - ...(typeof capacity === "number" ? { capacity } : {}), - ...(typeof operation === "string" ? { operation } : {}), - }); - } - if (error.group === "queue") return new QueueError({ cause: error }); if (error.group === "guard") return new GuardError({ cause: error }); if (error.group === "user") return new UnknownUserError({ cause: error }); return new UnknownError({ From ae7e3856724c896a85654f8f6320b3bcc401a8f5 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 17:33:10 +0200 Subject: [PATCH 188/306] refactor(effect): remove unused comments and update RivetError JSDoc formatting --- .../packages/effect/src/RivetError.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index f666d3ad8d..1d85559f3a 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -253,10 +253,6 @@ export class UnknownError extends Schema.TaggedErrorClass( } } -// ============================================================================ -// Reason union -// ============================================================================ - export type Reason = | Forbidden | ActorNotFound @@ -322,14 +318,10 @@ export const Reason: Schema.Union< export const isReason = (u: unknown): u is Reason => Predicate.hasProperty(u, ReasonTypeId); -// ============================================================================ -// Top-level RivetError -// ============================================================================ - /** - * The infrastructure-failure error surfaced by `@rivetkit/effect` action - * calls. Wraps a discriminated `reason` of all known engine and client - * failure modes. + * The infrastructure-failure error surfaced by `@rivetkit/effect` + * calls. Wraps a discriminated `reason` of all known failure + * modes. * * Recover with `Effect.catchReason` / `Effect.catchReasons` / * `Effect.unwrapReason`: From 4a5db3ebc9c5a2506d976bee8d946e213b7d7a68 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 17:53:05 +0200 Subject: [PATCH 189/306] refactor(effect): remove unused error classes and streamline Reason schema --- .../packages/effect/src/RivetError.ts | 54 ++----------------- 1 file changed, 3 insertions(+), 51 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 1d85559f3a..f4e4f3e67a 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -148,42 +148,6 @@ export class InvalidRequest extends Schema.TaggedErrorClass( } } -export class ConnectionOpenFailed extends Schema.TaggedErrorClass( - `${ReasonTypeId}/ConnectionOpenFailed`, -)("ConnectionOpenFailed", { cause: RivetkitRivetError.RivetkitRivetError }) { - readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { - return this.cause.message; - } - get isRetryable(): boolean { - return true; - } -} - -export class GetParamsFailed extends Schema.TaggedErrorClass( - `${ReasonTypeId}/GetParamsFailed`, -)("GetParamsFailed", { cause: RivetkitRivetError.RivetkitRivetError }) { - readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { - return this.cause.message; - } - get isRetryable(): boolean { - return false; - } -} - -export class ConnectionLost extends Schema.TaggedErrorClass( - `${ReasonTypeId}/ConnectionLost`, -)("ConnectionLost", { cause: RivetkitRivetError.RivetkitRivetError }) { - readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { - return this.cause.message; - } - get isRetryable(): boolean { - return true; - } -} - const guardRetryableCodes = new Set([ "actor_runner_failed", "actor_ready_timeout", @@ -265,12 +229,9 @@ export type Reason = | MessageTooLong | InvalidEncoding | InvalidRequest - | ConnectionOpenFailed - | GetParamsFailed - | ConnectionLost | GuardError - | UnknownUserError | InternalError + | UnknownUserError | UnknownError; export const Reason: Schema.Union< @@ -286,12 +247,9 @@ export const Reason: Schema.Union< typeof MessageTooLong, typeof InvalidEncoding, typeof InvalidRequest, - typeof ConnectionOpenFailed, - typeof GetParamsFailed, - typeof ConnectionLost, typeof GuardError, - typeof UnknownUserError, typeof InternalError, + typeof UnknownUserError, typeof UnknownError, ] > = Schema.Union([ @@ -306,12 +264,9 @@ export const Reason: Schema.Union< MessageTooLong, InvalidEncoding, InvalidRequest, - ConnectionOpenFailed, - GetParamsFailed, - ConnectionLost, GuardError, - UnknownUserError, InternalError, + UnknownUserError, UnknownError, ]); @@ -390,9 +345,6 @@ const simpleReasonByCode: Record< "message.outgoing_too_long": MessageTooLong, "encoding.invalid": InvalidEncoding, "request.invalid": InvalidRequest, - "client.connection_open_failed": ConnectionOpenFailed, - "client.get_params_failed": GetParamsFailed, - "ws.going_away": ConnectionLost, "core.internal_error": InternalError, "rivetkit.internal_error": InternalError, }; From ac3d4717b4ffb31e91adc839a078e5f2d259fbdf Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 18:09:22 +0200 Subject: [PATCH 190/306] refactor(effect): remove isRetryable accessor and simplify error Reason structure --- .../packages/effect/src/RivetError.ts | 138 +----------------- 1 file changed, 4 insertions(+), 134 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index f4e4f3e67a..f44c97bd4b 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -1,4 +1,4 @@ -import { Duration, Option, Predicate, Schema } from "effect"; +import { Option, Predicate, Schema } from "effect"; import * as RivetkitErrors from "rivetkit/errors"; import * as RivetkitRivetError from "./internal/RivetRivetError"; @@ -12,9 +12,6 @@ export class Forbidden extends Schema.TaggedErrorClass( override get message(): string { return this.cause.message; } - get isRetryable(): boolean { - return false; - } } export class ActorNotFound extends Schema.TaggedErrorClass( @@ -24,9 +21,6 @@ export class ActorNotFound extends Schema.TaggedErrorClass( override get message(): string { return this.cause.message; } - get isRetryable(): boolean { - return false; - } } export class ActorStopping extends Schema.TaggedErrorClass( @@ -36,27 +30,15 @@ export class ActorStopping extends Schema.TaggedErrorClass( override get message(): string { return this.cause.message; } - get isRetryable(): boolean { - return false; - } } export class ActorRestarting extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorRestarting`, -)("ActorRestarting", { - retryAfter: Schema.optional(Schema.Duration), - phase: Schema.optional( - Schema.Literals(["stopping", "sleeping", "waking", "runner_shutdown"]), - ), - cause: RivetkitRivetError.RivetkitRivetError, -}) { +)("ActorRestarting", { cause: RivetkitRivetError.RivetkitRivetError }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { return this.cause.message; } - get isRetryable(): boolean { - return true; - } } export class ActionNotFound extends Schema.TaggedErrorClass( @@ -66,9 +48,6 @@ export class ActionNotFound extends Schema.TaggedErrorClass( override get message(): string { return this.cause.message; } - get isRetryable(): boolean { - return false; - } } export class ActionTimedOut extends Schema.TaggedErrorClass( @@ -78,9 +57,6 @@ export class ActionTimedOut extends Schema.TaggedErrorClass( override get message(): string { return this.cause.message; } - get isRetryable(): boolean { - return true; - } } export class ActionAborted extends Schema.TaggedErrorClass( @@ -90,9 +66,6 @@ export class ActionAborted extends Schema.TaggedErrorClass( override get message(): string { return this.cause.message; } - get isRetryable(): boolean { - return false; - } } export class ActorOverloaded extends Schema.TaggedErrorClass( @@ -107,9 +80,6 @@ export class ActorOverloaded extends Schema.TaggedErrorClass( override get message(): string { return this.cause.message; } - get isRetryable(): boolean { - return true; - } } export class MessageTooLong extends Schema.TaggedErrorClass( @@ -119,9 +89,6 @@ export class MessageTooLong extends Schema.TaggedErrorClass( override get message(): string { return this.cause.message; } - get isRetryable(): boolean { - return false; - } } export class InvalidEncoding extends Schema.TaggedErrorClass( @@ -131,9 +98,6 @@ export class InvalidEncoding extends Schema.TaggedErrorClass( override get message(): string { return this.cause.message; } - get isRetryable(): boolean { - return false; - } } export class InvalidRequest extends Schema.TaggedErrorClass( @@ -143,17 +107,8 @@ export class InvalidRequest extends Schema.TaggedErrorClass( override get message(): string { return this.cause.message; } - get isRetryable(): boolean { - return false; - } } -const guardRetryableCodes = new Set([ - "actor_runner_failed", - "actor_ready_timeout", - "service_unavailable", -]); - export class GuardError extends Schema.TaggedErrorClass( `${ReasonTypeId}/GuardError`, )("GuardError", { cause: RivetkitRivetError.RivetkitRivetError }) { @@ -161,9 +116,6 @@ export class GuardError extends Schema.TaggedErrorClass( override get message(): string { return this.cause.message; } - get isRetryable(): boolean { - return guardRetryableCodes.has(this.cause.code); - } } export class InternalError extends Schema.TaggedErrorClass( @@ -173,9 +125,6 @@ export class InternalError extends Schema.TaggedErrorClass( override get message(): string { return this.cause.message; } - get isRetryable(): boolean { - return false; - } } /** @@ -195,9 +144,6 @@ export class UnknownUserError extends Schema.TaggedErrorClass( override get message(): string { return this.cause.message; } - get isRetryable(): boolean { - return false; - } } /** @@ -212,9 +158,6 @@ export class UnknownError extends Schema.TaggedErrorClass( cause: Schema.Union([RivetkitRivetError.RivetkitRivetError, Schema.Defect]), }) { readonly [ReasonTypeId] = ReasonTypeId; - get isRetryable(): boolean { - return false; - } } export type Reason = @@ -305,16 +248,6 @@ export class RivetError extends Schema.TaggedErrorClass( readonly [TypeId] = TypeId; override readonly cause = this.reason; - get isRetryable(): boolean { - return this.reason.isRetryable; - } - - get retryAfter(): Duration.Duration | undefined { - return "retryAfter" in this.reason - ? (this.reason.retryAfter as Duration.Duration | undefined) - : undefined; - } - override get message(): string { return this.reason.message || this.reason._tag; } @@ -323,74 +256,11 @@ export class RivetError extends Schema.TaggedErrorClass( export const isRivetError = (u: unknown): u is RivetError => Predicate.hasProperty(u, TypeId); -const readMetaField = (metadata: unknown, key: string): unknown => { - if (typeof metadata !== "object" || metadata === null) return undefined; - return (metadata as Record)[key]; -}; - -const simpleReasonByCode: Record< - string, - new (props: { - cause: RivetkitRivetError.RivetkitRivetError; - }) => Reason -> = { - "auth.forbidden": Forbidden, - "actor.not_found": ActorNotFound, - "actor.stopping": ActorStopping, - "actor.action_not_found": ActionNotFound, - "actor.action_timed_out": ActionTimedOut, - "actor.aborted": ActionAborted, - "actor.overloaded": ActorOverloaded, - "message.incoming_too_long": MessageTooLong, - "message.outgoing_too_long": MessageTooLong, - "encoding.invalid": InvalidEncoding, - "request.invalid": InvalidRequest, - "core.internal_error": InternalError, - "rivetkit.internal_error": InternalError, -}; - -// Static check that every key above is a canonical (group, code) pair -// recognized by `rivetkit/errors`. Renaming a wire code on the canonical -// side surfaces here as a runtime failure during module init. -for (const key of Object.keys(simpleReasonByCode)) { - const [group, code] = key.split(".") as [string, string]; - if ( - !RivetkitErrors.isRivetErrorCode( - { group, code, message: "" }, - group, - code, - ) - ) { - throw new Error(`unknown rivetkit error code: ${key}`); - } -} - -const allowedRestartingPhases = new Set([ - "stopping", - "sleeping", - "waking", - "runner_shutdown", -]); - const reasonFromRivetkitRivetError = ( error: RivetkitRivetError.RivetkitRivetError, ): Reason => { - const Cls = simpleReasonByCode[`${error.group}.${error.code}`]; - if (Cls) return new Cls({ cause: error }); - if (error.group === "actor" && error.code === "restarting") { - const retryAfterMs = readMetaField(error.metadata, "retryAfterMs"); - const phase = readMetaField(error.metadata, "phase"); - return new ActorRestarting({ - cause: error, - ...(typeof retryAfterMs === "number" - ? { retryAfter: Duration.millis(retryAfterMs) } - : {}), - ...(typeof phase === "string" && allowedRestartingPhases.has(phase) - ? { phase: phase as ActorRestarting["phase"] } - : {}), - }); - } - if (error.group === "guard") return new GuardError({ cause: error }); + // TODO: Implement + if (error.group === "user") return new UnknownUserError({ cause: error }); return new UnknownError({ message: error.message, From 4f66376b4faf19b717569e4e0e72cf1ada2a78b6 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 18:11:31 +0200 Subject: [PATCH 191/306] refactor(effect): remove outdated RivetError.catchReason example from JSDoc --- rivetkit-typescript/packages/effect/src/RivetError.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index f44c97bd4b..77ba0e2e90 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -226,9 +226,6 @@ export const isReason = (u: unknown): u is Reason => * * ```ts * program.pipe( - * Effect.catchReason("RivetError", "ActorRestarting", (r) => - * Effect.sleep(r.retryAfter ?? "100 millis").pipe(Effect.andThen(retry)), - * ), * Effect.catchReasons("RivetError", { * Forbidden: () => Effect.fail(new MyAuthError()), * ConnectionLost: () => Effect.logWarning("reconnecting"), From d97ef73c4ffb8f651c646df1c271b51a8d256a0d Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 18:19:37 +0200 Subject: [PATCH 192/306] refactor(effect): update error classes to improve consistency and rename MessageTooLong to IncomingMessageTooLong and OutgoingMessageTooLong --- .../packages/effect/src/RivetError.ts | 131 +++++++++++++++--- 1 file changed, 110 insertions(+), 21 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 77ba0e2e90..64dc43fe04 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -7,7 +7,9 @@ const TypeId = "~@rivetkit/effect/RivetError" as const; export class Forbidden extends Schema.TaggedErrorClass( `${ReasonTypeId}/Forbidden`, -)("Forbidden", { cause: RivetkitRivetError.RivetkitRivetError }) { +)("Forbidden", { + cause: RivetkitRivetError.RivetkitRivetError, +}) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { return this.cause.message; @@ -16,7 +18,9 @@ export class Forbidden extends Schema.TaggedErrorClass( export class ActorNotFound extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorNotFound`, -)("ActorNotFound", { cause: RivetkitRivetError.RivetkitRivetError }) { +)("ActorNotFound", { + cause: RivetkitRivetError.RivetkitRivetError, +}) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { return this.cause.message; @@ -25,7 +29,9 @@ export class ActorNotFound extends Schema.TaggedErrorClass( export class ActorStopping extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorStopping`, -)("ActorStopping", { cause: RivetkitRivetError.RivetkitRivetError }) { +)("ActorStopping", { + cause: RivetkitRivetError.RivetkitRivetError, +}) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { return this.cause.message; @@ -34,7 +40,9 @@ export class ActorStopping extends Schema.TaggedErrorClass( export class ActorRestarting extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorRestarting`, -)("ActorRestarting", { cause: RivetkitRivetError.RivetkitRivetError }) { +)("ActorRestarting", { + cause: RivetkitRivetError.RivetkitRivetError, +}) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { return this.cause.message; @@ -43,7 +51,9 @@ export class ActorRestarting extends Schema.TaggedErrorClass( export class ActionNotFound extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActionNotFound`, -)("ActionNotFound", { cause: RivetkitRivetError.RivetkitRivetError }) { +)("ActionNotFound", { + cause: RivetkitRivetError.RivetkitRivetError, +}) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { return this.cause.message; @@ -61,7 +71,9 @@ export class ActionTimedOut extends Schema.TaggedErrorClass( export class ActionAborted extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActionAborted`, -)("ActionAborted", { cause: RivetkitRivetError.RivetkitRivetError }) { +)("ActionAborted", { + cause: RivetkitRivetError.RivetkitRivetError, +}) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { return this.cause.message; @@ -71,9 +83,6 @@ export class ActionAborted extends Schema.TaggedErrorClass( export class ActorOverloaded extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorOverloaded`, )("ActorOverloaded", { - channel: Schema.optional(Schema.String), - capacity: Schema.optional(Schema.Number), - operation: Schema.optional(Schema.String), cause: RivetkitRivetError.RivetkitRivetError, }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -82,9 +91,22 @@ export class ActorOverloaded extends Schema.TaggedErrorClass( } } -export class MessageTooLong extends Schema.TaggedErrorClass( - `${ReasonTypeId}/MessageTooLong`, -)("MessageTooLong", { cause: RivetkitRivetError.RivetkitRivetError }) { +export class IncomingMessageTooLong extends Schema.TaggedErrorClass( + `${ReasonTypeId}/IncomingMessageTooLong`, +)("IncomingMessageTooLong", { + cause: RivetkitRivetError.RivetkitRivetError, +}) { + readonly [ReasonTypeId] = ReasonTypeId; + override get message(): string { + return this.cause.message; + } +} + +export class OutgoingMessageTooLong extends Schema.TaggedErrorClass( + `${ReasonTypeId}/OutgoingMessageTooLong`, +)("OutgoingMessageTooLong", { + cause: RivetkitRivetError.RivetkitRivetError, +}) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { return this.cause.message; @@ -93,7 +115,9 @@ export class MessageTooLong extends Schema.TaggedErrorClass( export class InvalidEncoding extends Schema.TaggedErrorClass( `${ReasonTypeId}/InvalidEncoding`, -)("InvalidEncoding", { cause: RivetkitRivetError.RivetkitRivetError }) { +)("InvalidEncoding", { + cause: RivetkitRivetError.RivetkitRivetError, +}) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { return this.cause.message; @@ -102,7 +126,9 @@ export class InvalidEncoding extends Schema.TaggedErrorClass( export class InvalidRequest extends Schema.TaggedErrorClass( `${ReasonTypeId}/InvalidRequest`, -)("InvalidRequest", { cause: RivetkitRivetError.RivetkitRivetError }) { +)("InvalidRequest", { + cause: RivetkitRivetError.RivetkitRivetError, +}) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { return this.cause.message; @@ -111,7 +137,9 @@ export class InvalidRequest extends Schema.TaggedErrorClass( export class GuardError extends Schema.TaggedErrorClass( `${ReasonTypeId}/GuardError`, -)("GuardError", { cause: RivetkitRivetError.RivetkitRivetError }) { +)("GuardError", { + cause: RivetkitRivetError.RivetkitRivetError, +}) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { return this.cause.message; @@ -120,7 +148,9 @@ export class GuardError extends Schema.TaggedErrorClass( export class InternalError extends Schema.TaggedErrorClass( `${ReasonTypeId}/InternalError`, -)("InternalError", { cause: RivetkitRivetError.RivetkitRivetError }) { +)("InternalError", { + cause: RivetkitRivetError.RivetkitRivetError, +}) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { return this.cause.message; @@ -169,7 +199,8 @@ export type Reason = | ActionTimedOut | ActionAborted | ActorOverloaded - | MessageTooLong + | IncomingMessageTooLong + | OutgoingMessageTooLong | InvalidEncoding | InvalidRequest | GuardError @@ -187,7 +218,8 @@ export const Reason: Schema.Union< typeof ActionTimedOut, typeof ActionAborted, typeof ActorOverloaded, - typeof MessageTooLong, + typeof IncomingMessageTooLong, + typeof OutgoingMessageTooLong, typeof InvalidEncoding, typeof InvalidRequest, typeof GuardError, @@ -204,7 +236,8 @@ export const Reason: Schema.Union< ActionTimedOut, ActionAborted, ActorOverloaded, - MessageTooLong, + IncomingMessageTooLong, + OutgoingMessageTooLong, InvalidEncoding, InvalidRequest, GuardError, @@ -256,9 +289,65 @@ export const isRivetError = (u: unknown): u is RivetError => const reasonFromRivetkitRivetError = ( error: RivetkitRivetError.RivetkitRivetError, ): Reason => { - // TODO: Implement + const { group, code } = error; + + switch (group) { + case "auth": { + if (code === "forbidden") return new Forbidden({ cause: error }); + break; + } + case "actor": { + switch (code) { + case "not_found": + return new ActorNotFound({ cause: error }); + case "stopping": + return new ActorStopping({ cause: error }); + case "restarting": + return new ActorRestarting({ cause: error }); + case "action_not_found": + return new ActionNotFound({ cause: error }); + case "action_timed_out": + return new ActionTimedOut({ cause: error }); + case "aborted": + return new ActionAborted({ cause: error }); + case "overloaded": + return new ActorOverloaded({ cause: error }); + case RivetkitErrors.INTERNAL_ERROR_CODE: + return new InternalError({ cause: error }); + } + break; + } + case "message": { + if (code === "incoming_too_long") { + return new IncomingMessageTooLong({ cause: error }); + } + if (code === "outgoing_too_long") { + return new OutgoingMessageTooLong({ cause: error }); + } + break; + } + case "encoding": { + if (code === "invalid") + return new InvalidEncoding({ cause: error }); + break; + } + case "request": { + if (code === "invalid") return new InvalidRequest({ cause: error }); + break; + } + case "guard": + return new GuardError({ cause: error }); + case "core": + case "rivetkit": { + if (code === RivetkitErrors.INTERNAL_ERROR_CODE) { + return new InternalError({ cause: error }); + } + break; + } + case "user": + return new UnknownUserError({ cause: error }); + } - if (error.group === "user") return new UnknownUserError({ cause: error }); return new UnknownError({ message: error.message, cause: error, From 93efdcba13fac28197280447f59c6bafb80a02c3 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 21:00:37 +0200 Subject: [PATCH 193/306] refactor(effect): simplify error classification by consolidating switch logic and refining fallback handling --- .../packages/effect/src/RivetError.ts | 95 +++++++------------ 1 file changed, 36 insertions(+), 59 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 64dc43fe04..7a9957365e 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -289,65 +289,41 @@ export const isRivetError = (u: unknown): u is RivetError => const reasonFromRivetkitRivetError = ( error: RivetkitRivetError.RivetkitRivetError, ): Reason => { - const { group, code } = error; - - switch (group) { - case "auth": { - if (code === "forbidden") return new Forbidden({ cause: error }); - break; - } - case "actor": { - switch (code) { - case "not_found": - return new ActorNotFound({ cause: error }); - case "stopping": - return new ActorStopping({ cause: error }); - case "restarting": - return new ActorRestarting({ cause: error }); - case "action_not_found": - return new ActionNotFound({ cause: error }); - case "action_timed_out": - return new ActionTimedOut({ cause: error }); - case "aborted": - return new ActionAborted({ cause: error }); - case "overloaded": - return new ActorOverloaded({ cause: error }); - case RivetkitErrors.INTERNAL_ERROR_CODE: - return new InternalError({ cause: error }); - } - break; - } - case "message": { - if (code === "incoming_too_long") { - return new IncomingMessageTooLong({ cause: error }); - } - if (code === "outgoing_too_long") { - return new OutgoingMessageTooLong({ cause: error }); - } - break; - } - case "encoding": { - if (code === "invalid") - return new InvalidEncoding({ cause: error }); - break; - } - case "request": { - if (code === "invalid") return new InvalidRequest({ cause: error }); - break; - } - case "guard": - return new GuardError({ cause: error }); - case "core": - case "rivetkit": { - if (code === RivetkitErrors.INTERNAL_ERROR_CODE) { - return new InternalError({ cause: error }); - } - break; - } - case "user": - return new UnknownUserError({ cause: error }); + switch (`${error.group}.${error.code}`) { + case `auth.${RivetkitErrors.forbiddenError().code}`: + return new Forbidden({ cause: error }); + case `actor.${RivetkitErrors.actorNotFound().code}`: + return new ActorNotFound({ cause: error }); + case `actor.${RivetkitErrors.actorStopping().code}`: + return new ActorStopping({ cause: error }); + case `actor.${RivetkitErrors.actorRestarting().code}`: + return new ActorRestarting({ cause: error }); + case `actor.action_not_found`: + return new ActionNotFound({ cause: error }); + case `actor.action_timed_out`: + return new ActionTimedOut({ cause: error }); + case `actor.aborted`: + return new ActionAborted({ cause: error }); + case `actor.overloaded`: + return new ActorOverloaded({ cause: error }); + case `actor.${RivetkitErrors.INTERNAL_ERROR_CODE}`: + case `core.${RivetkitErrors.INTERNAL_ERROR_CODE}`: + case `rivetkit.${RivetkitErrors.INTERNAL_ERROR_CODE}`: + return new InternalError({ cause: error }); + case `message.incoming_too_long`: + return new IncomingMessageTooLong({ cause: error }); + case `message.outgoing_too_long`: + return new OutgoingMessageTooLong({ cause: error }); + case `encoding.${RivetkitErrors.invalidEncoding().code}`: + return new InvalidEncoding({ cause: error }); + case `request.${RivetkitErrors.invalidRequest().code}`: + return new InvalidRequest({ cause: error }); } + // Group-wide fallbacks: any code under the group maps to a single reason. + if (error.group === "guard") return new GuardError({ cause: error }); + if (error.group === "user") return new UnknownUserError({ cause: error }); + return new UnknownError({ message: error.message, cause: error, @@ -365,12 +341,13 @@ const decodeRivetkitRivetErrorOption = Schema.decodeUnknownOption( export const fromUnknown = (cause: unknown): RivetError => { if (isRivetError(cause)) return cause; - const decoded = decodeRivetkitRivetErrorOption(cause); + const normalized = RivetkitErrors.toRivetError(cause); + const decoded = decodeRivetkitRivetErrorOption(normalized); if (Option.isSome(decoded)) return fromRivetkitRivetError(decoded.value); return new RivetError({ reason: new UnknownError({ - message: "Unknown error", + message: normalized.message, cause, }), }); From 6cd94724272c3dc56499ca614719515ab435bcb3 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 21:19:06 +0200 Subject: [PATCH 194/306] refactor(effect): replace RivetRivetError with RivetkitErrors for streamlined error handling and metadata decoding --- .../packages/effect/src/Actor.ts | 7 +-- .../packages/effect/src/Client.ts | 36 ++++++------ .../packages/effect/src/RivetError.ts | 58 ++++++++----------- .../{RivetRivetError.ts => ActionError.ts} | 24 +------- 4 files changed, 46 insertions(+), 79 deletions(-) rename rivetkit-typescript/packages/effect/src/internal/{RivetRivetError.ts => ActionError.ts} (55%) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 699af6f5b8..223e820462 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -21,7 +21,7 @@ import type * as RivetkitDb from "rivetkit/db"; import type * as Action from "./Action"; import type * as ActorState from "./ActorState"; import * as Client from "./Client"; -import * as RivetRivetError from "./internal/RivetRivetError"; +import * as ActionError from "./internal/ActionError"; import { readTraceMeta, rpcSystem } from "./internal/tracing"; import * as Registry from "./Registry"; import type * as RivetError from "./RivetError"; @@ -442,10 +442,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ).pipe(Effect.orDie); return yield* Effect.fail( - RivetRivetError.makeActionError( - action._tag, - encodedError, - ), + ActionError.make(action._tag, encodedError), ); } diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 2682477e9f..3f2fd5f3b5 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -1,8 +1,9 @@ import { Context, Effect, Exit, Layer, Record, Schema } from "effect"; import * as RivetkitClient from "rivetkit/client"; +import * as RivetkitErrors from "rivetkit/errors"; import type * as Action from "./Action"; import type * as Actor from "./Actor"; -import * as RivetRivetError from "./internal/RivetRivetError"; +import * as ActionError from "./internal/ActionError"; import { rpcSystem, type TraceMeta } from "./internal/tracing"; import * as RivetError from "./RivetError"; @@ -94,13 +95,15 @@ export const make = Effect.fnUntraced(function* (options: Options = {}) { new RivetError.RivetError({ reason: new RivetError.InvalidEncoding( { - cause: { - group: "encoding", - code: "invalid", - message: - "Could not encode action payload", - metadata: cause, - }, + cause: new RivetkitErrors.RivetError( + "encoding", + "invalid", + "Could not encode action payload", + { + public: true, + metadata: cause, + }, + ), }, ), }), @@ -134,23 +137,22 @@ export const make = Effect.fnUntraced(function* (options: Options = {}) { export const layer = (options: Options = {}): Layer.Layer => Layer.effect(Client, make(options)); +const decodeActionErrorMetadata = Schema.decodeUnknownEffect( + ActionError.ActionErrorMetadata, +); + const decodeRejectedActionCall = ( actionErrorSchema: E, ) => { - const decodeRivetkitRivetError = Schema.decodeUnknownEffect( - RivetRivetError.RivetkitRivetError, - ); - const decodeActionErrorMetadata = Schema.decodeUnknownEffect( - RivetRivetError.ActionErrorMetadata, - ); const decodeActionError = Schema.decodeUnknownEffect( Schema.toCodecJson(actionErrorSchema), ); return Effect.fnUntraced(function* (cause: unknown) { - const rivetkitRivetError = yield* decodeRivetkitRivetError(cause).pipe( - Effect.mapError(() => RivetError.fromUnknown(cause)), - ); + if (!RivetkitErrors.isRivetErrorLike(cause)) { + return yield* Effect.fail(RivetError.fromUnknown(cause)); + } + const rivetkitRivetError = RivetkitErrors.toRivetError(cause); const actionErrorMetadata = yield* Effect.exit( decodeActionErrorMetadata(rivetkitRivetError.metadata), diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 7a9957365e..ac03e6f959 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -1,6 +1,5 @@ -import { Option, Predicate, Schema } from "effect"; +import { Predicate, Schema } from "effect"; import * as RivetkitErrors from "rivetkit/errors"; -import * as RivetkitRivetError from "./internal/RivetRivetError"; const ReasonTypeId = "~@rivetkit/effect/RivetError/Reason" as const; const TypeId = "~@rivetkit/effect/RivetError" as const; @@ -8,7 +7,7 @@ const TypeId = "~@rivetkit/effect/RivetError" as const; export class Forbidden extends Schema.TaggedErrorClass( `${ReasonTypeId}/Forbidden`, )("Forbidden", { - cause: RivetkitRivetError.RivetkitRivetError, + cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { @@ -19,7 +18,7 @@ export class Forbidden extends Schema.TaggedErrorClass( export class ActorNotFound extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorNotFound`, )("ActorNotFound", { - cause: RivetkitRivetError.RivetkitRivetError, + cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { @@ -30,7 +29,7 @@ export class ActorNotFound extends Schema.TaggedErrorClass( export class ActorStopping extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorStopping`, )("ActorStopping", { - cause: RivetkitRivetError.RivetkitRivetError, + cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { @@ -41,7 +40,7 @@ export class ActorStopping extends Schema.TaggedErrorClass( export class ActorRestarting extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorRestarting`, )("ActorRestarting", { - cause: RivetkitRivetError.RivetkitRivetError, + cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { @@ -52,7 +51,7 @@ export class ActorRestarting extends Schema.TaggedErrorClass( export class ActionNotFound extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActionNotFound`, )("ActionNotFound", { - cause: RivetkitRivetError.RivetkitRivetError, + cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { @@ -62,7 +61,7 @@ export class ActionNotFound extends Schema.TaggedErrorClass( export class ActionTimedOut extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActionTimedOut`, -)("ActionTimedOut", { cause: RivetkitRivetError.RivetkitRivetError }) { +)("ActionTimedOut", { cause: Schema.instanceOf(RivetkitErrors.RivetError) }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { return this.cause.message; @@ -72,7 +71,7 @@ export class ActionTimedOut extends Schema.TaggedErrorClass( export class ActionAborted extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActionAborted`, )("ActionAborted", { - cause: RivetkitRivetError.RivetkitRivetError, + cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { @@ -83,7 +82,7 @@ export class ActionAborted extends Schema.TaggedErrorClass( export class ActorOverloaded extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActorOverloaded`, )("ActorOverloaded", { - cause: RivetkitRivetError.RivetkitRivetError, + cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { @@ -94,7 +93,7 @@ export class ActorOverloaded extends Schema.TaggedErrorClass( export class IncomingMessageTooLong extends Schema.TaggedErrorClass( `${ReasonTypeId}/IncomingMessageTooLong`, )("IncomingMessageTooLong", { - cause: RivetkitRivetError.RivetkitRivetError, + cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { @@ -105,7 +104,7 @@ export class IncomingMessageTooLong extends Schema.TaggedErrorClass( `${ReasonTypeId}/OutgoingMessageTooLong`, )("OutgoingMessageTooLong", { - cause: RivetkitRivetError.RivetkitRivetError, + cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { @@ -116,7 +115,7 @@ export class OutgoingMessageTooLong extends Schema.TaggedErrorClass( `${ReasonTypeId}/InvalidEncoding`, )("InvalidEncoding", { - cause: RivetkitRivetError.RivetkitRivetError, + cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { @@ -127,7 +126,7 @@ export class InvalidEncoding extends Schema.TaggedErrorClass( export class InvalidRequest extends Schema.TaggedErrorClass( `${ReasonTypeId}/InvalidRequest`, )("InvalidRequest", { - cause: RivetkitRivetError.RivetkitRivetError, + cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { @@ -138,7 +137,7 @@ export class InvalidRequest extends Schema.TaggedErrorClass( export class GuardError extends Schema.TaggedErrorClass( `${ReasonTypeId}/GuardError`, )("GuardError", { - cause: RivetkitRivetError.RivetkitRivetError, + cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { @@ -149,7 +148,7 @@ export class GuardError extends Schema.TaggedErrorClass( export class InternalError extends Schema.TaggedErrorClass( `${ReasonTypeId}/InternalError`, )("InternalError", { - cause: RivetkitRivetError.RivetkitRivetError, + cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { @@ -169,7 +168,7 @@ export class InternalError extends Schema.TaggedErrorClass( */ export class UnknownUserError extends Schema.TaggedErrorClass( `${ReasonTypeId}/UnknownUserError`, -)("UnknownUserError", { cause: RivetkitRivetError.RivetkitRivetError }) { +)("UnknownUserError", { cause: Schema.instanceOf(RivetkitErrors.RivetError) }) { readonly [ReasonTypeId] = ReasonTypeId; override get message(): string { return this.cause.message; @@ -185,7 +184,10 @@ export class UnknownError extends Schema.TaggedErrorClass( `${ReasonTypeId}/UnknownError`, )("UnknownError", { message: Schema.String, - cause: Schema.Union([RivetkitRivetError.RivetkitRivetError, Schema.Defect]), + cause: Schema.Union([ + Schema.instanceOf(RivetkitErrors.RivetError), + Schema.Defect, + ]), }) { readonly [ReasonTypeId] = ReasonTypeId; } @@ -287,7 +289,7 @@ export const isRivetError = (u: unknown): u is RivetError => Predicate.hasProperty(u, TypeId); const reasonFromRivetkitRivetError = ( - error: RivetkitRivetError.RivetkitRivetError, + error: RivetkitErrors.RivetError, ): Reason => { switch (`${error.group}.${error.code}`) { case `auth.${RivetkitErrors.forbiddenError().code}`: @@ -331,24 +333,10 @@ const reasonFromRivetkitRivetError = ( }; export const fromRivetkitRivetError = ( - e: RivetkitRivetError.RivetkitRivetError, + e: RivetkitErrors.RivetError, ): RivetError => new RivetError({ reason: reasonFromRivetkitRivetError(e) }); -const decodeRivetkitRivetErrorOption = Schema.decodeUnknownOption( - RivetkitRivetError.RivetkitRivetError, -); - export const fromUnknown = (cause: unknown): RivetError => { if (isRivetError(cause)) return cause; - - const normalized = RivetkitErrors.toRivetError(cause); - const decoded = decodeRivetkitRivetErrorOption(normalized); - if (Option.isSome(decoded)) return fromRivetkitRivetError(decoded.value); - - return new RivetError({ - reason: new UnknownError({ - message: normalized.message, - cause, - }), - }); + return fromRivetkitRivetError(RivetkitErrors.toRivetError(cause)); }; diff --git a/rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts b/rivetkit-typescript/packages/effect/src/internal/ActionError.ts similarity index 55% rename from rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts rename to rivetkit-typescript/packages/effect/src/internal/ActionError.ts index 069ed4687d..2e1aafc336 100644 --- a/rivetkit-typescript/packages/effect/src/internal/RivetRivetError.ts +++ b/rivetkit-typescript/packages/effect/src/internal/ActionError.ts @@ -14,15 +14,13 @@ export const ActionErrorMetadata = Schema.Struct({ export type ActionErrorMetadata = typeof ActionErrorMetadata.Type; -export const makeActionErrorMetadata = ( - error: unknown, -): ActionErrorMetadata => ({ +const makeActionErrorMetadata = (error: unknown): ActionErrorMetadata => ({ _tag: ActionErrorMetadataTag, version: ActionErrorSchemaVersion, error, }); -export const makeActionError = ( +export const make = ( actionTag: string, encodedError: unknown, ): Rivetkit.UserError => @@ -37,21 +35,3 @@ export const makeActionError = ( metadata: makeActionErrorMetadata(encodedError), }, ); - -const ActorSpecifier = Schema.Struct({ - actorId: Schema.String, - generation: Schema.Number, - key: Schema.optional(Schema.String), -}) satisfies Schema.Codec>; - -export const RivetkitRivetError = Schema.Struct({ - group: Schema.String, - code: Schema.String, - message: Schema.String, - metadata: Schema.optional(Schema.Unknown), - public: Schema.optional(Schema.Boolean), - statusCode: Schema.optional(Schema.Number), - actor: Schema.optional(ActorSpecifier), -}) satisfies Schema.Codec; - -export type RivetkitRivetError = typeof RivetkitRivetError.Type; From 7ce21b436c2c1e9a6c3bf91ee519f319e88dde38 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 21:28:43 +0200 Subject: [PATCH 195/306] feat(effect): expose group, code, metadata, and actor accessors on RivetError reasons --- .../packages/effect/src/RivetError.ts | 232 ++++++++++++++++-- 1 file changed, 216 insertions(+), 16 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index ac03e6f959..8df14200b0 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -10,9 +10,21 @@ export class Forbidden extends Schema.TaggedErrorClass( cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { + override get message() { return this.cause.message; } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } } export class ActorNotFound extends Schema.TaggedErrorClass( @@ -21,9 +33,21 @@ export class ActorNotFound extends Schema.TaggedErrorClass( cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { + override get message() { return this.cause.message; } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } } export class ActorStopping extends Schema.TaggedErrorClass( @@ -32,9 +56,21 @@ export class ActorStopping extends Schema.TaggedErrorClass( cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { + override get message() { return this.cause.message; } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } } export class ActorRestarting extends Schema.TaggedErrorClass( @@ -43,9 +79,21 @@ export class ActorRestarting extends Schema.TaggedErrorClass( cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { + override get message() { return this.cause.message; } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } } export class ActionNotFound extends Schema.TaggedErrorClass( @@ -54,18 +102,42 @@ export class ActionNotFound extends Schema.TaggedErrorClass( cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { + override get message() { return this.cause.message; } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } } export class ActionTimedOut extends Schema.TaggedErrorClass( `${ReasonTypeId}/ActionTimedOut`, )("ActionTimedOut", { cause: Schema.instanceOf(RivetkitErrors.RivetError) }) { readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { + override get message() { return this.cause.message; } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } } export class ActionAborted extends Schema.TaggedErrorClass( @@ -74,9 +146,21 @@ export class ActionAborted extends Schema.TaggedErrorClass( cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { + override get message() { return this.cause.message; } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } } export class ActorOverloaded extends Schema.TaggedErrorClass( @@ -85,9 +169,21 @@ export class ActorOverloaded extends Schema.TaggedErrorClass( cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { + override get message() { return this.cause.message; } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } } export class IncomingMessageTooLong extends Schema.TaggedErrorClass( @@ -96,9 +192,21 @@ export class IncomingMessageTooLong extends Schema.TaggedErrorClass( @@ -107,9 +215,21 @@ export class OutgoingMessageTooLong extends Schema.TaggedErrorClass( @@ -118,9 +238,21 @@ export class InvalidEncoding extends Schema.TaggedErrorClass( cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { + override get message() { return this.cause.message; } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } } export class InvalidRequest extends Schema.TaggedErrorClass( @@ -129,9 +261,21 @@ export class InvalidRequest extends Schema.TaggedErrorClass( cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { + override get message() { return this.cause.message; } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } } export class GuardError extends Schema.TaggedErrorClass( @@ -140,9 +284,21 @@ export class GuardError extends Schema.TaggedErrorClass( cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { + override get message() { return this.cause.message; } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } } export class InternalError extends Schema.TaggedErrorClass( @@ -151,9 +307,21 @@ export class InternalError extends Schema.TaggedErrorClass( cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { + override get message() { return this.cause.message; } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } } /** @@ -170,9 +338,21 @@ export class UnknownUserError extends Schema.TaggedErrorClass( `${ReasonTypeId}/UnknownUserError`, )("UnknownUserError", { cause: Schema.instanceOf(RivetkitErrors.RivetError) }) { readonly [ReasonTypeId] = ReasonTypeId; - override get message(): string { + override get message() { return this.cause.message; } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } } /** @@ -190,6 +370,26 @@ export class UnknownError extends Schema.TaggedErrorClass( ]), }) { readonly [ReasonTypeId] = ReasonTypeId; + get group() { + return this.cause instanceof RivetkitErrors.RivetError + ? this.cause.group + : undefined; + } + get code() { + return this.cause instanceof RivetkitErrors.RivetError + ? this.cause.code + : undefined; + } + get metadata() { + return this.cause instanceof RivetkitErrors.RivetError + ? this.cause.metadata + : undefined; + } + get actor() { + return this.cause instanceof RivetkitErrors.RivetError + ? this.cause.actor + : undefined; + } } export type Reason = @@ -280,7 +480,7 @@ export class RivetError extends Schema.TaggedErrorClass( readonly [TypeId] = TypeId; override readonly cause = this.reason; - override get message(): string { + override get message() { return this.reason.message || this.reason._tag; } } From 3a796d180d8edc10564e60da668f82b52ab6d1e1 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 21:32:41 +0200 Subject: [PATCH 196/306] test(effect): add RivetError tests and widen UnknownError cause to unknown --- .../packages/effect/src/RivetError.test.ts | 41 +++++++++++++++++++ .../packages/effect/src/RivetError.ts | 16 +++++--- 2 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/src/RivetError.test.ts diff --git a/rivetkit-typescript/packages/effect/src/RivetError.test.ts b/rivetkit-typescript/packages/effect/src/RivetError.test.ts new file mode 100644 index 0000000000..40c6942011 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/RivetError.test.ts @@ -0,0 +1,41 @@ +import { assert, describe, it } from "@effect/vitest"; +import * as RivetkitErrors from "rivetkit/errors"; +import * as RivetError from "./RivetError"; + +describe("RivetError", () => { + it("preserves non-Rivet causes as UnknownError", () => { + const cause = new Error("plain failure"); + const error = RivetError.fromUnknown(cause); + + assert.instanceOf(error, RivetError.RivetError); + assert.instanceOf(error.reason, RivetError.UnknownError); + assert.strictEqual(error.reason.message, "plain failure"); + assert.strictEqual(error.reason.cause, cause); + }); + + it("allows UnknownError to wrap arbitrary causes", () => { + const cause = { group: "not-a-rivet-error", code: 123 }; + const error = new RivetError.UnknownError({ + message: "malformed failure", + cause, + }); + + assert.strictEqual(error.cause, cause); + assert.strictEqual(error.group, undefined); + assert.strictEqual(error.code, undefined); + }); + + it("keeps structured Rivet errors classified by group and code", () => { + const cause = new RivetkitErrors.RivetError( + "rivetkit", + RivetkitErrors.INTERNAL_ERROR_CODE, + "internal failure", + ); + const error = RivetError.fromUnknown(cause); + + assert.instanceOf(error.reason, RivetError.InternalError); + assert.strictEqual(error.reason.group, cause.group); + assert.strictEqual(error.reason.code, cause.code); + assert.strictEqual(error.reason.message, cause.message); + }); +}); diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 8df14200b0..15be9ddbe9 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -364,10 +364,7 @@ export class UnknownError extends Schema.TaggedErrorClass( `${ReasonTypeId}/UnknownError`, )("UnknownError", { message: Schema.String, - cause: Schema.Union([ - Schema.instanceOf(RivetkitErrors.RivetError), - Schema.Defect, - ]), + cause: Schema.Unknown, }) { readonly [ReasonTypeId] = ReasonTypeId; get group() { @@ -538,5 +535,14 @@ export const fromRivetkitRivetError = ( export const fromUnknown = (cause: unknown): RivetError => { if (isRivetError(cause)) return cause; - return fromRivetkitRivetError(RivetkitErrors.toRivetError(cause)); + if (RivetkitErrors.isRivetErrorLike(cause)) { + return fromRivetkitRivetError(RivetkitErrors.toRivetError(cause)); + } + + return new RivetError({ + reason: new UnknownError({ + message: cause instanceof Error ? cause.message : String(cause), + cause, + }), + }); }; From 19607e1f8b2f72b9a78db945760ed41e202513f2 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 21:44:07 +0200 Subject: [PATCH 197/306] refactor(effect): split GuardError into per-code retryable reason classes --- .../packages/effect/src/RivetError.test.ts | 51 ++++ .../packages/effect/src/RivetError.ts | 271 +++++++++++++++--- .../packages/effect/test/e2e.test.ts | 7 +- 3 files changed, 288 insertions(+), 41 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.test.ts b/rivetkit-typescript/packages/effect/src/RivetError.test.ts index 40c6942011..b921d71131 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.test.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.test.ts @@ -38,4 +38,55 @@ describe("RivetError", () => { assert.strictEqual(error.reason.code, cause.code); assert.strictEqual(error.reason.message, cause.message); }); + + it("classifies known guard errors into specific reasons", () => { + const serviceUnavailable = RivetError.fromUnknown( + new RivetkitErrors.RivetError( + "guard", + "service_unavailable", + "service unavailable", + ), + ); + const readyTimeout = RivetError.fromUnknown( + new RivetkitErrors.RivetError( + "guard", + "actor_ready_timeout", + "actor ready timeout", + ), + ); + const tunnelTimeout = RivetError.fromUnknown( + new RivetkitErrors.RivetError( + "guard", + "tunnel_message_timeout", + "tunnel message timeout", + ), + ); + + assert.instanceOf( + serviceUnavailable.reason, + RivetError.GuardServiceUnavailable, + ); + assert.instanceOf( + readyTimeout.reason, + RivetError.GuardActorReadyTimeout, + ); + assert.instanceOf( + tunnelTimeout.reason, + RivetError.GuardTunnelMessageTimeout, + ); + assert.strictEqual(serviceUnavailable.reason.code, "service_unavailable"); + }); + + it("keeps unknown guard errors in UnknownError", () => { + const error = RivetError.fromUnknown( + new RivetkitErrors.RivetError( + "guard", + "new_guard_code", + "new guard code", + ), + ); + + assert.instanceOf(error.reason, RivetError.UnknownError); + assert.strictEqual(error.reason.code, "new_guard_code"); + }); }); diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 15be9ddbe9..0b06589a57 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -278,9 +278,170 @@ export class InvalidRequest extends Schema.TaggedErrorClass( } } -export class GuardError extends Schema.TaggedErrorClass( - `${ReasonTypeId}/GuardError`, -)("GuardError", { +export class GuardActorReadyTimeout extends Schema.TaggedErrorClass( + `${ReasonTypeId}/GuardActorReadyTimeout`, +)("GuardActorReadyTimeout", { + cause: Schema.instanceOf(RivetkitErrors.RivetError), +}) { + readonly [ReasonTypeId] = ReasonTypeId; + override get message() { + return this.cause.message; + } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } +} + +export class GuardActorRunnerFailed extends Schema.TaggedErrorClass( + `${ReasonTypeId}/GuardActorRunnerFailed`, +)("GuardActorRunnerFailed", { + cause: Schema.instanceOf(RivetkitErrors.RivetError), +}) { + readonly [ReasonTypeId] = ReasonTypeId; + override get message() { + return this.cause.message; + } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } +} + +export class GuardServiceUnavailable extends Schema.TaggedErrorClass( + `${ReasonTypeId}/GuardServiceUnavailable`, +)("GuardServiceUnavailable", { + cause: Schema.instanceOf(RivetkitErrors.RivetError), +}) { + readonly [ReasonTypeId] = ReasonTypeId; + override get message() { + return this.cause.message; + } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } +} + +export class GuardActorStoppedWhileWaiting extends Schema.TaggedErrorClass( + `${ReasonTypeId}/GuardActorStoppedWhileWaiting`, +)("GuardActorStoppedWhileWaiting", { + cause: Schema.instanceOf(RivetkitErrors.RivetError), +}) { + readonly [ReasonTypeId] = ReasonTypeId; + override get message() { + return this.cause.message; + } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } +} + +export class GuardTunnelRequestAborted extends Schema.TaggedErrorClass( + `${ReasonTypeId}/GuardTunnelRequestAborted`, +)("GuardTunnelRequestAborted", { + cause: Schema.instanceOf(RivetkitErrors.RivetError), +}) { + readonly [ReasonTypeId] = ReasonTypeId; + override get message() { + return this.cause.message; + } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } +} + +export class GuardTunnelMessageTimeout extends Schema.TaggedErrorClass( + `${ReasonTypeId}/GuardTunnelMessageTimeout`, +)("GuardTunnelMessageTimeout", { + cause: Schema.instanceOf(RivetkitErrors.RivetError), +}) { + readonly [ReasonTypeId] = ReasonTypeId; + override get message() { + return this.cause.message; + } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } +} + +export class GuardTunnelResponseClosed extends Schema.TaggedErrorClass( + `${ReasonTypeId}/GuardTunnelResponseClosed`, +)("GuardTunnelResponseClosed", { + cause: Schema.instanceOf(RivetkitErrors.RivetError), +}) { + readonly [ReasonTypeId] = ReasonTypeId; + override get message() { + return this.cause.message; + } + get group() { + return this.cause.group; + } + get code() { + return this.cause.code; + } + get metadata() { + return this.cause.metadata; + } + get actor() { + return this.cause.actor; + } +} + +export class GuardGatewayResponseStartTimeout extends Schema.TaggedErrorClass( + `${ReasonTypeId}/GuardGatewayResponseStartTimeout`, +)("GuardGatewayResponseStartTimeout", { cause: Schema.instanceOf(RivetkitErrors.RivetError), }) { readonly [ReasonTypeId] = ReasonTypeId; @@ -402,7 +563,14 @@ export type Reason = | OutgoingMessageTooLong | InvalidEncoding | InvalidRequest - | GuardError + | GuardActorReadyTimeout + | GuardActorRunnerFailed + | GuardServiceUnavailable + | GuardActorStoppedWhileWaiting + | GuardTunnelRequestAborted + | GuardTunnelMessageTimeout + | GuardTunnelResponseClosed + | GuardGatewayResponseStartTimeout | InternalError | UnknownUserError | UnknownError; @@ -421,7 +589,14 @@ export const Reason: Schema.Union< typeof OutgoingMessageTooLong, typeof InvalidEncoding, typeof InvalidRequest, - typeof GuardError, + typeof GuardActorReadyTimeout, + typeof GuardActorRunnerFailed, + typeof GuardServiceUnavailable, + typeof GuardActorStoppedWhileWaiting, + typeof GuardTunnelRequestAborted, + typeof GuardTunnelMessageTimeout, + typeof GuardTunnelResponseClosed, + typeof GuardGatewayResponseStartTimeout, typeof InternalError, typeof UnknownUserError, typeof UnknownError, @@ -439,7 +614,14 @@ export const Reason: Schema.Union< OutgoingMessageTooLong, InvalidEncoding, InvalidRequest, - GuardError, + GuardActorReadyTimeout, + GuardActorRunnerFailed, + GuardServiceUnavailable, + GuardActorStoppedWhileWaiting, + GuardTunnelRequestAborted, + GuardTunnelMessageTimeout, + GuardTunnelResponseClosed, + GuardGatewayResponseStartTimeout, InternalError, UnknownUserError, UnknownError, @@ -485,42 +667,53 @@ export class RivetError extends Schema.TaggedErrorClass( export const isRivetError = (u: unknown): u is RivetError => Predicate.hasProperty(u, TypeId); +type MakeReason = (error: RivetkitErrors.RivetError) => Reason; + +const reasonByCode: Record = { + "auth.forbidden": (error) => new Forbidden({ cause: error }), + "actor.not_found": (error) => new ActorNotFound({ cause: error }), + "actor.stopping": (error) => new ActorStopping({ cause: error }), + "actor.restarting": (error) => new ActorRestarting({ cause: error }), + "actor.action_not_found": (error) => new ActionNotFound({ cause: error }), + "actor.action_timed_out": (error) => new ActionTimedOut({ cause: error }), + "actor.aborted": (error) => new ActionAborted({ cause: error }), + "actor.overloaded": (error) => new ActorOverloaded({ cause: error }), + [`actor.${RivetkitErrors.INTERNAL_ERROR_CODE}`]: (error) => + new InternalError({ cause: error }), + [`core.${RivetkitErrors.INTERNAL_ERROR_CODE}`]: (error) => + new InternalError({ cause: error }), + [`rivetkit.${RivetkitErrors.INTERNAL_ERROR_CODE}`]: (error) => + new InternalError({ cause: error }), + "message.incoming_too_long": (error) => + new IncomingMessageTooLong({ cause: error }), + "message.outgoing_too_long": (error) => + new OutgoingMessageTooLong({ cause: error }), + "encoding.invalid": (error) => new InvalidEncoding({ cause: error }), + "request.invalid": (error) => new InvalidRequest({ cause: error }), + "guard.actor_ready_timeout": (error) => + new GuardActorReadyTimeout({ cause: error }), + "guard.actor_runner_failed": (error) => + new GuardActorRunnerFailed({ cause: error }), + "guard.service_unavailable": (error) => + new GuardServiceUnavailable({ cause: error }), + "guard.actor_stopped_while_waiting": (error) => + new GuardActorStoppedWhileWaiting({ cause: error }), + "guard.tunnel_request_aborted": (error) => + new GuardTunnelRequestAborted({ cause: error }), + "guard.tunnel_message_timeout": (error) => + new GuardTunnelMessageTimeout({ cause: error }), + "guard.tunnel_response_closed": (error) => + new GuardTunnelResponseClosed({ cause: error }), + "guard.gateway_response_start_timeout": (error) => + new GuardGatewayResponseStartTimeout({ cause: error }), +}; + const reasonFromRivetkitRivetError = ( error: RivetkitErrors.RivetError, ): Reason => { - switch (`${error.group}.${error.code}`) { - case `auth.${RivetkitErrors.forbiddenError().code}`: - return new Forbidden({ cause: error }); - case `actor.${RivetkitErrors.actorNotFound().code}`: - return new ActorNotFound({ cause: error }); - case `actor.${RivetkitErrors.actorStopping().code}`: - return new ActorStopping({ cause: error }); - case `actor.${RivetkitErrors.actorRestarting().code}`: - return new ActorRestarting({ cause: error }); - case `actor.action_not_found`: - return new ActionNotFound({ cause: error }); - case `actor.action_timed_out`: - return new ActionTimedOut({ cause: error }); - case `actor.aborted`: - return new ActionAborted({ cause: error }); - case `actor.overloaded`: - return new ActorOverloaded({ cause: error }); - case `actor.${RivetkitErrors.INTERNAL_ERROR_CODE}`: - case `core.${RivetkitErrors.INTERNAL_ERROR_CODE}`: - case `rivetkit.${RivetkitErrors.INTERNAL_ERROR_CODE}`: - return new InternalError({ cause: error }); - case `message.incoming_too_long`: - return new IncomingMessageTooLong({ cause: error }); - case `message.outgoing_too_long`: - return new OutgoingMessageTooLong({ cause: error }); - case `encoding.${RivetkitErrors.invalidEncoding().code}`: - return new InvalidEncoding({ cause: error }); - case `request.${RivetkitErrors.invalidRequest().code}`: - return new InvalidRequest({ cause: error }); - } - - // Group-wide fallbacks: any code under the group maps to a single reason. - if (error.group === "guard") return new GuardError({ cause: error }); + const makeReason = reasonByCode[`${error.group}.${error.code}`]; + if (makeReason) return makeReason(error); + if (error.group === "user") return new UnknownUserError({ cause: error }); return new UnknownError({ diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 13ca9d48f4..c0c8ef6e1a 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -425,9 +425,12 @@ layer(TestLayer)("end-to-end", (it) => { assert.isTrue(exit._tag === "Success"); if (exit._tag === "Success") { assert.instanceOf(exit.value, RivetError.RivetError); - assert.instanceOf(exit.value.reason, RivetError.GuardError); + assert.instanceOf( + exit.value.reason, + RivetError.GuardServiceUnavailable, + ); assert.strictEqual( - (exit.value.reason as RivetError.GuardError).code, + (exit.value.reason as RivetError.GuardServiceUnavailable).code, "service_unavailable", ); } From 388ace68d26f712f0e2ac2c8196a5f27eee11410 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 21:49:23 +0200 Subject: [PATCH 198/306] refactor(effect): rename Reason to RivetErrorReason --- .../packages/effect/src/RivetError.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 0b06589a57..0b84c822ce 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -550,7 +550,7 @@ export class UnknownError extends Schema.TaggedErrorClass( } } -export type Reason = +export type RivetErrorReason = | Forbidden | ActorNotFound | ActorStopping @@ -575,7 +575,7 @@ export type Reason = | UnknownUserError | UnknownError; -export const Reason: Schema.Union< +export const RivetErrorReason: Schema.Union< [ typeof Forbidden, typeof ActorNotFound, @@ -627,7 +627,7 @@ export const Reason: Schema.Union< UnknownError, ]); -export const isReason = (u: unknown): u is Reason => +export const isRivetErrorReason = (u: unknown): u is RivetErrorReason => Predicate.hasProperty(u, ReasonTypeId); /** @@ -654,7 +654,7 @@ export const isReason = (u: unknown): u is Reason => export class RivetError extends Schema.TaggedErrorClass( "@rivetkit/effect/RivetError", )("RivetError", { - reason: Reason, + reason: RivetErrorReason, }) { readonly [TypeId] = TypeId; override readonly cause = this.reason; @@ -667,9 +667,11 @@ export class RivetError extends Schema.TaggedErrorClass( export const isRivetError = (u: unknown): u is RivetError => Predicate.hasProperty(u, TypeId); -type MakeReason = (error: RivetkitErrors.RivetError) => Reason; +type MakeRivetErrorReason = ( + error: RivetkitErrors.RivetError, +) => RivetErrorReason; -const reasonByCode: Record = { +const reasonByCode: Record = { "auth.forbidden": (error) => new Forbidden({ cause: error }), "actor.not_found": (error) => new ActorNotFound({ cause: error }), "actor.stopping": (error) => new ActorStopping({ cause: error }), @@ -710,7 +712,7 @@ const reasonByCode: Record = { const reasonFromRivetkitRivetError = ( error: RivetkitErrors.RivetError, -): Reason => { +): RivetErrorReason => { const makeReason = reasonByCode[`${error.group}.${error.code}`]; if (makeReason) return makeReason(error); @@ -723,8 +725,10 @@ const reasonFromRivetkitRivetError = ( }; export const fromRivetkitRivetError = ( - e: RivetkitErrors.RivetError, -): RivetError => new RivetError({ reason: reasonFromRivetkitRivetError(e) }); + error: RivetkitErrors.RivetError, +): RivetError => { + return new RivetError({ reason: reasonFromRivetkitRivetError(error) }); +}; export const fromUnknown = (cause: unknown): RivetError => { if (isRivetError(cause)) return cause; From d807e68582e1787ee11777feb8d577cd95eb3400 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 22:05:30 +0200 Subject: [PATCH 199/306] feat(effect): add ActionErrorDecodeFailed reason for action error decode failures --- .../packages/effect/src/Client.ts | 44 ++++++++++++++----- .../packages/effect/src/RivetError.test.ts | 32 +++++++++++++- .../packages/effect/src/RivetError.ts | 15 +++++++ 3 files changed, 78 insertions(+), 13 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 3f2fd5f3b5..0f7a6c38d5 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -1,4 +1,4 @@ -import { Context, Effect, Exit, Layer, Record, Schema } from "effect"; +import { Context, Effect, Layer, Record, Schema } from "effect"; import * as RivetkitClient from "rivetkit/client"; import * as RivetkitErrors from "rivetkit/errors"; import type * as Action from "./Action"; @@ -149,29 +149,49 @@ const decodeRejectedActionCall = ( ); return Effect.fnUntraced(function* (cause: unknown) { + // Transport and runtime failures that are not structured Rivet errors + // cannot contain typed action-error metadata. if (!RivetkitErrors.isRivetErrorLike(cause)) { return yield* Effect.fail(RivetError.fromUnknown(cause)); } const rivetkitRivetError = RivetkitErrors.toRivetError(cause); - const actionErrorMetadata = yield* Effect.exit( - decodeActionErrorMetadata(rivetkitRivetError.metadata), + // Effect action errors are sent as UserError metadata. First decode + // that envelope so we can distinguish typed domain errors from + // ordinary unknown user errors. + const actionErrorMetadata = yield* decodeActionErrorMetadata( + rivetkitRivetError.metadata, + ).pipe( + Effect.mapError( + (cause) => + new RivetError.RivetError({ + reason: new RivetError.ActionErrorDecodeFailed({ + cause, + rivetError: rivetkitRivetError, + }), + }), + ), ); - if (Exit.isFailure(actionErrorMetadata)) { - return yield* Effect.fail( - RivetError.fromRivetkitRivetError(rivetkitRivetError), - ); - } - + // Then decode the embedded payload against the action's declared error + // schema. A schema mismatch means this client cannot safely recover the + // typed domain error, so expose a RivetError with decode context. const actionError = yield* decodeActionError( - actionErrorMetadata.value.error, + actionErrorMetadata.error, ).pipe( - Effect.mapError(() => - RivetError.fromRivetkitRivetError(rivetkitRivetError), + Effect.mapError( + (decodeError) => + new RivetError.RivetError({ + reason: new RivetError.ActionErrorDecodeFailed({ + cause: decodeError, + rivetError: rivetkitRivetError, + }), + }), ), ); + // Successfully decoded into the action's declared error type; + // flow it through the typed error channel as `E["Type"]`. return yield* Effect.fail(actionError as E["Type"]); }); }; diff --git a/rivetkit-typescript/packages/effect/src/RivetError.test.ts b/rivetkit-typescript/packages/effect/src/RivetError.test.ts index b921d71131..55d0bfd588 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.test.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.test.ts @@ -1,4 +1,5 @@ import { assert, describe, it } from "@effect/vitest"; +import { Effect, Schema } from "effect"; import * as RivetkitErrors from "rivetkit/errors"; import * as RivetError from "./RivetError"; @@ -74,7 +75,10 @@ describe("RivetError", () => { tunnelTimeout.reason, RivetError.GuardTunnelMessageTimeout, ); - assert.strictEqual(serviceUnavailable.reason.code, "service_unavailable"); + assert.strictEqual( + serviceUnavailable.reason.code, + "service_unavailable", + ); }); it("keeps unknown guard errors in UnknownError", () => { @@ -89,4 +93,30 @@ describe("RivetError", () => { assert.instanceOf(error.reason, RivetError.UnknownError); assert.strictEqual(error.reason.code, "new_guard_code"); }); + + it("exposes action error decode failures with decode context", () => { + const cause = new RivetkitErrors.RivetError( + "user", + "CounterOverflow", + "counter overflow", + { metadata: { _tag: "EffectActionError", version: 1, error: {} } }, + ); + const schemaError = Effect.runSync( + Schema.decodeUnknownEffect(Schema.String)(123).pipe(Effect.flip), + ); + const error = new RivetError.RivetError({ + reason: new RivetError.ActionErrorDecodeFailed({ + cause: schemaError, + rivetError: cause, + }), + }); + + assert.instanceOf(error.reason, RivetError.ActionErrorDecodeFailed); + assert.strictEqual(error.reason.cause, schemaError); + assert.strictEqual(error.reason.rivetError, cause); + assert.strictEqual( + error.reason.message, + "Failed to decode action error user.CounterOverflow", + ); + }); }); diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 0b84c822ce..eca8aecda9 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -485,6 +485,18 @@ export class InternalError extends Schema.TaggedErrorClass( } } +export class ActionErrorDecodeFailed extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ActionErrorDecodeFailed`, +)("ActionErrorDecodeFailed", { + cause: Schema.instanceOf(Schema.SchemaError), + rivetError: Schema.instanceOf(RivetkitErrors.RivetError), +}) { + readonly [ReasonTypeId] = ReasonTypeId; + override get message() { + return `Failed to decode action error ${this.rivetError.group}.${this.rivetError.code}`; + } +} + /** * Open-ended user error reason. Used when the actor threw `UserError` but * the failing action did not declare a matching schema in its `error` @@ -573,6 +585,7 @@ export type RivetErrorReason = | GuardGatewayResponseStartTimeout | InternalError | UnknownUserError + | ActionErrorDecodeFailed | UnknownError; export const RivetErrorReason: Schema.Union< @@ -598,6 +611,7 @@ export const RivetErrorReason: Schema.Union< typeof GuardTunnelResponseClosed, typeof GuardGatewayResponseStartTimeout, typeof InternalError, + typeof ActionErrorDecodeFailed, typeof UnknownUserError, typeof UnknownError, ] @@ -623,6 +637,7 @@ export const RivetErrorReason: Schema.Union< GuardTunnelResponseClosed, GuardGatewayResponseStartTimeout, InternalError, + ActionErrorDecodeFailed, UnknownUserError, UnknownError, ]); From 2d3037f0906de72d2b3db3a86ce7ac63661e2fff Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 22:15:37 +0200 Subject: [PATCH 200/306] feat(effect): route non-action structured errors through fromRivetkitRivetError --- rivetkit-typescript/packages/effect/src/Client.ts | 9 +++++++++ .../packages/effect/src/internal/ActionError.ts | 2 ++ 2 files changed, 11 insertions(+) diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 0f7a6c38d5..2d784ddc8a 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -156,6 +156,15 @@ const decodeRejectedActionCall = ( } const rivetkitRivetError = RivetkitErrors.toRivetError(cause); + // Most structured Rivet errors are infrastructure or runtime failures. + // Only errors with the Effect action-error marker should enter the + // typed action-error decode path. + if (!ActionError.isActionErrorMetadata(rivetkitRivetError.metadata)) { + return yield* Effect.fail( + RivetError.fromRivetkitRivetError(rivetkitRivetError), + ); + } + // Effect action errors are sent as UserError metadata. First decode // that envelope so we can distinguish typed domain errors from // ordinary unknown user errors. diff --git a/rivetkit-typescript/packages/effect/src/internal/ActionError.ts b/rivetkit-typescript/packages/effect/src/internal/ActionError.ts index 2e1aafc336..75e10fa992 100644 --- a/rivetkit-typescript/packages/effect/src/internal/ActionError.ts +++ b/rivetkit-typescript/packages/effect/src/internal/ActionError.ts @@ -14,6 +14,8 @@ export const ActionErrorMetadata = Schema.Struct({ export type ActionErrorMetadata = typeof ActionErrorMetadata.Type; +export const isActionErrorMetadata = Schema.is(ActionErrorMetadata); + const makeActionErrorMetadata = (error: unknown): ActionErrorMetadata => ({ _tag: ActionErrorMetadataTag, version: ActionErrorSchemaVersion, From f119d50293660c299cec084c54985a14649ad231 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 22:42:15 +0200 Subject: [PATCH 201/306] feat(effect): expose normalized isRetryable and retryAfter on RivetError reasons Drop reason-specific metadata-derived getters (phase, channel, capacity, operation, size, limit, actorId) in favor of a normalized isRetryable boolean on every reason and an optional retryAfter Duration on ActorRestarting. Top-level RivetError delegates both to the underlying reason, mirroring the AiError/SqlError pattern. --- .../packages/effect/src/RivetError.test.ts | 68 +++++++++++++- .../packages/effect/src/RivetError.ts | 90 ++++++++++++++++++- 2 files changed, 155 insertions(+), 3 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.test.ts b/rivetkit-typescript/packages/effect/src/RivetError.test.ts index 55d0bfd588..cbe1dce03b 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.test.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest"; -import { Effect, Schema } from "effect"; +import { Duration, Effect, Schema } from "effect"; import * as RivetkitErrors from "rivetkit/errors"; import * as RivetError from "./RivetError"; @@ -40,6 +40,72 @@ describe("RivetError", () => { assert.strictEqual(error.reason.message, cause.message); }); + it("exposes normalized isRetryable on every reason", () => { + const restarting = RivetError.fromUnknown( + new RivetkitErrors.RivetError("actor", "restarting", "restarting"), + ); + const forbidden = RivetError.fromUnknown( + new RivetkitErrors.RivetError("auth", "forbidden", "forbidden"), + ); + const overloaded = RivetError.fromUnknown( + new RivetkitErrors.RivetError("actor", "overloaded", "overloaded"), + ); + const serviceUnavailable = RivetError.fromUnknown( + new RivetkitErrors.RivetError( + "guard", + "service_unavailable", + "service unavailable", + ), + ); + const incomingTooLong = RivetError.fromUnknown( + new RivetkitErrors.RivetError( + "message", + "incoming_too_long", + "too long", + ), + ); + + assert.strictEqual(restarting.isRetryable, true); + assert.strictEqual(restarting.reason.isRetryable, true); + assert.strictEqual(forbidden.isRetryable, false); + assert.strictEqual(overloaded.isRetryable, true); + assert.strictEqual(serviceUnavailable.isRetryable, true); + assert.strictEqual(incomingTooLong.isRetryable, false); + }); + + it("exposes retryAfter from ActorRestarting metadata", () => { + const restarting = RivetError.fromUnknown( + new RivetkitErrors.RivetError( + "actor", + "restarting", + "actor restarting", + { metadata: { retryAfterMs: 250 } }, + ), + ); + const restartingNoHint = RivetError.fromUnknown( + new RivetkitErrors.RivetError( + "actor", + "restarting", + "actor restarting", + ), + ); + + assert.instanceOf(restarting.reason, RivetError.ActorRestarting); + assert.deepStrictEqual(restarting.retryAfter, Duration.millis(250)); + assert.deepStrictEqual( + restarting.reason.retryAfter, + Duration.millis(250), + ); + assert.strictEqual(restartingNoHint.retryAfter, undefined); + }); + + it("returns retryAfter undefined for reasons without retry-timing hints", () => { + const overloaded = RivetError.fromUnknown( + new RivetkitErrors.RivetError("actor", "overloaded", "overloaded"), + ); + assert.strictEqual(overloaded.retryAfter, undefined); + }); + it("classifies known guard errors into specific reasons", () => { const serviceUnavailable = RivetError.fromUnknown( new RivetkitErrors.RivetError( diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index eca8aecda9..2d6814f3fe 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -1,4 +1,4 @@ -import { Predicate, Schema } from "effect"; +import { Duration, Option, Predicate, Record, Schema } from "effect"; import * as RivetkitErrors from "rivetkit/errors"; const ReasonTypeId = "~@rivetkit/effect/RivetError/Reason" as const; @@ -25,6 +25,9 @@ export class Forbidden extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get isRetryable(): boolean { + return false; + } } export class ActorNotFound extends Schema.TaggedErrorClass( @@ -48,6 +51,9 @@ export class ActorNotFound extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get isRetryable(): boolean { + return false; + } } export class ActorStopping extends Schema.TaggedErrorClass( @@ -71,6 +77,9 @@ export class ActorStopping extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get isRetryable(): boolean { + return true; + } } export class ActorRestarting extends Schema.TaggedErrorClass( @@ -94,6 +103,17 @@ export class ActorRestarting extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get isRetryable(): boolean { + return true; + } + get retryAfter(): Duration.Duration | undefined { + if (!Predicate.isReadonlyObject(this.metadata)) return undefined; + return Record.get(this.metadata, "retryAfterMs").pipe( + Option.filter(Predicate.isNumber), + Option.map(Duration.millis), + Option.getOrUndefined, + ); + } } export class ActionNotFound extends Schema.TaggedErrorClass( @@ -117,6 +137,9 @@ export class ActionNotFound extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get isRetryable(): boolean { + return false; + } } export class ActionTimedOut extends Schema.TaggedErrorClass( @@ -138,6 +161,9 @@ export class ActionTimedOut extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get isRetryable(): boolean { + return true; + } } export class ActionAborted extends Schema.TaggedErrorClass( @@ -161,6 +187,9 @@ export class ActionAborted extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get isRetryable(): boolean { + return false; + } } export class ActorOverloaded extends Schema.TaggedErrorClass( @@ -184,6 +213,9 @@ export class ActorOverloaded extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get isRetryable(): boolean { + return true; + } } export class IncomingMessageTooLong extends Schema.TaggedErrorClass( @@ -207,6 +239,9 @@ export class IncomingMessageTooLong extends Schema.TaggedErrorClass( @@ -230,6 +265,9 @@ export class OutgoingMessageTooLong extends Schema.TaggedErrorClass( @@ -253,6 +291,9 @@ export class InvalidEncoding extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get isRetryable(): boolean { + return false; + } } export class InvalidRequest extends Schema.TaggedErrorClass( @@ -276,6 +317,9 @@ export class InvalidRequest extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get isRetryable(): boolean { + return false; + } } export class GuardActorReadyTimeout extends Schema.TaggedErrorClass( @@ -299,6 +343,9 @@ export class GuardActorReadyTimeout extends Schema.TaggedErrorClass( @@ -322,6 +369,9 @@ export class GuardActorRunnerFailed extends Schema.TaggedErrorClass( @@ -345,6 +395,9 @@ export class GuardServiceUnavailable extends Schema.TaggedErrorClass( @@ -368,6 +421,9 @@ export class GuardActorStoppedWhileWaiting extends Schema.TaggedErrorClass( @@ -391,6 +447,9 @@ export class GuardTunnelRequestAborted extends Schema.TaggedErrorClass( @@ -414,6 +473,9 @@ export class GuardTunnelMessageTimeout extends Schema.TaggedErrorClass( @@ -437,6 +499,9 @@ export class GuardTunnelResponseClosed extends Schema.TaggedErrorClass( @@ -460,6 +525,9 @@ export class GuardGatewayResponseStartTimeout extends Schema.TaggedErrorClass( @@ -483,6 +551,9 @@ export class InternalError extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get isRetryable(): boolean { + return false; + } } export class ActionErrorDecodeFailed extends Schema.TaggedErrorClass( @@ -495,6 +566,9 @@ export class ActionErrorDecodeFailed extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get isRetryable(): boolean { + return false; + } } /** @@ -560,6 +637,9 @@ export class UnknownError extends Schema.TaggedErrorClass( ? this.cause.actor : undefined; } + get isRetryable(): boolean { + return false; + } } export type RivetErrorReason = @@ -677,6 +757,12 @@ export class RivetError extends Schema.TaggedErrorClass( override get message() { return this.reason.message || this.reason._tag; } + get isRetryable(): boolean { + return this.reason.isRetryable; + } + get retryAfter(): Duration.Duration | undefined { + return "retryAfter" in this.reason ? this.reason.retryAfter : undefined; + } } export const isRivetError = (u: unknown): u is RivetError => @@ -686,7 +772,7 @@ type MakeRivetErrorReason = ( error: RivetkitErrors.RivetError, ) => RivetErrorReason; -const reasonByCode: Record = { +const reasonByCode: { [key: string]: MakeRivetErrorReason | undefined } = { "auth.forbidden": (error) => new Forbidden({ cause: error }), "actor.not_found": (error) => new ActorNotFound({ cause: error }), "actor.stopping": (error) => new ActorStopping({ cause: error }), From 0f2699b1102017e7d4c7173f0ed396794d5e30c6 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 22:49:07 +0200 Subject: [PATCH 202/306] feat(effect): add structured accessors for type, cause, message, isRetryable, and retryAfter on RivetError --- rivetkit-typescript/packages/effect/src/RivetError.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 2d6814f3fe..76efc82a32 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -751,15 +751,23 @@ export class RivetError extends Schema.TaggedErrorClass( )("RivetError", { reason: RivetErrorReason, }) { + /** Marks this value as the top-level Rivet error wrapper for runtime guards. */ readonly [TypeId] = TypeId; + + /** Exposes the structured Rivet error reason as the JavaScript error cause. */ override readonly cause = this.reason; + /** Uses the reason message when present, otherwise falls back to the reason tag. */ override get message() { return this.reason.message || this.reason._tag; } + + /** Delegates to the underlying reason's `isRetryable` getter. */ get isRetryable(): boolean { return this.reason.isRetryable; } + + /** Delegates to the underlying reason's `retryAfter` if present. */ get retryAfter(): Duration.Duration | undefined { return "retryAfter" in this.reason ? this.reason.retryAfter : undefined; } From fefa164fc24df98c8b2eae3987fd290e88708efb Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 17 May 2026 23:16:45 +0200 Subject: [PATCH 203/306] feat(effect): expose group, code, metadata, actor, statusCode, and public on RivetError --- .../packages/effect/src/RivetError.ts | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/rivetkit-typescript/packages/effect/src/RivetError.ts b/rivetkit-typescript/packages/effect/src/RivetError.ts index 76efc82a32..3fb88d4ed8 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -25,6 +25,12 @@ export class Forbidden extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } get isRetryable(): boolean { return false; } @@ -51,6 +57,12 @@ export class ActorNotFound extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } get isRetryable(): boolean { return false; } @@ -77,6 +89,12 @@ export class ActorStopping extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } get isRetryable(): boolean { return true; } @@ -103,6 +121,12 @@ export class ActorRestarting extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } get isRetryable(): boolean { return true; } @@ -137,6 +161,12 @@ export class ActionNotFound extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } get isRetryable(): boolean { return false; } @@ -161,6 +191,12 @@ export class ActionTimedOut extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } get isRetryable(): boolean { return true; } @@ -187,6 +223,12 @@ export class ActionAborted extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } get isRetryable(): boolean { return false; } @@ -213,6 +255,12 @@ export class ActorOverloaded extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } get isRetryable(): boolean { return true; } @@ -239,6 +287,12 @@ export class IncomingMessageTooLong extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } get isRetryable(): boolean { return false; } @@ -317,6 +383,12 @@ export class InvalidRequest extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } get isRetryable(): boolean { return false; } @@ -343,6 +415,12 @@ export class GuardActorReadyTimeout extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } get isRetryable(): boolean { return false; } @@ -566,6 +692,24 @@ export class ActionErrorDecodeFailed extends Schema.TaggedErrorClass( get actor() { return this.cause.actor; } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } get isRetryable(): boolean { return false; } @@ -637,6 +787,16 @@ export class UnknownError extends Schema.TaggedErrorClass( ? this.cause.actor : undefined; } + get statusCode() { + return this.cause instanceof RivetkitErrors.RivetError + ? this.cause.statusCode + : undefined; + } + get public() { + return this.cause instanceof RivetkitErrors.RivetError + ? this.cause.public + : undefined; + } get isRetryable(): boolean { return false; } @@ -762,6 +922,36 @@ export class RivetError extends Schema.TaggedErrorClass( return this.reason.message || this.reason._tag; } + /** Delegates to the underlying reason's `group` if present. */ + get group(): string | undefined { + return "group" in this.reason ? this.reason.group : undefined; + } + + /** Delegates to the underlying reason's `code` if present. */ + get code(): string | undefined { + return "code" in this.reason ? this.reason.code : undefined; + } + + /** Delegates to the underlying reason's `metadata` if present. */ + get metadata(): unknown { + return "metadata" in this.reason ? this.reason.metadata : undefined; + } + + /** Delegates to the underlying reason's `actor` if present. */ + get actor() { + return "actor" in this.reason ? this.reason.actor : undefined; + } + + /** Delegates to the underlying reason's `statusCode` if present. */ + get statusCode(): number | undefined { + return "statusCode" in this.reason ? this.reason.statusCode : undefined; + } + + /** Delegates to the underlying reason's `public` if present. */ + get public(): boolean | undefined { + return "public" in this.reason ? this.reason.public : undefined; + } + /** Delegates to the underlying reason's `isRetryable` getter. */ get isRetryable(): boolean { return this.reason.isRetryable; From 0888b3ab90734374a8f69592ae23bd3971ad601d Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 18 May 2026 11:48:28 +0200 Subject: [PATCH 204/306] test(effect): add Actor type tests with failing cases marked as todo --- .../packages/effect/package.json | 2 +- .../packages/effect/src/Actor.test-d.ts | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 rivetkit-typescript/packages/effect/src/Actor.test-d.ts diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index e460d6ad3f..867e67304f 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -28,7 +28,7 @@ "build": "tsup src/mod.ts", "dev": "tsup src/mod.ts --watch", "check-types": "tsc --noEmit", - "test": "vitest run" + "test": "vitest --typecheck" }, "dependencies": { "rivetkit": "workspace:*" diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts new file mode 100644 index 0000000000..9e564f8fe9 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -0,0 +1,90 @@ +import { Context, Effect, type Layer } from "effect"; +import { describe, expectTypeOf, test } from "vitest"; +import * as Action from "./Action"; +import * as Actor from "./Actor"; +import type * as Client from "./Client"; + +const TestActor = Actor.make("TestActor", { + actions: [Action.make("GetContext")], +}); + +type TestActions = (typeof TestActor.actions)[number]; + +describe("Actor.make", () => { + test("preserves the name literal", () => { + expectTypeOf(TestActor.name).toEqualTypeOf<"TestActor">(); + }); +}); + +describe("Actor.make(...).toLayer", () => { + test("is a function", () => { + expectTypeOf(TestActor.toLayer).toBeFunction(); + }); + + test("accepts a plain action handlers object", () => { + expectTypeOf(TestActor.toLayer).toBeCallableWith({ + GetContext: () => Effect.void, + }); + }); + + test("accepts an Effect of action handlers", () => { + expectTypeOf(TestActor.toLayer).toBeCallableWith( + Effect.succeed({ + GetContext: () => Effect.void, + }), + ); + }); + + test("returns a Layer", () => { + expectTypeOf(TestActor.toLayer).returns.toExtend(); + }); + + test("action handler's envelope is typed against the action", () => { + TestActor.toLayer({ + GetContext: (envelope) => { + expectTypeOf(envelope._tag).toEqualTypeOf<"GetContext">(); + expectTypeOf(envelope.action).toExtend(); + return Effect.void; + }, + }); + }); + + test("missing action handler is rejected", () => { + // @ts-expect-error: GetContext handler is required + TestActor.toLayer({}); + }); + + test.todo("unknown action handler key is rejected", () => { + TestActor.toLayer({ + GetContext: () => Effect.void, + // TODO: toLayer should reject unknown action handler keys + Unknown: () => Effect.void, + }); + }); + + test.todo("build-effect requirements surface in the Layer", () => { + class SomeDep extends Context.Service< + SomeDep, + { readonly x: number } + >()("SomeDep") {} + + const layer = TestActor.toLayer( + Effect.gen(function* () { + yield* SomeDep; + return { GetContext: () => Effect.void }; + }), + ); + type Reqs = + typeof layer extends Layer.Layer ? R : never; + // @ts-expect-error: TODO - expectTypeOf() no-arg generic form not resolving + expectTypeOf().toExtend(); + }); +}); + +describe("Actor.make(...).client", () => { + test("yields a typed Accessor", () => { + expectTypeOf(TestActor.client).toEqualTypeOf< + Effect.Effect, never, Client.Client> + >(); + }); +}); From 6bc2facecf6be0b8baf77facb5391cbd84e8e3f8 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 18 May 2026 12:12:04 +0200 Subject: [PATCH 205/306] feat(effect): accept build function and Effect.fn in Actor.toLayer --- .../packages/effect/src/Actor.test-d.ts | 26 ++++++++++++++++-- .../packages/effect/src/Actor.ts | 27 +++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts index 9e564f8fe9..f2406fddbf 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -29,8 +29,30 @@ describe("Actor.make(...).toLayer", () => { test("accepts an Effect of action handlers", () => { expectTypeOf(TestActor.toLayer).toBeCallableWith( - Effect.succeed({ - GetContext: () => Effect.void, + Effect.gen(function* () { + return { + GetContext: () => Effect.void, + }; + }), + ); + }); + + test("accepts a function returning an Effect of action handlers", () => { + expectTypeOf(TestActor.toLayer).toBeCallableWith((_wakeOptions) => + Effect.gen(function* () { + return { + GetContext: () => Effect.void, + }; + }), + ); + }); + + test("accepts an Effect.fn returning action handlers", () => { + expectTypeOf(TestActor.toLayer).toBeCallableWith( + Effect.fn("build")(function* (_wakeOptions) { + return { + GetContext: () => Effect.void, + }; }), ); }); diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 223e820462..d08a0a317c 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -160,11 +160,22 @@ export interface Actor< ): ActionHandlers; toLayer< + R, ActionHandlers extends ActionHandlersFrom, State extends ActorState.AnyWithProps = never, RX = never, >( - build: ActionHandlers | Effect.Effect, + build: + | ActionHandlers + | Effect.Effect + | ((wakeOptions: any) => Effect.Effect) + | Effect.Effect< + ( + wakeOptions: any, + ) => Effect.Effect, + never, + RX + >, options?: Options, ): Layer.Layer< never, @@ -177,6 +188,7 @@ export interface Actor< | RawRivetkitContext | State > + | R | ActionHandlerServices | Action.ServicesServer | Action.ServicesClient @@ -203,13 +215,24 @@ export type ActionHandlersFrom = { const Proto: Omit, "name" | "actions"> = { [TypeId]: TypeId, toLayer< + R, Actions extends Action.AnyWithProps, ActionHandlers extends ActionHandlersFrom, State extends ActorState.AnyWithProps = never, RX = never, >( this: Actor, - build: ActionHandlers | Effect.Effect, + build: + | ActionHandlers + | Effect.Effect + | ((wakeOptions: any) => Effect.Effect) + | Effect.Effect< + ( + wakeOptions: any, + ) => Effect.Effect, + never, + RX + >, options: Options = {}, ) { return makeRivetkitActor({ From 07caacfed8aaddb721cfd55b53c0fd0874fec0fc Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 18 May 2026 12:48:14 +0200 Subject: [PATCH 206/306] refactor(effect): rename build to wake in Actor.toLayer and support all wake forms --- .../packages/effect/src/Actor.test-d.ts | 34 ++++++++++--- .../packages/effect/src/Actor.ts | 50 +++++++++++++++---- .../packages/effect/src/ActorState.ts | 4 +- 3 files changed, 69 insertions(+), 19 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts index f2406fddbf..84d0f1f4d4 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -4,6 +4,10 @@ import * as Action from "./Action"; import * as Actor from "./Actor"; import type * as Client from "./Client"; +class SomeDep extends Context.Service()( + "SomeDep", +) {} + const TestActor = Actor.make("TestActor", { actions: [Action.make("GetContext")], }); @@ -37,6 +41,12 @@ describe("Actor.make(...).toLayer", () => { ); }); + test("accepts a function returning a plain action handlers object", () => { + expectTypeOf(TestActor.toLayer).toBeCallableWith((_wakeOptions) => ({ + GetContext: () => Effect.void, + })); + }); + test("accepts a function returning an Effect of action handlers", () => { expectTypeOf(TestActor.toLayer).toBeCallableWith((_wakeOptions) => Effect.gen(function* () { @@ -47,9 +57,24 @@ describe("Actor.make(...).toLayer", () => { ); }); + test("accepts an Effect that resolves to a wake function", () => { + expectTypeOf(TestActor.toLayer).toBeCallableWith( + Effect.gen(function* () { + // Allow for initialization logic before the per-entity wake function is called + + return (_wakeOptions: any) => + Effect.gen(function* () { + return { + GetContext: () => Effect.void, + }; + }); + }), + ); + }); + test("accepts an Effect.fn returning action handlers", () => { expectTypeOf(TestActor.toLayer).toBeCallableWith( - Effect.fn("build")(function* (_wakeOptions) { + Effect.fn("wake")(function* (_wakeOptions) { return { GetContext: () => Effect.void, }; @@ -84,12 +109,7 @@ describe("Actor.make(...).toLayer", () => { }); }); - test.todo("build-effect requirements surface in the Layer", () => { - class SomeDep extends Context.Service< - SomeDep, - { readonly x: number } - >()("SomeDep") {} - + test.todo("wake-effect requirements surface in the Layer", () => { const layer = TestActor.toLayer( Effect.gen(function* () { yield* SomeDep; diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index d08a0a317c..348a4a041c 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -69,7 +69,7 @@ const splitOptions = ( * - `(name, key)` — stable user-facing pair (e.g. "Counter", ["counter-123"]) * - `actorId` — opaque engine-assigned unique identifier * - * Available inside `Actor.toLayer`'s build effect via + * Available inside `Actor.toLayer`'s wake effect via * `yield* Actor.CurrentAddress`. */ export type ActorAddress = Pick< @@ -79,7 +79,7 @@ export type ActorAddress = Pick< /** * Context tag for the current actor instance's address. Provided - * once per wake when the build effect runs; capture it into a + * once per wake when the wake effect runs; capture it into a * closure if action handlers need it. */ export class CurrentAddress extends Context.Service< @@ -165,9 +165,10 @@ export interface Actor< State extends ActorState.AnyWithProps = never, RX = never, >( - build: + wake: | ActionHandlers | Effect.Effect + | ((wakeOptions: any) => ActionHandlers) | ((wakeOptions: any) => Effect.Effect) | Effect.Effect< ( @@ -222,9 +223,10 @@ const Proto: Omit, "name" | "actions"> = { RX = never, >( this: Actor, - build: + wake: | ActionHandlers | Effect.Effect + | ((wakeOptions: any) => ActionHandlers) | ((wakeOptions: any) => Effect.Effect) | Effect.Effect< ( @@ -235,11 +237,37 @@ const Proto: Omit, "name" | "actions"> = { >, options: Options = {}, ) { + const resolveWake: ( + wakeContext: Rivetkit.WakeContextOf, + ) => Effect.Effect = Effect.isEffect( + wake, + ) + ? (c) => + (wake as Effect.Effect).pipe( + Effect.flatMap((resolved: any) => + typeof resolved === "function" + ? (resolved(c) as Effect.Effect< + ActionHandlers, + never, + R + >) + : Effect.succeed(resolved as ActionHandlers), + ), + ) + : typeof wake === "function" + ? (c: any) => { + const result = (wake as Function)(c); + return ( + Effect.isEffect(result) + ? result + : Effect.succeed(result as ActionHandlers) + ) as Effect.Effect; + } + : () => Effect.succeed(wake as ActionHandlers); + return makeRivetkitActor({ actor: this, - buildActionHandlers: Effect.isEffect(build) - ? build - : Effect.succeed(build), + resolveWake, options, }).pipe( Effect.flatMap((rivetKitActor) => @@ -291,11 +319,13 @@ const makeRivetkitActor = Effect.fnUntraced(function* < State extends ActorState.AnyWithProps = never, >({ actor, - buildActionHandlers, + resolveWake, options, }: { readonly actor: Actor; - readonly buildActionHandlers: Effect.Effect; + readonly resolveWake: ( + wakeContext: Rivetkit.WakeContextOf, + ) => Effect.Effect; readonly options: Options; }) { // Snapshot the current Effect context so action callbacks @@ -378,7 +408,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < : Context.empty(), ); - const actionHandlers = yield* buildActionHandlers.pipe( + const actionHandlers = yield* resolveWake(c).pipe( Effect.provide(context), ); diff --git a/rivetkit-typescript/packages/effect/src/ActorState.ts b/rivetkit-typescript/packages/effect/src/ActorState.ts index fa1d59f7fd..a80eee2d44 100644 --- a/rivetkit-typescript/packages/effect/src/ActorState.ts +++ b/rivetkit-typescript/packages/effect/src/ActorState.ts @@ -5,7 +5,7 @@ const TypeId = "~@rivetkit/effect/ActorState"; /** * A typed, persistent state slot for one Rivet Actor. Yielded inside - * the wake-scope build effect to obtain a `State` whose committed + * the wake effect to obtain a `State` whose committed * changes are mirrored back to rivetkit's persisted state. * * State configuration (`schema` + `initial`) is server-only — it @@ -55,7 +55,7 @@ export const isActorState = (u: unknown): u is Any => * * `schema` is the persisted shape; `initialValue` produces the value used to * seed state on first wake. The returned value is itself a Context tag: - * `yield* MyState` inside the wake-scope build effect resolves to a + * `yield* MyState` inside the wake effect resolves to a * `SubscriptionRef`. * * @example From 596db5e390df23b978308717ab3894403323a8b1 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 18 May 2026 15:28:46 +0200 Subject: [PATCH 207/306] refactor(effect): rename resolveWake to wakeHandler in Actor and related functions --- rivetkit-typescript/packages/effect/src/Actor.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 348a4a041c..100835df42 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -237,7 +237,7 @@ const Proto: Omit, "name" | "actions"> = { >, options: Options = {}, ) { - const resolveWake: ( + const wakeHandler: ( wakeContext: Rivetkit.WakeContextOf, ) => Effect.Effect = Effect.isEffect( wake, @@ -267,7 +267,7 @@ const Proto: Omit, "name" | "actions"> = { return makeRivetkitActor({ actor: this, - resolveWake, + wakeHandler, options, }).pipe( Effect.flatMap((rivetKitActor) => @@ -319,11 +319,11 @@ const makeRivetkitActor = Effect.fnUntraced(function* < State extends ActorState.AnyWithProps = never, >({ actor, - resolveWake, + wakeHandler, options, }: { readonly actor: Actor; - readonly resolveWake: ( + readonly wakeHandler: ( wakeContext: Rivetkit.WakeContextOf, ) => Effect.Effect; readonly options: Options; @@ -408,7 +408,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < : Context.empty(), ); - const actionHandlers = yield* resolveWake(c).pipe( + const actionHandlers = yield* wakeHandler(c).pipe( Effect.provide(context), ); From 43953856eaf4c956fadf811feb52847d1c6b9539 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 18 May 2026 15:49:31 +0200 Subject: [PATCH 208/306] chore(node-client): remove node-client example directory --- examples/node-client/.gitignore | 2 - examples/node-client/README.md | 39 --- examples/node-client/package.json | 24 -- examples/node-client/src/client.ts | 88 ------ examples/node-client/src/index.ts | 190 ------------ examples/node-client/tests/chat-room.test.ts | 303 ------------------- examples/node-client/tsconfig.json | 15 - examples/node-client/turbo.json | 4 - examples/node-client/vitest.config.ts | 13 - pnpm-lock.yaml | 19 -- 10 files changed, 697 deletions(-) delete mode 100644 examples/node-client/.gitignore delete mode 100644 examples/node-client/README.md delete mode 100644 examples/node-client/package.json delete mode 100644 examples/node-client/src/client.ts delete mode 100644 examples/node-client/src/index.ts delete mode 100644 examples/node-client/tests/chat-room.test.ts delete mode 100644 examples/node-client/tsconfig.json delete mode 100644 examples/node-client/turbo.json delete mode 100644 examples/node-client/vitest.config.ts diff --git a/examples/node-client/.gitignore b/examples/node-client/.gitignore deleted file mode 100644 index dc6f607390..0000000000 --- a/examples/node-client/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.actorcore -node_modules diff --git a/examples/node-client/README.md b/examples/node-client/README.md deleted file mode 100644 index 2bdb3c0f3c..0000000000 --- a/examples/node-client/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Node Client - -A minimal RivetKit example with no UI framework. The actor lives in `src/index.ts` and a plain Node script in `src/client.ts` connects to it via `rivetkit/client`. - -## Getting Started - -```sh -git clone https://github.com/rivet-dev/rivet.git -cd rivet/examples/node-client -pnpm install -pnpm dev # starts the actor envoy + spawns a local engine -pnpm client # in another terminal: runs the client script -``` - -The example sets `startEngine: true`, so the registry spawns the engine binary itself. When running from this monorepo (no published platform package installed), point `RIVET_ENGINE_BINARY` at the workspace dev build: - -```sh -RIVET_ENGINE_BINARY=$(pwd)/../../target/debug/rivet-engine pnpm dev -``` - -## Features - -- **Actor definition**: Counter actor with persistent state and two actions -- **Type-safe client**: `createClient(endpoint)` for end-to-end type inference -- **No UI framework**: Pure Node script, suitable as a starting point for CLIs, scripts, or backend-to-actor calls - -## Implementation - -- **Actor + registry** ([`src/index.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/node-client/src/index.ts)) -- **Client script** ([`src/client.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/node-client/src/client.ts)) -- **Tests** ([`tests/counter.test.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/node-client/tests/counter.test.ts)) - -## Resources - -Read more about [actions](/docs/actors/actions), [state](/docs/actors/state), and [the client](/docs/clients). - -## License - -MIT diff --git a/examples/node-client/package.json b/examples/node-client/package.json deleted file mode 100644 index 9e78770c19..0000000000 --- a/examples/node-client/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "example-node-client", - "version": "2.0.21", - "private": true, - "type": "module", - "scripts": { - "dev": "tsx --watch src/index.ts", - "start": "tsx src/index.ts", - "client": "tsx src/client.ts", - "check-types": "tsc --noEmit", - "test": "vitest run" - }, - "dependencies": { - "rivetkit": "*" - }, - "devDependencies": { - "@types/node": "^22.13.9", - "tsx": "^3.12.7", - "typescript": "^5.5.2", - "vitest": "^3.1.1" - }, - "stableVersion": "0.8.0", - "license": "MIT" -} diff --git a/examples/node-client/src/client.ts b/examples/node-client/src/client.ts deleted file mode 100644 index a2669f2cdd..0000000000 --- a/examples/node-client/src/client.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { createClient } from "rivetkit/client"; -import type { registry } from "./index.ts"; - -const client = createClient("http://localhost:6420"); - -async function main() { - // getOrCreate: returns a stateless handle, seeding the actor with input - // the first time it is materialized via createState. - const room = client.chatRoom.getOrCreate(["general"], { - createWithInput: { name: "General" }, - }); - - // resolve(): turns a key-based handle into the underlying actor id, useful - // for caching or for re-deriving a handle later via getForId. - const roomId = await room.resolve(); - console.log("room actor id:", roomId); - - // get(): a key-based handle that does NOT auto-create; safe here because - // getOrCreate above just materialized the actor. - const sameRoom = client.chatRoom.get(["general"]); - console.log("members so far:", await sameRoom.getMembers()); - - // create(): always allocates a fresh actor for the supplied key. - const ephemeral = await client.chatRoom.create( - [`scratch-${Date.now()}`], - { input: { name: "Scratch" } }, - ); - const ephemeralId = await ephemeral.resolve(); - console.log("ephemeral room id:", ephemeralId); - - // connect(): opens a stateful WebSocket connection. Subscriptions are - // registered via .on(name, handler); actions can be invoked over the same - // connection just like on a stateless handle. - const conn = room.connect(); - conn.on("memberJoined", ({ member }) => - console.log(`-> ${member.name} joined`), - ); - conn.on("memberLeft", ({ name }) => console.log(`<- ${name} left`)); - conn.on("newMessage", (msg) => - console.log(`[${msg.sender}] ${msg.text}`), - ); - conn.on("announcement", ({ text }) => - console.log(`** announcement: ${text} **`), - ); - - // Action over the connection. Triggers a memberJoined broadcast. - await conn.join("alice"); - - // Completable round-trip. sendMessage internally calls - // c.queue.enqueueAndWait("moderation", ...). The run loop pulls the - // message with `completable: true` and calls msg.complete(verdict); - // only then does this await resolve. - console.log("send (clean):", await conn.sendMessage("alice", "hello world!")); - console.log( - "send (blocked):", - await conn.sendMessage("alice", "this is a spam test"), - ); - - // Scheduled action: server broadcasts an announcement after a delay. - await conn.scheduleAnnouncement("welcome to the channel", 500); - await new Promise((resolve) => setTimeout(resolve, 1000)); - - // Read persistent message history straight from the actor's SQLite db. - console.log("history:", await conn.getHistory()); - - // getForId(): re-derives a handle from a known actor id. Useful when you - // previously stored an id and want to talk to that exact instance. - const byId = client.chatRoom.getForId(roomId); - console.log("members via id handle:", await byId.getMembers()); - - // Cross-actor visibility: directory was registered by chatRoom.join. - const dir = client.directory.getOrCreate(["main"]); - console.log("rooms in directory:", await dir.listRooms()); - - // Moderator stats reflect every review the room delegated to it. - const moderatorStats = await client.moderator - .getOrCreate(["main"]) - .stats(); - console.log("moderator stats:", moderatorStats); - - await conn.dispose(); - await ephemeral.archive(); -} - -main().catch((error) => { - console.error("error:", error); - process.exit(1); -}); diff --git a/examples/node-client/src/index.ts b/examples/node-client/src/index.ts deleted file mode 100644 index e39158685f..0000000000 --- a/examples/node-client/src/index.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { actor, event, queue, setup } from "rivetkit"; -import { db } from "rivetkit/db"; - -// Singleton directory tracking which chat rooms are open. Exercised via -// actor-to-actor calls from chatRoom.onDestroy and chatRoom.join. -export const directory = actor({ - state: { - rooms: [] as Array<{ - name: string; - openedAt: number; - closedAt?: number; - }>, - }, - actions: { - registerRoom: (c, name: string) => { - if (c.state.rooms.some((r) => r.name === name)) return; - c.state.rooms.push({ name, openedAt: Date.now() }); - }, - closeRoom: (c, name: string) => { - const room = c.state.rooms.find((r) => r.name === name); - if (room) room.closedAt = Date.now(); - }, - listRooms: (c) => c.state.rooms, - }, -}); - -// Moderation service consumed by chat rooms via cross-actor RPC. -export const moderator = actor({ - state: { - bannedWords: ["spam", "scam"] as string[], - reviewed: 0, - }, - actions: { - review: (c, text: string) => { - c.state.reviewed += 1; - const hit = c.state.bannedWords.find((word) => - text.toLowerCase().includes(word), - ); - return hit - ? { - approved: false as const, - reason: `contains banned word "${hit}"`, - } - : { approved: true as const }; - }, - stats: (c) => ({ reviewed: c.state.reviewed }), - }, -}); - -interface RoomInput { - name: string; -} -interface Member { - name: string; - joinedAt: number; -} -interface RoomState { - name: string; - members: Member[]; - wakeCount: number; -} - -export const chatRoom = actor({ - // SQLite-backed message log that survives sleeps and restarts. - db: db({ - onMigrate: async (db) => { - await db.execute(` - CREATE TABLE IF NOT EXISTS messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - sender TEXT NOT NULL, - text TEXT NOT NULL, - created_at INTEGER NOT NULL - ) - `); - }, - }), - // Persistent state seeded from the createWithInput / input passed by the - // client on getOrCreate / create. - createState: (_c, input: RoomInput): RoomState => ({ - name: input.name, - members: [], - wakeCount: 0, - }), - // Per-instance vars regenerated each wake. Useful for tracing. - createVars: () => ({ - sessionId: crypto.randomUUID(), - }), - events: { - newMessage: event<{ sender: string; text: string; createdAt: number }>(), - memberJoined: event<{ member: Member }>(), - memberLeft: event<{ name: string }>(), - announcement: event<{ text: string }>(), - }, - // Completable queue: actions enqueueAndWait, the run loop calls complete(). - queues: { - moderation: queue< - { sender: string; text: string }, - { approved: boolean; reason?: string } - >(), - }, - onWake: (c) => { - c.state.wakeCount += 1; - c.log.info({ - msg: "room awake", - sessionId: c.vars.sessionId, - wakeCount: c.state.wakeCount, - }); - }, - onDestroy: async (c) => { - const client = c.client(); - await client.directory.getOrCreate(["main"]).closeRoom(c.state.name); - }, - // Drains moderation messages, reviews each via the moderator actor, then - // completes the corresponding enqueueAndWait waiter inside sendMessage. - run: async (c) => { - const client = c.client(); - const reviewer = client.moderator.getOrCreate(["main"]); - for await (const msg of c.queue.iter({ - names: ["moderation"], - completable: true, - })) { - const verdict = await reviewer.review(msg.body.text); - await msg.complete(verdict); - } - }, - actions: { - join: async (c, name: string): Promise => { - const member: Member = { name, joinedAt: Date.now() }; - c.state.members.push(member); - c.broadcast("memberJoined", { member }); - const client = c.client(); - await client.directory - .getOrCreate(["main"]) - .registerRoom(c.state.name); - return member; - }, - leave: (c, name: string) => { - c.state.members = c.state.members.filter((m) => m.name !== name); - c.broadcast("memberLeft", { name }); - }, - // Sends the message through the moderation pipeline before persisting. - // The action returns only after the run loop completes the queue entry. - sendMessage: async (c, sender: string, text: string) => { - const verdict = await c.queue.enqueueAndWait( - "moderation", - { sender, text }, - { timeout: 10_000 }, - ); - if (!verdict) { - throw new Error("moderation timed out"); - } - if (!verdict.approved) { - return { ok: false as const, reason: verdict.reason }; - } - const createdAt = Date.now(); - await c.db.execute( - "INSERT INTO messages (sender, text, created_at) VALUES (?, ?, ?)", - sender, - text, - createdAt, - ); - c.broadcast("newMessage", { sender, text, createdAt }); - return { ok: true as const, createdAt }; - }, - getHistory: async (c) => - c.db.execute( - "SELECT id, sender, text, created_at as createdAt FROM messages ORDER BY id", - ), - getMembers: (c) => c.state.members, - // Schedules a future broadcast. Implemented via c.schedule.after, which - // dispatches the named action with the supplied args. - scheduleAnnouncement: (c, text: string, delayMs: number) => { - c.schedule.after(delayMs, "triggerAnnouncement", text); - return { firesAt: Date.now() + delayMs }; - }, - triggerAnnouncement: (c, text: string) => { - c.broadcast("announcement", { text }); - }, - archive: (c) => { - c.destroy(); - }, - }, -}); - -export const registry = setup({ - use: { chatRoom, moderator, directory }, - startEngine: true, -}); - -registry.start(); diff --git a/examples/node-client/tests/chat-room.test.ts b/examples/node-client/tests/chat-room.test.ts deleted file mode 100644 index 2370cfa54b..0000000000 --- a/examples/node-client/tests/chat-room.test.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { setupTest } from "rivetkit/test"; -import { describe, expect, test } from "vitest"; -import { registry } from "../src/index.ts"; - -// Engine state persists across `setupTest` calls within a vitest run, so we -// derive a unique key per test to keep them isolated. -const uniqueKey = (label: string) => - `${label}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; - -interface DirectoryEntry { - name: string; - openedAt: number; - closedAt?: number; -} - -describe("chat room actor", () => { - test("createState seeds room from input", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - const room = client.chatRoom.getOrCreate([uniqueKey("create-state")], { - createWithInput: { name: "Alpha" }, - }); - - expect(await room.getMembers()).toEqual([]); - - await room.join("alice"); - const members = await room.getMembers(); - expect(members).toHaveLength(1); - expect(members[0]?.name).toBe("alice"); - }); - - test("sendMessage runs through completable moderation queue", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - const room = client.chatRoom.getOrCreate([uniqueKey("clean")], { - createWithInput: { name: "Clean" }, - }); - - const result = await room.sendMessage("alice", "hello world"); - expect(result.ok).toBe(true); - if (result.ok) { - expect(typeof result.createdAt).toBe("number"); - } - }); - - test("moderator rejects banned words via cross-actor RPC", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - const room = client.chatRoom.getOrCreate([uniqueKey("blocked")], { - createWithInput: { name: "Blocked" }, - }); - - const result = await room.sendMessage("alice", "this is spam"); - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.reason).toMatch(/spam/); - } - - // Blocked messages must not be persisted to the SQLite log. - expect(await room.getHistory()).toEqual([]); - }); - - test("getHistory reads from the SQLite db", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - const room = client.chatRoom.getOrCreate([uniqueKey("history")], { - createWithInput: { name: "History" }, - }); - - await room.sendMessage("alice", "first"); - await room.sendMessage("bob", "second"); - await room.sendMessage("carol", "third"); - - const history = (await room.getHistory()) as Array<{ - sender: string; - text: string; - }>; - - expect(history.map((m) => [m.sender, m.text])).toEqual([ - ["alice", "first"], - ["bob", "second"], - ["carol", "third"], - ]); - }); - - test("connect() receives broadcast events", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - const room = client.chatRoom.getOrCreate([uniqueKey("events")], { - createWithInput: { name: "Events" }, - }); - - const conn = room.connect(); - try { - const messageReceived = new Promise<{ - sender: string; - text: string; - }>((resolve) => { - conn.on("newMessage", (msg) => resolve(msg)); - }); - const memberJoined = new Promise<{ member: { name: string } }>( - (resolve) => { - conn.on("memberJoined", (payload) => resolve(payload)); - }, - ); - - await conn.join("alice"); - await conn.sendMessage("alice", "ping"); - - expect((await memberJoined).member.name).toBe("alice"); - expect(await messageReceived).toMatchObject({ - sender: "alice", - text: "ping", - }); - } finally { - await conn.dispose(); - } - }); - - test("scheduleAnnouncement broadcasts after the delay", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - const room = client.chatRoom.getOrCreate([uniqueKey("schedule")], { - createWithInput: { name: "Schedule" }, - }); - - const conn = room.connect(); - try { - const announcementReceived = new Promise<{ text: string }>( - (resolve) => { - conn.on("announcement", (payload) => resolve(payload)); - }, - ); - - await conn.scheduleAnnouncement("welcome!", 100); - - expect(await announcementReceived).toEqual({ text: "welcome!" }); - } finally { - await conn.dispose(); - } - }); - - test("leave removes the member and broadcasts memberLeft", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - const room = client.chatRoom.getOrCreate([uniqueKey("leave")], { - createWithInput: { name: "Leave" }, - }); - - const conn = room.connect(); - try { - await conn.join("alice"); - await conn.join("bob"); - - const memberLeft = new Promise<{ name: string }>((resolve) => { - conn.on("memberLeft", (payload) => resolve(payload)); - }); - - await conn.leave("alice"); - - expect((await memberLeft).name).toBe("alice"); - expect(await conn.getMembers()).toEqual([ - expect.objectContaining({ name: "bob" }), - ]); - } finally { - await conn.dispose(); - } - }); - - test("different keys are isolated", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - const roomA = client.chatRoom.getOrCreate([uniqueKey("isolated-a")], { - createWithInput: { name: "A" }, - }); - const roomB = client.chatRoom.getOrCreate([uniqueKey("isolated-b")], { - createWithInput: { name: "B" }, - }); - - await roomA.sendMessage("alice", "in a"); - - expect(await roomA.getHistory()).toHaveLength(1); - expect(await roomB.getHistory()).toHaveLength(0); - }); - - test("getForId(resolve()) targets the same instance", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - const room = client.chatRoom.getOrCreate([uniqueKey("resolve")], { - createWithInput: { name: "Resolve" }, - }); - await room.join("alice"); - - const actorId = await room.resolve(); - const byId = client.chatRoom.getForId(actorId); - - const members = (await byId.getMembers()) as Array<{ name: string }>; - expect(members.map((m) => m.name)).toEqual(["alice"]); - }); - - test("create() always allocates a fresh actor", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - const a = await client.chatRoom.create([uniqueKey("create-a")], { - input: { name: "First" }, - }); - const b = await client.chatRoom.create([uniqueKey("create-b")], { - input: { name: "Second" }, - }); - - expect(await a.resolve()).not.toBe(await b.resolve()); - }); -}); - -describe("moderator actor", () => { - test("review approves clean text", async (ctx) => { - const { client } = await setupTest(ctx, registry); - const moderator = client.moderator.getOrCreate(["main"]); - - const verdict = await moderator.review("hello there"); - expect(verdict.approved).toBe(true); - }); - - test("review rejects text with banned words", async (ctx) => { - const { client } = await setupTest(ctx, registry); - const moderator = client.moderator.getOrCreate(["main"]); - - const verdict = await moderator.review("totally a scam"); - expect(verdict.approved).toBe(false); - if (!verdict.approved) { - expect(verdict.reason).toMatch(/scam/); - } - }); - - test("stats counter increments on each review", async (ctx) => { - const { client } = await setupTest(ctx, registry); - const moderator = client.moderator.getOrCreate([uniqueKey("stats")]); - - const before = (await moderator.stats()).reviewed; - await moderator.review("ok"); - await moderator.review("also fine"); - const after = (await moderator.stats()).reviewed; - - expect(after).toBe(before + 2); - }); - - test("chatRoom.sendMessage drives traffic to moderator['main']", async (ctx) => { - const { client } = await setupTest(ctx, registry); - const moderator = client.moderator.getOrCreate(["main"]); - const room = client.chatRoom.getOrCreate([uniqueKey("stats-room")], { - createWithInput: { name: "Stats Room" }, - }); - - const before = (await moderator.stats()).reviewed; - await room.sendMessage("alice", "hello"); - await room.sendMessage("alice", "again"); - const after = (await moderator.stats()).reviewed; - - expect(after).toBe(before + 2); - }); -}); - -describe("directory actor", () => { - test("chatRoom.join registers the room with the directory", async (ctx) => { - const { client } = await setupTest(ctx, registry); - - const roomName = uniqueKey("Directory Test"); - const room = client.chatRoom.getOrCreate([uniqueKey("directory")], { - createWithInput: { name: roomName }, - }); - await room.join("alice"); - - const dir = client.directory.getOrCreate(["main"]); - const rooms = (await dir.listRooms()) as DirectoryEntry[]; - - expect(rooms.some((r) => r.name === roomName)).toBe(true); - }); - - test("registerRoom is idempotent", async (ctx) => { - const { client } = await setupTest(ctx, registry); - const dir = client.directory.getOrCreate([uniqueKey("idempotent")]); - const roomName = uniqueKey("only-once"); - - await dir.registerRoom(roomName); - await dir.registerRoom(roomName); - - const rooms = (await dir.listRooms()) as DirectoryEntry[]; - expect(rooms.filter((r) => r.name === roomName)).toHaveLength(1); - }); - - test("closeRoom marks the room as closed", async (ctx) => { - const { client } = await setupTest(ctx, registry); - const dir = client.directory.getOrCreate([uniqueKey("close-test")]); - const roomName = uniqueKey("closing"); - - await dir.registerRoom(roomName); - await dir.closeRoom(roomName); - - const rooms = (await dir.listRooms()) as DirectoryEntry[]; - const closed = rooms.find((r) => r.name === roomName); - expect(closed?.closedAt).toBeTypeOf("number"); - }); -}); diff --git a/examples/node-client/tsconfig.json b/examples/node-client/tsconfig.json deleted file mode 100644 index 4e95b1c507..0000000000 --- a/examples/node-client/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "esnext", - "lib": ["esnext"], - "module": "esnext", - "moduleResolution": "bundler", - "types": ["node"], - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "allowImportingTsExtensions": true, - "rewriteRelativeImportExtensions": true - }, - "include": ["src/**/*", "tests/**/*"] -} diff --git a/examples/node-client/turbo.json b/examples/node-client/turbo.json deleted file mode 100644 index 29d4cb2625..0000000000 --- a/examples/node-client/turbo.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://turbo.build/schema.json", - "extends": ["//"] -} diff --git a/examples/node-client/vitest.config.ts b/examples/node-client/vitest.config.ts deleted file mode 100644 index f14b5571e9..0000000000 --- a/examples/node-client/vitest.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - include: ["tests/**/*.test.ts"], - // setupTest re-spawns the local engine binary for every test, and the - // first action against a freshly-restarted engine occasionally hits the - // guard.service_unavailable retry window before the router is fully - // wired. Retry transient warm-up failures. - retry: 2, - testTimeout: 30_000, - }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 045e9ec12a..82a8fe3f2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2226,25 +2226,6 @@ importers: specifier: ^5 version: 5.9.3 - examples/node-client: - dependencies: - rivetkit: - specifier: workspace:* - version: link:../../rivetkit-typescript/packages/rivetkit - devDependencies: - '@types/node': - specifier: ^22.13.9 - version: 22.19.15 - tsx: - specifier: ^3.12.7 - version: 3.14.0 - typescript: - specifier: ^5.5.2 - version: 5.9.3 - vitest: - specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.15)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) - examples/per-tenant-database: dependencies: '@rivetkit/react': From f036319c71ab72db28f6787b9acc7a90fc3feb53 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 18 May 2026 16:14:49 +0200 Subject: [PATCH 209/306] test(effect): add unit tests for Actor.toWakeHandler with various wake forms --- .../packages/effect/src/Actor.test.ts | 159 ++++++++++++++++++ .../packages/effect/src/Actor.ts | 79 +++------ 2 files changed, 181 insertions(+), 57 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/src/Actor.test.ts diff --git a/rivetkit-typescript/packages/effect/src/Actor.test.ts b/rivetkit-typescript/packages/effect/src/Actor.test.ts new file mode 100644 index 0000000000..bcf18b5b22 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Actor.test.ts @@ -0,0 +1,159 @@ +import { assert, describe, it } from "@effect/vitest"; +import { Context, Effect, Layer } from "effect"; +import type * as Rivetkit from "rivetkit"; +import * as Actor from "./Actor"; + +class Prefix extends Context.Service()( + "Actor.test/Prefix", +) {} +const PrefixLive = Layer.succeed(Prefix, Prefix.of({ value: "svc" })); + +describe("Actor.toWakeHandler", () => { + it.effect("wraps a plain action handler object", () => + Effect.gen(function* () { + const wake = { Ping: () => Effect.succeed("pong") }; + const wakeHandler = Actor.toWakeHandler(wake); + const actionHandlers = yield* wakeHandler({} as Actor.WakeOptions); + + assert.strictEqual(actionHandlers, wake); + }), + ); + + it.effect("runs an Effect that resolves to action handlers", () => + Effect.gen(function* () { + const wake = Effect.gen(function* () { + const prefix = yield* Prefix; + + return { + Ping: () => Effect.succeed(`${prefix.value}:pong`), + }; + }); + const wakeHandler = Actor.toWakeHandler(wake); + const actionHandlers = yield* wakeHandler({} as Actor.WakeOptions); + + assert.strictEqual(yield* actionHandlers.Ping(), "svc:pong"); + }).pipe(Effect.provide(PrefixLive)), + ); + + it.effect("calls a wake function with the wake context", () => + Effect.gen(function* () { + const wakeOptions: Actor.WakeOptions = { + rawRivetkitContext: { + key: ["room", "1"], + } as Rivetkit.WakeContextOf, + }; + const wake = (wakeOptions: Actor.WakeOptions) => ({ + GetKey: () => + Effect.succeed( + wakeOptions.rawRivetkitContext.key.join("/"), + ), + }); + const wakeHandler = Actor.toWakeHandler(wake); + const actionHandlers = yield* wakeHandler(wakeOptions); + + assert.strictEqual(yield* actionHandlers.GetKey(), "room/1"); + }), + ); + + it.effect("flattens a wake function returning an Effect", () => + Effect.gen(function* () { + const wakeOptions: Actor.WakeOptions = { + rawRivetkitContext: { + key: ["room", "2"], + } as Rivetkit.WakeContextOf, + }; + const wake = (options: Actor.WakeOptions) => + Effect.gen(function* () { + const prefix = yield* Prefix; + + return { + GetKey: () => + Effect.succeed( + `${prefix.value}:${options.rawRivetkitContext.key.join("/")}`, + ), + }; + }); + const wakeHandler = Actor.toWakeHandler(wake); + const actionHandlers = yield* wakeHandler(wakeOptions); + + assert.strictEqual(yield* actionHandlers.GetKey(), "svc:room/2"); + }).pipe(Effect.provide(PrefixLive)), + ); + + it.effect("runs an Effect that resolves to a wake function", () => + Effect.gen(function* () { + const wakeOptions: Actor.WakeOptions = { + rawRivetkitContext: { + actorId: "actor-1", + } as Rivetkit.WakeContextOf, + }; + const wake = Effect.gen(function* () { + const prefix = yield* Prefix; + + return (options: Actor.WakeOptions) => + Effect.succeed({ + GetActorId: () => + Effect.succeed( + `${prefix.value}:${options.rawRivetkitContext.actorId}`, + ), + }); + }); + const wakeHandler = Actor.toWakeHandler(wake); + const actionHandlers = yield* wakeHandler(wakeOptions); + + assert.strictEqual( + yield* actionHandlers.GetActorId(), + "svc:actor-1", + ); + }).pipe(Effect.provide(PrefixLive)), + ); + + it.effect("accepts an Effect.fn wake function", () => + Effect.gen(function* () { + const wakeOptions: Actor.WakeOptions = { + rawRivetkitContext: { + key: ["effect", "fn"], + } as Rivetkit.WakeContextOf, + }; + const wake = Effect.fn("wake")(function* ( + options: Actor.WakeOptions, + ) { + const prefix = yield* Prefix; + + return { + GetKey: () => + Effect.succeed( + `${prefix.value}:${options.rawRivetkitContext.key.join("/")}`, + ), + }; + }); + const wakeHandler = Actor.toWakeHandler(wake); + const actionHandlers = yield* wakeHandler(wakeOptions); + + assert.strictEqual(yield* actionHandlers.GetKey(), "svc:effect/fn"); + }).pipe(Effect.provide(PrefixLive)), + ); + + it.effect( + "defers wake functions until the returned handler is invoked", + () => + Effect.gen(function* () { + let calls = 0; + const wake = () => { + calls++; + return { Count: () => Effect.succeed(calls) }; + }; + const wakeHandler = Actor.toWakeHandler(wake); + + assert.strictEqual(calls, 0); + + const first = yield* wakeHandler({} as Actor.WakeOptions); + assert.strictEqual(calls, 1); + assert.strictEqual(yield* first.Count(), 1); + + const second = yield* wakeHandler({} as Actor.WakeOptions); + assert.strictEqual(calls, 2); + assert.strictEqual(yield* second.Count(), 2); + }), + ); +}); diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 100835df42..c3e8472d7d 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -118,6 +118,20 @@ type ActionHandlerServices = { : never; }[keyof ActionHandlers]; +export type WakeOptions = { + readonly rawRivetkitContext: Rivetkit.WakeContextOf; +}; + +type WakeFunction = + | ((wakeOptions: WakeOptions) => ActionHandlers) + | ((wakeOptions: WakeOptions) => Effect.Effect); + +type Wake = + | ActionHandlers + | Effect.Effect + | WakeFunction + | Effect.Effect, never, RX>; + export type AccessorKeyParam = string | Rivetkit.ActorKey; /** @@ -165,18 +179,7 @@ export interface Actor< State extends ActorState.AnyWithProps = never, RX = never, >( - wake: - | ActionHandlers - | Effect.Effect - | ((wakeOptions: any) => ActionHandlers) - | ((wakeOptions: any) => Effect.Effect) - | Effect.Effect< - ( - wakeOptions: any, - ) => Effect.Effect, - never, - RX - >, + wake: Wake, options?: Options, ): Layer.Layer< never, @@ -223,51 +226,12 @@ const Proto: Omit, "name" | "actions"> = { RX = never, >( this: Actor, - wake: - | ActionHandlers - | Effect.Effect - | ((wakeOptions: any) => ActionHandlers) - | ((wakeOptions: any) => Effect.Effect) - | Effect.Effect< - ( - wakeOptions: any, - ) => Effect.Effect, - never, - RX - >, + wake: Wake, options: Options = {}, ) { - const wakeHandler: ( - wakeContext: Rivetkit.WakeContextOf, - ) => Effect.Effect = Effect.isEffect( - wake, - ) - ? (c) => - (wake as Effect.Effect).pipe( - Effect.flatMap((resolved: any) => - typeof resolved === "function" - ? (resolved(c) as Effect.Effect< - ActionHandlers, - never, - R - >) - : Effect.succeed(resolved as ActionHandlers), - ), - ) - : typeof wake === "function" - ? (c: any) => { - const result = (wake as Function)(c); - return ( - Effect.isEffect(result) - ? result - : Effect.succeed(result as ActionHandlers) - ) as Effect.Effect; - } - : () => Effect.succeed(wake as ActionHandlers); - return makeRivetkitActor({ actor: this, - wakeHandler, + wakeHandler: toWakeHandler(wake), options, }).pipe( Effect.flatMap((rivetKitActor) => @@ -323,9 +287,9 @@ const makeRivetkitActor = Effect.fnUntraced(function* < options, }: { readonly actor: Actor; - readonly wakeHandler: ( - wakeContext: Rivetkit.WakeContextOf, - ) => Effect.Effect; + readonly wakeHandler: (wakeOptions: { + rawRivetkitContext: Rivetkit.WakeContextOf; + }) => Effect.Effect; readonly options: Options; }) { // Snapshot the current Effect context so action callbacks @@ -408,7 +372,8 @@ const makeRivetkitActor = Effect.fnUntraced(function* < : Context.empty(), ); - const actionHandlers = yield* wakeHandler(c).pipe( + const wakeOptions = { rawRivetkitContext: c }; + const actionHandlers = yield* wakeHandler(wakeOptions).pipe( Effect.provide(context), ); From cf50484a04088eb236c4b20020124c11ab76beb8 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 18 May 2026 17:16:32 +0200 Subject: [PATCH 210/306] refactor(effect): update Counter.toLayer to use wakeOptions and improve handler structure --- .../packages/effect/test/e2e.test.ts | 7 +- .../packages/effect/test/fixtures/actors.ts | 308 +++++++++--------- 2 files changed, 164 insertions(+), 151 deletions(-) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index c0c8ef6e1a..fe4f83f3ee 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -430,7 +430,10 @@ layer(TestLayer)("end-to-end", (it) => { RivetError.GuardServiceUnavailable, ); assert.strictEqual( - (exit.value.reason as RivetError.GuardServiceUnavailable).code, + ( + exit.value + .reason as RivetError.GuardServiceUnavailable + ).code, "service_unavailable", ); } @@ -592,7 +595,7 @@ layer(TestLayer)("end-to-end", (it) => { }), ); - it.effect("writes through the db captured from RawRivetkitContext", () => + it.effect("writes through the db captured", () => Effect.gen(function* () { const counter = (yield* Counter.client).getOrCreate(["t-db-write"]); const afterFirst = yield* counter.LogEvent({ event: "alpha" }); diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts index a116316e37..3ea348e1b9 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts @@ -223,156 +223,166 @@ const CounterState = ActorState.make("CounterState", { }); export const CounterLive = Counter.toLayer( - Effect.gen(function* () { - const state = yield* CounterState; - const count = yield* Ref.make(0); - const flags = yield* Flags; - flags.set("on wake", true); - const greeter = yield* Greeter; - const wakeGreeting = greeter.greet("on wake"); - - const sleep = yield* Actor.Sleep; - // `RawRivetkitContext`'s `db` widens to `any` against - // `RunContextOf`. The provider configured on - // `Counter.toLayer` below is the `rivetkit/db` raw-access factory, - // so re-narrow to `RawAccess` for typed `execute` calls inside - // handler closures. - const ctx = yield* Actor.RawRivetkitContext; - const db = ctx.db as RawAccess; - // `Flags` is a process-wide Map shared across all tests in the - // suite, so the finalizer flag must be namespaced by actor key - // to keep cross-test wake/sleep cycles from leaking into each - // other's assertions. - const address = yield* Actor.CurrentAddress; - const finalizerFlag = `finalizer:${address.key.join("/")}`; - - yield* Effect.addFinalizer(() => - Effect.sync(() => { - flags.set(finalizerFlag, true); - }), - ); - - return Counter.of({ - Increment: ({ payload }) => - Effect.gen(function* () { - const next = yield* Ref.updateAndGet( - count, - (n) => n + payload.amount, - ); - if (next > 20) { - return yield* new CounterOverflowError({ - limit: 20, - message: `count ${next} would exceed limit 20`, - }); - } - return next; - }), - GetCount: () => Ref.get(count), - Crash: () => Effect.die("kaboom"), - EchoDate: ({ payload }) => Effect.succeed(payload.when), - Tags: ({ payload }) => Effect.succeed(payload.tags.length), - // Per-handler yield of a non-built-in service. Resolved on - // every call against the snapshotted Runner context. - Greet: ({ payload }) => - Effect.gen(function* () { - const g = yield* Greeter; - return g.greet(payload.name); - }), - WakeGreeting: () => Effect.succeed(wakeGreeting), - // User-defined sub-span. The SDK already wraps the handler - // in a server-side span; the inner `withSpan("step.double")` - // nests under it, demonstrating that hand-written spans - // inside a handler join the caller's trace transparently. - Compute: ({ payload }) => - Effect.succeed(payload.n * 2).pipe( - Effect.withSpan("step.double"), - ), - Scale: ({ payload }) => - Effect.gen(function* () { - if (payload.amount > 30) { - return yield* new ScaledOverflowError({ - limit: 30, - message: `amount ${payload.amount} would exceed limit 30`, - }); - } - // +100 makes the round-trip non-tautological: the - // test asserts on a value the client never sent, so - // the success path can't pass without the success - // and payload codec sites firing on both sides. - return payload.amount + 100; - }), - PersistAndSleep: ({ payload }) => - Effect.gen(function* () { - const { count } = yield* State.updateAndGet(state, (s) => ({ - ...s, - count: s.count + payload.amount, - })); - yield* sleep; - return count; - }), - PersistDateAndSleep: ({ payload }) => - Effect.gen(function* () { - const { when } = yield* State.updateAndGet(state, (s) => ({ - ...s, - when: payload.when, - })); - yield* sleep; - return when; - }), - PersistTagsAndSleep: ({ payload }) => - Effect.gen(function* () { - const { tags } = yield* State.updateAndGet(state, (s) => ({ - ...s, - tags: payload.tags, - })); - yield* sleep; - return tags; + (wakeOptions) => + Effect.gen(function* () { + const state = yield* CounterState; + const count = yield* Ref.make(0); + const flags = yield* Flags; + flags.set("on wake", true); + const greeter = yield* Greeter; + const wakeGreeting = greeter.greet("on wake"); + + const sleep = yield* Actor.Sleep; + // `rawRivetkitContext`'s `db` widens to `any` against + // `RunContextOf`. The provider configured on + // `Counter.toLayer` below is the `rivetkit/db` raw-access factory, + // so re-narrow to `RawAccess` for typed `execute` calls inside + // handler closures. + const ctx = wakeOptions.rawRivetkitContext; + const db = ctx.db as RawAccess; + // `Flags` is a process-wide Map shared across all tests in the + // suite, so the finalizer flag must be namespaced by actor key + // to keep cross-test wake/sleep cycles from leaking into each + // other's assertions. + const address = yield* Actor.CurrentAddress; + const finalizerFlag = `finalizer:${address.key.join("/")}`; + + yield* Effect.addFinalizer(() => + Effect.sync(() => { + flags.set(finalizerFlag, true); }), - PersistScaledAndSleep: ({ payload }) => - Effect.gen(function* () { - const { scaled } = yield* State.updateAndGet( - state, - (s) => ({ - ...s, - scaled: payload.amount, - }), - ); - yield* sleep; - return scaled; - }), - GetPersistedState: () => State.get(state), - // Per-actor SQLite is provisioned via the `db:` option on - // `Counter.toLayer` below. The build effect destructures `db` - // from `Actor.RawRivetkitContext`, so handlers reach SQLite - // through the captured client without going through `c.db`. - LogEvent: ({ payload }) => - Effect.tryPromise(async () => { - await db.execute( - "INSERT INTO events (event, created_at) VALUES (?, ?)", - payload.event, - Date.now(), - ); - const rows = await db.execute<{ count: number }>( - "SELECT COUNT(*) as count FROM events", - ); - return rows[0]?.count ?? 0; - }).pipe(Effect.orDie), - ListEvents: () => - Effect.tryPromise(async () => { - const rows = await db.execute<{ event: string }>( - "SELECT event FROM events ORDER BY id ASC", - ); - return rows.map((r) => r.event); - }).pipe(Effect.orDie), - CountEvents: () => - Effect.tryPromise(async () => { - const rows = await db.execute<{ count: number }>( - "SELECT COUNT(*) as count FROM events", - ); - return rows[0]?.count ?? 0; - }).pipe(Effect.orDie), - }); - }), + ); + + return Counter.of({ + Increment: ({ payload }) => + Effect.gen(function* () { + const next = yield* Ref.updateAndGet( + count, + (n) => n + payload.amount, + ); + if (next > 20) { + return yield* new CounterOverflowError({ + limit: 20, + message: `count ${next} would exceed limit 20`, + }); + } + return next; + }), + GetCount: () => Ref.get(count), + Crash: () => Effect.die("kaboom"), + EchoDate: ({ payload }) => Effect.succeed(payload.when), + Tags: ({ payload }) => Effect.succeed(payload.tags.length), + // Per-handler yield of a non-built-in service. Resolved on + // every call against the snapshotted Runner context. + Greet: ({ payload }) => + Effect.gen(function* () { + const g = yield* Greeter; + return g.greet(payload.name); + }), + WakeGreeting: () => Effect.succeed(wakeGreeting), + // User-defined sub-span. The SDK already wraps the handler + // in a server-side span; the inner `withSpan("step.double")` + // nests under it, demonstrating that hand-written spans + // inside a handler join the caller's trace transparently. + Compute: ({ payload }) => + Effect.succeed(payload.n * 2).pipe( + Effect.withSpan("step.double"), + ), + Scale: ({ payload }) => + Effect.gen(function* () { + if (payload.amount > 30) { + return yield* new ScaledOverflowError({ + limit: 30, + message: `amount ${payload.amount} would exceed limit 30`, + }); + } + // +100 makes the round-trip non-tautological: the + // test asserts on a value the client never sent, so + // the success path can't pass without the success + // and payload codec sites firing on both sides. + return payload.amount + 100; + }), + PersistAndSleep: ({ payload }) => + Effect.gen(function* () { + const { count } = yield* State.updateAndGet( + state, + (s) => ({ + ...s, + count: s.count + payload.amount, + }), + ); + yield* sleep; + return count; + }), + PersistDateAndSleep: ({ payload }) => + Effect.gen(function* () { + const { when } = yield* State.updateAndGet( + state, + (s) => ({ + ...s, + when: payload.when, + }), + ); + yield* sleep; + return when; + }), + PersistTagsAndSleep: ({ payload }) => + Effect.gen(function* () { + const { tags } = yield* State.updateAndGet( + state, + (s) => ({ + ...s, + tags: payload.tags, + }), + ); + yield* sleep; + return tags; + }), + PersistScaledAndSleep: ({ payload }) => + Effect.gen(function* () { + const { scaled } = yield* State.updateAndGet( + state, + (s) => ({ + ...s, + scaled: payload.amount, + }), + ); + yield* sleep; + return scaled; + }), + GetPersistedState: () => State.get(state), + // Per-actor SQLite is provisioned via the `db:` option on + // `Counter.toLayer` below. The build effect destructures `db` + // from `wakeOptions.rawRivetkitContext`, so handlers reach SQLite + // through the captured client without going through `c.db`. + LogEvent: ({ payload }) => + Effect.tryPromise(async () => { + await db.execute( + "INSERT INTO events (event, created_at) VALUES (?, ?)", + payload.event, + Date.now(), + ); + const rows = await db.execute<{ count: number }>( + "SELECT COUNT(*) as count FROM events", + ); + return rows[0]?.count ?? 0; + }).pipe(Effect.orDie), + ListEvents: () => + Effect.tryPromise(async () => { + const rows = await db.execute<{ event: string }>( + "SELECT event FROM events ORDER BY id ASC", + ); + return rows.map((r) => r.event); + }).pipe(Effect.orDie), + CountEvents: () => + Effect.tryPromise(async () => { + const rows = await db.execute<{ count: number }>( + "SELECT COUNT(*) as count FROM events", + ); + return rows[0]?.count ?? 0; + }).pipe(Effect.orDie), + }); + }), { state: CounterState, // Migration runs once before the wake-scope build effect, so the From f635f23f122613209cddaefe78441050f1de26f3 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 18 May 2026 17:24:05 +0200 Subject: [PATCH 211/306] feat(effect): Actor.toWakeHandler --- .../packages/effect/src/Actor.ts | 102 +++++++++++++++--- 1 file changed, 86 insertions(+), 16 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index c3e8472d7d..1f41d80d33 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -157,6 +157,32 @@ export type Accessor = { readonly getOrCreate: (key: AccessorKeyParam) => Handle; }; +type UnknownToNever = unknown extends T ? never : T; + +type ExcludeBuiltInWakeServices< + T, + State extends ActorState.AnyWithProps, +> = UnknownToNever< + Exclude< + T, + Scope.Scope | CurrentAddress | Sleep | RawRivetkitContext | State + > +>; + +type ToLayerRequirements< + Actions extends Action.Any, + ActionHandlers, + State extends ActorState.AnyWithProps, + R, + RX, +> = + | ExcludeBuiltInWakeServices + | ExcludeBuiltInWakeServices + | UnknownToNever> + | UnknownToNever> + | UnknownToNever> + | Registry.Registry; + /** * A Rivet Actor contract. It carries the action schemas and * display options, but no server implementation. @@ -174,9 +200,9 @@ export interface Actor< ): ActionHandlers; toLayer< - R, ActionHandlers extends ActionHandlersFrom, State extends ActorState.AnyWithProps = never, + R = never, RX = never, >( wake: Wake, @@ -184,19 +210,7 @@ export interface Actor< ): Layer.Layer< never, never, - | Exclude< - RX, - | Scope.Scope - | CurrentAddress - | Sleep - | RawRivetkitContext - | State - > - | R - | ActionHandlerServices - | Action.ServicesServer - | Action.ServicesClient - | Registry.Registry + ToLayerRequirements >; /** @@ -219,10 +233,10 @@ export type ActionHandlersFrom = { const Proto: Omit, "name" | "actions"> = { [TypeId]: TypeId, toLayer< - R, Actions extends Action.AnyWithProps, ActionHandlers extends ActionHandlersFrom, State extends ActorState.AnyWithProps = never, + R = never, RX = never, >( this: Actor, @@ -231,7 +245,7 @@ const Proto: Omit, "name" | "actions"> = { ) { return makeRivetkitActor({ actor: this, - wakeHandler: toWakeHandler(wake), + wakeHandler: toWakeHandler(wake), options, }).pipe( Effect.flatMap((rivetKitActor) => @@ -275,6 +289,62 @@ export const make = < return self; }; +export function toWakeHandler( + wake: Effect.Effect< + (wakeOptions: WakeOptions) => Effect.Effect, + never, + RX + >, +): (wakeOptions: WakeOptions) => Effect.Effect; +export function toWakeHandler( + wake: Effect.Effect< + (wakeOptions: WakeOptions) => ActionHandlers, + never, + RX + >, +): (wakeOptions: WakeOptions) => Effect.Effect; +export function toWakeHandler( + wake: (wakeOptions: WakeOptions) => Effect.Effect, +): (wakeOptions: WakeOptions) => Effect.Effect; +export function toWakeHandler( + wake: (wakeOptions: WakeOptions) => ActionHandlers, +): (wakeOptions: WakeOptions) => Effect.Effect; +export function toWakeHandler( + wake: Effect.Effect, +): (wakeOptions: WakeOptions) => Effect.Effect; +export function toWakeHandler( + wake: ActionHandlers, +): (wakeOptions: WakeOptions) => Effect.Effect; +export function toWakeHandler( + wake: Wake, +): (wakeOptions: WakeOptions) => Effect.Effect; +export function toWakeHandler( + wake: Wake, +) { + return (wakeOptions: WakeOptions) => { + const wakeEffect = Effect.isEffect(wake) + ? (wake as Effect.Effect< + ActionHandlers | WakeFunction, + never, + RX + >) + : Effect.succeed(wake); + + return wakeEffect.pipe( + Effect.flatMap((resolvedWake) => { + if (typeof resolvedWake === "function") { + const actionHandlers = resolvedWake(wakeOptions); + return Effect.isEffect(actionHandlers) + ? actionHandlers + : Effect.succeed(actionHandlers); + } + + return Effect.succeed(resolvedWake); + }), + ); + }; +} + const makeRivetkitActor = Effect.fnUntraced(function* < Name extends string, Actions extends Action.AnyWithProps, From 3ad33301a0919c16842ba7d2792a37168f01a890 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 18 May 2026 17:30:06 +0200 Subject: [PATCH 212/306] refactor(effect): remove RawRivetkitContext service The raw rivetkit context is now accessible via wakeOptions.rawRivetkitContext, making the Context service redundant. --- examples/effect/src/actors/chat-room/live.ts | 6 ++---- rivetkit-typescript/packages/effect/src/Actor.ts | 8 +------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts index 819fdc262d..8da3b87d0b 100644 --- a/examples/effect/src/actors/chat-room/live.ts +++ b/examples/effect/src/actors/chat-room/live.ts @@ -29,12 +29,10 @@ const ChatRoomState = ActorState.make("ChatRoomState", { }); export const ChatRoomLive = ChatRoom.toLayer( + (wakeOptions) => Effect.gen(function* () { const state = yield* ChatRoomState; - // RawRivetkitContext is the escape hatch for features that the - // Effect SDK has not modeled yet, including broadcasts, scheduling, - // destroy, SQLite access, and server-side actor clients. - const ctx = yield* Actor.RawRivetkitContext; + const ctx = wakeOptions.rawRivetkitContext; const database = ctx.db as RawAccess; const address = yield* Actor.CurrentAddress; // The plain SDK example stores this in createVars. The Effect SDK diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 1f41d80d33..ec454167de 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -91,11 +91,6 @@ export class Sleep extends Context.Service>()( "@rivetkit/effect/Actor/Sleep", ) {} -export class RawRivetkitContext extends Context.Service< - RawRivetkitContext, - Rivetkit.RunContextOf ->()("@rivetkit/effect/Actor/RawRivetkitContext") {} - export type ActionRequest = A extends Action.Action< infer Tag, @@ -165,7 +160,7 @@ type ExcludeBuiltInWakeServices< > = UnknownToNever< Exclude< T, - Scope.Scope | CurrentAddress | Sleep | RawRivetkitContext | State + Scope.Scope | CurrentAddress | Sleep | State > >; @@ -433,7 +428,6 @@ const makeRivetkitActor = Effect.fnUntraced(function* < Sleep, Effect.sync(() => c.sleep()), ), - Context.make(RawRivetkitContext, c), effectOptions.state ? Context.make( effectOptions.state, From b9e3748a5cc89ef383095ea4a2fd3134ea83181d Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 19 May 2026 11:21:07 +0200 Subject: [PATCH 213/306] Add type tests for concrete wake context --- .../packages/effect/src/Actor.test-d.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts index 84d0f1f4d4..f9210e1b34 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -1,7 +1,11 @@ import { Context, Effect, type Layer } from "effect"; +import { Schema } from "effect"; +import type { RawAccess } from "rivetkit/db"; +import { db } from "rivetkit/db"; import { describe, expectTypeOf, test } from "vitest"; import * as Action from "./Action"; import * as Actor from "./Actor"; +import * as ActorState from "./ActorState"; import type * as Client from "./Client"; class SomeDep extends Context.Service()( @@ -14,6 +18,13 @@ const TestActor = Actor.make("TestActor", { type TestActions = (typeof TestActor.actions)[number]; +const TestState = ActorState.make("TestState", { + schema: Schema.Struct({ + count: Schema.Number, + }), + initialValue: () => ({ count: 0 }), +}); + describe("Actor.make", () => { test("preserves the name literal", () => { expectTypeOf(TestActor.name).toEqualTypeOf<"TestActor">(); @@ -47,6 +58,36 @@ describe("Actor.make(...).toLayer", () => { })); }); + test("wake options carry the configured state type", () => { + TestActor.toLayer( + (wakeOptions) => { + expectTypeOf( + wakeOptions.rawRivetkitContext.state, + ).toEqualTypeOf<{ readonly count: number }>(); + + return { + GetContext: () => Effect.void, + }; + }, + { state: TestState }, + ); + }); + + test("wake options carry the configured database client type", () => { + TestActor.toLayer( + (wakeOptions) => { + expectTypeOf( + wakeOptions.rawRivetkitContext.db, + ).toEqualTypeOf(); + + return { + GetContext: () => Effect.void, + }; + }, + { db: db() }, + ); + }); + test("accepts a function returning an Effect of action handlers", () => { expectTypeOf(TestActor.toLayer).toBeCallableWith((_wakeOptions) => Effect.gen(function* () { From 089476fdf40826d3ab72bacb9ff70231547d0c20 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 19 May 2026 12:45:13 +0200 Subject: [PATCH 214/306] test(effect): transformed raw state handling in actors --- .../packages/effect/src/Actor.test-d.ts | 73 +++++++++++- .../packages/effect/test/e2e.test.ts | 105 +++++++++++++++++- .../packages/effect/test/fixtures/actors.ts | 96 +++++++++++++++- 3 files changed, 270 insertions(+), 4 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts index f9210e1b34..808344b406 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -1,5 +1,10 @@ -import { Context, Effect, type Layer } from "effect"; -import { Schema } from "effect"; +import { + Context, + Effect, + type Layer, + Schema, + SchemaTransformation, +} from "effect"; import type { RawAccess } from "rivetkit/db"; import { db } from "rivetkit/db"; import { describe, expectTypeOf, test } from "vitest"; @@ -25,6 +30,45 @@ const TestState = ActorState.make("TestState", { initialValue: () => ({ count: 0 }), }); +const TagsCsv = Schema.String.pipe( + Schema.decodeTo( + Schema.Array(Schema.String), + SchemaTransformation.transform({ + decode: (s: string): ReadonlyArray => s.split(","), + encode: (arr: ReadonlyArray) => arr.join(","), + }), + ), +); + +const TransformedState = ActorState.make("TransformedState", { + schema: Schema.Struct({ + when: Schema.DateFromString, + url: Schema.URLFromString, + id: Schema.BigIntFromString, + bytes: Schema.Uint8ArrayFromBase64, + tags: TagsCsv, + history: Schema.Array( + Schema.Struct({ + at: Schema.DateFromString, + payload: Schema.Uint8ArrayFromBase64, + }), + ), + }), + initialValue: () => ({ + when: new Date("2024-01-15T10:30:00.000Z"), + url: new URL("https://rivet.dev/docs"), + id: 1n, + bytes: new Uint8Array([1, 2, 3]), + tags: ["alpha", "beta"], + history: [ + { + at: new Date("2024-01-15T10:30:00.000Z"), + payload: new Uint8Array([4, 5, 6]), + }, + ], + }), +}); + describe("Actor.make", () => { test("preserves the name literal", () => { expectTypeOf(TestActor.name).toEqualTypeOf<"TestActor">(); @@ -73,6 +117,31 @@ describe("Actor.make(...).toLayer", () => { ); }); + test("wake options carry the encoded state type for transformed schemas", () => { + TestActor.toLayer( + (wakeOptions) => { + expectTypeOf( + wakeOptions.rawRivetkitContext.state, + ).toEqualTypeOf<{ + readonly when: string; + readonly url: string; + readonly id: string; + readonly bytes: string; + readonly tags: string; + readonly history: ReadonlyArray<{ + readonly at: string; + readonly payload: string; + }>; + }>(); + + return { + GetContext: () => Effect.void, + }; + }, + { state: TransformedState }, + ); + }); + test("wake options carry the configured database client type", () => { TestActor.toLayer( (wakeOptions) => { diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index fe4f83f3ee..e9775bf33f 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -1,7 +1,7 @@ import { assert, layer } from "@effect/vitest"; +import { Registry, RivetError } from "@rivetkit/effect"; import { Effect, Layer, Schedule } from "effect"; import { TestClock } from "effect/testing"; -import { Registry, RivetError } from "@rivetkit/effect"; import { inject } from "vitest"; import { BuildSetRejected, @@ -19,6 +19,8 @@ import { ScaledOverflowError, Strict, StrictLive, + TransformedStateActor, + TransformedStateActorLive, Unregistered, WakeDecodeFail, WakeDecodeFailLive, @@ -73,6 +75,7 @@ const TestLayer = ReadyForEnvoy.pipe( StrictLive, WakeDecodeFailLive, BuildSetRejectedLive, + TransformedStateActorLive, ), ), Layer.provideMerge(Flags.layer), @@ -367,6 +370,106 @@ layer(TestLayer)("end-to-end", (it) => { }), ); + it.effect( + "exposes transformed actor state as encoded raw wake context state", + () => + Effect.gen(function* () { + const actor = (yield* TransformedStateActor.client).getOrCreate( + ["t-raw-transformed-state"], + ); + const when = new Date("2024-04-05T06:07:08.000Z"); + const at = new Date("2024-04-06T07:08:09.000Z"); + const bytes = new Uint8Array([9, 8, 7]); + const payload = new Uint8Array([6, 5, 4]); + const url = new URL( + "https://rivet.dev/docs/actors?section=state", + ); + const id = 9_007_199_254_740_993n; + + yield* actor.SetTransformedStateAndSleep({ + when, + url, + id, + bytes, + tags: ["alpha", "beta", "gamma"], + history: [{ at, payload }], + }); + + const raw = yield* actor.GetRawWakeState().pipe( + Effect.repeat({ + until: (state) => state.id === id.toString(), + schedule: Schedule.spaced("100 millis"), + }), + TestClock.withLive, + ); + + assert.deepEqual(raw, { + when: when.toISOString(), + url: url.toString(), + id: id.toString(), + bytes: Buffer.from(bytes).toString("base64"), + tags: "alpha,beta,gamma", + history: [ + { + at: at.toISOString(), + payload: Buffer.from(payload).toString("base64"), + }, + ], + }); + }), + ); + + it.effect( + "decodes transformed state written through raw wake context state", + () => + Effect.gen(function* () { + const actor = (yield* TransformedStateActor.client).getOrCreate( + ["t-raw-set-transformed-state"], + ); + const when = "2024-05-06T07:08:09.000Z"; + const at = "2024-05-07T08:09:10.000Z"; + const url = "https://rivet.dev/docs/actors/state?source=raw"; + const id = "9007199254740995"; + const bytes = Buffer.from(new Uint8Array([1, 3, 5])).toString( + "base64", + ); + const payload = Buffer.from(new Uint8Array([2, 4, 6])).toString( + "base64", + ); + + yield* actor.SetRawWakeStateAndSleep({ + when, + url, + id, + bytes, + tags: "raw,encoded,state", + history: [{ at, payload }], + }); + + const decoded = yield* actor.GetDecodedState().pipe( + Effect.repeat({ + until: (state) => state.id === BigInt(id), + schedule: Schedule.spaced("100 millis"), + }), + TestClock.withLive, + ); + + assert.strictEqual(decoded.when.toISOString(), when); + assert.strictEqual(decoded.url.toString(), url); + assert.strictEqual(decoded.id, BigInt(id)); + assert.deepEqual( + Array.from(decoded.bytes), + Array.from(Buffer.from(bytes, "base64")), + ); + assert.deepEqual(decoded.tags, ["raw", "encoded", "state"]); + assert.strictEqual(decoded.history[0]?.at.toISOString(), at); + assert.deepEqual( + Array.from(decoded.history[0]?.payload ?? []), + Array.from(Buffer.from(payload, "base64")), + ); + }), + ); + it.effect("resolves a non-built-in service", () => Effect.gen(function* () { const counter = (yield* Counter.client).getOrCreate([ diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts index 3ea348e1b9..e82fdd78b9 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts @@ -1,3 +1,4 @@ +import { Action, Actor, ActorState, State } from "@rivetkit/effect"; import { Context, Effect, @@ -8,7 +9,6 @@ import { SchemaIssue, SchemaTransformation, } from "effect"; -import { Action, Actor, ActorState, State } from "@rivetkit/effect"; import { db, type RawAccess } from "rivetkit/db"; // --- Counter --- @@ -180,6 +180,100 @@ export const CountEvents = Action.make("CountEvents", { success: Schema.Number, }); +const EncodedTransformedState = Schema.Struct({ + when: Schema.String, + url: Schema.String, + id: Schema.String, + bytes: Schema.String, + tags: Schema.String, + history: Schema.Array( + Schema.Struct({ + at: Schema.String, + payload: Schema.String, + }), + ), +}); + +const TransformedStateShape = Schema.Struct({ + when: Schema.DateFromString, + url: Schema.URLFromString, + id: Schema.BigIntFromString, + bytes: Schema.Uint8ArrayFromBase64, + tags: TagsCsv, + history: Schema.Array( + Schema.Struct({ + at: Schema.DateFromString, + payload: Schema.Uint8ArrayFromBase64, + }), + ), +}); + +export const GetRawWakeState = Action.make("GetRawWakeState", { + success: EncodedTransformedState, +}); + +export const GetDecodedState = Action.make("GetDecodedState", { + success: TransformedStateShape, +}); + +export const SetTransformedStateAndSleep = Action.make( + "SetTransformedStateAndSleep", + { + payload: TransformedStateShape, + }, +); + +export const SetRawWakeStateAndSleep = Action.make("SetRawWakeStateAndSleep", { + payload: EncodedTransformedState, +}); + +export const TransformedStateActor = Actor.make("TransformedStateActor", { + actions: [ + GetRawWakeState, + GetDecodedState, + SetTransformedStateAndSleep, + SetRawWakeStateAndSleep, + ], +}); + +const TransformedActorState = ActorState.make("TransformedActorState", { + schema: TransformedStateShape, + initialValue: () => ({ + when: new Date("2024-01-01T00:00:00.000Z"), + url: new URL("https://rivet.dev/docs"), + id: 1n, + bytes: new Uint8Array([1, 2, 3]), + tags: ["initial"], + history: [], + }), +}); + +export const TransformedStateActorLive = TransformedStateActor.toLayer( + (wakeOptions) => + Effect.gen(function* () { + const state = yield* TransformedActorState; + const sleep = yield* Actor.Sleep; + const rawRivetkitContext = wakeOptions.rawRivetkitContext; + const rawWakeState = rawRivetkitContext.state; + + return TransformedStateActor.of({ + GetRawWakeState: () => Effect.succeed(rawWakeState), + GetDecodedState: () => State.get(state), + SetTransformedStateAndSleep: ({ payload }) => + State.set(state, payload).pipe(Effect.andThen(sleep)), + SetRawWakeStateAndSleep: ({ payload }) => + Effect.tryPromise(async () => { + rawRivetkitContext.state = payload; + await rawRivetkitContext.saveState({ + immediate: true, + }); + rawRivetkitContext.sleep(); + }).pipe(Effect.orDie), + }); + }), + { state: TransformedActorState }, +); + export const Counter = Actor.make("Counter", { actions: [ Increment, From 48030301535ed4876b0070a5145db4c1103a7b3f Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 19 May 2026 12:46:20 +0200 Subject: [PATCH 215/306] fix(effect): type wake raw context state --- .../packages/effect/src/Actor.ts | 259 +++++++++++++----- 1 file changed, 185 insertions(+), 74 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index ec454167de..6bb5de0b1f 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -49,14 +49,19 @@ export type RivetkitActorOptions = Pick< * `RivetkitActorOptions` (forwarded verbatim to `Rivetkit.actor`) * with the effect-SDK-only options. */ -export type Options = - Readonly & { - readonly state?: State; - readonly db?: RivetkitDb.AnyDatabaseProvider; - }; +export type Options< + State extends ActorState.AnyWithProps, + Database extends RivetkitDb.AnyDatabaseProvider = undefined, +> = Readonly & { + readonly state?: State; + readonly db?: Database; +}; -const splitOptions = ( - options: Options, +const splitOptions = < + State extends ActorState.AnyWithProps, + Database extends RivetkitDb.AnyDatabaseProvider, +>( + options: Options, ) => ({ rivetkitOptions: Struct.pick(options, rivetkitActorOptionsKeys), effectOptions: Struct.omit(options, rivetkitActorOptionsKeys), @@ -113,19 +118,85 @@ type ActionHandlerServices = { : never; }[keyof ActionHandlers]; -export type WakeOptions = { - readonly rawRivetkitContext: Rivetkit.WakeContextOf; +type ActorStateEncoded = + | State["schema"]["Encoded"] + | ([State] extends [never] ? undefined : never); + +type ActorStateDecoded = + State["schema"]["Type"]; + +type ActorStateCodec = { + readonly decode: ( + input: ActorStateEncoded, + ) => Effect.Effect< + ActorStateDecoded, + Schema.SchemaError, + State["schema"]["DecodingServices"] + >; + readonly decodeUnknown: ( + input: unknown, + ) => Effect.Effect< + ActorStateDecoded, + Schema.SchemaError, + State["schema"]["DecodingServices"] + >; + readonly encode: ( + input: ActorStateDecoded, + ) => Effect.Effect< + ActorStateEncoded, + Schema.SchemaError, + State["schema"]["EncodingServices"] + >; }; -type WakeFunction = - | ((wakeOptions: WakeOptions) => ActionHandlers) - | ((wakeOptions: WakeOptions) => Effect.Effect); +const makeActorStateCodec = ( + state: State, +): ActorStateCodec => { + const schema = state.schema as State["schema"]; -type Wake = + return { + decode: Schema.decodeEffect(schema), + decodeUnknown: Schema.decodeUnknownEffect(schema), + encode: Schema.encodeEffect(schema), + }; +}; + +type RivetkitActorDefinitionFor< + State extends ActorState.AnyWithProps, + Database extends RivetkitDb.AnyDatabaseProvider, +> = Rivetkit.ActorDefinition< + ActorStateEncoded, + undefined, + undefined, + undefined, + undefined, + Database, + Record, + Record, + any +>; + +export type WakeOptions< + ActorDefinition extends + Rivetkit.AnyActorDefinition = Rivetkit.AnyActorDefinition, +> = { + readonly rawRivetkitContext: Rivetkit.WakeContextOf; +}; + +type WakeOptionsFor< + State extends ActorState.AnyWithProps, + Database extends RivetkitDb.AnyDatabaseProvider, +> = WakeOptions>; + +type WakeFunction = + | ((wakeOptions: W) => ActionHandlers) + | ((wakeOptions: W) => Effect.Effect); + +type Wake = | ActionHandlers | Effect.Effect - | WakeFunction - | Effect.Effect, never, RX>; + | WakeFunction + | Effect.Effect, never, RX>; export type AccessorKeyParam = string | Rivetkit.ActorKey; @@ -157,12 +228,7 @@ type UnknownToNever = unknown extends T ? never : T; type ExcludeBuiltInWakeServices< T, State extends ActorState.AnyWithProps, -> = UnknownToNever< - Exclude< - T, - Scope.Scope | CurrentAddress | Sleep | State - > ->; +> = UnknownToNever>; type ToLayerRequirements< Actions extends Action.Any, @@ -197,11 +263,12 @@ export interface Actor< toLayer< ActionHandlers extends ActionHandlersFrom, State extends ActorState.AnyWithProps = never, + Database extends RivetkitDb.AnyDatabaseProvider = undefined, R = never, RX = never, >( - wake: Wake, - options?: Options, + wake: Wake>, + options?: Options, ): Layer.Layer< never, never, @@ -231,16 +298,22 @@ const Proto: Omit, "name" | "actions"> = { Actions extends Action.AnyWithProps, ActionHandlers extends ActionHandlersFrom, State extends ActorState.AnyWithProps = never, + Database extends RivetkitDb.AnyDatabaseProvider = undefined, R = never, RX = never, >( this: Actor, - wake: Wake, - options: Options = {}, + wake: Wake>, + options: Options = {}, ) { return makeRivetkitActor({ actor: this, - wakeHandler: toWakeHandler(wake), + wakeHandler: toWakeHandler< + ActionHandlers, + R, + RX, + WakeOptionsFor + >(wake), options, }).pipe( Effect.flatMap((rivetKitActor) => @@ -284,42 +357,69 @@ export const make = < return self; }; -export function toWakeHandler( - wake: Effect.Effect< - (wakeOptions: WakeOptions) => Effect.Effect, - never, - RX - >, -): (wakeOptions: WakeOptions) => Effect.Effect; -export function toWakeHandler( +export function toWakeHandler< + ActionHandlers extends object, + R, + RX, + W extends WakeOptions = WakeOptions, +>( wake: Effect.Effect< - (wakeOptions: WakeOptions) => ActionHandlers, + (wakeOptions: W) => Effect.Effect, never, RX >, -): (wakeOptions: WakeOptions) => Effect.Effect; -export function toWakeHandler( - wake: (wakeOptions: WakeOptions) => Effect.Effect, -): (wakeOptions: WakeOptions) => Effect.Effect; -export function toWakeHandler( - wake: (wakeOptions: WakeOptions) => ActionHandlers, -): (wakeOptions: WakeOptions) => Effect.Effect; -export function toWakeHandler( +): (wakeOptions: W) => Effect.Effect; +export function toWakeHandler< + ActionHandlers extends object, + RX, + W extends WakeOptions = WakeOptions, +>( + wake: Effect.Effect<(wakeOptions: W) => ActionHandlers, never, RX>, +): (wakeOptions: W) => Effect.Effect; +export function toWakeHandler< + ActionHandlers extends object, + R, + W extends WakeOptions = WakeOptions, +>( + wake: (wakeOptions: W) => Effect.Effect, +): (wakeOptions: W) => Effect.Effect; +export function toWakeHandler< + ActionHandlers extends object, + W extends WakeOptions = WakeOptions, +>( + wake: (wakeOptions: W) => ActionHandlers, +): (wakeOptions: W) => Effect.Effect; +export function toWakeHandler< + ActionHandlers extends object, + RX, + W extends WakeOptions = WakeOptions, +>( wake: Effect.Effect, -): (wakeOptions: WakeOptions) => Effect.Effect; -export function toWakeHandler( +): (wakeOptions: W) => Effect.Effect; +export function toWakeHandler< + ActionHandlers extends object, + W extends WakeOptions = WakeOptions, +>( wake: ActionHandlers, -): (wakeOptions: WakeOptions) => Effect.Effect; -export function toWakeHandler( - wake: Wake, -): (wakeOptions: WakeOptions) => Effect.Effect; -export function toWakeHandler( - wake: Wake, -) { - return (wakeOptions: WakeOptions) => { +): (wakeOptions: W) => Effect.Effect; +export function toWakeHandler< + ActionHandlers extends object, + R, + RX, + W extends WakeOptions = WakeOptions, +>( + wake: Wake, +): (wakeOptions: W) => Effect.Effect; +export function toWakeHandler< + ActionHandlers extends object, + R, + RX, + W extends WakeOptions = WakeOptions, +>(wake: Wake) { + return (wakeOptions: W) => { const wakeEffect = Effect.isEffect(wake) ? (wake as Effect.Effect< - ActionHandlers | WakeFunction, + ActionHandlers | WakeFunction, never, RX >) @@ -346,16 +446,17 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ActionHandlers extends ActionHandlersFrom, RX, State extends ActorState.AnyWithProps = never, + Database extends RivetkitDb.AnyDatabaseProvider = undefined, >({ actor, wakeHandler, options, }: { readonly actor: Actor; - readonly wakeHandler: (wakeOptions: { - rawRivetkitContext: Rivetkit.WakeContextOf; - }) => Effect.Effect; - readonly options: Options; + readonly wakeHandler: ( + wakeOptions: WakeOptionsFor, + ) => Effect.Effect; + readonly options: Options; }) { // Snapshot the current Effect context so action callbacks // (which run in rivetkit's plain Promise world) can run @@ -364,10 +465,10 @@ const makeRivetkitActor = Effect.fnUntraced(function* < const services = yield* Effect.context(); const { effectOptions, rivetkitOptions } = splitOptions(options); - const stateCodec = UndefinedOr.map(effectOptions.state, (state) => ({ - decode: Schema.decodeUnknownEffect(state.schema), - encode: Schema.encodeUnknownEffect(state.schema), - })); + const stateCodec = UndefinedOr.map( + effectOptions.state, + makeActorStateCodec, + ); const instances = MutableHashMap.empty< string, @@ -375,15 +476,15 @@ const makeRivetkitActor = Effect.fnUntraced(function* < readonly actionHandlers: ActionHandlers; readonly scope: Scope.Closeable; readonly state?: State.State< - State["schema"]["Type"], + ActorStateDecoded, Schema.SchemaError >; } >(); - const onWake = async ( - c: Rivetkit.WakeContextOf, - ) => { + type RivetkitDefinition = RivetkitActorDefinitionFor; + + const onWake = async (c: Rivetkit.WakeContextOf) => { await Effect.runPromiseWith(services)( Effect.gen(function* () { const scope = yield* Scope.make(); @@ -436,7 +537,9 @@ const makeRivetkitActor = Effect.fnUntraced(function* < : Context.empty(), ); - const wakeOptions = { rawRivetkitContext: c }; + const wakeOptions: WakeOptionsFor = { + rawRivetkitContext: c, + }; const actionHandlers = yield* wakeHandler(wakeOptions).pipe( Effect.provide(context), ); @@ -466,7 +569,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < return [ action._tag, async ( - c: Rivetkit.ActionContextOf, + c: Rivetkit.ActionContextOf, payload: Action.Payload, meta?: Client.ActionMeta, // TODO: Find better type ) => { @@ -554,7 +657,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < }); const onStateChange = ( - c: Rivetkit.WakeContextOf, + c: Rivetkit.WakeContextOf, newState: unknown, ) => { void Effect.runForkWith(services)( @@ -574,7 +677,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < state.semaphore, Effect.gen(function* () { const decoded = yield* stateCodec - .decode(newState) + .decodeUnknown(newState) .pipe(Effect.orDie); State.publishUnsafe(state, decoded); }), @@ -583,9 +686,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ); }; - const onSleep = async ( - c: Rivetkit.SleepContextOf, - ) => { + const onSleep = async (c: Rivetkit.SleepContextOf) => { await Effect.runPromiseWith(services)( Effect.gen(function* () { const instance = yield* MutableHashMap.get( @@ -600,7 +701,17 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ); }; - return Rivetkit.actor({ + return Rivetkit.actor< + ActorStateEncoded, + undefined, + undefined, + undefined, + undefined, + Database, + Record, + Record, + any + >({ options: rivetkitOptions, ...(effectOptions.db ? { db: effectOptions.db } : {}), onWake, From f5f47659108b0d4ffb8c5feffa2fedd2720e9fdf Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 19 May 2026 13:10:34 +0200 Subject: [PATCH 216/306] Switch effect wake handlers to wakeOptions.state --- .../packages/effect/src/Actor.test-d.ts | 46 +++++++++- .../packages/effect/src/Actor.test.ts | 50 +++++++++- .../packages/effect/test/e2e.test.ts | 6 +- .../packages/effect/test/fixtures/actors.ts | 91 ++++++++++--------- 4 files changed, 140 insertions(+), 53 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts index 808344b406..20f0f23567 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -12,6 +12,7 @@ import * as Action from "./Action"; import * as Actor from "./Actor"; import * as ActorState from "./ActorState"; import type * as Client from "./Client"; +import type * as State from "./State"; class SomeDep extends Context.Service()( "SomeDep", @@ -103,6 +104,49 @@ describe("Actor.make(...).toLayer", () => { }); test("wake options carry the configured state type", () => { + TestActor.toLayer( + (wakeOptions) => { + expectTypeOf(wakeOptions.state).toEqualTypeOf< + State.State<{ readonly count: number }, Schema.SchemaError> + >(); + + return { + GetContext: () => Effect.void, + }; + }, + { state: TestState }, + ); + }); + + test("wake options carry the transformed state type", () => { + TestActor.toLayer( + (wakeOptions) => { + expectTypeOf(wakeOptions.state).toEqualTypeOf< + State.State< + { + readonly when: Date; + readonly url: URL; + readonly id: bigint; + readonly bytes: Uint8Array; + readonly tags: ReadonlyArray; + readonly history: ReadonlyArray<{ + readonly at: Date; + readonly payload: Uint8Array; + }>; + }, + Schema.SchemaError + > + >(); + + return { + GetContext: () => Effect.void, + }; + }, + { state: TransformedState }, + ); + }); + + test("wake options carry the raw RivetKit context with the encoded configured state type", () => { TestActor.toLayer( (wakeOptions) => { expectTypeOf( @@ -117,7 +161,7 @@ describe("Actor.make(...).toLayer", () => { ); }); - test("wake options carry the encoded state type for transformed schemas", () => { + test("wake options carry the raw RivetKit context with the encoded transformed state type", () => { TestActor.toLayer( (wakeOptions) => { expectTypeOf( diff --git a/rivetkit-typescript/packages/effect/src/Actor.test.ts b/rivetkit-typescript/packages/effect/src/Actor.test.ts index bcf18b5b22..b851ba33ed 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test.ts @@ -2,6 +2,7 @@ import { assert, describe, it } from "@effect/vitest"; import { Context, Effect, Layer } from "effect"; import type * as Rivetkit from "rivetkit"; import * as Actor from "./Actor"; +import * as State from "./State"; class Prefix extends Context.Service()( "Actor.test/Prefix", @@ -35,12 +36,13 @@ describe("Actor.toWakeHandler", () => { }).pipe(Effect.provide(PrefixLive)), ); - it.effect("calls a wake function with the wake context", () => + it.effect("calls a wake function with wake options", () => Effect.gen(function* () { + const rawRivetkitContext = { + key: ["room", "1"], + } as Rivetkit.WakeContextOf; const wakeOptions: Actor.WakeOptions = { - rawRivetkitContext: { - key: ["room", "1"], - } as Rivetkit.WakeContextOf, + rawRivetkitContext, }; const wake = (wakeOptions: Actor.WakeOptions) => ({ GetKey: () => @@ -51,10 +53,50 @@ describe("Actor.toWakeHandler", () => { const wakeHandler = Actor.toWakeHandler(wake); const actionHandlers = yield* wakeHandler(wakeOptions); + assert.strictEqual(wakeOptions.rawRivetkitContext, rawRivetkitContext); assert.strictEqual(yield* actionHandlers.GetKey(), "room/1"); }), ); + it.effect("passes actor state through wake options", () => + Effect.gen(function* () { + const cell = { value: { count: 1 } }; + const state = yield* State.make( + () => Effect.sync(() => cell.value), + (value: { readonly count: number }) => + Effect.sync(() => { + cell.value = value; + }), + ); + type StatefulWakeOptions = Actor.WakeOptions & { + readonly state: State.State< + { readonly count: number }, + never, + never + >; + }; + const wakeOptions: StatefulWakeOptions = { + rawRivetkitContext: + {} as Rivetkit.WakeContextOf, + state, + }; + const wake = (wakeOptions: StatefulWakeOptions) => ({ + GetCount: () => State.get(wakeOptions.state), + SetCount: (count: number) => + State.set(wakeOptions.state, { count }), + }); + const wakeHandler = Actor.toWakeHandler(wake); + const actionHandlers = yield* wakeHandler(wakeOptions); + + assert.deepStrictEqual(yield* actionHandlers.GetCount(), { + count: 1, + }); + + yield* actionHandlers.SetCount(7); + assert.deepStrictEqual(cell.value, { count: 7 }); + }), + ); + it.effect("flattens a wake function returning an Effect", () => Effect.gen(function* () { const wakeOptions: Actor.WakeOptions = { diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index e9775bf33f..b1bb9b6fe7 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -420,7 +420,7 @@ layer(TestLayer)("end-to-end", (it) => { ); it.effect( - "decodes transformed state written through raw wake context state", + "wake options state decodes transformed state written through raw wake context state", () => Effect.gen(function* () { const actor = (yield* TransformedStateActor.client).getOrCreate( @@ -476,7 +476,7 @@ layer(TestLayer)("end-to-end", (it) => { "t-service-wake", ]); // `WakeGreeting` returns the string captured when `Greeter` - // was yielded inside the wake-scope build effect. + // was resolved inside the wake-scope build effect. const greeting = yield* counter.WakeGreeting(); assert.strictEqual(greeting, "Hello, on wake!"); }), @@ -592,7 +592,7 @@ layer(TestLayer)("end-to-end", (it) => { ); it.effect( - "State.make initial-read decode failure inside build effect surfaces as RivetError", + "wake options state decode failure inside build effect surfaces as RivetError", () => Effect.gen(function* () { const failing = (yield* WakeDecodeFail.client).getOrCreate([ diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts index e82fdd78b9..42660b86fb 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts @@ -251,7 +251,7 @@ const TransformedActorState = ActorState.make("TransformedActorState", { export const TransformedStateActorLive = TransformedStateActor.toLayer( (wakeOptions) => Effect.gen(function* () { - const state = yield* TransformedActorState; + const state = wakeOptions.state; const sleep = yield* Actor.Sleep; const rawRivetkitContext = wakeOptions.rawRivetkitContext; const rawWakeState = rawRivetkitContext.state; @@ -319,7 +319,7 @@ const CounterState = ActorState.make("CounterState", { export const CounterLive = Counter.toLayer( (wakeOptions) => Effect.gen(function* () { - const state = yield* CounterState; + const state = wakeOptions.state; const count = yield* Ref.make(0); const flags = yield* Flags; flags.set("on wake", true); @@ -532,21 +532,22 @@ export const Strict = Actor.make("Strict", { }); export const StrictLive = Strict.toLayer( - Effect.gen(function* () { - const state = yield* StrictState; - return Strict.of({ - StrictSet: ({ payload }) => - State.set(state, payload.value).pipe( - Effect.match({ - onFailure: () => "rejected" as const, - onSuccess: () => "ok" as const, - }), - ), - StrictSetUnhandled: ({ payload }) => - State.set(state, payload.value).pipe(Effect.as(payload.value)), - StrictGet: () => State.get(state), - }); - }), + (wakeOptions) => + Effect.gen(function* () { + const state = wakeOptions.state; + return Strict.of({ + StrictSet: ({ payload }) => + State.set(state, payload.value).pipe( + Effect.match({ + onFailure: () => "rejected" as const, + onSuccess: () => "ok" as const, + }), + ), + StrictSetUnhandled: ({ payload }) => + State.set(state, payload.value).pipe(Effect.as(payload.value)), + StrictGet: () => State.get(state), + }); + }), { state: StrictState }, ); @@ -585,8 +586,8 @@ export const Unregistered = Actor.make("Unregistered", { actions: [Echo] }); // --- WakeDecodeFail --- // Schema whose encode is permissive (identity) but whose decode rejects -// negatives. Used to plant an "invalid" value into `c.state` that the -// next wake's `State.make` initial-read decode will reject. +// negatives. Used to seed invalid persisted actor state so +// `wakeOptions.state` construction rejects on first wake. const PermissiveEncodeStrictDecode = Schema.Number.pipe( Schema.decodeTo( Schema.Number, @@ -607,8 +608,7 @@ const PermissiveEncodeStrictDecode = Schema.Number.pipe( const WakeDecodeFailState = ActorState.make("WakeDecodeFailState", { schema: PermissiveEncodeStrictDecode, // `-1` encodes successfully (encode is identity) so registry setup - // passes; but the wake-time decode rejects it, so State.make's - // initial read inside the build effect dies. + // passes, but the wake-time decode rejects before handlers are built. initialValue: () => -1, }); @@ -617,23 +617,22 @@ export const WakeDecodeFail = Actor.make("WakeDecodeFail", { }); export const WakeDecodeFailLive = WakeDecodeFail.toLayer( - Effect.gen(function* () { - const _state = yield* WakeDecodeFailState; - return WakeDecodeFail.of({ - Ping: () => Effect.succeed("never reached"), - }); - }), + (wakeOptions) => + Effect.gen(function* () { + const _state = wakeOptions.state; + return WakeDecodeFail.of({ + Ping: () => Effect.succeed("never reached"), + }); + }), { state: WakeDecodeFailState }, ); // --- BuildSetRejected --- -// Strict schema rejecting negatives on encode. The build effect below -// deliberately calls `State.set` with a value the schema rejects, -// catches the resulting `SchemaError` via `Effect.match`, and exposes -// the outcome via `BuildOutcome`. Demonstrates that the new typed-`E` -// channel on `State` is observable inside the wake-scope build effect, -// not just inside action handlers. +// Strict schema rejecting negatives on encode. The build effect deliberately +// calls `State.set` against `wakeOptions.state` with a value the schema +// rejects, catches the resulting `SchemaError` via `Effect.match`, and +// exposes the outcome via `BuildOutcome`. const StrictForBuildState = ActorState.make("StrictForBuildState", { schema: Schema.Number.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))), initialValue: () => 0, @@ -648,17 +647,19 @@ export const BuildSetRejected = Actor.make("BuildSetRejected", { }); export const BuildSetRejectedLive = BuildSetRejected.toLayer( - Effect.gen(function* () { - const state = yield* StrictForBuildState; - const wrote = yield* State.set(state, -1).pipe( - Effect.match({ - onFailure: () => false, - onSuccess: () => true, - }), - ); - return BuildSetRejected.of({ - BuildOutcome: () => Effect.succeed(wrote ? "wrote" : "rejected"), - }); - }), + (wakeOptions) => + Effect.gen(function* () { + const state = wakeOptions.state; + const wrote = yield* State.set(state, -1).pipe( + Effect.match({ + onFailure: () => false, + onSuccess: () => true, + }), + ); + return BuildSetRejected.of({ + BuildOutcome: () => + Effect.succeed(wrote ? "wrote" : "rejected"), + }); + }), { state: StrictForBuildState }, ); From 14a9eb2ee701bd887470dfbb4ecf7685e02b41d4 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 19 May 2026 14:23:00 +0200 Subject: [PATCH 217/306] Reorder toLayer wake and options parameters --- examples/effect/src/actors/chat-room/live.ts | 331 +++++++++--------- examples/effect/src/actors/counter/api.ts | 2 +- examples/effect/src/actors/counter/live.ts | 10 +- examples/effect/src/actors/directory/live.ts | 66 ++-- examples/effect/src/actors/moderator/live.ts | 64 ++-- .../packages/effect/src/Actor.test-d.ts | 24 +- .../packages/effect/src/Actor.ts | 63 +++- .../packages/effect/src/ActorState.ts | 24 +- .../packages/effect/test/fixtures/actors.ts | 29 +- 9 files changed, 343 insertions(+), 270 deletions(-) diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts index 8da3b87d0b..76d6fe54bd 100644 --- a/examples/effect/src/actors/chat-room/live.ts +++ b/examples/effect/src/actors/chat-room/live.ts @@ -29,180 +29,195 @@ const ChatRoomState = ActorState.make("ChatRoomState", { }); export const ChatRoomLive = ChatRoom.toLayer( - (wakeOptions) => - Effect.gen(function* () { - const state = yield* ChatRoomState; - const ctx = wakeOptions.rawRivetkitContext; - const database = ctx.db as RawAccess; - const address = yield* Actor.CurrentAddress; - // The plain SDK example stores this in createVars. The Effect SDK - // does not expose vars yet, so the wake-scope closure owns it. - const sessionId = crypto.randomUUID(); + ({ rawRivetkitContext, state }) => + Effect.gen(function* () { + const database = rawRivetkitContext.db; + const address = yield* Actor.CurrentAddress; + // The plain SDK example stores this in createVars. The Effect SDK + // does not expose vars yet, so the wake-scope closure owns it. + const sessionId = crypto.randomUUID(); - yield* State.update(state, (current) => ({ - ...current, - wakeCount: current.wakeCount + 1, - })).pipe(Effect.orDie); + yield* State.update(state, (current) => ({ + ...current, + wakeCount: current.wakeCount + 1, + })).pipe(Effect.orDie); - yield* Effect.log("room awake", { - actorId: address.actorId, - key: address.key.join("/"), - sessionId, - }); + yield* Effect.log("room awake", { + actorId: address.actorId, + key: address.key.join("/"), + sessionId, + }); - yield* Effect.addFinalizer(() => - Effect.gen(function* () { - const current = yield* State.get(state).pipe(Effect.orDie); - yield* Effect.log("room sleeping", { - actorId: address.actorId, - key: address.key.join("/"), - roomName: current.name, - sessionId, - wakeCount: current.wakeCount, - }); - }), - ); + yield* Effect.addFinalizer(() => + Effect.gen(function* () { + const current = yield* State.get(state).pipe(Effect.orDie); + yield* Effect.log("room sleeping", { + actorId: address.actorId, + key: address.key.join("/"), + roomName: current.name, + sessionId, + wakeCount: current.wakeCount, + }); + }), + ); - const directory = () => - // Server-side Effect actor clients are not available yet. Use the - // raw RivetKit actor client and keep the action shape explicit. - ctx.client().directory.getOrCreate(["main"]); - const moderator = () => - // The normal example uses a typed registry client here. This raw - // client keeps the runtime behavior while giving up type inference. - ctx.client().moderator.getOrCreate(["main"]); + const directory = () => + // Server-side Effect actor clients are not available yet. Use the + // raw RivetKit actor client and keep the action shape explicit. + rawRivetkitContext + .client() + .directory.getOrCreate(["main"]); + const moderator = () => + // The normal example uses a typed registry client here. This raw + // client keeps the runtime behavior while giving up type inference. + rawRivetkitContext + .client() + .moderator.getOrCreate(["main"]); - const roomName = State.get(state).pipe( - Effect.orDie, - Effect.map((s) => s.name), - ); + const roomName = State.get(state).pipe( + Effect.orDie, + Effect.map((s) => s.name), + ); - return ChatRoom.of({ - Initialize: ({ payload }) => - // This replaces createState(input). Callers should initialize - // a room before actions that depend on a persisted room name. - State.update(state, (current) => { - if (current.initialized) return current; - return { - ...current, - name: payload.name, - members: [], - initialized: true, - }; - }), - Join: ({ payload }) => - Effect.gen(function* () { - const member = { name: payload.name, joinedAt: Date.now() }; - const next = yield* State.updateAndGet( - state, - (current) => ({ + return ChatRoom.of({ + Initialize: ({ payload }) => + // This replaces createState(input). Callers should initialize + // a room before actions that depend on a persisted room name. + State.update(state, (current) => { + if (current.initialized) return current; + return { ...current, - members: [...current.members, member], - }), - ); + name: payload.name, + members: [], + initialized: true, + }; + }), + Join: ({ payload }) => + Effect.gen(function* () { + const member = { + name: payload.name, + joinedAt: Date.now(), + }; + const next = yield* State.updateAndGet( + state, + (current) => ({ + ...current, + members: [...current.members, member], + }), + ); - ctx.broadcast("memberJoined", { member }); + rawRivetkitContext.broadcast("memberJoined", { + member, + }); - if (next.name !== "") { - // Directory registration is still actor-to-actor RPC, but - // it uses the Effect action name and object payload. - yield* Effect.tryPromise(() => - directory().RegisterRoom({ name: next.name }), + if (next.name !== "") { + // Directory registration is still actor-to-actor RPC, but + // it uses the Effect action name and object payload. + yield* Effect.tryPromise(() => + directory().RegisterRoom({ name: next.name }), + ).pipe(Effect.orDie); + } + + return member; + }), + Leave: ({ payload }) => + Effect.gen(function* () { + yield* State.update(state, (current) => ({ + ...current, + members: current.members.filter( + (member) => member.name !== payload.name, + ), + })).pipe(Effect.orDie); + rawRivetkitContext.broadcast("memberLeft", { + name: payload.name, + }); + }), + SendMessage: ({ payload }) => + Effect.gen(function* () { + // The normal example sends moderation work through a + // completable queue drained by run(). The Effect SDK does + // not expose queues or run loops yet, so moderation is a + // direct actor RPC and has no queue timeout path. + const verdict = yield* Effect.tryPromise( + () => + moderator().Review({ + text: payload.text, + }) as Promise, ).pipe(Effect.orDie); - } - return member; - }), - Leave: ({ payload }) => - Effect.gen(function* () { - yield* State.update(state, (current) => ({ - ...current, - members: current.members.filter( - (member) => member.name !== payload.name, - ), - })).pipe(Effect.orDie); - ctx.broadcast("memberLeft", { name: payload.name }); - }), - SendMessage: ({ payload }) => - Effect.gen(function* () { - // The normal example sends moderation work through a - // completable queue drained by run(). The Effect SDK does - // not expose queues or run loops yet, so moderation is a - // direct actor RPC and has no queue timeout path. - const verdict = yield* Effect.tryPromise( - () => - moderator().Review({ - text: payload.text, - }) as Promise, - ).pipe(Effect.orDie); + if (!verdict.approved) { + return { ok: false, reason: verdict.reason }; + } - if (!verdict.approved) { - return { ok: false, reason: verdict.reason }; - } + const createdAt = Date.now(); + yield* Effect.tryPromise(() => + database.execute( + "INSERT INTO messages (sender, text, created_at) VALUES (?, ?, ?)", + payload.sender, + payload.text, + createdAt, + ), + ).pipe(Effect.orDie); - const createdAt = Date.now(); - yield* Effect.tryPromise(() => - database.execute( - "INSERT INTO messages (sender, text, created_at) VALUES (?, ?, ?)", - payload.sender, - payload.text, + rawRivetkitContext.broadcast("newMessage", { + sender: payload.sender, + text: payload.text, createdAt, + }); + return { ok: true, createdAt }; + }), + GetHistory: () => + Effect.tryPromise(() => + database.execute<{ + id: number; + sender: string; + text: string; + createdAt: number; + }>( + "SELECT id, sender, text, created_at as createdAt FROM messages ORDER BY id", ), - ).pipe(Effect.orDie); - - ctx.broadcast("newMessage", { - sender: payload.sender, - text: payload.text, - createdAt, - }); - return { ok: true, createdAt }; - }), - GetHistory: () => - Effect.tryPromise(() => - database.execute<{ - id: number; - sender: string; - text: string; - createdAt: number; - }>( - "SELECT id, sender, text, created_at as createdAt FROM messages ORDER BY id", + ).pipe(Effect.orDie), + GetMembers: () => + State.get(state).pipe( + Effect.orDie, + Effect.map((s) => s.members), ), - ).pipe(Effect.orDie), - GetMembers: () => - State.get(state).pipe( - Effect.orDie, - Effect.map((s) => s.members), - ), - ScheduleAnnouncement: ({ payload }) => - Effect.sync(() => { - const firesAt = Date.now() + payload.delayMs; - // The raw scheduler dispatches the Effect action by name - // with the same object payload that a client would send. - ctx.schedule.after(payload.delayMs, "TriggerAnnouncement", { - text: payload.text, - }); - return { firesAt }; - }), - TriggerAnnouncement: ({ payload }) => - Effect.sync(() => { - ctx.broadcast("announcement", { text: payload.text }); - }), - Archive: () => - Effect.gen(function* () { - const name = yield* roomName; - if (name !== "") { - // This only covers destruction through Archive. A future - // Effect onDestroy hook would cover every destroy path. - yield* Effect.tryPromise(() => - directory().CloseRoom({ name }), - ).pipe(Effect.orDie); - } - yield* Effect.sync(() => { - ctx.destroy(); - }); - }), - }); - }), + ScheduleAnnouncement: ({ payload }) => + Effect.sync(() => { + const firesAt = Date.now() + payload.delayMs; + // The raw scheduler dispatches the Effect action by name + // with the same object payload that a client would send. + rawRivetkitContext.schedule.after( + payload.delayMs, + "TriggerAnnouncement", + { + text: payload.text, + }, + ); + return { firesAt }; + }), + TriggerAnnouncement: ({ payload }) => + Effect.sync(() => { + rawRivetkitContext.broadcast("announcement", { + text: payload.text, + }); + }), + Archive: () => + Effect.gen(function* () { + const name = yield* roomName; + if (name !== "") { + // This only covers destruction through Archive. A future + // Effect onDestroy hook would cover every destroy path. + yield* Effect.tryPromise(() => + directory().CloseRoom({ name }), + ).pipe(Effect.orDie); + } + yield* Effect.sync(() => { + rawRivetkitContext.destroy(); + }); + }), + }); + }), { state: ChatRoomState, db: db({ diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts index 02a25cb2f9..19cb35024e 100644 --- a/examples/effect/src/actors/counter/api.ts +++ b/examples/effect/src/actors/counter/api.ts @@ -58,7 +58,7 @@ export const GetCount = Action.make("GetCount", { // The definition is the actor's public contract. It carries no // implementation and no persisted-state schema (state is server-only, -// configured via `ActorState.make` + `toLayer({ state })` in `live.ts`). +// configured via `ActorState.make` + `toLayer(wake, { state })` in `live.ts`). // Both server and client code import this; the implementation stays // server-only. export const Counter = Actor.make("Counter", { diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index aee3c6aefa..697ec5caca 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -24,7 +24,8 @@ const CounterState = ActorState.make("CounterState", { // calls within a wake. Finalizers run on sleep. export const CounterLive = Counter.toLayer( // Wake scope (runs each wake, finalizers run on sleep) - Effect.gen(function* () { + (wakeOptions) => + Effect.gen(function* () { // Actor-provided services are yielded from the Effect context. // They are scoped to this actor instance, not to individual // action calls. This means all action handlers below close @@ -39,10 +40,9 @@ export const CounterLive = Counter.toLayer( // - Swappable via layers. Tests can provide an in-memory KV // or a mock DB without changing the actor code. - // Yielding `CounterState` resolves to a `State` view over the - // persisted store. `State.changes` exposes every state changes - // commit as a stream. - const state = yield* CounterState; + // `wakeOptions.state` is a `State` view over the persisted store. + // `State.changes` exposes every state change commit as a stream. + const state = wakeOptions.state; // ^ State.State<{ count: number }> // const events = yield* Counter.Events // // ^ { countChanged: PubSub } diff --git a/examples/effect/src/actors/directory/live.ts b/examples/effect/src/actors/directory/live.ts index b16f0708a8..e4687fdf89 100644 --- a/examples/effect/src/actors/directory/live.ts +++ b/examples/effect/src/actors/directory/live.ts @@ -16,37 +16,43 @@ const DirectoryState = ActorState.make("DirectoryState", { }); export const DirectoryLive = Directory.toLayer( - Effect.gen(function* () { - const state = yield* DirectoryState; + ({ state }) => + Effect.gen(function* () { + return Directory.of({ + RegisterRoom: ({ payload }) => + // State writes go through Effect Schema validation. This + // example treats schema failures as defects instead of adding + // typed error channels to the action contract. + State.update(state, (current) => { + if ( + current.rooms.some( + (room) => room.name === payload.name, + ) + ) { + return current; + } - return Directory.of({ - RegisterRoom: ({ payload }) => - // State writes go through Effect Schema validation. This - // example treats schema failures as defects instead of adding - // typed error channels to the action contract. - State.update(state, (current) => { - if (current.rooms.some((room) => room.name === payload.name)) { - return current; - } - - return { - rooms: [ - ...current.rooms, - { name: payload.name, openedAt: Date.now() }, - ], - }; - }).pipe(Effect.orDie), - CloseRoom: ({ payload }) => - State.update(state, (current) => ({ - rooms: current.rooms.map((room) => - room.name === payload.name - ? { ...room, closedAt: Date.now() } - : room, + return { + rooms: [ + ...current.rooms, + { name: payload.name, openedAt: Date.now() }, + ], + }; + }).pipe(Effect.orDie), + CloseRoom: ({ payload }) => + State.update(state, (current) => ({ + rooms: current.rooms.map((room) => + room.name === payload.name + ? { ...room, closedAt: Date.now() } + : room, + ), + })).pipe(Effect.orDie), + ListRooms: () => + State.get(state).pipe( + Effect.orDie, + Effect.map((s) => s.rooms), ), - })).pipe(Effect.orDie), - ListRooms: () => - State.get(state).pipe(Effect.orDie, Effect.map((s) => s.rooms)), - }); - }), + }); + }), { state: DirectoryState, name: "Directory", icon: "folder" }, ); diff --git a/examples/effect/src/actors/moderator/live.ts b/examples/effect/src/actors/moderator/live.ts index d31f88e255..4a7f50b984 100644 --- a/examples/effect/src/actors/moderator/live.ts +++ b/examples/effect/src/actors/moderator/live.ts @@ -11,37 +11,39 @@ const ModeratorState = ActorState.make("ModeratorState", { }); export const ModeratorLive = Moderator.toLayer( - Effect.gen(function* () { - const state = yield* ModeratorState; + ({ state }) => + Effect.gen(function* () { + return Moderator.of({ + Review: ({ payload }) => + Effect.gen(function* () { + // State writes go through Effect Schema validation. This + // example treats schema failures as defects instead of adding + // typed error channels to the action contract. + const next = yield* State.updateAndGet( + state, + (current) => ({ + ...current, + reviewed: current.reviewed + 1, + }), + ).pipe(Effect.orDie); + const lower = payload.text.toLowerCase(); + const hit = next.bannedWords.find((word) => + lower.includes(word), + ); - return Moderator.of({ - Review: ({ payload }) => - Effect.gen(function* () { - // State writes go through Effect Schema validation. This - // example treats schema failures as defects instead of adding - // typed error channels to the action contract. - const next = yield* State.updateAndGet(state, (current) => ({ - ...current, - reviewed: current.reviewed + 1, - })).pipe(Effect.orDie); - const lower = payload.text.toLowerCase(); - const hit = next.bannedWords.find((word) => - lower.includes(word), - ); - - return hit - ? { - approved: false, - reason: `contains banned word "${hit}"`, - } - : { approved: true }; - }), - Stats: () => - State.get(state).pipe( - Effect.orDie, - Effect.map(({ reviewed }) => ({ reviewed })), - ), - }); - }), + return hit + ? { + approved: false, + reason: `contains banned word "${hit}"`, + } + : { approved: true }; + }), + Stats: () => + State.get(state).pipe( + Effect.orDie, + Effect.map(({ reviewed }) => ({ reviewed })), + ), + }); + }), { state: ModeratorState, name: "Moderator", icon: "shield" }, ); diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts index 20f0f23567..1c67174269 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -98,9 +98,25 @@ describe("Actor.make(...).toLayer", () => { }); test("accepts a function returning a plain action handlers object", () => { - expectTypeOf(TestActor.toLayer).toBeCallableWith((_wakeOptions) => ({ - GetContext: () => Effect.void, - })); + expectTypeOf(TestActor.toLayer).toBeCallableWith( + (_wakeOptions: any) => ({ + GetContext: () => Effect.void, + }), + ); + }); + + test("wake options omit state without a configured state type", () => { + TestActor.toLayer((wakeOptions) => { + // @ts-expect-error: stateless actors do not expose wakeOptions.state + wakeOptions.state; + expectTypeOf( + wakeOptions.rawRivetkitContext.state, + ).toEqualTypeOf(); + + return { + GetContext: () => Effect.void, + }; + }); }); test("wake options carry the configured state type", () => { @@ -202,7 +218,7 @@ describe("Actor.make(...).toLayer", () => { }); test("accepts a function returning an Effect of action handlers", () => { - expectTypeOf(TestActor.toLayer).toBeCallableWith((_wakeOptions) => + expectTypeOf(TestActor.toLayer).toBeCallableWith((_wakeOptions: any) => Effect.gen(function* () { return { GetContext: () => Effect.void, diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 6bb5de0b1f..e92484a7de 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -183,10 +183,38 @@ export type WakeOptions< readonly rawRivetkitContext: Rivetkit.WakeContextOf; }; -type WakeOptionsFor< +type RawWakeContextFor< State extends ActorState.AnyWithProps, Database extends RivetkitDb.AnyDatabaseProvider, -> = WakeOptions>; +> = { + [Key in keyof Rivetkit.WakeContextOf< + RivetkitActorDefinitionFor + >]: Key extends "state" + ? [State] extends [never] + ? never + : ActorStateEncoded + : Rivetkit.WakeContextOf< + RivetkitActorDefinitionFor + >[Key]; +}; + +type WakeOptionsFor< + ActorStateDefinition extends ActorState.AnyWithProps, + Database extends RivetkitDb.AnyDatabaseProvider, +> = { + readonly rawRivetkitContext: RawWakeContextFor< + ActorStateDefinition, + Database + >; +} & + ([ActorStateDefinition] extends [never] + ? {} + : { + readonly state: State.State< + ActorStateDecoded, + Schema.SchemaError + >; + }); type WakeFunction = | ((wakeOptions: W) => ActionHandlers) @@ -227,8 +255,8 @@ type UnknownToNever = unknown extends T ? never : T; type ExcludeBuiltInWakeServices< T, - State extends ActorState.AnyWithProps, -> = UnknownToNever>; + _State extends ActorState.AnyWithProps, +> = UnknownToNever>; type ToLayerRequirements< Actions extends Action.Any, @@ -262,13 +290,25 @@ export interface Actor< toLayer< ActionHandlers extends ActionHandlersFrom, - State extends ActorState.AnyWithProps = never, + R = never, + RX = never, + >( + wake: Wake>, + ): Layer.Layer< + never, + never, + ToLayerRequirements + >; + + toLayer< + ActionHandlers extends ActionHandlersFrom, + State extends ActorState.AnyWithProps, Database extends RivetkitDb.AnyDatabaseProvider = undefined, R = never, RX = never, >( wake: Wake>, - options?: Options, + options: Options, ): Layer.Layer< never, never, @@ -529,17 +569,12 @@ const makeRivetkitActor = Effect.fnUntraced(function* < Sleep, Effect.sync(() => c.sleep()), ), - effectOptions.state - ? Context.make( - effectOptions.state, - UndefinedOr.getOrThrow(state), - ) - : Context.empty(), ); - const wakeOptions: WakeOptionsFor = { + const wakeOptions = { rawRivetkitContext: c, - }; + ...(state ? { state } : {}), + } as WakeOptionsFor; const actionHandlers = yield* wakeHandler(wakeOptions).pipe( Effect.provide(context), ); diff --git a/rivetkit-typescript/packages/effect/src/ActorState.ts b/rivetkit-typescript/packages/effect/src/ActorState.ts index a80eee2d44..4a6718e57b 100644 --- a/rivetkit-typescript/packages/effect/src/ActorState.ts +++ b/rivetkit-typescript/packages/effect/src/ActorState.ts @@ -4,9 +4,9 @@ import type * as State from "./State"; const TypeId = "~@rivetkit/effect/ActorState"; /** - * A typed, persistent state slot for one Rivet Actor. Yielded inside - * the wake effect to obtain a `State` whose committed - * changes are mirrored back to rivetkit's persisted state. + * A typed, persistent state slot for one Rivet Actor. When configured + * on `Actor.toLayer`, the wake options receive a `State` whose + * committed changes are mirrored back to rivetkit's persisted state. * * State configuration (`schema` + `initial`) is server-only — it * describes the persisted shape and lives in implementation modules @@ -35,9 +35,8 @@ export interface Any { /** * Like `Any`, but with the prop fields (`schema`, `initialValue`) accessible. - * Used by the runtime to seed `c.state` and provide the `State` under - * the state's tag. The yielded `State` has no visible service requirement - * because schema services are resolved against the actor runner context. + * Used by the runtime to seed `c.state` and type `wakeOptions.state`. + * Schema services are resolved against the actor runner context. */ export interface AnyWithProps extends Context.Service> { @@ -54,19 +53,24 @@ export const isActorState = (u: unknown): u is Any => * Define a typed, persistent state slot for a Rivet Actor. * * `schema` is the persisted shape; `initialValue` produces the value used to - * seed state on first wake. The returned value is itself a Context tag: - * `yield* MyState` inside the wake effect resolves to a - * `SubscriptionRef`. + * seed state on first wake. Pass the returned value as the `state` option to + * `Actor.toLayer`; the wake function receives the live state at + * `wakeOptions.state`. * * @example * ```ts * import { Schema } from "effect" - * import { ActorState } from "@rivetkit/effect" + * import { Actor, ActorState, State } from "@rivetkit/effect" * + * const Counter = Actor.make("Counter") * const CounterState = ActorState.make("CounterState", { * schema: Schema.Number, * initialValue: () => 0, * }) + * + * Counter.toLayer((wakeOptions) => ({ + * Get: () => State.get(wakeOptions.state), + * }), { state: CounterState }) * ``` */ export const make = ( diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts index 42660b86fb..d2b9315d04 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts @@ -249,11 +249,9 @@ const TransformedActorState = ActorState.make("TransformedActorState", { }); export const TransformedStateActorLive = TransformedStateActor.toLayer( - (wakeOptions) => + ({ rawRivetkitContext, state }) => Effect.gen(function* () { - const state = wakeOptions.state; const sleep = yield* Actor.Sleep; - const rawRivetkitContext = wakeOptions.rawRivetkitContext; const rawWakeState = rawRivetkitContext.state; return TransformedStateActor.of({ @@ -317,9 +315,8 @@ const CounterState = ActorState.make("CounterState", { }); export const CounterLive = Counter.toLayer( - (wakeOptions) => + ({ rawRivetkitContext, state }) => Effect.gen(function* () { - const state = wakeOptions.state; const count = yield* Ref.make(0); const flags = yield* Flags; flags.set("on wake", true); @@ -332,8 +329,7 @@ export const CounterLive = Counter.toLayer( // `Counter.toLayer` below is the `rivetkit/db` raw-access factory, // so re-narrow to `RawAccess` for typed `execute` calls inside // handler closures. - const ctx = wakeOptions.rawRivetkitContext; - const db = ctx.db as RawAccess; + const db = rawRivetkitContext.db; // `Flags` is a process-wide Map shared across all tests in the // suite, so the finalizer flag must be namespaced by actor key // to keep cross-test wake/sleep cycles from leaking into each @@ -447,7 +443,7 @@ export const CounterLive = Counter.toLayer( GetPersistedState: () => State.get(state), // Per-actor SQLite is provisioned via the `db:` option on // `Counter.toLayer` below. The build effect destructures `db` - // from `wakeOptions.rawRivetkitContext`, so handlers reach SQLite + // from `rawRivetkitContext`, so handlers reach SQLite // through the captured client without going through `c.db`. LogEvent: ({ payload }) => Effect.tryPromise(async () => { @@ -532,9 +528,8 @@ export const Strict = Actor.make("Strict", { }); export const StrictLive = Strict.toLayer( - (wakeOptions) => + ({ state }) => Effect.gen(function* () { - const state = wakeOptions.state; return Strict.of({ StrictSet: ({ payload }) => State.set(state, payload.value).pipe( @@ -544,7 +539,9 @@ export const StrictLive = Strict.toLayer( }), ), StrictSetUnhandled: ({ payload }) => - State.set(state, payload.value).pipe(Effect.as(payload.value)), + State.set(state, payload.value).pipe( + Effect.as(payload.value), + ), StrictGet: () => State.get(state), }); }), @@ -587,7 +584,7 @@ export const Unregistered = Actor.make("Unregistered", { actions: [Echo] }); // Schema whose encode is permissive (identity) but whose decode rejects // negatives. Used to seed invalid persisted actor state so -// `wakeOptions.state` construction rejects on first wake. +// `state` construction rejects on first wake. const PermissiveEncodeStrictDecode = Schema.Number.pipe( Schema.decodeTo( Schema.Number, @@ -617,9 +614,8 @@ export const WakeDecodeFail = Actor.make("WakeDecodeFail", { }); export const WakeDecodeFailLive = WakeDecodeFail.toLayer( - (wakeOptions) => + () => Effect.gen(function* () { - const _state = wakeOptions.state; return WakeDecodeFail.of({ Ping: () => Effect.succeed("never reached"), }); @@ -630,7 +626,7 @@ export const WakeDecodeFailLive = WakeDecodeFail.toLayer( // --- BuildSetRejected --- // Strict schema rejecting negatives on encode. The build effect deliberately -// calls `State.set` against `wakeOptions.state` with a value the schema +// calls `State.set` against `state` with a value the schema // rejects, catches the resulting `SchemaError` via `Effect.match`, and // exposes the outcome via `BuildOutcome`. const StrictForBuildState = ActorState.make("StrictForBuildState", { @@ -647,9 +643,8 @@ export const BuildSetRejected = Actor.make("BuildSetRejected", { }); export const BuildSetRejectedLive = BuildSetRejected.toLayer( - (wakeOptions) => + ({ state }) => Effect.gen(function* () { - const state = wakeOptions.state; const wrote = yield* State.set(state, -1).pipe( Effect.match({ onFailure: () => false, From 251b63b89e9bdb6e8ec977e6b86c496f9b4f76c9 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 19 May 2026 14:35:33 +0200 Subject: [PATCH 218/306] Separate stateless actor toLayer overload --- .../packages/effect/src/Actor.test-d.ts | 13 ++++++++ .../packages/effect/src/Actor.ts | 31 ++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts index 1c67174269..6021e947ea 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -117,6 +117,19 @@ describe("Actor.make(...).toLayer", () => { GetContext: () => Effect.void, }; }); + + TestActor.toLayer((wakeOptions) => { + // @ts-expect-error: actors without a state option do not expose wakeOptions.state + wakeOptions.state; + + expectTypeOf( + wakeOptions.rawRivetkitContext.state, + ).toEqualTypeOf(); + + return { + GetContext: () => Effect.void, + }; + }, {}); }); test("wake options carry the configured state type", () => { diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index e92484a7de..7053fe91c2 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -57,6 +57,21 @@ export type Options< readonly db?: Database; }; +type StatelessOptions< + Database extends RivetkitDb.AnyDatabaseProvider = undefined, +> = Readonly & { + readonly state?: never; + readonly db?: Database; +}; + +type StatefulOptions< + State extends ActorState.AnyWithProps, + Database extends RivetkitDb.AnyDatabaseProvider = undefined, +> = Readonly & { + readonly state: State; + readonly db?: Database; +}; + const splitOptions = < State extends ActorState.AnyWithProps, Database extends RivetkitDb.AnyDatabaseProvider, @@ -288,6 +303,20 @@ export interface Actor< actionHandlers: ActionHandlers, ): ActionHandlers; + toLayer< + ActionHandlers extends ActionHandlersFrom, + Database extends RivetkitDb.AnyDatabaseProvider = undefined, + R = never, + RX = never, + >( + wake: Wake>, + options: StatelessOptions, + ): Layer.Layer< + never, + never, + ToLayerRequirements + >; + toLayer< ActionHandlers extends ActionHandlersFrom, R = never, @@ -308,7 +337,7 @@ export interface Actor< RX = never, >( wake: Wake>, - options: Options, + options: StatefulOptions, ): Layer.Layer< never, never, From 54dc8a3d0952c8843178b8ac3fdd7f54e09b45e0 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 19 May 2026 14:38:58 +0200 Subject: [PATCH 219/306] Switch effect actor state tests to direct state options --- .../packages/effect/src/Actor.test-d.ts | 9 +- .../packages/effect/test/fixtures/actors.ts | 178 +++++++++--------- 2 files changed, 95 insertions(+), 92 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts index 6021e947ea..38cf833a05 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -10,7 +10,6 @@ import { db } from "rivetkit/db"; import { describe, expectTypeOf, test } from "vitest"; import * as Action from "./Action"; import * as Actor from "./Actor"; -import * as ActorState from "./ActorState"; import type * as Client from "./Client"; import type * as State from "./State"; @@ -24,12 +23,12 @@ const TestActor = Actor.make("TestActor", { type TestActions = (typeof TestActor.actions)[number]; -const TestState = ActorState.make("TestState", { +const TestState = { schema: Schema.Struct({ count: Schema.Number, }), initialValue: () => ({ count: 0 }), -}); +}; const TagsCsv = Schema.String.pipe( Schema.decodeTo( @@ -41,7 +40,7 @@ const TagsCsv = Schema.String.pipe( ), ); -const TransformedState = ActorState.make("TransformedState", { +const TransformedState = { schema: Schema.Struct({ when: Schema.DateFromString, url: Schema.URLFromString, @@ -68,7 +67,7 @@ const TransformedState = ActorState.make("TransformedState", { }, ], }), -}); +}; describe("Actor.make", () => { test("preserves the name literal", () => { diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts index d2b9315d04..9118114bb6 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts @@ -1,4 +1,4 @@ -import { Action, Actor, ActorState, State } from "@rivetkit/effect"; +import { Action, Actor, State } from "@rivetkit/effect"; import { Context, Effect, @@ -9,7 +9,7 @@ import { SchemaIssue, SchemaTransformation, } from "effect"; -import { db, type RawAccess } from "rivetkit/db"; +import { db } from "rivetkit/db"; // --- Counter --- @@ -194,7 +194,7 @@ const EncodedTransformedState = Schema.Struct({ ), }); -const TransformedStateShape = Schema.Struct({ +const TransformedStateSchema = Schema.Struct({ when: Schema.DateFromString, url: Schema.URLFromString, id: Schema.BigIntFromString, @@ -213,13 +213,13 @@ export const GetRawWakeState = Action.make("GetRawWakeState", { }); export const GetDecodedState = Action.make("GetDecodedState", { - success: TransformedStateShape, + success: TransformedStateSchema, }); export const SetTransformedStateAndSleep = Action.make( "SetTransformedStateAndSleep", { - payload: TransformedStateShape, + payload: TransformedStateSchema, }, ); @@ -236,18 +236,6 @@ export const TransformedStateActor = Actor.make("TransformedStateActor", { ], }); -const TransformedActorState = ActorState.make("TransformedActorState", { - schema: TransformedStateShape, - initialValue: () => ({ - when: new Date("2024-01-01T00:00:00.000Z"), - url: new URL("https://rivet.dev/docs"), - id: 1n, - bytes: new Uint8Array([1, 2, 3]), - tags: ["initial"], - history: [], - }), -}); - export const TransformedStateActorLive = TransformedStateActor.toLayer( ({ rawRivetkitContext, state }) => Effect.gen(function* () { @@ -269,7 +257,19 @@ export const TransformedStateActorLive = TransformedStateActor.toLayer( }).pipe(Effect.orDie), }); }), - { state: TransformedActorState }, + { + state: { + schema: TransformedStateSchema, + initialValue: () => ({ + when: new Date("2024-01-01T00:00:00.000Z"), + url: new URL("https://rivet.dev/docs"), + id: 1n, + bytes: new Uint8Array([1, 2, 3]), + tags: ["initial"], + history: [], + }), + }, + }, ); export const Counter = Actor.make("Counter", { @@ -294,26 +294,6 @@ export const Counter = Actor.make("Counter", { ], }); -const CounterState = ActorState.make("CounterState", { - schema: Schema.Struct({ - count: Schema.Number, - when: Schema.DateFromString, - tags: TagsCsv, - // `scaled` is encoded/decoded through `ScaledNumber`, which - // yields `Multiplier` inside the transform. The Registry's state - // encode (write) and decode (wake) sites must resolve the - // service against the snapshotted Runner context, the same way - // action codec sites do. - scaled: ScaledNumber, - }), - initialValue: () => ({ - count: 0, - when: new Date(), - tags: ["default"], - scaled: 0, - }), -}); - export const CounterLive = Counter.toLayer( ({ rawRivetkitContext, state }) => Effect.gen(function* () { @@ -474,7 +454,25 @@ export const CounterLive = Counter.toLayer( }); }), { - state: CounterState, + state: { + schema: Schema.Struct({ + count: Schema.Number, + when: Schema.DateFromString, + tags: TagsCsv, + // `scaled` is encoded/decoded through `ScaledNumber`, which + // yields `Multiplier` inside the transform. The Registry's state + // encode (write) and decode (wake) sites must resolve the + // service against the snapshotted Runner context, the same way + // action codec sites do. + scaled: ScaledNumber, + }), + initialValue: () => ({ + count: 0, + when: new Date(), + tags: ["default"], + scaled: 0, + }), + }, // Migration runs once before the wake-scope build effect, so the // destructured `db` is already pointed at a migrated database // when handlers capture it. @@ -494,15 +492,6 @@ export const CounterLive = Counter.toLayer( // --- Strict --- -// State schema that rejects negative values. Used to exercise the -// typed-error channel on `State` writes: encoding a negative through -// `State.set` fails with `SchemaError`, which now flows through the -// handler effect instead of dying as a defect. -const StrictState = ActorState.make("StrictState", { - schema: Schema.Number.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))), - initialValue: () => 0, -}); - // Catches the `SchemaError` from `State.set` and reports the outcome. // Proves a handler can react to a schema failure that originates inside // the State layer — the new behavior since `State` carries `E`. @@ -545,7 +534,18 @@ export const StrictLive = Strict.toLayer( StrictGet: () => State.get(state), }); }), - { state: StrictState }, + { + state: { + // State schema that rejects negative values. Used to exercise the + // typed-error channel on `State` writes: encoding a negative through + // `State.set` fails with `SchemaError`, which now flows through the + // handler effect instead of dying as a defect. + schema: Schema.Number.pipe( + Schema.check(Schema.isGreaterThanOrEqualTo(0)), + ), + initialValue: () => 0, + }, + }, ); // --- Pinger --- @@ -582,33 +582,6 @@ export const Unregistered = Actor.make("Unregistered", { actions: [Echo] }); // --- WakeDecodeFail --- -// Schema whose encode is permissive (identity) but whose decode rejects -// negatives. Used to seed invalid persisted actor state so -// `state` construction rejects on first wake. -const PermissiveEncodeStrictDecode = Schema.Number.pipe( - Schema.decodeTo( - Schema.Number, - SchemaTransformation.transformOrFail({ - decode: (n: number) => - n >= 0 - ? Effect.succeed(n) - : Effect.fail( - new SchemaIssue.InvalidValue(Option.some(n), { - message: "decode rejects negative", - }), - ), - encode: (n: number) => Effect.succeed(n), - }), - ), -); - -const WakeDecodeFailState = ActorState.make("WakeDecodeFailState", { - schema: PermissiveEncodeStrictDecode, - // `-1` encodes successfully (encode is identity) so registry setup - // passes, but the wake-time decode rejects before handlers are built. - initialValue: () => -1, -}); - export const WakeDecodeFail = Actor.make("WakeDecodeFail", { actions: [Ping], }); @@ -620,20 +593,40 @@ export const WakeDecodeFailLive = WakeDecodeFail.toLayer( Ping: () => Effect.succeed("never reached"), }); }), - { state: WakeDecodeFailState }, + { + state: { + // Schema whose encode is permissive (identity) but whose decode rejects + // negatives. Used to seed invalid persisted actor state so + // `state` construction rejects on first wake. + schema: Schema.Number.pipe( + Schema.decodeTo( + Schema.Number, + SchemaTransformation.transformOrFail({ + decode: (n: number) => + n >= 0 + ? Effect.succeed(n) + : Effect.fail( + new SchemaIssue.InvalidValue( + Option.some(n), + { + message: + "decode rejects negative", + }, + ), + ), + encode: (n: number) => Effect.succeed(n), + }), + ), + ), + // `-1` encodes successfully (encode is identity) so registry setup + // passes, but the wake-time decode rejects before handlers are built. + initialValue: () => -1, + }, + }, ); // --- BuildSetRejected --- -// Strict schema rejecting negatives on encode. The build effect deliberately -// calls `State.set` against `state` with a value the schema -// rejects, catches the resulting `SchemaError` via `Effect.match`, and -// exposes the outcome via `BuildOutcome`. -const StrictForBuildState = ActorState.make("StrictForBuildState", { - schema: Schema.Number.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))), - initialValue: () => 0, -}); - export const BuildOutcome = Action.make("BuildOutcome", { success: Schema.Literals(["wrote", "rejected"]), }); @@ -656,5 +649,16 @@ export const BuildSetRejectedLive = BuildSetRejected.toLayer( Effect.succeed(wrote ? "wrote" : "rejected"), }); }), - { state: StrictForBuildState }, + { + state: { + // Strict schema rejecting negatives on encode. The build effect deliberately + // calls `State.set` against `state` with a value the schema + // rejects, catches the resulting `SchemaError` via `Effect.match`, and + // exposes the outcome via `BuildOutcome`. + schema: Schema.Number.pipe( + Schema.check(Schema.isGreaterThanOrEqualTo(0)), + ), + initialValue: () => 0, + }, + }, ); From 1a1fe744861fca15f538a234e5f4db10d06fcef4 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 19 May 2026 14:54:03 +0200 Subject: [PATCH 220/306] Move state options into internal module --- .../packages/effect/src/Actor.ts | 81 ++++++++--------- .../packages/effect/src/ActorState.ts | 89 ------------------- .../effect/src/internal/StateOptions.ts | 17 ++++ .../packages/effect/src/mod.ts | 1 - 4 files changed, 52 insertions(+), 136 deletions(-) delete mode 100644 rivetkit-typescript/packages/effect/src/ActorState.ts create mode 100644 rivetkit-typescript/packages/effect/src/internal/StateOptions.ts diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 7053fe91c2..a07a37c05e 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -19,9 +19,9 @@ import { import * as Rivetkit from "rivetkit"; import type * as RivetkitDb from "rivetkit/db"; import type * as Action from "./Action"; -import type * as ActorState from "./ActorState"; import * as Client from "./Client"; import * as ActionError from "./internal/ActionError"; +import type * as StateOptions from "./internal/StateOptions"; import { readTraceMeta, rpcSystem } from "./internal/tracing"; import * as Registry from "./Registry"; import type * as RivetError from "./RivetError"; @@ -50,7 +50,7 @@ export type RivetkitActorOptions = Pick< * with the effect-SDK-only options. */ export type Options< - State extends ActorState.AnyWithProps, + State extends StateOptions.Any, Database extends RivetkitDb.AnyDatabaseProvider = undefined, > = Readonly & { readonly state?: State; @@ -65,7 +65,7 @@ type StatelessOptions< }; type StatefulOptions< - State extends ActorState.AnyWithProps, + State extends StateOptions.Any, Database extends RivetkitDb.AnyDatabaseProvider = undefined, > = Readonly & { readonly state: State; @@ -73,7 +73,7 @@ type StatefulOptions< }; const splitOptions = < - State extends ActorState.AnyWithProps, + State extends StateOptions.Any, Database extends RivetkitDb.AnyDatabaseProvider, >( options: Options, @@ -133,40 +133,33 @@ type ActionHandlerServices = { : never; }[keyof ActionHandlers]; -type ActorStateEncoded = - | State["schema"]["Encoded"] - | ([State] extends [never] ? undefined : never); - -type ActorStateDecoded = - State["schema"]["Type"]; - -type ActorStateCodec = { +type StateOptionsCodec = { readonly decode: ( - input: ActorStateEncoded, + input: StateOptions.Encoded, ) => Effect.Effect< - ActorStateDecoded, + StateOptions.Decoded, Schema.SchemaError, State["schema"]["DecodingServices"] >; readonly decodeUnknown: ( input: unknown, ) => Effect.Effect< - ActorStateDecoded, + StateOptions.Decoded, Schema.SchemaError, State["schema"]["DecodingServices"] >; readonly encode: ( - input: ActorStateDecoded, + input: StateOptions.Decoded, ) => Effect.Effect< - ActorStateEncoded, + StateOptions.Encoded, Schema.SchemaError, State["schema"]["EncodingServices"] >; }; -const makeActorStateCodec = ( +const makeStateOptionsCodec = ( state: State, -): ActorStateCodec => { +): StateOptionsCodec => { const schema = state.schema as State["schema"]; return { @@ -177,10 +170,10 @@ const makeActorStateCodec = ( }; type RivetkitActorDefinitionFor< - State extends ActorState.AnyWithProps, + State extends StateOptions.Any, Database extends RivetkitDb.AnyDatabaseProvider, > = Rivetkit.ActorDefinition< - ActorStateEncoded, + StateOptions.Encoded, undefined, undefined, undefined, @@ -199,7 +192,7 @@ export type WakeOptions< }; type RawWakeContextFor< - State extends ActorState.AnyWithProps, + State extends StateOptions.Any, Database extends RivetkitDb.AnyDatabaseProvider, > = { [Key in keyof Rivetkit.WakeContextOf< @@ -207,29 +200,25 @@ type RawWakeContextFor< >]: Key extends "state" ? [State] extends [never] ? never - : ActorStateEncoded + : StateOptions.Encoded : Rivetkit.WakeContextOf< RivetkitActorDefinitionFor >[Key]; }; type WakeOptionsFor< - ActorStateDefinition extends ActorState.AnyWithProps, + StateDefinition extends StateOptions.Any, Database extends RivetkitDb.AnyDatabaseProvider, > = { - readonly rawRivetkitContext: RawWakeContextFor< - ActorStateDefinition, - Database - >; -} & - ([ActorStateDefinition] extends [never] - ? {} - : { - readonly state: State.State< - ActorStateDecoded, - Schema.SchemaError - >; - }); + readonly rawRivetkitContext: RawWakeContextFor; +} & ([StateDefinition] extends [never] + ? {} + : { + readonly state: State.State< + StateOptions.Decoded, + Schema.SchemaError + >; + }); type WakeFunction = | ((wakeOptions: W) => ActionHandlers) @@ -270,13 +259,13 @@ type UnknownToNever = unknown extends T ? never : T; type ExcludeBuiltInWakeServices< T, - _State extends ActorState.AnyWithProps, + _State extends StateOptions.Any, > = UnknownToNever>; type ToLayerRequirements< Actions extends Action.Any, ActionHandlers, - State extends ActorState.AnyWithProps, + State extends StateOptions.Any, R, RX, > = @@ -331,7 +320,7 @@ export interface Actor< toLayer< ActionHandlers extends ActionHandlersFrom, - State extends ActorState.AnyWithProps, + State extends StateOptions.Any, Database extends RivetkitDb.AnyDatabaseProvider = undefined, R = never, RX = never, @@ -366,7 +355,7 @@ const Proto: Omit, "name" | "actions"> = { toLayer< Actions extends Action.AnyWithProps, ActionHandlers extends ActionHandlersFrom, - State extends ActorState.AnyWithProps = never, + State extends StateOptions.Any = never, Database extends RivetkitDb.AnyDatabaseProvider = undefined, R = never, RX = never, @@ -514,7 +503,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < Actions extends Action.AnyWithProps, ActionHandlers extends ActionHandlersFrom, RX, - State extends ActorState.AnyWithProps = never, + State extends StateOptions.Any = never, Database extends RivetkitDb.AnyDatabaseProvider = undefined, >({ actor, @@ -536,7 +525,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < const { effectOptions, rivetkitOptions } = splitOptions(options); const stateCodec = UndefinedOr.map( effectOptions.state, - makeActorStateCodec, + makeStateOptionsCodec, ); const instances = MutableHashMap.empty< @@ -545,7 +534,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < readonly actionHandlers: ActionHandlers; readonly scope: Scope.Closeable; readonly state?: State.State< - ActorStateDecoded, + StateOptions.Decoded, Schema.SchemaError >; } @@ -582,7 +571,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < Effect.asVoid, ), ).pipe(Effect.orDie)) as State.State< - ActorState.AnyWithProps["schema"]["Type"], + StateOptions.Decoded, Schema.SchemaError >) : undefined; @@ -766,7 +755,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < }; return Rivetkit.actor< - ActorStateEncoded, + StateOptions.Encoded, undefined, undefined, undefined, diff --git a/rivetkit-typescript/packages/effect/src/ActorState.ts b/rivetkit-typescript/packages/effect/src/ActorState.ts deleted file mode 100644 index 4a6718e57b..0000000000 --- a/rivetkit-typescript/packages/effect/src/ActorState.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Context, Schema } from "effect"; -import type * as State from "./State"; - -const TypeId = "~@rivetkit/effect/ActorState"; - -/** - * A typed, persistent state slot for one Rivet Actor. When configured - * on `Actor.toLayer`, the wake options receive a `State` whose - * committed changes are mirrored back to rivetkit's persisted state. - * - * State configuration (`schema` + `initial`) is server-only — it - * describes the persisted shape and lives in implementation modules - * (`live.ts`), not on the actor contract shared with clients. - */ -export interface ActorState< - in out Name extends string, - in out S extends Schema.Top, -> extends Context.Service< - ActorState, - State.State - > { - readonly [TypeId]: typeof TypeId; - readonly _tag: Name; - readonly schema: S; - readonly initialValue: () => S["Type"]; -} - -/** - * Type-erased view of any `ActorState`. - */ -export interface Any { - readonly [TypeId]: typeof TypeId; - readonly _tag: string; -} - -/** - * Like `Any`, but with the prop fields (`schema`, `initialValue`) accessible. - * Used by the runtime to seed `c.state` and type `wakeOptions.state`. - * Schema services are resolved against the actor runner context. - */ -export interface AnyWithProps - extends Context.Service> { - readonly [TypeId]: typeof TypeId; - readonly _tag: string; - readonly schema: Schema.Top; - readonly initialValue: () => unknown; -} - -export const isActorState = (u: unknown): u is Any => - typeof u === "object" && u !== null && (u as any)[TypeId] === TypeId; - -/** - * Define a typed, persistent state slot for a Rivet Actor. - * - * `schema` is the persisted shape; `initialValue` produces the value used to - * seed state on first wake. Pass the returned value as the `state` option to - * `Actor.toLayer`; the wake function receives the live state at - * `wakeOptions.state`. - * - * @example - * ```ts - * import { Schema } from "effect" - * import { Actor, ActorState, State } from "@rivetkit/effect" - * - * const Counter = Actor.make("Counter") - * const CounterState = ActorState.make("CounterState", { - * schema: Schema.Number, - * initialValue: () => 0, - * }) - * - * Counter.toLayer((wakeOptions) => ({ - * Get: () => State.get(wakeOptions.state), - * }), { state: CounterState }) - * ``` - */ -export const make = ( - name: Name, - options: { readonly schema: S; readonly initialValue: () => S["Type"] }, -): ActorState => { - const tag = Context.Service< - ActorState, - State.State - >(`@rivetkit/effect/ActorState/${name}`) as ActorState; - (tag as any)[TypeId] = TypeId; - (tag as any)._tag = name; - (tag as any).schema = options.schema; - (tag as any).initialValue = options.initialValue; - return tag; -}; diff --git a/rivetkit-typescript/packages/effect/src/internal/StateOptions.ts b/rivetkit-typescript/packages/effect/src/internal/StateOptions.ts new file mode 100644 index 0000000000..95cb699093 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/internal/StateOptions.ts @@ -0,0 +1,17 @@ +import type { Schema } from "effect"; + +export interface StateOptions { + readonly schema: S; + readonly initialValue: () => S["Type"]; +} + +export interface Any { + readonly schema: Schema.Top; + readonly initialValue: () => unknown; +} + +export type Encoded = + | State["schema"]["Encoded"] + | ([State] extends [never] ? undefined : never); + +export type Decoded = State["schema"]["Type"]; diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index 36335a955a..65b2722c00 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,6 +1,5 @@ export * as Action from "./Action"; export * as Actor from "./Actor"; -export * as ActorState from "./ActorState"; export * as Client from "./Client"; export * as Registry from "./Registry"; export * as RivetError from "./RivetError"; From b006e105a98357dc7dce4742e7a21cf1b786b9ab Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 19 May 2026 14:57:26 +0200 Subject: [PATCH 221/306] Update effect example actors for inline state options --- examples/effect/src/actors/chat-room/live.ts | 44 +++-- examples/effect/src/actors/counter/live.ts | 187 +++++++++---------- examples/effect/src/actors/directory/live.ts | 32 ++-- examples/effect/src/actors/moderator/live.ts | 25 ++- 4 files changed, 143 insertions(+), 145 deletions(-) diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts index 76d6fe54bd..81cb230863 100644 --- a/examples/effect/src/actors/chat-room/live.ts +++ b/examples/effect/src/actors/chat-room/live.ts @@ -1,6 +1,6 @@ import { Effect, Schema } from "effect"; -import { Actor, ActorState, State } from "@rivetkit/effect"; -import { db, type RawAccess } from "rivetkit/db"; +import { Actor, State } from "@rivetkit/effect"; +import { db } from "rivetkit/db"; import { ChatRoom } from "./api.ts"; interface ModerationVerdict { @@ -8,26 +8,6 @@ interface ModerationVerdict { readonly reason?: string; } -const ChatRoomState = ActorState.make("ChatRoomState", { - schema: Schema.Struct({ - name: Schema.String, - members: Schema.Array( - Schema.Struct({ - name: Schema.String, - joinedAt: Schema.Number, - }), - ), - wakeCount: Schema.Number, - initialized: Schema.Boolean, - }), - initialValue: () => ({ - name: "", - members: [], - wakeCount: 0, - initialized: false, - }), -}); - export const ChatRoomLive = ChatRoom.toLayer( ({ rawRivetkitContext, state }) => Effect.gen(function* () { @@ -219,7 +199,25 @@ export const ChatRoomLive = ChatRoom.toLayer( }); }), { - state: ChatRoomState, + state: { + schema: Schema.Struct({ + name: Schema.String, + members: Schema.Array( + Schema.Struct({ + name: Schema.String, + joinedAt: Schema.Number, + }), + ), + wakeCount: Schema.Number, + initialized: Schema.Boolean, + }), + initialValue: () => ({ + name: "", + members: [], + wakeCount: 0, + initialized: false, + }), + }, db: db({ onMigrate: async (client) => { await client.execute(` diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index 697ec5caca..f233d7f437 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -1,20 +1,7 @@ import { Effect, Schema } from "effect"; -import { Actor, ActorState, State } from "@rivetkit/effect"; +import { Actor, State } from "@rivetkit/effect"; import { Counter, CounterOverflowError } from "./api.ts"; -// --- Actor State --- - -// State configuration (`schema` + `initialValue`) is server-only — it -// describes the persisted shape and must not leak into the client -// bundle. Defining it here keeps the contract in `api.ts` lean and -// shareable; the client never imports this file. -const CounterState = ActorState.make("CounterState", { - schema: Schema.Struct({ - count: Schema.Number, - }), - initialValue: () => ({ count: 0 }), -}); - // --- Actor Implementation --- // Counter.toLayer produces a Layer that registers this actor @@ -26,97 +13,103 @@ export const CounterLive = Counter.toLayer( // Wake scope (runs each wake, finalizers run on sleep) (wakeOptions) => Effect.gen(function* () { - // Actor-provided services are yielded from the Effect context. - // They are scoped to this actor instance, not to individual - // action calls. This means all action handlers below close - // over the same state, events, kv, and db references. - // - // Because services come through the context (not a context - // parameter like the current SDK's `c`), they are: - // - // - Visible in the type signature. The Effect's R channel - // declares exactly which services are required. - // - // - Swappable via layers. Tests can provide an in-memory KV - // or a mock DB without changing the actor code. + // Actor-provided services are yielded from the Effect context. + // They are scoped to this actor instance, not to individual + // action calls. This means all action handlers below close + // over the same state, events, kv, and db references. + // + // Because services come through the context (not a context + // parameter like the current SDK's `c`), they are: + // + // - Visible in the type signature. The Effect's R channel + // declares exactly which services are required. + // + // - Swappable via layers. Tests can provide an in-memory KV + // or a mock DB without changing the actor code. - // `wakeOptions.state` is a `State` view over the persisted store. - // `State.changes` exposes every state change commit as a stream. - const state = wakeOptions.state; - // ^ State.State<{ count: number }> - // const events = yield* Counter.Events - // // ^ { countChanged: PubSub } - // const messages = yield* Counter.Messages - // // ^ MessageQueue - // const kv = yield* Actor.Kv - // const db = yield* Actor.Db - const address = yield* Actor.CurrentAddress; - yield* Effect.log( - `waking ${address.name}/${address.key.join(",")} actorId=${address.actorId}`, - ); + // `wakeOptions.state` is a `State` view over the persisted store. + // `State.changes` exposes every state change commit as a stream. + const state = wakeOptions.state; + // ^ State.State<{ count: number }> + // const events = yield* Counter.Events + // // ^ { countChanged: PubSub } + // const messages = yield* Counter.Messages + // // ^ MessageQueue + // const kv = yield* Actor.Kv + // const db = yield* Actor.Db + const address = yield* Actor.CurrentAddress; + yield* Effect.log( + `waking ${address.name}/${address.key.join(",")} actorId=${address.actorId}`, + ); - yield* Effect.addFinalizer(() => - State.get(state).pipe( - Effect.orDie, - Effect.flatMap(({ count }) => - Effect.log( - `sleeping ${address.name}/${address.key.join(",")} count=${count}`, + yield* Effect.addFinalizer(() => + State.get(state).pipe( + Effect.orDie, + Effect.flatMap(({ count }) => + Effect.log( + `sleeping ${address.name}/${address.key.join(",")} count=${count}`, + ), ), ), - ), - ); + ); - // --- Message processing (not yet implemented) --- - // Pull-based: the actor controls when to take the next message. - // Forked into a scoped fiber, so it runs in the background and - // is canceled on sleep. Re-enable once Counter.Messages lands. - // - // yield* Effect.gen(function* () { - // const msg = yield* Queue.take(messages) - // yield* Match.value(msg).pipe( - // Match.tag("Reset", () => - // Effect.gen(function* () { - // yield* State.set(state, 0) - // yield* PubSub.publish(events.countChanged, 0) - // }) - // ), - // Match.tag("IncrementBy", ({ payload, complete }) => - // Effect.gen(function* () { - // const next = yield* State.updateAndGet( - // state, - // (s) => ({ count: s.count + payload.amount }), - // ) - // yield* PubSub.publish(events.countChanged, next.count) - // yield* complete(next.count) - // }) - // ), - // Match.exhaustive, - // ) - // }).pipe(Effect.forever, Effect.forkScoped) + // --- Message processing (not yet implemented) --- + // Pull-based: the actor controls when to take the next message. + // Forked into a scoped fiber, so it runs in the background and + // is canceled on sleep. Re-enable once Counter.Messages lands. + // + // yield* Effect.gen(function* () { + // const msg = yield* Queue.take(messages) + // yield* Match.value(msg).pipe( + // Match.tag("Reset", () => + // Effect.gen(function* () { + // yield* State.set(state, 0) + // yield* PubSub.publish(events.countChanged, 0) + // }) + // ), + // Match.tag("IncrementBy", ({ payload, complete }) => + // Effect.gen(function* () { + // const next = yield* State.updateAndGet( + // state, + // (s) => ({ count: s.count + payload.amount }), + // ) + // yield* PubSub.publish(events.countChanged, next.count) + // yield* complete(next.count) + // }) + // ), + // Match.exhaustive, + // ) + // }).pipe(Effect.forever, Effect.forkScoped) - // --- Action handlers (request-response) --- - return Counter.of({ - Increment: ({ payload }) => - Effect.gen(function* () { - const { count: next } = yield* State.updateAndGet( - state, - (s) => ({ count: s.count + payload.amount }), - ); - if (next > 20) { - return yield* new CounterOverflowError({ - limit: 20, - message: `count ${next} would exceed limit 20`, - }); - } - // yield* PubSub.publish(events.countChanged, next) - return next; - }), + // --- Action handlers (request-response) --- + return Counter.of({ + Increment: ({ payload }) => + Effect.gen(function* () { + const { count: next } = yield* State.updateAndGet( + state, + (s) => ({ count: s.count + payload.amount }), + ); + if (next > 20) { + return yield* new CounterOverflowError({ + limit: 20, + message: `count ${next} would exceed limit 20`, + }); + } + // yield* PubSub.publish(events.countChanged, next) + return next; + }), - GetCount: () => State.get(state).pipe(Effect.map((s) => s.count)), - }); - }), + GetCount: () => + State.get(state).pipe(Effect.map((s) => s.count)), + }); + }), { - state: CounterState, + state: { + schema: Schema.Struct({ + count: Schema.Number, + }), + initialValue: () => ({ count: 0 }), + }, name: "Counter", // Human-friendly display name icon: "comments", // FontAwesome icon name }, diff --git a/examples/effect/src/actors/directory/live.ts b/examples/effect/src/actors/directory/live.ts index e4687fdf89..8778cad0ce 100644 --- a/examples/effect/src/actors/directory/live.ts +++ b/examples/effect/src/actors/directory/live.ts @@ -1,20 +1,7 @@ import { Effect, Schema } from "effect"; -import { ActorState, State } from "@rivetkit/effect"; +import { State } from "@rivetkit/effect"; import { Directory } from "./api.ts"; -const DirectoryState = ActorState.make("DirectoryState", { - schema: Schema.Struct({ - rooms: Schema.Array( - Schema.Struct({ - name: Schema.String, - openedAt: Schema.Number, - closedAt: Schema.optionalKey(Schema.Number), - }), - ), - }), - initialValue: () => ({ rooms: [] }), -}); - export const DirectoryLive = Directory.toLayer( ({ state }) => Effect.gen(function* () { @@ -54,5 +41,20 @@ export const DirectoryLive = Directory.toLayer( ), }); }), - { state: DirectoryState, name: "Directory", icon: "folder" }, + { + state: { + schema: Schema.Struct({ + rooms: Schema.Array( + Schema.Struct({ + name: Schema.String, + openedAt: Schema.Number, + closedAt: Schema.optionalKey(Schema.Number), + }), + ), + }), + initialValue: () => ({ rooms: [] }), + }, + name: "Directory", + icon: "folder", + }, ); diff --git a/examples/effect/src/actors/moderator/live.ts b/examples/effect/src/actors/moderator/live.ts index 4a7f50b984..61e9535561 100644 --- a/examples/effect/src/actors/moderator/live.ts +++ b/examples/effect/src/actors/moderator/live.ts @@ -1,15 +1,7 @@ import { Effect, Schema } from "effect"; -import { ActorState, State } from "@rivetkit/effect"; +import { State } from "@rivetkit/effect"; import { Moderator } from "./api.ts"; -const ModeratorState = ActorState.make("ModeratorState", { - schema: Schema.Struct({ - bannedWords: Schema.Array(Schema.String), - reviewed: Schema.Number, - }), - initialValue: () => ({ bannedWords: ["spam", "scam"], reviewed: 0 }), -}); - export const ModeratorLive = Moderator.toLayer( ({ state }) => Effect.gen(function* () { @@ -45,5 +37,18 @@ export const ModeratorLive = Moderator.toLayer( ), }); }), - { state: ModeratorState, name: "Moderator", icon: "shield" }, + { + state: { + schema: Schema.Struct({ + bannedWords: Schema.Array(Schema.String), + reviewed: Schema.Number, + }), + initialValue: () => ({ + bannedWords: ["spam", "scam"], + reviewed: 0, + }), + }, + name: "Moderator", + icon: "shield", + }, ); From 7ba007c3ef418dcd3f4248181dbfcfa6ed6e3dc9 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 19 May 2026 17:21:28 +0200 Subject: [PATCH 222/306] refactor(effect): extract and simplify action failure classification Introduced `makeRivetkitActionFailureClassifier` to replace inline error classification logic. Simplified action error handling by consolidating decoding and classification functionality. Removed unused `isActionErrorMetadata`. --- .../packages/effect/src/Client.ts | 97 +++++++++---------- .../effect/src/internal/ActionError.ts | 2 - 2 files changed, 44 insertions(+), 55 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 2d784ddc8a..7ffdf106d3 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -1,4 +1,4 @@ -import { Context, Effect, Layer, Record, Schema } from "effect"; +import { Context, Effect, Layer, Record, Result, Schema } from "effect"; import * as RivetkitClient from "rivetkit/client"; import * as RivetkitErrors from "rivetkit/errors"; import type * as Action from "./Action"; @@ -64,9 +64,8 @@ export const make = Effect.fnUntraced(function* (options: Options = {}) { const decodeSuccess = Schema.decodeUnknownEffect( Schema.toCodecJson(action.successSchema), ); - const decodeError = decodeRejectedActionCall( - action.errorSchema, - ); + const classifyRivetkitActionFailure = + makeRivetkitActionFailureClassifier(action.errorSchema); const rpcMethod = `${actor.name}/${action._tag}`; @@ -119,7 +118,9 @@ export const make = Effect.fnUntraced(function* (options: Options = {}) { }), ).pipe( Effect.catch((unknownError) => - decodeError(unknownError.cause), + classifyRivetkitActionFailure( + unknownError.cause, + ).pipe(Effect.flatMap(Effect.fail)), ), ); @@ -141,66 +142,56 @@ const decodeActionErrorMetadata = Schema.decodeUnknownEffect( ActionError.ActionErrorMetadata, ); -const decodeRejectedActionCall = ( - actionErrorSchema: E, -) => { +/** @internal */ +export const makeRivetkitActionFailureClassifier = < + ActionErrorSchema extends Schema.Codec, +>( + actionErrorSchema: ActionErrorSchema, +): (( + cause: unknown, +) => Effect.Effect< + ActionErrorSchema["Type"] | RivetError.RivetError, + never, + ActionErrorSchema["DecodingServices"] +>) => { const decodeActionError = Schema.decodeUnknownEffect( Schema.toCodecJson(actionErrorSchema), ); - return Effect.fnUntraced(function* (cause: unknown) { - // Transport and runtime failures that are not structured Rivet errors - // cannot contain typed action-error metadata. + return Effect.fnUntraced(function* ( + cause: unknown, + ): Effect.fn.Return< + ActionErrorSchema["Type"] | RivetError.RivetError, + never, + ActionErrorSchema["DecodingServices"] + > { if (!RivetkitErrors.isRivetErrorLike(cause)) { - return yield* Effect.fail(RivetError.fromUnknown(cause)); + return RivetError.fromUnknown(cause); } + const rivetkitRivetError = RivetkitErrors.toRivetError(cause); - // Most structured Rivet errors are infrastructure or runtime failures. - // Only errors with the Effect action-error marker should enter the - // typed action-error decode path. - if (!ActionError.isActionErrorMetadata(rivetkitRivetError.metadata)) { - return yield* Effect.fail( - RivetError.fromRivetkitRivetError(rivetkitRivetError), - ); + const errorMetadataResult = yield* Effect.result( + decodeActionErrorMetadata(rivetkitRivetError.metadata), + ); + + if (Result.isFailure(errorMetadataResult)) { + return RivetError.fromRivetkitRivetError(rivetkitRivetError); } - // Effect action errors are sent as UserError metadata. First decode - // that envelope so we can distinguish typed domain errors from - // ordinary unknown user errors. - const actionErrorMetadata = yield* decodeActionErrorMetadata( - rivetkitRivetError.metadata, - ).pipe( - Effect.mapError( - (cause) => - new RivetError.RivetError({ - reason: new RivetError.ActionErrorDecodeFailed({ - cause, - rivetError: rivetkitRivetError, - }), - }), - ), + const actionErrorResult = yield* Effect.result( + decodeActionError(errorMetadataResult.success.error), ); - // Then decode the embedded payload against the action's declared error - // schema. A schema mismatch means this client cannot safely recover the - // typed domain error, so expose a RivetError with decode context. - const actionError = yield* decodeActionError( - actionErrorMetadata.error, - ).pipe( - Effect.mapError( - (decodeError) => - new RivetError.RivetError({ - reason: new RivetError.ActionErrorDecodeFailed({ - cause: decodeError, - rivetError: rivetkitRivetError, - }), - }), - ), - ); + if (Result.isFailure(actionErrorResult)) { + return new RivetError.RivetError({ + reason: new RivetError.ActionErrorDecodeFailed({ + cause: actionErrorResult.failure, + rivetError: rivetkitRivetError, + }), + }); + } - // Successfully decoded into the action's declared error type; - // flow it through the typed error channel as `E["Type"]`. - return yield* Effect.fail(actionError as E["Type"]); + return actionErrorResult.success; }); }; diff --git a/rivetkit-typescript/packages/effect/src/internal/ActionError.ts b/rivetkit-typescript/packages/effect/src/internal/ActionError.ts index 75e10fa992..2e1aafc336 100644 --- a/rivetkit-typescript/packages/effect/src/internal/ActionError.ts +++ b/rivetkit-typescript/packages/effect/src/internal/ActionError.ts @@ -14,8 +14,6 @@ export const ActionErrorMetadata = Schema.Struct({ export type ActionErrorMetadata = typeof ActionErrorMetadata.Type; -export const isActionErrorMetadata = Schema.is(ActionErrorMetadata); - const makeActionErrorMetadata = (error: unknown): ActionErrorMetadata => ({ _tag: ActionErrorMetadataTag, version: ActionErrorSchemaVersion, From b5bfb2eeff2881f28c950aeb3859dd157d295ba2 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 19 May 2026 17:24:25 +0200 Subject: [PATCH 223/306] test(effect): add unit tests for action failure classification --- .../packages/effect/src/Client.test.ts | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 rivetkit-typescript/packages/effect/src/Client.test.ts diff --git a/rivetkit-typescript/packages/effect/src/Client.test.ts b/rivetkit-typescript/packages/effect/src/Client.test.ts new file mode 100644 index 0000000000..b4be1e4b5c --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Client.test.ts @@ -0,0 +1,112 @@ +import { assert, describe, it } from "@effect/vitest"; +import { Effect, Schema } from "effect"; +import * as RivetkitErrors from "rivetkit/errors"; +import * as Client from "./Client"; +import * as ActionError from "./internal/ActionError"; +import * as RivetError from "./RivetError"; + +describe("makeRivetkitActionFailureClassifier", () => { + const ExpectedError = Schema.Struct({ + _tag: Schema.tag("CounterOverflow"), + message: Schema.String, + limit: Schema.Number, + }); + const classifyRivetkitActionFailure = + Client.makeRivetkitActionFailureClassifier(ExpectedError); + + it.effect("preserves non-Rivet failures as UnknownError", () => + Effect.gen(function* () { + const cause = new Error("plain failure"); + const error = yield* classifyRivetkitActionFailure(cause); + + assert.instanceOf(error, RivetError.RivetError); + assert.instanceOf(error.reason, RivetError.UnknownError); + assert.strictEqual(error.reason.message, "plain failure"); + assert.strictEqual(error.reason.cause, cause); + }), + ); + + it.effect("preserves structured non-action Rivet errors", () => + Effect.gen(function* () { + const cause = new RivetkitErrors.RivetError( + "actor", + "not_found", + "actor not found", + ); + const error = yield* classifyRivetkitActionFailure(cause); + + assert.instanceOf(error, RivetError.RivetError); + assert.instanceOf(error.reason, RivetError.ActorNotFound); + assert.strictEqual(error.reason.cause.group, "actor"); + assert.strictEqual(error.reason.cause.code, "not_found"); + assert.strictEqual(error.reason.cause.message, "actor not found"); + }), + ); + + it.effect( + "decodes action-error metadata into the declared error type", + () => + Effect.gen(function* () { + const cause = new RivetkitErrors.RivetError( + "user", + "CounterOverflow", + "counter overflow", + { + public: true, + metadata: { + _tag: ActionError.ActionErrorMetadataTag, + version: ActionError.ActionErrorSchemaVersion, + error: { + _tag: "CounterOverflow", + message: "counter overflow", + limit: 10, + }, + }, + }, + ); + const error = yield* classifyRivetkitActionFailure(cause); + + assert.deepStrictEqual(error, { + _tag: "CounterOverflow", + message: "counter overflow", + limit: 10, + }); + }), + ); + + it.effect( + "wraps invalid typed action-error payloads in ActionErrorDecodeFailed", + () => + Effect.gen(function* () { + const cause = new RivetkitErrors.RivetError( + "user", + "CounterOverflow", + "counter overflow", + { + metadata: { + _tag: ActionError.ActionErrorMetadataTag, + version: ActionError.ActionErrorSchemaVersion, + error: { + _tag: "CounterOverflow", + message: "counter overflow", + limit: "10", + }, + }, + }, + ); + + const error = yield* classifyRivetkitActionFailure(cause); + + assert.instanceOf(error, RivetError.RivetError); + assert.instanceOf( + error.reason, + RivetError.ActionErrorDecodeFailed, + ); + assert.strictEqual(error.reason.rivetError.group, "user"); + assert.strictEqual( + error.reason.rivetError.code, + "CounterOverflow", + ); + }), + ); +}); From 8938e163b67ca94b4b0584116b2a0f472d96d8b7 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 19 May 2026 17:38:59 +0200 Subject: [PATCH 224/306] refactor(effect): replace ActionError with ActionErrorEnvelope Refactored action error handling by replacing `ActionError` with `ActionErrorEnvelope`. Updated related imports, decoding logic, and test cases for consistency. Improved error classification clarity and encapsulation. --- .../packages/effect/src/Actor.ts | 20 +++++++++- .../packages/effect/src/Client.test.ts | 10 ++--- .../packages/effect/src/Client.ts | 20 ++++++---- .../effect/src/internal/ActionError.ts | 37 ------------------- .../src/internal/ActionErrorEnvelope.ts | 19 ++++++++++ 5 files changed, 55 insertions(+), 51 deletions(-) delete mode 100644 rivetkit-typescript/packages/effect/src/internal/ActionError.ts create mode 100644 rivetkit-typescript/packages/effect/src/internal/ActionErrorEnvelope.ts diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index a07a37c05e..f84d74dc5d 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -18,9 +18,10 @@ import { } from "effect"; import * as Rivetkit from "rivetkit"; import type * as RivetkitDb from "rivetkit/db"; +import { hasStringProperty } from "./internal/utils"; import type * as Action from "./Action"; import * as Client from "./Client"; -import * as ActionError from "./internal/ActionError"; +import * as ActionErrorEnvelope from "./internal/ActionErrorEnvelope"; import type * as StateOptions from "./internal/StateOptions"; import { readTraceMeta, rpcSystem } from "./internal/tracing"; import * as Registry from "./Registry"; @@ -680,7 +681,22 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ).pipe(Effect.orDie); return yield* Effect.fail( - ActionError.make(action._tag, encodedError), + new Rivetkit.UserError( + hasStringProperty("message")(encodedError) + ? encodedError.message + : `${action._tag} failed`, + { + code: hasStringProperty("_tag")( + encodedError, + ) + ? action._tag + : undefined, + metadata: + ActionErrorEnvelope.make( + encodedError, + ), + }, + ), ); } diff --git a/rivetkit-typescript/packages/effect/src/Client.test.ts b/rivetkit-typescript/packages/effect/src/Client.test.ts index b4be1e4b5c..b0f75b2933 100644 --- a/rivetkit-typescript/packages/effect/src/Client.test.ts +++ b/rivetkit-typescript/packages/effect/src/Client.test.ts @@ -2,7 +2,7 @@ import { assert, describe, it } from "@effect/vitest"; import { Effect, Schema } from "effect"; import * as RivetkitErrors from "rivetkit/errors"; import * as Client from "./Client"; -import * as ActionError from "./internal/ActionError"; +import * as ActionErrorEnvelope from "./internal/ActionErrorEnvelope"; import * as RivetError from "./RivetError"; describe("makeRivetkitActionFailureClassifier", () => { @@ -54,8 +54,8 @@ describe("makeRivetkitActionFailureClassifier", () => { { public: true, metadata: { - _tag: ActionError.ActionErrorMetadataTag, - version: ActionError.ActionErrorSchemaVersion, + _tag: ActionErrorEnvelope.tag, + version: ActionErrorEnvelope.schemaVersion, error: { _tag: "CounterOverflow", message: "counter overflow", @@ -84,8 +84,8 @@ describe("makeRivetkitActionFailureClassifier", () => { "counter overflow", { metadata: { - _tag: ActionError.ActionErrorMetadataTag, - version: ActionError.ActionErrorSchemaVersion, + _tag: ActionErrorEnvelope.tag, + version: ActionErrorEnvelope.schemaVersion, error: { _tag: "CounterOverflow", message: "counter overflow", diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 7ffdf106d3..05c2cd0c1e 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -3,7 +3,7 @@ import * as RivetkitClient from "rivetkit/client"; import * as RivetkitErrors from "rivetkit/errors"; import type * as Action from "./Action"; import type * as Actor from "./Actor"; -import * as ActionError from "./internal/ActionError"; +import * as ActionErrorEnvelope from "./internal/ActionErrorEnvelope"; import { rpcSystem, type TraceMeta } from "./internal/tracing"; import * as RivetError from "./RivetError"; @@ -138,8 +138,8 @@ export const make = Effect.fnUntraced(function* (options: Options = {}) { export const layer = (options: Options = {}): Layer.Layer => Layer.effect(Client, make(options)); -const decodeActionErrorMetadata = Schema.decodeUnknownEffect( - ActionError.ActionErrorMetadata, +const decodeActionErrorEnvelope = Schema.decodeUnknownEffect( + ActionErrorEnvelope.ActionErrorEnvelope, ); /** @internal */ @@ -165,24 +165,29 @@ export const makeRivetkitActionFailureClassifier = < never, ActionErrorSchema["DecodingServices"] > { + // In the case where the `cause` is not a `RivetError`. In principle, this shouldn't happen. if (!RivetkitErrors.isRivetErrorLike(cause)) { return RivetError.fromUnknown(cause); } const rivetkitRivetError = RivetkitErrors.toRivetError(cause); - const errorMetadataResult = yield* Effect.result( - decodeActionErrorMetadata(rivetkitRivetError.metadata), + const actionErrorEnvelope = yield* Effect.result( + decodeActionErrorEnvelope(rivetkitRivetError.metadata), ); - if (Result.isFailure(errorMetadataResult)) { + // If the error's `metadata` is not a valid action error envelope, then + // it means it's not a user-declared action error. + if (Result.isFailure(actionErrorEnvelope)) { return RivetError.fromRivetkitRivetError(rivetkitRivetError); } const actionErrorResult = yield* Effect.result( - decodeActionError(errorMetadataResult.success.error), + decodeActionError(actionErrorEnvelope.success.error), ); + // The envelope was valid, but the inner payload doesn't match the + // declared schema — surface as `ActionErrorDecodeFailed` if (Result.isFailure(actionErrorResult)) { return new RivetError.RivetError({ reason: new RivetError.ActionErrorDecodeFailed({ @@ -192,6 +197,7 @@ export const makeRivetkitActionFailureClassifier = < }); } + // Successfully decoded user-declared action error return actionErrorResult.success; }); }; diff --git a/rivetkit-typescript/packages/effect/src/internal/ActionError.ts b/rivetkit-typescript/packages/effect/src/internal/ActionError.ts deleted file mode 100644 index 2e1aafc336..0000000000 --- a/rivetkit-typescript/packages/effect/src/internal/ActionError.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Schema } from "effect"; -import * as Rivetkit from "rivetkit"; -import { hasStringProperty } from "./utils"; - -export const ActionErrorMetadataTag = "EffectActionError" as const; - -export const ActionErrorSchemaVersion = 1 as const; - -export const ActionErrorMetadata = Schema.Struct({ - _tag: Schema.tag(ActionErrorMetadataTag), - version: Schema.Literal(ActionErrorSchemaVersion), - error: Schema.Unknown, -}); - -export type ActionErrorMetadata = typeof ActionErrorMetadata.Type; - -const makeActionErrorMetadata = (error: unknown): ActionErrorMetadata => ({ - _tag: ActionErrorMetadataTag, - version: ActionErrorSchemaVersion, - error, -}); - -export const make = ( - actionTag: string, - encodedError: unknown, -): Rivetkit.UserError => - new Rivetkit.UserError( - hasStringProperty("message")(encodedError) - ? encodedError.message - : `${actionTag} failed`, - { - code: hasStringProperty("_tag")(encodedError) - ? encodedError._tag - : undefined, - metadata: makeActionErrorMetadata(encodedError), - }, - ); diff --git a/rivetkit-typescript/packages/effect/src/internal/ActionErrorEnvelope.ts b/rivetkit-typescript/packages/effect/src/internal/ActionErrorEnvelope.ts new file mode 100644 index 0000000000..ce933bdcf4 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/internal/ActionErrorEnvelope.ts @@ -0,0 +1,19 @@ +import { Schema } from "effect"; + +export const tag = "EffectActionError" as const; + +export const schemaVersion = 1 as const; + +export const ActionErrorEnvelope = Schema.Struct({ + _tag: Schema.tag(tag), + version: Schema.Literal(schemaVersion), + error: Schema.Unknown, +}); + +export type ActionErrorEnvelope = typeof ActionErrorEnvelope.Type; + +export const make = (error: unknown): ActionErrorEnvelope => ({ + _tag: tag, + version: schemaVersion, + error, +}); From 990f553fcf8bb2d739ef5318bbe9cac026b32b36 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 11:53:17 +0200 Subject: [PATCH 225/306] test(effect): inline TestActions --- rivetkit-typescript/packages/effect/src/Actor.test-d.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts index 38cf833a05..1ed9579e6d 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -21,8 +21,6 @@ const TestActor = Actor.make("TestActor", { actions: [Action.make("GetContext")], }); -type TestActions = (typeof TestActor.actions)[number]; - const TestState = { schema: Schema.Struct({ count: Schema.Number, @@ -308,7 +306,11 @@ describe("Actor.make(...).toLayer", () => { describe("Actor.make(...).client", () => { test("yields a typed Accessor", () => { expectTypeOf(TestActor.client).toEqualTypeOf< - Effect.Effect, never, Client.Client> + Effect.Effect< + Actor.Accessor<(typeof TestActor.actions)[number]>, + never, + Client.Client + > >(); }); }); From 049cc1d2da6b0cac2888bd1dac2e5f4678493c49 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 12:22:27 +0200 Subject: [PATCH 226/306] feat(effect): add registry web handler --- .../packages/effect/src/Registry.test.ts | 65 +++++++++++++++++++ .../packages/effect/src/Registry.ts | 54 ++++++++++++--- 2 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/src/Registry.test.ts diff --git a/rivetkit-typescript/packages/effect/src/Registry.test.ts b/rivetkit-typescript/packages/effect/src/Registry.test.ts new file mode 100644 index 0000000000..b5c987de34 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Registry.test.ts @@ -0,0 +1,65 @@ +import { assert, describe, it } from "@effect/vitest"; +import { Effect, Layer } from "effect"; +import * as Registry from "./Registry"; +import * as Action from "./Action"; +import * as Actor from "./Actor"; + +const TestActor = Actor.make("TestActor", { + actions: [Action.make("Test")], +}); + +const TestActorLive = TestActor.toLayer({ + Test: () => Effect.void, +}); + +const ActorsLayer = Layer.mergeAll(TestActorLive); + +const RegistryLive = ActorsLayer.pipe( + Layer.provideMerge( + Registry.layer({ + endpoint: "http://127.0.0.1:6420", + }), + ), +); + +describe("Registry.toWebHandler", () => { + it("serves registered actors as a Fetch handler", async () => { + const { handler, dispose } = Registry.toWebHandler(RegistryLive); + + try { + const response = await handler( + new Request("http://runner.test/api/rivet/metadata"), + ); + + assert.strictEqual(response.status, 200); + const body = (await response.json()) as { + readonly actorNames: Record; + }; + await assert.ok(body.actorNames.TestActor); + } finally { + await dispose(); + } + }); + + it("uses a custom serverless base path", async () => { + const { handler, dispose } = Registry.toWebHandler(RegistryLive, { + serverless: { + basePath: "/", + }, + }); + + try { + const response = await handler( + new Request("http://runner.test/metadata"), + ); + + assert.strictEqual(response.status, 200); + const body = (await response.json()) as { + readonly actorNames: Record; + }; + await assert.ok(body.actorNames.TestActor); + } finally { + await dispose(); + } + }); +}); diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 099d6a46b7..1eae1a222c 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -1,4 +1,5 @@ import { Context, Effect, Layer } from "effect"; +import { HttpEffect, HttpMiddleware } from "effect/unstable/http"; import * as Rivetkit from "rivetkit"; import * as Client from "./Client"; @@ -31,6 +32,22 @@ const make = (options: Options = {}): Registry => { export const layer = (options: Options = {}): Layer.Layer => Layer.succeed(Registry, make(options)); +const setupRivetkitRegistry = ( + registry: Registry, + options?: { + readonly serverless?: + | Rivetkit.RegistryConfigInput["serverless"] + | undefined; + }, +) => + Rivetkit.setup({ + use: Object.fromEntries(registry.rivetkitActors), + ...registry.options, + ...(options?.serverless === undefined + ? {} + : { serverless: options.serverless }), + }); + /** * Run the registered actors against the configured engine. Reads * the collected entries, materializes the underlying rivetkit @@ -39,10 +56,7 @@ export const layer = (options: Options = {}): Layer.Layer => export const serve: Layer.Layer = Layer.effectDiscard( Effect.gen(function* () { const registry = yield* Registry; - const rivetkitRegistry = Rivetkit.setup({ - use: Object.fromEntries(registry.rivetkitActors), - ...registry.options, - }); + const rivetkitRegistry = setupRivetkitRegistry(registry); yield* Effect.sync(() => rivetkitRegistry.start()); }), ); @@ -61,10 +75,7 @@ export const test: Layer.Layer = Layer.effect( Client.Client, Effect.gen(function* () { const registry = yield* Registry; - const rivetkitRegistry = Rivetkit.setup({ - use: Object.fromEntries(registry.rivetkitActors), - ...registry.options, - }); + const rivetkitRegistry = setupRivetkitRegistry(registry); rivetkitRegistry.config.test = { ...rivetkitRegistry.config.test, enabled: true, @@ -97,3 +108,30 @@ export const test: Layer.Layer = Layer.effect( }); }), ); + +export type ToWebHandlerOptions = { + readonly serverless?: + | Rivetkit.RegistryConfigInput["serverless"] + | undefined; + readonly middleware?: HttpMiddleware.HttpMiddleware | undefined; + readonly memoMap?: Layer.MemoMap | undefined; +}; + +export const toWebHandler = ( + registryLayer: Layer.Layer, + options?: ToWebHandlerOptions, +) => + HttpEffect.toWebHandlerLayerWith(registryLayer, { + toHandler: (context) => + Effect.sync(() => { + const registry = Context.get(context, Registry); + const rivetkitRegistry = setupRivetkitRegistry(registry, { + serverless: options?.serverless, + }); + return HttpEffect.fromWebHandler((request) => + rivetkitRegistry.handler(request), + ); + }), + middleware: options?.middleware, + memoMap: options?.memoMap, + }); From c5da6f8a9a4819ff30e4467f9ff489bd6b1394ce Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 12:24:41 +0200 Subject: [PATCH 227/306] feat(effect): expose registry http effect --- .../packages/effect/src/Registry.test.ts | 26 ++++++++ .../packages/effect/src/Registry.ts | 62 +++++++++++++++---- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Registry.test.ts b/rivetkit-typescript/packages/effect/src/Registry.test.ts index b5c987de34..cc6f7d7bd0 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test.ts @@ -1,5 +1,6 @@ import { assert, describe, it } from "@effect/vitest"; import { Effect, Layer } from "effect"; +import { HttpEffect } from "effect/unstable/http"; import * as Registry from "./Registry"; import * as Action from "./Action"; import * as Actor from "./Actor"; @@ -63,3 +64,28 @@ describe("Registry.toWebHandler", () => { } }); }); + +describe("Registry.toHttpEffect", () => { + it.effect("serves registered actors as an Effect HTTP handler", () => + Effect.scoped( + Effect.gen(function* () { + const httpEffect = + yield* Registry.toHttpEffect(RegistryLive); + const handler = HttpEffect.toWebHandler(httpEffect); + const response = yield* Effect.promise(() => + handler( + new Request("http://runner.test/api/rivet/metadata"), + ), + ); + + assert.strictEqual(response.status, 200); + const body = (yield* Effect.promise(() => + response.json(), + )) as { + readonly actorNames: Record; + }; + assert.ok(body.actorNames.TestActor); + }), + ), + ); +}); diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 1eae1a222c..4439ce6279 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -1,9 +1,17 @@ -import { Context, Effect, Layer } from "effect"; -import { HttpEffect, HttpMiddleware } from "effect/unstable/http"; +import { Context, Effect, Layer, Scope } from "effect"; +import { + HttpEffect, + HttpMiddleware, + HttpServerError, + HttpServerRequest, + HttpServerResponse, +} from "effect/unstable/http"; import * as Rivetkit from "rivetkit"; import * as Client from "./Client"; const TypeId = "~@rivetkit/effect/Registry"; +type ServerlessOptions = + Rivetkit.RegistryConfigInput["serverless"]; export type Options = Pick< Rivetkit.RegistryConfigInput, @@ -35,9 +43,7 @@ export const layer = (options: Options = {}): Layer.Layer => const setupRivetkitRegistry = ( registry: Registry, options?: { - readonly serverless?: - | Rivetkit.RegistryConfigInput["serverless"] - | undefined; + readonly serverless?: ServerlessOptions | undefined; }, ) => Rivetkit.setup({ @@ -109,10 +115,45 @@ export const test: Layer.Layer = Layer.effect( }), ); +const makeHttpEffect = ( + registry: Registry, + options?: ToHttpEffectOptions, +): Effect.Effect< + HttpServerResponse.HttpServerResponse, + HttpServerError.HttpServerError, + HttpServerRequest.HttpServerRequest +> => { + const rivetkitRegistry = setupRivetkitRegistry(registry, { + serverless: options?.serverless, + }); + return HttpEffect.fromWebHandler((request) => + rivetkitRegistry.handler(request), + ); +}; + +export type ToHttpEffectOptions = { + readonly serverless?: ServerlessOptions | undefined; +}; + +export const toHttpEffect = ( + registryLayer: Layer.Layer, + options?: ToHttpEffectOptions, +): Effect.Effect< + Effect.Effect< + HttpServerResponse.HttpServerResponse, + HttpServerError.HttpServerError, + HttpServerRequest.HttpServerRequest + >, + E, + Scope.Scope +> => + Effect.gen(function* () { + const context = yield* Layer.build(registryLayer); + return makeHttpEffect(Context.get(context, Registry), options); + }); + export type ToWebHandlerOptions = { - readonly serverless?: - | Rivetkit.RegistryConfigInput["serverless"] - | undefined; + readonly serverless?: ServerlessOptions | undefined; readonly middleware?: HttpMiddleware.HttpMiddleware | undefined; readonly memoMap?: Layer.MemoMap | undefined; }; @@ -125,12 +166,9 @@ export const toWebHandler = ( toHandler: (context) => Effect.sync(() => { const registry = Context.get(context, Registry); - const rivetkitRegistry = setupRivetkitRegistry(registry, { + return makeHttpEffect(registry, { serverless: options?.serverless, }); - return HttpEffect.fromWebHandler((request) => - rivetkitRegistry.handler(request), - ); }), middleware: options?.middleware, memoMap: options?.memoMap, From 3a5e75e4c1c7007cd661068a44798cbf21574ad3 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 12:33:37 +0200 Subject: [PATCH 228/306] style(effect): clean up formatting in Registry tests --- rivetkit-typescript/packages/effect/src/Registry.test.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Registry.test.ts b/rivetkit-typescript/packages/effect/src/Registry.test.ts index cc6f7d7bd0..b2949a332c 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test.ts @@ -1,9 +1,9 @@ import { assert, describe, it } from "@effect/vitest"; import { Effect, Layer } from "effect"; import { HttpEffect } from "effect/unstable/http"; -import * as Registry from "./Registry"; import * as Action from "./Action"; import * as Actor from "./Actor"; +import * as Registry from "./Registry"; const TestActor = Actor.make("TestActor", { actions: [Action.make("Test")], @@ -69,8 +69,7 @@ describe("Registry.toHttpEffect", () => { it.effect("serves registered actors as an Effect HTTP handler", () => Effect.scoped( Effect.gen(function* () { - const httpEffect = - yield* Registry.toHttpEffect(RegistryLive); + const httpEffect = yield* Registry.toHttpEffect(RegistryLive); const handler = HttpEffect.toWebHandler(httpEffect); const response = yield* Effect.promise(() => handler( @@ -79,9 +78,7 @@ describe("Registry.toHttpEffect", () => { ); assert.strictEqual(response.status, 200); - const body = (yield* Effect.promise(() => - response.json(), - )) as { + const body = (yield* Effect.promise(() => response.json())) as { readonly actorNames: Record; }; assert.ok(body.actorNames.TestActor); From f13a358b75f4c59c4c7f6437650b246b2e57e978 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 12:34:22 +0200 Subject: [PATCH 229/306] test(effect): cover registry handler types --- .../packages/effect/src/Registry.test-d.ts | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 rivetkit-typescript/packages/effect/src/Registry.test-d.ts diff --git a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts new file mode 100644 index 0000000000..7752dbd353 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts @@ -0,0 +1,91 @@ +import { Context, Effect, Layer, Scope } from "effect"; +import { + HttpServerError, + HttpServerRequest, + HttpServerResponse, +} from "effect/unstable/http"; +import { describe, expectTypeOf, test } from "vitest"; +import * as Action from "./Action"; +import * as Actor from "./Actor"; +import * as Registry from "./Registry"; + +const TestActor = Actor.make("TestActor", { + actions: [Action.make("Test")], +}); + +const TestActorLive = TestActor.toLayer({ + Test: () => Effect.void, +}); + +const RegistryLive = TestActorLive.pipe( + Layer.provideMerge(Registry.layer({ endpoint: "http://127.0.0.1:6420" })), +); + +describe("Registry.layer", () => { + test("accepts connection options", () => { + expectTypeOf(Registry.layer).toBeCallableWith({ + endpoint: "http://127.0.0.1:6420", + token: "dev-token", + namespace: "default", + }); + }); + + test("does not accept serverless options", () => { + Registry.layer({ + // @ts-expect-error: serverless routing belongs to toWebHandler and toHttpEffect options. + serverless: { + basePath: "/", + }, + }); + }); +}); + +describe("Registry.toWebHandler", () => { + test("accepts a registry layer", () => { + expectTypeOf(Registry.toWebHandler).toBeCallableWith(RegistryLive); + }); + + test("rejects actor registration layers that do not provide Registry", () => { + // @ts-expect-error: actor registration layers require Registry but do not provide it. + Registry.toWebHandler(TestActorLive); + }); + + test("accepts serverless routing options", () => { + expectTypeOf(Registry.toWebHandler).toBeCallableWith(RegistryLive, { + serverless: { + basePath: "/", + maxStartPayloadBytes: 1024, + }, + }); + }); + + test("returns a Fetch-compatible handler", () => { + const handler = Registry.toWebHandler(RegistryLive); + + expectTypeOf(handler.handler).toEqualTypeOf< + ( + request: Request, + context?: Context.Context | undefined, + ) => Promise + >(); + expectTypeOf(handler.dispose).toEqualTypeOf<() => Promise>(); + }); +}); + +describe("Registry.toHttpEffect", () => { + test("returns a scoped Effect HTTP handler", () => { + expectTypeOf( + Registry.toHttpEffect(RegistryLive), + ).toEqualTypeOf< + Effect.Effect< + Effect.Effect< + HttpServerResponse.HttpServerResponse, + HttpServerError.HttpServerError, + HttpServerRequest.HttpServerRequest + >, + never, + Scope.Scope + > + >(); + }); +}); From 7e42c649e48e2155fd786ecbe2e3deeda9fd411f Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 12:34:53 +0200 Subject: [PATCH 230/306] docs(effect): document registry handlers --- .../packages/effect/src/Registry.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 4439ce6279..62fbab9f8f 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -132,9 +132,19 @@ const makeHttpEffect = ( }; export type ToHttpEffectOptions = { + /** + * Serverless request routing configuration for the generated HTTP handler. + */ readonly serverless?: ServerlessOptions | undefined; }; +/** + * Builds a scoped Effect HTTP handler from a registry layer. + * + * The registry layer is built once in the surrounding scope. Registered Rivet + * Actors are materialized into a single underlying RivetKit registry, and each + * request is delegated to that registry's serverless handler. + */ export const toHttpEffect = ( registryLayer: Layer.Layer, options?: ToHttpEffectOptions, @@ -153,11 +163,27 @@ export const toHttpEffect = ( }); export type ToWebHandlerOptions = { + /** + * Serverless request routing configuration for the generated Web handler. + */ readonly serverless?: ServerlessOptions | undefined; + /** + * Effect HTTP middleware applied around the generated handler. + */ readonly middleware?: HttpMiddleware.HttpMiddleware | undefined; + /** + * Memo map used while building the registry layer. + */ readonly memoMap?: Layer.MemoMap | undefined; }; +/** + * Builds a Fetch-compatible request handler from a registry layer. + * + * This is the serverless entrypoint for the Effect SDK. The registry layer must + * provide `Registry`, usually by composing actor layers with `Registry.layer` + * via `Layer.provideMerge`. + */ export const toWebHandler = ( registryLayer: Layer.Layer, options?: ToWebHandlerOptions, From 023bab879673486e8860f58241315939f16f03b3 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 13:03:14 +0200 Subject: [PATCH 231/306] test(effect): expand registry handler coverage --- .../packages/effect/src/Registry.test-d.ts | 14 ++++ .../packages/effect/src/Registry.test.ts | 79 +++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts index 7752dbd353..d2ea13a3d0 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts @@ -73,6 +73,20 @@ describe("Registry.toWebHandler", () => { }); describe("Registry.toHttpEffect", () => { + test("accepts serverless routing options", () => { + expectTypeOf(Registry.toHttpEffect).toBeCallableWith(RegistryLive, { + serverless: { + basePath: "/", + maxStartPayloadBytes: 1024, + }, + }); + }); + + test("rejects actor registration layers that do not provide Registry", () => { + // @ts-expect-error: actor registration layers require Registry but do not provide it. + Registry.toHttpEffect(TestActorLive); + }); + test("returns a scoped Effect HTTP handler", () => { expectTypeOf( Registry.toHttpEffect(RegistryLive), diff --git a/rivetkit-typescript/packages/effect/src/Registry.test.ts b/rivetkit-typescript/packages/effect/src/Registry.test.ts index b2949a332c..a0a04d3a21 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test.ts @@ -63,6 +63,63 @@ describe("Registry.toWebHandler", () => { await dispose(); } }); + + it("builds the registry layer once across requests", async () => { + let builds = 0; + const CountingRegistryLive = Layer.mergeAll( + RegistryLive, + Layer.effectDiscard( + Effect.sync(() => { + builds += 1; + }), + ), + ); + const { handler, dispose } = + Registry.toWebHandler(CountingRegistryLive); + + try { + const first = await handler( + new Request("http://runner.test/api/rivet/metadata"), + ); + const second = await handler( + new Request("http://runner.test/api/rivet/metadata"), + ); + + assert.strictEqual(first.status, 200); + assert.strictEqual(second.status, 200); + assert.strictEqual(builds, 1); + } finally { + await dispose(); + } + }); + + it("closes registry layer finalizers on dispose", async () => { + let finalizers = 0; + const FinalizedRegistryLive = Layer.mergeAll( + RegistryLive, + Layer.effectDiscard( + Effect.addFinalizer(() => + Effect.sync(() => { + finalizers += 1; + }), + ), + ), + ); + const { handler, dispose } = + Registry.toWebHandler(FinalizedRegistryLive); + + try { + const response = await handler( + new Request("http://runner.test/api/rivet/metadata"), + ); + + assert.strictEqual(response.status, 200); + assert.strictEqual(finalizers, 0); + } finally { + await dispose(); + } + assert.strictEqual(finalizers, 1); + }); }); describe("Registry.toHttpEffect", () => { @@ -85,4 +142,26 @@ describe("Registry.toHttpEffect", () => { }), ), ); + + it.effect("uses a custom serverless base path", () => + Effect.scoped( + Effect.gen(function* () { + const httpEffect = yield* Registry.toHttpEffect(RegistryLive, { + serverless: { + basePath: "/", + }, + }); + const handler = HttpEffect.toWebHandler(httpEffect); + const response = yield* Effect.promise(() => + handler(new Request("http://runner.test/metadata")), + ); + + assert.strictEqual(response.status, 200); + const body = (yield* Effect.promise(() => response.json())) as { + readonly actorNames: Record; + }; + assert.ok(body.actorNames.TestActor); + }), + ), + ); }); From f34c7894668de9988674ccd1355481593f3c20df Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 13:26:15 +0200 Subject: [PATCH 232/306] test(effect): verify serverless handler options --- .../packages/effect/src/Registry.test.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/rivetkit-typescript/packages/effect/src/Registry.test.ts b/rivetkit-typescript/packages/effect/src/Registry.test.ts index a0a04d3a21..b5cbd66f6e 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test.ts @@ -64,6 +64,37 @@ describe("Registry.toWebHandler", () => { } }); + it("uses a custom serverless start payload size limit", async () => { + const { handler, dispose } = Registry.toWebHandler(RegistryLive, { + serverless: { + maxStartPayloadBytes: 1, + }, + }); + + try { + const response = await handler( + new Request("http://runner.test/api/rivet/start", { + method: "POST", + body: new Uint8Array([1, 2]), + }), + ); + + assert.strictEqual(response.status, 413); + const body = (await response.json()) as { + readonly group: string; + readonly code: string; + readonly message: string; + }; + assert.deepStrictEqual( + { group: body.group, code: body.code }, + { group: "message", code: "incoming_too_long" }, + ); + assert.match(body.message, /limit is 1 bytes/); + } finally { + await dispose(); + } + }); + it("builds the registry layer once across requests", async () => { let builds = 0; const CountingRegistryLive = Layer.mergeAll( @@ -164,4 +195,37 @@ describe("Registry.toHttpEffect", () => { }), ), ); + + it.effect("uses a custom serverless start payload size limit", () => + Effect.scoped( + Effect.gen(function* () { + const httpEffect = yield* Registry.toHttpEffect(RegistryLive, { + serverless: { + maxStartPayloadBytes: 1, + }, + }); + const handler = HttpEffect.toWebHandler(httpEffect); + const response = yield* Effect.promise(() => + handler( + new Request("http://runner.test/api/rivet/start", { + method: "POST", + body: new Uint8Array([1, 2]), + }), + ), + ); + + assert.strictEqual(response.status, 413); + const body = (yield* Effect.promise(() => response.json())) as { + readonly group: string; + readonly code: string; + readonly message: string; + }; + assert.deepStrictEqual( + { group: body.group, code: body.code }, + { group: "message", code: "incoming_too_long" }, + ); + assert.match(body.message, /limit is 1 bytes/); + }), + ), + ); }); From ad9e0e1516209f646fa8aab44be248b9ec507f33 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 13:43:17 +0200 Subject: [PATCH 233/306] test(effect): prove serverless base path routing --- .../packages/effect/src/Registry.test.ts | 145 +++++++++++++++--- 1 file changed, 122 insertions(+), 23 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Registry.test.ts b/rivetkit-typescript/packages/effect/src/Registry.test.ts index b5cbd66f6e..369f81deee 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test.ts @@ -64,6 +64,45 @@ describe("Registry.toWebHandler", () => { } }); + it("uses the custom base path to identify start requests", async () => { + const { handler, dispose } = Registry.toWebHandler(RegistryLive, { + serverless: { + basePath: "/custom", + maxStartPayloadBytes: 1, + }, + }); + + try { + const defaultPrefix = await handler( + new Request("http://runner.test/api/rivet/start", { + method: "POST", + body: new Uint8Array([1, 2]), + }), + ); + assert.notStrictEqual(defaultPrefix.status, 413); + + const customPrefix = await handler( + new Request("http://runner.test/custom/start", { + method: "POST", + body: new Uint8Array([1, 2]), + }), + ); + assert.strictEqual(customPrefix.status, 413); + const body = (await customPrefix.json()) as { + readonly group: string; + readonly code: string; + readonly message: string; + }; + assert.deepStrictEqual( + { group: body.group, code: body.code }, + { group: "message", code: "incoming_too_long" }, + ); + await assert.match(body.message, /limit is 1 bytes/); + } finally { + await dispose(); + } + }); + it("uses a custom serverless start payload size limit", async () => { const { handler, dispose } = Registry.toWebHandler(RegistryLive, { serverless: { @@ -89,7 +128,7 @@ describe("Registry.toWebHandler", () => { { group: body.group, code: body.code }, { group: "message", code: "incoming_too_long" }, ); - assert.match(body.message, /limit is 1 bytes/); + await assert.match(body.message, /limit is 1 bytes/); } finally { await dispose(); } @@ -136,8 +175,9 @@ describe("Registry.toWebHandler", () => { ), ), ); - const { handler, dispose } = - Registry.toWebHandler(FinalizedRegistryLive); + const { handler, dispose } = Registry.toWebHandler( + FinalizedRegistryLive, + ); try { const response = await handler( @@ -165,11 +205,15 @@ describe("Registry.toHttpEffect", () => { ), ); - assert.strictEqual(response.status, 200); - const body = (yield* Effect.promise(() => response.json())) as { - readonly actorNames: Record; - }; - assert.ok(body.actorNames.TestActor); + yield* Effect.promise(() => + (async (response: Response) => { + assert.strictEqual(response.status, 200); + const body = (await response.json()) as { + readonly actorNames: Record; + }; + await assert.ok(body.actorNames.TestActor); + })(response), + ); }), ), ); @@ -187,11 +231,62 @@ describe("Registry.toHttpEffect", () => { handler(new Request("http://runner.test/metadata")), ); - assert.strictEqual(response.status, 200); - const body = (yield* Effect.promise(() => response.json())) as { - readonly actorNames: Record; - }; - assert.ok(body.actorNames.TestActor); + yield* Effect.promise(() => + (async (response: Response) => { + assert.strictEqual(response.status, 200); + const body = (await response.json()) as { + readonly actorNames: Record; + }; + await assert.ok(body.actorNames.TestActor); + })(response), + ); + }), + ), + ); + + it.effect("uses the custom base path to identify start requests", () => + Effect.scoped( + Effect.gen(function* () { + const httpEffect = yield* Registry.toHttpEffect(RegistryLive, { + serverless: { + basePath: "/custom", + maxStartPayloadBytes: 1, + }, + }); + const handler = HttpEffect.toWebHandler(httpEffect); + const defaultPrefix = yield* Effect.promise(() => + handler( + new Request("http://runner.test/api/rivet/start", { + method: "POST", + body: new Uint8Array([1, 2]), + }), + ), + ); + assert.notStrictEqual(defaultPrefix.status, 413); + + const customPrefix = yield* Effect.promise(() => + handler( + new Request("http://runner.test/custom/start", { + method: "POST", + body: new Uint8Array([1, 2]), + }), + ), + ); + yield* Effect.promise(() => + (async (response: Response) => { + assert.strictEqual(response.status, 413); + const body = (await response.json()) as { + readonly group: string; + readonly code: string; + readonly message: string; + }; + assert.deepStrictEqual( + { group: body.group, code: body.code }, + { group: "message", code: "incoming_too_long" }, + ); + assert.match(body.message, /limit is 1 bytes/); + })(customPrefix), + ); }), ), ); @@ -214,17 +309,21 @@ describe("Registry.toHttpEffect", () => { ), ); - assert.strictEqual(response.status, 413); - const body = (yield* Effect.promise(() => response.json())) as { - readonly group: string; - readonly code: string; - readonly message: string; - }; - assert.deepStrictEqual( - { group: body.group, code: body.code }, - { group: "message", code: "incoming_too_long" }, + yield* Effect.promise(() => + (async (response: Response) => { + assert.strictEqual(response.status, 413); + const body = (await response.json()) as { + readonly group: string; + readonly code: string; + readonly message: string; + }; + assert.deepStrictEqual( + { group: body.group, code: body.code }, + { group: "message", code: "incoming_too_long" }, + ); + assert.match(body.message, /limit is 1 bytes/); + })(response), ); - assert.match(body.message, /limit is 1 bytes/); }), ), ); From ab1572bf456881d06a16ccceed5107a0676c857e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 15:02:12 +0200 Subject: [PATCH 234/306] feat(effect): flatten registry handler options --- .../packages/effect/src/Registry.test-d.ts | 16 +++----- .../packages/effect/src/Registry.test.ts | 28 ++++--------- .../packages/effect/src/Registry.ts | 39 +++++++++++-------- 3 files changed, 35 insertions(+), 48 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts index d2ea13a3d0..c852a225ab 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts @@ -52,10 +52,8 @@ describe("Registry.toWebHandler", () => { test("accepts serverless routing options", () => { expectTypeOf(Registry.toWebHandler).toBeCallableWith(RegistryLive, { - serverless: { - basePath: "/", - maxStartPayloadBytes: 1024, - }, + basePath: "/", + maxStartPayloadBytes: 1024, }); }); @@ -75,10 +73,8 @@ describe("Registry.toWebHandler", () => { describe("Registry.toHttpEffect", () => { test("accepts serverless routing options", () => { expectTypeOf(Registry.toHttpEffect).toBeCallableWith(RegistryLive, { - serverless: { - basePath: "/", - maxStartPayloadBytes: 1024, - }, + basePath: "/", + maxStartPayloadBytes: 1024, }); }); @@ -88,9 +84,7 @@ describe("Registry.toHttpEffect", () => { }); test("returns a scoped Effect HTTP handler", () => { - expectTypeOf( - Registry.toHttpEffect(RegistryLive), - ).toEqualTypeOf< + expectTypeOf(Registry.toHttpEffect(RegistryLive)).toEqualTypeOf< Effect.Effect< Effect.Effect< HttpServerResponse.HttpServerResponse, diff --git a/rivetkit-typescript/packages/effect/src/Registry.test.ts b/rivetkit-typescript/packages/effect/src/Registry.test.ts index 369f81deee..0285b99d50 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test.ts @@ -44,9 +44,7 @@ describe("Registry.toWebHandler", () => { it("uses a custom serverless base path", async () => { const { handler, dispose } = Registry.toWebHandler(RegistryLive, { - serverless: { - basePath: "/", - }, + basePath: "/", }); try { @@ -66,10 +64,8 @@ describe("Registry.toWebHandler", () => { it("uses the custom base path to identify start requests", async () => { const { handler, dispose } = Registry.toWebHandler(RegistryLive, { - serverless: { - basePath: "/custom", - maxStartPayloadBytes: 1, - }, + basePath: "/custom", + maxStartPayloadBytes: 1, }); try { @@ -105,9 +101,7 @@ describe("Registry.toWebHandler", () => { it("uses a custom serverless start payload size limit", async () => { const { handler, dispose } = Registry.toWebHandler(RegistryLive, { - serverless: { - maxStartPayloadBytes: 1, - }, + maxStartPayloadBytes: 1, }); try { @@ -222,9 +216,7 @@ describe("Registry.toHttpEffect", () => { Effect.scoped( Effect.gen(function* () { const httpEffect = yield* Registry.toHttpEffect(RegistryLive, { - serverless: { - basePath: "/", - }, + basePath: "/", }); const handler = HttpEffect.toWebHandler(httpEffect); const response = yield* Effect.promise(() => @@ -248,10 +240,8 @@ describe("Registry.toHttpEffect", () => { Effect.scoped( Effect.gen(function* () { const httpEffect = yield* Registry.toHttpEffect(RegistryLive, { - serverless: { - basePath: "/custom", - maxStartPayloadBytes: 1, - }, + basePath: "/custom", + maxStartPayloadBytes: 1, }); const handler = HttpEffect.toWebHandler(httpEffect); const defaultPrefix = yield* Effect.promise(() => @@ -295,9 +285,7 @@ describe("Registry.toHttpEffect", () => { Effect.scoped( Effect.gen(function* () { const httpEffect = yield* Registry.toHttpEffect(RegistryLive, { - serverless: { - maxStartPayloadBytes: 1, - }, + maxStartPayloadBytes: 1, }); const handler = HttpEffect.toWebHandler(httpEffect); const response = yield* Effect.promise(() => diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 62fbab9f8f..32b70b422a 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -10,8 +10,9 @@ import * as Rivetkit from "rivetkit"; import * as Client from "./Client"; const TypeId = "~@rivetkit/effect/Registry"; -type ServerlessOptions = - Rivetkit.RegistryConfigInput["serverless"]; +type ServerlessOptions = NonNullable< + Rivetkit.RegistryConfigInput["serverless"] +>; export type Options = Pick< Rivetkit.RegistryConfigInput, @@ -124,19 +125,14 @@ const makeHttpEffect = ( HttpServerRequest.HttpServerRequest > => { const rivetkitRegistry = setupRivetkitRegistry(registry, { - serverless: options?.serverless, + serverless: options, }); return HttpEffect.fromWebHandler((request) => rivetkitRegistry.handler(request), ); }; -export type ToHttpEffectOptions = { - /** - * Serverless request routing configuration for the generated HTTP handler. - */ - readonly serverless?: ServerlessOptions | undefined; -}; +export type ToHttpEffectOptions = ServerlessOptions; /** * Builds a scoped Effect HTTP handler from a registry layer. @@ -162,11 +158,7 @@ export const toHttpEffect = ( return makeHttpEffect(Context.get(context, Registry), options); }); -export type ToWebHandlerOptions = { - /** - * Serverless request routing configuration for the generated Web handler. - */ - readonly serverless?: ServerlessOptions | undefined; +export type ToWebHandlerOptions = ServerlessOptions & { /** * Effect HTTP middleware applied around the generated handler. */ @@ -177,6 +169,18 @@ export type ToWebHandlerOptions = { readonly memoMap?: Layer.MemoMap | undefined; }; +const toWebHandlerServerlessOptions = ( + options?: ToWebHandlerOptions, +): ServerlessOptions | undefined => { + if (options === undefined) { + return undefined; + } + + const { middleware: _middleware, memoMap: _memoMap, ...serverless } = + options; + return serverless; +}; + /** * Builds a Fetch-compatible request handler from a registry layer. * @@ -192,9 +196,10 @@ export const toWebHandler = ( toHandler: (context) => Effect.sync(() => { const registry = Context.get(context, Registry); - return makeHttpEffect(registry, { - serverless: options?.serverless, - }); + return makeHttpEffect( + registry, + toWebHandlerServerlessOptions(options), + ); }), middleware: options?.middleware, memoMap: options?.memoMap, From ffc388e623aff3a4e3ab3c481554c9a4b4eedbf7 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 15:22:11 +0200 Subject: [PATCH 235/306] test(effect): annotate diagnostics in Registry tests --- rivetkit-typescript/packages/effect/src/Registry.test-d.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts index c852a225ab..59b046c38c 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts @@ -1,5 +1,5 @@ -import { Context, Effect, Layer, Scope } from "effect"; -import { +import { type Context, Effect, Layer, type Scope } from "effect"; +import type { HttpServerError, HttpServerRequest, HttpServerResponse, @@ -47,6 +47,7 @@ describe("Registry.toWebHandler", () => { test("rejects actor registration layers that do not provide Registry", () => { // @ts-expect-error: actor registration layers require Registry but do not provide it. + // @effect-diagnostics effect/missingLayerContext:off effect/floatingEffect:off Registry.toWebHandler(TestActorLive); }); @@ -80,6 +81,7 @@ describe("Registry.toHttpEffect", () => { test("rejects actor registration layers that do not provide Registry", () => { // @ts-expect-error: actor registration layers require Registry but do not provide it. + // @effect-diagnostics effect/missingLayerContext:off effect/floatingEffect:off Registry.toHttpEffect(TestActorLive); }); From 4c8ae619a8c9faa1bc01e4b19d479a769838cb00 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 15:30:11 +0200 Subject: [PATCH 236/306] feat(effect): add registry serve adapter --- examples/effect/src/main.ts | 3 +-- .../packages/effect/src/Registry.test-d.ts | 12 +++++++++ .../packages/effect/src/Registry.ts | 26 ++++++++++++------- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index 7095f8acbc..aae501d53c 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -18,8 +18,7 @@ const ActorsLayer = Layer.mergeAll( // point at a remote engine). For dev builds without a packaged engine, // set RIVET_ENGINE_BINARY to the path of a `cargo build` binary, e.g.: // RIVET_ENGINE_BINARY=$(pwd)/target/debug/rivet-engine pnpm start -const MainLayer = Registry.serve.pipe( - Layer.provide(ActorsLayer), +const MainLayer = Registry.serve(ActorsLayer).pipe( Layer.provide(Registry.layer()), ) diff --git a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts index 59b046c38c..07f3d7e2c3 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts @@ -40,6 +40,18 @@ describe("Registry.layer", () => { }); }); +describe("Registry.serve", () => { + test("accepts an actor registration layer", () => { + expectTypeOf(Registry.serve).toBeCallableWith(TestActorLive); + }); + + test("returns a server layer that requires Registry", () => { + expectTypeOf(Registry.serve(TestActorLive)).toEqualTypeOf< + Layer.Layer + >(); + }); +}); + describe("Registry.toWebHandler", () => { test("accepts a registry layer", () => { expectTypeOf(Registry.toWebHandler).toBeCallableWith(RegistryLive); diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 32b70b422a..11c34b42a5 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -56,17 +56,23 @@ const setupRivetkitRegistry = ( }); /** - * Run the registered actors against the configured engine. Reads - * the collected entries, materializes the underlying rivetkit - * registry, and starts it. + * Runs an actor registration layer against the configured engine. + * + * The actor layer is built in the server layer scope. Registered Rivet Actors + * are collected from `Registry`, materialized into a single underlying RivetKit + * registry, and started. */ -export const serve: Layer.Layer = Layer.effectDiscard( - Effect.gen(function* () { - const registry = yield* Registry; - const rivetkitRegistry = setupRivetkitRegistry(registry); - yield* Effect.sync(() => rivetkitRegistry.start()); - }), -); +export const serve = ( + actorsLayer: Layer.Layer, +): Layer.Layer => + Layer.effectDiscard( + Effect.gen(function* () { + yield* Layer.build(actorsLayer); + const registry = yield* Registry; + const rivetkitRegistry = setupRivetkitRegistry(registry); + yield* Effect.sync(() => rivetkitRegistry.start()); + }), + ); /** * In-process test runtime. Boots the rivetkit registry against the From d4b42d9715c281c2117fc2c7ea099f76bee0127d Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 15:49:01 +0200 Subject: [PATCH 237/306] feat(effect): support quiet registry handlers --- .../packages/effect/src/Registry.test-d.ts | 15 +++++++++++ .../packages/effect/src/Registry.test.ts | 19 ++++++++++++++ .../packages/effect/src/Registry.ts | 25 +++++++++++-------- 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts index 07f3d7e2c3..58af3d6be0 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts @@ -27,6 +27,7 @@ describe("Registry.layer", () => { endpoint: "http://127.0.0.1:6420", token: "dev-token", namespace: "default", + noWelcome: true, }); }); @@ -70,6 +71,13 @@ describe("Registry.toWebHandler", () => { }); }); + test("rejects registry options", () => { + Registry.toWebHandler(RegistryLive, { + // @ts-expect-error: noWelcome belongs to Registry.layer options. + noWelcome: true, + }); + }); + test("returns a Fetch-compatible handler", () => { const handler = Registry.toWebHandler(RegistryLive); @@ -91,6 +99,13 @@ describe("Registry.toHttpEffect", () => { }); }); + test("rejects registry options", () => { + Registry.toHttpEffect(RegistryLive, { + // @ts-expect-error: noWelcome belongs to Registry.layer options. + noWelcome: true, + }); + }); + test("rejects actor registration layers that do not provide Registry", () => { // @ts-expect-error: actor registration layers require Registry but do not provide it. // @effect-diagnostics effect/missingLayerContext:off effect/floatingEffect:off diff --git a/rivetkit-typescript/packages/effect/src/Registry.test.ts b/rivetkit-typescript/packages/effect/src/Registry.test.ts index 0285b99d50..86913f0ef0 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test.ts @@ -1,6 +1,7 @@ import { assert, describe, it } from "@effect/vitest"; import { Effect, Layer } from "effect"; import { HttpEffect } from "effect/unstable/http"; +import { vi } from "vitest"; import * as Action from "./Action"; import * as Actor from "./Actor"; import * as Registry from "./Registry"; @@ -19,6 +20,7 @@ const RegistryLive = ActorsLayer.pipe( Layer.provideMerge( Registry.layer({ endpoint: "http://127.0.0.1:6420", + noWelcome: true, }), ), ); @@ -128,6 +130,23 @@ describe("Registry.toWebHandler", () => { } }); + it("does not print the welcome banner when disabled", async () => { + const log = vi.spyOn(console, "log").mockImplementation(() => {}); + const { handler, dispose } = Registry.toWebHandler(RegistryLive); + + try { + const response = await handler( + new Request("http://runner.test/api/rivet/metadata"), + ); + + assert.strictEqual(response.status, 200); + assert.strictEqual(log.mock.calls.length, 0); + } finally { + await dispose(); + log.mockRestore(); + } + }); + it("builds the registry layer once across requests", async () => { let builds = 0; const CountingRegistryLive = Layer.mergeAll( diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 11c34b42a5..1d65dddac7 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -1,10 +1,10 @@ -import { Context, Effect, Layer, Scope } from "effect"; +import { Context, Effect, Layer, type Scope } from "effect"; import { HttpEffect, - HttpMiddleware, - HttpServerError, - HttpServerRequest, - HttpServerResponse, + type HttpMiddleware, + type HttpServerError, + type HttpServerRequest, + type HttpServerResponse, } from "effect/unstable/http"; import * as Rivetkit from "rivetkit"; import * as Client from "./Client"; @@ -16,7 +16,7 @@ type ServerlessOptions = NonNullable< export type Options = Pick< Rivetkit.RegistryConfigInput, - "endpoint" | "token" | "namespace" + "endpoint" | "token" | "namespace" | "noWelcome" >; export interface Registry { @@ -175,16 +175,19 @@ export type ToWebHandlerOptions = ServerlessOptions & { readonly memoMap?: Layer.MemoMap | undefined; }; -const toWebHandlerServerlessOptions = ( +const toWebHandlerHandlerOptions = ( options?: ToWebHandlerOptions, ): ServerlessOptions | undefined => { if (options === undefined) { return undefined; } - const { middleware: _middleware, memoMap: _memoMap, ...serverless } = - options; - return serverless; + const { + middleware: _middleware, + memoMap: _memoMap, + ...handlerOptions + } = options; + return handlerOptions; }; /** @@ -204,7 +207,7 @@ export const toWebHandler = ( const registry = Context.get(context, Registry); return makeHttpEffect( registry, - toWebHandlerServerlessOptions(options), + toWebHandlerHandlerOptions(options), ); }), middleware: options?.middleware, From 124472c53f5cb1a9ced3bbcaca00100999799d50 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 15:55:45 +0200 Subject: [PATCH 238/306] refactor(effect): inline registry handler options --- .../packages/effect/src/Registry.ts | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 1d65dddac7..92e75cea86 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -175,21 +175,6 @@ export type ToWebHandlerOptions = ServerlessOptions & { readonly memoMap?: Layer.MemoMap | undefined; }; -const toWebHandlerHandlerOptions = ( - options?: ToWebHandlerOptions, -): ServerlessOptions | undefined => { - if (options === undefined) { - return undefined; - } - - const { - middleware: _middleware, - memoMap: _memoMap, - ...handlerOptions - } = options; - return handlerOptions; -}; - /** * Builds a Fetch-compatible request handler from a registry layer. * @@ -200,16 +185,25 @@ const toWebHandlerHandlerOptions = ( export const toWebHandler = ( registryLayer: Layer.Layer, options?: ToWebHandlerOptions, -) => - HttpEffect.toWebHandlerLayerWith(registryLayer, { +) => { + const { middleware, memoMap } = options ?? {}; + let serverlessOptions: ServerlessOptions | undefined; + if (options !== undefined) { + const { + middleware: _middleware, + memoMap: _memoMap, + ...handlerOptions + } = options; + serverlessOptions = handlerOptions; + } + + return HttpEffect.toWebHandlerLayerWith(registryLayer, { toHandler: (context) => Effect.sync(() => { const registry = Context.get(context, Registry); - return makeHttpEffect( - registry, - toWebHandlerHandlerOptions(options), - ); + return makeHttpEffect(registry, serverlessOptions); }), - middleware: options?.middleware, - memoMap: options?.memoMap, + middleware, + memoMap, }); +}; From 5df4deaf98e59347e37c0c1fbfdaa32840f579fc Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 16:03:23 +0200 Subject: [PATCH 239/306] docs(effect): showcase registry web handler --- examples/effect/src/main.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index aae501d53c..8bbf496bb9 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -24,3 +24,8 @@ const MainLayer = Registry.serve(ActorsLayer).pipe( // Keeps the layer alive. Tears down on SIGINT/SIGTERM. Layer.launch(MainLayer).pipe(NodeRuntime.runMain) + +// Or create a web handler, which can be used in serverless environments. +export const { handler, dispose } = Registry.toWebHandler( + ActorsLayer.pipe(Layer.provideMerge(Registry.layer())), +) From 5fba5dc0901a310e11f133a10d2ee29c5065b718 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Wed, 20 May 2026 16:40:10 +0200 Subject: [PATCH 240/306] feat(effect): add vitest coverage support --- pnpm-lock.yaml | 191 ++++++++++++++---- .../packages/effect/.gitignore | 1 + .../packages/effect/package.json | 4 +- .../packages/effect/vitest.config.ts | 4 + 4 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/.gitignore diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 332f158dce..b92a2357ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3369,7 +3369,7 @@ importers: version: 5.2.2(react-hook-form@7.62.0(react@19.1.0)) '@ladle/react': specifier: ^5.1.1 - version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) '@marsidev/react-turnstile': specifier: ^1.5.0 version: 1.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -3612,7 +3612,7 @@ importers: version: 2.4.3 better-auth: specifier: ^1.5.6 - version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) canvas-confetti: specifier: ^1.9.3 version: 1.9.3 @@ -3732,7 +3732,7 @@ importers: version: 5.2.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.13)(typescript@5.9.3))(typescript@5.9.3) unplugin-macros: specifier: ^0.18.3 - version: 0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + version: 0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.1.0) @@ -3754,7 +3754,7 @@ importers: version: 2.14.4(@types/node@20.19.13)(typescript@5.9.3) vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) frontend/packages/components: dependencies: @@ -4117,10 +4117,13 @@ importers: version: 0.85.1 '@effect/vitest': specifier: ^4.0.0-beta.66 - version: 4.0.0-beta.66(effect@4.0.0-beta.66)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.0.0-beta.66(effect@4.0.0-beta.66)(vitest@4.1.5) '@types/node': specifier: ^22.18.1 version: 22.19.15 + '@vitest/coverage-v8': + specifier: ^4.1.7 + version: 4.1.7(vitest@4.1.5) effect: specifier: ^4.0.0-beta.66 version: 4.0.0-beta.66 @@ -4132,7 +4135,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/coverage-v8@4.1.7)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) rivetkit-typescript/packages/engine-cli: {} @@ -5320,6 +5323,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-proposal-decorators@7.29.0': resolution: {integrity: sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==} engines: {node: '>=6.9.0'} @@ -5720,6 +5728,10 @@ packages: peerDependencies: '@bare-ts/lib': '>=0.3.0 <=0.4.0' + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@better-auth/core@1.5.6': resolution: {integrity: sha512-Ez9DZdIMFyxHremmoLz1emFPGNQomDC1jqqBPnZ6Ci+6TiGN3R9w/Y03cJn6I8r1ycKgOzeVMZtJ/erOZ27Gsw==} peerDependencies: @@ -10648,6 +10660,15 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/coverage-v8@4.1.7': + resolution: {integrity: sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==} + peerDependencies: + '@vitest/browser': 4.1.7 + vitest: 4.1.7 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@1.6.1': resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} @@ -10719,6 +10740,9 @@ packages: '@vitest/pretty-format@4.1.5': resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} + '@vitest/pretty-format@4.1.7': + resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + '@vitest/runner@1.6.1': resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} @@ -10779,6 +10803,9 @@ packages: '@vitest/utils@4.1.5': resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} + '@vitest/utils@4.1.7': + resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + '@volar/language-core@1.11.1': resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} @@ -11107,6 +11134,9 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} + ast-v8-to-istanbul@1.0.0: + resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} + astring@1.9.0: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true @@ -13704,6 +13734,9 @@ packages: resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} engines: {node: ^20.17.0 || >=22.9.0} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} @@ -14029,6 +14062,14 @@ packages: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -14132,6 +14173,9 @@ packages: js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -14562,10 +14606,17 @@ packages: magicast@0.5.1: resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==} + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -19479,6 +19530,10 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -19927,6 +19982,8 @@ snapshots: '@bare-ts/lib': 0.6.0 commander: 11.1.0 + '@bcoe/v8-coverage@1.0.2': {} + '@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0)': dependencies: '@better-auth/utils': 0.3.1 @@ -20229,10 +20286,10 @@ snapshots: - bufferutil - utf-8-validate - '@effect/vitest@4.0.0-beta.66(effect@4.0.0-beta.66)(vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)))': + '@effect/vitest@4.0.0-beta.66(effect@4.0.0-beta.66)(vitest@4.1.5)': dependencies: effect: 4.0.0-beta.66 - vitest: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/coverage-v8@4.1.7)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@emnapi/runtime@1.7.1': dependencies: @@ -21624,7 +21681,7 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': + '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': dependencies: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0 @@ -21636,8 +21693,8 @@ snapshots: '@ladle/react-context': 1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mdx-js/mdx': 3.1.1 '@mdx-js/react': 3.1.1(@types/react@19.2.13)(react@19.1.0) - '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) - '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) axe-core: 4.11.1 boxen: 8.0.1 chokidar: 4.0.3 @@ -21664,8 +21721,8 @@ snapshots: remark-gfm: 4.0.1 source-map: 0.7.6 vfile: 6.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - '@swc/helpers' - '@types/node' @@ -25738,11 +25795,11 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 - '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.27 '@swc/core': 1.15.11(@swc/helpers@0.5.17) - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@swc/helpers' @@ -25782,7 +25839,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -25790,7 +25847,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -25818,6 +25875,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/coverage-v8@4.1.7(vitest@4.1.5)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.7 + ast-v8-to-istanbul: 1.0.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.1 + std-env: 4.1.0 + tinyrainbow: 3.1.0 + vitest: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/coverage-v8@4.1.7)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/expect@1.6.1': dependencies: '@vitest/spy': 1.6.1 @@ -25846,7 +25917,7 @@ snapshots: '@vitest/spy': 4.0.18 '@vitest/utils': 4.0.18 chai: 6.2.2 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 '@vitest/expect@4.1.5': dependencies: @@ -25893,14 +25964,14 @@ snapshots: msw: 2.14.4(@types/node@22.19.15)(typescript@5.9.3) vite: 5.4.21(@types/node@22.19.15)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) - '@vitest/mocker@4.0.18(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.0.18(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.14.4(@types/node@20.19.13)(typescript@5.9.3) - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/mocker@4.1.5(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: @@ -25921,12 +25992,16 @@ snapshots: '@vitest/pretty-format@4.0.18': dependencies: - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 '@vitest/pretty-format@4.1.5': dependencies: tinyrainbow: 3.1.0 + '@vitest/pretty-format@4.1.7': + dependencies: + tinyrainbow: 3.1.0 + '@vitest/runner@1.6.1': dependencies: '@vitest/utils': 1.6.1 @@ -26023,7 +26098,7 @@ snapshots: '@vitest/utils@4.0.18': dependencies: '@vitest/pretty-format': 4.0.18 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 '@vitest/utils@4.1.5': dependencies: @@ -26031,6 +26106,12 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@vitest/utils@4.1.7': + dependencies: + '@vitest/pretty-format': 4.1.7 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@volar/language-core@1.11.1': dependencies: '@volar/source-map': 1.11.1 @@ -26445,6 +26526,12 @@ snapshots: dependencies: tslib: 2.8.1 + ast-v8-to-istanbul@1.0.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + astring@1.9.0: {} astro@5.16.9(@types/node@25.0.7)(idb-keyval@6.2.1)(ioredis@5.10.1)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(rollup@4.57.1)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): @@ -26754,7 +26841,7 @@ snapshots: bcryptjs@2.4.3: {} - better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0) '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0)) @@ -26781,7 +26868,7 @@ snapshots: pg: 8.17.2 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -29519,6 +29606,8 @@ snapshots: dependencies: lru-cache: 11.2.6 + html-escaper@2.0.2: {} + html-escaper@3.0.3: {} html-url-attributes@3.0.1: {} @@ -29807,6 +29896,17 @@ snapshots: transitivePeerDependencies: - supports-color + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -29930,6 +30030,8 @@ snapshots: js-base64@3.7.8: {} + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -30343,12 +30445,22 @@ snapshots: '@babel/types': 7.29.0 source-map-js: 1.2.1 + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + make-dir@2.1.0: dependencies: pify: 4.0.1 semver: 5.7.2 optional: true + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + make-error@1.3.6: {} makeerror@1.0.12: @@ -34496,13 +34608,13 @@ snapshots: unpipe@1.0.0: {} - unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: ast-kit: 2.2.0 magic-string-ast: 1.0.3 unplugin: 2.3.10 - vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-node: 5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-node: 5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -34792,13 +34904,13 @@ snapshots: - supports-color - terser - vite-node@5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite-node@5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: cac: 6.7.14 es-module-lexer: 1.7.0 obug: 2.0.0(ms@2.1.3) pathe: 2.0.3 - vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -34871,13 +34983,13 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - typescript @@ -34935,7 +35047,7 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 - vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -34946,7 +35058,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.6.1 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -34995,7 +35107,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vite@7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -35006,7 +35118,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.6.1 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -35300,10 +35412,10 @@ snapshots: - supports-color - terser - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.0.18(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -35320,7 +35432,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -35338,7 +35450,7 @@ snapshots: - tsx - yaml - vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/coverage-v8@4.1.7)(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.5 '@vitest/mocker': 4.1.5(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) @@ -35363,6 +35475,7 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 22.19.15 + '@vitest/coverage-v8': 4.1.7(vitest@4.1.5) transitivePeerDependencies: - msw diff --git a/rivetkit-typescript/packages/effect/.gitignore b/rivetkit-typescript/packages/effect/.gitignore new file mode 100644 index 0000000000..404abb2212 --- /dev/null +++ b/rivetkit-typescript/packages/effect/.gitignore @@ -0,0 +1 @@ +coverage/ diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index 867e67304f..4761da2a70 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -28,7 +28,8 @@ "build": "tsup src/mod.ts", "dev": "tsup src/mod.ts --watch", "check-types": "tsc --noEmit", - "test": "vitest --typecheck" + "test": "vitest --typecheck", + "coverage": "vitest run --coverage" }, "dependencies": { "rivetkit": "workspace:*" @@ -40,6 +41,7 @@ "@effect/language-service": "^0.85.1", "@effect/vitest": "^4.0.0-beta.66", "@types/node": "^22.18.1", + "@vitest/coverage-v8": "^4.1.7", "effect": "^4.0.0-beta.66", "tsup": "^8.4.0", "typescript": "^5.9.2", diff --git a/rivetkit-typescript/packages/effect/vitest.config.ts b/rivetkit-typescript/packages/effect/vitest.config.ts index e786fac908..e9c535bb4b 100644 --- a/rivetkit-typescript/packages/effect/vitest.config.ts +++ b/rivetkit-typescript/packages/effect/vitest.config.ts @@ -27,5 +27,9 @@ export default defineConfig({ fileParallelism: false, sequence: { concurrent: false }, globalSetup: ["./test/global-setup.ts"], + coverage: { + include: ["src/**/*.ts"], + exclude: ["*.test-d.ts"], + }, }, }); From 306037d6f889786f8cf84de1fc5bd8d0a2c73fe8 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 10:09:58 +0200 Subject: [PATCH 241/306] fix(rivetkit-core): preserve user error metadata --- .../packages/rivetkit-core/src/error.rs | 1 + .../tests/modules/action_dispatch_error.rs | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/rivetkit-rust/packages/rivetkit-core/src/error.rs b/rivetkit-rust/packages/rivetkit-core/src/error.rs index 5fc977ffbb..02fa6be186 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/error.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/error.rs @@ -4,6 +4,7 @@ use serde_json::Value as JsonValue; pub fn public_error_status_code(group: &str, code: &str) -> Option { match (group, code) { + ("user", _) => Some(400), ("auth", "forbidden") => Some(403), ("actor", "action_not_found") => Some(404), ("actor", "action_timed_out") => Some(408), diff --git a/rivetkit-rust/packages/rivetkit-core/tests/modules/action_dispatch_error.rs b/rivetkit-rust/packages/rivetkit-core/tests/modules/action_dispatch_error.rs index 027005f632..a716386bd3 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/modules/action_dispatch_error.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/modules/action_dispatch_error.rs @@ -29,6 +29,31 @@ fn preserves_public_error_message_for_client_boundary() { assert_eq!(error.client_message(), "action `missing` was not found"); } +#[test] +fn preserves_user_error_metadata_for_client_boundary() { + let metadata = serde_json::json!({ + "error": { + "_tag": "CounterOverflowError", + "limit": 20, + }, + }); + let error = ActionDispatchError::from_anyhow(anyhow::Error::new(RivetError { + kind: RivetErrorKind::Dynamic { + group: "user".to_owned(), + code: "Increment".to_owned(), + default_message: "count 25 would exceed limit 20".to_owned(), + }, + meta: serde_json::value::to_raw_value(&metadata).ok(), + message: None, + actor: None, + })); + + assert_eq!(error.group, "user"); + assert_eq!(error.code, "Increment"); + assert_eq!(error.client_message(), "count 25 would exceed limit 20"); + assert_eq!(error.client_metadata(), Some(&metadata)); +} + #[test] fn masks_private_structured_message_at_client_boundary() { static TEST_ERROR: RivetErrorSchema = RivetErrorSchema { From 9e0011bde6d9175800103cc33549766f14ab1af0 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 11:15:20 +0200 Subject: [PATCH 242/306] fix(effect): no-payload actions for raw clients --- .../packages/effect/src/Action.ts | 10 +++++++++ .../packages/effect/src/Actor.ts | 9 +++++++- .../packages/effect/test/e2e.test.ts | 21 ++++++++++++++++++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index a78f85371c..ffe1d55a96 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -17,6 +17,12 @@ export interface Action< readonly [TypeId]: typeof TypeId; readonly _tag: Tag; readonly key: string; + /** + * Raw RivetKit clients omit the argument for no-payload actions, so + * the actor wrapper uses this to adapt only those calls to the Effect + * JSON Void codec's null representation. + */ + readonly hasPayload: boolean; readonly payloadSchema: Payload; readonly successSchema: Success; readonly errorSchema: Error; @@ -40,6 +46,7 @@ export interface AnyWithProps { readonly [TypeId]: typeof TypeId; readonly _tag: string; readonly key: string; + readonly hasPayload: boolean; readonly payloadSchema: Schema.Top; readonly successSchema: Schema.Top; readonly errorSchema: Schema.Top; @@ -158,6 +165,7 @@ const makeProto = < Error extends Schema.Top, >(options: { readonly _tag: Tag; + readonly hasPayload: boolean; readonly payloadSchema: Payload; readonly successSchema: Success; readonly errorSchema: Error; @@ -207,6 +215,7 @@ export const make = < > => { const successSchema = options?.success ?? Schema.Void; const errorSchema = options?.error ?? Schema.Never; + const hasPayload = options?.payload !== undefined; const payloadSchema: Schema.Top = Schema.isSchema(options?.payload) ? (options?.payload as any) : options?.payload @@ -214,6 +223,7 @@ export const make = < : Schema.Void; return makeProto({ _tag: tag, + hasPayload, payloadSchema, successSchema, errorSchema, diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index f84d74dc5d..cc3d1e51ca 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -651,8 +651,15 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ] as ( envelope: ActionRequest, ) => Action.ResultFrom; + // Raw RivetKit clients call no-argument actions with an + // absent first argument. The Effect JSON Void codec expects + // null, so adapt only actions that declared no payload. + const payloadForDecode = + !action.hasPayload && payload === undefined + ? null + : payload; const decodedPayload = yield* decodePayload( - payload, + payloadForDecode, ).pipe(Effect.orDie); // The payload was decoded with this action's schema, // so this is the runtime boundary that restores the diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index b1bb9b6fe7..1628397f5a 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -2,6 +2,7 @@ import { assert, layer } from "@effect/vitest"; import { Registry, RivetError } from "@rivetkit/effect"; import { Effect, Layer, Schedule } from "effect"; import { TestClock } from "effect/testing"; +import { createClient } from "rivetkit/client"; import { inject } from "vitest"; import { BuildSetRejected, @@ -105,6 +106,24 @@ layer(TestLayer)("end-to-end", (it) => { }), ); + it.effect( + "accepts raw client no-arg calls for actions without payloads", + () => + Effect.gen(function* () { + const client = yield* Effect.acquireRelease( + Effect.sync(() => + createClient({ endpoint, token, namespace }), + ), + (client) => Effect.promise(() => client.dispose()), + ); + const counter = client.Counter.getOrCreate("t-raw-no-arg"); + assert.strictEqual( + yield* Effect.promise(() => counter.GetCount()), + 0, + ); + }), + ); + it.effect("isolates in-wake state across keys", () => Effect.gen(function* () { const client = yield* Counter.client; @@ -314,7 +333,7 @@ layer(TestLayer)("end-to-end", (it) => { }), ); - it.effect.skip( + it.effect( "surfaces an expected handler error back into the original error", () => Effect.gen(function* () { From dc1c737b0aec369a226a4256ddcf0e28e117cf04 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 11:16:08 +0200 Subject: [PATCH 243/306] feat(effect): add unique identifiers to clients and room instances --- examples/effect/src/client-raw.ts | 38 +++++++++++++++---------------- examples/effect/src/client.ts | 10 ++++---- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/examples/effect/src/client-raw.ts b/examples/effect/src/client-raw.ts index 9aad7f2ca0..acf24ca09e 100644 --- a/examples/effect/src/client-raw.ts +++ b/examples/effect/src/client-raw.ts @@ -1,34 +1,34 @@ -import { createClient } from "rivetkit/client" +import { createClient } from "rivetkit/client"; -const client = createClient("http://127.0.0.1:6420") as any +const client = createClient("http://127.0.0.1:6420") as any; async function main() { - const counter = client.Counter.getOrCreate("counter-raw") + const runId = crypto.randomUUID(); + const counter = client.Counter.getOrCreate(`counter-raw-${runId}`); - const initial = await counter.GetCount() - console.log("GetCount (initial):", initial) + const initial = await counter.GetCount(); + console.log("GetCount (initial):", initial); - const afterFive = await counter.Increment({ amount: 5 }) - console.log("Increment(5):", afterFive) + const afterFive = await counter.Increment({ amount: 5 }); + console.log("Increment(5):", afterFive); - const afterEight = await counter.Increment({ amount: 3 }) - console.log("Increment(3):", afterEight) + const afterEight = await counter.Increment({ amount: 3 }); + console.log("Increment(3):", afterEight); - const total = await counter.GetCount() - console.log("GetCount (total):", total) + const total = await counter.GetCount(); + console.log("GetCount (total):", total); // Trigger overflow (limit: 20). Plain client surfaces this as a - // thrown rivetkit RivetError; group should be "user" once typed - // errors are wired and "actor" otherwise. + // thrown rivetkit RivetError with Effect action-error metadata. try { - const overflowed = await counter.Increment({ amount: 20 }) - console.log("Increment(20) [unexpected success]:", overflowed) + const overflowed = await counter.Increment({ amount: 20 }); + console.log("Increment(20) [unexpected success]:", overflowed); } catch (err) { - console.log("Increment(20) [expected error]:", err) + console.log("Increment(20) [expected error]:", err); } } main().catch((err) => { - console.error("client smoke test failed:", err) - process.exit(1) -}) + console.error("client smoke test failed:", err); + process.exit(1); +}); diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 8b8b6c5438..98c9e2f06b 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,4 +1,4 @@ -import { Effect } from "effect"; +import { Effect, Random } from "effect"; import { Client } from "@rivetkit/effect"; import { Counter /*, IncrementBy */ } from "./actors/counter/api.ts"; import { ChatRoom } from "./actors/chat-room/api.ts"; @@ -6,8 +6,9 @@ import { Directory } from "./actors/directory/api.ts"; import { Moderator } from "./actors/moderator/api.ts"; const program = Effect.gen(function* () { + const runId = yield* Random.nextUUIDv4; const counterClient = yield* Counter.client; - const counter = counterClient.getOrCreate(["counter-effect"]); + const counter = counterClient.getOrCreate([`counter-effect-${runId}`]); const count = yield* counter.Increment({ amount: 5 }); yield* Effect.log(`Increment(5) -> ${count}`); @@ -19,11 +20,12 @@ const program = Effect.gen(function* () { const directoryClient = yield* Directory.client; const moderatorClient = yield* Moderator.client; - const room = chatRoomClient.getOrCreate(["effect-room"]); + const roomName = `effect-room-${runId}`; + const room = chatRoomClient.getOrCreate([roomName]); const directory = directoryClient.getOrCreate(["main"]); const moderator = moderatorClient.getOrCreate(["main"]); - yield* room.Initialize({ name: "effect-room" }); + yield* room.Initialize({ name: roomName }); yield* Effect.log(`ChatRoom.Initialize`); const member = yield* room.Join({ name: "Alice" }); From 9f77d025966777734dd2cbcbfb6998ed5dd79311 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 11:16:25 +0200 Subject: [PATCH 244/306] feat(effect): use Random.nextUUIDv4 for session ID generation --- examples/effect/src/actors/chat-room/live.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts index 81cb230863..71a1db5a56 100644 --- a/examples/effect/src/actors/chat-room/live.ts +++ b/examples/effect/src/actors/chat-room/live.ts @@ -1,4 +1,4 @@ -import { Effect, Schema } from "effect"; +import { Effect, Random, Schema } from "effect"; import { Actor, State } from "@rivetkit/effect"; import { db } from "rivetkit/db"; import { ChatRoom } from "./api.ts"; @@ -15,7 +15,7 @@ export const ChatRoomLive = ChatRoom.toLayer( const address = yield* Actor.CurrentAddress; // The plain SDK example stores this in createVars. The Effect SDK // does not expose vars yet, so the wake-scope closure owns it. - const sessionId = crypto.randomUUID(); + const sessionId = yield* Random.nextUUIDv4; yield* State.update(state, (current) => ({ ...current, From 8b1364e9e6834ad021436cecc44892d0a73dc13d Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 10:09:58 +0200 Subject: [PATCH 245/306] fix(rivetkit-core): preserve user error metadata --- .../packages/rivetkit-core/src/error.rs | 1 + .../packages/rivetkit-core/tests/context.rs | 2 ++ .../tests/modules/action_dispatch_error.rs | 23 +++++++++++++++++++ .../packages/rivetkit-core/tests/schedule.rs | 1 + .../packages/rivetkit-core/tests/sqlite.rs | 16 +++++++++++-- .../packages/rivetkit-core/tests/task.rs | 10 +++++++- 6 files changed, 50 insertions(+), 3 deletions(-) diff --git a/rivetkit-rust/packages/rivetkit-core/src/error.rs b/rivetkit-rust/packages/rivetkit-core/src/error.rs index 5fc977ffbb..02fa6be186 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/error.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/error.rs @@ -4,6 +4,7 @@ use serde_json::Value as JsonValue; pub fn public_error_status_code(group: &str, code: &str) -> Option { match (group, code) { + ("user", _) => Some(400), ("auth", "forbidden") => Some(403), ("actor", "action_not_found") => Some(404), ("actor", "action_timed_out") => Some(408), diff --git a/rivetkit-rust/packages/rivetkit-core/tests/context.rs b/rivetkit-rust/packages/rivetkit-core/tests/context.rs index 1cccea3680..000e8da61e 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/context.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/context.rs @@ -327,6 +327,7 @@ mod moved_tests { )), protocol_metadata: Arc::new(tokio::sync::Mutex::new(None)), shutting_down: std::sync::atomic::AtomicBool::new(false), + last_ping_ts: std::sync::atomic::AtomicI64::new(now_timestamp_ms()), stopped_tx: tokio::sync::watch::channel(true).0, }); shared @@ -373,6 +374,7 @@ mod moved_tests { )), protocol_metadata: Arc::new(tokio::sync::Mutex::new(None)), shutting_down: std::sync::atomic::AtomicBool::new(false), + last_ping_ts: std::sync::atomic::AtomicI64::new(now_timestamp_ms()), stopped_tx: tokio::sync::watch::channel(true).0, }); EnvoyHandle::from_shared(shared) diff --git a/rivetkit-rust/packages/rivetkit-core/tests/modules/action_dispatch_error.rs b/rivetkit-rust/packages/rivetkit-core/tests/modules/action_dispatch_error.rs index 027005f632..fff56cb0e2 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/modules/action_dispatch_error.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/modules/action_dispatch_error.rs @@ -29,6 +29,29 @@ fn preserves_public_error_message_for_client_boundary() { assert_eq!(error.client_message(), "action `missing` was not found"); } +#[test] +fn preserves_user_error_message_and_metadata_for_client_boundary() { + let metadata = serde_json::json!({ + "limit": 20, + "attempted": 25, + }); + let error = ActionDispatchError::from_anyhow(anyhow::Error::new(RivetError { + kind: RivetErrorKind::Dynamic { + group: "user".to_owned(), + code: "quota_exceeded".to_owned(), + default_message: "quota exceeded".to_owned(), + }, + meta: serde_json::value::to_raw_value(&metadata).ok(), + message: None, + actor: None, + })); + + assert_eq!(error.group, "user"); + assert_eq!(error.code, "quota_exceeded"); + assert_eq!(error.client_message(), "quota exceeded"); + assert_eq!(error.client_metadata(), Some(&metadata)); +} + #[test] fn masks_private_structured_message_at_client_boundary() { static TEST_ERROR: RivetErrorSchema = RivetErrorSchema { diff --git a/rivetkit-rust/packages/rivetkit-core/tests/schedule.rs b/rivetkit-rust/packages/rivetkit-core/tests/schedule.rs index 81cf3c6abb..663e2291f2 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/schedule.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/schedule.rs @@ -96,6 +96,7 @@ mod moved_tests { )), protocol_metadata: Arc::new(tokio::sync::Mutex::new(None)), shutting_down: AtomicBool::new(false), + last_ping_ts: std::sync::atomic::AtomicI64::new(now_timestamp_ms()), stopped_tx: tokio::sync::watch::channel(true).0, }); diff --git a/rivetkit-rust/packages/rivetkit-core/tests/sqlite.rs b/rivetkit-rust/packages/rivetkit-core/tests/sqlite.rs index 174c327657..99b4b44675 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/sqlite.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/sqlite.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; use std::sync::Arc; -use std::sync::atomic::AtomicBool; use std::sync::Mutex as StdMutex; +use std::sync::atomic::AtomicBool; +use std::time::{SystemTime, UNIX_EPOCH}; use super::*; use depot_client_types::{HEAD_FENCE_MISMATCH_CODE, HEAD_FENCE_MISMATCH_GROUP}; @@ -34,6 +35,13 @@ struct SqliteOperationLog { error_message: Option, } +fn now_timestamp_ms() -> i64 { + let duration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + i64::try_from(duration.as_millis()).unwrap_or(i64::MAX) +} + #[derive(Clone)] struct SqliteOperationLogLayer { records: Arc>>, @@ -173,6 +181,7 @@ fn test_envoy_handle() -> (EnvoyHandle, mpsc::UnboundedReceiver) ws_tx: Arc::new(AsyncMutex::new(None::>)), protocol_metadata: Arc::new(AsyncMutex::new(None)), shutting_down: AtomicBool::new(false), + last_ping_ts: std::sync::atomic::AtomicI64::new(now_timestamp_ms()), stopped_tx: tokio::sync::watch::channel(true).0, }); @@ -318,7 +327,10 @@ async fn remote_execute_logs_operation_context_at_source() { let result = db .execute( "SELECT ?", - Some(vec![BindParam::Integer(1), BindParam::Text("two".to_owned())]), + Some(vec![ + BindParam::Integer(1), + BindParam::Text("two".to_owned()), + ]), ) .await; diff --git a/rivetkit-rust/packages/rivetkit-core/tests/task.rs b/rivetkit-rust/packages/rivetkit-core/tests/task.rs index 6b7e2e0b9f..09cef4af2e 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/task.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/task.rs @@ -6,7 +6,7 @@ mod moved_tests { use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Mutex, OnceLock}; use std::task::Poll; - use std::time::{Duration, Instant}; + use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use futures::{FutureExt, poll}; use rivet_envoy_client::config::{ @@ -52,6 +52,13 @@ mod moved_tests { use crate::{ActorConfig, ActorContext, ActorFactory}; use rivet_envoy_client::utils::EnvoyShutdownError; + fn now_timestamp_ms() -> i64 { + let duration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + i64::try_from(duration.as_millis()).unwrap_or(i64::MAX) + } + fn test_hook_lock() -> &'static AsyncMutex<()> { static LOCK: OnceLock> = OnceLock::new(); LOCK.get_or_init(|| AsyncMutex::new(())) @@ -254,6 +261,7 @@ mod moved_tests { )), protocol_metadata: Arc::new(tokio::sync::Mutex::new(None)), shutting_down: AtomicBool::new(false), + last_ping_ts: std::sync::atomic::AtomicI64::new(now_timestamp_ms()), stopped_tx: tokio::sync::watch::channel(true).0, }); From bfb13ca8f10173c2234c22f67eb58f00b421011f Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 12:08:52 +0200 Subject: [PATCH 246/306] test(effect): unskip codec services e2e --- rivetkit-typescript/packages/effect/test/e2e.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 1628397f5a..74cc2a6ca2 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -636,7 +636,7 @@ layer(TestLayer)("end-to-end", (it) => { }), ); - it.effect.skip( + it.effect( "runs encoding/decoding services for an action's payload, success, and error", () => Effect.gen(function* () { From 56927a014b8b223fc8db8480b1fd10e8cfcc9240 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 12:39:32 +0200 Subject: [PATCH 247/306] chore(effect-example): use schema-native time types --- examples/effect/src/actors/chat-room/api.ts | 10 ++-- examples/effect/src/actors/chat-room/live.ts | 35 ++++++++---- examples/effect/src/actors/directory/api.ts | 5 +- examples/effect/src/actors/directory/live.ts | 60 +++++++++++--------- 4 files changed, 66 insertions(+), 44 deletions(-) diff --git a/examples/effect/src/actors/chat-room/api.ts b/examples/effect/src/actors/chat-room/api.ts index b4d0028251..ee567f9d54 100644 --- a/examples/effect/src/actors/chat-room/api.ts +++ b/examples/effect/src/actors/chat-room/api.ts @@ -3,20 +3,20 @@ import { Action, Actor } from "@rivetkit/effect"; export const Member = Schema.Struct({ name: Schema.String, - joinedAt: Schema.Number, + joinedAt: Schema.DateTimeUtc, }); export const Message = Schema.Struct({ id: Schema.Number, sender: Schema.String, text: Schema.String, - createdAt: Schema.Number, + createdAt: Schema.DateTimeUtc, }); export const SendMessageResult = Schema.Struct({ ok: Schema.Boolean, reason: Schema.optionalKey(Schema.String), - createdAt: Schema.optionalKey(Schema.Number), + createdAt: Schema.optionalKey(Schema.DateTimeUtc), }); // The plain RivetKit example uses createState input to name the room at @@ -54,10 +54,10 @@ export const GetMembers = Action.make("GetMembers", { export const ScheduleAnnouncement = Action.make("ScheduleAnnouncement", { payload: { text: Schema.String, - delayMs: Schema.Number, + delay: Schema.DurationFromMillis, }, success: Schema.Struct({ - firesAt: Schema.Number, + firesAt: Schema.DateTimeUtc, }), }); diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts index 71a1db5a56..0a8a036d7b 100644 --- a/examples/effect/src/actors/chat-room/live.ts +++ b/examples/effect/src/actors/chat-room/live.ts @@ -1,4 +1,4 @@ -import { Effect, Random, Schema } from "effect"; +import { DateTime, Duration, Effect, Random, Schema } from "effect"; import { Actor, State } from "@rivetkit/effect"; import { db } from "rivetkit/db"; import { ChatRoom } from "./api.ts"; @@ -74,9 +74,10 @@ export const ChatRoomLive = ChatRoom.toLayer( }), Join: ({ payload }) => Effect.gen(function* () { + const joinedAt = yield* DateTime.now; const member = { name: payload.name, - joinedAt: Date.now(), + joinedAt, }; const next = yield* State.updateAndGet( state, @@ -87,7 +88,10 @@ export const ChatRoomLive = ChatRoom.toLayer( ); rawRivetkitContext.broadcast("memberJoined", { - member, + member: { + ...member, + joinedAt: DateTime.formatIso(member.joinedAt), + }, }); if (next.name !== "") { @@ -129,20 +133,20 @@ export const ChatRoomLive = ChatRoom.toLayer( return { ok: false, reason: verdict.reason }; } - const createdAt = Date.now(); + const createdAt = yield* DateTime.now; yield* Effect.tryPromise(() => database.execute( "INSERT INTO messages (sender, text, created_at) VALUES (?, ?, ?)", payload.sender, payload.text, - createdAt, + DateTime.toEpochMillis(createdAt), ), ).pipe(Effect.orDie); rawRivetkitContext.broadcast("newMessage", { sender: payload.sender, text: payload.text, - createdAt, + createdAt: DateTime.formatIso(createdAt), }); return { ok: true, createdAt }; }), @@ -156,7 +160,15 @@ export const ChatRoomLive = ChatRoom.toLayer( }>( "SELECT id, sender, text, created_at as createdAt FROM messages ORDER BY id", ), - ).pipe(Effect.orDie), + ).pipe( + Effect.map((rows) => + rows.map((row) => ({ + ...row, + createdAt: DateTime.makeUnsafe(row.createdAt), + })), + ), + Effect.orDie, + ), GetMembers: () => State.get(state).pipe( Effect.orDie, @@ -164,11 +176,14 @@ export const ChatRoomLive = ChatRoom.toLayer( ), ScheduleAnnouncement: ({ payload }) => Effect.sync(() => { - const firesAt = Date.now() + payload.delayMs; + const firesAt = DateTime.addDuration( + DateTime.nowUnsafe(), + payload.delay, + ); // The raw scheduler dispatches the Effect action by name // with the same object payload that a client would send. rawRivetkitContext.schedule.after( - payload.delayMs, + Duration.toMillis(payload.delay), "TriggerAnnouncement", { text: payload.text, @@ -205,7 +220,7 @@ export const ChatRoomLive = ChatRoom.toLayer( members: Schema.Array( Schema.Struct({ name: Schema.String, - joinedAt: Schema.Number, + joinedAt: Schema.DateTimeUtc, }), ), wakeCount: Schema.Number, diff --git a/examples/effect/src/actors/directory/api.ts b/examples/effect/src/actors/directory/api.ts index afe46923c6..7948e7bc4e 100644 --- a/examples/effect/src/actors/directory/api.ts +++ b/examples/effect/src/actors/directory/api.ts @@ -3,8 +3,8 @@ import { Action, Actor } from "@rivetkit/effect"; export const RoomEntry = Schema.Struct({ name: Schema.String, - openedAt: Schema.Number, - closedAt: Schema.optionalKey(Schema.Number), + openedAt: Schema.DateTimeUtc, + closedAt: Schema.optionalKey(Schema.DateTimeUtc), }); export const RegisterRoom = Action.make("RegisterRoom", { @@ -22,4 +22,3 @@ export const ListRooms = Action.make("ListRooms", { export const Directory = Actor.make("directory", { actions: [RegisterRoom, CloseRoom, ListRooms], }); - diff --git a/examples/effect/src/actors/directory/live.ts b/examples/effect/src/actors/directory/live.ts index 8778cad0ce..bee2ad2aa3 100644 --- a/examples/effect/src/actors/directory/live.ts +++ b/examples/effect/src/actors/directory/live.ts @@ -1,6 +1,6 @@ -import { Effect, Schema } from "effect"; +import { DateTime, Effect, Schema } from "effect"; import { State } from "@rivetkit/effect"; -import { Directory } from "./api.ts"; +import { Directory, RoomEntry } from "./api.ts"; export const DirectoryLive = Directory.toLayer( ({ state }) => @@ -10,30 +10,38 @@ export const DirectoryLive = Directory.toLayer( // State writes go through Effect Schema validation. This // example treats schema failures as defects instead of adding // typed error channels to the action contract. - State.update(state, (current) => { - if ( - current.rooms.some( - (room) => room.name === payload.name, - ) - ) { - return current; - } + Effect.gen(function* () { + const openedAt = yield* DateTime.now; - return { - rooms: [ - ...current.rooms, - { name: payload.name, openedAt: Date.now() }, - ], - }; - }).pipe(Effect.orDie), + yield* State.update(state, (current) => { + if ( + current.rooms.some( + (room) => room.name === payload.name, + ) + ) { + return current; + } + + return { + rooms: [ + ...current.rooms, + { name: payload.name, openedAt }, + ], + }; + }).pipe(Effect.orDie); + }), CloseRoom: ({ payload }) => - State.update(state, (current) => ({ - rooms: current.rooms.map((room) => - room.name === payload.name - ? { ...room, closedAt: Date.now() } - : room, - ), - })).pipe(Effect.orDie), + Effect.gen(function* () { + const closedAt = yield* DateTime.now; + + yield* State.update(state, (current) => ({ + rooms: current.rooms.map((room) => + room.name === payload.name + ? { ...room, closedAt } + : room, + ), + })).pipe(Effect.orDie); + }), ListRooms: () => State.get(state).pipe( Effect.orDie, @@ -47,8 +55,8 @@ export const DirectoryLive = Directory.toLayer( rooms: Schema.Array( Schema.Struct({ name: Schema.String, - openedAt: Schema.Number, - closedAt: Schema.optionalKey(Schema.Number), + openedAt: Schema.DateTimeUtc, + closedAt: Schema.optionalKey(Schema.DateTimeUtc), }), ), }), From 1c9d8bd1f7d5c0d85326e87d461a33a2edaf764c Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 13:18:45 +0200 Subject: [PATCH 248/306] refactor(effect): inline `makeStateOptionsCodec` logic --- .../packages/effect/src/Actor.ts | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index cc3d1e51ca..b8b6e054e1 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -18,12 +18,12 @@ import { } from "effect"; import * as Rivetkit from "rivetkit"; import type * as RivetkitDb from "rivetkit/db"; -import { hasStringProperty } from "./internal/utils"; import type * as Action from "./Action"; import * as Client from "./Client"; import * as ActionErrorEnvelope from "./internal/ActionErrorEnvelope"; import type * as StateOptions from "./internal/StateOptions"; import { readTraceMeta, rpcSystem } from "./internal/tracing"; +import { hasStringProperty } from "./internal/utils"; import * as Registry from "./Registry"; import type * as RivetError from "./RivetError"; import * as State from "./State"; @@ -158,18 +158,6 @@ type StateOptionsCodec = { >; }; -const makeStateOptionsCodec = ( - state: State, -): StateOptionsCodec => { - const schema = state.schema as State["schema"]; - - return { - decode: Schema.decodeEffect(schema), - decodeUnknown: Schema.decodeUnknownEffect(schema), - encode: Schema.encodeEffect(schema), - }; -}; - type RivetkitActorDefinitionFor< State extends StateOptions.Any, Database extends RivetkitDb.AnyDatabaseProvider, @@ -524,10 +512,11 @@ const makeRivetkitActor = Effect.fnUntraced(function* < const services = yield* Effect.context(); const { effectOptions, rivetkitOptions } = splitOptions(options); - const stateCodec = UndefinedOr.map( - effectOptions.state, - makeStateOptionsCodec, - ); + const stateCodec = UndefinedOr.map(effectOptions.state, (state) => ({ + decode: Schema.decodeEffect(state.schema), + decodeUnknown: Schema.decodeUnknownEffect(state.schema), + encode: Schema.encodeEffect(state.schema), + })); const instances = MutableHashMap.empty< string, From cfa96c1b0ff3343f64cb4f01ef724bde6860c634 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 13:41:58 +0200 Subject: [PATCH 249/306] fix(effect): encode actor state as json --- .../packages/effect/src/Actor.ts | 9 +++---- .../packages/effect/test/e2e.test.ts | 11 ++++++++- .../packages/effect/test/fixtures/actors.ts | 24 ++++++++++++------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index b8b6e054e1..cd9e8aac8c 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -513,9 +513,10 @@ const makeRivetkitActor = Effect.fnUntraced(function* < const { effectOptions, rivetkitOptions } = splitOptions(options); const stateCodec = UndefinedOr.map(effectOptions.state, (state) => ({ - decode: Schema.decodeEffect(state.schema), - decodeUnknown: Schema.decodeUnknownEffect(state.schema), - encode: Schema.encodeEffect(state.schema), + decodeUnknown: Schema.decodeUnknownEffect( + Schema.toCodecJson(state.schema), + ), + encode: Schema.encodeEffect(Schema.toCodecJson(state.schema)), })); const instances = MutableHashMap.empty< @@ -550,7 +551,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < // context satisfies them at runtime, so we erase // R at the boundary. ((yield* State.make( - () => stateCodec.decode(c.state), + () => stateCodec.decodeUnknown(c.state), (next) => stateCodec.encode(next).pipe( Effect.tap((encoded) => diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 74cc2a6ca2..0b2fbf86bd 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -1,6 +1,6 @@ import { assert, layer } from "@effect/vitest"; import { Registry, RivetError } from "@rivetkit/effect"; -import { Effect, Layer, Schedule } from "effect"; +import { DateTime, Effect, Layer, Schedule } from "effect"; import { TestClock } from "effect/testing"; import { createClient } from "rivetkit/client"; import { inject } from "vitest"; @@ -397,6 +397,7 @@ layer(TestLayer)("end-to-end", (it) => { ["t-raw-transformed-state"], ); const when = new Date("2024-04-05T06:07:08.000Z"); + const instant = DateTime.makeUnsafe(1_712_298_428_000); const at = new Date("2024-04-06T07:08:09.000Z"); const bytes = new Uint8Array([9, 8, 7]); const payload = new Uint8Array([6, 5, 4]); @@ -407,6 +408,7 @@ layer(TestLayer)("end-to-end", (it) => { yield* actor.SetTransformedStateAndSleep({ when, + instant, url, id, bytes, @@ -424,6 +426,7 @@ layer(TestLayer)("end-to-end", (it) => { assert.deepEqual(raw, { when: when.toISOString(), + instant: DateTime.formatIso(instant), url: url.toString(), id: id.toString(), bytes: Buffer.from(bytes).toString("base64"), @@ -446,6 +449,7 @@ layer(TestLayer)("end-to-end", (it) => { ["t-raw-set-transformed-state"], ); const when = "2024-05-06T07:08:09.000Z"; + const instant = "2024-05-06T07:08:09.123Z"; const at = "2024-05-07T08:09:10.000Z"; const url = "https://rivet.dev/docs/actors/state?source=raw"; const id = "9007199254740995"; @@ -458,6 +462,7 @@ layer(TestLayer)("end-to-end", (it) => { yield* actor.SetRawWakeStateAndSleep({ when, + instant, url, id, bytes, @@ -474,6 +479,10 @@ layer(TestLayer)("end-to-end", (it) => { ); assert.strictEqual(decoded.when.toISOString(), when); + assert.strictEqual( + DateTime.toEpochMillis(decoded.instant), + Date.parse(instant), + ); assert.strictEqual(decoded.url.toString(), url); assert.strictEqual(decoded.id, BigInt(id)); assert.deepEqual( diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts index 9118114bb6..ec52ac173e 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts @@ -1,6 +1,7 @@ import { Action, Actor, State } from "@rivetkit/effect"; import { Context, + DateTime, Effect, Layer, Option, @@ -182,6 +183,7 @@ export const CountEvents = Action.make("CountEvents", { const EncodedTransformedState = Schema.Struct({ when: Schema.String, + instant: Schema.String, url: Schema.String, id: Schema.String, bytes: Schema.String, @@ -195,15 +197,16 @@ const EncodedTransformedState = Schema.Struct({ }); const TransformedStateSchema = Schema.Struct({ - when: Schema.DateFromString, - url: Schema.URLFromString, - id: Schema.BigIntFromString, - bytes: Schema.Uint8ArrayFromBase64, + when: Schema.Date, + instant: Schema.DateTimeUtc, + url: Schema.URL, + id: Schema.BigInt, + bytes: Schema.Uint8Array, tags: TagsCsv, history: Schema.Array( Schema.Struct({ - at: Schema.DateFromString, - payload: Schema.Uint8ArrayFromBase64, + at: Schema.Date, + payload: Schema.Uint8Array, }), ), }); @@ -243,13 +246,17 @@ export const TransformedStateActorLive = TransformedStateActor.toLayer( const rawWakeState = rawRivetkitContext.state; return TransformedStateActor.of({ - GetRawWakeState: () => Effect.succeed(rawWakeState), + GetRawWakeState: () => + Effect.succeed( + rawWakeState as unknown as typeof EncodedTransformedState.Type, + ), GetDecodedState: () => State.get(state), SetTransformedStateAndSleep: ({ payload }) => State.set(state, payload).pipe(Effect.andThen(sleep)), SetRawWakeStateAndSleep: ({ payload }) => Effect.tryPromise(async () => { - rawRivetkitContext.state = payload; + rawRivetkitContext.state = + payload as unknown as typeof rawRivetkitContext.state; await rawRivetkitContext.saveState({ immediate: true, }); @@ -262,6 +269,7 @@ export const TransformedStateActorLive = TransformedStateActor.toLayer( schema: TransformedStateSchema, initialValue: () => ({ when: new Date("2024-01-01T00:00:00.000Z"), + instant: DateTime.makeUnsafe(1_704_067_200_000), url: new URL("https://rivet.dev/docs"), id: 1n, bytes: new Uint8Array([1, 2, 3]), From 52e2f850901905fbdd8cf5fff29ca430f82c9ea8 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 13:49:59 +0200 Subject: [PATCH 250/306] fix(effect-example): replace `log` with `logError` for error handling --- examples/effect/src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 98c9e2f06b..af53fda2cf 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -74,7 +74,7 @@ const program = Effect.gen(function* () { yield* Effect.log(`Increment(100) [unexpected success]: ${overflowed}`); }).pipe( Effect.catchTag("CounterOverflowError", (e) => - Effect.log( + Effect.logError( `CounterOverflowError caught: limit=${e.limit} message="${e.message}"`, ), ), From 0af342883b660c86f74cc58edc9ee19a5b4c2e42 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 13:57:06 +0200 Subject: [PATCH 251/306] test(effect): add stricter assertions for CounterOverflowError properties --- rivetkit-typescript/packages/effect/test/e2e.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 0b2fbf86bd..8706f3fa28 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -346,6 +346,12 @@ layer(TestLayer)("end-to-end", (it) => { assert.isTrue(exit._tag === "Success"); if (exit._tag === "Success") { assert.instanceOf(exit.value, CounterOverflowError); + assert.instanceOf(exit.value, Error); + assert.strictEqual( + exit.value.constructor, + CounterOverflowError, + ); + assert.strictEqual(exit.value._tag, "CounterOverflowError"); assert.strictEqual(exit.value.limit, 20); assert.match(exit.value.message, /exceed limit 20/); } From 8883f838f59feb4f2eb027f77713f731de1d8e81 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 14:09:36 +0200 Subject: [PATCH 252/306] fix(effect-example): add Logger layer for improved error handling --- examples/effect/src/client.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index af53fda2cf..b1986d5074 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,4 +1,4 @@ -import { Effect, Random } from "effect"; +import { Effect, Logger, Random } from "effect"; import { Client } from "@rivetkit/effect"; import { Counter /*, IncrementBy */ } from "./actors/counter/api.ts"; import { ChatRoom } from "./actors/chat-room/api.ts"; @@ -81,8 +81,10 @@ const program = Effect.gen(function* () { ); const ClientLayer = Client.layer({ endpoint: "http://127.0.0.1:6420" }); +const LoggerLayer = Logger.layer([Logger.consolePretty()]); -program.pipe(Effect.provide(ClientLayer), Effect.runPromise).catch((err) => { - console.error("client failed:", err); +Effect.runPromise( + program.pipe(Effect.provide(ClientLayer), Effect.provide(LoggerLayer)), +).catch(() => { process.exit(1); }); From 90abb0d5de4296349abb71502dec3bd814e48649 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 14:31:22 +0200 Subject: [PATCH 253/306] feat(effect): use actor client in actors --- examples/effect/src/actors/chat-room/api.ts | 2 +- examples/effect/src/actors/chat-room/live.ts | 43 ++++++-------------- examples/effect/src/actors/counter/api.ts | 2 +- examples/effect/src/actors/counter/live.ts | 2 +- examples/effect/src/actors/moderator/live.ts | 2 +- examples/effect/src/main.ts | 22 +++++----- 6 files changed, 27 insertions(+), 46 deletions(-) diff --git a/examples/effect/src/actors/chat-room/api.ts b/examples/effect/src/actors/chat-room/api.ts index ee567f9d54..fddf5953cf 100644 --- a/examples/effect/src/actors/chat-room/api.ts +++ b/examples/effect/src/actors/chat-room/api.ts @@ -1,5 +1,5 @@ -import { Schema } from "effect"; import { Action, Actor } from "@rivetkit/effect"; +import { Schema } from "effect"; export const Member = Schema.Struct({ name: Schema.String, diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts index 0a8a036d7b..cae6b5e2d3 100644 --- a/examples/effect/src/actors/chat-room/live.ts +++ b/examples/effect/src/actors/chat-room/live.ts @@ -1,18 +1,19 @@ -import { DateTime, Duration, Effect, Random, Schema } from "effect"; import { Actor, State } from "@rivetkit/effect"; +import { DateTime, Duration, Effect, Random, Schema } from "effect"; import { db } from "rivetkit/db"; +import { Directory, Moderator } from "../mod.ts"; import { ChatRoom } from "./api.ts"; -interface ModerationVerdict { - readonly approved: boolean; - readonly reason?: string; -} - export const ChatRoomLive = ChatRoom.toLayer( ({ rawRivetkitContext, state }) => Effect.gen(function* () { const database = rawRivetkitContext.db; const address = yield* Actor.CurrentAddress; + const moderatorClient = yield* Moderator.client; + const directoryClient = yield* Directory.client; + + const directory = directoryClient.getOrCreate(["main"]); + const moderator = moderatorClient.getOrCreate(["main"]); // The plain SDK example stores this in createVars. The Effect SDK // does not expose vars yet, so the wake-scope closure owns it. const sessionId = yield* Random.nextUUIDv4; @@ -41,19 +42,6 @@ export const ChatRoomLive = ChatRoom.toLayer( }), ); - const directory = () => - // Server-side Effect actor clients are not available yet. Use the - // raw RivetKit actor client and keep the action shape explicit. - rawRivetkitContext - .client() - .directory.getOrCreate(["main"]); - const moderator = () => - // The normal example uses a typed registry client here. This raw - // client keeps the runtime behavior while giving up type inference. - rawRivetkitContext - .client() - .moderator.getOrCreate(["main"]); - const roomName = State.get(state).pipe( Effect.orDie, Effect.map((s) => s.name), @@ -97,9 +85,7 @@ export const ChatRoomLive = ChatRoom.toLayer( if (next.name !== "") { // Directory registration is still actor-to-actor RPC, but // it uses the Effect action name and object payload. - yield* Effect.tryPromise(() => - directory().RegisterRoom({ name: next.name }), - ).pipe(Effect.orDie); + yield* directory.RegisterRoom({ name: next.name }); } return member; @@ -122,12 +108,9 @@ export const ChatRoomLive = ChatRoom.toLayer( // completable queue drained by run(). The Effect SDK does // not expose queues or run loops yet, so moderation is a // direct actor RPC and has no queue timeout path. - const verdict = yield* Effect.tryPromise( - () => - moderator().Review({ - text: payload.text, - }) as Promise, - ).pipe(Effect.orDie); + const verdict = yield* moderator.Review({ + text: payload.text, + }); if (!verdict.approved) { return { ok: false, reason: verdict.reason }; @@ -203,9 +186,7 @@ export const ChatRoomLive = ChatRoom.toLayer( if (name !== "") { // This only covers destruction through Archive. A future // Effect onDestroy hook would cover every destroy path. - yield* Effect.tryPromise(() => - directory().CloseRoom({ name }), - ).pipe(Effect.orDie); + yield* directory.CloseRoom({ name }); } yield* Effect.sync(() => { rawRivetkitContext.destroy(); diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts index 19cb35024e..a8fa0ac65b 100644 --- a/examples/effect/src/actors/counter/api.ts +++ b/examples/effect/src/actors/counter/api.ts @@ -1,5 +1,5 @@ +import { Action, Actor } from "@rivetkit/effect"; import { Schema } from "effect"; -import { Actor, Action } from "@rivetkit/effect"; // --- Errors --- diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts index f233d7f437..993df3850a 100644 --- a/examples/effect/src/actors/counter/live.ts +++ b/examples/effect/src/actors/counter/live.ts @@ -1,5 +1,5 @@ -import { Effect, Schema } from "effect"; import { Actor, State } from "@rivetkit/effect"; +import { Effect, Schema } from "effect"; import { Counter, CounterOverflowError } from "./api.ts"; // --- Actor Implementation --- diff --git a/examples/effect/src/actors/moderator/live.ts b/examples/effect/src/actors/moderator/live.ts index 61e9535561..68182ff2d2 100644 --- a/examples/effect/src/actors/moderator/live.ts +++ b/examples/effect/src/actors/moderator/live.ts @@ -1,5 +1,5 @@ -import { Effect, Schema } from "effect"; import { State } from "@rivetkit/effect"; +import { Effect, Schema } from "effect"; import { Moderator } from "./api.ts"; export const ModeratorLive = Moderator.toLayer( diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index 8bbf496bb9..d09e8c80fb 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -1,17 +1,17 @@ -import { Layer } from "effect" -import { NodeRuntime } from "@effect/platform-node" -import { Registry } from "@rivetkit/effect" -import { CounterLive } from "./actors/counter/live.ts" -import { ChatRoomLive } from "./actors/chat-room/live.ts" -import { DirectoryLive } from "./actors/directory/live.ts" -import { ModeratorLive } from "./actors/moderator/live.ts" +import { Layer } from "effect"; +import { NodeRuntime } from "@effect/platform-node"; +import { Registry, Client } from "@rivetkit/effect"; +import { CounterLive } from "./actors/counter/live.ts"; +import { ChatRoomLive } from "./actors/chat-room/live.ts"; +import { DirectoryLive } from "./actors/directory/live.ts"; +import { ModeratorLive } from "./actors/moderator/live.ts"; const ActorsLayer = Layer.mergeAll( CounterLive, DirectoryLive, ModeratorLive, ChatRoomLive, -) +).pipe(Layer.provide(Client.layer({ endpoint: "http://127.0.0.1:6420" }))); // Engine config defaults to spawning a local rivet-engine process and // listening on http://127.0.0.1:6420 (override via RIVET_ENDPOINT to @@ -20,12 +20,12 @@ const ActorsLayer = Layer.mergeAll( // RIVET_ENGINE_BINARY=$(pwd)/target/debug/rivet-engine pnpm start const MainLayer = Registry.serve(ActorsLayer).pipe( Layer.provide(Registry.layer()), -) +); // Keeps the layer alive. Tears down on SIGINT/SIGTERM. -Layer.launch(MainLayer).pipe(NodeRuntime.runMain) +Layer.launch(MainLayer).pipe(NodeRuntime.runMain); // Or create a web handler, which can be used in serverless environments. export const { handler, dispose } = Registry.toWebHandler( ActorsLayer.pipe(Layer.provideMerge(Registry.layer())), -) +); From 8a52b1b3585762486956f016376e529dbb9f790d Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 15:58:17 +0200 Subject: [PATCH 254/306] fix(effect-example): handle moderation errors --- examples/effect/src/actors/chat-room/api.ts | 9 +---- examples/effect/src/actors/chat-room/live.ts | 11 +----- examples/effect/src/actors/directory/api.ts | 2 +- examples/effect/src/actors/directory/live.ts | 7 +--- examples/effect/src/actors/moderator/api.ts | 15 ++++---- examples/effect/src/actors/moderator/live.ts | 35 +++++++---------- examples/effect/src/client.ts | 40 ++++++++++++-------- examples/effect/src/main.ts | 4 +- 8 files changed, 55 insertions(+), 68 deletions(-) diff --git a/examples/effect/src/actors/chat-room/api.ts b/examples/effect/src/actors/chat-room/api.ts index fddf5953cf..39aa41978b 100644 --- a/examples/effect/src/actors/chat-room/api.ts +++ b/examples/effect/src/actors/chat-room/api.ts @@ -1,5 +1,6 @@ import { Action, Actor } from "@rivetkit/effect"; import { Schema } from "effect"; +import { BannerWordsError } from "../mod"; export const Member = Schema.Struct({ name: Schema.String, @@ -13,12 +14,6 @@ export const Message = Schema.Struct({ createdAt: Schema.DateTimeUtc, }); -export const SendMessageResult = Schema.Struct({ - ok: Schema.Boolean, - reason: Schema.optionalKey(Schema.String), - createdAt: Schema.optionalKey(Schema.DateTimeUtc), -}); - // The plain RivetKit example uses createState input to name the room at // creation time. The Effect SDK does not expose create input yet, so this // action initializes the persisted room state explicitly after getOrCreate. @@ -40,7 +35,7 @@ export const SendMessage = Action.make("SendMessage", { sender: Schema.String, text: Schema.String, }, - success: SendMessageResult, + error: BannerWordsError, }); export const GetHistory = Action.make("GetHistory", { diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts index cae6b5e2d3..932aa2d3a7 100644 --- a/examples/effect/src/actors/chat-room/live.ts +++ b/examples/effect/src/actors/chat-room/live.ts @@ -104,18 +104,10 @@ export const ChatRoomLive = ChatRoom.toLayer( }), SendMessage: ({ payload }) => Effect.gen(function* () { - // The normal example sends moderation work through a - // completable queue drained by run(). The Effect SDK does - // not expose queues or run loops yet, so moderation is a - // direct actor RPC and has no queue timeout path. - const verdict = yield* moderator.Review({ + yield* moderator.Review({ text: payload.text, }); - if (!verdict.approved) { - return { ok: false, reason: verdict.reason }; - } - const createdAt = yield* DateTime.now; yield* Effect.tryPromise(() => database.execute( @@ -131,7 +123,6 @@ export const ChatRoomLive = ChatRoom.toLayer( text: payload.text, createdAt: DateTime.formatIso(createdAt), }); - return { ok: true, createdAt }; }), GetHistory: () => Effect.tryPromise(() => diff --git a/examples/effect/src/actors/directory/api.ts b/examples/effect/src/actors/directory/api.ts index 7948e7bc4e..1dd856fc22 100644 --- a/examples/effect/src/actors/directory/api.ts +++ b/examples/effect/src/actors/directory/api.ts @@ -1,5 +1,5 @@ -import { Schema } from "effect"; import { Action, Actor } from "@rivetkit/effect"; +import { Schema } from "effect"; export const RoomEntry = Schema.Struct({ name: Schema.String, diff --git a/examples/effect/src/actors/directory/live.ts b/examples/effect/src/actors/directory/live.ts index bee2ad2aa3..f1d4cbbe4e 100644 --- a/examples/effect/src/actors/directory/live.ts +++ b/examples/effect/src/actors/directory/live.ts @@ -1,15 +1,12 @@ -import { DateTime, Effect, Schema } from "effect"; import { State } from "@rivetkit/effect"; -import { Directory, RoomEntry } from "./api.ts"; +import { DateTime, Effect, Schema } from "effect"; +import { Directory } from "./api.ts"; export const DirectoryLive = Directory.toLayer( ({ state }) => Effect.gen(function* () { return Directory.of({ RegisterRoom: ({ payload }) => - // State writes go through Effect Schema validation. This - // example treats schema failures as defects instead of adding - // typed error channels to the action contract. Effect.gen(function* () { const openedAt = yield* DateTime.now; diff --git a/examples/effect/src/actors/moderator/api.ts b/examples/effect/src/actors/moderator/api.ts index 7d4f126648..5eb9a09e84 100644 --- a/examples/effect/src/actors/moderator/api.ts +++ b/examples/effect/src/actors/moderator/api.ts @@ -1,14 +1,16 @@ -import { Schema } from "effect"; import { Action, Actor } from "@rivetkit/effect"; +import { Schema } from "effect"; -export const ModerationVerdict = Schema.Struct({ - approved: Schema.Boolean, - reason: Schema.optionalKey(Schema.String), -}); +export class BannerWordsError extends Schema.TaggedErrorClass()( + "BannerWordsError", + { + message: Schema.String, + }, +) {} export const Review = Action.make("Review", { payload: { text: Schema.String }, - success: ModerationVerdict, + error: BannerWordsError, }); export const Stats = Action.make("Stats", { @@ -20,4 +22,3 @@ export const Stats = Action.make("Stats", { export const Moderator = Actor.make("moderator", { actions: [Review, Stats], }); - diff --git a/examples/effect/src/actors/moderator/live.ts b/examples/effect/src/actors/moderator/live.ts index 68182ff2d2..19e254f258 100644 --- a/examples/effect/src/actors/moderator/live.ts +++ b/examples/effect/src/actors/moderator/live.ts @@ -1,6 +1,8 @@ import { State } from "@rivetkit/effect"; import { Effect, Schema } from "effect"; -import { Moderator } from "./api.ts"; +import { BannerWordsError, Moderator } from "./api.ts"; + +const bannedWords = ["spam", "scam"]; export const ModeratorLive = Moderator.toLayer( ({ state }) => @@ -8,27 +10,20 @@ export const ModeratorLive = Moderator.toLayer( return Moderator.of({ Review: ({ payload }) => Effect.gen(function* () { - // State writes go through Effect Schema validation. This - // example treats schema failures as defects instead of adding - // typed error channels to the action contract. - const next = yield* State.updateAndGet( - state, - (current) => ({ - ...current, - reviewed: current.reviewed + 1, - }), - ).pipe(Effect.orDie); + yield* State.update(state, (current) => ({ + ...current, + reviewed: current.reviewed + 1, + })).pipe(Effect.orDie); + const lower = payload.text.toLowerCase(); - const hit = next.bannedWords.find((word) => + const hit = bannedWords.find((word) => lower.includes(word), ); - - return hit - ? { - approved: false, - reason: `contains banned word "${hit}"`, - } - : { approved: true }; + if (hit !== undefined) { + return yield* new BannerWordsError({ + message: `contains banned word "${hit}"`, + }); + } }), Stats: () => State.get(state).pipe( @@ -40,11 +35,9 @@ export const ModeratorLive = Moderator.toLayer( { state: { schema: Schema.Struct({ - bannedWords: Schema.Array(Schema.String), reviewed: Schema.Number, }), initialValue: () => ({ - bannedWords: ["spam", "scam"], reviewed: 0, }), }, diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index b1986d5074..1e6a0e2bcc 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,9 +1,12 @@ -import { Effect, Logger, Random } from "effect"; import { Client } from "@rivetkit/effect"; -import { Counter /*, IncrementBy */ } from "./actors/counter/api.ts"; -import { ChatRoom } from "./actors/chat-room/api.ts"; -import { Directory } from "./actors/directory/api.ts"; -import { Moderator } from "./actors/moderator/api.ts"; +import { Effect, Logger, Random } from "effect"; +import { + type BannerWordsError, + ChatRoom, + Counter, + Directory, + Moderator, +} from "./actors/mod.ts"; const program = Effect.gen(function* () { const runId = yield* Random.nextUUIDv4; @@ -31,19 +34,22 @@ const program = Effect.gen(function* () { const member = yield* room.Join({ name: "Alice" }); yield* Effect.log(`ChatRoom.Join -> ${member.name}`); - const sent = yield* room.SendMessage({ + yield* room.SendMessage({ sender: "Alice", text: "hello from Effect", }); - yield* Effect.log(`ChatRoom.SendMessage -> ok=${sent.ok}`); - - const rejected = yield* room.SendMessage({ - sender: "Alice", - text: "this contains spam", - }); - yield* Effect.log( - `ChatRoom.SendMessage rejected -> ok=${rejected.ok} reason=${rejected.reason}`, - ); + yield* Effect.log(`ChatRoom.SendMessage`); + + yield* room + .SendMessage({ + sender: "Alice", + text: "this contains spam", + }) + .pipe( + Effect.catchTag("BannerWordsError", (e: BannerWordsError) => + Effect.logError(`ChatRoom.SendMessage rejected: ${e.message}`), + ), + ); const history = yield* room.GetHistory(); yield* Effect.log(`ChatRoom.GetHistory -> ${history.length} messages`); @@ -80,7 +86,9 @@ const program = Effect.gen(function* () { ), ); -const ClientLayer = Client.layer({ endpoint: "http://127.0.0.1:6420" }); +const ClientLayer = Client.layer({ + endpoint: process.env.RIVET_ENDPOINT ?? "http://127.0.0.1:6420", +}); const LoggerLayer = Logger.layer([Logger.consolePretty()]); Effect.runPromise( diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index d09e8c80fb..e6b150ad03 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -6,12 +6,14 @@ import { ChatRoomLive } from "./actors/chat-room/live.ts"; import { DirectoryLive } from "./actors/directory/live.ts"; import { ModeratorLive } from "./actors/moderator/live.ts"; +const endpoint = process.env.RIVET_ENDPOINT ?? "http://127.0.0.1:6420"; + const ActorsLayer = Layer.mergeAll( CounterLive, DirectoryLive, ModeratorLive, ChatRoomLive, -).pipe(Layer.provide(Client.layer({ endpoint: "http://127.0.0.1:6420" }))); +).pipe(Layer.provide(Client.layer({ endpoint }))); // Engine config defaults to spawning a local rivet-engine process and // listening on http://127.0.0.1:6420 (override via RIVET_ENDPOINT to From f5d405e68ac726f0a1292571f3e65302afb0e17e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 15:58:50 +0200 Subject: [PATCH 255/306] style(rust): apply hook formatting --- engine/packages/error/src/error.rs | 3 +- .../envoy-client/src/connection/native.rs | 4 +- .../packages/client-protocol/src/versioned.rs | 94 ++++--- .../inspector-protocol/src/versioned.rs | 234 +++++++----------- .../rivetkit-core/src/actor/context.rs | 2 +- .../rivetkit-core/src/actor/metrics.rs | 94 +++++-- .../packages/rivetkit-core/src/actor/sleep.rs | 32 ++- .../rivetkit-core/src/actor/sqlite.rs | 5 +- .../packages/rivetkit-core/src/actor/task.rs | 3 +- .../src/registry/actor_connect.rs | 9 +- .../rivetkit-core/src/registry/http.rs | 3 +- .../rivetkit-core/src/registry/inspector.rs | 6 +- .../rivetkit-core/src/registry/mod.rs | 7 +- .../packages/rivetkit-core/tests/metrics.rs | 7 +- .../rivetkit-core/tests/registry_http.rs | 3 +- .../packages/rivetkit-core/tests/state.rs | 5 +- .../rivetkit-napi/src/napi_actor_events.rs | 2 +- .../packages/rivetkit-wasm/src/lib.rs | 17 +- 18 files changed, 272 insertions(+), 258 deletions(-) diff --git a/engine/packages/error/src/error.rs b/engine/packages/error/src/error.rs index 9e7390be6c..939e05a307 100644 --- a/engine/packages/error/src/error.rs +++ b/engine/packages/error/src/error.rs @@ -181,8 +181,7 @@ impl Serialize for RivetError { { use serde::ser::SerializeStruct; - let field_count = - 3 + usize::from(self.meta.is_some()) + usize::from(self.actor.is_some()); + let field_count = 3 + usize::from(self.meta.is_some()) + usize::from(self.actor.is_some()); let mut state = serializer.serialize_struct("RivetError", field_count)?; state.serialize_field("group", self.group())?; diff --git a/engine/sdks/rust/envoy-client/src/connection/native.rs b/engine/sdks/rust/envoy-client/src/connection/native.rs index 59d76e587d..aa02fead4f 100644 --- a/engine/sdks/rust/envoy-client/src/connection/native.rs +++ b/engine/sdks/rust/envoy-client/src/connection/native.rs @@ -133,9 +133,7 @@ async fn single_connection( while let Some(msg) = ws_rx.recv().await { match msg { WsTxMessage::Send(data) => { - let result = write - .send(tungstenite::Message::Binary(data.into())) - .await; + let result = write.send(tungstenite::Message::Binary(data.into())).await; if let Err(e) = result { tracing::error!(?e, "failed to send ws message"); break; diff --git a/rivetkit-rust/packages/client-protocol/src/versioned.rs b/rivetkit-rust/packages/client-protocol/src/versioned.rs index 713ebb8e6c..e70e995664 100644 --- a/rivetkit-rust/packages/client-protocol/src/versioned.rs +++ b/rivetkit-rust/packages/client-protocol/src/versioned.rs @@ -268,10 +268,14 @@ macro_rules! impl_to_server_pair { macro_rules! impl_common_pair { ($left:ident, $right:ident) => { impl_same_fields_pair!($left, $right, ActionRequest { id, name, args }); - impl_same_fields_pair!($left, $right, SubscriptionRequest { - event_name, - subscribe, - }); + impl_same_fields_pair!( + $left, + $right, + SubscriptionRequest { + event_name, + subscribe, + } + ); impl_to_server_pair!($left, $right); impl_same_fields_pair!($left, $right, HttpActionRequest { args }); impl_same_fields_pair!($left, $right, HttpActionResponse { output }); @@ -281,17 +285,25 @@ macro_rules! impl_common_pair { macro_rules! impl_to_client_v2_v3_pair { () => { - impl_same_fields_pair!(v2, v3, Init { - actor_id, - connection_id, - }); - impl_same_fields_pair!(v2, v3, Error { - group, - code, - message, - metadata, - action_id, - }); + impl_same_fields_pair!( + v2, + v3, + Init { + actor_id, + connection_id, + } + ); + impl_same_fields_pair!( + v2, + v3, + Error { + group, + code, + message, + metadata, + action_id, + } + ); impl_same_fields_pair!(v2, v3, ActionResponse { id, output }); impl_same_fields_pair!(v2, v3, Event { name, args }); @@ -343,24 +355,36 @@ impl_common_pair!(v1, v2); impl_common_pair!(v2, v3); impl_common_pair!(v3, v4); impl_to_client_v2_v3_pair!(); -impl_same_fields_pair!(v1, v2, HttpResponseError { - group, - code, - message, - metadata, -}); -impl_same_fields_pair!(v2, v3, HttpResponseError { - group, - code, - message, - metadata, -}); -impl_same_fields_pair!(v3, v4, HttpQueueSendRequest { - body, - name, - wait, - timeout, -}); +impl_same_fields_pair!( + v1, + v2, + HttpResponseError { + group, + code, + message, + metadata, + } +); +impl_same_fields_pair!( + v2, + v3, + HttpResponseError { + group, + code, + message, + metadata, + } +); +impl_same_fields_pair!( + v3, + v4, + HttpQueueSendRequest { + body, + name, + wait, + timeout, + } +); impl_same_fields_pair!(v3, v4, HttpQueueSendResponse { status, response }); macro_rules! impl_versioned_manual { @@ -609,7 +633,9 @@ impl OwnedVersionedData for HttpResponseError { (Self::V2(data), 2) => serde_bare::to_vec(&data).map_err(Into::into), (Self::V3(data), 3) => serde_bare::to_vec(&data).map_err(Into::into), (Self::V4(data), 4) => serde_bare::to_vec(&data).map_err(Into::into), - (_, version) => bail!("unexpected client protocol version for HttpResponseError: {version}"), + (_, version) => { + bail!("unexpected client protocol version for HttpResponseError: {version}") + } } } diff --git a/rivetkit-rust/packages/inspector-protocol/src/versioned.rs b/rivetkit-rust/packages/inspector-protocol/src/versioned.rs index c58f4db48f..35c757f235 100644 --- a/rivetkit-rust/packages/inspector-protocol/src/versioned.rs +++ b/rivetkit-rust/packages/inspector-protocol/src/versioned.rs @@ -69,18 +69,12 @@ impl ToServer { v1::ToServerBody::PatchStateRequest(req) => { v2::ToServerBody::PatchStateRequest(req.into()) } - v1::ToServerBody::StateRequest(req) => { - v2::ToServerBody::StateRequest(req.into()) - } + v1::ToServerBody::StateRequest(req) => v2::ToServerBody::StateRequest(req.into()), v1::ToServerBody::ConnectionsRequest(req) => { v2::ToServerBody::ConnectionsRequest(req.into()) } - v1::ToServerBody::ActionRequest(req) => { - v2::ToServerBody::ActionRequest(req.into()) - } - v1::ToServerBody::RpcsListRequest(req) => { - v2::ToServerBody::RpcsListRequest(req.into()) - } + v1::ToServerBody::ActionRequest(req) => v2::ToServerBody::ActionRequest(req.into()), + v1::ToServerBody::RpcsListRequest(req) => v2::ToServerBody::RpcsListRequest(req.into()), v1::ToServerBody::EventsRequest(_) | v1::ToServerBody::ClearEventsRequest(_) => { bail!("cannot convert inspector v1 events requests to v2") } @@ -105,24 +99,16 @@ impl ToServer { v3::ToServerBody::PatchStateRequest(req) => { v4::ToServerBody::PatchStateRequest(req.into()) } - v3::ToServerBody::StateRequest(req) => { - v4::ToServerBody::StateRequest(req.into()) - } + v3::ToServerBody::StateRequest(req) => v4::ToServerBody::StateRequest(req.into()), v3::ToServerBody::ConnectionsRequest(req) => { v4::ToServerBody::ConnectionsRequest(req.into()) } - v3::ToServerBody::ActionRequest(req) => { - v4::ToServerBody::ActionRequest(req.into()) - } - v3::ToServerBody::RpcsListRequest(req) => { - v4::ToServerBody::RpcsListRequest(req.into()) - } + v3::ToServerBody::ActionRequest(req) => v4::ToServerBody::ActionRequest(req.into()), + v3::ToServerBody::RpcsListRequest(req) => v4::ToServerBody::RpcsListRequest(req.into()), v3::ToServerBody::TraceQueryRequest(req) => { v4::ToServerBody::TraceQueryRequest(req.into()) } - v3::ToServerBody::QueueRequest(req) => { - v4::ToServerBody::QueueRequest(req.into()) - } + v3::ToServerBody::QueueRequest(req) => v4::ToServerBody::QueueRequest(req.into()), v3::ToServerBody::WorkflowHistoryRequest(req) => { v4::ToServerBody::WorkflowHistoryRequest(req.into()) } @@ -146,24 +132,16 @@ impl ToServer { v4::ToServerBody::PatchStateRequest(req) => { v3::ToServerBody::PatchStateRequest(req.into()) } - v4::ToServerBody::StateRequest(req) => { - v3::ToServerBody::StateRequest(req.into()) - } + v4::ToServerBody::StateRequest(req) => v3::ToServerBody::StateRequest(req.into()), v4::ToServerBody::ConnectionsRequest(req) => { v3::ToServerBody::ConnectionsRequest(req.into()) } - v4::ToServerBody::ActionRequest(req) => { - v3::ToServerBody::ActionRequest(req.into()) - } - v4::ToServerBody::RpcsListRequest(req) => { - v3::ToServerBody::RpcsListRequest(req.into()) - } + v4::ToServerBody::ActionRequest(req) => v3::ToServerBody::ActionRequest(req.into()), + v4::ToServerBody::RpcsListRequest(req) => v3::ToServerBody::RpcsListRequest(req.into()), v4::ToServerBody::TraceQueryRequest(req) => { v3::ToServerBody::TraceQueryRequest(req.into()) } - v4::ToServerBody::QueueRequest(req) => { - v3::ToServerBody::QueueRequest(req.into()) - } + v4::ToServerBody::QueueRequest(req) => v3::ToServerBody::QueueRequest(req.into()), v4::ToServerBody::WorkflowHistoryRequest(req) => { v3::ToServerBody::WorkflowHistoryRequest(req.into()) } @@ -190,24 +168,16 @@ impl ToServer { v3::ToServerBody::PatchStateRequest(req) => { v2::ToServerBody::PatchStateRequest(req.into()) } - v3::ToServerBody::StateRequest(req) => { - v2::ToServerBody::StateRequest(req.into()) - } + v3::ToServerBody::StateRequest(req) => v2::ToServerBody::StateRequest(req.into()), v3::ToServerBody::ConnectionsRequest(req) => { v2::ToServerBody::ConnectionsRequest(req.into()) } - v3::ToServerBody::ActionRequest(req) => { - v2::ToServerBody::ActionRequest(req.into()) - } - v3::ToServerBody::RpcsListRequest(req) => { - v2::ToServerBody::RpcsListRequest(req.into()) - } + v3::ToServerBody::ActionRequest(req) => v2::ToServerBody::ActionRequest(req.into()), + v3::ToServerBody::RpcsListRequest(req) => v2::ToServerBody::RpcsListRequest(req.into()), v3::ToServerBody::TraceQueryRequest(req) => { v2::ToServerBody::TraceQueryRequest(req.into()) } - v3::ToServerBody::QueueRequest(req) => { - v2::ToServerBody::QueueRequest(req.into()) - } + v3::ToServerBody::QueueRequest(req) => v2::ToServerBody::QueueRequest(req.into()), v3::ToServerBody::WorkflowHistoryRequest(req) => { v2::ToServerBody::WorkflowHistoryRequest(req.into()) } @@ -229,18 +199,12 @@ impl ToServer { v2::ToServerBody::PatchStateRequest(req) => { v1::ToServerBody::PatchStateRequest(req.into()) } - v2::ToServerBody::StateRequest(req) => { - v1::ToServerBody::StateRequest(req.into()) - } + v2::ToServerBody::StateRequest(req) => v1::ToServerBody::StateRequest(req.into()), v2::ToServerBody::ConnectionsRequest(req) => { v1::ToServerBody::ConnectionsRequest(req.into()) } - v2::ToServerBody::ActionRequest(req) => { - v1::ToServerBody::ActionRequest(req.into()) - } - v2::ToServerBody::RpcsListRequest(req) => { - v1::ToServerBody::RpcsListRequest(req.into()) - } + v2::ToServerBody::ActionRequest(req) => v1::ToServerBody::ActionRequest(req.into()), + v2::ToServerBody::RpcsListRequest(req) => v1::ToServerBody::RpcsListRequest(req.into()), v2::ToServerBody::TraceQueryRequest(_) | v2::ToServerBody::QueueRequest(_) | v2::ToServerBody::WorkflowHistoryRequest(_) => { @@ -309,24 +273,18 @@ impl ToClient { }; let body = match data.body { - v1::ToClientBody::StateResponse(resp) => { - v2::ToClientBody::StateResponse(resp.into()) - } + v1::ToClientBody::StateResponse(resp) => v2::ToClientBody::StateResponse(resp.into()), v1::ToClientBody::ConnectionsResponse(resp) => { v2::ToClientBody::ConnectionsResponse(resp.into()) } - v1::ToClientBody::ActionResponse(resp) => { - v2::ToClientBody::ActionResponse(resp.into()) - } + v1::ToClientBody::ActionResponse(resp) => v2::ToClientBody::ActionResponse(resp.into()), v1::ToClientBody::RpcsListResponse(resp) => { v2::ToClientBody::RpcsListResponse(resp.into()) } v1::ToClientBody::ConnectionsUpdated(update) => { v2::ToClientBody::ConnectionsUpdated(update.into()) } - v1::ToClientBody::StateUpdated(update) => { - v2::ToClientBody::StateUpdated(update.into()) - } + v1::ToClientBody::StateUpdated(update) => v2::ToClientBody::StateUpdated(update.into()), v1::ToClientBody::Error(error) => v2::ToClientBody::Error(error.into()), v1::ToClientBody::Init(init) => v2::ToClientBody::Init(v2::Init { connections: convert_vec(init.connections), @@ -359,24 +317,16 @@ impl ToClient { }; let body = match data.body { - v3::ToClientBody::StateResponse(resp) => { - v4::ToClientBody::StateResponse(resp.into()) - } + v3::ToClientBody::StateResponse(resp) => v4::ToClientBody::StateResponse(resp.into()), v3::ToClientBody::ConnectionsResponse(resp) => { v4::ToClientBody::ConnectionsResponse(resp.into()) } - v3::ToClientBody::ActionResponse(resp) => { - v4::ToClientBody::ActionResponse(resp.into()) - } + v3::ToClientBody::ActionResponse(resp) => v4::ToClientBody::ActionResponse(resp.into()), v3::ToClientBody::ConnectionsUpdated(update) => { v4::ToClientBody::ConnectionsUpdated(update.into()) } - v3::ToClientBody::QueueUpdated(update) => { - v4::ToClientBody::QueueUpdated(update.into()) - } - v3::ToClientBody::StateUpdated(update) => { - v4::ToClientBody::StateUpdated(update.into()) - } + v3::ToClientBody::QueueUpdated(update) => v4::ToClientBody::QueueUpdated(update.into()), + v3::ToClientBody::StateUpdated(update) => v4::ToClientBody::StateUpdated(update.into()), v3::ToClientBody::WorkflowHistoryUpdated(update) => { v4::ToClientBody::WorkflowHistoryUpdated(update.into()) } @@ -386,9 +336,7 @@ impl ToClient { v3::ToClientBody::TraceQueryResponse(resp) => { v4::ToClientBody::TraceQueryResponse(resp.into()) } - v3::ToClientBody::QueueResponse(resp) => { - v4::ToClientBody::QueueResponse(resp.into()) - } + v3::ToClientBody::QueueResponse(resp) => v4::ToClientBody::QueueResponse(resp.into()), v3::ToClientBody::WorkflowHistoryResponse(resp) => { v4::ToClientBody::WorkflowHistoryResponse(resp.into()) } @@ -411,24 +359,16 @@ impl ToClient { }; let body = match data.body { - v4::ToClientBody::StateResponse(resp) => { - v3::ToClientBody::StateResponse(resp.into()) - } + v4::ToClientBody::StateResponse(resp) => v3::ToClientBody::StateResponse(resp.into()), v4::ToClientBody::ConnectionsResponse(resp) => { v3::ToClientBody::ConnectionsResponse(resp.into()) } - v4::ToClientBody::ActionResponse(resp) => { - v3::ToClientBody::ActionResponse(resp.into()) - } + v4::ToClientBody::ActionResponse(resp) => v3::ToClientBody::ActionResponse(resp.into()), v4::ToClientBody::ConnectionsUpdated(update) => { v3::ToClientBody::ConnectionsUpdated(update.into()) } - v4::ToClientBody::QueueUpdated(update) => { - v3::ToClientBody::QueueUpdated(update.into()) - } - v4::ToClientBody::StateUpdated(update) => { - v3::ToClientBody::StateUpdated(update.into()) - } + v4::ToClientBody::QueueUpdated(update) => v3::ToClientBody::QueueUpdated(update.into()), + v4::ToClientBody::StateUpdated(update) => v3::ToClientBody::StateUpdated(update.into()), v4::ToClientBody::WorkflowHistoryUpdated(update) => { v3::ToClientBody::WorkflowHistoryUpdated(update.into()) } @@ -438,9 +378,7 @@ impl ToClient { v4::ToClientBody::TraceQueryResponse(resp) => { v3::ToClientBody::TraceQueryResponse(resp.into()) } - v4::ToClientBody::QueueResponse(resp) => { - v3::ToClientBody::QueueResponse(resp.into()) - } + v4::ToClientBody::QueueResponse(resp) => v3::ToClientBody::QueueResponse(resp.into()), v4::ToClientBody::WorkflowHistoryResponse(resp) => { v3::ToClientBody::WorkflowHistoryResponse(resp.into()) } @@ -466,24 +404,16 @@ impl ToClient { }; let body = match data.body { - v3::ToClientBody::StateResponse(resp) => { - v2::ToClientBody::StateResponse(resp.into()) - } + v3::ToClientBody::StateResponse(resp) => v2::ToClientBody::StateResponse(resp.into()), v3::ToClientBody::ConnectionsResponse(resp) => { v2::ToClientBody::ConnectionsResponse(resp.into()) } - v3::ToClientBody::ActionResponse(resp) => { - v2::ToClientBody::ActionResponse(resp.into()) - } + v3::ToClientBody::ActionResponse(resp) => v2::ToClientBody::ActionResponse(resp.into()), v3::ToClientBody::ConnectionsUpdated(update) => { v2::ToClientBody::ConnectionsUpdated(update.into()) } - v3::ToClientBody::QueueUpdated(update) => { - v2::ToClientBody::QueueUpdated(update.into()) - } - v3::ToClientBody::StateUpdated(update) => { - v2::ToClientBody::StateUpdated(update.into()) - } + v3::ToClientBody::QueueUpdated(update) => v2::ToClientBody::QueueUpdated(update.into()), + v3::ToClientBody::StateUpdated(update) => v2::ToClientBody::StateUpdated(update.into()), v3::ToClientBody::WorkflowHistoryUpdated(update) => { v2::ToClientBody::WorkflowHistoryUpdated(update.into()) } @@ -493,9 +423,7 @@ impl ToClient { v3::ToClientBody::TraceQueryResponse(resp) => { v2::ToClientBody::TraceQueryResponse(resp.into()) } - v3::ToClientBody::QueueResponse(resp) => { - v2::ToClientBody::QueueResponse(resp.into()) - } + v3::ToClientBody::QueueResponse(resp) => v2::ToClientBody::QueueResponse(resp.into()), v3::ToClientBody::WorkflowHistoryResponse(resp) => { v2::ToClientBody::WorkflowHistoryResponse(resp.into()) } @@ -516,21 +444,15 @@ impl ToClient { }; let body = match data.body { - v2::ToClientBody::StateResponse(resp) => { - v1::ToClientBody::StateResponse(resp.into()) - } + v2::ToClientBody::StateResponse(resp) => v1::ToClientBody::StateResponse(resp.into()), v2::ToClientBody::ConnectionsResponse(resp) => { v1::ToClientBody::ConnectionsResponse(resp.into()) } - v2::ToClientBody::ActionResponse(resp) => { - v1::ToClientBody::ActionResponse(resp.into()) - } + v2::ToClientBody::ActionResponse(resp) => v1::ToClientBody::ActionResponse(resp.into()), v2::ToClientBody::ConnectionsUpdated(update) => { v1::ToClientBody::ConnectionsUpdated(update.into()) } - v2::ToClientBody::StateUpdated(update) => { - v1::ToClientBody::StateUpdated(update.into()) - } + v2::ToClientBody::StateUpdated(update) => v1::ToClientBody::StateUpdated(update.into()), v2::ToClientBody::RpcsListResponse(resp) => { v1::ToClientBody::RpcsListResponse(resp.into()) } @@ -720,11 +642,15 @@ macro_rules! impl_common_actor_pair { impl_same_fields_pair!($left, $right, Connection { id, details }); impl_connections_response_pair!($left, $right); impl_connection_list_pair!($left, $right, ConnectionsUpdated); - impl_same_fields_pair!($left, $right, StateResponse { - rid, - state, - is_state_enabled, - }); + impl_same_fields_pair!( + $left, + $right, + StateResponse { + rid, + state, + is_state_enabled, + } + ); impl_same_fields_pair!($left, $right, ActionResponse { rid, output }); impl_same_fields_pair!($left, $right, StateUpdated { state }); impl_same_fields_pair!($left, $right, RpcsListResponse { rid, rpcs }); @@ -734,28 +660,40 @@ macro_rules! impl_common_actor_pair { macro_rules! impl_queue_workflow_pair { ($left:ident, $right:ident) => { - impl_same_fields_pair!($left, $right, TraceQueryRequest { - id, - start_ms, - end_ms, - limit, - }); + impl_same_fields_pair!( + $left, + $right, + TraceQueryRequest { + id, + start_ms, + end_ms, + limit, + } + ); impl_same_fields_pair!($left, $right, TraceQueryResponse { rid, payload }); impl_same_fields_pair!($left, $right, QueueRequest { id, limit }); - impl_same_fields_pair!($left, $right, QueueMessageSummary { - id, - name, - created_at_ms, - }); + impl_same_fields_pair!( + $left, + $right, + QueueMessageSummary { + id, + name, + created_at_ms, + } + ); impl_queue_status_pair!($left, $right); impl_queue_response_pair!($left, $right); impl_same_fields_pair!($left, $right, QueueUpdated { queue_size }); impl_same_fields_pair!($left, $right, WorkflowHistoryRequest { id }); - impl_same_fields_pair!($left, $right, WorkflowHistoryResponse { - rid, - history, - is_workflow_enabled, - }); + impl_same_fields_pair!( + $left, + $right, + WorkflowHistoryResponse { + rid, + history, + is_workflow_enabled, + } + ); impl_same_fields_pair!($left, $right, WorkflowHistoryUpdated { history }); impl_init_pair!($left, $right); }; @@ -765,12 +703,16 @@ macro_rules! impl_database_pair { ($left:ident, $right:ident) => { impl_same_fields_pair!($left, $right, DatabaseSchemaRequest { id }); impl_same_fields_pair!($left, $right, DatabaseSchemaResponse { rid, schema }); - impl_same_fields_pair!($left, $right, DatabaseTableRowsRequest { - id, - table, - limit, - offset, - }); + impl_same_fields_pair!( + $left, + $right, + DatabaseTableRowsRequest { + id, + table, + limit, + offset, + } + ); impl_same_fields_pair!($left, $right, DatabaseTableRowsResponse { rid, result }); }; } @@ -813,9 +755,7 @@ impl From for v3::ToClientBody { v2::ToClientBody::StateResponse(resp) => Self::StateResponse(resp.into()), v2::ToClientBody::ConnectionsResponse(resp) => Self::ConnectionsResponse(resp.into()), v2::ToClientBody::ActionResponse(resp) => Self::ActionResponse(resp.into()), - v2::ToClientBody::ConnectionsUpdated(update) => { - Self::ConnectionsUpdated(update.into()) - } + v2::ToClientBody::ConnectionsUpdated(update) => Self::ConnectionsUpdated(update.into()), v2::ToClientBody::QueueUpdated(update) => Self::QueueUpdated(update.into()), v2::ToClientBody::StateUpdated(update) => Self::StateUpdated(update.into()), v2::ToClientBody::WorkflowHistoryUpdated(update) => { diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/context.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/context.rs index e67c096f3a..69a19bc821 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/context.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/context.rs @@ -10,9 +10,9 @@ use crate::time::{Instant, SystemTime, UNIX_EPOCH}; use anyhow::{Context as AnyhowContext, Result}; use futures::future::BoxFuture; use parking_lot::{Mutex, RwLock}; -use rivet_error::ActorSpecifier; use rivet_envoy_client::handle::EnvoyHandle; use rivet_envoy_client::tunnel::HibernatingWebSocketMetadata; +use rivet_error::ActorSpecifier; use scc::HashMap as SccHashMap; use tokio::runtime::Handle; use tokio::sync::{Mutex as AsyncMutex, Notify, OnceCell, broadcast, mpsc, oneshot}; diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/metrics.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/metrics.rs index 6d5f6e11c3..66d30d9239 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/metrics.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/metrics.rs @@ -29,8 +29,13 @@ const SQLITE_COMMIT_PHASE_LABELS: &[&str] = &["actor_id_gen", "actor_key", "envo const SQLITE_WORKER_COMMAND_LABELS: &[&str] = &["actor_id_gen", "actor_key", "envoy_key", "operation"]; #[cfg(feature = "sqlite-local")] -const SQLITE_WORKER_ERROR_LABELS: &[&str] = - &["actor_id_gen", "actor_key", "envoy_key", "operation", "code"]; +const SQLITE_WORKER_ERROR_LABELS: &[&str] = &[ + "actor_id_gen", + "actor_key", + "envoy_key", + "operation", + "code", +]; pub(crate) struct ActorMetrics { labels: Arc, @@ -157,7 +162,10 @@ impl ActorMetricCollectors { ) .expect("create actor_queue_depth gauge"); let queue_messages_sent_total = IntCounterVec::new( - Opts::new("actor_queue_messages_sent_total", "total queue messages sent"), + Opts::new( + "actor_queue_messages_sent_total", + "total queue messages sent", + ), ACTOR_LABELS, ) .expect("create actor_queue_messages_sent_total counter"); @@ -452,12 +460,18 @@ impl ActorMetricCollectors { register_metric(&rivet_metrics::REGISTRY, create_vars_ms.clone()); register_metric(&rivet_metrics::REGISTRY, queue_depth.clone()); register_metric(&rivet_metrics::REGISTRY, queue_messages_sent_total.clone()); - register_metric(&rivet_metrics::REGISTRY, queue_messages_received_total.clone()); + register_metric( + &rivet_metrics::REGISTRY, + queue_messages_received_total.clone(), + ); register_metric(&rivet_metrics::REGISTRY, active_connections.clone()); register_metric(&rivet_metrics::REGISTRY, connections_total.clone()); register_metric(&rivet_metrics::REGISTRY, lifecycle_inbox_depth.clone()); register_metric(&rivet_metrics::REGISTRY, dispatch_inbox_depth.clone()); - register_metric(&rivet_metrics::REGISTRY, lifecycle_event_inbox_depth.clone()); + register_metric( + &rivet_metrics::REGISTRY, + lifecycle_event_inbox_depth.clone(), + ); register_metric(&rivet_metrics::REGISTRY, user_tasks_active.clone()); register_metric(&rivet_metrics::REGISTRY, user_task_duration_seconds.clone()); register_metric(&rivet_metrics::REGISTRY, shutdown_wait_seconds.clone()); @@ -469,7 +483,10 @@ impl ActorMetricCollectors { ); #[cfg(feature = "sqlite-local")] { - register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_resolve_pages_total.clone()); + register_metric( + &rivet_metrics::REGISTRY, + sqlite_vfs_resolve_pages_total.clone(), + ); register_metric( &rivet_metrics::REGISTRY, sqlite_vfs_resolve_pages_requested_total.clone(), @@ -483,10 +500,22 @@ impl ActorMetricCollectors { sqlite_vfs_resolve_pages_cache_misses_total.clone(), ); register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_get_pages_total.clone()); - register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_pages_fetched_total.clone()); - register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_prefetch_pages_total.clone()); - register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_bytes_fetched_total.clone()); - register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_prefetch_bytes_total.clone()); + register_metric( + &rivet_metrics::REGISTRY, + sqlite_vfs_pages_fetched_total.clone(), + ); + register_metric( + &rivet_metrics::REGISTRY, + sqlite_vfs_prefetch_pages_total.clone(), + ); + register_metric( + &rivet_metrics::REGISTRY, + sqlite_vfs_bytes_fetched_total.clone(), + ); + register_metric( + &rivet_metrics::REGISTRY, + sqlite_vfs_prefetch_bytes_total.clone(), + ); register_metric( &rivet_metrics::REGISTRY, sqlite_vfs_get_pages_duration_seconds.clone(), @@ -501,16 +530,31 @@ impl ActorMetricCollectors { sqlite_vfs_commit_duration_seconds_total.clone(), ); register_metric(&rivet_metrics::REGISTRY, sqlite_worker_queue_depth.clone()); - register_metric(&rivet_metrics::REGISTRY, sqlite_worker_queue_overload_total.clone()); + register_metric( + &rivet_metrics::REGISTRY, + sqlite_worker_queue_overload_total.clone(), + ); register_metric( &rivet_metrics::REGISTRY, sqlite_worker_command_duration_seconds.clone(), ); - register_metric(&rivet_metrics::REGISTRY, sqlite_worker_command_error_total.clone()); - register_metric(&rivet_metrics::REGISTRY, sqlite_worker_close_duration_seconds.clone()); - register_metric(&rivet_metrics::REGISTRY, sqlite_worker_close_timeout_total.clone()); + register_metric( + &rivet_metrics::REGISTRY, + sqlite_worker_command_error_total.clone(), + ); + register_metric( + &rivet_metrics::REGISTRY, + sqlite_worker_close_duration_seconds.clone(), + ); + register_metric( + &rivet_metrics::REGISTRY, + sqlite_worker_close_timeout_total.clone(), + ); register_metric(&rivet_metrics::REGISTRY, sqlite_worker_crash_total.clone()); - register_metric(&rivet_metrics::REGISTRY, sqlite_worker_unclean_close_total.clone()); + register_metric( + &rivet_metrics::REGISTRY, + sqlite_worker_unclean_close_total.clone(), + ); } Self { @@ -691,10 +735,7 @@ impl ActorMetrics { }); let labels = self.actor_labels(); let labels = [labels[0], labels[1], labels[2], kind.as_metric_label()]; - METRICS - .user_tasks_active - .with_label_values(&labels) - .dec(); + METRICS.user_tasks_active.with_label_values(&labels).dec(); METRICS .user_task_duration_seconds .with_label_values(&labels) @@ -827,7 +868,13 @@ impl depot_client::vfs::SqliteVfsMetrics for ActorMetrics { total_ns: u64, ) { record_retained_actor_metrics(&self.labels, |retained| { - for phase in ["request_build", "serialize", "transport", "state_update", "total"] { + for phase in [ + "request_build", + "serialize", + "transport", + "state_update", + "total", + ] { push_unique(&mut retained.sqlite_commit_phases, phase); } }); @@ -1106,7 +1153,12 @@ fn remove_retained_actor_metrics(labels: &ActorMetricLabels, retained: &Retained ); } for operation in &retained.sqlite_worker_operations { - let labels = [actor_labels[0], actor_labels[1], actor_labels[2], *operation]; + let labels = [ + actor_labels[0], + actor_labels[1], + actor_labels[2], + *operation, + ]; ignore_missing_labels( metrics .sqlite_worker_command_duration_seconds diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/sleep.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/sleep.rs index 9144fc1648..1901076d5b 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/sleep.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/sleep.rs @@ -13,22 +13,20 @@ use tokio::task::JoinHandle; use tracing::Instrument; use crate::actor::config::ActorConfig; +use crate::actor::context::ActorContext; #[cfg(not(feature = "wasm-runtime"))] use crate::actor::context::ActorWorkRegion; -use crate::actor::context::ActorContext; use crate::actor::task_types::ShutdownKind; -#[cfg(feature = "wasm-runtime")] -use crate::actor::work_registry::LocalShutdownTask; #[cfg(not(feature = "wasm-runtime"))] use crate::actor::work_registry::ActorWorkPolicy; -use crate::actor::work_registry::{ - ActorWorkKind, CountGuard, RegionGuard, WorkRegistry, -}; +#[cfg(feature = "wasm-runtime")] +use crate::actor::work_registry::LocalShutdownTask; +use crate::actor::work_registry::{ActorWorkKind, CountGuard, RegionGuard, WorkRegistry}; #[cfg(feature = "wasm-runtime")] use crate::runtime::RuntimeSpawner; -use crate::time::{Instant, sleep}; #[cfg(test)] use crate::time::sleep_until; +use crate::time::{Instant, sleep}; #[cfg(test)] use crate::types::ActorKey; #[cfg(feature = "wasm-runtime")] @@ -504,7 +502,10 @@ impl ActorContext { F: Future + Send + 'static, { if Handle::try_current().is_err() { - tracing::warn!(kind = kind.label(), "actor work spawned without tokio runtime"); + tracing::warn!( + kind = kind.label(), + "actor work spawned without tokio runtime" + ); return false; } @@ -512,7 +513,10 @@ impl ActorContext { if policy.aborts_at_shutdown_deadline { let mut shutdown_tasks = self.0.sleep.work.shutdown_tasks.lock(); if self.0.sleep.work.teardown_started.load(Ordering::Acquire) { - tracing::warn!(kind = kind.label(), "actor work spawned after teardown; aborting immediately"); + tracing::warn!( + kind = kind.label(), + "actor work spawned after teardown; aborting immediately" + ); return false; } let region = self.begin_work_region(kind); @@ -521,7 +525,10 @@ impl ActorContext { let mut unabortable_shutdown_tasks = self.0.sleep.work.unabortable_shutdown_tasks.lock(); if self.0.sleep.work.teardown_started.load(Ordering::Acquire) { - tracing::warn!(kind = kind.label(), "actor work spawned after teardown; aborting immediately"); + tracing::warn!( + kind = kind.label(), + "actor work spawned after teardown; aborting immediately" + ); return false; } let region = self.begin_work_region(kind); @@ -574,7 +581,10 @@ impl ActorContext { { let mut local_shutdown_tasks = self.0.sleep.work.local_shutdown_tasks.lock(); if self.0.sleep.work.teardown_started.load(Ordering::Acquire) { - tracing::warn!(kind = kind.label(), "actor work spawned after teardown; aborting immediately"); + tracing::warn!( + kind = kind.label(), + "actor work spawned after teardown; aborting immediately" + ); return false; } diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs index a401cefe2b..35b3de3bbf 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs @@ -10,9 +10,9 @@ use depot_client_types::is_head_fence_mismatch; pub use depot_client_types::{BindParam, ColumnValue, ExecResult, ExecuteResult, QueryResult}; #[cfg(feature = "sqlite-local")] use parking_lot::Mutex; -use rivet_error::{ActorSpecifier, RivetError}; use rivet_envoy_client::protocol; use rivet_envoy_client::{handle::EnvoyHandle, utils::RemoteSqliteIndeterminateResultError}; +use rivet_error::{ActorSpecifier, RivetError}; use serde::Serialize; use serde_json::{Map as JsonMap, Value as JsonValue}; #[cfg(feature = "sqlite-local")] @@ -507,8 +507,7 @@ impl SqliteDb { } fn actor_specifier(&self) -> Option { - let mut specifier = - ActorSpecifier::new(self.actor_id.as_ref()?.clone(), self.generation?); + let mut specifier = ActorSpecifier::new(self.actor_id.as_ref()?.clone(), self.generation?); if let Some(key) = self.actor_key.as_ref() { specifier = specifier.with_key(key.clone()); } diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs index 2f771e5c28..3083975fd2 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs @@ -890,7 +890,8 @@ impl ActorTask { let _action_keep_awake = action_keep_awake; match tracked_reply_rx.await { Ok(result) => { - let result = result.map_err(|error| ctx.attach_actor_to_error(error)); + let result = + result.map_err(|error| ctx.attach_actor_to_error(error)); tracing::info!( actor_id = %actor_id, action_name = %action_name_for_log, diff --git a/rivetkit-rust/packages/rivetkit-core/src/registry/actor_connect.rs b/rivetkit-rust/packages/rivetkit-core/src/registry/actor_connect.rs index f70cfce65d..1b9c43bd31 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/registry/actor_connect.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/registry/actor_connect.rs @@ -64,13 +64,14 @@ pub(super) fn encode_actor_connect_message(message: &ActorConnectToClient) -> Re .as_ref() .map(|metadata| metadata.as_ref().to_vec()), action_id: payload.action_id.map(serde_bare::Uint), - actor: payload.actor.as_ref().map(|actor| { - client_protocol::ActorSpecifier { + actor: payload + .actor + .as_ref() + .map(|actor| client_protocol::ActorSpecifier { actor_id: actor.actor_id.clone(), generation: serde_bare::Uint(actor.generation), key: actor.key.clone(), - } - }), + }), }) } ActorConnectToClient::ActionResponse(payload) => { diff --git a/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs b/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs index 3527621953..f24612503d 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs @@ -1,7 +1,7 @@ use super::dispatch::*; use super::inspector::*; use super::*; -use crate::error::{client_error_message, client_error_metadata, ProtocolError}; +use crate::error::{ProtocolError, client_error_message, client_error_metadata}; use ::http; const HEADER_RIVET_ACTOR: &str = "x-rivet-actor"; @@ -378,7 +378,6 @@ impl RegistryDispatcher { } } } - } enum RegistryHttpRoute { diff --git a/rivetkit-rust/packages/rivetkit-core/src/registry/inspector.rs b/rivetkit-rust/packages/rivetkit-core/src/registry/inspector.rs index 56816c4aac..1c2929df00 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/registry/inspector.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/registry/inspector.rs @@ -1,7 +1,7 @@ use super::dispatch::*; use super::http::*; use super::*; -use crate::error::{client_error_message, ProtocolError}; +use crate::error::{ProtocolError, client_error_message}; use ::http; #[derive(rivet_error::RivetError, serde::Serialize)] @@ -346,9 +346,7 @@ impl RegistryDispatcher { ) -> Result<(bool, Option>)> { let result = instance .ctx - .internal_keep_awake(dispatch_workflow_history_through_task( - &instance.dispatch, - )) + .internal_keep_awake(dispatch_workflow_history_through_task(&instance.dispatch)) .await .context("load inspector workflow history"); diff --git a/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs b/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs index 3c98819220..75f3f444d2 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs @@ -625,11 +625,8 @@ impl RegistryDispatcher { let (start_tx, start_rx) = oneshot::channel(); let result: Result> = async { - try_send_lifecycle_command( - &lifecycle_tx, - LifecycleCommand::Start { reply: start_tx }, - ) - .context("send actor task start command")?; + try_send_lifecycle_command(&lifecycle_tx, LifecycleCommand::Start { reply: start_tx }) + .context("send actor task start command")?; start_rx .await .context("receive actor task start reply")? diff --git a/rivetkit-rust/packages/rivetkit-core/tests/metrics.rs b/rivetkit-rust/packages/rivetkit-core/tests/metrics.rs index 6a636edf35..73075682a9 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/metrics.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/metrics.rs @@ -8,8 +8,8 @@ mod moved_tests { use rivet_metrics::prometheus::{IntGauge, Opts, Registry}; - use super::*; use super::metrics_helpers::{metric_line_for_actor, render_global_metrics}; + use super::*; #[test] fn duplicate_metric_registration_uses_noop_fallback() { @@ -102,6 +102,9 @@ mod moved_tests { && line.contains("envoy_key=\"envoy-1\"") }) .unwrap_or_else(|| panic!("{name} should render")); - assert!(line.ends_with(value), "{name} should have value {value}: {line}"); + assert!( + line.ends_with(value), + "{name} should have value {value}: {line}" + ); } } diff --git a/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs b/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs index 23b54ac5d6..23e0c365af 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs @@ -8,8 +8,7 @@ mod moved_tests { HttpResponseEncoding, authorization_bearer_token, authorization_bearer_token_map, framework_action_error_response, framework_anyhow_error_response_with_actor, is_actor_request_path, message_boundary_error_response, - message_boundary_error_response_with_actor, normalize_actor_request_path, - request_encoding, + message_boundary_error_response_with_actor, normalize_actor_request_path, request_encoding, workflow_dispatch_result, }; use crate::actor::action::ActionDispatchError; diff --git a/rivetkit-rust/packages/rivetkit-core/tests/state.rs b/rivetkit-rust/packages/rivetkit-core/tests/state.rs index 400fe53d14..abf648e2e6 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/state.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/state.rs @@ -178,10 +178,7 @@ mod moved_tests { #[tokio::test] async fn request_save_coalesces_and_escalates_to_immediate() { - let state = ActorContext::new_for_state_tests( - new_in_memory(), - ActorConfig::default(), - ); + let state = ActorContext::new_for_state_tests(new_in_memory(), ActorConfig::default()); let (events_tx, mut events_rx) = mpsc::unbounded_channel(); state.configure_lifecycle_events(Some(events_tx)); diff --git a/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs b/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs index 4553e11d32..d9e28c25c5 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs +++ b/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs @@ -1,5 +1,5 @@ -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use anyhow::Result; diff --git a/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs b/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs index a1ea940dd6..f3f1207b62 100644 --- a/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs +++ b/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs @@ -7,18 +7,15 @@ use std::time::Duration; use anyhow::{Result, anyhow}; use js_sys::{Array, Function, Object, Promise, Reflect, Uint8Array}; -use rivet_error::{ - ActorSpecifier, RivetError as RivetTransportError, RivetErrorKind, -}; +use rivet_error::{ActorSpecifier, RivetError as RivetTransportError, RivetErrorKind}; use rivetkit_core::error::public_error_status_code; use rivetkit_core::inspector::InspectorAuth; use rivetkit_core::{ ActorConfig, ActorConfigInput, ActorEvent, ActorFactory as CoreActorFactory, ActorStart, - ActorWorkKind, - BindParam, ColumnValue, CoreRegistry as NativeCoreRegistry, CoreServerlessRuntime, - EnqueueAndWaitOpts, KeepAwakeRegion, ListOpts, QueueMessage, QueueNextBatchOpts, - QueueSendResult, QueueSendStatus, QueueTryNextBatchOpts, QueueWaitOpts, Request, - RequestSaveOpts, Response, RuntimeSpawner, SerializeStateReason, ServeConfig, + ActorWorkKind, BindParam, ColumnValue, CoreRegistry as NativeCoreRegistry, + CoreServerlessRuntime, EnqueueAndWaitOpts, KeepAwakeRegion, ListOpts, QueueMessage, + QueueNextBatchOpts, QueueSendResult, QueueSendStatus, QueueTryNextBatchOpts, QueueWaitOpts, + Request, RequestSaveOpts, Response, RuntimeSpawner, SerializeStateReason, ServeConfig, ServerlessRequest, StateDelta, WebSocket, WebSocketCallbackRegion, WsMessage, }; use tokio::sync::oneshot; @@ -2865,9 +2862,7 @@ mod tests { } fn transport_message(error: &anyhow::Error) -> String { - transport_error(error) - .message() - .to_owned() + transport_error(error).message().to_owned() } #[test] From 33c4a9810d9d65dc800bfcad860c11c9b2121bcb Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 15:59:50 +0200 Subject: [PATCH 256/306] Revert "style(rust): apply hook formatting" This reverts commit f5d405e68ac726f0a1292571f3e65302afb0e17e. --- engine/packages/error/src/error.rs | 3 +- .../envoy-client/src/connection/native.rs | 4 +- .../packages/client-protocol/src/versioned.rs | 94 +++---- .../inspector-protocol/src/versioned.rs | 234 +++++++++++------- .../rivetkit-core/src/actor/context.rs | 2 +- .../rivetkit-core/src/actor/metrics.rs | 94 ++----- .../packages/rivetkit-core/src/actor/sleep.rs | 32 +-- .../rivetkit-core/src/actor/sqlite.rs | 5 +- .../packages/rivetkit-core/src/actor/task.rs | 3 +- .../src/registry/actor_connect.rs | 9 +- .../rivetkit-core/src/registry/http.rs | 3 +- .../rivetkit-core/src/registry/inspector.rs | 6 +- .../rivetkit-core/src/registry/mod.rs | 7 +- .../packages/rivetkit-core/tests/metrics.rs | 7 +- .../rivetkit-core/tests/registry_http.rs | 3 +- .../packages/rivetkit-core/tests/state.rs | 5 +- .../rivetkit-napi/src/napi_actor_events.rs | 2 +- .../packages/rivetkit-wasm/src/lib.rs | 17 +- 18 files changed, 258 insertions(+), 272 deletions(-) diff --git a/engine/packages/error/src/error.rs b/engine/packages/error/src/error.rs index 939e05a307..9e7390be6c 100644 --- a/engine/packages/error/src/error.rs +++ b/engine/packages/error/src/error.rs @@ -181,7 +181,8 @@ impl Serialize for RivetError { { use serde::ser::SerializeStruct; - let field_count = 3 + usize::from(self.meta.is_some()) + usize::from(self.actor.is_some()); + let field_count = + 3 + usize::from(self.meta.is_some()) + usize::from(self.actor.is_some()); let mut state = serializer.serialize_struct("RivetError", field_count)?; state.serialize_field("group", self.group())?; diff --git a/engine/sdks/rust/envoy-client/src/connection/native.rs b/engine/sdks/rust/envoy-client/src/connection/native.rs index aa02fead4f..59d76e587d 100644 --- a/engine/sdks/rust/envoy-client/src/connection/native.rs +++ b/engine/sdks/rust/envoy-client/src/connection/native.rs @@ -133,7 +133,9 @@ async fn single_connection( while let Some(msg) = ws_rx.recv().await { match msg { WsTxMessage::Send(data) => { - let result = write.send(tungstenite::Message::Binary(data.into())).await; + let result = write + .send(tungstenite::Message::Binary(data.into())) + .await; if let Err(e) = result { tracing::error!(?e, "failed to send ws message"); break; diff --git a/rivetkit-rust/packages/client-protocol/src/versioned.rs b/rivetkit-rust/packages/client-protocol/src/versioned.rs index e70e995664..713ebb8e6c 100644 --- a/rivetkit-rust/packages/client-protocol/src/versioned.rs +++ b/rivetkit-rust/packages/client-protocol/src/versioned.rs @@ -268,14 +268,10 @@ macro_rules! impl_to_server_pair { macro_rules! impl_common_pair { ($left:ident, $right:ident) => { impl_same_fields_pair!($left, $right, ActionRequest { id, name, args }); - impl_same_fields_pair!( - $left, - $right, - SubscriptionRequest { - event_name, - subscribe, - } - ); + impl_same_fields_pair!($left, $right, SubscriptionRequest { + event_name, + subscribe, + }); impl_to_server_pair!($left, $right); impl_same_fields_pair!($left, $right, HttpActionRequest { args }); impl_same_fields_pair!($left, $right, HttpActionResponse { output }); @@ -285,25 +281,17 @@ macro_rules! impl_common_pair { macro_rules! impl_to_client_v2_v3_pair { () => { - impl_same_fields_pair!( - v2, - v3, - Init { - actor_id, - connection_id, - } - ); - impl_same_fields_pair!( - v2, - v3, - Error { - group, - code, - message, - metadata, - action_id, - } - ); + impl_same_fields_pair!(v2, v3, Init { + actor_id, + connection_id, + }); + impl_same_fields_pair!(v2, v3, Error { + group, + code, + message, + metadata, + action_id, + }); impl_same_fields_pair!(v2, v3, ActionResponse { id, output }); impl_same_fields_pair!(v2, v3, Event { name, args }); @@ -355,36 +343,24 @@ impl_common_pair!(v1, v2); impl_common_pair!(v2, v3); impl_common_pair!(v3, v4); impl_to_client_v2_v3_pair!(); -impl_same_fields_pair!( - v1, - v2, - HttpResponseError { - group, - code, - message, - metadata, - } -); -impl_same_fields_pair!( - v2, - v3, - HttpResponseError { - group, - code, - message, - metadata, - } -); -impl_same_fields_pair!( - v3, - v4, - HttpQueueSendRequest { - body, - name, - wait, - timeout, - } -); +impl_same_fields_pair!(v1, v2, HttpResponseError { + group, + code, + message, + metadata, +}); +impl_same_fields_pair!(v2, v3, HttpResponseError { + group, + code, + message, + metadata, +}); +impl_same_fields_pair!(v3, v4, HttpQueueSendRequest { + body, + name, + wait, + timeout, +}); impl_same_fields_pair!(v3, v4, HttpQueueSendResponse { status, response }); macro_rules! impl_versioned_manual { @@ -633,9 +609,7 @@ impl OwnedVersionedData for HttpResponseError { (Self::V2(data), 2) => serde_bare::to_vec(&data).map_err(Into::into), (Self::V3(data), 3) => serde_bare::to_vec(&data).map_err(Into::into), (Self::V4(data), 4) => serde_bare::to_vec(&data).map_err(Into::into), - (_, version) => { - bail!("unexpected client protocol version for HttpResponseError: {version}") - } + (_, version) => bail!("unexpected client protocol version for HttpResponseError: {version}"), } } diff --git a/rivetkit-rust/packages/inspector-protocol/src/versioned.rs b/rivetkit-rust/packages/inspector-protocol/src/versioned.rs index 35c757f235..c58f4db48f 100644 --- a/rivetkit-rust/packages/inspector-protocol/src/versioned.rs +++ b/rivetkit-rust/packages/inspector-protocol/src/versioned.rs @@ -69,12 +69,18 @@ impl ToServer { v1::ToServerBody::PatchStateRequest(req) => { v2::ToServerBody::PatchStateRequest(req.into()) } - v1::ToServerBody::StateRequest(req) => v2::ToServerBody::StateRequest(req.into()), + v1::ToServerBody::StateRequest(req) => { + v2::ToServerBody::StateRequest(req.into()) + } v1::ToServerBody::ConnectionsRequest(req) => { v2::ToServerBody::ConnectionsRequest(req.into()) } - v1::ToServerBody::ActionRequest(req) => v2::ToServerBody::ActionRequest(req.into()), - v1::ToServerBody::RpcsListRequest(req) => v2::ToServerBody::RpcsListRequest(req.into()), + v1::ToServerBody::ActionRequest(req) => { + v2::ToServerBody::ActionRequest(req.into()) + } + v1::ToServerBody::RpcsListRequest(req) => { + v2::ToServerBody::RpcsListRequest(req.into()) + } v1::ToServerBody::EventsRequest(_) | v1::ToServerBody::ClearEventsRequest(_) => { bail!("cannot convert inspector v1 events requests to v2") } @@ -99,16 +105,24 @@ impl ToServer { v3::ToServerBody::PatchStateRequest(req) => { v4::ToServerBody::PatchStateRequest(req.into()) } - v3::ToServerBody::StateRequest(req) => v4::ToServerBody::StateRequest(req.into()), + v3::ToServerBody::StateRequest(req) => { + v4::ToServerBody::StateRequest(req.into()) + } v3::ToServerBody::ConnectionsRequest(req) => { v4::ToServerBody::ConnectionsRequest(req.into()) } - v3::ToServerBody::ActionRequest(req) => v4::ToServerBody::ActionRequest(req.into()), - v3::ToServerBody::RpcsListRequest(req) => v4::ToServerBody::RpcsListRequest(req.into()), + v3::ToServerBody::ActionRequest(req) => { + v4::ToServerBody::ActionRequest(req.into()) + } + v3::ToServerBody::RpcsListRequest(req) => { + v4::ToServerBody::RpcsListRequest(req.into()) + } v3::ToServerBody::TraceQueryRequest(req) => { v4::ToServerBody::TraceQueryRequest(req.into()) } - v3::ToServerBody::QueueRequest(req) => v4::ToServerBody::QueueRequest(req.into()), + v3::ToServerBody::QueueRequest(req) => { + v4::ToServerBody::QueueRequest(req.into()) + } v3::ToServerBody::WorkflowHistoryRequest(req) => { v4::ToServerBody::WorkflowHistoryRequest(req.into()) } @@ -132,16 +146,24 @@ impl ToServer { v4::ToServerBody::PatchStateRequest(req) => { v3::ToServerBody::PatchStateRequest(req.into()) } - v4::ToServerBody::StateRequest(req) => v3::ToServerBody::StateRequest(req.into()), + v4::ToServerBody::StateRequest(req) => { + v3::ToServerBody::StateRequest(req.into()) + } v4::ToServerBody::ConnectionsRequest(req) => { v3::ToServerBody::ConnectionsRequest(req.into()) } - v4::ToServerBody::ActionRequest(req) => v3::ToServerBody::ActionRequest(req.into()), - v4::ToServerBody::RpcsListRequest(req) => v3::ToServerBody::RpcsListRequest(req.into()), + v4::ToServerBody::ActionRequest(req) => { + v3::ToServerBody::ActionRequest(req.into()) + } + v4::ToServerBody::RpcsListRequest(req) => { + v3::ToServerBody::RpcsListRequest(req.into()) + } v4::ToServerBody::TraceQueryRequest(req) => { v3::ToServerBody::TraceQueryRequest(req.into()) } - v4::ToServerBody::QueueRequest(req) => v3::ToServerBody::QueueRequest(req.into()), + v4::ToServerBody::QueueRequest(req) => { + v3::ToServerBody::QueueRequest(req.into()) + } v4::ToServerBody::WorkflowHistoryRequest(req) => { v3::ToServerBody::WorkflowHistoryRequest(req.into()) } @@ -168,16 +190,24 @@ impl ToServer { v3::ToServerBody::PatchStateRequest(req) => { v2::ToServerBody::PatchStateRequest(req.into()) } - v3::ToServerBody::StateRequest(req) => v2::ToServerBody::StateRequest(req.into()), + v3::ToServerBody::StateRequest(req) => { + v2::ToServerBody::StateRequest(req.into()) + } v3::ToServerBody::ConnectionsRequest(req) => { v2::ToServerBody::ConnectionsRequest(req.into()) } - v3::ToServerBody::ActionRequest(req) => v2::ToServerBody::ActionRequest(req.into()), - v3::ToServerBody::RpcsListRequest(req) => v2::ToServerBody::RpcsListRequest(req.into()), + v3::ToServerBody::ActionRequest(req) => { + v2::ToServerBody::ActionRequest(req.into()) + } + v3::ToServerBody::RpcsListRequest(req) => { + v2::ToServerBody::RpcsListRequest(req.into()) + } v3::ToServerBody::TraceQueryRequest(req) => { v2::ToServerBody::TraceQueryRequest(req.into()) } - v3::ToServerBody::QueueRequest(req) => v2::ToServerBody::QueueRequest(req.into()), + v3::ToServerBody::QueueRequest(req) => { + v2::ToServerBody::QueueRequest(req.into()) + } v3::ToServerBody::WorkflowHistoryRequest(req) => { v2::ToServerBody::WorkflowHistoryRequest(req.into()) } @@ -199,12 +229,18 @@ impl ToServer { v2::ToServerBody::PatchStateRequest(req) => { v1::ToServerBody::PatchStateRequest(req.into()) } - v2::ToServerBody::StateRequest(req) => v1::ToServerBody::StateRequest(req.into()), + v2::ToServerBody::StateRequest(req) => { + v1::ToServerBody::StateRequest(req.into()) + } v2::ToServerBody::ConnectionsRequest(req) => { v1::ToServerBody::ConnectionsRequest(req.into()) } - v2::ToServerBody::ActionRequest(req) => v1::ToServerBody::ActionRequest(req.into()), - v2::ToServerBody::RpcsListRequest(req) => v1::ToServerBody::RpcsListRequest(req.into()), + v2::ToServerBody::ActionRequest(req) => { + v1::ToServerBody::ActionRequest(req.into()) + } + v2::ToServerBody::RpcsListRequest(req) => { + v1::ToServerBody::RpcsListRequest(req.into()) + } v2::ToServerBody::TraceQueryRequest(_) | v2::ToServerBody::QueueRequest(_) | v2::ToServerBody::WorkflowHistoryRequest(_) => { @@ -273,18 +309,24 @@ impl ToClient { }; let body = match data.body { - v1::ToClientBody::StateResponse(resp) => v2::ToClientBody::StateResponse(resp.into()), + v1::ToClientBody::StateResponse(resp) => { + v2::ToClientBody::StateResponse(resp.into()) + } v1::ToClientBody::ConnectionsResponse(resp) => { v2::ToClientBody::ConnectionsResponse(resp.into()) } - v1::ToClientBody::ActionResponse(resp) => v2::ToClientBody::ActionResponse(resp.into()), + v1::ToClientBody::ActionResponse(resp) => { + v2::ToClientBody::ActionResponse(resp.into()) + } v1::ToClientBody::RpcsListResponse(resp) => { v2::ToClientBody::RpcsListResponse(resp.into()) } v1::ToClientBody::ConnectionsUpdated(update) => { v2::ToClientBody::ConnectionsUpdated(update.into()) } - v1::ToClientBody::StateUpdated(update) => v2::ToClientBody::StateUpdated(update.into()), + v1::ToClientBody::StateUpdated(update) => { + v2::ToClientBody::StateUpdated(update.into()) + } v1::ToClientBody::Error(error) => v2::ToClientBody::Error(error.into()), v1::ToClientBody::Init(init) => v2::ToClientBody::Init(v2::Init { connections: convert_vec(init.connections), @@ -317,16 +359,24 @@ impl ToClient { }; let body = match data.body { - v3::ToClientBody::StateResponse(resp) => v4::ToClientBody::StateResponse(resp.into()), + v3::ToClientBody::StateResponse(resp) => { + v4::ToClientBody::StateResponse(resp.into()) + } v3::ToClientBody::ConnectionsResponse(resp) => { v4::ToClientBody::ConnectionsResponse(resp.into()) } - v3::ToClientBody::ActionResponse(resp) => v4::ToClientBody::ActionResponse(resp.into()), + v3::ToClientBody::ActionResponse(resp) => { + v4::ToClientBody::ActionResponse(resp.into()) + } v3::ToClientBody::ConnectionsUpdated(update) => { v4::ToClientBody::ConnectionsUpdated(update.into()) } - v3::ToClientBody::QueueUpdated(update) => v4::ToClientBody::QueueUpdated(update.into()), - v3::ToClientBody::StateUpdated(update) => v4::ToClientBody::StateUpdated(update.into()), + v3::ToClientBody::QueueUpdated(update) => { + v4::ToClientBody::QueueUpdated(update.into()) + } + v3::ToClientBody::StateUpdated(update) => { + v4::ToClientBody::StateUpdated(update.into()) + } v3::ToClientBody::WorkflowHistoryUpdated(update) => { v4::ToClientBody::WorkflowHistoryUpdated(update.into()) } @@ -336,7 +386,9 @@ impl ToClient { v3::ToClientBody::TraceQueryResponse(resp) => { v4::ToClientBody::TraceQueryResponse(resp.into()) } - v3::ToClientBody::QueueResponse(resp) => v4::ToClientBody::QueueResponse(resp.into()), + v3::ToClientBody::QueueResponse(resp) => { + v4::ToClientBody::QueueResponse(resp.into()) + } v3::ToClientBody::WorkflowHistoryResponse(resp) => { v4::ToClientBody::WorkflowHistoryResponse(resp.into()) } @@ -359,16 +411,24 @@ impl ToClient { }; let body = match data.body { - v4::ToClientBody::StateResponse(resp) => v3::ToClientBody::StateResponse(resp.into()), + v4::ToClientBody::StateResponse(resp) => { + v3::ToClientBody::StateResponse(resp.into()) + } v4::ToClientBody::ConnectionsResponse(resp) => { v3::ToClientBody::ConnectionsResponse(resp.into()) } - v4::ToClientBody::ActionResponse(resp) => v3::ToClientBody::ActionResponse(resp.into()), + v4::ToClientBody::ActionResponse(resp) => { + v3::ToClientBody::ActionResponse(resp.into()) + } v4::ToClientBody::ConnectionsUpdated(update) => { v3::ToClientBody::ConnectionsUpdated(update.into()) } - v4::ToClientBody::QueueUpdated(update) => v3::ToClientBody::QueueUpdated(update.into()), - v4::ToClientBody::StateUpdated(update) => v3::ToClientBody::StateUpdated(update.into()), + v4::ToClientBody::QueueUpdated(update) => { + v3::ToClientBody::QueueUpdated(update.into()) + } + v4::ToClientBody::StateUpdated(update) => { + v3::ToClientBody::StateUpdated(update.into()) + } v4::ToClientBody::WorkflowHistoryUpdated(update) => { v3::ToClientBody::WorkflowHistoryUpdated(update.into()) } @@ -378,7 +438,9 @@ impl ToClient { v4::ToClientBody::TraceQueryResponse(resp) => { v3::ToClientBody::TraceQueryResponse(resp.into()) } - v4::ToClientBody::QueueResponse(resp) => v3::ToClientBody::QueueResponse(resp.into()), + v4::ToClientBody::QueueResponse(resp) => { + v3::ToClientBody::QueueResponse(resp.into()) + } v4::ToClientBody::WorkflowHistoryResponse(resp) => { v3::ToClientBody::WorkflowHistoryResponse(resp.into()) } @@ -404,16 +466,24 @@ impl ToClient { }; let body = match data.body { - v3::ToClientBody::StateResponse(resp) => v2::ToClientBody::StateResponse(resp.into()), + v3::ToClientBody::StateResponse(resp) => { + v2::ToClientBody::StateResponse(resp.into()) + } v3::ToClientBody::ConnectionsResponse(resp) => { v2::ToClientBody::ConnectionsResponse(resp.into()) } - v3::ToClientBody::ActionResponse(resp) => v2::ToClientBody::ActionResponse(resp.into()), + v3::ToClientBody::ActionResponse(resp) => { + v2::ToClientBody::ActionResponse(resp.into()) + } v3::ToClientBody::ConnectionsUpdated(update) => { v2::ToClientBody::ConnectionsUpdated(update.into()) } - v3::ToClientBody::QueueUpdated(update) => v2::ToClientBody::QueueUpdated(update.into()), - v3::ToClientBody::StateUpdated(update) => v2::ToClientBody::StateUpdated(update.into()), + v3::ToClientBody::QueueUpdated(update) => { + v2::ToClientBody::QueueUpdated(update.into()) + } + v3::ToClientBody::StateUpdated(update) => { + v2::ToClientBody::StateUpdated(update.into()) + } v3::ToClientBody::WorkflowHistoryUpdated(update) => { v2::ToClientBody::WorkflowHistoryUpdated(update.into()) } @@ -423,7 +493,9 @@ impl ToClient { v3::ToClientBody::TraceQueryResponse(resp) => { v2::ToClientBody::TraceQueryResponse(resp.into()) } - v3::ToClientBody::QueueResponse(resp) => v2::ToClientBody::QueueResponse(resp.into()), + v3::ToClientBody::QueueResponse(resp) => { + v2::ToClientBody::QueueResponse(resp.into()) + } v3::ToClientBody::WorkflowHistoryResponse(resp) => { v2::ToClientBody::WorkflowHistoryResponse(resp.into()) } @@ -444,15 +516,21 @@ impl ToClient { }; let body = match data.body { - v2::ToClientBody::StateResponse(resp) => v1::ToClientBody::StateResponse(resp.into()), + v2::ToClientBody::StateResponse(resp) => { + v1::ToClientBody::StateResponse(resp.into()) + } v2::ToClientBody::ConnectionsResponse(resp) => { v1::ToClientBody::ConnectionsResponse(resp.into()) } - v2::ToClientBody::ActionResponse(resp) => v1::ToClientBody::ActionResponse(resp.into()), + v2::ToClientBody::ActionResponse(resp) => { + v1::ToClientBody::ActionResponse(resp.into()) + } v2::ToClientBody::ConnectionsUpdated(update) => { v1::ToClientBody::ConnectionsUpdated(update.into()) } - v2::ToClientBody::StateUpdated(update) => v1::ToClientBody::StateUpdated(update.into()), + v2::ToClientBody::StateUpdated(update) => { + v1::ToClientBody::StateUpdated(update.into()) + } v2::ToClientBody::RpcsListResponse(resp) => { v1::ToClientBody::RpcsListResponse(resp.into()) } @@ -642,15 +720,11 @@ macro_rules! impl_common_actor_pair { impl_same_fields_pair!($left, $right, Connection { id, details }); impl_connections_response_pair!($left, $right); impl_connection_list_pair!($left, $right, ConnectionsUpdated); - impl_same_fields_pair!( - $left, - $right, - StateResponse { - rid, - state, - is_state_enabled, - } - ); + impl_same_fields_pair!($left, $right, StateResponse { + rid, + state, + is_state_enabled, + }); impl_same_fields_pair!($left, $right, ActionResponse { rid, output }); impl_same_fields_pair!($left, $right, StateUpdated { state }); impl_same_fields_pair!($left, $right, RpcsListResponse { rid, rpcs }); @@ -660,40 +734,28 @@ macro_rules! impl_common_actor_pair { macro_rules! impl_queue_workflow_pair { ($left:ident, $right:ident) => { - impl_same_fields_pair!( - $left, - $right, - TraceQueryRequest { - id, - start_ms, - end_ms, - limit, - } - ); + impl_same_fields_pair!($left, $right, TraceQueryRequest { + id, + start_ms, + end_ms, + limit, + }); impl_same_fields_pair!($left, $right, TraceQueryResponse { rid, payload }); impl_same_fields_pair!($left, $right, QueueRequest { id, limit }); - impl_same_fields_pair!( - $left, - $right, - QueueMessageSummary { - id, - name, - created_at_ms, - } - ); + impl_same_fields_pair!($left, $right, QueueMessageSummary { + id, + name, + created_at_ms, + }); impl_queue_status_pair!($left, $right); impl_queue_response_pair!($left, $right); impl_same_fields_pair!($left, $right, QueueUpdated { queue_size }); impl_same_fields_pair!($left, $right, WorkflowHistoryRequest { id }); - impl_same_fields_pair!( - $left, - $right, - WorkflowHistoryResponse { - rid, - history, - is_workflow_enabled, - } - ); + impl_same_fields_pair!($left, $right, WorkflowHistoryResponse { + rid, + history, + is_workflow_enabled, + }); impl_same_fields_pair!($left, $right, WorkflowHistoryUpdated { history }); impl_init_pair!($left, $right); }; @@ -703,16 +765,12 @@ macro_rules! impl_database_pair { ($left:ident, $right:ident) => { impl_same_fields_pair!($left, $right, DatabaseSchemaRequest { id }); impl_same_fields_pair!($left, $right, DatabaseSchemaResponse { rid, schema }); - impl_same_fields_pair!( - $left, - $right, - DatabaseTableRowsRequest { - id, - table, - limit, - offset, - } - ); + impl_same_fields_pair!($left, $right, DatabaseTableRowsRequest { + id, + table, + limit, + offset, + }); impl_same_fields_pair!($left, $right, DatabaseTableRowsResponse { rid, result }); }; } @@ -755,7 +813,9 @@ impl From for v3::ToClientBody { v2::ToClientBody::StateResponse(resp) => Self::StateResponse(resp.into()), v2::ToClientBody::ConnectionsResponse(resp) => Self::ConnectionsResponse(resp.into()), v2::ToClientBody::ActionResponse(resp) => Self::ActionResponse(resp.into()), - v2::ToClientBody::ConnectionsUpdated(update) => Self::ConnectionsUpdated(update.into()), + v2::ToClientBody::ConnectionsUpdated(update) => { + Self::ConnectionsUpdated(update.into()) + } v2::ToClientBody::QueueUpdated(update) => Self::QueueUpdated(update.into()), v2::ToClientBody::StateUpdated(update) => Self::StateUpdated(update.into()), v2::ToClientBody::WorkflowHistoryUpdated(update) => { diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/context.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/context.rs index 69a19bc821..e67c096f3a 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/context.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/context.rs @@ -10,9 +10,9 @@ use crate::time::{Instant, SystemTime, UNIX_EPOCH}; use anyhow::{Context as AnyhowContext, Result}; use futures::future::BoxFuture; use parking_lot::{Mutex, RwLock}; +use rivet_error::ActorSpecifier; use rivet_envoy_client::handle::EnvoyHandle; use rivet_envoy_client::tunnel::HibernatingWebSocketMetadata; -use rivet_error::ActorSpecifier; use scc::HashMap as SccHashMap; use tokio::runtime::Handle; use tokio::sync::{Mutex as AsyncMutex, Notify, OnceCell, broadcast, mpsc, oneshot}; diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/metrics.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/metrics.rs index 66d30d9239..6d5f6e11c3 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/metrics.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/metrics.rs @@ -29,13 +29,8 @@ const SQLITE_COMMIT_PHASE_LABELS: &[&str] = &["actor_id_gen", "actor_key", "envo const SQLITE_WORKER_COMMAND_LABELS: &[&str] = &["actor_id_gen", "actor_key", "envoy_key", "operation"]; #[cfg(feature = "sqlite-local")] -const SQLITE_WORKER_ERROR_LABELS: &[&str] = &[ - "actor_id_gen", - "actor_key", - "envoy_key", - "operation", - "code", -]; +const SQLITE_WORKER_ERROR_LABELS: &[&str] = + &["actor_id_gen", "actor_key", "envoy_key", "operation", "code"]; pub(crate) struct ActorMetrics { labels: Arc, @@ -162,10 +157,7 @@ impl ActorMetricCollectors { ) .expect("create actor_queue_depth gauge"); let queue_messages_sent_total = IntCounterVec::new( - Opts::new( - "actor_queue_messages_sent_total", - "total queue messages sent", - ), + Opts::new("actor_queue_messages_sent_total", "total queue messages sent"), ACTOR_LABELS, ) .expect("create actor_queue_messages_sent_total counter"); @@ -460,18 +452,12 @@ impl ActorMetricCollectors { register_metric(&rivet_metrics::REGISTRY, create_vars_ms.clone()); register_metric(&rivet_metrics::REGISTRY, queue_depth.clone()); register_metric(&rivet_metrics::REGISTRY, queue_messages_sent_total.clone()); - register_metric( - &rivet_metrics::REGISTRY, - queue_messages_received_total.clone(), - ); + register_metric(&rivet_metrics::REGISTRY, queue_messages_received_total.clone()); register_metric(&rivet_metrics::REGISTRY, active_connections.clone()); register_metric(&rivet_metrics::REGISTRY, connections_total.clone()); register_metric(&rivet_metrics::REGISTRY, lifecycle_inbox_depth.clone()); register_metric(&rivet_metrics::REGISTRY, dispatch_inbox_depth.clone()); - register_metric( - &rivet_metrics::REGISTRY, - lifecycle_event_inbox_depth.clone(), - ); + register_metric(&rivet_metrics::REGISTRY, lifecycle_event_inbox_depth.clone()); register_metric(&rivet_metrics::REGISTRY, user_tasks_active.clone()); register_metric(&rivet_metrics::REGISTRY, user_task_duration_seconds.clone()); register_metric(&rivet_metrics::REGISTRY, shutdown_wait_seconds.clone()); @@ -483,10 +469,7 @@ impl ActorMetricCollectors { ); #[cfg(feature = "sqlite-local")] { - register_metric( - &rivet_metrics::REGISTRY, - sqlite_vfs_resolve_pages_total.clone(), - ); + register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_resolve_pages_total.clone()); register_metric( &rivet_metrics::REGISTRY, sqlite_vfs_resolve_pages_requested_total.clone(), @@ -500,22 +483,10 @@ impl ActorMetricCollectors { sqlite_vfs_resolve_pages_cache_misses_total.clone(), ); register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_get_pages_total.clone()); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_vfs_pages_fetched_total.clone(), - ); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_vfs_prefetch_pages_total.clone(), - ); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_vfs_bytes_fetched_total.clone(), - ); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_vfs_prefetch_bytes_total.clone(), - ); + register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_pages_fetched_total.clone()); + register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_prefetch_pages_total.clone()); + register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_bytes_fetched_total.clone()); + register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_prefetch_bytes_total.clone()); register_metric( &rivet_metrics::REGISTRY, sqlite_vfs_get_pages_duration_seconds.clone(), @@ -530,31 +501,16 @@ impl ActorMetricCollectors { sqlite_vfs_commit_duration_seconds_total.clone(), ); register_metric(&rivet_metrics::REGISTRY, sqlite_worker_queue_depth.clone()); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_worker_queue_overload_total.clone(), - ); + register_metric(&rivet_metrics::REGISTRY, sqlite_worker_queue_overload_total.clone()); register_metric( &rivet_metrics::REGISTRY, sqlite_worker_command_duration_seconds.clone(), ); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_worker_command_error_total.clone(), - ); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_worker_close_duration_seconds.clone(), - ); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_worker_close_timeout_total.clone(), - ); + register_metric(&rivet_metrics::REGISTRY, sqlite_worker_command_error_total.clone()); + register_metric(&rivet_metrics::REGISTRY, sqlite_worker_close_duration_seconds.clone()); + register_metric(&rivet_metrics::REGISTRY, sqlite_worker_close_timeout_total.clone()); register_metric(&rivet_metrics::REGISTRY, sqlite_worker_crash_total.clone()); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_worker_unclean_close_total.clone(), - ); + register_metric(&rivet_metrics::REGISTRY, sqlite_worker_unclean_close_total.clone()); } Self { @@ -735,7 +691,10 @@ impl ActorMetrics { }); let labels = self.actor_labels(); let labels = [labels[0], labels[1], labels[2], kind.as_metric_label()]; - METRICS.user_tasks_active.with_label_values(&labels).dec(); + METRICS + .user_tasks_active + .with_label_values(&labels) + .dec(); METRICS .user_task_duration_seconds .with_label_values(&labels) @@ -868,13 +827,7 @@ impl depot_client::vfs::SqliteVfsMetrics for ActorMetrics { total_ns: u64, ) { record_retained_actor_metrics(&self.labels, |retained| { - for phase in [ - "request_build", - "serialize", - "transport", - "state_update", - "total", - ] { + for phase in ["request_build", "serialize", "transport", "state_update", "total"] { push_unique(&mut retained.sqlite_commit_phases, phase); } }); @@ -1153,12 +1106,7 @@ fn remove_retained_actor_metrics(labels: &ActorMetricLabels, retained: &Retained ); } for operation in &retained.sqlite_worker_operations { - let labels = [ - actor_labels[0], - actor_labels[1], - actor_labels[2], - *operation, - ]; + let labels = [actor_labels[0], actor_labels[1], actor_labels[2], *operation]; ignore_missing_labels( metrics .sqlite_worker_command_duration_seconds diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/sleep.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/sleep.rs index 1901076d5b..9144fc1648 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/sleep.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/sleep.rs @@ -13,20 +13,22 @@ use tokio::task::JoinHandle; use tracing::Instrument; use crate::actor::config::ActorConfig; -use crate::actor::context::ActorContext; #[cfg(not(feature = "wasm-runtime"))] use crate::actor::context::ActorWorkRegion; +use crate::actor::context::ActorContext; use crate::actor::task_types::ShutdownKind; -#[cfg(not(feature = "wasm-runtime"))] -use crate::actor::work_registry::ActorWorkPolicy; #[cfg(feature = "wasm-runtime")] use crate::actor::work_registry::LocalShutdownTask; -use crate::actor::work_registry::{ActorWorkKind, CountGuard, RegionGuard, WorkRegistry}; +#[cfg(not(feature = "wasm-runtime"))] +use crate::actor::work_registry::ActorWorkPolicy; +use crate::actor::work_registry::{ + ActorWorkKind, CountGuard, RegionGuard, WorkRegistry, +}; #[cfg(feature = "wasm-runtime")] use crate::runtime::RuntimeSpawner; +use crate::time::{Instant, sleep}; #[cfg(test)] use crate::time::sleep_until; -use crate::time::{Instant, sleep}; #[cfg(test)] use crate::types::ActorKey; #[cfg(feature = "wasm-runtime")] @@ -502,10 +504,7 @@ impl ActorContext { F: Future + Send + 'static, { if Handle::try_current().is_err() { - tracing::warn!( - kind = kind.label(), - "actor work spawned without tokio runtime" - ); + tracing::warn!(kind = kind.label(), "actor work spawned without tokio runtime"); return false; } @@ -513,10 +512,7 @@ impl ActorContext { if policy.aborts_at_shutdown_deadline { let mut shutdown_tasks = self.0.sleep.work.shutdown_tasks.lock(); if self.0.sleep.work.teardown_started.load(Ordering::Acquire) { - tracing::warn!( - kind = kind.label(), - "actor work spawned after teardown; aborting immediately" - ); + tracing::warn!(kind = kind.label(), "actor work spawned after teardown; aborting immediately"); return false; } let region = self.begin_work_region(kind); @@ -525,10 +521,7 @@ impl ActorContext { let mut unabortable_shutdown_tasks = self.0.sleep.work.unabortable_shutdown_tasks.lock(); if self.0.sleep.work.teardown_started.load(Ordering::Acquire) { - tracing::warn!( - kind = kind.label(), - "actor work spawned after teardown; aborting immediately" - ); + tracing::warn!(kind = kind.label(), "actor work spawned after teardown; aborting immediately"); return false; } let region = self.begin_work_region(kind); @@ -581,10 +574,7 @@ impl ActorContext { { let mut local_shutdown_tasks = self.0.sleep.work.local_shutdown_tasks.lock(); if self.0.sleep.work.teardown_started.load(Ordering::Acquire) { - tracing::warn!( - kind = kind.label(), - "actor work spawned after teardown; aborting immediately" - ); + tracing::warn!(kind = kind.label(), "actor work spawned after teardown; aborting immediately"); return false; } diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs index 35b3de3bbf..a401cefe2b 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs @@ -10,9 +10,9 @@ use depot_client_types::is_head_fence_mismatch; pub use depot_client_types::{BindParam, ColumnValue, ExecResult, ExecuteResult, QueryResult}; #[cfg(feature = "sqlite-local")] use parking_lot::Mutex; +use rivet_error::{ActorSpecifier, RivetError}; use rivet_envoy_client::protocol; use rivet_envoy_client::{handle::EnvoyHandle, utils::RemoteSqliteIndeterminateResultError}; -use rivet_error::{ActorSpecifier, RivetError}; use serde::Serialize; use serde_json::{Map as JsonMap, Value as JsonValue}; #[cfg(feature = "sqlite-local")] @@ -507,7 +507,8 @@ impl SqliteDb { } fn actor_specifier(&self) -> Option { - let mut specifier = ActorSpecifier::new(self.actor_id.as_ref()?.clone(), self.generation?); + let mut specifier = + ActorSpecifier::new(self.actor_id.as_ref()?.clone(), self.generation?); if let Some(key) = self.actor_key.as_ref() { specifier = specifier.with_key(key.clone()); } diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs index 3083975fd2..2f771e5c28 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs @@ -890,8 +890,7 @@ impl ActorTask { let _action_keep_awake = action_keep_awake; match tracked_reply_rx.await { Ok(result) => { - let result = - result.map_err(|error| ctx.attach_actor_to_error(error)); + let result = result.map_err(|error| ctx.attach_actor_to_error(error)); tracing::info!( actor_id = %actor_id, action_name = %action_name_for_log, diff --git a/rivetkit-rust/packages/rivetkit-core/src/registry/actor_connect.rs b/rivetkit-rust/packages/rivetkit-core/src/registry/actor_connect.rs index 1b9c43bd31..f70cfce65d 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/registry/actor_connect.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/registry/actor_connect.rs @@ -64,14 +64,13 @@ pub(super) fn encode_actor_connect_message(message: &ActorConnectToClient) -> Re .as_ref() .map(|metadata| metadata.as_ref().to_vec()), action_id: payload.action_id.map(serde_bare::Uint), - actor: payload - .actor - .as_ref() - .map(|actor| client_protocol::ActorSpecifier { + actor: payload.actor.as_ref().map(|actor| { + client_protocol::ActorSpecifier { actor_id: actor.actor_id.clone(), generation: serde_bare::Uint(actor.generation), key: actor.key.clone(), - }), + } + }), }) } ActorConnectToClient::ActionResponse(payload) => { diff --git a/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs b/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs index f24612503d..3527621953 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs @@ -1,7 +1,7 @@ use super::dispatch::*; use super::inspector::*; use super::*; -use crate::error::{ProtocolError, client_error_message, client_error_metadata}; +use crate::error::{client_error_message, client_error_metadata, ProtocolError}; use ::http; const HEADER_RIVET_ACTOR: &str = "x-rivet-actor"; @@ -378,6 +378,7 @@ impl RegistryDispatcher { } } } + } enum RegistryHttpRoute { diff --git a/rivetkit-rust/packages/rivetkit-core/src/registry/inspector.rs b/rivetkit-rust/packages/rivetkit-core/src/registry/inspector.rs index 1c2929df00..56816c4aac 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/registry/inspector.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/registry/inspector.rs @@ -1,7 +1,7 @@ use super::dispatch::*; use super::http::*; use super::*; -use crate::error::{ProtocolError, client_error_message}; +use crate::error::{client_error_message, ProtocolError}; use ::http; #[derive(rivet_error::RivetError, serde::Serialize)] @@ -346,7 +346,9 @@ impl RegistryDispatcher { ) -> Result<(bool, Option>)> { let result = instance .ctx - .internal_keep_awake(dispatch_workflow_history_through_task(&instance.dispatch)) + .internal_keep_awake(dispatch_workflow_history_through_task( + &instance.dispatch, + )) .await .context("load inspector workflow history"); diff --git a/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs b/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs index 75f3f444d2..3c98819220 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs @@ -625,8 +625,11 @@ impl RegistryDispatcher { let (start_tx, start_rx) = oneshot::channel(); let result: Result> = async { - try_send_lifecycle_command(&lifecycle_tx, LifecycleCommand::Start { reply: start_tx }) - .context("send actor task start command")?; + try_send_lifecycle_command( + &lifecycle_tx, + LifecycleCommand::Start { reply: start_tx }, + ) + .context("send actor task start command")?; start_rx .await .context("receive actor task start reply")? diff --git a/rivetkit-rust/packages/rivetkit-core/tests/metrics.rs b/rivetkit-rust/packages/rivetkit-core/tests/metrics.rs index 73075682a9..6a636edf35 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/metrics.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/metrics.rs @@ -8,8 +8,8 @@ mod moved_tests { use rivet_metrics::prometheus::{IntGauge, Opts, Registry}; - use super::metrics_helpers::{metric_line_for_actor, render_global_metrics}; use super::*; + use super::metrics_helpers::{metric_line_for_actor, render_global_metrics}; #[test] fn duplicate_metric_registration_uses_noop_fallback() { @@ -102,9 +102,6 @@ mod moved_tests { && line.contains("envoy_key=\"envoy-1\"") }) .unwrap_or_else(|| panic!("{name} should render")); - assert!( - line.ends_with(value), - "{name} should have value {value}: {line}" - ); + assert!(line.ends_with(value), "{name} should have value {value}: {line}"); } } diff --git a/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs b/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs index 23e0c365af..23b54ac5d6 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs @@ -8,7 +8,8 @@ mod moved_tests { HttpResponseEncoding, authorization_bearer_token, authorization_bearer_token_map, framework_action_error_response, framework_anyhow_error_response_with_actor, is_actor_request_path, message_boundary_error_response, - message_boundary_error_response_with_actor, normalize_actor_request_path, request_encoding, + message_boundary_error_response_with_actor, normalize_actor_request_path, + request_encoding, workflow_dispatch_result, }; use crate::actor::action::ActionDispatchError; diff --git a/rivetkit-rust/packages/rivetkit-core/tests/state.rs b/rivetkit-rust/packages/rivetkit-core/tests/state.rs index abf648e2e6..400fe53d14 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/state.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/state.rs @@ -178,7 +178,10 @@ mod moved_tests { #[tokio::test] async fn request_save_coalesces_and_escalates_to_immediate() { - let state = ActorContext::new_for_state_tests(new_in_memory(), ActorConfig::default()); + let state = ActorContext::new_for_state_tests( + new_in_memory(), + ActorConfig::default(), + ); let (events_tx, mut events_rx) = mpsc::unbounded_channel(); state.configure_lifecycle_events(Some(events_tx)); diff --git a/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs b/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs index d9e28c25c5..4553e11d32 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs +++ b/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs @@ -1,5 +1,5 @@ -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::Duration; use anyhow::Result; diff --git a/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs b/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs index f3f1207b62..a1ea940dd6 100644 --- a/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs +++ b/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs @@ -7,15 +7,18 @@ use std::time::Duration; use anyhow::{Result, anyhow}; use js_sys::{Array, Function, Object, Promise, Reflect, Uint8Array}; -use rivet_error::{ActorSpecifier, RivetError as RivetTransportError, RivetErrorKind}; +use rivet_error::{ + ActorSpecifier, RivetError as RivetTransportError, RivetErrorKind, +}; use rivetkit_core::error::public_error_status_code; use rivetkit_core::inspector::InspectorAuth; use rivetkit_core::{ ActorConfig, ActorConfigInput, ActorEvent, ActorFactory as CoreActorFactory, ActorStart, - ActorWorkKind, BindParam, ColumnValue, CoreRegistry as NativeCoreRegistry, - CoreServerlessRuntime, EnqueueAndWaitOpts, KeepAwakeRegion, ListOpts, QueueMessage, - QueueNextBatchOpts, QueueSendResult, QueueSendStatus, QueueTryNextBatchOpts, QueueWaitOpts, - Request, RequestSaveOpts, Response, RuntimeSpawner, SerializeStateReason, ServeConfig, + ActorWorkKind, + BindParam, ColumnValue, CoreRegistry as NativeCoreRegistry, CoreServerlessRuntime, + EnqueueAndWaitOpts, KeepAwakeRegion, ListOpts, QueueMessage, QueueNextBatchOpts, + QueueSendResult, QueueSendStatus, QueueTryNextBatchOpts, QueueWaitOpts, Request, + RequestSaveOpts, Response, RuntimeSpawner, SerializeStateReason, ServeConfig, ServerlessRequest, StateDelta, WebSocket, WebSocketCallbackRegion, WsMessage, }; use tokio::sync::oneshot; @@ -2862,7 +2865,9 @@ mod tests { } fn transport_message(error: &anyhow::Error) -> String { - transport_error(error).message().to_owned() + transport_error(error) + .message() + .to_owned() } #[test] From e3f8146e7871d71646289dd53d71d707ed8e2c17 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Thu, 21 May 2026 16:23:15 +0200 Subject: [PATCH 257/306] fix(effect): scope example helper actors by run --- examples/effect/src/actors/chat-room/live.ts | 7 +++++-- examples/effect/src/client.ts | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts index 932aa2d3a7..9066904c11 100644 --- a/examples/effect/src/actors/chat-room/live.ts +++ b/examples/effect/src/actors/chat-room/live.ts @@ -12,8 +12,11 @@ export const ChatRoomLive = ChatRoom.toLayer( const moderatorClient = yield* Moderator.client; const directoryClient = yield* Directory.client; - const directory = directoryClient.getOrCreate(["main"]); - const moderator = moderatorClient.getOrCreate(["main"]); + // This is a workaround. Scope helper actors to this run so stale + // singleton actors left in the local engine DB cannot trap nested RPCs. + const runKey = ["run", ...address.key]; + const directory = directoryClient.getOrCreate(runKey); + const moderator = moderatorClient.getOrCreate(runKey); // The plain SDK example stores this in createVars. The Effect SDK // does not expose vars yet, so the wake-scope closure owns it. const sessionId = yield* Random.nextUUIDv4; diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 1e6a0e2bcc..6a62324844 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -23,10 +23,13 @@ const program = Effect.gen(function* () { const directoryClient = yield* Directory.client; const moderatorClient = yield* Moderator.client; + // This is a workaround. Scope helper actors to this run so stale + // singleton actors left in the local engine DB cannot trap nested RPCs. const roomName = `effect-room-${runId}`; + const runKey = ["run", roomName]; const room = chatRoomClient.getOrCreate([roomName]); - const directory = directoryClient.getOrCreate(["main"]); - const moderator = moderatorClient.getOrCreate(["main"]); + const directory = directoryClient.getOrCreate(runKey); + const moderator = moderatorClient.getOrCreate(runKey); yield* room.Initialize({ name: roomName }); yield* Effect.log(`ChatRoom.Initialize`); From 6a602fc3ea5101273d5d3eb1fe6f5f811a6644a7 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 22 May 2026 13:01:04 +0200 Subject: [PATCH 258/306] Add chat room membership errors --- examples/effect/src/actors/chat-room/api.ts | 15 ++++++++++++++- examples/effect/src/actors/chat-room/live.ts | 17 ++++++++++++++++- examples/effect/src/client.ts | 16 ++++++++++++++-- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/examples/effect/src/actors/chat-room/api.ts b/examples/effect/src/actors/chat-room/api.ts index 39aa41978b..9b3e7339a3 100644 --- a/examples/effect/src/actors/chat-room/api.ts +++ b/examples/effect/src/actors/chat-room/api.ts @@ -2,6 +2,18 @@ import { Action, Actor } from "@rivetkit/effect"; import { Schema } from "effect"; import { BannerWordsError } from "../mod"; +// --- Errors --- + +export class MemberNotInRoomError extends Schema.TaggedErrorClass()( + "MemberNotInRoomError", + { + name: Schema.String, + message: Schema.String, + }, +) {} + +// --- Actions --- + export const Member = Schema.Struct({ name: Schema.String, joinedAt: Schema.DateTimeUtc, @@ -28,6 +40,7 @@ export const Join = Action.make("Join", { export const Leave = Action.make("Leave", { payload: { name: Schema.String }, + error: MemberNotInRoomError, }); export const SendMessage = Action.make("SendMessage", { @@ -35,7 +48,7 @@ export const SendMessage = Action.make("SendMessage", { sender: Schema.String, text: Schema.String, }, - error: BannerWordsError, + error: Schema.Union([MemberNotInRoomError, BannerWordsError]), }); export const GetHistory = Action.make("GetHistory", { diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts index 9066904c11..50f39724bf 100644 --- a/examples/effect/src/actors/chat-room/live.ts +++ b/examples/effect/src/actors/chat-room/live.ts @@ -2,7 +2,7 @@ import { Actor, State } from "@rivetkit/effect"; import { DateTime, Duration, Effect, Random, Schema } from "effect"; import { db } from "rivetkit/db"; import { Directory, Moderator } from "../mod.ts"; -import { ChatRoom } from "./api.ts"; +import { ChatRoom, MemberNotInRoomError } from "./api.ts"; export const ChatRoomLive = ChatRoom.toLayer( ({ rawRivetkitContext, state }) => @@ -50,6 +50,19 @@ export const ChatRoomLive = ChatRoom.toLayer( Effect.map((s) => s.name), ); + const ensureMember = (name: string) => + State.get(state).pipe( + Effect.orDie, + Effect.flatMap((current) => + current.members.some((member) => member.name === name) + ? Effect.void + : new MemberNotInRoomError({ + name, + message: `${name} is not a member of this room`, + }), + ), + ); + return ChatRoom.of({ Initialize: ({ payload }) => // This replaces createState(input). Callers should initialize @@ -95,6 +108,7 @@ export const ChatRoomLive = ChatRoom.toLayer( }), Leave: ({ payload }) => Effect.gen(function* () { + yield* ensureMember(payload.name); yield* State.update(state, (current) => ({ ...current, members: current.members.filter( @@ -107,6 +121,7 @@ export const ChatRoomLive = ChatRoom.toLayer( }), SendMessage: ({ payload }) => Effect.gen(function* () { + yield* ensureMember(payload.sender); yield* moderator.Review({ text: payload.text, }); diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 6a62324844..adc148636a 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -5,6 +5,7 @@ import { ChatRoom, Counter, Directory, + type MemberNotInRoomError, Moderator, } from "./actors/mod.ts"; @@ -23,8 +24,8 @@ const program = Effect.gen(function* () { const directoryClient = yield* Directory.client; const moderatorClient = yield* Moderator.client; - // This is a workaround. Scope helper actors to this run so stale - // singleton actors left in the local engine DB cannot trap nested RPCs. + // This is a workaround. Scope helper actors to this run so stale + // singleton actors left in the local engine DB cannot trap nested RPCs. const roomName = `effect-room-${runId}`; const runKey = ["run", roomName]; const room = chatRoomClient.getOrCreate([roomName]); @@ -43,6 +44,17 @@ const program = Effect.gen(function* () { }); yield* Effect.log(`ChatRoom.SendMessage`); + yield* room + .SendMessage({ + sender: "Mallory", + text: "I should not be able to post", + }) + .pipe( + Effect.catchTag("MemberNotInRoomError", (e: MemberNotInRoomError) => + Effect.logError(`ChatRoom.SendMessage rejected: ${e.message}`), + ), + ); + yield* room .SendMessage({ sender: "Alice", From dad1b67ba95c3e3bac3cdc686fa3ea0ce7c79cf5 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 22 May 2026 13:01:34 +0200 Subject: [PATCH 259/306] refactor(effect): rename BannerWordsError to BannedWordsError --- examples/effect/src/actors/moderator/api.ts | 6 +++--- examples/effect/src/actors/moderator/live.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/effect/src/actors/moderator/api.ts b/examples/effect/src/actors/moderator/api.ts index 5eb9a09e84..b82b70d431 100644 --- a/examples/effect/src/actors/moderator/api.ts +++ b/examples/effect/src/actors/moderator/api.ts @@ -1,8 +1,8 @@ import { Action, Actor } from "@rivetkit/effect"; import { Schema } from "effect"; -export class BannerWordsError extends Schema.TaggedErrorClass()( - "BannerWordsError", +export class BannedWordsError extends Schema.TaggedErrorClass()( + "BannedWordsError", { message: Schema.String, }, @@ -10,7 +10,7 @@ export class BannerWordsError extends Schema.TaggedErrorClass( export const Review = Action.make("Review", { payload: { text: Schema.String }, - error: BannerWordsError, + error: BannedWordsError, }); export const Stats = Action.make("Stats", { diff --git a/examples/effect/src/actors/moderator/live.ts b/examples/effect/src/actors/moderator/live.ts index 19e254f258..b5e13f94a9 100644 --- a/examples/effect/src/actors/moderator/live.ts +++ b/examples/effect/src/actors/moderator/live.ts @@ -1,6 +1,6 @@ import { State } from "@rivetkit/effect"; import { Effect, Schema } from "effect"; -import { BannerWordsError, Moderator } from "./api.ts"; +import { BannedWordsError, Moderator } from "./api.ts"; const bannedWords = ["spam", "scam"]; @@ -20,7 +20,7 @@ export const ModeratorLive = Moderator.toLayer( lower.includes(word), ); if (hit !== undefined) { - return yield* new BannerWordsError({ + return yield* new BannedWordsError({ message: `contains banned word "${hit}"`, }); } From c9b9775c14a568aff2ab089294a8305b56647678 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 22 May 2026 13:01:46 +0200 Subject: [PATCH 260/306] refactor(effect): rename BannerWordsError to BannedWordsError --- examples/effect/src/actors/chat-room/api.ts | 4 ++-- examples/effect/src/client.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/effect/src/actors/chat-room/api.ts b/examples/effect/src/actors/chat-room/api.ts index 9b3e7339a3..e947d5f39f 100644 --- a/examples/effect/src/actors/chat-room/api.ts +++ b/examples/effect/src/actors/chat-room/api.ts @@ -1,6 +1,6 @@ import { Action, Actor } from "@rivetkit/effect"; import { Schema } from "effect"; -import { BannerWordsError } from "../mod"; +import { BannedWordsError } from "../mod"; // --- Errors --- @@ -48,7 +48,7 @@ export const SendMessage = Action.make("SendMessage", { sender: Schema.String, text: Schema.String, }, - error: Schema.Union([MemberNotInRoomError, BannerWordsError]), + error: Schema.Union([MemberNotInRoomError, BannedWordsError]), }); export const GetHistory = Action.make("GetHistory", { diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index adc148636a..5fd4dd0b84 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,7 +1,7 @@ import { Client } from "@rivetkit/effect"; import { Effect, Logger, Random } from "effect"; import { - type BannerWordsError, + type BannedWordsError, ChatRoom, Counter, Directory, @@ -61,7 +61,7 @@ const program = Effect.gen(function* () { text: "this contains spam", }) .pipe( - Effect.catchTag("BannerWordsError", (e: BannerWordsError) => + Effect.catchTag("BannedWordsError", (e: BannedWordsError) => Effect.logError(`ChatRoom.SendMessage rejected: ${e.message}`), ), ); From 4f136a7a4b98a8c6ee3c323a0a4acf7505eba219 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 22 May 2026 13:24:30 +0200 Subject: [PATCH 261/306] Document Chat Room action schema benefits --- examples/effect/src/actors/chat-room/api.ts | 42 +++++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/examples/effect/src/actors/chat-room/api.ts b/examples/effect/src/actors/chat-room/api.ts index e947d5f39f..b33d29bf8b 100644 --- a/examples/effect/src/actors/chat-room/api.ts +++ b/examples/effect/src/actors/chat-room/api.ts @@ -14,6 +14,17 @@ export class MemberNotInRoomError extends Schema.TaggedErrorClass Date: Fri, 22 May 2026 15:02:16 +0200 Subject: [PATCH 262/306] refactor(effect): remove unused StateOptionsCodec type --- .../packages/effect/src/Actor.ts | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index cd9e8aac8c..a7ae430700 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -134,30 +134,6 @@ type ActionHandlerServices = { : never; }[keyof ActionHandlers]; -type StateOptionsCodec = { - readonly decode: ( - input: StateOptions.Encoded, - ) => Effect.Effect< - StateOptions.Decoded, - Schema.SchemaError, - State["schema"]["DecodingServices"] - >; - readonly decodeUnknown: ( - input: unknown, - ) => Effect.Effect< - StateOptions.Decoded, - Schema.SchemaError, - State["schema"]["DecodingServices"] - >; - readonly encode: ( - input: StateOptions.Decoded, - ) => Effect.Effect< - StateOptions.Encoded, - Schema.SchemaError, - State["schema"]["EncodingServices"] - >; -}; - type RivetkitActorDefinitionFor< State extends StateOptions.Any, Database extends RivetkitDb.AnyDatabaseProvider, From 1595d3d87e458ba3a0b910665931daa7595d4448 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 22 May 2026 15:07:11 +0200 Subject: [PATCH 263/306] fix(effect): correct `_tag` property reference in encoded error --- rivetkit-typescript/packages/effect/src/Actor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index a7ae430700..dc2b256521 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -662,7 +662,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < code: hasStringProperty("_tag")( encodedError, ) - ? action._tag + ? encodedError._tag : undefined, metadata: ActionErrorEnvelope.make( From b327e4bbc62af4293f32a9432efc320a0ae8f74e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 22 May 2026 15:07:33 +0200 Subject: [PATCH 264/306] Refactor effect chat room example around room policy --- examples/effect/src/actors/chat-room/api.ts | 60 ++---- examples/effect/src/actors/chat-room/live.ts | 206 +++++++++++-------- examples/effect/src/actors/counter/api.ts | 68 ------ examples/effect/src/actors/counter/live.ts | 116 ----------- examples/effect/src/actors/directory/api.ts | 24 --- examples/effect/src/actors/directory/live.ts | 65 ------ examples/effect/src/actors/mod.ts | 6 +- examples/effect/src/client-raw.ts | 92 +++++++-- examples/effect/src/client.ts | 144 ++++++------- examples/effect/src/main.ts | 8 +- 10 files changed, 272 insertions(+), 517 deletions(-) delete mode 100644 examples/effect/src/actors/counter/api.ts delete mode 100644 examples/effect/src/actors/counter/live.ts delete mode 100644 examples/effect/src/actors/directory/api.ts delete mode 100644 examples/effect/src/actors/directory/live.ts diff --git a/examples/effect/src/actors/chat-room/api.ts b/examples/effect/src/actors/chat-room/api.ts index b33d29bf8b..5599defc2d 100644 --- a/examples/effect/src/actors/chat-room/api.ts +++ b/examples/effect/src/actors/chat-room/api.ts @@ -1,6 +1,6 @@ import { Action, Actor } from "@rivetkit/effect"; import { Schema } from "effect"; -import { BannedWordsError } from "../mod"; +import { BannedWordsError } from "../moderator/api.ts"; // --- Errors --- @@ -25,25 +25,16 @@ export class MemberNotInRoomError extends Schema.TaggedErrorClass, + name: string, + ) => Effect.Effect; + } +>()("RoomPolicy") {} + +export const RoomPolicyLive = Layer.succeed( + RoomPolicy, + RoomPolicy.of({ + requireMember: (members, name) => + members.some((member) => member.name === name) + ? Effect.void + : Effect.fail( + new MemberNotInRoomError({ + name, + message: `${name} is not a member of this room`, + }), + ), + }), +); + +// --- Actor Implementation --- + +// `.toLayer` produces a Layer that registers this actor +// with the `Registry` service that is in context. The first parameter +// is a `wake` function that runs once when the actor awakes +// and returns the action handlers. export const ChatRoomLive = ChatRoom.toLayer( + // Wake scope (runs on each wake) ({ rawRivetkitContext, state }) => Effect.gen(function* () { - const database = rawRivetkitContext.db; + // Actor-provided services, custom services, and actor clients are all + // yielded from the Effect context for this wake. They are scoped to + // this actor instance, not to individual action calls. const address = yield* Actor.CurrentAddress; + const roomPolicy = yield* RoomPolicy; const moderatorClient = yield* Moderator.client; - const directoryClient = yield* Directory.client; - - // This is a workaround. Scope helper actors to this run so stale - // singleton actors left in the local engine DB cannot trap nested RPCs. - const runKey = ["run", ...address.key]; - const directory = directoryClient.getOrCreate(runKey); - const moderator = moderatorClient.getOrCreate(runKey); - // The plain SDK example stores this in createVars. The Effect SDK - // does not expose vars yet, so the wake-scope closure owns it. - const sessionId = yield* Random.nextUUIDv4; - - yield* State.update(state, (current) => ({ - ...current, - wakeCount: current.wakeCount + 1, - })).pipe(Effect.orDie); + + // Access the actor's persisted `state` with a `SubscriptionRef`-like API + const name = State.get(state).pipe( + Effect.orDie, + Effect.map((s) => s.name), + ); yield* Effect.log("room awake", { actorId: address.actorId, key: address.key.join("/"), - sessionId, + name, }); + // Finalizers run on sleep yield* Effect.addFinalizer(() => Effect.gen(function* () { - const current = yield* State.get(state).pipe(Effect.orDie); yield* Effect.log("room sleeping", { actorId: address.actorId, key: address.key.join("/"), - roomName: current.name, - sessionId, - wakeCount: current.wakeCount, + name, }); }), ); - const roomName = State.get(state).pipe( - Effect.orDie, - Effect.map((s) => s.name), + // `State.changes` streams every committed state change for this actor wake. + yield* State.changes(state).pipe( + Stream.runForEach((current) => + Effect.log("room state changed", { + actorId: address.actorId, + name: current.name, + memberCount: current.members.length, + }), + ), + Effect.forkScoped, ); + // Combine persisted actor state with a custom service-owned domain guard. const ensureMember = (name: string) => State.get(state).pipe( Effect.orDie, Effect.flatMap((current) => - current.members.some((member) => member.name === name) - ? Effect.void - : new MemberNotInRoomError({ - name, - message: `${name} is not a member of this room`, - }), + roomPolicy.requireMember(current.members, name), ), ); + // --- Message processing (not yet implemented) --- + // Pull-based: the actor controls when to take the next message. + // Forked into a scoped fiber, so it runs in the background and + // is canceled on sleep. Re-enable once ChatRoom messages land. + // + // yield* Effect.gen(function* () { + // const msg = yield* Queue.take(messages) + // yield* Match.value(msg).pipe( + // Match.tag("Reset", () => + // Effect.gen(function* () { + // yield* State.set(state, 0) + // yield* PubSub.publish(events.countChanged, 0) + // }) + // ), + // Match.tag("SendSystemMessage", ({ payload, complete }) => + // Effect.gen(function* () { + // yield* complete(payload.text) + // }) + // ), + // Match.exhaustive, + // ) + // }).pipe(Effect.forever, Effect.forkScoped) + + // --- Action handlers (request-response) --- return ChatRoom.of({ Initialize: ({ payload }) => - // This replaces createState(input). Callers should initialize + // This replaces `createState(input)`. Callers should initialize // a room before actions that depend on a persisted room name. State.update(state, (current) => { if (current.initialized) return current; return { ...current, name: payload.name, - members: [], initialized: true, }; }), @@ -98,23 +152,30 @@ export const ChatRoomLive = ChatRoom.toLayer( }, }); - if (next.name !== "") { - // Directory registration is still actor-to-actor RPC, but - // it uses the Effect action name and object payload. - yield* directory.RegisterRoom({ name: next.name }); - } + // The raw scheduler dispatches the Effect action by name + // with the same object payload that a client would send. + rawRivetkitContext.schedule.after( + 1_000, + "SendMessage", + { + sender: "Admin", + text: `Welcome to the room, ${payload.name}!`, + }, + ); - return member; + return { memberCount: next.members.length }; }), Leave: ({ payload }) => Effect.gen(function* () { yield* ensureMember(payload.name); + yield* State.update(state, (current) => ({ ...current, members: current.members.filter( (member) => member.name !== payload.name, ), })).pipe(Effect.orDie); + rawRivetkitContext.broadcast("memberLeft", { name: payload.name, }); @@ -122,13 +183,20 @@ export const ChatRoomLive = ChatRoom.toLayer( SendMessage: ({ payload }) => Effect.gen(function* () { yield* ensureMember(payload.sender); - yield* moderator.Review({ - text: payload.text, - }); + + // This is a workaround. Scope helper actors to this run so stale + // singleton actors left in the local engine DB cannot trap nested RPCs. + const runKey = ["run", ...address.key]; + // Actor-to-actor RPC uses the same API as client-to-actor RPC. + const moderator = moderatorClient.getOrCreate(runKey); + + // If Review fails with BannedWordsError, that typed error + // flows through SendMessage's declared error channel. + yield* moderator.Review({ text: payload.text }); const createdAt = yield* DateTime.now; yield* Effect.tryPromise(() => - database.execute( + rawRivetkitContext.db.execute( "INSERT INTO messages (sender, text, created_at) VALUES (?, ?, ?)", payload.sender, payload.text, @@ -144,7 +212,7 @@ export const ChatRoomLive = ChatRoom.toLayer( }), GetHistory: () => Effect.tryPromise(() => - database.execute<{ + rawRivetkitContext.db.execute<{ id: number; sender: string; text: string; @@ -161,45 +229,9 @@ export const ChatRoomLive = ChatRoom.toLayer( ), Effect.orDie, ), - GetMembers: () => - State.get(state).pipe( - Effect.orDie, - Effect.map((s) => s.members), - ), - ScheduleAnnouncement: ({ payload }) => - Effect.sync(() => { - const firesAt = DateTime.addDuration( - DateTime.nowUnsafe(), - payload.delay, - ); - // The raw scheduler dispatches the Effect action by name - // with the same object payload that a client would send. - rawRivetkitContext.schedule.after( - Duration.toMillis(payload.delay), - "TriggerAnnouncement", - { - text: payload.text, - }, - ); - return { firesAt }; - }), - TriggerAnnouncement: ({ payload }) => - Effect.sync(() => { - rawRivetkitContext.broadcast("announcement", { - text: payload.text, - }); - }), Archive: () => - Effect.gen(function* () { - const name = yield* roomName; - if (name !== "") { - // This only covers destruction through Archive. A future - // Effect onDestroy hook would cover every destroy path. - yield* directory.CloseRoom({ name }); - } - yield* Effect.sync(() => { - rawRivetkitContext.destroy(); - }); + Effect.sync(() => { + rawRivetkitContext.destroy(); }), }); }), @@ -213,13 +245,11 @@ export const ChatRoomLive = ChatRoom.toLayer( joinedAt: Schema.DateTimeUtc, }), ), - wakeCount: Schema.Number, initialized: Schema.Boolean, }), initialValue: () => ({ name: "", - members: [], - wakeCount: 0, + members: [{ name: "Admin", joinedAt: DateTime.nowUnsafe() }], initialized: false, }), }, @@ -235,7 +265,7 @@ export const ChatRoomLive = ChatRoom.toLayer( `); }, }), - name: "Chat Room", - icon: "comments", + name: "Chat Room", // Human-friendly display name + icon: "comments", // FontAwesome icon name }, ); diff --git a/examples/effect/src/actors/counter/api.ts b/examples/effect/src/actors/counter/api.ts deleted file mode 100644 index a8fa0ac65b..0000000000 --- a/examples/effect/src/actors/counter/api.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Action, Actor } from "@rivetkit/effect"; -import { Schema } from "effect"; - -// --- Errors --- - -export class CounterOverflowError extends Schema.TaggedErrorClass()( - "CounterOverflowError", - { - limit: Schema.Number, - message: Schema.String, - }, -) {} - -// --- Actions --- - -// Actions use explicit schemas rather than inferring types from -// the handler signature (like the current Rivet SDK does) because: -// -// - Runtime validation. Client-to-server is an untrusted boundary. -// Schemas validate wire data before it reaches handler code. -// Handler inference erases types at runtime and trusts whatever -// arrives. -// -// - Wire encoding control. Effect Schema distinguishes encoded -// (wire) and decoded (runtime) types, e.g. Schema.Date decodes -// a string into a Date. Handler inference only gives the decoded -// type. -// -// Actions are standalone values (vs. embedded in the actor -// definition) because: -// -// - Shared action protocols. A Ping health-check or GetMetrics -// action defined once and composed into multiple actors. -export const Increment = Action.make("Increment", { - payload: { amount: Schema.Number }, - success: Schema.Number, - error: CounterOverflowError, -}); - -export const GetCount = Action.make("GetCount", { - success: Schema.Number, -}); - -// --- Messages (not yet implemented) --- -// -// // Non-completable (fire-and-forget) -// export const Reset = Message.make("Reset", { -// payload: { reason: Schema.String }, -// }) -// -// // Completable (sender can await a typed response) -// export const IncrementBy = Message.make("IncrementBy", { -// payload: { amount: Schema.Number }, -// success: Schema.Number, -// }) - -// --- Actor Definition --- - -// The definition is the actor's public contract. It carries no -// implementation and no persisted-state schema (state is server-only, -// configured via `ActorState.make` + `toLayer(wake, { state })` in `live.ts`). -// Both server and client code import this; the implementation stays -// server-only. -export const Counter = Actor.make("Counter", { - actions: [Increment, GetCount], - // messages: [Reset, IncrementBy], // durable, queued, background - // events: { countChanged: Schema.Number }, -}); diff --git a/examples/effect/src/actors/counter/live.ts b/examples/effect/src/actors/counter/live.ts deleted file mode 100644 index 993df3850a..0000000000 --- a/examples/effect/src/actors/counter/live.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Actor, State } from "@rivetkit/effect"; -import { Effect, Schema } from "effect"; -import { Counter, CounterOverflowError } from "./api.ts"; - -// --- Actor Implementation --- - -// Counter.toLayer produces a Layer that registers this actor -// with whatever registry is in context. The Effect inside runs -// once per actor instance (not once per action call), so -// yielded refs are instance-scoped and survive across action -// calls within a wake. Finalizers run on sleep. -export const CounterLive = Counter.toLayer( - // Wake scope (runs each wake, finalizers run on sleep) - (wakeOptions) => - Effect.gen(function* () { - // Actor-provided services are yielded from the Effect context. - // They are scoped to this actor instance, not to individual - // action calls. This means all action handlers below close - // over the same state, events, kv, and db references. - // - // Because services come through the context (not a context - // parameter like the current SDK's `c`), they are: - // - // - Visible in the type signature. The Effect's R channel - // declares exactly which services are required. - // - // - Swappable via layers. Tests can provide an in-memory KV - // or a mock DB without changing the actor code. - - // `wakeOptions.state` is a `State` view over the persisted store. - // `State.changes` exposes every state change commit as a stream. - const state = wakeOptions.state; - // ^ State.State<{ count: number }> - // const events = yield* Counter.Events - // // ^ { countChanged: PubSub } - // const messages = yield* Counter.Messages - // // ^ MessageQueue - // const kv = yield* Actor.Kv - // const db = yield* Actor.Db - const address = yield* Actor.CurrentAddress; - yield* Effect.log( - `waking ${address.name}/${address.key.join(",")} actorId=${address.actorId}`, - ); - - yield* Effect.addFinalizer(() => - State.get(state).pipe( - Effect.orDie, - Effect.flatMap(({ count }) => - Effect.log( - `sleeping ${address.name}/${address.key.join(",")} count=${count}`, - ), - ), - ), - ); - - // --- Message processing (not yet implemented) --- - // Pull-based: the actor controls when to take the next message. - // Forked into a scoped fiber, so it runs in the background and - // is canceled on sleep. Re-enable once Counter.Messages lands. - // - // yield* Effect.gen(function* () { - // const msg = yield* Queue.take(messages) - // yield* Match.value(msg).pipe( - // Match.tag("Reset", () => - // Effect.gen(function* () { - // yield* State.set(state, 0) - // yield* PubSub.publish(events.countChanged, 0) - // }) - // ), - // Match.tag("IncrementBy", ({ payload, complete }) => - // Effect.gen(function* () { - // const next = yield* State.updateAndGet( - // state, - // (s) => ({ count: s.count + payload.amount }), - // ) - // yield* PubSub.publish(events.countChanged, next.count) - // yield* complete(next.count) - // }) - // ), - // Match.exhaustive, - // ) - // }).pipe(Effect.forever, Effect.forkScoped) - - // --- Action handlers (request-response) --- - return Counter.of({ - Increment: ({ payload }) => - Effect.gen(function* () { - const { count: next } = yield* State.updateAndGet( - state, - (s) => ({ count: s.count + payload.amount }), - ); - if (next > 20) { - return yield* new CounterOverflowError({ - limit: 20, - message: `count ${next} would exceed limit 20`, - }); - } - // yield* PubSub.publish(events.countChanged, next) - return next; - }), - - GetCount: () => - State.get(state).pipe(Effect.map((s) => s.count)), - }); - }), - { - state: { - schema: Schema.Struct({ - count: Schema.Number, - }), - initialValue: () => ({ count: 0 }), - }, - name: "Counter", // Human-friendly display name - icon: "comments", // FontAwesome icon name - }, -); diff --git a/examples/effect/src/actors/directory/api.ts b/examples/effect/src/actors/directory/api.ts deleted file mode 100644 index 1dd856fc22..0000000000 --- a/examples/effect/src/actors/directory/api.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Action, Actor } from "@rivetkit/effect"; -import { Schema } from "effect"; - -export const RoomEntry = Schema.Struct({ - name: Schema.String, - openedAt: Schema.DateTimeUtc, - closedAt: Schema.optionalKey(Schema.DateTimeUtc), -}); - -export const RegisterRoom = Action.make("RegisterRoom", { - payload: { name: Schema.String }, -}); - -export const CloseRoom = Action.make("CloseRoom", { - payload: { name: Schema.String }, -}); - -export const ListRooms = Action.make("ListRooms", { - success: Schema.Array(RoomEntry), -}); - -export const Directory = Actor.make("directory", { - actions: [RegisterRoom, CloseRoom, ListRooms], -}); diff --git a/examples/effect/src/actors/directory/live.ts b/examples/effect/src/actors/directory/live.ts deleted file mode 100644 index f1d4cbbe4e..0000000000 --- a/examples/effect/src/actors/directory/live.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { State } from "@rivetkit/effect"; -import { DateTime, Effect, Schema } from "effect"; -import { Directory } from "./api.ts"; - -export const DirectoryLive = Directory.toLayer( - ({ state }) => - Effect.gen(function* () { - return Directory.of({ - RegisterRoom: ({ payload }) => - Effect.gen(function* () { - const openedAt = yield* DateTime.now; - - yield* State.update(state, (current) => { - if ( - current.rooms.some( - (room) => room.name === payload.name, - ) - ) { - return current; - } - - return { - rooms: [ - ...current.rooms, - { name: payload.name, openedAt }, - ], - }; - }).pipe(Effect.orDie); - }), - CloseRoom: ({ payload }) => - Effect.gen(function* () { - const closedAt = yield* DateTime.now; - - yield* State.update(state, (current) => ({ - rooms: current.rooms.map((room) => - room.name === payload.name - ? { ...room, closedAt } - : room, - ), - })).pipe(Effect.orDie); - }), - ListRooms: () => - State.get(state).pipe( - Effect.orDie, - Effect.map((s) => s.rooms), - ), - }); - }), - { - state: { - schema: Schema.Struct({ - rooms: Schema.Array( - Schema.Struct({ - name: Schema.String, - openedAt: Schema.DateTimeUtc, - closedAt: Schema.optionalKey(Schema.DateTimeUtc), - }), - ), - }), - initialValue: () => ({ rooms: [] }), - }, - name: "Directory", - icon: "folder", - }, -); diff --git a/examples/effect/src/actors/mod.ts b/examples/effect/src/actors/mod.ts index 21ccfe82eb..2d76185742 100644 --- a/examples/effect/src/actors/mod.ts +++ b/examples/effect/src/actors/mod.ts @@ -1,4 +1,2 @@ -export * from "./counter/api.ts" -export * from "./directory/api.ts" -export * from "./moderator/api.ts" -export * from "./chat-room/api.ts" +export * from "./chat-room/api.ts"; +export * from "./moderator/api.ts"; diff --git a/examples/effect/src/client-raw.ts b/examples/effect/src/client-raw.ts index acf24ca09e..ee1dc93495 100644 --- a/examples/effect/src/client-raw.ts +++ b/examples/effect/src/client-raw.ts @@ -1,34 +1,88 @@ import { createClient } from "rivetkit/client"; +import { RivetError } from "rivetkit/errors"; -const client = createClient("http://127.0.0.1:6420") as any; +const client = createClient( + process.env.RIVET_ENDPOINT ?? "http://127.0.0.1:6420", +) as any; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function main() { const runId = crypto.randomUUID(); - const counter = client.Counter.getOrCreate(`counter-raw-${runId}`); + const roomName = `raw-room-${runId}`; + const room = client.chatRoom.getOrCreate([roomName]); + + try { + await room.Initialize({ name: roomName }); + console.log(`created room ${roomName}`); - const initial = await counter.GetCount(); - console.log("GetCount (initial):", initial); + const { memberCount } = await room.Join({ name: "Alice" }); + console.log(`Alice joined; members=${memberCount}`); - const afterFive = await counter.Increment({ amount: 5 }); - console.log("Increment(5):", afterFive); + await room.SendMessage({ + sender: "Alice", + text: "hello from the raw client", + }); + console.log("Alice sent a message"); - const afterEight = await counter.Increment({ amount: 3 }); - console.log("Increment(3):", afterEight); + // Plain clients see declared Effect action errors as thrown RivetErrors + // with the encoded Effect action-error metadata attached. + try { + await room.SendMessage({ + sender: "Mallory", + text: "I should not be able to post", + }); + } catch (error) { + if (!(error instanceof RivetError)) throw error; - const total = await counter.GetCount(); - console.log("GetCount (total):", total); + if (error.code === "MemberNotInRoomError") { + const metadata = error.metadata as { + readonly error?: { readonly name?: string }; + }; + const memberName = + typeof metadata.error?.name === "string" + ? metadata.error.name + : "unknown member"; + console.warn( + `rejected non-member message from ${memberName}: ${error.message}`, + ); + } else { + throw error; + } + } - // Trigger overflow (limit: 20). Plain client surfaces this as a - // thrown rivetkit RivetError with Effect action-error metadata. - try { - const overflowed = await counter.Increment({ amount: 20 }); - console.log("Increment(20) [unexpected success]:", overflowed); - } catch (err) { - console.log("Increment(20) [expected error]:", err); + try { + await room.SendMessage({ + sender: "Alice", + text: "this contains spam", + }); + } catch (error) { + if (!(error instanceof RivetError)) throw error; + + if (error.code === "BannedWordsError") { + console.warn(`rejected banned message: ${error.message}`); + } else { + throw error; + } + } + + await sleep(1_500); + + const history = await room.GetHistory(); + const transcript = history + .map( + (message: { sender: string; text: string }) => + ` ${message.sender}: ${message.text}`, + ) + .join("\n"); + console.log(`message history:\n${transcript}`); + } finally { + await room.Archive(); + console.log("archived room"); } } main().catch((err) => { - console.error("client smoke test failed:", err); - process.exit(1); + console.error("raw client failed:", err); + process.exitCode = 1; }); diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 5fd4dd0b84..4a85e0f386 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -3,107 +3,79 @@ import { Effect, Logger, Random } from "effect"; import { type BannedWordsError, ChatRoom, - Counter, - Directory, type MemberNotInRoomError, - Moderator, } from "./actors/mod.ts"; const program = Effect.gen(function* () { const runId = yield* Random.nextUUIDv4; - const counterClient = yield* Counter.client; - const counter = counterClient.getOrCreate([`counter-effect-${runId}`]); - - const count = yield* counter.Increment({ amount: 5 }); - yield* Effect.log(`Increment(5) -> ${count}`); - - const total = yield* counter.GetCount(); - yield* Effect.log(`GetCount -> ${total}`); + // `Actor.client` yields a typed accessor backed by the Effect SDK client layer. const chatRoomClient = yield* ChatRoom.client; - const directoryClient = yield* Directory.client; - const moderatorClient = yield* Moderator.client; - - // This is a workaround. Scope helper actors to this run so stale - // singleton actors left in the local engine DB cannot trap nested RPCs. const roomName = `effect-room-${runId}`; - const runKey = ["run", roomName]; const room = chatRoomClient.getOrCreate([roomName]); - const directory = directoryClient.getOrCreate(runKey); - const moderator = moderatorClient.getOrCreate(runKey); - - yield* room.Initialize({ name: roomName }); - yield* Effect.log(`ChatRoom.Initialize`); - const member = yield* room.Join({ name: "Alice" }); - yield* Effect.log(`ChatRoom.Join -> ${member.name}`); + yield* Effect.gen(function* () { + yield* room.Initialize({ name: roomName }); + yield* Effect.log(`created room ${roomName}`); - yield* room.SendMessage({ - sender: "Alice", - text: "hello from Effect", - }); - yield* Effect.log(`ChatRoom.SendMessage`); + const { memberCount } = yield* room.Join({ name: "Alice" }); + yield* Effect.log(`Alice joined; members=${memberCount}`); - yield* room - .SendMessage({ - sender: "Mallory", - text: "I should not be able to post", - }) - .pipe( - Effect.catchTag("MemberNotInRoomError", (e: MemberNotInRoomError) => - Effect.logError(`ChatRoom.SendMessage rejected: ${e.message}`), - ), - ); - - yield* room - .SendMessage({ + yield* room.SendMessage({ sender: "Alice", - text: "this contains spam", - }) - .pipe( - Effect.catchTag("BannedWordsError", (e: BannedWordsError) => - Effect.logError(`ChatRoom.SendMessage rejected: ${e.message}`), - ), - ); - - const history = yield* room.GetHistory(); - yield* Effect.log(`ChatRoom.GetHistory -> ${history.length} messages`); - - const members = yield* room.GetMembers(); - yield* Effect.log(`ChatRoom.GetMembers -> ${members.length} members`); - - const rooms = yield* directory.ListRooms(); - yield* Effect.log(`Directory.ListRooms -> ${rooms.length} rooms`); - - const stats = yield* moderator.Stats(); - yield* Effect.log(`Moderator.Stats -> reviewed=${stats.reviewed}`); - - // const newCount = yield* counter.send(IncrementBy({ amount: 3 })) - // yield* Effect.log(`IncrementBy(3) -> ${newCount}`) - // - // // subscribe returns a Stream typed from the event schema. - // yield* counter.subscribe("countChanged").pipe( - // Stream.take(3), - // Stream.runForEach((n) => Effect.log(`countChanged: ${n}`)), - // ) - - // Trigger overflow (limit: 20). The typed CounterOverflowError - // round-trips through a UserError on the wire and decodes back - // into the original error class — caught by the outer - // `catchTag("CounterOverflowError", ...)`. - const overflowed = yield* counter.Increment({ amount: 100 }); - yield* Effect.log(`Increment(100) [unexpected success]: ${overflowed}`); -}).pipe( - Effect.catchTag("CounterOverflowError", (e) => - Effect.logError( - `CounterOverflowError caught: limit=${e.limit} message="${e.message}"`, + text: "hello from Effect", + }); + yield* Effect.log("Alice sent a message"); + + // Domain errors declared on the action schema are caught by tag. + yield* room + .SendMessage({ + sender: "Mallory", + text: "I should not be able to post", + }) + .pipe( + Effect.catchTag( + "MemberNotInRoomError", + (e: MemberNotInRoomError) => + Effect.logWarning( + `rejected non-member message: ${e.message}`, + ), + ), + ); + + // Errors from nested actor-to-actor RPCs can flow through the caller action. + yield* room + .SendMessage({ + sender: "Alice", + text: "this contains spam", + }) + .pipe( + Effect.catchTag("BannedWordsError", (e: BannedWordsError) => + Effect.logWarning(`rejected banned message: ${e.message}`), + ), + ); + + // A welcome message is scheduled by Join and internally dispatched through SendMessage. + yield* Effect.sleep("1500 millis"); + + const history = yield* room.GetHistory(); + const transcript = history + .map((message) => ` ${message.sender}: ${message.text}`) + .join("\n"); + yield* Effect.log(`message history:\n${transcript}`); + }).pipe( + Effect.ensuring( + room + .Archive() + .pipe( + Effect.orDie, + Effect.andThen(Effect.log("archived room")), + ), ), - ), -); - -const ClientLayer = Client.layer({ - endpoint: process.env.RIVET_ENDPOINT ?? "http://127.0.0.1:6420", + ); }); + +const ClientLayer = Client.layer({ endpoint: "http://127.0.0.1:6420" }); const LoggerLayer = Logger.layer([Logger.consolePretty()]); Effect.runPromise( diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index e6b150ad03..d5da6bc8aa 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -1,18 +1,14 @@ import { Layer } from "effect"; import { NodeRuntime } from "@effect/platform-node"; import { Registry, Client } from "@rivetkit/effect"; -import { CounterLive } from "./actors/counter/live.ts"; -import { ChatRoomLive } from "./actors/chat-room/live.ts"; -import { DirectoryLive } from "./actors/directory/live.ts"; +import { ChatRoomLive, RoomPolicyLive } from "./actors/chat-room/live.ts"; import { ModeratorLive } from "./actors/moderator/live.ts"; const endpoint = process.env.RIVET_ENDPOINT ?? "http://127.0.0.1:6420"; const ActorsLayer = Layer.mergeAll( - CounterLive, - DirectoryLive, ModeratorLive, - ChatRoomLive, + ChatRoomLive.pipe(Layer.provide(RoomPolicyLive)), ).pipe(Layer.provide(Client.layer({ endpoint }))); // Engine config defaults to spawning a local rivet-engine process and From 93acdebb2cca49873ed8db6050ec1a4fd409c3ca Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 22 May 2026 15:24:52 +0200 Subject: [PATCH 265/306] Simplify effect chat room example --- examples/effect/src/actors/chat-room/live.ts | 5 +- examples/effect/src/actors/moderator/api.ts | 8 +- examples/effect/src/actors/moderator/live.ts | 5 - examples/effect/src/client-raw.ts | 3 +- examples/effect/src/client.ts | 106 +++++++++---------- 5 files changed, 51 insertions(+), 76 deletions(-) diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts index 36f1eeadaa..0079543ca6 100644 --- a/examples/effect/src/actors/chat-room/live.ts +++ b/examples/effect/src/actors/chat-room/live.ts @@ -184,11 +184,8 @@ export const ChatRoomLive = ChatRoom.toLayer( Effect.gen(function* () { yield* ensureMember(payload.sender); - // This is a workaround. Scope helper actors to this run so stale - // singleton actors left in the local engine DB cannot trap nested RPCs. - const runKey = ["run", ...address.key]; // Actor-to-actor RPC uses the same API as client-to-actor RPC. - const moderator = moderatorClient.getOrCreate(runKey); + const moderator = moderatorClient.getOrCreate("main"); // If Review fails with BannedWordsError, that typed error // flows through SendMessage's declared error channel. diff --git a/examples/effect/src/actors/moderator/api.ts b/examples/effect/src/actors/moderator/api.ts index b82b70d431..3268f88fcb 100644 --- a/examples/effect/src/actors/moderator/api.ts +++ b/examples/effect/src/actors/moderator/api.ts @@ -13,12 +13,6 @@ export const Review = Action.make("Review", { error: BannedWordsError, }); -export const Stats = Action.make("Stats", { - success: Schema.Struct({ - reviewed: Schema.Number, - }), -}); - export const Moderator = Actor.make("moderator", { - actions: [Review, Stats], + actions: [Review], }); diff --git a/examples/effect/src/actors/moderator/live.ts b/examples/effect/src/actors/moderator/live.ts index b5e13f94a9..b80d3dbcf7 100644 --- a/examples/effect/src/actors/moderator/live.ts +++ b/examples/effect/src/actors/moderator/live.ts @@ -25,11 +25,6 @@ export const ModeratorLive = Moderator.toLayer( }); } }), - Stats: () => - State.get(state).pipe( - Effect.orDie, - Effect.map(({ reviewed }) => ({ reviewed })), - ), }); }), { diff --git a/examples/effect/src/client-raw.ts b/examples/effect/src/client-raw.ts index ee1dc93495..5d757761d9 100644 --- a/examples/effect/src/client-raw.ts +++ b/examples/effect/src/client-raw.ts @@ -8,8 +8,7 @@ const client = createClient( const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function main() { - const runId = crypto.randomUUID(); - const roomName = `raw-room-${runId}`; + const roomName = `chatroom_${crypto.randomUUID()}`; const room = client.chatRoom.getOrCreate([roomName]); try { diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 4a85e0f386..beaa73ed80 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -7,73 +7,63 @@ import { } from "./actors/mod.ts"; const program = Effect.gen(function* () { - const runId = yield* Random.nextUUIDv4; - // `Actor.client` yields a typed accessor backed by the Effect SDK client layer. const chatRoomClient = yield* ChatRoom.client; - const roomName = `effect-room-${runId}`; - const room = chatRoomClient.getOrCreate([roomName]); + const roomName = `chatroom_${yield* Random.nextUUIDv4}`; + const room = chatRoomClient.getOrCreate(roomName); - yield* Effect.gen(function* () { - yield* room.Initialize({ name: roomName }); - yield* Effect.log(`created room ${roomName}`); + yield* Effect.addFinalizer(() => + Effect.gen(function* () { + yield* room.Archive().pipe(Effect.orDie); + yield* Effect.log("archived room"); + }), + ); - const { memberCount } = yield* room.Join({ name: "Alice" }); - yield* Effect.log(`Alice joined; members=${memberCount}`); + yield* room.Initialize({ name: roomName }); + yield* Effect.log(`created room ${roomName}`); - yield* room.SendMessage({ - sender: "Alice", - text: "hello from Effect", - }); - yield* Effect.log("Alice sent a message"); + const { memberCount } = yield* room.Join({ name: "Alice" }); + yield* Effect.log(`Alice joined; members=${memberCount}`); - // Domain errors declared on the action schema are caught by tag. - yield* room - .SendMessage({ - sender: "Mallory", - text: "I should not be able to post", - }) - .pipe( - Effect.catchTag( - "MemberNotInRoomError", - (e: MemberNotInRoomError) => - Effect.logWarning( - `rejected non-member message: ${e.message}`, - ), - ), - ); + yield* room.SendMessage({ + sender: "Alice", + text: "hello from Effect", + }); + yield* Effect.log("Alice sent a message"); - // Errors from nested actor-to-actor RPCs can flow through the caller action. - yield* room - .SendMessage({ - sender: "Alice", - text: "this contains spam", - }) - .pipe( - Effect.catchTag("BannedWordsError", (e: BannedWordsError) => - Effect.logWarning(`rejected banned message: ${e.message}`), - ), - ); + // Domain errors declared on the action schema are caught by tag. + yield* room + .SendMessage({ + sender: "Mallory", + text: "I should not be able to post", + }) + .pipe( + Effect.catchTag("MemberNotInRoomError", (e: MemberNotInRoomError) => + Effect.logWarning(`rejected non-member message: ${e.message}`), + ), + ); - // A welcome message is scheduled by Join and internally dispatched through SendMessage. - yield* Effect.sleep("1500 millis"); + // Errors from nested actor-to-actor RPCs can flow through the caller action. + yield* room + .SendMessage({ + sender: "Alice", + text: "this contains spam", + }) + .pipe( + Effect.catchTag("BannedWordsError", (e: BannedWordsError) => + Effect.logWarning(`rejected banned message: ${e.message}`), + ), + ); - const history = yield* room.GetHistory(); - const transcript = history - .map((message) => ` ${message.sender}: ${message.text}`) - .join("\n"); - yield* Effect.log(`message history:\n${transcript}`); - }).pipe( - Effect.ensuring( - room - .Archive() - .pipe( - Effect.orDie, - Effect.andThen(Effect.log("archived room")), - ), - ), - ); -}); + // A welcome message is scheduled by Join and internally dispatched through SendMessage. + yield* Effect.sleep("1500 millis"); + + const history = yield* room.GetHistory(); + const transcript = history + .map((message) => ` ${message.sender}: ${message.text}`) + .join("\n"); + yield* Effect.log(`message history:\n${transcript}`); +}).pipe(Effect.scoped); const ClientLayer = Client.layer({ endpoint: "http://127.0.0.1:6420" }); const LoggerLayer = Logger.layer([Logger.consolePretty()]); From 35910ea0edec310250528a00fe78b5b0f4f5564e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 22 May 2026 15:39:26 +0200 Subject: [PATCH 266/306] Refactor effect chat room example to streamline state access, update actor definitions, and improve initialization consistency --- examples/effect/src/actors/chat-room/api.ts | 11 ++--------- examples/effect/src/actors/chat-room/live.ts | 17 +++++++++-------- examples/effect/src/actors/moderator/api.ts | 2 +- examples/effect/src/client-raw.ts | 6 +++--- examples/effect/src/client.ts | 6 ++++-- 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/examples/effect/src/actors/chat-room/api.ts b/examples/effect/src/actors/chat-room/api.ts index 5599defc2d..8fce4faf48 100644 --- a/examples/effect/src/actors/chat-room/api.ts +++ b/examples/effect/src/actors/chat-room/api.ts @@ -82,18 +82,11 @@ export const Archive = Action.make("Archive"); // The definition is the actor's public contract. It carries no // implementation or server-only configuration, so it does not leak // server-specific implementation details when importing from the client. -export const ChatRoom = Actor.make("chatRoom", { +export const ChatRoom = Actor.make("ChatRoom", { // Actions are standalone values (vs. embedded in the actor definition) // as it allows for shared action protocols (e.g., a `Ping` health check // or `GetMetrics` action defined once and composed into multiple actors). - actions: [ - Initialize, - Join, - Leave, - SendMessage, - GetHistory, - Archive, - ], + actions: [Initialize, Join, Leave, SendMessage, GetHistory, Archive], // messages: [Reset, SendSystemMessage], // durable, queued, background // events: { messageAdded: Schema.String }, }); diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts index 0079543ca6..1e77b5e4d2 100644 --- a/examples/effect/src/actors/chat-room/live.ts +++ b/examples/effect/src/actors/chat-room/live.ts @@ -50,21 +50,19 @@ export const ChatRoomLive = ChatRoom.toLayer( const roomPolicy = yield* RoomPolicy; const moderatorClient = yield* Moderator.client; - // Access the actor's persisted `state` with a `SubscriptionRef`-like API - const name = State.get(state).pipe( - Effect.orDie, - Effect.map((s) => s.name), - ); - yield* Effect.log("room awake", { actorId: address.actorId, key: address.key.join("/"), - name, }); // Finalizers run on sleep yield* Effect.addFinalizer(() => Effect.gen(function* () { + // Access the actor's persisted `state` with a `SubscriptionRef`-like API + const name = yield* State.get(state).pipe( + Effect.orDie, + Effect.map((s) => s.name), + ); yield* Effect.log("room sleeping", { actorId: address.actorId, key: address.key.join("/"), @@ -185,7 +183,10 @@ export const ChatRoomLive = ChatRoom.toLayer( yield* ensureMember(payload.sender); // Actor-to-actor RPC uses the same API as client-to-actor RPC. - const moderator = moderatorClient.getOrCreate("main"); + const moderator = moderatorClient.getOrCreate([ + ...address.key, + "main", + ]); // If Review fails with BannedWordsError, that typed error // flows through SendMessage's declared error channel. diff --git a/examples/effect/src/actors/moderator/api.ts b/examples/effect/src/actors/moderator/api.ts index 3268f88fcb..805a2bfbe0 100644 --- a/examples/effect/src/actors/moderator/api.ts +++ b/examples/effect/src/actors/moderator/api.ts @@ -13,6 +13,6 @@ export const Review = Action.make("Review", { error: BannedWordsError, }); -export const Moderator = Actor.make("moderator", { +export const Moderator = Actor.make("Moderator", { actions: [Review], }); diff --git a/examples/effect/src/client-raw.ts b/examples/effect/src/client-raw.ts index 5d757761d9..dc80c35e10 100644 --- a/examples/effect/src/client-raw.ts +++ b/examples/effect/src/client-raw.ts @@ -8,11 +8,11 @@ const client = createClient( const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function main() { - const roomName = `chatroom_${crypto.randomUUID()}`; - const room = client.chatRoom.getOrCreate([roomName]); + const room = client.chatRoom.getOrCreate(`chatroom_${crypto.randomUUID()}`); try { - await room.Initialize({ name: roomName }); + const roomName = "Effect Lovers"; + await room.Initialize({ name: "Effect Lovers" }); console.log(`created room ${roomName}`); const { memberCount } = await room.Join({ name: "Alice" }); diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index beaa73ed80..0f4840d07d 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -9,8 +9,9 @@ import { const program = Effect.gen(function* () { // `Actor.client` yields a typed accessor backed by the Effect SDK client layer. const chatRoomClient = yield* ChatRoom.client; - const roomName = `chatroom_${yield* Random.nextUUIDv4}`; - const room = chatRoomClient.getOrCreate(roomName); + const room = chatRoomClient.getOrCreate( + `chatroom_${yield* Random.nextUUIDv4}`, + ); yield* Effect.addFinalizer(() => Effect.gen(function* () { @@ -19,6 +20,7 @@ const program = Effect.gen(function* () { }), ); + const roomName = "Effect Lovers"; yield* room.Initialize({ name: roomName }); yield* Effect.log(`created room ${roomName}`); From 37c579d41f1f5784df2ca8c1d9e3208fcba0b617 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 22 May 2026 16:18:27 +0200 Subject: [PATCH 267/306] chore(rust): revert hook formatting changes --- engine/packages/error/src/error.rs | 3 +- .../envoy-client/src/connection/native.rs | 4 +- .../packages/client-protocol/src/versioned.rs | 94 +++---- .../inspector-protocol/src/versioned.rs | 234 +++++++++++------- .../rivetkit-core/src/actor/context.rs | 2 +- .../rivetkit-core/src/actor/metrics.rs | 94 ++----- .../packages/rivetkit-core/src/actor/sleep.rs | 32 +-- .../rivetkit-core/src/actor/sqlite.rs | 5 +- .../packages/rivetkit-core/src/actor/task.rs | 3 +- .../src/registry/actor_connect.rs | 9 +- .../rivetkit-core/src/registry/http.rs | 3 +- .../rivetkit-core/src/registry/inspector.rs | 6 +- .../rivetkit-core/src/registry/mod.rs | 7 +- .../packages/rivetkit-core/tests/metrics.rs | 7 +- .../rivetkit-core/tests/registry_http.rs | 3 +- .../packages/rivetkit-core/tests/state.rs | 5 +- .../rivetkit-napi/src/napi_actor_events.rs | 2 +- .../packages/rivetkit-wasm/src/lib.rs | 17 +- 18 files changed, 258 insertions(+), 272 deletions(-) diff --git a/engine/packages/error/src/error.rs b/engine/packages/error/src/error.rs index 939e05a307..9e7390be6c 100644 --- a/engine/packages/error/src/error.rs +++ b/engine/packages/error/src/error.rs @@ -181,7 +181,8 @@ impl Serialize for RivetError { { use serde::ser::SerializeStruct; - let field_count = 3 + usize::from(self.meta.is_some()) + usize::from(self.actor.is_some()); + let field_count = + 3 + usize::from(self.meta.is_some()) + usize::from(self.actor.is_some()); let mut state = serializer.serialize_struct("RivetError", field_count)?; state.serialize_field("group", self.group())?; diff --git a/engine/sdks/rust/envoy-client/src/connection/native.rs b/engine/sdks/rust/envoy-client/src/connection/native.rs index aa02fead4f..59d76e587d 100644 --- a/engine/sdks/rust/envoy-client/src/connection/native.rs +++ b/engine/sdks/rust/envoy-client/src/connection/native.rs @@ -133,7 +133,9 @@ async fn single_connection( while let Some(msg) = ws_rx.recv().await { match msg { WsTxMessage::Send(data) => { - let result = write.send(tungstenite::Message::Binary(data.into())).await; + let result = write + .send(tungstenite::Message::Binary(data.into())) + .await; if let Err(e) = result { tracing::error!(?e, "failed to send ws message"); break; diff --git a/rivetkit-rust/packages/client-protocol/src/versioned.rs b/rivetkit-rust/packages/client-protocol/src/versioned.rs index e70e995664..713ebb8e6c 100644 --- a/rivetkit-rust/packages/client-protocol/src/versioned.rs +++ b/rivetkit-rust/packages/client-protocol/src/versioned.rs @@ -268,14 +268,10 @@ macro_rules! impl_to_server_pair { macro_rules! impl_common_pair { ($left:ident, $right:ident) => { impl_same_fields_pair!($left, $right, ActionRequest { id, name, args }); - impl_same_fields_pair!( - $left, - $right, - SubscriptionRequest { - event_name, - subscribe, - } - ); + impl_same_fields_pair!($left, $right, SubscriptionRequest { + event_name, + subscribe, + }); impl_to_server_pair!($left, $right); impl_same_fields_pair!($left, $right, HttpActionRequest { args }); impl_same_fields_pair!($left, $right, HttpActionResponse { output }); @@ -285,25 +281,17 @@ macro_rules! impl_common_pair { macro_rules! impl_to_client_v2_v3_pair { () => { - impl_same_fields_pair!( - v2, - v3, - Init { - actor_id, - connection_id, - } - ); - impl_same_fields_pair!( - v2, - v3, - Error { - group, - code, - message, - metadata, - action_id, - } - ); + impl_same_fields_pair!(v2, v3, Init { + actor_id, + connection_id, + }); + impl_same_fields_pair!(v2, v3, Error { + group, + code, + message, + metadata, + action_id, + }); impl_same_fields_pair!(v2, v3, ActionResponse { id, output }); impl_same_fields_pair!(v2, v3, Event { name, args }); @@ -355,36 +343,24 @@ impl_common_pair!(v1, v2); impl_common_pair!(v2, v3); impl_common_pair!(v3, v4); impl_to_client_v2_v3_pair!(); -impl_same_fields_pair!( - v1, - v2, - HttpResponseError { - group, - code, - message, - metadata, - } -); -impl_same_fields_pair!( - v2, - v3, - HttpResponseError { - group, - code, - message, - metadata, - } -); -impl_same_fields_pair!( - v3, - v4, - HttpQueueSendRequest { - body, - name, - wait, - timeout, - } -); +impl_same_fields_pair!(v1, v2, HttpResponseError { + group, + code, + message, + metadata, +}); +impl_same_fields_pair!(v2, v3, HttpResponseError { + group, + code, + message, + metadata, +}); +impl_same_fields_pair!(v3, v4, HttpQueueSendRequest { + body, + name, + wait, + timeout, +}); impl_same_fields_pair!(v3, v4, HttpQueueSendResponse { status, response }); macro_rules! impl_versioned_manual { @@ -633,9 +609,7 @@ impl OwnedVersionedData for HttpResponseError { (Self::V2(data), 2) => serde_bare::to_vec(&data).map_err(Into::into), (Self::V3(data), 3) => serde_bare::to_vec(&data).map_err(Into::into), (Self::V4(data), 4) => serde_bare::to_vec(&data).map_err(Into::into), - (_, version) => { - bail!("unexpected client protocol version for HttpResponseError: {version}") - } + (_, version) => bail!("unexpected client protocol version for HttpResponseError: {version}"), } } diff --git a/rivetkit-rust/packages/inspector-protocol/src/versioned.rs b/rivetkit-rust/packages/inspector-protocol/src/versioned.rs index 35c757f235..c58f4db48f 100644 --- a/rivetkit-rust/packages/inspector-protocol/src/versioned.rs +++ b/rivetkit-rust/packages/inspector-protocol/src/versioned.rs @@ -69,12 +69,18 @@ impl ToServer { v1::ToServerBody::PatchStateRequest(req) => { v2::ToServerBody::PatchStateRequest(req.into()) } - v1::ToServerBody::StateRequest(req) => v2::ToServerBody::StateRequest(req.into()), + v1::ToServerBody::StateRequest(req) => { + v2::ToServerBody::StateRequest(req.into()) + } v1::ToServerBody::ConnectionsRequest(req) => { v2::ToServerBody::ConnectionsRequest(req.into()) } - v1::ToServerBody::ActionRequest(req) => v2::ToServerBody::ActionRequest(req.into()), - v1::ToServerBody::RpcsListRequest(req) => v2::ToServerBody::RpcsListRequest(req.into()), + v1::ToServerBody::ActionRequest(req) => { + v2::ToServerBody::ActionRequest(req.into()) + } + v1::ToServerBody::RpcsListRequest(req) => { + v2::ToServerBody::RpcsListRequest(req.into()) + } v1::ToServerBody::EventsRequest(_) | v1::ToServerBody::ClearEventsRequest(_) => { bail!("cannot convert inspector v1 events requests to v2") } @@ -99,16 +105,24 @@ impl ToServer { v3::ToServerBody::PatchStateRequest(req) => { v4::ToServerBody::PatchStateRequest(req.into()) } - v3::ToServerBody::StateRequest(req) => v4::ToServerBody::StateRequest(req.into()), + v3::ToServerBody::StateRequest(req) => { + v4::ToServerBody::StateRequest(req.into()) + } v3::ToServerBody::ConnectionsRequest(req) => { v4::ToServerBody::ConnectionsRequest(req.into()) } - v3::ToServerBody::ActionRequest(req) => v4::ToServerBody::ActionRequest(req.into()), - v3::ToServerBody::RpcsListRequest(req) => v4::ToServerBody::RpcsListRequest(req.into()), + v3::ToServerBody::ActionRequest(req) => { + v4::ToServerBody::ActionRequest(req.into()) + } + v3::ToServerBody::RpcsListRequest(req) => { + v4::ToServerBody::RpcsListRequest(req.into()) + } v3::ToServerBody::TraceQueryRequest(req) => { v4::ToServerBody::TraceQueryRequest(req.into()) } - v3::ToServerBody::QueueRequest(req) => v4::ToServerBody::QueueRequest(req.into()), + v3::ToServerBody::QueueRequest(req) => { + v4::ToServerBody::QueueRequest(req.into()) + } v3::ToServerBody::WorkflowHistoryRequest(req) => { v4::ToServerBody::WorkflowHistoryRequest(req.into()) } @@ -132,16 +146,24 @@ impl ToServer { v4::ToServerBody::PatchStateRequest(req) => { v3::ToServerBody::PatchStateRequest(req.into()) } - v4::ToServerBody::StateRequest(req) => v3::ToServerBody::StateRequest(req.into()), + v4::ToServerBody::StateRequest(req) => { + v3::ToServerBody::StateRequest(req.into()) + } v4::ToServerBody::ConnectionsRequest(req) => { v3::ToServerBody::ConnectionsRequest(req.into()) } - v4::ToServerBody::ActionRequest(req) => v3::ToServerBody::ActionRequest(req.into()), - v4::ToServerBody::RpcsListRequest(req) => v3::ToServerBody::RpcsListRequest(req.into()), + v4::ToServerBody::ActionRequest(req) => { + v3::ToServerBody::ActionRequest(req.into()) + } + v4::ToServerBody::RpcsListRequest(req) => { + v3::ToServerBody::RpcsListRequest(req.into()) + } v4::ToServerBody::TraceQueryRequest(req) => { v3::ToServerBody::TraceQueryRequest(req.into()) } - v4::ToServerBody::QueueRequest(req) => v3::ToServerBody::QueueRequest(req.into()), + v4::ToServerBody::QueueRequest(req) => { + v3::ToServerBody::QueueRequest(req.into()) + } v4::ToServerBody::WorkflowHistoryRequest(req) => { v3::ToServerBody::WorkflowHistoryRequest(req.into()) } @@ -168,16 +190,24 @@ impl ToServer { v3::ToServerBody::PatchStateRequest(req) => { v2::ToServerBody::PatchStateRequest(req.into()) } - v3::ToServerBody::StateRequest(req) => v2::ToServerBody::StateRequest(req.into()), + v3::ToServerBody::StateRequest(req) => { + v2::ToServerBody::StateRequest(req.into()) + } v3::ToServerBody::ConnectionsRequest(req) => { v2::ToServerBody::ConnectionsRequest(req.into()) } - v3::ToServerBody::ActionRequest(req) => v2::ToServerBody::ActionRequest(req.into()), - v3::ToServerBody::RpcsListRequest(req) => v2::ToServerBody::RpcsListRequest(req.into()), + v3::ToServerBody::ActionRequest(req) => { + v2::ToServerBody::ActionRequest(req.into()) + } + v3::ToServerBody::RpcsListRequest(req) => { + v2::ToServerBody::RpcsListRequest(req.into()) + } v3::ToServerBody::TraceQueryRequest(req) => { v2::ToServerBody::TraceQueryRequest(req.into()) } - v3::ToServerBody::QueueRequest(req) => v2::ToServerBody::QueueRequest(req.into()), + v3::ToServerBody::QueueRequest(req) => { + v2::ToServerBody::QueueRequest(req.into()) + } v3::ToServerBody::WorkflowHistoryRequest(req) => { v2::ToServerBody::WorkflowHistoryRequest(req.into()) } @@ -199,12 +229,18 @@ impl ToServer { v2::ToServerBody::PatchStateRequest(req) => { v1::ToServerBody::PatchStateRequest(req.into()) } - v2::ToServerBody::StateRequest(req) => v1::ToServerBody::StateRequest(req.into()), + v2::ToServerBody::StateRequest(req) => { + v1::ToServerBody::StateRequest(req.into()) + } v2::ToServerBody::ConnectionsRequest(req) => { v1::ToServerBody::ConnectionsRequest(req.into()) } - v2::ToServerBody::ActionRequest(req) => v1::ToServerBody::ActionRequest(req.into()), - v2::ToServerBody::RpcsListRequest(req) => v1::ToServerBody::RpcsListRequest(req.into()), + v2::ToServerBody::ActionRequest(req) => { + v1::ToServerBody::ActionRequest(req.into()) + } + v2::ToServerBody::RpcsListRequest(req) => { + v1::ToServerBody::RpcsListRequest(req.into()) + } v2::ToServerBody::TraceQueryRequest(_) | v2::ToServerBody::QueueRequest(_) | v2::ToServerBody::WorkflowHistoryRequest(_) => { @@ -273,18 +309,24 @@ impl ToClient { }; let body = match data.body { - v1::ToClientBody::StateResponse(resp) => v2::ToClientBody::StateResponse(resp.into()), + v1::ToClientBody::StateResponse(resp) => { + v2::ToClientBody::StateResponse(resp.into()) + } v1::ToClientBody::ConnectionsResponse(resp) => { v2::ToClientBody::ConnectionsResponse(resp.into()) } - v1::ToClientBody::ActionResponse(resp) => v2::ToClientBody::ActionResponse(resp.into()), + v1::ToClientBody::ActionResponse(resp) => { + v2::ToClientBody::ActionResponse(resp.into()) + } v1::ToClientBody::RpcsListResponse(resp) => { v2::ToClientBody::RpcsListResponse(resp.into()) } v1::ToClientBody::ConnectionsUpdated(update) => { v2::ToClientBody::ConnectionsUpdated(update.into()) } - v1::ToClientBody::StateUpdated(update) => v2::ToClientBody::StateUpdated(update.into()), + v1::ToClientBody::StateUpdated(update) => { + v2::ToClientBody::StateUpdated(update.into()) + } v1::ToClientBody::Error(error) => v2::ToClientBody::Error(error.into()), v1::ToClientBody::Init(init) => v2::ToClientBody::Init(v2::Init { connections: convert_vec(init.connections), @@ -317,16 +359,24 @@ impl ToClient { }; let body = match data.body { - v3::ToClientBody::StateResponse(resp) => v4::ToClientBody::StateResponse(resp.into()), + v3::ToClientBody::StateResponse(resp) => { + v4::ToClientBody::StateResponse(resp.into()) + } v3::ToClientBody::ConnectionsResponse(resp) => { v4::ToClientBody::ConnectionsResponse(resp.into()) } - v3::ToClientBody::ActionResponse(resp) => v4::ToClientBody::ActionResponse(resp.into()), + v3::ToClientBody::ActionResponse(resp) => { + v4::ToClientBody::ActionResponse(resp.into()) + } v3::ToClientBody::ConnectionsUpdated(update) => { v4::ToClientBody::ConnectionsUpdated(update.into()) } - v3::ToClientBody::QueueUpdated(update) => v4::ToClientBody::QueueUpdated(update.into()), - v3::ToClientBody::StateUpdated(update) => v4::ToClientBody::StateUpdated(update.into()), + v3::ToClientBody::QueueUpdated(update) => { + v4::ToClientBody::QueueUpdated(update.into()) + } + v3::ToClientBody::StateUpdated(update) => { + v4::ToClientBody::StateUpdated(update.into()) + } v3::ToClientBody::WorkflowHistoryUpdated(update) => { v4::ToClientBody::WorkflowHistoryUpdated(update.into()) } @@ -336,7 +386,9 @@ impl ToClient { v3::ToClientBody::TraceQueryResponse(resp) => { v4::ToClientBody::TraceQueryResponse(resp.into()) } - v3::ToClientBody::QueueResponse(resp) => v4::ToClientBody::QueueResponse(resp.into()), + v3::ToClientBody::QueueResponse(resp) => { + v4::ToClientBody::QueueResponse(resp.into()) + } v3::ToClientBody::WorkflowHistoryResponse(resp) => { v4::ToClientBody::WorkflowHistoryResponse(resp.into()) } @@ -359,16 +411,24 @@ impl ToClient { }; let body = match data.body { - v4::ToClientBody::StateResponse(resp) => v3::ToClientBody::StateResponse(resp.into()), + v4::ToClientBody::StateResponse(resp) => { + v3::ToClientBody::StateResponse(resp.into()) + } v4::ToClientBody::ConnectionsResponse(resp) => { v3::ToClientBody::ConnectionsResponse(resp.into()) } - v4::ToClientBody::ActionResponse(resp) => v3::ToClientBody::ActionResponse(resp.into()), + v4::ToClientBody::ActionResponse(resp) => { + v3::ToClientBody::ActionResponse(resp.into()) + } v4::ToClientBody::ConnectionsUpdated(update) => { v3::ToClientBody::ConnectionsUpdated(update.into()) } - v4::ToClientBody::QueueUpdated(update) => v3::ToClientBody::QueueUpdated(update.into()), - v4::ToClientBody::StateUpdated(update) => v3::ToClientBody::StateUpdated(update.into()), + v4::ToClientBody::QueueUpdated(update) => { + v3::ToClientBody::QueueUpdated(update.into()) + } + v4::ToClientBody::StateUpdated(update) => { + v3::ToClientBody::StateUpdated(update.into()) + } v4::ToClientBody::WorkflowHistoryUpdated(update) => { v3::ToClientBody::WorkflowHistoryUpdated(update.into()) } @@ -378,7 +438,9 @@ impl ToClient { v4::ToClientBody::TraceQueryResponse(resp) => { v3::ToClientBody::TraceQueryResponse(resp.into()) } - v4::ToClientBody::QueueResponse(resp) => v3::ToClientBody::QueueResponse(resp.into()), + v4::ToClientBody::QueueResponse(resp) => { + v3::ToClientBody::QueueResponse(resp.into()) + } v4::ToClientBody::WorkflowHistoryResponse(resp) => { v3::ToClientBody::WorkflowHistoryResponse(resp.into()) } @@ -404,16 +466,24 @@ impl ToClient { }; let body = match data.body { - v3::ToClientBody::StateResponse(resp) => v2::ToClientBody::StateResponse(resp.into()), + v3::ToClientBody::StateResponse(resp) => { + v2::ToClientBody::StateResponse(resp.into()) + } v3::ToClientBody::ConnectionsResponse(resp) => { v2::ToClientBody::ConnectionsResponse(resp.into()) } - v3::ToClientBody::ActionResponse(resp) => v2::ToClientBody::ActionResponse(resp.into()), + v3::ToClientBody::ActionResponse(resp) => { + v2::ToClientBody::ActionResponse(resp.into()) + } v3::ToClientBody::ConnectionsUpdated(update) => { v2::ToClientBody::ConnectionsUpdated(update.into()) } - v3::ToClientBody::QueueUpdated(update) => v2::ToClientBody::QueueUpdated(update.into()), - v3::ToClientBody::StateUpdated(update) => v2::ToClientBody::StateUpdated(update.into()), + v3::ToClientBody::QueueUpdated(update) => { + v2::ToClientBody::QueueUpdated(update.into()) + } + v3::ToClientBody::StateUpdated(update) => { + v2::ToClientBody::StateUpdated(update.into()) + } v3::ToClientBody::WorkflowHistoryUpdated(update) => { v2::ToClientBody::WorkflowHistoryUpdated(update.into()) } @@ -423,7 +493,9 @@ impl ToClient { v3::ToClientBody::TraceQueryResponse(resp) => { v2::ToClientBody::TraceQueryResponse(resp.into()) } - v3::ToClientBody::QueueResponse(resp) => v2::ToClientBody::QueueResponse(resp.into()), + v3::ToClientBody::QueueResponse(resp) => { + v2::ToClientBody::QueueResponse(resp.into()) + } v3::ToClientBody::WorkflowHistoryResponse(resp) => { v2::ToClientBody::WorkflowHistoryResponse(resp.into()) } @@ -444,15 +516,21 @@ impl ToClient { }; let body = match data.body { - v2::ToClientBody::StateResponse(resp) => v1::ToClientBody::StateResponse(resp.into()), + v2::ToClientBody::StateResponse(resp) => { + v1::ToClientBody::StateResponse(resp.into()) + } v2::ToClientBody::ConnectionsResponse(resp) => { v1::ToClientBody::ConnectionsResponse(resp.into()) } - v2::ToClientBody::ActionResponse(resp) => v1::ToClientBody::ActionResponse(resp.into()), + v2::ToClientBody::ActionResponse(resp) => { + v1::ToClientBody::ActionResponse(resp.into()) + } v2::ToClientBody::ConnectionsUpdated(update) => { v1::ToClientBody::ConnectionsUpdated(update.into()) } - v2::ToClientBody::StateUpdated(update) => v1::ToClientBody::StateUpdated(update.into()), + v2::ToClientBody::StateUpdated(update) => { + v1::ToClientBody::StateUpdated(update.into()) + } v2::ToClientBody::RpcsListResponse(resp) => { v1::ToClientBody::RpcsListResponse(resp.into()) } @@ -642,15 +720,11 @@ macro_rules! impl_common_actor_pair { impl_same_fields_pair!($left, $right, Connection { id, details }); impl_connections_response_pair!($left, $right); impl_connection_list_pair!($left, $right, ConnectionsUpdated); - impl_same_fields_pair!( - $left, - $right, - StateResponse { - rid, - state, - is_state_enabled, - } - ); + impl_same_fields_pair!($left, $right, StateResponse { + rid, + state, + is_state_enabled, + }); impl_same_fields_pair!($left, $right, ActionResponse { rid, output }); impl_same_fields_pair!($left, $right, StateUpdated { state }); impl_same_fields_pair!($left, $right, RpcsListResponse { rid, rpcs }); @@ -660,40 +734,28 @@ macro_rules! impl_common_actor_pair { macro_rules! impl_queue_workflow_pair { ($left:ident, $right:ident) => { - impl_same_fields_pair!( - $left, - $right, - TraceQueryRequest { - id, - start_ms, - end_ms, - limit, - } - ); + impl_same_fields_pair!($left, $right, TraceQueryRequest { + id, + start_ms, + end_ms, + limit, + }); impl_same_fields_pair!($left, $right, TraceQueryResponse { rid, payload }); impl_same_fields_pair!($left, $right, QueueRequest { id, limit }); - impl_same_fields_pair!( - $left, - $right, - QueueMessageSummary { - id, - name, - created_at_ms, - } - ); + impl_same_fields_pair!($left, $right, QueueMessageSummary { + id, + name, + created_at_ms, + }); impl_queue_status_pair!($left, $right); impl_queue_response_pair!($left, $right); impl_same_fields_pair!($left, $right, QueueUpdated { queue_size }); impl_same_fields_pair!($left, $right, WorkflowHistoryRequest { id }); - impl_same_fields_pair!( - $left, - $right, - WorkflowHistoryResponse { - rid, - history, - is_workflow_enabled, - } - ); + impl_same_fields_pair!($left, $right, WorkflowHistoryResponse { + rid, + history, + is_workflow_enabled, + }); impl_same_fields_pair!($left, $right, WorkflowHistoryUpdated { history }); impl_init_pair!($left, $right); }; @@ -703,16 +765,12 @@ macro_rules! impl_database_pair { ($left:ident, $right:ident) => { impl_same_fields_pair!($left, $right, DatabaseSchemaRequest { id }); impl_same_fields_pair!($left, $right, DatabaseSchemaResponse { rid, schema }); - impl_same_fields_pair!( - $left, - $right, - DatabaseTableRowsRequest { - id, - table, - limit, - offset, - } - ); + impl_same_fields_pair!($left, $right, DatabaseTableRowsRequest { + id, + table, + limit, + offset, + }); impl_same_fields_pair!($left, $right, DatabaseTableRowsResponse { rid, result }); }; } @@ -755,7 +813,9 @@ impl From for v3::ToClientBody { v2::ToClientBody::StateResponse(resp) => Self::StateResponse(resp.into()), v2::ToClientBody::ConnectionsResponse(resp) => Self::ConnectionsResponse(resp.into()), v2::ToClientBody::ActionResponse(resp) => Self::ActionResponse(resp.into()), - v2::ToClientBody::ConnectionsUpdated(update) => Self::ConnectionsUpdated(update.into()), + v2::ToClientBody::ConnectionsUpdated(update) => { + Self::ConnectionsUpdated(update.into()) + } v2::ToClientBody::QueueUpdated(update) => Self::QueueUpdated(update.into()), v2::ToClientBody::StateUpdated(update) => Self::StateUpdated(update.into()), v2::ToClientBody::WorkflowHistoryUpdated(update) => { diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/context.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/context.rs index 69a19bc821..e67c096f3a 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/context.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/context.rs @@ -10,9 +10,9 @@ use crate::time::{Instant, SystemTime, UNIX_EPOCH}; use anyhow::{Context as AnyhowContext, Result}; use futures::future::BoxFuture; use parking_lot::{Mutex, RwLock}; +use rivet_error::ActorSpecifier; use rivet_envoy_client::handle::EnvoyHandle; use rivet_envoy_client::tunnel::HibernatingWebSocketMetadata; -use rivet_error::ActorSpecifier; use scc::HashMap as SccHashMap; use tokio::runtime::Handle; use tokio::sync::{Mutex as AsyncMutex, Notify, OnceCell, broadcast, mpsc, oneshot}; diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/metrics.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/metrics.rs index 66d30d9239..6d5f6e11c3 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/metrics.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/metrics.rs @@ -29,13 +29,8 @@ const SQLITE_COMMIT_PHASE_LABELS: &[&str] = &["actor_id_gen", "actor_key", "envo const SQLITE_WORKER_COMMAND_LABELS: &[&str] = &["actor_id_gen", "actor_key", "envoy_key", "operation"]; #[cfg(feature = "sqlite-local")] -const SQLITE_WORKER_ERROR_LABELS: &[&str] = &[ - "actor_id_gen", - "actor_key", - "envoy_key", - "operation", - "code", -]; +const SQLITE_WORKER_ERROR_LABELS: &[&str] = + &["actor_id_gen", "actor_key", "envoy_key", "operation", "code"]; pub(crate) struct ActorMetrics { labels: Arc, @@ -162,10 +157,7 @@ impl ActorMetricCollectors { ) .expect("create actor_queue_depth gauge"); let queue_messages_sent_total = IntCounterVec::new( - Opts::new( - "actor_queue_messages_sent_total", - "total queue messages sent", - ), + Opts::new("actor_queue_messages_sent_total", "total queue messages sent"), ACTOR_LABELS, ) .expect("create actor_queue_messages_sent_total counter"); @@ -460,18 +452,12 @@ impl ActorMetricCollectors { register_metric(&rivet_metrics::REGISTRY, create_vars_ms.clone()); register_metric(&rivet_metrics::REGISTRY, queue_depth.clone()); register_metric(&rivet_metrics::REGISTRY, queue_messages_sent_total.clone()); - register_metric( - &rivet_metrics::REGISTRY, - queue_messages_received_total.clone(), - ); + register_metric(&rivet_metrics::REGISTRY, queue_messages_received_total.clone()); register_metric(&rivet_metrics::REGISTRY, active_connections.clone()); register_metric(&rivet_metrics::REGISTRY, connections_total.clone()); register_metric(&rivet_metrics::REGISTRY, lifecycle_inbox_depth.clone()); register_metric(&rivet_metrics::REGISTRY, dispatch_inbox_depth.clone()); - register_metric( - &rivet_metrics::REGISTRY, - lifecycle_event_inbox_depth.clone(), - ); + register_metric(&rivet_metrics::REGISTRY, lifecycle_event_inbox_depth.clone()); register_metric(&rivet_metrics::REGISTRY, user_tasks_active.clone()); register_metric(&rivet_metrics::REGISTRY, user_task_duration_seconds.clone()); register_metric(&rivet_metrics::REGISTRY, shutdown_wait_seconds.clone()); @@ -483,10 +469,7 @@ impl ActorMetricCollectors { ); #[cfg(feature = "sqlite-local")] { - register_metric( - &rivet_metrics::REGISTRY, - sqlite_vfs_resolve_pages_total.clone(), - ); + register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_resolve_pages_total.clone()); register_metric( &rivet_metrics::REGISTRY, sqlite_vfs_resolve_pages_requested_total.clone(), @@ -500,22 +483,10 @@ impl ActorMetricCollectors { sqlite_vfs_resolve_pages_cache_misses_total.clone(), ); register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_get_pages_total.clone()); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_vfs_pages_fetched_total.clone(), - ); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_vfs_prefetch_pages_total.clone(), - ); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_vfs_bytes_fetched_total.clone(), - ); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_vfs_prefetch_bytes_total.clone(), - ); + register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_pages_fetched_total.clone()); + register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_prefetch_pages_total.clone()); + register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_bytes_fetched_total.clone()); + register_metric(&rivet_metrics::REGISTRY, sqlite_vfs_prefetch_bytes_total.clone()); register_metric( &rivet_metrics::REGISTRY, sqlite_vfs_get_pages_duration_seconds.clone(), @@ -530,31 +501,16 @@ impl ActorMetricCollectors { sqlite_vfs_commit_duration_seconds_total.clone(), ); register_metric(&rivet_metrics::REGISTRY, sqlite_worker_queue_depth.clone()); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_worker_queue_overload_total.clone(), - ); + register_metric(&rivet_metrics::REGISTRY, sqlite_worker_queue_overload_total.clone()); register_metric( &rivet_metrics::REGISTRY, sqlite_worker_command_duration_seconds.clone(), ); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_worker_command_error_total.clone(), - ); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_worker_close_duration_seconds.clone(), - ); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_worker_close_timeout_total.clone(), - ); + register_metric(&rivet_metrics::REGISTRY, sqlite_worker_command_error_total.clone()); + register_metric(&rivet_metrics::REGISTRY, sqlite_worker_close_duration_seconds.clone()); + register_metric(&rivet_metrics::REGISTRY, sqlite_worker_close_timeout_total.clone()); register_metric(&rivet_metrics::REGISTRY, sqlite_worker_crash_total.clone()); - register_metric( - &rivet_metrics::REGISTRY, - sqlite_worker_unclean_close_total.clone(), - ); + register_metric(&rivet_metrics::REGISTRY, sqlite_worker_unclean_close_total.clone()); } Self { @@ -735,7 +691,10 @@ impl ActorMetrics { }); let labels = self.actor_labels(); let labels = [labels[0], labels[1], labels[2], kind.as_metric_label()]; - METRICS.user_tasks_active.with_label_values(&labels).dec(); + METRICS + .user_tasks_active + .with_label_values(&labels) + .dec(); METRICS .user_task_duration_seconds .with_label_values(&labels) @@ -868,13 +827,7 @@ impl depot_client::vfs::SqliteVfsMetrics for ActorMetrics { total_ns: u64, ) { record_retained_actor_metrics(&self.labels, |retained| { - for phase in [ - "request_build", - "serialize", - "transport", - "state_update", - "total", - ] { + for phase in ["request_build", "serialize", "transport", "state_update", "total"] { push_unique(&mut retained.sqlite_commit_phases, phase); } }); @@ -1153,12 +1106,7 @@ fn remove_retained_actor_metrics(labels: &ActorMetricLabels, retained: &Retained ); } for operation in &retained.sqlite_worker_operations { - let labels = [ - actor_labels[0], - actor_labels[1], - actor_labels[2], - *operation, - ]; + let labels = [actor_labels[0], actor_labels[1], actor_labels[2], *operation]; ignore_missing_labels( metrics .sqlite_worker_command_duration_seconds diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/sleep.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/sleep.rs index 1901076d5b..9144fc1648 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/sleep.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/sleep.rs @@ -13,20 +13,22 @@ use tokio::task::JoinHandle; use tracing::Instrument; use crate::actor::config::ActorConfig; -use crate::actor::context::ActorContext; #[cfg(not(feature = "wasm-runtime"))] use crate::actor::context::ActorWorkRegion; +use crate::actor::context::ActorContext; use crate::actor::task_types::ShutdownKind; -#[cfg(not(feature = "wasm-runtime"))] -use crate::actor::work_registry::ActorWorkPolicy; #[cfg(feature = "wasm-runtime")] use crate::actor::work_registry::LocalShutdownTask; -use crate::actor::work_registry::{ActorWorkKind, CountGuard, RegionGuard, WorkRegistry}; +#[cfg(not(feature = "wasm-runtime"))] +use crate::actor::work_registry::ActorWorkPolicy; +use crate::actor::work_registry::{ + ActorWorkKind, CountGuard, RegionGuard, WorkRegistry, +}; #[cfg(feature = "wasm-runtime")] use crate::runtime::RuntimeSpawner; +use crate::time::{Instant, sleep}; #[cfg(test)] use crate::time::sleep_until; -use crate::time::{Instant, sleep}; #[cfg(test)] use crate::types::ActorKey; #[cfg(feature = "wasm-runtime")] @@ -502,10 +504,7 @@ impl ActorContext { F: Future + Send + 'static, { if Handle::try_current().is_err() { - tracing::warn!( - kind = kind.label(), - "actor work spawned without tokio runtime" - ); + tracing::warn!(kind = kind.label(), "actor work spawned without tokio runtime"); return false; } @@ -513,10 +512,7 @@ impl ActorContext { if policy.aborts_at_shutdown_deadline { let mut shutdown_tasks = self.0.sleep.work.shutdown_tasks.lock(); if self.0.sleep.work.teardown_started.load(Ordering::Acquire) { - tracing::warn!( - kind = kind.label(), - "actor work spawned after teardown; aborting immediately" - ); + tracing::warn!(kind = kind.label(), "actor work spawned after teardown; aborting immediately"); return false; } let region = self.begin_work_region(kind); @@ -525,10 +521,7 @@ impl ActorContext { let mut unabortable_shutdown_tasks = self.0.sleep.work.unabortable_shutdown_tasks.lock(); if self.0.sleep.work.teardown_started.load(Ordering::Acquire) { - tracing::warn!( - kind = kind.label(), - "actor work spawned after teardown; aborting immediately" - ); + tracing::warn!(kind = kind.label(), "actor work spawned after teardown; aborting immediately"); return false; } let region = self.begin_work_region(kind); @@ -581,10 +574,7 @@ impl ActorContext { { let mut local_shutdown_tasks = self.0.sleep.work.local_shutdown_tasks.lock(); if self.0.sleep.work.teardown_started.load(Ordering::Acquire) { - tracing::warn!( - kind = kind.label(), - "actor work spawned after teardown; aborting immediately" - ); + tracing::warn!(kind = kind.label(), "actor work spawned after teardown; aborting immediately"); return false; } diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs index 35b3de3bbf..a401cefe2b 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs @@ -10,9 +10,9 @@ use depot_client_types::is_head_fence_mismatch; pub use depot_client_types::{BindParam, ColumnValue, ExecResult, ExecuteResult, QueryResult}; #[cfg(feature = "sqlite-local")] use parking_lot::Mutex; +use rivet_error::{ActorSpecifier, RivetError}; use rivet_envoy_client::protocol; use rivet_envoy_client::{handle::EnvoyHandle, utils::RemoteSqliteIndeterminateResultError}; -use rivet_error::{ActorSpecifier, RivetError}; use serde::Serialize; use serde_json::{Map as JsonMap, Value as JsonValue}; #[cfg(feature = "sqlite-local")] @@ -507,7 +507,8 @@ impl SqliteDb { } fn actor_specifier(&self) -> Option { - let mut specifier = ActorSpecifier::new(self.actor_id.as_ref()?.clone(), self.generation?); + let mut specifier = + ActorSpecifier::new(self.actor_id.as_ref()?.clone(), self.generation?); if let Some(key) = self.actor_key.as_ref() { specifier = specifier.with_key(key.clone()); } diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs index 3083975fd2..2f771e5c28 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs @@ -890,8 +890,7 @@ impl ActorTask { let _action_keep_awake = action_keep_awake; match tracked_reply_rx.await { Ok(result) => { - let result = - result.map_err(|error| ctx.attach_actor_to_error(error)); + let result = result.map_err(|error| ctx.attach_actor_to_error(error)); tracing::info!( actor_id = %actor_id, action_name = %action_name_for_log, diff --git a/rivetkit-rust/packages/rivetkit-core/src/registry/actor_connect.rs b/rivetkit-rust/packages/rivetkit-core/src/registry/actor_connect.rs index 1b9c43bd31..f70cfce65d 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/registry/actor_connect.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/registry/actor_connect.rs @@ -64,14 +64,13 @@ pub(super) fn encode_actor_connect_message(message: &ActorConnectToClient) -> Re .as_ref() .map(|metadata| metadata.as_ref().to_vec()), action_id: payload.action_id.map(serde_bare::Uint), - actor: payload - .actor - .as_ref() - .map(|actor| client_protocol::ActorSpecifier { + actor: payload.actor.as_ref().map(|actor| { + client_protocol::ActorSpecifier { actor_id: actor.actor_id.clone(), generation: serde_bare::Uint(actor.generation), key: actor.key.clone(), - }), + } + }), }) } ActorConnectToClient::ActionResponse(payload) => { diff --git a/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs b/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs index f24612503d..3527621953 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs @@ -1,7 +1,7 @@ use super::dispatch::*; use super::inspector::*; use super::*; -use crate::error::{ProtocolError, client_error_message, client_error_metadata}; +use crate::error::{client_error_message, client_error_metadata, ProtocolError}; use ::http; const HEADER_RIVET_ACTOR: &str = "x-rivet-actor"; @@ -378,6 +378,7 @@ impl RegistryDispatcher { } } } + } enum RegistryHttpRoute { diff --git a/rivetkit-rust/packages/rivetkit-core/src/registry/inspector.rs b/rivetkit-rust/packages/rivetkit-core/src/registry/inspector.rs index 1c2929df00..56816c4aac 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/registry/inspector.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/registry/inspector.rs @@ -1,7 +1,7 @@ use super::dispatch::*; use super::http::*; use super::*; -use crate::error::{ProtocolError, client_error_message}; +use crate::error::{client_error_message, ProtocolError}; use ::http; #[derive(rivet_error::RivetError, serde::Serialize)] @@ -346,7 +346,9 @@ impl RegistryDispatcher { ) -> Result<(bool, Option>)> { let result = instance .ctx - .internal_keep_awake(dispatch_workflow_history_through_task(&instance.dispatch)) + .internal_keep_awake(dispatch_workflow_history_through_task( + &instance.dispatch, + )) .await .context("load inspector workflow history"); diff --git a/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs b/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs index 75f3f444d2..3c98819220 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs @@ -625,8 +625,11 @@ impl RegistryDispatcher { let (start_tx, start_rx) = oneshot::channel(); let result: Result> = async { - try_send_lifecycle_command(&lifecycle_tx, LifecycleCommand::Start { reply: start_tx }) - .context("send actor task start command")?; + try_send_lifecycle_command( + &lifecycle_tx, + LifecycleCommand::Start { reply: start_tx }, + ) + .context("send actor task start command")?; start_rx .await .context("receive actor task start reply")? diff --git a/rivetkit-rust/packages/rivetkit-core/tests/metrics.rs b/rivetkit-rust/packages/rivetkit-core/tests/metrics.rs index 73075682a9..6a636edf35 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/metrics.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/metrics.rs @@ -8,8 +8,8 @@ mod moved_tests { use rivet_metrics::prometheus::{IntGauge, Opts, Registry}; - use super::metrics_helpers::{metric_line_for_actor, render_global_metrics}; use super::*; + use super::metrics_helpers::{metric_line_for_actor, render_global_metrics}; #[test] fn duplicate_metric_registration_uses_noop_fallback() { @@ -102,9 +102,6 @@ mod moved_tests { && line.contains("envoy_key=\"envoy-1\"") }) .unwrap_or_else(|| panic!("{name} should render")); - assert!( - line.ends_with(value), - "{name} should have value {value}: {line}" - ); + assert!(line.ends_with(value), "{name} should have value {value}: {line}"); } } diff --git a/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs b/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs index 23e0c365af..23b54ac5d6 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs @@ -8,7 +8,8 @@ mod moved_tests { HttpResponseEncoding, authorization_bearer_token, authorization_bearer_token_map, framework_action_error_response, framework_anyhow_error_response_with_actor, is_actor_request_path, message_boundary_error_response, - message_boundary_error_response_with_actor, normalize_actor_request_path, request_encoding, + message_boundary_error_response_with_actor, normalize_actor_request_path, + request_encoding, workflow_dispatch_result, }; use crate::actor::action::ActionDispatchError; diff --git a/rivetkit-rust/packages/rivetkit-core/tests/state.rs b/rivetkit-rust/packages/rivetkit-core/tests/state.rs index abf648e2e6..400fe53d14 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/state.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/state.rs @@ -178,7 +178,10 @@ mod moved_tests { #[tokio::test] async fn request_save_coalesces_and_escalates_to_immediate() { - let state = ActorContext::new_for_state_tests(new_in_memory(), ActorConfig::default()); + let state = ActorContext::new_for_state_tests( + new_in_memory(), + ActorConfig::default(), + ); let (events_tx, mut events_rx) = mpsc::unbounded_channel(); state.configure_lifecycle_events(Some(events_tx)); diff --git a/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs b/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs index d9e28c25c5..4553e11d32 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs +++ b/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs @@ -1,5 +1,5 @@ -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::Duration; use anyhow::Result; diff --git a/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs b/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs index f3f1207b62..a1ea940dd6 100644 --- a/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs +++ b/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs @@ -7,15 +7,18 @@ use std::time::Duration; use anyhow::{Result, anyhow}; use js_sys::{Array, Function, Object, Promise, Reflect, Uint8Array}; -use rivet_error::{ActorSpecifier, RivetError as RivetTransportError, RivetErrorKind}; +use rivet_error::{ + ActorSpecifier, RivetError as RivetTransportError, RivetErrorKind, +}; use rivetkit_core::error::public_error_status_code; use rivetkit_core::inspector::InspectorAuth; use rivetkit_core::{ ActorConfig, ActorConfigInput, ActorEvent, ActorFactory as CoreActorFactory, ActorStart, - ActorWorkKind, BindParam, ColumnValue, CoreRegistry as NativeCoreRegistry, - CoreServerlessRuntime, EnqueueAndWaitOpts, KeepAwakeRegion, ListOpts, QueueMessage, - QueueNextBatchOpts, QueueSendResult, QueueSendStatus, QueueTryNextBatchOpts, QueueWaitOpts, - Request, RequestSaveOpts, Response, RuntimeSpawner, SerializeStateReason, ServeConfig, + ActorWorkKind, + BindParam, ColumnValue, CoreRegistry as NativeCoreRegistry, CoreServerlessRuntime, + EnqueueAndWaitOpts, KeepAwakeRegion, ListOpts, QueueMessage, QueueNextBatchOpts, + QueueSendResult, QueueSendStatus, QueueTryNextBatchOpts, QueueWaitOpts, Request, + RequestSaveOpts, Response, RuntimeSpawner, SerializeStateReason, ServeConfig, ServerlessRequest, StateDelta, WebSocket, WebSocketCallbackRegion, WsMessage, }; use tokio::sync::oneshot; @@ -2862,7 +2865,9 @@ mod tests { } fn transport_message(error: &anyhow::Error) -> String { - transport_error(error).message().to_owned() + transport_error(error) + .message() + .to_owned() } #[test] From 956312d45d673bec84847f4e35b7ba9ddd067677 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 31 May 2026 15:45:22 +0200 Subject: [PATCH 268/306] Expand Actor.toLayer type tests for wake shapes --- .../packages/effect/src/Actor.test-d.ts | 245 ++++++++++++++++-- 1 file changed, 222 insertions(+), 23 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts index 1ed9579e6d..9147e05792 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -17,8 +17,13 @@ class SomeDep extends Context.Service()( "SomeDep", ) {} +const Ping = Action.make("Ping", { + success: Schema.Number, + error: Schema.String, +}); + const TestActor = Actor.make("TestActor", { - actions: [Action.make("GetContext")], + actions: [Ping], }); const TestState = { @@ -80,15 +85,15 @@ describe("Actor.make(...).toLayer", () => { test("accepts a plain action handlers object", () => { expectTypeOf(TestActor.toLayer).toBeCallableWith({ - GetContext: () => Effect.void, + Ping: () => Effect.succeed(0), }); }); - test("accepts an Effect of action handlers", () => { + test("accepts an effect of action handlers", () => { expectTypeOf(TestActor.toLayer).toBeCallableWith( Effect.gen(function* () { return { - GetContext: () => Effect.void, + Ping: () => Effect.succeed(0), }; }), ); @@ -97,7 +102,7 @@ describe("Actor.make(...).toLayer", () => { test("accepts a function returning a plain action handlers object", () => { expectTypeOf(TestActor.toLayer).toBeCallableWith( (_wakeOptions: any) => ({ - GetContext: () => Effect.void, + Ping: () => Effect.succeed(0), }), ); }); @@ -111,7 +116,7 @@ describe("Actor.make(...).toLayer", () => { ).toEqualTypeOf(); return { - GetContext: () => Effect.void, + Ping: () => Effect.succeed(0), }; }); @@ -124,7 +129,7 @@ describe("Actor.make(...).toLayer", () => { ).toEqualTypeOf(); return { - GetContext: () => Effect.void, + Ping: () => Effect.succeed(0), }; }, {}); }); @@ -137,7 +142,7 @@ describe("Actor.make(...).toLayer", () => { >(); return { - GetContext: () => Effect.void, + Ping: () => Effect.succeed(0), }; }, { state: TestState }, @@ -165,7 +170,7 @@ describe("Actor.make(...).toLayer", () => { >(); return { - GetContext: () => Effect.void, + Ping: () => Effect.succeed(0), }; }, { state: TransformedState }, @@ -180,7 +185,7 @@ describe("Actor.make(...).toLayer", () => { ).toEqualTypeOf<{ readonly count: number }>(); return { - GetContext: () => Effect.void, + Ping: () => Effect.succeed(0), }; }, { state: TestState }, @@ -205,7 +210,7 @@ describe("Actor.make(...).toLayer", () => { }>(); return { - GetContext: () => Effect.void, + Ping: () => Effect.succeed(0), }; }, { state: TransformedState }, @@ -220,24 +225,24 @@ describe("Actor.make(...).toLayer", () => { ).toEqualTypeOf(); return { - GetContext: () => Effect.void, + Ping: () => Effect.succeed(0), }; }, { db: db() }, ); }); - test("accepts a function returning an Effect of action handlers", () => { + test("accepts a function returning an effect of action handlers", () => { expectTypeOf(TestActor.toLayer).toBeCallableWith((_wakeOptions: any) => Effect.gen(function* () { return { - GetContext: () => Effect.void, + Ping: () => Effect.succeed(0), }; }), ); }); - test("accepts an Effect that resolves to a wake function", () => { + test("accepts an effect that resolves to a wake function", () => { expectTypeOf(TestActor.toLayer).toBeCallableWith( Effect.gen(function* () { // Allow for initialization logic before the per-entity wake function is called @@ -245,7 +250,7 @@ describe("Actor.make(...).toLayer", () => { return (_wakeOptions: any) => Effect.gen(function* () { return { - GetContext: () => Effect.void, + Ping: () => Effect.succeed(0), }; }); }), @@ -256,7 +261,7 @@ describe("Actor.make(...).toLayer", () => { expectTypeOf(TestActor.toLayer).toBeCallableWith( Effect.fn("wake")(function* (_wakeOptions) { return { - GetContext: () => Effect.void, + Ping: () => Effect.succeed(0), }; }), ); @@ -268,22 +273,216 @@ describe("Actor.make(...).toLayer", () => { test("action handler's envelope is typed against the action", () => { TestActor.toLayer({ - GetContext: (envelope) => { - expectTypeOf(envelope._tag).toEqualTypeOf<"GetContext">(); + Ping: (envelope) => { + expectTypeOf(envelope._tag).toEqualTypeOf<"Ping">(); expectTypeOf(envelope.action).toExtend(); - return Effect.void; + return Effect.succeed(0); }, }); }); + test("action handler return success is type checked", () => { + // Plain action handlers object. + expectTypeOf(TestActor.toLayer).toBeCallableWith({ + Ping: () => Effect.succeed(0), + }); + + TestActor.toLayer({ + // @ts-expect-error: Ping must return the declared number success type. + Ping: () => Effect.succeed("not a number"), + }); + + // Effect of action handlers. + expectTypeOf(TestActor.toLayer).toBeCallableWith( + Effect.gen(function* () { + return { + Ping: () => Effect.succeed(0), + }; + }), + ); + + TestActor.toLayer( + // @ts-expect-error: Ping must return the declared number success type. + Effect.gen(function* () { + return { + Ping: () => Effect.succeed("not a number"), + }; + }), + ); + + // Function returning a plain action handlers object. + expectTypeOf(TestActor.toLayer).toBeCallableWith(() => ({ + Ping: () => Effect.succeed(0), + })); + + // @ts-expect-error: Ping must return the declared number success type. + TestActor.toLayer(() => ({ + Ping: () => Effect.succeed("not a number"), + })); + + // Function returning an effect of action handlers. + expectTypeOf(TestActor.toLayer).toBeCallableWith(() => + Effect.gen(function* () { + return { + Ping: () => Effect.succeed(0), + }; + }), + ); + + // @ts-expect-error: Ping must return the declared number success type. + TestActor.toLayer(() => + Effect.gen(function* () { + return { + Ping: () => Effect.succeed("not a number"), + }; + }), + ); + + // Effect that resolves to a wake function. + expectTypeOf(TestActor.toLayer).toBeCallableWith( + Effect.gen(function* () { + return () => ({ + Ping: () => Effect.succeed(0), + }); + }), + ); + + TestActor.toLayer( + // @ts-expect-error: Ping must return the declared number success type. + Effect.gen(function* () { + return () => ({ + Ping: () => Effect.succeed("not a number"), + }); + }), + ); + + // Effect.fn returning action handlers. + expectTypeOf(TestActor.toLayer).toBeCallableWith( + Effect.fn("wake")(function* () { + return { + Ping: () => Effect.succeed(0), + }; + }), + ); + + TestActor.toLayer( + // @ts-expect-error: Ping must return the declared number success type. + Effect.fn("wake")(function* () { + return { + Ping: () => Effect.succeed("not a number"), + }; + }), + ); + }); + + test("action handler return error is type checked", () => { + // Plain action handlers object. + expectTypeOf(TestActor.toLayer).toBeCallableWith({ + Ping: () => Effect.succeed(0), + }); + + TestActor.toLayer({ + // @ts-expect-error: Ping can only fail with its declared action error type. + // @effect-diagnostics effect/missingEffectError:off + Ping: () => Effect.fail(1), + }); + + // Effect of action handlers. + expectTypeOf(TestActor.toLayer).toBeCallableWith( + Effect.gen(function* () { + return { + Ping: () => Effect.succeed(0), + }; + }), + ); + + TestActor.toLayer( + // @ts-expect-error: Ping can only fail with its declared action error type. + Effect.gen(function* () { + return { + // @effect-diagnostics effect/missingEffectError:off + Ping: () => Effect.fail(1), + }; + }), + ); + + // Function returning a plain action handlers object. + expectTypeOf(TestActor.toLayer).toBeCallableWith(() => ({ + Ping: () => Effect.succeed(0), + })); + + // @ts-expect-error: Ping can only fail with its declared action error type. + TestActor.toLayer(() => ({ + // @effect-diagnostics effect/missingEffectError:off + Ping: () => Effect.fail(1), + })); + + // Function returning an effect of action handlers. + expectTypeOf(TestActor.toLayer).toBeCallableWith(() => + Effect.gen(function* () { + return { + Ping: () => Effect.succeed(0), + }; + }), + ); + + // @ts-expect-error: Ping can only fail with its declared action error type. + TestActor.toLayer(() => + Effect.gen(function* () { + return { + // @effect-diagnostics effect/missingEffectError:off + Ping: () => Effect.fail(1), + }; + }), + ); + + // Effect that resolves to a wake function. + expectTypeOf(TestActor.toLayer).toBeCallableWith( + Effect.gen(function* () { + return () => ({ + Ping: () => Effect.succeed(0), + }); + }), + ); + + TestActor.toLayer( + // @ts-expect-error: Ping can only fail with its declared action error type. + Effect.gen(function* () { + return () => ({ + // @effect-diagnostics effect/missingEffectError:off + Ping: () => Effect.fail(1), + }); + }), + ); + + // Effect.fn returning action handlers. + expectTypeOf(TestActor.toLayer).toBeCallableWith( + Effect.fn("wake")(function* () { + return { + Ping: () => Effect.succeed(0), + }; + }), + ); + + TestActor.toLayer( + // @ts-expect-error: Ping can only fail with its declared action error type. + Effect.fn("wake")(function* () { + return { + // @effect-diagnostics effect/missingEffectError:off + Ping: () => Effect.fail(1), + }; + }), + ); + }); + test("missing action handler is rejected", () => { - // @ts-expect-error: GetContext handler is required + // @ts-expect-error: Ping handler is required TestActor.toLayer({}); }); test.todo("unknown action handler key is rejected", () => { TestActor.toLayer({ - GetContext: () => Effect.void, + Ping: () => Effect.succeed(0), // TODO: toLayer should reject unknown action handler keys Unknown: () => Effect.void, }); @@ -293,7 +492,7 @@ describe("Actor.make(...).toLayer", () => { const layer = TestActor.toLayer( Effect.gen(function* () { yield* SomeDep; - return { GetContext: () => Effect.void }; + return { Ping: () => Effect.succeed(0) }; }), ); type Reqs = From 57cb40eb4978ff46cb71e3f2390b32925cc18879 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 31 May 2026 15:51:02 +0200 Subject: [PATCH 269/306] Add type tests for Actor.of --- .../packages/effect/src/Actor.test-d.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts index 9147e05792..577f2fc99a 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -502,6 +502,49 @@ describe("Actor.make(...).toLayer", () => { }); }); +describe("Actor.make(...).of", () => { + test("preserves the action handlers object type", () => { + const handlers = { + Ping: () => Effect.succeed(0), + }; + + expectTypeOf(TestActor.of(handlers)).toEqualTypeOf(); + }); + + test("action handler's envelope is typed against the action", () => { + TestActor.of({ + Ping: (envelope) => { + expectTypeOf(envelope._tag).toEqualTypeOf<"Ping">(); + expectTypeOf(envelope.action).toEqualTypeOf(); + return Effect.succeed(0); + }, + }); + }); + + test("action handler return success is type checked", () => { + expectTypeOf(TestActor.of).toBeCallableWith({ + Ping: () => Effect.succeed(0), + }); + + TestActor.of({ + // @ts-expect-error: Ping must return the declared number success type. + Ping: () => Effect.succeed("not a number"), + }); + }); + + test("action handler return error is type checked", () => { + expectTypeOf(TestActor.of).toBeCallableWith({ + Ping: () => Effect.succeed(0), + }); + + TestActor.of({ + // @ts-expect-error: Ping can only fail with its declared action error type. + // @effect-diagnostics effect/missingEffectError:off + Ping: () => Effect.fail(1), + }); + }); +}); + describe("Actor.make(...).client", () => { test("yields a typed Accessor", () => { expectTypeOf(TestActor.client).toEqualTypeOf< From 6e1f75fc462f930fd42d2f55d1db8f646f60502c Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 31 May 2026 16:44:37 +0200 Subject: [PATCH 270/306] fix(effect): tighten public action handler types --- .../packages/effect/src/Action.ts | 8 +++--- .../packages/effect/src/Actor.ts | 10 +++---- .../packages/effect/src/State.ts | 27 +++++++------------ 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index ffe1d55a96..447cdbf81b 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -9,10 +9,10 @@ export const isAction = (u: unknown): u is Action => * A value-level definition for a non-durable, request-response call. */ export interface Action< - in out Tag extends string, - out Payload extends Schema.Top = Schema.Void, - out Success extends Schema.Top = Schema.Void, - out Error extends Schema.Top = Schema.Never, + Tag extends string, + Payload extends Schema.Top = Schema.Void, + Success extends Schema.Top = Schema.Void, + Error extends Schema.Top = Schema.Never, > { readonly [TypeId]: typeof TypeId; readonly _tag: Tag; diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index dc2b256521..0ced1e6915 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -246,8 +246,8 @@ type ToLayerRequirements< * display options, but no server implementation. */ export interface Actor< - in out Name extends string, - in out Actions extends Action.Any = never, + Name extends string, + Actions extends Action.Any = never, > { readonly [TypeId]: typeof TypeId; readonly name: Name; @@ -310,9 +310,9 @@ export interface Actor< export type Any = Actor; export type ActionHandlersFrom = { - readonly [Action in Actions as Action["_tag"]]: ( - envelope: ActionRequest, - ) => Action.ResultFrom; + readonly [A in Actions as A["_tag"]]: ( + envelope: ActionRequest, + ) => Action.ResultFrom; }; const Proto: Omit, "name" | "actions"> = { diff --git a/rivetkit-typescript/packages/effect/src/State.ts b/rivetkit-typescript/packages/effect/src/State.ts index 2bf88e12cb..ded4792253 100644 --- a/rivetkit-typescript/packages/effect/src/State.ts +++ b/rivetkit-typescript/packages/effect/src/State.ts @@ -41,8 +41,8 @@ const TypeId = "~@rivetkit/effect/State"; * `SchemaError` when read/write decode/encode against a schema) * - `R` — the read/write closures' service requirements */ -export interface State - extends State.Variance, +export interface State + extends Variance, Pipeable.Pipeable, Inspectable.Inspectable { readonly read: () => Effect.Effect; @@ -61,14 +61,12 @@ export interface State export const isState = (u: unknown): u is State => Predicate.hasProperty(u, TypeId); -export declare namespace State { - export interface Variance { - readonly [TypeId]: { - readonly _A: Types.Invariant; - readonly _E: Types.Covariant; - readonly _R: Types.Covariant; - }; - } +export interface Variance { + readonly [TypeId]: { + readonly _A: Types.Invariant; + readonly _E: Types.Covariant; + readonly _R: Types.Covariant; + }; } const Proto = { @@ -134,10 +132,7 @@ export const update: { ( f: (a: A) => A, ): (self: State) => Effect.Effect; - ( - self: State, - f: (a: A) => A, - ): Effect.Effect; + (self: State, f: (a: A) => A): Effect.Effect; } = dual( 2, ( @@ -155,9 +150,7 @@ export const update: { * read/apply/write triple is atomic across fibers. */ export const updateAndGet: { - ( - f: (a: A) => A, - ): (self: State) => Effect.Effect; + (f: (a: A) => A): (self: State) => Effect.Effect; (self: State, f: (a: A) => A): Effect.Effect; } = dual( 2, From 0fb9e7292034d913649e486010012493d7eff65a Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 31 May 2026 16:50:13 +0200 Subject: [PATCH 271/306] test(effect): convert fixture state failures to defects --- .../packages/effect/test/fixtures/actors.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts index ec52ac173e..d24f2836fd 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts @@ -250,9 +250,12 @@ export const TransformedStateActorLive = TransformedStateActor.toLayer( Effect.succeed( rawWakeState as unknown as typeof EncodedTransformedState.Type, ), - GetDecodedState: () => State.get(state), + GetDecodedState: () => State.get(state).pipe(Effect.orDie), SetTransformedStateAndSleep: ({ payload }) => - State.set(state, payload).pipe(Effect.andThen(sleep)), + State.set(state, payload).pipe( + Effect.andThen(sleep), + Effect.orDie, + ), SetRawWakeStateAndSleep: ({ payload }) => Effect.tryPromise(async () => { rawRivetkitContext.state = @@ -388,7 +391,7 @@ export const CounterLive = Counter.toLayer( ...s, count: s.count + payload.amount, }), - ); + ).pipe(Effect.orDie); yield* sleep; return count; }), @@ -400,7 +403,7 @@ export const CounterLive = Counter.toLayer( ...s, when: payload.when, }), - ); + ).pipe(Effect.orDie); yield* sleep; return when; }), @@ -412,7 +415,7 @@ export const CounterLive = Counter.toLayer( ...s, tags: payload.tags, }), - ); + ).pipe(Effect.orDie); yield* sleep; return tags; }), @@ -424,11 +427,11 @@ export const CounterLive = Counter.toLayer( ...s, scaled: payload.amount, }), - ); + ).pipe(Effect.orDie); yield* sleep; return scaled; }), - GetPersistedState: () => State.get(state), + GetPersistedState: () => State.get(state).pipe(Effect.orDie), // Per-actor SQLite is provisioned via the `db:` option on // `Counter.toLayer` below. The build effect destructures `db` // from `rawRivetkitContext`, so handlers reach SQLite @@ -538,8 +541,9 @@ export const StrictLive = Strict.toLayer( StrictSetUnhandled: ({ payload }) => State.set(state, payload.value).pipe( Effect.as(payload.value), + Effect.orDie, ), - StrictGet: () => State.get(state), + StrictGet: () => State.get(state).pipe(Effect.orDie), }); }), { From bc77aa6db895d33e0600eb28869e369170972692 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 31 May 2026 16:51:06 +0200 Subject: [PATCH 272/306] Handle schema and RPC errors in chat-room example --- examples/effect/src/actors/chat-room/live.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts index 1e77b5e4d2..d8b5a31b07 100644 --- a/examples/effect/src/actors/chat-room/live.ts +++ b/examples/effect/src/actors/chat-room/live.ts @@ -127,7 +127,7 @@ export const ChatRoomLive = ChatRoom.toLayer( name: payload.name, initialized: true, }; - }), + }).pipe(Effect.orDie), Join: ({ payload }) => Effect.gen(function* () { const joinedAt = yield* DateTime.now; @@ -141,7 +141,7 @@ export const ChatRoomLive = ChatRoom.toLayer( ...current, members: [...current.members, member], }), - ); + ).pipe(Effect.orDie); rawRivetkitContext.broadcast("memberJoined", { member: { @@ -190,7 +190,9 @@ export const ChatRoomLive = ChatRoom.toLayer( // If Review fails with BannedWordsError, that typed error // flows through SendMessage's declared error channel. - yield* moderator.Review({ text: payload.text }); + yield* moderator + .Review({ text: payload.text }) + .pipe(Effect.catchTag("RivetError", Effect.die)); const createdAt = yield* DateTime.now; yield* Effect.tryPromise(() => From 4699fe09fdc9091519eef9c5453afa8bbb2462a2 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 31 May 2026 22:43:01 +0200 Subject: [PATCH 273/306] chore(effect): switch package build to tsc --- pnpm-lock.yaml | 3 -- .../packages/effect/package.json | 31 ++++++++----------- .../packages/effect/src/Actor.test-d.ts | 10 +++--- .../packages/effect/src/Actor.test.ts | 3 +- .../packages/effect/src/Actor.ts | 18 +++++------ .../packages/effect/src/Client.test.ts | 3 +- .../packages/effect/src/Client.ts | 10 +++--- .../packages/effect/src/Registry.test-d.ts | 4 +-- .../packages/effect/src/Registry.test.ts | 4 +-- .../packages/effect/src/Registry.ts | 2 +- .../packages/effect/src/RivetError.test.ts | 2 +- .../packages/effect/src/State.test.ts | 2 +- .../packages/effect/src/mod.ts | 12 +++---- .../packages/effect/tsconfig.build.json | 19 ++++++++++++ .../packages/effect/tsconfig.json | 7 +++-- .../packages/effect/tsup.config.ts | 4 --- 16 files changed, 70 insertions(+), 64 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/tsconfig.build.json delete mode 100644 rivetkit-typescript/packages/effect/tsup.config.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 159a81e578..df73192d30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4133,9 +4133,6 @@ importers: effect: specifier: ^4.0.0-beta.66 version: 4.0.0-beta.66 - tsup: - specifier: ^8.4.0 - version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.15))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) typescript: specifier: ^5.9.2 version: 5.9.3 diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index 4761da2a70..899fc73895 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -4,29 +4,25 @@ "description": "Effect SDK for Rivet Actors", "license": "Apache-2.0", "type": "module", - "sideEffects": [ - "./dist/chunk-*.js", - "./dist/chunk-*.cjs" - ], + "sideEffects": false, "files": [ - "dist", - "package.json" + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map" ], "exports": { - ".": { - "import": { - "types": "./dist/mod.d.ts", - "default": "./dist/mod.js" - }, - "require": { - "types": "./dist/mod.d.cts", - "default": "./dist/mod.cjs" - } + ".": "./src/mod.ts" + }, + "publishConfig": { + "access": "public", + "exports": { + ".": "./dist/mod.js" } }, "scripts": { - "build": "tsup src/mod.ts", - "dev": "tsup src/mod.ts --watch", + "build": "tsc -p tsconfig.build.json", "check-types": "tsc --noEmit", "test": "vitest --typecheck", "coverage": "vitest run --coverage" @@ -43,7 +39,6 @@ "@types/node": "^22.18.1", "@vitest/coverage-v8": "^4.1.7", "effect": "^4.0.0-beta.66", - "tsup": "^8.4.0", "typescript": "^5.9.2", "vitest": "^4.1.5" } diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts index 577f2fc99a..89e77d9a7a 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -5,13 +5,15 @@ import { Schema, SchemaTransformation, } from "effect"; +import { + Action, + Actor, + type Client, + type State, +} from "@rivetkit/effect"; import type { RawAccess } from "rivetkit/db"; import { db } from "rivetkit/db"; import { describe, expectTypeOf, test } from "vitest"; -import * as Action from "./Action"; -import * as Actor from "./Actor"; -import type * as Client from "./Client"; -import type * as State from "./State"; class SomeDep extends Context.Service()( "SomeDep", diff --git a/rivetkit-typescript/packages/effect/src/Actor.test.ts b/rivetkit-typescript/packages/effect/src/Actor.test.ts index b851ba33ed..a911d3ed3e 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test.ts @@ -1,8 +1,7 @@ import { assert, describe, it } from "@effect/vitest"; +import { Actor, State } from "@rivetkit/effect"; import { Context, Effect, Layer } from "effect"; import type * as Rivetkit from "rivetkit"; -import * as Actor from "./Actor"; -import * as State from "./State"; class Prefix extends Context.Service()( "Actor.test/Prefix", diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 0ced1e6915..0cfdc2f317 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -18,15 +18,15 @@ import { } from "effect"; import * as Rivetkit from "rivetkit"; import type * as RivetkitDb from "rivetkit/db"; -import type * as Action from "./Action"; -import * as Client from "./Client"; -import * as ActionErrorEnvelope from "./internal/ActionErrorEnvelope"; -import type * as StateOptions from "./internal/StateOptions"; -import { readTraceMeta, rpcSystem } from "./internal/tracing"; -import { hasStringProperty } from "./internal/utils"; -import * as Registry from "./Registry"; -import type * as RivetError from "./RivetError"; -import * as State from "./State"; +import type * as Action from "./Action.ts"; +import * as Client from "./Client.ts"; +import * as ActionErrorEnvelope from "./internal/ActionErrorEnvelope.ts"; +import type * as StateOptions from "./internal/StateOptions.ts"; +import { readTraceMeta, rpcSystem } from "./internal/tracing.ts"; +import { hasStringProperty } from "./internal/utils.ts"; +import * as Registry from "./Registry.ts"; +import type * as RivetError from "./RivetError.ts"; +import * as State from "./State.ts"; const TypeId = "~@rivetkit/effect/Actor"; diff --git a/rivetkit-typescript/packages/effect/src/Client.test.ts b/rivetkit-typescript/packages/effect/src/Client.test.ts index b0f75b2933..0648c200ce 100644 --- a/rivetkit-typescript/packages/effect/src/Client.test.ts +++ b/rivetkit-typescript/packages/effect/src/Client.test.ts @@ -1,9 +1,8 @@ import { assert, describe, it } from "@effect/vitest"; +import { Client, RivetError } from "@rivetkit/effect"; import { Effect, Schema } from "effect"; import * as RivetkitErrors from "rivetkit/errors"; -import * as Client from "./Client"; import * as ActionErrorEnvelope from "./internal/ActionErrorEnvelope"; -import * as RivetError from "./RivetError"; describe("makeRivetkitActionFailureClassifier", () => { const ExpectedError = Schema.Struct({ diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index 05c2cd0c1e..bc4ed0dbd2 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -1,11 +1,11 @@ import { Context, Effect, Layer, Record, Result, Schema } from "effect"; import * as RivetkitClient from "rivetkit/client"; import * as RivetkitErrors from "rivetkit/errors"; -import type * as Action from "./Action"; -import type * as Actor from "./Actor"; -import * as ActionErrorEnvelope from "./internal/ActionErrorEnvelope"; -import { rpcSystem, type TraceMeta } from "./internal/tracing"; -import * as RivetError from "./RivetError"; +import type * as Action from "./Action.ts"; +import type * as Actor from "./Actor.ts"; +import * as ActionErrorEnvelope from "./internal/ActionErrorEnvelope.ts"; +import { rpcSystem, type TraceMeta } from "./internal/tracing.ts"; +import * as RivetError from "./RivetError.ts"; const TypeId = "~@rivetkit/effect/Client"; diff --git a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts index 58af3d6be0..1a61563355 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts @@ -1,13 +1,11 @@ import { type Context, Effect, Layer, type Scope } from "effect"; +import { Action, Actor, Registry } from "@rivetkit/effect"; import type { HttpServerError, HttpServerRequest, HttpServerResponse, } from "effect/unstable/http"; import { describe, expectTypeOf, test } from "vitest"; -import * as Action from "./Action"; -import * as Actor from "./Actor"; -import * as Registry from "./Registry"; const TestActor = Actor.make("TestActor", { actions: [Action.make("Test")], diff --git a/rivetkit-typescript/packages/effect/src/Registry.test.ts b/rivetkit-typescript/packages/effect/src/Registry.test.ts index 86913f0ef0..d2538313be 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test.ts @@ -1,10 +1,8 @@ import { assert, describe, it } from "@effect/vitest"; +import { Action, Actor, Registry } from "@rivetkit/effect"; import { Effect, Layer } from "effect"; import { HttpEffect } from "effect/unstable/http"; import { vi } from "vitest"; -import * as Action from "./Action"; -import * as Actor from "./Actor"; -import * as Registry from "./Registry"; const TestActor = Actor.make("TestActor", { actions: [Action.make("Test")], diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 92e75cea86..54faf07106 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -7,7 +7,7 @@ import { type HttpServerResponse, } from "effect/unstable/http"; import * as Rivetkit from "rivetkit"; -import * as Client from "./Client"; +import * as Client from "./Client.ts"; const TypeId = "~@rivetkit/effect/Registry"; type ServerlessOptions = NonNullable< diff --git a/rivetkit-typescript/packages/effect/src/RivetError.test.ts b/rivetkit-typescript/packages/effect/src/RivetError.test.ts index cbe1dce03b..dad3e9bfe5 100644 --- a/rivetkit-typescript/packages/effect/src/RivetError.test.ts +++ b/rivetkit-typescript/packages/effect/src/RivetError.test.ts @@ -1,7 +1,7 @@ import { assert, describe, it } from "@effect/vitest"; +import { RivetError } from "@rivetkit/effect"; import { Duration, Effect, Schema } from "effect"; import * as RivetkitErrors from "rivetkit/errors"; -import * as RivetError from "./RivetError"; describe("RivetError", () => { it("preserves non-Rivet causes as UnknownError", () => { diff --git a/rivetkit-typescript/packages/effect/src/State.test.ts b/rivetkit-typescript/packages/effect/src/State.test.ts index 3ec3cbb5f3..a1ef2c2a48 100644 --- a/rivetkit-typescript/packages/effect/src/State.test.ts +++ b/rivetkit-typescript/packages/effect/src/State.test.ts @@ -1,6 +1,6 @@ import { assert, describe, it } from "@effect/vitest"; +import { State } from "@rivetkit/effect"; import { Effect, Exit, PubSub, Stream } from "effect"; -import * as State from "./State"; // Helper: build a State backed by a plain mutable cell, with // Effect-typed read/write closures. Mirrors how Registry wires diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index 65b2722c00..6b727a582b 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,6 +1,6 @@ -export * as Action from "./Action"; -export * as Actor from "./Actor"; -export * as Client from "./Client"; -export * as Registry from "./Registry"; -export * as RivetError from "./RivetError"; -export * as State from "./State"; +export * as Action from "./Action.ts"; +export * as Actor from "./Actor.ts"; +export * as Client from "./Client.ts"; +export * as Registry from "./Registry.ts"; +export * as RivetError from "./RivetError.ts"; +export * as State from "./State.ts"; diff --git a/rivetkit-typescript/packages/effect/tsconfig.build.json b/rivetkit-typescript/packages/effect/tsconfig.build.json new file mode 100644 index 0000000000..6999ef8f54 --- /dev/null +++ b/rivetkit-typescript/packages/effect/tsconfig.build.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + // Use incremental builds with project references. + "incremental": true, + "composite": true, + // Target modern JavaScript (ES2022+) whilst staying closely compatible with the Node.js module system. + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": false, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts", "src/**/*.test-d.ts"] +} diff --git a/rivetkit-typescript/packages/effect/tsconfig.json b/rivetkit-typescript/packages/effect/tsconfig.json index 588bd72ffb..5c5526d756 100644 --- a/rivetkit-typescript/packages/effect/tsconfig.json +++ b/rivetkit-typescript/packages/effect/tsconfig.json @@ -2,7 +2,10 @@ "extends": "../../../tsconfig.base.json", "compilerOptions": { "types": [], - "verbatimModuleSyntax": true, + "moduleDetection": "force", // Treat every non-declaration file as a module. + "verbatimModuleSyntax": true, // Only transform/eliminate type-only import/export statements. + "rewriteRelativeImportExtensions": true, // Rewrite `.ts` imports to `.js` at build time. + "noEmit": true, "plugins": [ { "name": "@effect/language-service", @@ -14,5 +17,5 @@ } ] }, - "include": ["src/**/*", "test/**/*"] + "include": ["src/**/*.ts", "test/**/*.ts"] } diff --git a/rivetkit-typescript/packages/effect/tsup.config.ts b/rivetkit-typescript/packages/effect/tsup.config.ts deleted file mode 100644 index e7d8e5f88d..0000000000 --- a/rivetkit-typescript/packages/effect/tsup.config.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { defineConfig } from "tsup"; -import defaultConfig from "../../../tsup.base"; - -export default defineConfig(defaultConfig); From e47358b08e6dce157b36ece742dcdf8d6fd21b76 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 31 May 2026 22:46:14 +0200 Subject: [PATCH 274/306] chore(effect): update turbo inputs --- .../packages/effect/turbo.json | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/turbo.json b/rivetkit-typescript/packages/effect/turbo.json index 29d4cb2625..cd6b26c82b 100644 --- a/rivetkit-typescript/packages/effect/turbo.json +++ b/rivetkit-typescript/packages/effect/turbo.json @@ -1,4 +1,41 @@ { "$schema": "https://turbo.build/schema.json", - "extends": ["//"] + "extends": ["//"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "inputs": [ + "package.json", + "src/**", + "tsconfig.json", + "tsconfig.build.json", + "../../../tsconfig.base.json" + ], + "outputs": ["dist/**"], + "env": ["FAST_BUILD", "SKIP_NAPI_BUILD", "SKIP_WASM_BUILD"] + }, + "check-types": { + "dependsOn": ["^build"], + "inputs": [ + "package.json", + "src/**", + "test/**", + "tsconfig.json", + "../../../tsconfig.base.json", + "../rivetkit/tests/shared-engine.ts" + ] + }, + "test": { + "dependsOn": ["^build", "check-types"], + "inputs": [ + "package.json", + "src/**", + "test/**", + "tsconfig.json", + "vitest.config.ts", + "../../../tsconfig.base.json", + "../rivetkit/tests/shared-engine.ts" + ] + } + } } From fbc8bd6d72e9377a402bbe73b6d4f1131e17b551 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 31 May 2026 22:57:10 +0200 Subject: [PATCH 275/306] Add publint and attw checks to effect package --- pnpm-lock.yaml | 214 ++++++++++++++++++ .../packages/effect/package.json | 4 + .../packages/effect/turbo.json | 14 ++ 3 files changed, 232 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df73192d30..38eedc20bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4118,6 +4118,9 @@ importers: specifier: workspace:* version: link:../rivetkit devDependencies: + '@arethetypeswrong/cli': + specifier: ^0.18.3 + version: 0.18.3 '@effect/language-service': specifier: ^0.85.1 version: 0.85.1 @@ -4133,6 +4136,9 @@ importers: effect: specifier: ^4.0.0-beta.66 version: 4.0.0-beta.66 + publint: + specifier: ^0.3.21 + version: 0.3.21 typescript: specifier: ^5.9.2 version: 5.9.3 @@ -5012,6 +5018,9 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@andrewbranch/untar.js@1.0.3': + resolution: {integrity: sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==} + '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} @@ -5024,6 +5033,15 @@ packages: zod: optional: true + '@arethetypeswrong/cli@0.18.3': + resolution: {integrity: sha512-GeAlc+lUD4gKHD/LDQNvQY30FfQ+xAXg2inbQKUjFZgTOdI5ygEweaOnGHGBPSKXSLGQC7VLhpXu9zMnYk/4sQ==} + engines: {node: '>=20'} + hasBin: true + + '@arethetypeswrong/core@0.18.3': + resolution: {integrity: sha512-sWBB/tdIktaT5xMq0Dz6CJyqcf6oMNdmiKiuPU1lWoJLTL6gjRSsksBuSgqot21hylkklBQY1wiSu+PkZhW7sw==} + engines: {node: '>=20'} + '@asteasolutions/zod-to-openapi@8.2.0': resolution: {integrity: sha512-u05zNUirlukJAf9oEHmxSF31L1XQhz9XdpVILt7+xhrz65oQqBpiOWFkGvRWL0IpjOUJ878idKoNmYPxrFnkeg==} peerDependencies: @@ -5866,6 +5884,9 @@ packages: '@borewit/text-codec@0.2.2': resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + '@braidai/lang@1.1.2': + resolution: {integrity: sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==} + '@braintree/sanitize-url@7.1.1': resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} @@ -5987,6 +6008,10 @@ packages: '@codemirror/view@6.38.2': resolution: {integrity: sha512-bTWAJxL6EOFLPzTx+O5P5xAO3gTqpatQ2b/ARQ8itfU/v2LlpS3pH2fkL0A3E/Fx8Y2St2KES7ZEV0sHTsSW/A==} + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + '@copilotkit/aimock@1.7.0': resolution: {integrity: sha512-X6B2z0MgGTg8N/geRg6zRVVgEp3krP+gYapwXCt2w3JU7BSf2q0laa4iHC+BZqPXf29iVDVwDM7BxB5LqhjcAg==} engines: {node: '>=20.15.0'} @@ -7581,6 +7606,9 @@ packages: cpu: [x64] os: [win32] + '@loaderkit/resolve@1.0.6': + resolution: {integrity: sha512-G8FdIoF5CypfwmD9rl8BXod5HDn8JqB0CCNBXDTaRZ+yRYhARrrSToX1zg1zy9jX3zLqigsELwhT4gNtkdQAUg==} + '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} @@ -8227,6 +8255,10 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@publint/pack@0.1.4': + resolution: {integrity: sha512-HDVTWq3H0uTXiU0eeSQntcVUTPP3GamzeXI41+x7uU9J65JgWQh3qWZHblR1i0npXfFtF+mxBiU2nJH8znxWnQ==} + engines: {node: '>=18'} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -9536,6 +9568,10 @@ packages: '@sinclair/typebox@0.34.41': resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + '@sindresorhus/merge-streams@2.3.0': resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} @@ -11050,6 +11086,10 @@ packages: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + ansi-regex@4.1.1: resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} engines: {node: '>=6'} @@ -11631,6 +11671,10 @@ packages: change-case@4.1.2: resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -11714,6 +11758,9 @@ packages: resolution: {integrity: sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==} engines: {node: '>= 0.10'} + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cjs-module-lexer@2.2.0: resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} @@ -11743,6 +11790,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -11823,6 +11874,10 @@ packages: resolution: {integrity: sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==} engines: {node: '>=14'} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -12699,6 +12754,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -12726,6 +12784,10 @@ packages: resolution: {integrity: sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==} engines: {node: '>=8'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + errno@0.1.8: resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} hasBin: true @@ -13208,6 +13270,9 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -14639,6 +14704,12 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked-terminal@7.3.0: + resolution: {integrity: sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==} + engines: {node: '>=16.0.0'} + peerDependencies: + marked: '>=1 <16' + marked@14.0.0: resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} engines: {node: '>= 18'} @@ -14654,6 +14725,11 @@ packages: engines: {node: '>= 20'} hasBin: true + marked@9.1.6: + resolution: {integrity: sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==} + engines: {node: '>= 16'} + hasBin: true + marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} @@ -15155,6 +15231,10 @@ packages: react-dom: optional: true + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -15291,6 +15371,10 @@ packages: engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} + node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} @@ -16140,6 +16224,11 @@ packages: public-encrypt@4.0.3: resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} + publint@0.3.21: + resolution: {integrity: sha512-OqejcnMV6E9zel2oCrUOJEiiFkGiAAni0A6ibfQNh1k9Gu5z4F+Yso8lllam7AzmV6Do0vp7u3UpZNRBwuXaHQ==} + engines: {node: '>=18'} + hasBin: true + pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -16712,6 +16801,10 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -16925,6 +17018,10 @@ packages: engines: {node: '>=14.0.0', npm: '>=6.0.0'} hasBin: true + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -17206,6 +17303,10 @@ packages: resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} engines: {node: '>=8'} + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -17609,6 +17710,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.6.1-rc: + resolution: {integrity: sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.8.2: resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} engines: {node: '>=14.17'} @@ -17666,6 +17772,10 @@ packages: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + unicode-match-property-ecmascript@2.0.0: resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} engines: {node: '>=4'} @@ -18854,6 +18964,8 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@andrewbranch/untar.js@1.0.3': {} + '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 @@ -18871,6 +18983,27 @@ snapshots: optionalDependencies: zod: 4.1.13 + '@arethetypeswrong/cli@0.18.3': + dependencies: + '@arethetypeswrong/core': 0.18.3 + chalk: 4.1.2 + cli-table3: 0.6.5 + commander: 10.0.1 + marked: 9.1.6 + marked-terminal: 7.3.0(marked@9.1.6) + semver: 7.7.4 + + '@arethetypeswrong/core@0.18.3': + dependencies: + '@andrewbranch/untar.js': 1.0.3 + '@loaderkit/resolve': 1.0.6 + cjs-module-lexer: 1.4.3 + fflate: 0.8.3 + lru-cache: 11.2.6 + semver: 7.7.4 + typescript: 5.6.1-rc + validate-npm-package-name: 5.0.1 + '@asteasolutions/zod-to-openapi@8.2.0(zod@4.1.13)': dependencies: openapi3-ts: 4.5.0 @@ -20087,6 +20220,8 @@ snapshots: '@borewit/text-codec@0.2.2': {} + '@braidai/lang@1.1.2': {} + '@braintree/sanitize-url@7.1.1': {} '@capsizecss/unpack@4.0.0': @@ -20234,6 +20369,9 @@ snapshots: style-mod: 4.1.3 w3c-keyname: 2.2.8 + '@colors/colors@1.5.0': + optional: true + '@copilotkit/aimock@1.7.0': {} '@copilotkit/llmock@1.7.1': @@ -21795,6 +21933,10 @@ snapshots: '@lmdb/lmdb-win32-x64@3.4.4': optional: true + '@loaderkit/resolve@1.0.6': + dependencies: + '@braidai/lang': 1.1.2 + '@marijn/find-cluster-break@1.0.2': {} '@mariozechner/clipboard-darwin-arm64@0.3.2': @@ -22805,6 +22947,8 @@ snapshots: '@protobufjs/utf8@1.1.0': {} + '@publint/pack@0.1.4': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -24471,6 +24615,8 @@ snapshots: '@sinclair/typebox@0.34.41': {} + '@sindresorhus/is@4.6.0': {} + '@sindresorhus/merge-streams@2.3.0': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -26463,6 +26609,10 @@ snapshots: dependencies: type-fest: 0.21.3 + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + ansi-regex@4.1.1: {} ansi-regex@5.0.1: {} @@ -27244,6 +27394,8 @@ snapshots: snake-case: 3.0.4 tslib: 2.8.1 + char-regex@1.0.2: {} + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -27338,6 +27490,8 @@ snapshots: safe-buffer: 5.2.1 to-buffer: 1.2.2 + cjs-module-lexer@1.4.3: {} + cjs-module-lexer@2.2.0: {} class-variance-authority@0.7.1: @@ -27365,6 +27519,12 @@ snapshots: cli-spinners@2.9.2: {} + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + cli-width@4.1.0: {} client-only@0.0.1: {} @@ -27451,6 +27611,8 @@ snapshots: commander@10.0.0: {} + commander@10.0.1: {} + commander@11.1.0: {} commander@12.1.0: {} @@ -28172,6 +28334,8 @@ snapshots: emoji-regex@9.2.2: {} + emojilib@2.4.0: {} + encodeurl@1.0.2: {} encodeurl@2.0.0: {} @@ -28191,6 +28355,8 @@ snapshots: env-editor@0.4.2: {} + environment@1.1.0: {} + errno@0.1.8: dependencies: prr: 1.0.1 @@ -28884,6 +29050,8 @@ snapshots: fflate@0.8.2: {} + fflate@0.8.3: {} + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -30488,12 +30656,25 @@ snapshots: markdown-table@3.0.4: {} + marked-terminal@7.3.0(marked@9.1.6): + dependencies: + ansi-escapes: 7.3.0 + ansi-regex: 6.2.2 + chalk: 5.6.2 + cli-highlight: 2.1.11 + cli-table3: 0.6.5 + marked: 9.1.6 + node-emoji: 2.2.0 + supports-hyperlinks: 3.2.0 + marked@14.0.0: {} marked@15.0.12: {} marked@16.4.2: {} + marked@9.1.6: {} + marky@1.3.0: {} math-expression-evaluator@1.4.0: {} @@ -31480,6 +31661,8 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + mri@1.2.0: {} + mrmime@2.0.1: {} ms@2.0.0: {} @@ -31674,6 +31857,13 @@ snapshots: node-domexception@1.0.0: {} + node-emoji@2.2.0: + dependencies: + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + node-fetch-native@1.6.7: {} node-fetch@2.7.0: @@ -32543,6 +32733,13 @@ snapshots: randombytes: 2.1.0 safe-buffer: 5.2.1 + publint@0.3.21: + dependencies: + '@publint/pack': 0.1.4 + package-manager-detector: 1.6.0 + picocolors: 1.1.1 + sade: 1.8.1 + pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -33313,6 +33510,10 @@ snapshots: dependencies: tslib: 2.8.1 + sade@1.8.1: + dependencies: + mri: 1.2.0 + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -33652,6 +33853,10 @@ snapshots: arg: 5.0.2 sax: 1.4.4 + skin-tone@2.0.0: + dependencies: + unicode-emoji-modifier-base: 1.0.0 + slash@3.0.0: {} slash@5.1.0: {} @@ -33912,6 +34117,11 @@ snapshots: has-flag: 4.0.0 supports-color: 7.2.0 + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + supports-preserve-symlinks-flag@1.0.0: {} svgo@4.0.0: @@ -34490,6 +34700,8 @@ snapshots: typescript@5.4.2: {} + typescript@5.6.1-rc: {} + typescript@5.8.2: {} typescript@5.9.3: {} @@ -34528,6 +34740,8 @@ snapshots: unicode-canonical-property-names-ecmascript@2.0.1: {} + unicode-emoji-modifier-base@1.0.0: {} + unicode-match-property-ecmascript@2.0.0: dependencies: unicode-canonical-property-names-ecmascript: 2.0.1 diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index 899fc73895..0e268da919 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -24,6 +24,8 @@ "scripts": { "build": "tsc -p tsconfig.build.json", "check-types": "tsc --noEmit", + "lint:publint": "publint --strict", + "lint:attw": "attw --pack . --profile esm-only", "test": "vitest --typecheck", "coverage": "vitest run --coverage" }, @@ -34,11 +36,13 @@ "effect": "^4.0.0-beta.66" }, "devDependencies": { + "@arethetypeswrong/cli": "^0.18.3", "@effect/language-service": "^0.85.1", "@effect/vitest": "^4.0.0-beta.66", "@types/node": "^22.18.1", "@vitest/coverage-v8": "^4.1.7", "effect": "^4.0.0-beta.66", + "publint": "^0.3.21", "typescript": "^5.9.2", "vitest": "^4.1.5" } diff --git a/rivetkit-typescript/packages/effect/turbo.json b/rivetkit-typescript/packages/effect/turbo.json index cd6b26c82b..435a537a49 100644 --- a/rivetkit-typescript/packages/effect/turbo.json +++ b/rivetkit-typescript/packages/effect/turbo.json @@ -25,6 +25,20 @@ "../rivetkit/tests/shared-engine.ts" ] }, + "lint:publint": { + "dependsOn": ["build"], + "inputs": [ + "package.json", + "dist/**" + ] + }, + "lint:attw": { + "dependsOn": ["build"], + "inputs": [ + "package.json", + "dist/**" + ] + }, "test": { "dependsOn": ["^build", "check-types"], "inputs": [ From 363116974c917a1051879b4df6b26bee57ff7af5 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Sun, 31 May 2026 23:02:24 +0200 Subject: [PATCH 276/306] refactor(effect): remove unnecessary `await` in tests and adjust type declarations --- .../packages/effect/src/Actor.test-d.ts | 7 +------ rivetkit-typescript/packages/effect/src/Actor.ts | 10 ++++------ .../packages/effect/src/Registry.test-d.ts | 2 +- .../packages/effect/src/Registry.test.ts | 12 ++++++------ 4 files changed, 12 insertions(+), 19 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts index 89e77d9a7a..5b42f4ce53 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -1,3 +1,4 @@ +import { Action, Actor, type Client, type State } from "@rivetkit/effect"; import { Context, Effect, @@ -5,12 +6,6 @@ import { Schema, SchemaTransformation, } from "effect"; -import { - Action, - Actor, - type Client, - type State, -} from "@rivetkit/effect"; import type { RawAccess } from "rivetkit/db"; import { db } from "rivetkit/db"; import { describe, expectTypeOf, test } from "vitest"; diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 0cfdc2f317..132d4c48e1 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -411,7 +411,7 @@ export function toWakeHandler< W extends WakeOptions = WakeOptions, >( wake: (wakeOptions: W) => ActionHandlers, -): (wakeOptions: W) => Effect.Effect; +): (wakeOptions: W) => Effect.Effect; export function toWakeHandler< ActionHandlers extends object, RX, @@ -422,9 +422,7 @@ export function toWakeHandler< export function toWakeHandler< ActionHandlers extends object, W extends WakeOptions = WakeOptions, ->( - wake: ActionHandlers, -): (wakeOptions: W) => Effect.Effect; +>(wake: ActionHandlers): (wakeOptions: W) => Effect.Effect; export function toWakeHandler< ActionHandlers extends object, R, @@ -482,7 +480,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < readonly options: Options; }) { // Snapshot the current Effect context so action callbacks - // (which run in rivetkit's plain Promise world) can run + // (which run in rivetkit’s plain Promise world) can run // handler effects against the same services the Registry.start / // Registry.test layer was provided with. const services = yield* Effect.context(); @@ -595,7 +593,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ) => { // Always wrap in a server-side span so the handler has a // live `currentSpan` even when the caller didn't ship trace - // context (e.g. a non-Effect-SDK client). When trace context + // context (e.g., a non-Effect-SDK client). When trace context // is present, reattach it as the parent so the server span // joins the caller's trace. const rpcMethod = `${actor.name}/${action._tag}`; diff --git a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts index 1a61563355..49ff135edf 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts @@ -1,5 +1,5 @@ -import { type Context, Effect, Layer, type Scope } from "effect"; import { Action, Actor, Registry } from "@rivetkit/effect"; +import { type Context, Effect, Layer, type Scope } from "effect"; import type { HttpServerError, HttpServerRequest, diff --git a/rivetkit-typescript/packages/effect/src/Registry.test.ts b/rivetkit-typescript/packages/effect/src/Registry.test.ts index d2538313be..ad3156124c 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test.ts @@ -36,7 +36,7 @@ describe("Registry.toWebHandler", () => { const body = (await response.json()) as { readonly actorNames: Record; }; - await assert.ok(body.actorNames.TestActor); + assert.ok(body.actorNames.TestActor); } finally { await dispose(); } @@ -56,7 +56,7 @@ describe("Registry.toWebHandler", () => { const body = (await response.json()) as { readonly actorNames: Record; }; - await assert.ok(body.actorNames.TestActor); + assert.ok(body.actorNames.TestActor); } finally { await dispose(); } @@ -93,7 +93,7 @@ describe("Registry.toWebHandler", () => { { group: body.group, code: body.code }, { group: "message", code: "incoming_too_long" }, ); - await assert.match(body.message, /limit is 1 bytes/); + assert.match(body.message, /limit is 1 bytes/); } finally { await dispose(); } @@ -122,7 +122,7 @@ describe("Registry.toWebHandler", () => { { group: body.group, code: body.code }, { group: "message", code: "incoming_too_long" }, ); - await assert.match(body.message, /limit is 1 bytes/); + assert.match(body.message, /limit is 1 bytes/); } finally { await dispose(); } @@ -222,7 +222,7 @@ describe("Registry.toHttpEffect", () => { const body = (await response.json()) as { readonly actorNames: Record; }; - await assert.ok(body.actorNames.TestActor); + assert.ok(body.actorNames.TestActor); })(response), ); }), @@ -246,7 +246,7 @@ describe("Registry.toHttpEffect", () => { const body = (await response.json()) as { readonly actorNames: Record; }; - await assert.ok(body.actorNames.TestActor); + assert.ok(body.actorNames.TestActor); })(response), ); }), From 7962f02570a7a16b67aa64aa73ad7e965a75a26b Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 09:54:59 +0200 Subject: [PATCH 277/306] chore(effect): disable returnEffectInGen diagnostic in Registry --- rivetkit-typescript/packages/effect/src/Registry.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 54faf07106..28eb85cb72 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -161,6 +161,7 @@ export const toHttpEffect = ( > => Effect.gen(function* () { const context = yield* Layer.build(registryLayer); + // @effect-diagnostics-next-line returnEffectInGen:off return makeHttpEffect(Context.get(context, Registry), options); }); From 6fd47772252f78192d3ac3ef1398d8185fb86d7f Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 10:24:09 +0200 Subject: [PATCH 278/306] fix(rivetkit): avoid manual content length on actor requests --- .../packages/rivetkit/src/engine-client/actor-http-client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-http-client.ts b/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-http-client.ts index 3f5f5e8ad7..36a5fc3be6 100644 --- a/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-http-client.ts +++ b/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-http-client.ts @@ -34,9 +34,9 @@ export async function sendHttpRequestToGateway( bodyToSend = reqBody; // If this is a streaming request, we need to convert the headers - // for the basic array buffer + // for the basic array buffer. guardHeaders.delete("transfer-encoding"); - guardHeaders.set("content-length", String(bodyToSend.byteLength)); + guardHeaders.delete("content-length"); } } From 66e213098d876b6143fac232cad22876745e721b Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 10:24:18 +0200 Subject: [PATCH 279/306] chore(effect-example): rename package --- examples/effect/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/effect/package.json b/examples/effect/package.json index 6e6b794c90..acbebc5ce2 100644 --- a/examples/effect/package.json +++ b/examples/effect/package.json @@ -1,5 +1,5 @@ { - "name": "effect", + "name": "example-effect", "private": true, "type": "module", "scripts": { From ac30a4e89063cda9f8efa2dceaffdabcdffcf2d6 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 10:24:22 +0200 Subject: [PATCH 280/306] fix(effect-example): use registered actor name in raw client --- examples/effect/src/client-raw.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/effect/src/client-raw.ts b/examples/effect/src/client-raw.ts index dc80c35e10..74fc1f67c1 100644 --- a/examples/effect/src/client-raw.ts +++ b/examples/effect/src/client-raw.ts @@ -8,7 +8,7 @@ const client = createClient( const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function main() { - const room = client.chatRoom.getOrCreate(`chatroom_${crypto.randomUUID()}`); + const room = client.ChatRoom.getOrCreate(`chatroom_${crypto.randomUUID()}`); try { const roomName = "Effect Lovers"; From 836c2e263bf8a3e4a6a169043cc006a2311bff49 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 11:15:29 +0200 Subject: [PATCH 281/306] Scope actor state-change fibers and cleanup on destroy --- .../packages/effect/src/Actor.ts | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 132d4c48e1..9f23eb3063 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -3,6 +3,7 @@ import { Context, Effect, Exit, + FiberSet, identity, Layer, MutableHashMap, @@ -497,6 +498,9 @@ const makeRivetkitActor = Effect.fnUntraced(function* < string, { readonly actionHandlers: ActionHandlers; + readonly runFork: ( + effect: Effect.Effect, + ) => unknown; readonly scope: Scope.Closeable; readonly state?: State.State< StateOptions.Decoded, @@ -553,7 +557,6 @@ const makeRivetkitActor = Effect.fnUntraced(function* < Effect.sync(() => c.sleep()), ), ); - const wakeOptions = { rawRivetkitContext: c, ...(state ? { state } : {}), @@ -561,10 +564,16 @@ const makeRivetkitActor = Effect.fnUntraced(function* < const actionHandlers = yield* wakeHandler(wakeOptions).pipe( Effect.provide(context), ); + const runFork = yield* FiberSet.makeRuntime< + any, + void, + unknown + >().pipe(Effect.provide(Context.merge(services, context))); yield* Effect.sync(() => MutableHashMap.set(instances, c.actorId, { actionHandlers, + runFork, scope, state, }), @@ -700,15 +709,15 @@ const makeRivetkitActor = Effect.fnUntraced(function* < c: Rivetkit.WakeContextOf, newState: unknown, ) => { - void Effect.runForkWith(services)( - Effect.gen(function* () { - if (!stateCodec) return; - - const instance = yield* MutableHashMap.get( - instances, - c.actorId, - ).pipe(Effect.fromOption, Effect.orDie); + const instance = MutableHashMap.get(instances, c.actorId).pipe( + Option.getOrUndefined, + ); + // Late state-change callbacks can arrive after teardown removed the + // instance. There is no live Effect state stream left to update. + if (!stateCodec || !instance) return; + instance.runFork( + Effect.gen(function* () { const state = yield* Effect.fromNullishOr(instance.state).pipe( Effect.orDie, ); @@ -726,19 +735,24 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ); }; + const cleanupInstance = Effect.fnUntraced(function* (actorId: string) { + const instance = MutableHashMap.get(instances, actorId); + // Actor teardown can be reported more than once across sleep + // and destroy paths. Treat missing entries as already cleaned up. + if (Option.isNone(instance)) return; + + MutableHashMap.remove(instances, actorId); + yield* Scope.close(instance.value.scope, Exit.void); + }); + const onSleep = async (c: Rivetkit.SleepContextOf) => { - await Effect.runPromiseWith(services)( - Effect.gen(function* () { - const instance = yield* MutableHashMap.get( - instances, - c.actorId, - ).pipe(Effect.fromOption, Effect.orDie); - yield* Scope.close(instance.scope, Exit.void); - yield* Effect.sync(() => { - MutableHashMap.remove(instances, c.actorId); - }); - }), - ); + await Effect.runPromiseWith(services)(cleanupInstance(c.actorId)); + }; + + const onDestroy = async ( + c: Rivetkit.DestroyContextOf, + ) => { + await Effect.runPromiseWith(services)(cleanupInstance(c.actorId)); }; return Rivetkit.actor< @@ -772,5 +786,6 @@ const makeRivetkitActor = Effect.fnUntraced(function* < actions, onStateChange, onSleep, + onDestroy, }); }); From 84b4962a9bfcfaa392cbf784ad32e398d33e214a Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 12:13:05 +0200 Subject: [PATCH 282/306] test(effect): avoid action polling for sleep waits --- .../packages/effect/test/e2e.test.ts | 126 +++++++----------- 1 file changed, 46 insertions(+), 80 deletions(-) diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 8706f3fa28..cd0537cd7f 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -139,34 +139,26 @@ layer(TestLayer)("end-to-end", (it) => { it.effect("persists state across a sleep/wake cycle", () => Effect.gen(function* () { - const counter = (yield* Counter.client).getOrCreate([ - "t-persist-state", - ]); - - // Bump the in-memory `Ref` so we can later assert that - // the wake actually rebuilt the actor (the ref should - // reset to 0 on each wake). - yield* counter.Increment({ amount: 7 }); + const key = "t-persist-state"; + const counter = (yield* Counter.client).getOrCreate([key]); + const flags = yield* Flags; + const flagName = `finalizer:${key}`; const beforeSleep = yield* counter.PersistAndSleep({ amount: 11, }); assert.strictEqual(beforeSleep, 11); - // Engine-side sleep teardown is asynchronous. `count` - // is `Ref.make(0)` per wake, so seeing 0 is the deterministic - // signal that the prior wake torn down and a fresh one started. - // `TestClock.withLive` swaps in the real Clock for the duration - // of the poll so the schedule's interval and the timeout both - // elapse in wall time (the suite otherwise runs under TestClock). - const inMemoryAfterWake = yield* counter.GetCount().pipe( + const finalizerFired = yield* Effect.sync(() => + flags.get(flagName), + ).pipe( Effect.repeat({ - until: (n) => n === 0, + until: (v) => v === true, schedule: Schedule.spaced("100 millis"), }), TestClock.withLive, ); - assert.strictEqual(inMemoryAfterWake, 0); + assert.strictEqual(finalizerFired, true); const persistedAfterWake = yield* counter.GetPersistedState(); assert.strictEqual(persistedAfterWake.count, 11); @@ -175,14 +167,10 @@ layer(TestLayer)("end-to-end", (it) => { it.effect("persists state with a non-trivial schema (Date)", () => Effect.gen(function* () { - const counter = (yield* Counter.client).getOrCreate([ - "t-persist-state-date", - ]); - - // Bump the in-memory `Ref` so we can later assert that - // the wake actually rebuilt the actor (the ref should - // reset to 0 on each wake). - yield* counter.Increment({ amount: 7 }); + const key = "t-persist-state-date"; + const counter = (yield* Counter.client).getOrCreate([key]); + const flags = yield* Flags; + const flagName = `finalizer:${key}`; const when = new Date("2024-01-15T10:30:00.000Z"); const beforeSleep = yield* counter.PersistDateAndSleep({ @@ -190,20 +178,16 @@ layer(TestLayer)("end-to-end", (it) => { }); assert.strictEqual(beforeSleep.toISOString(), when.toISOString()); - // Engine-side sleep teardown is asynchronous. `count` - // is `Ref.make(0)` per wake, so seeing 0 is the deterministic - // signal that the prior wake torn down and a fresh one started. - // `TestClock.withLive` swaps in the real Clock for the duration - // of the poll so the schedule's interval and the timeout both - // elapse in wall time (the suite otherwise runs under TestClock). - const inMemoryAfterWake = yield* counter.GetCount().pipe( + const finalizerFired = yield* Effect.sync(() => + flags.get(flagName), + ).pipe( Effect.repeat({ - until: (n) => n === 0, + until: (v) => v === true, schedule: Schedule.spaced("100 millis"), }), TestClock.withLive, ); - assert.strictEqual(inMemoryAfterWake, 0); + assert.strictEqual(finalizerFired, true); const persistedAfterWake = yield* counter.GetPersistedState(); assert.strictEqual( @@ -215,14 +199,10 @@ layer(TestLayer)("end-to-end", (it) => { it.effect("persists state with a custom Schema.transform", () => Effect.gen(function* () { - const counter = (yield* Counter.client).getOrCreate([ - "t-persist-state-transform", - ]); - - // Bump the in-memory `Ref` so we can later assert that - // the wake actually rebuilt the actor (the ref should - // reset to 0 on each wake). - yield* counter.Increment({ amount: 7 }); + const key = "t-persist-state-transform"; + const counter = (yield* Counter.client).getOrCreate([key]); + const flags = yield* Flags; + const flagName = `finalizer:${key}`; const tags = ["alpha", "beta", "gamma"]; const beforeSleep = yield* counter.PersistTagsAndSleep({ @@ -230,20 +210,16 @@ layer(TestLayer)("end-to-end", (it) => { }); assert.deepEqual(beforeSleep, tags); - // Engine-side sleep teardown is asynchronous. `count` - // is `Ref.make(0)` per wake, so seeing 0 is the deterministic - // signal that the prior wake torn down and a fresh one started. - // `TestClock.withLive` swaps in the real Clock for the duration - // of the poll so the schedule's interval and the timeout both - // elapse in wall time (the suite otherwise runs under TestClock). - const inMemoryAfterWake = yield* counter.GetCount().pipe( + const finalizerFired = yield* Effect.sync(() => + flags.get(flagName), + ).pipe( Effect.repeat({ - until: (n) => n === 0, + until: (v) => v === true, schedule: Schedule.spaced("100 millis"), }), TestClock.withLive, ); - assert.strictEqual(inMemoryAfterWake, 0); + assert.strictEqual(finalizerFired, true); const persistedAfterWake = yield* counter.GetPersistedState(); assert.deepEqual(persistedAfterWake.tags, tags); @@ -252,14 +228,10 @@ layer(TestLayer)("end-to-end", (it) => { it.effect("persists state through a service-dependent transform", () => Effect.gen(function* () { - const counter = (yield* Counter.client).getOrCreate([ - "t-persist-state-scaled", - ]); - - // Bump the in-memory `Ref` so we can later assert that - // the wake actually rebuilt the actor (the ref should - // reset to 0 on each wake). - yield* counter.Increment({ amount: 7 }); + const key = "t-persist-state-scaled"; + const counter = (yield* Counter.client).getOrCreate([key]); + const flags = yield* Flags; + const flagName = `finalizer:${key}`; // 14 is the decoded (in-memory) value. With `factor: 2`, // the state schema's encode (write) divides 14 -> 7 and @@ -272,20 +244,16 @@ layer(TestLayer)("end-to-end", (it) => { }); assert.strictEqual(beforeSleep, 14); - // Engine-side sleep teardown is asynchronous. `count` - // is `Ref.make(0)` per wake, so seeing 0 is the deterministic - // signal that the prior wake torn down and a fresh one started. - // `TestClock.withLive` swaps in the real Clock for the duration - // of the poll so the schedule's interval and the timeout both - // elapse in wall time (the suite otherwise runs under TestClock). - const inMemoryAfterWake = yield* counter.GetCount().pipe( + const finalizerFired = yield* Effect.sync(() => + flags.get(flagName), + ).pipe( Effect.repeat({ - until: (n) => n === 0, + until: (v) => v === true, schedule: Schedule.spaced("100 millis"), }), TestClock.withLive, ); - assert.strictEqual(inMemoryAfterWake, 0); + assert.strictEqual(finalizerFired, true); const persistedAfterWake = yield* counter.GetPersistedState(); assert.strictEqual(persistedAfterWake.scaled, 14); @@ -770,26 +738,24 @@ layer(TestLayer)("end-to-end", (it) => { it.effect("persists db rows across a sleep/wake cycle", () => Effect.gen(function* () { - const counter = (yield* Counter.client).getOrCreate([ - "t-db-persist", - ]); + const key = "t-db-persist"; + const counter = (yield* Counter.client).getOrCreate([key]); yield* counter.LogEvent({ event: "before-sleep" }); - // `PersistAndSleep` signals `c.sleep()` after writing state; the - // engine tears the wake scope down asynchronously. The - // `in-memory Ref` resets to 0 on the next wake, so polling - // `GetCount` until it reads 0 is the deterministic signal that - // a fresh wake started. `TestClock.withLive` runs the poll in - // wall time since the suite otherwise drives `TestClock`. + const flags = yield* Flags; + const flagName = `finalizer:${key}`; + yield* counter.PersistAndSleep({ amount: 1 }); - const inMemoryAfterWake = yield* counter.GetCount().pipe( + const finalizerFired = yield* Effect.sync(() => + flags.get(flagName), + ).pipe( Effect.repeat({ - until: (n) => n === 0, + until: (v) => v === true, schedule: Schedule.spaced("100 millis"), }), TestClock.withLive, ); - assert.strictEqual(inMemoryAfterWake, 0); + assert.strictEqual(finalizerFired, true); yield* counter.LogEvent({ event: "after-wake" }); assert.deepStrictEqual(yield* counter.ListEvents(), [ From efa4cc642cf486cfff330ee5932daf0a248c9d7d Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 12:55:31 +0200 Subject: [PATCH 283/306] Handle actor-scope action interruptions as actor aborted --- .../packages/effect/src/Actor.ts | 203 ++++++++++-------- .../packages/effect/test/e2e.test.ts | 52 ++++- .../packages/effect/test/fixtures/actors.ts | 23 ++ 3 files changed, 187 insertions(+), 91 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 9f23eb3063..4905ea30f7 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -3,6 +3,7 @@ import { Context, Effect, Exit, + type Fiber, FiberSet, identity, Layer, @@ -498,9 +499,10 @@ const makeRivetkitActor = Effect.fnUntraced(function* < string, { readonly actionHandlers: ActionHandlers; - readonly runFork: ( - effect: Effect.Effect, - ) => unknown; + readonly runFork: ( + effect: Effect.Effect, + options?: Effect.RunOptions, + ) => Fiber.Fiber; readonly scope: Scope.Closeable; readonly state?: State.State< StateOptions.Decoded, @@ -566,7 +568,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ); const runFork = yield* FiberSet.makeRuntime< any, - void, + unknown, unknown >().pipe(Effect.provide(Context.merge(services, context))); @@ -608,98 +610,114 @@ const makeRivetkitActor = Effect.fnUntraced(function* < const rpcMethod = `${actor.name}/${action._tag}`; const traceMeta = readTraceMeta(meta); - const exit = await Effect.runPromiseExitWith(services)( - Effect.gen(function* () { - const instance = yield* MutableHashMap.get( - instances, - c.actorId, - ).pipe(Effect.fromOption, Effect.orDie); - // The handler map is keyed by the same action - // definitions being registered here, but - // TypeScript loses that relationship once the - // actions are widened into the RivetKit actions - // record. - const actionHandler = instance.actionHandlers[ - action._tag as keyof ActionHandlers - ] as ( - envelope: ActionRequest, - ) => Action.ResultFrom; - // Raw RivetKit clients call no-argument actions with an - // absent first argument. The Effect JSON Void codec expects - // null, so adapt only actions that declared no payload. - const payloadForDecode = - !action.hasPayload && payload === undefined - ? null - : payload; - const decodedPayload = yield* decodePayload( - payloadForDecode, - ).pipe(Effect.orDie); - // The payload was decoded with this action's schema, - // so this is the runtime boundary that restores the - // typed envelope expected by the user handler. - const actionRequest = { - _tag: action._tag, - action, - payload: decodedPayload, - } as ActionRequest; - - const resultExit = yield* Effect.exit( - actionHandler(actionRequest), + const instance = MutableHashMap.get(instances, c.actorId).pipe( + Option.getOrUndefined, + ); + if (!instance) { + if (c.abortSignal.aborted) throw makeActorAbortedError(); + throw new Error("actor instance missing"); + } + + const actionEffect = Effect.gen(function* () { + // The handler map is keyed by the same action + // definitions being registered here, but + // TypeScript loses that relationship once the + // actions are widened into the RivetKit actions + // record. + const actionHandler = instance.actionHandlers[ + action._tag as keyof ActionHandlers + ] as ( + envelope: ActionRequest, + ) => Action.ResultFrom; + // Raw RivetKit clients call no-argument actions with an + // absent first argument. The Effect JSON Void codec expects + // null, so adapt only actions that declared no payload. + const payloadForDecode = + !action.hasPayload && payload === undefined + ? null + : payload; + const decodedPayload = yield* decodePayload( + payloadForDecode, + ).pipe(Effect.orDie); + // The payload was decoded with this action's schema, + // so this is the runtime boundary that restores the + // typed envelope expected by the user handler. + const actionRequest = { + _tag: action._tag, + action, + payload: decodedPayload, + } as ActionRequest; + + const resultExit = yield* Effect.exit( + actionHandler(actionRequest), + ); + + if (Exit.isSuccess(resultExit)) { + return yield* encodeSuccess(resultExit.value).pipe( + Effect.orDie, ); + } - if (Exit.isSuccess(resultExit)) { - return yield* encodeSuccess(resultExit.value).pipe( - Effect.orDie, - ); - } - - const expectedError = Exit.findErrorOption(resultExit); - - if (Option.isSome(expectedError)) { - const encodedError = yield* encodeError( - expectedError.value, - ).pipe(Effect.orDie); - - return yield* Effect.fail( - new Rivetkit.UserError( - hasStringProperty("message")(encodedError) - ? encodedError.message - : `${action._tag} failed`, - { - code: hasStringProperty("_tag")( - encodedError, - ) - ? encodedError._tag - : undefined, - metadata: - ActionErrorEnvelope.make( - encodedError, - ), - }, - ), - ); - } + const expectedError = Exit.findErrorOption(resultExit); + + if (Option.isSome(expectedError)) { + const encodedError = yield* encodeError( + expectedError.value, + ).pipe(Effect.orDie); - // Defect / interruption. Do not encode these as action errors. - // Let them escape, so Rivetkit maps them to its internal_error shape. - return yield* Effect.die( - Cause.squash(resultExit.cause), + return yield* Effect.fail( + new Rivetkit.UserError( + hasStringProperty("message")(encodedError) + ? encodedError.message + : `${action._tag} failed`, + { + code: hasStringProperty("_tag")( + encodedError, + ) + ? encodedError._tag + : undefined, + metadata: + ActionErrorEnvelope.make(encodedError), + }, + ), ); - }).pipe( - Effect.withSpan(rpcMethod, { - parent: traceMeta - ? Tracer.externalSpan(traceMeta) - : undefined, - kind: "server", - attributes: { - "rpc.system.name": rpcSystem, - "rpc.method": rpcMethod, - }, - }), - ), + } + + return yield* Effect.failCause(resultExit.cause); + }).pipe( + Effect.withSpan(rpcMethod, { + parent: traceMeta + ? Tracer.externalSpan(traceMeta) + : undefined, + kind: "server", + attributes: { + "rpc.system.name": rpcSystem, + "rpc.method": rpcMethod, + }, + }), ); - + const fiber = instance.runFork(Effect.exit(actionEffect), { + signal: c.abortSignal, + }); + const fiberExit = await new Promise< + Exit.Exit> + >((resolve) => fiber.addObserver(resolve)); + + if (Exit.isFailure(fiberExit)) { + if (Cause.hasInterruptsOnly(fiberExit.cause)) { + throw makeActorAbortedError(); + } + throw Cause.squash(fiberExit.cause); + } + const exit = fiberExit.value; if (Exit.isSuccess(exit)) return exit.value; + // Action fibers can be interrupted by a caller abort signal + // or by the actor instance scope closing during sleep, destroy, + // or shutdown. Surface those lifecycle exits as RivetKit's + // structured action-aborted error instead of an internal error. + if (Cause.hasInterruptsOnly(exit.cause)) { + throw makeActorAbortedError(); + } throw Cause.squash(exit.cause); }, ]; @@ -789,3 +807,8 @@ const makeRivetkitActor = Effect.fnUntraced(function* < onDestroy, }); }); + +const makeActorAbortedError = () => + new Rivetkit.RivetError("actor", "aborted", "Actor aborted", { + public: true, + }); diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index cd0537cd7f..f60e9ef3f4 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -1,6 +1,6 @@ import { assert, layer } from "@effect/vitest"; import { Registry, RivetError } from "@rivetkit/effect"; -import { DateTime, Effect, Layer, Schedule } from "effect"; +import { DateTime, Effect, Fiber, Layer, Schedule } from "effect"; import { TestClock } from "effect/testing"; import { createClient } from "rivetkit/client"; import { inject } from "vitest"; @@ -337,6 +337,56 @@ layer(TestLayer)("end-to-end", (it) => { }), ); + it.effect("interrupts action effects when actor scope closes", () => + Effect.gen(function* () { + const key = "t-action-scope-close"; + const flags = yield* Flags; + const counter = (yield* Counter.client).getOrCreate([key]); + const actionFiber = yield* counter.SleepDuringAction().pipe( + Effect.flip, + Effect.forkChild({ startImmediately: true }), + ); + + const started = yield* Effect.sync(() => + flags.get(`sleep-during-action-started:${key}`), + ).pipe( + Effect.repeat({ + until: (v) => v === true, + schedule: Schedule.spaced("100 millis"), + }), + TestClock.withLive, + ); + assert.strictEqual(started, true); + + const finalizerFired = yield* Effect.sync(() => + flags.get(`finalizer:${key}`), + ).pipe( + Effect.repeat({ + until: (v) => v === true, + schedule: Schedule.spaced("100 millis"), + }), + TestClock.withLive, + ); + assert.strictEqual(finalizerFired, true); + + const interrupted = yield* Effect.sync(() => + flags.get(`sleep-during-action-interrupted:${key}`), + ).pipe( + Effect.repeat({ + until: (v) => v === true, + schedule: Schedule.spaced("100 millis"), + }), + TestClock.withLive, + ); + assert.strictEqual(interrupted, true); + + const error = yield* Fiber.join(actionFiber); + assert.instanceOf(error, RivetError.RivetError); + assert.strictEqual(error.group, "actor"); + assert.strictEqual(error.code, "aborted"); + }), + ); + it.effect("round-trips a non-trivial schema (Date)", () => Effect.gen(function* () { const counter = (yield* Counter.client).getOrCreate(["t-date"]); diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts index d24f2836fd..6e4f37795d 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts @@ -181,6 +181,10 @@ export const CountEvents = Action.make("CountEvents", { success: Schema.Number, }); +export const SleepDuringAction = Action.make("SleepDuringAction", { + success: Schema.String, +}); + const EncodedTransformedState = Schema.Struct({ when: Schema.String, instant: Schema.String, @@ -302,6 +306,7 @@ export const Counter = Actor.make("Counter", { LogEvent, ListEvents, CountEvents, + SleepDuringAction, ], }); @@ -462,6 +467,24 @@ export const CounterLive = Counter.toLayer( ); return rows[0]?.count ?? 0; }).pipe(Effect.orDie), + SleepDuringAction: () => + Effect.gen(function* () { + const key = address.key.join("/"); + yield* Effect.sync(() => { + flags.set(`sleep-during-action-started:${key}`, true); + }); + yield* sleep; + return yield* Effect.never.pipe( + Effect.onInterrupt(() => + Effect.sync(() => { + flags.set( + `sleep-during-action-interrupted:${key}`, + true, + ); + }), + ), + ); + }), }); }), { From a0b08e7212b58d9774c1a7f4ce0451091609dfee Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 13:15:41 +0200 Subject: [PATCH 284/306] fix(effect): simplify action fiber exit handling --- rivetkit-typescript/packages/effect/src/Actor.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 4905ea30f7..17b89c6673 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -696,20 +696,13 @@ const makeRivetkitActor = Effect.fnUntraced(function* < }, }), ); - const fiber = instance.runFork(Effect.exit(actionEffect), { + const fiber = instance.runFork(actionEffect, { signal: c.abortSignal, }); - const fiberExit = await new Promise< - Exit.Exit> - >((resolve) => fiber.addObserver(resolve)); + const exit = await new Promise>( + (resolve) => fiber.addObserver(resolve), + ); - if (Exit.isFailure(fiberExit)) { - if (Cause.hasInterruptsOnly(fiberExit.cause)) { - throw makeActorAbortedError(); - } - throw Cause.squash(fiberExit.cause); - } - const exit = fiberExit.value; if (Exit.isSuccess(exit)) return exit.value; // Action fibers can be interrupted by a caller abort signal // or by the actor instance scope closing during sleep, destroy, From 695cedd5348d8b923b58864f95418502f36a672f Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 14:44:00 +0200 Subject: [PATCH 285/306] chore(effect): remove test typecheck dependency --- rivetkit-typescript/packages/effect/turbo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/turbo.json b/rivetkit-typescript/packages/effect/turbo.json index 435a537a49..e053d2d9a1 100644 --- a/rivetkit-typescript/packages/effect/turbo.json +++ b/rivetkit-typescript/packages/effect/turbo.json @@ -40,7 +40,7 @@ ] }, "test": { - "dependsOn": ["^build", "check-types"], + "dependsOn": ["^build"], "inputs": [ "package.json", "src/**", From 577cae1731f9de85dfd80b6f79627e81661b24a5 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 15:48:55 +0200 Subject: [PATCH 286/306] refactor(effect): extract state runtime --- .../packages/effect/src/Actor.ts | 124 +++++------------- .../effect/src/internal/StateRuntime.ts | 88 +++++++++++++ 2 files changed, 121 insertions(+), 91 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/src/internal/StateRuntime.ts diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 17b89c6673..38a2d8cc16 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -3,7 +3,6 @@ import { Context, Effect, Exit, - type Fiber, FiberSet, identity, Layer, @@ -13,22 +12,21 @@ import { Record, Schema, Scope, - Semaphore, Struct, Tracer, - UndefinedOr, } from "effect"; import * as Rivetkit from "rivetkit"; import type * as RivetkitDb from "rivetkit/db"; import type * as Action from "./Action.ts"; import * as Client from "./Client.ts"; import * as ActionErrorEnvelope from "./internal/ActionErrorEnvelope.ts"; +import * as StateRuntime from "./internal/StateRuntime.ts"; import type * as StateOptions from "./internal/StateOptions.ts"; import { readTraceMeta, rpcSystem } from "./internal/tracing.ts"; import { hasStringProperty } from "./internal/utils.ts"; import * as Registry from "./Registry.ts"; import type * as RivetError from "./RivetError.ts"; -import * as State from "./State.ts"; +import type * as State from "./State.ts"; const TypeId = "~@rivetkit/effect/Actor"; @@ -463,6 +461,14 @@ export function toWakeHandler< }; } +type ActorInstance< + ActionHandlers, + StateDefinition extends StateOptions.Any, +> = StateRuntime.Instance & { + readonly actionHandlers: ActionHandlers; + readonly scope: Scope.Closeable; +}; + const makeRivetkitActor = Effect.fnUntraced(function* < Name extends string, Actions extends Action.AnyWithProps, @@ -488,27 +494,14 @@ const makeRivetkitActor = Effect.fnUntraced(function* < const services = yield* Effect.context(); const { effectOptions, rivetkitOptions } = splitOptions(options); - const stateCodec = UndefinedOr.map(effectOptions.state, (state) => ({ - decodeUnknown: Schema.decodeUnknownEffect( - Schema.toCodecJson(state.schema), - ), - encode: Schema.encodeEffect(Schema.toCodecJson(state.schema)), - })); + const stateRuntime = + effectOptions.state === undefined + ? undefined + : yield* StateRuntime.make(effectOptions.state); const instances = MutableHashMap.empty< string, - { - readonly actionHandlers: ActionHandlers; - readonly runFork: ( - effect: Effect.Effect, - options?: Effect.RunOptions, - ) => Fiber.Fiber; - readonly scope: Scope.Closeable; - readonly state?: State.State< - StateOptions.Decoded, - Schema.SchemaError - >; - } + ActorInstance >(); type RivetkitDefinition = RivetkitActorDefinitionFor; @@ -517,34 +510,8 @@ const makeRivetkitActor = Effect.fnUntraced(function* < await Effect.runPromiseWith(services)( Effect.gen(function* () { const scope = yield* Scope.make(); - - const state = stateCodec - ? // `c.state` IS the state — `State` is just a typed - // view + change stream over it. Effect-typed - // read/write so async schema transforms work, - // and `SchemaError` flows through `State.get` / - // `set` / `update` to action handlers. The - // wake-time initial read still dies if persisted - // state can't be decoded — no caller exists yet - // to handle it. `Schema.Top`'s requirements show - // up as `unknown`; the captured `services` - // context satisfies them at runtime, so we erase - // R at the boundary. - ((yield* State.make( - () => stateCodec.decodeUnknown(c.state), - (next) => - stateCodec.encode(next).pipe( - Effect.tap((encoded) => - Effect.sync(() => { - c.state = encoded; - }), - ), - Effect.asVoid, - ), - ).pipe(Effect.orDie)) as State.State< - StateOptions.Decoded, - Schema.SchemaError - >) + const state = stateRuntime + ? yield* stateRuntime.makeStateView(c) : undefined; const context = Context.mergeAll( @@ -716,35 +683,21 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ]; }); - const onStateChange = ( - c: Rivetkit.WakeContextOf, - newState: unknown, - ) => { - const instance = MutableHashMap.get(instances, c.actorId).pipe( - Option.getOrUndefined, - ); - // Late state-change callbacks can arrive after teardown removed the - // instance. There is no live Effect state stream left to update. - if (!stateCodec || !instance) return; - - instance.runFork( - Effect.gen(function* () { - const state = yield* Effect.fromNullishOr(instance.state).pipe( - Effect.orDie, + const onStateChange = stateRuntime + ? ( + c: Rivetkit.WakeContextOf, + newState: unknown, + ) => { + const instance = MutableHashMap.get(instances, c.actorId).pipe( + Option.getOrUndefined, ); + // Late state-change callbacks can arrive after teardown removed the + // instance. There is no live Effect state stream left to update. + if (!instance) return; - yield* Semaphore.withPermit( - state.semaphore, - Effect.gen(function* () { - const decoded = yield* stateCodec - .decodeUnknown(newState) - .pipe(Effect.orDie); - State.publishUnsafe(state, decoded); - }), - ); - }), - ); - }; + stateRuntime.publishChange(instance, newState); + } + : undefined; const cleanupInstance = Effect.fnUntraced(function* (actorId: string) { const instance = MutableHashMap.get(instances, actorId); @@ -780,22 +733,11 @@ const makeRivetkitActor = Effect.fnUntraced(function* < options: rivetkitOptions, ...(effectOptions.db ? { db: effectOptions.db } : {}), onWake, - ...(options.state - ? { - createState: () => - Effect.runPromiseWith(services)( - UndefinedOr.getOrThrow(stateCodec) - .encode( - UndefinedOr.getOrThrow( - options.state, - ).initialValue(), - ) - .pipe(Effect.orDie), - ), - } + ...(stateRuntime + ? { createState: stateRuntime.createInitialState } : {}), actions, - onStateChange, + ...(onStateChange ? { onStateChange } : {}), onSleep, onDestroy, }); diff --git a/rivetkit-typescript/packages/effect/src/internal/StateRuntime.ts b/rivetkit-typescript/packages/effect/src/internal/StateRuntime.ts new file mode 100644 index 0000000000..708bc92c71 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/internal/StateRuntime.ts @@ -0,0 +1,88 @@ +import { Effect, type Fiber, Schema, Semaphore } from "effect"; +import * as State from "../State.ts"; +import type * as StateOptions from "./StateOptions.ts"; + +export type ActorState = State.State< + StateOptions.Decoded, + Schema.SchemaError +>; + +export type Instance = { + readonly runFork: ( + effect: Effect.Effect, + options?: Effect.RunOptions, + ) => Fiber.Fiber; + readonly state?: ActorState; +}; + +export type Runtime = { + readonly makeStateView: ( + c: { state: StateOptions.Encoded }, + ) => Effect.Effect, never, any>; + readonly createInitialState: () => Promise< + StateOptions.Encoded + >; + readonly publishChange: ( + instance: Instance, + newState: unknown, + ) => void; +}; + +export const make = Effect.fnUntraced(function* < + StateDefinition extends StateOptions.Any, +>( + stateOptions: StateDefinition, +): Effect.fn.Return, never, any> { + const services = yield* Effect.context(); + + const stateCodec = { + decodeUnknown: Schema.decodeUnknownEffect( + Schema.toCodecJson(stateOptions.schema), + ), + encode: Schema.encodeEffect(Schema.toCodecJson(stateOptions.schema)), + }; + + return { + makeStateView: (c) => + State.make( + () => stateCodec.decodeUnknown(c.state), + (next) => + stateCodec.encode(next).pipe( + Effect.tap((encoded) => + Effect.sync(() => { + c.state = encoded; + }), + ), + Effect.asVoid, + ), + ).pipe( + Effect.orDie, + Effect.map((state) => state as ActorState), + ), + createInitialState: () => + Effect.runPromiseWith(services)( + stateCodec.encode(stateOptions.initialValue()).pipe( + Effect.orDie, + ), + ), + publishChange: (instance, newState) => { + instance.runFork( + Effect.gen(function* () { + const state = yield* Effect.fromNullishOr( + instance.state, + ).pipe(Effect.orDie); + + yield* Semaphore.withPermit( + state.semaphore, + Effect.gen(function* () { + const decoded = yield* stateCodec + .decodeUnknown(newState) + .pipe(Effect.orDie); + State.publishUnsafe(state, decoded); + }), + ); + }), + ); + }, + }; +}); From 3deff33170df7ca2841b880511cffcfe2d348789 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 15:51:07 +0200 Subject: [PATCH 287/306] refactor(effect): use map for actor instances --- .../packages/effect/src/Actor.ts | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 38a2d8cc16..4876896956 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -6,7 +6,6 @@ import { FiberSet, identity, Layer, - MutableHashMap, Option, Predicate, Record, @@ -499,10 +498,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ? undefined : yield* StateRuntime.make(effectOptions.state); - const instances = MutableHashMap.empty< - string, - ActorInstance - >(); + const instances = new Map>(); type RivetkitDefinition = RivetkitActorDefinitionFor; @@ -540,7 +536,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < >().pipe(Effect.provide(Context.merge(services, context))); yield* Effect.sync(() => - MutableHashMap.set(instances, c.actorId, { + instances.set(c.actorId, { actionHandlers, runFork, scope, @@ -577,9 +573,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < const rpcMethod = `${actor.name}/${action._tag}`; const traceMeta = readTraceMeta(meta); - const instance = MutableHashMap.get(instances, c.actorId).pipe( - Option.getOrUndefined, - ); + const instance = instances.get(c.actorId); if (!instance) { if (c.abortSignal.aborted) throw makeActorAbortedError(); throw new Error("actor instance missing"); @@ -688,9 +682,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < c: Rivetkit.WakeContextOf, newState: unknown, ) => { - const instance = MutableHashMap.get(instances, c.actorId).pipe( - Option.getOrUndefined, - ); + const instance = instances.get(c.actorId); // Late state-change callbacks can arrive after teardown removed the // instance. There is no live Effect state stream left to update. if (!instance) return; @@ -700,13 +692,13 @@ const makeRivetkitActor = Effect.fnUntraced(function* < : undefined; const cleanupInstance = Effect.fnUntraced(function* (actorId: string) { - const instance = MutableHashMap.get(instances, actorId); + const instance = instances.get(actorId); // Actor teardown can be reported more than once across sleep // and destroy paths. Treat missing entries as already cleaned up. - if (Option.isNone(instance)) return; + if (!instance) return; - MutableHashMap.remove(instances, actorId); - yield* Scope.close(instance.value.scope, Exit.void); + instances.delete(actorId); + yield* Scope.close(instance.scope, Exit.void); }); const onSleep = async (c: Rivetkit.SleepContextOf) => { From d6d8f93478245b6c8e86bcce6506d6297ec6b5a0 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 15:52:58 +0200 Subject: [PATCH 288/306] fix(effect): default actor actions to empty array --- rivetkit-typescript/packages/effect/src/Actor.test.ts | 6 ++++++ rivetkit-typescript/packages/effect/src/Actor.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.test.ts b/rivetkit-typescript/packages/effect/src/Actor.test.ts index a911d3ed3e..28f984122e 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test.ts @@ -9,6 +9,12 @@ class Prefix extends Context.Service()( const PrefixLive = Layer.succeed(Prefix, Prefix.of({ value: "svc" })); describe("Actor.toWakeHandler", () => { + it("defaults actions to an empty array", () => { + const actor = Actor.make("NoActions"); + + assert.deepStrictEqual(actor.actions, []); + }); + it.effect("wraps a plain action handler object", () => Effect.gen(function* () { const wake = { Ping: () => Effect.succeed("pong") }; diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 4876896956..1c30745ca8 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -375,7 +375,7 @@ export const make = < ): Actor => { const self = Object.create(Proto); self.name = name; - self.actions = options?.actions; + self.actions = options?.actions ?? []; return self; }; From 0a0ecb75b9769593c5714bf862393905fd5741fa Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 15:59:31 +0200 Subject: [PATCH 289/306] refactor(effect): extract action dispatcher --- .../packages/effect/src/Actor.ts | 152 ++------------- .../effect/src/internal/ActionDispatcher.ts | 178 ++++++++++++++++++ 2 files changed, 189 insertions(+), 141 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 1c30745ca8..d56013f71b 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -1,28 +1,23 @@ import { - Cause, Context, Effect, Exit, FiberSet, identity, Layer, - Option, Predicate, Record, Schema, Scope, Struct, - Tracer, } from "effect"; import * as Rivetkit from "rivetkit"; import type * as RivetkitDb from "rivetkit/db"; import type * as Action from "./Action.ts"; import * as Client from "./Client.ts"; -import * as ActionErrorEnvelope from "./internal/ActionErrorEnvelope.ts"; +import * as ActionDispatcher from "./internal/ActionDispatcher.ts"; import * as StateRuntime from "./internal/StateRuntime.ts"; import type * as StateOptions from "./internal/StateOptions.ts"; -import { readTraceMeta, rpcSystem } from "./internal/tracing.ts"; -import { hasStringProperty } from "./internal/utils.ts"; import * as Registry from "./Registry.ts"; import type * as RivetError from "./RivetError.ts"; import type * as State from "./State.ts"; @@ -463,8 +458,8 @@ export function toWakeHandler< type ActorInstance< ActionHandlers, StateDefinition extends StateOptions.Any, -> = StateRuntime.Instance & { - readonly actionHandlers: ActionHandlers; +> = ActionDispatcher.Instance & + StateRuntime.Instance & { readonly scope: Scope.Closeable; }; @@ -547,134 +542,14 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ); }; - const actions = Record.fromIterableWith(actor.actions, (action) => { - const decodePayload = Schema.decodeUnknownEffect( - Schema.toCodecJson(action.payloadSchema), - ); - const encodeSuccess = Schema.encodeEffect( - Schema.toCodecJson(action.successSchema), - ); - const encodeError = Schema.encodeEffect( - Schema.toCodecJson(action.errorSchema), - ); - - return [ - action._tag, - async ( - c: Rivetkit.ActionContextOf, - payload: Action.Payload, - meta?: Client.ActionMeta, // TODO: Find better type - ) => { - // Always wrap in a server-side span so the handler has a - // live `currentSpan` even when the caller didn't ship trace - // context (e.g., a non-Effect-SDK client). When trace context - // is present, reattach it as the parent so the server span - // joins the caller's trace. - const rpcMethod = `${actor.name}/${action._tag}`; - const traceMeta = readTraceMeta(meta); - - const instance = instances.get(c.actorId); - if (!instance) { - if (c.abortSignal.aborted) throw makeActorAbortedError(); - throw new Error("actor instance missing"); - } - - const actionEffect = Effect.gen(function* () { - // The handler map is keyed by the same action - // definitions being registered here, but - // TypeScript loses that relationship once the - // actions are widened into the RivetKit actions - // record. - const actionHandler = instance.actionHandlers[ - action._tag as keyof ActionHandlers - ] as ( - envelope: ActionRequest, - ) => Action.ResultFrom; - // Raw RivetKit clients call no-argument actions with an - // absent first argument. The Effect JSON Void codec expects - // null, so adapt only actions that declared no payload. - const payloadForDecode = - !action.hasPayload && payload === undefined - ? null - : payload; - const decodedPayload = yield* decodePayload( - payloadForDecode, - ).pipe(Effect.orDie); - // The payload was decoded with this action's schema, - // so this is the runtime boundary that restores the - // typed envelope expected by the user handler. - const actionRequest = { - _tag: action._tag, - action, - payload: decodedPayload, - } as ActionRequest; - - const resultExit = yield* Effect.exit( - actionHandler(actionRequest), - ); - - if (Exit.isSuccess(resultExit)) { - return yield* encodeSuccess(resultExit.value).pipe( - Effect.orDie, - ); - } - - const expectedError = Exit.findErrorOption(resultExit); - - if (Option.isSome(expectedError)) { - const encodedError = yield* encodeError( - expectedError.value, - ).pipe(Effect.orDie); - - return yield* Effect.fail( - new Rivetkit.UserError( - hasStringProperty("message")(encodedError) - ? encodedError.message - : `${action._tag} failed`, - { - code: hasStringProperty("_tag")( - encodedError, - ) - ? encodedError._tag - : undefined, - metadata: - ActionErrorEnvelope.make(encodedError), - }, - ), - ); - } - - return yield* Effect.failCause(resultExit.cause); - }).pipe( - Effect.withSpan(rpcMethod, { - parent: traceMeta - ? Tracer.externalSpan(traceMeta) - : undefined, - kind: "server", - attributes: { - "rpc.system.name": rpcSystem, - "rpc.method": rpcMethod, - }, - }), - ); - const fiber = instance.runFork(actionEffect, { - signal: c.abortSignal, - }); - const exit = await new Promise>( - (resolve) => fiber.addObserver(resolve), - ); - - if (Exit.isSuccess(exit)) return exit.value; - // Action fibers can be interrupted by a caller abort signal - // or by the actor instance scope closing during sleep, destroy, - // or shutdown. Surface those lifecycle exits as RivetKit's - // structured action-aborted error instead of an internal error. - if (Cause.hasInterruptsOnly(exit.cause)) { - throw makeActorAbortedError(); - } - throw Cause.squash(exit.cause); - }, - ]; + const actions = ActionDispatcher.make< + Name, + Actions, + ActionHandlers, + RivetkitDefinition + >({ + actor, + getInstance: (actorId) => instances.get(actorId), }); const onStateChange = stateRuntime @@ -734,8 +609,3 @@ const makeRivetkitActor = Effect.fnUntraced(function* < onDestroy, }); }); - -const makeActorAbortedError = () => - new Rivetkit.RivetError("actor", "aborted", "Actor aborted", { - public: true, - }); diff --git a/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts b/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts new file mode 100644 index 0000000000..a4d0c50139 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts @@ -0,0 +1,178 @@ +import { + Cause, + Effect, + Exit, + type Fiber, + Option, + Record, + Schema, + Tracer, +} from "effect"; +import * as Rivetkit from "rivetkit"; +import type * as Action from "../Action.ts"; +import type { + ActionHandlersFrom, + ActionRequest, + Actor, +} from "../Actor.ts"; +import type * as Client from "../Client.ts"; +import * as ActionErrorEnvelope from "./ActionErrorEnvelope.ts"; +import { readTraceMeta, rpcSystem } from "./tracing.ts"; +import { hasStringProperty } from "./utils.ts"; + +export type Instance = { + readonly actionHandlers: ActionHandlers; + readonly runFork: ( + effect: Effect.Effect, + options?: Effect.RunOptions, + ) => Fiber.Fiber; +}; + +export const make = < + Name extends string, + Actions extends Action.AnyWithProps, + ActionHandlers extends ActionHandlersFrom, + ActorDefinition extends Rivetkit.AnyActorDefinition, +>({ + actor, + getInstance, +}: { + readonly actor: Actor; + readonly getInstance: ( + actorId: string, + ) => Instance | undefined; +}) => + Record.fromIterableWith(actor.actions, (action) => { + const decodePayload = Schema.decodeUnknownEffect( + Schema.toCodecJson(action.payloadSchema), + ); + const encodeSuccess = Schema.encodeEffect( + Schema.toCodecJson(action.successSchema), + ); + const encodeError = Schema.encodeEffect( + Schema.toCodecJson(action.errorSchema), + ); + + return [ + action._tag, + async ( + c: Rivetkit.ActionContextOf, + payload: Action.Payload, + meta?: Client.ActionMeta, // TODO: Find better type + ) => { + // Always wrap in a server-side span so the handler has a + // live `currentSpan` even when the caller didn't ship trace + // context (e.g., a non-Effect-SDK client). When trace context + // is present, reattach it as the parent so the server span + // joins the caller's trace. + const rpcMethod = `${actor.name}/${action._tag}`; + const traceMeta = readTraceMeta(meta); + + const instance = getInstance(c.actorId); + if (!instance) { + if (c.abortSignal.aborted) throw makeActorAbortedError(); + throw new Error("actor instance missing"); + } + + const actionEffect = Effect.gen(function* () { + // The handler map is keyed by the same action + // definitions being registered here, but + // TypeScript loses that relationship once the + // actions are widened into the RivetKit actions + // record. + const actionHandler = instance.actionHandlers[ + action._tag as keyof ActionHandlers + ] as ( + envelope: ActionRequest, + ) => Action.ResultFrom; + // Raw RivetKit clients call no-argument actions with an + // absent first argument. The Effect JSON Void codec expects + // null, so adapt only actions that declared no payload. + const payloadForDecode = + !action.hasPayload && payload === undefined + ? null + : payload; + const decodedPayload = yield* decodePayload( + payloadForDecode, + ).pipe(Effect.orDie); + // The payload was decoded with this action's schema, + // so this is the runtime boundary that restores the + // typed envelope expected by the user handler. + const actionRequest = { + _tag: action._tag, + action, + payload: decodedPayload, + } as ActionRequest; + + const resultExit = yield* Effect.exit( + actionHandler(actionRequest), + ); + + if (Exit.isSuccess(resultExit)) { + return yield* encodeSuccess(resultExit.value).pipe( + Effect.orDie, + ); + } + + const expectedError = Exit.findErrorOption(resultExit); + + if (Option.isSome(expectedError)) { + const encodedError = yield* encodeError( + expectedError.value, + ).pipe(Effect.orDie); + + return yield* Effect.fail( + new Rivetkit.UserError( + hasStringProperty("message")(encodedError) + ? encodedError.message + : `${action._tag} failed`, + { + code: hasStringProperty("_tag")( + encodedError, + ) + ? encodedError._tag + : undefined, + metadata: + ActionErrorEnvelope.make(encodedError), + }, + ), + ); + } + + return yield* Effect.failCause(resultExit.cause); + }).pipe( + Effect.withSpan(rpcMethod, { + parent: traceMeta + ? Tracer.externalSpan(traceMeta) + : undefined, + kind: "server", + attributes: { + "rpc.system.name": rpcSystem, + "rpc.method": rpcMethod, + }, + }), + ); + const fiber = instance.runFork(actionEffect, { + signal: c.abortSignal, + }); + const exit = await new Promise>( + (resolve) => fiber.addObserver(resolve), + ); + + if (Exit.isSuccess(exit)) return exit.value; + // Action fibers can be interrupted by a caller abort signal + // or by the actor instance scope closing during sleep, destroy, + // or shutdown. Surface those lifecycle exits as RivetKit's + // structured action-aborted error instead of an internal error. + if (Cause.hasInterruptsOnly(exit.cause)) { + throw makeActorAbortedError(); + } + throw Cause.squash(exit.cause); + }, + ]; + }); + +const makeActorAbortedError = () => + new Rivetkit.RivetError("actor", "aborted", "Actor aborted", { + public: true, + }); From 31db65602b131e683759d5639b48208a32b84088 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 16:05:52 +0200 Subject: [PATCH 290/306] refactor(effect): extract wake instance creation --- .../packages/effect/src/Actor.ts | 86 ++++++++++--------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index d56013f71b..e68a63890f 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -497,48 +497,56 @@ const makeRivetkitActor = Effect.fnUntraced(function* < type RivetkitDefinition = RivetkitActorDefinitionFor; + const makeInstance = Effect.fnUntraced(function* ( + c: Rivetkit.WakeContextOf, + ): Effect.fn.Return, never, any> { + const scope = yield* Scope.make(); + const state = stateRuntime + ? yield* stateRuntime.makeStateView(c) + : undefined; + + const context = Context.mergeAll( + Context.make(CurrentAddress, { + actorId: c.actorId, + name: c.name, + key: c.key, + }), + Context.make(Scope.Scope, scope), + Context.make( + Sleep, + Effect.sync(() => c.sleep()), + ), + ); + const wakeOptions = { + rawRivetkitContext: c, + ...(state ? { state } : {}), + } as WakeOptionsFor; + const actionHandlers = yield* wakeHandler(wakeOptions).pipe( + Effect.provide(context), + ); + const runFork = yield* FiberSet.makeRuntime< + any, + unknown, + unknown + >().pipe(Effect.provide(Context.merge(services, context))); + + return { + actionHandlers, + runFork, + scope, + state, + }; + }); + const onWake = async (c: Rivetkit.WakeContextOf) => { await Effect.runPromiseWith(services)( - Effect.gen(function* () { - const scope = yield* Scope.make(); - const state = stateRuntime - ? yield* stateRuntime.makeStateView(c) - : undefined; - - const context = Context.mergeAll( - Context.make(CurrentAddress, { - actorId: c.actorId, - name: c.name, - key: c.key, + makeInstance(c).pipe( + Effect.tap((instance) => + Effect.sync(() => { + instances.set(c.actorId, instance); }), - Context.make(Scope.Scope, scope), - Context.make( - Sleep, - Effect.sync(() => c.sleep()), - ), - ); - const wakeOptions = { - rawRivetkitContext: c, - ...(state ? { state } : {}), - } as WakeOptionsFor; - const actionHandlers = yield* wakeHandler(wakeOptions).pipe( - Effect.provide(context), - ); - const runFork = yield* FiberSet.makeRuntime< - any, - unknown, - unknown - >().pipe(Effect.provide(Context.merge(services, context))); - - yield* Effect.sync(() => - instances.set(c.actorId, { - actionHandlers, - runFork, - scope, - state, - }), - ); - }), + ), + ), ); }; From d2cabc72dea61f74b6d277d756eb12ec26d56ba6 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 16:31:08 +0200 Subject: [PATCH 291/306] refactor(effect): remove deferred action results --- rivetkit-typescript/packages/effect/src/Action.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts index 447cdbf81b..514a296d6a 100644 --- a/rivetkit-typescript/packages/effect/src/Action.ts +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -1,4 +1,4 @@ -import { type Deferred, type Effect, Predicate, Schema } from "effect"; +import { type Effect, Predicate, Schema } from "effect"; const TypeId = "~@rivetkit/effect/Action"; @@ -144,12 +144,7 @@ export type ResultFrom = R extends Action< infer _Success, infer _Error > - ? Effect.Effect< - | _Success["Type"] - | Deferred.Deferred<_Success["Type"], _Error["Type"]>, - _Error["Type"], - Services - > + ? Effect.Effect<_Success["Type"], _Error["Type"], Services> : never; // --- Implementation ------------------------------------------------- From 928304298261840f5885e6df4da1e47656847586 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 18:56:42 +0200 Subject: [PATCH 292/306] Refactor actor instance runtime --- .../packages/effect/src/Actor.ts | 152 +++++------------- .../src/internal/ActorInstanceRuntime.ts | 135 ++++++++++++++++ .../effect/src/internal/StateRuntime.ts | 16 +- 3 files changed, 181 insertions(+), 122 deletions(-) create mode 100644 rivetkit-typescript/packages/effect/src/internal/ActorInstanceRuntime.ts diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index e68a63890f..683cbc588b 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -1,13 +1,11 @@ import { Context, Effect, - Exit, - FiberSet, identity, Layer, Predicate, - Record, - Schema, + type Record, + type Schema, Scope, Struct, } from "effect"; @@ -16,8 +14,9 @@ import type * as RivetkitDb from "rivetkit/db"; import type * as Action from "./Action.ts"; import * as Client from "./Client.ts"; import * as ActionDispatcher from "./internal/ActionDispatcher.ts"; -import * as StateRuntime from "./internal/StateRuntime.ts"; +import * as ActorInstanceRuntime from "./internal/ActorInstanceRuntime.ts"; import type * as StateOptions from "./internal/StateOptions.ts"; +import * as StateRuntime from "./internal/StateRuntime.ts"; import * as Registry from "./Registry.ts"; import type * as RivetError from "./RivetError.ts"; import type * as State from "./State.ts"; @@ -171,7 +170,7 @@ type WakeOptionsFor< > = { readonly rawRivetkitContext: RawWakeContextFor; } & ([StateDefinition] extends [never] - ? {} + ? unknown : { readonly state: State.State< StateOptions.Decoded, @@ -455,14 +454,6 @@ export function toWakeHandler< }; } -type ActorInstance< - ActionHandlers, - StateDefinition extends StateOptions.Any, -> = ActionDispatcher.Instance & - StateRuntime.Instance & { - readonly scope: Scope.Closeable; -}; - const makeRivetkitActor = Effect.fnUntraced(function* < Name extends string, Actions extends Action.AnyWithProps, @@ -481,119 +472,50 @@ const makeRivetkitActor = Effect.fnUntraced(function* < ) => Effect.Effect; readonly options: Options; }) { - // Snapshot the current Effect context so action callbacks - // (which run in rivetkit’s plain Promise world) can run - // handler effects against the same services the Registry.start / - // Registry.test layer was provided with. - const services = yield* Effect.context(); - const { effectOptions, rivetkitOptions } = splitOptions(options); const stateRuntime = effectOptions.state === undefined ? undefined : yield* StateRuntime.make(effectOptions.state); - const instances = new Map>(); - - type RivetkitDefinition = RivetkitActorDefinitionFor; - - const makeInstance = Effect.fnUntraced(function* ( - c: Rivetkit.WakeContextOf, - ): Effect.fn.Return, never, any> { - const scope = yield* Scope.make(); - const state = stateRuntime - ? yield* stateRuntime.makeStateView(c) - : undefined; - - const context = Context.mergeAll( - Context.make(CurrentAddress, { - actorId: c.actorId, - name: c.name, - key: c.key, - }), - Context.make(Scope.Scope, scope), - Context.make( - Sleep, - Effect.sync(() => c.sleep()), - ), - ); - const wakeOptions = { - rawRivetkitContext: c, - ...(state ? { state } : {}), - } as WakeOptionsFor; - const actionHandlers = yield* wakeHandler(wakeOptions).pipe( - Effect.provide(context), - ); - const runFork = yield* FiberSet.makeRuntime< - any, - unknown, - unknown - >().pipe(Effect.provide(Context.merge(services, context))); - - return { - actionHandlers, - runFork, - scope, - state, - }; - }); - - const onWake = async (c: Rivetkit.WakeContextOf) => { - await Effect.runPromiseWith(services)( - makeInstance(c).pipe( - Effect.tap((instance) => - Effect.sync(() => { - instances.set(c.actorId, instance); - }), + const instanceRuntime = yield* ActorInstanceRuntime.make< + ActionHandlers, + State, + Database, + WakeOptionsFor + >({ + wakeHandler, + stateRuntime, + makeContext: (c, scope) => + Context.mergeAll( + Context.make(CurrentAddress, { + actorId: c.actorId, + name: c.name, + key: c.key, + }), + Context.make(Scope.Scope, scope), + Context.make( + Sleep, + Effect.sync(() => c.sleep()), ), ), - ); - }; + makeWakeOptions: (c, state) => + ({ + rawRivetkitContext: c, + ...(state === undefined ? {} : { state }), + }) as WakeOptionsFor, + }); const actions = ActionDispatcher.make< Name, Actions, ActionHandlers, - RivetkitDefinition + RivetkitActorDefinitionFor >({ actor, - getInstance: (actorId) => instances.get(actorId), - }); - - const onStateChange = stateRuntime - ? ( - c: Rivetkit.WakeContextOf, - newState: unknown, - ) => { - const instance = instances.get(c.actorId); - // Late state-change callbacks can arrive after teardown removed the - // instance. There is no live Effect state stream left to update. - if (!instance) return; - - stateRuntime.publishChange(instance, newState); - } - : undefined; - - const cleanupInstance = Effect.fnUntraced(function* (actorId: string) { - const instance = instances.get(actorId); - // Actor teardown can be reported more than once across sleep - // and destroy paths. Treat missing entries as already cleaned up. - if (!instance) return; - - instances.delete(actorId); - yield* Scope.close(instance.scope, Exit.void); + getInstance: instanceRuntime.get, }); - const onSleep = async (c: Rivetkit.SleepContextOf) => { - await Effect.runPromiseWith(services)(cleanupInstance(c.actorId)); - }; - - const onDestroy = async ( - c: Rivetkit.DestroyContextOf, - ) => { - await Effect.runPromiseWith(services)(cleanupInstance(c.actorId)); - }; - return Rivetkit.actor< StateOptions.Encoded, undefined, @@ -607,13 +529,15 @@ const makeRivetkitActor = Effect.fnUntraced(function* < >({ options: rivetkitOptions, ...(effectOptions.db ? { db: effectOptions.db } : {}), - onWake, + onWake: instanceRuntime.onWake, ...(stateRuntime ? { createState: stateRuntime.createInitialState } : {}), actions, - ...(onStateChange ? { onStateChange } : {}), - onSleep, - onDestroy, + ...(instanceRuntime.onStateChange + ? { onStateChange: instanceRuntime.onStateChange } + : {}), + onSleep: instanceRuntime.onTeardown, + onDestroy: instanceRuntime.onTeardown, }); }); diff --git a/rivetkit-typescript/packages/effect/src/internal/ActorInstanceRuntime.ts b/rivetkit-typescript/packages/effect/src/internal/ActorInstanceRuntime.ts new file mode 100644 index 0000000000..858ed23494 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/internal/ActorInstanceRuntime.ts @@ -0,0 +1,135 @@ +import { Context, Effect, Exit, type Fiber, FiberSet, Scope } from "effect"; +import type * as Rivetkit from "rivetkit"; +import type * as RivetkitDb from "rivetkit/db"; +import type * as StateOptions from "./StateOptions.ts"; +import type * as StateRuntime from "./StateRuntime.ts"; + +type RivetkitDefinitionFor< + StateDefinition extends StateOptions.Any, + Database extends RivetkitDb.AnyDatabaseProvider, +> = Rivetkit.ActorDefinition< + StateOptions.Encoded, + undefined, + undefined, + undefined, + undefined, + Database, + Record, + Record, + any +>; + +type WakeContext< + StateDefinition extends StateOptions.Any, + Database extends RivetkitDb.AnyDatabaseProvider, +> = Rivetkit.WakeContextOf>; + +export type Instance< + ActionHandlers, + StateDefinition extends StateOptions.Any, +> = { + readonly actionHandlers: ActionHandlers; + readonly runFork: ( + effect: Effect.Effect, + options?: Effect.RunOptions, + ) => Fiber.Fiber; + readonly scope: Scope.Closeable; + readonly state?: StateRuntime.ActorState; +}; + +export const make = Effect.fnUntraced(function* < + ActionHandlers, + StateDefinition extends StateOptions.Any, + Database extends RivetkitDb.AnyDatabaseProvider, + WakeOptions, +>({ + wakeHandler, + stateRuntime, + makeContext, + makeWakeOptions, +}: { + readonly wakeHandler: ( + wakeOptions: WakeOptions, + ) => Effect.Effect; + readonly stateRuntime: StateRuntime.Runtime | undefined; + readonly makeContext: ( + c: WakeContext, + scope: Scope.Closeable, + ) => Context.Context; + readonly makeWakeOptions: ( + c: WakeContext, + state: StateRuntime.ActorState | undefined, + ) => WakeOptions; +}) { + const instances = new Map< + string, + Instance + >(); + + const services = yield* Effect.context(); + const runPromise = Effect.runPromiseWith(services); + + const makeInstance = Effect.fnUntraced(function* ( + c: WakeContext, + ): Effect.fn.Return, never, any> { + const scope = yield* Scope.make(); + const state = stateRuntime + ? yield* stateRuntime.makeStateView(c) + : undefined; + const context = makeContext(c, scope); + const actionHandlers = yield* wakeHandler( + makeWakeOptions(c, state), + ).pipe(Effect.provide(context)); + const runFork = yield* FiberSet.makeRuntime< + any, + unknown, + unknown + >().pipe(Effect.provide(Context.merge(services, context))); + + return { + actionHandlers, + runFork, + scope, + state, + }; + }); + + return { + get: (actorId: string) => instances.get(actorId), + onWake: async (c: WakeContext) => { + await runPromise( + makeInstance(c).pipe( + Effect.tap((instance) => + Effect.sync(() => { + instances.set(c.actorId, instance); + }), + ), + ), + ); + }, + onStateChange: stateRuntime + ? ( + c: WakeContext, + newState: unknown, + ) => { + const instance = instances.get(c.actorId); + // State changes can arrive after teardown removes the instance. + if (!instance) return; + + stateRuntime.publishChange(instance, newState); + } + : undefined, + onTeardown: async (c: { readonly actorId: string }) => { + return runPromise( + Effect.gen(function* () { + const instance = instances.get(c.actorId); + // Teardown can be reported through multiple lifecycle callbacks. + if (!instance) return; + + instances.delete(c.actorId); + yield* Scope.close(instance.scope, Exit.void); + }), + ); + }, + }; +}); diff --git a/rivetkit-typescript/packages/effect/src/internal/StateRuntime.ts b/rivetkit-typescript/packages/effect/src/internal/StateRuntime.ts index 708bc92c71..bfa24be035 100644 --- a/rivetkit-typescript/packages/effect/src/internal/StateRuntime.ts +++ b/rivetkit-typescript/packages/effect/src/internal/StateRuntime.ts @@ -7,7 +7,7 @@ export type ActorState = State.State< Schema.SchemaError >; -export type Instance = { +type StateInstance = { readonly runFork: ( effect: Effect.Effect, options?: Effect.RunOptions, @@ -16,14 +16,14 @@ export type Instance = { }; export type Runtime = { - readonly makeStateView: ( - c: { state: StateOptions.Encoded }, - ) => Effect.Effect, never, any>; + readonly makeStateView: (c: { + state: StateOptions.Encoded; + }) => Effect.Effect, never, any>; readonly createInitialState: () => Promise< StateOptions.Encoded >; readonly publishChange: ( - instance: Instance, + instance: StateInstance, newState: unknown, ) => void; }; @@ -61,9 +61,9 @@ export const make = Effect.fnUntraced(function* < ), createInitialState: () => Effect.runPromiseWith(services)( - stateCodec.encode(stateOptions.initialValue()).pipe( - Effect.orDie, - ), + stateCodec + .encode(stateOptions.initialValue()) + .pipe(Effect.orDie), ), publishChange: (instance, newState) => { instance.runFork( From eba7e378ece284d4e819f553fd01e3c430665258 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 19:33:25 +0200 Subject: [PATCH 293/306] Refactor effect actor state and instance runtimes --- .../packages/effect/src/Actor.ts | 28 +++++++++---------- ...anceRuntime.ts => ActorInstanceManager.ts} | 20 +++++++------ .../{StateRuntime.ts => ActorStateAdapter.ts} | 4 +-- 3 files changed, 27 insertions(+), 25 deletions(-) rename rivetkit-typescript/packages/effect/src/internal/{ActorInstanceRuntime.ts => ActorInstanceManager.ts} (87%) rename rivetkit-typescript/packages/effect/src/internal/{StateRuntime.ts => ActorStateAdapter.ts} (94%) diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 683cbc588b..49ca7c1f8f 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -14,9 +14,9 @@ import type * as RivetkitDb from "rivetkit/db"; import type * as Action from "./Action.ts"; import * as Client from "./Client.ts"; import * as ActionDispatcher from "./internal/ActionDispatcher.ts"; -import * as ActorInstanceRuntime from "./internal/ActorInstanceRuntime.ts"; +import * as ActorInstanceManager from "./internal/ActorInstanceManager.ts"; +import * as ActorStateAdapter from "./internal/ActorStateAdapter.ts"; import type * as StateOptions from "./internal/StateOptions.ts"; -import * as StateRuntime from "./internal/StateRuntime.ts"; import * as Registry from "./Registry.ts"; import type * as RivetError from "./RivetError.ts"; import type * as State from "./State.ts"; @@ -473,19 +473,19 @@ const makeRivetkitActor = Effect.fnUntraced(function* < readonly options: Options; }) { const { effectOptions, rivetkitOptions } = splitOptions(options); - const stateRuntime = + const stateAdapter = effectOptions.state === undefined ? undefined - : yield* StateRuntime.make(effectOptions.state); + : yield* ActorStateAdapter.make(effectOptions.state); - const instanceRuntime = yield* ActorInstanceRuntime.make< + const instanceManager = yield* ActorInstanceManager.make< ActionHandlers, State, Database, WakeOptionsFor >({ wakeHandler, - stateRuntime, + stateAdapter, makeContext: (c, scope) => Context.mergeAll( Context.make(CurrentAddress, { @@ -513,7 +513,7 @@ const makeRivetkitActor = Effect.fnUntraced(function* < RivetkitActorDefinitionFor >({ actor, - getInstance: instanceRuntime.get, + getInstance: instanceManager.get, }); return Rivetkit.actor< @@ -529,15 +529,15 @@ const makeRivetkitActor = Effect.fnUntraced(function* < >({ options: rivetkitOptions, ...(effectOptions.db ? { db: effectOptions.db } : {}), - onWake: instanceRuntime.onWake, - ...(stateRuntime - ? { createState: stateRuntime.createInitialState } + onWake: instanceManager.onWake, + ...(stateAdapter + ? { createState: stateAdapter.createInitialState } : {}), actions, - ...(instanceRuntime.onStateChange - ? { onStateChange: instanceRuntime.onStateChange } + ...(instanceManager.onStateChange + ? { onStateChange: instanceManager.onStateChange } : {}), - onSleep: instanceRuntime.onTeardown, - onDestroy: instanceRuntime.onTeardown, + onSleep: instanceManager.onTeardown, + onDestroy: instanceManager.onTeardown, }); }); diff --git a/rivetkit-typescript/packages/effect/src/internal/ActorInstanceRuntime.ts b/rivetkit-typescript/packages/effect/src/internal/ActorInstanceManager.ts similarity index 87% rename from rivetkit-typescript/packages/effect/src/internal/ActorInstanceRuntime.ts rename to rivetkit-typescript/packages/effect/src/internal/ActorInstanceManager.ts index 858ed23494..deef99101f 100644 --- a/rivetkit-typescript/packages/effect/src/internal/ActorInstanceRuntime.ts +++ b/rivetkit-typescript/packages/effect/src/internal/ActorInstanceManager.ts @@ -1,8 +1,8 @@ import { Context, Effect, Exit, type Fiber, FiberSet, Scope } from "effect"; import type * as Rivetkit from "rivetkit"; import type * as RivetkitDb from "rivetkit/db"; +import type * as ActorStateAdapter from "./ActorStateAdapter.ts"; import type * as StateOptions from "./StateOptions.ts"; -import type * as StateRuntime from "./StateRuntime.ts"; type RivetkitDefinitionFor< StateDefinition extends StateOptions.Any, @@ -34,7 +34,7 @@ export type Instance< options?: Effect.RunOptions, ) => Fiber.Fiber; readonly scope: Scope.Closeable; - readonly state?: StateRuntime.ActorState; + readonly state?: ActorStateAdapter.ActorState; }; export const make = Effect.fnUntraced(function* < @@ -44,21 +44,23 @@ export const make = Effect.fnUntraced(function* < WakeOptions, >({ wakeHandler, - stateRuntime, + stateAdapter, makeContext, makeWakeOptions, }: { readonly wakeHandler: ( wakeOptions: WakeOptions, ) => Effect.Effect; - readonly stateRuntime: StateRuntime.Runtime | undefined; + readonly stateAdapter: + | ActorStateAdapter.Adapter + | undefined; readonly makeContext: ( c: WakeContext, scope: Scope.Closeable, ) => Context.Context; readonly makeWakeOptions: ( c: WakeContext, - state: StateRuntime.ActorState | undefined, + state: ActorStateAdapter.ActorState | undefined, ) => WakeOptions; }) { const instances = new Map< @@ -73,8 +75,8 @@ export const make = Effect.fnUntraced(function* < c: WakeContext, ): Effect.fn.Return, never, any> { const scope = yield* Scope.make(); - const state = stateRuntime - ? yield* stateRuntime.makeStateView(c) + const state = stateAdapter + ? yield* stateAdapter.makeStateView(c) : undefined; const context = makeContext(c, scope); const actionHandlers = yield* wakeHandler( @@ -107,7 +109,7 @@ export const make = Effect.fnUntraced(function* < ), ); }, - onStateChange: stateRuntime + onStateChange: stateAdapter ? ( c: WakeContext, newState: unknown, @@ -116,7 +118,7 @@ export const make = Effect.fnUntraced(function* < // State changes can arrive after teardown removes the instance. if (!instance) return; - stateRuntime.publishChange(instance, newState); + stateAdapter.publishChange(instance, newState); } : undefined, onTeardown: async (c: { readonly actorId: string }) => { diff --git a/rivetkit-typescript/packages/effect/src/internal/StateRuntime.ts b/rivetkit-typescript/packages/effect/src/internal/ActorStateAdapter.ts similarity index 94% rename from rivetkit-typescript/packages/effect/src/internal/StateRuntime.ts rename to rivetkit-typescript/packages/effect/src/internal/ActorStateAdapter.ts index bfa24be035..fa5d105758 100644 --- a/rivetkit-typescript/packages/effect/src/internal/StateRuntime.ts +++ b/rivetkit-typescript/packages/effect/src/internal/ActorStateAdapter.ts @@ -15,7 +15,7 @@ type StateInstance = { readonly state?: ActorState; }; -export type Runtime = { +export type Adapter = { readonly makeStateView: (c: { state: StateOptions.Encoded; }) => Effect.Effect, never, any>; @@ -32,7 +32,7 @@ export const make = Effect.fnUntraced(function* < StateDefinition extends StateOptions.Any, >( stateOptions: StateDefinition, -): Effect.fn.Return, never, any> { +): Effect.fn.Return, never, any> { const services = yield* Effect.context(); const stateCodec = { From bd1f6bd378d8a67092d005d6f5d0160a9e63c6ce Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 20:12:54 +0200 Subject: [PATCH 294/306] chore(effect): use node runtime for client entrypoint --- examples/effect/src/client.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 0f4840d07d..60e15cf39c 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,4 +1,5 @@ import { Client } from "@rivetkit/effect"; +import { NodeRuntime } from "@effect/platform-node"; import { Effect, Logger, Random } from "effect"; import { type BannedWordsError, @@ -70,8 +71,6 @@ const program = Effect.gen(function* () { const ClientLayer = Client.layer({ endpoint: "http://127.0.0.1:6420" }); const LoggerLayer = Logger.layer([Logger.consolePretty()]); -Effect.runPromise( - program.pipe(Effect.provide(ClientLayer), Effect.provide(LoggerLayer)), -).catch(() => { - process.exit(1); -}); +program + .pipe(Effect.provide(ClientLayer), Effect.provide(LoggerLayer)) + .pipe(NodeRuntime.runMain); From e867cbd6b954dfda17763fb34bb713511ab5abad Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Mon, 1 Jun 2026 20:14:19 +0200 Subject: [PATCH 295/306] refactor(effect): use fnUntraced for effect callbacks --- examples/effect/src/actors/chat-room/live.ts | 341 +++++++++--------- examples/effect/src/actors/moderator/live.ts | 38 +- examples/effect/src/client.ts | 4 +- .../packages/effect/src/Registry.ts | 15 +- .../packages/effect/src/State.ts | 25 +- 5 files changed, 203 insertions(+), 220 deletions(-) diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts index d8b5a31b07..4897a1a174 100644 --- a/examples/effect/src/actors/chat-room/live.ts +++ b/examples/effect/src/actors/chat-room/live.ts @@ -41,200 +41,189 @@ export const RoomPolicyLive = Layer.succeed( // and returns the action handlers. export const ChatRoomLive = ChatRoom.toLayer( // Wake scope (runs on each wake) - ({ rawRivetkitContext, state }) => - Effect.gen(function* () { - // Actor-provided services, custom services, and actor clients are all - // yielded from the Effect context for this wake. They are scoped to - // this actor instance, not to individual action calls. - const address = yield* Actor.CurrentAddress; - const roomPolicy = yield* RoomPolicy; - const moderatorClient = yield* Moderator.client; + Effect.fnUntraced(function* ({ rawRivetkitContext, state }) { + // Actor-provided services, custom services, and actor clients are all + // yielded from the Effect context for this wake. They are scoped to + // this actor instance, not to individual action calls. + const address = yield* Actor.CurrentAddress; + const roomPolicy = yield* RoomPolicy; + const moderatorClient = yield* Moderator.client; - yield* Effect.log("room awake", { - actorId: address.actorId, - key: address.key.join("/"), - }); + yield* Effect.log("room awake", { + actorId: address.actorId, + key: address.key.join("/"), + }); - // Finalizers run on sleep - yield* Effect.addFinalizer(() => - Effect.gen(function* () { - // Access the actor's persisted `state` with a `SubscriptionRef`-like API - const name = yield* State.get(state).pipe( - Effect.orDie, - Effect.map((s) => s.name), - ); - yield* Effect.log("room sleeping", { - actorId: address.actorId, - key: address.key.join("/"), - name, - }); + // Finalizers run on sleep + yield* Effect.addFinalizer( + Effect.fnUntraced(function* () { + // Access the actor's persisted `state` with a `SubscriptionRef`-like API + const name = yield* State.get(state).pipe( + Effect.orDie, + Effect.map((s) => s.name), + ); + yield* Effect.log("room sleeping", { + actorId: address.actorId, + key: address.key.join("/"), + name, + }); + }), + ); + + // `State.changes` streams every committed state change for this actor wake. + yield* State.changes(state).pipe( + Stream.runForEach((current) => + Effect.log("room state changed", { + actorId: address.actorId, + name: current.name, + memberCount: current.members.length, }), - ); + ), + Effect.forkScoped, + ); - // `State.changes` streams every committed state change for this actor wake. - yield* State.changes(state).pipe( - Stream.runForEach((current) => - Effect.log("room state changed", { - actorId: address.actorId, - name: current.name, - memberCount: current.members.length, - }), + // Combine persisted actor state with a custom service-owned domain guard. + const ensureMember = (name: string) => + State.get(state).pipe( + Effect.orDie, + Effect.flatMap((current) => + roomPolicy.requireMember(current.members, name), ), - Effect.forkScoped, ); - // Combine persisted actor state with a custom service-owned domain guard. - const ensureMember = (name: string) => - State.get(state).pipe( - Effect.orDie, - Effect.flatMap((current) => - roomPolicy.requireMember(current.members, name), - ), - ); - - // --- Message processing (not yet implemented) --- - // Pull-based: the actor controls when to take the next message. - // Forked into a scoped fiber, so it runs in the background and - // is canceled on sleep. Re-enable once ChatRoom messages land. - // - // yield* Effect.gen(function* () { - // const msg = yield* Queue.take(messages) - // yield* Match.value(msg).pipe( - // Match.tag("Reset", () => - // Effect.gen(function* () { - // yield* State.set(state, 0) - // yield* PubSub.publish(events.countChanged, 0) - // }) - // ), - // Match.tag("SendSystemMessage", ({ payload, complete }) => - // Effect.gen(function* () { - // yield* complete(payload.text) - // }) - // ), - // Match.exhaustive, - // ) - // }).pipe(Effect.forever, Effect.forkScoped) + // --- Message processing (not yet implemented) --- + // Pull-based: the actor controls when to take the next message. + // Forked into a scoped fiber, so it runs in the background and + // is canceled on sleep. Re-enable once ChatRoom messages land. + // + // yield* Effect.gen(function* () { + // const msg = yield* Queue.take(messages) + // yield* Match.value(msg).pipe( + // Match.tag("Reset", () => + // Effect.gen(function* () { + // yield* State.set(state, 0) + // yield* PubSub.publish(events.countChanged, 0) + // }) + // ), + // Match.tag("SendSystemMessage", ({ payload, complete }) => + // Effect.gen(function* () { + // yield* complete(payload.text) + // }) + // ), + // Match.exhaustive, + // ) + // }).pipe(Effect.forever, Effect.forkScoped) - // --- Action handlers (request-response) --- - return ChatRoom.of({ - Initialize: ({ payload }) => - // This replaces `createState(input)`. Callers should initialize - // a room before actions that depend on a persisted room name. - State.update(state, (current) => { - if (current.initialized) return current; - return { - ...current, - name: payload.name, - initialized: true, - }; - }).pipe(Effect.orDie), - Join: ({ payload }) => - Effect.gen(function* () { - const joinedAt = yield* DateTime.now; - const member = { - name: payload.name, - joinedAt, - }; - const next = yield* State.updateAndGet( - state, - (current) => ({ - ...current, - members: [...current.members, member], - }), - ).pipe(Effect.orDie); + // --- Action handlers (request-response) --- + return ChatRoom.of({ + Initialize: ({ payload }) => + // This replaces `createState(input)`. Callers should initialize + // a room before actions that depend on a persisted room name. + State.update(state, (current) => { + if (current.initialized) return current; + return { + ...current, + name: payload.name, + initialized: true, + }; + }).pipe(Effect.orDie), + Join: Effect.fnUntraced(function* ({ payload }) { + const joinedAt = yield* DateTime.now; + const member = { + name: payload.name, + joinedAt, + }; + const next = yield* State.updateAndGet(state, (current) => ({ + ...current, + members: [...current.members, member], + })).pipe(Effect.orDie); - rawRivetkitContext.broadcast("memberJoined", { - member: { - ...member, - joinedAt: DateTime.formatIso(member.joinedAt), - }, - }); + rawRivetkitContext.broadcast("memberJoined", { + member: { + ...member, + joinedAt: DateTime.formatIso(member.joinedAt), + }, + }); - // The raw scheduler dispatches the Effect action by name - // with the same object payload that a client would send. - rawRivetkitContext.schedule.after( - 1_000, - "SendMessage", - { - sender: "Admin", - text: `Welcome to the room, ${payload.name}!`, - }, - ); + // The raw scheduler dispatches the Effect action by name + // with the same object payload that a client would send. + rawRivetkitContext.schedule.after(1_000, "SendMessage", { + sender: "Admin", + text: `Welcome to the room, ${payload.name}!`, + }); - return { memberCount: next.members.length }; - }), - Leave: ({ payload }) => - Effect.gen(function* () { - yield* ensureMember(payload.name); + return { memberCount: next.members.length }; + }), + Leave: Effect.fnUntraced(function* ({ payload }) { + yield* ensureMember(payload.name); - yield* State.update(state, (current) => ({ - ...current, - members: current.members.filter( - (member) => member.name !== payload.name, - ), - })).pipe(Effect.orDie); + yield* State.update(state, (current) => ({ + ...current, + members: current.members.filter( + (member) => member.name !== payload.name, + ), + })).pipe(Effect.orDie); - rawRivetkitContext.broadcast("memberLeft", { - name: payload.name, - }); - }), - SendMessage: ({ payload }) => - Effect.gen(function* () { - yield* ensureMember(payload.sender); + rawRivetkitContext.broadcast("memberLeft", { + name: payload.name, + }); + }), + SendMessage: Effect.fnUntraced(function* ({ payload }) { + yield* ensureMember(payload.sender); - // Actor-to-actor RPC uses the same API as client-to-actor RPC. - const moderator = moderatorClient.getOrCreate([ - ...address.key, - "main", - ]); + // Actor-to-actor RPC uses the same API as client-to-actor RPC. + const moderator = moderatorClient.getOrCreate([ + ...address.key, + "main", + ]); - // If Review fails with BannedWordsError, that typed error - // flows through SendMessage's declared error channel. - yield* moderator - .Review({ text: payload.text }) - .pipe(Effect.catchTag("RivetError", Effect.die)); + // If Review fails with BannedWordsError, that typed error + // flows through SendMessage's declared error channel. + yield* moderator + .Review({ text: payload.text }) + .pipe(Effect.catchTag("RivetError", Effect.die)); - const createdAt = yield* DateTime.now; - yield* Effect.tryPromise(() => - rawRivetkitContext.db.execute( - "INSERT INTO messages (sender, text, created_at) VALUES (?, ?, ?)", - payload.sender, - payload.text, - DateTime.toEpochMillis(createdAt), - ), - ).pipe(Effect.orDie); + const createdAt = yield* DateTime.now; + yield* Effect.tryPromise(() => + rawRivetkitContext.db.execute( + "INSERT INTO messages (sender, text, created_at) VALUES (?, ?, ?)", + payload.sender, + payload.text, + DateTime.toEpochMillis(createdAt), + ), + ).pipe(Effect.orDie); - rawRivetkitContext.broadcast("newMessage", { - sender: payload.sender, - text: payload.text, - createdAt: DateTime.formatIso(createdAt), - }); - }), - GetHistory: () => - Effect.tryPromise(() => - rawRivetkitContext.db.execute<{ - id: number; - sender: string; - text: string; - createdAt: number; - }>( - "SELECT id, sender, text, created_at as createdAt FROM messages ORDER BY id", - ), - ).pipe( - Effect.map((rows) => - rows.map((row) => ({ - ...row, - createdAt: DateTime.makeUnsafe(row.createdAt), - })), - ), - Effect.orDie, + rawRivetkitContext.broadcast("newMessage", { + sender: payload.sender, + text: payload.text, + createdAt: DateTime.formatIso(createdAt), + }); + }), + GetHistory: () => + Effect.tryPromise(() => + rawRivetkitContext.db.execute<{ + id: number; + sender: string; + text: string; + createdAt: number; + }>( + "SELECT id, sender, text, created_at as createdAt FROM messages ORDER BY id", ), - Archive: () => - Effect.sync(() => { - rawRivetkitContext.destroy(); - }), - }); - }), + ).pipe( + Effect.map((rows) => + rows.map((row) => ({ + ...row, + createdAt: DateTime.makeUnsafe(row.createdAt), + })), + ), + Effect.orDie, + ), + Archive: () => + Effect.sync(() => { + rawRivetkitContext.destroy(); + }), + }); + }), { state: { schema: Schema.Struct({ diff --git a/examples/effect/src/actors/moderator/live.ts b/examples/effect/src/actors/moderator/live.ts index b80d3dbcf7..70f08e4834 100644 --- a/examples/effect/src/actors/moderator/live.ts +++ b/examples/effect/src/actors/moderator/live.ts @@ -5,28 +5,24 @@ import { BannedWordsError, Moderator } from "./api.ts"; const bannedWords = ["spam", "scam"]; export const ModeratorLive = Moderator.toLayer( - ({ state }) => - Effect.gen(function* () { - return Moderator.of({ - Review: ({ payload }) => - Effect.gen(function* () { - yield* State.update(state, (current) => ({ - ...current, - reviewed: current.reviewed + 1, - })).pipe(Effect.orDie); + Effect.fnUntraced(function* ({ state }) { + return Moderator.of({ + Review: Effect.fnUntraced(function* ({ payload }) { + yield* State.update(state, (current) => ({ + ...current, + reviewed: current.reviewed + 1, + })).pipe(Effect.orDie); - const lower = payload.text.toLowerCase(); - const hit = bannedWords.find((word) => - lower.includes(word), - ); - if (hit !== undefined) { - return yield* new BannedWordsError({ - message: `contains banned word "${hit}"`, - }); - } - }), - }); - }), + const lower = payload.text.toLowerCase(); + const hit = bannedWords.find((word) => lower.includes(word)); + if (hit !== undefined) { + return yield* new BannedWordsError({ + message: `contains banned word "${hit}"`, + }); + } + }), + }); + }), { state: { schema: Schema.Struct({ diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index 60e15cf39c..d84dd9f676 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -14,8 +14,8 @@ const program = Effect.gen(function* () { `chatroom_${yield* Random.nextUUIDv4}`, ); - yield* Effect.addFinalizer(() => - Effect.gen(function* () { + yield* Effect.addFinalizer( + Effect.fnUntraced(function* () { yield* room.Archive().pipe(Effect.orDie); yield* Effect.log("archived room"); }), diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index 28eb85cb72..f1095f7e28 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -147,10 +147,10 @@ export type ToHttpEffectOptions = ServerlessOptions; * Actors are materialized into a single underlying RivetKit registry, and each * request is delegated to that registry's serverless handler. */ -export const toHttpEffect = ( +export const toHttpEffect = Effect.fnUntraced(function* ( registryLayer: Layer.Layer, options?: ToHttpEffectOptions, -): Effect.Effect< +): Effect.fn.Return< Effect.Effect< HttpServerResponse.HttpServerResponse, HttpServerError.HttpServerError, @@ -158,12 +158,11 @@ export const toHttpEffect = ( >, E, Scope.Scope -> => - Effect.gen(function* () { - const context = yield* Layer.build(registryLayer); - // @effect-diagnostics-next-line returnEffectInGen:off - return makeHttpEffect(Context.get(context, Registry), options); - }); +> { + const context = yield* Layer.build(registryLayer); + // @effect-diagnostics-next-line returnEffectInGen:off + return makeHttpEffect(Context.get(context, Registry), options); +}); export type ToWebHandlerOptions = ServerlessOptions & { /** diff --git a/rivetkit-typescript/packages/effect/src/State.ts b/rivetkit-typescript/packages/effect/src/State.ts index ded4792253..afe18ffdf9 100644 --- a/rivetkit-typescript/packages/effect/src/State.ts +++ b/rivetkit-typescript/packages/effect/src/State.ts @@ -89,21 +89,20 @@ const Proto = { * The PubSub is not explicitly shut down — it's reclaimed by GC when * the `State` and any subscribers become unreachable. */ -export const make = ( +export const make = Effect.fnUntraced(function* ( read: () => Effect.Effect, write: (value: A) => Effect.Effect, -): Effect.Effect, E, R> => - Effect.gen(function* () { - const pubsub = yield* PubSub.unbounded({ replay: 1 }); - const initial = yield* read(); - PubSub.publishUnsafe(pubsub, initial); - const self = Object.create(Proto); - self.read = read; - self.write = write; - self.pubsub = pubsub; - self.semaphore = Semaphore.makeUnsafe(1); - return self; - }); +): Effect.fn.Return, E, R> { + const pubsub = yield* PubSub.unbounded({ replay: 1 }); + const initial = yield* read(); + PubSub.publishUnsafe(pubsub, initial); + const self = Object.create(Proto); + self.read = read; + self.write = write; + self.pubsub = pubsub; + self.semaphore = Semaphore.makeUnsafe(1); + return self; +}); /** * Reads the current value. From 3c7268e6d39026ec6f36ecedb34b84eefc9785fd Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 2 Jun 2026 12:11:14 +0200 Subject: [PATCH 296/306] fix(effect): preserve client schema services --- .../packages/effect/src/Actor.test-d.ts | 52 ++++++++++++++++++- .../packages/effect/src/Actor.ts | 11 ++-- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts index 5b42f4ce53..c896e77a66 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.test-d.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -1,4 +1,4 @@ -import { Action, Actor, type Client, type State } from "@rivetkit/effect"; +import { Action, Actor, Client, type State } from "@rivetkit/effect"; import { Context, Effect, @@ -8,7 +8,7 @@ import { } from "effect"; import type { RawAccess } from "rivetkit/db"; import { db } from "rivetkit/db"; -import { describe, expectTypeOf, test } from "vitest"; +import { describe, expectTypeOf, it, test } from "@effect/vitest"; class SomeDep extends Context.Service()( "SomeDep", @@ -40,6 +40,42 @@ const TagsCsv = Schema.String.pipe( ), ); +const ServiceDependentNumber = Schema.Number.pipe( + Schema.decodeTo( + Schema.Number, + SchemaTransformation.transformOrFail({ + decode: (n: number) => + Effect.gen(function* () { + const dep = yield* SomeDep; + return n + dep.x; + }), + encode: (n: number) => + Effect.gen(function* () { + const dep = yield* SomeDep; + return n - dep.x; + }), + }), + ), +); + +class ServiceDependentError extends Schema.TaggedErrorClass()( + "ServiceDependentError", + { + limit: ServiceDependentNumber, + message: Schema.String, + }, +) {} + +const ServiceDependentAction = Action.make("ServiceDependentAction", { + payload: { amount: ServiceDependentNumber }, + success: ServiceDependentNumber, + error: ServiceDependentError, +}); + +const ServiceDependentActor = Actor.make("ServiceDependentActor", { + actions: [ServiceDependentAction], +}); + const TransformedState = { schema: Schema.Struct({ when: Schema.DateFromString, @@ -552,4 +588,16 @@ describe("Actor.make(...).client", () => { > >(); }); + + it.effect("handle calls require client-side schema services", () => + Effect.gen(function* () { + const actor = (yield* ServiceDependentActor.client).getOrCreate( + "t-service-dependent", + ); + const actionEffect = actor.ServiceDependentAction({ amount: 10 }); + type ActionClientServices = Effect.Services; + + expectTypeOf().toExtend(); + }).pipe(Effect.provide(Client.layer())), + ); }); diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index 49ca7c1f8f..a7589a5ee8 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -199,11 +199,12 @@ export type AccessorKeyParam = string | Rivetkit.ActorKey; export type Handle = { readonly [A in Actions as Action.Tag]: ( payload: Action.PayloadConstructor, - ) => Effect.Effect< - Action.Success, - Action.Error | RivetError.RivetError - >; -}; + ) => Effect.Effect< + Action.Success, + Action.Error | RivetError.RivetError, + Action.ServicesClient + >; + }; /** * Yielded by `Actor.client`. Address an actor instance by key, then From 4f5ec818f69bfe3d016172cddf9620378e5c49fe Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 2 Jun 2026 16:18:38 +0200 Subject: [PATCH 297/306] fix(effect): close wake scope on failed wake --- .../src/internal/ActorInstanceManager.ts | 52 +++++++------ .../packages/effect/test/e2e.test.ts | 74 ++++++++++++++----- .../packages/effect/test/fixtures/actors.ts | 33 +++++++++ 3 files changed, 116 insertions(+), 43 deletions(-) diff --git a/rivetkit-typescript/packages/effect/src/internal/ActorInstanceManager.ts b/rivetkit-typescript/packages/effect/src/internal/ActorInstanceManager.ts index deef99101f..c01b99c482 100644 --- a/rivetkit-typescript/packages/effect/src/internal/ActorInstanceManager.ts +++ b/rivetkit-typescript/packages/effect/src/internal/ActorInstanceManager.ts @@ -71,30 +71,36 @@ export const make = Effect.fnUntraced(function* < const services = yield* Effect.context(); const runPromise = Effect.runPromiseWith(services); - const makeInstance = Effect.fnUntraced(function* ( - c: WakeContext, - ): Effect.fn.Return, never, any> { - const scope = yield* Scope.make(); - const state = stateAdapter - ? yield* stateAdapter.makeStateView(c) - : undefined; - const context = makeContext(c, scope); - const actionHandlers = yield* wakeHandler( - makeWakeOptions(c, state), - ).pipe(Effect.provide(context)); - const runFork = yield* FiberSet.makeRuntime< - any, - unknown, - unknown - >().pipe(Effect.provide(Context.merge(services, context))); + const makeInstance = Effect.fnUntraced(function* ( + c: WakeContext, + ): Effect.fn.Return, never, any> { + const scope = yield* Scope.make(); + return yield* Effect.gen(function* () { + const state = stateAdapter + ? yield* stateAdapter.makeStateView(c) + : undefined; + const context = makeContext(c, scope); + const actionHandlers = yield* wakeHandler( + makeWakeOptions(c, state), + ).pipe(Effect.provide(context)); + const runFork = yield* FiberSet.makeRuntime< + any, + unknown, + unknown + >().pipe(Effect.provide(Context.merge(services, context))); - return { - actionHandlers, - runFork, - scope, - state, - }; - }); + return { + actionHandlers, + runFork, + scope, + state, + }; + }).pipe( + Effect.onError((cause) => + Scope.close(scope, Exit.failCause(cause)), + ), + ); + }); return { get: (actorId: string) => instances.get(actorId), diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index f60e9ef3f4..475332e01e 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -12,6 +12,8 @@ import { CounterOverflowError, FailingActor, FailingActorLive, + FailingWakeCleanup, + FailingWakeCleanupLive, Flags, Greeter, Multiplier, @@ -69,15 +71,16 @@ const TestLayer = ReadyForEnvoy.pipe( Layer.provideMerge( Registry.test.pipe( Layer.provideMerge( - Layer.mergeAll( - CounterLive, - PingerLive, - FailingActorLive, - StrictLive, - WakeDecodeFailLive, - BuildSetRejectedLive, - TransformedStateActorLive, - ), + Layer.mergeAll( + CounterLive, + PingerLive, + FailingActorLive, + FailingWakeCleanupLive, + StrictLive, + WakeDecodeFailLive, + BuildSetRejectedLive, + TransformedStateActorLive, + ), ), Layer.provideMerge(Flags.layer), Layer.provide(GreeterLive), @@ -627,11 +630,11 @@ layer(TestLayer)("end-to-end", (it) => { }), ); - it.effect("surfaces an error thrown inside an actor's build effect", () => - Effect.gen(function* () { - // `getOrCreate` only builds a typed proxy on the client and - // rivetkit's wake is lazy on first action, so the build - // defect surfaces on `.Ping()`, not here. + it.effect("surfaces an error thrown inside an actor's build effect", () => + Effect.gen(function* () { + // `getOrCreate` only builds a typed proxy on the client and + // rivetkit's wake is lazy on first action, so the build + // defect surfaces on `.Ping()`, not here. const failing = (yield* FailingActor.client).getOrCreate([ "t-build-error", ]); @@ -639,13 +642,44 @@ layer(TestLayer)("end-to-end", (it) => { assert.isTrue(exit._tag === "Success"); if (exit._tag === "Success") { assert.instanceOf(exit.value, RivetError.RivetError); - } - }), - ); + } + }), + ); + + it.effect( + "closes the wake scope when wake fails after registering scoped resources", + () => + Effect.gen(function* () { + const key = "t-failed-wake-cleanup"; + const flags = yield* Flags; + const actor = (yield* FailingWakeCleanup.client).getOrCreate([ + key, + ]); + + const exit = yield* actor.Ping().pipe(Effect.flip, Effect.exit); + assert.isTrue(exit._tag === "Success"); + if (exit._tag === "Success") { + assert.instanceOf(exit.value, RivetError.RivetError); + } - it.effect( - "wake options state decode failure inside build effect surfaces as RivetError", - () => + assert.strictEqual( + flags.get(`failed-wake-started:${key}`), + true, + ); + assert.strictEqual( + flags.get(`failed-wake-finalizer:${key}`), + true, + ); + assert.strictEqual( + flags.get(`failed-wake-fiber-interrupted:${key}`), + true, + ); + }), + ); + + it.effect( + "wake options state decode failure inside build effect surfaces as RivetError", + () => Effect.gen(function* () { const failing = (yield* WakeDecodeFail.client).getOrCreate([ "t-wake-decode-fail", diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts index 6e4f37795d..2cf2a8f968 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts @@ -605,6 +605,39 @@ export const FailingActorLive = FailingActor.toLayer( Effect.die("build effect failed"), ); +// --- FailingWakeCleanup --- + +export const FailingWakeCleanup = Actor.make("FailingWakeCleanup", { + actions: [Ping], +}); + +export const FailingWakeCleanupLive = FailingWakeCleanup.toLayer(() => + Effect.gen(function* () { + const flags = yield* Flags; + const address = yield* Actor.CurrentAddress; + const key = address.key.join("/"); + + flags.set(`failed-wake-started:${key}`, true); + + yield* Effect.addFinalizer(() => + Effect.sync(() => { + flags.set(`failed-wake-finalizer:${key}`, true); + }), + ); + + yield* Effect.never.pipe( + Effect.onInterrupt(() => + Effect.sync(() => { + flags.set(`failed-wake-fiber-interrupted:${key}`, true); + }), + ), + Effect.forkScoped({ startImmediately: true }), + ); + + return yield* Effect.die("wake failed after scoped resources"); + }), +); + // --- Unregistered --- // Used solely to test the failure shape when calling an actor whose From ce2ced55a6015ac26b9e6dc0d70a8d85c09bb8c9 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 2 Jun 2026 16:49:25 +0200 Subject: [PATCH 298/306] fix(effect): classify malformed raw payloads --- .../effect/src/internal/ActionDispatcher.ts | 14 ++++++++++- .../packages/effect/test/e2e.test.ts | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts b/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts index a4d0c50139..547bdba892 100644 --- a/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts +++ b/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts @@ -94,7 +94,15 @@ export const make = < : payload; const decodedPayload = yield* decodePayload( payloadForDecode, - ).pipe(Effect.orDie); + ).pipe( + Effect.mapError(() => + new Rivetkit.RivetError( + "request", + "invalid", + `Invalid payload for action ${actor.name}/${action._tag}`, + ), + ), + ); // The payload was decoded with this action's schema, // so this is the runtime boundary that restores the // typed envelope expected by the user handler. @@ -167,6 +175,10 @@ export const make = < if (Cause.hasInterruptsOnly(exit.cause)) { throw makeActorAbortedError(); } + const expectedError = Exit.findErrorOption(exit); + if (Option.isSome(expectedError)) { + throw expectedError.value; + } throw Cause.squash(exit.cause); }, ]; diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 475332e01e..16c2108700 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -3,6 +3,7 @@ import { Registry, RivetError } from "@rivetkit/effect"; import { DateTime, Effect, Fiber, Layer, Schedule } from "effect"; import { TestClock } from "effect/testing"; import { createClient } from "rivetkit/client"; +import * as RawRivetErrors from "rivetkit/errors"; import { inject } from "vitest"; import { BuildSetRejected, @@ -127,6 +128,28 @@ layer(TestLayer)("end-to-end", (it) => { }), ); + it.effect("rejects malformed raw action payloads as request.invalid", () => + Effect.gen(function* () { + const client = yield* Effect.acquireRelease( + Effect.sync(() => createClient({ endpoint, token, namespace })), + (client) => Effect.promise(() => client.dispose()), + ); + const counter = client.Counter.getOrCreate("t-raw-invalid-payload"); + + const error = yield* Effect.promise(async () => { + try { + await counter.Increment({ amount: "not-a-number" } as any); + throw new Error("expected malformed payload to fail"); + } catch (error) { + return RawRivetErrors.toRivetError(error); + } + }); + + assert.strictEqual(error.group, "request"); + assert.strictEqual(error.code, "invalid"); + }), + ); + it.effect("isolates in-wake state across keys", () => Effect.gen(function* () { const client = yield* Counter.client; From 535604b3a4bd3e5f0f764e8ffb49afc2c2d24426 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 2 Jun 2026 20:23:30 +0200 Subject: [PATCH 299/306] refactor(effect): rename state variable for clarity in chat-room actor logs --- examples/effect/src/actors/chat-room/live.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/effect/src/actors/chat-room/live.ts b/examples/effect/src/actors/chat-room/live.ts index 4897a1a174..d2c61549cf 100644 --- a/examples/effect/src/actors/chat-room/live.ts +++ b/examples/effect/src/actors/chat-room/live.ts @@ -58,14 +58,14 @@ export const ChatRoomLive = ChatRoom.toLayer( yield* Effect.addFinalizer( Effect.fnUntraced(function* () { // Access the actor's persisted `state` with a `SubscriptionRef`-like API - const name = yield* State.get(state).pipe( + const roomName = yield* State.get(state).pipe( Effect.orDie, Effect.map((s) => s.name), ); yield* Effect.log("room sleeping", { actorId: address.actorId, key: address.key.join("/"), - name, + roomName, }); }), ); @@ -75,7 +75,7 @@ export const ChatRoomLive = ChatRoom.toLayer( Stream.runForEach((current) => Effect.log("room state changed", { actorId: address.actorId, - name: current.name, + roomName: current.name, memberCount: current.members.length, }), ), From 474081df95c907f0ec845d88f3758d38385c7dd7 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 2 Jun 2026 22:05:10 +0200 Subject: [PATCH 300/306] Integrate Effect SDK logging --- examples/effect/package.json | 6 +- examples/effect/src/client.ts | 8 +- examples/effect/src/logger.ts | 8 + examples/effect/src/main.ts | 11 +- pnpm-lock.yaml | 6 + .../packages/effect/src/Actor.ts | 20 +- .../packages/effect/src/Client.test.ts | 103 ++++++- .../packages/effect/src/Client.ts | 17 +- .../packages/effect/src/Logger.ts | 43 +++ .../packages/effect/src/Registry.test.ts | 78 ++++- .../packages/effect/src/Registry.ts | 84 +++-- .../effect/src/internal/ActionDispatcher.ts | 2 + .../effect/src/internal/logging.test.ts | 288 ++++++++++++++++++ .../packages/effect/src/internal/logging.ts | 237 ++++++++++++++ .../packages/effect/src/mod.ts | 1 + 15 files changed, 866 insertions(+), 46 deletions(-) create mode 100644 examples/effect/src/logger.ts create mode 100644 rivetkit-typescript/packages/effect/src/Logger.ts create mode 100644 rivetkit-typescript/packages/effect/src/internal/logging.test.ts create mode 100644 rivetkit-typescript/packages/effect/src/internal/logging.ts diff --git a/examples/effect/package.json b/examples/effect/package.json index acbebc5ce2..9610c629cd 100644 --- a/examples/effect/package.json +++ b/examples/effect/package.json @@ -10,10 +10,12 @@ "check-types": "tsc --noEmit" }, "dependencies": { - "rivetkit": "workspace:*", + "@effect/platform-node": "4.0.0-beta.66", "@rivetkit/effect": "workspace:*", "effect": "4.0.0-beta.66", - "@effect/platform-node": "4.0.0-beta.66" + "pino": "9.9.5", + "pino-pretty": "13.1.2", + "rivetkit": "workspace:*" }, "devDependencies": { "@types/node": "^22.13.9", diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts index d84dd9f676..2892a3163d 100644 --- a/examples/effect/src/client.ts +++ b/examples/effect/src/client.ts @@ -1,11 +1,12 @@ -import { Client } from "@rivetkit/effect"; import { NodeRuntime } from "@effect/platform-node"; -import { Effect, Logger, Random } from "effect"; +import { Client } from "@rivetkit/effect"; +import { Effect, Random } from "effect"; import { type BannedWordsError, ChatRoom, type MemberNotInRoomError, } from "./actors/mod.ts"; +import { PrettyLoggerLayer } from "./logger.ts"; const program = Effect.gen(function* () { // `Actor.client` yields a typed accessor backed by the Effect SDK client layer. @@ -69,8 +70,7 @@ const program = Effect.gen(function* () { }).pipe(Effect.scoped); const ClientLayer = Client.layer({ endpoint: "http://127.0.0.1:6420" }); -const LoggerLayer = Logger.layer([Logger.consolePretty()]); program - .pipe(Effect.provide(ClientLayer), Effect.provide(LoggerLayer)) + .pipe(Effect.provide(ClientLayer), Effect.provide(PrettyLoggerLayer)) .pipe(NodeRuntime.runMain); diff --git a/examples/effect/src/logger.ts b/examples/effect/src/logger.ts new file mode 100644 index 0000000000..f7cc932806 --- /dev/null +++ b/examples/effect/src/logger.ts @@ -0,0 +1,8 @@ +import { Logger } from "@rivetkit/effect"; +import { pino } from "pino"; + +// This layer replaces the default RivetKit Effect logger with a custom Pino +// logger. It affects both Effect.log* calls and the underlying RivetKit logs. +export const PrettyLoggerLayer = Logger.layerPino( + pino({ transport: { target: "pino-pretty" } }), +); diff --git a/examples/effect/src/main.ts b/examples/effect/src/main.ts index d5da6bc8aa..7f48441978 100644 --- a/examples/effect/src/main.ts +++ b/examples/effect/src/main.ts @@ -1,8 +1,9 @@ -import { Layer } from "effect"; import { NodeRuntime } from "@effect/platform-node"; -import { Registry, Client } from "@rivetkit/effect"; +import { Client, Registry } from "@rivetkit/effect"; +import { Layer } from "effect"; import { ChatRoomLive, RoomPolicyLive } from "./actors/chat-room/live.ts"; import { ModeratorLive } from "./actors/moderator/live.ts"; +import { PrettyLoggerLayer } from "./logger.ts"; const endpoint = process.env.RIVET_ENDPOINT ?? "http://127.0.0.1:6420"; @@ -18,6 +19,7 @@ const ActorsLayer = Layer.mergeAll( // RIVET_ENGINE_BINARY=$(pwd)/target/debug/rivet-engine pnpm start const MainLayer = Registry.serve(ActorsLayer).pipe( Layer.provide(Registry.layer()), + Layer.provide(PrettyLoggerLayer), ); // Keeps the layer alive. Tears down on SIGINT/SIGTERM. @@ -25,5 +27,8 @@ Layer.launch(MainLayer).pipe(NodeRuntime.runMain); // Or create a web handler, which can be used in serverless environments. export const { handler, dispose } = Registry.toWebHandler( - ActorsLayer.pipe(Layer.provideMerge(Registry.layer())), + ActorsLayer.pipe( + Layer.provideMerge(Registry.layer()), + Layer.provide(PrettyLoggerLayer), + ), ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38eedc20bf..c13dfcc935 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1159,6 +1159,12 @@ importers: effect: specifier: 4.0.0-beta.66 version: 4.0.0-beta.66 + pino: + specifier: 9.9.5 + version: 9.9.5 + pino-pretty: + specifier: 13.1.2 + version: 13.1.2 rivetkit: specifier: workspace:* version: link:../../rivetkit-typescript/packages/rivetkit diff --git a/rivetkit-typescript/packages/effect/src/Actor.ts b/rivetkit-typescript/packages/effect/src/Actor.ts index a7589a5ee8..f723fb9822 100644 --- a/rivetkit-typescript/packages/effect/src/Actor.ts +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -16,6 +16,7 @@ import * as Client from "./Client.ts"; import * as ActionDispatcher from "./internal/ActionDispatcher.ts"; import * as ActorInstanceManager from "./internal/ActorInstanceManager.ts"; import * as ActorStateAdapter from "./internal/ActorStateAdapter.ts"; +import { makeActorLogAnnotations } from "./internal/logging.ts"; import type * as StateOptions from "./internal/StateOptions.ts"; import * as Registry from "./Registry.ts"; import type * as RivetError from "./RivetError.ts"; @@ -199,12 +200,12 @@ export type AccessorKeyParam = string | Rivetkit.ActorKey; export type Handle = { readonly [A in Actions as Action.Tag]: ( payload: Action.PayloadConstructor, - ) => Effect.Effect< - Action.Success, - Action.Error | RivetError.RivetError, - Action.ServicesClient - >; - }; + ) => Effect.Effect< + Action.Success, + Action.Error | RivetError.RivetError, + Action.ServicesClient + >; +}; /** * Yielded by `Actor.client`. Address an actor instance by key, then @@ -485,7 +486,12 @@ const makeRivetkitActor = Effect.fnUntraced(function* < Database, WakeOptionsFor >({ - wakeHandler, + wakeHandler: (wakeOptions) => + wakeHandler(wakeOptions).pipe( + Effect.annotateLogs( + makeActorLogAnnotations(wakeOptions.rawRivetkitContext), + ), + ), stateAdapter, makeContext: (c, scope) => Context.mergeAll( diff --git a/rivetkit-typescript/packages/effect/src/Client.test.ts b/rivetkit-typescript/packages/effect/src/Client.test.ts index 0648c200ce..854e2ca56c 100644 --- a/rivetkit-typescript/packages/effect/src/Client.test.ts +++ b/rivetkit-typescript/packages/effect/src/Client.test.ts @@ -1,9 +1,108 @@ import { assert, describe, it } from "@effect/vitest"; -import { Client, RivetError } from "@rivetkit/effect"; -import { Effect, Schema } from "effect"; +import { Client, Logger, RivetError } from "@rivetkit/effect"; +import { Effect, Layer, Schema } from "effect"; import * as RivetkitErrors from "rivetkit/errors"; +import { + configureDefaultLogger, + getBaseLogger, + type Logger as PinoLogger, +} from "rivetkit/log"; import * as ActionErrorEnvelope from "./internal/ActionErrorEnvelope"; +function makeTestLogger( + entries?: Array<{ + readonly level: string; + readonly fields: Record; + readonly msg: string | undefined; + }>, +): PinoLogger { + const logger: Record = { + level: "debug", + child: () => logger, + }; + for (const level of [ + "trace", + "debug", + "info", + "warn", + "error", + "fatal", + ]) { + logger[level] = ( + fields: Record, + msg?: string, + ): void => { + entries?.push({ level, fields, msg }); + }; + } + + return logger as unknown as PinoLogger; +} + +describe("Client", () => { + it.effect("configures the underlying RivetKit client logger", () => + Effect.scoped( + Effect.gen(function* () { + const baseLogger = makeTestLogger(); + + yield* Effect.addFinalizer(() => + Effect.sync(() => configureDefaultLogger("silent")), + ); + yield* Client.make({ + endpoint: "http://127.0.0.1:6420", + }).pipe(Effect.provide(Logger.layerPino(baseLogger))); + + assert.strictEqual(getBaseLogger(), baseLogger); + }), + ), + ); + + it.effect("installs the RivetKit Effect logger for client programs", () => + Effect.scoped( + Effect.gen(function* () { + const entries: Array<{ + readonly level: string; + readonly fields: Record; + readonly msg: string | undefined; + }> = []; + const baseLogger = makeTestLogger(entries); + + yield* Effect.addFinalizer(() => + Effect.sync(() => configureDefaultLogger("silent")), + ); + yield* Effect.gen(function* () { + yield* Client.Client; + yield* Effect.logInfo("client effect log", { + clientId: "test-client", + }); + }).pipe( + Effect.provide( + Client.layer({ + endpoint: "http://127.0.0.1:6420", + }).pipe( + Layer.provideMerge(Logger.layerPino(baseLogger)), + ), + ), + ); + + assert.deepStrictEqual(entries[0], { + level: "info", + fields: { clientId: "test-client" }, + msg: "client effect log", + }); + assert.ok( + entries.some( + (entry) => + entry.level === "debug" && + (entry.fields as { msg?: unknown }).msg === + "disposing client", + ), + ); + }), + ), + ); +}); + describe("makeRivetkitActionFailureClassifier", () => { const ExpectedError = Schema.Struct({ _tag: Schema.tag("CounterOverflow"), diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts index bc4ed0dbd2..c4e1fcd8c6 100644 --- a/rivetkit-typescript/packages/effect/src/Client.ts +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -1,10 +1,13 @@ import { Context, Effect, Layer, Record, Result, Schema } from "effect"; import * as RivetkitClient from "rivetkit/client"; import * as RivetkitErrors from "rivetkit/errors"; +import { configureBaseLogger } from "rivetkit/log"; import type * as Action from "./Action.ts"; import type * as Actor from "./Actor.ts"; import * as ActionErrorEnvelope from "./internal/ActionErrorEnvelope.ts"; +import { getOrCreateBaseLogger } from "./internal/logging.ts"; import { rpcSystem, type TraceMeta } from "./internal/tracing.ts"; +import * as Logger from "./Logger.ts"; import * as RivetError from "./RivetError.ts"; const TypeId = "~@rivetkit/effect/Client"; @@ -43,8 +46,12 @@ export const Client: Context.Service = Context.Service( ); export const make = Effect.fnUntraced(function* (options: Options = {}) { + const baseLogger = yield* getOrCreateBaseLogger; const rivetkitClient = yield* Effect.acquireRelease( - Effect.sync(() => RivetkitClient.createClient(options)), + Effect.sync(() => { + configureBaseLogger(baseLogger); + return RivetkitClient.createClient(options); + }), (c) => Effect.promise(() => c.dispose()), ); @@ -136,7 +143,13 @@ export const make = Effect.fnUntraced(function* (options: Options = {}) { }); export const layer = (options: Options = {}): Layer.Layer => - Layer.effect(Client, make(options)); + Layer.unwrap( + Effect.map(getOrCreateBaseLogger, (baseLogger) => + Layer.effect(Client, make(options)).pipe( + Layer.provideMerge(Logger.layerPino(baseLogger)), + ), + ), + ); const decodeActionErrorEnvelope = Schema.decodeUnknownEffect( ActionErrorEnvelope.ActionErrorEnvelope, diff --git a/rivetkit-typescript/packages/effect/src/Logger.ts b/rivetkit-typescript/packages/effect/src/Logger.ts new file mode 100644 index 0000000000..bb1139a7e5 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Logger.ts @@ -0,0 +1,43 @@ +import { Effect, Logger as EffectLogger, Layer } from "effect"; +import type { Logger as PinoLogger } from "rivetkit/log"; +import { + BaseLogger, + getOrCreateBaseLogger, + makeEffectLogger, +} from "./internal/logging.ts"; + +/** + * Builds a logging layer from a custom Pino-compatible logger. + * + * The layer installs the matching Effect logger and configures the underlying + * RivetKit TypeScript SDK logs to go through the same logger. + * + * @example + * ```ts + * import { Logger } from "@rivetkit/effect" + * import { pino } from "pino" + * + * const LoggerLive = Logger.layerPino( + * pino({ transport: { target: "pino-pretty" } }) + * ) + * ``` + */ +export const layerPino = (baseLogger: PinoLogger) => + Layer.mergeAll( + Layer.succeed(BaseLogger, baseLogger), + EffectLogger.layer([ + EffectLogger.tracerLogger, + makeEffectLogger(baseLogger), + ]), + ); + +/** + * Default RivetKit Effect logging layer. + * + * The layer creates a base logger from `References.MinimumLogLevel` and installs + * the Effect logger adapter. Applications that want custom formatting or + * transports should provide {@link layerPino} instead. + */ +export const layer: Layer.Layer = Layer.unwrap( + Effect.map(getOrCreateBaseLogger, layerPino), +); diff --git a/rivetkit-typescript/packages/effect/src/Registry.test.ts b/rivetkit-typescript/packages/effect/src/Registry.test.ts index ad3156124c..e015447b96 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.test.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.test.ts @@ -1,7 +1,12 @@ import { assert, describe, it } from "@effect/vitest"; -import { Action, Actor, Registry } from "@rivetkit/effect"; +import { Action, Actor, Logger, Registry } from "@rivetkit/effect"; import { Effect, Layer } from "effect"; import { HttpEffect } from "effect/unstable/http"; +import { + configureDefaultLogger, + getBaseLogger, + type Logger as PinoLogger, +} from "rivetkit/log"; import { vi } from "vitest"; const TestActor = Actor.make("TestActor", { @@ -23,6 +28,25 @@ const RegistryLive = ActorsLayer.pipe( ), ); +function makeTestLogger(): PinoLogger { + const logger: Record = { + level: "debug", + child: () => logger, + }; + for (const level of [ + "trace", + "debug", + "info", + "warn", + "error", + "fatal", + ]) { + logger[level] = (): void => {}; + } + + return logger as unknown as PinoLogger; +} + describe("Registry.toWebHandler", () => { it("serves registered actors as a Fetch handler", async () => { const { handler, dispose } = Registry.toWebHandler(RegistryLive); @@ -174,6 +198,58 @@ describe("Registry.toWebHandler", () => { } }); + it("initializes the underlying RivetKit registry once across requests", async () => { + const log = vi.spyOn(console, "log").mockImplementation(() => {}); + const WelcomeRegistryLive = ActorsLayer.pipe( + Layer.provideMerge( + Registry.layer({ + endpoint: "http://127.0.0.1:6420", + }), + ), + ); + const { handler, dispose } = + Registry.toWebHandler(WelcomeRegistryLive); + + try { + const first = await handler( + new Request("http://runner.test/api/rivet/metadata"), + ); + const callsAfterFirst = log.mock.calls.length; + const second = await handler( + new Request("http://runner.test/api/rivet/metadata"), + ); + + assert.strictEqual(first.status, 200); + assert.strictEqual(second.status, 200); + assert.ok(callsAfterFirst > 0); + assert.strictEqual(log.mock.calls.length, callsAfterFirst); + } finally { + await dispose(); + log.mockRestore(); + } + }); + + it("uses a custom logger layer for the underlying RivetKit registry", async () => { + const baseLogger = makeTestLogger(); + const CustomLoggerRegistryLive = RegistryLive.pipe( + Layer.provide(Logger.layerPino(baseLogger)), + ); + const { handler, dispose } = + Registry.toWebHandler(CustomLoggerRegistryLive); + + try { + const response = await handler( + new Request("http://runner.test/api/rivet/metadata"), + ); + + assert.strictEqual(response.status, 200); + assert.strictEqual(getBaseLogger(), baseLogger); + } finally { + await dispose(); + configureDefaultLogger("silent"); + } + }); + it("closes registry layer finalizers on dispose", async () => { let finalizers = 0; const FinalizedRegistryLive = Layer.mergeAll( diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts index f1095f7e28..71b72e7d1b 100644 --- a/rivetkit-typescript/packages/effect/src/Registry.ts +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -7,7 +7,13 @@ import { type HttpServerResponse, } from "effect/unstable/http"; import * as Rivetkit from "rivetkit"; +import { + configureBaseLogger, + type Logger as PinoLogger, +} from "rivetkit/log"; import * as Client from "./Client.ts"; +import { BaseLogger, getOrCreateBaseLogger } from "./internal/logging.ts"; +import * as Logger from "./Logger.ts"; const TypeId = "~@rivetkit/effect/Registry"; type ServerlessOptions = NonNullable< @@ -24,36 +30,47 @@ export interface Registry { readonly options: Options; + readonly baseLogger: PinoLogger; + readonly rivetkitActors: Map; } export const Registry: Context.Service = Context.Service("@rivetkit/effect/Registry"); -const make = (options: Options = {}): Registry => { +const make = (options: Options, baseLogger: PinoLogger): Registry => { return Registry.of({ [TypeId]: TypeId, options, + baseLogger, rivetkitActors: new Map(), }); }; export const layer = (options: Options = {}): Layer.Layer => - Layer.succeed(Registry, make(options)); + Layer.effect( + Registry, + Effect.map(getOrCreateBaseLogger, (baseLogger) => + make(options, baseLogger), + ), + ); const setupRivetkitRegistry = ( registry: Registry, options?: { readonly serverless?: ServerlessOptions | undefined; }, -) => - Rivetkit.setup({ +) => { + configureBaseLogger(registry.baseLogger); + return Rivetkit.setup({ use: Object.fromEntries(registry.rivetkitActors), ...registry.options, + logging: { baseLogger: registry.baseLogger }, ...(options?.serverless === undefined ? {} : { serverless: options.serverless }), }); +}; /** * Runs an actor registration layer against the configured engine. @@ -64,14 +81,19 @@ const setupRivetkitRegistry = ( */ export const serve = ( actorsLayer: Layer.Layer, -): Layer.Layer => - Layer.effectDiscard( - Effect.gen(function* () { - yield* Layer.build(actorsLayer); - const registry = yield* Registry; - const rivetkitRegistry = setupRivetkitRegistry(registry); - yield* Effect.sync(() => rivetkitRegistry.start()); - }), + ): Layer.Layer => + Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* Registry; + const baseLogger = registry.baseLogger; + yield* Layer.build( + actorsLayer.pipe( + Layer.provideMerge(Logger.layerPino(baseLogger)), + ), + ); + const rivetkitRegistry = setupRivetkitRegistry(registry); + yield* Effect.sync(() => rivetkitRegistry.start()); + }), ); /** @@ -115,12 +137,14 @@ export const test: Layer.Layer = Layer.effect( // to its (warning-emitting) default. const resolvedEndpoint = rivetkitRegistry.parseConfig().endpoint; - return yield* Client.make({ - ...registry.options, - endpoint: registry.options.endpoint ?? resolvedEndpoint, - }); - }), -); + return yield* Client.make({ + ...registry.options, + endpoint: registry.options.endpoint ?? resolvedEndpoint, + }).pipe( + Effect.provideService(BaseLogger, registry.baseLogger), + ); + }), + ); const makeHttpEffect = ( registry: Registry, @@ -159,9 +183,13 @@ export const toHttpEffect = Effect.fnUntraced(function* ( E, Scope.Scope > { - const context = yield* Layer.build(registryLayer); + const context = yield* Layer.build( + registryLayer.pipe(Layer.provideMerge(Logger.layer)), + ); // @effect-diagnostics-next-line returnEffectInGen:off - return makeHttpEffect(Context.get(context, Registry), options); + return makeHttpEffect(Context.get(context, Registry), options).pipe( + Effect.provide(context), + ); }); export type ToWebHandlerOptions = ServerlessOptions & { @@ -197,12 +225,18 @@ export const toWebHandler = ( serverlessOptions = handlerOptions; } - return HttpEffect.toWebHandlerLayerWith(registryLayer, { + const registryLayerWithLogging = registryLayer.pipe( + Layer.provideMerge(Logger.layer), + ); + + return HttpEffect.toWebHandlerLayerWith(registryLayerWithLogging, { toHandler: (context) => - Effect.sync(() => { - const registry = Context.get(context, Registry); - return makeHttpEffect(registry, serverlessOptions); - }), + Effect.succeed( + makeHttpEffect( + Context.get(context, Registry), + serverlessOptions, + ).pipe(Effect.provide(context)), + ), middleware, memoMap, }); diff --git a/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts b/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts index 547bdba892..21cc63c42f 100644 --- a/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts +++ b/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts @@ -17,6 +17,7 @@ import type { } from "../Actor.ts"; import type * as Client from "../Client.ts"; import * as ActionErrorEnvelope from "./ActionErrorEnvelope.ts"; +import { makeActorLogAnnotations } from "./logging.ts"; import { readTraceMeta, rpcSystem } from "./tracing.ts"; import { hasStringProperty } from "./utils.ts"; @@ -159,6 +160,7 @@ export const make = < "rpc.method": rpcMethod, }, }), + Effect.annotateLogs(makeActorLogAnnotations(c)), ); const fiber = instance.runFork(actionEffect, { signal: c.abortSignal, diff --git a/rivetkit-typescript/packages/effect/src/internal/logging.test.ts b/rivetkit-typescript/packages/effect/src/internal/logging.test.ts new file mode 100644 index 0000000000..eca5943b91 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/internal/logging.test.ts @@ -0,0 +1,288 @@ +import { assert, describe, it } from "@effect/vitest"; +import { + Config, + ConfigProvider, + Effect, + Layer, + Logger as EffectLogger, + References, +} from "effect"; +import type { Logger as PinoLogger } from "rivetkit/log"; +import * as Logging from "./logging.ts"; + +type LogEntry = { + readonly level: string; + readonly fields: Record; + readonly msg: string | undefined; +}; + +function makeTestLogger(entries: Array): PinoLogger { + const logger: Record = {}; + for (const level of [ + "trace", + "debug", + "info", + "warn", + "error", + "fatal", + ]) { + logger[level] = ( + fields: Record, + msg?: string, + ): void => { + entries.push({ level, fields, msg }); + }; + } + + return logger as unknown as PinoLogger; +} + +describe("internal/logging", () => { + it("serializes actor keys like the RivetKit actor runtime logger", () => { + assert.strictEqual(Logging.serializeActorKey([]), "/"); + assert.strictEqual(Logging.serializeActorKey(["room", "1"]), "room/1"); + assert.strictEqual(Logging.serializeActorKey(["room/1"]), "room\\/1"); + assert.strictEqual(Logging.serializeActorKey([""]), "\\0"); + assert.strictEqual(Logging.serializeActorKey(["a\\b"]), "a\\\\b"); + }); + + it("builds actor log annotations with serialized keys", () => { + assert.deepStrictEqual( + Logging.makeActorLogAnnotations({ + name: "ChatRoom", + key: ["room/1"], + actorId: "actor-1", + }), + { + actor: "ChatRoom", + key: "room\\/1", + actorId: "actor-1", + }, + ); + }); + + it.effect("writes Effect logs through the RivetKit base logger", () => + Effect.gen(function* () { + const entries: Array = []; + const baseLogger = makeTestLogger(entries); + + yield* Effect.logInfo("room awake", { roomId: "abc" }).pipe( + Effect.annotateLogs({ + actor: "ChatRoom", + key: "room-1", + actorId: "actor-1", + }), + Effect.provide( + EffectLogger.layer([Logging.makeEffectLogger(baseLogger)]), + ), + ); + + assert.deepStrictEqual(entries, [ + { + level: "info", + fields: { + roomId: "abc", + actor: "ChatRoom", + key: "room-1", + actorId: "actor-1", + }, + msg: "room awake", + }, + ]); + }), + ); + + it.effect("preserves Error log messages as structured error fields", () => + Effect.gen(function* () { + const entries: Array = []; + const baseLogger = makeTestLogger(entries); + const error = new Error("room failed to wake"); + + yield* Effect.logError(error).pipe( + Effect.provide( + EffectLogger.layer([Logging.makeEffectLogger(baseLogger)]), + ), + ); + + const entry = entries[0]; + assert.ok(entry !== undefined); + assert.strictEqual(entry.level, "error"); + assert.strictEqual(entry.fields.error, error); + assert.strictEqual(entry.msg, error.message); + }), + ); + + it.effect("preserves Error log messages with additional fields", () => + Effect.gen(function* () { + const entries: Array = []; + const baseLogger = makeTestLogger(entries); + const error = new Error("action dispatch failed"); + + yield* Effect.logError(error, { + actorId: "actor-1", + action: "SendMessage", + }).pipe( + Effect.provide( + EffectLogger.layer([Logging.makeEffectLogger(baseLogger)]), + ), + ); + + const entry = entries[0]; + assert.ok(entry !== undefined); + assert.strictEqual(entry.level, "error"); + assert.strictEqual(entry.fields.error, error); + assert.strictEqual(entry.fields.actorId, "actor-1"); + assert.strictEqual(entry.fields.action, "SendMessage"); + assert.strictEqual(entry.msg, error.message); + }), + ); + + it.effect("uses References.MinimumLogLevel when creating the base logger", () => + Effect.gen(function* () { + const baseLogger = yield* Logging.makeDefaultBaseLogger; + + assert.strictEqual(baseLogger.level, "debug"); + }).pipe(Effect.provideService(References.MinimumLogLevel, "Debug")), + ); + + it.effect("accepts the shared Pino RIVET_LOG_LEVEL values", () => + Effect.gen(function* () { + const baseLogger = yield* Logging.makeDefaultBaseLogger; + + assert.strictEqual(baseLogger.level, "silent"); + }).pipe( + Effect.provideService( + ConfigProvider.ConfigProvider, + ConfigProvider.fromEnv({ + env: { + RIVET_LOG_LEVEL: "silent", + }, + }), + ), + ), + ); + + it.effect("prefers References.MinimumLogLevel over shared env values", () => + Effect.gen(function* () { + const baseLogger = yield* Logging.makeDefaultBaseLogger; + + assert.strictEqual(baseLogger.level, "debug"); + }).pipe( + Effect.provideService(References.MinimumLogLevel, "Debug"), + Effect.provideService( + ConfigProvider.ConfigProvider, + ConfigProvider.fromEnv({ + env: { + RIVET_LOG_LEVEL: "silent", + }, + }), + ), + ), + ); + + it.effect("preserves an explicit Info minimum log level", () => + Effect.gen(function* () { + const baseLogger = yield* Logging.makeDefaultBaseLogger; + + assert.strictEqual(baseLogger.level, "info"); + }).pipe( + Effect.provideService(References.MinimumLogLevel, "Info"), + Effect.provideService( + ConfigProvider.ConfigProvider, + ConfigProvider.fromEnv({ + env: { + RIVET_LOG_LEVEL: "silent", + }, + }), + ), + ), + ); + + it.effect("uses Config.logLevel values provided to References.MinimumLogLevel", () => + Effect.gen(function* () { + const baseLogger = yield* Logging.makeDefaultBaseLogger; + + assert.strictEqual(baseLogger.level, "trace"); + }).pipe( + Effect.provide( + Layer.effect( + References.MinimumLogLevel, + Config.logLevel("RIVET_LOG_LEVEL"), + ), + ), + Effect.provideService( + ConfigProvider.ConfigProvider, + ConfigProvider.fromEnv({ + env: { + RIVET_LOG_LEVEL: "Trace", + }, + }), + ), + ), + ); + + it.effect("uses References.CurrentLogLevel for plain Effect.log calls", () => + Effect.gen(function* () { + const entries: Array = []; + const baseLogger = makeTestLogger(entries); + + yield* Effect.log("plain log").pipe( + Effect.provideService(References.CurrentLogLevel, "Debug"), + Effect.provideService(References.MinimumLogLevel, "Debug"), + Effect.provide( + EffectLogger.layer([Logging.makeEffectLogger(baseLogger)]), + ), + ); + + assert.deepStrictEqual(entries, [ + { + level: "debug", + fields: {}, + msg: "plain log", + }, + ]); + }), + ); + + it.effect("does not call a Pino method for the None current log level", () => + Effect.gen(function* () { + const entries: Array = []; + const baseLogger = makeTestLogger(entries); + + yield* Effect.log("hidden log").pipe( + Effect.provideService(References.CurrentLogLevel, "None"), + Effect.provideService(References.MinimumLogLevel, "All"), + Effect.provide( + EffectLogger.layer([Logging.makeEffectLogger(baseLogger)]), + ), + ); + + assert.deepStrictEqual(entries, []); + }), + ); + + it.effect("emits References.CurrentLogSpans as structured span durations", () => + Effect.gen(function* () { + const entries: Array = []; + const baseLogger = makeTestLogger(entries); + + yield* Effect.logInfo("checkout complete").pipe( + Effect.withLogSpan("checkout"), + Effect.provide( + EffectLogger.layer([Logging.makeEffectLogger(baseLogger)]), + ), + ); + + assert.strictEqual(entries.length, 1); + assert.strictEqual(entries[0]?.level, "info"); + assert.strictEqual(entries[0]?.msg, "checkout complete"); + assert.deepStrictEqual(Object.keys(entries[0]?.fields ?? {}), [ + "spans", + ]); + const spans = entries[0]?.fields.spans as + | Record + | undefined; + assert.strictEqual(typeof spans?.checkout, "number"); + }), + ); +}); diff --git a/rivetkit-typescript/packages/effect/src/internal/logging.ts b/rivetkit-typescript/packages/effect/src/internal/logging.ts new file mode 100644 index 0000000000..fb9145bd91 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/internal/logging.ts @@ -0,0 +1,237 @@ +import { + Cause, + Config, + Context, + Effect, + Logger as EffectLogger, + type LogLevel, + References, +} from "effect"; +import type * as Rivetkit from "rivetkit"; +import { + configureDefaultLogger, + getBaseLogger, + type Logger as PinoLogger, + type LogLevel as PinoLogLevel, +} from "rivetkit/log"; + +const EMPTY_KEY = "/"; +const KEY_SEPARATOR = "/"; + +type ActorLogContext = { + readonly name: string; + readonly key: Rivetkit.ActorKey; + readonly actorId: string; +}; + +export class BaseLogger extends Context.Service()( + "@rivetkit/effect/Logger/BaseLogger", +) {} + +const PinoLevelByEffectLevel: Record = { + All: "trace", + Trace: "trace", + Debug: "debug", + Info: "info", + Warn: "warn", + Error: "error", + Fatal: "fatal", + None: "silent", +}; + +export const toPinoLevel = (logLevel: LogLevel.LogLevel): PinoLogLevel => + PinoLevelByEffectLevel[logLevel]; + +const EffectLevelByPinoLevel: Record = { + trace: "Trace", + debug: "Debug", + info: "Info", + warn: "Warn", + error: "Error", + fatal: "Fatal", + silent: "None", +}; + +const pinoLogLevelFromEnv = Config.string("RIVET_LOG_LEVEL").pipe( + Config.map((value) => { + const pinoLevel = value.toLowerCase(); + if (pinoLevel in EffectLevelByPinoLevel) { + return EffectLevelByPinoLevel[pinoLevel as PinoLogLevel]; + } + + return "Info"; + }), +); + +const logLevelFromEnv = Config.logLevel("RIVET_LOG_LEVEL").pipe( + Config.orElse(() => pinoLogLevelFromEnv), + Effect.option, +); + +export const makeDefaultBaseLogger: Effect.Effect = Effect.gen( + function* () { + const context = yield* Effect.context(); + const providedMinimumLogLevel = Context.getOrUndefined( + context, + References.MinimumLogLevel, + ); + const envLogLevel = yield* logLevelFromEnv; + const logLevel = + providedMinimumLogLevel !== undefined + ? providedMinimumLogLevel + : envLogLevel._tag === "Some" + ? envLogLevel.value + : yield* References.MinimumLogLevel; + + return yield* Effect.sync(() => { + configureDefaultLogger(toPinoLevel(logLevel)); + return getBaseLogger(); + }); + }, +); + +export const getOrCreateBaseLogger: Effect.Effect = Effect.gen( + function* () { + const provided = yield* Effect.serviceOption(BaseLogger); + if (provided._tag === "Some") { + return provided.value; + } + + return yield* makeDefaultBaseLogger; + }, +); + +export function makeActorLogAnnotations(context: ActorLogContext): { + readonly actor: string; + readonly key: string; + readonly actorId: string; +} { + return { + actor: context.name, + key: serializeActorKey(context.key), + actorId: context.actorId, + }; +} + +export function serializeActorKey(key: Rivetkit.ActorKey): string { + if (key.length === 0) { + return EMPTY_KEY; + } + + return key + .map((part) => { + if (part === "") { + return "\\0"; + } + + return part + .replace(/\\/g, "\\\\") + .replace(/\//g, `\\${KEY_SEPARATOR}`); + }) + .join(KEY_SEPARATOR); +} + +function structuredValue(value: unknown): unknown { + if (value instanceof Error) { + return value; + } + + return value; +} + +function extractMessageAndFields(message: unknown): { + readonly msg: string | undefined; + readonly fields: Record; +} { + const values = Array.isArray(message) ? message : [message]; + if (values.length === 0) { + return { msg: undefined, fields: {} }; + } + + const [first, ...rest] = values; + const fields: Record = {}; + let msg: string | undefined; + + if (first instanceof Error) { + fields.error = first; + msg = first.message; + } else if (first !== null && typeof first === "object") { + const firstFields = first as Record; + for (const [key, value] of Object.entries(firstFields)) { + if (key === "msg") { + if (value !== undefined) { + msg = String(value); + } + } else { + fields[key] = structuredValue(value); + } + } + } else if (first !== undefined) { + msg = String(first); + } + + const args: Array = []; + for (const value of rest) { + if (value instanceof Error) { + fields.error = value; + } else if ( + value !== null && + typeof value === "object" && + !Array.isArray(value) + ) { + for (const [key, fieldValue] of Object.entries( + value as Record, + )) { + fields[key] = structuredValue(fieldValue); + } + } else { + args.push(value); + } + } + + if (args.length > 0) { + fields.args = args; + } + + return { msg, fields }; +} + +export function makeEffectLogger( + baseLogger: PinoLogger, +): EffectLogger.Logger { + return EffectLogger.make(({ cause, date, fiber, logLevel, message }) => { + const { msg, fields } = extractMessageAndFields(message); + + for (const [key, value] of Object.entries( + fiber.getRef(References.CurrentLogAnnotations), + )) { + fields[key] = structuredValue(value); + } + + const spans: Record = {}; + for (const [label, startTime] of fiber.getRef( + References.CurrentLogSpans, + )) { + spans[label] = date.getTime() - startTime; + } + if (Object.keys(spans).length > 0) { + fields.spans = spans; + } + + if (cause.reasons.length > 0) { + fields.cause = Cause.pretty(cause); + } + + const pinoLevel = toPinoLevel(logLevel); + if (pinoLevel === "silent") { + return; + } + + const logger = baseLogger[pinoLevel]; + if (msg === undefined) { + logger.call(baseLogger, fields); + } else { + logger.call(baseLogger, fields, msg); + } + }); +} diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts index 6b727a582b..bf15e095ab 100644 --- a/rivetkit-typescript/packages/effect/src/mod.ts +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -1,6 +1,7 @@ export * as Action from "./Action.ts"; export * as Actor from "./Actor.ts"; export * as Client from "./Client.ts"; +export * as Logger from "./Logger.ts"; export * as Registry from "./Registry.ts"; export * as RivetError from "./RivetError.ts"; export * as State from "./State.ts"; From 53fc48e80741c6b4426a9293396bcec272e29176 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 2 Jun 2026 22:11:55 +0200 Subject: [PATCH 301/306] fix(effect): mark rivetkit as peer dependency --- pnpm-lock.yaml | 7 +++---- rivetkit-typescript/packages/effect/package.json | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c13dfcc935..7b70ee48c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4119,10 +4119,6 @@ importers: version: 5.9.3 rivetkit-typescript/packages/effect: - dependencies: - rivetkit: - specifier: workspace:* - version: link:../rivetkit devDependencies: '@arethetypeswrong/cli': specifier: ^0.18.3 @@ -4145,6 +4141,9 @@ importers: publint: specifier: ^0.3.21 version: 0.3.21 + rivetkit: + specifier: workspace:* + version: link:../rivetkit typescript: specifier: ^5.9.2 version: 5.9.3 diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index 0e268da919..c92483d562 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -29,11 +29,9 @@ "test": "vitest --typecheck", "coverage": "vitest run --coverage" }, - "dependencies": { - "rivetkit": "workspace:*" - }, "peerDependencies": { - "effect": "^4.0.0-beta.66" + "effect": "^4.0.0-beta.66", + "rivetkit": "workspace:*" }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.3", @@ -43,6 +41,7 @@ "@vitest/coverage-v8": "^4.1.7", "effect": "^4.0.0-beta.66", "publint": "^0.3.21", + "rivetkit": "workspace:*", "typescript": "^5.9.2", "vitest": "^4.1.5" } From d64c62534e0f8670a8c639dd822cf90e3111d13c Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 2 Jun 2026 22:14:32 +0200 Subject: [PATCH 302/306] fix(effect): rely on peer dependencies --- pnpm-lock.yaml | 97 ++++++++++--------- .../packages/effect/package.json | 2 - 2 files changed, 49 insertions(+), 50 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b70ee48c3..ecf6131cea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3375,7 +3375,7 @@ importers: version: 5.2.2(react-hook-form@7.62.0(react@19.1.0)) '@ladle/react': specifier: ^5.1.1 - version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) '@marsidev/react-turnstile': specifier: ^1.5.0 version: 1.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -3624,7 +3624,7 @@ importers: version: 2.4.3 better-auth: specifier: ^1.5.6 - version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) canvas-confetti: specifier: ^1.9.3 version: 1.9.3 @@ -3744,7 +3744,7 @@ importers: version: 5.2.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.13)(typescript@5.9.3))(typescript@5.9.3) unplugin-macros: specifier: ^0.18.3 - version: 0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + version: 0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.1.0) @@ -3766,7 +3766,7 @@ importers: version: 2.14.4(@types/node@20.19.13)(typescript@5.9.3) vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) frontend/packages/components: dependencies: @@ -4119,6 +4119,13 @@ importers: version: 5.9.3 rivetkit-typescript/packages/effect: + dependencies: + effect: + specifier: ^4.0.0-beta.66 + version: 4.0.0-beta.66 + rivetkit: + specifier: workspace:* + version: link:../rivetkit devDependencies: '@arethetypeswrong/cli': specifier: ^0.18.3 @@ -4135,15 +4142,9 @@ importers: '@vitest/coverage-v8': specifier: ^4.1.7 version: 4.1.7(vitest@4.1.7) - effect: - specifier: ^4.0.0-beta.66 - version: 4.0.0-beta.66 publint: specifier: ^0.3.21 version: 0.3.21 - rivetkit: - specifier: workspace:* - version: link:../rivetkit typescript: specifier: ^5.9.2 version: 5.9.3 @@ -20933,7 +20934,7 @@ snapshots: terminal-link: 2.1.1 undici: 6.24.1 wrap-ansi: 7.0.0 - ws: 8.19.0 + ws: 8.20.1 optionalDependencies: expo-router: 4.0.21(expo-constants@18.0.13)(expo-linking@7.0.5)(expo@54.0.18)(react-dom@19.1.0(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native-screens@4.17.1(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0))(react-native@0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0))(react@19.1.0) react-native: 0.82.1(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.1.0) @@ -21105,7 +21106,7 @@ snapshots: '@expo/mcp-tunnel@0.0.8(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))': dependencies: - ws: 8.19.0 + ws: 8.20.1 zod: 3.25.76 zod-to-json-schema: 3.25.1(zod@3.25.76) optionalDependencies: @@ -21836,7 +21837,7 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)': + '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)': dependencies: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0 @@ -21848,8 +21849,8 @@ snapshots: '@ladle/react-context': 1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mdx-js/mdx': 3.1.1 '@mdx-js/react': 3.1.1(@types/react@19.2.13)(react@19.1.0) - '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) - '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) axe-core: 4.11.1 boxen: 8.0.1 chokidar: 4.0.3 @@ -21876,8 +21877,8 @@ snapshots: remark-gfm: 4.0.1 source-map: 0.7.6 vfile: 6.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) - vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - '@swc/helpers' - '@types/node' @@ -23661,7 +23662,7 @@ snapshots: '@react-native/codegen@0.81.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.3 glob: 7.2.3 hermes-parser: 0.29.1 invariant: 2.2.4 @@ -23671,7 +23672,7 @@ snapshots: '@react-native/codegen@0.82.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.3 glob: 7.2.3 hermes-parser: 0.32.0 invariant: 2.2.4 @@ -25973,11 +25974,11 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 - '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.27 '@swc/core': 1.15.11(@swc/helpers@0.5.17) - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - '@swc/helpers' @@ -26017,7 +26018,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -26025,7 +26026,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -26142,14 +26143,14 @@ snapshots: msw: 2.14.4(@types/node@22.19.15)(typescript@5.9.3) vite: 5.4.21(@types/node@22.19.15)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) - '@vitest/mocker@4.0.18(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/mocker@4.0.18(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.14.4(@types/node@20.19.13)(typescript@5.9.3) - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) '@vitest/mocker@4.1.7(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: @@ -27013,7 +27014,7 @@ snapshots: bcryptjs@2.4.3: {} - better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)): + better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0) '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0)) @@ -27040,7 +27041,7 @@ snapshots: pg: 8.17.2 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -30079,7 +30080,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.3 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -30970,7 +30971,7 @@ snapshots: metro-cache: 0.83.2 metro-core: 0.83.2 metro-runtime: 0.83.2 - yaml: 2.8.2 + yaml: 2.9.0 transitivePeerDependencies: - bufferutil - supports-color @@ -30985,7 +30986,7 @@ snapshots: metro-cache: 0.83.5 metro-core: 0.83.5 metro-runtime: 0.83.5 - yaml: 2.8.2 + yaml: 2.9.0 transitivePeerDependencies: - bufferutil - supports-color @@ -31136,7 +31137,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.3 '@babel/types': 7.29.0 flow-enums-runtime: 0.0.6 metro: 0.83.2 @@ -31156,7 +31157,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.3 '@babel/types': 7.29.0 flow-enums-runtime: 0.0.6 metro: 0.83.5 @@ -31177,7 +31178,7 @@ snapshots: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0 '@babel/generator': 7.29.1 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.3 '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 @@ -31224,7 +31225,7 @@ snapshots: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0 '@babel/generator': 7.29.1 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.3 '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 @@ -34848,13 +34849,13 @@ snapshots: unpipe@1.0.0: {} - unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: ast-kit: 2.2.0 magic-string-ast: 1.0.3 unplugin: 2.3.10 - vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) - vite-node: 5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite-node: 5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -35144,13 +35145,13 @@ snapshots: - supports-color - terser - vite-node@5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vite-node@5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: cac: 6.7.14 es-module-lexer: 1.7.0 obug: 2.0.0(ms@2.1.3) pathe: 2.0.3 - vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -35223,13 +35224,13 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - typescript @@ -35287,7 +35288,7 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 - vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -35298,7 +35299,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 1.21.7 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -35347,7 +35348,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vite@7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -35358,7 +35359,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 1.21.7 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -35652,10 +35653,10 @@ snapshots: - supports-color - terser - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/mocker': 4.0.18(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -35672,7 +35673,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 diff --git a/rivetkit-typescript/packages/effect/package.json b/rivetkit-typescript/packages/effect/package.json index c92483d562..cb081b7394 100644 --- a/rivetkit-typescript/packages/effect/package.json +++ b/rivetkit-typescript/packages/effect/package.json @@ -39,9 +39,7 @@ "@effect/vitest": "^4.0.0-beta.66", "@types/node": "^22.18.1", "@vitest/coverage-v8": "^4.1.7", - "effect": "^4.0.0-beta.66", "publint": "^0.3.21", - "rivetkit": "workspace:*", "typescript": "^5.9.2", "vitest": "^4.1.5" } From f5d45751f1aa0e8690f127d5a015bf10243ee807 Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Tue, 2 Jun 2026 22:28:46 +0200 Subject: [PATCH 303/306] fix(effect): add shared vitest config to turbo.json --- rivetkit-typescript/packages/effect/turbo.json | 1 + 1 file changed, 1 insertion(+) diff --git a/rivetkit-typescript/packages/effect/turbo.json b/rivetkit-typescript/packages/effect/turbo.json index e053d2d9a1..5e7787cf4e 100644 --- a/rivetkit-typescript/packages/effect/turbo.json +++ b/rivetkit-typescript/packages/effect/turbo.json @@ -48,6 +48,7 @@ "tsconfig.json", "vitest.config.ts", "../../../tsconfig.base.json", + "../../../vitest.base.ts", "../rivetkit/tests/shared-engine.ts" ] } From 0d308806c595a81abf1145e22acd59382143b4fe Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 12 Jun 2026 12:58:39 +0200 Subject: [PATCH 304/306] chore: remove formatting-only PR changes --- rivetkit-rust/packages/rivetkit-core/src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivetkit-rust/packages/rivetkit-core/src/error.rs b/rivetkit-rust/packages/rivetkit-core/src/error.rs index 5dbf26a17c..4241551135 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/error.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/error.rs @@ -4,7 +4,6 @@ use serde_json::Value as JsonValue; pub fn public_error_status_code(group: &str, code: &str) -> Option { match (group, code) { - ("user", _) => Some(400), ("auth", "forbidden") => Some(403), ("actor", "action_not_found") => Some(404), ("actor", "action_timed_out") => Some(408), @@ -22,6 +21,7 @@ pub fn public_error_status_code(group: &str, code: &str) -> Option { | "complete_not_configured" | "timed_out", ) => Some(400), + ("user", _) => Some(400), _ => None, } } From 96bb65bbd15b03d92fb4436e18774a1d2d3c5d7c Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 12 Jun 2026 13:07:56 +0200 Subject: [PATCH 305/306] chore: restore pre-existing formatting --- .../scripts/counter-latency/src/main.rs | 2 +- .../scripts/counter-latency/src/tee.rs | 15 +- .../scripts/counter-latency/src/ws.rs | 4 +- .../actors/actor-details-skeleton.tsx | 5 +- .../src/components/actors/actor-not-found.tsx | 4 +- .../client/src/simple/async/handle.rs | 329 +++++++++--------- .../client/src/simple/sync/handle.rs | 291 ++++++++-------- .../rivetkit-core/tests/modules/connection.rs | 6 +- .../rivetkit-core/tests/modules/queue.rs | 16 +- .../rivetkit/tests/registry-shutdown.test.ts | 9 +- 10 files changed, 335 insertions(+), 346 deletions(-) diff --git a/examples/kitchen-sink/scripts/counter-latency/src/main.rs b/examples/kitchen-sink/scripts/counter-latency/src/main.rs index 5076a8470d..5197423aa1 100644 --- a/examples/kitchen-sink/scripts/counter-latency/src/main.rs +++ b/examples/kitchen-sink/scripts/counter-latency/src/main.rs @@ -21,7 +21,7 @@ use std::sync::Arc; use crate::args::{Args, EnvConfig}; use crate::concurrent::{WorkloadCtx, print_concurrent_summary, spawn_scale_down}; use crate::endpoint::Endpoint; -use crate::log::{BOLD, COLOR_MAX_MS, COLOR_MIN_MS, DIM, RESET, gradient_color}; +use crate::log::{BOLD, COLOR_MIN_MS, COLOR_MAX_MS, DIM, RESET, gradient_color}; use crate::stats::State; fn main() { diff --git a/examples/kitchen-sink/scripts/counter-latency/src/tee.rs b/examples/kitchen-sink/scripts/counter-latency/src/tee.rs index 602b78afe3..d12c47c274 100644 --- a/examples/kitchen-sink/scripts/counter-latency/src/tee.rs +++ b/examples/kitchen-sink/scripts/counter-latency/src/tee.rs @@ -11,15 +11,12 @@ static LOG_FILE_PATH: OnceLock = OnceLock::new(); pub fn init(id: &str) -> std::io::Result { let path = format!("/tmp/counter-latency-{}.txt", id); let file = File::create(&path)?; - LOG_FILE.set(Mutex::new(file)).map_err(|_| { - std::io::Error::new( - std::io::ErrorKind::AlreadyExists, - "log file already initialized", - ) - })?; - LOG_FILE_PATH.set(path.clone()).map_err(|_| { - std::io::Error::new(std::io::ErrorKind::AlreadyExists, "log path already set") - })?; + LOG_FILE + .set(Mutex::new(file)) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::AlreadyExists, "log file already initialized"))?; + LOG_FILE_PATH + .set(path.clone()) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::AlreadyExists, "log path already set"))?; Ok(path) } diff --git a/examples/kitchen-sink/scripts/counter-latency/src/ws.rs b/examples/kitchen-sink/scripts/counter-latency/src/ws.rs index 0c939668be..f6b90b481a 100644 --- a/examples/kitchen-sink/scripts/counter-latency/src/ws.rs +++ b/examples/kitchen-sink/scripts/counter-latency/src/ws.rs @@ -18,8 +18,6 @@ pub async fn open_raw_ws(url: &str) -> Result { "Sec-WebSocket-Protocol", HeaderValue::from_static("rivet, rivet_encoding.json"), ); - let (ws, _resp) = connect_async(req) - .await - .context("websocket connect failed")?; + let (ws, _resp) = connect_async(req).await.context("websocket connect failed")?; Ok(ws) } diff --git a/frontend/src/components/actors/actor-details-skeleton.tsx b/frontend/src/components/actors/actor-details-skeleton.tsx index babfb3c3ac..55f9d6de70 100644 --- a/frontend/src/components/actors/actor-details-skeleton.tsx +++ b/frontend/src/components/actors/actor-details-skeleton.tsx @@ -49,7 +49,10 @@ export function ActorDetailsSkeleton({ shimmer, children, className }: Props) { return (
diff --git a/frontend/src/components/actors/actor-not-found.tsx b/frontend/src/components/actors/actor-not-found.tsx index b36b5d1286..cad44737b0 100644 --- a/frontend/src/components/actors/actor-not-found.tsx +++ b/frontend/src/components/actors/actor-not-found.tsx @@ -29,9 +29,7 @@ export function ActorNotFound({ actorId }: { actorId?: ActorId }) { icon={faQuestionSquare} className="text-4xl" /> -

- {copy.actorNotFound} -

+

{copy.actorNotFound}

{copy.actorNotFoundDescription}

diff --git a/rivetkit-python/client/src/simple/async/handle.rs b/rivetkit-python/client/src/simple/async/handle.rs index a31d3e79ff..f2f6459bb6 100644 --- a/rivetkit-python/client/src/simple/async/handle.rs +++ b/rivetkit-python/client/src/simple/async/handle.rs @@ -1,180 +1,185 @@ -use futures_util::FutureExt; -use pyo3::{ - prelude::*, - types::{PyList, PyString, PyTuple}, -}; use rivetkit_client::{self as rivetkit_rs}; +use futures_util::FutureExt; +use pyo3::{prelude::*, types::{PyList, PyString, PyTuple}}; use tokio::sync::mpsc; use crate::util; struct ActorEvent { - name: String, - args: Vec, + name: String, + args: Vec, } #[pyclass] pub struct ActorHandle { - handle: rivetkit_rs::connection::ActorHandle, - event_rx: Option>, - event_tx: mpsc::UnboundedSender, + handle: rivetkit_rs::connection::ActorHandle, + event_rx: Option>, + event_tx: mpsc::UnboundedSender, } impl ActorHandle { - pub fn new(handle: rivetkit_rs::connection::ActorHandle) -> Self { - let (event_tx, event_rx) = mpsc::unbounded_channel(); - - Self { - handle, - event_tx, - event_rx: Some(event_rx), - } - } + pub fn new(handle: rivetkit_rs::connection::ActorHandle) -> Self { + let (event_tx, event_rx) = mpsc::unbounded_channel(); + + Self { + handle, + event_tx, + event_rx: Some(event_rx), + } + } } #[pymethods] impl ActorHandle { - #[new] - pub fn py_new() -> PyResult { - Err(py_runtime_err!( - "Actor handle cannot be instantiated directly", - )) - } - - pub fn action<'a>( - &self, - py: Python<'a>, - method: &str, - args: Vec, - ) -> PyResult> { - let method = method.to_string(); - let handle = self.handle.clone(); - - pyo3_async_runtimes::tokio::future_into_py(py, async move { - let args = Python::with_gil(|py| util::py_to_json_value(py, &args))?; - let result = handle.action(&method, args).await; - let Ok(result) = result else { - return Err(py_runtime_err!("Failed to call action: {:?}", result.err())); - }; - let mut result = - Python::with_gil(|py| match util::json_to_py_value(py, &vec![result]) { - Ok(value) => Ok(value - .iter() - .map(|x| x.clone().unbind()) - .collect::>()), - Err(e) => Err(e), - })?; - let Some(result) = result.drain(0..1).next() else { - return Err(py_runtime_err!("Expected one result, got {}", result.len())); - }; - - Ok(result) - }) - } - - pub fn subscribe<'a>(&self, py: Python<'a>, event_name: &str) -> PyResult> { - let event_name = event_name.to_string(); - let handle = self.handle.clone(); - let tx = self.event_tx.clone(); - - pyo3_async_runtimes::tokio::future_into_py(py, async move { - handle - .on_event(&event_name.clone(), move |args| { - let event_name = event_name.clone(); - let args = args.clone(); - let tx = tx.clone(); - - tokio::spawn(async move { - let event = ActorEvent { - name: event_name, - args: args.clone(), - }; - // Send this upstream(?) - tx.send(event) - .map_err(|e| py_runtime_err!("Failed to send via inner tx: {}", e)) - .ok(); - }); - }) - .await; - - Ok(()) - }) - } - - #[pyo3(signature=(count, timeout=None))] - pub fn receive<'a>( - &mut self, - py: Python<'a>, - count: u32, - timeout: Option, - ) -> PyResult> { - let mut rx = self - .event_rx - .take() - .ok_or_else(|| py_runtime_err!("Two .receive() calls cannot co-exist"))?; - - pyo3_async_runtimes::tokio::future_into_py(py, async move { - let result: Vec = { - let mut events: Vec = Vec::new(); - - loop { - if events.len() >= count as usize { - break; - } - - let timeout_rx_future = match timeout { - Some(timeout) => { - let timeout = std::time::Duration::from_secs_f64(timeout); - tokio::time::timeout(timeout, rx.recv()) - .map(|x| x.unwrap_or(None)) - .boxed() - } - None => rx.recv().boxed(), - }; - - tokio::select! { - result = timeout_rx_future => { - match result { - Some(event) => events.push(event), - None => break, - } - }, - // TODO: Add more signal support - _ = tokio::signal::ctrl_c() => { - Python::with_gil(|py| py.check_signals())?; - } - }; - } - - Ok::<_, PyErr>(events) - }?; - - // Convert events to Python objects - Python::with_gil(|py| { - let py_events = PyList::empty(py); - for event in result { - let event = PyTuple::new( - py, - &[ - PyString::new(py, &event.name).as_any(), - PyList::new(py, &util::json_to_py_value(py, &event.args)?)?.as_any(), - ], - )?; - py_events.append(event)?; - } - - Ok(py_events.unbind()) - }) - }) - } - - pub fn disconnect<'a>(&self, py: Python<'a>) -> PyResult> { - let handle = self.handle.clone(); - - pyo3_async_runtimes::tokio::future_into_py(py, async move { - handle.disconnect().await; - - Ok(()) - }) - } + #[new] + pub fn py_new() -> PyResult { + Err(py_runtime_err!( + "Actor handle cannot be instantiated directly", + )) + } + + pub fn action<'a>( + &self, + py: Python<'a>, + method: &str, + args: Vec + ) -> PyResult> { + let method = method.to_string(); + let handle = self.handle.clone(); + + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let args = Python::with_gil(|py| util::py_to_json_value(py, &args))?; + let result = handle.action(&method, args).await; + let Ok(result) = result else { + return Err(py_runtime_err!( + "Failed to call action: {:?}", + result.err() + )); + }; + let mut result = Python::with_gil(|py| { + match util::json_to_py_value(py, &vec![result]) { + Ok(value) => Ok( + value.iter() + .map(|x| x.clone().unbind()) + .collect::>() + ), + Err(e) => Err(e), + } + })?; + let Some(result) = result.drain(0..1).next() else { + return Err(py_runtime_err!( + "Expected one result, got {}", + result.len() + )); + }; + + Ok(result) + }) + } + + pub fn subscribe<'a>( + &self, + py: Python<'a>, + event_name: &str + ) -> PyResult> { + let event_name = event_name.to_string(); + let handle = self.handle.clone(); + let tx = self.event_tx.clone(); + + pyo3_async_runtimes::tokio::future_into_py(py, async move { + handle.on_event(&event_name.clone(), move |args| { + let event_name = event_name.clone(); + let args = args.clone(); + let tx = tx.clone(); + + tokio::spawn(async move { + let event = ActorEvent { + name: event_name, + args: args.clone(), + }; + // Send this upstream(?) + tx.send(event).map_err(|e| { + py_runtime_err!( + "Failed to send via inner tx: {}", + e + ) + }).ok(); + }); + }).await; + + Ok(()) + }) + } + + #[pyo3(signature=(count, timeout=None))] + pub fn receive<'a>( + &mut self, + py: Python<'a>, + count: u32, + timeout: Option + ) -> PyResult> { + let mut rx = self.event_rx.take().ok_or_else(|| { + py_runtime_err!("Two .receive() calls cannot co-exist") + })?; + + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let result: Vec = { + let mut events: Vec = Vec::new(); + + loop { + if events.len() >= count as usize { + break; + } + + let timeout_rx_future = match timeout { + Some(timeout) => { + let timeout = std::time::Duration::from_secs_f64(timeout); + tokio::time::timeout(timeout, rx.recv()) + .map(|x| x.unwrap_or(None)).boxed() + }, + None => rx.recv().boxed() + }; + + tokio::select! { + result = timeout_rx_future => { + match result { + Some(event) => events.push(event), + None => break, + } + }, + // TODO: Add more signal support + _ = tokio::signal::ctrl_c() => { + Python::with_gil(|py| py.check_signals())?; + } + }; + } + + Ok::<_, PyErr>(events) + }?; + + // Convert events to Python objects + Python::with_gil(|py| { + let py_events = PyList::empty(py); + for event in result { + let event = PyTuple::new(py, &[ + PyString::new(py, &event.name).as_any(), + PyList::new(py, &util::json_to_py_value(py, &event.args)?)?.as_any(), + ])?; + py_events.append(event)?; + } + + Ok(py_events.unbind()) + }) + }) + } + + pub fn disconnect<'a>(&self, py: Python<'a>) -> PyResult> { + let handle = self.handle.clone(); + + pyo3_async_runtimes::tokio::future_into_py(py, async move { + handle.disconnect().await; + + Ok(()) + }) + } } diff --git a/rivetkit-python/client/src/simple/sync/handle.rs b/rivetkit-python/client/src/simple/sync/handle.rs index d4b0bdc6b8..f623576f07 100644 --- a/rivetkit-python/client/src/simple/sync/handle.rs +++ b/rivetkit-python/client/src/simple/sync/handle.rs @@ -1,160 +1,167 @@ -use futures_util::FutureExt; -use pyo3::{ - prelude::*, - types::{PyList, PyString, PyTuple}, -}; use rivetkit_client::{self as rivetkit_rs}; +use futures_util::FutureExt; +use pyo3::{prelude::*, types::{PyList, PyString, PyTuple}}; use tokio::sync::mpsc; use crate::util::{self, SYNC_RUNTIME}; struct ActorEvent { - name: String, - args: Vec, + name: String, + args: Vec, } #[pyclass] pub struct ActorHandle { - handle: rivetkit_rs::connection::ActorHandle, - event_rx: Option>, - event_tx: mpsc::UnboundedSender, + handle: rivetkit_rs::connection::ActorHandle, + event_rx: Option>, + event_tx: mpsc::UnboundedSender, } impl ActorHandle { - pub fn new(handle: rivetkit_rs::connection::ActorHandle) -> Self { - let (event_tx, event_rx) = mpsc::unbounded_channel(); - - Self { - handle, - event_tx, - event_rx: Some(event_rx), - } - } + pub fn new(handle: rivetkit_rs::connection::ActorHandle) -> Self { + let (event_tx, event_rx) = mpsc::unbounded_channel(); + + Self { + handle, + event_tx, + event_rx: Some(event_rx), + } + } } #[pymethods] impl ActorHandle { - #[new] - pub fn py_new() -> PyResult { - Err(py_runtime_err!( - "Actor handle cannot be instantiated directly" - )) - } - - #[pyo3(signature=(method, *py_args))] - pub fn action<'a>( - &self, - py: Python<'a>, - method: &str, - py_args: &Bound<'_, PyTuple>, - ) -> PyResult> { - let args = py_args.extract::>()?; - - let result = self - .handle - .action(method, util::py_to_json_value(py, &args)?); - let result = SYNC_RUNTIME.block_on(result); - - let Ok(result) = result else { - return Err(py_runtime_err!("Failed to call action: {:?}", result.err())); - }; - - let mut result = util::json_to_py_value(py, &vec![result])?; - let Some(result) = result.drain(0..1).next() else { - return Err(py_runtime_err!("Expected one result, got {}", result.len())); - }; - - Ok(result) - } - - pub fn subscribe(&self, event_name: &str) -> PyResult<()> { - let event_name = event_name.to_string(); - let tx = self.event_tx.clone(); - - SYNC_RUNTIME.block_on(self.handle.on_event(&event_name.clone(), move |args| { - let event_name = event_name.clone(); - let args = args.clone(); - let tx = tx.clone(); - - tokio::spawn(async move { - let event = ActorEvent { - name: event_name, - args: args.clone(), - }; - // Send this upstream(?) - tx.send(event) - .map_err(|e| py_runtime_err!("Failed to send via inner tx: {}", e)) - .ok(); - }); - })); - - Ok(()) - } - - #[pyo3(signature=(count, timeout=None))] - pub fn receive<'a>( - &mut self, - py: Python<'a>, - count: u32, - timeout: Option, - ) -> PyResult> { - let mut rx = self - .event_rx - .take() - .ok_or_else(|| py_runtime_err!("Two .receive() calls cannot co-exist"))?; - - let result: Vec = SYNC_RUNTIME.block_on(async { - let mut events: Vec = Vec::new(); - - loop { - if events.len() >= count as usize { - break; - } - - let timeout_rx_future = match timeout { - Some(timeout) => { - let timeout = std::time::Duration::from_secs_f64(timeout); - tokio::time::timeout(timeout, rx.recv()) - .map(|x| x.unwrap_or(None)) - .boxed() - } - None => rx.recv().boxed(), - }; - - tokio::select! { - result = timeout_rx_future => { - match result { - Some(event) => events.push(event), - None => break, - } - }, - // TODO: Add more signal support - _ = tokio::signal::ctrl_c() => { - py.check_signals()?; - } - }; - } - - Ok::<_, PyErr>(events) - })?; - - // Convert events to Python objects - let py_events = PyList::empty(py); - for event in result { - let event = PyTuple::new( - py, - &[ - PyString::new(py, &event.name).as_any(), - PyList::new(py, &util::json_to_py_value(py, &event.args)?)?.as_any(), - ], - )?; - py_events.append(event)?; - } - - Ok(py_events) - } - - pub fn disconnect(&self) { - SYNC_RUNTIME.block_on(self.handle.disconnect()) - } + #[new] + pub fn py_new() -> PyResult { + Err(py_runtime_err!("Actor handle cannot be instantiated directly")) + } + + #[pyo3(signature=(method, *py_args))] + pub fn action<'a>( + &self, + py: Python<'a>, + method: &str, + py_args: &Bound<'_, PyTuple>, + ) -> PyResult> { + let args = py_args.extract::>()?; + + let result = self.handle.action( + method, + util::py_to_json_value(py, &args)? + ); + let result = SYNC_RUNTIME.block_on(result); + + let Ok(result) = result else { + return Err(py_runtime_err!( + "Failed to call action: {:?}", + result.err() + )); + }; + + let mut result = util::json_to_py_value(py, &vec![result])?; + let Some(result) = result.drain(0..1).next() else { + return Err(py_runtime_err!( + "Expected one result, got {}", + result.len() + )); + }; + + Ok(result) + } + + pub fn subscribe( + &self, + event_name: &str, + ) -> PyResult<()> { + let event_name = event_name.to_string(); + let tx = self.event_tx.clone(); + + SYNC_RUNTIME.block_on( + self.handle.on_event(&event_name.clone(), move |args| { + let event_name = event_name.clone(); + let args = args.clone(); + let tx = tx.clone(); + + tokio::spawn(async move { + let event = ActorEvent { + name: event_name, + args: args.clone(), + }; + // Send this upstream(?) + tx.send(event).map_err(|e| { + py_runtime_err!( + "Failed to send via inner tx: {}", + e + ) + }).ok(); + }); + }) + ); + + Ok(()) + } + + #[pyo3(signature=(count, timeout=None))] + pub fn receive<'a>( + &mut self, + py: Python<'a>, + count: u32, + timeout: Option + ) -> PyResult> { + let mut rx = self.event_rx.take().ok_or_else(|| { + py_runtime_err!("Two .receive() calls cannot co-exist") + })?; + + let result: Vec = SYNC_RUNTIME.block_on(async { + let mut events: Vec = Vec::new(); + + loop { + if events.len() >= count as usize { + break; + } + + let timeout_rx_future = match timeout { + Some(timeout) => { + let timeout = std::time::Duration::from_secs_f64(timeout); + tokio::time::timeout(timeout, rx.recv()) + .map(|x| x.unwrap_or(None)).boxed() + }, + None => rx.recv().boxed() + }; + + tokio::select! { + result = timeout_rx_future => { + match result { + Some(event) => events.push(event), + None => break, + } + }, + // TODO: Add more signal support + _ = tokio::signal::ctrl_c() => { + py.check_signals()?; + } + }; + } + + Ok::<_, PyErr>(events) + })?; + + // Convert events to Python objects + let py_events = PyList::empty(py); + for event in result { + let event = PyTuple::new(py, &[ + PyString::new(py, &event.name).as_any(), + PyList::new(py, &util::json_to_py_value(py, &event.args)?)?.as_any(), + ])?; + py_events.append(event)?; + } + + Ok(py_events) + } + + pub fn disconnect(&self) { + SYNC_RUNTIME.block_on( + self.handle.disconnect() + ) + } } diff --git a/rivetkit-rust/packages/rivetkit-core/tests/modules/connection.rs b/rivetkit-rust/packages/rivetkit-core/tests/modules/connection.rs index e9b524853c..01bb70c963 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/modules/connection.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/modules/connection.rs @@ -21,8 +21,8 @@ mod moved_tests { use crate::actor::callbacks::ActorInstanceCallbacks; use crate::actor::config::ActorConfig; use crate::actor::context::ActorContext; - use crate::actor::context::tests::new_with_kv; use crate::actor::keys::make_connection_key; + use crate::actor::context::tests::new_with_kv; use super::metrics_helpers::{metric_line_for_actor, render_global_metrics}; @@ -268,9 +268,7 @@ mod moved_tests { let metrics = render_global_metrics(); let total_line = metrics .lines() - .find(|line| { - metric_line_for_actor(line, "rivetkit_actor_connections_total", "conn-metrics") - }) + .find(|line| metric_line_for_actor(line, "rivetkit_actor_connections_total", "conn-metrics")) .expect("connections total metric line"); assert!(total_line.ends_with(" 1")); diff --git a/rivetkit-rust/packages/rivetkit-core/tests/modules/queue.rs b/rivetkit-rust/packages/rivetkit-core/tests/modules/queue.rs index 4f59ae7cc9..85f886d692 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/modules/queue.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/modules/queue.rs @@ -150,23 +150,11 @@ mod moved_tests { let metrics = render_global_metrics(); let sent_line = metrics .lines() - .find(|line| { - metric_line_for_actor( - line, - "rivetkit_actor_queue_messages_sent_total", - "queue-metrics", - ) - }) + .find(|line| metric_line_for_actor(line, "rivetkit_actor_queue_messages_sent_total", "queue-metrics")) .expect("sent metric line"); let received_line = metrics .lines() - .find(|line| { - metric_line_for_actor( - line, - "rivetkit_actor_queue_messages_received_total", - "queue-metrics", - ) - }) + .find(|line| metric_line_for_actor(line, "rivetkit_actor_queue_messages_received_total", "queue-metrics")) .expect("received metric line"); assert!(sent_line.ends_with(" 1")); diff --git a/rivetkit-typescript/packages/rivetkit/tests/registry-shutdown.test.ts b/rivetkit-typescript/packages/rivetkit/tests/registry-shutdown.test.ts index e398904e14..4b769d027b 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/registry-shutdown.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/registry-shutdown.test.ts @@ -76,10 +76,7 @@ function createFake(): Fake { handleServerlessRequest: async ( _registry: unknown, _req: unknown, - onStreamEvent: ( - error: unknown, - event?: { kind: string }, - ) => unknown, + onStreamEvent: (error: unknown, event?: { kind: string }) => unknown, ) => { await onStreamEvent(null, { kind: "end" }); return { status: 200, headers: {} }; @@ -90,9 +87,7 @@ function createFake(): Fake { state.builderCalls += 1; // A distinct handle per build so fan-out can prove both modes were // torn down (Mode A and Mode B build separate registries). - const registry = { - id: state.builderCalls, - } as unknown as RegistryHandle; + const registry = { id: state.builderCalls } as unknown as RegistryHandle; const serveConfig = { serverlessBasePath: "/api/rivet", serverlessMaxStartPayloadBytes: 1024, From 8d8a6e077dbb30c82ef8215eb65b94cb48c5b43e Mon Sep 17 00:00:00 2001 From: Igor Gassmann Date: Fri, 12 Jun 2026 13:20:34 +0200 Subject: [PATCH 306/306] chore: clear branch diff --- .../tests/modules/action_dispatch_error.rs | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/rivetkit-rust/packages/rivetkit-core/tests/modules/action_dispatch_error.rs b/rivetkit-rust/packages/rivetkit-core/tests/modules/action_dispatch_error.rs index fff56cb0e2..d0ab408d9e 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/modules/action_dispatch_error.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/modules/action_dispatch_error.rs @@ -31,25 +31,19 @@ fn preserves_public_error_message_for_client_boundary() { #[test] fn preserves_user_error_message_and_metadata_for_client_boundary() { - let metadata = serde_json::json!({ - "limit": 20, - "attempted": 25, - }); - let error = ActionDispatchError::from_anyhow(anyhow::Error::new(RivetError { - kind: RivetErrorKind::Dynamic { - group: "user".to_owned(), - code: "quota_exceeded".to_owned(), - default_message: "quota exceeded".to_owned(), - }, - meta: serde_json::value::to_raw_value(&metadata).ok(), - message: None, + let error = ActionDispatchError { + group: "user".to_owned(), + code: "detailed_error".to_owned(), + message: "Detailed error message".to_owned(), + metadata: Some(serde_json::json!({ "reason": "test" })), actor: None, - })); + }; - assert_eq!(error.group, "user"); - assert_eq!(error.code, "quota_exceeded"); - assert_eq!(error.client_message(), "quota exceeded"); - assert_eq!(error.client_metadata(), Some(&metadata)); + assert_eq!(error.client_message(), "Detailed error message"); + assert_eq!( + error.client_metadata(), + Some(&serde_json::json!({ "reason": "test" })) + ); } #[test]