diff --git a/packages/core/fixtures/driver-test-suite/action-inputs.ts b/packages/core/fixtures/driver-test-suite/action-inputs.ts new file mode 100644 index 000000000..062b5ee3f --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/action-inputs.ts @@ -0,0 +1,30 @@ +import { actor } from "@rivetkit/core"; + +export interface State { + initialInput?: unknown; + onCreateInput?: unknown; +} + +// Test actor that can capture input during creation +export const inputActor = actor({ + onAuth: () => {}, + createState: (c, input): State => { + return { + initialInput: input, + onCreateInput: undefined, + }; + }, + + onCreate: (c, input) => { + c.state.onCreateInput = input; + }, + + actions: { + getInputs: (c) => { + return { + initialInput: c.state.initialInput, + onCreateInput: c.state.onCreateInput, + }; + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/action-timeout.ts b/packages/core/fixtures/driver-test-suite/action-timeout.ts new file mode 100644 index 000000000..73e7e8180 --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/action-timeout.ts @@ -0,0 +1,68 @@ +import { actor } from "@rivetkit/core"; + +// Short timeout actor +export const shortTimeoutActor = actor({ + onAuth: () => {}, + state: { value: 0 }, + options: { + action: { + timeout: 50, // 50ms timeout + }, + }, + actions: { + quickAction: async (c) => { + return "quick response"; + }, + slowAction: async (c) => { + // This action should timeout + await new Promise((resolve) => setTimeout(resolve, 100)); + return "slow response"; + }, + }, +}); + +// Long timeout actor +export const longTimeoutActor = actor({ + onAuth: () => {}, + state: { value: 0 }, + options: { + action: { + timeout: 200, // 200ms timeout + }, + }, + actions: { + delayedAction: async (c) => { + // This action should complete within timeout + await new Promise((resolve) => setTimeout(resolve, 100)); + return "delayed response"; + }, + }, +}); + +// Default timeout actor +export const defaultTimeoutActor = actor({ + onAuth: () => {}, + state: { value: 0 }, + actions: { + normalAction: async (c) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + return "normal response"; + }, + }, +}); + +// Sync actor (timeout shouldn't apply) +export const syncTimeoutActor = actor({ + onAuth: () => {}, + state: { value: 0 }, + options: { + action: { + timeout: 50, // 50ms timeout + }, + }, + actions: { + syncAction: (c) => { + return "sync response"; + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/action-types.ts b/packages/core/fixtures/driver-test-suite/action-types.ts new file mode 100644 index 000000000..e8a4f825a --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/action-types.ts @@ -0,0 +1,85 @@ +import { actor, UserError } from "@rivetkit/core"; + +// Actor with synchronous actions +export const syncActionActor = actor({ + onAuth: () => {}, + state: { value: 0 }, + actions: { + // Simple synchronous action that returns a value directly + increment: (c, amount = 1) => { + c.state.value += amount; + return c.state.value; + }, + // Synchronous action that returns an object + getInfo: (c) => { + return { + currentValue: c.state.value, + timestamp: Date.now(), + }; + }, + // Synchronous action with no return value (void) + reset: (c) => { + c.state.value = 0; + }, + }, +}); + +// Actor with asynchronous actions +export const asyncActionActor = actor({ + onAuth: () => {}, + state: { value: 0, data: null as any }, + actions: { + // Async action with a delay + delayedIncrement: async (c, amount = 1) => { + await Promise.resolve(); + c.state.value += amount; + return c.state.value; + }, + // Async action that simulates an API call + fetchData: async (c, id: string) => { + await Promise.resolve(); + + // Simulate response data + const data = { id, timestamp: Date.now() }; + c.state.data = data; + return data; + }, + // Async action with error handling + asyncWithError: async (c, shouldError: boolean) => { + await Promise.resolve(); + + if (shouldError) { + throw new UserError("Intentional error"); + } + + return "Success"; + }, + }, +}); + +// Actor with promise actions +export const promiseActor = actor({ + onAuth: () => {}, + state: { results: [] as string[] }, + actions: { + // Action that returns a resolved promise + resolvedPromise: (c) => { + return Promise.resolve("resolved value"); + }, + // Action that returns a promise that resolves after a delay + delayedPromise: (c): Promise => { + return new Promise((resolve) => { + c.state.results.push("delayed"); + resolve("delayed value"); + }); + }, + // Action that returns a rejected promise + rejectedPromise: (c) => { + return Promise.reject(new UserError("promised rejection")); + }, + // Action to check the collected results + getResults: (c) => { + return c.state.results; + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/auth.ts b/packages/core/fixtures/driver-test-suite/auth.ts new file mode 100644 index 000000000..d7c0910e5 --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/auth.ts @@ -0,0 +1,103 @@ +import { actor, UserError } from "@rivetkit/core"; + +// Basic auth actor - requires API key +export const authActor = actor({ + state: { requests: 0 }, + onAuth: (opts, params: { apiKey?: string } | undefined) => { + const apiKey = params?.apiKey; + if (!apiKey) { + throw new UserError("API key required", { code: "missing_auth" }); + } + + if (apiKey !== "valid-api-key") { + throw new UserError("Invalid API key", { code: "invalid_auth" }); + } + + return { userId: "user123", token: apiKey }; + }, + actions: { + getRequests: (c) => { + c.state.requests++; + return c.state.requests; + }, + getUserAuth: (c) => c.conn.auth, + }, +}); + +// Intent-specific auth actor - checks different permissions for different intents +export const intentAuthActor = actor({ + state: { value: 0 }, + onAuth: ({ request, intents }, params: { role: string }) => { + console.log("intents", intents, params); + const role = params.role; + + if (intents.has("create") && role !== "admin") { + throw new UserError("Admin role required for create operations", { + code: "insufficient_permissions", + }); + } + + if (intents.has("action") && !["admin", "user"].includes(role || "")) { + throw new UserError("User or admin role required for actions", { + code: "insufficient_permissions", + }); + } + + return { role, timestamp: Date.now() }; + }, + actions: { + getValue: (c) => c.state.value, + setValue: (c, value: number) => { + c.state.value = value; + return value; + }, + getAuth: (c) => c.conn.auth, + }, +}); + +// Public actor - empty onAuth to allow public access +export const publicActor = actor({ + state: { visitors: 0 }, + onAuth: () => { + return null; // Allow public access + }, + actions: { + visit: (c) => { + c.state.visitors++; + return c.state.visitors; + }, + }, +}); + +// No auth actor - should fail when accessed publicly (no onAuth defined) +export const noAuthActor = actor({ + state: { value: 42 }, + actions: { + getValue: (c) => c.state.value, + }, +}); + +// Async auth actor - tests promise-based authentication +export const asyncAuthActor = actor({ + state: { count: 0 }, + onAuth: async (opts, params: { token?: string } | undefined) => { + const token = params?.token; + if (!token) { + throw new UserError("Token required", { code: "missing_token" }); + } + + // Simulate token validation + if (token === "invalid") { + throw new UserError("Token is invalid", { code: "invalid_token" }); + } + + return { userId: `user-${token}`, validated: true }; + }, + actions: { + increment: (c) => { + c.state.count++; + return c.state.count; + }, + getAuthData: (c) => c.conn.auth, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/conn-liveness.ts b/packages/core/fixtures/driver-test-suite/conn-liveness.ts new file mode 100644 index 000000000..fe77595cc --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/conn-liveness.ts @@ -0,0 +1,52 @@ +import { actor, CONNECTION_DRIVER_WEBSOCKET } from "@rivetkit/core"; + +export const connLivenessActor = actor({ + onAuth: () => {}, + state: { + counter: 0, + acceptingConnections: true, + }, + options: { + lifecycle: { + connectionLivenessInterval: 5_000, + connectionLivenessTimeout: 2_500, + }, + }, + onConnect: (c, conn) => { + if (!c.state.acceptingConnections) { + conn.disconnect(); + throw new Error("Actor is not accepting connections"); + } + }, + actions: { + getWsConnectionsLiveness: (c) => { + return Array.from(c.conns.values()) + .filter((conn) => conn.driver === CONNECTION_DRIVER_WEBSOCKET) + .map((conn) => ({ + id: conn.id, + status: conn.status, + lastSeen: conn.lastSeen, + })); + }, + getConnectionId: (c) => { + return c.conn.id; + }, + kill: (c, connId: string) => { + c.state.acceptingConnections = false; + // Disconnect the connection with the given ID + // This simulates a network failure or a manual disconnection + // The connection will be cleaned up by the actor manager after the timeout + const conn = c.conns.get(connId); + if (conn) { + conn.disconnect(); + } + }, + getCounter: (c) => { + return c.state.counter; + }, + increment: (c, amount: number) => { + c.state.counter += amount; + return c.state.counter; + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/conn-params.ts b/packages/core/fixtures/driver-test-suite/conn-params.ts new file mode 100644 index 000000000..49200db25 --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/conn-params.ts @@ -0,0 +1,28 @@ +import { actor } from "@rivetkit/core"; + +export const counterWithParams = actor({ + onAuth: () => {}, + state: { count: 0, initializers: [] as string[] }, + createConnState: (c, opts, params: { name?: string }) => { + return { + name: params.name || "anonymous", + }; + }, + onConnect: (c, conn) => { + // Record connection name + c.state.initializers.push(conn.state.name); + }, + actions: { + increment: (c, x: number) => { + c.state.count += x; + c.broadcast("newCount", { + count: c.state.count, + by: c.conn.state.name, + }); + return c.state.count; + }, + getInitializers: (c) => { + return c.state.initializers; + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/conn-state.ts b/packages/core/fixtures/driver-test-suite/conn-state.ts new file mode 100644 index 000000000..22edc2192 --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/conn-state.ts @@ -0,0 +1,102 @@ +import { actor } from "@rivetkit/core"; + +export type ConnState = { + username: string; + role: string; + counter: number; + createdAt: number; +}; + +export const connStateActor = actor({ + onAuth: () => {}, + state: { + sharedCounter: 0, + disconnectionCount: 0, + }, + // Define connection state + createConnState: ( + c, + opts, + params: { username?: string; role?: string }, + ): ConnState => { + return { + username: params?.username || "anonymous", + role: params?.role || "user", + counter: 0, + createdAt: Date.now(), + }; + }, + // Lifecycle hook when a connection is established + onConnect: (c, conn) => { + // Broadcast event about the new connection + c.broadcast("userConnected", { + id: conn.id, + username: "anonymous", + role: "user", + }); + }, + // Lifecycle hook when a connection is closed + onDisconnect: (c, conn) => { + c.state.disconnectionCount += 1; + c.broadcast("userDisconnected", { + id: conn.id, + }); + }, + actions: { + // Action to increment the connection's counter + incrementConnCounter: (c, amount = 1) => { + c.conn.state.counter += amount; + }, + + // Action to increment the shared counter + incrementSharedCounter: (c, amount = 1) => { + c.state.sharedCounter += amount; + return c.state.sharedCounter; + }, + + // Get the connection state + getConnectionState: (c) => { + return { id: c.conn.id, ...c.conn.state }; + }, + + // Check all active connections + getConnectionIds: (c) => { + return c.conns.keys().toArray(); + }, + + // Get disconnection count + getDisconnectionCount: (c) => { + return c.state.disconnectionCount; + }, + + // Get all active connection states + getAllConnectionStates: (c) => { + return c.conns + .entries() + .map(([id, conn]) => ({ id, ...conn.state })) + .toArray(); + }, + + // Send message to a specific connection with matching ID + sendToConnection: (c, targetId: string, message: string) => { + if (c.conns.has(targetId)) { + c.conns + .get(targetId)! + .send("directMessage", { from: c.conn.id, message }); + return true; + } else { + return false; + } + }, + + // Update connection state (simulated for tests) + updateConnection: ( + c, + updates: Partial<{ username: string; role: string }>, + ) => { + if (updates.username) c.conn.state.username = updates.username; + if (updates.role) c.conn.state.role = updates.role; + return c.conn.state; + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/counter.ts b/packages/core/fixtures/driver-test-suite/counter.ts new file mode 100644 index 000000000..752aec3e4 --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/counter.ts @@ -0,0 +1,16 @@ +import { actor } from "@rivetkit/core"; + +export const counter = actor({ + onAuth: () => {}, + state: { count: 0 }, + actions: { + increment: (c, x: number) => { + c.state.count += x; + c.broadcast("newCount", c.state.count); + return c.state.count; + }, + getCount: (c) => { + return c.state.count; + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/error-handling.ts b/packages/core/fixtures/driver-test-suite/error-handling.ts new file mode 100644 index 000000000..d816f8517 --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/error-handling.ts @@ -0,0 +1,97 @@ +import { actor, UserError } from "@rivetkit/core"; + +export const errorHandlingActor = actor({ + onAuth: () => {}, + state: { + errorLog: [] as string[], + }, + actions: { + // Action that throws a UserError with just a message + throwSimpleError: () => { + throw new UserError("Simple error message"); + }, + + // Action that throws a UserError with code and metadata + throwDetailedError: () => { + throw new UserError("Detailed error message", { + code: "detailed_error", + metadata: { + reason: "test", + timestamp: Date.now(), + }, + }); + }, + + // Action that throws an internal error + throwInternalError: () => { + throw new Error("This is an internal error"); + }, + + // Action that returns successfully + successfulAction: () => { + return "success"; + }, + + // Action that times out (simulated with a long delay) + timeoutAction: async (c) => { + // This action should time out if the timeout is configured + return new Promise((resolve) => { + setTimeout(() => { + resolve("This should not be reached if timeout works"); + }, 10000); // 10 seconds + }); + }, + + // Action with configurable delay to test timeout edge cases + delayedAction: async (c, delayMs: number) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(`Completed after ${delayMs}ms`); + }, delayMs); + }); + }, + + // Log an error for inspection + logError: (c, error: string) => { + c.state.errorLog.push(error); + return c.state.errorLog; + }, + + // Get the error log + getErrorLog: (c) => { + return c.state.errorLog; + }, + + // Clear the error log + clearErrorLog: (c) => { + c.state.errorLog = []; + return true; + }, + }, + options: { + // Set a short timeout for this actor's actions + action: { + timeout: 500, // 500ms timeout for actions + }, + }, +}); + +// Actor with custom timeout +export const customTimeoutActor = actor({ + state: {}, + actions: { + quickAction: async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + return "Quick action completed"; + }, + slowAction: async () => { + await new Promise((resolve) => setTimeout(resolve, 300)); + return "Slow action completed"; + }, + }, + options: { + action: { + timeout: 200, // 200ms timeout + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/inline-client.ts b/packages/core/fixtures/driver-test-suite/inline-client.ts new file mode 100644 index 000000000..0e1f0e19b --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/inline-client.ts @@ -0,0 +1,65 @@ +import { actor } from "@rivetkit/core"; +import type { registry } from "./registry"; + +export const inlineClientActor = actor({ + onAuth: () => {}, + state: { messages: [] as string[] }, + actions: { + // Action that uses client to call another actor (stateless) + callCounterIncrement: async (c, amount: number) => { + const client = c.client(); + const result = await client.counter + .getOrCreate(["inline-test"]) + .increment(amount); + c.state.messages.push( + `Called counter.increment(${amount}), result: ${result}`, + ); + return result; + }, + + // Action that uses client to get counter state (stateless) + getCounterState: async (c) => { + const client = c.client(); + const count = await client.counter + .getOrCreate(["inline-test"]) + .getCount(); + c.state.messages.push(`Got counter state: ${count}`); + return count; + }, + + // Action that uses client with .connect() for stateful communication + connectToCounterAndIncrement: async (c, amount: number) => { + const client = c.client(); + const handle = client.counter.getOrCreate(["inline-test-stateful"]); + const connection = handle.connect(); + + // Set up event listener + const events: number[] = []; + connection.on("newCount", (count: number) => { + events.push(count); + }); + + // Perform increments + const result1 = await connection.increment(amount); + const result2 = await connection.increment(amount * 2); + + await connection.dispose(); + + c.state.messages.push( + `Connected to counter, incremented by ${amount} and ${amount * 2}, results: ${result1}, ${result2}, events: ${JSON.stringify(events)}`, + ); + + return { result1, result2, events }; + }, + + // Get all messages from this actor's state + getMessages: (c) => { + return c.state.messages; + }, + + // Clear messages + clearMessages: (c) => { + c.state.messages = []; + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/lifecycle.ts b/packages/core/fixtures/driver-test-suite/lifecycle.ts new file mode 100644 index 000000000..2b6ee5b65 --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/lifecycle.ts @@ -0,0 +1,35 @@ +import { actor } from "@rivetkit/core"; + +type ConnParams = { trackLifecycle?: boolean } | undefined; + +export const counterWithLifecycle = actor({ + onAuth: () => {}, + state: { + count: 0, + events: [] as string[], + }, + createConnState: (c, opts, params: ConnParams) => ({ + joinTime: Date.now(), + }), + onStart: (c) => { + c.state.events.push("onStart"); + }, + onBeforeConnect: (c, opts, params: ConnParams) => { + if (params?.trackLifecycle) c.state.events.push("onBeforeConnect"); + }, + onConnect: (c, conn) => { + if (conn.params?.trackLifecycle) c.state.events.push("onConnect"); + }, + onDisconnect: (c, conn) => { + if (conn.params?.trackLifecycle) c.state.events.push("onDisconnect"); + }, + actions: { + getEvents: (c) => { + return c.state.events; + }, + increment: (c, x: number) => { + c.state.count += x; + return c.state.count; + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/metadata.ts b/packages/core/fixtures/driver-test-suite/metadata.ts new file mode 100644 index 000000000..ff5878203 --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/metadata.ts @@ -0,0 +1,76 @@ +import { actor } from "@rivetkit/core"; + +// Note: For testing only - metadata API will need to be mocked +// in tests since this is implementation-specific +export const metadataActor = actor({ + onAuth: () => {}, + state: { + lastMetadata: null as any, + actorName: "", + // Store tags and region in state for testing since they may not be + // available in the context in all environments + storedTags: {} as Record, + storedRegion: null as string | null, + }, + onStart: (c) => { + // Store the actor name during initialization + c.state.actorName = c.name; + }, + actions: { + // Set up test tags - this will be called by tests to simulate tags + setupTestTags: (c, tags: Record) => { + c.state.storedTags = tags; + return tags; + }, + + // Set up test region - this will be called by tests to simulate region + setupTestRegion: (c, region: string) => { + c.state.storedRegion = region; + return region; + }, + + // Get all available metadata + getMetadata: (c) => { + // Create metadata object from stored values + const metadata = { + name: c.name, + tags: c.state.storedTags, + region: c.state.storedRegion, + }; + + // Store for later inspection + c.state.lastMetadata = metadata; + return metadata; + }, + + // Get the actor name + getActorName: (c) => { + return c.name; + }, + + // Get a specific tag by key + getTag: (c, key: string) => { + return c.state.storedTags[key] || null; + }, + + // Get all tags + getTags: (c) => { + return c.state.storedTags; + }, + + // Get the region + getRegion: (c) => { + return c.state.storedRegion; + }, + + // Get the stored actor name (from onStart) + getStoredActorName: (c) => { + return c.state.actorName; + }, + + // Get last retrieved metadata + getLastMetadata: (c) => { + return c.state.lastMetadata; + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/raw-http-auth.ts b/packages/core/fixtures/driver-test-suite/raw-http-auth.ts new file mode 100644 index 000000000..1b8017742 --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/raw-http-auth.ts @@ -0,0 +1,173 @@ +import { type ActorContext, actor, UserError } from "@rivetkit/core"; + +// Raw HTTP actor with authentication - requires API key +export const rawHttpAuthActor = actor({ + state: { + requestCount: 0, + }, + onAuth: (opts, params: { apiKey?: string }) => { + const apiKey = params.apiKey; + if (!apiKey) { + throw new UserError("API key required", { code: "missing_auth" }); + } + + if (apiKey !== "valid-api-key") { + throw new UserError("Invalid API key", { code: "invalid_auth" }); + } + + return { userId: "user123", token: apiKey }; + }, + onFetch( + ctx: ActorContext, + request: Request, + ) { + const url = new URL(request.url); + ctx.state.requestCount++; + + // Auth info endpoint - onAuth was already called + if (url.pathname === "/api/auth-info") { + return new Response( + JSON.stringify({ + message: "Authenticated request", + requestCount: ctx.state.requestCount, + }), + { + headers: { "Content-Type": "application/json" }, + }, + ); + } + + if (url.pathname === "/api/protected") { + return new Response( + JSON.stringify({ + message: "This is protected content", + }), + { + headers: { "Content-Type": "application/json" }, + }, + ); + } + + return new Response("Not Found", { status: 404 }); + }, + actions: { + getRequestCount(ctx: any) { + return ctx.state.requestCount; + }, + getAuthFromConnections(ctx: any) { + // Get auth data from first connection if available + const firstConn = ctx.conns.values().next().value; + return firstConn?.auth; + }, + }, +}); + +// Raw HTTP actor without onAuth - should deny access +export const rawHttpNoAuthActor = actor({ + state: { + value: 42, + }, + onFetch( + ctx: ActorContext, + request: Request, + ) { + return new Response( + JSON.stringify({ + value: ctx.state.value, + }), + { + headers: { "Content-Type": "application/json" }, + }, + ); + }, + actions: { + getValue(ctx: any) { + return ctx.state.value; + }, + }, +}); + +// Raw HTTP actor with public access (empty onAuth) +export const rawHttpPublicActor = actor({ + state: { + visitors: 0, + }, + onAuth: () => { + return null; // Allow public access + }, + onFetch( + ctx: ActorContext, + request: Request, + ) { + ctx.state.visitors++; + return new Response( + JSON.stringify({ + message: "Welcome visitor!", + count: ctx.state.visitors, + }), + { + headers: { "Content-Type": "application/json" }, + }, + ); + }, + actions: { + getVisitorCount(ctx: any) { + return ctx.state.visitors; + }, + }, +}); + +// Raw HTTP actor with custom auth in onFetch (no onAuth) +export const rawHttpCustomAuthActor = actor({ + state: { + authorized: 0, + unauthorized: 0, + }, + onAuth: () => { + // Allow all connections - auth will be handled in onFetch + return {}; + }, + onFetch( + ctx: ActorContext, + request: Request, + ) { + // Custom auth check in onFetch + const authHeader = request.headers.get("Authorization"); + + if (!authHeader || !authHeader.startsWith("Bearer ")) { + ctx.state.unauthorized++; + return new Response(JSON.stringify({ error: "Unauthorized" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + + const token = authHeader.substring(7); + if (token !== "custom-token") { + ctx.state.unauthorized++; + return new Response(JSON.stringify({ error: "Invalid token" }), { + status: 403, + headers: { "Content-Type": "application/json" }, + }); + } + + ctx.state.authorized++; + return new Response( + JSON.stringify({ + message: "Authorized!", + authorized: ctx.state.authorized, + }), + { + headers: { "Content-Type": "application/json" }, + }, + ); + }, + actions: { + getStats(ctx: any) { + return { + authorized: ctx.state.authorized, + unauthorized: ctx.state.unauthorized, + }; + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/raw-http-request-properties.ts b/packages/core/fixtures/driver-test-suite/raw-http-request-properties.ts new file mode 100644 index 000000000..f19297465 --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/raw-http-request-properties.ts @@ -0,0 +1,91 @@ +import { type ActorContext, actor } from "@rivetkit/core"; + +export const rawHttpRequestPropertiesActor = actor({ + onAuth() { + // Allow public access - empty onAuth + return {}; + }, + actions: {}, + onFetch( + ctx: ActorContext, + request: Request, + ) { + // Extract all relevant Request properties + const url = new URL(request.url); + const method = request.method; + + // Get all headers + const headers = Object.fromEntries(request.headers.entries()); + + // Handle body based on content type + const handleBody = async () => { + if (!request.body) { + return null; + } + + const contentType = request.headers.get("content-type") || ""; + + try { + if (contentType.includes("application/json")) { + const text = await request.text(); + return text ? JSON.parse(text) : null; + } else { + // For non-JSON, return as text + const text = await request.text(); + return text || null; // Return null for empty bodies + } + } catch (error) { + // If body parsing fails, return null + return null; + } + }; + + // Special handling for HEAD requests - return empty body + if (method === "HEAD") { + return new Response(null, { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + // Return all request properties as JSON + return handleBody().then((body) => { + const responseData = { + // URL properties + url: request.url, + pathname: url.pathname, + search: url.search, + searchParams: Object.fromEntries(url.searchParams.entries()), + hash: url.hash, + + // Method + method: request.method, + + // Headers + headers: headers, + + // Body + body, + bodyText: + typeof body === "string" + ? body + : body === null && request.body !== null + ? "" + : null, + + // Additional properties that might be available + // Note: Some properties like cache, credentials, mode, etc. + // might not be available in all environments + cache: request.cache || null, + credentials: request.credentials || null, + mode: request.mode || null, + redirect: request.redirect || null, + referrer: request.referrer || null, + }; + + return new Response(JSON.stringify(responseData), { + headers: { "Content-Type": "application/json" }, + }); + }); + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/raw-http.ts b/packages/core/fixtures/driver-test-suite/raw-http.ts new file mode 100644 index 000000000..3b08f4bf6 --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/raw-http.ts @@ -0,0 +1,130 @@ +import { type ActorContext, actor } from "@rivetkit/core"; +import { Hono } from "hono"; + +export const rawHttpActor = actor({ + state: { + requestCount: 0, + }, + onAuth() { + // Allow public access - empty onAuth + return {}; + }, + onFetch( + ctx: ActorContext, + request: Request, + ) { + const url = new URL(request.url); + const method = request.method; + + // Track request count + ctx.state.requestCount++; + + // Handle different endpoints + if (url.pathname === "/api/hello") { + return new Response(JSON.stringify({ message: "Hello from actor!" }), { + headers: { "Content-Type": "application/json" }, + }); + } + + if (url.pathname === "/api/echo" && method === "POST") { + return new Response(request.body, { + headers: request.headers, + }); + } + + if (url.pathname === "/api/state") { + return new Response( + JSON.stringify({ + requestCount: ctx.state.requestCount, + }), + { + headers: { "Content-Type": "application/json" }, + }, + ); + } + + if (url.pathname === "/api/headers") { + const headers = Object.fromEntries(request.headers.entries()); + return new Response(JSON.stringify(headers), { + headers: { "Content-Type": "application/json" }, + }); + } + + // Return 404 for unhandled paths + return new Response("Not Found", { status: 404 }); + }, + actions: {}, +}); + +export const rawHttpNoHandlerActor = actor({ + // No onFetch handler - all requests should return 404 + onAuth() { + // Allow public access - empty onAuth + return {}; + }, + actions: {}, +}); + +export const rawHttpVoidReturnActor = actor({ + onAuth() { + // Allow public access - empty onAuth + return {}; + }, + onFetch(ctx, request) { + // Intentionally return void to test error handling + return undefined as any; + }, + actions: {}, +}); + +export const rawHttpHonoActor = actor({ + onAuth() { + // Allow public access + return {}; + }, + createVars() { + const router = new Hono(); + + // Set up routes + router.get("/", (c: any) => c.json({ message: "Welcome to Hono actor!" })); + + router.get("/users", (c: any) => + c.json([ + { id: 1, name: "Alice" }, + { id: 2, name: "Bob" }, + ]), + ); + + router.get("/users/:id", (c: any) => { + const id = c.req.param("id"); + return c.json({ id: parseInt(id), name: id === "1" ? "Alice" : "Bob" }); + }); + + router.post("/users", async (c: any) => { + const body = await c.req.json(); + return c.json({ id: 3, ...body }, 201); + }); + + router.put("/users/:id", async (c: any) => { + const id = c.req.param("id"); + const body = await c.req.json(); + return c.json({ id: parseInt(id), ...body }); + }); + + router.delete("/users/:id", (c: any) => { + const id = c.req.param("id"); + return c.json({ message: `User ${id} deleted` }); + }); + + // Return the router as a var + return { router }; + }, + onFetch( + ctx: ActorContext, + request: Request, + ) { + // Use the Hono router from vars + return ctx.vars.router.fetch(request); + }, + actions: {}, +}); diff --git a/packages/core/fixtures/driver-test-suite/raw-websocket-auth.ts b/packages/core/fixtures/driver-test-suite/raw-websocket-auth.ts new file mode 100644 index 000000000..8bcdf9cd6 --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/raw-websocket-auth.ts @@ -0,0 +1,186 @@ +import { + type ActorContext, + actor, + type UniversalWebSocket, + UserError, +} from "@rivetkit/core"; + +// Raw WebSocket actor with authentication +export const rawWebSocketAuthActor = actor({ + state: { + connectionCount: 0, + messageCount: 0, + }, + onAuth: (opts, params: { apiKey?: string }) => { + const apiKey = params.apiKey; + if (!apiKey) { + throw new UserError("API key required", { code: "missing_auth" }); + } + + if (apiKey !== "valid-api-key") { + throw new UserError("Invalid API key", { code: "invalid_auth" }); + } + + return { userId: "user123", token: apiKey }; + }, + onWebSocket(ctx, websocket) { + ctx.state.connectionCount++; + + // Send welcome message on connect + websocket.send( + JSON.stringify({ + type: "welcome", + message: "Authenticated WebSocket connection", + connectionCount: ctx.state.connectionCount, + }), + ); + + websocket.addEventListener("message", (event: any) => { + ctx.state.messageCount++; + const data = event.data; + + if (typeof data === "string") { + try { + const parsed = JSON.parse(data); + if (parsed.type === "getAuth") { + websocket.send( + JSON.stringify({ + type: "authInfo", + authenticated: true, + }), + ); + } else { + // Echo message back + websocket.send( + JSON.stringify({ + type: "echo", + original: parsed, + }), + ); + } + } catch { + websocket.send(data); + } + } + }); + + websocket.addEventListener("close", () => { + ctx.state.connectionCount--; + }); + }, + actions: { + getStats(ctx: any) { + return { + connectionCount: ctx.state.connectionCount, + messageCount: ctx.state.messageCount, + }; + }, + }, +}); + +// Raw WebSocket actor without onAuth - should deny access +export const rawWebSocketNoAuthActor = actor({ + state: { + connections: 0, + }, + onWebSocket(ctx, websocket) { + ctx.state.connections++; + websocket.send( + JSON.stringify({ + type: "connected", + connections: ctx.state.connections, + }), + ); + }, + actions: { + getConnectionCount(ctx: any) { + return ctx.state.connections; + }, + }, +}); + +// Raw WebSocket actor with public access +export const rawWebSocketPublicActor = actor({ + state: { + visitors: 0, + }, + onAuth: () => { + return null; // Allow public access + }, + onWebSocket(ctx, websocket) { + ctx.state.visitors++; + + websocket.send( + JSON.stringify({ + type: "welcome", + message: "Public WebSocket connection", + visitorNumber: ctx.state.visitors, + }), + ); + + websocket.addEventListener("message", (event: any) => { + // Echo messages + websocket.send(event.data); + }); + }, + actions: { + getVisitorCount(ctx: any) { + return ctx.state.visitors; + }, + }, +}); + +// Raw WebSocket with custom auth in onWebSocket +export const rawWebSocketCustomAuthActor = actor({ + state: { + authorized: 0, + unauthorized: 0, + }, + onAuth: () => { + // Allow all connections - auth will be handled in onWebSocket + return {}; + }, + onWebSocket(ctx, websocket, opts) { + // Check for auth token in URL or headers + const url = new URL(opts.request.url); + const token = url.searchParams.get("token"); + + if (!token || token !== "custom-ws-token") { + ctx.state.unauthorized++; + websocket.send( + JSON.stringify({ + type: "error", + message: "Unauthorized", + }), + ); + websocket.close(1008, "Unauthorized"); + return; + } + + ctx.state.authorized++; + websocket.send( + JSON.stringify({ + type: "authorized", + message: "Welcome authenticated user!", + }), + ); + + websocket.addEventListener("message", (event: any) => { + websocket.send( + JSON.stringify({ + type: "echo", + data: event.data, + authenticated: true, + }), + ); + }); + }, + actions: { + getStats(ctx: any) { + return { + authorized: ctx.state.authorized, + unauthorized: ctx.state.unauthorized, + }; + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/raw-websocket.ts b/packages/core/fixtures/driver-test-suite/raw-websocket.ts new file mode 100644 index 000000000..48124b3df --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/raw-websocket.ts @@ -0,0 +1,132 @@ +import { + type ActorContext, + actor, + type UniversalWebSocket, +} from "@rivetkit/core"; + +export const rawWebSocketActor = actor({ + state: { + connectionCount: 0, + messageCount: 0, + }, + onAuth(params) { + // Allow all connections and pass through connection params + return { connParams: params }; + }, + onWebSocket(ctx, websocket, opts) { + ctx.state.connectionCount = ctx.state.connectionCount + 1; + console.log(`[ACTOR] New connection, count: ${ctx.state.connectionCount}`); + + // Send welcome message + websocket.send( + JSON.stringify({ + type: "welcome", + connectionCount: ctx.state.connectionCount, + }), + ); + console.log("[ACTOR] Sent welcome message"); + + // Echo messages back + websocket.addEventListener("message", (event: any) => { + ctx.state.messageCount = ctx.state.messageCount + 1; + console.log( + `[ACTOR] Message received, total count: ${ctx.state.messageCount}, data:`, + event.data, + ); + + const data = event.data; + if (typeof data === "string") { + try { + const parsed = JSON.parse(data); + if (parsed.type === "ping") { + websocket.send( + JSON.stringify({ + type: "pong", + timestamp: Date.now(), + }), + ); + } else if (parsed.type === "getStats") { + console.log( + `[ACTOR] Sending stats - connections: ${ctx.state.connectionCount}, messages: ${ctx.state.messageCount}`, + ); + websocket.send( + JSON.stringify({ + type: "stats", + connectionCount: ctx.state.connectionCount, + messageCount: ctx.state.messageCount, + }), + ); + } else if (parsed.type === "getAuthData") { + // Auth data is not directly available in raw WebSocket handler + // Send a message indicating this limitation + websocket.send( + JSON.stringify({ + type: "authData", + authData: null, + message: "Auth data not available in raw WebSocket handler", + }), + ); + } else if (parsed.type === "getRequestInfo") { + // Send back the request URL info + websocket.send( + JSON.stringify({ + type: "requestInfo", + url: opts.request.url, + pathname: new URL(opts.request.url).pathname, + search: new URL(opts.request.url).search, + }), + ); + } else { + // Echo back + websocket.send(data); + } + } catch { + // If not JSON, just echo it back + websocket.send(data); + } + } else { + // Echo binary data + websocket.send(data); + } + }); + + // Handle close + websocket.addEventListener("close", () => { + ctx.state.connectionCount = ctx.state.connectionCount - 1; + console.log( + `[ACTOR] Connection closed, count: ${ctx.state.connectionCount}`, + ); + }); + }, + actions: { + getStats(ctx: any) { + return { + connectionCount: ctx.state.connectionCount, + messageCount: ctx.state.messageCount, + }; + }, + }, +}); + +export const rawWebSocketBinaryActor = actor({ + onAuth() { + // Allow all connections + return {}; + }, + onWebSocket(ctx, websocket, opts) { + // Handle binary data + websocket.addEventListener("message", (event: any) => { + const data = event.data; + if (data instanceof ArrayBuffer || data instanceof Uint8Array) { + // Reverse the bytes and send back + const bytes = new Uint8Array(data); + const reversed = new Uint8Array(bytes.length); + for (let i = 0; i < bytes.length; i++) { + reversed[i] = bytes[bytes.length - 1 - i]; + } + websocket.send(reversed); + } + }); + }, + actions: {}, +}); diff --git a/packages/core/fixtures/driver-test-suite/registry.ts b/packages/core/fixtures/driver-test-suite/registry.ts new file mode 100644 index 000000000..d58b2384a --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/registry.ts @@ -0,0 +1,131 @@ +import { setup } from "@rivetkit/core"; + +import { inputActor } from "./action-inputs"; +import { + defaultTimeoutActor, + longTimeoutActor, + shortTimeoutActor, + syncTimeoutActor, +} from "./action-timeout"; +import { + asyncActionActor, + promiseActor, + syncActionActor, +} from "./action-types"; +import { + asyncAuthActor, + authActor, + intentAuthActor, + noAuthActor, + publicActor, +} from "./auth"; +import { connLivenessActor } from "./conn-liveness"; +import { counterWithParams } from "./conn-params"; +import { connStateActor } from "./conn-state"; +// Import actors from individual files +import { counter } from "./counter"; +import { customTimeoutActor, errorHandlingActor } from "./error-handling"; +import { inlineClientActor } from "./inline-client"; +import { counterWithLifecycle } from "./lifecycle"; +import { metadataActor } from "./metadata"; +import { + rawHttpActor, + rawHttpHonoActor, + rawHttpNoHandlerActor, + rawHttpVoidReturnActor, +} from "./raw-http"; +import { + rawHttpAuthActor, + rawHttpCustomAuthActor, + rawHttpNoAuthActor, + rawHttpPublicActor, +} from "./raw-http-auth"; +import { rawHttpRequestPropertiesActor } from "./raw-http-request-properties"; +import { rawWebSocketActor, rawWebSocketBinaryActor } from "./raw-websocket"; +import { + rawWebSocketAuthActor, + rawWebSocketCustomAuthActor, + rawWebSocketNoAuthActor, + rawWebSocketPublicActor, +} from "./raw-websocket-auth"; +import { requestAccessActor } from "./request-access"; +import { requestAccessAuthActor } from "./request-access-auth"; +import { scheduled } from "./scheduled"; +import { + driverCtxActor, + dynamicVarActor, + nestedVarActor, + staticVarActor, + uniqueVarActor, +} from "./vars"; + +// Consolidated setup with all actors +export const registry = setup({ + use: { + // From counter.ts + counter, + // From lifecycle.ts + counterWithLifecycle, + // From scheduled.ts + scheduled, + // From error-handling.ts + errorHandlingActor, + customTimeoutActor, + // From inline-client.ts + inlineClientActor, + // From action-inputs.ts + inputActor, + // From action-timeout.ts + shortTimeoutActor, + longTimeoutActor, + defaultTimeoutActor, + syncTimeoutActor, + // From action-types.ts + syncActionActor, + asyncActionActor, + promiseActor, + // From conn-params.ts + counterWithParams, + // From conn-state.ts + connStateActor, + // From actor-conn.ts + connLivenessActor, + // From metadata.ts + metadataActor, + // From vars.ts + staticVarActor, + nestedVarActor, + dynamicVarActor, + uniqueVarActor, + driverCtxActor, + // From auth.ts + authActor, + intentAuthActor, + publicActor, + noAuthActor, + asyncAuthActor, + // From raw-http.ts + rawHttpActor, + rawHttpNoHandlerActor, + rawHttpVoidReturnActor, + rawHttpHonoActor, + // From raw-http-auth.ts + rawHttpAuthActor, + rawHttpNoAuthActor, + rawHttpPublicActor, + rawHttpCustomAuthActor, + // From raw-http-request-properties.ts + rawHttpRequestPropertiesActor, + // From raw-websocket.ts + rawWebSocketActor, + rawWebSocketBinaryActor, + // From raw-websocket-auth.ts + rawWebSocketAuthActor, + rawWebSocketNoAuthActor, + rawWebSocketPublicActor, + rawWebSocketCustomAuthActor, + // From request-access.ts + requestAccessActor, + requestAccessAuthActor, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/request-access-auth.ts b/packages/core/fixtures/driver-test-suite/request-access-auth.ts new file mode 100644 index 000000000..fe1322ff2 --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/request-access-auth.ts @@ -0,0 +1,48 @@ +import { actor } from "@rivetkit/core"; + +/** + * Test fixture to verify request object access in onAuth hook + * onAuth runs on the HTTP server, not in the actor, so we test it separately + */ +export const requestAccessAuthActor = actor({ + onAuth: ({ request, intents }, params: { trackRequest?: boolean }) => { + if (params?.trackRequest) { + // Extract request info and return it as auth data + const headers: Record = {}; + request.headers.forEach((value, key) => { + headers[key] = value; + }); + + return { + hasRequest: true, + requestUrl: request.url, + requestMethod: request.method, + requestHeaders: headers, + intents: Array.from(intents), + }; + } + + // Return empty auth data when not tracking + return {}; + }, + state: { + authData: null as any, + }, + onConnect: (c, conn) => { + // Store auth data in state so we can retrieve it + c.state.authData = conn.auth; + }, + actions: { + getAuthRequestInfo: (c) => { + // Return the stored auth data or a default object + const authData = c.state.authData || { + hasRequest: false, + requestUrl: null, + requestMethod: null, + requestHeaders: {}, + intents: [], + }; + return authData; + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/request-access.ts b/packages/core/fixtures/driver-test-suite/request-access.ts new file mode 100644 index 000000000..6a801e45d --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/request-access.ts @@ -0,0 +1,141 @@ +import { actor } from "@rivetkit/core"; + +/** + * Test fixture to verify request object access in all lifecycle hooks + */ +export const requestAccessActor = actor({ + onAuth: () => {}, // Allow unauthenticated connections + state: { + // Track request info from different hooks + onBeforeConnectRequest: { + hasRequest: false, + requestUrl: null as string | null, + requestMethod: null as string | null, + requestHeaders: {} as Record, + }, + createConnStateRequest: { + hasRequest: false, + requestUrl: null as string | null, + requestMethod: null as string | null, + requestHeaders: {} as Record, + }, + onFetchRequest: { + hasRequest: false, + requestUrl: null as string | null, + requestMethod: null as string | null, + requestHeaders: {} as Record, + }, + onWebSocketRequest: { + hasRequest: false, + requestUrl: null as string | null, + requestMethod: null as string | null, + requestHeaders: {} as Record, + }, + }, + createConnState: (c, { request }, params: { trackRequest?: boolean }) => { + // In createConnState, the state isn't available yet. + + return { + trackRequest: params?.trackRequest || false, + requestInfo: + params?.trackRequest && request + ? { + hasRequest: true, + requestUrl: request.url, + requestMethod: request.method, + requestHeaders: Object.fromEntries(request.headers.entries()), + } + : null, + }; + }, + onConnect: (c, conn) => { + // Copy request info from connection state if it was tracked + if (conn.state.requestInfo) { + c.state.createConnStateRequest = conn.state.requestInfo; + } + }, + onBeforeConnect: (c, { request }, params) => { + if (params?.trackRequest) { + if (request) { + c.state.onBeforeConnectRequest.hasRequest = true; + c.state.onBeforeConnectRequest.requestUrl = request.url; + c.state.onBeforeConnectRequest.requestMethod = request.method; + + // Store select headers + const headers: Record = {}; + request.headers.forEach((value, key) => { + headers[key] = value; + }); + c.state.onBeforeConnectRequest.requestHeaders = headers; + } else { + // Track that we tried but request was not available + c.state.onBeforeConnectRequest.hasRequest = false; + } + } + }, + onFetch: (c, request) => { + // Store request info + c.state.onFetchRequest.hasRequest = true; + c.state.onFetchRequest.requestUrl = request.url; + c.state.onFetchRequest.requestMethod = request.method; + + // Store select headers + const headers: Record = {}; + request.headers.forEach((value, key) => { + headers[key] = value; + }); + c.state.onFetchRequest.requestHeaders = headers; + + // Return response with request info + return new Response( + JSON.stringify({ + hasRequest: true, + requestUrl: request.url, + requestMethod: request.method, + requestHeaders: headers, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + }, + onWebSocket: (c, websocket, { request }) => { + // Store request info + c.state.onWebSocketRequest.hasRequest = true; + c.state.onWebSocketRequest.requestUrl = request.url; + c.state.onWebSocketRequest.requestMethod = request.method; + + // Store select headers + const headers: Record = {}; + request.headers.forEach((value, key) => { + headers[key] = value; + }); + c.state.onWebSocketRequest.requestHeaders = headers; + + // Send request info on connection + websocket.send( + JSON.stringify({ + hasRequest: true, + requestUrl: request.url, + requestMethod: request.method, + requestHeaders: headers, + }), + ); + + // Echo messages back + websocket.addEventListener("message", (event) => { + websocket.send(event.data); + }); + }, + actions: { + getRequestInfo: (c) => { + return { + onBeforeConnect: c.state.onBeforeConnectRequest, + createConnState: c.state.createConnStateRequest, + onFetch: c.state.onFetchRequest, + onWebSocket: c.state.onWebSocketRequest, + }; + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/scheduled.ts b/packages/core/fixtures/driver-test-suite/scheduled.ts new file mode 100644 index 000000000..10ad94a75 --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/scheduled.ts @@ -0,0 +1,77 @@ +import { actor } from "@rivetkit/core"; + +export const scheduled = actor({ + onAuth: () => {}, + state: { + lastRun: 0, + scheduledCount: 0, + taskHistory: [] as string[], + }, + actions: { + // Schedule using 'at' with specific timestamp + scheduleTaskAt: (c, timestamp: number) => { + c.schedule.at(timestamp, "onScheduledTask"); + return timestamp; + }, + + // Schedule using 'after' with delay + scheduleTaskAfter: (c, delayMs: number) => { + c.schedule.after(delayMs, "onScheduledTask"); + return Date.now() + delayMs; + }, + + // Schedule with a task ID for ordering tests + scheduleTaskAfterWithId: (c, taskId: string, delayMs: number) => { + c.schedule.after(delayMs, "onScheduledTaskWithId", taskId); + return { taskId, scheduledFor: Date.now() + delayMs }; + }, + + // Original method for backward compatibility + scheduleTask: (c, delayMs: number) => { + const timestamp = Date.now() + delayMs; + c.schedule.at(timestamp, "onScheduledTask"); + return timestamp; + }, + + // Getters for state + getLastRun: (c) => { + return c.state.lastRun; + }, + + getScheduledCount: (c) => { + return c.state.scheduledCount; + }, + + getTaskHistory: (c) => { + return c.state.taskHistory; + }, + + clearHistory: (c) => { + c.state.taskHistory = []; + c.state.scheduledCount = 0; + c.state.lastRun = 0; + return true; + }, + + // Scheduled task handlers + onScheduledTask: (c) => { + c.state.lastRun = Date.now(); + c.state.scheduledCount++; + c.broadcast("scheduled", { + time: c.state.lastRun, + count: c.state.scheduledCount, + }); + }, + + onScheduledTaskWithId: (c, taskId: string) => { + c.state.lastRun = Date.now(); + c.state.scheduledCount++; + c.state.taskHistory.push(taskId); + c.broadcast("scheduledWithId", { + taskId, + time: c.state.lastRun, + count: c.state.scheduledCount, + }); + }, + }, +}); diff --git a/packages/core/fixtures/driver-test-suite/vars.ts b/packages/core/fixtures/driver-test-suite/vars.ts new file mode 100644 index 000000000..4ac2d879e --- /dev/null +++ b/packages/core/fixtures/driver-test-suite/vars.ts @@ -0,0 +1,96 @@ +import { actor } from "@rivetkit/core"; + +// Actor with static vars +export const staticVarActor = actor({ + onAuth: () => {}, + state: { value: 0 }, + connState: { hello: "world" }, + vars: { counter: 42, name: "test-actor" }, + actions: { + getVars: (c) => { + return c.vars; + }, + getName: (c) => { + return c.vars.name; + }, + }, +}); + +// Actor with nested vars +export const nestedVarActor = actor({ + onAuth: () => {}, + state: { value: 0 }, + connState: { hello: "world" }, + vars: { + counter: 42, + nested: { + value: "original", + array: [1, 2, 3], + obj: { key: "value" }, + }, + }, + actions: { + getVars: (c) => { + return c.vars; + }, + modifyNested: (c) => { + // Attempt to modify the nested object + c.vars.nested.value = "modified"; + c.vars.nested.array.push(4); + c.vars.nested.obj.key = "new-value"; + return c.vars; + }, + }, +}); + +// Actor with dynamic vars +export const dynamicVarActor = actor({ + onAuth: () => {}, + state: { value: 0 }, + connState: { hello: "world" }, + createVars: () => { + return { + random: Math.random(), + computed: `Actor-${Math.floor(Math.random() * 1000)}`, + }; + }, + actions: { + getVars: (c) => { + return c.vars; + }, + }, +}); + +// Actor with unique vars per instance +export const uniqueVarActor = actor({ + onAuth: () => {}, + state: { value: 0 }, + connState: { hello: "world" }, + createVars: () => { + return { + id: Math.floor(Math.random() * 1000000), + }; + }, + actions: { + getVars: (c) => { + return c.vars; + }, + }, +}); + +// Actor that uses driver context +export const driverCtxActor = actor({ + onAuth: () => {}, + state: { value: 0 }, + connState: { hello: "world" }, + createVars: (c, driverCtx: any) => { + return { + hasDriverCtx: Boolean(driverCtx?.isTest), + }; + }, + actions: { + getVars: (c) => { + return c.vars; + }, + }, +});