diff --git a/examples/actor-actions/package.json b/examples/actor-actions/package.json index 71c93b8d4e..7367b7914f 100644 --- a/examples/actor-actions/package.json +++ b/examples/actor-actions/package.json @@ -18,8 +18,8 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vitest": "^3.1.1", - "vite-plugin-srvx": "^1.0.0" + "vite-plugin-srvx": "^1.0.2", + "vitest": "^3.1.1" }, "dependencies": { "@hono/node-server": "^1.19.7", diff --git a/examples/ai-agent-vercel/README.md b/examples/ai-agent-vercel/README.md index 20e4b758c0..846de305bf 100644 --- a/examples/ai-agent-vercel/README.md +++ b/examples/ai-agent-vercel/README.md @@ -12,10 +12,11 @@ Example project demonstrating queue-driven Rivet Actor AI agents with streaming ```sh git clone https://github.com/rivet-dev/rivet.git cd rivet/examples/ai-agent -pnpm install -pnpm dev +npm install +npm run dev ``` + ## Features - Actor-per-agent pattern with a coordinating manager Rivet Actor diff --git a/examples/ai-agent/README.md b/examples/ai-agent/README.md index c96b3c9004..da98be1861 100644 --- a/examples/ai-agent/README.md +++ b/examples/ai-agent/README.md @@ -7,10 +7,11 @@ Example project demonstrating queue-driven Rivet Actor AI agents with streaming ```sh git clone https://github.com/rivet-dev/rivet.git cd rivet/examples/ai-agent -pnpm install -pnpm dev +npm install +npm run dev ``` + ## Features - Actor-per-agent pattern with a coordinating manager Rivet Actor diff --git a/examples/ai-agent/package.json b/examples/ai-agent/package.json index ebca9a3b6f..610eeb4c3b 100644 --- a/examples/ai-agent/package.json +++ b/examples/ai-agent/package.json @@ -18,7 +18,7 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vite-plugin-srvx": "^1.0.0", + "vite-plugin-srvx": "^1.0.2", "vitest": "^3.1.1" }, "dependencies": { diff --git a/examples/ai-and-user-generated-actors-freestyle/package.json b/examples/ai-and-user-generated-actors-freestyle/package.json index 1011e46f9f..8f7b7cf9e2 100644 --- a/examples/ai-and-user-generated-actors-freestyle/package.json +++ b/examples/ai-and-user-generated-actors-freestyle/package.json @@ -31,8 +31,8 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vitest": "^2.1.8", - "vite-plugin-srvx": "^1.0.0" + "vite-plugin-srvx": "^1.0.2", + "vitest": "^2.1.8" }, "template": { "technologies": [ diff --git a/examples/chat-room/package.json b/examples/chat-room/package.json index 40cda910d6..50cbf21e7c 100644 --- a/examples/chat-room/package.json +++ b/examples/chat-room/package.json @@ -18,7 +18,7 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vite-plugin-srvx": "^1.0.0", + "vite-plugin-srvx": "^1.0.2", "vitest": "^3.1.1" }, "dependencies": { diff --git a/examples/collaborative-document-vercel/.gitignore b/examples/collaborative-document-vercel/.gitignore new file mode 100644 index 0000000000..e28f3d79db --- /dev/null +++ b/examples/collaborative-document-vercel/.gitignore @@ -0,0 +1,4 @@ +.actorcore +node_modules +dist +.vercel diff --git a/examples/collaborative-document-vercel/README.md b/examples/collaborative-document-vercel/README.md index 205d878a9c..80713c725b 100644 --- a/examples/collaborative-document-vercel/README.md +++ b/examples/collaborative-document-vercel/README.md @@ -12,10 +12,11 @@ A shared text editor that uses Rivet Actors with Yjs for real-time CRDT sync and ```sh git clone https://github.com/rivet-dev/rivet.git cd rivet/examples/collaborative-document -pnpm install -pnpm dev +npm install +npm run dev ``` + ## Features - **Coordinator pattern**: One documentList actor per workspace indexes document actors diff --git a/examples/collaborative-document/README.md b/examples/collaborative-document/README.md index 3f3ffdd5e1..3c44501eee 100644 --- a/examples/collaborative-document/README.md +++ b/examples/collaborative-document/README.md @@ -7,10 +7,11 @@ A shared text editor that uses Rivet Actors with Yjs for real-time CRDT sync and ```sh git clone https://github.com/rivet-dev/rivet.git cd rivet/examples/collaborative-document -pnpm install -pnpm dev +npm install +npm run dev ``` + ## Features - **Coordinator pattern**: One documentList actor per workspace indexes document actors diff --git a/examples/collaborative-document/package.json b/examples/collaborative-document/package.json index 808c106733..13f2fb0ac0 100644 --- a/examples/collaborative-document/package.json +++ b/examples/collaborative-document/package.json @@ -18,7 +18,7 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vite-plugin-srvx": "^1.0.0", + "vite-plugin-srvx": "^1.0.2", "vitest": "^3.1.1" }, "dependencies": { diff --git a/examples/cross-actor-actions/package.json b/examples/cross-actor-actions/package.json index 44e1763984..0bcfa4aafc 100644 --- a/examples/cross-actor-actions/package.json +++ b/examples/cross-actor-actions/package.json @@ -18,8 +18,8 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vitest": "^3.1.1", - "vite-plugin-srvx": "^1.0.0" + "vite-plugin-srvx": "^1.0.2", + "vitest": "^3.1.1" }, "dependencies": { "@hono/node-server": "^1.19.7", diff --git a/examples/cursors-raw-websocket/package.json b/examples/cursors-raw-websocket/package.json index 3f7dd8f908..50e52930d1 100644 --- a/examples/cursors-raw-websocket/package.json +++ b/examples/cursors-raw-websocket/package.json @@ -18,8 +18,8 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vitest": "^3.1.1", - "vite-plugin-srvx": "^1.0.0" + "vite-plugin-srvx": "^1.0.2", + "vitest": "^3.1.1" }, "dependencies": { "@hono/node-server": "^1.19.7", diff --git a/examples/cursors/package.json b/examples/cursors/package.json index bb591dbeb8..3abf542313 100644 --- a/examples/cursors/package.json +++ b/examples/cursors/package.json @@ -18,8 +18,8 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vitest": "^3.1.1", - "vite-plugin-srvx": "^1.0.0" + "vite-plugin-srvx": "^1.0.2", + "vitest": "^3.1.1" }, "dependencies": { "@hono/node-server": "^1.19.7", diff --git a/examples/experimental-durable-streams-ai-agent/package.json b/examples/experimental-durable-streams-ai-agent/package.json index 2804449bde..9330a904fd 100644 --- a/examples/experimental-durable-streams-ai-agent/package.json +++ b/examples/experimental-durable-streams-ai-agent/package.json @@ -18,8 +18,8 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vitest": "^3.1.1", - "vite-plugin-srvx": "^1.0.0" + "vite-plugin-srvx": "^1.0.2", + "vitest": "^3.1.1" }, "dependencies": { "@ai-sdk/anthropic": "^1.0.0", diff --git a/examples/geo-distributed-database-vercel/.gitignore b/examples/geo-distributed-database-vercel/.gitignore new file mode 100644 index 0000000000..e28f3d79db --- /dev/null +++ b/examples/geo-distributed-database-vercel/.gitignore @@ -0,0 +1,4 @@ +.actorcore +node_modules +dist +.vercel diff --git a/examples/geo-distributed-database-vercel/README.md b/examples/geo-distributed-database-vercel/README.md index 5102be705d..7a4e731d90 100644 --- a/examples/geo-distributed-database-vercel/README.md +++ b/examples/geo-distributed-database-vercel/README.md @@ -9,11 +9,14 @@ Store user session state in edge-local Rivet Actors so preferences and activity ## Getting Started -```bash -pnpm install -pnpm dev +```sh +git clone https://github.com/rivet-dev/rivet.git +cd rivet/examples/geo-distributed-database +npm install +npm run dev ``` + ## Features - Creates a region-specific Rivet Actor using `createInRegion` and `createState` input. diff --git a/examples/geo-distributed-database-vercel/package.json b/examples/geo-distributed-database-vercel/package.json index 146635f847..4a121cdd9e 100644 --- a/examples/geo-distributed-database-vercel/package.json +++ b/examples/geo-distributed-database-vercel/package.json @@ -33,9 +33,7 @@ "typescript" ], "tags": [ - "state", - "edge", - "multi-region" + "database" ], "priority": 1000, "frontendPort": 5173 diff --git a/examples/geo-distributed-database/README.md b/examples/geo-distributed-database/README.md index a81ac4ada9..b5e5d92f72 100644 --- a/examples/geo-distributed-database/README.md +++ b/examples/geo-distributed-database/README.md @@ -4,11 +4,14 @@ Store user session state in edge-local Rivet Actors so preferences and activity ## Getting Started -```bash -pnpm install -pnpm dev +```sh +git clone https://github.com/rivet-dev/rivet.git +cd rivet/examples/geo-distributed-database +npm install +npm run dev ``` + ## Features - Creates a region-specific Rivet Actor using `createInRegion` and `createState` input. diff --git a/examples/geo-distributed-database/package.json b/examples/geo-distributed-database/package.json index f164b427c8..1f0b541fc3 100644 --- a/examples/geo-distributed-database/package.json +++ b/examples/geo-distributed-database/package.json @@ -19,8 +19,8 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vitest": "^3.1.1", - "vite-plugin-srvx": "^1.0.0" + "vite-plugin-srvx": "^1.0.2", + "vitest": "^3.1.1" }, "dependencies": { "@hono/node-server": "^1.19.7", @@ -38,9 +38,7 @@ "typescript" ], "tags": [ - "state", - "edge", - "multi-region" + "database" ], "priority": 1000, "frontendPort": 5173 diff --git a/examples/hello-world/package.json b/examples/hello-world/package.json index d1509e277c..ade51b81f2 100644 --- a/examples/hello-world/package.json +++ b/examples/hello-world/package.json @@ -18,7 +18,7 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vite-plugin-srvx": "^1.0.0", + "vite-plugin-srvx": "^1.0.2", "vitest": "^3.1.1" }, "dependencies": { diff --git a/examples/hono-react/package.json b/examples/hono-react/package.json index 085bf96f18..ab14281d48 100644 --- a/examples/hono-react/package.json +++ b/examples/hono-react/package.json @@ -18,8 +18,8 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vitest": "^3.1.1", - "vite-plugin-srvx": "^1.0.0" + "vite-plugin-srvx": "^1.0.2", + "vitest": "^3.1.1" }, "dependencies": { "@hono/node-server": "^1.19.1", diff --git a/examples/kitchen-sink/package.json b/examples/kitchen-sink/package.json index 86403f2be4..59d0cf865e 100644 --- a/examples/kitchen-sink/package.json +++ b/examples/kitchen-sink/package.json @@ -28,7 +28,7 @@ "tsx": "^4.7.1", "typescript": "^5.2.2", "vite": "^5.2.0", - "vite-plugin-srvx": "^1.0.0" + "vite-plugin-srvx": "^1.0.2" }, "template": { "technologies": [ diff --git a/examples/multiplayer-game-vercel/README.md b/examples/multiplayer-game-vercel/README.md index 4f3e61ca27..a144ab2316 100644 --- a/examples/multiplayer-game-vercel/README.md +++ b/examples/multiplayer-game-vercel/README.md @@ -12,10 +12,11 @@ A real-time Agar.io style arena showing a matchmaker coordinator and GameRoom da ```sh git clone https://github.com/rivet-dev/rivet.git cd rivet/examples/multiplayer-game -pnpm install -pnpm dev +npm install +npm run dev ``` + ## Features - **Coordinator pattern**: Matchmaker Rivet Actor that indexes and assigns GameRoom actors diff --git a/examples/multiplayer-game/README.md b/examples/multiplayer-game/README.md index 19015d264b..f759baf959 100644 --- a/examples/multiplayer-game/README.md +++ b/examples/multiplayer-game/README.md @@ -7,10 +7,11 @@ A real-time Agar.io style arena showing a matchmaker coordinator and GameRoom da ```sh git clone https://github.com/rivet-dev/rivet.git cd rivet/examples/multiplayer-game -pnpm install -pnpm dev +npm install +npm run dev ``` + ## Features - **Coordinator pattern**: Matchmaker Rivet Actor that indexes and assigns GameRoom actors diff --git a/examples/multiplayer-game/package.json b/examples/multiplayer-game/package.json index bc9822d701..d3216cf0e0 100644 --- a/examples/multiplayer-game/package.json +++ b/examples/multiplayer-game/package.json @@ -18,7 +18,7 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vite-plugin-srvx": "^1.0.0", + "vite-plugin-srvx": "^1.0.2", "vitest": "^3.1.1" }, "dependencies": { diff --git a/examples/native-websockets/package.json b/examples/native-websockets/package.json index feaee73d4d..dca4be81b8 100644 --- a/examples/native-websockets/package.json +++ b/examples/native-websockets/package.json @@ -19,9 +19,9 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", + "vite-plugin-srvx": "^1.0.2", "vitest": "^3.1.1", - "ws": "^8.16.0", - "vite-plugin-srvx": "^1.0.0" + "ws": "^8.16.0" }, "dependencies": { "@hono/node-server": "^1.19.7", diff --git a/examples/per-tenant-database-vercel/.gitignore b/examples/per-tenant-database-vercel/.gitignore new file mode 100644 index 0000000000..e28f3d79db --- /dev/null +++ b/examples/per-tenant-database-vercel/.gitignore @@ -0,0 +1,4 @@ +.actorcore +node_modules +dist +.vercel diff --git a/examples/per-tenant-database-vercel/README.md b/examples/per-tenant-database-vercel/README.md new file mode 100644 index 0000000000..fdc3703f0f --- /dev/null +++ b/examples/per-tenant-database-vercel/README.md @@ -0,0 +1,39 @@ +> **Note:** This is the Vercel-optimized version of the [per-tenant-database](../per-tenant-database) example. +> It uses the `hono/vercel` adapter and is configured for Vercel deployment. + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Frivet-gg%2Frivet%2Ftree%2Fmain%2Fexamples%2Fper-tenant-database-vercel&project-name=per-tenant-database-vercel) + +# Per-Tenant Database + +Example project demonstrating per-company database isolation with Rivet Actor state. + +## Getting Started + +```sh +git clone https://github.com/rivet-dev/rivet.git +cd rivet/examples/per-tenant-database +npm install +npm run dev +``` + + +## Features + +- **Per-tenant isolation**: Each company name maps to its own CompanyDatabase actor and state +- **State as the database**: Employees and projects live in `c.state` for every actor instance +- **Action-driven updates**: Add and list data through actions like `addEmployee` and `listProjects` +- **Live switching**: Swap company names in the UI to see fully isolated datasets + +## Implementation + +The dashboard connects to one CompanyDatabase actor per company. The actor key is the company name, and its state becomes that company database. + +See the implementation in [`src/actors.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/per-tenant-database/src/actors.ts) and [`frontend/App.tsx`](https://github.com/rivet-dev/rivet/tree/main/examples/per-tenant-database/frontend/App.tsx). + +## Resources + +Read more about [state](/docs/actors/state), [actions](/docs/actors/actions), and [events](/docs/actors/events). + +## License + +MIT diff --git a/examples/per-tenant-database-vercel/api/index.ts b/examples/per-tenant-database-vercel/api/index.ts new file mode 100644 index 0000000000..07a830391f --- /dev/null +++ b/examples/per-tenant-database-vercel/api/index.ts @@ -0,0 +1,3 @@ +import app from "../src/server.ts"; + +export default app; diff --git a/examples/per-tenant-database-vercel/frontend/App.tsx b/examples/per-tenant-database-vercel/frontend/App.tsx new file mode 100644 index 0000000000..80dff31641 --- /dev/null +++ b/examples/per-tenant-database-vercel/frontend/App.tsx @@ -0,0 +1,375 @@ +import { createRivetKit } from "@rivetkit/react"; +import { type FormEvent, useEffect, useMemo, useState } from "react"; +import type { + CompanyStats, + Employee, + Project, + ProjectStatus, + registry, +} from "../src/actors.ts"; + +const { useActor } = createRivetKit( + `${location.origin}/api/rivet`, +); + +const COMPANY_PRESETS = [ + "Aurora Analytics", + "Cedarline Logistics", + "Juniper Devices", + "Mariner Studio", +]; + +const STATUS_LABELS: Record = { + planning: "Planning", + active: "Active", + done: "Done", +}; + +const formatDate = (timestamp: number) => + new Date(timestamp).toLocaleString(); + +const getInitials = (name: string) => + name + .split(" ") + .filter(Boolean) + .map((part) => part[0]) + .join("") + .slice(0, 2) + .toUpperCase(); + +export function App() { + const [companyName, setCompanyName] = useState(null); + const [companyInput, setCompanyInput] = useState(COMPANY_PRESETS[0]); + const [preset, setPreset] = useState(COMPANY_PRESETS[0]); + + const handlePresetChange = (value: string) => { + setPreset(value); + setCompanyInput(value); + }; + + const handleSignIn = () => { + const trimmed = companyInput.trim(); + if (trimmed.length === 0) return; + setCompanyName(trimmed); + }; + + return ( +
+
+
Per-tenant database demo
+

Isolate every company with a single Rivet Actor

+

+ Each company gets its own CompanyDatabase actor keyed by name. The + actor state is the database, so switching companies swaps the + entire dataset. +

+
+ + {companyName ? ( + + ) : ( +
+

Sign in to a company

+

+ Pick a company or enter a new one. The company name becomes the + actor key. +

+
+ + + + + setCompanyInput(event.target.value)} + placeholder="Enter company name" + /> + + +
+
+ )} +
+ ); +} + +type CompanyDashboardProps = { + companyName: string; + onSwitch: (companyName: string | null) => void; +}; + +function CompanyDashboard({ companyName, onSwitch }: CompanyDashboardProps) { + const company = useActor({ + name: "companyDatabase", + key: [companyName], + }); + + const [employees, setEmployees] = useState([]); + const [projects, setProjects] = useState([]); + const [stats, setStats] = useState(null); + const [employeeName, setEmployeeName] = useState(""); + const [employeeRole, setEmployeeRole] = useState(""); + const [projectName, setProjectName] = useState(""); + const [projectStatus, setProjectStatus] = useState( + "planning", + ); + const [switchName, setSwitchName] = useState(companyName); + + useEffect(() => { + setSwitchName(companyName); + }, [companyName]); + + const refreshStats = async () => { + if (!company.connection) return; + const latestStats = await company.connection.getStats(); + setStats(latestStats); + }; + + useEffect(() => { + let canceled = false; + setEmployees([]); + setProjects([]); + setStats(null); + + const loadData = async () => { + if (!company.connection) return; + const [employeeRows, projectRows, latestStats] = await Promise.all([ + company.connection.listEmployees(), + company.connection.listProjects(), + company.connection.getStats(), + ]); + + if (canceled) return; + setEmployees(employeeRows); + setProjects(projectRows); + setStats(latestStats); + }; + + loadData(); + + return () => { + canceled = true; + }; + }, [company.connection, companyName]); + + company.useEvent("employeeAdded", (employee: Employee) => { + setEmployees((prev) => [...prev, employee]); + void refreshStats(); + }); + + company.useEvent("projectAdded", (project: Project) => { + setProjects((prev) => [...prev, project]); + void refreshStats(); + }); + + const companyInitials = useMemo(() => getInitials(companyName), [companyName]); + + const handleAddEmployee = async (event: FormEvent) => { + event.preventDefault(); + if (!company.connection) return; + await company.connection.addEmployee(employeeName, employeeRole); + setEmployeeName(""); + setEmployeeRole(""); + }; + + const handleAddProject = async (event: FormEvent) => { + event.preventDefault(); + if (!company.connection) return; + await company.connection.addProject(projectName, projectStatus); + setProjectName(""); + }; + + const handleSwitchCompany = () => { + const trimmed = switchName.trim(); + if (!trimmed) return; + onSwitch(trimmed); + }; + + const connectionLabel = company.connection + ? "Connected to CompanyDatabase actor" + : "Connecting to CompanyDatabase actor"; + + return ( +
+
+
+
{companyInitials}
+
+

{companyName}

+

+ Each company name maps to a unique actor with isolated state. +

+
+
+
+ Actor key: {companyName} + {connectionLabel} +
+
+ +
+
+
+

Company stats

+ Live snapshot +
+ {stats ? ( +
+
+
{stats.employee_count}
+
Employees stored
+
+
+
{stats.project_count}
+
Projects tracked
+
+
+
+ {formatDate(stats.created_at)} +
+
Database created
+
+
+
+ {formatDate(stats.updated_at)} +
+
Last updated
+
+
+ ) : ( +
Loading stats from actor state.
+ )} +
+ +
+
+

Switch company

+ Same UI, new tenant +
+
+ setSwitchName(event.target.value)} + placeholder="Enter a different company name" + /> + +
+
+ Tip: + + Add employees or projects, then switch to another company. + +
+
+
+ +
+
+
+

Employees

+ {employees.length} total +
+
+ setEmployeeName(event.target.value)} + placeholder="Employee name" + required + /> + setEmployeeRole(event.target.value)} + placeholder="Role or team" + required + /> + +
+
+ {employees.length === 0 ? ( +
+ No employees yet. Add a few to this company. +
+ ) : ( + employees.map((employee) => ( +
+ {employee.name} + {employee.role} + + Added {formatDate(employee.created_at)} + +
+ )) + )} +
+
+ +
+
+

