diff --git a/examples/ai-agent/src/frontend/App.tsx b/examples/ai-agent/src/frontend/App.tsx index 3633f38f22..f2ed2ef4e5 100644 --- a/examples/ai-agent/src/frontend/App.tsx +++ b/examples/ai-agent/src/frontend/App.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { registry } from "../backend/registry"; import type { Message } from "../backend/types"; -const { useActor } = createRivetKit("http://localhost:8080"); +const { useActor } = createRivetKit("http://localhost:6420"); export function App() { const aiAgent = useActor({ diff --git a/examples/better-auth-external-db/README.md b/examples/better-auth-external-db/README.md index 9e8aff5b92..72f96bb6b6 100644 --- a/examples/better-auth-external-db/README.md +++ b/examples/better-auth-external-db/README.md @@ -27,7 +27,7 @@ npm install npm run dev ``` -The database migrations will run automatically on startup. Open your browser to `http://localhost:5173` to see the frontend and the backend will be running on `http://localhost:8080`. +The database migrations will run automatically on startup. Open your browser to `http://localhost:5173` to see the frontend and the backend will be running on `http://localhost:6420`. ## Features diff --git a/examples/better-auth-external-db/src/frontend/auth-client.ts b/examples/better-auth-external-db/src/frontend/auth-client.ts index 64c8a7b5a5..dab0b8fd8d 100644 --- a/examples/better-auth-external-db/src/frontend/auth-client.ts +++ b/examples/better-auth-external-db/src/frontend/auth-client.ts @@ -1,5 +1,5 @@ import { createAuthClient } from "better-auth/react"; export const authClient = createAuthClient({ - baseURL: "http://localhost:8080", + baseURL: "http://localhost:6420", }); diff --git a/examples/better-auth-external-db/src/frontend/components/ChatRoom.tsx b/examples/better-auth-external-db/src/frontend/components/ChatRoom.tsx index bba4c8c398..83c3836887 100644 --- a/examples/better-auth-external-db/src/frontend/components/ChatRoom.tsx +++ b/examples/better-auth-external-db/src/frontend/components/ChatRoom.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import type { registry } from "../../backend/registry"; import { authClient } from "../auth-client"; -const { useActor } = createRivetKit("http://localhost:8080"); +const { useActor } = createRivetKit("http://localhost:6420"); interface ChatRoomProps { user: { id: string; email: string }; diff --git a/examples/counter-serverless/src/server.ts b/examples/counter-serverless/src/server.ts index 9dc17c0304..79472a223b 100644 --- a/examples/counter-serverless/src/server.ts +++ b/examples/counter-serverless/src/server.ts @@ -2,5 +2,5 @@ import { registry } from "./registry"; registry.start({ runnerKind: "serverless", - autoConfigureServerless: { url: "http://localhost:8080" }, + autoConfigureServerless: { url: "http://localhost:6420" }, }); diff --git a/examples/crdt/src/frontend/App.tsx b/examples/crdt/src/frontend/App.tsx index 26e4942a61..506202babe 100644 --- a/examples/crdt/src/frontend/App.tsx +++ b/examples/crdt/src/frontend/App.tsx @@ -4,7 +4,7 @@ import * as Y from "yjs"; import { applyUpdate, encodeStateAsUpdate } from "yjs"; import type { registry } from "../backend/registry"; -const { useActor } = createRivetKit("http://localhost:8080"); +const { useActor } = createRivetKit("http://localhost:6420"); function YjsEditor({ documentId }: { documentId: string }) { const yjsDocument = useActor({ diff --git a/examples/database/src/frontend/App.tsx b/examples/database/src/frontend/App.tsx index 754bc2673d..23930c26d6 100644 --- a/examples/database/src/frontend/App.tsx +++ b/examples/database/src/frontend/App.tsx @@ -2,7 +2,7 @@ import { createRivetKit } from "@rivetkit/react"; import { useEffect, useState } from "react"; import type { Note, registry } from "../backend/registry"; -const { useActor } = createRivetKit("http://localhost:8080"); +const { useActor } = createRivetKit("http://localhost:6420"); function NotesApp({ userId }: { userId: string }) { const [notes, setNotes] = useState([]); diff --git a/examples/deno/scripts/connect.ts b/examples/deno/scripts/connect.ts index ca63be72e3..f6e6f3f5d6 100644 --- a/examples/deno/scripts/connect.ts +++ b/examples/deno/scripts/connect.ts @@ -2,7 +2,7 @@ import { createClient } from "rivetkit/client"; import type { Registry } from "../src/registry.ts"; async function main() { - const client = createClient("http://localhost:8080"); + const client = createClient("http://localhost:6420"); const counter = client.counter.getOrCreate().connect(); diff --git a/examples/deno/src/server.ts b/examples/deno/src/server.ts index 40f8a5440c..b4c18e457c 100644 --- a/examples/deno/src/server.ts +++ b/examples/deno/src/server.ts @@ -4,7 +4,7 @@ import { registry } from "./registry.ts"; const { fetch } = registry.start({ // Deno requires using Deno.serve disableDefaultServer: true, - overrideServerAddress: "http://localhost:8080", + overrideServerAddress: "http://localhost:6420", // Specify Deno-specific upgradeWebSocket getUpgradeWebSocket: () => upgradeWebSocket, }); diff --git a/examples/elysia/src/server.ts b/examples/elysia/src/server.ts index a8a48b60d8..17c57dc6e3 100644 --- a/examples/elysia/src/server.ts +++ b/examples/elysia/src/server.ts @@ -16,4 +16,4 @@ new Elysia() }) .listen(8080); -console.log("Listening at http://localhost:8080"); +console.log("Listening at http://localhost:6420"); diff --git a/examples/express/src/server.ts b/examples/express/src/server.ts index a53eccbefb..3ad4600907 100644 --- a/examples/express/src/server.ts +++ b/examples/express/src/server.ts @@ -18,7 +18,7 @@ app.post("/increment/:name", async (req, res) => { }); app.listen(8080, () => { - console.log("Listening at http://localhost:8080"); + console.log("Listening at http://localhost:6420"); }); export default app; diff --git a/examples/freestyle/src/backend/server.ts b/examples/freestyle/src/backend/server.ts index 1d69ebef6f..ef7804eda0 100644 --- a/examples/freestyle/src/backend/server.ts +++ b/examples/freestyle/src/backend/server.ts @@ -9,7 +9,7 @@ const serverOutput = registry.start({ disableDefaultServer: true, basePath: "/api", getUpgradeWebSocket: () => upgradeWebSocket, - overrideServerAddress: `${process.env.FREESTYLE_ENDPOINT ?? "http://localhost:8080"}/api`, + overrideServerAddress: `${process.env.FREESTYLE_ENDPOINT ?? "http://localhost:6420"}/api`, cors: { origin: process.env.FREESTYLE_ENDPOINT ?? "http://localhost:5173", credentials: true, diff --git a/examples/freestyle/src/frontend/App.tsx b/examples/freestyle/src/frontend/App.tsx index f8d8e17b6a..354ebb8116 100644 --- a/examples/freestyle/src/frontend/App.tsx +++ b/examples/freestyle/src/frontend/App.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import type { Message, registry } from "../backend/registry"; const { useActor } = createRivetKit({ - endpoint: import.meta.env.VITE_RIVET_ENDPOINT ?? "http://localhost:8080/api", + endpoint: import.meta.env.VITE_RIVET_ENDPOINT ?? "http://localhost:6420/api", namespace: import.meta.env.VITE_RIVET_NAMESPACE, runnerName: import.meta.env.VITE_RIVET_RUNNER_NAME ?? "freestyle-runner", }); diff --git a/examples/game/src/frontend/App.tsx b/examples/game/src/frontend/App.tsx index 8eff9abad6..6a080c8920 100644 --- a/examples/game/src/frontend/App.tsx +++ b/examples/game/src/frontend/App.tsx @@ -2,7 +2,7 @@ import { createRivetKit } from "@rivetkit/react"; import { useEffect, useRef, useState } from "react"; import type { Player, registry } from "../backend/registry"; -const { useActor } = createRivetKit("http://localhost:8080"); +const { useActor } = createRivetKit("http://localhost:6420"); export function App() { const [players, setPlayers] = useState([]); diff --git a/examples/hono-bun/README.md b/examples/hono-bun/README.md index 9638b7bde1..0616b05911 100644 --- a/examples/hono-bun/README.md +++ b/examples/hono-bun/README.md @@ -33,7 +33,7 @@ Open your browser to [http://localhost:5173](http://localhost:5173) to see the c You can also test the server directly by running: ```sh -curl -X POST http://localhost:8080/increment/test +curl -X POST http://localhost:6420/increment/test ``` ## License diff --git a/examples/hono-bun/scripts/connect.ts b/examples/hono-bun/scripts/connect.ts index 8ce6b7516f..b272e72002 100644 --- a/examples/hono-bun/scripts/connect.ts +++ b/examples/hono-bun/scripts/connect.ts @@ -2,7 +2,7 @@ import { createClient } from "rivetkit/client"; import type { Registry } from "../src/registry"; async function main() { - const client = createClient("http://localhost:8080/rivet"); + const client = createClient("http://localhost:6420/rivet"); const counter = client.counter.getOrCreate().connect(); diff --git a/examples/hono-bun/src/backend/server.ts b/examples/hono-bun/src/backend/server.ts index b2d846ba43..068be1b634 100644 --- a/examples/hono-bun/src/backend/server.ts +++ b/examples/hono-bun/src/backend/server.ts @@ -8,7 +8,7 @@ const { client, fetch } = registry.start({ // Hono requires using Hono.serve disableDefaultServer: true, // Override endpoint - overrideServerAddress: "http://localhost:8080/rivet", + overrideServerAddress: "http://localhost:6420/rivet", // Specify Hono-specific upgradeWebSocket getUpgradeWebSocket: () => upgradeWebSocket, cors: { @@ -48,4 +48,4 @@ Bun.serve({ websocket, }); -console.log("Listening at http://localhost:8080"); +console.log("Listening at http://localhost:6420"); diff --git a/examples/hono-bun/src/frontend/App.tsx b/examples/hono-bun/src/frontend/App.tsx index d53ca6a0e3..81d8b548cc 100644 --- a/examples/hono-bun/src/frontend/App.tsx +++ b/examples/hono-bun/src/frontend/App.tsx @@ -2,7 +2,7 @@ import { createRivetKit } from "@rivetkit/react"; import { useState } from "react"; import type { registry } from "../backend/registry"; -const { useActor } = createRivetKit("http://localhost:8080/rivet"); +const { useActor } = createRivetKit("http://localhost:6420/rivet"); function App() { const [count, setCount] = useState(0); diff --git a/examples/hono-react/src/frontend/App.tsx b/examples/hono-react/src/frontend/App.tsx index 0fbc28b278..73494dbfca 100644 --- a/examples/hono-react/src/frontend/App.tsx +++ b/examples/hono-react/src/frontend/App.tsx @@ -2,7 +2,7 @@ import { createRivetKit } from "@rivetkit/react"; import { useState } from "react"; import type { registry } from "../backend/registry"; -const { useActor } = createRivetKit("http://localhost:8080"); +const { useActor } = createRivetKit("http://localhost:6420"); function App() { const [count, setCount] = useState(0); diff --git a/examples/hono/scripts/client.ts b/examples/hono/scripts/client.ts index 1c194f5f92..b040ab6591 100644 --- a/examples/hono/scripts/client.ts +++ b/examples/hono/scripts/client.ts @@ -1,5 +1,5 @@ async function main() { - const endpoint = process.env.RIVETKIT_ENDPOINT || "http://localhost:8080"; + const endpoint = process.env.RIVETKIT_ENDPOINT || "http://localhost:6420"; const res = await fetch(`${endpoint}/increment/foo`, { method: "POST", }); diff --git a/examples/kitchen-sink/src/frontend/components/tabs/RawHttpTab.tsx b/examples/kitchen-sink/src/frontend/components/tabs/RawHttpTab.tsx index cd77f9706d..e692cce8bf 100644 --- a/examples/kitchen-sink/src/frontend/components/tabs/RawHttpTab.tsx +++ b/examples/kitchen-sink/src/frontend/components/tabs/RawHttpTab.tsx @@ -25,7 +25,7 @@ export default function RawHttpTab({ state }: TabProps) { }; const getActorUrl = () => { - const baseUrl = "http://localhost:8080"; + const baseUrl = "http://localhost:6420"; const actorPath = state.actorKey ? `/actors/${state.actorName}/${encodeURIComponent(state.actorKey)}` : `/actors/${state.actorName}`; diff --git a/examples/kitchen-sink/src/frontend/components/tabs/RawWebSocketTab.tsx b/examples/kitchen-sink/src/frontend/components/tabs/RawWebSocketTab.tsx index fb7a7bdce6..aca7ea8099 100644 --- a/examples/kitchen-sink/src/frontend/components/tabs/RawWebSocketTab.tsx +++ b/examples/kitchen-sink/src/frontend/components/tabs/RawWebSocketTab.tsx @@ -32,7 +32,7 @@ export default function RawWebSocketTab({ state }: TabProps) { const actorPath = state.actorKey ? `/actors/${state.actorName}/${encodeURIComponent(state.actorKey)}/ws` : `/actors/${state.actorName}/ws`; - return `${wsProtocol}//localhost:8080${actorPath}`; + return `${wsProtocol}//localhost:6420${actorPath}`; }; const addMessage = (type: "sent" | "received", data: string, isBinary = false) => { diff --git a/examples/rate/src/frontend/App.tsx b/examples/rate/src/frontend/App.tsx index d31a76e357..33ced1c7fa 100644 --- a/examples/rate/src/frontend/App.tsx +++ b/examples/rate/src/frontend/App.tsx @@ -2,7 +2,7 @@ import { createRivetKit } from "@rivetkit/react"; import { useEffect, useState } from "react"; import type { RateLimitResult, registry } from "../backend/registry"; -const { useActor } = createRivetKit("http://localhost:8080"); +const { useActor } = createRivetKit("http://localhost:6420"); function RateLimiterDemo({ userId }: { userId: string }) { const [result, setResult] = useState(null); diff --git a/examples/raw-fetch-handler/src/frontend/App.tsx b/examples/raw-fetch-handler/src/frontend/App.tsx index fef7c1bcd4..0563c225d3 100644 --- a/examples/raw-fetch-handler/src/frontend/App.tsx +++ b/examples/raw-fetch-handler/src/frontend/App.tsx @@ -3,7 +3,7 @@ import { createClient } from "@rivetkit/react"; import type { registry } from "../backend/registry"; // Create a client that connects to the running server -const client = createClient("http://localhost:8080"); +const client = createClient("http://localhost:6420"); function Counter({ name }: { name: string }) { const [count, setCount] = useState(null); @@ -33,7 +33,7 @@ function Counter({ name }: { name: string }) { setLoading(true); try { // Method 2: Using the forward endpoint - const response = await fetch(`http://localhost:8080/forward/${name}/increment`, { + const response = await fetch(`http://localhost:6420/forward/${name}/increment`, { method: "POST", }); const data = await response.json(); diff --git a/examples/raw-websocket-handler-proxy/src/frontend/App.tsx b/examples/raw-websocket-handler-proxy/src/frontend/App.tsx index ba25e4d295..b57474f840 100644 --- a/examples/raw-websocket-handler-proxy/src/frontend/App.tsx +++ b/examples/raw-websocket-handler-proxy/src/frontend/App.tsx @@ -16,7 +16,7 @@ export default function App() { `conn_params.${encodeURIComponent(JSON.stringify({ apiKey: "your-api-key" }))}` ]; - const ws = new WebSocket("ws://localhost:8080/registry/actors/chatRoom/ws/", protocols); + const ws = new WebSocket("ws://localhost:6420/registry/actors/chatRoom/ws/", protocols); ws.onopen = () => { setIsConnected(true); diff --git a/examples/raw-websocket-handler/src/frontend/App.tsx b/examples/raw-websocket-handler/src/frontend/App.tsx index 167bef54ef..dbe7c5edc5 100644 --- a/examples/raw-websocket-handler/src/frontend/App.tsx +++ b/examples/raw-websocket-handler/src/frontend/App.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react"; import { createRivetKit } from "@rivetkit/react"; import type { registry } from "../backend/registry"; -const { useActor } = createRivetKit("http://localhost:8080"); +const { useActor } = createRivetKit("http://localhost:6420"); export default function App() { const [messages, setMessages] = useState>([]); diff --git a/examples/react/src/frontend/App.tsx b/examples/react/src/frontend/App.tsx index 0fbc28b278..73494dbfca 100644 --- a/examples/react/src/frontend/App.tsx +++ b/examples/react/src/frontend/App.tsx @@ -2,7 +2,7 @@ import { createRivetKit } from "@rivetkit/react"; import { useState } from "react"; import type { registry } from "../backend/registry"; -const { useActor } = createRivetKit("http://localhost:8080"); +const { useActor } = createRivetKit("http://localhost:6420"); function App() { const [count, setCount] = useState(0); diff --git a/examples/starter/README.md b/examples/starter/README.md index 2bd2133c3c..c0c779619b 100644 --- a/examples/starter/README.md +++ b/examples/starter/README.md @@ -39,7 +39,7 @@ export RIVET_ENVIRONMENT=your_environment npm run dev ``` -This will start the RivetKit server locally at http://localhost:8080. +This will start the RivetKit server locally at http://localhost:6420. ### Testing the Client diff --git a/examples/starter/scripts/client.ts b/examples/starter/scripts/client.ts index 4c9b58a275..8de7c4f7e5 100644 --- a/examples/starter/scripts/client.ts +++ b/examples/starter/scripts/client.ts @@ -2,7 +2,7 @@ import { createClient } from "rivetkit/client"; import type { registry } from "../src/registry.js"; // Get endpoint from environment variable or default to localhost -const endpoint = process.env.RIVETKIT_ENDPOINT ?? "http://localhost:8080"; +const endpoint = process.env.RIVETKIT_ENDPOINT ?? "http://localhost:6420"; console.log("🔗 Using endpoint:", endpoint); // Create RivetKit client diff --git a/examples/stream/src/frontend/App.tsx b/examples/stream/src/frontend/App.tsx index 7dc73e6ea6..3a3f6a98d2 100644 --- a/examples/stream/src/frontend/App.tsx +++ b/examples/stream/src/frontend/App.tsx @@ -2,7 +2,7 @@ import { createRivetKit } from "@rivetkit/react"; import { useEffect, useState } from "react"; import type { registry } from "../backend/registry"; -const { useActor } = createRivetKit("http://localhost:8080"); +const { useActor } = createRivetKit("http://localhost:6420"); export function App() { const [topValues, setTopValues] = useState([]); diff --git a/examples/sync/src/frontend/App.tsx b/examples/sync/src/frontend/App.tsx index da9fa829f8..b1405b4dbc 100644 --- a/examples/sync/src/frontend/App.tsx +++ b/examples/sync/src/frontend/App.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react"; import type { Contact } from "../backend/types"; import type { registry } from "../backend/registry"; -const { useActor } = createRivetKit("http://localhost:8080"); +const { useActor } = createRivetKit("http://localhost:6420"); export function App() { const [contacts, setContacts] = useState([]); diff --git a/examples/tenant/src/frontend/App.tsx b/examples/tenant/src/frontend/App.tsx index 0e7c2c1f56..be3da2f40a 100644 --- a/examples/tenant/src/frontend/App.tsx +++ b/examples/tenant/src/frontend/App.tsx @@ -2,7 +2,7 @@ import { createRivetKit } from "@rivetkit/react"; import { useEffect, useState } from "react"; import type { Member, registry } from "../backend/registry"; -const { useActor } = createRivetKit("http://localhost:8080"); +const { useActor } = createRivetKit("http://localhost:6420"); const ORG_ID = "org-1"; diff --git a/frontend/src/app/changelog.tsx b/frontend/src/app/changelog.tsx index bdb61547ff..fd2119222c 100644 --- a/frontend/src/app/changelog.tsx +++ b/frontend/src/app/changelog.tsx @@ -1,5 +1,5 @@ import { faSparkle, Icon } from "@rivet-gg/icons"; -import { useSuspenseQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { useLocalStorage } from "usehooks-ts"; import { Avatar, @@ -113,13 +113,27 @@ interface ChangelogProps { } export function Changelog({ className, children, ...props }: ChangelogProps) { - const { data } = useSuspenseQuery(changelogQueryOptions()); + const { data, isLoading, isError } = useQuery(changelogQueryOptions()); const [lastChangelog, setLast] = useLocalStorage( "rivet-lastchangelog", null, ); + if (isLoading || isError || !data || data.length === 0) { + return ( + + {children} + + ); + } + const hasNewChangelog = !lastChangelog ? data.length > 0 : data.some( diff --git a/frontend/src/components/actors/actor-status-label.tsx b/frontend/src/components/actors/actor-status-label.tsx index 0182cad7ca..bb1f27fb9b 100644 --- a/frontend/src/components/actors/actor-status-label.tsx +++ b/frontend/src/components/actors/actor-status-label.tsx @@ -13,7 +13,11 @@ export const ACTOR_STATUS_LABEL_MAP = { } satisfies Record; export const ActorStatusLabel = ({ status }: { status?: ActorStatus }) => { - return {status ? ACTOR_STATUS_LABEL_MAP[status] : "Unknown"}; + return ( + + {status ? ACTOR_STATUS_LABEL_MAP[status] : "Unknown"} + + ); }; export const QueriedActorStatusLabel = ({ actorId }: { actorId: ActorId }) => { diff --git a/frontend/src/components/actors/actors-actor-details.tsx b/frontend/src/components/actors/actors-actor-details.tsx index 4b782e3dd5..a862c43277 100644 --- a/frontend/src/components/actors/actors-actor-details.tsx +++ b/frontend/src/components/actors/actors-actor-details.tsx @@ -4,6 +4,7 @@ import { memo, type ReactNode, Suspense } from "react"; import { cn, Flex, + ScrollArea, Tabs, TabsContent, TabsList, @@ -52,7 +53,7 @@ export const ActorsActorDetails = memo( return ( -
+
-
- - {supportsState ? ( - div]:h-full", + }} + > +
+ + {supportsState ? ( + + State + + ) : null} + {supportsConnections ? ( + + Connections + + ) : null} + {supportsEvents ? ( + + Events + + ) : null} + {supportsDatabase ? ( + + Database + + ) : null} + {supportsLogs ? ( + + Logs + + ) : null} + {supportsMetadata ? ( + + Metadata + + ) : null} + {supportsMetrics ? ( + + Metrics + + ) : null} + + {actorId ? ( + - State - +
+ + + ) : null} - {supportsConnections ? ( - - Connections - - ) : null} - {supportsEvents ? ( - - Events - - ) : null} - {supportsDatabase ? ( - - Database - - ) : null} - {supportsLogs ? ( - - Logs - - ) : null} - {supportsMetadata ? ( - - Metadata - - ) : null} - {supportsMetrics ? ( - - Metrics - - ) : null} - - {actorId ? ( - - - - - ) : null} -
+
+
{actorId ? ( <> diff --git a/frontend/src/components/tailwind-base.ts b/frontend/src/components/tailwind-base.ts index a11d21957c..dfd80ba584 100644 --- a/frontend/src/components/tailwind-base.ts +++ b/frontend/src/components/tailwind-base.ts @@ -183,6 +183,12 @@ const config = { ".justify-safe-center": { "justify-content": "safe center", }, + ".items-safe-start": { + "align-items": "safe start", + }, + ".items-safe-end": { + "align-items": "safe end", + }, }); }), ], diff --git a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/connect.tsx b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/connect.tsx index 8959b70b12..21eac7b39c 100644 --- a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/connect.tsx +++ b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/connect.tsx @@ -116,7 +116,7 @@ export function RouteComponent() {
-
+

Create New Project

diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts index 230f60c616..bf133ab6aa 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts @@ -107,6 +107,12 @@ export class EngineActorDriver implements ActorDriver { metadata: { inspectorToken: this.#runConfig.inspector.token(), }, + getActorHibernationConfig(actorId, requestId) { + return { + enabled: false, + lastMsgIndex: 0, + }; + }, prepopulateActorNames: buildActorNames(registryConfig), onConnected: () => { if (hasDisconnected) { diff --git a/website/src/data/examples/examples.ts b/website/src/data/examples/examples.ts index 9de4d2931e..9beba0d926 100644 --- a/website/src/data/examples/examples.ts +++ b/website/src/data/examples/examples.ts @@ -30,7 +30,7 @@ export const examples: ExampleData[] = [ "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"lib\": [\"esnext\", \"dom\"],\n \"jsx\": \"react-jsx\",\n \"module\": \"esnext\",\n \"moduleResolution\": \"bundler\",\n \"types\": [\"node\", \"vite/client\"],\n \"resolveJsonModule\": true,\n \"allowJs\": true,\n \"checkJs\": false,\n \"noEmit\": true,\n \"isolatedModules\": true,\n \"allowSyntheticDefaultImports\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"strict\": true,\n \"skipLibCheck\": true\n },\n \"include\": [\"src/**/*\"],\n \"exclude\": [\"node_modules\", \"dist\"]\n}\n", "vite.config.ts": "import react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n\tplugins: [react()],\n\troot: \"src/frontend\",\n\tserver: {\n\t\tport: 3000,\n\t},\n});\n", "vitest.config.ts": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n\ttest: {\n\t\tinclude: [\"tests/**/*.test.ts\"],\n\t\ttestTimeout: 30000,\n\t},\n});\n", - "src/frontend/App.tsx": "import { createRivetKit } from \"@rivetkit/react\";\nimport { useEffect, useState } from \"react\";\nimport { registry } from \"../backend/registry\";\nimport type { Message } from \"../backend/types\";\n\nconst { useActor } = createRivetKit(\"http://localhost:8080\");\n\nexport function App() {\n\tconst aiAgent = useActor({\n\t\tname: \"aiAgent\",\n\t\tkey: [\"default\"],\n\t});\n\tconst [messages, setMessages] = useState([]);\n\tconst [input, setInput] = useState(\"\");\n\tconst [isLoading, setIsLoading] = useState(false);\n\n\tuseEffect(() => {\n\t\tif (aiAgent.connection) {\n\t\t\taiAgent.connection.getMessages().then(setMessages);\n\t\t}\n\t}, [aiAgent.connection]);\n\n\taiAgent.useEvent(\"messageReceived\", (message: Message) => {\n\t\tsetMessages((prev) => [...prev, message]);\n\t\tsetIsLoading(false);\n\t});\n\n\tconst handleSendMessage = async () => {\n\t\tif (aiAgent.connection && input.trim()) {\n\t\t\tsetIsLoading(true);\n\n\t\t\tconst userMessage = { role: \"user\", content: input, timestamp: Date.now() } as Message;\n\t\t\tsetMessages((prev) => [...prev, userMessage]);\n\n\t\t\tawait aiAgent.connection.sendMessage(input);\n\t\t\tsetInput(\"\");\n\t\t}\n\t};\n\n\treturn (\n\t\t

\n\t\t\t
\n\t\t\t\t{messages.length === 0 ? (\n\t\t\t\t\t
\n\t\t\t\t\t\tAsk the AI assistant a question to get started\n\t\t\t\t\t
\n\t\t\t\t) : (\n\t\t\t\t\tmessages.map((msg, i) => (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
{msg.role === \"user\" ? \"👤\" : \"🤖\"}
\n\t\t\t\t\t\t\t
{msg.content}
\n\t\t\t\t\t\t
\n\t\t\t\t\t))\n\t\t\t\t)}\n\t\t\t\t{isLoading && (\n\t\t\t\t\t
\n\t\t\t\t\t\t
🤖
\n\t\t\t\t\t\t
Thinking...
\n\t\t\t\t\t
\n\t\t\t\t)}\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t setInput(e.target.value)}\n\t\t\t\t\tonKeyPress={(e) => e.key === \"Enter\" && handleSendMessage()}\n\t\t\t\t\tplaceholder=\"Ask the AI assistant...\"\n\t\t\t\t\tdisabled={isLoading}\n\t\t\t\t/>\n\t\t\t\t\n\t\t\t\t\tSend\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t);\n}\n", + "src/frontend/App.tsx": "import { createRivetKit } from \"@rivetkit/react\";\nimport { useEffect, useState } from \"react\";\nimport { registry } from \"../backend/registry\";\nimport type { Message } from \"../backend/types\";\n\nconst { useActor } = createRivetKit(\"http://localhost:6420\");\n\nexport function App() {\n\tconst aiAgent = useActor({\n\t\tname: \"aiAgent\",\n\t\tkey: [\"default\"],\n\t});\n\tconst [messages, setMessages] = useState([]);\n\tconst [input, setInput] = useState(\"\");\n\tconst [isLoading, setIsLoading] = useState(false);\n\n\tuseEffect(() => {\n\t\tif (aiAgent.connection) {\n\t\t\taiAgent.connection.getMessages().then(setMessages);\n\t\t}\n\t}, [aiAgent.connection]);\n\n\taiAgent.useEvent(\"messageReceived\", (message: Message) => {\n\t\tsetMessages((prev) => [...prev, message]);\n\t\tsetIsLoading(false);\n\t});\n\n\tconst handleSendMessage = async () => {\n\t\tif (aiAgent.connection && input.trim()) {\n\t\t\tsetIsLoading(true);\n\n\t\t\tconst userMessage = { role: \"user\", content: input, timestamp: Date.now() } as Message;\n\t\t\tsetMessages((prev) => [...prev, userMessage]);\n\n\t\t\tawait aiAgent.connection.sendMessage(input);\n\t\t\tsetInput(\"\");\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t{messages.length === 0 ? (\n\t\t\t\t\t
\n\t\t\t\t\t\tAsk the AI assistant a question to get started\n\t\t\t\t\t
\n\t\t\t\t) : (\n\t\t\t\t\tmessages.map((msg, i) => (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
{msg.role === \"user\" ? \"👤\" : \"🤖\"}
\n\t\t\t\t\t\t\t
{msg.content}
\n\t\t\t\t\t\t
\n\t\t\t\t\t))\n\t\t\t\t)}\n\t\t\t\t{isLoading && (\n\t\t\t\t\t
\n\t\t\t\t\t\t
🤖
\n\t\t\t\t\t\t
Thinking...
\n\t\t\t\t\t
\n\t\t\t\t)}\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t setInput(e.target.value)}\n\t\t\t\t\tonKeyPress={(e) => e.key === \"Enter\" && handleSendMessage()}\n\t\t\t\t\tplaceholder=\"Ask the AI assistant...\"\n\t\t\t\t\tdisabled={isLoading}\n\t\t\t\t/>\n\t\t\t\t\n\t\t\t\t\tSend\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t);\n}\n", "src/frontend/main.tsx": "import { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { App } from \"./App\";\n\nconst root = document.getElementById(\"root\");\nif (!root) throw new Error(\"Root element not found\");\n\ncreateRoot(root).render(\n\t\n\t\t\n\t\n);", "src/frontend/index.html": "\n\n\n \n \n AI Agent Example\n \n\n\n
\n \n\n", "src/backend/my-tools.ts": "export async function getWeather(location: string) {\n\t// Mock weather API response\n\treturn {\n\t\tlocation,\n\t\ttemperature: Math.floor(Math.random() * 30) + 10,\n\t\tcondition: [\"sunny\", \"cloudy\", \"rainy\", \"snowy\"][\n\t\t\tMath.floor(Math.random() * 4)\n\t\t],\n\t\thumidity: Math.floor(Math.random() * 50) + 30,\n\t};\n}\n", @@ -56,7 +56,7 @@ export const examples: ExampleData[] = [ "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"lib\": [\"esnext\", \"dom\"],\n \"jsx\": \"react-jsx\",\n \"module\": \"esnext\",\n \"moduleResolution\": \"bundler\",\n \"types\": [\"node\", \"vite/client\"],\n \"resolveJsonModule\": true,\n \"allowJs\": true,\n \"checkJs\": false,\n \"noEmit\": true,\n \"isolatedModules\": true,\n \"allowSyntheticDefaultImports\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"strict\": true,\n \"skipLibCheck\": true\n },\n \"include\": [\"src/**/*\"],\n \"exclude\": [\"node_modules\", \"dist\"]\n}\n", "vite.config.ts": "import react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n\tplugins: [react()],\n\troot: \"src/frontend\",\n\tserver: {\n\t\tport: 3000,\n\t},\n});\n", "vitest.config.ts": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n\ttest: {\n\t\tinclude: [\"tests/**/*.test.ts\"],\n\t},\n});\n", - "src/frontend/App.tsx": "import { createRivetKit } from \"@rivetkit/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport * as Y from \"yjs\";\nimport { applyUpdate, encodeStateAsUpdate } from \"yjs\";\nimport type { registry } from \"../backend/registry\";\n\nconst { useActor } = createRivetKit(\"http://localhost:8080\");\n\nfunction YjsEditor({ documentId }: { documentId: string }) {\n\tconst yjsDocument = useActor({\n\t\tname: \"yjsDocument\",\n\t\tkey: [documentId],\n\t});\n\n\tconst [isLoading, setIsLoading] = useState(true);\n\tconst [text, setText] = useState(\"\");\n\n\tconst yDocRef = useRef(null);\n\tconst updatingFromServer = useRef(false);\n\tconst updatingFromLocal = useRef(false);\n\tconst observationInitialized = useRef(false);\n\n\tuseEffect(() => {\n\t\tconst yDoc = new Y.Doc();\n\t\tyDocRef.current = yDoc;\n\t\tsetIsLoading(false);\n\n\t\treturn () => {\n\t\t\tyDoc.destroy();\n\t\t};\n\t}, [yjsDocument.connection]);\n\n\tuseEffect(() => {\n\t\tconst yDoc = yDocRef.current;\n\t\tif (!yDoc || observationInitialized.current) return;\n\n\t\tconst yText = yDoc.getText(\"content\");\n\n\t\tyText.observe(() => {\n\t\t\tif (!updatingFromServer.current) {\n\t\t\t\tsetText(yText.toString());\n\n\t\t\t\tif (yjsDocument.connection && !updatingFromLocal.current) {\n\t\t\t\t\tupdatingFromLocal.current = true;\n\n\t\t\t\t\tconst update = encodeStateAsUpdate(yDoc);\n\t\t\t\t\tyjsDocument.connection.applyUpdate(update).finally(() => {\n\t\t\t\t\t\tupdatingFromLocal.current = false;\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tobservationInitialized.current = true;\n\t}, [yjsDocument.connection]);\n\n\tyjsDocument.useEvent(\"initialState\", ({ update }: { update: Uint8Array }) => {\n\t\tconst yDoc = yDocRef.current;\n\t\tif (!yDoc) return;\n\n\t\tupdatingFromServer.current = true;\n\n\t\ttry {\n\t\t\tapplyUpdate(yDoc, update);\n\n\t\t\tconst yText = yDoc.getText(\"content\");\n\t\t\tsetText(yText.toString());\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Error applying initial update:\", error);\n\t\t} finally {\n\t\t\tupdatingFromServer.current = false;\n\t\t}\n\t});\n\n\tyjsDocument.useEvent(\"update\", ({ update }: { update: Uint8Array }) => {\n\t\tconst yDoc = yDocRef.current;\n\t\tif (!yDoc) return;\n\n\t\tupdatingFromServer.current = true;\n\n\t\ttry {\n\t\t\tapplyUpdate(yDoc, update);\n\n\t\t\tconst yText = yDoc.getText(\"content\");\n\t\t\tsetText(yText.toString());\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Error applying update:\", error);\n\t\t} finally {\n\t\t\tupdatingFromServer.current = false;\n\t\t}\n\t});\n\n\tconst handleTextChange = (e: React.ChangeEvent) => {\n\t\tif (!yDocRef.current) return;\n\n\t\tconst newText = e.target.value;\n\t\tconst yText = yDocRef.current.getText(\"content\");\n\n\t\tif (newText !== yText.toString()) {\n\t\t\tupdatingFromLocal.current = true;\n\n\t\t\tyDocRef.current.transact(() => {\n\t\t\t\tyText.delete(0, yText.length);\n\t\t\t\tyText.insert(0, newText);\n\t\t\t});\n\n\t\t\tupdatingFromLocal.current = false;\n\t\t}\n\t};\n\n\tif (isLoading) {\n\t\treturn
Loading collaborative document...
;\n\t}\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t

Document: {documentId}

\n\t\t\t\t
\n\t\t\t\t\t{yjsDocument.connection ? 'Connected' : 'Disconnected'}\n\t\t\t\t
\n\t\t\t
\n\t\t\t\n\t\t
\n\t);\n}\n\nexport function App() {\n\tconst [documentId, setDocumentId] = useState(\"shared-doc\");\n\tconst [inputDocId, setInputDocId] = useState(\"shared-doc\");\n\n\tconst switchDocument = () => {\n\t\tsetDocumentId(inputDocId);\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t

CRDT Collaborative Editor

\n\t\t\t\t

Real-time collaborative text editing powered by Yjs and RivetKit

\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

How it works

\n\t\t\t\t

\n\t\t\t\t\tThis editor uses Conflict-free Replicated Data Types (CRDTs) with Yjs to enable\n\t\t\t\t\treal-time collaborative editing. Open multiple browser tabs or share the URL\n\t\t\t\t\twith others to see live collaboration in action!\n\t\t\t\t

\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t setInputDocId(e.target.value)}\n\t\t\t\t\tplaceholder=\"Enter document ID\"\n\t\t\t\t/>\n\t\t\t\t\n\t\t\t
\n\n\t\t\t\n\t\t
\n\t);\n}\n", + "src/frontend/App.tsx": "import { createRivetKit } from \"@rivetkit/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport * as Y from \"yjs\";\nimport { applyUpdate, encodeStateAsUpdate } from \"yjs\";\nimport type { registry } from \"../backend/registry\";\n\nconst { useActor } = createRivetKit(\"http://localhost:6420\");\n\nfunction YjsEditor({ documentId }: { documentId: string }) {\n\tconst yjsDocument = useActor({\n\t\tname: \"yjsDocument\",\n\t\tkey: [documentId],\n\t});\n\n\tconst [isLoading, setIsLoading] = useState(true);\n\tconst [text, setText] = useState(\"\");\n\n\tconst yDocRef = useRef(null);\n\tconst updatingFromServer = useRef(false);\n\tconst updatingFromLocal = useRef(false);\n\tconst observationInitialized = useRef(false);\n\n\tuseEffect(() => {\n\t\tconst yDoc = new Y.Doc();\n\t\tyDocRef.current = yDoc;\n\t\tsetIsLoading(false);\n\n\t\treturn () => {\n\t\t\tyDoc.destroy();\n\t\t};\n\t}, [yjsDocument.connection]);\n\n\tuseEffect(() => {\n\t\tconst yDoc = yDocRef.current;\n\t\tif (!yDoc || observationInitialized.current) return;\n\n\t\tconst yText = yDoc.getText(\"content\");\n\n\t\tyText.observe(() => {\n\t\t\tif (!updatingFromServer.current) {\n\t\t\t\tsetText(yText.toString());\n\n\t\t\t\tif (yjsDocument.connection && !updatingFromLocal.current) {\n\t\t\t\t\tupdatingFromLocal.current = true;\n\n\t\t\t\t\tconst update = encodeStateAsUpdate(yDoc);\n\t\t\t\t\tyjsDocument.connection.applyUpdate(update).finally(() => {\n\t\t\t\t\t\tupdatingFromLocal.current = false;\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tobservationInitialized.current = true;\n\t}, [yjsDocument.connection]);\n\n\tyjsDocument.useEvent(\"initialState\", ({ update }: { update: Uint8Array }) => {\n\t\tconst yDoc = yDocRef.current;\n\t\tif (!yDoc) return;\n\n\t\tupdatingFromServer.current = true;\n\n\t\ttry {\n\t\t\tapplyUpdate(yDoc, update);\n\n\t\t\tconst yText = yDoc.getText(\"content\");\n\t\t\tsetText(yText.toString());\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Error applying initial update:\", error);\n\t\t} finally {\n\t\t\tupdatingFromServer.current = false;\n\t\t}\n\t});\n\n\tyjsDocument.useEvent(\"update\", ({ update }: { update: Uint8Array }) => {\n\t\tconst yDoc = yDocRef.current;\n\t\tif (!yDoc) return;\n\n\t\tupdatingFromServer.current = true;\n\n\t\ttry {\n\t\t\tapplyUpdate(yDoc, update);\n\n\t\t\tconst yText = yDoc.getText(\"content\");\n\t\t\tsetText(yText.toString());\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Error applying update:\", error);\n\t\t} finally {\n\t\t\tupdatingFromServer.current = false;\n\t\t}\n\t});\n\n\tconst handleTextChange = (e: React.ChangeEvent) => {\n\t\tif (!yDocRef.current) return;\n\n\t\tconst newText = e.target.value;\n\t\tconst yText = yDocRef.current.getText(\"content\");\n\n\t\tif (newText !== yText.toString()) {\n\t\t\tupdatingFromLocal.current = true;\n\n\t\t\tyDocRef.current.transact(() => {\n\t\t\t\tyText.delete(0, yText.length);\n\t\t\t\tyText.insert(0, newText);\n\t\t\t});\n\n\t\t\tupdatingFromLocal.current = false;\n\t\t}\n\t};\n\n\tif (isLoading) {\n\t\treturn
Loading collaborative document...
;\n\t}\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t

Document: {documentId}

\n\t\t\t\t
\n\t\t\t\t\t{yjsDocument.connection ? 'Connected' : 'Disconnected'}\n\t\t\t\t
\n\t\t\t
\n\t\t\t\n\t\t
\n\t);\n}\n\nexport function App() {\n\tconst [documentId, setDocumentId] = useState(\"shared-doc\");\n\tconst [inputDocId, setInputDocId] = useState(\"shared-doc\");\n\n\tconst switchDocument = () => {\n\t\tsetDocumentId(inputDocId);\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t

CRDT Collaborative Editor

\n\t\t\t\t

Real-time collaborative text editing powered by Yjs and RivetKit

\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

How it works

\n\t\t\t\t

\n\t\t\t\t\tThis editor uses Conflict-free Replicated Data Types (CRDTs) with Yjs to enable\n\t\t\t\t\treal-time collaborative editing. Open multiple browser tabs or share the URL\n\t\t\t\t\twith others to see live collaboration in action!\n\t\t\t\t

\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t setInputDocId(e.target.value)}\n\t\t\t\t\tplaceholder=\"Enter document ID\"\n\t\t\t\t/>\n\t\t\t\t\n\t\t\t
\n\n\t\t\t\n\t\t
\n\t);\n}\n", "src/frontend/main.tsx": "import { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { App } from \"./App\";\n\nconst root = document.getElementById(\"root\");\nif (!root) throw new Error(\"Root element not found\");\n\ncreateRoot(root).render(\n\t\n\t\t\n\t\n);", "src/frontend/index.html": "\n\n\n \n \n CRDT Collaborative Editor\n \n\n\n
\n \n\n", "src/backend/registry.ts": "import { actor, setup } from \"rivetkit\";\nimport * as Y from \"yjs\";\nimport { applyUpdate, encodeStateAsUpdate } from \"yjs\";\n\nexport const yjsDocument = actor({\n\t// Persistent state that survives restarts.\n\tstate: {\n\t\tdocData: new Uint8Array(), // Raw Yjs document snapshot\n\t\tlastModified: 0,\n\t},\n\n\tcreateVars: () => ({\n\t\tdoc: new Y.Doc(),\n\t}),\n\n\tonStart: (c) => {\n\t\tif (c.state.docData.length > 0) {\n\t\t\tapplyUpdate(c.vars.doc, c.state.docData);\n\t\t}\n\t},\n\n\t// Handle client connections.\n\tonConnect: (c, conn) => {\n\t\tconst update = encodeStateAsUpdate(c.vars.doc);\n\t\tconn.send(\"initialState\", { update });\n\t},\n\n\tactions: {\n\t\t// Callable functions from clients.\n\t\tapplyUpdate: (c, update: Uint8Array) => {\n\t\t\tapplyUpdate(c.vars.doc, update);\n\n\t\t\tconst fullState = encodeStateAsUpdate(\n\t\t\t\tc.vars.doc,\n\t\t\t) as Uint8Array;\n\t\t\t// State changes are automatically persisted\n\t\t\tc.state.docData = fullState;\n\t\t\tc.state.lastModified = Date.now();\n\n\t\t\t// Send events to all connected clients.\n\t\t\tc.broadcast(\"update\", { update });\n\t\t},\n\n\t\tgetState: (c) => ({\n\t\t\tdocData: c.state.docData,\n\t\t\tlastModified: c.state.lastModified,\n\t\t}),\n\t},\n});\n\n// Register actors for use.\nexport const registry = setup({\n\tuse: { yjsDocument },\n});\n", @@ -100,7 +100,7 @@ export const examples: ExampleData[] = [ "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"ESNext\",\n \"skipLibCheck\": true,\n \"moduleResolution\": \"bundler\",\n \"allowImportingTsExtensions\": true,\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n \"strict\": true,\n \"noUnusedLocals\": true,\n \"noUnusedParameters\": true,\n \"noFallthroughCasesInSwitch\": true\n },\n \"include\": [\"src\", \"tests\"],\n \"exclude\": [\"node_modules\", \"dist\"]\n}\n", "vite.config.ts": "import react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n\tplugins: [react()],\n\troot: \"src/frontend\",\n\tserver: {\n\t\tport: 3000,\n\t},\n\tbuild: {\n\t\toutDir: \"../../dist\",\n\t\temptyOutDir: true,\n\t},\n});\n", "vitest.config.ts": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n\ttest: {\n\t\tenvironment: \"node\",\n\t},\n});\n", - "src/frontend/App.tsx": "import { createRivetKit } from \"@rivetkit/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { Contact } from \"../backend/types\";\nimport type { registry } from \"../backend/registry\";\n\nconst { useActor } = createRivetKit(\"http://localhost:8080\");\n\nexport function App() {\n\tconst [contacts, setContacts] = useState([]);\n\tconst [name, setName] = useState(\"\");\n\tconst [email, setEmail] = useState(\"\");\n\tconst [phone, setPhone] = useState(\"\");\n\tconst [syncStatus, setSyncStatus] = useState<\"Idle\" | \"Syncing\" | \"Synced\" | \"Offline\">(\"Idle\");\n\tconst [stats, setStats] = useState({ totalContacts: 0, lastSyncTime: 0, deletedContacts: 0 });\n\n\tconst lastSyncTime = useRef(0);\n\tconst syncIntervalRef = useRef(null);\n\n\tconst contactsActor = useActor({\n\t\tname: \"contacts\",\n\t\tkey: [\"global\"],\n\t});\n\n\t// Load initial contacts and stats\n\tuseEffect(() => {\n\t\tif (!contactsActor.connection) return;\n\n\t\tconst loadInitialData = async () => {\n\t\t\ttry {\n\t\t\t\tconst data = await contactsActor.connection!.getChanges(0);\n\t\t\t\tsetContacts(data.changes);\n\t\t\t\tlastSyncTime.current = data.timestamp;\n\t\t\t\tsetSyncStatus(\"Synced\");\n\n\t\t\t\tconst statsData = await contactsActor.connection!.getSyncStats();\n\t\t\t\tsetStats(statsData);\n\t\t\t} catch (error) {\n\t\t\t\tsetSyncStatus(\"Offline\");\n\t\t\t}\n\t\t};\n\n\t\tloadInitialData();\n\t}, [contactsActor.connection]);\n\n\t// Handle contact events from other clients\n\tcontactsActor.useEvent(\"contactsChanged\", ({ contacts: updatedContacts }: { contacts: Contact[] }) => {\n\t\tsetContacts((prev) => {\n\t\t\tconst contactMap = new Map(prev.map((c) => [c.id, c]));\n\n\t\t\tupdatedContacts.forEach((contact) => {\n\t\t\t\tconst existing = contactMap.get(contact.id);\n\t\t\t\tif (!existing || existing.updatedAt < contact.updatedAt) {\n\t\t\t\t\tcontactMap.set(contact.id, contact);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\treturn Array.from(contactMap.values()).filter(c => c.name !== \"\");\n\t\t});\n\n\t\t// Update stats when contacts change\n\t\tif (contactsActor.connection) {\n\t\t\tcontactsActor.connection.getSyncStats().then(setStats);\n\t\t}\n\t});\n\n\t// Periodic sync - every 5 seconds\n\tuseEffect(() => {\n\t\tif (!contactsActor.connection) return;\n\n\t\tconst sync = async () => {\n\t\t\tsetSyncStatus(\"Syncing\");\n\n\t\t\ttry {\n\t\t\t\t// Get remote changes\n\t\t\t\tconst changes = await contactsActor.connection!.getChanges(lastSyncTime.current);\n\n\t\t\t\t// Apply remote changes\n\t\t\t\tif (changes.changes.length > 0) {\n\t\t\t\t\tsetContacts((prev) => {\n\t\t\t\t\t\tconst contactMap = new Map(prev.map((c) => [c.id, c]));\n\n\t\t\t\t\t\tchanges.changes.forEach((contact) => {\n\t\t\t\t\t\t\tconst existing = contactMap.get(contact.id);\n\t\t\t\t\t\t\tif (!existing || existing.updatedAt < contact.updatedAt) {\n\t\t\t\t\t\t\t\tcontactMap.set(contact.id, contact);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\treturn Array.from(contactMap.values()).filter(c => c.name !== \"\");\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\t// Push local changes\n\t\t\t\tconst localChanges = contacts.filter(\n\t\t\t\t\t(c) => c.updatedAt > lastSyncTime.current,\n\t\t\t\t);\n\t\t\t\tif (localChanges.length > 0) {\n\t\t\t\t\tawait contactsActor.connection!.pushChanges(localChanges);\n\t\t\t\t}\n\n\t\t\t\tlastSyncTime.current = changes.timestamp;\n\t\t\t\tsetSyncStatus(\"Synced\");\n\n\t\t\t\t// Update stats\n\t\t\t\tconst statsData = await contactsActor.connection!.getSyncStats();\n\t\t\t\tsetStats(statsData);\n\t\t\t} catch (error) {\n\t\t\t\tsetSyncStatus(\"Offline\");\n\t\t\t}\n\t\t};\n\n\t\tsyncIntervalRef.current = setInterval(sync, 5000);\n\n\t\treturn () => {\n\t\t\tif (syncIntervalRef.current) {\n\t\t\t\tclearInterval(syncIntervalRef.current);\n\t\t\t\tsyncIntervalRef.current = null;\n\t\t\t}\n\t\t};\n\t}, [contactsActor.connection, contacts]);\n\n\t// Add new contact (local first)\n\tconst addContact = async () => {\n\t\tif (!name.trim()) return;\n\n\t\tconst newContact: Contact = {\n\t\t\tid: Date.now().toString(),\n\t\t\tname,\n\t\t\temail,\n\t\t\tphone,\n\t\t\tupdatedAt: Date.now(),\n\t\t};\n\n\t\t// Add locally first for immediate UI feedback\n\t\tsetContacts((prev) => [...prev, newContact]);\n\n\t\t// Then sync to server\n\t\tif (contactsActor.connection) {\n\t\t\ttry {\n\t\t\t\tawait contactsActor.connection.pushChanges([newContact]);\n\t\t\t\tconst statsData = await contactsActor.connection.getSyncStats();\n\t\t\t\tsetStats(statsData);\n\t\t\t} catch (error) {\n\t\t\t\tsetSyncStatus(\"Offline\");\n\t\t\t}\n\t\t}\n\n\t\tsetName(\"\");\n\t\tsetEmail(\"\");\n\t\tsetPhone(\"\");\n\t};\n\n\t// Delete contact (implemented as update with empty name)\n\tconst deleteContact = async (id: string) => {\n\t\tconst deletedContact = contacts.find(c => c.id === id);\n\t\tif (!deletedContact) return;\n\n\t\tconst updatedContact: Contact = {\n\t\t\t...deletedContact,\n\t\t\tname: \"\", // Mark as deleted\n\t\t\tupdatedAt: Date.now()\n\t\t};\n\n\t\t// Remove locally first for immediate UI feedback\n\t\tsetContacts((prev) => prev.filter((c) => c.id !== id));\n\n\t\t// Then sync to server\n\t\tif (contactsActor.connection) {\n\t\t\ttry {\n\t\t\t\tawait contactsActor.connection.pushChanges([updatedContact]);\n\t\t\t\tconst statsData = await contactsActor.connection.getSyncStats();\n\t\t\t\tsetStats(statsData);\n\t\t\t} catch (error) {\n\t\t\t\tsetSyncStatus(\"Offline\");\n\t\t\t}\n\t\t}\n\t};\n\n\t// Manual sync\n\tconst handleSync = async () => {\n\t\tif (!contactsActor.connection) return;\n\n\t\tsetSyncStatus(\"Syncing\");\n\n\t\ttry {\n\t\t\t// Push all contacts\n\t\t\tawait contactsActor.connection.pushChanges(contacts);\n\n\t\t\t// Get all changes\n\t\t\tconst changes = await contactsActor.connection.getChanges(0);\n\n\t\t\tsetContacts(changes.changes.filter(c => c.name !== \"\"));\n\t\t\tlastSyncTime.current = changes.timestamp;\n\t\t\tsetSyncStatus(\"Synced\");\n\n\t\t\t// Update stats\n\t\t\tconst statsData = await contactsActor.connection.getSyncStats();\n\t\t\tsetStats(statsData);\n\t\t} catch (error) {\n\t\t\tsetSyncStatus(\"Offline\");\n\t\t}\n\t};\n\n\t// Reset all data\n\tconst handleReset = async () => {\n\t\tif (!contactsActor.connection) return;\n\n\t\ttry {\n\t\t\tawait contactsActor.connection.reset();\n\t\t\tsetContacts([]);\n\t\t\tlastSyncTime.current = Date.now();\n\t\t\tsetSyncStatus(\"Synced\");\n\t\t\tsetStats({ totalContacts: 0, lastSyncTime: Date.now(), deletedContacts: 0 });\n\t\t} catch (error) {\n\t\t\tsetSyncStatus(\"Offline\");\n\t\t}\n\t};\n\n\t// Handle form submission\n\tconst handleSubmit = (e: React.FormEvent) => {\n\t\te.preventDefault();\n\t\taddContact();\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t

Sync Contacts

\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t{syncStatus}\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

How it works

\n\t\t\t\t

\n\t\t\t\t\tThis contact sync system demonstrates offline-first synchronization with conflict resolution. \n\t\t\t\t\tAdd contacts and they'll sync across all connected clients. The system handles conflicts using \n\t\t\t\t\t\"last write wins\" based on timestamps, and supports offline operation with automatic sync when reconnected.\n\t\t\t\t

\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

Add New Contact

\n\t\t\t\t
\n\t\t\t\t\t setName(e.target.value)}\n\t\t\t\t\t\trequired\n\t\t\t\t\t\tdisabled={!contactsActor.connection}\n\t\t\t\t\t/>\n\t\t\t\t\t setEmail(e.target.value)}\n\t\t\t\t\t\tdisabled={!contactsActor.connection}\n\t\t\t\t\t/>\n\t\t\t\t\t setPhone(e.target.value)}\n\t\t\t\t\t\tdisabled={!contactsActor.connection}\n\t\t\t\t\t/>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

