diff --git a/examples/queue-sandbox-vercel/api/index.ts b/examples/queue-sandbox-vercel/api/index.ts deleted file mode 100644 index 07a830391f..0000000000 --- a/examples/queue-sandbox-vercel/api/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import app from "../src/server.ts"; - -export default app; diff --git a/examples/queue-sandbox-vercel/frontend/App.tsx b/examples/queue-sandbox-vercel/frontend/App.tsx deleted file mode 100644 index 31d182ff40..0000000000 --- a/examples/queue-sandbox-vercel/frontend/App.tsx +++ /dev/null @@ -1,651 +0,0 @@ -import { createRivetKit } from "@rivetkit/react"; -import { useState, useEffect, useCallback } from "react"; -import type { registry } from "../src/actors.ts"; -import type { ReceivedMessage } from "../src/actors/sender.ts"; -import type { QueueMessage } from "../src/actors/multi-queue.ts"; -import type { TimeoutResult } from "../src/actors/timeout.ts"; -import type { WorkerState } from "../src/actors/worker.ts"; -import type { SelfSenderState } from "../src/actors/self-sender.ts"; -import type { KeepAwakeState } from "../src/actors/keep-awake.ts"; - -const { useActor } = createRivetKit( - `${location.origin}/api/rivet`, -); - -type TabName = "send" | "multi-queue" | "timeout" | "worker" | "self-send" | "keep-awake"; - -// Generate unique key for actor instances -const instanceKey = `demo-${Date.now()}`; - -export function App() { - const [activeTab, setActiveTab] = useState("send"); - - return ( -
-
-

Queue Sandbox

-

Explore all the ways to use queues in RivetKit

