From 72102496668d8d635362135aafd6b5242478d432 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Thu, 13 Nov 2025 14:04:11 -0800 Subject: [PATCH] feat(cloudflare-workers): add `createInlineClient` --- .../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 +- pnpm-lock.yaml | 55 +++++++++++++----- .../cloudflare-workers/src/handler.ts | 56 ++++++++++++++++--- .../packages/cloudflare-workers/src/mod.ts | 9 ++- 13 files changed, 346 insertions(+), 25 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 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/pnpm-lock.yaml b/pnpm-lock.yaml index 58e87b6255..39ce463989 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -499,6 +499,31 @@ importers: specifier: ^4.22.0 version: 4.44.0(@cloudflare/workers-types@4.20251014.0) + examples/cloudflare-workers-inline-client: + dependencies: + '@rivetkit/cloudflare-workers': + specifier: workspace:* + version: link:../../rivetkit-typescript/packages/cloudflare-workers + rivetkit: + specifier: workspace:* + version: link:../../rivetkit-typescript/packages/rivetkit + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20250129.0 + version: 4.20251014.0 + '@types/node': + specifier: ^22.13.9 + version: 22.19.1 + tsx: + specifier: ^3.12.7 + version: 3.14.0 + typescript: + specifier: ^5.5.2 + version: 5.9.3 + wrangler: + specifier: ^4.22.0 + version: 4.44.0(@cloudflare/workers-types@4.20251014.0) + examples/counter: devDependencies: '@types/node': @@ -17120,7 +17145,7 @@ snapshots: dependencies: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.18.1 + '@types/node': 22.19.1 '@types/yargs': 15.0.19 chalk: 4.1.2 @@ -19436,7 +19461,7 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.18.1 + '@types/node': 22.19.1 '@types/bun@1.3.0(@types/react@19.2.2)': dependencies: @@ -19453,11 +19478,11 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 22.18.1 + '@types/node': 22.19.1 '@types/cors@2.8.19': dependencies: - '@types/node': 24.10.1 + '@types/node': 22.19.1 '@types/d3-array@3.2.1': {} @@ -19513,7 +19538,7 @@ snapshots: '@types/express-serve-static-core@4.19.7': dependencies: - '@types/node': 22.18.1 + '@types/node': 22.19.1 '@types/qs': 6.9.8 '@types/range-parser': 1.2.7 '@types/send': 1.2.0 @@ -19604,6 +19629,7 @@ snapshots: '@types/node@24.10.1': dependencies: undici-types: 7.16.0 + optional: true '@types/node@24.3.1': dependencies: @@ -19651,16 +19677,16 @@ snapshots: '@types/send@0.17.5': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.18.1 + '@types/node': 22.19.1 '@types/send@1.2.0': dependencies: - '@types/node': 22.18.1 + '@types/node': 22.19.1 '@types/serve-static@1.15.9': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 22.18.1 + '@types/node': 22.19.1 '@types/send': 0.17.5 '@types/stack-utils@2.0.3': {} @@ -20872,7 +20898,7 @@ snapshots: bun-types@1.3.0(@types/react@19.2.2): dependencies: - '@types/node': 22.18.1 + '@types/node': 22.19.1 '@types/react': 19.2.2 bundle-require@5.1.0(esbuild@0.25.9): @@ -21592,7 +21618,7 @@ snapshots: engine.io@6.6.4: dependencies: '@types/cors': 2.8.19 - '@types/node': 24.10.1 + '@types/node': 22.19.1 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 @@ -21936,7 +21962,7 @@ snapshots: eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.1(jiti@1.21.7)) @@ -21969,7 +21995,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) transitivePeerDependencies: - supports-color @@ -21984,7 +22010,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -27559,7 +27585,8 @@ snapshots: undici-types@7.14.0: {} - undici-types@7.16.0: {} + undici-types@7.16.0: + optional: true undici@6.22.0: {} diff --git a/rivetkit-typescript/packages/cloudflare-workers/src/handler.ts b/rivetkit-typescript/packages/cloudflare-workers/src/handler.ts index 2d186564ce..3b87959152 100644 --- a/rivetkit-typescript/packages/cloudflare-workers/src/handler.ts +++ b/rivetkit-typescript/packages/cloudflare-workers/src/handler.ts @@ -1,11 +1,11 @@ import { env } from "cloudflare:workers"; -import type { Registry, RunConfig } from "rivetkit"; +import type { Client, Registry, RunConfig } from "rivetkit"; import { type ActorHandlerInterface, createActorDurableObject, type DurableObjectConstructor, } from "./actor-handler-do"; -import { ConfigSchema, type InputConfig } from "./config"; +import { type Config, ConfigSchema, type InputConfig } from "./config"; import { CloudflareActorsManagerDriver } from "./manager-driver"; import { upgradeWebSocket } from "./websocket"; @@ -24,15 +24,35 @@ export function getCloudflareAmbientEnv(): Bindings { return env as unknown as Bindings; } -interface Handler { +export interface InlineOutput> { + /** Client to communicate with the actors. */ + client: Client; + + /** Fetch handler to manually route requests to the Rivet manager API. */ + fetch: (request: Request, ...args: any) => Response | Promise; + + config: Config; + + ActorHandler: DurableObjectConstructor; +} + +export interface HandlerOutput { handler: ExportedHandler; ActorHandler: DurableObjectConstructor; } -export function createHandler>( +/** + * Creates an inline client for accessing Rivet Actors privately without a public manager API. + * + * If you want to expose a public manager API, either: + * + * - Use `createHandler` to expose the Rivet API on `/rivet` + * - Forward Rivet API requests to `InlineOutput::fetch` + */ +export function createInlineClient>( registry: R, inputConfig?: InputConfig, -): Handler { +): InlineOutput { // HACK: Cloudflare does not support using `crypto.randomUUID()` before start, so we pass a default value // // Runner key is not used on Cloudflare @@ -57,16 +77,34 @@ export function createHandler>( const ActorHandler = createActorDurableObject(registry, runConfig); // Create server - const serverOutputPromise = registry.start(runConfig); + const { client, fetch } = registry.start(runConfig); + + return { client, fetch, config, ActorHandler }; +} + +/** + * Creates a handler to be exported from a Cloudflare Worker. + * + * This will automatically expose the Rivet manager API on `/rivet`. + * + * This includes a `fetch` handler and `ActorHandler` Durable Object. + */ +export function createHandler>( + registry: R, + inputConfig?: InputConfig, +): HandlerOutput { + const { client, fetch, config, ActorHandler } = createInlineClient( + registry, + inputConfig, + ); // Create Cloudflare handler const handler = { fetch: async (request, cfEnv, ctx) => { - const serverOutput = await serverOutputPromise; const url = new URL(request.url); // Inject Rivet env - const env = Object.assign({ RIVET: serverOutput.client }, cfEnv); + const env = Object.assign({ RIVET: client }, cfEnv); // Mount Rivet manager API if (url.pathname.startsWith(config.managerPath)) { @@ -75,7 +113,7 @@ export function createHandler>( ); url.pathname = strippedPath; const modifiedRequest = new Request(url.toString(), request); - return serverOutput.fetch(modifiedRequest, env, ctx); + return fetch(modifiedRequest, env, ctx); } if (config.fetch) { diff --git a/rivetkit-typescript/packages/cloudflare-workers/src/mod.ts b/rivetkit-typescript/packages/cloudflare-workers/src/mod.ts index 12ad512c14..40129e55fe 100644 --- a/rivetkit-typescript/packages/cloudflare-workers/src/mod.ts +++ b/rivetkit-typescript/packages/cloudflare-workers/src/mod.ts @@ -1,4 +1,11 @@ export type { Client } from "rivetkit"; export type { DriverContext } from "./actor-driver"; +export { createActorDurableObject } from "./actor-handler-do"; export type { InputConfig as Config } from "./config"; -export { type Bindings, createHandler } from "./handler"; +export { + type Bindings, + createHandler, + createInlineClient, + HandlerOutput, + InlineOutput, +} from "./handler";