diff --git a/examples/effect/package.json b/examples/effect/package.json new file mode 100644 index 0000000000..9610c629cd --- /dev/null +++ b/examples/effect/package.json @@ -0,0 +1,30 @@ +{ + "name": "example-effect", + "private": true, + "type": "module", + "scripts": { + "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" + }, + "dependencies": { + "@effect/platform-node": "4.0.0-beta.66", + "@rivetkit/effect": "workspace:*", + "effect": "4.0.0-beta.66", + "pino": "9.9.5", + "pino-pretty": "13.1.2", + "rivetkit": "workspace:*" + }, + "devDependencies": { + "@types/node": "^22.13.9", + "tsx": "^4.20.5", + "typescript": "^5.5.2" + }, + "template": { + "noFrontend": true, + "skipVercel": true + }, + "license": "MIT" +} 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..8fce4faf48 --- /dev/null +++ b/examples/effect/src/actors/chat-room/api.ts @@ -0,0 +1,92 @@ +import { Action, Actor } from "@rivetkit/effect"; +import { Schema } from "effect"; +import { BannedWordsError } from "../moderator/api.ts"; + +// --- Errors --- + +export class MemberNotInRoomError extends Schema.TaggedErrorClass()( + "MemberNotInRoomError", + { + name: Schema.String, + message: Schema.String, + }, +) {} + +// --- Actions --- + +// Actions use explicit schemas which enable: +// +// - Runtime validation. Schemas validate encoded data end to end +// which protects from malformed, stale, or malicious data. +// +// - Encoding/decoding control. Effect Schema distinguishes encoded +// (wire) and decoded (runtime) types. Values like `URL`, `bigint`, +// or custom domain types can have safe encoded forms on the wire +// and rich decoded forms in action handlers. Schemas can also require +// custom services during encode/decode. + +// This action replaces passing an `input` when creating an actor. +export const Initialize = Action.make("Initialize", { + payload: { name: Schema.String }, +}); + +export const Join = Action.make("Join", { + payload: { name: Schema.String }, + success: Schema.Struct({ + memberCount: Schema.Number, + }), +}); + +export const Leave = Action.make("Leave", { + payload: { name: Schema.String }, + error: MemberNotInRoomError, +}); + +export const SendMessage = Action.make("SendMessage", { + payload: { + sender: Schema.String, + text: Schema.String, + }, + error: Schema.Union([MemberNotInRoomError, BannedWordsError]), +}); + +export const GetHistory = Action.make("GetHistory", { + success: Schema.Array( + Schema.Struct({ + id: Schema.Number, + sender: Schema.String, + text: Schema.String, + createdAt: Schema.DateTimeUtc, + }), + ), +}); + +// This action replaces passing an `input` when creating an actor. +export const Archive = Action.make("Archive"); + +// --- 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 SendSystemMessage = Message.make("SendSystemMessage", { +// payload: { text: Schema.String }, +// success: Schema.String, +// }) + +// --- Actor Definition --- + +// 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", { + // 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], + // 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 new file mode 100644 index 0000000000..d2c61549cf --- /dev/null +++ b/examples/effect/src/actors/chat-room/live.ts @@ -0,0 +1,260 @@ +import { Actor, State } from "@rivetkit/effect"; +import { Context, DateTime, Effect, Layer, Schema, Stream } from "effect"; +import { db } from "rivetkit/db"; +import { Moderator } from "../moderator/api.ts"; +import { ChatRoom, MemberNotInRoomError } from "./api.ts"; + +// --- Services --- + +// Actors can use custom Effect services like any other Effect program. +// Provide the service layer to the actor layer, then yield it in the wake scope. +export class RoomPolicy extends Context.Service< + RoomPolicy, + { + readonly requireMember: ( + members: ReadonlyArray<{ readonly name: string }>, + 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) + 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("/"), + }); + + // Finalizers run on sleep + yield* Effect.addFinalizer( + Effect.fnUntraced(function* () { + // Access the actor's persisted `state` with a `SubscriptionRef`-like API + 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("/"), + roomName, + }); + }), + ); + + // `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, + roomName: 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) => + 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 + // 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), + }, + }); + + // 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: 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); + + 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", + ]); + + // 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); + + 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, + ), + Archive: () => + Effect.sync(() => { + rawRivetkitContext.destroy(); + }), + }); + }), + { + state: { + schema: Schema.Struct({ + name: Schema.String, + members: Schema.Array( + Schema.Struct({ + name: Schema.String, + joinedAt: Schema.DateTimeUtc, + }), + ), + initialized: Schema.Boolean, + }), + initialValue: () => ({ + name: "", + members: [{ name: "Admin", joinedAt: DateTime.nowUnsafe() }], + initialized: false, + }), + }, + 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", // Human-friendly display name + icon: "comments", // FontAwesome icon name + }, +); diff --git a/examples/effect/src/actors/mod.ts b/examples/effect/src/actors/mod.ts new file mode 100644 index 0000000000..2d76185742 --- /dev/null +++ b/examples/effect/src/actors/mod.ts @@ -0,0 +1,2 @@ +export * from "./chat-room/api.ts"; +export * from "./moderator/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..805a2bfbe0 --- /dev/null +++ b/examples/effect/src/actors/moderator/api.ts @@ -0,0 +1,18 @@ +import { Action, Actor } from "@rivetkit/effect"; +import { Schema } from "effect"; + +export class BannedWordsError extends Schema.TaggedErrorClass()( + "BannedWordsError", + { + message: Schema.String, + }, +) {} + +export const Review = Action.make("Review", { + payload: { text: Schema.String }, + error: BannedWordsError, +}); + +export const Moderator = Actor.make("Moderator", { + actions: [Review], +}); diff --git a/examples/effect/src/actors/moderator/live.ts b/examples/effect/src/actors/moderator/live.ts new file mode 100644 index 0000000000..70f08e4834 --- /dev/null +++ b/examples/effect/src/actors/moderator/live.ts @@ -0,0 +1,38 @@ +import { State } from "@rivetkit/effect"; +import { Effect, Schema } from "effect"; +import { BannedWordsError, Moderator } from "./api.ts"; + +const bannedWords = ["spam", "scam"]; + +export const ModeratorLive = Moderator.toLayer( + 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}"`, + }); + } + }), + }); + }), + { + state: { + schema: Schema.Struct({ + reviewed: Schema.Number, + }), + initialValue: () => ({ + reviewed: 0, + }), + }, + name: "Moderator", + icon: "shield", + }, +); diff --git a/examples/effect/src/client-raw.ts b/examples/effect/src/client-raw.ts new file mode 100644 index 0000000000..74fc1f67c1 --- /dev/null +++ b/examples/effect/src/client-raw.ts @@ -0,0 +1,87 @@ +import { createClient } from "rivetkit/client"; +import { RivetError } from "rivetkit/errors"; + +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 room = client.ChatRoom.getOrCreate(`chatroom_${crypto.randomUUID()}`); + + try { + const roomName = "Effect Lovers"; + await room.Initialize({ name: "Effect Lovers" }); + console.log(`created room ${roomName}`); + + const { memberCount } = await room.Join({ name: "Alice" }); + console.log(`Alice joined; members=${memberCount}`); + + await room.SendMessage({ + sender: "Alice", + text: "hello from the raw client", + }); + console.log("Alice sent a message"); + + // 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; + + 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; + } + } + + 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("raw client failed:", err); + process.exitCode = 1; +}); diff --git a/examples/effect/src/client.ts b/examples/effect/src/client.ts new file mode 100644 index 0000000000..2892a3163d --- /dev/null +++ b/examples/effect/src/client.ts @@ -0,0 +1,76 @@ +import { NodeRuntime } from "@effect/platform-node"; +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. + const chatRoomClient = yield* ChatRoom.client; + const room = chatRoomClient.getOrCreate( + `chatroom_${yield* Random.nextUUIDv4}`, + ); + + yield* Effect.addFinalizer( + Effect.fnUntraced(function* () { + yield* room.Archive().pipe(Effect.orDie); + yield* Effect.log("archived room"); + }), + ); + + const roomName = "Effect Lovers"; + yield* room.Initialize({ name: roomName }); + yield* Effect.log(`created room ${roomName}`); + + const { memberCount } = yield* room.Join({ name: "Alice" }); + yield* Effect.log(`Alice joined; members=${memberCount}`); + + yield* room.SendMessage({ + sender: "Alice", + 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.scoped); + +const ClientLayer = Client.layer({ endpoint: "http://127.0.0.1:6420" }); + +program + .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 new file mode 100644 index 0000000000..7f48441978 --- /dev/null +++ b/examples/effect/src/main.ts @@ -0,0 +1,34 @@ +import { NodeRuntime } from "@effect/platform-node"; +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"; + +const ActorsLayer = Layer.mergeAll( + ModeratorLive, + ChatRoomLive.pipe(Layer.provide(RoomPolicyLive)), +).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 +// 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(ActorsLayer).pipe( + Layer.provide(Registry.layer()), + Layer.provide(PrettyLoggerLayer), +); + +// 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()), + Layer.provide(PrettyLoggerLayer), + ), +); 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": ["//"] +} diff --git a/package.json b/package.json index 09b7443fc3..0c715eaa54 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@rivetkit/rivetkit-napi": "workspace:*", "@rivetkit/rivetkit-wasm": "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 fcd5151d8c..3446c6ad79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,7 @@ overrides: '@rivetkit/rivetkit-napi': workspace:* '@rivetkit/rivetkit-wasm': workspace:* '@rivetkit/engine-cli': workspace:* + '@rivetkit/effect': workspace:* '@types/react': ^19 '@types/react-dom': ^19 react: 19.1.0 @@ -61,7 +62,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.9.0) turbo: specifier: ^2.5.6 version: 2.5.6 @@ -129,7 +130,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.9.0) typescript: specifier: ^5.9.2 version: 5.9.3 @@ -166,7 +167,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.9.0) tsx: specifier: ^4.20.5 version: 4.21.0 @@ -246,7 +247,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.1)(zod@3.25.76) rivetkit: specifier: workspace:* version: link:../../rivetkit-typescript/packages/rivetkit @@ -329,7 +330,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.1) react: specifier: 19.1.0 version: 19.1.0 @@ -794,6 +795,37 @@ 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.14.4(@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.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.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 + devDependencies: + '@types/node': + specifier: ^22.13.9 + version: 22.19.15 + tsx: + specifier: ^4.20.5 + version: 4.21.0 + typescript: + specifier: ^5.5.2 + version: 5.9.3 + examples/elysia: dependencies: elysia: @@ -1477,7 +1509,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.9.0)) concurrently: specifier: ^9.1.2 version: 9.2.1 @@ -1489,7 +1521,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.9.0) 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.14.4(@types/node@22.19.10)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) @@ -1535,7 +1567,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.9.0)) concurrently: specifier: ^9.1.2 version: 9.2.1 @@ -1547,7 +1579,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.9.0) 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.14.4(@types/node@22.19.10)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) @@ -2058,7 +2090,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@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) @@ -2160,10 +2192,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.9.0)) '@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.9.0)) '@tanstack/history': specifier: ^1.133.28 version: 1.133.28 @@ -2298,7 +2330,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.1) autoprefixer: specifier: ^10.4.21 version: 10.4.22(postcss@8.5.6) @@ -2307,7 +2339,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.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@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 @@ -2412,10 +2444,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.9.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.9.0)) ts-pattern: specifier: ^5.8.0 version: 5.8.0 @@ -2427,7 +2459,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@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) @@ -2449,7 +2481,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.2) + 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: @@ -2563,10 +2595,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.9.0)) '@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.9.0)) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -2650,7 +2682,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.9.0)) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.1.0) @@ -2681,7 +2713,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.9.0) 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) @@ -2796,11 +2828,45 @@ 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.9.0) typescript: specifier: ^5.5.2 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 + version: 0.18.3 + '@effect/language-service': + specifier: ^0.85.1 + version: 0.85.1 + '@effect/vitest': + specifier: ^4.0.0-beta.66 + version: 4.0.0-beta.70(effect@4.0.0-beta.66)(vitest@4.1.7) + '@types/node': + specifier: ^22.18.1 + version: 22.19.15 + '@vitest/coverage-v8': + specifier: ^4.1.7 + version: 4.1.7(vitest@4.1.7) + publint: + specifier: ^0.3.21 + version: 0.3.21 + typescript: + specifier: ^5.9.2 + version: 5.9.3 + vitest: + specifier: ^4.1.5 + version: 4.1.7(@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.9.0)) + rivetkit-typescript/packages/engine-cli: {} rivetkit-typescript/packages/engine-runner: @@ -2832,7 +2898,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.9.0) tsx: specifier: ^4.20.5 version: 4.21.0 @@ -2854,7 +2920,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.9.0) typescript: specifier: ^5.9.2 version: 5.9.3 @@ -2873,7 +2939,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.9.0) typescript: specifier: ^5.5.2 version: 5.9.3 @@ -2898,7 +2964,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.9.0) typescript: specifier: ^5.7.3 version: 5.9.3 @@ -2935,7 +3001,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.9.0) typescript: specifier: ^5.5.2 version: 5.9.3 @@ -2966,7 +3032,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.9.0) typescript: specifier: ^5.5.2 version: 5.9.3 @@ -3072,7 +3138,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.9.0) tsx: specifier: ^4.19.4 version: 4.21.0 @@ -3081,7 +3147,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.9.0)) 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.14.4(@types/node@22.19.10)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) @@ -3114,7 +3180,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.9.0) rivetkit-typescript/packages/traces: dependencies: @@ -3142,7 +3208,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.9.0) tsx: specifier: ^4.7.0 version: 4.21.0 @@ -3185,7 +3251,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.9.0) tsx: specifier: ^4.7.0 version: 4.21.0 @@ -3259,7 +3325,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.9.0) typescript: specifier: ^5.7.3 version: 5.9.3 @@ -3268,7 +3334,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) @@ -3277,7 +3343,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 @@ -3313,7 +3379,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 @@ -3340,7 +3406,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) @@ -3673,6 +3739,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==} @@ -3685,6 +3754,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: @@ -3832,6 +3910,10 @@ packages: resolution: {integrity: sha512-eoyTMgd6OzoE1dq50um5Y53NrosEkWsjH0W6pswi7vrv1W9hY/7hR43jDcPevqqj+OQksf/5lc++FTqRlb8Y1Q==} engines: {node: '>=20.0.0'} + '@aws-sdk/types@3.973.5': + resolution: {integrity: sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/types@3.973.6': resolution: {integrity: sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==} engines: {node: '>=20.0.0'} @@ -3983,6 +4065,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'} @@ -4383,6 +4470,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: @@ -4514,6 +4605,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==} @@ -4635,6 +4729,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'} @@ -4682,6 +4780,29 @@ 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.70': + resolution: {integrity: sha512-3VXuL63IDmq13We+ApRKn2JW3Rb9g5gj1YEmfb8u2b73norur1VsIJ/pRE4qjShevg19dQYi2JsLawSZ6gApug==} + engines: {node: '>=18.0.0'} + peerDependencies: + effect: ^4.0.0-beta.70 + + '@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.66 + ioredis: ^5.7.0 + + '@effect/vitest@4.0.0-beta.70': + resolution: {integrity: sha512-XDteNN0xfOgoMauAVoN5iylxVgEjp7kFsGFq18tZ5XYjek0eOZa0nOoes5s7Bs71VvwjnCeCbFMD7IhxswEt8A==} + peerDependencies: + effect: ^4.0.0-beta.70 + vitest: ^3.0.0 || ^4.0.0 + '@emnapi/runtime@1.7.1': resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} @@ -6058,6 +6179,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} @@ -6203,6 +6327,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==} @@ -6849,6 +6976,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==} @@ -8158,6 +8289,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'} @@ -8292,6 +8427,10 @@ packages: resolution: {integrity: sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA==} engines: {node: '>=18.0.0'} + '@smithy/types@4.13.0': + resolution: {integrity: sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==} + engines: {node: '>=18.0.0'} + '@smithy/types@4.13.1': resolution: {integrity: sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==} engines: {node: '>=18.0.0'} @@ -9293,6 +9432,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==} @@ -9305,6 +9453,9 @@ packages: '@vitest/expect@4.0.18': resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/expect@4.1.7': + resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} + '@vitest/mocker@2.1.9': resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} peerDependencies: @@ -9338,6 +9489,17 @@ packages: vite: optional: true + '@vitest/mocker@4.1.7': + resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} + 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==} @@ -9347,6 +9509,9 @@ packages: '@vitest/pretty-format@4.0.18': resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@4.1.7': + resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + '@vitest/runner@1.6.1': resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} @@ -9359,6 +9524,9 @@ packages: '@vitest/runner@4.0.18': resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/runner@4.1.7': + resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} + '@vitest/snapshot@1.6.1': resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} @@ -9371,6 +9539,9 @@ packages: '@vitest/snapshot@4.0.18': resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/snapshot@4.1.7': + resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} + '@vitest/spy@1.6.1': resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} @@ -9383,6 +9554,9 @@ packages: '@vitest/spy@4.0.18': resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/spy@4.1.7': + resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} + '@vitest/utils@1.6.1': resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} @@ -9395,6 +9569,9 @@ packages: '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.7': + resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + '@volar/language-core@1.11.1': resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} @@ -9630,6 +9807,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'} @@ -9723,6 +9904,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 @@ -10200,6 +10384,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==} @@ -10279,6 +10467,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==} @@ -10308,6 +10499,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'} @@ -10334,6 +10529,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: @@ -10384,6 +10583,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'} @@ -10896,6 +11099,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'} @@ -11223,6 +11430,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + 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==} @@ -11253,6 +11463,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'} @@ -11280,6 +11493,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 @@ -11301,6 +11518,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'} @@ -11523,6 +11743,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: @@ -11638,6 +11862,10 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true + fast-check@4.8.0: + resolution: {integrity: sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==} + engines: {node: '>=12.17.0'} + fast-copy@3.0.2: resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} @@ -11751,6 +11979,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'} @@ -11793,6 +12024,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'} @@ -12284,6 +12518,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==} @@ -12410,6 +12647,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==} @@ -12429,6 +12670,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'} @@ -12601,6 +12846,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==} @@ -12704,6 +12957,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==} @@ -12814,6 +13070,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'} @@ -13027,10 +13286,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. @@ -13125,10 +13390,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==} @@ -13142,6 +13414,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'} @@ -13157,6 +13435,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==} @@ -13532,6 +13815,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'} @@ -13641,6 +13929,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'} @@ -13655,6 +13947,9 @@ packages: resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} hasBin: true + msgpackr@1.11.12: + resolution: {integrity: sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==} + msgpackr@1.11.5: resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} @@ -13671,6 +13966,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@3.0.0: resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} engines: {node: ^20.17.0 || >=22.9.0} @@ -13771,6 +14069,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==} @@ -14620,6 +14922,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==} @@ -14630,6 +14937,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'} @@ -14948,6 +15258,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==} @@ -15182,6 +15500,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==} @@ -15395,6 +15717,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'} @@ -15497,6 +15823,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==} @@ -15514,6 +15843,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==} @@ -15670,6 +16002,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'} @@ -15825,6 +16161,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'} @@ -15866,6 +16206,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.1: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} @@ -16060,6 +16404,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'} @@ -16106,10 +16455,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'} @@ -16118,10 +16463,18 @@ packages: resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==} engines: {node: '>=20.18.1'} + undici@8.3.0: + resolution: {integrity: sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==} + engines: {node: '>=22.19.0'} + unicode-canonical-property-names-ecmascript@2.0.1: 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'} @@ -16365,6 +16718,10 @@ packages: resolution: {integrity: sha512-USe1zesMYh4fjCA8ZH5+X5WIVD0J4V1Jksm1bFTVBX2F/cwSXt0RO5w/3UXbdLKmZX65MiWV+hwhSS8p6oBTGA==} hasBin: true + uuid@13.0.2: + resolution: {integrity: sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==} + hasBin: true + 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). @@ -16679,6 +17036,47 @@ packages: jsdom: optional: true + vitest@4.1.7: + resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} + 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.7 + '@vitest/browser-preview': 4.1.7 + '@vitest/browser-webdriverio': 4.1.7 + '@vitest/coverage-istanbul': 4.1.7 + '@vitest/coverage-v8': 4.1.7 + '@vitest/ui': 4.1.7 + 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==} @@ -16898,6 +17296,18 @@ packages: utf-8-validate: optional: true + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + 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'} @@ -16966,6 +17376,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -17252,6 +17667,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 @@ -17269,6 +17686,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 @@ -17304,12 +17742,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 @@ -17356,9 +17794,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)) @@ -17381,7 +17819,7 @@ snapshots: '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.5 tslib: 2.8.1 '@aws-crypto/sha256-browser@5.2.0': @@ -17389,7 +17827,7 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.5 '@aws-sdk/util-locate-window': 3.965.5 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -17397,7 +17835,7 @@ snapshots: '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.5 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -17406,7 +17844,7 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.5 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -17718,6 +18156,11 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/types@3.973.5': + dependencies: + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@aws-sdk/types@3.973.6': dependencies: '@smithy/types': 4.13.1 @@ -17935,6 +18378,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 @@ -18383,6 +18830,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 @@ -18474,6 +18923,8 @@ snapshots: '@borewit/text-codec@0.2.2': {} + '@braidai/lang@1.1.2': {} + '@braintree/sanitize-url@7.1.1': {} '@capsizecss/unpack@4.0.0': @@ -18621,6 +19072,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': @@ -18663,6 +19117,33 @@ 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.70(effect@4.0.0-beta.66)': + dependencies: + '@types/ws': 8.18.1 + effect: 4.0.0-beta.66 + ws: 8.20.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@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.70(effect@4.0.0-beta.66) + effect: 4.0.0-beta.66 + ioredis: 5.10.1 + mime: 4.1.0 + undici: 8.3.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@effect/vitest@4.0.0-beta.70(effect@4.0.0-beta.66)(vitest@4.1.7)': + dependencies: + effect: 4.0.0-beta.66 + vitest: 4.1.7(@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.9.0)) + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 @@ -19150,7 +19631,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) @@ -19322,7 +19803,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: @@ -19434,7 +19915,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 @@ -19921,6 +20402,8 @@ snapshots: '@types/node': 22.19.15 optional: true + '@ioredis/commands@1.5.1': {} + '@isaacs/balanced-match@4.0.1': optional: true @@ -20051,7 +20534,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@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 @@ -20063,8 +20546,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@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 @@ -20091,8 +20574,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@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' @@ -20153,6 +20636,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': @@ -20204,9 +20691,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.1)(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.1)(zod@3.25.76) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - aws-crt @@ -20228,7 +20715,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.1)(zod@3.25.76)': dependencies: '@anthropic-ai/sdk': 0.73.0(zod@3.25.76) '@aws-sdk/client-bedrock-runtime': 3.1024.0 @@ -20238,7 +20725,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.1)(zod@3.25.76) partial-json: 0.1.7 proxy-agent: 6.5.0 undici: 7.24.7 @@ -20276,11 +20763,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.1)(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.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.20.1)(zod@3.25.76) '@mariozechner/pi-tui': 0.60.0 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 @@ -20600,7 +21087,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.9.0)) zod: 3.25.76 transitivePeerDependencies: - '@cfworker/json-schema' @@ -21163,6 +21650,8 @@ snapshots: '@protobufjs/utf8@1.1.0': {} + '@publint/pack@0.1.4': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -21870,7 +22359,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 @@ -21880,7 +22369,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 @@ -22072,11 +22561,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.1)(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.1)(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.1)(zod@3.25.76) '@rivet-dev/agent-os-core': 0.1.1(pyodide@0.28.3) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -22497,13 +22986,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 @@ -22829,6 +23318,8 @@ snapshots: '@sinclair/typebox@0.34.41': {} + '@sindresorhus/is@4.6.0': {} + '@sindresorhus/merge-streams@2.3.0': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -23042,6 +23533,10 @@ snapshots: '@smithy/util-stream': 4.5.21 tslib: 2.8.1 + '@smithy/types@4.13.0': + dependencies: + tslib: 2.8.1 + '@smithy/types@4.13.1': dependencies: tslib: 2.8.1 @@ -23225,9 +23720,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.9.0))': dependencies: - tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.2) + tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.9.0) '@tailwindcss/forms@0.5.10(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2))': dependencies: @@ -23308,6 +23803,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.9.0))': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.9.0) + '@tailwindcss/typography@0.5.19(tailwindcss@4.2.2)': dependencies: postcss-selector-parser: 6.0.10 @@ -24171,11 +24671,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@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@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@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' @@ -24215,7 +24715,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@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) @@ -24223,11 +24723,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@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 - '@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.9.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -24235,7 +24735,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.9.0) transitivePeerDependencies: - supports-color @@ -24251,6 +24751,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/coverage-v8@4.1.7(vitest@4.1.7)': + 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.7(@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.9.0)) + '@vitest/expect@1.6.1': dependencies: '@vitest/spy': 1.6.1 @@ -24281,6 +24795,15 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 + '@vitest/expect@4.1.7': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + chai: 6.2.2 + tinyrainbow: 3.1.0 + '@vitest/mocker@2.1.9(msw@2.14.4(@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 @@ -24317,14 +24840,23 @@ 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.2))': + '@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@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@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: + '@vitest/spy': 4.1.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + 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) '@vitest/pretty-format@2.1.9': dependencies: @@ -24338,6 +24870,10 @@ snapshots: dependencies: tinyrainbow: 3.0.3 + '@vitest/pretty-format@4.1.7': + dependencies: + tinyrainbow: 3.1.0 + '@vitest/runner@1.6.1': dependencies: '@vitest/utils': 1.6.1 @@ -24360,6 +24896,11 @@ snapshots: '@vitest/utils': 4.0.18 pathe: 2.0.3 + '@vitest/runner@4.1.7': + dependencies: + '@vitest/utils': 4.1.7 + pathe: 2.0.3 + '@vitest/snapshot@1.6.1': dependencies: magic-string: 0.30.21 @@ -24384,6 +24925,13 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/snapshot@4.1.7': + dependencies: + '@vitest/pretty-format': 4.1.7 + '@vitest/utils': 4.1.7 + magic-string: 0.30.21 + pathe: 2.0.3 + '@vitest/spy@1.6.1': dependencies: tinyspy: 2.2.1 @@ -24398,6 +24946,8 @@ snapshots: '@vitest/spy@4.0.18': {} + '@vitest/spy@4.1.7': {} + '@vitest/utils@1.6.1': dependencies: diff-sequences: 29.6.3 @@ -24422,6 +24972,12 @@ snapshots: '@vitest/pretty-format': 4.0.18 tinyrainbow: 3.0.3 + '@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 @@ -24627,7 +25183,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.1): dependencies: cbor-x: 1.6.0 hono: 4.11.9 @@ -24636,7 +25192,7 @@ snapshots: zod: 3.25.76 optionalDependencies: eventsource: 3.0.7 - ws: 8.19.0 + ws: 8.20.1 agent-base@6.0.2: dependencies: @@ -24756,6 +25312,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: {} @@ -24836,9 +25396,15 @@ 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)(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 @@ -24893,7 +25459,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)) @@ -25139,7 +25705,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.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@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)) @@ -25166,7 +25732,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.2) + 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' @@ -25517,6 +26083,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: {} @@ -25609,6 +26177,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: @@ -25636,6 +26206,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: {} @@ -25662,6 +26238,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) @@ -25720,6 +26298,8 @@ snapshots: commander@10.0.0: {} + commander@10.0.1: {} + commander@11.1.0: {} commander@12.1.0: {} @@ -26242,6 +26822,8 @@ snapshots: delegates@1.0.0: {} + denque@2.1.0: {} + depd@1.1.2: {} depd@2.0.0: {} @@ -26395,6 +26977,19 @@ snapshots: ee-first@1.1.1: {} + effect@4.0.0-beta.66: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 4.8.0 + find-my-way-ts: 0.1.6 + ini: 6.0.0 + kubernetes-types: 1.30.0 + msgpackr: 1.11.12 + multipasta: 0.2.7 + toml: 4.1.1 + uuid: 13.0.2 + yaml: 2.9.0 + electron-to-chromium@1.5.286: {} elliptic@6.6.1: @@ -26426,6 +27021,8 @@ snapshots: emoji-regex@9.2.2: {} + emojilib@2.4.0: {} + encodeurl@1.0.2: {} encodeurl@2.0.0: {} @@ -26445,6 +27042,8 @@ snapshots: env-editor@0.4.2: {} + environment@1.1.0: {} + errno@0.1.8: dependencies: prr: 1.0.1 @@ -26464,6 +27063,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 @@ -26846,6 +27447,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 @@ -27032,6 +27635,10 @@ snapshots: transitivePeerDependencies: - supports-color + fast-check@4.8.0: + dependencies: + pure-rand: 8.4.0 + fast-copy@3.0.2: {} fast-decode-uri-component@1.0.1: {} @@ -27130,6 +27737,8 @@ snapshots: fflate@0.8.2: {} + fflate@0.8.3: {} + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -27184,6 +27793,8 @@ snapshots: transitivePeerDependencies: - supports-color + find-my-way-ts@0.1.6: {} + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -27279,14 +27890,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.1): 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.1)(zod@3.25.76) openapi: 1.0.1 react: 19.1.0 zod: 3.25.76 @@ -27306,16 +27917,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.1): 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.1) 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.1)(zod@3.25.76) openapi: 1.0.1 react: 19.1.0 zod: 3.25.76 @@ -27872,6 +28483,8 @@ snapshots: dependencies: lru-cache: 11.2.6 + html-escaper@2.0.2: {} + html-escaper@3.0.3: {} html-url-attributes@3.0.1: {} @@ -27993,6 +28606,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): @@ -28008,6 +28623,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: {} @@ -28137,13 +28766,24 @@ 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 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 @@ -28267,6 +28907,8 @@ snapshots: js-base64@3.7.8: {} + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -28400,6 +29042,8 @@ snapshots: kolorist@1.8.0: {} + kubernetes-types@1.30.0: {} + kysely@0.28.15: {} lan-network@0.1.7: {} @@ -28592,8 +29236,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: {} @@ -28674,12 +29322,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: @@ -28690,12 +29348,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: {} @@ -28986,7 +29657,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 @@ -29001,7 +29672,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 @@ -29152,7 +29823,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 @@ -29172,7 +29843,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 @@ -29193,7 +29864,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 @@ -29240,7 +29911,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 @@ -29578,6 +30249,8 @@ snapshots: mime@4.0.7: {} + mime@4.1.0: {} + mimic-fn@1.2.0: {} mimic-fn@2.1.0: {} @@ -29669,6 +30342,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: {} @@ -29687,6 +30362,10 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 optional: true + msgpackr@1.11.12: + optionalDependencies: + msgpackr-extract: 3.0.3 + msgpackr@1.11.5: optionalDependencies: msgpackr-extract: 3.0.3 @@ -29770,6 +30449,8 @@ snapshots: muggle-string@0.3.1: {} + multipasta@0.2.7: {} + mute-stream@3.0.0: {} mz@2.7.0: @@ -29856,6 +30537,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: @@ -30057,7 +30745,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.1)(zod@3.25.76): dependencies: '@types/node': 18.19.130 '@types/node-fetch': 2.6.11 @@ -30067,21 +30755,21 @@ snapshots: formdata-node: 4.4.1 node-fetch: 2.7.0 optionalDependencies: - ws: 8.19.0 + ws: 8.20.1 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.1)(zod@3.25.76): + optionalDependencies: + ws: 8.20.1 + zod: 3.25.76 + openapi-types@12.1.3: {} openapi3-ts@4.5.0: @@ -30497,14 +31185,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.9.0): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + tsx: 4.21.0 + yaml: 2.9.0 + + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 postcss: 8.5.6 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.9.0 postcss-modules-extract-imports@3.1.0(postcss@8.5.6): dependencies: @@ -30716,6 +31413,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 @@ -30725,6 +31429,8 @@ snapshots: punycode@2.3.1: {} + pure-rand@8.4.0: {} + pyodide@0.28.3: dependencies: ws: 8.19.0 @@ -31119,6 +31825,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 @@ -31478,6 +32190,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: {} @@ -31817,6 +32533,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: {} @@ -31901,6 +32621,8 @@ snapshots: dependencies: type-fest: 0.7.1 + standard-as-callback@2.1.0: {} + state-local@1.0.7: {} statuses@1.5.0: {} @@ -31911,6 +32633,8 @@ snapshots: std-env@3.9.0: {} + std-env@4.1.0: {} + stream-browserify@3.0.0: dependencies: inherits: 2.0.4 @@ -32073,6 +32797,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: @@ -32097,9 +32826,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.9.0)): dependencies: - tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.2) + tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.9.0) tailwindcss-animate@1.0.7(tailwindcss@4.2.2): dependencies: @@ -32133,6 +32862,34 @@ snapshots: - tsx - yaml + tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.9.0): + 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.9.0) + 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: {} @@ -32243,6 +33000,8 @@ snapshots: tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} + tinyspy@2.2.1: {} tinyspy@3.0.2: {} @@ -32277,6 +33036,8 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + toml@4.1.1: {} + tough-cookie@6.0.1: dependencies: tldts: 7.0.23 @@ -32379,7 +33140,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.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) cac: 6.7.14 @@ -32390,7 +33151,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.9.0) resolve-from: 5.0.0 rollup: 4.57.1 source-map: 0.7.6 @@ -32409,7 +33170,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.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) cac: 6.7.14 @@ -32420,7 +33181,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.9.0) resolve-from: 5.0.0 rollup: 4.57.1 source-map: 0.7.6 @@ -32439,7 +33200,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.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) cac: 6.7.14 @@ -32450,7 +33211,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.9.0) resolve-from: 5.0.0 rollup: 4.57.1 source-map: 0.7.6 @@ -32469,7 +33230,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.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) cac: 6.7.14 @@ -32480,7 +33241,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.9.0) resolve-from: 5.0.0 rollup: 4.57.1 source-map: 0.7.6 @@ -32610,6 +33371,8 @@ snapshots: typescript@5.4.2: {} + typescript@5.6.1-rc: {} + typescript@5.8.2: {} typescript@5.9.3: {} @@ -32643,14 +33406,16 @@ snapshots: undici-types@7.16.0: optional: true - undici@6.23.0: {} - undici@6.24.1: {} undici@7.24.7: {} + undici@8.3.0: {} + 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 @@ -32752,13 +33517,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@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@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@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 @@ -32799,7 +33564,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 @@ -32811,6 +33576,7 @@ snapshots: ufo: 1.6.1 optionalDependencies: idb-keyval: 6.2.1 + ioredis: 5.10.1 until-async@3.0.2: {} @@ -32893,6 +33659,8 @@ snapshots: uuid@12.0.0: {} + uuid@13.0.2: {} + uuid@7.0.3: {} v8-compile-cache-lib@3.0.1: {} @@ -33045,13 +33813,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@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@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@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 @@ -33124,24 +33892,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@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@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@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 - 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.9.0)): 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.9.0) transitivePeerDependencies: - supports-color - typescript @@ -33188,7 +33956,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@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) @@ -33206,9 +33974,9 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.9.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.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -33226,7 +33994,7 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.9.0 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: @@ -33248,7 +34016,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@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) @@ -33266,9 +34034,9 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.9.0 - 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.9.0): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -33286,9 +34054,29 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.9.0 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.9.0): + 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.9.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.2): dependencies: esbuild: 0.27.3 @@ -33533,10 +34321,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.2): + 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@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.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 @@ -33553,7 +34341,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@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 @@ -33571,6 +34359,35 @@ snapshots: - tsx - yaml + vitest@4.1.7(@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.9.0)): + dependencies: + '@vitest/expect': 4.1.7 + '@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)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + 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.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 22.19.15 + '@vitest/coverage-v8': 4.1.7(vitest@4.1.7) + transitivePeerDependencies: + - msw + vlq@1.0.1: {} vm-browserify@1.1.2: {} @@ -33778,6 +34595,8 @@ snapshots: ws@8.19.0: {} + ws@8.20.1: {} + wsl-utils@0.1.0: dependencies: is-wsl: 3.1.0 @@ -33828,6 +34647,8 @@ snapshots: yaml@2.8.2: {} + yaml@2.9.0: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} 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 new file mode 100644 index 0000000000..cb081b7394 --- /dev/null +++ b/rivetkit-typescript/packages/effect/package.json @@ -0,0 +1,46 @@ +{ + "name": "@rivetkit/effect", + "version": "2.3.0-rc.4", + "description": "Effect SDK for Rivet Actors", + "license": "Apache-2.0", + "type": "module", + "sideEffects": false, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map" + ], + "exports": { + ".": "./src/mod.ts" + }, + "publishConfig": { + "access": "public", + "exports": { + ".": "./dist/mod.js" + } + }, + "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" + }, + "peerDependencies": { + "effect": "^4.0.0-beta.66", + "rivetkit": "workspace:*" + }, + "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", + "publint": "^0.3.21", + "typescript": "^5.9.2", + "vitest": "^4.1.5" + } +} diff --git a/rivetkit-typescript/packages/effect/src/Action.ts b/rivetkit-typescript/packages/effect/src/Action.ts new file mode 100644 index 0000000000..514a296d6a --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Action.ts @@ -0,0 +1,231 @@ +import { type Effect, Predicate, Schema } from "effect"; + +const TypeId = "~@rivetkit/effect/Action"; + +export const isAction = (u: unknown): u is Action => + Predicate.hasProperty(u, TypeId); + +/** + * A value-level definition for a non-durable, request-response call. + */ +export interface Action< + 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; + 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; +} + +/** + * Type-erased view of any `Action`. Useful for collections of actions + * where the specific schemas don't matter. + */ +export interface Any { + 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 { + 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; +} + +// --- 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 { + readonly _tag: Tag; +} + ? R + : never; + +export type ResultFrom = R extends Action< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error +> + ? Effect.Effect<_Success["Type"], _Error["Type"], Services> + : never; + +// --- Implementation ------------------------------------------------- + +const Proto = { + [TypeId]: TypeId, +}; + +const makeProto = < + const Tag extends string, + Payload extends Schema.Top, + Success extends Schema.Top, + Error extends Schema.Top, +>(options: { + readonly _tag: Tag; + readonly hasPayload: boolean; + readonly payloadSchema: Payload; + readonly successSchema: Success; + readonly errorSchema: Error; +}): Action => { + const self = Object.assign(Object.create(Proto), options); + self.key = `@rivetkit/effect/Action/${options._tag}`; + return self; +}; + +/** + * 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; + }, +): 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 hasPayload = options?.payload !== 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, + hasPayload, + payloadSchema, + successSchema, + errorSchema, + }) as Action< + Tag, + Payload extends Schema.Struct.Fields ? Schema.Struct : Payload, + Success, + Error + >; +}; 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..c896e77a66 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Actor.test-d.ts @@ -0,0 +1,603 @@ +import { Action, Actor, Client, type State } from "@rivetkit/effect"; +import { + Context, + Effect, + type Layer, + Schema, + SchemaTransformation, +} from "effect"; +import type { RawAccess } from "rivetkit/db"; +import { db } from "rivetkit/db"; +import { describe, expectTypeOf, it, test } from "@effect/vitest"; + +class SomeDep extends Context.Service()( + "SomeDep", +) {} + +const Ping = Action.make("Ping", { + success: Schema.Number, + error: Schema.String, +}); + +const TestActor = Actor.make("TestActor", { + actions: [Ping], +}); + +const TestState = { + schema: Schema.Struct({ + count: Schema.Number, + }), + 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 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, + 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">(); + }); +}); + +describe("Actor.make(...).toLayer", () => { + test("is a function", () => { + expectTypeOf(TestActor.toLayer).toBeFunction(); + }); + + test("accepts a plain action handlers object", () => { + expectTypeOf(TestActor.toLayer).toBeCallableWith({ + Ping: () => Effect.succeed(0), + }); + }); + + test("accepts an effect of action handlers", () => { + expectTypeOf(TestActor.toLayer).toBeCallableWith( + Effect.gen(function* () { + return { + Ping: () => Effect.succeed(0), + }; + }), + ); + }); + + test("accepts a function returning a plain action handlers object", () => { + expectTypeOf(TestActor.toLayer).toBeCallableWith( + (_wakeOptions: any) => ({ + Ping: () => Effect.succeed(0), + }), + ); + }); + + 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 { + Ping: () => Effect.succeed(0), + }; + }); + + TestActor.toLayer((wakeOptions) => { + // @ts-expect-error: actors without a state option do not expose wakeOptions.state + wakeOptions.state; + + expectTypeOf( + wakeOptions.rawRivetkitContext.state, + ).toEqualTypeOf(); + + return { + Ping: () => Effect.succeed(0), + }; + }, {}); + }); + + test("wake options carry the configured state type", () => { + TestActor.toLayer( + (wakeOptions) => { + expectTypeOf(wakeOptions.state).toEqualTypeOf< + State.State<{ readonly count: number }, Schema.SchemaError> + >(); + + return { + Ping: () => Effect.succeed(0), + }; + }, + { 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 { + Ping: () => Effect.succeed(0), + }; + }, + { state: TransformedState }, + ); + }); + + test("wake options carry the raw RivetKit context with the encoded configured state type", () => { + TestActor.toLayer( + (wakeOptions) => { + expectTypeOf( + wakeOptions.rawRivetkitContext.state, + ).toEqualTypeOf<{ readonly count: number }>(); + + return { + Ping: () => Effect.succeed(0), + }; + }, + { state: TestState }, + ); + }); + + test("wake options carry the raw RivetKit context with the encoded transformed state type", () => { + 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 { + Ping: () => Effect.succeed(0), + }; + }, + { state: TransformedState }, + ); + }); + + test("wake options carry the configured database client type", () => { + TestActor.toLayer( + (wakeOptions) => { + expectTypeOf( + wakeOptions.rawRivetkitContext.db, + ).toEqualTypeOf(); + + return { + Ping: () => Effect.succeed(0), + }; + }, + { db: db() }, + ); + }); + + test("accepts a function returning an effect of action handlers", () => { + expectTypeOf(TestActor.toLayer).toBeCallableWith((_wakeOptions: any) => + Effect.gen(function* () { + return { + Ping: () => Effect.succeed(0), + }; + }), + ); + }); + + 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 { + Ping: () => Effect.succeed(0), + }; + }); + }), + ); + }); + + test("accepts an Effect.fn returning action handlers", () => { + expectTypeOf(TestActor.toLayer).toBeCallableWith( + Effect.fn("wake")(function* (_wakeOptions) { + return { + Ping: () => Effect.succeed(0), + }; + }), + ); + }); + + test("returns a Layer", () => { + expectTypeOf(TestActor.toLayer).returns.toExtend(); + }); + + test("action handler's envelope is typed against the action", () => { + TestActor.toLayer({ + Ping: (envelope) => { + expectTypeOf(envelope._tag).toEqualTypeOf<"Ping">(); + expectTypeOf(envelope.action).toExtend(); + 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: Ping handler is required + TestActor.toLayer({}); + }); + + test.todo("unknown action handler key is rejected", () => { + TestActor.toLayer({ + Ping: () => Effect.succeed(0), + // TODO: toLayer should reject unknown action handler keys + Unknown: () => Effect.void, + }); + }); + + test.todo("wake-effect requirements surface in the Layer", () => { + const layer = TestActor.toLayer( + Effect.gen(function* () { + yield* SomeDep; + return { Ping: () => Effect.succeed(0) }; + }), + ); + 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(...).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< + Effect.Effect< + Actor.Accessor<(typeof TestActor.actions)[number]>, + never, + Client.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.test.ts b/rivetkit-typescript/packages/effect/src/Actor.test.ts new file mode 100644 index 0000000000..28f984122e --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Actor.test.ts @@ -0,0 +1,206 @@ +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"; + +class Prefix extends Context.Service()( + "Actor.test/Prefix", +) {} +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") }; + 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 wake options", () => + Effect.gen(function* () { + const rawRivetkitContext = { + key: ["room", "1"], + } as Rivetkit.WakeContextOf; + const wakeOptions: Actor.WakeOptions = { + rawRivetkitContext, + }; + const wake = (wakeOptions: Actor.WakeOptions) => ({ + GetKey: () => + Effect.succeed( + wakeOptions.rawRivetkitContext.key.join("/"), + ), + }); + 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 = { + 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 new file mode 100644 index 0000000000..f723fb9822 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Actor.ts @@ -0,0 +1,550 @@ +import { + Context, + Effect, + identity, + Layer, + Predicate, + type Record, + type Schema, + Scope, + Struct, +} 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 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"; +import type * as State from "./State.ts"; + +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, + (typeof rivetkitActorOptionsKeys)[number] +>; + +/** + * Per-actor instance options. Combines the public + * `RivetkitActorOptions` (forwarded verbatim to `Rivetkit.actor`) + * with the effect-SDK-only options. + */ +export type Options< + State extends StateOptions.Any, + Database extends RivetkitDb.AnyDatabaseProvider = undefined, +> = Readonly & { + readonly state?: State; + readonly db?: Database; +}; + +type StatelessOptions< + Database extends RivetkitDb.AnyDatabaseProvider = undefined, +> = Readonly & { + readonly state?: never; + readonly db?: Database; +}; + +type StatefulOptions< + State extends StateOptions.Any, + Database extends RivetkitDb.AnyDatabaseProvider = undefined, +> = Readonly & { + readonly state: State; + readonly db?: Database; +}; + +const splitOptions = < + State extends StateOptions.Any, + Database extends RivetkitDb.AnyDatabaseProvider, +>( + options: Options, +) => ({ + rivetkitOptions: Struct.pick(options, rivetkitActorOptionsKeys), + effectOptions: Struct.omit(options, rivetkitActorOptionsKeys), +}); + +/** + * 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 wake effect via + * `yield* Actor.CurrentAddress`. + */ +export type ActorAddress = Pick< + Rivetkit.ActorContext, + "actorId" | "name" | "key" +>; + +/** + * Context tag for the current actor instance's address. Provided + * once per wake when the wake effect runs; capture it into a + * closure if action handlers need it. + */ +export class CurrentAddress extends Context.Service< + CurrentAddress, + ActorAddress +>()("@rivetkit/effect/Actor/CurrentAddress") {} + +export class Sleep extends Context.Service>()( + "@rivetkit/effect/Actor/Sleep", +) {} + +export type ActionRequest = + A extends Action.Action< + infer Tag, + infer Payload, + infer _Success, + infer _Error + > + ? { + readonly _tag: Tag; + readonly action: A; + readonly payload: Payload["Type"]; + } + : never; + +type ActionHandlerServices = { + readonly [Name in keyof ActionHandlers]: ActionHandlers[Name] extends ( + ...args: ReadonlyArray + ) => Effect.Effect + ? R + : never; +}[keyof ActionHandlers]; + +type RivetkitActorDefinitionFor< + State extends StateOptions.Any, + Database extends RivetkitDb.AnyDatabaseProvider, +> = Rivetkit.ActorDefinition< + StateOptions.Encoded, + undefined, + undefined, + undefined, + undefined, + Database, + Record, + Record, + any +>; + +export type WakeOptions< + ActorDefinition extends + Rivetkit.AnyActorDefinition = Rivetkit.AnyActorDefinition, +> = { + readonly rawRivetkitContext: Rivetkit.WakeContextOf; +}; + +type RawWakeContextFor< + State extends StateOptions.Any, + Database extends RivetkitDb.AnyDatabaseProvider, +> = { + [Key in keyof Rivetkit.WakeContextOf< + RivetkitActorDefinitionFor + >]: Key extends "state" + ? [State] extends [never] + ? never + : StateOptions.Encoded + : Rivetkit.WakeContextOf< + RivetkitActorDefinitionFor + >[Key]; +}; + +type WakeOptionsFor< + StateDefinition extends StateOptions.Any, + Database extends RivetkitDb.AnyDatabaseProvider, +> = { + readonly rawRivetkitContext: RawWakeContextFor; +} & ([StateDefinition] extends [never] + ? unknown + : { + readonly state: State.State< + StateOptions.Decoded, + Schema.SchemaError + >; + }); + +type WakeFunction = + | ((wakeOptions: W) => ActionHandlers) + | ((wakeOptions: W) => Effect.Effect); + +type Wake = + | ActionHandlers + | Effect.Effect + | WakeFunction + | Effect.Effect, never, RX>; + +export type AccessorKeyParam = string | Rivetkit.ActorKey; + +/** + * 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, + Action.ServicesClient + >; +}; + +/** + * Yielded by `Actor.client`. Address an actor instance by key, then + * dispatch typed action calls against the returned `Handle`. + */ +export type Accessor = { + readonly getOrCreate: (key: AccessorKeyParam) => Handle; +}; + +type UnknownToNever = unknown extends T ? never : T; + +type ExcludeBuiltInWakeServices< + T, + _State extends StateOptions.Any, +> = UnknownToNever>; + +type ToLayerRequirements< + Actions extends Action.Any, + ActionHandlers, + State extends StateOptions.Any, + 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. + */ +export interface Actor< + Name extends string, + Actions extends Action.Any = never, +> { + readonly [TypeId]: typeof TypeId; + readonly name: Name; + readonly actions: ReadonlyArray; + + of>( + 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, + RX = never, + >( + wake: Wake>, + ): Layer.Layer< + never, + never, + ToLayerRequirements + >; + + toLayer< + ActionHandlers extends ActionHandlersFrom, + State extends StateOptions.Any, + Database extends RivetkitDb.AnyDatabaseProvider = undefined, + R = never, + RX = never, + >( + wake: Wake>, + options: StatefulOptions, + ): Layer.Layer< + never, + never, + ToLayerRequirements + >; + + /** + * 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. + */ + readonly client: Effect.Effect, never, Client.Client>; +} + +export type Any = Actor; + +export type ActionHandlersFrom = { + readonly [A in Actions as A["_tag"]]: ( + envelope: ActionRequest, + ) => Action.ResultFrom; +}; + +const Proto: Omit, "name" | "actions"> = { + [TypeId]: TypeId, + toLayer< + Actions extends Action.AnyWithProps, + ActionHandlers extends ActionHandlersFrom, + State extends StateOptions.Any = never, + Database extends RivetkitDb.AnyDatabaseProvider = undefined, + R = never, + RX = never, + >( + this: Actor, + wake: Wake>, + options: Options = {}, + ) { + return makeRivetkitActor({ + actor: this, + wakeHandler: toWakeHandler< + ActionHandlers, + R, + RX, + WakeOptionsFor + >(wake), + options, + }).pipe( + Effect.flatMap((rivetKitActor) => + Registry.Registry.pipe( + Effect.flatMap((registry) => + Effect.sync(() => + registry.rivetkitActors.set( + this.name, + rivetKitActor, + ), + ), + ), + ), + ), + Layer.effectDiscard, + ); + }, + get client() { + return Client.Client.pipe( + Effect.map((client) => client.makeActorAccessor(this as Any)), + ); + }, + of: identity, +}; + +/** + * Define a Rivet Actor contract. + */ +export const make = < + const Name extends string, + const Actions extends ReadonlyArray = readonly [], +>( + name: Name, + options?: { + readonly actions?: Actions; + }, +): Actor => { + const self = Object.create(Proto); + self.name = name; + self.actions = options?.actions ?? []; + return self; +}; + +export function toWakeHandler< + ActionHandlers extends object, + R, + RX, + W extends WakeOptions = WakeOptions, +>( + wake: Effect.Effect< + (wakeOptions: W) => Effect.Effect, + never, + RX + >, +): (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: W) => Effect.Effect; +export function toWakeHandler< + ActionHandlers extends object, + W extends WakeOptions = WakeOptions, +>(wake: ActionHandlers): (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, + 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, + ActionHandlers extends ActionHandlersFrom, + RX, + State extends StateOptions.Any = never, + Database extends RivetkitDb.AnyDatabaseProvider = undefined, +>({ + actor, + wakeHandler, + options, +}: { + readonly actor: Actor; + readonly wakeHandler: ( + wakeOptions: WakeOptionsFor, + ) => Effect.Effect; + readonly options: Options; +}) { + const { effectOptions, rivetkitOptions } = splitOptions(options); + const stateAdapter = + effectOptions.state === undefined + ? undefined + : yield* ActorStateAdapter.make(effectOptions.state); + + const instanceManager = yield* ActorInstanceManager.make< + ActionHandlers, + State, + Database, + WakeOptionsFor + >({ + wakeHandler: (wakeOptions) => + wakeHandler(wakeOptions).pipe( + Effect.annotateLogs( + makeActorLogAnnotations(wakeOptions.rawRivetkitContext), + ), + ), + stateAdapter, + 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, + RivetkitActorDefinitionFor + >({ + actor, + getInstance: instanceManager.get, + }); + + return Rivetkit.actor< + StateOptions.Encoded, + undefined, + undefined, + undefined, + undefined, + Database, + Record, + Record, + any + >({ + options: rivetkitOptions, + ...(effectOptions.db ? { db: effectOptions.db } : {}), + onWake: instanceManager.onWake, + ...(stateAdapter + ? { createState: stateAdapter.createInitialState } + : {}), + actions, + ...(instanceManager.onStateChange + ? { onStateChange: instanceManager.onStateChange } + : {}), + onSleep: instanceManager.onTeardown, + onDestroy: instanceManager.onTeardown, + }); +}); 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..854e2ca56c --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Client.test.ts @@ -0,0 +1,210 @@ +import { assert, describe, it } from "@effect/vitest"; +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"), + 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: ActionErrorEnvelope.tag, + version: ActionErrorEnvelope.schemaVersion, + 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: ActionErrorEnvelope.tag, + version: ActionErrorEnvelope.schemaVersion, + 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", + ); + }), + ); +}); diff --git a/rivetkit-typescript/packages/effect/src/Client.ts b/rivetkit-typescript/packages/effect/src/Client.ts new file mode 100644 index 0000000000..c4e1fcd8c6 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Client.ts @@ -0,0 +1,216 @@ +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"; + +/** + * Connection options for the Rivet Engine client transport. Mirrors + * the `(endpoint, token, namespace)` subset of rivetkit's + * `ClientConfigInput`. + */ +export type Options = 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; +} + +export interface Client { + readonly [TypeId]: typeof TypeId; + + readonly makeActorAccessor: ( + actor: Actor.Actor, + ) => Actor.Accessor; +} + +export const Client: Context.Service = Context.Service( + "@rivetkit/effect/Client", +); + +export const make = Effect.fnUntraced(function* (options: Options = {}) { + const baseLogger = yield* getOrCreateBaseLogger; + const rivetkitClient = yield* Effect.acquireRelease( + Effect.sync(() => { + configureBaseLogger(baseLogger); + return 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.encodeEffect( + Schema.toCodecJson(action.payloadSchema), + ); + const decodeSuccess = Schema.decodeUnknownEffect( + Schema.toCodecJson(action.successSchema), + ); + const classifyRivetkitActionFailure = + makeRivetkitActionFailureClassifier(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, + ).pipe( + Effect.mapError( + (cause) => + new RivetError.RivetError({ + reason: new RivetError.InvalidEncoding( + { + cause: new RivetkitErrors.RivetError( + "encoding", + "invalid", + "Could not encode action payload", + { + public: true, + metadata: cause, + }, + ), + }, + ), + }), + ), + ); + + const encodedSuccess = yield* Effect.tryPromise( + (abortSignal) => + rivetkitActorHandle.action({ + name: action._tag, + args: [encodedPayload, meta], + signal: abortSignal, + }), + ).pipe( + Effect.catch((unknownError) => + classifyRivetkitActionFailure( + unknownError.cause, + ).pipe(Effect.flatMap(Effect.fail)), + ), + ); + + return yield* decodeSuccess(encodedSuccess).pipe( + Effect.orDie, + ); + }), + ]; + }) as Actor.Handle<(typeof actor.actions)[number]>; + }, + }), + }); +}); + +export const layer = (options: Options = {}): Layer.Layer => + Layer.unwrap( + Effect.map(getOrCreateBaseLogger, (baseLogger) => + Layer.effect(Client, make(options)).pipe( + Layer.provideMerge(Logger.layerPino(baseLogger)), + ), + ), + ); + +const decodeActionErrorEnvelope = Schema.decodeUnknownEffect( + ActionErrorEnvelope.ActionErrorEnvelope, +); + +/** @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, + ): Effect.fn.Return< + ActionErrorSchema["Type"] | RivetError.RivetError, + 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 actionErrorEnvelope = yield* Effect.result( + decodeActionErrorEnvelope(rivetkitRivetError.metadata), + ); + + // 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(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({ + cause: actionErrorResult.failure, + rivetError: rivetkitRivetError, + }), + }); + } + + // Successfully decoded user-declared action error + return actionErrorResult.success; + }); +}; 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-d.ts b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts new file mode 100644 index 0000000000..49ff135edf --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Registry.test-d.ts @@ -0,0 +1,126 @@ +import { Action, Actor, Registry } from "@rivetkit/effect"; +import { type Context, Effect, Layer, type Scope } from "effect"; +import type { + HttpServerError, + HttpServerRequest, + HttpServerResponse, +} from "effect/unstable/http"; +import { describe, expectTypeOf, test } from "vitest"; + +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", + noWelcome: true, + }); + }); + + test("does not accept serverless options", () => { + Registry.layer({ + // @ts-expect-error: serverless routing belongs to toWebHandler and toHttpEffect options. + serverless: { + basePath: "/", + }, + }); + }); +}); + +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); + }); + + 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); + }); + + test("accepts serverless routing options", () => { + expectTypeOf(Registry.toWebHandler).toBeCallableWith(RegistryLive, { + basePath: "/", + maxStartPayloadBytes: 1024, + }); + }); + + 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); + + expectTypeOf(handler.handler).toEqualTypeOf< + ( + request: Request, + context?: Context.Context | undefined, + ) => Promise + >(); + expectTypeOf(handler.dispose).toEqualTypeOf<() => Promise>(); + }); +}); + +describe("Registry.toHttpEffect", () => { + test("accepts serverless routing options", () => { + expectTypeOf(Registry.toHttpEffect).toBeCallableWith(RegistryLive, { + basePath: "/", + maxStartPayloadBytes: 1024, + }); + }); + + 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 + Registry.toHttpEffect(TestActorLive); + }); + + 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 + > + >(); + }); +}); 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..e015447b96 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Registry.test.ts @@ -0,0 +1,411 @@ +import { assert, describe, it } from "@effect/vitest"; +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", { + 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", + noWelcome: true, + }), + ), +); + +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); + + 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; + }; + assert.ok(body.actorNames.TestActor); + } finally { + await dispose(); + } + }); + + it("uses a custom serverless base path", async () => { + const { handler, dispose } = Registry.toWebHandler(RegistryLive, { + 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; + }; + assert.ok(body.actorNames.TestActor); + } finally { + await dispose(); + } + }); + + it("uses the custom base path to identify start requests", async () => { + const { handler, dispose } = Registry.toWebHandler(RegistryLive, { + 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" }, + ); + 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, { + 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("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( + 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("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( + 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", () => { + 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"), + ), + ); + + yield* Effect.promise(() => + (async (response: Response) => { + assert.strictEqual(response.status, 200); + const body = (await response.json()) as { + readonly actorNames: Record; + }; + assert.ok(body.actorNames.TestActor); + })(response), + ); + }), + ), + ); + + it.effect("uses a custom serverless base path", () => + Effect.scoped( + Effect.gen(function* () { + const httpEffect = yield* Registry.toHttpEffect(RegistryLive, { + basePath: "/", + }); + const handler = HttpEffect.toWebHandler(httpEffect); + const response = yield* Effect.promise(() => + handler(new Request("http://runner.test/metadata")), + ); + + yield* Effect.promise(() => + (async (response: Response) => { + assert.strictEqual(response.status, 200); + const body = (await response.json()) as { + readonly actorNames: Record; + }; + 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, { + 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), + ); + }), + ), + ); + + it.effect("uses a custom serverless start payload size limit", () => + Effect.scoped( + Effect.gen(function* () { + const httpEffect = yield* Registry.toHttpEffect(RegistryLive, { + 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]), + }), + ), + ); + + 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), + ); + }), + ), + ); +}); diff --git a/rivetkit-typescript/packages/effect/src/Registry.ts b/rivetkit-typescript/packages/effect/src/Registry.ts new file mode 100644 index 0000000000..71b72e7d1b --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/Registry.ts @@ -0,0 +1,243 @@ +import { Context, Effect, Layer, type Scope } from "effect"; +import { + HttpEffect, + type HttpMiddleware, + type HttpServerError, + type HttpServerRequest, + 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< + Rivetkit.RegistryConfigInput["serverless"] +>; + +export type Options = Pick< + Rivetkit.RegistryConfigInput, + "endpoint" | "token" | "namespace" | "noWelcome" +>; + +export interface Registry { + readonly [TypeId]: typeof TypeId; + + readonly options: Options; + + readonly baseLogger: PinoLogger; + + readonly rivetkitActors: Map; +} + +export const Registry: Context.Service = + Context.Service("@rivetkit/effect/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.effect( + Registry, + Effect.map(getOrCreateBaseLogger, (baseLogger) => + make(options, baseLogger), + ), + ); + +const setupRivetkitRegistry = ( + registry: Registry, + options?: { + readonly serverless?: ServerlessOptions | undefined; + }, +) => { + 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. + * + * 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 = ( + actorsLayer: Layer.Layer, + ): 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()); + }), + ); + +/** + * 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.Client, + Effect.gen(function* () { + const registry = yield* Registry; + const rivetkitRegistry = setupRivetkitRegistry(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; + + return yield* Client.make({ + ...registry.options, + endpoint: registry.options.endpoint ?? resolvedEndpoint, + }).pipe( + Effect.provideService(BaseLogger, registry.baseLogger), + ); + }), + ); + +const makeHttpEffect = ( + registry: Registry, + options?: ToHttpEffectOptions, +): Effect.Effect< + HttpServerResponse.HttpServerResponse, + HttpServerError.HttpServerError, + HttpServerRequest.HttpServerRequest +> => { + const rivetkitRegistry = setupRivetkitRegistry(registry, { + serverless: options, + }); + return HttpEffect.fromWebHandler((request) => + rivetkitRegistry.handler(request), + ); +}; + +export type ToHttpEffectOptions = ServerlessOptions; + +/** + * 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 = Effect.fnUntraced(function* ( + registryLayer: Layer.Layer, + options?: ToHttpEffectOptions, +): Effect.fn.Return< + Effect.Effect< + HttpServerResponse.HttpServerResponse, + HttpServerError.HttpServerError, + HttpServerRequest.HttpServerRequest + >, + E, + Scope.Scope +> { + const context = yield* Layer.build( + registryLayer.pipe(Layer.provideMerge(Logger.layer)), + ); + // @effect-diagnostics-next-line returnEffectInGen:off + return makeHttpEffect(Context.get(context, Registry), options).pipe( + Effect.provide(context), + ); +}); + +export type ToWebHandlerOptions = ServerlessOptions & { + /** + * 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, +) => { + const { middleware, memoMap } = options ?? {}; + let serverlessOptions: ServerlessOptions | undefined; + if (options !== undefined) { + const { + middleware: _middleware, + memoMap: _memoMap, + ...handlerOptions + } = options; + serverlessOptions = handlerOptions; + } + + const registryLayerWithLogging = registryLayer.pipe( + Layer.provideMerge(Logger.layer), + ); + + return HttpEffect.toWebHandlerLayerWith(registryLayerWithLogging, { + toHandler: (context) => + Effect.succeed( + makeHttpEffect( + Context.get(context, Registry), + serverlessOptions, + ).pipe(Effect.provide(context)), + ), + middleware, + memoMap, + }); +}; 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..dad3e9bfe5 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/RivetError.test.ts @@ -0,0 +1,188 @@ +import { assert, describe, it } from "@effect/vitest"; +import { RivetError } from "@rivetkit/effect"; +import { Duration, Effect, Schema } from "effect"; +import * as RivetkitErrors from "rivetkit/errors"; + +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); + }); + + 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( + "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"); + }); + + 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 new file mode 100644 index 0000000000..3fb88d4ed8 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/RivetError.ts @@ -0,0 +1,1044 @@ +import { Duration, Option, Predicate, Record, Schema } from "effect"; +import * as RivetkitErrors from "rivetkit/errors"; + +const ReasonTypeId = "~@rivetkit/effect/RivetError/Reason" as const; +const TypeId = "~@rivetkit/effect/RivetError" as const; + +export class Forbidden extends Schema.TaggedErrorClass( + `${ReasonTypeId}/Forbidden`, +)("Forbidden", { + 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return false; + } +} + +export class ActorNotFound extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ActorNotFound`, +)("ActorNotFound", { + 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return false; + } +} + +export class ActorStopping extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ActorStopping`, +)("ActorStopping", { + 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return true; + } +} + +export class ActorRestarting extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ActorRestarting`, +)("ActorRestarting", { + 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + 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( + `${ReasonTypeId}/ActionNotFound`, +)("ActionNotFound", { + 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return false; + } +} + +export class ActionTimedOut extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ActionTimedOut`, +)("ActionTimedOut", { 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return true; + } +} + +export class ActionAborted extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ActionAborted`, +)("ActionAborted", { + 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return false; + } +} + +export class ActorOverloaded extends Schema.TaggedErrorClass( + `${ReasonTypeId}/ActorOverloaded`, +)("ActorOverloaded", { + 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return true; + } +} + +export class IncomingMessageTooLong extends Schema.TaggedErrorClass( + `${ReasonTypeId}/IncomingMessageTooLong`, +)("IncomingMessageTooLong", { + 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return false; + } +} + +export class OutgoingMessageTooLong extends Schema.TaggedErrorClass( + `${ReasonTypeId}/OutgoingMessageTooLong`, +)("OutgoingMessageTooLong", { + 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return false; + } +} + +export class InvalidEncoding extends Schema.TaggedErrorClass( + `${ReasonTypeId}/InvalidEncoding`, +)("InvalidEncoding", { + 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return false; + } +} + +export class InvalidRequest extends Schema.TaggedErrorClass( + `${ReasonTypeId}/InvalidRequest`, +)("InvalidRequest", { + 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return false; + } +} + +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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return true; + } +} + +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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return false; + } +} + +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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return true; + } +} + +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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return true; + } +} + +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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return true; + } +} + +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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return true; + } +} + +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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return true; + } +} + +export class GuardGatewayResponseStartTimeout extends Schema.TaggedErrorClass( + `${ReasonTypeId}/GuardGatewayResponseStartTimeout`, +)("GuardGatewayResponseStartTimeout", { + 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return true; + } +} + +export class InternalError extends Schema.TaggedErrorClass( + `${ReasonTypeId}/InternalError`, +)("InternalError", { + 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return false; + } +} + +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}`; + } + get group() { + return this.rivetError.group; + } + get code() { + return this.rivetError.code; + } + get metadata() { + return this.rivetError.metadata; + } + get actor() { + return this.rivetError.actor; + } + get statusCode() { + return this.rivetError.statusCode; + } + get public() { + return this.rivetError.public; + } + get isRetryable(): boolean { + return false; + } +} + +/** + * 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 UnknownUserError extends Schema.TaggedErrorClass( + `${ReasonTypeId}/UnknownUserError`, +)("UnknownUserError", { 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; + } + get statusCode() { + return this.cause.statusCode; + } + get public() { + return this.cause.public; + } + get isRetryable(): boolean { + return false; + } +} + +/** + * Forward-compatible catch-all for `(group, code)` pairs the SDK does + * 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", { + message: Schema.String, + cause: Schema.Unknown, +}) { + 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; + } + 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; + } +} + +export type RivetErrorReason = + | Forbidden + | ActorNotFound + | ActorStopping + | ActorRestarting + | ActionNotFound + | ActionTimedOut + | ActionAborted + | ActorOverloaded + | IncomingMessageTooLong + | OutgoingMessageTooLong + | InvalidEncoding + | InvalidRequest + | GuardActorReadyTimeout + | GuardActorRunnerFailed + | GuardServiceUnavailable + | GuardActorStoppedWhileWaiting + | GuardTunnelRequestAborted + | GuardTunnelMessageTimeout + | GuardTunnelResponseClosed + | GuardGatewayResponseStartTimeout + | InternalError + | UnknownUserError + | ActionErrorDecodeFailed + | UnknownError; + +export const RivetErrorReason: Schema.Union< + [ + typeof Forbidden, + typeof ActorNotFound, + typeof ActorStopping, + typeof ActorRestarting, + typeof ActionNotFound, + typeof ActionTimedOut, + typeof ActionAborted, + typeof ActorOverloaded, + typeof IncomingMessageTooLong, + typeof OutgoingMessageTooLong, + typeof InvalidEncoding, + typeof InvalidRequest, + typeof GuardActorReadyTimeout, + typeof GuardActorRunnerFailed, + typeof GuardServiceUnavailable, + typeof GuardActorStoppedWhileWaiting, + typeof GuardTunnelRequestAborted, + typeof GuardTunnelMessageTimeout, + typeof GuardTunnelResponseClosed, + typeof GuardGatewayResponseStartTimeout, + typeof InternalError, + typeof ActionErrorDecodeFailed, + typeof UnknownUserError, + typeof UnknownError, + ] +> = Schema.Union([ + Forbidden, + ActorNotFound, + ActorStopping, + ActorRestarting, + ActionNotFound, + ActionTimedOut, + ActionAborted, + ActorOverloaded, + IncomingMessageTooLong, + OutgoingMessageTooLong, + InvalidEncoding, + InvalidRequest, + GuardActorReadyTimeout, + GuardActorRunnerFailed, + GuardServiceUnavailable, + GuardActorStoppedWhileWaiting, + GuardTunnelRequestAborted, + GuardTunnelMessageTimeout, + GuardTunnelResponseClosed, + GuardGatewayResponseStartTimeout, + InternalError, + ActionErrorDecodeFailed, + UnknownUserError, + UnknownError, +]); + +export const isRivetErrorReason = (u: unknown): u is RivetErrorReason => + Predicate.hasProperty(u, ReasonTypeId); + +/** + * 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`: + * + * ```ts + * program.pipe( + * Effect.catchReasons("RivetError", { + * Forbidden: () => Effect.fail(new MyAuthError()), + * ConnectionLost: () => Effect.logWarning("reconnecting"), + * }), + * ) + * ``` + * + * 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( + "@rivetkit/effect/RivetError", +)("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 `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; + } + + /** Delegates to the underlying reason's `retryAfter` if present. */ + get retryAfter(): Duration.Duration | undefined { + return "retryAfter" in this.reason ? this.reason.retryAfter : undefined; + } +} + +export const isRivetError = (u: unknown): u is RivetError => + Predicate.hasProperty(u, TypeId); + +type MakeRivetErrorReason = ( + error: RivetkitErrors.RivetError, +) => RivetErrorReason; + +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 }), + "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, +): RivetErrorReason => { + const makeReason = reasonByCode[`${error.group}.${error.code}`]; + if (makeReason) return makeReason(error); + + if (error.group === "user") return new UnknownUserError({ cause: error }); + + return new UnknownError({ + message: error.message, + cause: error, + }); +}; + +export const fromRivetkitRivetError = ( + error: RivetkitErrors.RivetError, +): RivetError => { + return new RivetError({ reason: reasonFromRivetkitRivetError(error) }); +}; + +export const fromUnknown = (cause: unknown): RivetError => { + if (isRivetError(cause)) return 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, + }), + }); +}; 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..a1ef2c2a48 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/State.test.ts @@ -0,0 +1,181 @@ +import { assert, describe, it } from "@effect/vitest"; +import { State } from "@rivetkit/effect"; +import { Effect, Exit, PubSub, Stream } from "effect"; + +// 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); + }), + ); + + 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 new file mode 100644 index 0000000000..afe18ffdf9 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/State.ts @@ -0,0 +1,224 @@ +/** + * `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 + * - `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 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 interface Variance { + readonly [TypeId]: { + readonly _A: Types.Invariant; + readonly _E: Types.Covariant; + readonly _R: Types.Covariant; + }; +} + +const Proto = { + ...Pipeable.Prototype, + ...Inspectable.BaseProto, + [TypeId]: { _A: identity, _E: 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 = Effect.fnUntraced(function* ( + read: () => Effect.Effect, + write: (value: A) => Effect.Effect, +): 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. + */ +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/internal/ActionDispatcher.ts b/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts new file mode 100644 index 0000000000..21cc63c42f --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts @@ -0,0 +1,192 @@ +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 { makeActorLogAnnotations } from "./logging.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.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. + 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, + }, + }), + Effect.annotateLogs(makeActorLogAnnotations(c)), + ); + 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(); + } + const expectedError = Exit.findErrorOption(exit); + if (Option.isSome(expectedError)) { + throw expectedError.value; + } + throw Cause.squash(exit.cause); + }, + ]; + }); + +const makeActorAbortedError = () => + new Rivetkit.RivetError("actor", "aborted", "Actor aborted", { + public: true, + }); 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, +}); diff --git a/rivetkit-typescript/packages/effect/src/internal/ActorInstanceManager.ts b/rivetkit-typescript/packages/effect/src/internal/ActorInstanceManager.ts new file mode 100644 index 0000000000..c01b99c482 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/internal/ActorInstanceManager.ts @@ -0,0 +1,143 @@ +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"; + +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?: ActorStateAdapter.ActorState; +}; + +export const make = Effect.fnUntraced(function* < + ActionHandlers, + StateDefinition extends StateOptions.Any, + Database extends RivetkitDb.AnyDatabaseProvider, + WakeOptions, +>({ + wakeHandler, + stateAdapter, + makeContext, + makeWakeOptions, +}: { + readonly wakeHandler: ( + wakeOptions: WakeOptions, + ) => Effect.Effect; + readonly stateAdapter: + | ActorStateAdapter.Adapter + | undefined; + readonly makeContext: ( + c: WakeContext, + scope: Scope.Closeable, + ) => Context.Context; + readonly makeWakeOptions: ( + c: WakeContext, + state: ActorStateAdapter.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(); + 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, + }; + }).pipe( + Effect.onError((cause) => + Scope.close(scope, Exit.failCause(cause)), + ), + ); + }); + + 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: stateAdapter + ? ( + c: WakeContext, + newState: unknown, + ) => { + const instance = instances.get(c.actorId); + // State changes can arrive after teardown removes the instance. + if (!instance) return; + + stateAdapter.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/ActorStateAdapter.ts b/rivetkit-typescript/packages/effect/src/internal/ActorStateAdapter.ts new file mode 100644 index 0000000000..fa5d105758 --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/internal/ActorStateAdapter.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 +>; + +type StateInstance = { + readonly runFork: ( + effect: Effect.Effect, + options?: Effect.RunOptions, + ) => Fiber.Fiber; + readonly state?: ActorState; +}; + +export type Adapter = { + readonly makeStateView: (c: { + state: StateOptions.Encoded; + }) => Effect.Effect, never, any>; + readonly createInitialState: () => Promise< + StateOptions.Encoded + >; + readonly publishChange: ( + instance: StateInstance, + 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); + }), + ); + }), + ); + }, + }; +}); 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/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/internal/tracing.ts b/rivetkit-typescript/packages/effect/src/internal/tracing.ts new file mode 100644 index 0000000000..bdb91d92a0 --- /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.actors"; + +/** + * 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/src/internal/utils.ts b/rivetkit-typescript/packages/effect/src/internal/utils.ts new file mode 100644 index 0000000000..6ad3734cef --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/internal/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]); diff --git a/rivetkit-typescript/packages/effect/src/mod.ts b/rivetkit-typescript/packages/effect/src/mod.ts new file mode 100644 index 0000000000..bf15e095ab --- /dev/null +++ b/rivetkit-typescript/packages/effect/src/mod.ts @@ -0,0 +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"; diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts new file mode 100644 index 0000000000..16c2108700 --- /dev/null +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -0,0 +1,874 @@ +import { assert, layer } from "@effect/vitest"; +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, + BuildSetRejectedLive, + Counter, + CounterLive, + CounterOverflowError, + FailingActor, + FailingActorLive, + FailingWakeCleanup, + FailingWakeCleanupLive, + Flags, + Greeter, + Multiplier, + Pinger, + PingerLive, + ScaledOverflowError, + Strict, + StrictLive, + TransformedStateActor, + TransformedStateActorLive, + Unregistered, + WakeDecodeFail, + WakeDecodeFailLive, +} from "./fixtures/actors"; +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, + Greeter.of({ + greet: (name) => `Hello, ${name}!`, + }), +); + +// `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 })); + +// 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( + Registry.test.pipe( + Layer.provideMerge( + Layer.mergeAll( + CounterLive, + PingerLive, + FailingActorLive, + FailingWakeCleanupLive, + StrictLive, + WakeDecodeFailLive, + BuildSetRejectedLive, + TransformedStateActorLive, + ), + ), + Layer.provideMerge(Flags.layer), + Layer.provide(GreeterLive), + Layer.provideMerge(MultiplierLive), + Layer.provideMerge(TestTracer.layer()), + Layer.provide(Registry.layer({ endpoint, token, namespace })), + ), + ), +); + +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( + "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("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; + 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 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); + + 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); + + const persistedAfterWake = yield* counter.GetPersistedState(); + assert.strictEqual(persistedAfterWake.count, 11); + }), + ); + + it.effect("persists state with a non-trivial schema (Date)", () => + Effect.gen(function* () { + 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({ + when, + }); + assert.strictEqual(beforeSleep.toISOString(), when.toISOString()); + + 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); + + const persistedAfterWake = yield* counter.GetPersistedState(); + assert.strictEqual( + persistedAfterWake.when.toISOString(), + when.toISOString(), + ); + }), + ); + + it.effect("persists state with a custom Schema.transform", () => + Effect.gen(function* () { + 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({ + tags, + }); + assert.deepEqual(beforeSleep, tags); + + 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); + + const persistedAfterWake = yield* counter.GetPersistedState(); + assert.deepEqual(persistedAfterWake.tags, tags); + }), + ); + + it.effect("persists state through a service-dependent transform", () => + Effect.gen(function* () { + 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 + // 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); + + 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); + + const persistedAfterWake = yield* counter.GetPersistedState(); + assert.strictEqual(persistedAfterWake.scaled, 14); + }), + ); + + 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( + "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.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/); + } + }), + ); + + 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("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"]); + 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( + "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 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]); + const url = new URL( + "https://rivet.dev/docs/actors?section=state", + ); + const id = 9_007_199_254_740_993n; + + yield* actor.SetTransformedStateAndSleep({ + when, + instant, + 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(), + instant: DateTime.formatIso(instant), + 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( + "wake options state 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 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"; + 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, + instant, + 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( + DateTime.toEpochMillis(decoded.instant), + Date.parse(instant), + ); + 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([ + "t-service-wake", + ]); + // `WakeGreeting` returns the string captured when `Greeter` + // was resolved 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-service-handler", + ]); + // `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("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.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.instanceOf( + exit.value.reason, + RivetError.GuardServiceUnavailable, + ); + assert.strictEqual( + ( + exit.value + .reason as RivetError.GuardServiceUnavailable + ).code, + "service_unavailable", + ); + } + }), + ); + + 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* () { + // `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( + "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); + } + + 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", + ]); + 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( + "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.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, + ); + } + } + }), + ); + + 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" }); + 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 key = "t-db-persist"; + const counter = (yield* Counter.client).getOrCreate([key]); + yield* counter.LogEvent({ event: "before-sleep" }); + + const flags = yield* Flags; + const flagName = `finalizer:${key}`; + + yield* counter.PersistAndSleep({ amount: 1 }); + 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); + + yield* counter.LogEvent({ event: "after-wake" }); + assert.deepStrictEqual(yield* counter.ListEvents(), [ + "before-sleep", + "after-wake", + ]); + }), + ); +}); diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts new file mode 100644 index 0000000000..2cf2a8f968 --- /dev/null +++ b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts @@ -0,0 +1,732 @@ +import { Action, Actor, State } from "@rivetkit/effect"; +import { + Context, + DateTime, + Effect, + Layer, + Option, + Ref, + Schema, + SchemaIssue, + SchemaTransformation, +} from "effect"; +import { db } from "rivetkit/db"; + +// --- Counter --- + +export class CounterOverflowError extends Schema.TaggedErrorClass()( + "CounterOverflowError", + { + limit: Schema.Number, + message: Schema.String, + }, +) {} + +export class Flags extends Context.Service()("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 + * 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, +}); + +// 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 +// `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 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 PersistTagsAndSleep = Action.make("PersistTagsAndSleep", { + payload: { tags: TagsCsv }, + 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, + }), +}); + +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 SleepDuringAction = Action.make("SleepDuringAction", { + success: Schema.String, +}); + +const EncodedTransformedState = Schema.Struct({ + when: Schema.String, + instant: 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 TransformedStateSchema = Schema.Struct({ + when: Schema.Date, + instant: Schema.DateTimeUtc, + url: Schema.URL, + id: Schema.BigInt, + bytes: Schema.Uint8Array, + tags: TagsCsv, + history: Schema.Array( + Schema.Struct({ + at: Schema.Date, + payload: Schema.Uint8Array, + }), + ), +}); + +export const GetRawWakeState = Action.make("GetRawWakeState", { + success: EncodedTransformedState, +}); + +export const GetDecodedState = Action.make("GetDecodedState", { + success: TransformedStateSchema, +}); + +export const SetTransformedStateAndSleep = Action.make( + "SetTransformedStateAndSleep", + { + payload: TransformedStateSchema, + }, +); + +export const SetRawWakeStateAndSleep = Action.make("SetRawWakeStateAndSleep", { + payload: EncodedTransformedState, +}); + +export const TransformedStateActor = Actor.make("TransformedStateActor", { + actions: [ + GetRawWakeState, + GetDecodedState, + SetTransformedStateAndSleep, + SetRawWakeStateAndSleep, + ], +}); + +export const TransformedStateActorLive = TransformedStateActor.toLayer( + ({ rawRivetkitContext, state }) => + Effect.gen(function* () { + const sleep = yield* Actor.Sleep; + const rawWakeState = rawRivetkitContext.state; + + return TransformedStateActor.of({ + GetRawWakeState: () => + Effect.succeed( + rawWakeState as unknown as typeof EncodedTransformedState.Type, + ), + GetDecodedState: () => State.get(state).pipe(Effect.orDie), + SetTransformedStateAndSleep: ({ payload }) => + State.set(state, payload).pipe( + Effect.andThen(sleep), + Effect.orDie, + ), + SetRawWakeStateAndSleep: ({ payload }) => + Effect.tryPromise(async () => { + rawRivetkitContext.state = + payload as unknown as typeof rawRivetkitContext.state; + await rawRivetkitContext.saveState({ + immediate: true, + }); + rawRivetkitContext.sleep(); + }).pipe(Effect.orDie), + }); + }), + { + state: { + 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]), + tags: ["initial"], + history: [], + }), + }, + }, +); + +export const Counter = Actor.make("Counter", { + actions: [ + Increment, + GetCount, + Crash, + EchoDate, + Tags, + Greet, + WakeGreeting, + Compute, + Scale, + PersistAndSleep, + PersistDateAndSleep, + PersistTagsAndSleep, + PersistScaledAndSleep, + GetPersistedState, + LogEvent, + ListEvents, + CountEvents, + SleepDuringAction, + ], +}); + +export const CounterLive = Counter.toLayer( + ({ rawRivetkitContext, state }) => + Effect.gen(function* () { + 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 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 + // 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, + }), + ).pipe(Effect.orDie); + yield* sleep; + return count; + }), + PersistDateAndSleep: ({ payload }) => + Effect.gen(function* () { + const { when } = yield* State.updateAndGet( + state, + (s) => ({ + ...s, + when: payload.when, + }), + ).pipe(Effect.orDie); + yield* sleep; + return when; + }), + PersistTagsAndSleep: ({ payload }) => + Effect.gen(function* () { + const { tags } = yield* State.updateAndGet( + state, + (s) => ({ + ...s, + tags: payload.tags, + }), + ).pipe(Effect.orDie); + yield* sleep; + return tags; + }), + PersistScaledAndSleep: ({ payload }) => + Effect.gen(function* () { + const { scaled } = yield* State.updateAndGet( + state, + (s) => ({ + ...s, + scaled: payload.amount, + }), + ).pipe(Effect.orDie); + yield* sleep; + return scaled; + }), + 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 + // 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), + 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, + ); + }), + ), + ); + }), + }); + }), + { + 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. + 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 --- + +// 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( + ({ state }) => + Effect.gen(function* () { + 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), + Effect.orDie, + ), + StrictGet: () => State.get(state).pipe(Effect.orDie), + }); + }), + { + 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 --- + +// 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"), +}); + +// --- FailingActor --- + +export const FailingActor = Actor.make("FailingBuild", { + actions: [Ping], +}); + +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 +// `*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] }); + +// --- WakeDecodeFail --- + +export const WakeDecodeFail = Actor.make("WakeDecodeFail", { + actions: [Ping], +}); + +export const WakeDecodeFailLive = WakeDecodeFail.toLayer( + () => + Effect.gen(function* () { + return WakeDecodeFail.of({ + Ping: () => Effect.succeed("never reached"), + }); + }), + { + 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 --- + +export const BuildOutcome = Action.make("BuildOutcome", { + success: Schema.Literals(["wrote", "rejected"]), +}); + +export const BuildSetRejected = Actor.make("BuildSetRejected", { + actions: [BuildOutcome], +}); + +export const BuildSetRejectedLive = BuildSetRejected.toLayer( + ({ state }) => + Effect.gen(function* () { + const wrote = yield* State.set(state, -1).pipe( + Effect.match({ + onFailure: () => false, + onSuccess: () => true, + }), + ); + return BuildSetRejected.of({ + BuildOutcome: () => + Effect.succeed(wrote ? "wrote" : "rejected"), + }); + }), + { + 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, + }, + }, +); 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)); + }), + ); + } +} 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/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/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 new file mode 100644 index 0000000000..5c5526d756 --- /dev/null +++ b/rivetkit-typescript/packages/effect/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "types": [], + "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", + "namespaceImportPackages": [ + "effect", + "@effect/*", + "@rivetkit/effect" + ] + } + ] + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/rivetkit-typescript/packages/effect/turbo.json b/rivetkit-typescript/packages/effect/turbo.json new file mode 100644 index 0000000000..5e7787cf4e --- /dev/null +++ b/rivetkit-typescript/packages/effect/turbo.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://turbo.build/schema.json", + "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" + ] + }, + "lint:publint": { + "dependsOn": ["build"], + "inputs": [ + "package.json", + "dist/**" + ] + }, + "lint:attw": { + "dependsOn": ["build"], + "inputs": [ + "package.json", + "dist/**" + ] + }, + "test": { + "dependsOn": ["^build"], + "inputs": [ + "package.json", + "src/**", + "test/**", + "tsconfig.json", + "vitest.config.ts", + "../../../tsconfig.base.json", + "../../../vitest.base.ts", + "../rivetkit/tests/shared-engine.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..e9c535bb4b --- /dev/null +++ b/rivetkit-typescript/packages/effect/vitest.config.ts @@ -0,0 +1,35 @@ +// +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"), + // The shared vitest base sets RIVET_LOG_LEVEL=DEBUG, which floods the + // terminal with engine + runtime logs. Keep this suite quiet. + RIVET_LOG_LEVEL: "SILENT", +}; + +export default defineConfig({ + ...defaultConfig, + test: { + ...defaultConfig.test, + env, + // 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 }, + globalSetup: ["./test/global-setup.ts"], + coverage: { + include: ["src/**/*.ts"], + exclude: ["*.test-d.ts"], + }, + }, +}); diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts index 95e1665b74..4a8dd570b2 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts @@ -1637,10 +1637,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 ?? {}), }; } diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/mod.ts b/rivetkit-typescript/packages/rivetkit/src/actor/mod.ts index f85418a905..d373f76e2c 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/mod.ts @@ -52,6 +52,8 @@ export { export { ActorError, RivetError, + type RivetErrorLike, + type RivetErrorOptions, UserError, type UserErrorOptions, } from "./errors"; 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 a7ed307fb1..5d8578980c 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"); } }