diff --git a/CLAUDE.md b/CLAUDE.md index 4329cb5e68..a8ea90f735 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,9 +9,8 @@ - Dashboard: `https://hub.rivet.dev` - Documentation: `https://rivet.dev/docs` -The `rivet.gg` domain is deprecated and should never be used in this codebase. - -**ALWAYS use `github.com/rivet-dev/rivet` - NEVER use `rivet-dev/rivetkit` or `rivet-gg/*`** +- The `rivet.gg` domain is deprecated and should never be used in this codebase. +- ALWAYS use `github.com/rivet-dev/rivet`; never use `rivet-dev/rivetkit` or `rivet-gg/*`. ## Commands @@ -69,7 +68,7 @@ docker-compose up -d gt c -m "chore(my-pkg): foo bar" ``` -**Never push to `main` unless explicitly specified by the user.** +- Never push to `main` unless explicitly specified by the user. ## Graphite CLI Commands ```bash @@ -82,12 +81,22 @@ gt m ### pnpm Workspace - Use pnpm for all npm-related commands. We're using a pnpm workspace. +### RivetKit Type Build Troubleshooting +- If `rivetkit` type or DTS builds fail with missing `@rivetkit/*` declarations, run `pnpm build -F rivetkit` from repo root (Turbo build path) before changing TypeScript `paths`. +- Do not add temporary `@rivetkit/*` path aliases in `rivetkit-typescript/packages/rivetkit/tsconfig.json` to work around stale or missing built declarations. + +### RivetKit Driver Registry Variants +- Keep `rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry.ts` as the canonical type anchor for fixtures and test typing. +- Run driver runtime suites through `registry-static.ts` and `registry-dynamic.ts` instead of executing `registry.ts` directly. +- Load static fixture actors with dynamic ESM `import()` from the `fixtures/driver-test-suite/actors/` directory. +- Skip dynamic registry parity only for the explicit nested dynamic harness gate or missing secure-exec dist, and still treat full static and dynamic compatibility as the target for all normal driver suites. + ### SQLite Package - Use `@rivetkit/sqlite` for SQLite WebAssembly support. - Do not use the legacy upstream package directly. `@rivetkit/sqlite` is the maintained fork used in this repository and is sourced from `rivet-dev/wa-sqlite`. ### RivetKit Package Resolutions -The root `/package.json` contains `resolutions` that map RivetKit packages to their local workspace versions: +- The root `/package.json` contains `resolutions` that map RivetKit packages to local workspace versions: ```json { @@ -100,7 +109,7 @@ The root `/package.json` contains `resolutions` that map RivetKit packages to th } ``` -When adding RivetKit dependencies to examples in `/examples/`, use `*` as the version. The root resolutions will automatically resolve these to the local workspace packages: +- Use `*` as the dependency version when adding RivetKit packages to `/examples/`, because root resolutions map them to local workspace packages: ```json { @@ -111,7 +120,19 @@ When adding RivetKit dependencies to examples in `/examples/`, use `*` as the ve } ``` -If you need to add a new `@rivetkit/*` package that isn't already in the root resolutions, add it to the `resolutions` object in `/package.json` with `"workspace:*"` as the value. Internal packages like `@rivetkit/workflow-engine` should be re-exported from `rivetkit` subpaths (e.g., `rivetkit/workflow`) rather than added as direct dependencies. +- Add new internal `@rivetkit/*` packages to root `resolutions` with `"workspace:*"` if missing, and prefer re-exporting internal packages (for example `@rivetkit/workflow-engine`) from `rivetkit` subpaths like `rivetkit/workflow` instead of direct dependencies. + +### Dynamic Import Pattern +- For runtime-only dependencies, use dynamic loading so bundlers do not eagerly include them. +- Build the module specifier from string parts (for example with `["pkg", "name"].join("-")` or `["@scope", "pkg"].join("/")`) instead of a single string literal. +- Prefer this pattern for modules like `@rivetkit/sqlite-vfs`, `sandboxed-node`, and `isolated-vm`. +- If loading by resolved file path, resolve first and then import via `pathToFileURL(...).href`. + +### Fail-By-Default Runtime Behavior +- Avoid silent no-ops for required runtime behavior. +- Do not use optional chaining for required lifecycle and bridge operations (for example sleep, destroy, alarm dispatch, ack, and websocket dispatch paths). +- If a capability is required, validate it and throw an explicit error with actionable context instead of returning early. +- Optional chaining is acceptable only for best-effort diagnostics and cleanup paths (for example logging hooks and dispose/release cleanup). ### Rust Dependencies @@ -125,7 +146,7 @@ If you need to add a new `@rivetkit/*` package that isn't already in the root re ### Docs (`website/src/content/docs/**/*.mdx`) -Required frontmatter fields: +- Required frontmatter fields: - `title` (string) - `description` (string) @@ -133,7 +154,7 @@ Required frontmatter fields: ### Blog + Changelog (`website/src/content/posts/**/page.mdx`) -Required frontmatter fields: +- Required frontmatter fields: - `title` (string) - `description` (string) @@ -141,22 +162,23 @@ Required frontmatter fields: - `published` (date string) - `category` (enum: `changelog`, `monthly-update`, `launch-week`, `technical`, `guide`, `frogs`) -Optional frontmatter fields: +- Optional frontmatter fields: - `keywords` (string array) ## Examples -All example READMEs in `/examples/` should follow the format defined in `.claude/resources/EXAMPLE_TEMPLATE.md`. +- All example READMEs in `/examples/` should follow the format defined in `.claude/resources/EXAMPLE_TEMPLATE.md`. ## Notes Tracking - When the user asks to track something in a note, store it in `.agent/notes/` by default. +- When the user asks to update any `CLAUDE.md`, add one-line bullet points only, or add a new section containing one-line bullet points. ## Architecture ### Monorepo Structure -This is a Rust workspace-based monorepo for Rivet. Key packages and components: +- This is a Rust workspace-based monorepo for Rivet with the following key packages and components: - **Core Engine** (`packages/core/engine/`) - Main orchestration service that coordinates all operations - **Workflow Engine** (`packages/common/gasoline/`) - Handles complex multi-step operations with reliability and observability @@ -172,7 +194,7 @@ This is a Rust workspace-based monorepo for Rivet. Key packages and components: - Custom error system at `packages/common/error/` - Uses derive macros with struct-based error definitions -To use custom errors: +- Use this pattern for custom errors: ```rust use rivet_error::*; @@ -201,13 +223,13 @@ let error = AuthInvalidToken.build(); let error_with_meta = ApiRateLimited { limit: 100, reset_at: 1234567890 }.build(); ``` -Key points: +- Key points: - Use `#[derive(RivetError)]` on struct definitions - Use `#[error(group, code, description)]` or `#[error(group, code, description, formatted_message)]` attribute - Group errors by module/domain (e.g., "auth", "actor", "namespace") - Add `Serialize, Deserialize` derives for errors with metadata fields - Always return anyhow errors from failable functions - - For example: `fn foo() -> Result { /* ... */ }` +- For example: `fn foo() -> Result { /* ... */ }` - Do not glob import (`::*`) from anyhow. Instead, import individual types and traits **Rust Dependency Management** @@ -233,7 +255,7 @@ Key points: ## Naming Conventions -Data structures often include: +- Data structures often include: - `id` (uuid) - `name` (machine-readable name, must be valid DNS subdomain, convention is using kebab case) @@ -252,7 +274,7 @@ Data structures often include: ### Structured Logging - Use tracing for logging. Do not format parameters into the main message, instead use tracing's structured logging. - - For example, instead of `tracing::info!("foo {x}")`, do `tracing::info!(?x, "foo")` +- For example, instead of `tracing::info!("foo {x}")`, do `tracing::info!(?x, "foo")` - Log messages should be lowercase unless mentioning specific code symbols. For example, `tracing::info!("inserted UserRow")` instead of `tracing::info!("Inserted UserRow")` ## Configuration Management @@ -278,7 +300,7 @@ Data structures often include: - When talking about "Rivet Actors" make sure to capitalize "Rivet Actor" as a proper noun and lowercase "actor" as a generic noun ### Documentation Sync -When making changes to the engine or RivetKit, ensure the corresponding documentation is updated: +- Ensure corresponding documentation is updated when making engine or RivetKit changes: - **Limits changes** (e.g., max message sizes, timeouts): Update `website/src/content/docs/actors/limits.mdx` - **Config changes** (e.g., new config options in `engine/packages/config/`): Update `website/src/content/docs/self-hosting/configuration.mdx` - **RivetKit config changes** (e.g., `rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts` or `rivetkit-typescript/packages/rivetkit/src/actor/config.ts`): Update `website/src/content/docs/actors/limits.mdx` if they affect limits/timeouts @@ -295,8 +317,8 @@ When making changes to the engine or RivetKit, ensure the corresponding document #### Common Vercel Example Errors -After regenerating Vercel examples, you may see type check errors like: +- You may see type-check errors like the following after regenerating Vercel examples: ``` error TS2688: Cannot find type definition file for 'vite/client'. ``` -with warnings about `node_modules missing`. This happens because the regenerated examples need their dependencies reinstalled. Fix by running `pnpm install` before running type checks. +- You may also see `node_modules missing` warnings; fix this by running `pnpm install` before type checks because regenerated examples need dependencies reinstalled. diff --git a/docs-internal/rivetkit-typescript/DYNAMIC_ACTORS_ARCHITECTURE.md b/docs-internal/rivetkit-typescript/DYNAMIC_ACTORS_ARCHITECTURE.md new file mode 100644 index 0000000000..6408100316 --- /dev/null +++ b/docs-internal/rivetkit-typescript/DYNAMIC_ACTORS_ARCHITECTURE.md @@ -0,0 +1,95 @@ +# Dynamic Actors Architecture + +## Overview + +Dynamic actors let a registry entry resolve actor source code at actor start time. + +Dynamic actors are represented by `dynamicActor(loader, config)` and still +participate in normal registry routing and actor lifecycle. + +Driver parity is verified by running the same driver test suites against two +fixture registries: + +- `fixtures/driver-test-suite/registry-static.ts` +- `fixtures/driver-test-suite/registry-dynamic.ts` + +Both registries are built from `fixtures/driver-test-suite/actors/` to keep +actor behavior consistent between static and dynamic execution. + +## Main Components + +- Host runtime manager: + `rivetkit-typescript/packages/rivetkit/src/dynamic/isolate-runtime.ts` + Creates and owns one `NodeProcess` isolate per dynamic actor instance. +- Isolate bootstrap runtime: + `rivetkit-typescript/packages/rivetkit/dynamic-isolate-runtime/src/index.cts` + Runs inside the isolate and exports envelope handlers. +- Bridge contract: + `rivetkit-typescript/packages/rivetkit/src/dynamic/runtime-bridge.ts` + Shared envelope and callback payload types for host and isolate. +- Driver integration: + `drivers/file-system/global-state.ts` and `drivers/engine/actor-driver.ts` + Branch on definition type, construct dynamic runtime, and proxy fetch and websocket traffic. + +## Lifecycle + +1. Driver resolves actor definition from registry. +2. If definition is dynamic, driver creates `DynamicActorIsolateRuntime`. +3. Runtime calls loader and gets `{ source, sourceFormat?, nodeProcess? }`. +4. Runtime writes source into actor runtime dir: + - `sourceFormat: "esm-js"` -> `dynamic-source.mjs` (written unchanged) + - `sourceFormat: "commonjs-js"` -> `dynamic-source.cjs` (written unchanged) + - default `sourceFormat: "typescript"` -> transpiled to `dynamic-source.cjs` +5. Runtime writes isolate bootstrap entry into actor runtime dir. +6. Runtime builds a locked down sandbox driver and creates `NodeProcess`. +7. Runtime injects host bridge refs and bootstrap config into isolate globals. +8. Runtime loads bootstrap module and captures exported envelope refs. + +## Bridge Contract + +Host to isolate calls: + +- `dynamicFetchEnvelope` +- `dynamicOpenWebSocketEnvelope` +- `dynamicWebSocketSendEnvelope` +- `dynamicWebSocketCloseEnvelope` +- `dynamicDispatchAlarmEnvelope` +- `dynamicStopEnvelope` +- `dynamicGetHibernatingWebSocketsEnvelope` +- `dynamicDisposeEnvelope` + +Isolate to host callbacks: + +- KV: `kvBatchPut`, `kvBatchGet`, `kvBatchDelete`, `kvListPrefix` +- Lifecycle: `setAlarm`, `startSleep`, `startDestroy` +- Networking: `dispatch` for websocket events +- Runner ack path: `ackHibernatableWebSocketMessage` +- Inline client bridge: `clientCall` + +Binary payloads are normalized to `ArrayBuffer` at the host and isolate boundary. + +## Security Model + +- Each dynamic actor runs in its own sandboxed `NodeProcess`. +- Sandbox permissions deny network and child process access. +- Filesystem access is restricted to dynamic runtime root and read only `node_modules` paths. +- Environment is explicitly injected by host config for the isolate process. + +## Temporary Compatibility Layer + +Current implementation materializes a runtime `node_modules` tree under `/tmp` +and patches specific dependencies to CJS safe output. + +This is temporary. Remove this path when sandboxed-node can load required +RivetKit runtime dependencies without package source patching. + +## Driver Test Skip Gate + +The dynamic registry variant in driver tests has a narrow skip gate for two +cases only: + +- secure-exec dist is not available on the local machine +- nested dynamic harness mode is explicitly enabled for tests + +This gate is only to avoid invalid test harness setups. Static and dynamic +behavior parity remains the expected target for normal driver test execution. diff --git a/examples/CLAUDE.md b/examples/CLAUDE.md index 3d66514264..07e5e379cf 100644 --- a/examples/CLAUDE.md +++ b/examples/CLAUDE.md @@ -1,10 +1,10 @@ # examples/CLAUDE.md -Guidelines for creating and maintaining examples in this repository. +- Follow these guidelines when creating and maintaining examples in this repository. ## README Format -All example READMEs must follow the template defined in `.claude/resources/EXAMPLE_TEMPLATE.md`. Key requirements: +- All example READMEs must follow `.claude/resources/EXAMPLE_TEMPLATE.md` and meet the key requirements below. - Use exact section headings: `## Getting Started`, `## Features`, `## Implementation`, `## Resources`, `## License` - Include `## Prerequisites` only for non-obvious dependencies (API keys, external services) - Focus features on RivetKit concepts demonstrated, not just app functionality @@ -14,7 +14,7 @@ All example READMEs must follow the template defined in `.claude/resources/EXAMP ### Directory Layout -Examples with frontend (using vite-plugin-srvx): +- Use this layout for examples with frontend (using `vite-plugin-srvx`): ``` example-name/ ├── src/ @@ -34,7 +34,7 @@ example-name/ └── README.md ``` -Examples with separate frontend/backend dev servers: +- Use this layout for examples with separate frontend and backend dev servers: ``` example-name/ ├── src/ @@ -52,7 +52,7 @@ example-name/ └── README.md ``` -Backend-only examples: +- Use this layout for backend-only examples: ``` example-name/ ├── src/ @@ -75,7 +75,7 @@ example-name/ ### Required Scripts -For examples with frontend (using vite-plugin-srvx): +- Use these scripts for examples with frontend (using `vite-plugin-srvx`): ```json { "scripts": { @@ -88,7 +88,7 @@ For examples with frontend (using vite-plugin-srvx): } ``` -For examples with separate frontend/backend dev servers: +- Use these scripts for examples with separate frontend and backend dev servers: ```json { "scripts": { @@ -105,7 +105,7 @@ For examples with separate frontend/backend dev servers: } ``` -For backend-only examples: +- Use these scripts for backend-only examples: ```json { "scripts": { @@ -141,18 +141,18 @@ For backend-only examples: - Use `"rivetkit": "*"` for the main RivetKit package - Use `"@rivetkit/react": "*"` for React integration - Common dev dependencies: - - `tsx` for running TypeScript in development - - `typescript` for type checking - - `vite` and `@vitejs/plugin-react` for frontend - - `vite-plugin-srvx` for unified dev server (when using vite-plugin-srvx pattern) - - `vitest` for testing - - `tsup` for bundling (only for separate frontend/backend examples) - - `concurrently` for parallel dev servers (only for separate frontend/backend examples) +- `tsx` for running TypeScript in development +- `typescript` for type checking +- `vite` and `@vitejs/plugin-react` for frontend +- `vite-plugin-srvx` for unified dev server (when using vite-plugin-srvx pattern) +- `vitest` for testing +- `tsup` for bundling (only for separate frontend/backend examples) +- `concurrently` for parallel dev servers (only for separate frontend/backend examples) - Common production dependencies: - - `hono` for the server framework (required for Vercel detection) - - `srvx` for serving in production (used by `start` script) - - `@hono/node-server` for Node.js HTTP server adapter - - `@hono/node-ws` for Node.js WebSocket support +- `hono` for the server framework (required for Vercel detection) +- `srvx` for serving in production (used by `start` script) +- `@hono/node-server` for Node.js HTTP server adapter +- `@hono/node-ws` for Node.js WebSocket support ## Configuration Files @@ -177,7 +177,7 @@ For backend-only examples: } ``` -Notes: +- Notes: - Include `"dom"` in lib for frontend examples - Include `"vite/client"` in types when using Vite - Omit `"frontend/**/*"` and `"tests/**/*"` from include if they don't exist @@ -185,7 +185,7 @@ Notes: ### tsup.config.ts -Only needed for examples with separate frontend/backend dev servers (not using vite-plugin-srvx): +- Use `tsup.config.ts` only for examples with separate frontend and backend dev servers (not using `vite-plugin-srvx`). ```typescript import { defineConfig } from "tsup"; @@ -204,7 +204,7 @@ export default defineConfig({ ### vite.config.ts -For examples using vite-plugin-srvx (unified dev): +- Use this `vite.config.ts` for examples using `vite-plugin-srvx` (unified dev): ```typescript import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; @@ -215,7 +215,7 @@ export default defineConfig({ }); ``` -For examples with separate dev servers: +- Use this `vite.config.ts` for examples with separate dev servers: ```typescript import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; @@ -254,7 +254,7 @@ export default defineConfig({ ### vercel.json -Vercel auto-detects Vite when it sees a `vite.config.ts` and ignores Hono. We must explicitly set the framework to Hono: +- Vercel auto-detects Vite when it sees `vite.config.ts` and ignores Hono, so explicitly set the framework to Hono: ```json { @@ -264,7 +264,7 @@ Vercel auto-detects Vite when it sees a `vite.config.ts` and ignores Hono. We mu ### turbo.json -All examples should extend the root turbo config: +- Extend the root turbo config in all examples: ```json { "$schema": "https://turbo.build/schema.json", @@ -283,7 +283,7 @@ node_modules ### Actor File Structure -Actor definitions (`export const myActor = actor({...})`) must appear at the top of the file, before any helper functions. Helper functions, type definitions used only by helpers, and utilities go after the actor definition. This keeps the actor's public API front-and-center. +- Put actor definitions (`export const myActor = actor({...})`) at the top of the file before helper functions, and place helper-only types and utilities after the actor definition. ```typescript // Good @@ -303,7 +303,7 @@ function helperFunction(...) { ... } export const myActor = actor({...}); ``` -Shared types/interfaces used by both the actor definition and helpers (e.g. `State`, `PlayerEntry`) should go above the actor since the actor definition depends on them. +- Put shared types and interfaces used by both actor definitions and helpers (for example `State` and `PlayerEntry`) above the actor definition. ### Actor Definitions (src/actors.ts) @@ -338,9 +338,9 @@ export const registry = setup({ ### Server Entry Point (src/server.ts) -You must explicitly import from `"hono"` for Vercel to detect the framework. +- Explicitly import from `"hono"` so Vercel can detect the framework. -Minimum required: +- Include at least: ```typescript import { Hono } from "hono"; @@ -351,7 +351,7 @@ app.all("/api/rivet/*", (c) => registry.handler(c.req.raw)); export default app; ``` -With additional routes: +- Use this pattern with additional routes: ```typescript import { Hono } from "hono"; @@ -421,7 +421,7 @@ test("Description of test", async (ctx) => { ## HTML Entry Point -For Vite-based examples: +- Use this HTML pattern for Vite-based examples: ```html @@ -442,7 +442,7 @@ For Vite-based examples: ## ESM Import Requirements -All imports must be ESM-compliant with explicit `.ts` extensions for relative imports: +- Keep all imports ESM-compliant with explicit `.ts` extensions for relative imports: ```typescript // Correct @@ -454,7 +454,7 @@ import { registry } from "./actors"; import { someUtil } from "../utils/helper"; ``` -This is enforced by the tsconfig options `allowImportingTsExtensions` and `rewriteRelativeImportExtensions`. +- This is enforced by `allowImportingTsExtensions` and `rewriteRelativeImportExtensions` in `tsconfig`. ## Best Practices @@ -468,7 +468,7 @@ This is enforced by the tsconfig options `allowImportingTsExtensions` and `rewri ## Vercel Examples -Vercel-optimized versions of examples are automatically generated using the script at `scripts/vercel-examples/generate-vercel-examples.ts`. These examples use the `hono/vercel` adapter and are configured specifically for Vercel serverless deployment. +- Generate Vercel-optimized example variants with `scripts/vercel-examples/generate-vercel-examples.ts`; these variants use `hono/vercel` and Vercel-focused serverless config. ### Generation Script @@ -488,13 +488,13 @@ npx tsx scripts/vercel-examples/generate-vercel-examples.ts --dry-run ### Naming Convention -Vercel examples are placed at `examples/{original-name}-vercel/`. For example: +- Place generated Vercel examples at `examples/{original-name}-vercel/`, for example: - `hello-world` → `hello-world-vercel` - `chat-room` → `chat-room-vercel` ### Directory Layout -Vercel examples with frontend: +- Use this layout for Vercel examples with frontend: ``` example-name-vercel/ ├── api/ @@ -514,7 +514,7 @@ example-name-vercel/ └── README.md # With Vercel-specific note and deploy button ``` -Vercel examples without frontend (API-only): +- Use this layout for Vercel examples without frontend (API-only): ``` example-name-vercel/ ├── api/ @@ -533,7 +533,7 @@ example-name-vercel/ #### api/index.ts -The API entry point uses the Hono Vercel adapter (built into the `hono` package): +- Use the Hono Vercel adapter (built into `hono`) in the API entry point: ```typescript import app from "../src/server.ts"; @@ -543,7 +543,7 @@ export default app; #### vercel.json -For examples with frontend: +- Use this `vercel.json` for examples with frontend: ```json { "framework": "vite", @@ -553,7 +553,7 @@ For examples with frontend: } ``` -For API-only examples: +- Use this `vercel.json` for API-only examples: ```json { "rewrites": [ @@ -564,7 +564,7 @@ For API-only examples: #### package.json -Key differences from origin examples: +- Apply these key differences from origin examples: - Removes `srvx` and `vite-plugin-srvx` - Uses `vercel dev` for development - Simplified build scripts @@ -572,11 +572,11 @@ Key differences from origin examples: #### README.md -Each Vercel example README includes: +- Include the following in each Vercel example README: - A note explaining it's the Vercel-optimized version with a link back to the origin - A "Deploy with Vercel" button for one-click deployment -Example header: +- Use this example header: ```markdown > **Note:** This is the Vercel-optimized version of the [hello-world](../hello-world) example. > It uses the `hono/vercel` adapter and is configured for Vercel deployment. @@ -586,7 +586,7 @@ Example header: ### Skipped Examples -The following example types are not converted to Vercel: +- Do not convert these example types to Vercel: - **Next.js examples** (`*-next-js`): Next.js has its own Vercel integration - **Cloudflare examples** (`*-cloudflare*`): Different runtime environment - **Deno examples**: Different runtime environment @@ -601,7 +601,7 @@ The following example types are not converted to Vercel: ## Frontend Style Guide -Examples should follow these design conventions: +- Follow these design conventions in examples: **Color Palette (Dark Theme)** - Primary accent: `#ff4f00` (orange) for interactive elements and highlights @@ -657,7 +657,7 @@ Examples should follow these design conventions: **Component Patterns** -*Buttons* +- Buttons: - Primary: `#ff4f00` background, white text - Secondary: `#2c2c2e` background, white text - Ghost: transparent background, `#ff4f00` text @@ -665,7 +665,7 @@ Examples should follow these design conventions: - Success: `#30d158` background, white text - Disabled: 50% opacity, `cursor: not-allowed` -*Form Inputs* +- Form Inputs: - Background: `#2c2c2e` - Border: 1px solid `#3a3a3c` - Border radius: 8px @@ -673,21 +673,21 @@ Examples should follow these design conventions: - Focus: border-color `#ff4f00`, box-shadow `0 0 0 3px rgba(255, 79, 0, 0.2)` - Placeholder text: `#6e6e73` -*Cards/Containers* +- Cards and containers: - Background: `#1c1c1e` - Border: 1px solid `#2c2c2e` - Border radius: 8px - Padding: 20px - Box shadow: `0 1px 3px rgba(0, 0, 0, 0.3)` - Header style (when applicable): - - Background: `#2c2c2e` - - Padding: 16px 20px - - Font size: 18px, weight 600 - - Border bottom: 1px solid `#2c2c2e` - - Border radius: 8px 8px 0 0 (top corners only) - - Negative margin to align with card edges: `-20px -20px 20px -20px` - -*Modals/Overlays* +- Background: `#2c2c2e` +- Padding: 16px 20px +- Font size: 18px, weight 600 +- Border bottom: 1px solid `#2c2c2e` +- Border radius: 8px 8px 0 0 (top corners only) +- Negative margin to align with card edges: `-20px -20px 20px -20px` + +- Modals and overlays: - Backdrop: `rgba(0, 0, 0, 0.75)` - Modal background: `#1c1c1e` - Border radius: 8px @@ -695,19 +695,19 @@ Examples should follow these design conventions: - Padding: 24px - Close button: top-right, 8px from edges -*Lists* +- Lists: - Item padding: 12px 16px - Dividers: 1px solid `#2c2c2e` - Hover background: `#2c2c2e` - Selected/active background: `rgba(255, 79, 0, 0.15)` -*Badges/Tags* +- Badges and tags: - Padding: 4px 8px - Border radius: 6px - Font size: 12px - Font weight: 500 -*Tabs* +- Tabs: - Container: `border-bottom: 1px solid #2c2c2e`, flex-wrap for overflow - Tab: `padding: 12px 16px`, no background, `border-radius: 0` - Tab border: `border-bottom: 2px solid transparent`, `margin-bottom: -1px` @@ -718,33 +718,32 @@ Examples should follow these design conventions: **UI States** -*Loading States* +- Loading states: - Spinner: 20px for inline, 32px for page-level - Skeleton placeholders: `#2c2c2e` background with subtle pulse animation - Loading text: "Loading..." in muted color - Button loading: show spinner, disable interaction, keep button width stable -*Empty States* +- Empty states: - Center content vertically and horizontally - Icon: 48px, muted color (`#6e6e73`) - Heading: 18px, primary text color - Description: 14px, muted color - Optional action button below description -*Error States* +- Error states: - Inline errors: `#ff3b30` text below input, 12px font size - Error banners: `#ff3b30` left border (4px), `rgba(255, 59, 48, 0.1)` background - Form validation: highlight input border in `#ff3b30` - Error icon: Lucide `AlertCircle` or `XCircle` -*Disabled States* +- Disabled states: - Opacity: 50% - Cursor: `not-allowed` - No hover/focus effects - Preserve layout (don't collapse or hide) -*Success States* +- Success states: - Color: `#30d158` - Icon: Lucide `CheckCircle` or `Check` - Toast/banner: `rgba(48, 209, 88, 0.1)` background with green left border - diff --git a/examples/dynamic-actors/README.md b/examples/dynamic-actors/README.md new file mode 100644 index 0000000000..116ec3c199 --- /dev/null +++ b/examples/dynamic-actors/README.md @@ -0,0 +1,42 @@ +# Dynamic Actors + +Example showing a user-editable actor source workflow with `dynamicActor`. + +## Getting Started + +```sh +cd examples/dynamic-actors +pnpm install +pnpm dev +``` + +## Features + +- Dynamic actor loading via `dynamicActor` from `rivetkit/dynamic` +- Actor-to-actor source loading where `dynamicWorkflow` loads code from `sourceCode` +- In-browser editor to update actor source at runtime +- User-controlled dynamic actor key input with one-click random key generation to force fresh actor loads + +## Prerequisites + +- Build `sandboxed-node` in your Secure Exec checkout and make it resolvable by this project +- If needed, set `RIVETKIT_DYNAMIC_SECURE_EXEC_SPECIFIER` to a file URL for `sandboxed-node/dist/index.js` + +## Implementation + +The actor definitions are in [`src/actors.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/dynamic-actors/src/actors.ts). + +- `sourceCode` stores editable source and revision in actor state +- `dynamicWorkflow` loads current source from `sourceCode` in its loader context, then evaluates and runs it + +The server wiring is in [`src/server.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/dynamic-actors/src/server.ts). + +The UI is in [`frontend/App.tsx`](https://github.com/rivet-dev/rivet/tree/main/examples/dynamic-actors/frontend/App.tsx) and provides save + execute controls. + +## Resources + +Read more about [AI and user-generated Rivet Actors](/docs/actors/ai-and-user-generated-actors), [actions](/docs/actors/actions), and [communicating between actors](/docs/actors/communicating-between-actors). + +## License + +MIT diff --git a/examples/dynamic-actors/frontend/App.tsx b/examples/dynamic-actors/frontend/App.tsx new file mode 100644 index 0000000000..11c8d6e80e --- /dev/null +++ b/examples/dynamic-actors/frontend/App.tsx @@ -0,0 +1,233 @@ +import { useEffect, useState } from "react"; + +const SOURCE_TEMPLATE = `import { actor } from "rivetkit"; + +export default actor({ + state: { + count: 0, + }, + actions: { + increment: (c, amount = 1) => { + c.state.count += amount; + return c.state.count; + }, + getCount: (c) => c.state.count, + }, +}); +`; + +type SourceResponse = { + source: string; + revision: number; +}; + +async function requestJson( + input: string, + init?: RequestInit, +): Promise { + const response = await fetch(input, init); + if (!response.ok) { + const body = await response.text(); + throw new Error(`${response.status} ${response.statusText}: ${body}`); + } + return (await response.json()) as T; +} + +function App() { + const [source, setSource] = useState(SOURCE_TEMPLATE); + const [revision, setRevision] = useState(1); + const [dynamicKey, setDynamicKey] = useState("dynamic-main"); + const [count, setCount] = useState(null); + const [amount, setAmount] = useState(1); + const [status, setStatus] = useState("Loading source actor..."); + const [loading, setLoading] = useState(false); + + useEffect(() => { + void loadSource(); + }, []); + + const loadSource = async () => { + setLoading(true); + setStatus("Loading source..."); + try { + const state = await requestJson("/api/source"); + setSource(state.source); + setRevision(state.revision); + setStatus(`Loaded source at revision ${state.revision}.`); + } catch (error) { + setStatus(`Failed to load source: ${String(error)}`); + } finally { + setLoading(false); + } + }; + + const saveSource = async () => { + setLoading(true); + setStatus("Saving source..."); + try { + const result = await requestJson<{ revision: number }>("/api/source", { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ source }), + }); + setRevision(result.revision); + setCount(null); + setStatus( + `Saved source at revision ${result.revision}. Generate a new dynamic key to force a fresh actor load.`, + ); + } catch (error) { + setStatus(`Failed to save source: ${String(error)}`); + } finally { + setLoading(false); + } + }; + + const generateRandomKey = () => { + const random = Math.random().toString(36).slice(2, 10); + const nextKey = `dynamic-${random}`; + setDynamicKey(nextKey); + setCount(null); + setStatus(`Switched to dynamic key "${nextKey}".`); + }; + + const getCount = async () => { + const normalizedKey = dynamicKey.trim(); + if (!normalizedKey) { + setStatus("Dynamic key cannot be empty."); + return; + } + + setLoading(true); + setStatus("Running getCount() on dynamic actor..."); + try { + const result = await requestJson<{ count: number }>( + `/api/dynamic/${encodeURIComponent(normalizedKey)}/count`, + ); + setCount(result.count); + setStatus(`Dynamic actor count is ${result.count} (key "${normalizedKey}").`); + } catch (error) { + setStatus(`Dynamic actor call failed: ${String(error)}`); + } finally { + setLoading(false); + } + }; + + const increment = async () => { + const normalizedKey = dynamicKey.trim(); + if (!normalizedKey) { + setStatus("Dynamic key cannot be empty."); + return; + } + + setLoading(true); + setStatus("Running increment() on dynamic actor..."); + try { + const result = await requestJson<{ count: number }>( + `/api/dynamic/${encodeURIComponent(normalizedKey)}/increment`, + { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ amount }), + }, + ); + setCount(result.count); + setStatus( + `Incremented by ${amount}. Dynamic actor count is ${result.count} (key "${normalizedKey}").`, + ); + } catch (error) { + setStatus(`Dynamic actor call failed: ${String(error)}`); + } finally { + setLoading(false); + } + }; + + return ( +
+

Dynamic Actor Editor

+

+ This example has two actors: sourceCode stores your source, + and dynamicWorkflow loads and runs that source. +

+ +
+ + + +
+ +