Projects

+ {projects.length} total +
+
+ setProjectName(event.target.value)} + placeholder="Project name" + required + /> + + +
+
+ {projects.length === 0 ? ( +
+ No projects yet. Add one to see it here. +
+ ) : ( + projects.map((project) => ( +
+ {project.name} + Status: {STATUS_LABELS[project.status]} + + Added {formatDate(project.created_at)} + +
+ )) + )} +
+
+
+
+ ); +} diff --git a/examples/per-tenant-database-vercel/frontend/main.tsx b/examples/per-tenant-database-vercel/frontend/main.tsx new file mode 100644 index 0000000000..2efcb334fc --- /dev/null +++ b/examples/per-tenant-database-vercel/frontend/main.tsx @@ -0,0 +1,12 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App.tsx"; + +const root = document.getElementById("root"); +if (!root) throw new Error("Root element not found"); + +createRoot(root).render( + + + , +); diff --git a/examples/per-tenant-database-vercel/index.html b/examples/per-tenant-database-vercel/index.html new file mode 100644 index 0000000000..a408d8e9fb --- /dev/null +++ b/examples/per-tenant-database-vercel/index.html @@ -0,0 +1,340 @@ + + + + + + Per-Tenant Database Example + + + +
+ + + diff --git a/examples/per-tenant-database-vercel/package.json b/examples/per-tenant-database-vercel/package.json new file mode 100644 index 0000000000..8d2174d723 --- /dev/null +++ b/examples/per-tenant-database-vercel/package.json @@ -0,0 +1,43 @@ +{ + "name": "per-tenant-database-vercel", + "version": "2.0.21", + "private": true, + "type": "module", + "scripts": { + "dev": "vercel dev", + "build": "vite build", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "@hono/node-server": "^1.19.7", + "@hono/node-ws": "^1.3.0", + "@rivetkit/react": "^2.0.38", + "hono": "^4.11.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rivetkit": "^2.0.38" + }, + "devDependencies": { + "@types/node": "^22.13.9", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "tsx": "^3.12.7", + "typescript": "^5.5.2", + "vite": "^5.0.0", + "vitest": "^3.1.1" + }, + "template": { + "technologies": [ + "react", + "typescript" + ], + "tags": [ + "database" + ], + "priority": 200, + "frontendPort": 5173 + }, + "stableVersion": "0.8.0", + "license": "MIT" +} diff --git a/examples/per-tenant-database-vercel/src/actors.ts b/examples/per-tenant-database-vercel/src/actors.ts new file mode 100644 index 0000000000..6950c86025 --- /dev/null +++ b/examples/per-tenant-database-vercel/src/actors.ts @@ -0,0 +1,104 @@ +import { actor, setup } from "rivetkit"; + +export type Employee = { + id: string; + name: string; + role: string; + created_at: number; +}; + +export type ProjectStatus = "planning" | "active" | "done"; + +export type Project = { + id: string; + name: string; + status: ProjectStatus; + created_at: number; +}; + +export type CompanyStats = { + employee_count: number; + project_count: number; + created_at: number; + updated_at: number; +}; + +export type CompanyDatabaseState = { + company_name: string; + employees: Employee[]; + projects: Project[]; + created_at: number; + updated_at: number; +}; + +const createId = (prefix: string) => + `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; + +const getCompanyName = (key: unknown) => { + if (typeof key === "string" && key.trim()) { + return key.trim(); + } + + return "Unknown Company"; +}; + +export const companyDatabase = actor({ + // Persistent state is isolated per company. https://rivet.dev/docs/actors/state + createState: (c): CompanyDatabaseState => { + const now = Date.now(); + return { + company_name: getCompanyName(c.key[0]), + employees: [], + projects: [], + created_at: now, + updated_at: now, + }; + }, + + // Callable functions from clients. https://rivet.dev/docs/actors/actions + actions: { + addEmployee: (c, name: string, role: string) => { + const employee: Employee = { + id: createId("emp"), + name: name.trim() || "New Employee", + role: role.trim() || "Generalist", + created_at: Date.now(), + }; + + c.state.employees.push(employee); + c.state.updated_at = Date.now(); + c.broadcast("employeeAdded", employee); + return employee; + }, + + listEmployees: (c) => c.state.employees, + + addProject: (c, name: string, status: ProjectStatus) => { + const project: Project = { + id: createId("proj"), + name: name.trim() || "New Project", + status, + created_at: Date.now(), + }; + + c.state.projects.push(project); + c.state.updated_at = Date.now(); + c.broadcast("projectAdded", project); + return project; + }, + + listProjects: (c) => c.state.projects, + + getStats: (c): CompanyStats => ({ + employee_count: c.state.employees.length, + project_count: c.state.projects.length, + created_at: c.state.created_at, + updated_at: c.state.updated_at, + }), + }, +}); + +// Register actors for use. https://rivet.dev/docs/setup +export const registry = setup({ + use: { companyDatabase }, +}); diff --git a/examples/per-tenant-database-vercel/src/server.ts b/examples/per-tenant-database-vercel/src/server.ts new file mode 100644 index 0000000000..95c8895f94 --- /dev/null +++ b/examples/per-tenant-database-vercel/src/server.ts @@ -0,0 +1,6 @@ +import { Hono } from "hono"; +import { registry } from "./actors.ts"; + +const app = new Hono(); +app.all("/api/rivet/*", (c) => registry.handler(c.req.raw)); +export default app; diff --git a/examples/per-tenant-database-vercel/tests/per-tenant-database.test.ts b/examples/per-tenant-database-vercel/tests/per-tenant-database.test.ts new file mode 100644 index 0000000000..bb0fcba4f2 --- /dev/null +++ b/examples/per-tenant-database-vercel/tests/per-tenant-database.test.ts @@ -0,0 +1,45 @@ +import { setupTest } from "rivetkit/test"; +import { expect, test } from "vitest"; +import { registry } from "../src/actors.ts"; + +test("Company data is isolated by actor key", async (ctx) => { + const { client } = await setupTest(ctx, registry); + const alpha = client.companyDatabase.getOrCreate(["Alpha Co"]); + const beta = client.companyDatabase.getOrCreate(["Beta Co"]); + + await alpha.addEmployee("Ava", "Engineering"); + await alpha.addProject("Phoenix", "active"); + await beta.addEmployee("Ben", "Sales"); + + const alphaEmployees = await alpha.listEmployees(); + const betaEmployees = await beta.listEmployees(); + const alphaProjects = await alpha.listProjects(); + const betaProjects = await beta.listProjects(); + + expect(alphaEmployees).toHaveLength(1); + expect(betaEmployees).toHaveLength(1); + expect(alphaEmployees[0].name).toBe("Ava"); + expect(betaEmployees[0].name).toBe("Ben"); + expect(alphaProjects).toHaveLength(1); + expect(betaProjects).toHaveLength(0); +}); + +test("Stats reflect per-company state", async (ctx) => { + const { client } = await setupTest(ctx, registry); + const company = client.companyDatabase.getOrCreate(["Stats Co"]); + + const initialStats = await company.getStats(); + expect(initialStats.employee_count).toBe(0); + expect(initialStats.project_count).toBe(0); + + await company.addEmployee("Lina", "Finance"); + await company.addEmployee("Omar", "Support"); + await company.addProject("Drift", "planning"); + + const updatedStats = await company.getStats(); + expect(updatedStats.employee_count).toBe(2); + expect(updatedStats.project_count).toBe(1); + expect(updatedStats.updated_at).toBeGreaterThanOrEqual( + updatedStats.created_at, + ); +}); diff --git a/examples/per-tenant-database-vercel/tsconfig.json b/examples/per-tenant-database-vercel/tsconfig.json new file mode 100644 index 0000000000..91635880bc --- /dev/null +++ b/examples/per-tenant-database-vercel/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": [ + "esnext", + "dom", + "dom.iterable" + ], + "jsx": "react-jsx", + "module": "esnext", + "moduleResolution": "bundler", + "types": [ + "node", + "vite/client" + ], + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true + }, + "include": [ + "src/**/*", + "api/**/*", + "frontend/**/*" + ] +} diff --git a/examples/per-tenant-database-vercel/turbo.json b/examples/per-tenant-database-vercel/turbo.json new file mode 100644 index 0000000000..c3d3d3b9bb --- /dev/null +++ b/examples/per-tenant-database-vercel/turbo.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": [ + "//" + ] +} diff --git a/examples/per-tenant-database-vercel/vercel.json b/examples/per-tenant-database-vercel/vercel.json new file mode 100644 index 0000000000..64a8d9b467 --- /dev/null +++ b/examples/per-tenant-database-vercel/vercel.json @@ -0,0 +1,9 @@ +{ + "framework": "vite", + "rewrites": [ + { + "source": "/api/(.*)", + "destination": "/api" + } + ] +} diff --git a/examples/per-tenant-database-vercel/vite.config.ts b/examples/per-tenant-database-vercel/vite.config.ts new file mode 100644 index 0000000000..f9f0d5ec2f --- /dev/null +++ b/examples/per-tenant-database-vercel/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], +}); diff --git a/examples/per-tenant-database-vercel/vitest.config.ts b/examples/per-tenant-database-vercel/vitest.config.ts new file mode 100644 index 0000000000..f913a97abd --- /dev/null +++ b/examples/per-tenant-database-vercel/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + server: { + port: 5173, + }, + test: { + include: ["tests/**/*.test.ts"], + }, +}); diff --git a/examples/per-tenant-database/README.md b/examples/per-tenant-database/README.md index af4e37705a..fb67a9d297 100644 --- a/examples/per-tenant-database/README.md +++ b/examples/per-tenant-database/README.md @@ -7,10 +7,11 @@ Example project demonstrating per-company database isolation with Rivet Actor st ```sh git clone https://github.com/rivet-dev/rivet.git cd rivet/examples/per-tenant-database -pnpm install -pnpm dev +npm install +npm run dev ``` + ## Features - **Per-tenant isolation**: Each company name maps to its own CompanyDatabase actor and state diff --git a/examples/per-tenant-database/package.json b/examples/per-tenant-database/package.json index fdef7d7751..94fcfa1b7d 100644 --- a/examples/per-tenant-database/package.json +++ b/examples/per-tenant-database/package.json @@ -18,7 +18,7 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vite-plugin-srvx": "^1.0.0", + "vite-plugin-srvx": "^1.0.2", "vitest": "^3.1.1" }, "dependencies": { @@ -38,8 +38,7 @@ "typescript" ], "tags": [ - "state", - "multi-tenant" + "database" ], "priority": 200, "frontendPort": 5173 diff --git a/examples/raw-fetch-handler/package.json b/examples/raw-fetch-handler/package.json index bc06c6f63c..c0da75812a 100644 --- a/examples/raw-fetch-handler/package.json +++ b/examples/raw-fetch-handler/package.json @@ -18,8 +18,8 @@ "tsx": "^4.20.0", "typescript": "^5.7.3", "vite": "^5.4.19", - "vitest": "^3.1.1", - "vite-plugin-srvx": "^1.0.0" + "vite-plugin-srvx": "^1.0.2", + "vitest": "^3.1.1" }, "dependencies": { "@hono/node-server": "^1.19.1", diff --git a/examples/raw-websocket-handler-proxy/package.json b/examples/raw-websocket-handler-proxy/package.json index 05d5669ac2..4469afbaad 100644 --- a/examples/raw-websocket-handler-proxy/package.json +++ b/examples/raw-websocket-handler-proxy/package.json @@ -27,8 +27,8 @@ "tsx": "^4.19.2", "typescript": "^5.7.2", "vite": "^6.0.5", - "vitest": "^3.1.1", - "vite-plugin-srvx": "^1.0.0" + "vite-plugin-srvx": "^1.0.2", + "vitest": "^3.1.1" }, "dependencies": { "@hono/node-server": "^1.19.1", diff --git a/examples/raw-websocket-handler/package.json b/examples/raw-websocket-handler/package.json index 6435516cc0..b64308cb31 100644 --- a/examples/raw-websocket-handler/package.json +++ b/examples/raw-websocket-handler/package.json @@ -26,18 +26,18 @@ "tsx": "^4.19.2", "typescript": "^5.7.2", "vite": "^6.0.5", - "vitest": "^3.1.1", - "vite-plugin-srvx": "^1.0.0" + "vite-plugin-srvx": "^1.0.2", + "vitest": "^3.1.1" }, "dependencies": { "@hono/node-server": "^1.19.7", "@hono/node-ws": "^1.3.0", "@rivetkit/react": "^2.0.38", "hono": "^4.7.0", - "srvx": "^0.10.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "rivetkit": "^2.0.38" + "rivetkit": "^2.0.38", + "srvx": "^0.10.0" }, "template": { "technologies": [ diff --git a/examples/react/package.json b/examples/react/package.json index b3383ef432..c7ed57adcf 100644 --- a/examples/react/package.json +++ b/examples/react/package.json @@ -18,7 +18,7 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vite-plugin-srvx": "^1.0.0", + "vite-plugin-srvx": "^1.0.2", "vitest": "^3.1.1" }, "dependencies": { diff --git a/examples/sandbox-coding-agent-vercel/README.md b/examples/sandbox-coding-agent-vercel/README.md index d27edf5f8b..0328791a09 100644 --- a/examples/sandbox-coding-agent-vercel/README.md +++ b/examples/sandbox-coding-agent-vercel/README.md @@ -12,10 +12,11 @@ Example project demonstrating queue-driven Rivet Actor sessions that control a S ```sh git clone https://github.com/rivet-dev/rivet.git cd rivet/examples/sandbox-coding-agent -pnpm install -pnpm dev +npm install +npm run dev ``` + ## Features - Actor-per-agent pattern with a coordinating manager Rivet Actor diff --git a/examples/sandbox-coding-agent-vercel/package.json b/examples/sandbox-coding-agent-vercel/package.json index 6fc564a0c1..f48c3ba9e0 100644 --- a/examples/sandbox-coding-agent-vercel/package.json +++ b/examples/sandbox-coding-agent-vercel/package.json @@ -35,7 +35,6 @@ ], "tags": [ "ai", - "sandbox", "real-time" ], "priority": 120, diff --git a/examples/sandbox-coding-agent/README.md b/examples/sandbox-coding-agent/README.md index a22c1676f9..d52525c9c1 100644 --- a/examples/sandbox-coding-agent/README.md +++ b/examples/sandbox-coding-agent/README.md @@ -7,10 +7,11 @@ Example project demonstrating queue-driven Rivet Actor sessions that control a S ```sh git clone https://github.com/rivet-dev/rivet.git cd rivet/examples/sandbox-coding-agent -pnpm install -pnpm dev +npm install +npm run dev ``` + ## Features - Actor-per-agent pattern with a coordinating manager Rivet Actor diff --git a/examples/sandbox-coding-agent/package.json b/examples/sandbox-coding-agent/package.json index dbdc3113e4..67c074dd3c 100644 --- a/examples/sandbox-coding-agent/package.json +++ b/examples/sandbox-coding-agent/package.json @@ -18,7 +18,7 @@ "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vite-plugin-srvx": "^1.0.0", + "vite-plugin-srvx": "^1.0.2", "vitest": "^3.1.1" }, "dependencies": { @@ -40,7 +40,6 @@ ], "tags": [ "ai", - "sandbox", "real-time" ], "priority": 120, diff --git a/examples/sandbox-vercel/.gitignore b/examples/sandbox-vercel/.gitignore new file mode 100644 index 0000000000..e28f3d79db --- /dev/null +++ b/examples/sandbox-vercel/.gitignore @@ -0,0 +1,4 @@ +.actorcore +node_modules +dist +.vercel diff --git a/examples/sandbox-vercel/README.md b/examples/sandbox-vercel/README.md new file mode 100644 index 0000000000..4a21741ff2 --- /dev/null +++ b/examples/sandbox-vercel/README.md @@ -0,0 +1,46 @@ +> **Note:** This is the Vercel-optimized version of the [sandbox](../sandbox) example. +> It uses the `hono/vercel` adapter and is configured for Vercel deployment. + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Frivet-gg%2Frivet%2Ftree%2Fmain%2Fexamples%2Fsandbox-vercel&project-name=sandbox-vercel) + +# Sandbox + +Unified sandbox showcasing Rivet Actor features with a single registry, grouped navigation, and interactive demos. + +## Getting Started + +```sh +git clone https://github.com/rivet-dev/rivet.git +cd rivet/examples/sandbox +npm install +npm run dev +``` + + +## Features + +- Unified registry that aggregates actor fixtures and example actors +- Sidebar navigation grouped by core actor feature areas +- Action runner and event listener for quick experimentation +- Raw HTTP and WebSocket demos for handler-based actors +- Workflow and queue pattern coverage in a single sandbox + +## Prerequisites + +- OpenAI API key (set `OPENAI_API_KEY`) for the AI actor demo + +## Implementation + +The sandbox registry imports fixtures and example actors into one setup so each page can expose a curated subset. + +See the registry in [`src/actors.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/sandbox/src/actors.ts) and the UI in [`frontend/App.tsx`](https://github.com/rivet-dev/rivet/tree/main/examples/sandbox/frontend/App.tsx). + +## Resources + +Read more about [Rivet Actors](https://rivet.dev/docs/actors), +[actions](https://rivet.dev/docs/actors/actions), and +[connections](https://rivet.dev/docs/actors/connections). + +## License + +MIT diff --git a/examples/sandbox-vercel/api/index.ts b/examples/sandbox-vercel/api/index.ts new file mode 100644 index 0000000000..07a830391f --- /dev/null +++ b/examples/sandbox-vercel/api/index.ts @@ -0,0 +1,3 @@ +import app from "../src/server.ts"; + +export default app; diff --git a/examples/sandbox-vercel/frontend/App.tsx b/examples/sandbox-vercel/frontend/App.tsx new file mode 100644 index 0000000000..44b7231e27 --- /dev/null +++ b/examples/sandbox-vercel/frontend/App.tsx @@ -0,0 +1,981 @@ +import { createRivetKit } from "@rivetkit/react"; +import mermaid from "mermaid"; +import { Highlight, themes } from "prism-react-renderer"; +import { + Code, + Compass, + Database, + FlaskConical, + GitBranch, + Globe, + List, + Network, + Radio, + RefreshCw, +} from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { registry } from "../src/actors.ts"; +import { + ACTION_TEMPLATES, + type ActionTemplate, + PAGE_GROUPS, + PAGE_INDEX, + type PageConfig, +} from "./page-data.ts"; + +type ActorName = (typeof registry)["config"]["use"] extends Record ? K & string : never; + +const GROUP_ICONS: Record> = { + compass: Compass, + code: Code, + database: Database, + radio: Radio, + globe: Globe, + "refresh-cw": RefreshCw, + list: List, + "git-branch": GitBranch, + network: Network, + "flask-conical": FlaskConical, +}; + +mermaid.initialize({ + startOnLoad: false, + theme: "dark", + themeVariables: { + darkMode: true, + background: "#0a0a0a", + primaryColor: "#1c1c1e", + primaryTextColor: "#ffffff", + primaryBorderColor: "#3a3a3c", + lineColor: "#3a3a3c", + secondaryColor: "#2c2c2e", + tertiaryColor: "#0f0f0f", + fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, sans-serif", + fontSize: "13px", + }, +}); + +function MermaidDiagram({ chart }: { chart: string }) { + const ref = useRef(null); + const [svg, setSvg] = useState(""); + + useEffect(() => { + let cancelled = false; + const id = `mermaid-${Math.random().toString(36).slice(2)}`; + mermaid.render(id, chart).then(({ svg: renderedSvg }) => { + if (!cancelled) setSvg(renderedSvg); + }); + return () => { cancelled = true; }; + }, [chart]); + + return
; +} + +const { useActor } = createRivetKit( + `${location.origin}/api/rivet`, +); + +type JsonResult = { ok: true; value: T } | { ok: false; error: string }; + +function parseJson(value: string): JsonResult { + try { + const parsed = JSON.parse(value) as T; + return { ok: true, value: parsed }; + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : "Invalid JSON", + }; + } +} + +function parseKey(value: string): JsonResult { + const trimmed = value.trim(); + if (trimmed.startsWith("[")) { + return parseJson(trimmed); + } + return { ok: true, value: trimmed || "demo" }; +} + +function formatJson(value: unknown) { + return JSON.stringify(value, null, 2); +} + +function usePersistedState(key: string, initial: T) { + const [state, setState] = useState(() => { + const stored = localStorage.getItem(key); + return stored ? (JSON.parse(stored) as T) : initial; + }); + + useEffect(() => { + localStorage.setItem(key, JSON.stringify(state)); + }, [key, state]); + + return [state, setState] as const; +} + +function resolvePage(pageId: string) { + return PAGE_INDEX.find((page) => page.id === pageId) ?? PAGE_INDEX[0]; +} + +function formatActorName(name: string) { + return name + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + .replace(/-/g, " ") + .replace(/_/g, " ") + .replace(/^\w/, (char) => char.toUpperCase()); +} + +function getStateAction(actorName: string): string | undefined { + const templates = ACTION_TEMPLATES[actorName] ?? []; + return templates.find(t => t.args.length === 0)?.action; +} + +// ── Main App ────────────────────────────────────── + +export function App() { + const [activePageId, setActivePageId] = usePersistedState( + "sandbox:page", + PAGE_GROUPS[0].pages[0].id, + ); + const activePage = resolvePage(activePageId); + + return ( +
+ + +
+
+

