env-runner
Advanced tools
@@ -53,2 +53,3 @@ import { EnvRunner, RunnerMessageListener, WorkerAddress, WorkerHooks } from "./types.mjs"; | ||
| get ready(): boolean; | ||
| get address(): WorkerAddress | undefined; | ||
| fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>; | ||
@@ -55,0 +56,0 @@ upgrade(context: { |
@@ -26,2 +26,5 @@ import { resolveVirtualModules } from "./virtual-loader.mjs"; | ||
| } | ||
| get address() { | ||
| return this._address; | ||
| } | ||
| async fetch(input, init) { | ||
@@ -33,3 +36,7 @@ for (let i = 0; i < 5 && !this._address && !this.closed; i++) await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i))); | ||
| async upgrade(context) { | ||
| if (!this.ready || !this._address) return; | ||
| if (!this.ready) await this.waitForReady().catch(() => {}); | ||
| if (!this.ready || !this._address) { | ||
| context.node.socket.destroy(); | ||
| return; | ||
| } | ||
| try { | ||
@@ -45,3 +52,3 @@ await proxyUpgrade(this._address, context.node.req, context.node.socket, context.node.head); | ||
| } | ||
| waitForReady(timeout = 5e3) { | ||
| waitForReady(timeout = 15e3) { | ||
| if (this.ready) return Promise.resolve(); | ||
@@ -48,0 +55,0 @@ if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Runner closed before becoming ready")); |
| import { watch } from "node:fs"; | ||
| async function createRunnerWSProxyPlugin(getRunner) { | ||
| const isBun = "Bun" in globalThis; | ||
| const isDeno = "Deno" in globalThis; | ||
| if (!isBun && !isDeno) return (server) => { | ||
| server.ready().then(() => { | ||
| (server.node?.server)?.on("upgrade", (req, socket, head) => { | ||
| getRunner()?.upgrade?.({ node: { | ||
| req, | ||
| socket, | ||
| head | ||
| } }); | ||
| }); | ||
| }).catch(() => {}); | ||
| }; | ||
| const { createWebSocketProxy } = await import("crossws"); | ||
| const { plugin } = isBun ? await import("crossws/server/bun") : await import("crossws/server/deno"); | ||
| const proxy = createWebSocketProxy({ target: async (peer) => { | ||
| await getRunner()?.waitForReady?.().catch(() => {}); | ||
| return resolveWSProxyTarget(getRunner()?.address, peer.request.url); | ||
| } }); | ||
| return plugin({ resolve: () => proxy }); | ||
| } | ||
| function resolveWSProxyTarget(address, requestUrl) { | ||
| if (!address) throw new Error("env runner worker is not ready"); | ||
| const { pathname, search } = new URL(requestUrl); | ||
| if (address.socketPath) return `ws+unix://${address.socketPath}:${pathname}${search}`; | ||
| if (!address.port) throw new Error("env runner worker is not ready"); | ||
| const host = address.host || "127.0.0.1"; | ||
| return `ws://${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${address.port}${pathname}${search}`; | ||
| } | ||
| var RunnerManager = class { | ||
@@ -12,2 +42,3 @@ _runner; | ||
| _readyListeners = /* @__PURE__ */ new Set(); | ||
| _readyRejectors = /* @__PURE__ */ new Set(); | ||
| constructor(runner) { | ||
@@ -22,2 +53,5 @@ if (runner) this._attach(runner); | ||
| } | ||
| get address() { | ||
| return this._runner?.address; | ||
| } | ||
| async reload(runner) { | ||
@@ -54,5 +88,13 @@ this._reloading = true; | ||
| } | ||
| upgrade = (context) => { | ||
| this._runner?.upgrade?.(context); | ||
| upgrade = async (context) => { | ||
| const runner = await this._waitForRunner(); | ||
| if (!runner?.upgrade) { | ||
| context.node.socket.destroy(); | ||
| return; | ||
| } | ||
| await runner.upgrade(context); | ||
| }; | ||
| wsSrvxPlugin() { | ||
| return createRunnerWSProxyPlugin(() => this); | ||
| } | ||
| sendMessage(message) { | ||
@@ -73,7 +115,13 @@ if (!this._runner || !this._runner.ready) { | ||
| } | ||
| waitForReady(timeout = 5e3) { | ||
| waitForReady(timeout = 15e3) { | ||
| if (this.ready) return Promise.resolve(); | ||
| if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("Runner closed before becoming ready")); | ||
| return new Promise((resolve, reject) => { | ||
| const cleanup = () => { | ||
| clearTimeout(timer); | ||
| this.offMessage(listener); | ||
| this._readyRejectors.delete(onClose); | ||
| }; | ||
| const timer = setTimeout(() => { | ||
| this._messageListeners.delete(listener); | ||
| cleanup(); | ||
| reject(/* @__PURE__ */ new Error("Runner did not become ready in time")); | ||
@@ -83,8 +131,12 @@ }, timeout); | ||
| if (message?.address || this.ready) { | ||
| clearTimeout(timer); | ||
| this._messageListeners.delete(listener); | ||
| cleanup(); | ||
| resolve(); | ||
| } | ||
| }; | ||
| this._messageListeners.add(listener); | ||
| const onClose = () => { | ||
| cleanup(); | ||
| reject(/* @__PURE__ */ new Error("Runner closed before becoming ready")); | ||
| }; | ||
| this.onMessage(listener); | ||
| this._readyRejectors.add(onClose); | ||
| }); | ||
@@ -132,2 +184,4 @@ } | ||
| this._messageQueue.length = 0; | ||
| for (const rejectReady of this._readyRejectors) rejectReady(); | ||
| this._readyRejectors.clear(); | ||
| const runner = this._runner; | ||
@@ -134,0 +188,0 @@ this._detach(); |
@@ -65,2 +65,4 @@ import { IncomingMessage } from "node:http"; | ||
| readonly closed: boolean; | ||
| /** Address the worker is listening at, once ready (`undefined` before then). */ | ||
| readonly address?: WorkerAddress; | ||
| /** Proxy an HTTP request to the worker. */ | ||
@@ -67,0 +69,0 @@ fetch: FetchHandler; |
@@ -47,3 +47,5 @@ import { NodeWorkerEnvRunner } from "./node-worker-runner.mjs"; | ||
| function generateVercelId() { | ||
| return `dev1::${_podId}-${Date.now().toString(36)}-${randomBytes(6).toString("hex")}`; | ||
| const timestamp = Date.now().toString(36); | ||
| const random = randomBytes(6).toString("hex"); | ||
| return `dev1::${_podId}-${timestamp}-${random}`; | ||
| } | ||
@@ -50,0 +52,0 @@ var VercelEnvRunner = class extends NodeWorkerEnvRunner { |
+2
-8
@@ -53,12 +53,6 @@ import { EnvServer } from "./_chunks/server.mjs"; | ||
| gracefulShutdown: false, | ||
| fetch: (request) => envServer.fetch(request) | ||
| fetch: (request) => envServer.fetch(request), | ||
| plugins: [await envServer.wsSrvxPlugin()] | ||
| }); | ||
| await server.ready(); | ||
| server.node?.server?.on("upgrade", (req, socket, head) => { | ||
| envServer.upgrade({ node: { | ||
| req, | ||
| socket, | ||
| head | ||
| } }); | ||
| }); | ||
| for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, async () => { | ||
@@ -65,0 +59,0 @@ console.log(`\n\x1B[2mShutting down...\x1B[0m`); |
+11
-1
@@ -7,3 +7,3 @@ import { EnvRunner, FetchHandler, NodeUpgradeContext, RPCOptions, RunnerMessageListener, RunnerRPCHooks, UpgradeContext, UpgradeHandler, WorkerAddress, WorkerHooks } from "./_chunks/types.mjs"; | ||
| import { NetlifyEnvRunner } from "./_chunks/netlify-runner.mjs"; | ||
| import { ServerOptions } from "srvx"; | ||
| import { ServerOptions, ServerPlugin } from "srvx"; | ||
| import { Hooks } from "crossws"; | ||
@@ -24,5 +24,7 @@ /** | ||
| private _readyListeners; | ||
| private _readyRejectors; | ||
| constructor(runner?: EnvRunner); | ||
| get ready(): boolean; | ||
| get closed(): boolean; | ||
| get address(): WorkerAddress | undefined; | ||
| /** | ||
@@ -46,2 +48,10 @@ * Replace the active runner with a new one. Closes the previous runner. | ||
| upgrade: UpgradeHandler; | ||
| /** | ||
| * Create a runtime-native WebSocket reverse-proxy plugin for the public srvx | ||
| * server. Attach it via `serve({ plugins: [await manager.wsSrvxPlugin()] })`: | ||
| * on Node it proxies the raw upgrade socket to the worker, and on Bun/Deno it | ||
| * bridges the WebSocket with crossws. The plugin reads the active runner | ||
| * lazily, so it keeps working across hot-reloads. | ||
| */ | ||
| wsSrvxPlugin(): Promise<ServerPlugin>; | ||
| sendMessage(message: unknown): void; | ||
@@ -48,0 +58,0 @@ onMessage(listener: RunnerMessageListener): void; |
+4
-4
| { | ||
| "name": "env-runner", | ||
| "version": "0.1.15", | ||
| "version": "0.1.16", | ||
| "description": "Generic environment runner for JavaScript runtimes.", | ||
@@ -46,6 +46,6 @@ "license": "MIT", | ||
| "dependencies": { | ||
| "crossws": "^0.4.6", | ||
| "crossws": "^0.4.8", | ||
| "exsolve": "^1.1.0", | ||
| "httpxy": "^0.5.3", | ||
| "srvx": "^0.11.17" | ||
| "httpxy": "^0.5.4", | ||
| "srvx": "^0.11.19" | ||
| }, | ||
@@ -52,0 +52,0 @@ "devDependencies": { |
+29
-1
@@ -76,5 +76,32 @@ # env-runner | ||
| fetch: (request) => envServer.fetch(request), | ||
| // Proxy WebSocket upgrades to the worker (see "WebSocket proxying" below) | ||
| plugins: [await envServer.wsSrvxPlugin()], | ||
| }); | ||
| ``` | ||
| #### WebSocket proxying | ||
| To proxy WebSocket upgrades to the worker, attach the plugin returned by | ||
| `wsSrvxPlugin()` (available on both `RunnerManager` and `EnvServer`) to your | ||
| [srvx](https://srvx.h3.dev) server: | ||
| ```ts | ||
| const server = serve({ | ||
| fetch: (request) => envServer.fetch(request), | ||
| plugins: [await envServer.wsSrvxPlugin()], | ||
| }); | ||
| ``` | ||
| The plugin picks the proxy strategy by **host** runtime: | ||
| - **Node** — proxies the raw upgrade socket to the worker (transparent | ||
| passthrough; subprotocol/extension negotiation stays end-to-end). | ||
| - **Bun/Deno** — those runtimes serve natively and expose no Node upgrade | ||
| socket, so the client WebSocket is terminated with [crossws](https://crossws.h3.dev) | ||
| and bridged to the worker over a standard `WebSocket` client. | ||
| It reads the active runner lazily, so it keeps working across hot-reloads, and | ||
| waits for the worker to become ready before proxying. Your entry module should | ||
| expose WebSocket hooks via the `websocket` field (see [Workers](#workers)). | ||
| ### Manager (`RunnerManager`) | ||
@@ -151,3 +178,4 @@ | ||
| // Proxy WebSocket upgrades | ||
| // Proxy a raw WebSocket upgrade to the worker (Node host only — low-level; | ||
| // prefer `manager.wsSrvxPlugin()` for cross-runtime proxying) | ||
| runner.upgrade?.({ node: { req, socket, head } }); | ||
@@ -154,0 +182,0 @@ |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
205908
1.99%3146
1.85%656
4.46%Updated
Updated
Updated