-
- -
- - - - - - -
- -
- {activeTab === "send" && } - {activeTab === "multi-queue" && } - {activeTab === "timeout" && } - {activeTab === "worker" && } - {activeTab === "self-send" && } - {activeTab === "keep-awake" && } -
-
- ); -} - -function SendTab() { - const [messageText, setMessageText] = useState("Hello, Queue!"); - const [messages, setMessages] = useState([]); - - const actor = useActor({ name: "sender", key: [instanceKey] }); - - useEffect(() => { - if (actor.connection) { - actor.connection.getMessages().then(setMessages); - } - }, [actor.connection]); - - const sendMessage = async () => { - if (actor.handle) { - await actor.handle.queue.task.send({ text: messageText }); - } - }; - - const receiveMessage = async () => { - if (actor.connection) { - const result = await actor.connection.receiveOne(); - if (result) { - setMessages((prev) => [...prev, result]); - } - } - }; - - const clearMessages = async () => { - if (actor.connection) { - await actor.connection.clearMessages(); - setMessages([]); - } - }; - - return ( -
-

Send Messages to Queue

-

- Client sends messages to actor queue; actor receives and displays them -

- -
- - setMessageText(e.target.value)} - placeholder="Enter message text" - /> -
- -
- - - -
- -
-

Received Messages ({messages.length})

- {messages.length === 0 ? ( -

No messages received yet

- ) : ( -
    - {messages.map((msg, i) => ( -
  • - {msg.name} - - {JSON.stringify(msg.body)} - - - {new Date(msg.receivedAt).toLocaleTimeString()} - -
  • - ))} -
- )} -
-
- ); -} - -function MultiQueueTab() { - const [messages, setMessages] = useState([]); - - const actor = useActor({ name: "multiQueue", key: [instanceKey] }); - - useEffect(() => { - if (actor.connection) { - actor.connection.getMessages().then(setMessages); - } - }, [actor.connection]); - - const sendToQueue = async (queueName: string) => { - if (actor.handle) { - await actor.handle.queue[queueName].send({ - priority: queueName, - timestamp: Date.now(), - }); - } - }; - - const receiveFromQueues = async (queues: string[]) => { - if (actor.connection) { - const received = await actor.connection.receiveFromQueues(queues, 5); - if (received.length > 0) { - setMessages((prev) => [ - ...prev, - ...received.map((r: { name: string; body: unknown }) => ({ - name: r.name, - body: r.body, - })), - ]); - } - } - }; - - const clearMessages = async () => { - if (actor.connection) { - await actor.connection.clearMessages(); - setMessages([]); - } - }; - - return ( -
-

Multi-Queue

-

- Listen to multiple named queues simultaneously -

- -
- - - -
- -
- - - - -
- -
-

Received Messages ({messages.length})

- {messages.length === 0 ? ( -

No messages received yet

- ) : ( -
    - {messages.map((msg, i) => ( -
  • - - {msg.name.toUpperCase()} - - - {JSON.stringify(msg.body)} - -
  • - ))} -
- )} -
-
- ); -} - -function TimeoutTab() { - const [timeoutMs, setTimeoutMs] = useState(3000); - const [isWaiting, setIsWaiting] = useState(false); - const [lastResult, setLastResult] = useState(null); - const [waitStartedAt, setWaitStartedAt] = useState(null); - - const actor = useActor({ name: "timeout", key: [instanceKey] }); - - const waitForMessage = async () => { - if (actor.connection) { - setIsWaiting(true); - setWaitStartedAt(Date.now()); - const result = await actor.connection.waitForMessage(timeoutMs); - setLastResult(result); - setIsWaiting(false); - setWaitStartedAt(null); - } - }; - - const sendMessage = async () => { - if (actor.handle) { - await actor.handle.queue.work.send({ text: "Hello!", sentAt: Date.now() }); - } - }; - - return ( -
-

Timeout

-

- Demonstrate timeout option when no messages arrive -

- -
- - setTimeoutMs(Number(e.target.value))} - /> -
- -
- - -
- - {isWaiting && waitStartedAt && ( -
- -
- )} - - {lastResult && ( -
-

{lastResult.timedOut ? "Timed Out" : "Message Received"}

- {lastResult.message !== undefined && ( -

Message: {JSON.stringify(lastResult.message)}

- )} -

Waited: {lastResult.waitedMs}ms

-
- )} -
- ); -} - -function CountdownTimer({ startedAt, timeoutMs }: { startedAt: number; timeoutMs: number }) { - const [remaining, setRemaining] = useState(timeoutMs); - - useEffect(() => { - const interval = setInterval(() => { - const elapsed = Date.now() - startedAt; - setRemaining(Math.max(0, timeoutMs - elapsed)); - }, 100); - return () => clearInterval(interval); - }, [startedAt, timeoutMs]); - - const progress = 1 - remaining / timeoutMs; - - return ( -
-
-
-
- {(remaining / 1000).toFixed(1)}s remaining -
- ); -} - -function WorkerTab() { - const [state, setState] = useState({ - status: "idle", - processed: 0, - lastJob: null, - }); - const [jobData, setJobData] = useState("task-1"); - - const actor = useActor({ name: "worker", key: [instanceKey] }); - - useEffect(() => { - if (actor.connection) { - const fetchState = async () => { - const s = await actor.connection!.getState(); - setState(s); - }; - fetchState(); - const interval = setInterval(fetchState, 1000); - return () => clearInterval(interval); - } - }, [actor.connection]); - - const submitJob = async () => { - if (actor.handle) { - await actor.handle.queue.jobs.send({ id: jobData, submittedAt: Date.now() }); - } - }; - - return ( -
-

Worker

-

- Run handler consuming queue messages in a loop -

- -
-
- {state.status === "running" ? "Running" : "Idle"} -
-
- Processed: - {state.processed} -
-
- -
- - setJobData(e.target.value)} - placeholder="Enter job identifier" - /> -
- -
- -
- - {state.lastJob !== null && ( -
-

Last Processed Job

-
{JSON.stringify(state.lastJob, null, 2)}
-
- )} -
- ); -} - -function SelfSendTab() { - const [state, setState] = useState({ - sentCount: 0, - receivedCount: 0, - messages: [], - }); - const [messageBody, setMessageBody] = useState("self-message"); - - const actor = useActor({ name: "selfSender", key: [instanceKey] }); - - const refreshState = useCallback(async () => { - if (actor.connection) { - const s = await actor.connection.getState(); - setState(s); - } - }, [actor.connection]); - - useEffect(() => { - refreshState(); - }, [refreshState]); - - const sendToSelf = async () => { - if (actor.handle && actor.connection) { - await actor.handle.queue.self.send({ content: messageBody, sentAt: Date.now() }); - await actor.connection.incrementSentCount(); - await refreshState(); - } - }; - - const receiveFromSelf = async () => { - if (actor.connection) { - await actor.connection.receiveFromSelf(); - await refreshState(); - } - }; - - const clearMessages = async () => { - if (actor.connection) { - await actor.connection.clearMessages(); - await refreshState(); - } - }; - - return ( -
-

Self-Send

-

- Actor sends messages to its own queue via inline client -

- -
-
- Sent: - {state.sentCount} -
-
- Received: - {state.receivedCount} -
-
- -
- - setMessageBody(e.target.value)} - placeholder="Enter message content" - /> -
- -
- - - -
- -
-

Received Messages ({state.messages.length})

- {state.messages.length === 0 ? ( -

No messages received yet

- ) : ( -
    - {state.messages.map((msg, i) => ( -
  • - {JSON.stringify(msg)} -
  • - ))} -
- )} -
-
- ); -} - -function KeepAwakeTab() { - const [state, setState] = useState({ - currentTask: null, - completedTasks: [], - }); - const [durationMs, setDurationMs] = useState(3000); - - const actor = useActor({ name: "keepAwake", key: [instanceKey] }); - - useEffect(() => { - if (actor.connection) { - const fetchState = async () => { - const s = await actor.connection!.getState(); - setState(s); - }; - fetchState(); - const interval = setInterval(fetchState, 500); - return () => clearInterval(interval); - } - }, [actor.connection]); - - const submitTask = async () => { - if (actor.handle) { - await actor.handle.queue.tasks.send({ durationMs }); - } - }; - - const clearTasks = async () => { - if (actor.connection) { - await actor.connection.clearTasks(); - const s = await actor.connection.getState(); - setState(s); - } - }; - - return ( -
-

Keep Awake

-

- Consume queue message, then do long-running task wrapped in keepAwake() -

- -
- - setDurationMs(Number(e.target.value))} - /> -
- -
- - -
- - {state.currentTask && ( -
-

Current Task

- -
- )} - -
-

Completed Tasks ({state.completedTasks.length})

- {state.completedTasks.length === 0 ? ( -

No tasks completed yet

- ) : ( -
    - {state.completedTasks.map((task) => ( -
  • - {task.id.slice(0, 8)}... - - Completed: {new Date(task.completedAt).toLocaleTimeString()} - -
  • - ))} -
- )} -
-
- ); -} - -function TaskProgress({ task }: { task: { id: string; startedAt: number; durationMs: number } }) { - const [progress, setProgress] = useState(0); - - useEffect(() => { - const interval = setInterval(() => { - const elapsed = Date.now() - task.startedAt; - setProgress(Math.min(1, elapsed / task.durationMs)); - }, 100); - return () => clearInterval(interval); - }, [task.startedAt, task.durationMs]); - - return ( -
-
-
-
-
- Task: {task.id.slice(0, 8)}... - {Math.round(progress * 100)}% -
-
- ); -} diff --git a/examples/queue-sandbox-vercel/index.html b/examples/queue-sandbox-vercel/index.html deleted file mode 100644 index 413c852bf1..0000000000 --- a/examples/queue-sandbox-vercel/index.html +++ /dev/null @@ -1,312 +0,0 @@ - - - - - - Queue Sandbox - - - -
- - - diff --git a/examples/queue-sandbox-vercel/package.json b/examples/queue-sandbox-vercel/package.json deleted file mode 100644 index c90c72960c..0000000000 --- a/examples/queue-sandbox-vercel/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "queue-sandbox-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.2.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" - }, - "template": { - "technologies": [ - "typescript", - "react" - ], - "tags": [ - "queues" - ], - "priority": 100, - "frontendPort": 5173 - }, - "license": "MIT" -} diff --git a/examples/queue-sandbox-vercel/src/actors.ts b/examples/queue-sandbox-vercel/src/actors.ts deleted file mode 100644 index c581fe152e..0000000000 --- a/examples/queue-sandbox-vercel/src/actors.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { setup } from "rivetkit"; -import { sender } from "./actors/sender.ts"; -import { multiQueue } from "./actors/multi-queue.ts"; -import { timeout } from "./actors/timeout.ts"; -import { worker } from "./actors/worker.ts"; -import { selfSender } from "./actors/self-sender.ts"; -import { keepAwake } from "./actors/keep-awake.ts"; - -export const registry = setup({ - use: { - sender, - multiQueue, - timeout, - worker, - selfSender, - keepAwake, - }, -}); diff --git a/examples/queue-sandbox-vercel/src/server.ts b/examples/queue-sandbox-vercel/src/server.ts deleted file mode 100644 index 95c8895f94..0000000000 --- a/examples/queue-sandbox-vercel/src/server.ts +++ /dev/null @@ -1,6 +0,0 @@ -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/queue-sandbox-vercel/tsconfig.json b/examples/queue-sandbox-vercel/tsconfig.json deleted file mode 100644 index 91635880bc..0000000000 --- a/examples/queue-sandbox-vercel/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "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/queue-sandbox-vercel/turbo.json b/examples/queue-sandbox-vercel/turbo.json deleted file mode 100644 index c3d3d3b9bb..0000000000 --- a/examples/queue-sandbox-vercel/turbo.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "https://turbo.build/schema.json", - "extends": [ - "//" - ] -} diff --git a/examples/queue-sandbox-vercel/vercel.json b/examples/queue-sandbox-vercel/vercel.json deleted file mode 100644 index 64a8d9b467..0000000000 --- a/examples/queue-sandbox-vercel/vercel.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "framework": "vite", - "rewrites": [ - { - "source": "/api/(.*)", - "destination": "/api" - } - ] -} diff --git a/examples/queue-sandbox-vercel/vite.config.ts b/examples/queue-sandbox-vercel/vite.config.ts deleted file mode 100644 index f9f0d5ec2f..0000000000 --- a/examples/queue-sandbox-vercel/vite.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -export default defineConfig({ - plugins: [react()], -}); diff --git a/examples/queue-sandbox/frontend/App.tsx b/examples/queue-sandbox/frontend/App.tsx deleted file mode 100644 index 31d182ff40..0000000000 --- a/examples/queue-sandbox/frontend/App.tsx +++ /dev/null @@ -1,651 +0,0 @@ -import { createRivetKit } from "@rivetkit/react"; -import { useState, useEffect, useCallback } from "react"; -import type { registry } from "../src/actors.ts"; -import type { ReceivedMessage } from "../src/actors/sender.ts"; -import type { QueueMessage } from "../src/actors/multi-queue.ts"; -import type { TimeoutResult } from "../src/actors/timeout.ts"; -import type { WorkerState } from "../src/actors/worker.ts"; -import type { SelfSenderState } from "../src/actors/self-sender.ts"; -import type { KeepAwakeState } from "../src/actors/keep-awake.ts"; - -const { useActor } = createRivetKit( - `${location.origin}/api/rivet`, -); - -type TabName = "send" | "multi-queue" | "timeout" | "worker" | "self-send" | "keep-awake"; - -// Generate unique key for actor instances -const instanceKey = `demo-${Date.now()}`; - -export function App() { - const [activeTab, setActiveTab] = useState("send"); - - return ( -
-
-

