diff --git a/rivetkit-typescript/packages/rivetkit/runtime/index.ts b/rivetkit-typescript/packages/rivetkit/runtime/index.ts index c883b1d7aa..2cb00b06ff 100644 --- a/rivetkit-typescript/packages/rivetkit/runtime/index.ts +++ b/rivetkit-typescript/packages/rivetkit/runtime/index.ts @@ -42,17 +42,27 @@ async function ensureLocalRunnerConfig(config: RegistryConfig): Promise { const clientConfig = convertRegistryConfigToClientConfig(config); const dcsRes = await getDatacenters(clientConfig); - await updateRunnerConfig(clientConfig, config.envoy.poolName, { - datacenters: Object.fromEntries( - dcsRes.datacenters.map((dc) => [ - dc.name, - { - normal: {}, - drain_on_version_upgrade: true, - }, - ]), - ), - }); + const datacenters = Object.fromEntries( + dcsRes.datacenters.map((dc) => [ + dc.name, + { + normal: {}, + drain_on_version_upgrade: true, + }, + ]), + ); + + // Register the main pool as well as the native database pool. The native + // database envoy (used by any actor that opens `rivetkit/db`) registers + // under `${poolName}-native-db` via EngineActorDriver, so the engine must + // have a runner config for that pool or envoy registration fails with + // `no_runner_config`. + await Promise.all([ + updateRunnerConfig(clientConfig, config.envoy.poolName, { datacenters }), + updateRunnerConfig(clientConfig, `${config.envoy.poolName}-native-db`, { + datacenters, + }), + ]); } export class Runtime { @@ -62,7 +72,7 @@ export class Runtime { #actorDriver?: EngineActorDriver; #startKind?: StartKind; - managerPort?: number; + httpPort?: number; #serverlessRouter?: ReturnType["router"]; get config() { @@ -77,12 +87,12 @@ export class Runtime { registry: Registry, config: RegistryConfig, engineClient: EngineControlClient, - managerPort?: number, + httpPort?: number, ) { this.#registry = registry; this.#config = config; this.#engineClient = engineClient; - this.managerPort = managerPort; + this.httpPort = httpPort; } static async create( @@ -99,7 +109,7 @@ export class Runtime { } const shouldSpawnEngine = - config.serverless.spawnEngine || (config.serveManager && !config.endpoint); + config.serverless.spawnEngine || (config.serveHttp && !config.endpoint); if (shouldSpawnEngine) { config.endpoint = ENGINE_ENDPOINT; @@ -117,9 +127,9 @@ export class Runtime { ); await ensureLocalRunnerConfig(config); - let managerPort: number | undefined; - if (config.serveManager) { - const configuredManagerPort = config.managerPort; + let httpPort: number | undefined; + if (config.serveHttp) { + const configuredHttpPort = config.httpPort; const serveRuntime = detectRuntime(); let upgradeWebSocket: any; const getUpgradeWebSocket: GetUpgradeWebSocket = () => @@ -133,27 +143,27 @@ export class Runtime { serveRuntime, ); - managerPort = await findFreePort(config.managerPort); + httpPort = await findFreePort(config.httpPort); - if (managerPort !== configuredManagerPort) { + if (httpPort !== configuredHttpPort) { logger().warn({ - msg: `port ${configuredManagerPort} is in use, using ${managerPort}`, + msg: `port ${configuredHttpPort} is in use, using ${httpPort}`, }); } logger().debug({ - msg: "serving runtime router", - port: managerPort, + msg: "serving HTTP router", + port: httpPort, }); if ( config.publicEndpoint === - `http://127.0.0.1:${configuredManagerPort}` + `http://127.0.0.1:${configuredHttpPort}` ) { - config.publicEndpoint = `http://127.0.0.1:${managerPort}`; + config.publicEndpoint = `http://127.0.0.1:${httpPort}`; config.serverless.publicEndpoint = config.publicEndpoint; } - config.managerPort = managerPort; + config.httpPort = httpPort; let serverApp = runtimeRouter; if (config.publicDir) { @@ -181,7 +191,7 @@ export class Runtime { const out = await crossPlatformServe( config, - managerPort, + httpPort, serverApp, serveRuntime, ); @@ -196,7 +206,7 @@ export class Runtime { } } - const runtime = new Runtime(registry, config, engineClient, managerPort); + const runtime = new Runtime(registry, config, engineClient, httpPort); logger().info({ msg: "rivetkit ready", @@ -247,9 +257,11 @@ export class Runtime { #printWelcome(): void { if (this.#config.noWelcome) return; - const inspectorUrl = this.managerPort - ? getInspectorUrl(this.#config, this.managerPort) - : undefined; + // Inspector URL falls back through getInspectorUrl's own chain + // (inspector.defaultEndpoint → config.endpoint → HTTP port). In + // envoy mode there is no HTTP port, but the engine serves `/ui/` + // on its endpoint so the inspector stays reachable. + const inspectorUrl = getInspectorUrl(this.#config, this.httpPort ?? 0); console.log(); console.log( diff --git a/rivetkit-typescript/packages/rivetkit/src/actor-gateway/gateway.ts b/rivetkit-typescript/packages/rivetkit/src/actor-gateway/gateway.ts index d313198607..b0ca0cde98 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor-gateway/gateway.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor-gateway/gateway.ts @@ -151,10 +151,10 @@ export async function actorGateway( // Strip basePath from the request path let strippedPath = c.req.path; if ( - config.managerBasePath && - strippedPath.startsWith(config.managerBasePath) + config.httpBasePath && + strippedPath.startsWith(config.httpBasePath) ) { - strippedPath = strippedPath.slice(config.managerBasePath.length); + strippedPath = strippedPath.slice(config.httpBasePath.length); // Ensure the path starts with / if (!strippedPath.startsWith("/")) { strippedPath = `/${strippedPath}`; diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/errors.ts b/rivetkit-typescript/packages/rivetkit/src/actor/errors.ts index 5d4724c0bd..e72ee2b982 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/errors.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/errors.ts @@ -486,7 +486,7 @@ export class InvalidCanPublishResponse extends ActorError { } } -// Manager-specific errors +// HTTP server specific errors export class MissingActorHeader extends ActorError { constructor() { super( diff --git a/rivetkit-typescript/packages/rivetkit/src/common/actor-router-consts.ts b/rivetkit-typescript/packages/rivetkit/src/common/actor-router-consts.ts index f618bd683a..7e687af1e6 100644 --- a/rivetkit-typescript/packages/rivetkit/src/common/actor-router-consts.ts +++ b/rivetkit-typescript/packages/rivetkit/src/common/actor-router-consts.ts @@ -18,7 +18,7 @@ export const HEADER_ACTOR_ID = "x-rivet-actor"; export const HEADER_RIVET_TOKEN = "x-rivet-token"; -// MARK: Manager Gateway Headers +// MARK: HTTP Server Gateway Headers export const HEADER_RIVET_TARGET = "x-rivet-target"; export const HEADER_RIVET_ACTOR = "x-rivet-actor"; export const HEADER_RIVET_NAMESPACE = "x-rivet-namespace"; diff --git a/rivetkit-typescript/packages/rivetkit/src/common/websocket.ts b/rivetkit-typescript/packages/rivetkit/src/common/websocket.ts index a5b4b07f01..591aa2850f 100644 --- a/rivetkit-typescript/packages/rivetkit/src/common/websocket.ts +++ b/rivetkit-typescript/packages/rivetkit/src/common/websocket.ts @@ -20,7 +20,9 @@ export async function importWebSocket(): Promise { // Node.js environment try { const moduleName = "ws"; - const ws = await import(/* webpackIgnore: true */ moduleName); + const ws = await import( + /* webpackIgnore: true */ /* @vite-ignore */ moduleName + ); _WebSocket = ws.default as unknown as typeof WebSocket; logger().debug("using websocket from npm"); } catch { diff --git a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-sleep.ts b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-sleep.ts index 1cd6a69959..b17973cadd 100644 --- a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-sleep.ts +++ b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-sleep.ts @@ -123,7 +123,7 @@ async function readAfterSleepCycle( // `getCounts` // // To fix this, we need to imeplment some event system to be able to check for -// when an actor has slept. OR we can expose an HTTP endpoint on the manager +// when an actor has slept. OR we can expose an HTTP endpoint on the HTTP router // for `.test` that checks if na actor is sleeping that we can poll. export function runActorSleepTests(driverTestConfig: DriverTestConfig) { const describeSleepTests = driverTestConfig.skip?.sleep diff --git a/rivetkit-typescript/packages/rivetkit/src/engine-process/mod.ts b/rivetkit-typescript/packages/rivetkit/src/engine-process/mod.ts index 6a24b918e4..19116075e4 100644 --- a/rivetkit-typescript/packages/rivetkit/src/engine-process/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/engine-process/mod.ts @@ -204,6 +204,28 @@ export async function ensureEngineProcess( stderrStream.end(); }); + // Terminate child when parent exits so tsx --watch restarts and + // Ctrl+C don't leave orphaned engine processes holding port 6420. + let cleanedUp = false; + const terminateChild = (parentSignal?: NodeJS.Signals) => { + if (cleanedUp) return; + cleanedUp = true; + try { + if (child.exitCode === null && !child.killed) { + child.kill(parentSignal ?? "SIGTERM"); + } + } catch { + // Best effort; child may already be gone. + } + }; + process.once("exit", () => terminateChild()); + for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"] as const) { + process.once(sig, () => { + terminateChild(sig); + process.exit(sig === "SIGINT" ? 130 : sig === "SIGTERM" ? 143 : 129); + }); + } + // Wait for engine to be ready await waitForEngineHealth(); diff --git a/rivetkit-typescript/packages/rivetkit/src/inspector/utils.ts b/rivetkit-typescript/packages/rivetkit/src/inspector/utils.ts index 1b78fcd453..06c49050c6 100644 --- a/rivetkit-typescript/packages/rivetkit/src/inspector/utils.ts +++ b/rivetkit-typescript/packages/rivetkit/src/inspector/utils.ts @@ -22,11 +22,18 @@ export const secureInspector = (config: RegistryConfig) => export function getInspectorUrl( config: RegistryConfig, - managerPort: number, + httpPort: number, ): string | undefined { if (!config.inspector.enabled) return undefined; + // Prefer the engine endpoint for the inspector URL when we know the engine + // serves the UI locally. The engine always hosts `/ui/` on its own port + // (6420 by default) so pointing users at the engine URL keeps the + // inspector discoverable at the standard Rivet port even when the + // local RivetKit HTTP server runs on a different port (e.g. 8080). const base = - config.inspector.defaultEndpoint ?? `http://127.0.0.1:${managerPort}`; + config.inspector.defaultEndpoint ?? + config.endpoint ?? + `http://127.0.0.1:${httpPort}`; return new URL("/ui/", base).href; } diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts b/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts index 49ffe5f6be..d6f1fe5320 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts @@ -19,7 +19,7 @@ import { isDev, } from "@/utils/env-vars"; import { EnvoyConfigSchema } from "./envoy"; -import { ServerlessConfigSchema } from "./serverless"; +import { ConfigurePoolSchema, ServerlessConfigSchema } from "./serverless"; export const ActorsSchema = z.record( z.string(), @@ -106,16 +106,16 @@ export const RegistryConfigSchema = z // TODO: // client: ClientConfigSchema.optional(), - // MARK: Manager + // MARK: HTTP server /** - * Whether to start the local RivetKit server. + * Whether to start the local RivetKit HTTP server. * Auto-determined based on endpoint and NODE_ENV if not specified. */ - serveManager: z.boolean().optional(), + serveHttp: z.boolean().optional(), /** * Directory to serve static files from. * - * When set, the local RivetKit server will serve static files from this + * When set, the local RivetKit HTTP server will serve static files from this * directory. This is used by `registry.start()` to serve a frontend * alongside the actor API. */ @@ -127,33 +127,85 @@ export const RegistryConfigSchema = z * For example, if the base path is `/foo`, then the route `/actors` * will be available at `/foo/actors`. */ - managerBasePath: z.string().optional().default("/"), + httpBasePath: z.string().optional().default("/"), /** * @experimental * - * What port to run the manager on. + * Port for the local RivetKit HTTP server. Defaults to 8080 so it never + * collides with the local engine (fixed on 6420). */ - managerPort: z.number().optional().default(6420), + httpPort: z.number().optional().default(8080), /** * @experimental * - * What host to bind the local RivetKit server to. + * What host to bind the local RivetKit HTTP server to. */ - managerHost: z.string().optional(), + httpHost: z.string().optional(), /** @experimental */ inspector: InspectorConfigSchema, + // MARK: Runtime mode + /** + * Deployment mode for this registry. Governs whether the user app + * runs an HTTP server (`serverless`) or connects out to the engine + * as an envoy (`envoy`). + * + * When omitted the mode is derived from the environment via the + * decision matrix: + * + * | Default | NODE_ENV=prod | RIVET_ENDPOINT≠null | mode=envoy override + * spawn_engine | y | error if no | n | n + * | | RIVET_ENDPOINT| | + * mode | envoy | serverless | serverless | envoy + * + * Mode-specific options (e.g. `configurePool`, `publicEndpoint`) + * live inside this block and are type-narrowed per mode. + */ + runtime: z + .discriminatedUnion("mode", [ + z.object({ + mode: z.literal("envoy"), + /** + * When set, `registry.start()` spawns a local engine on the + * default port. Defaults are derived from the mode matrix. + */ + spawnEngine: z.boolean().optional(), + engineVersion: z.string().optional(), + poolName: z.string().optional(), + envoyKey: z.string().optional(), + version: z.number().optional(), + }), + z.object({ + mode: z.literal("serverless"), + spawnEngine: z.boolean().optional(), + engineVersion: z.string().optional(), + poolName: z.string().optional(), + envoyKey: z.string().optional(), + version: z.number().optional(), + configurePool: ConfigurePoolSchema, + basePath: z.string().optional(), + publicEndpoint: z.string().optional(), + publicToken: z.string().optional(), + }), + ]) + .optional(), + // MARK: Runtime-specific + /** @deprecated Use `runtime` with `mode: "serverless"` instead. */ serverless: ServerlessConfigSchema.optional().default(() => ServerlessConfigSchema.parse({}), ), + /** @deprecated Use `runtime` with `mode: "envoy"` instead. */ envoy: EnvoyConfigSchema.optional().default(() => EnvoyConfigSchema.parse({}), ), }) .transform((config, ctx) => { const isDevEnv = isDev(); + const isProduction = + typeof process !== "undefined" && + process.env.NODE_ENV === "production"; // Parse endpoint string (env var fallback is applied via transform above) const parsedEndpoint = config.endpoint @@ -165,10 +217,44 @@ export const RegistryConfigSchema = z }) : undefined; - if (parsedEndpoint && config.serveManager) { + // Resolve runtime mode when the user didn't explicitly set + // `runtime`. Localdev Just-Works as envoy (engine spawns locally); + // only `NODE_ENV=production` flips the auto-default to serverless. + // `RIVET_ENDPOINT` alone does NOT force serverless — an envoy-mode + // app can still connect to a remote engine. Users who want + // serverless in dev must pass `runtime: { mode: "serverless" }`. + if (config.runtime === undefined) { + if (isProduction && !parsedEndpoint) { + ctx.addIssue({ + code: "custom", + path: ["runtime"], + message: + "rivetkit: NODE_ENV=production requires RIVET_ENDPOINT " + + "(or an explicit `endpoint` config) to connect to a " + + "hosted engine. Set the env var, or pass " + + "`runtime: { mode: \"envoy\" }` to opt out of the " + + "prod serverless default.", + }); + } + config.runtime = isProduction + ? { mode: "serverless", configurePool: undefined } + : { mode: "envoy" }; + } + + // Normalize spawnEngine: `runtime.spawnEngine` is the canonical + // location. Fall through to the legacy `serverless.spawnEngine` + // field for back-compat so existing callers keep working during + // the migration away from rooting spawn config under `serverless`. + const spawnEngine = + config.runtime.spawnEngine ?? config.serverless.spawnEngine; + if (spawnEngine !== undefined) { + config.serverless.spawnEngine = spawnEngine; + } + + if (parsedEndpoint && config.serveHttp) { ctx.addIssue({ code: "custom", - message: "cannot specify both endpoint and serveManager", + message: "cannot specify both endpoint and serveHttp", }); } @@ -224,14 +310,14 @@ export const RegistryConfigSchema = z }); } - // Determine serveManager: default to true in dev mode without endpoint, false otherwise - const serveManager = config.serveManager ?? (isDevEnv && !endpoint); + // Determine serveHttp: default to true in dev mode without endpoint, false otherwise + const serveHttp = config.serveHttp ?? (isDevEnv && !endpoint); - // In dev mode, fall back to 127.0.0.1 if serving manager + // In dev mode, fall back to 127.0.0.1 if serving the local HTTP server const publicEndpoint = parsedPublicEndpoint?.endpoint ?? - (isDevEnv && (serveManager || config.serverless.spawnEngine) - ? `http://127.0.0.1:${config.managerPort}` + (isDevEnv && (serveHttp || config.serverless.spawnEngine) + ? `http://127.0.0.1:${config.httpPort}` : undefined); // We extract publicNamespace to validate that it matches the backend // namespace (see validation above), not for functional use. @@ -239,7 +325,7 @@ export const RegistryConfigSchema = z const publicToken = parsedPublicEndpoint?.token ?? config.serverless.publicToken; - // If endpoint is set or spawning engine, we'll use engine driver - disable manager inspector + // If endpoint is set or spawning engine, we'll use engine driver - disable HTTP server inspector const willUseEngine = !!endpoint || config.serverless.spawnEngine; const inspector = willUseEngine ? { @@ -253,7 +339,7 @@ export const RegistryConfigSchema = z endpoint, namespace, token, - serveManager, + serveHttp, publicEndpoint, publicNamespace, publicToken, @@ -497,26 +583,26 @@ export const DocRegistryConfigSchema = z .describe( "Additional headers to include in requests to Rivet Engine.", ), - serveManager: z + serveHttp: z .boolean() .optional() .describe( - "Whether to start the local RivetKit server. Auto-determined based on endpoint and NODE_ENV if not specified.", + "Whether to start the local RivetKit HTTP server. Auto-determined based on endpoint and NODE_ENV if not specified.", ), publicDir: z .string() .optional() .describe( - "Directory to serve static files from. When set, the local RivetKit server serves static files alongside the actor API. Used by registry.start().", + "Directory to serve static files from. When set, the local RivetKit HTTP server serves static files alongside the actor API. Used by registry.start().", ), - managerBasePath: z + httpBasePath: z .string() .optional() .describe("Base path for the local RivetKit API. Default: '/'"), - managerPort: z + httpPort: z .number() .optional() - .describe("Port to run the manager on. Default: 6420"), + .describe("Port for the local RivetKit HTTP server. Default: 8080"), inspector: DocInspectorConfigSchema, serverless: DocServerlessConfigSchema.optional(), envoy: DocEnvoyConfigSchema.optional(), diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/index.ts b/rivetkit-typescript/packages/rivetkit/src/registry/index.ts index 8528f32e0c..289f819b32 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/index.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/index.ts @@ -5,6 +5,7 @@ import { type RegistryConfigInput, RegistryConfigSchema, } from "./config"; +import { logger } from "./log"; export type FetchHandler = ( request: Request, @@ -33,16 +34,24 @@ export class Registry { constructor(config: RegistryConfigInput) { this.#config = config; - // Start the local runtime or engine before /api/rivet is hit so clients can - // reach the public endpoint preemptively. This waits one tick because some - // integrations mutate registry config immediately after setup() returns. - if (config.serverless?.spawnEngine || config.serveManager) { + // Start the local runtime or engine before /api/rivet is hit so + // clients can reach the public endpoint preemptively. This waits + // one tick because some integrations mutate registry config + // immediately after setup() returns. + // + // Check both the canonical `runtime.spawnEngine` location and + // the legacy `serverless.spawnEngine` so either config shape + // triggers the preemptive runtime. + const willSpawnEngine = !!( + config.runtime?.spawnEngine ?? config.serverless?.spawnEngine + ); + if (willSpawnEngine || config.serveHttp) { setTimeout(() => { const parsedConfig = this.parseConfig(); if ( parsedConfig.serverless.spawnEngine || - parsedConfig.serveManager + parsedConfig.serveHttp ) { // biome-ignore lint/nursery/noFloatingPromises: fire-and-forget auto-prepare this.#ensureRuntime(); @@ -121,21 +130,81 @@ export class Registry { this.#config.publicDir = "public"; } - // Force serveManager when there's no remote endpoint so the - // local runtime starts and serves the API + static files. - // When an endpoint IS configured, the config transform handles - // the mode (serveManager defaults to false, spawnEngine may be - // true, etc.) and we just start the envoy. - if (this.#config.serveManager === undefined) { - const hasEndpoint = !!( - this.#config.endpoint || - (typeof process !== "undefined" && - (process.env.RIVET_ENGINE || process.env.RIVET_ENDPOINT)) - ); - const willSpawnEngine = !!this.#config.serverless?.spawnEngine; - if (!hasEndpoint && !willSpawnEngine) { - this.#config.serveManager = true; + // Resolve the runtime mode + spawn-engine decision via this matrix: + // + // | Default | NODE_ENV=prod | RIVET_ENDPOINT!=null | mode=envoy override + // spawn_engine | y | error if no | n | n + // | | RIVET_ENDPOINT| | + // mode | envoy | serverless | serverless | envoy + // + // The user can override the mode explicitly by passing + // `runtime: { mode: "envoy" }` (or `"serverless"`) to `setup()`. + // `start()` drives the envoy path today; serverless deployments + // still call `registry.handler()` directly. + // + // TODO (pending upstream refactors): + // - dispatch `start()` to startServerless when mode=serverless + // - drop "runner" terminology + // - migrate existing `serverless.spawnEngine` callers to + // top-level `runtime.spawnEngine` (field already exists) + const runtimeCfg = this.#config.runtime; + const hasEndpoint = !!( + this.#config.endpoint || + (typeof process !== "undefined" && + (process.env.RIVET_ENGINE || process.env.RIVET_ENDPOINT)) + ); + const isProduction = + typeof process !== "undefined" && + process.env.NODE_ENV === "production"; + + // Resolve mode: explicit override wins, otherwise fall back to the + // matrix — envoy by default, only NODE_ENV=production flips the + // auto-default to serverless. RIVET_ENDPOINT alone does NOT force + // serverless; envoy mode can still connect to a remote engine. + const resolvedMode: "envoy" | "serverless" = + runtimeCfg?.mode ?? (isProduction ? "serverless" : "envoy"); + + // Resolve spawnEngine. Explicit `runtime.spawnEngine` and the + // legacy `serverless.spawnEngine` both win when set. Otherwise the + // matrix decides. In envoy mode without an endpoint we auto-spawn + // the local engine; in serverless or with an endpoint we don't. + const explicitSpawn = + runtimeCfg?.spawnEngine ?? this.#config.serverless?.spawnEngine; + if (explicitSpawn === undefined) { + if (resolvedMode === "serverless" && isProduction && !hasEndpoint) { + throw new Error( + "rivetkit: NODE_ENV=production requires RIVET_ENDPOINT " + + "(or an explicit `endpoint` config) to connect to a " + + "hosted engine.", + ); } + if (resolvedMode === "envoy" && !hasEndpoint) { + // Envoy-mode dev default: spawn the engine locally so the + // registry boots with zero config. The user app runs no + // HTTP server — the engine on 6420 is the public surface. + // Write to the canonical `runtime.spawnEngine` location; + // the schema transform normalizes it into the legacy + // `serverless.spawnEngine` field that downstream code + // still reads. + this.#config.runtime = { + ...(this.#config.runtime ?? { mode: "envoy" as const }), + spawnEngine: true, + }; + } + // All other cells leave spawnEngine undefined → schema default + // resolves to `false` (connect to remote without spawning). + } + + // `start()` drives the envoy path. When the user explicitly picks + // `mode: "serverless"` but still calls `start()`, log a hint so the + // mis-wiring is obvious — they should use `registry.handler()`. + if (resolvedMode === "serverless") { + logger().warn({ + msg: "registry.start() called with runtime.mode=serverless; " + + "serverless deployments should use `registry.handler()` " + + "to mount the /api/rivet/* fetch handler in your HTTP " + + "server instead.", + }); } // biome-ignore lint/nursery/noFloatingPromises: fire-and-forget diff --git a/rivetkit-typescript/packages/rivetkit/src/runtime-router/router.ts b/rivetkit-typescript/packages/rivetkit/src/runtime-router/router.ts index 65f93cc9a5..eee5004f2a 100644 --- a/rivetkit-typescript/packages/rivetkit/src/runtime-router/router.ts +++ b/rivetkit-typescript/packages/rivetkit/src/runtime-router/router.ts @@ -55,7 +55,7 @@ export function buildRuntimeRouter( getUpgradeWebSocket: GetUpgradeWebSocket | undefined, runtime: Runtime = "node", ) { - return createRouter(config.managerBasePath, (router) => { + return createRouter(config.httpBasePath, (router) => { // Actor gateway router.use( "*", @@ -70,7 +70,7 @@ export function buildRuntimeRouter( // GET / router.get("/", (c) => { return c.text( - "This is a RivetKit server.\n\nLearn more at https://rivet.dev", + "This is a RivetKit HTTP server.\n\nLearn more at https://rivet.dev", ); }); diff --git a/rivetkit-typescript/packages/rivetkit/src/serverless/configure.ts b/rivetkit-typescript/packages/rivetkit/src/serverless/configure.ts index 3c34ec82f0..9c7cee579d 100644 --- a/rivetkit-typescript/packages/rivetkit/src/serverless/configure.ts +++ b/rivetkit-typescript/packages/rivetkit/src/serverless/configure.ts @@ -64,11 +64,21 @@ export async function configureServerlessPool( customConfig.drainOnVersionUpgrade ?? true, metadataPollInterval: customConfig.metadataPollInterval ?? 1000, }; - await updateRunnerConfig(clientConfig, poolName, { - datacenters: Object.fromEntries( - dcsRes.datacenters.map((dc) => [dc.name, serverlessConfig]), - ), - }); + const datacenters = Object.fromEntries( + dcsRes.datacenters.map((dc) => [dc.name, serverlessConfig]), + ); + + // Register both the main pool and the native database pool so the + // `rivetkit/db` native envoy (pool name `${poolName}-native-db`) can + // register with the engine. Without the second config the engine + // rejects the native envoy's registration with `no_runner_config` + // and every DB-backed actor times out waiting to become ready. + await Promise.all([ + updateRunnerConfig(clientConfig, poolName, { datacenters }), + updateRunnerConfig(clientConfig, `${poolName}-native-db`, { + datacenters, + }), + ]); logger().info({ msg: "serverless pool configured successfully", diff --git a/rivetkit-typescript/packages/rivetkit/src/serverless/router.ts b/rivetkit-typescript/packages/rivetkit/src/serverless/router.ts index bb66730b79..41a342edc3 100644 --- a/rivetkit-typescript/packages/rivetkit/src/serverless/router.ts +++ b/rivetkit-typescript/packages/rivetkit/src/serverless/router.ts @@ -19,7 +19,7 @@ export function buildServerlessRouter(config: RegistryConfig) { // GET / router.get("/", (c) => { return c.text( - "This is a RivetKit server.\n\nLearn more at https://rivetkit.org", + "This is a RivetKit HTTP server.\n\nLearn more at https://rivetkit.org", ); }); diff --git a/rivetkit-typescript/packages/rivetkit/src/test/mod.ts b/rivetkit-typescript/packages/rivetkit/src/test/mod.ts index 7dfd137060..e1db28244a 100644 --- a/rivetkit-typescript/packages/rivetkit/src/test/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/test/mod.ts @@ -54,8 +54,12 @@ async function closeNodeServer( resolve(); }); - server.closeIdleConnections?.(); - server.closeAllConnections?.(); + const closable = server as { + closeIdleConnections?: () => void; + closeAllConnections?: () => void; + }; + closable.closeIdleConnections?.(); + closable.closeAllConnections?.(); }); } diff --git a/rivetkit-typescript/packages/rivetkit/src/utils/serve.ts b/rivetkit-typescript/packages/rivetkit/src/utils/serve.ts index 31bc22f76e..142c63c9ae 100644 --- a/rivetkit-typescript/packages/rivetkit/src/utils/serve.ts +++ b/rivetkit-typescript/packages/rivetkit/src/utils/serve.ts @@ -37,7 +37,7 @@ export async function findFreePort( export async function crossPlatformServe( config: RegistryConfig, - managerPort: number, + httpPort: number, app: Hono, runtime: Runtime = detectRuntime(), ): Promise<{ upgradeWebSocket: any; closeServer?: () => void }> { @@ -45,13 +45,13 @@ export async function crossPlatformServe( switch (runtime) { case "deno": - return serveDeno(config, managerPort, app); + return serveDeno(config, httpPort, app); case "bun": - return serveBun(config, managerPort, app); + return serveBun(config, httpPort, app); case "node": - return serveNode(config, managerPort, app); + return serveNode(config, httpPort, app); default: - return serveNode(config, managerPort, app); + return serveNode(config, httpPort, app); } } @@ -87,7 +87,7 @@ export async function loadRuntimeServeStatic( async function serveNode( config: RegistryConfig, - managerPort: number, + httpPort: number, app: Hono, ): Promise<{ upgradeWebSocket: any; closeServer: () => void }> { // Import @hono/node-server using string variable to prevent static analysis @@ -130,8 +130,8 @@ async function serveNode( }); // Start server - const port = managerPort; - const hostname = config.managerHost; + const port = httpPort; + const hostname = config.httpHost; const server = serve({ fetch: app.fetch, port, hostname }, () => logger().info({ msg: "server listening", port, hostname }), ); @@ -146,7 +146,7 @@ async function serveNode( async function serveDeno( config: RegistryConfig, - managerPort: number, + httpPort: number, app: Hono, ): Promise<{ upgradeWebSocket: any }> { // Import hono/deno using string variable to prevent static analysis @@ -166,8 +166,8 @@ async function serveDeno( process.exit(1); } - const port = config.managerPort; - const hostname = config.managerHost; + const port = config.httpPort; + const hostname = config.httpHost; // Use Deno.serve Deno.serve({ port, hostname }, app.fetch); @@ -178,7 +178,7 @@ async function serveDeno( async function serveBun( config: RegistryConfig, - managerPort: number, + httpPort: number, app: Hono, ): Promise<{ upgradeWebSocket: any }> { // Import hono/bun using string variable to prevent static analysis @@ -200,8 +200,8 @@ async function serveBun( const { websocket, upgradeWebSocket } = createBunWebSocket(); - const port = config.managerPort; - const hostname = config.managerHost; + const port = config.httpPort; + const hostname = config.httpHost; // Use Bun.serve // @ts-expect-error - Bun global diff --git a/rivetkit-typescript/packages/rivetkit/tests/registry-constructor.test.ts b/rivetkit-typescript/packages/rivetkit/tests/registry-constructor.test.ts index a7cd94422f..455d7c7cea 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/registry-constructor.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/registry-constructor.test.ts @@ -47,10 +47,10 @@ describe("Registry constructor", () => { test("reads config mutations made before the prestart tick", async () => { const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); const initialTimeoutCalls = setTimeoutSpy.mock.calls.length; - let managerPort: number | undefined; + let httpPort: number | undefined; vi.spyOn(Runtime, "create").mockImplementation(async (registry) => { - managerPort = registry.parseConfig().managerPort; + httpPort = registry.parseConfig().httpPort; return createMockRuntime(); }); @@ -59,14 +59,14 @@ describe("Registry constructor", () => { use: { test: testActor, }, - serveManager: true, + serveHttp: true, }); - registry.config.managerPort = 7777; + registry.config.httpPort = 7777; expect(setTimeoutSpy.mock.calls).toHaveLength(initialTimeoutCalls + 1); await vi.runAllTimersAsync(); - expect(managerPort).toBe(7777); + expect(httpPort).toBe(7777); }); });