From 21324dd5ec0b102290f8869864e38eaf895e77a7 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Thu, 23 Oct 2025 01:14:21 -0700 Subject: [PATCH 1/3] chore(scripts): add pg backup & restore scripts for dev --- biome.json | 3 +- pnpm-lock.yaml | 98 +++++++++++++++++++++++++++++++++ scripts/run/backup-postgres.sh | 41 ++++++++++++++ scripts/run/engine-postgres.sh | 17 +++++- scripts/run/nuke-postgres.sh | 33 +++++++++++ scripts/run/restore-postgres.sh | 56 +++++++++++++++++++ 6 files changed, 244 insertions(+), 4 deletions(-) create mode 100755 scripts/run/backup-postgres.sh create mode 100755 scripts/run/nuke-postgres.sh create mode 100755 scripts/run/restore-postgres.sh diff --git a/biome.json b/biome.json index f73b7b236e..e56369e5af 100644 --- a/biome.json +++ b/biome.json @@ -8,8 +8,7 @@ "!engine/artifacts", "!engine/sdks", "!engine/sdks/typescript/api-full", - "!engine/sdks/typescript/runner-protocol" - "!examples/snippets", + "!engine/sdks/typescript/runner-protocol", "!frontend", "!rivetkit-openapi/openapi.json", "!scripts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1bda78540f..e5386fc7a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -537,6 +537,104 @@ importers: specifier: ^3.1.1 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + examples/cursors: + dependencies: + rivetkit: + specifier: workspace:* + version: link:../../rivetkit-typescript/packages/rivetkit + devDependencies: + '@rivetkit/react': + specifier: workspace:* + version: link:../../rivetkit-typescript/packages/react + '@types/node': + specifier: ^22.13.9 + version: 22.18.1 + '@types/prompts': + specifier: ^2 + version: 2.4.9 + '@types/react': + specifier: ^18.2.0 + version: 18.3.24 + '@types/react-dom': + specifier: ^18.2.0 + version: 18.3.7(@types/react@18.3.24) + '@vitejs/plugin-react': + specifier: ^4.2.0 + version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) + concurrently: + specifier: ^8.2.2 + version: 8.2.2 + prompts: + specifier: ^2.4.2 + version: 2.4.2 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + tsx: + specifier: ^3.12.7 + version: 3.14.0 + typescript: + specifier: ^5.5.2 + version: 5.9.2 + vite: + specifier: ^5.0.0 + version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + vitest: + specifier: ^3.1.1 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + + examples/cursors-raw-websocket: + dependencies: + rivetkit: + specifier: workspace:* + version: link:../../rivetkit-typescript/packages/rivetkit + devDependencies: + '@rivetkit/react': + specifier: workspace:* + version: link:../../rivetkit-typescript/packages/react + '@types/node': + specifier: ^22.13.9 + version: 22.18.1 + '@types/prompts': + specifier: ^2 + version: 2.4.9 + '@types/react': + specifier: ^18.2.0 + version: 18.3.24 + '@types/react-dom': + specifier: ^18.2.0 + version: 18.3.7(@types/react@18.3.24) + '@vitejs/plugin-react': + specifier: ^4.2.0 + version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) + concurrently: + specifier: ^8.2.2 + version: 8.2.2 + prompts: + specifier: ^2.4.2 + version: 2.4.2 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + tsx: + specifier: ^3.12.7 + version: 3.14.0 + typescript: + specifier: ^5.5.2 + version: 5.9.2 + vite: + specifier: ^5.0.0 + version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + vitest: + specifier: ^3.1.1 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + examples/database: dependencies: '@rivetkit/react': diff --git a/scripts/run/backup-postgres.sh b/scripts/run/backup-postgres.sh new file mode 100755 index 0000000000..9704f94226 --- /dev/null +++ b/scripts/run/backup-postgres.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONTAINER_NAME="rivet-engine-postgres" + +if [ $# -ne 1 ]; then + echo "Usage: $0 " + echo "Example: $0 /path/to/backup/postgres-backup.tar.gz" + exit 1 +fi + +BACKUP_PATH="$1" + +# Check if container exists +if ! docker ps --all --format '{{.Names}}' | grep -qw "${CONTAINER_NAME}"; then + echo "error: container '${CONTAINER_NAME}' not found" + exit 1 +fi + +# Get the volume name +VOLUME_NAME=$(docker inspect "${CONTAINER_NAME}" --format '{{range .Mounts}}{{if eq .Destination "/var/lib/postgresql/data"}}{{.Name}}{{end}}{{end}}') + +if [ -z "${VOLUME_NAME}" ]; then + echo "error: could not find postgres data volume for container '${CONTAINER_NAME}'" + exit 1 +fi + +echo "Backing up postgres data from volume '${VOLUME_NAME}'..." + +# Create backup directory if it doesn't exist +BACKUP_DIR="$(dirname "${BACKUP_PATH}")" +mkdir -p "${BACKUP_DIR}" + +# Backup the volume data +docker run --rm \ + -v "${VOLUME_NAME}":/data \ + -v "${BACKUP_DIR}":/backup \ + alpine \ + tar czf "/backup/$(basename "${BACKUP_PATH}")" -C /data . + +echo "Backup completed: ${BACKUP_PATH}" diff --git a/scripts/run/engine-postgres.sh b/scripts/run/engine-postgres.sh index 317263034a..f93511125d 100755 --- a/scripts/run/engine-postgres.sh +++ b/scripts/run/engine-postgres.sh @@ -11,8 +11,21 @@ fi if ! nc -z localhost 5432 >/dev/null 2>&1; then echo "Postgres is not reachable at localhost:5432." - echo "Hint: run scripts/dev/run-postgres.sh to start the local Postgres container." - exit 1 + echo "Starting postgres container..." + "${SCRIPT_DIR}/postgres.sh" + + echo "Waiting for postgres to be ready..." + for i in {1..30}; do + if nc -z localhost 5432 >/dev/null 2>&1; then + echo "Postgres is ready!" + break + fi + if [ $i -eq 30 ]; then + echo "error: postgres did not become ready in time" + exit 1 + fi + sleep 1 + done fi cd "${REPO_ROOT}" diff --git a/scripts/run/nuke-postgres.sh b/scripts/run/nuke-postgres.sh new file mode 100755 index 0000000000..7e6f098b94 --- /dev/null +++ b/scripts/run/nuke-postgres.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONTAINER_NAME="rivet-engine-postgres" + +echo "Nuking postgres container and data..." + +# Get the volume name before removing the container +VOLUME_NAME="" +if docker ps --all --format '{{.Names}}' | grep -qw "${CONTAINER_NAME}"; then + VOLUME_NAME=$(docker inspect "${CONTAINER_NAME}" --format '{{range .Mounts}}{{if eq .Destination "/var/lib/postgresql/data"}}{{.Name}}{{end}}{{end}}' 2>/dev/null || true) +fi + +# Stop and remove container +if docker ps --all --format '{{.Names}}' | grep -qw "${CONTAINER_NAME}"; then + echo "Stopping and removing container '${CONTAINER_NAME}'..." + if docker ps --format '{{.Names}}' | grep -qw "${CONTAINER_NAME}"; then + docker stop "${CONTAINER_NAME}" >/dev/null 2>&1 || true + fi + docker rm "${CONTAINER_NAME}" >/dev/null 2>&1 || true + echo "Container removed." +else + echo "Container '${CONTAINER_NAME}' not found." +fi + +# Remove volume if it exists +if [ -n "${VOLUME_NAME}" ]; then + echo "Removing volume '${VOLUME_NAME}'..." + docker volume rm "${VOLUME_NAME}" >/dev/null 2>&1 || true + echo "Volume removed." +fi + +echo "Postgres nuked successfully!" diff --git a/scripts/run/restore-postgres.sh b/scripts/run/restore-postgres.sh new file mode 100755 index 0000000000..bd2b50e0b4 --- /dev/null +++ b/scripts/run/restore-postgres.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONTAINER_NAME="rivet-engine-postgres" +POSTGRES_IMAGE="postgres:17" + +if [ $# -ne 1 ]; then + echo "Usage: $0 " + echo "Example: $0 /path/to/backup/postgres-backup.tar.gz" + exit 1 +fi + +BACKUP_PATH="$1" + +if [ ! -f "${BACKUP_PATH}" ]; then + echo "error: backup file '${BACKUP_PATH}' not found" + exit 1 +fi + +# Stop and remove existing container if it exists +if docker ps --all --format '{{.Names}}' | grep -qw "${CONTAINER_NAME}"; then + echo "Stopping and removing existing container '${CONTAINER_NAME}'..." + if docker ps --format '{{.Names}}' | grep -qw "${CONTAINER_NAME}"; then + docker stop "${CONTAINER_NAME}" >/dev/null 2>&1 || true + fi + docker rm "${CONTAINER_NAME}" >/dev/null 2>&1 || true +fi + +# Create a new volume +echo "Creating new volume..." +VOLUME_NAME=$(docker volume create) +echo "Created volume: ${VOLUME_NAME}" + +# Restore the data to the new volume +echo "Restoring data from '${BACKUP_PATH}'..." +BACKUP_DIR="$(dirname "${BACKUP_PATH}")" +docker run --rm \ + -v "${VOLUME_NAME}":/data \ + -v "${BACKUP_DIR}":/backup \ + alpine \ + tar xzf "/backup/$(basename "${BACKUP_PATH}")" -C /data + +# Create and start the container +echo "Starting container '${CONTAINER_NAME}'..." +docker run \ + --detach \ + --name "${CONTAINER_NAME}" \ + --publish 5432:5432 \ + --env POSTGRES_PASSWORD=postgres \ + --env POSTGRES_USER=postgres \ + --env POSTGRES_DB=postgres \ + -v "${VOLUME_NAME}":/var/lib/postgresql/data \ + "${POSTGRES_IMAGE}" + +echo "Restore completed successfully!" +echo "Container ID: $(docker ps -q -f name=${CONTAINER_NAME})" From 1fd2b526e89c204c7b0ef5c84beb49d525148940 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 24 Oct 2025 13:38:59 -0700 Subject: [PATCH 2/3] ci: fix release scripts for engine --- .dockerignore | 24 + .github/workflows/release.yaml | 27 +- .github/workflows/rust.yml | 12 +- biome.json | 3 +- engine/artifacts/errors/api.rate_limited.json | 8 +- .../errors/namespace.invalid_name.json | 8 +- .../errors/test.input_too_large.json | 8 +- .../artifacts/errors/test.key_too_large.json | 8 +- engine/artifacts/errors/test.meta_error.json | 8 +- engine/artifacts/errors/test.not_found.json | 8 +- .../artifacts/errors/test.simple_error.json | 8 +- engine/artifacts/errors/test.test_error.json | 8 +- engine/artifacts/openapi.json | 3322 +++++++++-------- engine/contrib-docs/DOCKER.md | 2 +- engine/docker/engine/build.sh | 11 +- engine/docker/engine/linux-aarch64.Dockerfile | 41 +- engine/docker/engine/linux-x86_64.Dockerfile | 11 +- engine/docker/engine/macos-aarch64.Dockerfile | 11 +- engine/docker/engine/macos-x86_64.Dockerfile | 11 +- engine/docker/engine/windows.Dockerfile | 11 +- engine/docker/template/src/context.ts | 4 +- engine/docker/template/src/main.ts | 4 +- .../template/src/services/core/grafana.ts | 4 +- engine/docker/universal/Dockerfile | 17 +- engine/sdks/typescript/runner/src/mod.ts | 86 +- engine/sdks/typescript/runner/src/tunnel.ts | 19 +- .../sdks/typescript/runner/vitest.config.ts | 2 +- .../sdks/typescript/test-runner/package.json | 2 +- .../sdks/typescript/test-runner/src/index.ts | 75 +- engine/sdks/typescript/test-runner/src/log.ts | 8 +- .../typescript/test-runner/vitest.config.ts | 2 +- examples/cursors/tests/cursors.test.ts | 132 - .../freestyle/scripts/freestyle-deploy.ts | 4 +- frontend/package.json | 4 +- justfile | 4 +- .../packages/rivetkit/scripts/compile-bare.ts | 4 +- .../src/drivers/engine/actor-driver.ts | 7 +- .../packages/rivetkit/vitest.config.ts | 2 +- scripts/docker/build-push.sh | 2 +- scripts/release/sdk.ts | 12 +- website/scripts/generateExamples.mjs | 6 +- website/scripts/generateNavigation.ts | 2 +- website/scripts/generateReadme.mjs | 4 +- 43 files changed, 2067 insertions(+), 1889 deletions(-) delete mode 100644 examples/cursors/tests/cursors.test.ts diff --git a/.dockerignore b/.dockerignore index fe2aaff472..fdd1e06e24 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,29 @@ # MARK: Docker-specific .git/ +.gitignore +.gitattributes +.dockerignore +**/Dockerfile* +**/docker-compose*.yml + +# CI/CD +.github/ +.gitlab-ci.yml +.travis.yml +.circleci/ + +# Documentation +*.md +!README.md +docs/ +LICENSE* + +# Editor configs +.vscode/ +.idea/ +*.swp +*.swo +*~ # MARK: Copied from .gitignore **/.DS_Store diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 65224250e4..2ff027202a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -83,17 +83,25 @@ jobs: target: x86_64-unknown-linux-musl binary_ext: "" arch: x86_64 + # TODO: Add back when working + # - platform: linux + # runner: depot-ubuntu-24.04-arm-4 + # target: aarch64-unknown-linux-musl + # binary_ext: "" + # arch: aarch64 - platform: windows runner: depot-ubuntu-24.04-4 target: x86_64-pc-windows-gnu binary_ext: ".exe" arch: x86_64 - platform: macos + # Use Linux instead of macOS builders since macOS does not support Docker runner: depot-ubuntu-24.04-4 target: x86_64-apple-darwin binary_ext: "" arch: x86_64 - platform: macos + # Use Linux instead of macOS builders since macOS does not support Docker runner: depot-ubuntu-24.04-4 target: aarch64-apple-darwin binary_ext: "" @@ -111,9 +119,9 @@ jobs: run: | # Use Docker BuildKit export DOCKER_BUILDKIT=1 - + # Build the binary using our Dockerfile - docker/engine/build.sh ${{ matrix.target }} + engine/docker/engine/build.sh ${{ matrix.target }} # Make sure dist directory exists and binary is there ls -la dist/ @@ -153,15 +161,12 @@ jobs: strategy: matrix: include: - # TODO(RVT-4479): Add back ARM builder once manifest generation fixed - # - platform: linux/arm64 - # runner: depot-ubuntu-24.04-4 - # arch_suffix: -arm64 + - platform: linux/arm64 + runner: depot-ubuntu-24.04-arm-4 + arch_suffix: -arm64 - platform: linux/x86_64 runner: depot-ubuntu-24.04-4 - # TODO: Replace with appropriate arch_suffix when needed - # arch_suffix: -amd64 - arch_suffix: '' + arch_suffix: -amd64 runs-on: ${{ matrix.runner }} steps: - name: Setup Docker on macOS @@ -188,7 +193,7 @@ jobs: context: . push: true tags: rivetkit/engine:full-${{ steps.vars.outputs.sha_short }}${{ matrix.arch_suffix }} - file: docker/universal/Dockerfile + file: engine/docker/universal/Dockerfile target: engine-full platforms: ${{ matrix.platform }} build-args: | @@ -205,7 +210,7 @@ jobs: context: . push: true tags: rivetkit/engine:slim-${{ steps.vars.outputs.sha_short }}${{ matrix.arch_suffix }} - file: docker/universal/Dockerfile + file: engine/docker/universal/Dockerfile target: engine-slim platforms: ${{ matrix.platform }} build-args: | diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 745f5ae9cd..646821f1c2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -32,12 +32,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - + - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: stable components: rustfmt - + - name: Check formatting run: cargo fmt --all -- --check @@ -62,11 +62,11 @@ jobs: runs-on: depot-ubuntu-24.04 steps: - uses: actions/checkout@v4 - + - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: stable - + - uses: Swatinem/rust-cache@v2 - name: Check @@ -80,11 +80,11 @@ jobs: runs-on: depot-ubuntu-24.04 steps: - uses: actions/checkout@v4 - + - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: stable - + - uses: Swatinem/rust-cache@v2 - name: Run tests diff --git a/biome.json b/biome.json index e56369e5af..bcc74e911b 100644 --- a/biome.json +++ b/biome.json @@ -31,7 +31,8 @@ "rules": { "recommended": true, "style": { - "noUselessElse": "off" + "noUselessElse": "off", + "useNodejsImportProtocol": "error" }, "correctness": { "noUnusedImports": "warn" diff --git a/engine/artifacts/errors/api.rate_limited.json b/engine/artifacts/errors/api.rate_limited.json index ddbe2878e0..4f42c1974d 100644 --- a/engine/artifacts/errors/api.rate_limited.json +++ b/engine/artifacts/errors/api.rate_limited.json @@ -1,5 +1,5 @@ { - "code": "rate_limited", - "group": "api", - "message": "\n\tRate limit exceeded.\n\t\n\tThe API rate limit has been exceeded for this endpoint.\n\tPlease wait before making additional requests.\n\t" -} + "code": "rate_limited", + "group": "api", + "message": "\n\tRate limit exceeded.\n\t\n\tThe API rate limit has been exceeded for this endpoint.\n\tPlease wait before making additional requests.\n\t" +} \ No newline at end of file diff --git a/engine/artifacts/errors/namespace.invalid_name.json b/engine/artifacts/errors/namespace.invalid_name.json index c797516a42..537dba4c8f 100644 --- a/engine/artifacts/errors/namespace.invalid_name.json +++ b/engine/artifacts/errors/namespace.invalid_name.json @@ -1,5 +1,5 @@ { - "code": "invalid_name", - "group": "namespace", - "message": "\n\tInvalid namespace name.\n\t\n\tNamespace names must be valid DNS subdomains.\n\t" -} + "code": "invalid_name", + "group": "namespace", + "message": "\n\tInvalid namespace name.\n\t\n\tNamespace names must be valid DNS subdomains.\n\t" +} \ No newline at end of file diff --git a/engine/artifacts/errors/test.input_too_large.json b/engine/artifacts/errors/test.input_too_large.json index c173e92ea4..63f47fcc3a 100644 --- a/engine/artifacts/errors/test.input_too_large.json +++ b/engine/artifacts/errors/test.input_too_large.json @@ -1,5 +1,5 @@ { - "code": "input_too_large", - "group": "test", - "message": "Input too large." -} + "code": "input_too_large", + "group": "test", + "message": "Input too large." +} \ No newline at end of file diff --git a/engine/artifacts/errors/test.key_too_large.json b/engine/artifacts/errors/test.key_too_large.json index 26428df670..d36bf40a1b 100644 --- a/engine/artifacts/errors/test.key_too_large.json +++ b/engine/artifacts/errors/test.key_too_large.json @@ -1,5 +1,5 @@ { - "code": "key_too_large", - "group": "test", - "message": "Key too large." -} + "code": "key_too_large", + "group": "test", + "message": "Key too large." +} \ No newline at end of file diff --git a/engine/artifacts/errors/test.meta_error.json b/engine/artifacts/errors/test.meta_error.json index 575f38a76f..5fa944bfae 100644 --- a/engine/artifacts/errors/test.meta_error.json +++ b/engine/artifacts/errors/test.meta_error.json @@ -1,5 +1,5 @@ { - "code": "meta_error", - "group": "test", - "message": "Default message" -} + "code": "meta_error", + "group": "test", + "message": "Default message" +} \ No newline at end of file diff --git a/engine/artifacts/errors/test.not_found.json b/engine/artifacts/errors/test.not_found.json index 02c454c27d..b24f1a7a1d 100644 --- a/engine/artifacts/errors/test.not_found.json +++ b/engine/artifacts/errors/test.not_found.json @@ -1,5 +1,5 @@ { - "code": "not_found", - "group": "test", - "message": "The resource does not exist." -} + "code": "not_found", + "group": "test", + "message": "The resource does not exist." +} \ No newline at end of file diff --git a/engine/artifacts/errors/test.simple_error.json b/engine/artifacts/errors/test.simple_error.json index 80a6afaed7..f7c8e635e2 100644 --- a/engine/artifacts/errors/test.simple_error.json +++ b/engine/artifacts/errors/test.simple_error.json @@ -1,5 +1,5 @@ { - "code": "simple_error", - "group": "test", - "message": "This is a simple test error" -} + "code": "simple_error", + "group": "test", + "message": "This is a simple test error" +} \ No newline at end of file diff --git a/engine/artifacts/errors/test.test_error.json b/engine/artifacts/errors/test.test_error.json index 33c40e6980..32ae63e73c 100644 --- a/engine/artifacts/errors/test.test_error.json +++ b/engine/artifacts/errors/test.test_error.json @@ -1,5 +1,5 @@ { - "code": "test_error", - "group": "test", - "message": "Test error" -} + "code": "test_error", + "group": "test", + "message": "Test error" +} \ No newline at end of file diff --git a/engine/artifacts/openapi.json b/engine/artifacts/openapi.json index e76e41fab6..d73b549032 100644 --- a/engine/artifacts/openapi.json +++ b/engine/artifacts/openapi.json @@ -1,1564 +1,1760 @@ { - "openapi": "3.1.0", - "info": { - "title": "rivet-api-public", - "description": "", - "contact": { - "name": "Rivet Gaming, LLC", - "email": "developer@rivet.gg" - }, - "license": { - "name": "Apache-2.0", - "identifier": "Apache-2.0" - }, - "version": "25.8.1" - }, - "paths": { - "/actors": { - "get": { - "tags": ["actors::list"], - "summary": " ## Datacenter Round Trips", - "description": " **If key is some & `include_destroyed` is false**\n\n 2 round trips:\n - namespace::ops::resolve_for_name_global\n - GET /actors (multiple DCs based on actor IDs)\n\n\tThis path is optimized because we can read the actor IDs fro the key directly from Epoxy with\n\tstale consistency to determine which datacenter the actor lives in. Under most circumstances,\n\tthis means we don't need to fan out to all datacenters (like normal list does).\n\n\tThe reason `include_destroyed` has to be false is Epoxy only stores currently active actors. If\n\t`include_destroyed` is true, we show all previous iterations of actors with the same key.\n\n **Otherwise**\n\n 2 round trips:\n - namespace::ops::resolve_for_name_global\n - GET /actors (fanout)\n\n ## Optimized Alternative Routes", - "operationId": "actors_list", - "parameters": [ - { - "name": "namespace", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "name", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "key", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "actor_ids", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "include_destroyed", - "in": "query", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 0 - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActorsListResponse" - } - } - } - } - } - }, - "put": { - "tags": ["actors::get_or_create"], - "summary": "## Datacenter Round Trips", - "description": "**If actor exists**\n\n2 round trips:\n- namespace::ops::resolve_for_name_global\n- GET /actors/{}\n\n**If actor does not exist and is created in the current datacenter:**\n\n2 round trips:\n- namespace::ops::resolve_for_name_global\n- [pegboard::workflows::actor] Create actor workflow (includes Epoxy key allocation)\n\n**If actor does not exist and is created in a different datacenter:**\n\n3 round trips:\n- namespace::ops::resolve_for_name_global\n- POST /actors to remote datacenter\n- [pegboard::workflows::actor] Create actor workflow (includes Epoxy key allocation)\n\nactor::get will always be in the same datacenter.\n\n## Optimized Alternative Routes", - "operationId": "actors_get_or_create", - "parameters": [ - { - "name": "namespace", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActorsGetOrCreateRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActorsGetOrCreateResponse" - } - } - } - } - } - }, - "post": { - "tags": ["actors::create"], - "summary": "## Datacenter Round Trips", - "description": "**If actor is created in the current datacenter:**\n\n2 round trips:\n- namespace::ops::resolve_for_name_global\n- [pegboard::workflows::actor] Create actor workflow (includes Epoxy key allocation)\n\n**If actor is created in a different datacenter:**\n\n3 round trips:\n- namespace::ops::resolve_for_name_global\n- POST /actors to remote datacenter\n- [pegboard::workflows::actor] Create actor workflow (includes Epoxy key allocation)\n\nactor::get will always be in the same datacenter.", - "operationId": "actors_create", - "parameters": [ - { - "name": "namespace", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActorsCreateRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActorsCreateResponse" - } - } - } - } - } - } - }, - "/actors/names": { - "get": { - "tags": ["actors::list_names"], - "summary": "## Datacenter Round Trips", - "description": "2 round trips:\n- GET /actors/names (fanout)\n- [api-peer] namespace::ops::resolve_for_name_global", - "operationId": "actors_list_names", - "parameters": [ - { - "name": "namespace", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 0 - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActorsListNamesResponse" - } - } - } - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/actors/{actor_id}": { - "delete": { - "tags": ["actors::delete"], - "summary": "## Datacenter Round Trips", - "description": "2 round trip:\n- DELETE /actors/{}\n- [api-peer] namespace::ops::resolve_for_name_global", - "operationId": "actors_delete", - "parameters": [ - { - "name": "actor_id", - "in": "path", - "required": true, - "schema": { - "$ref": "#/components/schemas/RivetId" - } - }, - { - "name": "namespace", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActorsDeleteResponse" - } - } - } - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/datacenters": { - "get": { - "tags": ["datacenters"], - "operationId": "datacenters_list", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DatacentersListResponse" - } - } - } - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/health/fanout": { - "get": { - "tags": ["health"], - "operationId": "health_fanout", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HealthFanoutResponse" - } - } - } - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/namespaces": { - "get": { - "tags": ["namespaces"], - "operationId": "namespaces_list", - "parameters": [ - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 0 - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "name", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "namespace_ids", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NamespaceListResponse" - } - } - } - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - }, - "post": { - "tags": ["namespaces"], - "operationId": "namespaces_create", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NamespacesCreateRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NamespacesCreateResponse" - } - } - } - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/runner-configs": { - "get": { - "tags": ["runner_configs::list"], - "operationId": "runner_configs_list", - "parameters": [ - { - "name": "namespace", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 0 - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "variant", - "in": "query", - "required": false, - "schema": { - "$ref": "#/components/schemas/RunnerConfigVariant" - } - }, - { - "name": "runner_names", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RunnerConfigsListResponse" - } - } - } - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/runner-configs/serverless-health-check": { - "post": { - "tags": ["runner_configs::serverless_health_check"], - "operationId": "runner_configs_serverless_health_check", - "parameters": [ - { - "name": "namespace", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RunnerConfigsServerlessHealthCheckRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RunnerConfigsServerlessHealthCheckResponse" - } - } - } - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/runner-configs/{runner_name}": { - "put": { - "tags": ["runner_configs::upsert"], - "operationId": "runner_configs_upsert", - "parameters": [ - { - "name": "runner_name", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "namespace", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RunnerConfigsUpsertRequestBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RunnerConfigsUpsertResponse" - } - } - } - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - }, - "delete": { - "tags": ["runner_configs::delete"], - "operationId": "runner_configs_delete", - "parameters": [ - { - "name": "runner_name", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "namespace", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RunnerConfigsDeleteResponse" - } - } - } - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/runner-configs/{runner_name}/refresh-metadata": { - "post": { - "tags": ["runner_configs::refresh_metadata"], - "operationId": "runner_configs_refresh_metadata", - "parameters": [ - { - "name": "runner_name", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "namespace", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RunnerConfigsRefreshMetadataRequestBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RunnerConfigsRefreshMetadataResponse" - } - } - } - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/runners": { - "get": { - "tags": ["runners"], - "operationId": "runners_list", - "parameters": [ - { - "name": "namespace", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "name", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "runner_ids", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "include_stopped", - "in": "query", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 0 - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RunnersListResponse" - } - } - } - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/runners/names": { - "get": { - "tags": ["runners"], - "summary": "## Datacenter Round Trips", - "description": "2 round trips:\n- GET /runners/names (fanout)\n- [api-peer] namespace::ops::resolve_for_name_global", - "operationId": "runners_list_names", - "parameters": [ - { - "name": "namespace", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 0 - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RunnersListNamesResponse" - } - } - } - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - } - }, - "components": { - "schemas": { - "Actor": { - "type": "object", - "required": [ - "actor_id", - "name", - "namespace_id", - "datacenter", - "runner_name_selector", - "crash_policy", - "create_ts" - ], - "properties": { - "actor_id": { - "$ref": "#/components/schemas/RivetId" - }, - "connectable_ts": { - "type": ["integer", "null"], - "format": "int64" - }, - "crash_policy": { - "$ref": "#/components/schemas/CrashPolicy" - }, - "create_ts": { - "type": "integer", - "format": "int64" - }, - "datacenter": { - "type": "string" - }, - "destroy_ts": { - "type": ["integer", "null"], - "format": "int64" - }, - "key": { - "type": ["string", "null"] - }, - "name": { - "type": "string" - }, - "namespace_id": { - "$ref": "#/components/schemas/RivetId" - }, - "pending_allocation_ts": { - "type": ["integer", "null"], - "format": "int64" - }, - "runner_name_selector": { - "type": "string" - }, - "sleep_ts": { - "type": ["integer", "null"], - "format": "int64" - }, - "start_ts": { - "type": ["integer", "null"], - "format": "int64" - } - } - }, - "ActorName": { - "type": "object", - "required": ["metadata"], - "properties": { - "metadata": { - "type": "object", - "additionalProperties": {}, - "propertyNames": { - "type": "string" - } - } - } - }, - "ActorsCreateRequest": { - "type": "object", - "required": ["name", "runner_name_selector", "crash_policy"], - "properties": { - "crash_policy": { - "$ref": "#/components/schemas/CrashPolicy" - }, - "datacenter": { - "type": ["string", "null"] - }, - "input": { - "type": ["string", "null"] - }, - "key": { - "type": ["string", "null"] - }, - "name": { - "type": "string" - }, - "runner_name_selector": { - "type": "string" - } - }, - "additionalProperties": false - }, - "ActorsCreateResponse": { - "type": "object", - "required": ["actor"], - "properties": { - "actor": { - "$ref": "#/components/schemas/Actor" - } - }, - "additionalProperties": false - }, - "ActorsDeleteResponse": { - "type": "object" - }, - "ActorsGetOrCreateRequest": { - "type": "object", - "required": [ - "name", - "key", - "runner_name_selector", - "crash_policy" - ], - "properties": { - "crash_policy": { - "$ref": "#/components/schemas/CrashPolicy" - }, - "datacenter": { - "type": ["string", "null"] - }, - "input": { - "type": ["string", "null"] - }, - "key": { - "type": "string" - }, - "name": { - "type": "string" - }, - "runner_name_selector": { - "type": "string" - } - }, - "additionalProperties": false - }, - "ActorsGetOrCreateResponse": { - "type": "object", - "required": ["actor", "created"], - "properties": { - "actor": { - "$ref": "#/components/schemas/Actor" - }, - "created": { - "type": "boolean" - } - } - }, - "ActorsListNamesResponse": { - "type": "object", - "required": ["names", "pagination"], - "properties": { - "names": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/ActorName" - }, - "propertyNames": { - "type": "string" - } - }, - "pagination": { - "$ref": "#/components/schemas/Pagination" - } - }, - "additionalProperties": false - }, - "ActorsListResponse": { - "type": "object", - "required": ["actors", "pagination"], - "properties": { - "actors": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Actor" - } - }, - "pagination": { - "$ref": "#/components/schemas/Pagination" - } - }, - "additionalProperties": false - }, - "CrashPolicy": { - "type": "string", - "enum": ["restart", "sleep", "destroy"] - }, - "Datacenter": { - "type": "object", - "required": ["label", "name", "url"], - "properties": { - "label": { - "type": "integer", - "format": "int32", - "minimum": 0 - }, - "name": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "additionalProperties": false - }, - "DatacenterHealth": { - "type": "object", - "required": ["datacenter_label", "datacenter_name", "status"], - "properties": { - "datacenter_label": { - "type": "integer", - "format": "int32", - "minimum": 0 - }, - "datacenter_name": { - "type": "string" - }, - "error": { - "type": ["string", "null"] - }, - "response": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/HealthResponse" - } - ] - }, - "rtt_ms": { - "type": ["number", "null"], - "format": "double" - }, - "status": { - "$ref": "#/components/schemas/HealthStatus" - } - } - }, - "DatacentersListResponse": { - "type": "object", - "required": ["datacenters", "pagination"], - "properties": { - "datacenters": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Datacenter" - } - }, - "pagination": { - "$ref": "#/components/schemas/Pagination" - } - }, - "additionalProperties": false - }, - "HealthFanoutResponse": { - "type": "object", - "required": ["datacenters"], - "properties": { - "datacenters": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DatacenterHealth" - } - } - } - }, - "HealthResponse": { - "type": "object", - "required": ["runtime", "status", "version"], - "properties": { - "runtime": { - "type": "string" - }, - "status": { - "type": "string" - }, - "version": { - "type": "string" - } - } - }, - "HealthStatus": { - "type": "string", - "enum": ["ok", "error"] - }, - "Namespace": { - "type": "object", - "required": [ - "namespace_id", - "name", - "display_name", - "create_ts" - ], - "properties": { - "create_ts": { - "type": "integer", - "format": "int64" - }, - "display_name": { - "type": "string" - }, - "name": { - "type": "string" - }, - "namespace_id": { - "$ref": "#/components/schemas/RivetId" - } - } - }, - "NamespaceListResponse": { - "type": "object", - "required": ["namespaces", "pagination"], - "properties": { - "namespaces": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Namespace" - } - }, - "pagination": { - "$ref": "#/components/schemas/Pagination" - } - }, - "additionalProperties": false - }, - "NamespacesCreateRequest": { - "type": "object", - "required": ["name", "display_name"], - "properties": { - "display_name": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "additionalProperties": false - }, - "NamespacesCreateResponse": { - "type": "object", - "required": ["namespace"], - "properties": { - "namespace": { - "$ref": "#/components/schemas/Namespace" - } - }, - "additionalProperties": false - }, - "Pagination": { - "type": "object", - "properties": { - "cursor": { - "type": ["string", "null"] - } - }, - "additionalProperties": false - }, - "RivetId": { - "type": "string" - }, - "Runner": { - "type": "object", - "required": [ - "runner_id", - "namespace_id", - "datacenter", - "name", - "key", - "version", - "total_slots", - "remaining_slots", - "create_ts", - "last_ping_ts", - "last_rtt" - ], - "properties": { - "create_ts": { - "type": "integer", - "format": "int64" - }, - "datacenter": { - "type": "string" - }, - "drain_ts": { - "type": ["integer", "null"], - "format": "int64" - }, - "key": { - "type": "string" - }, - "last_connected_ts": { - "type": ["integer", "null"], - "format": "int64" - }, - "last_ping_ts": { - "type": "integer", - "format": "int64" - }, - "last_rtt": { - "type": "integer", - "format": "int32", - "minimum": 0 - }, - "metadata": { - "type": ["object", "null"], - "additionalProperties": {}, - "propertyNames": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "namespace_id": { - "$ref": "#/components/schemas/RivetId" - }, - "remaining_slots": { - "type": "integer", - "format": "int32", - "minimum": 0 - }, - "runner_id": { - "$ref": "#/components/schemas/RivetId" - }, - "stop_ts": { - "type": ["integer", "null"], - "format": "int64" - }, - "total_slots": { - "type": "integer", - "format": "int32", - "minimum": 0 - }, - "version": { - "type": "integer", - "format": "int32", - "minimum": 0 - } - }, - "additionalProperties": false - }, - "RunnerConfig": { - "allOf": [ - { - "$ref": "#/components/schemas/RunnerConfigKind" - }, - { - "type": "object", - "properties": { - "metadata": {} - } - } - ] - }, - "RunnerConfigKind": { - "oneOf": [ - { - "type": "object", - "required": ["normal"], - "properties": { - "normal": { - "type": "object" - } - } - }, - { - "type": "object", - "required": ["serverless"], - "properties": { - "serverless": { - "type": "object", - "required": [ - "url", - "request_lifespan", - "slots_per_runner", - "max_runners" - ], - "properties": { - "headers": { - "type": ["object", "null"], - "additionalProperties": { - "type": "string" - }, - "propertyNames": { - "type": "string" - } - }, - "max_runners": { - "type": "integer", - "format": "int32", - "minimum": 0 - }, - "min_runners": { - "type": ["integer", "null"], - "format": "int32", - "minimum": 0 - }, - "request_lifespan": { - "type": "integer", - "format": "int32", - "description": "Seconds.", - "minimum": 0 - }, - "runners_margin": { - "type": ["integer", "null"], - "format": "int32", - "minimum": 0 - }, - "slots_per_runner": { - "type": "integer", - "format": "int32", - "minimum": 0 - }, - "url": { - "type": "string" - } - } - } - } - } - ] - }, - "RunnerConfigVariant": { - "type": "string", - "enum": ["serverless", "normal"] - }, - "RunnerConfigsDeleteResponse": { - "type": "object" - }, - "RunnerConfigsListResponse": { - "type": "object", - "required": ["runner_configs", "pagination"], - "properties": { - "pagination": { - "$ref": "#/components/schemas/Pagination" - }, - "runner_configs": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/RunnerConfigsListResponseRunnerConfigsValue" - }, - "propertyNames": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "RunnerConfigsListResponseRunnerConfigsValue": { - "type": "object", - "required": ["datacenters"], - "properties": { - "datacenters": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/RunnerConfig" - }, - "propertyNames": { - "type": "string" - } - } - } - }, - "RunnerConfigsRefreshMetadataRequestBody": { - "type": "object", - "additionalProperties": false - }, - "RunnerConfigsRefreshMetadataResponse": { - "type": "object", - "additionalProperties": false - }, - "RunnerConfigsServerlessHealthCheckRequest": { - "type": "object", - "required": ["url"], - "properties": { - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "propertyNames": { - "type": "string" - } - }, - "url": { - "type": "string" - } - }, - "additionalProperties": false - }, - "RunnerConfigsServerlessHealthCheckResponse": { - "oneOf": [ - { - "type": "object", - "required": ["success"], - "properties": { - "success": { - "type": "object", - "required": ["version"], - "properties": { - "version": { - "type": "string" - } - } - } - } - }, - { - "type": "object", - "required": ["failure"], - "properties": { - "failure": { - "type": "object", - "required": ["error"], - "properties": { - "error": { - "$ref": "#/components/schemas/RunnerConfigsServerlessMetadataError" - } - } - } - } - } - ] - }, - "RunnerConfigsServerlessMetadataError": { - "oneOf": [ - { - "type": "object", - "required": ["invalid_request"], - "properties": { - "invalid_request": { - "type": "object" - } - } - }, - { - "type": "object", - "required": ["request_failed"], - "properties": { - "request_failed": { - "type": "object" - } - } - }, - { - "type": "object", - "required": ["request_timed_out"], - "properties": { - "request_timed_out": { - "type": "object" - } - } - }, - { - "type": "object", - "required": ["non_success_status"], - "properties": { - "non_success_status": { - "type": "object", - "required": ["status_code", "body"], - "properties": { - "body": { - "type": "string" - }, - "status_code": { - "type": "integer", - "format": "int32", - "minimum": 0 - } - } - } - } - }, - { - "type": "object", - "required": ["invalid_response_json"], - "properties": { - "invalid_response_json": { - "type": "object", - "required": ["body"], - "properties": { - "body": { - "type": "string" - } - } - } - } - }, - { - "type": "object", - "required": ["invalid_response_schema"], - "properties": { - "invalid_response_schema": { - "type": "object", - "required": ["runtime", "version"], - "properties": { - "runtime": { - "type": "string" - }, - "version": { - "type": "string" - } - } - } - } - } - ] - }, - "RunnerConfigsUpsertRequestBody": { - "type": "object", - "required": ["datacenters"], - "properties": { - "datacenters": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/RunnerConfig" - }, - "propertyNames": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "RunnerConfigsUpsertResponse": { - "type": "object", - "required": ["endpoint_config_changed"], - "properties": { - "endpoint_config_changed": { - "type": "boolean" - } - } - }, - "RunnersListNamesResponse": { - "type": "object", - "required": ["names", "pagination"], - "properties": { - "names": { - "type": "array", - "items": { - "type": "string" - } - }, - "pagination": { - "$ref": "#/components/schemas/Pagination" - } - }, - "additionalProperties": false - }, - "RunnersListResponse": { - "type": "object", - "required": ["runners", "pagination"], - "properties": { - "pagination": { - "$ref": "#/components/schemas/Pagination" - }, - "runners": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Runner" - } - } - }, - "additionalProperties": false - } - }, - "securitySchemes": { - "bearer_auth": { - "type": "http", - "scheme": "bearer" - } - } - }, - "security": [ - { - "bearer_auth": [] - } - ] -} + "openapi": "3.1.0", + "info": { + "title": "rivet-api-public", + "description": "", + "contact": { + "name": "Rivet Gaming, LLC", + "email": "developer@rivet.gg" + }, + "license": { + "name": "Apache-2.0", + "identifier": "Apache-2.0" + }, + "version": "25.8.1" + }, + "paths": { + "/actors": { + "get": { + "tags": [ + "actors::list" + ], + "summary": " ## Datacenter Round Trips", + "description": " **If key is some & `include_destroyed` is false**\n\n 2 round trips:\n - namespace::ops::resolve_for_name_global\n - GET /actors (multiple DCs based on actor IDs)\n\n\tThis path is optimized because we can read the actor IDs fro the key directly from Epoxy with\n\tstale consistency to determine which datacenter the actor lives in. Under most circumstances,\n\tthis means we don't need to fan out to all datacenters (like normal list does).\n\n\tThe reason `include_destroyed` has to be false is Epoxy only stores currently active actors. If\n\t`include_destroyed` is true, we show all previous iterations of actors with the same key.\n\n **Otherwise**\n\n 2 round trips:\n - namespace::ops::resolve_for_name_global\n - GET /actors (fanout)\n\n ## Optimized Alternative Routes", + "operationId": "actors_list", + "parameters": [ + { + "name": "namespace", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "key", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "actor_ids", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "include_destroyed", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActorsListResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "actors::get_or_create" + ], + "summary": "## Datacenter Round Trips", + "description": "**If actor exists**\n\n2 round trips:\n- namespace::ops::resolve_for_name_global\n- GET /actors/{}\n\n**If actor does not exist and is created in the current datacenter:**\n\n2 round trips:\n- namespace::ops::resolve_for_name_global\n- [pegboard::workflows::actor] Create actor workflow (includes Epoxy key allocation)\n\n**If actor does not exist and is created in a different datacenter:**\n\n3 round trips:\n- namespace::ops::resolve_for_name_global\n- POST /actors to remote datacenter\n- [pegboard::workflows::actor] Create actor workflow (includes Epoxy key allocation)\n\nactor::get will always be in the same datacenter.\n\n## Optimized Alternative Routes", + "operationId": "actors_get_or_create", + "parameters": [ + { + "name": "namespace", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActorsGetOrCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActorsGetOrCreateResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "actors::create" + ], + "summary": "## Datacenter Round Trips", + "description": "**If actor is created in the current datacenter:**\n\n2 round trips:\n- namespace::ops::resolve_for_name_global\n- [pegboard::workflows::actor] Create actor workflow (includes Epoxy key allocation)\n\n**If actor is created in a different datacenter:**\n\n3 round trips:\n- namespace::ops::resolve_for_name_global\n- POST /actors to remote datacenter\n- [pegboard::workflows::actor] Create actor workflow (includes Epoxy key allocation)\n\nactor::get will always be in the same datacenter.", + "operationId": "actors_create", + "parameters": [ + { + "name": "namespace", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActorsCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActorsCreateResponse" + } + } + } + } + } + } + }, + "/actors/names": { + "get": { + "tags": [ + "actors::list_names" + ], + "summary": "## Datacenter Round Trips", + "description": "2 round trips:\n- GET /actors/names (fanout)\n- [api-peer] namespace::ops::resolve_for_name_global", + "operationId": "actors_list_names", + "parameters": [ + { + "name": "namespace", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActorsListNamesResponse" + } + } + } + } + }, + "security": [ + { + "bearer_auth": [] + } + ] + } + }, + "/actors/{actor_id}": { + "delete": { + "tags": [ + "actors::delete" + ], + "summary": "## Datacenter Round Trips", + "description": "2 round trip:\n- DELETE /actors/{}\n- [api-peer] namespace::ops::resolve_for_name_global", + "operationId": "actors_delete", + "parameters": [ + { + "name": "actor_id", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/RivetId" + } + }, + { + "name": "namespace", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActorsDeleteResponse" + } + } + } + } + }, + "security": [ + { + "bearer_auth": [] + } + ] + } + }, + "/datacenters": { + "get": { + "tags": [ + "datacenters" + ], + "operationId": "datacenters_list", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatacentersListResponse" + } + } + } + } + }, + "security": [ + { + "bearer_auth": [] + } + ] + } + }, + "/health/fanout": { + "get": { + "tags": [ + "health" + ], + "operationId": "health_fanout", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthFanoutResponse" + } + } + } + } + }, + "security": [ + { + "bearer_auth": [] + } + ] + } + }, + "/namespaces": { + "get": { + "tags": [ + "namespaces" + ], + "operationId": "namespaces_list", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "namespace_ids", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NamespaceListResponse" + } + } + } + } + }, + "security": [ + { + "bearer_auth": [] + } + ] + }, + "post": { + "tags": [ + "namespaces" + ], + "operationId": "namespaces_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NamespacesCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NamespacesCreateResponse" + } + } + } + } + }, + "security": [ + { + "bearer_auth": [] + } + ] + } + }, + "/runner-configs": { + "get": { + "tags": [ + "runner_configs::list" + ], + "operationId": "runner_configs_list", + "parameters": [ + { + "name": "namespace", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "variant", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/RunnerConfigVariant" + } + }, + { + "name": "runner_names", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunnerConfigsListResponse" + } + } + } + } + }, + "security": [ + { + "bearer_auth": [] + } + ] + } + }, + "/runner-configs/serverless-health-check": { + "post": { + "tags": [ + "runner_configs::serverless_health_check" + ], + "operationId": "runner_configs_serverless_health_check", + "parameters": [ + { + "name": "namespace", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunnerConfigsServerlessHealthCheckRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunnerConfigsServerlessHealthCheckResponse" + } + } + } + } + }, + "security": [ + { + "bearer_auth": [] + } + ] + } + }, + "/runner-configs/{runner_name}": { + "put": { + "tags": [ + "runner_configs::upsert" + ], + "operationId": "runner_configs_upsert", + "parameters": [ + { + "name": "runner_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "namespace", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunnerConfigsUpsertRequestBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunnerConfigsUpsertResponse" + } + } + } + } + }, + "security": [ + { + "bearer_auth": [] + } + ] + }, + "delete": { + "tags": [ + "runner_configs::delete" + ], + "operationId": "runner_configs_delete", + "parameters": [ + { + "name": "runner_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "namespace", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunnerConfigsDeleteResponse" + } + } + } + } + }, + "security": [ + { + "bearer_auth": [] + } + ] + } + }, + "/runner-configs/{runner_name}/refresh-metadata": { + "post": { + "tags": [ + "runner_configs::refresh_metadata" + ], + "operationId": "runner_configs_refresh_metadata", + "parameters": [ + { + "name": "runner_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "namespace", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunnerConfigsRefreshMetadataRequestBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunnerConfigsRefreshMetadataResponse" + } + } + } + } + }, + "security": [ + { + "bearer_auth": [] + } + ] + } + }, + "/runners": { + "get": { + "tags": [ + "runners" + ], + "operationId": "runners_list", + "parameters": [ + { + "name": "namespace", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "runner_ids", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "include_stopped", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunnersListResponse" + } + } + } + } + }, + "security": [ + { + "bearer_auth": [] + } + ] + } + }, + "/runners/names": { + "get": { + "tags": [ + "runners" + ], + "summary": "## Datacenter Round Trips", + "description": "2 round trips:\n- GET /runners/names (fanout)\n- [api-peer] namespace::ops::resolve_for_name_global", + "operationId": "runners_list_names", + "parameters": [ + { + "name": "namespace", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunnersListNamesResponse" + } + } + } + } + }, + "security": [ + { + "bearer_auth": [] + } + ] + } + } + }, + "components": { + "schemas": { + "Actor": { + "type": "object", + "required": [ + "actor_id", + "name", + "namespace_id", + "datacenter", + "runner_name_selector", + "crash_policy", + "create_ts" + ], + "properties": { + "actor_id": { + "$ref": "#/components/schemas/RivetId" + }, + "connectable_ts": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "crash_policy": { + "$ref": "#/components/schemas/CrashPolicy" + }, + "create_ts": { + "type": "integer", + "format": "int64" + }, + "datacenter": { + "type": "string" + }, + "destroy_ts": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "key": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "namespace_id": { + "$ref": "#/components/schemas/RivetId" + }, + "pending_allocation_ts": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "runner_name_selector": { + "type": "string" + }, + "sleep_ts": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "start_ts": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + } + }, + "ActorName": { + "type": "object", + "required": [ + "metadata" + ], + "properties": { + "metadata": { + "type": "object", + "additionalProperties": {}, + "propertyNames": { + "type": "string" + } + } + } + }, + "ActorsCreateRequest": { + "type": "object", + "required": [ + "name", + "runner_name_selector", + "crash_policy" + ], + "properties": { + "crash_policy": { + "$ref": "#/components/schemas/CrashPolicy" + }, + "datacenter": { + "type": [ + "string", + "null" + ] + }, + "input": { + "type": [ + "string", + "null" + ] + }, + "key": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "runner_name_selector": { + "type": "string" + } + }, + "additionalProperties": false + }, + "ActorsCreateResponse": { + "type": "object", + "required": [ + "actor" + ], + "properties": { + "actor": { + "$ref": "#/components/schemas/Actor" + } + }, + "additionalProperties": false + }, + "ActorsDeleteResponse": { + "type": "object" + }, + "ActorsGetOrCreateRequest": { + "type": "object", + "required": [ + "name", + "key", + "runner_name_selector", + "crash_policy" + ], + "properties": { + "crash_policy": { + "$ref": "#/components/schemas/CrashPolicy" + }, + "datacenter": { + "type": [ + "string", + "null" + ] + }, + "input": { + "type": [ + "string", + "null" + ] + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "runner_name_selector": { + "type": "string" + } + }, + "additionalProperties": false + }, + "ActorsGetOrCreateResponse": { + "type": "object", + "required": [ + "actor", + "created" + ], + "properties": { + "actor": { + "$ref": "#/components/schemas/Actor" + }, + "created": { + "type": "boolean" + } + } + }, + "ActorsListNamesResponse": { + "type": "object", + "required": [ + "names", + "pagination" + ], + "properties": { + "names": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ActorName" + }, + "propertyNames": { + "type": "string" + } + }, + "pagination": { + "$ref": "#/components/schemas/Pagination" + } + }, + "additionalProperties": false + }, + "ActorsListResponse": { + "type": "object", + "required": [ + "actors", + "pagination" + ], + "properties": { + "actors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Actor" + } + }, + "pagination": { + "$ref": "#/components/schemas/Pagination" + } + }, + "additionalProperties": false + }, + "CrashPolicy": { + "type": "string", + "enum": [ + "restart", + "sleep", + "destroy" + ] + }, + "Datacenter": { + "type": "object", + "required": [ + "label", + "name", + "url" + ], + "properties": { + "label": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "DatacenterHealth": { + "type": "object", + "required": [ + "datacenter_label", + "datacenter_name", + "status" + ], + "properties": { + "datacenter_label": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "datacenter_name": { + "type": "string" + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "response": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/HealthResponse" + } + ] + }, + "rtt_ms": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "status": { + "$ref": "#/components/schemas/HealthStatus" + } + } + }, + "DatacentersListResponse": { + "type": "object", + "required": [ + "datacenters", + "pagination" + ], + "properties": { + "datacenters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Datacenter" + } + }, + "pagination": { + "$ref": "#/components/schemas/Pagination" + } + }, + "additionalProperties": false + }, + "HealthFanoutResponse": { + "type": "object", + "required": [ + "datacenters" + ], + "properties": { + "datacenters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DatacenterHealth" + } + } + } + }, + "HealthResponse": { + "type": "object", + "required": [ + "runtime", + "status", + "version" + ], + "properties": { + "runtime": { + "type": "string" + }, + "status": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "HealthStatus": { + "type": "string", + "enum": [ + "ok", + "error" + ] + }, + "Namespace": { + "type": "object", + "required": [ + "namespace_id", + "name", + "display_name", + "create_ts" + ], + "properties": { + "create_ts": { + "type": "integer", + "format": "int64" + }, + "display_name": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace_id": { + "$ref": "#/components/schemas/RivetId" + } + } + }, + "NamespaceListResponse": { + "type": "object", + "required": [ + "namespaces", + "pagination" + ], + "properties": { + "namespaces": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Namespace" + } + }, + "pagination": { + "$ref": "#/components/schemas/Pagination" + } + }, + "additionalProperties": false + }, + "NamespacesCreateRequest": { + "type": "object", + "required": [ + "name", + "display_name" + ], + "properties": { + "display_name": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "NamespacesCreateResponse": { + "type": "object", + "required": [ + "namespace" + ], + "properties": { + "namespace": { + "$ref": "#/components/schemas/Namespace" + } + }, + "additionalProperties": false + }, + "Pagination": { + "type": "object", + "properties": { + "cursor": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "RivetId": { + "type": "string" + }, + "Runner": { + "type": "object", + "required": [ + "runner_id", + "namespace_id", + "datacenter", + "name", + "key", + "version", + "total_slots", + "remaining_slots", + "create_ts", + "last_ping_ts", + "last_rtt" + ], + "properties": { + "create_ts": { + "type": "integer", + "format": "int64" + }, + "datacenter": { + "type": "string" + }, + "drain_ts": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "key": { + "type": "string" + }, + "last_connected_ts": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "last_ping_ts": { + "type": "integer", + "format": "int64" + }, + "last_rtt": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "propertyNames": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "namespace_id": { + "$ref": "#/components/schemas/RivetId" + }, + "remaining_slots": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "runner_id": { + "$ref": "#/components/schemas/RivetId" + }, + "stop_ts": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "total_slots": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "version": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + }, + "additionalProperties": false + }, + "RunnerConfig": { + "allOf": [ + { + "$ref": "#/components/schemas/RunnerConfigKind" + }, + { + "type": "object", + "properties": { + "metadata": {} + } + } + ] + }, + "RunnerConfigKind": { + "oneOf": [ + { + "type": "object", + "required": [ + "normal" + ], + "properties": { + "normal": { + "type": "object" + } + } + }, + { + "type": "object", + "required": [ + "serverless" + ], + "properties": { + "serverless": { + "type": "object", + "required": [ + "url", + "request_lifespan", + "slots_per_runner", + "max_runners" + ], + "properties": { + "headers": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + }, + "propertyNames": { + "type": "string" + } + }, + "max_runners": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "min_runners": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "minimum": 0 + }, + "request_lifespan": { + "type": "integer", + "format": "int32", + "description": "Seconds.", + "minimum": 0 + }, + "runners_margin": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "minimum": 0 + }, + "slots_per_runner": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "url": { + "type": "string" + } + } + } + } + } + ] + }, + "RunnerConfigVariant": { + "type": "string", + "enum": [ + "serverless", + "normal" + ] + }, + "RunnerConfigsDeleteResponse": { + "type": "object" + }, + "RunnerConfigsListResponse": { + "type": "object", + "required": [ + "runner_configs", + "pagination" + ], + "properties": { + "pagination": { + "$ref": "#/components/schemas/Pagination" + }, + "runner_configs": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/RunnerConfigsListResponseRunnerConfigsValue" + }, + "propertyNames": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "RunnerConfigsListResponseRunnerConfigsValue": { + "type": "object", + "required": [ + "datacenters" + ], + "properties": { + "datacenters": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/RunnerConfig" + }, + "propertyNames": { + "type": "string" + } + } + } + }, + "RunnerConfigsRefreshMetadataRequestBody": { + "type": "object", + "additionalProperties": false + }, + "RunnerConfigsRefreshMetadataResponse": { + "type": "object", + "additionalProperties": false + }, + "RunnerConfigsServerlessHealthCheckRequest": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "propertyNames": { + "type": "string" + } + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "RunnerConfigsServerlessHealthCheckResponse": { + "oneOf": [ + { + "type": "object", + "required": [ + "success" + ], + "properties": { + "success": { + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "required": [ + "failure" + ], + "properties": { + "failure": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "$ref": "#/components/schemas/RunnerConfigsServerlessMetadataError" + } + } + } + } + } + ] + }, + "RunnerConfigsServerlessMetadataError": { + "oneOf": [ + { + "type": "object", + "required": [ + "invalid_request" + ], + "properties": { + "invalid_request": { + "type": "object" + } + } + }, + { + "type": "object", + "required": [ + "request_failed" + ], + "properties": { + "request_failed": { + "type": "object" + } + } + }, + { + "type": "object", + "required": [ + "request_timed_out" + ], + "properties": { + "request_timed_out": { + "type": "object" + } + } + }, + { + "type": "object", + "required": [ + "non_success_status" + ], + "properties": { + "non_success_status": { + "type": "object", + "required": [ + "status_code", + "body" + ], + "properties": { + "body": { + "type": "string" + }, + "status_code": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + } + } + }, + { + "type": "object", + "required": [ + "invalid_response_json" + ], + "properties": { + "invalid_response_json": { + "type": "object", + "required": [ + "body" + ], + "properties": { + "body": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "required": [ + "invalid_response_schema" + ], + "properties": { + "invalid_response_schema": { + "type": "object", + "required": [ + "runtime", + "version" + ], + "properties": { + "runtime": { + "type": "string" + }, + "version": { + "type": "string" + } + } + } + } + } + ] + }, + "RunnerConfigsUpsertRequestBody": { + "type": "object", + "required": [ + "datacenters" + ], + "properties": { + "datacenters": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/RunnerConfig" + }, + "propertyNames": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "RunnerConfigsUpsertResponse": { + "type": "object", + "required": [ + "endpoint_config_changed" + ], + "properties": { + "endpoint_config_changed": { + "type": "boolean" + } + } + }, + "RunnersListNamesResponse": { + "type": "object", + "required": [ + "names", + "pagination" + ], + "properties": { + "names": { + "type": "array", + "items": { + "type": "string" + } + }, + "pagination": { + "$ref": "#/components/schemas/Pagination" + } + }, + "additionalProperties": false + }, + "RunnersListResponse": { + "type": "object", + "required": [ + "runners", + "pagination" + ], + "properties": { + "pagination": { + "$ref": "#/components/schemas/Pagination" + }, + "runners": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Runner" + } + } + }, + "additionalProperties": false + } + }, + "securitySchemes": { + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + } + }, + "security": [ + { + "bearer_auth": [] + } + ] +} \ No newline at end of file diff --git a/engine/contrib-docs/DOCKER.md b/engine/contrib-docs/DOCKER.md index a4397e0f70..381e5a3de1 100644 --- a/engine/contrib-docs/DOCKER.md +++ b/engine/contrib-docs/DOCKER.md @@ -1,5 +1,5 @@ # Runner Docker Compose -`docker compose -f ./docker/dev/docker-compose.yml up --build` +`docker compose -f ../docker/dev/docker-compose.yml up --build` > Sometimes adding `--force-recreate` is required to get it to deploy the new build diff --git a/engine/docker/engine/build.sh b/engine/docker/engine/build.sh index 6aa2491ab7..3d6bdac637 100755 --- a/engine/docker/engine/build.sh +++ b/engine/docker/engine/build.sh @@ -10,30 +10,35 @@ case $TARGET in DOCKERFILE="linux-x86_64.Dockerfile" TARGET_STAGE="x86_64-builder" BINARY="rivet-engine-$TARGET" + PLATFORM="linux/amd64" ;; aarch64-unknown-linux-musl) echo "Building for Linux ARM64 platform" DOCKERFILE="linux-aarch64.Dockerfile" TARGET_STAGE="aarch64-builder" BINARY="rivet-engine-$TARGET" + PLATFORM="linux/arm64" ;; aarch64-apple-darwin) echo "Building for macOS ARM64 platform" DOCKERFILE="macos-aarch64.Dockerfile" TARGET_STAGE="aarch64-builder" BINARY="rivet-engine-$TARGET" + PLATFORM="linux/arm64" ;; x86_64-apple-darwin) echo "Building for macOS x86_64 platform" - DOCKERFILE="macos-x86_64.Dockerfile" + DOCKERFILE="macos-x86_64.Dockerfile" TARGET_STAGE="x86_64-builder" BINARY="rivet-engine-$TARGET" + PLATFORM="linux/amd64" ;; x86_64-pc-windows-gnu) echo "Building for Windows platform" DOCKERFILE="windows.Dockerfile" TARGET_STAGE="" # No target stage for Windows BINARY="rivet-engine-$TARGET.exe" + PLATFORM="linux/amd64" ;; *) echo "Unsupported target: $TARGET" @@ -43,9 +48,9 @@ esac # Build docker image with target stage (if specified) if [ -n "$TARGET_STAGE" ]; then - DOCKER_BUILDKIT=1 docker build --target $TARGET_STAGE -f docker/engine/$DOCKERFILE -t rivet-engine-builder-$TARGET . + DOCKER_BUILDKIT=1 docker build --target $TARGET_STAGE -f engine/docker/engine/$DOCKERFILE -t rivet-engine-builder-$TARGET . else - DOCKER_BUILDKIT=1 docker build -f docker/engine/$DOCKERFILE -t rivet-engine-builder-$TARGET . + DOCKER_BUILDKIT=1 docker build -f engine/docker/engine/$DOCKERFILE -t rivet-engine-builder-$TARGET . fi # Extract binary diff --git a/engine/docker/engine/linux-aarch64.Dockerfile b/engine/docker/engine/linux-aarch64.Dockerfile index 28eabff4ad..54d3e8069b 100644 --- a/engine/docker/engine/linux-aarch64.Dockerfile +++ b/engine/docker/engine/linux-aarch64.Dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.4 +# syntax=docker/dockerfile:1.10.0 FROM rust:1.88.0 AS base ARG BUILD_FRONTEND=true @@ -16,16 +16,17 @@ RUN apt-get update && apt-get install -y \ protobuf-compiler \ ca-certificates \ g++ \ - g++-multilib \ git-lfs \ - curl && \ + curl \ + linux-libc-dev \ + xz-utils && \ curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ apt-get install -y nodejs && \ corepack enable && \ rm -rf /var/lib/apt/lists/* && \ - wget -q https://github.com/cross-tools/musl-cross/releases/download/20250815/aarch64-unknown-linux-musl.tar.xz && \ - tar -xzf aarch64-unknown-linux-musl.tgz -C /opt/ && \ - rm aarch64-unknown-linux-musl.tgz + wget -q https://musl.cc/aarch64-linux-musl-cross.tgz && \ + tar -xzf aarch64-linux-musl-cross.tgz -C /opt/ && \ + rm aarch64-linux-musl-cross.tgz # Disable interactive prompt ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 @@ -34,13 +35,13 @@ ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 RUN rustup target add aarch64-unknown-linux-musl # Set environment variables -ENV PATH="/opt/aarch64-unknown-linux-musl/bin:$PATH" \ +ENV PATH="/opt/aarch64-linux-musl-cross/bin:$PATH" \ LIBCLANG_PATH=/usr/lib/llvm-14/lib \ CLANG_PATH=/usr/bin/clang-14 \ - CC_aarch64_unknown_linux_musl=aarch64-unknown-linux-musl-gcc \ - CXX_aarch64_unknown_linux_musl=aarch64-unknown-linux-musl-g++ \ - AR_aarch64_unknown_linux_musl=aarch64-unknown-linux-musl-ar \ - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-unknown-linux-musl-gcc \ + CC_aarch64_unknown_linux_musl=aarch64-linux-musl-gcc \ + CXX_aarch64_unknown_linux_musl=aarch64-linux-musl-g++ \ + AR_aarch64_unknown_linux_musl=aarch64-linux-musl-ar \ + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-gcc \ CARGO_INCREMENTAL=0 \ RUSTFLAGS="--cfg tokio_unstable -C target-feature=+crt-static -C link-arg=-static-libgcc" \ CARGO_NET_GIT_FETCH_WITH_CLI=true @@ -56,9 +57,11 @@ ENV SSL_VER=1.1.1w RUN wget https://www.openssl.org/source/openssl-$SSL_VER.tar.gz \ && tar -xzf openssl-$SSL_VER.tar.gz \ && cd openssl-$SSL_VER \ - && ./Configure no-shared no-async --prefix=/musl-aarch64 --openssldir=/musl-aarch64/ssl linux-aarch64 \ - && make -j$(nproc) \ - && make install_sw \ + && CC=aarch64-linux-musl-gcc \ + ./Configure no-shared no-async no-tests --prefix=/musl-aarch64 --openssldir=/musl-aarch64/ssl linux-aarch64 \ + && make -j$(nproc) build_libs \ + && make install_dev \ + && mkdir -p /musl-aarch64/ssl \ && cd .. \ && rm -rf openssl-$SSL_VER* @@ -66,6 +69,7 @@ RUN wget https://www.openssl.org/source/openssl-$SSL_VER.tar.gz \ ENV OPENSSL_DIR=/musl-aarch64 \ OPENSSL_INCLUDE_DIR=/musl-aarch64/include \ OPENSSL_LIB_DIR=/musl-aarch64/lib \ + OPENSSL_STATIC=1 \ PKG_CONFIG_ALLOW_CROSS=1 # Copy the source code @@ -73,13 +77,12 @@ COPY . . # Build frontend RUN if [ "$BUILD_FRONTEND" = "true" ]; then \ - (cd sdks/typescript/api-full && pnpm install && pnpm run build) && \ - (cd frontend && pnpm install && \ + pnpm install && \ if [ -n "$VITE_APP_API_URL" ]; then \ - VITE_APP_API_URL="${VITE_APP_API_URL}" pnpm run build:engine; \ + VITE_APP_API_URL="${VITE_APP_API_URL}" npx turbo build:engine -F @rivetkit/engine-frontend; \ else \ - pnpm run build:engine; \ - fi); \ + npx turbo build:engine -F @rivetkit/engine-frontend; \ + fi; \ fi # Build for Linux with musl (static binary) - aarch64 diff --git a/engine/docker/engine/linux-x86_64.Dockerfile b/engine/docker/engine/linux-x86_64.Dockerfile index 31675fb20f..a2c2dde596 100644 --- a/engine/docker/engine/linux-x86_64.Dockerfile +++ b/engine/docker/engine/linux-x86_64.Dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.4 +# syntax=docker/dockerfile:1.10.0 FROM rust:1.88.0 AS base ARG BUILD_FRONTEND=true @@ -73,13 +73,12 @@ COPY . . # Build frontend RUN if [ "$BUILD_FRONTEND" = "true" ]; then \ - (cd sdks/typescript/api-full && pnpm install && pnpm run build) && \ - (cd frontend && pnpm install && \ + pnpm install && \ if [ -n "$VITE_APP_API_URL" ]; then \ - VITE_APP_API_URL="${VITE_APP_API_URL}" pnpm run build:engine; \ + VITE_APP_API_URL="${VITE_APP_API_URL}" npx turbo build:engine -F @rivetkit/engine-frontend; \ else \ - pnpm run build:engine; \ - fi); \ + npx turbo build:engine -F @rivetkit/engine-frontend; \ + fi; \ fi # Build for Linux with musl (static binary) - x86_64 diff --git a/engine/docker/engine/macos-aarch64.Dockerfile b/engine/docker/engine/macos-aarch64.Dockerfile index 4878a79236..4b08efde00 100644 --- a/engine/docker/engine/macos-aarch64.Dockerfile +++ b/engine/docker/engine/macos-aarch64.Dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.4 +# syntax=docker/dockerfile:1.10.0 FROM rust:1.88.0 AS base ARG BUILD_FRONTEND=true @@ -72,13 +72,12 @@ COPY . . # Build frontend RUN if [ "$BUILD_FRONTEND" = "true" ]; then \ - (cd sdks/typescript/api-full && pnpm install && pnpm run build) && \ - (cd frontend && pnpm install && \ + pnpm install && \ if [ -n "$VITE_APP_API_URL" ]; then \ - VITE_APP_API_URL="${VITE_APP_API_URL}" pnpm run build:engine; \ + VITE_APP_API_URL="${VITE_APP_API_URL}" npx turbo build:engine -F @rivetkit/engine-frontend; \ else \ - pnpm run build:engine; \ - fi); \ + npx turbo build:engine -F @rivetkit/engine-frontend; \ + fi; \ fi # Build for ARM64 macOS diff --git a/engine/docker/engine/macos-x86_64.Dockerfile b/engine/docker/engine/macos-x86_64.Dockerfile index fdd3b503e4..5e38886bb2 100644 --- a/engine/docker/engine/macos-x86_64.Dockerfile +++ b/engine/docker/engine/macos-x86_64.Dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.4 +# syntax=docker/dockerfile:1.10.0 FROM rust:1.88.0 AS base ARG BUILD_FRONTEND=true @@ -72,13 +72,12 @@ COPY . . # Build frontend RUN if [ "$BUILD_FRONTEND" = "true" ]; then \ - (cd sdks/typescript/api-full && pnpm install && pnpm run build) && \ - (cd frontend && pnpm install && \ + pnpm install && \ if [ -n "$VITE_APP_API_URL" ]; then \ - VITE_APP_API_URL="${VITE_APP_API_URL}" pnpm run build:engine; \ + VITE_APP_API_URL="${VITE_APP_API_URL}" npx turbo build:engine -F @rivetkit/engine-frontend; \ else \ - pnpm run build:engine; \ - fi); \ + npx turbo build:engine -F @rivetkit/engine-frontend; \ + fi; \ fi # Build for x86_64 macOS diff --git a/engine/docker/engine/windows.Dockerfile b/engine/docker/engine/windows.Dockerfile index b5b1a68a9b..e4650e2fc4 100644 --- a/engine/docker/engine/windows.Dockerfile +++ b/engine/docker/engine/windows.Dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.4 +# syntax=docker/dockerfile:1.10.0 FROM rust:1.88.0 ARG BUILD_FRONTEND=true @@ -60,13 +60,12 @@ COPY . . # Build frontend RUN if [ "$BUILD_FRONTEND" = "true" ]; then \ - (cd sdks/typescript/api-full && pnpm install && pnpm run build) && \ - (cd frontend && pnpm install && \ + pnpm install && \ if [ -n "$VITE_APP_API_URL" ]; then \ - VITE_APP_API_URL="${VITE_APP_API_URL}" pnpm run build:engine; \ + VITE_APP_API_URL="${VITE_APP_API_URL}" npx turbo build:engine -F @rivetkit/engine-frontend; \ else \ - pnpm run build:engine; \ - fi); \ + npx turbo build:engine -F @rivetkit/engine-frontend; \ + fi; \ fi # Build for Windows diff --git a/engine/docker/template/src/context.ts b/engine/docker/template/src/context.ts index bd3a47bcd5..533b9b0b0a 100644 --- a/engine/docker/template/src/context.ts +++ b/engine/docker/template/src/context.ts @@ -1,5 +1,5 @@ -import * as fs from "fs"; -import * as path from "path"; +import * as fs from "node:fs"; +import * as path from "node:path"; import type { TemplateConfig } from "./config"; export const CORE_NETWORK_NAME = "rivet-core-network"; diff --git a/engine/docker/template/src/main.ts b/engine/docker/template/src/main.ts index 2c7dcef0eb..81ce5df379 100644 --- a/engine/docker/template/src/main.ts +++ b/engine/docker/template/src/main.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node -import * as fs from "fs"; -import * as path from "path"; +import * as fs from "node:fs"; +import * as path from "node:path"; import { TEMPLATES, type TemplateConfig } from "./config"; import { TemplateContext } from "./context"; import { generateDockerCompose } from "./docker-compose"; diff --git a/engine/docker/template/src/services/core/grafana.ts b/engine/docker/template/src/services/core/grafana.ts index 76a7d2f021..d5e3ad2fa0 100644 --- a/engine/docker/template/src/services/core/grafana.ts +++ b/engine/docker/template/src/services/core/grafana.ts @@ -1,6 +1,6 @@ -import * as fs from "fs"; +import * as fs from "node:fs"; +import * as path from "node:path"; import * as yaml from "js-yaml"; -import * as path from "path"; import type { TemplateContext } from "../../context"; export function generateCoreGrafana(context: TemplateContext) { diff --git a/engine/docker/universal/Dockerfile b/engine/docker/universal/Dockerfile index 226adaa5b3..e411855677 100644 --- a/engine/docker/universal/Dockerfile +++ b/engine/docker/universal/Dockerfile @@ -2,7 +2,7 @@ # MARK: Builder # TODO(RVT-4168): Compile libfdb from scratch for ARM -FROM --platform=linux/amd64 rust:1.88.0-bookworm AS builder +FROM rust:1.88.0-bookworm AS builder # Docker automatically provides TARGETARCH ARG TARGETARCH @@ -49,13 +49,12 @@ COPY . . # Build frontend RUN if [ "$BUILD_FRONTEND" = "true" ]; then \ - (cd sdks/typescript/api-full && pnpm install && pnpm run build) && \ - (cd frontend && pnpm install && \ + pnpm install && \ if [ -n "$VITE_APP_API_URL" ]; then \ - VITE_APP_API_URL="${VITE_APP_API_URL}" pnpm run build:engine; \ + VITE_APP_API_URL="${VITE_APP_API_URL}" npx turbo build:engine -F @rivetkit/engine-frontend; \ else \ - pnpm run build:engine; \ - fi); \ + npx turbo build:engine -F @rivetkit/engine-frontend; \ + fi; \ fi # Build and copy all binaries from target directory into an empty image (it is not @@ -76,7 +75,7 @@ RUN \ cp target/$CARGO_BUILD_MODE/rivet-engine /app/dist/ # MARK: Engine (full, base) -FROM --platform=linux/amd64 debian:12.9-slim AS engine-full-base +FROM debian:12.9-slim AS engine-full-base # Docker automatically provides TARGETARCH ARG TARGETARCH @@ -99,7 +98,7 @@ RUN apt-get update -y && \ fi # MARK: Engine (Full) -FROM --platform=linux/amd64 engine-full-base AS engine-full +FROM engine-full-base AS engine-full LABEL org.opencontainers.image.source=https://github.com/rivet-gg/rivet @@ -109,7 +108,7 @@ ENTRYPOINT ["/usr/bin/rivet-engine"] CMD ["start"] # MARK: Engine (Slim) -FROM --platform=linux/amd64 debian:12.9-slim AS engine-slim +FROM debian:12.9-slim AS engine-slim LABEL org.opencontainers.image.source=https://github.com/rivet-gg/rivet diff --git a/engine/sdks/typescript/runner/src/mod.ts b/engine/sdks/typescript/runner/src/mod.ts index c451e2c530..8f9d2ff2c8 100644 --- a/engine/sdks/typescript/runner/src/mod.ts +++ b/engine/sdks/typescript/runner/src/mod.ts @@ -45,8 +45,17 @@ export interface RunnerConfig { onConnected: () => void; onDisconnected: () => void; onShutdown: () => void; - fetch: (runner: Runner, actorId: string, request: Request) => Promise; - websocket?: (runner: Runner, actorId: string, ws: any, request: Request) => Promise; + fetch: ( + runner: Runner, + actorId: string, + request: Request, + ) => Promise; + websocket?: ( + runner: Runner, + actorId: string, + ws: any, + request: Request, + ) => Promise; onActorStart: ( actorId: string, generation: number, @@ -183,7 +192,11 @@ export class Runner { getActor(actorId: string, generation?: number): ActorInstance | undefined { const actor = this.#actors.get(actorId); if (!actor) { - logger()?.error({ msg: "actor not found", runnerId: this.runnerId, actorId }); + logger()?.error({ + msg: "actor not found", + runnerId: this.runnerId, + actorId, + }); return undefined; } if (generation !== undefined && actor.generation !== generation) { @@ -214,7 +227,11 @@ export class Runner { ): ActorInstance | undefined { const actor = this.#actors.get(actorId); if (!actor) { - logger()?.error({ msg: "actor not found for removal", runnerId: this.runnerId, actorId }); + logger()?.error({ + msg: "actor not found for removal", + runnerId: this.runnerId, + actorId, + }); return undefined; } if (generation !== undefined && actor.generation !== generation) { @@ -272,7 +289,7 @@ export class Runner { process.on("SIGTERM", () => { logger()?.debug("received SIGTERM"); - for (let handler of SIGNAL_HANDLERS) { + for (const handler of SIGNAL_HANDLERS) { handler(); } @@ -281,7 +298,7 @@ export class Runner { process.on("SIGINT", () => { logger()?.debug("received SIGINT"); - for (let handler of SIGNAL_HANDLERS) { + for (const handler of SIGNAL_HANDLERS) { handler(); } @@ -294,15 +311,20 @@ export class Runner { } SIGNAL_HANDLERS.push(() => { - let weak = new WeakRef(this); - weak.deref()?.shutdown(false, false) + const weak = new WeakRef(this); + weak.deref()?.shutdown(false, false); }); } } // MARK: Shutdown async shutdown(immediate: boolean, exit: boolean = false) { - logger()?.info({ msg: "starting shutdown", runnerId: this.runnerId, immediate, exit }); + logger()?.info({ + msg: "starting shutdown", + runnerId: this.runnerId, + immediate, + exit, + }); this.#shutdown = true; // Clear reconnect timeout @@ -395,12 +417,18 @@ export class Runner { // TODO: Wait for all actors to stop before closing ws - logger()?.info({ msg: "closing WebSocket", runnerId: this.runnerId }); + logger()?.info({ + msg: "closing WebSocket", + runnerId: this.runnerId, + }); pegboardWebSocket.close(1000, "Stopping"); await closePromise; - logger()?.info({ msg: "websocket shutdown completed", runnerId: this.runnerId }); + logger()?.info({ + msg: "websocket shutdown completed", + runnerId: this.runnerId, + }); } catch (error) { logger()?.error({ msg: "error during websocket shutdown:", @@ -506,7 +534,10 @@ export class Runner { }); } else { clearInterval(pingLoop); - logger()?.info({ msg: "WebSocket not open, stopping ping loop", runnerId: this.runnerId }); + logger()?.info({ + msg: "WebSocket not open, stopping ping loop", + runnerId: this.runnerId, + }); } }, pingInterval); this.#pingLoop = pingLoop; @@ -518,7 +549,10 @@ export class Runner { this.#sendCommandAcknowledgment(); } else { clearInterval(ackLoop); - logger()?.info({ msg: "WebSocket not open, stopping ack loop", runnerId: this.runnerId }); + logger()?.info({ + msg: "WebSocket not open, stopping ack loop", + runnerId: this.runnerId, + }); } }, ackInterval); this.#ackInterval = ackLoop; @@ -583,7 +617,10 @@ export class Runner { }); ws.addEventListener("error", (ev) => { - logger()?.error({ msg: `WebSocket error: ${ev.error}`, runnerId: this.runnerId }); + logger()?.error({ + msg: `WebSocket error: ${ev.error}`, + runnerId: this.runnerId, + }); if (!this.#shutdown) { // Start runner lost timeout if we have a threshold and are not shutting down @@ -618,7 +655,10 @@ export class Runner { this.#config.onDisconnected(); if (ev.reason.toString().startsWith("ws.eviction")) { - logger()?.info({ msg: "runner evicted", runnerId: this.runnerId }); + logger()?.info({ + msg: "runner evicted", + runnerId: this.runnerId, + }); await this.shutdown(true); } @@ -666,7 +706,11 @@ export class Runner { }); for (const commandWrapper of commands) { - logger()?.info({ msg: "received command", runnerId: this.runnerId, commandWrapper }); + logger()?.info({ + msg: "received command", + runnerId: this.runnerId, + commandWrapper, + }); if (commandWrapper.inner.tag === "CommandStartActor") { this.#handleCommandStartActor(commandWrapper); } else if (commandWrapper.inner.tag === "CommandStopActor") { @@ -781,7 +825,10 @@ export class Runner { intentType: "sleep" | "stop", ) { if (this.#shutdown) { - logger()?.warn({ msg: "Runner is shut down, cannot send actor intent", runnerId: this.runnerId }); + logger()?.warn({ + msg: "Runner is shut down, cannot send actor intent", + runnerId: this.runnerId, + }); return; } let actorIntent: protocol.ActorIntent; @@ -1363,7 +1410,10 @@ export class Runner { #scheduleReconnect() { if (this.#shutdown) { - logger()?.debug({ msg: "Runner is shut down, not attempting reconnect", runnerId: this.runnerId }); + logger()?.debug({ + msg: "Runner is shut down, not attempting reconnect", + runnerId: this.runnerId, + }); return; } diff --git a/engine/sdks/typescript/runner/src/tunnel.ts b/engine/sdks/typescript/runner/src/tunnel.ts index c41a231680..3e9dfc24e2 100644 --- a/engine/sdks/typescript/runner/src/tunnel.ts +++ b/engine/sdks/typescript/runner/src/tunnel.ts @@ -210,7 +210,11 @@ export class Tunnel { return new Response("Actor not found", { status: 404 }); } - const fetchHandler = this.#runner.config.fetch(this.#runner, actorId, request); + const fetchHandler = this.#runner.config.fetch( + this.#runner, + actorId, + request, + ); if (!fetchHandler) { return new Response("Not Implemented", { status: 501 }); @@ -307,8 +311,8 @@ export class Tunnel { existing.actorId = req.actorId; } else { this.#actorPendingRequests.set(requestIdStr, { - resolve: () => { }, - reject: () => { }, + resolve: () => {}, + reject: () => {}, streamController: controller, actorId: req.actorId, }); @@ -475,7 +479,7 @@ export class Tunnel { const dataBuffer = typeof data === "string" ? (new TextEncoder().encode(data) - .buffer as ArrayBuffer) + .buffer as ArrayBuffer) : data; this.#sendMessage(requestId, { @@ -539,7 +543,12 @@ export class Tunnel { }); // Call websocket handler - await websocketHandler(this.#runner, open.actorId, adapter, request); + await websocketHandler( + this.#runner, + open.actorId, + adapter, + request, + ); } catch (error) { logger()?.error({ msg: "error handling websocket open", error }); // Send close on error diff --git a/engine/sdks/typescript/runner/vitest.config.ts b/engine/sdks/typescript/runner/vitest.config.ts index 1a86637f83..b6fc098098 100644 --- a/engine/sdks/typescript/runner/vitest.config.ts +++ b/engine/sdks/typescript/runner/vitest.config.ts @@ -1,4 +1,4 @@ -import { resolve } from "path"; +import { resolve } from "node:path"; import { defineConfig } from "vitest/config"; import defaultConfig from "../../../../vitest.base.ts"; diff --git a/engine/sdks/typescript/test-runner/package.json b/engine/sdks/typescript/test-runner/package.json index 1ba7437674..a9f557ee16 100644 --- a/engine/sdks/typescript/test-runner/package.json +++ b/engine/sdks/typescript/test-runner/package.json @@ -24,4 +24,4 @@ "typescript": "^5.9.2", "vitest": "^1.6.1" } -} \ No newline at end of file +} diff --git a/engine/sdks/typescript/test-runner/src/index.ts b/engine/sdks/typescript/test-runner/src/index.ts index d30868f232..50aa20c899 100644 --- a/engine/sdks/typescript/test-runner/src/index.ts +++ b/engine/sdks/typescript/test-runner/src/index.ts @@ -1,11 +1,11 @@ import { serve } from "@hono/node-server"; import type { ActorConfig, RunnerConfig } from "@rivetkit/engine-runner"; import { Runner } from "@rivetkit/engine-runner"; -import { Hono, type Context as HonoContext, Next } from "hono"; +import { Hono, type Context as HonoContext, type Next } from "hono"; import { streamSSE } from "hono/streaming"; -import { getLogger } from "./log" -import { type Logger } from "pino"; +import type { Logger } from "pino"; import type WebSocket from "ws"; +import { getLogger } from "./log"; const INTERNAL_SERVER_PORT = process.env.INTERNAL_SERVER_PORT ? Number(process.env.INTERNAL_SERVER_PORT) @@ -106,35 +106,43 @@ if (AUTOSTART_SERVER) { ); } -if (AUTOSTART_RUNNER) [runner, runnerStarted, runnerStopped] = await startRunner(); +if (AUTOSTART_RUNNER) + [runner, runnerStarted, runnerStopped] = await startRunner(); async function autoConfigureServerless() { - let res = await fetch(`http://127.0.0.1:6420/runner-configs/${RIVET_RUNNER_NAME}?namespace=${RIVET_NAMESPACE}`, { - method: "PUT", - headers: { - "Authorization": `Bearer ${RIVET_TOKEN}`, - "Content-Type": "application/json", + const res = await fetch( + `http://127.0.0.1:6420/runner-configs/${RIVET_RUNNER_NAME}?namespace=${RIVET_NAMESPACE}`, + { + method: "PUT", + headers: { + Authorization: `Bearer ${RIVET_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + datacenters: { + default: { + serverless: { + url: `http://localhost:${INTERNAL_SERVER_PORT}`, + max_runners: 10, + slots_per_runner: 1, + request_lifespan: 15, + }, + }, + }, + }), }, - body: JSON.stringify({ - datacenters: { - default: { - serverless: { - url: `http://localhost:${INTERNAL_SERVER_PORT}`, - max_runners: 10, - slots_per_runner: 1, - request_lifespan: 15, - } - } - } - }), - }); + ); if (!res.ok) { - throw new Error(`request failed: ${res.statusText} (${res.status}):\n${await res.text()}`); + throw new Error( + `request failed: ${res.statusText} (${res.status}):\n${await res.text()}`, + ); } } -async function startRunner(): Promise<[Runner, PromiseWithResolvers, PromiseWithResolvers]> { +async function startRunner(): Promise< + [Runner, PromiseWithResolvers, PromiseWithResolvers] +> { getLogger().info("Starting runner"); const runnerStarted = Promise.withResolvers(); @@ -153,7 +161,7 @@ async function startRunner(): Promise<[Runner, PromiseWithResolvers, Pr onConnected: () => { runnerStarted.resolve(undefined); }, - onDisconnected: () => { }, + onDisconnected: () => {}, onShutdown: () => { runnerStopped.resolve(undefined); }, @@ -177,7 +185,7 @@ async function startRunner(): Promise<[Runner, PromiseWithResolvers, Pr } else if (url.pathname === "/sleep") { runner.sleepActor(actorId); - return new Response('ok', { + return new Response("ok", { status: 200, headers: { "Content-Type": "application/json" }, }); @@ -199,7 +207,12 @@ async function startRunner(): Promise<[Runner, PromiseWithResolvers, Pr `Actor ${_actorId} stopped (generation ${_generation})`, ); }, - websocket: async (_runner: Runner, actorId: string, ws: WebSocket, request: Request) => { + websocket: async ( + _runner: Runner, + actorId: string, + ws: WebSocket, + request: Request, + ) => { getLogger().info(`WebSocket connected for actor ${actorId}`); actorWebSockets.set(actorId, ws); @@ -207,7 +220,8 @@ async function startRunner(): Promise<[Runner, PromiseWithResolvers, Pr ws.addEventListener("message", (event) => { const data = event.data; getLogger().info({ - msg: `WebSocket message from actor ${actorId}`, data + msg: `WebSocket message from actor ${actorId}`, + data, }); ws.send(`Echo: ${data}`); }); @@ -218,7 +232,10 @@ async function startRunner(): Promise<[Runner, PromiseWithResolvers, Pr }); ws.addEventListener("error", (error) => { - getLogger().error({ msg: `WebSocket error for actor ${actorId}:`, error }); + getLogger().error({ + msg: `WebSocket error for actor ${actorId}:`, + error, + }); }); }, }; diff --git a/engine/sdks/typescript/test-runner/src/log.ts b/engine/sdks/typescript/test-runner/src/log.ts index 4e035c5a39..767a67afff 100644 --- a/engine/sdks/typescript/test-runner/src/log.ts +++ b/engine/sdks/typescript/test-runner/src/log.ts @@ -25,7 +25,9 @@ export function getPinoLevel(logLevel?: Level): LevelWithSilent { return configuredLogLevel; } - return (process.env["LOG_LEVEL"] || "warn").toString().toLowerCase() as LevelWithSilent; + return (process.env["LOG_LEVEL"] || "warn") + .toString() + .toLowerCase() as LevelWithSilent; } export function getIncludeTarget(): boolean { @@ -88,9 +90,7 @@ function customWrite(level: string, o: any) { /** * Configure the default logger with optional log level. */ -export async function configureDefaultLogger( - logLevel?: Level, -): Promise { +export async function configureDefaultLogger(logLevel?: Level): Promise { // Store the configured log level if (logLevel) { configuredLogLevel = logLevel; diff --git a/engine/sdks/typescript/test-runner/vitest.config.ts b/engine/sdks/typescript/test-runner/vitest.config.ts index 1a86637f83..b6fc098098 100644 --- a/engine/sdks/typescript/test-runner/vitest.config.ts +++ b/engine/sdks/typescript/test-runner/vitest.config.ts @@ -1,4 +1,4 @@ -import { resolve } from "path"; +import { resolve } from "node:path"; import { defineConfig } from "vitest/config"; import defaultConfig from "../../../../vitest.base.ts"; diff --git a/examples/cursors/tests/cursors.test.ts b/examples/cursors/tests/cursors.test.ts deleted file mode 100644 index 235b87cb2b..0000000000 --- a/examples/cursors/tests/cursors.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { setupTest } from "rivetkit/test"; -import { expect, test } from "vitest"; -import { registry } from "../src/backend/registry"; - -test("Cursor room can handle cursor updates", async (ctx) => { - const { client } = await setupTest(ctx, registry); - const room = client.cursorRoom.getOrCreate(["test-room"]); - - // Test initial state - const initialCursors = await room.getCursors(); - expect(initialCursors).toEqual({}); - - // Update cursor position - const cursor1 = await room.updateCursor("user1", 100, 200); - - // Verify cursor structure - expect(cursor1).toMatchObject({ - userId: "user1", - x: 100, - y: 200, - timestamp: expect.any(Number), - }); - - // Update another cursor - await room.updateCursor("user2", 300, 400); - - // Verify cursors are stored - const cursors = await room.getCursors(); - expect(Object.keys(cursors)).toHaveLength(2); - expect(cursors.user1).toBeDefined(); - expect(cursors.user2).toBeDefined(); - expect(cursors.user1.x).toBe(100); - expect(cursors.user1.y).toBe(200); - expect(cursors.user2.x).toBe(300); - expect(cursors.user2.y).toBe(400); -}); - -test("Cursor room can place text labels", async (ctx) => { - const { client } = await setupTest(ctx, registry); - const room = client.cursorRoom.getOrCreate(["test-text"]); - - // Test initial state - const initialLabels = await room.getTextLabels(); - expect(initialLabels).toEqual([]); - - // Place text - const label1 = await room.placeText("user1", "Hello", 50, 75); - - // Verify label structure - expect(label1).toMatchObject({ - id: expect.any(String), - userId: "user1", - text: "Hello", - x: 50, - y: 75, - timestamp: expect.any(Number), - }); - - // Place another text - const label2 = await room.placeText("user2", "World", 150, 175); - - // Verify labels are stored in order - const labels = await room.getTextLabels(); - expect(labels).toHaveLength(2); - expect(labels[0]).toEqual(label1); - expect(labels[1]).toEqual(label2); -}); - -test("Cursor room can remove cursors", async (ctx) => { - const { client } = await setupTest(ctx, registry); - const room = client.cursorRoom.getOrCreate(["test-remove"]); - - // Add some cursors - await room.updateCursor("user1", 100, 200); - await room.updateCursor("user2", 300, 400); - await room.updateCursor("user3", 500, 600); - - let cursors = await room.getCursors(); - expect(Object.keys(cursors)).toHaveLength(3); - - // Remove one cursor - await room.removeCursor("user2"); - - cursors = await room.getCursors(); - expect(Object.keys(cursors)).toHaveLength(2); - expect(cursors.user1).toBeDefined(); - expect(cursors.user3).toBeDefined(); - expect(cursors.user2).toBeUndefined(); -}); - -test("Cursor updates overwrite previous positions", async (ctx) => { - const { client } = await setupTest(ctx, registry); - const room = client.cursorRoom.getOrCreate(["test-overwrite"]); - - // Update cursor multiple times - await room.updateCursor("user1", 100, 200); - const cursor2 = await room.updateCursor("user1", 300, 400); - const cursor3 = await room.updateCursor("user1", 500, 600); - - const cursors = await room.getCursors(); - expect(Object.keys(cursors)).toHaveLength(1); - expect(cursors.user1.x).toBe(500); - expect(cursors.user1.y).toBe(600); - expect(cursors.user1.timestamp).toBe(cursor3.timestamp); - expect(cursor3.timestamp).toBeGreaterThanOrEqual(cursor2.timestamp); -}); - -test("Multiple users can place text in the same room", async (ctx) => { - const { client } = await setupTest(ctx, registry); - const room = client.cursorRoom.getOrCreate(["test-multiuser-text"]); - - // Multiple users placing text - await room.placeText("Alice", "Hello!", 10, 10); - await room.placeText("Bob", "Hi there!", 50, 50); - await room.placeText("Charlie", "Good day!", 100, 100); - await room.placeText("Alice", "How are you?", 150, 150); - - const labels = await room.getTextLabels(); - expect(labels).toHaveLength(4); - - // Verify users - expect(labels[0].userId).toBe("Alice"); - expect(labels[1].userId).toBe("Bob"); - expect(labels[2].userId).toBe("Charlie"); - expect(labels[3].userId).toBe("Alice"); - - // Verify text content - expect(labels[0].text).toBe("Hello!"); - expect(labels[1].text).toBe("Hi there!"); - expect(labels[2].text).toBe("Good day!"); - expect(labels[3].text).toBe("How are you?"); -}); diff --git a/examples/freestyle/scripts/freestyle-deploy.ts b/examples/freestyle/scripts/freestyle-deploy.ts index 0f3cf23fdf..5aa230cb15 100644 --- a/examples/freestyle/scripts/freestyle-deploy.ts +++ b/examples/freestyle/scripts/freestyle-deploy.ts @@ -1,9 +1,9 @@ +import { execSync } from "node:child_process"; +import { readFileSync } from "node:fs"; import { type Rivet, RivetClient } from "@rivetkit/engine-api-full"; -import { execSync } from "child_process"; import dotenv from "dotenv"; import { FreestyleSandboxes } from "freestyle-sandboxes"; import { prepareDirForDeploymentSync } from "freestyle-sandboxes/utils"; -import { readFileSync } from "fs"; dotenv.config({ path: new URL("../.env", import.meta.url).pathname }); diff --git a/frontend/package.json b/frontend/package.json index db9f8209c9..b041b37066 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -120,7 +120,7 @@ "react-inspector": "^6.0.2", "react-resizable-panels": "^2.1.9", "recharts": "^2.15.4", - "rivetkit": "*", + "rivetkit": "workspace:*", "shiki": "^3.12.2", "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", @@ -135,4 +135,4 @@ "vite-tsconfig-paths": "^5.1.4", "zod": "^3.25.76" } -} \ No newline at end of file +} diff --git a/justfile b/justfile index 8b25d83401..cd9ea182f1 100644 --- a/justfile +++ b/justfile @@ -12,11 +12,11 @@ release-nolatest VERSION: [group('docker')] docker-build: - docker build -f docker/universal/Dockerfile --target engine-full -t rivetkit/engine:local --platform linux/x86_64 . + docker build -f engine/docker/universal/Dockerfile --target engine-full -t rivetkit/engine:local --platform linux/x86_64 . [group('docker')] docker-build-frontend: - docker build -f docker/universal/Dockerfile --target engine-full -t rivetkit/engine:local --platform linux/x86_64 --build-arg BUILD_FRONTEND=true . + docker build -f engine/docker/universal/Dockerfile --target engine-full -t rivetkit/engine:local --platform linux/x86_64 --build-arg BUILD_FRONTEND=true . [group('docker')] docker-run: diff --git a/rivetkit-typescript/packages/rivetkit/scripts/compile-bare.ts b/rivetkit-typescript/packages/rivetkit/scripts/compile-bare.ts index 25509f82cc..160b5fdf3c 100755 --- a/rivetkit-typescript/packages/rivetkit/scripts/compile-bare.ts +++ b/rivetkit-typescript/packages/rivetkit/scripts/compile-bare.ts @@ -1,9 +1,9 @@ #!/usr/bin/env -S tsx +import * as fs from "node:fs/promises"; +import * as path from "node:path"; import { type Config, transform } from "@bare-ts/tools"; import { Command } from "commander"; -import * as fs from "fs/promises"; -import * as path from "path"; const program = new Command(); diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts index 4520de7fa8..06b84689ff 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts @@ -303,7 +303,11 @@ export class EngineActorDriver implements ActorDriver { logger().debug({ msg: "runner actor stopped", actorId }); } - async #runnerFetch(actorId: string, request: Request): Promise { + async #runnerFetch( + runner: Runner, + actorId: string, + request: Request, + ): Promise { logger().debug({ msg: "runner fetch", actorId, @@ -314,6 +318,7 @@ export class EngineActorDriver implements ActorDriver { } async #runnerWebSocket( + runner: Runner, actorId: string, websocketRaw: any, request: Request, diff --git a/rivetkit-typescript/packages/rivetkit/vitest.config.ts b/rivetkit-typescript/packages/rivetkit/vitest.config.ts index e3082dcf61..fc6a4f6450 100644 --- a/rivetkit-typescript/packages/rivetkit/vitest.config.ts +++ b/rivetkit-typescript/packages/rivetkit/vitest.config.ts @@ -1,4 +1,4 @@ -import { resolve } from "path"; +import { resolve } from "node:path"; import { defineConfig } from "vitest/config"; import defaultConfig from "../../../vitest.base.ts"; diff --git a/scripts/docker/build-push.sh b/scripts/docker/build-push.sh index 0e64e06abc..d327e4e76e 100755 --- a/scripts/docker/build-push.sh +++ b/scripts/docker/build-push.sh @@ -13,7 +13,7 @@ IMAGE_REPO=$1 shift TAGS=("$@") -DOCKERFILE=${DOCKERFILE:-docker/universal/Dockerfile} +DOCKERFILE=${DOCKERFILE:-engine/docker/universal/Dockerfile} TARGET=${TARGET:-engine-full} CONTEXT=${CONTEXT:-.} diff --git a/scripts/release/sdk.ts b/scripts/release/sdk.ts index 668886ddad..85c0708d2d 100644 --- a/scripts/release/sdk.ts +++ b/scripts/release/sdk.ts @@ -1,6 +1,6 @@ import { $ } from "execa"; -import { readFile } from "fs/promises"; -import { join } from "path"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; import type { ReleaseOpts } from "./main"; async function npmVersionExists( @@ -37,9 +37,9 @@ async function npmVersionExists( export async function publishSdk(opts: ReleaseOpts) { const packagePaths = [ - `${opts.root}/sdks/typescript/runner`, - `${opts.root}/sdks/typescript/runner-protocol`, - `${opts.root}/sdks/typescript/api-full`, + `${opts.root}/engine/sdks/typescript/runner`, + `${opts.root}/engine/sdks/typescript/runner-protocol`, + `${opts.root}/engine/sdks/typescript/api-full`, ]; for (const path of packagePaths) { @@ -70,6 +70,6 @@ export async function publishSdk(opts: ReleaseOpts) { await $({ stdio: "inherit", - })`pnpm --filter ${name} publish --access public --tag ${tag}`; + })`pnpm --filter ${name} publish --access public --tag ${tag} --no-git-checks`; } } diff --git a/website/scripts/generateExamples.mjs b/website/scripts/generateExamples.mjs index 778a623eb7..c1d54f0732 100644 --- a/website/scripts/generateExamples.mjs +++ b/website/scripts/generateExamples.mjs @@ -1,8 +1,8 @@ #!/usr/bin/env node -import { execSync } from 'child_process'; -import { readFileSync, writeFileSync, existsSync, mkdirSync, cpSync, rmSync } from 'fs'; -import { join } from 'path'; +import { execSync } from 'node:child_process'; +import { readFileSync, writeFileSync, existsSync, mkdirSync, cpSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; import { EXAMPLE_METADATA } from './examplesData.mjs'; const REPO_URL = 'https://github.com/rivet-dev/rivetkit.git'; diff --git a/website/scripts/generateNavigation.ts b/website/scripts/generateNavigation.ts index 70040135e7..79955cf198 100644 --- a/website/scripts/generateNavigation.ts +++ b/website/scripts/generateNavigation.ts @@ -2,7 +2,7 @@ // import engineStyles from '../src/lib/engineStyles.json' assert { type: 'json' }; import { slugifyWithCounter } from "@sindresorhus/slugify"; import glob from "fast-glob"; -import { readFile, writeFile } from "fs/promises"; +import { readFile, writeFile } from "node:fs/promises"; import { toString } from "mdast-util-to-string"; import { remark } from "remark"; import { visit } from "unist-util-visit"; diff --git a/website/scripts/generateReadme.mjs b/website/scripts/generateReadme.mjs index deececad94..40d59a92f7 100755 --- a/website/scripts/generateReadme.mjs +++ b/website/scripts/generateReadme.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { readFileSync, writeFileSync, existsSync } from 'fs'; -import { join } from 'path'; +import { readFileSync, writeFileSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; import { EXAMPLE_METADATA } from './examplesData.mjs'; const RIVET_TEMPLATE_PATH = join(process.cwd(), '..', 'README.rivet.tpl.md'); From fbfaf7f8f0d1cf1192cbc7d75554322d76edc6d9 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Sat, 25 Oct 2025 02:17:00 -0700 Subject: [PATCH 3/3] chore(rivetkit-typescript): remove dependency on node modules --- biome.json | 53 +++++- engine/package.json | 27 +-- engine/sdks/rust/runner-protocol/build.rs | 31 ++-- .../typescript/runner-protocol/src/index.ts | 8 +- pnpm-lock.yaml | 114 +++++++++--- pnpm-workspace.yaml | 1 + .../packages/rivetkit/package.json | 5 + .../packages/rivetkit/scripts/dump-openapi.ts | 6 +- .../packages/rivetkit/src/drivers/default.ts | 6 +- .../src/drivers/file-system/global-state.ts | 35 +++- .../rivetkit/src/drivers/file-system/mod.ts | 14 +- .../rivetkit/src/drivers/file-system/utils.ts | 21 ++- .../rivetkit/src/engine-process/mod.ts | 51 ++++-- .../packages/rivetkit/src/inspector/utils.ts | 11 +- .../packages/rivetkit/src/mod.ts | 9 +- .../packages/rivetkit/src/registry/mod.ts | 41 +++-- .../packages/rivetkit/src/registry/serve.ts | 10 +- .../packages/rivetkit/src/test/mod.ts | 10 +- .../packages/rivetkit/src/utils/node.ts | 169 ++++++++++++++++++ .../rivetkit/tests/driver-file-system.test.ts | 2 +- .../rivetkit/tests/driver-memory.test.ts | 2 +- 21 files changed, 488 insertions(+), 138 deletions(-) create mode 100644 rivetkit-typescript/packages/rivetkit/src/utils/node.ts diff --git a/biome.json b/biome.json index bcc74e911b..bfde915277 100644 --- a/biome.json +++ b/biome.json @@ -41,5 +41,56 @@ "noExplicitAny": "off" } } - } + }, + "overrides": [ + { + "includes": [ + "rivetkit-typescript/packages/rivetkit/src/**/*", + "!rivetkit-typescript/packages/rivetkit/src/test/**/*" + ], + "linter": { + "rules": { + "style": { + "noRestrictedImports": { + "level": "error", + "options": { + "paths": { + "node:crypto": "Use '@/utils/node' getNodeCrypto() instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "node:fs": "Use '@/utils/node' getNodeFsSync() instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "node:fs/promises": "Use '@/utils/node' getNodeFs() instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "node:path": "Use '@/utils/node' getNodePath() instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "node:os": "Use '@/utils/node' getNodeOs() instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "node:child_process": "Use '@/utils/node' getNodeChildProcess() instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "node:stream": "Use '@/utils/node' getNodeStream() instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "node:net": "Use '@/utils/node' instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "node:url": "Use '@/utils/node' instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "crypto": "Use '@/utils/node' getNodeCrypto() instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "fs": "Use '@/utils/node' getNodeFsSync() or getNodeFs() instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "fs/promises": "Use '@/utils/node' getNodeFs() instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "path": "Use '@/utils/node' getNodePath() instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "os": "Use '@/utils/node' getNodeOs() instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "child_process": "Use '@/utils/node' getNodeChildProcess() instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "stream": "Use '@/utils/node' getNodeStream() instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "net": "Use '@/utils/node' instead. Direct Node.js imports are only allowed in src/utils/node.ts", + "url": "Use '@/utils/node' instead. Direct Node.js imports are only allowed in src/utils/node.ts" + } + } + } + } + } + } + }, + { + "includes": [ + "rivetkit-typescript/packages/rivetkit/src/utils/node.ts" + ], + "linter": { + "rules": { + "style": { + "noRestrictedImports": "off" + } + } + } + } + ] } diff --git a/engine/package.json b/engine/package.json index e7fd9ca8ee..93070d1322 100644 --- a/engine/package.json +++ b/engine/package.json @@ -1,28 +1,11 @@ { "name": "@rivetkit/engine", - "private": true, + "version": "1.0.0", + "keywords": [], + "author": "", + "license": "ISC", "packageManager": "pnpm@10.13.1", - "scripts": { - "start": "npx turbo watch build", - "build": "npx turbo build", - "test": "npx turbo test", - "test:watch": "npx turbo watch test", - "check-types": "npx turbo check-types", - "fmt": "pnpm biome check --write --diagnostic-level=error ." - }, - "devDependencies": { - "@bare-ts/tools": "0.15.0", - "@biomejs/biome": "^2.2.3", - "lefthook": "^1.12.4", - "tsup": "^8.5.0", - "turbo": "^2.5.6", - "typescript": "^5.9.2" - }, "dependencies": { - "@sentry/vite-plugin": "^2.23.1" - }, - "resolutions": { - "rivetkit": "workspace:*", - "@clerk/shared": "3.27.1" + "@vbare/compiler": "^0.0.3" } } diff --git a/engine/sdks/rust/runner-protocol/build.rs b/engine/sdks/rust/runner-protocol/build.rs index 4f1bb3902a..1486193cf3 100644 --- a/engine/sdks/rust/runner-protocol/build.rs +++ b/engine/sdks/rust/runner-protocol/build.rs @@ -22,9 +22,16 @@ fn main() -> Result<(), Box> { vbare_compiler::process_schemas_with_config(&schema_dir, &cfg)?; // TypeScript SDK generation - let cli_js_path = workspace_root.join("node_modules/@bare-ts/tools/dist/bin/cli.js"); + let cli_js_path = workspace_root.join("node_modules/@vbare/compiler/dist/cli.js"); + let vbare_compiler_dir = workspace_root.join("node_modules/@vbare/compiler"); + + // Rerun build script if @vbare/compiler changes + if vbare_compiler_dir.exists() { + println!("cargo:rerun-if-changed={}", vbare_compiler_dir.display()); + } + if cli_js_path.exists() { - typescript::generate_sdk(&schema_dir); + typescript::generate_sdk(&schema_dir, &cli_js_path); } else { println!( "cargo:warning=TypeScript SDK generation skipped: cli.js not found at {}. Run `pnpm install` to install.", @@ -38,7 +45,7 @@ fn main() -> Result<(), Box> { mod typescript { use super::*; - pub fn generate_sdk(schema_dir: &Path) { + pub fn generate_sdk(schema_dir: &Path, cli_js_path: &Path) { let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); let workspace_root = Path::new(&manifest_dir) .parent() @@ -59,16 +66,14 @@ mod typescript { panic!("Failed to create SDK directory: {}", e); } - let output = - Command::new(workspace_root.join("node_modules/@bare-ts/tools/dist/bin/cli.js")) - .arg("compile") - .arg("--generator") - .arg("ts") - .arg(highest_version_path) - .arg("-o") - .arg(src_dir.join("index.ts")) - .output() - .expect("Failed to execute bare compiler for TypeScript"); + let output = Command::new(cli_js_path) + .arg("--generator") + .arg("ts") + .arg("-o") + .arg(src_dir.join("index.ts")) + .arg(highest_version_path) + .output() + .expect("Failed to execute bare compiler for TypeScript"); if !output.status.success() { panic!( diff --git a/engine/sdks/typescript/runner-protocol/src/index.ts b/engine/sdks/typescript/runner-protocol/src/index.ts index 798b86a04d..ec0408e62d 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({}) @@ -1862,3 +1862,9 @@ 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/pnpm-lock.yaml b/pnpm-lock.yaml index e5386fc7a6..764a7eb4c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,12 @@ importers: specifier: ^5.9.2 version: 5.9.2 + 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': @@ -891,9 +897,6 @@ importers: specifier: ^4.7.0 version: 4.9.8 devDependencies: - '@hono/node-server': - specifier: ^1.19.1 - version: 1.19.1(hono@4.9.8) '@types/node': specifier: ^22.13.9 version: 22.18.1 @@ -1969,7 +1972,7 @@ importers: version: 3.13.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@uiw/codemirror-extensions-basic-setup': specifier: ^4.25.1 - version: 4.25.1(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2) + version: 4.25.1(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2) '@uiw/codemirror-theme-github': specifier: ^4.25.1 version: 4.25.1(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2) @@ -2332,6 +2335,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) @@ -2437,13 +2443,13 @@ importers: version: 6.0.1 '@mdx-js/loader': specifier: ^3.1.1 - version: 3.1.1(webpack@5.101.3(esbuild@0.25.9)) + version: 3.1.1(webpack@5.101.3) '@mdx-js/react': specifier: ^3.1.1 version: 3.1.1(@types/react@19.2.2)(react@19.1.1) '@next/mdx': specifier: ^15.5.2 - version: 15.5.2(@mdx-js/loader@3.1.1(webpack@5.101.3(esbuild@0.25.9)))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.1.1)) + version: 15.5.2(@mdx-js/loader@3.1.1(webpack@5.101.3))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.1.1)) '@next/third-parties': specifier: latest version: 16.0.0(next@15.5.2(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.93.2))(react@19.1.1) @@ -2633,7 +2639,7 @@ importers: version: 13.0.2(eslint@8.26.0)(typescript@5.9.2) file-loader: specifier: ^6.2.0 - version: 6.2.0(webpack@5.101.3(esbuild@0.25.9)) + version: 6.2.0(webpack@5.101.3) prettier: specifier: ^2.8.8 version: 2.8.8 @@ -3242,6 +3248,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==} @@ -6949,6 +6962,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} @@ -14327,6 +14345,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@18.3.24)(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 @@ -16049,12 +16071,12 @@ snapshots: '@marijn/find-cluster-break@1.0.2': {} - '@mdx-js/loader@3.1.1(webpack@5.101.3(esbuild@0.25.9))': + '@mdx-js/loader@3.1.1(webpack@5.101.3)': dependencies: '@mdx-js/mdx': 3.1.1 source-map: 0.7.6 optionalDependencies: - webpack: 5.101.3(esbuild@0.25.9) + webpack: 5.101.3 transitivePeerDependencies: - supports-color @@ -16232,11 +16254,11 @@ snapshots: dependencies: glob: 7.1.7 - '@next/mdx@15.5.2(@mdx-js/loader@3.1.1(webpack@5.101.3(esbuild@0.25.9)))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.1.1))': + '@next/mdx@15.5.2(@mdx-js/loader@3.1.1(webpack@5.101.3))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.1.1))': dependencies: source-map: 0.7.6 optionalDependencies: - '@mdx-js/loader': 3.1.1(webpack@5.101.3(esbuild@0.25.9)) + '@mdx-js/loader': 3.1.1(webpack@5.101.3) '@mdx-js/react': 3.1.1(@types/react@19.2.2)(react@19.1.1) '@next/swc-darwin-arm64@15.4.5': @@ -18452,16 +18474,6 @@ snapshots: '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.2 - '@uiw/codemirror-extensions-basic-setup@4.25.1(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2)': - dependencies: - '@codemirror/autocomplete': 6.19.0 - '@codemirror/commands': 6.9.0 - '@codemirror/language': 6.11.3 - '@codemirror/lint': 6.9.0 - '@codemirror/search': 6.5.11 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.2 - '@uiw/codemirror-theme-github@4.25.1(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2)': dependencies: '@uiw/codemirror-themes': 4.25.1(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2) @@ -18524,6 +18536,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 @@ -20523,7 +20542,7 @@ snapshots: eslint: 8.26.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.32.0(eslint@8.26.0))(eslint@8.26.0) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.32.0(eslint@8.26.0))(eslint@8.26.0))(eslint@8.26.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1)(eslint@8.26.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.26.0) eslint-plugin-react: 7.37.5(eslint@8.26.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.26.0) @@ -20545,7 +20564,7 @@ snapshots: dependencies: debug: 4.4.1 eslint: 8.26.0 - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.32.0(eslint@8.26.0))(eslint@8.26.0))(eslint@8.26.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1)(eslint@8.26.0) glob: 7.2.3 is-glob: 4.0.3 resolve: 1.22.10 @@ -20564,7 +20583,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.32.0(eslint@8.26.0))(eslint@8.26.0))(eslint@8.26.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1)(eslint@8.26.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -21063,11 +21082,11 @@ snapshots: dependencies: flat-cache: 3.2.0 - file-loader@6.2.0(webpack@5.101.3(esbuild@0.25.9)): + file-loader@6.2.0(webpack@5.101.3): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.101.3(esbuild@0.25.9) + webpack: 5.101.3 file-saver@2.0.5: {} @@ -25577,6 +25596,16 @@ snapshots: webpack: 5.101.3(esbuild@0.25.9) optionalDependencies: esbuild: 0.25.9 + optional: true + + terser-webpack-plugin@5.3.14(webpack@5.101.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + terser: 5.44.0 + webpack: 5.101.3 terser@5.44.0: dependencies: @@ -26689,6 +26718,38 @@ snapshots: webpack-virtual-modules@0.6.2: {} + webpack@5.101.3: + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.26.3 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.3 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.14(webpack@5.101.3) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + webpack@5.101.3(esbuild@0.25.9): dependencies: '@types/eslint-scope': 3.7.7 @@ -26720,6 +26781,7 @@ snapshots: - '@swc/core' - esbuild - uglify-js + optional: true whatwg-fetch@3.6.20: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8b6cb70c52..bf8dba696b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ packages: + - engine - engine/docker/template - engine/sdks/typescript/api-full - engine/sdks/typescript/runner diff --git a/rivetkit-typescript/packages/rivetkit/package.json b/rivetkit-typescript/packages/rivetkit/package.json index 09ee333f42..e5476c21ab 100644 --- a/rivetkit-typescript/packages/rivetkit/package.json +++ b/rivetkit-typescript/packages/rivetkit/package.json @@ -155,6 +155,10 @@ "build": "tsup src/mod.ts src/client/mod.ts src/common/log.ts src/common/websocket.ts src/actor/errors.ts src/topologies/coordinate/mod.ts src/topologies/partition/mod.ts src/utils.ts src/driver-helpers/mod.ts src/driver-test-suite/mod.ts src/test/mod.ts src/inspector/mod.ts", "build:schema": "./scripts/compile-bare.ts compile schemas/client-protocol/v1.bare -o dist/schemas/client-protocol/v1.ts && ./scripts/compile-bare.ts compile schemas/file-system-driver/v1.bare -o dist/schemas/file-system-driver/v1.ts && ./scripts/compile-bare.ts compile schemas/actor-persist/v1.bare -o dist/schemas/actor-persist/v1.ts", "check-types": "tsc --noEmit", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format .", + "format:write": "biome format --write .", "test": "vitest run", "test:watch": "vitest", "dump-openapi": "tsx scripts/dump-openapi.ts" @@ -176,6 +180,7 @@ }, "devDependencies": { "@bare-ts/tools": "^0.13.0", + "@biomejs/biome": "^2.2.3", "@hono/node-server": "^1.18.2", "@hono/node-ws": "^1.1.1", "@types/invariant": "^2", diff --git a/rivetkit-typescript/packages/rivetkit/scripts/dump-openapi.ts b/rivetkit-typescript/packages/rivetkit/scripts/dump-openapi.ts index fa31da47e7..3bf19bd4aa 100644 --- a/rivetkit-typescript/packages/rivetkit/scripts/dump-openapi.ts +++ b/rivetkit-typescript/packages/rivetkit/scripts/dump-openapi.ts @@ -13,14 +13,14 @@ import { import { type RunnerConfig, RunnerConfigSchema } from "@/registry/run-config"; import { VERSION } from "@/utils"; -function main() { +async function main() { const registryConfig: RegistryConfig = RegistryConfigSchema.parse({ use: {}, }); const registry = setup(registryConfig); const driverConfig: RunnerConfig = RunnerConfigSchema.parse({ - driver: createFileSystemOrMemoryDriver(false), + driver: await createFileSystemOrMemoryDriver(false), getUpgradeWebSocket: () => () => unimplemented(), inspector: { enabled: false, @@ -70,7 +70,7 @@ function main() { "rivetkit-openapi", "openapi.json", ); - fs.writeFile(outputPath, JSON.stringify(openApiDoc, null, 2)); + await fs.writeFile(outputPath, JSON.stringify(openApiDoc, null, 2)); console.log("Dumped OpenAPI to", outputPath); } diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/default.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/default.ts index 8fab1b3d65..d55c407d8f 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/default.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/default.ts @@ -7,7 +7,9 @@ import type { DriverConfig, RunnerConfig } from "@/registry/run-config"; /** * Chooses the appropriate driver based on the run configuration. */ -export function chooseDefaultDriver(runConfig: RunnerConfig): DriverConfig { +export async function chooseDefaultDriver( + runConfig: RunnerConfig, +): Promise { if (runConfig.endpoint && runConfig.driver) { throw new UserError( "Cannot specify both 'endpoint' and 'driver' in configuration", @@ -31,5 +33,5 @@ export function chooseDefaultDriver(runConfig: RunnerConfig): DriverConfig { } loggerWithoutContext().debug({ msg: "using default file system driver" }); - return createFileSystemOrMemoryDriver(true); + return await createFileSystemOrMemoryDriver(true); } diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/global-state.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/global-state.ts index 2a88c8da79..6b4366e7a7 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/global-state.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/global-state.ts @@ -1,7 +1,3 @@ -import * as crypto from "node:crypto"; -import * as fsSync from "node:fs"; -import * as fs from "node:fs/promises"; -import * as path from "node:path"; import invariant from "invariant"; import { lookupInRegistry } from "@/actor/definition"; import { ActorAlreadyExists } from "@/actor/errors"; @@ -27,6 +23,12 @@ import { setLongTimeout, stringifyError, } from "@/utils"; +import { + getNodeCrypto, + getNodeFs, + getNodeFsSync, + getNodePath, +} from "@/utils/node"; import { logger } from "./log"; import { ensureDirectoryExists, @@ -93,6 +95,7 @@ export class FileSystemGlobalState { constructor(persist: boolean = true, customPath?: string) { this.#persist = persist; this.#storagePath = persist ? getStoragePath(customPath) : "/tmp"; + const path = getNodePath(); this.#stateDir = path.join(this.#storagePath, "state"); this.#dbsDir = path.join(this.#storagePath, "databases"); this.#alarmsDir = path.join(this.#storagePath, "alarms"); @@ -104,6 +107,7 @@ export class FileSystemGlobalState { ensureDirectoryExistsSync(this.#alarmsDir); try { + const fsSync = getNodeFsSync(); const actorIds = fsSync.readdirSync(this.#stateDir); this.#actorCountOnStartup = actorIds.length; } catch (error) { @@ -131,15 +135,15 @@ export class FileSystemGlobalState { } getActorStatePath(actorId: string): string { - return path.join(this.#stateDir, actorId); + return getNodePath().join(this.#stateDir, actorId); } getActorDbPath(actorId: string): string { - return path.join(this.#dbsDir, `${actorId}.db`); + return getNodePath().join(this.#dbsDir, `${actorId}.db`); } getActorAlarmPath(actorId: string): string { - return path.join(this.#alarmsDir, actorId); + return getNodePath().join(this.#alarmsDir, actorId); } async *getActorsIterator(params: { @@ -148,6 +152,7 @@ export class FileSystemGlobalState { let actorIds = Array.from(this.#actors.keys()).sort(); // Check if state directory exists first + const fsSync = getNodeFsSync(); if (fsSync.existsSync(this.#stateDir)) { actorIds = fsSync .readdirSync(this.#stateDir) @@ -258,6 +263,7 @@ export class FileSystemGlobalState { // Read & parse file try { + const fs = getNodeFs(); const stateData = await fs.readFile(stateFilePath); // Cache the loaded state in handler @@ -352,8 +358,10 @@ export class FileSystemGlobalState { // Persist alarm to disk if (this.#persist) { const alarmPath = this.getActorAlarmPath(actorId); + const crypto = getNodeCrypto(); const tempPath = `${alarmPath}.tmp.${crypto.randomUUID()}`; try { + const path = getNodePath(); await ensureDirectoryExists(path.dirname(alarmPath)); const alarmData: schema.ActorAlarm = { actorId, @@ -363,10 +371,12 @@ export class FileSystemGlobalState { ACTOR_ALARM_VERSIONED.serializeWithEmbeddedVersion( alarmData, ); + const fs = getNodeFs(); await fs.writeFile(tempPath, data); await fs.rename(tempPath, alarmPath); } catch (error) { try { + const fs = getNodeFs(); await fs.unlink(tempPath); } catch {} logger().error({ @@ -391,10 +401,12 @@ export class FileSystemGlobalState { ): Promise { const dataPath = this.getActorStatePath(actorId); // Generate unique temp filename to prevent any race conditions + const crypto = getNodeCrypto(); const tempPath = `${dataPath}.tmp.${crypto.randomUUID()}`; try { // Create directory if needed + const path = getNodePath(); await ensureDirectoryExists(path.dirname(dataPath)); // Convert to BARE types for serialization @@ -409,11 +421,13 @@ export class FileSystemGlobalState { // Perform atomic write const serializedState = ACTOR_STATE_VERSIONED.serializeWithEmbeddedVersion(bareState); + const fs = getNodeFs(); await fs.writeFile(tempPath, serializedState); await fs.rename(tempPath, dataPath); } catch (error) { // Cleanup temp file on error try { + const fs = getNodeFs(); await fs.unlink(tempPath); } catch { // Ignore cleanup errors @@ -548,12 +562,14 @@ export class FileSystemGlobalState { */ #loadAlarmsSync(): void { try { + const fsSync = getNodeFsSync(); const files = fsSync.existsSync(this.#alarmsDir) ? fsSync.readdirSync(this.#alarmsDir) : []; for (const file of files) { // Skip temp files if (file.includes(".tmp.")) continue; + const path = getNodePath(); const fullPath = path.join(this.#alarmsDir, file); try { const buf = fsSync.readFileSync(fullPath); @@ -622,6 +638,7 @@ export class FileSystemGlobalState { // On trigger: remove persisted alarm file if (this.#persist) { try { + const fs = getNodeFs(); await fs.unlink(this.getActorAlarmPath(actorId)); } catch (err: any) { if (err?.code !== "ENOENT") { @@ -668,6 +685,8 @@ export class FileSystemGlobalState { } getOrCreateInspectorAccessToken(): string { + const path = getNodePath(); + const fsSync = getNodeFsSync(); const tokenPath = path.join(this.#storagePath, "inspector-token"); if (fsSync.existsSync(tokenPath)) { return fsSync.readFileSync(tokenPath, "utf-8"); @@ -683,6 +702,7 @@ export class FileSystemGlobalState { */ #cleanupTempFilesSync(): void { try { + const fsSync = getNodeFsSync(); const files = fsSync.readdirSync(this.#stateDir); const tempFiles = files.filter((f) => f.includes(".tmp.")); @@ -690,6 +710,7 @@ export class FileSystemGlobalState { for (const tempFile of tempFiles) { try { + const path = getNodePath(); const fullPath = path.join(this.#stateDir, tempFile); const stat = fsSync.statSync(fullPath); diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/mod.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/mod.ts index e9d33a12c0..e5fe8cf7c1 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/mod.ts @@ -1,4 +1,5 @@ import type { DriverConfig } from "@/registry/run-config"; +import { importNodeDependencies } from "@/utils/node"; import { FileSystemActorDriver } from "./actor"; import { FileSystemGlobalState } from "./global-state"; import { FileSystemManagerDriver } from "./manager"; @@ -8,10 +9,13 @@ export { FileSystemGlobalState } from "./global-state"; export { FileSystemManagerDriver } from "./manager"; export { getStoragePath } from "./utils"; -export function createFileSystemOrMemoryDriver( +export async function createFileSystemOrMemoryDriver( persist: boolean = true, customPath?: string, -): DriverConfig { +): Promise { + // Import Node.js dependencies before creating the state + await importNodeDependencies(); + const state = new FileSystemGlobalState(persist, customPath); const driverConfig: DriverConfig = { name: persist ? "file-system" : "memory", @@ -44,10 +48,12 @@ export function createFileSystemOrMemoryDriver( return driverConfig; } -export function createFileSystemDriver(opts?: { path?: string }): DriverConfig { +export async function createFileSystemDriver(opts?: { + path?: string; +}): Promise { return createFileSystemOrMemoryDriver(true, opts?.path); } -export function createMemoryDriver(): DriverConfig { +export async function createMemoryDriver(): Promise { return createFileSystemOrMemoryDriver(false); } diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/utils.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/utils.ts index f6e09e011c..785aa4808d 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/utils.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/utils.ts @@ -1,9 +1,11 @@ -import * as crypto from "node:crypto"; -import * as fsSync from "node:fs"; -import * as fs from "node:fs/promises"; -import * as os from "node:os"; -import * as path from "node:path"; import type { ActorKey } from "@/actor/mod"; +import { + getNodeCrypto, + getNodeFs, + getNodeFsSync, + getNodeOs, + getNodePath, +} from "@/utils/node"; /** * Generate a deterministic actor ID from name and key @@ -13,6 +15,7 @@ export function generateActorId(name: string, key: ActorKey): string { const jsonString = JSON.stringify([name, key]); // Hash to ensure safe file system names + const crypto = getNodeCrypto(); const hash = crypto .createHash("sha256") .update(jsonString) @@ -26,6 +29,7 @@ export function generateActorId(name: string, key: ActorKey): string { * Create a hash for a path, normalizing it first */ function createHashForPath(dirPath: string): string { + const path = getNodePath(); // Normalize the path first const normalizedPath = path.normalize(dirPath); @@ -33,6 +37,7 @@ function createHashForPath(dirPath: string): string { const lastComponent = path.basename(normalizedPath); // Create SHA-256 hash + const crypto = getNodeCrypto(); const hash = crypto .createHash("sha256") .update(normalizedPath) @@ -49,6 +54,7 @@ export function getStoragePath(customPath?: string): string { const dataPath = getDataPath("rivetkit"); const pathToHash = customPath || process.cwd(); const dirHash = createHashForPath(pathToHash); + const path = getNodePath(); return path.join(dataPath, dirHash); } @@ -57,6 +63,7 @@ export function getStoragePath(customPath?: string): string { */ export async function pathExists(path: string): Promise { try { + const fs = getNodeFs(); await fs.access(path); return true; } catch { @@ -71,6 +78,7 @@ export async function ensureDirectoryExists( directoryPath: string, ): Promise { if (!(await pathExists(directoryPath))) { + const fs = getNodeFs(); await fs.mkdir(directoryPath, { recursive: true }); } } @@ -80,6 +88,7 @@ export async function ensureDirectoryExists( * All other operations use the async version */ export function ensureDirectoryExistsSync(directoryPath: string): void { + const fsSync = getNodeFsSync(); if (!fsSync.existsSync(directoryPath)) { fsSync.mkdirSync(directoryPath, { recursive: true }); } @@ -90,7 +99,9 @@ export function ensureDirectoryExistsSync(directoryPath: string): void { */ function getDataPath(appName: string): string { const platform = process.platform; + const os = getNodeOs(); const homeDir = os.homedir(); + const path = getNodePath(); switch (platform) { case "win32": diff --git a/rivetkit-typescript/packages/rivetkit/src/engine-process/mod.ts b/rivetkit-typescript/packages/rivetkit/src/engine-process/mod.ts index bd6f01e8f2..a2d5da5003 100644 --- a/rivetkit-typescript/packages/rivetkit/src/engine-process/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/engine-process/mod.ts @@ -1,13 +1,16 @@ -import { spawn } from "node:child_process"; -import { randomUUID } from "node:crypto"; -import { createWriteStream } from "node:fs"; -import * as fs from "node:fs/promises"; -import * as path from "node:path"; -import { pipeline } from "node:stream/promises"; import { ensureDirectoryExists, getStoragePath, } from "@/drivers/file-system/utils"; +import { + getNodeChildProcess, + getNodeCrypto, + getNodeFs, + getNodeFsSync, + getNodePath, + getNodeStream, + importNodeDependencies, +} from "@/utils/node"; import { logger } from "./log"; export const ENGINE_PORT = 6420; @@ -23,10 +26,15 @@ interface EnsureEngineProcessOptions { export async function ensureEngineProcess( options: EnsureEngineProcessOptions, ): Promise { + // Import Node.js dependencies first + await importNodeDependencies(); + logger().debug({ msg: "ensuring engine process", version: options.version, }); + + const path = getNodePath(); const storageRoot = getStoragePath(); const binDir = path.join(storageRoot, "bin"); const varDir = path.join(storageRoot, "var"); @@ -61,7 +69,6 @@ export async function ensureEngineProcess( ); } } - // Create log file streams with timestamp in the filename const timestamp = new Date() .toISOString() @@ -70,8 +77,13 @@ export async function ensureEngineProcess( const stdoutLogPath = path.join(logsDir, `engine-${timestamp}-stdout.log`); const stderrLogPath = path.join(logsDir, `engine-${timestamp}-stderr.log`); - const stdoutStream = createWriteStream(stdoutLogPath, { flags: "a" }); - const stderrStream = createWriteStream(stderrLogPath, { flags: "a" }); + const fsSync = getNodeFsSync(); + const stdoutStream = fsSync.createWriteStream(stdoutLogPath, { + flags: "a", + }); + const stderrStream = fsSync.createWriteStream(stderrLogPath, { + flags: "a", + }); logger().debug({ msg: "creating engine log files", @@ -79,7 +91,8 @@ export async function ensureEngineProcess( stderr: stderrLogPath, }); - const child = spawn(binaryPath, ["start"], { + const childProcess = getNodeChildProcess(); + const child = childProcess.spawn(binaryPath, ["start"], { cwd: path.dirname(binaryPath), stdio: ["inherit", "pipe", "pipe"], env: { @@ -98,7 +111,6 @@ export async function ensureEngineProcess( if (child.stderr) { child.stderr.pipe(stderrStream); } - logger().debug({ msg: "spawned engine process", pid: child.pid, @@ -175,7 +187,8 @@ async function downloadEngineBinaryIfNeeded( } // Generate unique temp file name to prevent parallel download conflicts - const tempPath = `${binaryPath}.${randomUUID()}.tmp`; + const crypto = getNodeCrypto(); + const tempPath = `${binaryPath}.${crypto.randomUUID()}.tmp`; const startTime = Date.now(); logger().debug({ @@ -193,12 +206,18 @@ async function downloadEngineBinaryIfNeeded( }, 5000); try { - await pipeline(response.body, createWriteStream(tempPath)); + const stream = getNodeStream(); + const fsSync = getNodeFsSync(); + await stream.pipeline( + response.body, + fsSync.createWriteStream(tempPath), + ); // Clear the slow download warning clearTimeout(slowDownloadWarning); // Get file size to verify download + const fs = getNodeFs(); const stats = await fs.stat(tempPath); const downloadDuration = Date.now() - startTime; @@ -232,6 +251,7 @@ async function downloadEngineBinaryIfNeeded( support: "https://rivet.dev/discord", }); try { + const fs = getNodeFs(); await fs.unlink(tempPath); } catch (unlinkError) { // Ignore errors when cleaning up (file may not exist) @@ -239,7 +259,7 @@ async function downloadEngineBinaryIfNeeded( throw error; } } - +// function resolveTargetTriplet(): { targetTriplet: string; extension: string } { return resolveTargetTripletFor(process.platform, process.arch); } @@ -279,7 +299,6 @@ export function resolveTargetTripletFor( `unsupported platform for rivet engine binary: ${platform}/${arch}`, ); } - async function isEngineRunning(): Promise { // Check if the engine is running on the port return await checkIfEngineAlreadyRunningOnPort(ENGINE_PORT); @@ -328,9 +347,9 @@ async function checkIfEngineAlreadyRunningOnPort( // Port responded but not with OK status return false; } - async function fileExists(filePath: string): Promise { try { + const fs = getNodeFs(); await fs.access(filePath); return true; } catch { diff --git a/rivetkit-typescript/packages/rivetkit/src/inspector/utils.ts b/rivetkit-typescript/packages/rivetkit/src/inspector/utils.ts index cd86ab686c..f9c1c3789a 100644 --- a/rivetkit-typescript/packages/rivetkit/src/inspector/utils.ts +++ b/rivetkit-typescript/packages/rivetkit/src/inspector/utils.ts @@ -1,4 +1,4 @@ -import crypto from "node:crypto"; +// import crypto from "node:crypto"; import { createMiddleware } from "hono/factory"; import type { ManagerDriver } from "@/driver-helpers/mod"; import type { RunConfig } from "@/mod"; @@ -20,10 +20,11 @@ export function compareSecrets(providedSecret: string, validSecret: string) { return false; } - // Perform timing-safe comparison - if (!crypto.timingSafeEqual(a, b)) { - return false; - } + // TODO: + // // Perform timing-safe comparison + // if (!crypto.timingSafeEqual(a, b)) { + // return false; + // } return true; } diff --git a/rivetkit-typescript/packages/rivetkit/src/mod.ts b/rivetkit-typescript/packages/rivetkit/src/mod.ts index 924a7aa299..156ae3daaa 100644 --- a/rivetkit-typescript/packages/rivetkit/src/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/mod.ts @@ -8,11 +8,10 @@ export { export { InlineWebSocketAdapter2 } from "@/common/inline-websocket-adapter2"; export { noopNext } from "@/common/utils"; export { createEngineDriver } from "@/drivers/engine/mod"; -export { - createFileSystemDriver, - createMemoryDriver, -} from "@/drivers/file-system/mod"; -// Re-export important protocol types and utilities needed by drivers +// export { +// createFileSystemDriver, +// createMemoryDriver, +// } from "@/drivers/file-system/mod"; export type { ActorQuery } from "@/manager/protocol/query"; export * from "@/registry/mod"; export { toUint8Array } from "@/utils"; diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/mod.ts b/rivetkit-typescript/packages/rivetkit/src/registry/mod.ts index 5befc2a6ad..9104ea3d86 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/mod.ts @@ -58,7 +58,9 @@ export class Registry { /** * Runs the registry for a server. */ - public start(inputConfig?: RunnerConfigInput): ServerOutput { + public async start( + inputConfig?: RunnerConfigInput, + ): Promise> { const config = RunnerConfigSchema.parse(inputConfig); // Validate autoConfigureServerless is only used with serverless runner @@ -72,7 +74,7 @@ export class Registry { } // Promise for any async operations we need to wait to complete - const readyPromises = []; + const readyPromises: Promise[] = []; // Disable health check if using serverless // @@ -135,7 +137,7 @@ export class Registry { } // Choose the driver based on configuration - const driver = chooseDefaultDriver(config); + const driver = await chooseDefaultDriver(config); // Set defaults based on the driver if (driver.name === "engine") { @@ -209,6 +211,23 @@ export class Registry { console.log(); } + const { router: hono } = createManagerRouter( + this.#config, + config, + managerDriver, + driver, + client, + ); + + // Start server + if (!config.disableDefaultServer) { + const serverPromise = (async () => { + const out = await crossPlatformServe(config, hono, undefined); + upgradeWebSocket = out.upgradeWebSocket; + })(); + readyPromises.push(serverPromise); + } + // HACK: We need to find a better way to let the driver itself decide when to start the actor driver // Create runner // @@ -230,22 +249,6 @@ export class Registry { }); } - const { router: hono } = createManagerRouter( - this.#config, - config, - managerDriver, - driver, - client, - ); - - // Start server - if (!config.disableDefaultServer) { - (async () => { - const out = await crossPlatformServe(config, hono, undefined); - upgradeWebSocket = out.upgradeWebSocket; - })(); - } - return { client, fetch: hono.fetch.bind(hono), diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/serve.ts b/rivetkit-typescript/packages/rivetkit/src/registry/serve.ts index 44b2b09896..0aaaa18990 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/serve.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/serve.ts @@ -9,12 +9,13 @@ export async function crossPlatformServe( ) { const app = userRouter ?? new Hono(); - // Import @hono/node-server + // Import @hono/node-server using string variable to prevent static analysis + const nodeServerModule = "@hono/node-server"; let serve: any; try { const dep = await import( /* webpackIgnore: true */ - "@hono/node-server" + nodeServerModule ); serve = dep.serve; } catch (err) { @@ -28,12 +29,13 @@ export async function crossPlatformServe( // app.route("/registry", rivetKitRouter); app.route("/", rivetKitRouter); - // Import @hono/node-ws + // Import @hono/node-ws using string variable to prevent static analysis + const nodeWsModule = "@hono/node-ws"; let createNodeWebSocket: any; try { const dep = await import( /* webpackIgnore: true */ - "@hono/node-ws" + nodeWsModule ); createNodeWebSocket = dep.createNodeWebSocket; } catch (err) { diff --git a/rivetkit-typescript/packages/rivetkit/src/test/mod.ts b/rivetkit-typescript/packages/rivetkit/src/test/mod.ts index 7fddf5ee49..c6764a4ae4 100644 --- a/rivetkit-typescript/packages/rivetkit/src/test/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/test/mod.ts @@ -17,7 +17,10 @@ import { RunnerConfigSchema } from "@/registry/run-config"; import { ConfigSchema, type InputConfig } from "./config"; import { logger } from "./log"; -function serve(registry: Registry, inputConfig?: InputConfig): ServerType { +async function serve( + registry: Registry, + inputConfig?: InputConfig, +): Promise { // Configure default configuration inputConfig ??= {}; @@ -30,7 +33,8 @@ function serve(registry: Registry, inputConfig?: InputConfig): ServerType { // Create router const runConfig = RunnerConfigSchema.parse(inputConfig); - const driver = inputConfig.driver ?? createFileSystemOrMemoryDriver(false); + const driver = + inputConfig.driver ?? (await createFileSystemOrMemoryDriver(false)); const managerDriver = driver.manager(registry.config, config); const client = createClientWithDriver( managerDriver, @@ -92,7 +96,7 @@ export async function setupTest>( // Start server with a random port const port = await getPort(); - const server = serve(registry, { port }); + const server = await serve(registry, { port }); c.onTestFinished( async () => await new Promise((resolve) => server.close(() => resolve())), diff --git a/rivetkit-typescript/packages/rivetkit/src/utils/node.ts b/rivetkit-typescript/packages/rivetkit/src/utils/node.ts new file mode 100644 index 0000000000..746df0ba65 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/src/utils/node.ts @@ -0,0 +1,169 @@ +// Global variables for Node.js modules +let nodeCrypto: typeof import("node:crypto") | undefined; +let nodeFsSync: typeof import("node:fs") | undefined; +let nodeFs: typeof import("node:fs/promises") | undefined; +let nodePath: typeof import("node:path") | undefined; +let nodeOs: typeof import("node:os") | undefined; +let nodeChildProcess: typeof import("node:child_process") | undefined; +let nodeStream: typeof import("node:stream/promises") | undefined; + +// Singleton promise to ensure imports happen only once +let importPromise: Promise | undefined; + +/** + * Dynamically imports all required Node.js dependencies. + * This function is idempotent and will only import once. + * @throws Error if Node.js modules are not available (e.g., in browser/edge environments) + */ +export async function importNodeDependencies(): Promise { + if (importPromise) return importPromise; + + importPromise = (async () => { + try { + // Dynamic imports with webpack ignore comment to prevent bundling + const cryptoModule = "node:crypto"; + const fsModule = "node:fs"; + const fsPromisesModule = "node:fs/promises"; + const pathModule = "node:path"; + const osModule = "node:os"; + const childProcessModule = "node:child_process"; + const streamModule = "node:stream/promises"; + + const modules = await Promise.all([ + import(/* webpackIgnore: true */ cryptoModule), + import(/* webpackIgnore: true */ fsModule), + import(/* webpackIgnore: true */ fsPromisesModule), + import(/* webpackIgnore: true */ pathModule), + import(/* webpackIgnore: true */ osModule), + import(/* webpackIgnore: true */ childProcessModule), + import(/* webpackIgnore: true */ streamModule), + ]); + + [ + nodeCrypto, + nodeFsSync, + nodeFs, + nodePath, + nodeOs, + nodeChildProcess, + nodeStream, + ] = modules; + } catch (err) { + // Node.js not available - will use memory driver fallback + console.warn( + "Node.js modules not available, file system driver will not work", + err, + ); + throw err; + } + })(); + + return importPromise; +} + +/** + * Gets the Node.js crypto module. + * @throws Error if crypto module is not loaded + */ +export function getNodeCrypto(): typeof import("node:crypto") { + if (!nodeCrypto) { + throw new Error( + "Node crypto module not loaded. Ensure importNodeDependencies() has been called.", + ); + } + return nodeCrypto; +} + +/** + * Gets the Node.js fs module. + * @throws Error if fs module is not loaded + */ +export function getNodeFsSync(): typeof import("node:fs") { + if (!nodeFsSync) { + throw new Error( + "Node fs module not loaded. Ensure importNodeDependencies() has been called.", + ); + } + return nodeFsSync; +} + +/** + * Gets the Node.js fs/promises module. + * @throws Error if fs/promises module is not loaded + */ +export function getNodeFs(): typeof import("node:fs/promises") { + if (!nodeFs) { + throw new Error( + "Node fs/promises module not loaded. Ensure importNodeDependencies() has been called.", + ); + } + return nodeFs; +} + +/** + * Gets the Node.js path module. + * @throws Error if path module is not loaded + */ +export function getNodePath(): typeof import("node:path") { + if (!nodePath) { + throw new Error( + "Node path module not loaded. Ensure importNodeDependencies() has been called.", + ); + } + return nodePath; +} + +/** + * Gets the Node.js os module. + * @throws Error if os module is not loaded + */ +export function getNodeOs(): typeof import("node:os") { + if (!nodeOs) { + throw new Error( + "Node os module not loaded. Ensure importNodeDependencies() has been called.", + ); + } + return nodeOs; +} + +/** + * Gets the Node.js child_process module. + * @throws Error if child_process module is not loaded + */ +export function getNodeChildProcess(): typeof import("node:child_process") { + if (!nodeChildProcess) { + throw new Error( + "Node child_process module not loaded. Ensure importNodeDependencies() has been called.", + ); + } + return nodeChildProcess; +} + +/** + * Gets the Node.js stream/promises module. + * @throws Error if stream/promises module is not loaded + */ +export function getNodeStream(): typeof import("node:stream/promises") { + if (!nodeStream) { + throw new Error( + "Node stream/promises module not loaded. Ensure importNodeDependencies() has been called.", + ); + } + return nodeStream; +} + +/** + * Checks if Node.js dependencies are available. + * @returns true if all Node.js modules are loaded + */ +export function areNodeDependenciesAvailable(): boolean { + return !!( + nodeCrypto && + nodeFsSync && + nodeFs && + nodePath && + nodeOs && + nodeChildProcess && + nodeStream + ); +} diff --git a/rivetkit-typescript/packages/rivetkit/tests/driver-file-system.test.ts b/rivetkit-typescript/packages/rivetkit/tests/driver-file-system.test.ts index 1c5b662b5a..197bc26020 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/driver-file-system.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/driver-file-system.test.ts @@ -10,7 +10,7 @@ runDriverTests({ join(__dirname, "../fixtures/driver-test-suite/registry.ts"), async () => { return { - driver: createFileSystemOrMemoryDriver( + driver: await createFileSystemOrMemoryDriver( true, `/tmp/test-${crypto.randomUUID()}`, ), diff --git a/rivetkit-typescript/packages/rivetkit/tests/driver-memory.test.ts b/rivetkit-typescript/packages/rivetkit/tests/driver-memory.test.ts index c299c0d042..20912e9e36 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/driver-memory.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/driver-memory.test.ts @@ -14,7 +14,7 @@ runDriverTests({ join(__dirname, "../fixtures/driver-test-suite/registry.ts"), async () => { return { - driver: createFileSystemOrMemoryDriver(false), + driver: await createFileSystemOrMemoryDriver(false), }; }, );