From feecacd1822e39a48b29139ae9c8ef08861aca2a Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Sat, 22 Nov 2025 12:46:36 -0800 Subject: [PATCH] chore(examples): fix types --- .../README.md | 33 +++ .../package.json | 25 ++ .../scripts/client-http.ts | 24 ++ .../scripts/client-rivetkit.ts | 31 +++ .../src/index.ts | 43 ++++ .../src/registry.ts | 16 ++ .../tsconfig.json | 43 ++++ .../turbo.json | 4 + .../wrangler.json | 30 +++ examples/cloudflare-workers/src/registry.ts | 2 +- examples/cloudflare-workers/wrangler.json | 2 +- examples/counter-next-js/.gitignore | 41 ++++ examples/counter-next-js/README.md | 54 +++++ examples/counter-next-js/next.config.ts | 7 + examples/counter-next-js/package.json | 27 +++ examples/counter-next-js/scripts/connect.ts | 19 ++ .../src/app/api/rivet/[...all]/route.ts | 6 + examples/counter-next-js/src/app/globals.css | 220 ++++++++++++++++++ examples/counter-next-js/src/app/layout.tsx | 32 +++ examples/counter-next-js/src/app/page.tsx | 5 + .../src/components/Counter.tsx | 103 ++++++++ .../counter-next-js/src/rivet/registry.ts | 21 ++ examples/counter-next-js/tsconfig.json | 27 +++ .../src/backend/registry.ts | 7 +- .../kitchen-sink/src/backend/actors/demo.ts | 2 +- .../src/backend/actors/websocket.ts | 8 +- examples/kitchen-sink/src/frontend/App.tsx | 6 +- .../frontend/components/ConnectionScreen.tsx | 22 -- .../frontend/components/InteractionScreen.tsx | 2 +- .../components/tabs/ConnectionsTab.tsx | 4 - .../raw-fetch-handler/src/backend/registry.ts | 2 +- 31 files changed, 826 insertions(+), 42 deletions(-) create mode 100644 examples/cloudflare-workers-inline-client/README.md create mode 100644 examples/cloudflare-workers-inline-client/package.json create mode 100644 examples/cloudflare-workers-inline-client/scripts/client-http.ts create mode 100644 examples/cloudflare-workers-inline-client/scripts/client-rivetkit.ts create mode 100644 examples/cloudflare-workers-inline-client/src/index.ts create mode 100644 examples/cloudflare-workers-inline-client/src/registry.ts create mode 100644 examples/cloudflare-workers-inline-client/tsconfig.json create mode 100644 examples/cloudflare-workers-inline-client/turbo.json create mode 100644 examples/cloudflare-workers-inline-client/wrangler.json create mode 100644 examples/counter-next-js/.gitignore create mode 100644 examples/counter-next-js/README.md create mode 100644 examples/counter-next-js/next.config.ts create mode 100644 examples/counter-next-js/package.json create mode 100644 examples/counter-next-js/scripts/connect.ts create mode 100644 examples/counter-next-js/src/app/api/rivet/[...all]/route.ts create mode 100644 examples/counter-next-js/src/app/globals.css create mode 100644 examples/counter-next-js/src/app/layout.tsx create mode 100644 examples/counter-next-js/src/app/page.tsx create mode 100644 examples/counter-next-js/src/components/Counter.tsx create mode 100644 examples/counter-next-js/src/rivet/registry.ts create mode 100644 examples/counter-next-js/tsconfig.json diff --git a/examples/cloudflare-workers-inline-client/README.md b/examples/cloudflare-workers-inline-client/README.md new file mode 100644 index 0000000000..a522c3f103 --- /dev/null +++ b/examples/cloudflare-workers-inline-client/README.md @@ -0,0 +1,33 @@ +# Cloudflare Workers Inline Client Example + +Simple example demonstrating accessing Rivet Actors via Cloudflare Workers without exposing a public API. This uses the `createInlineClient` function to connect directly to your Durable Object. + +## Getting Started + +Install dependencies: + +```sh +pnpm install +``` + +Start the development server: + +```sh +pnpm run dev +``` + +In a separate terminal, test the endpoint: + +```sh +pnpm run client-http +``` + +Or: + +```sh +pnpm run client-rivetkit +``` + +## License + +Apache 2.0 diff --git a/examples/cloudflare-workers-inline-client/package.json b/examples/cloudflare-workers-inline-client/package.json new file mode 100644 index 0000000000..c02f508085 --- /dev/null +++ b/examples/cloudflare-workers-inline-client/package.json @@ -0,0 +1,25 @@ +{ + "name": "example-cloudflare-workers-inline-client", + "version": "2.0.21", + "private": true, + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "check-types": "tsc --noEmit", + "client-http": "tsx scripts/client-http.ts", + "client-rivetkit": "tsx scripts/client-rivetkit.ts" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250129.0", + "@types/node": "^22.13.9", + "tsx": "^3.12.7", + "typescript": "^5.5.2", + "wrangler": "^4.22.0" + }, + "dependencies": { + "rivetkit": "workspace:*", + "@rivetkit/cloudflare-workers": "workspace:*" + }, + "stableVersion": "0.8.0" +} diff --git a/examples/cloudflare-workers-inline-client/scripts/client-http.ts b/examples/cloudflare-workers-inline-client/scripts/client-http.ts new file mode 100644 index 0000000000..bdd5d51e5d --- /dev/null +++ b/examples/cloudflare-workers-inline-client/scripts/client-http.ts @@ -0,0 +1,24 @@ +const baseUrl = process.env.BASE_URL ?? "http://localhost:8787"; + +async function main() { + console.log("🚀 Cloudflare Workers Client Demo"); + + try { + for (let i = 0; i < 3; i++) { + // Increment counter + console.log("Incrementing counter..."); + const response = await fetch(`${baseUrl}/increment/demo`, { + method: "POST", + }); + const result = await response.text(); + console.log(result); + } + + console.log("✅ Demo completed!"); + } catch (error) { + console.error("❌ Error:", error); + process.exit(1); + } +} + +main().catch(console.error); diff --git a/examples/cloudflare-workers-inline-client/scripts/client-rivetkit.ts b/examples/cloudflare-workers-inline-client/scripts/client-rivetkit.ts new file mode 100644 index 0000000000..0cd1fbce46 --- /dev/null +++ b/examples/cloudflare-workers-inline-client/scripts/client-rivetkit.ts @@ -0,0 +1,31 @@ +import { createClient } from "rivetkit/client"; +import type { registry } from "../src/registry"; + +// Create RivetKit client +const client = createClient( + process.env.RIVETKIT_ENDPOINT ?? "http://localhost:8787/rivet", +); + +async function main() { + console.log("🚀 Cloudflare Workers Client Demo"); + + try { + const counter = client.counter.getOrCreate("demo").connect(); + + for (let i = 0; i < 3; i++) { + // Increment counter + console.log("Incrementing counter..."); + const result1 = await counter.increment(1); + console.log("New count:", result1); + } + + await counter.dispose(); + + console.log("✅ Demo completed!"); + } catch (error) { + console.error("❌ Error:", error); + process.exit(1); + } +} + +main().catch(console.error); diff --git a/examples/cloudflare-workers-inline-client/src/index.ts b/examples/cloudflare-workers-inline-client/src/index.ts new file mode 100644 index 0000000000..0c7db7d9fc --- /dev/null +++ b/examples/cloudflare-workers-inline-client/src/index.ts @@ -0,0 +1,43 @@ +import { createInlineClient } from "@rivetkit/cloudflare-workers"; +import { registry } from "./registry"; + +const { + client, + fetch: rivetFetch, + ActorHandler, +} = createInlineClient(registry); + +// IMPORTANT: Your Durable Object must be exported here +export { ActorHandler }; + +export default { + fetch: async (request, env, ctx) => { + const url = new URL(request.url); + + // Custom request handler + if ( + request.method === "POST" && + url.pathname.startsWith("/increment/") + ) { + const name = url.pathname.slice("/increment/".length); + + const counter = client.counter.getOrCreate(name); + const newCount = await counter.increment(1); + + return new Response(`New Count: ${newCount}`, { + headers: { "Content-Type": "text/plain" }, + }); + } + + // Optional: If you want to access Rivet Actors publicly, mount the path + if (url.pathname.startsWith("/rivet")) { + const strippedPath = url.pathname.substring("/rivet".length); + url.pathname = strippedPath; + console.log("URL", url.toString()); + const modifiedRequest = new Request(url.toString(), request); + return rivetFetch(modifiedRequest, env, ctx); + } + + return new Response("Not Found", { status: 404 }); + }, +} satisfies ExportedHandler; diff --git a/examples/cloudflare-workers-inline-client/src/registry.ts b/examples/cloudflare-workers-inline-client/src/registry.ts new file mode 100644 index 0000000000..4afe732a3c --- /dev/null +++ b/examples/cloudflare-workers-inline-client/src/registry.ts @@ -0,0 +1,16 @@ +import { actor, setup } from "rivetkit"; + +export const counter = actor({ + state: { count: 0 }, + actions: { + increment: (c, x: number) => { + c.state.count += x; + c.broadcast("newCount", c.state.count); + return c.state.count; + }, + }, +}); + +export const registry = setup({ + use: { counter }, +}); diff --git a/examples/cloudflare-workers-inline-client/tsconfig.json b/examples/cloudflare-workers-inline-client/tsconfig.json new file mode 100644 index 0000000000..f4bdc4cddf --- /dev/null +++ b/examples/cloudflare-workers-inline-client/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "esnext", + /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["esnext"], + /* Specify what JSX code is generated. */ + "jsx": "react-jsx", + + /* Specify what module code is generated. */ + "module": "esnext", + /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "bundler", + /* Specify type package names to be included without being referenced in a source file. */ + "types": ["@cloudflare/workers-types"], + /* Enable importing .json files */ + "resolveJsonModule": true, + + /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + "allowJs": true, + /* Enable error reporting in type-checked JavaScript files. */ + "checkJs": false, + + /* Disable emitting files from a compilation. */ + "noEmit": true, + + /* Ensure that each file can be safely transpiled without relying on other imports. */ + "isolatedModules": true, + /* Allow 'import x from y' when a module doesn't have a default export. */ + "allowSyntheticDefaultImports": true, + /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true, + + /* Enable all strict type-checking options. */ + "strict": true, + + /* Skip type checking all .d.ts files. */ + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/examples/cloudflare-workers-inline-client/turbo.json b/examples/cloudflare-workers-inline-client/turbo.json new file mode 100644 index 0000000000..29d4cb2625 --- /dev/null +++ b/examples/cloudflare-workers-inline-client/turbo.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"] +} diff --git a/examples/cloudflare-workers-inline-client/wrangler.json b/examples/cloudflare-workers-inline-client/wrangler.json new file mode 100644 index 0000000000..f5b84c4ef6 --- /dev/null +++ b/examples/cloudflare-workers-inline-client/wrangler.json @@ -0,0 +1,30 @@ +{ + "name": "rivetkit-cloudflare-workers-example", + "main": "src/index.ts", + "compatibility_date": "2025-01-20", + "compatibility_flags": ["nodejs_compat"], + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["ActorHandler"] + } + ], + "durable_objects": { + "bindings": [ + { + "name": "ACTOR_DO", + "class_name": "ActorHandler" + } + ] + }, + "kv_namespaces": [ + { + "binding": "ACTOR_KV", + "id": "example_namespace", + "preview_id": "example_namespace_preview" + } + ], + "observability": { + "enabled": true + } +} diff --git a/examples/cloudflare-workers/src/registry.ts b/examples/cloudflare-workers/src/registry.ts index 24277ebeb8..4afe732a3c 100644 --- a/examples/cloudflare-workers/src/registry.ts +++ b/examples/cloudflare-workers/src/registry.ts @@ -1,7 +1,7 @@ import { actor, setup } from "rivetkit"; export const counter = actor({ - state: { count: 0, connectionCount: 0, messageCount: 0 }, + state: { count: 0 }, actions: { increment: (c, x: number) => { c.state.count += x; diff --git a/examples/cloudflare-workers/wrangler.json b/examples/cloudflare-workers/wrangler.json index 29b055cf3f..f5b84c4ef6 100644 --- a/examples/cloudflare-workers/wrangler.json +++ b/examples/cloudflare-workers/wrangler.json @@ -6,7 +6,7 @@ "migrations": [ { "tag": "v1", - "new_classes": ["ActorHandler"] + "new_sqlite_classes": ["ActorHandler"] } ], "durable_objects": { diff --git a/examples/counter-next-js/.gitignore b/examples/counter-next-js/.gitignore new file mode 100644 index 0000000000..5ef6a52078 --- /dev/null +++ b/examples/counter-next-js/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/counter-next-js/README.md b/examples/counter-next-js/README.md new file mode 100644 index 0000000000..4409287b70 --- /dev/null +++ b/examples/counter-next-js/README.md @@ -0,0 +1,54 @@ +# Counter for RivetKit (Next.js) + +Example Next.js project demonstrating basic actor state management and real-time updates with [RivetKit](https://rivetkit.org). + +This example combines the counter functionality from the basic counter example with a Next.js application structure. + +[Learn More →](https://github.com/rivet-dev/rivetkit) + +[Discord](https://rivet.dev/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-dev/rivetkit/issues) + +## Getting Started + +### Prerequisites + +- Node.js + +### Installation + +```sh +git clone https://github.com/rivet-dev/rivetkit +cd rivetkit/examples/counter-next-js +pnpm install +``` + +### Development + +```sh +pnpm dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the counter in action. + +The counter is shared across all clients using the same Counter ID. Try opening the page in multiple tabs or browsers to see real-time synchronization! + +### Testing with the Connect Script + +Run the connect script to interact with the counter from the command line: + +```sh +pnpm connect +``` + +This will connect to the counter and increment it every second. You'll see the updates in both the terminal and the web interface! + +## Features + +- Real-time counter synchronization across multiple clients +- Next.js 15 with App Router +- TypeScript support +- Customizable counter IDs for multiple independent counters + +## License + +Apache 2.0 diff --git a/examples/counter-next-js/next.config.ts b/examples/counter-next-js/next.config.ts new file mode 100644 index 0000000000..7921f35d74 --- /dev/null +++ b/examples/counter-next-js/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/examples/counter-next-js/package.json b/examples/counter-next-js/package.json new file mode 100644 index 0000000000..5397b59dbe --- /dev/null +++ b/examples/counter-next-js/package.json @@ -0,0 +1,27 @@ +{ + "name": "example-counter-next-js", + "version": "2.0.21", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "connect": "tsx scripts/connect.ts" + }, + "dependencies": { + "react": "19.1.0", + "react-dom": "19.1.0", + "next": "15.4.5", + "@rivetkit/next-js": "workspace:*", + "rivetkit": "workspace:*" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tsx": "^3.12.7" + }, + "stableVersion": "0.8.0" +} diff --git a/examples/counter-next-js/scripts/connect.ts b/examples/counter-next-js/scripts/connect.ts new file mode 100644 index 0000000000..9d00969e18 --- /dev/null +++ b/examples/counter-next-js/scripts/connect.ts @@ -0,0 +1,19 @@ +import { createClient } from "rivetkit/client"; +import type { registry } from "../src/rivet/registry"; + +async function main() { + const client = createClient("http://localhost:3000/api/rivet"); + + const counter = client.counter.getOrCreate().connect(); + + counter.on("newCount", (count: number) => console.log("Event:", count)); + + while (true) { + const out = await counter.increment(1); + console.log("RPC:", out); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + } +} + +main(); diff --git a/examples/counter-next-js/src/app/api/rivet/[...all]/route.ts b/examples/counter-next-js/src/app/api/rivet/[...all]/route.ts new file mode 100644 index 0000000000..a9ebd3e0a7 --- /dev/null +++ b/examples/counter-next-js/src/app/api/rivet/[...all]/route.ts @@ -0,0 +1,6 @@ +import { toNextHandler } from "@rivetkit/next-js"; +import { registry } from "@/rivet/registry"; + +export const maxDuration = 300; + +export const { GET, POST, PUT, PATCH, HEAD, OPTIONS } = toNextHandler(registry); diff --git a/examples/counter-next-js/src/app/globals.css b/examples/counter-next-js/src/app/globals.css new file mode 100644 index 0000000000..87993fb782 --- /dev/null +++ b/examples/counter-next-js/src/app/globals.css @@ -0,0 +1,220 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, sans-serif; + background: #000000; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + color: #ffffff; + padding: 20px; +} + +.counter-app { + width: 100%; + max-width: 500px; + margin: 0 auto; +} + +.counter-container { + background: #1c1c1e; + border-radius: 16px; + border: 1px solid #2c2c2e; + overflow: hidden; +} + +.counter-header { + padding: 24px; + border-bottom: 1px solid #2c2c2e; + display: flex; + justify-content: space-between; + align-items: center; +} + +.counter-header h1 { + font-size: 24px; + font-weight: 600; + color: #ffffff; +} + +.status-indicator { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 500; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + transition: background-color 0.2s ease; +} + +.status-indicator.connected .status-dot { + background-color: #30d158; +} + +.status-indicator.disconnected .status-dot { + background-color: #ff3b30; +} + +.status-indicator.connected { + color: #30d158; +} + +.status-indicator.disconnected { + color: #ff3b30; +} + +.counter-settings { + padding: 24px; + border-bottom: 1px solid #2c2c2e; +} + +.setting-group { + margin-bottom: 0; +} + +.setting-group label { + display: block; + font-size: 14px; + font-weight: 600; + color: #8e8e93; + margin-bottom: 8px; +} + +.setting-input { + width: 100%; + padding: 10px 14px; + border: 1px solid #3a3a3c; + border-radius: 8px; + font-size: 14px; + transition: all 0.2s ease; + background: #2c2c2e; + color: #ffffff; +} + +.setting-input:focus { + outline: none; + border-color: #007aff; + box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1); +} + +.setting-input::placeholder { + color: #8e8e93; +} + +.counter-display { + padding: 48px 24px; + text-align: center; + background: #000000; +} + +.count-value { + font-size: 72px; + font-weight: 700; + color: #007aff; + line-height: 1; + margin-bottom: 12px; + font-variant-numeric: tabular-nums; +} + +.count-label { + font-size: 16px; + color: #8e8e93; + font-weight: 500; +} + +.counter-controls { + padding: 24px; + display: flex; + gap: 12px; + border-bottom: 1px solid #2c2c2e; +} + +.counter-button { + flex: 1; + padding: 16px; + border: none; + border-radius: 12px; + font-size: 18px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + color: white; +} + +.counter-button.increment-1 { + background: #007aff; +} + +.counter-button.increment-1:hover:not(:disabled) { + background: #0056cc; + transform: translateY(-2px); +} + +.counter-button.increment-5 { + background: #34c759; +} + +.counter-button.increment-5:hover:not(:disabled) { + background: #28a745; + transform: translateY(-2px); +} + +.counter-button.increment-10 { + background: #ff9500; +} + +.counter-button.increment-10:hover:not(:disabled) { + background: #e68600; + transform: translateY(-2px); +} + +.counter-button:disabled { + background: #3a3a3c; + cursor: not-allowed; + color: #8e8e93; + transform: none; +} + +.info-box { + padding: 24px; + background: #2c2c2e; +} + +.info-box p { + font-size: 14px; + color: #8e8e93; + line-height: 1.6; + margin-bottom: 8px; +} + +.info-box p:last-child { + margin-bottom: 0; +} + +@media (max-width: 480px) { + .counter-header h1 { + font-size: 20px; + } + + .count-value { + font-size: 56px; + } + + .counter-controls { + flex-direction: column; + } + + .counter-button { + width: 100%; + } +} diff --git a/examples/counter-next-js/src/app/layout.tsx b/examples/counter-next-js/src/app/layout.tsx new file mode 100644 index 0000000000..c4f1400687 --- /dev/null +++ b/examples/counter-next-js/src/app/layout.tsx @@ -0,0 +1,32 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Rivet Counter", + description: "Real-time counter powered by RivetKit", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/examples/counter-next-js/src/app/page.tsx b/examples/counter-next-js/src/app/page.tsx new file mode 100644 index 0000000000..97cba5bdfc --- /dev/null +++ b/examples/counter-next-js/src/app/page.tsx @@ -0,0 +1,5 @@ +import { Counter } from "@/components/Counter"; + +export default function Home() { + return ; +} diff --git a/examples/counter-next-js/src/components/Counter.tsx b/examples/counter-next-js/src/components/Counter.tsx new file mode 100644 index 0000000000..2ed06b99d6 --- /dev/null +++ b/examples/counter-next-js/src/components/Counter.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { createRivetKit } from "@rivetkit/next-js/client"; +import { useEffect, useState } from "react"; +import type { registry } from "../rivet/registry"; + +export const { useActor } = createRivetKit({ + endpoint: process.env.NEXT_PUBLIC_RIVET_ENDPOINT ?? "http://localhost:3000/api/rivet", + namespace: process.env.NEXT_PUBLIC_RIVET_NAMESPACE, + token: process.env.NEXT_PUBLIC_RIVET_TOKEN, +}); + +export function Counter() { + const [counterId, setCounterId] = useState("default"); + const [count, setCount] = useState(0); + const [isConnected, setIsConnected] = useState(false); + + const counter = useActor({ + name: "counter", + key: [counterId], + }); + + useEffect(() => { + if (counter.connection) { + setIsConnected(true); + counter.connection.getCount().then(setCount); + } else { + setIsConnected(false); + } + }, [counter.connection]); + + counter.useEvent("newCount", (newCount: number) => { + setCount(newCount); + }); + + const increment = async (amount: number) => { + if (counter.connection) { + await counter.connection.increment(amount); + } + }; + + return ( +
+
+
+