{activePage.title}

+

{activePage.description}

+
+ + {activePage.id === "welcome" ? ( + + ) : ( + + )} +
+
+ ); +} + +// ── Demo Panel Router ───────────────────────────── + +function DemoPanel({ page }: { page: PageConfig }) { + if (page.demo === "diagram") { + return ; + } + if (page.actors.length === 0) { + return ; + } + if (page.demo === "raw-http") { + return ; + } + if (page.demo === "raw-websocket") { + return ; + } + return ; +} + +// ── Actor Demo Panel (tabs + view + code) ───────── + +function ActorDemoPanel({ page }: { page: PageConfig }) { + const [selectedIdx, setSelectedIdx] = useState(0); + + useEffect(() => { setSelectedIdx(0); }, [page.id]); + + const actorName = page.actors[selectedIdx] ?? page.actors[0]; + + return ( +
+ {page.actors.length > 1 && ( +
+ {page.actors.map((name, idx) => ( + + ))} +
+ )} + + + +
+
+ Source +
+ +
+
+ ); +} + +// ── Actor View (two-column: controls | inspector) ─ + +function ActorView({ actorName, page }: { actorName: string; page: PageConfig }) { + const [keyInput, setKeyInput] = usePersistedState( + `sandbox:${page.id}:${actorName}:key`, + `demo-${page.id}`, + ); + const [paramsInput, setParamsInput] = usePersistedState( + `sandbox:${page.id}:${actorName}:params`, + "{}", + ); + const [createInput, setCreateInput] = usePersistedState( + `sandbox:${page.id}:${actorName}:input`, + "{}", + ); + + const parsedKey = useMemo(() => parseKey(keyInput), [keyInput]); + const parsedParams = useMemo( + () => parseJson>(paramsInput), + [paramsInput], + ); + const parsedInput = useMemo( + () => parseJson(createInput), + [createInput], + ); + + const resolvedParams = + parsedParams.ok && paramsInput.trim() !== "{}" + ? parsedParams.value + : undefined; + const resolvedInput = + parsedInput.ok && createInput.trim() !== "{}" + ? parsedInput.value + : undefined; + + const actor = useActor({ + name: actorName as ActorName, + key: parsedKey.ok ? parsedKey.value : "demo", + params: resolvedParams, + createWithInput: resolvedInput, + }); + + const templates = ACTION_TEMPLATES[actorName] ?? []; + const stateAction = getStateAction(actorName); + + const [stateRefreshCounter, setStateRefreshCounter] = useState(0); + const triggerStateRefresh = useCallback( + () => setStateRefreshCounter(c => c + 1), + [], + ); + + return ( +
+
+
+ {page.actors.length === 1 && ( + {formatActorName(actorName)} + )} +
+ + {actor.connStatus ?? "idle"} +
+
+
+
+ + setKeyInput(e.target.value)} + placeholder="demo" + /> +
+
+ + setParamsInput(e.target.value)} + placeholder="{}" + /> +
+
+ + setCreateInput(e.target.value)} + placeholder="{}" + /> +
+
+
+ +
+
+
Actions
+ +
+ +
+ {stateAction && ( + + )} + +
+
+
+ ); +} + +// ── State Panel ─────────────────────────────────── + +function StatePanel({ + actor, + stateAction, + refreshTrigger, +}: { + actor: ReturnType; + stateAction: string; + refreshTrigger: number; +}) { + const [state, setState] = useState(""); + const [isRefreshing, setIsRefreshing] = useState(false); + const handleRef = useRef(actor.handle); + handleRef.current = actor.handle; + + const refresh = useCallback(async () => { + const handle = handleRef.current; + if (!handle) return; + setIsRefreshing(true); + try { + const result = await handle.action({ + name: stateAction, + args: [], + }); + setState(formatJson(result)); + } catch (err) { + setState(`Error: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setIsRefreshing(false); + } + }, [stateAction]); + + useEffect(() => { + if (actor.connStatus === "connected") { + refresh(); + } + }, [actor.connStatus, refresh]); + + useEffect(() => { + if (refreshTrigger > 0) { + refresh(); + } + }, [refreshTrigger, refresh]); + + return ( +
+
+ State + +
+
+ {actor.connStatus !== "connected" + ? "Connecting\u2026" + : state || "Loading\u2026"} +
+
+ ); +} + +// ── Events Panel ────────────────────────────────── + +function EventsPanel({ actor }: { actor: ReturnType }) { + const [eventName, setEventName] = useState(""); + const [events, setEvents] = useState>([]); + + useEffect(() => { + if (!eventName.trim() || !actor.connection) return; + + const stop = actor.connection.on(eventName, (...args: unknown[]) => { + const now = new Date(); + const time = [now.getHours(), now.getMinutes(), now.getSeconds()] + .map(n => n.toString().padStart(2, "0")) + .join(":"); + setEvents((prev) => [ + { time, data: formatJson(args.length === 1 ? args[0] : args) }, + ...prev.slice(0, 49), + ]); + }); + + return () => { stop(); }; + }, [actor.connection, eventName]); + + return ( +
+
+ Events +
+ setEventName(e.target.value)} + placeholder="event name" + className="inspector-input" + /> + {events.length > 0 && ( + + )} +
+
+
+ {events.length === 0 ? ( +
+ {eventName + ? "Waiting for events\u2026" + : "Enter an event name to listen"} +
+ ) : ( + events.map((entry, i) => ( +
+ {entry.time} + {entry.data} +
+ )) + )} +
+
+ ); +} + +// ── Code Block ──────────────────────────────────── + +function CodeBlock({ code }: { code: string }) { + return ( + + {({ tokens, getLineProps, getTokenProps }) => ( +
+					{tokens.map((line, i) => (
+						
+ {line.map((token, key) => ( + + ))} +
+ ))} +
+ )} +
+ ); +} + +// ── Action Runner ───────────────────────────────── + +function ActionRunner({ + actor, + templates, + onActionComplete, +}: { + actor: ReturnType; + templates: ActionTemplate[]; + onActionComplete?: () => void; +}) { + const [selectedIdx, setSelectedIdx] = useState(0); + const selectedTemplate = templates[selectedIdx]; + const [argsInput, setArgsInput] = useState( + selectedTemplate ? JSON.stringify(selectedTemplate.args) : "[]", + ); + const [result, setResult] = useState(""); + const [error, setError] = useState(""); + const [isRunning, setIsRunning] = useState(false); + + useEffect(() => { + setSelectedIdx(0); + if (templates[0]) { + setArgsInput(JSON.stringify(templates[0].args)); + } else { + setArgsInput("[]"); + } + }, [templates]); + + const selectTemplate = (idx: number) => { + setSelectedIdx(idx); + setArgsInput(JSON.stringify(templates[idx].args)); + setResult(""); + setError(""); + }; + + const parsedArgs = useMemo( + () => parseJson(argsInput), + [argsInput], + ); + + const runAction = async () => { + setError(""); + setResult(""); + const actionName = selectedTemplate?.action; + if (!actor.handle) { + setError("Actor handle is not ready."); + return; + } + if (!actionName) { + setError("Select an action to run."); + return; + } + if (!parsedArgs.ok) { + setError(parsedArgs.error); + return; + } + + setIsRunning(true); + try { + const response = await actor.handle.action({ + name: actionName, + args: parsedArgs.value, + }); + setResult(formatJson(response)); + onActionComplete?.(); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setIsRunning(false); + } + }; + + if (templates.length === 0) { + return
No actions available for this actor.
; + } + + return ( +
+
+ {templates.map((template, idx) => ( + + ))} +
+ +
+
+ setArgsInput(event.target.value)} + placeholder="[]" + className="action-args-input" + /> + +
+ {!parsedArgs.ok &&
{parsedArgs.error}
} + {error &&
{error}
} + {result &&
{result}
} +
+
+ ); +} + +// ── Welcome / Diagram / Config ──────────────────── + +function WelcomePanel() { + return ( +
+

+ This sandbox lets you interact with every Rivet Actor feature + in one place. Pick a topic from the sidebar to connect to + live actors, invoke actions, and observe events in real time. +

+
+ ); +} + +function DiagramPanel({ page }: { page: PageConfig }) { + if (!page.diagram) return null; + + return ( +
+ +
+ ); +} + +function ConfigPlayground() { + const [jsonInput, setJsonInput] = useState( + '{\n "key": ["demo"],\n "params": {\n "region": "local"\n }\n}', + ); + const parsed = parseJson>(jsonInput); + + return ( +
+
+

Configuration Playground

+

+ Edit JSON to explore how actor configuration payloads are + shaped. +

+
+
+
+ +