Contacts ({contacts.length})

\n\t\t\t\t{contacts.length === 0 ? (\n\t\t\t\t\t
\n\t\t\t\t\t\tNo contacts yet. Add some contacts to get started!\n\t\t\t\t\t
\n\t\t\t\t) : (\n\t\t\t\t\tcontacts.map((contact) => (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
{contact.name}
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{contact.email && (\n\t\t\t\t\t\t\t\t\t\t
📧 {contact.email}
\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{contact.phone && (\n\t\t\t\t\t\t\t\t\t\t
📞 {contact.phone}
\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t deleteContact(contact.id)}\n\t\t\t\t\t\t\t\tdisabled={!contactsActor.connection}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tDelete\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t))\n\t\t\t\t)}\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
{stats.totalContacts}
\n\t\t\t\t\t
Total Contacts
\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
{stats.deletedContacts}
\n\t\t\t\t\t
Deleted Items
\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{stats.lastSyncTime ? new Date(stats.lastSyncTime).toLocaleTimeString() : \"—\"}\n\t\t\t\t\t
\n\t\t\t\t\t
Last Sync
\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t);\n}\n", + "src/frontend/App.tsx": "import { createRivetKit } from \"@rivetkit/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { Contact } from \"../backend/types\";\nimport type { registry } from \"../backend/registry\";\n\nconst { useActor } = createRivetKit(\"http://localhost:6420\");\n\nexport function App() {\n\tconst [contacts, setContacts] = useState([]);\n\tconst [name, setName] = useState(\"\");\n\tconst [email, setEmail] = useState(\"\");\n\tconst [phone, setPhone] = useState(\"\");\n\tconst [syncStatus, setSyncStatus] = useState<\"Idle\" | \"Syncing\" | \"Synced\" | \"Offline\">(\"Idle\");\n\tconst [stats, setStats] = useState({ totalContacts: 0, lastSyncTime: 0, deletedContacts: 0 });\n\n\tconst lastSyncTime = useRef(0);\n\tconst syncIntervalRef = useRef(null);\n\n\tconst contactsActor = useActor({\n\t\tname: \"contacts\",\n\t\tkey: [\"global\"],\n\t});\n\n\t// Load initial contacts and stats\n\tuseEffect(() => {\n\t\tif (!contactsActor.connection) return;\n\n\t\tconst loadInitialData = async () => {\n\t\t\ttry {\n\t\t\t\tconst data = await contactsActor.connection!.getChanges(0);\n\t\t\t\tsetContacts(data.changes);\n\t\t\t\tlastSyncTime.current = data.timestamp;\n\t\t\t\tsetSyncStatus(\"Synced\");\n\n\t\t\t\tconst statsData = await contactsActor.connection!.getSyncStats();\n\t\t\t\tsetStats(statsData);\n\t\t\t} catch (error) {\n\t\t\t\tsetSyncStatus(\"Offline\");\n\t\t\t}\n\t\t};\n\n\t\tloadInitialData();\n\t}, [contactsActor.connection]);\n\n\t// Handle contact events from other clients\n\tcontactsActor.useEvent(\"contactsChanged\", ({ contacts: updatedContacts }: { contacts: Contact[] }) => {\n\t\tsetContacts((prev) => {\n\t\t\tconst contactMap = new Map(prev.map((c) => [c.id, c]));\n\n\t\t\tupdatedContacts.forEach((contact) => {\n\t\t\t\tconst existing = contactMap.get(contact.id);\n\t\t\t\tif (!existing || existing.updatedAt < contact.updatedAt) {\n\t\t\t\t\tcontactMap.set(contact.id, contact);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\treturn Array.from(contactMap.values()).filter(c => c.name !== \"\");\n\t\t});\n\n\t\t// Update stats when contacts change\n\t\tif (contactsActor.connection) {\n\t\t\tcontactsActor.connection.getSyncStats().then(setStats);\n\t\t}\n\t});\n\n\t// Periodic sync - every 5 seconds\n\tuseEffect(() => {\n\t\tif (!contactsActor.connection) return;\n\n\t\tconst sync = async () => {\n\t\t\tsetSyncStatus(\"Syncing\");\n\n\t\t\ttry {\n\t\t\t\t// Get remote changes\n\t\t\t\tconst changes = await contactsActor.connection!.getChanges(lastSyncTime.current);\n\n\t\t\t\t// Apply remote changes\n\t\t\t\tif (changes.changes.length > 0) {\n\t\t\t\t\tsetContacts((prev) => {\n\t\t\t\t\t\tconst contactMap = new Map(prev.map((c) => [c.id, c]));\n\n\t\t\t\t\t\tchanges.changes.forEach((contact) => {\n\t\t\t\t\t\t\tconst existing = contactMap.get(contact.id);\n\t\t\t\t\t\t\tif (!existing || existing.updatedAt < contact.updatedAt) {\n\t\t\t\t\t\t\t\tcontactMap.set(contact.id, contact);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\treturn Array.from(contactMap.values()).filter(c => c.name !== \"\");\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\t// Push local changes\n\t\t\t\tconst localChanges = contacts.filter(\n\t\t\t\t\t(c) => c.updatedAt > lastSyncTime.current,\n\t\t\t\t);\n\t\t\t\tif (localChanges.length > 0) {\n\t\t\t\t\tawait contactsActor.connection!.pushChanges(localChanges);\n\t\t\t\t}\n\n\t\t\t\tlastSyncTime.current = changes.timestamp;\n\t\t\t\tsetSyncStatus(\"Synced\");\n\n\t\t\t\t// Update stats\n\t\t\t\tconst statsData = await contactsActor.connection!.getSyncStats();\n\t\t\t\tsetStats(statsData);\n\t\t\t} catch (error) {\n\t\t\t\tsetSyncStatus(\"Offline\");\n\t\t\t}\n\t\t};\n\n\t\tsyncIntervalRef.current = setInterval(sync, 5000);\n\n\t\treturn () => {\n\t\t\tif (syncIntervalRef.current) {\n\t\t\t\tclearInterval(syncIntervalRef.current);\n\t\t\t\tsyncIntervalRef.current = null;\n\t\t\t}\n\t\t};\n\t}, [contactsActor.connection, contacts]);\n\n\t// Add new contact (local first)\n\tconst addContact = async () => {\n\t\tif (!name.trim()) return;\n\n\t\tconst newContact: Contact = {\n\t\t\tid: Date.now().toString(),\n\t\t\tname,\n\t\t\temail,\n\t\t\tphone,\n\t\t\tupdatedAt: Date.now(),\n\t\t};\n\n\t\t// Add locally first for immediate UI feedback\n\t\tsetContacts((prev) => [...prev, newContact]);\n\n\t\t// Then sync to server\n\t\tif (contactsActor.connection) {\n\t\t\ttry {\n\t\t\t\tawait contactsActor.connection.pushChanges([newContact]);\n\t\t\t\tconst statsData = await contactsActor.connection.getSyncStats();\n\t\t\t\tsetStats(statsData);\n\t\t\t} catch (error) {\n\t\t\t\tsetSyncStatus(\"Offline\");\n\t\t\t}\n\t\t}\n\n\t\tsetName(\"\");\n\t\tsetEmail(\"\");\n\t\tsetPhone(\"\");\n\t};\n\n\t// Delete contact (implemented as update with empty name)\n\tconst deleteContact = async (id: string) => {\n\t\tconst deletedContact = contacts.find(c => c.id === id);\n\t\tif (!deletedContact) return;\n\n\t\tconst updatedContact: Contact = {\n\t\t\t...deletedContact,\n\t\t\tname: \"\", // Mark as deleted\n\t\t\tupdatedAt: Date.now()\n\t\t};\n\n\t\t// Remove locally first for immediate UI feedback\n\t\tsetContacts((prev) => prev.filter((c) => c.id !== id));\n\n\t\t// Then sync to server\n\t\tif (contactsActor.connection) {\n\t\t\ttry {\n\t\t\t\tawait contactsActor.connection.pushChanges([updatedContact]);\n\t\t\t\tconst statsData = await contactsActor.connection.getSyncStats();\n\t\t\t\tsetStats(statsData);\n\t\t\t} catch (error) {\n\t\t\t\tsetSyncStatus(\"Offline\");\n\t\t\t}\n\t\t}\n\t};\n\n\t// Manual sync\n\tconst handleSync = async () => {\n\t\tif (!contactsActor.connection) return;\n\n\t\tsetSyncStatus(\"Syncing\");\n\n\t\ttry {\n\t\t\t// Push all contacts\n\t\t\tawait contactsActor.connection.pushChanges(contacts);\n\n\t\t\t// Get all changes\n\t\t\tconst changes = await contactsActor.connection.getChanges(0);\n\n\t\t\tsetContacts(changes.changes.filter(c => c.name !== \"\"));\n\t\t\tlastSyncTime.current = changes.timestamp;\n\t\t\tsetSyncStatus(\"Synced\");\n\n\t\t\t// Update stats\n\t\t\tconst statsData = await contactsActor.connection.getSyncStats();\n\t\t\tsetStats(statsData);\n\t\t} catch (error) {\n\t\t\tsetSyncStatus(\"Offline\");\n\t\t}\n\t};\n\n\t// Reset all data\n\tconst handleReset = async () => {\n\t\tif (!contactsActor.connection) return;\n\n\t\ttry {\n\t\t\tawait contactsActor.connection.reset();\n\t\t\tsetContacts([]);\n\t\t\tlastSyncTime.current = Date.now();\n\t\t\tsetSyncStatus(\"Synced\");\n\t\t\tsetStats({ totalContacts: 0, lastSyncTime: Date.now(), deletedContacts: 0 });\n\t\t} catch (error) {\n\t\t\tsetSyncStatus(\"Offline\");\n\t\t}\n\t};\n\n\t// Handle form submission\n\tconst handleSubmit = (e: React.FormEvent) => {\n\t\te.preventDefault();\n\t\taddContact();\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t

Sync Contacts

\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t{syncStatus}\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

How it works

\n\t\t\t\t

\n\t\t\t\t\tThis contact sync system demonstrates offline-first synchronization with conflict resolution. \n\t\t\t\t\tAdd contacts and they'll sync across all connected clients. The system handles conflicts using \n\t\t\t\t\t\"last write wins\" based on timestamps, and supports offline operation with automatic sync when reconnected.\n\t\t\t\t

\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

Add New Contact

\n\t\t\t\t
\n\t\t\t\t\t setName(e.target.value)}\n\t\t\t\t\t\trequired\n\t\t\t\t\t\tdisabled={!contactsActor.connection}\n\t\t\t\t\t/>\n\t\t\t\t\t setEmail(e.target.value)}\n\t\t\t\t\t\tdisabled={!contactsActor.connection}\n\t\t\t\t\t/>\n\t\t\t\t\t setPhone(e.target.value)}\n\t\t\t\t\t\tdisabled={!contactsActor.connection}\n\t\t\t\t\t/>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