Counter Demo

+
+
+ {isConnected ? 'Connected' : 'Disconnected'} +
+
+ +
+
+ + setCounterId(e.target.value)} + placeholder="Enter counter ID" + className="setting-input" + /> +
+
+ +
+
{count}
+

Current Count

+
+ +
+ + + +
+ +
+

This counter is shared across all clients using the same Counter ID.

+

Try opening this page in multiple tabs or browsers!

+
+
+
+ ); +} diff --git a/examples/counter-next-js/src/rivet/registry.ts b/examples/counter-next-js/src/rivet/registry.ts new file mode 100644 index 0000000000..1e6cdc4fca --- /dev/null +++ b/examples/counter-next-js/src/rivet/registry.ts @@ -0,0 +1,21 @@ +import { actor, setup } from "rivetkit"; + +const counter = actor({ + state: { + count: 0, + }, + actions: { + increment: (c, x: number) => { + c.state.count += x; + c.broadcast("newCount", c.state.count); + return c.state.count; + }, + getCount: (c) => { + return c.state.count; + }, + }, +}); + +export const registry = setup({ + use: { counter }, +}); diff --git a/examples/counter-next-js/tsconfig.json b/examples/counter-next-js/tsconfig.json new file mode 100644 index 0000000000..7df89e76da --- /dev/null +++ b/examples/counter-next-js/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/cursors-raw-websocket/src/backend/registry.ts b/examples/cursors-raw-websocket/src/backend/registry.ts index a7c6a1954a..a2598d5ea4 100644 --- a/examples/cursors-raw-websocket/src/backend/registry.ts +++ b/examples/cursors-raw-websocket/src/backend/registry.ts @@ -57,7 +57,12 @@ export const cursorRoom = actor({ // Handle WebSocket connections onWebSocket: async (c, websocket: UniversalWebSocket) => { - const url = new URL(request.url); + if (!c.request) { + websocket.close(1008, "no request"); + return; + } + + const url = new URL(c.request.url); const sessionId = url.searchParams.get("sessionId"); if (!sessionId) { diff --git a/examples/kitchen-sink/src/backend/actors/demo.ts b/examples/kitchen-sink/src/backend/actors/demo.ts index deb5360bd3..b29887321e 100644 --- a/examples/kitchen-sink/src/backend/actors/demo.ts +++ b/examples/kitchen-sink/src/backend/actors/demo.ts @@ -35,7 +35,7 @@ export const demo = actor({ onDisconnect: (c) => { c.log.info("client disconnected"); }, - onFetch: handleHttpRequest, + onRequest: handleHttpRequest, onWebSocket: handleWebSocket, actions: { // Sync actions diff --git a/examples/kitchen-sink/src/backend/actors/websocket.ts b/examples/kitchen-sink/src/backend/actors/websocket.ts index ebb0d69b78..9ac319ed58 100644 --- a/examples/kitchen-sink/src/backend/actors/websocket.ts +++ b/examples/kitchen-sink/src/backend/actors/websocket.ts @@ -1,10 +1,6 @@ import type { UniversalWebSocket } from "rivetkit"; -export function handleWebSocket( - c: any, - websocket: UniversalWebSocket, - opts: any, -) { +export function handleWebSocket(c: any, websocket: UniversalWebSocket) { const connectionId = `conn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Initialize WebSocket state if not exists @@ -16,7 +12,7 @@ export function handleWebSocket( c.log.info("websocket connected", { connectionCount: c.state.connectionCount, connectionId, - url: opts.request.url, + url: c.request.url, }); // Send welcome message diff --git a/examples/kitchen-sink/src/frontend/App.tsx b/examples/kitchen-sink/src/frontend/App.tsx index 7bda68bfec..df59ac96c2 100644 --- a/examples/kitchen-sink/src/frontend/App.tsx +++ b/examples/kitchen-sink/src/frontend/App.tsx @@ -6,7 +6,6 @@ import InteractionScreen from "./components/InteractionScreen"; export interface AppState { // Configuration - transport: "websocket" | "sse"; encoding: "json" | "cbor" | "bare"; connectionMode: "handle" | "connection"; @@ -38,16 +37,15 @@ function App() { setState(prev => prev ? { ...prev, ...updates } : null); }; - // Create client with user-selected encoding and transport + // Create client with user-selected encoding const client = useMemo(() => { if (!state) return null; return createClient({ endpoint: "http://localhost:6420", encoding: state.encoding, - transport: state.transport, }); - }, [state?.encoding, state?.transport]); + }, [state?.encoding]); // Create the connection/handle once based on state const [actorHandle, setActorHandle] = useState(null); diff --git a/examples/kitchen-sink/src/frontend/components/ConnectionScreen.tsx b/examples/kitchen-sink/src/frontend/components/ConnectionScreen.tsx index f47bcb970a..f6d5386da1 100644 --- a/examples/kitchen-sink/src/frontend/components/ConnectionScreen.tsx +++ b/examples/kitchen-sink/src/frontend/components/ConnectionScreen.tsx @@ -14,7 +14,6 @@ export default function ConnectionScreen({ onConnect }: ConnectionScreenProps) { const [actorId, setActorId] = useState(""); const [actorRegion, setActorRegion] = useState(""); const [createInput, setCreateInput] = useState(""); - const [transport, setTransport] = useState<"websocket" | "sse">("websocket"); const [encoding, setEncoding] = useState<"json" | "cbor" | "bare">("bare"); const [connectionMode, setConnectionMode] = useState<"connection" | "handle">("handle"); const [isConnecting, setIsConnecting] = useState(false); @@ -29,7 +28,6 @@ export default function ConnectionScreen({ onConnect }: ConnectionScreenProps) { actorId, actorRegion, createInput, - transport, encoding, connectionMode, isConnected: true, @@ -162,26 +160,6 @@ export default function ConnectionScreen({ onConnect }: ConnectionScreenProps) { - {connectionMode === "connection" && ( -
- -
- - -
-
- )} -
diff --git a/examples/kitchen-sink/src/frontend/components/InteractionScreen.tsx b/examples/kitchen-sink/src/frontend/components/InteractionScreen.tsx index ca6f1cca4e..ff64c10e44 100644 --- a/examples/kitchen-sink/src/frontend/components/InteractionScreen.tsx +++ b/examples/kitchen-sink/src/frontend/components/InteractionScreen.tsx @@ -101,7 +101,7 @@ export default function InteractionScreen({
{state.actorName} {state.actorKey && #{state.actorKey}} - {state.transport.toUpperCase()}/{state.encoding.toUpperCase()} + {state.encoding.toUpperCase()} {state.connectionMode === "connection" ? "🔗 Connected" : "🔧 Handle"} diff --git a/examples/kitchen-sink/src/frontend/components/tabs/ConnectionsTab.tsx b/examples/kitchen-sink/src/frontend/components/tabs/ConnectionsTab.tsx index 2567495e8e..5b5bf41c62 100644 --- a/examples/kitchen-sink/src/frontend/components/tabs/ConnectionsTab.tsx +++ b/examples/kitchen-sink/src/frontend/components/tabs/ConnectionsTab.tsx @@ -24,7 +24,6 @@ export default function ConnectionsTab({ state, actorHandle }: TabProps) { setCurrentConnectionInfo({ isConnected: true, connectionId: actorHandle.connectionId || "N/A", - transport: state.transport, encoding: state.encoding, actorName: state.actorName, actorKey: state.actorKey, @@ -87,9 +86,6 @@ export default function ConnectionsTab({ state, actorHandle }: TabProps) { {currentConnectionInfo.connectionId} - Transport: - {currentConnectionInfo.transport.toUpperCase()} - Encoding: {currentConnectionInfo.encoding.toUpperCase()} diff --git a/examples/raw-fetch-handler/src/backend/registry.ts b/examples/raw-fetch-handler/src/backend/registry.ts index 761c1cc3d1..b27d945857 100644 --- a/examples/raw-fetch-handler/src/backend/registry.ts +++ b/examples/raw-fetch-handler/src/backend/registry.ts @@ -9,7 +9,7 @@ export const counter = actor({ // Setup router return { router: createCounterRouter() }; }, - onFetch: (c, request) => { + onRequest: (c, request) => { return c.vars.router.fetch(request, { actor: c }); }, actions: {