diff --git a/engine/sdks/typescript/runner-protocol/src/index.ts b/engine/sdks/typescript/runner-protocol/src/index.ts index 343802861a..c6405665cb 100644 --- a/engine/sdks/typescript/runner-protocol/src/index.ts +++ b/engine/sdks/typescript/runner-protocol/src/index.ts @@ -1,4 +1,4 @@ - +import assert from "node:assert" import * as bare from "@bare-ts/lib" const DEFAULT_CONFIG = /* @__PURE__ */ bare.Config({}) @@ -1906,9 +1906,3 @@ export function decodeToServerlessServer(bytes: Uint8Array): ToServerlessServer } return result } - - -function assert(condition: boolean, message?: string): asserts condition { - if (!condition) throw new Error(message ?? "Assertion failed") -} - diff --git a/examples/cloudflare-workers-inline-client/README.md b/examples/cloudflare-workers-inline-client/README.md new file mode 100644 index 0000000000..6b1ecf9c3e --- /dev/null +++ b/examples/cloudflare-workers-inline-client/README.md @@ -0,0 +1,27 @@ +# 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 +``` + +## 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..1cc3163a45 --- /dev/null +++ b/examples/cloudflare-workers-inline-client/package.json @@ -0,0 +1,24 @@ +{ + "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": "tsx scripts/client.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.ts b/examples/cloudflare-workers-inline-client/scripts/client.ts new file mode 100644 index 0000000000..560891aa83 --- /dev/null +++ b/examples/cloudflare-workers-inline-client/scripts/client.ts @@ -0,0 +1,38 @@ +const baseUrl = process.env.BASE_URL ?? "http://localhost:8787"; + +async function main() { + console.log("🚀 Cloudflare Workers Client Demo"); + + try { + // Increment counter 'demo' + console.log("Incrementing counter 'demo'..."); + const response1 = await fetch(`${baseUrl}/increment/demo`, { + method: "POST", + }); + const result1 = await response1.text(); + console.log(result1); + + // Increment counter 'demo' again + console.log("Incrementing counter 'demo' again..."); + const response2 = await fetch(`${baseUrl}/increment/demo`, { + method: "POST", + }); + const result2 = await response2.text(); + console.log(result2); + + // Increment counter 'another' + console.log("Incrementing counter 'another'..."); + const response3 = await fetch(`${baseUrl}/increment/another`, { + method: "POST", + }); + const result3 = await response3.text(); + console.log(result3); + + 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..a2f225baf6 --- /dev/null +++ b/examples/cloudflare-workers-inline-client/src/index.ts @@ -0,0 +1,29 @@ +import { createInlineClient } from "@rivetkit/cloudflare-workers"; +import { registry } from "./registry"; + +const { client, ActorHandler } = createInlineClient(registry); + +// IMPORTANT: Your Durable Object must be exported here +export { ActorHandler }; + +export default { + fetch: async (request) => { + const url = new URL(request.url); + + 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" }, + }); + } + + 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 04665f8cac..2700ec6581 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,12 @@ importers: specifier: ^8.8.5 version: 8.8.5 + engine: + dependencies: + '@vbare/compiler': + specifier: ^0.0.3 + version: 0.0.3(@bare-ts/lib@0.4.0) + engine/docker/template: dependencies: '@types/js-yaml': @@ -493,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.18.1 + tsx: + specifier: ^3.12.7 + version: 3.14.0 + typescript: + specifier: ^5.5.2 + version: 5.9.2 + wrangler: + specifier: ^4.22.0 + version: 4.44.0(@cloudflare/workers-types@4.20251014.0) + examples/counter: devDependencies: '@types/node': @@ -2331,6 +2362,9 @@ importers: '@bare-ts/tools': specifier: ^0.13.0 version: 0.13.0(@bare-ts/lib@0.3.0) + '@biomejs/biome': + specifier: ^2.2.3 + version: 2.2.3 '@hono/node-server': specifier: ^1.18.2 version: 1.19.1(hono@4.9.8) @@ -3250,6 +3284,13 @@ packages: peerDependencies: '@bare-ts/lib': '>=0.3.0 <=0.4.0' + '@bare-ts/tools@0.16.1': + resolution: {integrity: sha512-eKXTnVqzuKDxr1ozKsFSZfM1wcN4g/iMRnG9GB2fA8oyUcHxwokJC50CANfuSLe6rLnjhZ8Ave1Y2TnZqUqGcQ==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@bare-ts/lib': '>=0.3.0 <=0.4.0' + '@base-org/account@2.0.1': resolution: {integrity: sha512-tySVNx+vd6XEynZL0uvB10uKiwnAfThr8AbKTwILVG86mPbLAhEOInQIk+uDnvpTvfdUhC1Bi5T/46JvFoLZQQ==} @@ -7021,6 +7062,11 @@ packages: peerDependencies: '@urql/core': ^5.0.0 + '@vbare/compiler@0.0.3': + resolution: {integrity: sha512-Dhz0iwYjIhyGAPsNpiqDmDqgwLXfEonjFJLVQ0m/s4Tt9CsTjY0WV3KiQtJi5BdPt9481HR+0uwExH36FuuR2A==} + engines: {node: '>=18.0.0'} + hasBin: true + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -14436,6 +14482,10 @@ snapshots: '@bare-ts/lib': 0.4.0 commander: 11.1.0 + '@bare-ts/tools@0.16.1(@bare-ts/lib@0.4.0)': + dependencies: + '@bare-ts/lib': 0.4.0 + '@base-org/account@2.0.1(@types/react@19.2.2)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.1.1))(zod@3.25.76)': dependencies: '@noble/hashes': 1.4.0 @@ -18687,6 +18737,13 @@ snapshots: '@urql/core': 5.2.0 wonka: 6.3.5 + '@vbare/compiler@0.0.3(@bare-ts/lib@0.4.0)': + dependencies: + '@bare-ts/tools': 0.16.1(@bare-ts/lib@0.4.0) + commander: 11.1.0 + transitivePeerDependencies: + - '@bare-ts/lib' + '@vitejs/plugin-react@4.7.0(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))': dependencies: '@babel/core': 7.28.4 diff --git a/rivetkit-openapi/openapi.json b/rivetkit-openapi/openapi.json index 90803b4757..4ab454fa07 100644 --- a/rivetkit-openapi/openapi.json +++ b/rivetkit-openapi/openapi.json @@ -113,6 +113,7 @@ }, "put": { "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -225,6 +226,7 @@ }, "post": { "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -385,283 +387,6 @@ } } } - }, - "/gateway/{actorId}/health": { - "get": { - "parameters": [ - { - "name": "actorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The ID of the actor to target" - } - ], - "responses": { - "200": { - "description": "Health check", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, - "/gateway/{actorId}/action/{action}": { - "post": { - "parameters": [ - { - "name": "actorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The ID of the actor to target" - }, - { - "name": "action", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The name of the action to execute" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "args": {} - }, - "additionalProperties": false - } - } - } - }, - "responses": { - "200": { - "description": "Action executed successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "output": {} - }, - "additionalProperties": false - } - } - } - }, - "400": { - "description": "Invalid action" - }, - "500": { - "description": "Internal error" - } - } - } - }, - "/gateway/{actorId}/request/{path}": { - "get": { - "parameters": [ - { - "name": "actorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The ID of the actor to target" - }, - { - "name": "path", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The HTTP path to forward to the actor" - } - ], - "responses": { - "200": { - "description": "Response from actor's raw HTTP handler" - } - } - }, - "post": { - "parameters": [ - { - "name": "actorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The ID of the actor to target" - }, - { - "name": "path", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The HTTP path to forward to the actor" - } - ], - "responses": { - "200": { - "description": "Response from actor's raw HTTP handler" - } - } - }, - "put": { - "parameters": [ - { - "name": "actorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The ID of the actor to target" - }, - { - "name": "path", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The HTTP path to forward to the actor" - } - ], - "responses": { - "200": { - "description": "Response from actor's raw HTTP handler" - } - } - }, - "delete": { - "parameters": [ - { - "name": "actorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The ID of the actor to target" - }, - { - "name": "path", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The HTTP path to forward to the actor" - } - ], - "responses": { - "200": { - "description": "Response from actor's raw HTTP handler" - } - } - }, - "patch": { - "parameters": [ - { - "name": "actorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The ID of the actor to target" - }, - { - "name": "path", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The HTTP path to forward to the actor" - } - ], - "responses": { - "200": { - "description": "Response from actor's raw HTTP handler" - } - } - }, - "head": { - "parameters": [ - { - "name": "actorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The ID of the actor to target" - }, - { - "name": "path", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The HTTP path to forward to the actor" - } - ], - "responses": { - "200": { - "description": "Response from actor's raw HTTP handler" - } - } - }, - "options": { - "parameters": [ - { - "name": "actorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The ID of the actor to target" - }, - { - "name": "path", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The HTTP path to forward to the actor" - } - ], - "responses": { - "200": { - "description": "Response from actor's raw HTTP handler" - } - } - } } } } \ No newline at end of file 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";