Contacts ({contacts.length})

\n\t\t\t\t{contacts.length === 0 ? (\n\t\t\t\t\t
\n\t\t\t\t\t\tNo contacts yet. Add some contacts to get started!\n\t\t\t\t\t
\n\t\t\t\t) : (\n\t\t\t\t\tcontacts.map((contact) => (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
{contact.name}
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{contact.email && (\n\t\t\t\t\t\t\t\t\t\t
📧 {contact.email}
\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{contact.phone && (\n\t\t\t\t\t\t\t\t\t\t
📞 {contact.phone}
\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t deleteContact(contact.id)}\n\t\t\t\t\t\t\t\tdisabled={!contactsActor.connection}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tDelete\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t))\n\t\t\t\t)}\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
{stats.totalContacts}
\n\t\t\t\t\t
Total Contacts
\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
{stats.deletedContacts}
\n\t\t\t\t\t
Deleted Items
\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{stats.lastSyncTime ? new Date(stats.lastSyncTime).toLocaleTimeString() : \"—\"}\n\t\t\t\t\t
\n\t\t\t\t\t
Last Sync
\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t);\n}\n", "src/frontend/main.tsx": "import { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { App } from \"./App\";\n\nconst root = document.getElementById(\"root\");\nif (!root) throw new Error(\"Root element not found\");\n\ncreateRoot(root).render(\n\t\n\t\t\n\t\n);", "src/frontend/index.html": "\n\n\n \n \n Sync Contacts - RivetKit\n \n\n\n
\n \n\n", "src/backend/types.ts": "export type Contact = {\n\tid: string;\n\tname: string;\n\temail: string;\n\tphone: string;\n\tupdatedAt: number;\n};\n", @@ -125,7 +125,7 @@ export const examples: ExampleData[] = [ "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"lib\": [\"esnext\", \"dom\"],\n \"jsx\": \"react-jsx\",\n \"module\": \"esnext\",\n \"moduleResolution\": \"bundler\",\n \"types\": [\"node\", \"vite/client\"],\n \"resolveJsonModule\": true,\n \"allowJs\": true,\n \"checkJs\": false,\n \"noEmit\": true,\n \"isolatedModules\": true,\n \"allowSyntheticDefaultImports\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"strict\": true,\n \"skipLibCheck\": true\n },\n \"include\": [\"src/**/*\"],\n \"exclude\": [\"node_modules\", \"dist\"]\n}\n", "vite.config.ts": "import react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n\tplugins: [react()],\n\troot: \"src/frontend\",\n\tserver: {\n\t\tport: 3000,\n\t},\n});\n", "vitest.config.ts": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n\ttest: {\n\t\tinclude: [\"tests/**/*.test.ts\"],\n\t},\n});\n", - "src/frontend/App.tsx": "import { createRivetKit } from \"@rivetkit/react\";\nimport { useEffect, useState } from \"react\";\nimport type { Note, registry } from \"../backend/registry\";\n\nconst { useActor } = createRivetKit(\"http://localhost:8080\");\n\nfunction NotesApp({ userId }: { userId: string }) {\n\tconst [notes, setNotes] = useState([]);\n\tconst [newNote, setNewNote] = useState(\"\");\n\tconst [editingNote, setEditingNote] = useState(null);\n\tconst [editContent, setEditContent] = useState(\"\");\n\n\tconst notesActor = useActor({\n\t\tname: \"notes\",\n\t\tkey: [userId],\n\t\tparams: { userId, token: \"demo-token\" },\n\t});\n\n\tuseEffect(() => {\n\t\tif (notesActor.connection) {\n\t\t\tnotesActor.connection.getNotes().then(setNotes);\n\t\t}\n\t}, [notesActor.connection]);\n\n\tnotesActor.useEvent(\"noteAdded\", (note: Note) => {\n\t\tsetNotes((prev) => [...prev, note]);\n\t});\n\n\tnotesActor.useEvent(\"noteUpdated\", (updatedNote: Note) => {\n\t\tsetNotes((prev) =>\n\t\t\tprev.map((note) => (note.id === updatedNote.id ? updatedNote : note))\n\t\t);\n\t\tsetEditingNote(null);\n\t});\n\n\tnotesActor.useEvent(\"noteDeleted\", ({ id }: { id: string }) => {\n\t\tsetNotes((prev) => prev.filter((note) => note.id !== id));\n\t});\n\n\tconst addNote = async () => {\n\t\tif (notesActor.connection && newNote.trim()) {\n\t\t\tawait notesActor.connection.updateNote({ \n\t\t\t\tid: `note-${Date.now()}`, \n\t\t\t\tcontent: newNote \n\t\t\t});\n\t\t\tsetNewNote(\"\");\n\t\t}\n\t};\n\n\tconst startEdit = (note: Note) => {\n\t\tsetEditingNote(note.id);\n\t\tsetEditContent(note.content);\n\t};\n\n\tconst saveEdit = async () => {\n\t\tif (notesActor.connection && editingNote) {\n\t\t\tawait notesActor.connection.updateNote({ \n\t\t\t\tid: editingNote, \n\t\t\t\tcontent: editContent \n\t\t\t});\n\t\t}\n\t};\n\n\tconst cancelEdit = () => {\n\t\tsetEditingNote(null);\n\t\tsetEditContent(\"\");\n\t};\n\n\tconst deleteNote = async (id: string) => {\n\t\tif (notesActor.connection && confirm(\"Are you sure you want to delete this note?\")) {\n\t\t\tawait notesActor.connection.deleteNote({ id });\n\t\t}\n\t};\n\n\tconst handleKeyPress = (e: React.KeyboardEvent, action: () => void) => {\n\t\tif (e.key === \"Enter\") {\n\t\t\taction();\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t{notesActor.connection ? '✓ Connected' : '⚠ Disconnected'}\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t setNewNote(e.target.value)}\n\t\t\t\t\tonKeyPress={(e) => handleKeyPress(e, addNote)}\n\t\t\t\t\tplaceholder=\"Enter a new note...\"\n\t\t\t\t\tdisabled={!notesActor.connection}\n\t\t\t\t/>\n\t\t\t\t\n\t\t\t\t\tAdd Note\n\t\t\t\t\n\t\t\t
\n\n\t\t\t{notes.length === 0 ? (\n\t\t\t\t
\n\t\t\t\t\tNo notes yet. Add your first note above!\n\t\t\t\t
\n\t\t\t) : (\n\t\t\t\t
    \n\t\t\t\t\t{notes\n\t\t\t\t\t\t.sort((a, b) => b.updatedAt - a.updatedAt)\n\t\t\t\t\t\t.map((note) => (\n\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t{editingNote === note.id ? (\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t setEditContent(e.target.value)}\n\t\t\t\t\t\t\t\t\t\tonKeyPress={(e) => handleKeyPress(e, saveEdit)}\n\t\t\t\t\t\t\t\t\t\tclassName=\"edit-input\"\n\t\t\t\t\t\t\t\t\t\tautoFocus\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t
    {note.content}
    \n\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\tLast updated: {new Date(note.updatedAt).toLocaleString()}\n\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t startEdit(note)}\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"edit-btn\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tEdit\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t deleteNote(note.id)}\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"delete-btn\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tDelete\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t
  • \n\t\t\t\t\t))}\n\t\t\t\t
\n\t\t\t)}\n\t\t
\n\t);\n}\n\nexport function App() {\n\tconst [selectedUser, setSelectedUser] = useState(\"user1\");\n\n\tconst users = [\n\t\t{ id: \"user1\", name: \"Alice\" },\n\t\t{ id: \"user2\", name: \"Bob\" },\n\t\t{ id: \"user3\", name: \"Charlie\" },\n\t];\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t

Database Notes

\n\t\t\t\t

Persistent note-taking with real-time updates

\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t setSelectedUser(e.target.value)}\n\t\t\t\t>\n\t\t\t\t\t{users.map((user) => (\n\t\t\t\t\t\t\n\t\t\t\t\t))}\n\t\t\t\t\n\t\t\t
\n\n\t\t\t\n\t\t
\n\t);\n}\n", + "src/frontend/App.tsx": "import { createRivetKit } from \"@rivetkit/react\";\nimport { useEffect, useState } from \"react\";\nimport type { Note, registry } from \"../backend/registry\";\n\nconst { useActor } = createRivetKit(\"http://localhost:6420\");\n\nfunction NotesApp({ userId }: { userId: string }) {\n\tconst [notes, setNotes] = useState([]);\n\tconst [newNote, setNewNote] = useState(\"\");\n\tconst [editingNote, setEditingNote] = useState(null);\n\tconst [editContent, setEditContent] = useState(\"\");\n\n\tconst notesActor = useActor({\n\t\tname: \"notes\",\n\t\tkey: [userId],\n\t\tparams: { userId, token: \"demo-token\" },\n\t});\n\n\tuseEffect(() => {\n\t\tif (notesActor.connection) {\n\t\t\tnotesActor.connection.getNotes().then(setNotes);\n\t\t}\n\t}, [notesActor.connection]);\n\n\tnotesActor.useEvent(\"noteAdded\", (note: Note) => {\n\t\tsetNotes((prev) => [...prev, note]);\n\t});\n\n\tnotesActor.useEvent(\"noteUpdated\", (updatedNote: Note) => {\n\t\tsetNotes((prev) =>\n\t\t\tprev.map((note) => (note.id === updatedNote.id ? updatedNote : note))\n\t\t);\n\t\tsetEditingNote(null);\n\t});\n\n\tnotesActor.useEvent(\"noteDeleted\", ({ id }: { id: string }) => {\n\t\tsetNotes((prev) => prev.filter((note) => note.id !== id));\n\t});\n\n\tconst addNote = async () => {\n\t\tif (notesActor.connection && newNote.trim()) {\n\t\t\tawait notesActor.connection.updateNote({ \n\t\t\t\tid: `note-${Date.now()}`, \n\t\t\t\tcontent: newNote \n\t\t\t});\n\t\t\tsetNewNote(\"\");\n\t\t}\n\t};\n\n\tconst startEdit = (note: Note) => {\n\t\tsetEditingNote(note.id);\n\t\tsetEditContent(note.content);\n\t};\n\n\tconst saveEdit = async () => {\n\t\tif (notesActor.connection && editingNote) {\n\t\t\tawait notesActor.connection.updateNote({ \n\t\t\t\tid: editingNote, \n\t\t\t\tcontent: editContent \n\t\t\t});\n\t\t}\n\t};\n\n\tconst cancelEdit = () => {\n\t\tsetEditingNote(null);\n\t\tsetEditContent(\"\");\n\t};\n\n\tconst deleteNote = async (id: string) => {\n\t\tif (notesActor.connection && confirm(\"Are you sure you want to delete this note?\")) {\n\t\t\tawait notesActor.connection.deleteNote({ id });\n\t\t}\n\t};\n\n\tconst handleKeyPress = (e: React.KeyboardEvent, action: () => void) => {\n\t\tif (e.key === \"Enter\") {\n\t\t\taction();\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t{notesActor.connection ? '✓ Connected' : '⚠ Disconnected'}\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t setNewNote(e.target.value)}\n\t\t\t\t\tonKeyPress={(e) => handleKeyPress(e, addNote)}\n\t\t\t\t\tplaceholder=\"Enter a new note...\"\n\t\t\t\t\tdisabled={!notesActor.connection}\n\t\t\t\t/>\n\t\t\t\t\n\t\t\t\t\tAdd Note\n\t\t\t\t\n\t\t\t
\n\n\t\t\t{notes.length === 0 ? (\n\t\t\t\t
\n\t\t\t\t\tNo notes yet. Add your first note above!\n\t\t\t\t
\n\t\t\t) : (\n\t\t\t\t
    \n\t\t\t\t\t{notes\n\t\t\t\t\t\t.sort((a, b) => b.updatedAt - a.updatedAt)\n\t\t\t\t\t\t.map((note) => (\n\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t{editingNote === note.id ? (\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t setEditContent(e.target.value)}\n\t\t\t\t\t\t\t\t\t\tonKeyPress={(e) => handleKeyPress(e, saveEdit)}\n\t\t\t\t\t\t\t\t\t\tclassName=\"edit-input\"\n\t\t\t\t\t\t\t\t\t\tautoFocus\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t
    {note.content}
    \n\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t\tLast updated: {new Date(note.updatedAt).toLocaleString()}\n\t\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\t startEdit(note)}\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"edit-btn\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tEdit\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t deleteNote(note.id)}\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"delete-btn\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tDelete\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t
  • \n\t\t\t\t\t))}\n\t\t\t\t
\n\t\t\t)}\n\t\t
\n\t);\n}\n\nexport function App() {\n\tconst [selectedUser, setSelectedUser] = useState(\"user1\");\n\n\tconst users = [\n\t\t{ id: \"user1\", name: \"Alice\" },\n\t\t{ id: \"user2\", name: \"Bob\" },\n\t\t{ id: \"user3\", name: \"Charlie\" },\n\t];\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t

Database Notes

\n\t\t\t\t

Persistent note-taking with real-time updates

\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t setSelectedUser(e.target.value)}\n\t\t\t\t>\n\t\t\t\t\t{users.map((user) => (\n\t\t\t\t\t\t\n\t\t\t\t\t))}\n\t\t\t\t\n\t\t\t
\n\n\t\t\t\n\t\t
\n\t);\n}\n", "src/frontend/main.tsx": "import { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { App } from \"./App\";\n\nconst root = document.getElementById(\"root\");\nif (!root) throw new Error(\"Root element not found\");\n\ncreateRoot(root).render(\n\t\n\t\t\n\t\n);", "src/frontend/index.html": "\n\n\n \n \n Database Notes Example\n \n\n\n
\n \n\n", "src/backend/registry.ts": "import { actor, setup } from \"rivetkit\";\nimport { authenticate } from \"./my-utils\";\n\nexport type Note = { id: string; content: string; updatedAt: number };\n\nexport const notes = actor({\n\t// Persistent state that survives restarts: https://rivet.dev/docs/actors/state\n\tstate: {\n\t\tnotes: [] as Note[],\n\t},\n\n\tactions: {\n\t\t// Callable functions from clients: https://rivet.dev/docs/actors/actions\n\t\tgetNotes: (c) => c.state.notes,\n\n\t\tupdateNote: (c, { id, content }: { id?: string; content: string }) => {\n\t\t\tconst noteIndex = c.state.notes.findIndex((note) => note.id === id);\n\t\t\tlet note: Note;\n\n\t\t\tif (noteIndex >= 0) {\n\t\t\t\t// Update existing note\n\t\t\t\tnote = c.state.notes[noteIndex];\n\t\t\t\tnote.content = content;\n\t\t\t\tnote.updatedAt = Date.now();\n\t\t\t\t// Send events to all connected clients: https://rivet.dev/docs/actors/events\n\t\t\t\tc.broadcast(\"noteUpdated\", note);\n\t\t\t} else {\n\t\t\t\t// Create new note\n\t\t\t\tnote = {\n\t\t\t\t\tid: id || `note-${Date.now()}`,\n\t\t\t\t\tcontent,\n\t\t\t\t\tupdatedAt: Date.now(),\n\t\t\t\t};\n\t\t\t\t// State changes are automatically persisted\n\t\t\t\tc.state.notes.push(note);\n\t\t\t\tc.broadcast(\"noteAdded\", note);\n\t\t\t}\n\n\t\t\treturn note;\n\t\t},\n\n\t\tdeleteNote: (c, { id }: { id: string }) => {\n\t\t\tconst noteIndex = c.state.notes.findIndex((note) => note.id === id);\n\t\t\tif (noteIndex >= 0) {\n\t\t\t\tc.state.notes.splice(noteIndex, 1);\n\t\t\t\tc.broadcast(\"noteDeleted\", { id });\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t},\n\t},\n});\n\n// Register actors for use: https://rivet.dev/docs/setup\nexport const registry = setup({\n\tuse: { notes },\n});\n", @@ -170,7 +170,7 @@ export const examples: ExampleData[] = [ "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"lib\": [\"esnext\", \"dom\"],\n \"jsx\": \"react-jsx\",\n \"module\": \"esnext\",\n \"moduleResolution\": \"bundler\",\n \"types\": [\"node\", \"vite/client\"],\n \"resolveJsonModule\": true,\n \"allowJs\": true,\n \"checkJs\": false,\n \"noEmit\": true,\n \"isolatedModules\": true,\n \"allowSyntheticDefaultImports\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"strict\": true,\n \"skipLibCheck\": true\n },\n \"include\": [\"src/**/*\"],\n \"exclude\": [\"node_modules\", \"dist\"]\n}\n", "vite.config.ts": "import react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n\tplugins: [react()],\n\troot: \"src/frontend\",\n\tserver: {\n\t\tport: 3000,\n\t},\n});\n", "vitest.config.ts": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n\ttest: {\n\t\tinclude: [\"tests/**/*.test.ts\"],\n\t},\n});\n", - "src/frontend/App.tsx": "import { createRivetKit } from \"@rivetkit/react\";\nimport { useEffect, useState } from \"react\";\nimport type { RateLimitResult, registry } from \"../backend/registry\";\n\nconst { useActor } = createRivetKit(\"http://localhost:8080\");\n\nfunction RateLimiterDemo({ userId }: { userId: string }) {\n\tconst [result, setResult] = useState(null);\n\tconst [loading, setLoading] = useState(false);\n\n\tconst rateLimiter = useActor({\n\t\tname: \"rateLimiter\",\n\t\tkey: [userId],\n\t});\n\n\tuseEffect(() => {\n\t\tif (rateLimiter.connection) {\n\t\t\t// Get initial status\n\t\t\trateLimiter.connection.getStatus().then((status) => {\n\t\t\t\tsetResult({\n\t\t\t\t\tallowed: status.remaining > 0,\n\t\t\t\t\tremaining: status.remaining,\n\t\t\t\t\tresetsIn: status.resetsIn,\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t}, [rateLimiter.connection]);\n\n\tconst makeRequest = async () => {\n\t\tif (!rateLimiter.connection || loading) return;\n\n\t\tsetLoading(true);\n\t\ttry {\n\t\t\tconst response = await rateLimiter.connection.checkLimit();\n\t\t\tsetResult(response);\n\t\t} finally {\n\t\t\tsetLoading(false);\n\t\t}\n\t};\n\n\tconst resetLimiter = async () => {\n\t\tif (!rateLimiter.connection) return;\n\n\t\tawait rateLimiter.connection.reset();\n\t\t// Get updated status\n\t\tconst status = await rateLimiter.connection.getStatus();\n\t\tsetResult({\n\t\t\tallowed: status.remaining > 0,\n\t\t\tremaining: status.remaining,\n\t\t\tresetsIn: status.resetsIn,\n\t\t});\n\t};\n\n\tconst usagePercentage = result ? ((5 - result.remaining) / 5) * 100 : 0;\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t{loading ? \"Making Request...\" : \"Make API Request\"}\n\t\t\t\n\n\t\t\t{result && (\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\tStatus:\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{result.allowed ? \"✓ Request Allowed\" : \"✖ Request Blocked\"}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\tRemaining Requests:\n\t\t\t\t\t\t{result.remaining} / 5\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\tRate Limit Usage:\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\tResets In:\n\t\t\t\t\t\t{result.resetsIn} seconds\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t\n\t\t
\n\t);\n}\n\nexport function App() {\n\tconst [selectedUser, setSelectedUser] = useState(\"user-1\");\n\n\tconst users = [\n\t\t{ id: \"user-1\", name: \"User 1\" },\n\t\t{ id: \"user-2\", name: \"User 2\" },\n\t\t{ id: \"user-3\", name: \"User 3\" },\n\t\t{ id: \"api-client-1\", name: \"API Client 1\" },\n\t\t{ id: \"api-client-2\", name: \"API Client 2\" },\n\t];\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t

Rate Limiter Demo

\n\t\t\t\t

5 requests per minute per user/client

\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t

How it works

\n\t\t\t\t\t

\n\t\t\t\t\t\tThis rate limiter allows 5 requests per minute per user. Each user gets their own \n\t\t\t\t\t\tindependent rate limit counter. When the limit is exceeded, further requests are \n\t\t\t\t\t\tblocked until the window resets. Switch between users to see isolated rate limiting.\n\t\t\t\t\t

\n\t\t\t\t
\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t setSelectedUser(e.target.value)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{users.map((user) => (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t))}\n\t\t\t\t\t\n\t\t\t\t
\n\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t);\n}\n", + "src/frontend/App.tsx": "import { createRivetKit } from \"@rivetkit/react\";\nimport { useEffect, useState } from \"react\";\nimport type { RateLimitResult, registry } from \"../backend/registry\";\n\nconst { useActor } = createRivetKit(\"http://localhost:6420\");\n\nfunction RateLimiterDemo({ userId }: { userId: string }) {\n\tconst [result, setResult] = useState(null);\n\tconst [loading, setLoading] = useState(false);\n\n\tconst rateLimiter = useActor({\n\t\tname: \"rateLimiter\",\n\t\tkey: [userId],\n\t});\n\n\tuseEffect(() => {\n\t\tif (rateLimiter.connection) {\n\t\t\t// Get initial status\n\t\t\trateLimiter.connection.getStatus().then((status) => {\n\t\t\t\tsetResult({\n\t\t\t\t\tallowed: status.remaining > 0,\n\t\t\t\t\tremaining: status.remaining,\n\t\t\t\t\tresetsIn: status.resetsIn,\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t}, [rateLimiter.connection]);\n\n\tconst makeRequest = async () => {\n\t\tif (!rateLimiter.connection || loading) return;\n\n\t\tsetLoading(true);\n\t\ttry {\n\t\t\tconst response = await rateLimiter.connection.checkLimit();\n\t\t\tsetResult(response);\n\t\t} finally {\n\t\t\tsetLoading(false);\n\t\t}\n\t};\n\n\tconst resetLimiter = async () => {\n\t\tif (!rateLimiter.connection) return;\n\n\t\tawait rateLimiter.connection.reset();\n\t\t// Get updated status\n\t\tconst status = await rateLimiter.connection.getStatus();\n\t\tsetResult({\n\t\t\tallowed: status.remaining > 0,\n\t\t\tremaining: status.remaining,\n\t\t\tresetsIn: status.resetsIn,\n\t\t});\n\t};\n\n\tconst usagePercentage = result ? ((5 - result.remaining) / 5) * 100 : 0;\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t{loading ? \"Making Request...\" : \"Make API Request\"}\n\t\t\t\n\n\t\t\t{result && (\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\tStatus:\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{result.allowed ? \"✓ Request Allowed\" : \"✖ Request Blocked\"}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\tRemaining Requests:\n\t\t\t\t\t\t{result.remaining} / 5\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\tRate Limit Usage:\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\tResets In:\n\t\t\t\t\t\t{result.resetsIn} seconds\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t\n\t\t
\n\t);\n}\n\nexport function App() {\n\tconst [selectedUser, setSelectedUser] = useState(\"user-1\");\n\n\tconst users = [\n\t\t{ id: \"user-1\", name: \"User 1\" },\n\t\t{ id: \"user-2\", name: \"User 2\" },\n\t\t{ id: \"user-3\", name: \"User 3\" },\n\t\t{ id: \"api-client-1\", name: \"API Client 1\" },\n\t\t{ id: \"api-client-2\", name: \"API Client 2\" },\n\t];\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t

Rate Limiter Demo

\n\t\t\t\t

5 requests per minute per user/client

\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t

How it works

\n\t\t\t\t\t

\n\t\t\t\t\t\tThis rate limiter allows 5 requests per minute per user. Each user gets their own \n\t\t\t\t\t\tindependent rate limit counter. When the limit is exceeded, further requests are \n\t\t\t\t\t\tblocked until the window resets. Switch between users to see isolated rate limiting.\n\t\t\t\t\t

\n\t\t\t\t
\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t setSelectedUser(e.target.value)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{users.map((user) => (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t))}\n\t\t\t\t\t\n\t\t\t\t
\n\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t);\n}\n", "src/frontend/main.tsx": "import { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { App } from \"./App\";\n\nconst root = document.getElementById(\"root\");\nif (!root) throw new Error(\"Root element not found\");\n\ncreateRoot(root).render(\n\t\n\t\t\n\t\n);", "src/frontend/index.html": "\n\n\n \n \n Rate Limiter Example\n \n\n\n
\n \n\n", "src/backend/registry.ts": "import { actor, setup } from \"rivetkit\";\n\nexport type RateLimitResult = {\n\tallowed: boolean;\n\tremaining: number;\n\tresetsIn: number;\n};\n\nexport const rateLimiter = actor({\n\t// Persistent state that survives restarts: https://rivet.dev/docs/actors/state\n\tstate: {\n\t\tcount: 0,\n\t\tresetAt: 0,\n\t},\n\n\tactions: {\n\t\t// Callable functions from clients: https://rivet.dev/docs/actors/actions\n\t\tcheckLimit: (c): RateLimitResult => {\n\t\t\tconst now = Date.now();\n\n\t\t\t// Reset if expired\n\t\t\tif (now > c.state.resetAt) {\n\t\t\t\t// State changes are automatically persisted\n\t\t\t\tc.state.count = 0;\n\t\t\t\tc.state.resetAt = now + 60000; // 1 minute window\n\t\t\t}\n\n\t\t\tconst allowed = c.state.count < 5;\n\n\t\t\t// Increment if allowed\n\t\t\tif (allowed) {\n\t\t\t\tc.state.count++;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tallowed,\n\t\t\t\tremaining: Math.max(0, 5 - c.state.count),\n\t\t\t\tresetsIn: Math.max(0, Math.round((c.state.resetAt - now) / 1000)),\n\t\t\t};\n\t\t},\n\n\t\tgetStatus: (c) => ({\n\t\t\tcount: c.state.count,\n\t\t\tresetAt: c.state.resetAt,\n\t\t\tremaining: Math.max(0, 5 - c.state.count),\n\t\t\tresetsIn: Math.max(0, Math.round((c.state.resetAt - Date.now()) / 1000)),\n\t\t}),\n\n\t\treset: (c) => {\n\t\t\tc.state.count = 0;\n\t\t\tc.state.resetAt = 0;\n\t\t\treturn { success: true };\n\t\t},\n\t},\n});\n\n// Register actors for use: https://rivet.dev/docs/setup\nexport const registry = setup({\n\tuse: { rateLimiter },\n});\n", @@ -194,7 +194,7 @@ export const examples: ExampleData[] = [ "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"ESNext\",\n \"skipLibCheck\": true,\n \"moduleResolution\": \"bundler\",\n \"allowImportingTsExtensions\": true,\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n \"strict\": true,\n \"noUnusedLocals\": true,\n \"noUnusedParameters\": true,\n \"noFallthroughCasesInSwitch\": true\n },\n \"include\": [\"src\", \"tests\"],\n \"exclude\": [\"node_modules\", \"dist\"]\n}\n", "vite.config.ts": "import react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n\tplugins: [react()],\n\troot: \"src/frontend\",\n\tserver: {\n\t\tport: 3000,\n\t},\n\tbuild: {\n\t\toutDir: \"../../dist\",\n\t\temptyOutDir: true,\n\t},\n});\n", "vitest.config.ts": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n\ttest: {\n\t\tenvironment: \"node\",\n\t},\n});\n", - "src/frontend/App.tsx": "import { createRivetKit } from \"@rivetkit/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { Player, registry } from \"../backend/registry\";\n\nconst { useActor } = createRivetKit(\"http://localhost:8080\");\n\nexport function App() {\n\tconst [players, setPlayers] = useState([]);\n\tconst [isConnected, setIsConnected] = useState(false);\n\tconst [currentPlayerId, setCurrentPlayerId] = useState(null);\n\tconst canvasRef = useRef(null);\n\tconst keysPressed = useRef>({});\n\tconst inputIntervalRef = useRef(null);\n\tconst animationRef = useRef(null);\n\n\tconst gameRoom = useActor({\n\t\tname: \"gameRoom\",\n\t\tkey: [\"global\"],\n\t});\n\n\t// Track connection status\n\tuseEffect(() => {\n\t\tsetIsConnected(!!gameRoom.connection);\n\t}, [gameRoom.connection]);\n\n\t// Set up game controls and rendering\n\tuseEffect(() => {\n\t\tif (!gameRoom.connection) return;\n\n\t\t// Set up keyboard handlers\n\t\tconst handleKeyDown = (e: KeyboardEvent) => {\n\t\t\tkeysPressed.current[e.key.toLowerCase()] = true;\n\t\t};\n\n\t\tconst handleKeyUp = (e: KeyboardEvent) => {\n\t\t\tkeysPressed.current[e.key.toLowerCase()] = false;\n\t\t};\n\n\t\twindow.addEventListener(\"keydown\", handleKeyDown);\n\t\twindow.addEventListener(\"keyup\", handleKeyUp);\n\n\t\t// Input update loop\n\t\tinputIntervalRef.current = setInterval(() => {\n\t\t\tconst input = { x: 0, y: 0 };\n\n\t\t\tif (keysPressed.current[\"w\"] || keysPressed.current[\"arrowup\"])\n\t\t\t\tinput.y = -1;\n\t\t\tif (keysPressed.current[\"s\"] || keysPressed.current[\"arrowdown\"])\n\t\t\t\tinput.y = 1;\n\t\t\tif (keysPressed.current[\"a\"] || keysPressed.current[\"arrowleft\"])\n\t\t\t\tinput.x = -1;\n\t\t\tif (keysPressed.current[\"d\"] || keysPressed.current[\"arrowright\"])\n\t\t\t\tinput.x = 1;\n\n\t\t\tgameRoom.connection?.setInput(input);\n\t\t}, 50);\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"keydown\", handleKeyDown);\n\t\t\twindow.removeEventListener(\"keyup\", handleKeyUp);\n\t\t\t\n\t\t\tif (inputIntervalRef.current) {\n\t\t\t\tclearInterval(inputIntervalRef.current);\n\t\t\t\tinputIntervalRef.current = null;\n\t\t\t}\n\t\t};\n\t}, [gameRoom.connection]);\n\n\t// Rendering loop\n\tuseEffect(() => {\n\t\tconst renderLoop = () => {\n\t\t\tconst canvas = canvasRef.current;\n\t\t\tif (!canvas) return;\n\n\t\t\tconst ctx = canvas.getContext(\"2d\");\n\t\t\tif (!ctx) return;\n\n\t\t\t// Clear canvas\n\t\t\tctx.clearRect(0, 0, canvas.width, canvas.height);\n\n\t\t\t// Draw grid\n\t\t\tctx.strokeStyle = \"#e0e0e0\";\n\t\t\tctx.lineWidth = 1;\n\t\t\tfor (let i = 0; i <= canvas.width; i += 50) {\n\t\t\t\tctx.beginPath();\n\t\t\t\tctx.moveTo(i, 0);\n\t\t\t\tctx.lineTo(i, canvas.height);\n\t\t\t\tctx.stroke();\n\t\t\t}\n\t\t\tfor (let i = 0; i <= canvas.height; i += 50) {\n\t\t\t\tctx.beginPath();\n\t\t\t\tctx.moveTo(0, i);\n\t\t\t\tctx.lineTo(canvas.width, i);\n\t\t\t\tctx.stroke();\n\t\t\t}\n\n\t\t\t// Draw players\n\t\t\tfor (const player of players) {\n\t\t\t\tconst isCurrentPlayer = currentPlayerId && player.id === currentPlayerId;\n\t\t\t\t\n\t\t\t\t// Draw player shadow\n\t\t\t\tctx.fillStyle = \"rgba(0, 0, 0, 0.2)\";\n\t\t\t\tctx.beginPath();\n\t\t\t\tctx.arc(player.position.x + 2, player.position.y + 2, 12, 0, Math.PI * 2);\n\t\t\t\tctx.fill();\n\n\t\t\t\t// Draw player\n\t\t\t\tctx.fillStyle = isCurrentPlayer ? \"#4287f5\" : \"#888\";\n\t\t\t\tctx.beginPath();\n\t\t\t\tctx.arc(player.position.x, player.position.y, 10, 0, Math.PI * 2);\n\t\t\t\tctx.fill();\n\n\t\t\t\t// Draw player border\n\t\t\t\tctx.strokeStyle = \"#333\";\n\t\t\t\tctx.lineWidth = 2;\n\t\t\t\tctx.stroke();\n\n\t\t\t\t// Draw player ID\n\t\t\t\tctx.fillStyle = \"#333\";\n\t\t\t\tctx.font = \"12px Arial\";\n\t\t\t\tctx.textAlign = \"center\";\n\t\t\t\tctx.fillText(\n\t\t\t\t\tisCurrentPlayer ? \"YOU\" : player.id.substring(0, 8),\n\t\t\t\t\tplayer.position.x,\n\t\t\t\t\tplayer.position.y - 15\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tanimationRef.current = requestAnimationFrame(renderLoop);\n\t\t};\n\n\t\tanimationRef.current = requestAnimationFrame(renderLoop);\n\n\t\treturn () => {\n\t\t\tif (animationRef.current) {\n\t\t\t\tcancelAnimationFrame(animationRef.current);\n\t\t\t\tanimationRef.current = null;\n\t\t\t}\n\t\t};\n\t}, [players, gameRoom.connection]);\n\n\t// Listen for world updates\n\tgameRoom.useEvent(\"worldUpdate\", ({ playerList }: { playerList: Player[] }) => {\n\t\tsetPlayers(playerList);\n\t\t\n\t\t// Try to identify current player - this is a simple approach\n\t\t// In a real implementation, we'd get the connection ID from the server\n\t\tif (currentPlayerId === null && playerList.length > 0) {\n\t\t\tsetCurrentPlayerId(playerList[playerList.length - 1].id);\n\t\t}\n\t});\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{isConnected ? \"Connected\" : \"Disconnected\"}\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

Multiplayer Game

\n\t\t\t\t

Real-time multiplayer movement with RivetKit

\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

How to Play

\n\t\t\t\t

\n\t\t\t\t\tUse WASD or arrow keys to move your character around the game world. \n\t\t\t\t\tYour character is shown in blue, while other players appear in gray. \n\t\t\t\t\tThe game updates in real-time, so you'll see other players moving as they play.\n\t\t\t\t

\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\tYou\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\tOther Players\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

Controls:

\n\t\t\t\t

Move: WASD or Arrow Keys

\n\t\t\t\t

Players online: {players.length}

\n\t\t\t
\n\t\t
\n\t);\n}\n", + "src/frontend/App.tsx": "import { createRivetKit } from \"@rivetkit/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { Player, registry } from \"../backend/registry\";\n\nconst { useActor } = createRivetKit(\"http://localhost:6420\");\n\nexport function App() {\n\tconst [players, setPlayers] = useState([]);\n\tconst [isConnected, setIsConnected] = useState(false);\n\tconst [currentPlayerId, setCurrentPlayerId] = useState(null);\n\tconst canvasRef = useRef(null);\n\tconst keysPressed = useRef>({});\n\tconst inputIntervalRef = useRef(null);\n\tconst animationRef = useRef(null);\n\n\tconst gameRoom = useActor({\n\t\tname: \"gameRoom\",\n\t\tkey: [\"global\"],\n\t});\n\n\t// Track connection status\n\tuseEffect(() => {\n\t\tsetIsConnected(!!gameRoom.connection);\n\t}, [gameRoom.connection]);\n\n\t// Set up game controls and rendering\n\tuseEffect(() => {\n\t\tif (!gameRoom.connection) return;\n\n\t\t// Set up keyboard handlers\n\t\tconst handleKeyDown = (e: KeyboardEvent) => {\n\t\t\tkeysPressed.current[e.key.toLowerCase()] = true;\n\t\t};\n\n\t\tconst handleKeyUp = (e: KeyboardEvent) => {\n\t\t\tkeysPressed.current[e.key.toLowerCase()] = false;\n\t\t};\n\n\t\twindow.addEventListener(\"keydown\", handleKeyDown);\n\t\twindow.addEventListener(\"keyup\", handleKeyUp);\n\n\t\t// Input update loop\n\t\tinputIntervalRef.current = setInterval(() => {\n\t\t\tconst input = { x: 0, y: 0 };\n\n\t\t\tif (keysPressed.current[\"w\"] || keysPressed.current[\"arrowup\"])\n\t\t\t\tinput.y = -1;\n\t\t\tif (keysPressed.current[\"s\"] || keysPressed.current[\"arrowdown\"])\n\t\t\t\tinput.y = 1;\n\t\t\tif (keysPressed.current[\"a\"] || keysPressed.current[\"arrowleft\"])\n\t\t\t\tinput.x = -1;\n\t\t\tif (keysPressed.current[\"d\"] || keysPressed.current[\"arrowright\"])\n\t\t\t\tinput.x = 1;\n\n\t\t\tgameRoom.connection?.setInput(input);\n\t\t}, 50);\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"keydown\", handleKeyDown);\n\t\t\twindow.removeEventListener(\"keyup\", handleKeyUp);\n\t\t\t\n\t\t\tif (inputIntervalRef.current) {\n\t\t\t\tclearInterval(inputIntervalRef.current);\n\t\t\t\tinputIntervalRef.current = null;\n\t\t\t}\n\t\t};\n\t}, [gameRoom.connection]);\n\n\t// Rendering loop\n\tuseEffect(() => {\n\t\tconst renderLoop = () => {\n\t\t\tconst canvas = canvasRef.current;\n\t\t\tif (!canvas) return;\n\n\t\t\tconst ctx = canvas.getContext(\"2d\");\n\t\t\tif (!ctx) return;\n\n\t\t\t// Clear canvas\n\t\t\tctx.clearRect(0, 0, canvas.width, canvas.height);\n\n\t\t\t// Draw grid\n\t\t\tctx.strokeStyle = \"#e0e0e0\";\n\t\t\tctx.lineWidth = 1;\n\t\t\tfor (let i = 0; i <= canvas.width; i += 50) {\n\t\t\t\tctx.beginPath();\n\t\t\t\tctx.moveTo(i, 0);\n\t\t\t\tctx.lineTo(i, canvas.height);\n\t\t\t\tctx.stroke();\n\t\t\t}\n\t\t\tfor (let i = 0; i <= canvas.height; i += 50) {\n\t\t\t\tctx.beginPath();\n\t\t\t\tctx.moveTo(0, i);\n\t\t\t\tctx.lineTo(canvas.width, i);\n\t\t\t\tctx.stroke();\n\t\t\t}\n\n\t\t\t// Draw players\n\t\t\tfor (const player of players) {\n\t\t\t\tconst isCurrentPlayer = currentPlayerId && player.id === currentPlayerId;\n\t\t\t\t\n\t\t\t\t// Draw player shadow\n\t\t\t\tctx.fillStyle = \"rgba(0, 0, 0, 0.2)\";\n\t\t\t\tctx.beginPath();\n\t\t\t\tctx.arc(player.position.x + 2, player.position.y + 2, 12, 0, Math.PI * 2);\n\t\t\t\tctx.fill();\n\n\t\t\t\t// Draw player\n\t\t\t\tctx.fillStyle = isCurrentPlayer ? \"#4287f5\" : \"#888\";\n\t\t\t\tctx.beginPath();\n\t\t\t\tctx.arc(player.position.x, player.position.y, 10, 0, Math.PI * 2);\n\t\t\t\tctx.fill();\n\n\t\t\t\t// Draw player border\n\t\t\t\tctx.strokeStyle = \"#333\";\n\t\t\t\tctx.lineWidth = 2;\n\t\t\t\tctx.stroke();\n\n\t\t\t\t// Draw player ID\n\t\t\t\tctx.fillStyle = \"#333\";\n\t\t\t\tctx.font = \"12px Arial\";\n\t\t\t\tctx.textAlign = \"center\";\n\t\t\t\tctx.fillText(\n\t\t\t\t\tisCurrentPlayer ? \"YOU\" : player.id.substring(0, 8),\n\t\t\t\t\tplayer.position.x,\n\t\t\t\t\tplayer.position.y - 15\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tanimationRef.current = requestAnimationFrame(renderLoop);\n\t\t};\n\n\t\tanimationRef.current = requestAnimationFrame(renderLoop);\n\n\t\treturn () => {\n\t\t\tif (animationRef.current) {\n\t\t\t\tcancelAnimationFrame(animationRef.current);\n\t\t\t\tanimationRef.current = null;\n\t\t\t}\n\t\t};\n\t}, [players, gameRoom.connection]);\n\n\t// Listen for world updates\n\tgameRoom.useEvent(\"worldUpdate\", ({ playerList }: { playerList: Player[] }) => {\n\t\tsetPlayers(playerList);\n\t\t\n\t\t// Try to identify current player - this is a simple approach\n\t\t// In a real implementation, we'd get the connection ID from the server\n\t\tif (currentPlayerId === null && playerList.length > 0) {\n\t\t\tsetCurrentPlayerId(playerList[playerList.length - 1].id);\n\t\t}\n\t});\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{isConnected ? \"Connected\" : \"Disconnected\"}\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

Multiplayer Game

\n\t\t\t\t

Real-time multiplayer movement with RivetKit

\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

How to Play

\n\t\t\t\t

\n\t\t\t\t\tUse WASD or arrow keys to move your character around the game world. \n\t\t\t\t\tYour character is shown in blue, while other players appear in gray. \n\t\t\t\t\tThe game updates in real-time, so you'll see other players moving as they play.\n\t\t\t\t

\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\tYou\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\tOther Players\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

Controls:

\n\t\t\t\t

Move: WASD or Arrow Keys

\n\t\t\t\t

Players online: {players.length}

\n\t\t\t
\n\t\t
\n\t);\n}\n", "src/frontend/main.tsx": "import { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { App } from \"./App\";\n\nconst root = document.getElementById(\"root\");\nif (!root) throw new Error(\"Root element not found\");\n\ncreateRoot(root).render(\n\t\n\t\t\n\t\n);", "src/frontend/index.html": "\n\n\n \n \n Multiplayer Game - RivetKit\n \n\n\n
\n \n\n", "src/backend/types.ts": "export type Position = { x: number; y: number };\nexport type Input = { x: number; y: number };\nexport type Player = { id: string; position: Position; input: Input };\n\nexport type GameVars = {\n\tgameLoopInterval?: ReturnType;\n};\n",