diff --git a/biome.json b/biome.json index 8c472ef33c..d7e19f04c7 100644 --- a/biome.json +++ b/biome.json @@ -43,5 +43,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/artifacts/openapi.json b/engine/artifacts/openapi.json index ff5cbd4f6b..0efd9567ad 100644 --- a/engine/artifacts/openapi.json +++ b/engine/artifacts/openapi.json @@ -11,7 +11,7 @@ "name": "Apache-2.0", "identifier": "Apache-2.0" }, - "version": "2.0.23" + "version": "2.0.24-rc.1" }, "paths": { "/actors": { 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/typescript/runner-protocol/src/index.ts b/engine/sdks/typescript/runner-protocol/src/index.ts index c6405665cb..343802861a 100644 --- a/engine/sdks/typescript/runner-protocol/src/index.ts +++ b/engine/sdks/typescript/runner-protocol/src/index.ts @@ -1,4 +1,4 @@ -import assert from "node:assert" + import * as bare from "@bare-ts/lib" const DEFAULT_CONFIG = /* @__PURE__ */ bare.Config({}) @@ -1906,3 +1906,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-workspace.yaml b/pnpm-workspace.yaml index 20074e1e18..5469ff1502 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-asyncapi/asyncapi.json b/rivetkit-asyncapi/asyncapi.json index 0d9c7c7f98..e6074c7754 100644 --- a/rivetkit-asyncapi/asyncapi.json +++ b/rivetkit-asyncapi/asyncapi.json @@ -1,436 +1,489 @@ { - "asyncapi": "3.0.0", - "info": { - "title": "RivetKit WebSocket Protocol", - "version": "2.0.24-rc.1", - "description": "WebSocket protocol for bidirectional communication between RivetKit clients and actors" - }, - "channels": { - "/gateway/{actorId}/connect": { - "address": "/gateway/{actorId}/connect", - "parameters": { - "actorId": { - "description": "The unique identifier for the actor instance" - } - }, - "messages": { - "toClient": { - "$ref": "#/components/messages/ToClient" - }, - "toServer": { - "$ref": "#/components/messages/ToServer" - } - } - } - }, - "operations": { - "sendToClient": { - "action": "send", - "channel": { - "$ref": "#/channels/~1gateway~1{actorId}~1connect" - }, - "messages": [ - { - "$ref": "#/channels/~1gateway~1{actorId}~1connect/messages/toClient" - } - ], - "summary": "Send messages from server to client", - "description": "Messages sent from the RivetKit actor to connected clients" - }, - "receiveFromClient": { - "action": "receive", - "channel": { - "$ref": "#/channels/~1gateway~1{actorId}~1connect" - }, - "messages": [ - { - "$ref": "#/channels/~1gateway~1{actorId}~1connect/messages/toServer" - } - ], - "summary": "Receive messages from client", - "description": "Messages received by the RivetKit actor from connected clients" - } - }, - "components": { - "messages": { - "ToClient": { - "name": "ToClient", - "title": "Message To Client", - "summary": "A message sent from the server to the client", - "contentType": "application/json", - "payload": { - "type": "object", - "properties": { - "body": { - "anyOf": [ - { - "type": "object", - "properties": { - "tag": { - "type": "string", - "const": "Init" - }, - "val": { - "type": "object", - "properties": { - "actorId": { - "type": "string" - }, - "connectionId": { - "type": "string" - } - }, - "required": [ - "actorId", - "connectionId" - ], - "additionalProperties": false - } - }, - "required": ["tag", "val"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "tag": { - "type": "string", - "const": "Error" - }, - "val": { - "type": "object", - "properties": { - "group": { - "type": "string" - }, - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "metadata": {}, - "actionId": { - "type": ["integer", "null"] - } - }, - "required": [ - "group", - "code", - "message", - "actionId" - ], - "additionalProperties": false - } - }, - "required": ["tag", "val"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "tag": { - "type": "string", - "const": "ActionResponse" - }, - "val": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "output": {} - }, - "required": ["id"], - "additionalProperties": false - } - }, - "required": ["tag", "val"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "tag": { - "type": "string", - "const": "Event" - }, - "val": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "args": {} - }, - "required": ["name"], - "additionalProperties": false - } - }, - "required": ["tag", "val"], - "additionalProperties": false - } - ] - } - }, - "required": ["body"], - "additionalProperties": false - }, - "examples": [ - { - "name": "Init message", - "summary": "Initial connection message", - "payload": { - "body": { - "tag": "Init", - "val": { - "actorId": "actor_123", - "connectionId": "conn_456" - } - } - } - }, - { - "name": "Error message", - "summary": "Error response", - "payload": { - "body": { - "tag": "Error", - "val": { - "group": "auth", - "code": "unauthorized", - "message": "Authentication failed", - "actionId": null - } - } - } - }, - { - "name": "Action response", - "summary": "Response to an action request", - "payload": { - "body": { - "tag": "ActionResponse", - "val": { - "id": "123", - "output": { - "result": "success" - } - } - } - } - }, - { - "name": "Event", - "summary": "Event broadcast to subscribed clients", - "payload": { - "body": { - "tag": "Event", - "val": { - "name": "stateChanged", - "args": { - "newState": "active" - } - } - } - } - } - ] - }, - "ToServer": { - "name": "ToServer", - "title": "Message To Server", - "summary": "A message sent from the client to the server", - "contentType": "application/json", - "payload": { - "type": "object", - "properties": { - "body": { - "anyOf": [ - { - "type": "object", - "properties": { - "tag": { - "type": "string", - "const": "ActionRequest" - }, - "val": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - }, - "args": {} - }, - "required": ["id", "name"], - "additionalProperties": false - } - }, - "required": ["tag", "val"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "tag": { - "type": "string", - "const": "SubscriptionRequest" - }, - "val": { - "type": "object", - "properties": { - "eventName": { - "type": "string" - }, - "subscribe": { - "type": "boolean" - } - }, - "required": [ - "eventName", - "subscribe" - ], - "additionalProperties": false - } - }, - "required": ["tag", "val"], - "additionalProperties": false - } - ] - } - }, - "required": ["body"], - "additionalProperties": false - }, - "examples": [ - { - "name": "Action request", - "summary": "Request to execute an action", - "payload": { - "body": { - "tag": "ActionRequest", - "val": { - "id": "123", - "name": "updateState", - "args": { - "key": "value" - } - } - } - } - }, - { - "name": "Subscription request", - "summary": "Request to subscribe/unsubscribe from an event", - "payload": { - "body": { - "tag": "SubscriptionRequest", - "val": { - "eventName": "stateChanged", - "subscribe": true - } - } - } - } - ] - } - }, - "schemas": { - "Init": { - "type": "object", - "properties": { - "actorId": { - "type": "string" - }, - "connectionId": { - "type": "string" - } - }, - "required": ["actorId", "connectionId"], - "additionalProperties": false, - "description": "Initial connection message sent from server to client" - }, - "Error": { - "type": "object", - "properties": { - "group": { - "type": "string" - }, - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "metadata": {}, - "actionId": { - "type": ["integer", "null"] - } - }, - "required": ["group", "code", "message", "actionId"], - "additionalProperties": false, - "description": "Error message sent from server to client" - }, - "ActionResponse": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "output": {} - }, - "required": ["id"], - "additionalProperties": false, - "description": "Response to an action request" - }, - "Event": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "args": {} - }, - "required": ["name"], - "additionalProperties": false, - "description": "Event broadcast to subscribed clients" - }, - "ActionRequest": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - }, - "args": {} - }, - "required": ["id", "name"], - "additionalProperties": false, - "description": "Request to execute an action on the actor" - }, - "SubscriptionRequest": { - "type": "object", - "properties": { - "eventName": { - "type": "string" - }, - "subscribe": { - "type": "boolean" - } - }, - "required": ["eventName", "subscribe"], - "additionalProperties": false, - "description": "Request to subscribe or unsubscribe from an event" - } - } - } -} + "asyncapi": "3.0.0", + "info": { + "title": "RivetKit WebSocket Protocol", + "version": "2.0.24-rc.1", + "description": "WebSocket protocol for bidirectional communication between RivetKit clients and actors" + }, + "channels": { + "/gateway/{actorId}/connect": { + "address": "/gateway/{actorId}/connect", + "parameters": { + "actorId": { + "description": "The unique identifier for the actor instance" + } + }, + "messages": { + "toClient": { + "$ref": "#/components/messages/ToClient" + }, + "toServer": { + "$ref": "#/components/messages/ToServer" + } + } + } + }, + "operations": { + "sendToClient": { + "action": "send", + "channel": { + "$ref": "#/channels/~1gateway~1{actorId}~1connect" + }, + "messages": [ + { + "$ref": "#/channels/~1gateway~1{actorId}~1connect/messages/toClient" + } + ], + "summary": "Send messages from server to client", + "description": "Messages sent from the RivetKit actor to connected clients" + }, + "receiveFromClient": { + "action": "receive", + "channel": { + "$ref": "#/channels/~1gateway~1{actorId}~1connect" + }, + "messages": [ + { + "$ref": "#/channels/~1gateway~1{actorId}~1connect/messages/toServer" + } + ], + "summary": "Receive messages from client", + "description": "Messages received by the RivetKit actor from connected clients" + } + }, + "components": { + "messages": { + "ToClient": { + "name": "ToClient", + "title": "Message To Client", + "summary": "A message sent from the server to the client", + "contentType": "application/json", + "payload": { + "type": "object", + "properties": { + "body": { + "anyOf": [ + { + "type": "object", + "properties": { + "tag": { + "type": "string", + "const": "Init" + }, + "val": { + "type": "object", + "properties": { + "actorId": { + "type": "string" + }, + "connectionId": { + "type": "string" + } + }, + "required": [ + "actorId", + "connectionId" + ], + "additionalProperties": false + } + }, + "required": [ + "tag", + "val" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "tag": { + "type": "string", + "const": "Error" + }, + "val": { + "type": "object", + "properties": { + "group": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "metadata": {}, + "actionId": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "group", + "code", + "message", + "actionId" + ], + "additionalProperties": false + } + }, + "required": [ + "tag", + "val" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "tag": { + "type": "string", + "const": "ActionResponse" + }, + "val": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "output": {} + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "required": [ + "tag", + "val" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "tag": { + "type": "string", + "const": "Event" + }, + "val": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "args": {} + }, + "required": [ + "name" + ], + "additionalProperties": false + } + }, + "required": [ + "tag", + "val" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "body" + ], + "additionalProperties": false + }, + "examples": [ + { + "name": "Init message", + "summary": "Initial connection message", + "payload": { + "body": { + "tag": "Init", + "val": { + "actorId": "actor_123", + "connectionId": "conn_456" + } + } + } + }, + { + "name": "Error message", + "summary": "Error response", + "payload": { + "body": { + "tag": "Error", + "val": { + "group": "auth", + "code": "unauthorized", + "message": "Authentication failed", + "actionId": null + } + } + } + }, + { + "name": "Action response", + "summary": "Response to an action request", + "payload": { + "body": { + "tag": "ActionResponse", + "val": { + "id": "123", + "output": { + "result": "success" + } + } + } + } + }, + { + "name": "Event", + "summary": "Event broadcast to subscribed clients", + "payload": { + "body": { + "tag": "Event", + "val": { + "name": "stateChanged", + "args": { + "newState": "active" + } + } + } + } + } + ] + }, + "ToServer": { + "name": "ToServer", + "title": "Message To Server", + "summary": "A message sent from the client to the server", + "contentType": "application/json", + "payload": { + "type": "object", + "properties": { + "body": { + "anyOf": [ + { + "type": "object", + "properties": { + "tag": { + "type": "string", + "const": "ActionRequest" + }, + "val": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "args": {} + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false + } + }, + "required": [ + "tag", + "val" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "tag": { + "type": "string", + "const": "SubscriptionRequest" + }, + "val": { + "type": "object", + "properties": { + "eventName": { + "type": "string" + }, + "subscribe": { + "type": "boolean" + } + }, + "required": [ + "eventName", + "subscribe" + ], + "additionalProperties": false + } + }, + "required": [ + "tag", + "val" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "body" + ], + "additionalProperties": false + }, + "examples": [ + { + "name": "Action request", + "summary": "Request to execute an action", + "payload": { + "body": { + "tag": "ActionRequest", + "val": { + "id": "123", + "name": "updateState", + "args": { + "key": "value" + } + } + } + } + }, + { + "name": "Subscription request", + "summary": "Request to subscribe/unsubscribe from an event", + "payload": { + "body": { + "tag": "SubscriptionRequest", + "val": { + "eventName": "stateChanged", + "subscribe": true + } + } + } + } + ] + } + }, + "schemas": { + "Init": { + "type": "object", + "properties": { + "actorId": { + "type": "string" + }, + "connectionId": { + "type": "string" + } + }, + "required": [ + "actorId", + "connectionId" + ], + "additionalProperties": false, + "description": "Initial connection message sent from server to client" + }, + "Error": { + "type": "object", + "properties": { + "group": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "metadata": {}, + "actionId": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "group", + "code", + "message", + "actionId" + ], + "additionalProperties": false, + "description": "Error message sent from server to client" + }, + "ActionResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "output": {} + }, + "required": [ + "id" + ], + "additionalProperties": false, + "description": "Response to an action request" + }, + "Event": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "args": {} + }, + "required": [ + "name" + ], + "additionalProperties": false, + "description": "Event broadcast to subscribed clients" + }, + "ActionRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "args": {} + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false, + "description": "Request to execute an action on the actor" + }, + "SubscriptionRequest": { + "type": "object", + "properties": { + "eventName": { + "type": "string" + }, + "subscribe": { + "type": "boolean" + } + }, + "required": [ + "eventName", + "subscribe" + ], + "additionalProperties": false, + "description": "Request to subscribe or unsubscribe from an event" + } + } + } +} \ No newline at end of file diff --git a/rivetkit-typescript/packages/rivetkit/package.json b/rivetkit-typescript/packages/rivetkit/package.json index 8dd09f52bd..46d975fe96 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/client-protocol/v2.bare -o dist/schemas/client-protocol/v2.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/file-system-driver/v2.bare -o dist/schemas/file-system-driver/v2.ts && ./scripts/compile-bare.ts compile schemas/actor-persist/v1.bare -o dist/schemas/actor-persist/v1.ts && ./scripts/compile-bare.ts compile schemas/actor-persist/v2.bare -o dist/schemas/actor-persist/v2.ts && ./scripts/compile-bare.ts compile schemas/actor-persist/v3.bare -o dist/schemas/actor-persist/v3.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", @@ -178,6 +182,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 b655db6749..005fc4e210 100644 --- a/rivetkit-typescript/packages/rivetkit/scripts/dump-openapi.ts +++ b/rivetkit-typescript/packages/rivetkit/scripts/dump-openapi.ts @@ -1,6 +1,5 @@ import * as fs from "node:fs/promises"; import { resolve } from "node:path"; -import { zodToJsonSchema } from "zod-to-json-schema"; import { ClientConfigSchema } from "@/client/config"; import { createFileSystemOrMemoryDriver } from "@/drivers/file-system/mod"; import type { ManagerDriver } from "@/manager/driver"; @@ -12,13 +11,9 @@ import { setup, } from "@/mod"; import { type RunnerConfig, RunnerConfigSchema } from "@/registry/run-config"; -import { - HttpActionRequestSchema, - HttpActionResponseSchema, -} from "@/schemas/client-protocol-zod/mod"; import { VERSION } from "@/utils"; -function main() { +async function main() { const registryConfig: RegistryConfig = RegistryConfigSchema.parse({ use: {}, }); @@ -37,13 +32,13 @@ function main() { getWithKey: unimplemented, getOrCreateWithKey: unimplemented, createActor: unimplemented, - listActors: unimplemented, sendRequest: unimplemented, openWebSocket: unimplemented, proxyRequest: unimplemented, proxyWebSocket: unimplemented, displayInformation: unimplemented, getOrCreateInspectorAccessToken: unimplemented, + listActors: unimplemented, }; const client = createClientWithDriver( @@ -51,7 +46,7 @@ function main() { ClientConfigSchema.parse({}), ); - const { openapi: managerOpenapi } = createManagerRouter( + const { openapi } = createManagerRouter( registryConfig, driverConfig, managerDriver, @@ -59,8 +54,7 @@ function main() { client, ); - // Get OpenAPI document - const managerOpenApiDoc = managerOpenapi.getOpenAPIDocument({ + const openApiDoc = openapi.getOpenAPIDocument({ openapi: "3.0.0", info: { version: VERSION, @@ -68,9 +62,6 @@ function main() { }, }); - // Inject actor router paths - injectActorRouter(managerOpenApiDoc); - const outputPath = resolve( import.meta.dirname, "..", @@ -80,136 +71,10 @@ function main() { "rivetkit-openapi", "openapi.json", ); - fs.writeFile(outputPath, JSON.stringify(managerOpenApiDoc, null, 2)); + await fs.writeFile(outputPath, JSON.stringify(openApiDoc, null, 2)); console.log("Dumped OpenAPI to", outputPath); } -/** - * Manually inject actor router paths into the OpenAPI spec. - * - * We do this manually instead of extracting from the actual router since the - * actor routes support multiple encodings (JSON, CBOR, bare), but OpenAPI - * specs are JSON-focused and don't cleanly represent multi-encoding routes. - */ -function injectActorRouter(openApiDoc: any) { - if (!openApiDoc.paths) { - openApiDoc.paths = {}; - } - - // Convert Zod schemas to JSON Schema and remove $schema property - const actionRequestSchema = zodToJsonSchema(HttpActionRequestSchema, { - $refStrategy: "none", - }); - delete (actionRequestSchema as any).$schema; - - const actionResponseSchema = zodToJsonSchema(HttpActionResponseSchema, { - $refStrategy: "none", - }); - delete (actionResponseSchema as any).$schema; - - // Common actorId parameter - const actorIdParam = { - name: "actorId", - in: "path" as const, - required: true, - schema: { - type: "string", - }, - description: "The ID of the actor to target", - }; - - // GET /gateway/{actorId}/health - openApiDoc.paths["/gateway/{actorId}/health"] = { - get: { - parameters: [actorIdParam], - responses: { - 200: { - description: "Health check", - content: { - "text/plain": { - schema: { - type: "string", - }, - }, - }, - }, - }, - }, - }; - - // POST /gateway/{actorId}/action/{action} - openApiDoc.paths["/gateway/{actorId}/action/{action}"] = { - post: { - parameters: [ - actorIdParam, - { - name: "action", - in: "path" as const, - required: true, - schema: { - type: "string", - }, - description: "The name of the action to execute", - }, - ], - requestBody: { - content: { - "application/json": { - schema: actionRequestSchema, - }, - }, - }, - responses: { - 200: { - description: "Action executed successfully", - content: { - "application/json": { - schema: actionResponseSchema, - }, - }, - }, - 400: { - description: "Invalid action", - }, - 500: { - description: "Internal error", - }, - }, - }, - }; - - // ALL /gateway/{actorId}/request/{path} - const requestPath = { - parameters: [ - actorIdParam, - { - name: "path", - in: "path" as const, - required: true, - schema: { - type: "string", - }, - description: "The HTTP path to forward to the actor", - }, - ], - responses: { - 200: { - description: "Response from actor's raw HTTP handler", - }, - }, - }; - - openApiDoc.paths["/gateway/{actorId}/request/{path}"] = { - get: requestPath, - post: requestPath, - put: requestPath, - delete: requestPath, - patch: requestPath, - head: requestPath, - options: requestPath, - }; -} - function unimplemented(): never { throw new Error("UNIMPLEMENTED"); } 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 9bc76f87a8..98578aecf1 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"; @@ -28,6 +24,12 @@ import { setLongTimeout, stringifyError, } from "@/utils"; +import { + getNodeCrypto, + getNodeFs, + getNodeFsSync, + getNodePath, +} from "@/utils/node"; import { logger } from "./log"; import { ensureDirectoryExists, @@ -94,6 +96,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"); @@ -105,6 +108,7 @@ export class FileSystemGlobalState { ensureDirectoryExistsSync(this.#alarmsDir); try { + const fsSync = getNodeFsSync(); const actorIds = fsSync.readdirSync(this.#stateDir); this.#actorCountOnStartup = actorIds.length; } catch (error) { @@ -132,15 +136,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: { @@ -149,6 +153,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) @@ -267,6 +272,7 @@ export class FileSystemGlobalState { // Read & parse file try { + const fs = getNodeFs(); const stateData = await fs.readFile(stateFilePath); // Cache the loaded state in handler @@ -368,8 +374,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, @@ -379,10 +387,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({ @@ -407,10 +417,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 @@ -425,11 +437,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 @@ -564,12 +578,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); @@ -638,6 +654,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") { @@ -684,6 +701,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"); @@ -699,6 +718,7 @@ export class FileSystemGlobalState { */ #cleanupTempFilesSync(): void { try { + const fsSync = getNodeFsSync(); const files = fsSync.readdirSync(this.#stateDir); const tempFiles = files.filter((f) => f.includes(".tmp.")); @@ -706,6 +726,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..707399991d 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"; @@ -12,6 +13,8 @@ export function createFileSystemOrMemoryDriver( persist: boolean = true, customPath?: string, ): DriverConfig { + importNodeDependencies(); + const state = new FileSystemGlobalState(persist, customPath); const driverConfig: DriverConfig = { name: persist ? "file-system" : "memory", 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 1b9c91f62c..b732ae9265 100644 --- a/rivetkit-typescript/packages/rivetkit/src/engine-process/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/engine-process/mod.ts @@ -1,14 +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 { EXTRA_ERROR_LOG } from "@/utils"; +import { + getNodeChildProcess, + getNodeCrypto, + getNodeFs, + getNodeFsSync, + getNodePath, + getNodeStream, + importNodeDependencies, +} from "@/utils/node"; import { logger } from "./log"; export const ENGINE_PORT = 6420; @@ -24,10 +26,14 @@ interface EnsureEngineProcessOptions { export async function ensureEngineProcess( options: EnsureEngineProcessOptions, ): Promise { + 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"); @@ -62,7 +68,6 @@ export async function ensureEngineProcess( ); } } - // Create log file streams with timestamp in the filename const timestamp = new Date() .toISOString() @@ -71,8 +76,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", @@ -80,30 +90,12 @@ 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: { ...process.env, - // In development, runners can be terminated without a graceful - // shutdown (i.e. SIGKILL instead of SIGTERM). This is treated as a - // crash by Rivet Engine in production and implements a backoff for - // rescheduling actors in case of a crash loop. - // - // This is problematic in development since this will cause actors - // to become unresponsive if frequently killing your dev server. - // - // We reduce the timeouts for resetting a runner as healthy in - // order to account for this. - RIVET__PEGBOARD__RETRY_RESET_DURATION: "100", - RIVET__PEGBOARD__BASE_RETRY_TIMEOUT: "100", - // Set max exponent to 1 to have a maximum of base_retry_timeout - RIVET__PEGBOARD__RESCHEDULE_BACKOFF_MAX_EXPONENT: "1", - // Reduce thresholds for faster development iteration - // - // Default ping interval is 3s, this gives a 2s & 4s grace - RIVET__PEGBOARD__RUNNER_ELIGIBLE_THRESHOLD: "5000", - RIVET__PEGBOARD__RUNNER_LOST_THRESHOLD: "7000", }, }); @@ -118,7 +110,6 @@ export async function ensureEngineProcess( if (child.stderr) { child.stderr.pipe(stderrStream); } - logger().debug({ msg: "spawned engine process", pid: child.pid, @@ -130,7 +121,8 @@ export async function ensureEngineProcess( msg: "engine process exited, please report this error", code, signal, - ...EXTRA_ERROR_LOG, + issues: "https://github.com/rivet-dev/rivetkit/issues", + support: "https://rivet.dev/discord", }); // Clean up log streams stdoutStream.end(); @@ -194,7 +186,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({ @@ -212,12 +205,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; @@ -247,9 +246,11 @@ async function downloadEngineBinaryIfNeeded( msg: "engine download failed, please report this error", tempPath, error, - ...EXTRA_ERROR_LOG, + issues: "https://github.com/rivet-dev/rivetkit/issues", + support: "https://rivet.dev/discord", }); try { + const fs = getNodeFs(); await fs.unlink(tempPath); } catch (unlinkError) { // Ignore errors when cleaning up (file may not exist) @@ -257,7 +258,7 @@ async function downloadEngineBinaryIfNeeded( throw error; } } - +// function resolveTargetTriplet(): { targetTriplet: string; extension: string } { return resolveTargetTripletFor(process.platform, process.arch); } @@ -297,7 +298,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); @@ -346,9 +346,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 edb2b78daa..413078653d 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/manager/router.ts b/rivetkit-typescript/packages/rivetkit/src/manager/router.ts index b54523335e..5a537e647a 100644 --- a/rivetkit-typescript/packages/rivetkit/src/manager/router.ts +++ b/rivetkit-typescript/packages/rivetkit/src/manager/router.ts @@ -79,6 +79,17 @@ function buildOpenApiResponses(schema: T) { }; } +function buildOpenApiRequestBody(schema: T) { + return { + required: true, + content: { + "application/json": { + schema, + }, + }, + }; +} + export function createManagerRouter( registryConfig: RegistryConfig, runConfig: RunnerConfig, @@ -405,13 +416,7 @@ function addManagerRoutes( method: "put", path: "/actors", request: { - body: { - content: { - "application/json": { - schema: ActorsGetOrCreateRequestSchema, - }, - }, - }, + body: buildOpenApiRequestBody(ActorsGetOrCreateRequestSchema), }, responses: buildOpenApiResponses(ActorsGetOrCreateResponseSchema), }); @@ -457,13 +462,7 @@ function addManagerRoutes( method: "post", path: "/actors", request: { - body: { - content: { - "application/json": { - schema: ActorsCreateRequestSchema, - }, - }, - }, + body: buildOpenApiRequestBody(ActorsCreateRequestSchema), }, responses: buildOpenApiResponses(ActorsCreateResponseSchema), }); diff --git a/rivetkit-typescript/packages/rivetkit/src/mod.ts b/rivetkit-typescript/packages/rivetkit/src/mod.ts index 9156949d8a..f75e91f73b 100644 --- a/rivetkit-typescript/packages/rivetkit/src/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/mod.ts @@ -7,11 +7,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 78fb798778..24622a5518 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/mod.ts @@ -72,7 +72,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 // @@ -209,6 +209,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); + 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 +247,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..00093b133d 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/serve.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/serve.ts @@ -1,20 +1,18 @@ -import { Hono } from "hono"; +import type { Hono } from "hono"; import { logger } from "./log"; import type { RunnerConfig } from "./run-config"; export async function crossPlatformServe( runConfig: RunnerConfig, - rivetKitRouter: Hono, - userRouter: Hono | undefined, + app: Hono, ) { - 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) { @@ -24,16 +22,13 @@ export async function crossPlatformServe( process.exit(1); } - // Mount registry - // 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) { @@ -45,7 +40,7 @@ export async function crossPlatformServe( // Inject WS const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ - app, + app: app, }); // Start server 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..371443c5e3 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/src/utils/node.ts @@ -0,0 +1,169 @@ +import { createRequire } from "node:module"; + +// Global variables for Node.js modules. +// +// We use synchronous require() instead of async import() for Node.js module loading because: +// 1. These modules are only needed in Node.js environments (not browser/edge) +// 2. registry.start() cannot be async and needs immediate access to Node modules +// 3. The setup process must be synchronous to avoid breaking the API +// +// Biome only allows imports of node modules in this file in order to ensure +// we're forcing the use of dynamic imports. +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; + +let hasImportedDependencies = false; + +// Helper to get a require function that works in both CommonJS and ESM. +// We use require() instead of await import() because registry.start() cannot +// be async and needs immediate access to Node.js modules during setup. +function getRequireFn() { + // CommonJS context - use global require + if (typeof require !== "undefined") { + return require; + } + + // ESM context - use createRequire with import.meta.url + // @ts-ignore - import.meta.url is available in ESM + return createRequire(import.meta.url); +} + +/** + * Dynamically imports all required Node.js dependencies. We do this early in a + * single function call in order to surface errors early. + * + * 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 function importNodeDependencies(): void { + // Check if already loaded + if (hasImportedDependencies) return; + + try { + // Get a require function that works in both CommonJS and ESM + const requireFn = getRequireFn(); + + // Use requireFn with webpack ignore comment to prevent bundling + // @ts-ignore - dynamic require usage + nodeCrypto = requireFn(/* webpackIgnore: true */ "node:crypto"); + // @ts-ignore + nodeFsSync = requireFn(/* webpackIgnore: true */ "node:fs"); + // @ts-ignore + nodeFs = requireFn(/* webpackIgnore: true */ "node:fs/promises"); + // @ts-ignore + nodePath = requireFn(/* webpackIgnore: true */ "node:path"); + // @ts-ignore + nodeOs = requireFn(/* webpackIgnore: true */ "node:os"); + // @ts-ignore + nodeChildProcess = requireFn( + /* webpackIgnore: true */ "node:child_process", + ); + // @ts-ignore + nodeStream = requireFn(/* webpackIgnore: true */ "node:stream/promises"); + + hasImportedDependencies = true; + } catch (err) { + console.warn( + "Node.js modules not available, file system driver will not work", + err, + ); + throw err; + } +} + +/** + * 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; +} 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), }; }, );