Queue Sandbox

-

Explore all the ways to use queues in RivetKit

-
- -
- - - - - - -
- -
- {activeTab === "send" && } - {activeTab === "multi-queue" && } - {activeTab === "timeout" && } - {activeTab === "worker" && } - {activeTab === "self-send" && } - {activeTab === "keep-awake" && } -
-
- ); -} - -function SendTab() { - const [messageText, setMessageText] = useState("Hello, Queue!"); - const [messages, setMessages] = useState([]); - - const actor = useActor({ name: "sender", key: [instanceKey] }); - - useEffect(() => { - if (actor.connection) { - actor.connection.getMessages().then(setMessages); - } - }, [actor.connection]); - - const sendMessage = async () => { - if (actor.handle) { - await actor.handle.queue.task.send({ text: messageText }); - } - }; - - const receiveMessage = async () => { - if (actor.connection) { - const result = await actor.connection.receiveOne(); - if (result) { - setMessages((prev) => [...prev, result]); - } - } - }; - - const clearMessages = async () => { - if (actor.connection) { - await actor.connection.clearMessages(); - setMessages([]); - } - }; - - return ( -
-

Send Messages to Queue

-

- Client sends messages to actor queue; actor receives and displays them -

- -
- - setMessageText(e.target.value)} - placeholder="Enter message text" - /> -
- -
- - - -
- -
-

Received Messages ({messages.length})

- {messages.length === 0 ? ( -

No messages received yet

- ) : ( -
    - {messages.map((msg, i) => ( -
  • - {msg.name} - - {JSON.stringify(msg.body)} - - - {new Date(msg.receivedAt).toLocaleTimeString()} - -
  • - ))} -
- )} -
-
- ); -} - -function MultiQueueTab() { - const [messages, setMessages] = useState([]); - - const actor = useActor({ name: "multiQueue", key: [instanceKey] }); - - useEffect(() => { - if (actor.connection) { - actor.connection.getMessages().then(setMessages); - } - }, [actor.connection]); - - const sendToQueue = async (queueName: string) => { - if (actor.handle) { - await actor.handle.queue[queueName].send({ - priority: queueName, - timestamp: Date.now(), - }); - } - }; - - const receiveFromQueues = async (queues: string[]) => { - if (actor.connection) { - const received = await actor.connection.receiveFromQueues(queues, 5); - if (received.length > 0) { - setMessages((prev) => [ - ...prev, - ...received.map((r: { name: string; body: unknown }) => ({ - name: r.name, - body: r.body, - })), - ]); - } - } - }; - - const clearMessages = async () => { - if (actor.connection) { - await actor.connection.clearMessages(); - setMessages([]); - } - }; - - return ( -
-

Multi-Queue

-

- Listen to multiple named queues simultaneously -

- -
- - - -
- -
- - - - -
- -
-

Received Messages ({messages.length})

- {messages.length === 0 ? ( -

No messages received yet

- ) : ( -
    - {messages.map((msg, i) => ( -
  • - - {msg.name.toUpperCase()} - - - {JSON.stringify(msg.body)} - -
  • - ))} -
- )} -
-
- ); -} - -function TimeoutTab() { - const [timeoutMs, setTimeoutMs] = useState(3000); - const [isWaiting, setIsWaiting] = useState(false); - const [lastResult, setLastResult] = useState(null); - const [waitStartedAt, setWaitStartedAt] = useState(null); - - const actor = useActor({ name: "timeout", key: [instanceKey] }); - - const waitForMessage = async () => { - if (actor.connection) { - setIsWaiting(true); - setWaitStartedAt(Date.now()); - const result = await actor.connection.waitForMessage(timeoutMs); - setLastResult(result); - setIsWaiting(false); - setWaitStartedAt(null); - } - }; - - const sendMessage = async () => { - if (actor.handle) { - await actor.handle.queue.work.send({ text: "Hello!", sentAt: Date.now() }); - } - }; - - return ( -
-

Timeout

-

- Demonstrate timeout option when no messages arrive -

- -
- - setTimeoutMs(Number(e.target.value))} - /> -
- -
- - -
- - {isWaiting && waitStartedAt && ( -
- -
- )} - - {lastResult && ( -
-

{lastResult.timedOut ? "Timed Out" : "Message Received"}

- {lastResult.message !== undefined && ( -

Message: {JSON.stringify(lastResult.message)}

- )} -

Waited: {lastResult.waitedMs}ms

-
- )} -
- ); -} - -function CountdownTimer({ startedAt, timeoutMs }: { startedAt: number; timeoutMs: number }) { - const [remaining, setRemaining] = useState(timeoutMs); - - useEffect(() => { - const interval = setInterval(() => { - const elapsed = Date.now() - startedAt; - setRemaining(Math.max(0, timeoutMs - elapsed)); - }, 100); - return () => clearInterval(interval); - }, [startedAt, timeoutMs]); - - const progress = 1 - remaining / timeoutMs; - - return ( -
-
-
-
- {(remaining / 1000).toFixed(1)}s remaining -
- ); -} - -function WorkerTab() { - const [state, setState] = useState({ - status: "idle", - processed: 0, - lastJob: null, - }); - const [jobData, setJobData] = useState("task-1"); - - const actor = useActor({ name: "worker", key: [instanceKey] }); - - useEffect(() => { - if (actor.connection) { - const fetchState = async () => { - const s = await actor.connection!.getState(); - setState(s); - }; - fetchState(); - const interval = setInterval(fetchState, 1000); - return () => clearInterval(interval); - } - }, [actor.connection]); - - const submitJob = async () => { - if (actor.handle) { - await actor.handle.queue.jobs.send({ id: jobData, submittedAt: Date.now() }); - } - }; - - return ( -
-

Worker

-

- Run handler consuming queue messages in a loop -

- -
-
- {state.status === "running" ? "Running" : "Idle"} -
-
- Processed: - {state.processed} -
-
- -
- - setJobData(e.target.value)} - placeholder="Enter job identifier" - /> -
- -
- -
- - {state.lastJob !== null && ( -
-

Last Processed Job

-
{JSON.stringify(state.lastJob, null, 2)}
-
- )} -
- ); -} - -function SelfSendTab() { - const [state, setState] = useState({ - sentCount: 0, - receivedCount: 0, - messages: [], - }); - const [messageBody, setMessageBody] = useState("self-message"); - - const actor = useActor({ name: "selfSender", key: [instanceKey] }); - - const refreshState = useCallback(async () => { - if (actor.connection) { - const s = await actor.connection.getState(); - setState(s); - } - }, [actor.connection]); - - useEffect(() => { - refreshState(); - }, [refreshState]); - - const sendToSelf = async () => { - if (actor.handle && actor.connection) { - await actor.handle.queue.self.send({ content: messageBody, sentAt: Date.now() }); - await actor.connection.incrementSentCount(); - await refreshState(); - } - }; - - const receiveFromSelf = async () => { - if (actor.connection) { - await actor.connection.receiveFromSelf(); - await refreshState(); - } - }; - - const clearMessages = async () => { - if (actor.connection) { - await actor.connection.clearMessages(); - await refreshState(); - } - }; - - return ( -
-

Self-Send

-

- Actor sends messages to its own queue via inline client -

- -
-
- Sent: - {state.sentCount} -
-
- Received: - {state.receivedCount} -
-
- -
- - setMessageBody(e.target.value)} - placeholder="Enter message content" - /> -
- -
- - - -
- -
-

Received Messages ({state.messages.length})

- {state.messages.length === 0 ? ( -

No messages received yet

- ) : ( -
    - {state.messages.map((msg, i) => ( -
  • - {JSON.stringify(msg)} -
  • - ))} -
- )} -
-
- ); -} - -function KeepAwakeTab() { - const [state, setState] = useState({ - currentTask: null, - completedTasks: [], - }); - const [durationMs, setDurationMs] = useState(3000); - - const actor = useActor({ name: "keepAwake", key: [instanceKey] }); - - useEffect(() => { - if (actor.connection) { - const fetchState = async () => { - const s = await actor.connection!.getState(); - setState(s); - }; - fetchState(); - const interval = setInterval(fetchState, 500); - return () => clearInterval(interval); - } - }, [actor.connection]); - - const submitTask = async () => { - if (actor.handle) { - await actor.handle.queue.tasks.send({ durationMs }); - } - }; - - const clearTasks = async () => { - if (actor.connection) { - await actor.connection.clearTasks(); - const s = await actor.connection.getState(); - setState(s); - } - }; - - return ( -
-

Keep Awake

-

- Consume queue message, then do long-running task wrapped in keepAwake() -

- -
- - setDurationMs(Number(e.target.value))} - /> -
- -
- - -
- - {state.currentTask && ( -
-

Current Task

- -
- )} - -
-

Completed Tasks ({state.completedTasks.length})

- {state.completedTasks.length === 0 ? ( -

No tasks completed yet

- ) : ( -
    - {state.completedTasks.map((task) => ( -
  • - {task.id.slice(0, 8)}... - - Completed: {new Date(task.completedAt).toLocaleTimeString()} - -
  • - ))} -
- )} -
-
- ); -} - -function TaskProgress({ task }: { task: { id: string; startedAt: number; durationMs: number } }) { - const [progress, setProgress] = useState(0); - - useEffect(() => { - const interval = setInterval(() => { - const elapsed = Date.now() - task.startedAt; - setProgress(Math.min(1, elapsed / task.durationMs)); - }, 100); - return () => clearInterval(interval); - }, [task.startedAt, task.durationMs]); - - return ( -
-
-
-
-
- Task: {task.id.slice(0, 8)}... - {Math.round(progress * 100)}% -
-
- ); -} diff --git a/examples/queue-sandbox/frontend/main.tsx b/examples/queue-sandbox/frontend/main.tsx deleted file mode 100644 index 2efcb334fc..0000000000 --- a/examples/queue-sandbox/frontend/main.tsx +++ /dev/null @@ -1,12 +0,0 @@ -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/queue-sandbox/index.html b/examples/queue-sandbox/index.html deleted file mode 100644 index 413c852bf1..0000000000 --- a/examples/queue-sandbox/index.html +++ /dev/null @@ -1,312 +0,0 @@ - - - - - - Queue Sandbox - - - -
- - - diff --git a/examples/queue-sandbox/src/actors.ts b/examples/queue-sandbox/src/actors.ts deleted file mode 100644 index c581fe152e..0000000000 --- a/examples/queue-sandbox/src/actors.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { setup } from "rivetkit"; -import { sender } from "./actors/sender.ts"; -import { multiQueue } from "./actors/multi-queue.ts"; -import { timeout } from "./actors/timeout.ts"; -import { worker } from "./actors/worker.ts"; -import { selfSender } from "./actors/self-sender.ts"; -import { keepAwake } from "./actors/keep-awake.ts"; - -export const registry = setup({ - use: { - sender, - multiQueue, - timeout, - worker, - selfSender, - keepAwake, - }, -}); diff --git a/examples/queue-sandbox/src/actors/keep-awake.ts b/examples/queue-sandbox/src/actors/keep-awake.ts deleted file mode 100644 index dcf9a0a28e..0000000000 --- a/examples/queue-sandbox/src/actors/keep-awake.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { actor } from "rivetkit"; - -export interface CurrentTask { - id: string; - startedAt: number; - durationMs: number; -} - -export interface CompletedTask { - id: string; - completedAt: number; -} - -export interface KeepAwakeState { - currentTask: CurrentTask | null; - completedTasks: CompletedTask[]; -} - -export const keepAwake = actor({ - state: { - currentTask: null as CurrentTask | null, - completedTasks: [] as CompletedTask[], - }, - async run(c) { - while (!c.abortSignal.aborted) { - const job = await c.queue.next("tasks", { timeout: 1000 }); - if (job) { - const taskId = crypto.randomUUID(); - const { durationMs } = job.body as { durationMs: number }; - - c.state.currentTask = { - id: taskId, - startedAt: Date.now(), - durationMs, - }; - c.broadcast("taskStarted", c.state.currentTask); - - // Wrap long-running work in keepAwake so actor doesn't sleep - await c.keepAwake( - new Promise((resolve) => setTimeout(resolve, durationMs)), - ); - - c.state.completedTasks.push({ id: taskId, completedAt: Date.now() }); - c.state.currentTask = null; - c.broadcast("taskCompleted", { - taskId, - completedTasks: c.state.completedTasks, - }); - } - } - }, - actions: { - getState(c): KeepAwakeState { - return { - currentTask: c.state.currentTask, - completedTasks: c.state.completedTasks, - }; - }, - clearTasks(c) { - c.state.completedTasks = []; - c.broadcast("taskCompleted", { - taskId: null, - completedTasks: [], - }); - }, - }, - options: { - sleepTimeout: 2000, - }, -}); diff --git a/examples/queue-sandbox/src/actors/multi-queue.ts b/examples/queue-sandbox/src/actors/multi-queue.ts deleted file mode 100644 index 57d57e7ad8..0000000000 --- a/examples/queue-sandbox/src/actors/multi-queue.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { actor } from "rivetkit"; - -export interface QueueMessage { - name: string; - body: unknown; -} - -export const multiQueue = actor({ - state: { - messages: [] as QueueMessage[], - }, - actions: { - async receiveFromQueues(c, names: string[], count: number) { - const msgs = await c.queue.next(names, { count, timeout: 100 }); - if (msgs && msgs.length > 0) { - for (const msg of msgs) { - c.state.messages.push({ name: msg.name, body: msg.body }); - } - c.broadcast("messagesReceived", c.state.messages); - } - return msgs ?? []; - }, - getMessages(c): QueueMessage[] { - return c.state.messages; - }, - clearMessages(c) { - c.state.messages = []; - c.broadcast("messagesReceived", c.state.messages); - }, - }, -}); diff --git a/examples/queue-sandbox/src/actors/self-sender.ts b/examples/queue-sandbox/src/actors/self-sender.ts deleted file mode 100644 index bfd1b3cd1a..0000000000 --- a/examples/queue-sandbox/src/actors/self-sender.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { actor } from "rivetkit"; - -export interface SelfSenderState { - sentCount: number; - receivedCount: number; - messages: unknown[]; -} - -export const selfSender = actor({ - state: { - sentCount: 0, - receivedCount: 0, - messages: [] as unknown[], - }, - actions: { - async receiveFromSelf(c) { - const msg = await c.queue.next("self", { timeout: 100 }); - if (msg) { - c.state.receivedCount += 1; - c.state.messages.push(msg.body); - c.broadcast("received", { - receivedCount: c.state.receivedCount, - message: msg.body, - }); - return msg.body; - } - return null; - }, - getState(c): SelfSenderState { - return { - sentCount: c.state.sentCount, - receivedCount: c.state.receivedCount, - messages: c.state.messages, - }; - }, - clearMessages(c) { - c.state.sentCount = 0; - c.state.receivedCount = 0; - c.state.messages = []; - c.broadcast("sent", { sentCount: 0 }); - c.broadcast("received", { receivedCount: 0, message: null }); - }, - incrementSentCount(c) { - c.state.sentCount += 1; - c.broadcast("sent", { sentCount: c.state.sentCount }); - }, - }, -}); diff --git a/examples/queue-sandbox/src/actors/sender.ts b/examples/queue-sandbox/src/actors/sender.ts deleted file mode 100644 index 0db2c49a0b..0000000000 --- a/examples/queue-sandbox/src/actors/sender.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { actor } from "rivetkit"; - -export interface ReceivedMessage { - name: string; - body: unknown; - receivedAt: number; -} - -export const sender = actor({ - state: { - messages: [] as ReceivedMessage[], - }, - actions: { - getMessages(c): ReceivedMessage[] { - return c.state.messages; - }, - async receiveOne(c) { - const msg = await c.queue.next("task", { timeout: 100 }); - if (msg) { - const received: ReceivedMessage = { - name: msg.name, - body: msg.body, - receivedAt: Date.now(), - }; - c.state.messages.push(received); - c.broadcast("messageReceived", c.state.messages); - return received; - } - return null; - }, - clearMessages(c) { - c.state.messages = []; - c.broadcast("messageReceived", c.state.messages); - }, - }, -}); diff --git a/examples/queue-sandbox/src/actors/timeout.ts b/examples/queue-sandbox/src/actors/timeout.ts deleted file mode 100644 index be0ff8fea7..0000000000 --- a/examples/queue-sandbox/src/actors/timeout.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { actor } from "rivetkit"; - -export interface TimeoutResult { - timedOut: boolean; - message?: unknown; - waitedMs: number; -} - -export interface TimeoutState { - lastResult: TimeoutResult | null; - waitStartedAt: number | null; -} - -export const timeout = actor({ - state: { - lastResult: null as TimeoutResult | null, - waitStartedAt: null as number | null, - }, - actions: { - async waitForMessage(c, timeoutMs: number): Promise { - const startedAt = Date.now(); - c.state.waitStartedAt = startedAt; - c.broadcast("waitStarted", { startedAt, timeoutMs }); - - const msg = await c.queue.next("work", { timeout: timeoutMs }); - - const waitedMs = Date.now() - startedAt; - const result: TimeoutResult = msg - ? { timedOut: false, message: msg.body, waitedMs } - : { timedOut: true, waitedMs }; - - c.state.lastResult = result; - c.state.waitStartedAt = null; - c.broadcast("waitCompleted", result); - return result; - }, - getState(c): TimeoutState { - return { - lastResult: c.state.lastResult, - waitStartedAt: c.state.waitStartedAt, - }; - }, - }, -}); diff --git a/examples/queue-sandbox/src/actors/worker.ts b/examples/queue-sandbox/src/actors/worker.ts deleted file mode 100644 index e7b710f5dc..0000000000 --- a/examples/queue-sandbox/src/actors/worker.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { actor } from "rivetkit"; - -export interface WorkerState { - status: "idle" | "running"; - processed: number; - lastJob: unknown; -} - -export const worker = actor({ - state: { - status: "idle" as "idle" | "running", - processed: 0, - lastJob: null as unknown, - }, - async run(c) { - c.state.status = "running"; - c.broadcast("statusChanged", { - status: c.state.status, - processed: c.state.processed, - }); - - while (!c.abortSignal.aborted) { - const job = await c.queue.next("jobs", { timeout: 1000 }); - if (job) { - c.state.processed += 1; - c.state.lastJob = job.body; - c.broadcast("jobProcessed", { - processed: c.state.processed, - job: job.body, - }); - } - } - - c.state.status = "idle"; - }, - actions: { - getState(c): WorkerState { - return { - status: c.state.status, - processed: c.state.processed, - lastJob: c.state.lastJob, - }; - }, - }, -}); diff --git a/examples/queue-sandbox/src/server.ts b/examples/queue-sandbox/src/server.ts deleted file mode 100644 index 95c8895f94..0000000000 --- a/examples/queue-sandbox/src/server.ts +++ /dev/null @@ -1,6 +0,0 @@ -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/queue-sandbox/tsconfig.json b/examples/queue-sandbox/tsconfig.json deleted file mode 100644 index 081970ac06..0000000000 --- a/examples/queue-sandbox/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "esnext", - "lib": ["esnext", "dom"], - "jsx": "react-jsx", - "module": "esnext", - "moduleResolution": "bundler", - "types": ["node", "vite/client"], - "resolveJsonModule": true, - "allowJs": true, - "checkJs": false, - "noEmit": true, - "isolatedModules": true, - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitAny": false, - "skipLibCheck": true, - "allowImportingTsExtensions": true, - "rewriteRelativeImportExtensions": true - }, - "include": ["src/**/*", "frontend/**/*"] -} diff --git a/examples/queue-sandbox/turbo.json b/examples/queue-sandbox/turbo.json deleted file mode 100644 index c5e71016d3..0000000000 --- a/examples/queue-sandbox/turbo.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "https://turbo.build/schema.json", - "extends": ["//"], - "tasks": { - "build": { - "dependsOn": ["@rivetkit/react#build", "rivetkit#build"] - } - } -} diff --git a/examples/queue-sandbox/vite.config.ts b/examples/queue-sandbox/vite.config.ts deleted file mode 100644 index 06dae893f5..0000000000 --- a/examples/queue-sandbox/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import srvx from "vite-plugin-srvx"; - -export default defineConfig({ - plugins: [react(), ...srvx({ entry: "src/server.ts" })], -}); diff --git a/examples/queue-sandbox-vercel/.gitignore b/examples/sandbox/.gitignore similarity index 64% rename from examples/queue-sandbox-vercel/.gitignore rename to examples/sandbox/.gitignore index e28f3d79db..dc6f607390 100644 --- a/examples/queue-sandbox-vercel/.gitignore +++ b/examples/sandbox/.gitignore @@ -1,4 +1,2 @@ .actorcore node_modules -dist -.vercel diff --git a/examples/sandbox/README.md b/examples/sandbox/README.md new file mode 100644 index 0000000000..202910d159 --- /dev/null +++ b/examples/sandbox/README.md @@ -0,0 +1,39 @@ +# Sandbox + +Unified sandbox showcasing Rivet Actor features with a single registry, grouped navigation, and interactive demos. + +## Getting Started + +```bash +cd examples/sandbox +pnpm install +pnpm 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/frontend/App.tsx b/examples/sandbox/frontend/App.tsx new file mode 100644 index 0000000000..4b358e18eb --- /dev/null +++ b/examples/sandbox/frontend/App.tsx @@ -0,0 +1,810 @@ +import { createRivetKit } from "@rivetkit/react"; +import { 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"; + +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()); +} + +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.docs.length > 0 && ( +
+ {activePage.docs.map((doc) => ( + + {doc.label} + + ))} +
+ )} +
+ + + +
+
+

Code Snippet

+

+ Use this snippet as a starting point for the actor + features in this page. +

+
+
{activePage.snippet}
+
+
+
+ ); +} + +function InteractivePanel({ page }: { page: PageConfig }) { + if (page.actors.length === 0) { + return ; + } + + if (page.demo === "raw-http") { + return ; + } + + if (page.demo === "raw-websocket") { + return ; + } + + 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. +

+
+
+
+ +