@setu-ts/runtime
Each adapter supplies IRuntimeServices.reportError: one JSON line to stderr on Node, Bun and Deno,
or the platform log channel on Workers. Serialization and write failures never escape. The kernel
and errorHandler use it only without a registered logger, for unhandled errors and handled 5xx
respectively; handled 4xx stays silent without a logger.
RuntimePlugin and runtime adapters providing IRuntimeServices for Node.js, Deno, Bun, and
Cloudflare Workers.
This package implements the runtime-independence seam: every runtime-specific operation the
framework needs is abstracted behind IRuntimeServices (defined in @setu-ts/common) and provided
here under the CAPABILITIES.RUNTIME token by the RuntimePlugin. No other package ever touches
process, Deno, Bun, or node:/deno:/bun: modules directly.
Installation
# npm (pnpm add / yarn add / bun add work the same way)
npm install @setu-ts/runtime
# Deno
deno add npm:@setu-ts/runtime
Local diagnostics listener
RuntimePlugin provides CAPABILITIES.LOCAL_DIAGNOSTICS_LISTENER for an explicitly activated local
diagnostics connector on Deno and Node. It binds one additional 127.0.0.1 port (1024–65535), keeps
the application listener separate, and exposes only an idempotent close() handle. Bun and Workers
refuse before binding.
Node uses node:http directly, preserves raw request-target spellings, checks duplicate singleton
header lines, and applies explicit parser/time limits and a 32-connection cap. Parser errors answer
a fixed bare 400 with no body. A port conflict rejects startup; shutdown destroys open
connections. See
the protocol
for the refusal families and trust limits.
What’s Inside
| Area | Exports |
|---|---|
| Plugin | RuntimePlugin, RuntimeOptions, HttpAdapterOptions, RequestBodyTooLargeError |
| Detection | detectRuntime, GlobalScope |
| Deno adapter | createDenoRuntimeServices, DenoHost, DenoFileInfo, DenoDirEntry |
| Node adapter | createNodeRuntimeServices, NodeHost, NodeFsInfo |
| Bun adapter | createBunRuntimeServices, BunHost, BunFileInfo |
| Workers adapter | createCloudflareRuntimeServices, CloudflareRuntimeOptions |
| HTTP adapters | DenoHttpAdapter, NodeHttpAdapter, BunHttpAdapter, CloudflareWorkersHttpAdapter |
Usage
Register the plugin in every application — it is mandatory:
import { createApplication } from '@setu-ts/kernel';
import { RuntimePlugin } from '@setu-ts/runtime';
const app = createApplication({
plugins: [RuntimePlugin()],
});
app.router.get('/info', (ctx) => {
const runtime = ctx.services.get<IRuntimeServices>(CAPABILITIES.RUNTIME);
return ctx.response.json({
platform: runtime.platform(),
requestId: runtime.uuid(),
});
});
await app.start({ port: 3000 });
Force a specific platform (useful for testing):
RuntimePlugin({ platform: 'node' });
Options
| Option | Type | Default | Description |
|---|---|---|---|
platform | RuntimePlatform | detectRuntime() | Force a platform instead of auto-detecting. |
env | Record<string, unknown> | — | Cloudflare Workers env. Only its string entries populate runtime.env. |
adapters | RuntimeAdapterFactories | — | Internal: override runtime adapter factories. |
httpAdapters | HttpAdapterFactories | built-in four | Internal: override HTTP adapter factories. |
maxBodyBytes | number | — (unbounded) | Cap on the request-body read, enforced where the body is consumed. |
Bounding the request body
RuntimePlugin({ maxBodyBytes: 10 * 1024 * 1024 });
Omitted, the read is unbounded — the released behaviour, byte for byte.
This is the layer no request header can switch off.
HttpSecurityPlugin({ requestSize: { maxBodySize } }) refuses on a declared Content-Length
before anything is read, which is cheaper and reports earlier; a chunked request declares no
length, and since the body read became lazy it happens inside the handler, after every middleware
has returned. So the middleware bounds declared lengths and this option bounds the read itself.
There are two knobs because the request mapping runs before any plugin and there is no channel
between them: mapWebRequestToFrameworkRequest receives a Request and nothing else. Set both, and
set maxBodyBytes to the same value as maxBodySize or higher.
A body past the cap rejects with RequestBodyTooLargeError, branded with a 413 HTTP status hint,
so an application running errorHandler answers 413 Payload Too Large in its configured format
rather than a masked 500. With no errorHandler registered the brand has no reader and the
kernel’s opaque 500 answers instead.
0 refuses every request that carries a body — it does NOT disable the check, which is what 0
means for maxDepth/maxNodes/maxBatchSize elsewhere in this framework. Omitting the option is
the one way to say unbounded. A value that is not a non-negative integer throws at
RuntimePlugin(...); NaN in particular would make every comparison against the cap false and
silently disable the bound, and Number() of an unset environment variable is exactly NaN.
For in-process oversized-upload tests on Node 24 or Node 26 (up to 26.12.0), build the multipart
request body from a Blob or use inject() instead of
app.fetch(new Request(..., { body: formData })) with a file. Those releases bundle undici
7.20.0–8.11.2, whose FormData request body throws an uncaught ERR_INVALID_STATE when the runtime
cancels it after the cap is exceeded; undici 8.12.0 fixes it, and no released Node bundles that yet.
Node 22, Bun and Deno are unaffected, as are real-socket uploads and inject(). The runtime still
cancels oversized bodies to enforce the read bound.
Reporting a failed WebSocket handshake
HttpAdapterOptions accepts reportError?: (record: Readonly<Record<string, unknown>>) => void
(@since 0.10.0). RuntimePlugin supplies one resolved at CALL time: the registered logger’s
error, else IRuntimeServices.reportError, and a throwing logger never fails the report. The Node
adapter calls it at most once per distinct message per handle when a server-side handshake fails —
ws is missing or throws — before answering 500; Deno, Workers and Bun ignore the option.
Omitted, the refusal is silent as before.
adapters and httpAdapters are marked @internal — they exist so unit tests can run without OS
permissions or real runtime globals, not as application configuration.
env matters only on Workers: the edge has no ambient environment, so without it runtime.env is
empty and ConfigPlugin reads nothing. Object bindings (KV, R2, D1, …) are filtered out here and
published separately by CloudflarePlugin.
Architecture
Cross-runtime operations (UUID, random bytes, SubtleCrypto, now, hrtime, timers) are identical
across Node 22+, Deno, and Bun because they rely on web-standard APIs on globalThis (the
framework’s floor is Node 22; globalThis.crypto alone would have put it at 19). They are
implemented once in src/services/cross-runtime.ts.
Divergent operations (platform, version, hostname, env, exit, fs, workers, dns) are implemented
per-adapter via dependency injection: each factory accepts a *Host interface describing only what
it needs, defaulting to the real runtime global via a single boundary cast. This makes every adapter
fully unit-testable on Deno by passing a fake host — no real Node/Bun, no OS permissions.
Scope
This package provides the runtime services abstraction and the HTTP server adapters. Every
adapter implements IHttpAdapter and is registered under CAPABILITIES.HTTP_ADAPTER by
RuntimePlugin; app.start({ port }) throws when none is registered for the detected platform.
Platform coverage is not uniform, and the gaps are deliberate rather than pending: on Cloudflare
Workers fs is undefined (no edge filesystem), workers and dns are omitted, and exit()
throws — there is no process to end.
See the repository’s
PUBLIC_API.md
for the full API contract and
ARCHITECTURE.md for how this
package fits the plugin architecture.
Exports
@setu-ts/runtime
| Export | Kind |
|---|---|
adaptWsModule | function |
asUpgradeEmitter | function |
bindCloudflareSocketToSink | function |
bindDenoSocketToSink | function |
bindWsSocketToSink | function |
buildBunHost | function |
buildNodeHost | function |
createBunRuntimeServices | function |
createBunWebSocketHandlers | function |
createCloudflareRuntimeServices | function |
createDefaultCloudflareWebSocketHost | function |
createDenoDnsResolver | function |
createDenoRuntimeServices | function |
createNodeDnsResolver | function |
createNodeRuntimeServices | function |
createNodeWorkerHost | function |
createRuntimeServices | function |
createUpgradeRequest | function |
createWebSocketTransport | function |
createWebWorkerHost | function |
createWsTransport | function |
detectRuntime | function |
isWebSocketUpgradeRequest | function |
loadWsModule | function |
normalizeFrame | function |
rejectRawUpgrade | function |
RuntimePlugin | function |
toReadyState | function |
toTransportError | function |
toWsReadyState | function |
BunHttpAdapter | class |
CloudflareWorkersHttpAdapter | class |
DenoHttpAdapter | class |
NodeHttpAdapter | class |
NodeUpgradeCoordinator | class |
RequestBodyTooLargeError | class |
RpcInterceptorStore | class |
BunFileInfo | interface |
BunHost | interface |
BunModules | interface |
BunServeHost | interface |
BunServer | interface |
BunServerWebSocket | interface |
BunSocketData | interface |
BunWebSocketHandlers | interface |
CloudflareEnv | interface |
CloudflareRuntimeOptions | interface |
CloudflareServerSocket | interface |
CloudflareWebSocketHost | interface |
CloudflareWebSocketPair | interface |
CreateRuntimeServicesOptions | interface |
DenoDirEntry | interface |
DenoDnsHost | interface |
DenoFileInfo | interface |
DenoHost | interface |
DenoServeHost | interface |
DenoServer | interface |
DenoSrvRecord | interface |
DenoWebSocketLike | interface |
DenoWebSocketUpgrade | interface |
GlobalScope | interface |
HttpAdapterFactories | interface |
HttpAdapterOptions | interface |
NodeDnsModule | interface |
NodeFsInfo | interface |
NodeHost | interface |
NodeIncomingMessage | interface |
NodeModules | interface |
NodeServeHost | interface |
NodeServer | interface |
NodeWorkerLike | interface |
NodeWorkerModules | interface |
RawUpgradeSocket | interface |
RuntimeAdapterFactories | interface |
RuntimeOptions | interface |
UpgradeEmitter | interface |
WebSocketLike | interface |
WebWorkerGlobals | interface |
WebWorkerHostOptions | interface |
WebWorkerLike | interface |
WsModuleLike | interface |
WsServerLike | interface |
WsSocketLike | interface |
@setu-ts/runtime/worker
| Export | Kind |
|---|---|
defineWorkerTask | function |
Generated from the package barrel by pnpm run docs:exports; pnpm run check:docs fails when it
drifts.
API reference
RuntimePlugin() (@setu-ts/runtime)
Provides runtime-agnostic services (UUID, timers, crypto, env, HTTP server).
Registration
import { RuntimePlugin } from '@setu-ts/runtime';
app.register(
RuntimePlugin({
httpAdapter: 'auto', // 'node' | 'deno' | 'bun' | 'auto'
}),
);
Deno shutdown
Since 0.10.0, Deno adapter close() waits for the first of shutdown() and a 1000 ms grace,
catching a rejected shutdown. After the kernel’s bounded drain, shutdown and close hooks can run
while a request is still in flight. Register resource cleanup in lifecycle hooks.
Bounding the request body
maxBodyBytes caps the request-body read where the body is actually consumed:
app.register(RuntimePlugin({ maxBodyBytes: 10 * 1024 * 1024 }));
Use it with HttpSecurityPlugin({ requestSize: { maxBodySize } }): that middleware rejects a
declared Content-Length before reading, while this option bounds chunked bodies as they are read.
Set both to the same value or make maxBodyBytes larger. Omit maxBodyBytes for an unbounded read;
0 refuses every request that carries a body. A non-negative integer is required. An over-limit
read rejects with a RequestBodyTooLargeError carrying a 413 status hint, so errorHandler
answers 413 Payload Too Large in its configured format.
A component that wraps its own body read in a try must honour that hint rather than reporting its
own status: the read can now REJECT, and a catch that assumes a parse failure reports the wrong
cause and the wrong remedy. @setu-ts/graphql-plugin’s two HTTP transports answer 413 with
extensions.code: 'REQUEST_BODY_TOO_LARGE', and @setu-ts/storage-plugin’s upload middleware
serves the hint through respondWithError.
Reporting a failed WebSocket handshake
HttpAdapterOptions.reportError?: (record: Readonly<Record<string, unknown>>) => void
(@since 0.10.0) is a last-resort sink for a server-side Node handshake failure: ws is not
installed, or it throws while upgrading. RuntimePlugin supplies one resolved at call time — the
registered logger’s error, else IRuntimeServices.reportError, with a throwing logger swallowed.
The Node adapter reports
{ level: 'error', msg: 'WebSocket handshake failed', ...serializeError(cause) } at most once per
distinct message per handle, then refuses the upgrade with the same 500 and no body as before.
Deno, Workers and Bun ignore the option; omitted, the refusal is silent.
Accessing Runtime Services
app.router.get('/info', (ctx) => {
const runtime = ctx.services.get<IRuntimeServices>('runtime');
return ctx.response.json({
platform: runtime.platform(),
version: runtime.version(),
hostname: runtime.hostname(),
requestId: runtime.uuid(),
});
});
Available Runtime Services
interface IRuntimeServices {
platform(): 'node' | 'deno' | 'bun' | 'cloudflare-workers';
version(): string;
hostname(): string;
uuid(): string;
randomBytes(length: number): Uint8Array;
subtle: SubtleCrypto;
now(): number;
hrtime(): number;
setTimeout(fn: () => void, ms: number): TimerHandle;
clearTimeout(handle: TimerHandle): void;
setInterval(fn: () => void, ms: number): TimerHandle;
clearInterval(handle: TimerHandle): void;
env: Record<string, string | undefined>;
exit(code?: number): never;
fs?: IFileSystem;
workers?: IWorkerHost;
dns?: IDnsResolver;
onSignal?(signal: RuntimeSignal, handler: () => void): void;
}
type RuntimeSignal = 'SIGTERM' | 'SIGINT';
IFileSystem provides readFile/writeFile/stat/readdir/mkdir/rm, plus an optional
realPath(path): Promise<string> that canonicalizes a path following symlinks. realPath is
implemented by the Node/Deno/Bun runtime adapters and absent on runtimes that cannot canonicalize;
callers must degrade gracefully when it is not present (e.g. the React Router plugin’s static-asset
handler uses it for symlink-safe containment when available, falling back to lexical containment).
workers is an optional IWorkerHost for spawning worker threads. It is implemented by the
Node/Deno/Bun runtime adapters and absent on Cloudflare Workers (no threads on the edge).
Callers must degrade gracefully when it is not present — the WorkerPoolPlugin fails run() with a
typed WorkerPoolUnavailableError rather than throwing at startup.
interface IWorkerHost {
spawn(specifier: string): IWorkerHandle;
availableParallelism(): number;
// Answerable BEFORE a worker exists, which is why it sits on the host: a
// consumer that must warn about undetectable worker death needs to know at
// registration time. A host that omits it is treated as reporting `false`.
reportsExit?(): boolean;
}
interface IWorkerHandle {
postMessage(message: unknown): void;
onMessage(listener: (message: unknown) => void): void;
onError(listener: (error: Error) => void): void;
// The worker's THREAD ENDING, however it ended — a clean self-termination
// included, which raises no error at all. `code` is `null` when the runtime
// ends the worker without reporting one, and the listener also fires for a
// host-requested `terminate()` on runtimes that implement it that way.
onExit?(listener: (code: number | null) => void): void;
terminate(): Promise<void>;
}
onExit/reportsExit are omitted, not no-ops, on a runtime that cannot report a worker exit:
their absence means “this runtime cannot tell me a worker died”, never “no worker has died”. The
Node host implements both over node:worker_threads’ 'exit'; the shared web-worker host does when
its runtime names an event that fires, which Bun does ('close') and Deno does not — measured,
Deno’s Worker emits nothing on self.close() and a later postMessage still resolves.
dns is an optional IDnsResolver for name resolution. It is implemented by the Node, Deno,
and Bun runtime adapters and absent on Cloudflare Workers, whose network access is fetch —
that resolves names internally and exposes no lookup surface. Callers must degrade gracefully when
it is not present; the ServiceDiscoveryPlugin’s 'dns' provider throws a typed
DiscoveryUnavailableError during register(), naming the alternatives.
| Member | Node | Deno | Bun | Workers |
|---|---|---|---|---|
fs | ✅ | ✅ | ✅ | ❌ |
workers | ✅ | ✅ | ✅ | ❌ |
dns | ✅ | ✅ | ✅ | ❌ |
onSignal | ✅ | ✅ (not Win32) | ✅ | ❌ |
onSignal registers a process-termination handler so an application can run app.stop() — its
shutdown hooks, discovery deregistration, and broker disconnects — before the process dies. Handlers
are additive and are never removed: a process that received a termination signal is ending.
It is the one member absent for two unrelated reasons, and a caller must treat both the same way
(runtime.onSignal?.(...)). Cloudflare Workers omits it because an isolate is evicted rather than
signalled, so there is nothing to register for. The Deno adapter omits it on Windows, where
registering for SIGTERM throws rather than no-opping — so the presence of the key means “this
runtime can register a handler”, never “this platform raises signals”. A no-op was rejected
deliberately: it would let a caller conclude its shutdown handler runs when it never would.
interface IDnsResolver {
resolveSrv(hostname: string): Promise<readonly SrvRecord[]>;
resolveHost(hostname: string): Promise<readonly string[]>;
}
interface SrvRecord {
readonly host: string;
readonly port: number;
readonly priority: number;
readonly weight: number;
}
SrvRecord.host is deliberately named that rather than reusing a runtime’s own spelling: Deno calls
the field target and Node calls it name, and passing either through unchanged would type-check
on both runtimes while producing undefined hostnames on one. resolveHost concatenates A and
AAAA results and rejects only when both families fail, because an IPv4-only host has no AAAA
record at all. The runtime package exports both resolver factories: createNodeDnsResolver(dns?)
(shared by the Node and Bun adapters, over node:dns/promises) and createDenoDnsResolver(host)
(over Deno.resolveDns).
The runtime package also exports the worker host factories createWebWorkerHost(globals?)
(Deno/Bun, over the web Worker API) and createNodeWorkerHost(mods?) (Node, over
node:worker_threads), each behind an injectable seam, plus a @setu-ts/runtime/worker subpath
whose sole export is defineWorkerTask (see the WorkerPoolPlugin section).
API Reference: @setu-ts/runtime
RuntimePlugin and runtime adapters providing IRuntimeServices for Node.js, Deno, Bun, and
Cloudflare Workers.
M23 replaced the old HTTP server adapters with the new
IHttpAdaptercontract (setHandler/fetch/listen/close) and added the Cloudflare Workers adapter.
M87 widened two of those members to accept and return sync-or-async —
setHandlernow takes(request: IRequest) => IResponse | Promise<IResponse>andfetchreturnsResponse | Promise<Response>. This is breaking for an out-of-repo adapter: the handler’s return type sits in a contravariant position, so an implementation declaring the narrowerPromise-only shape is no longer a faithful implementor (TypeScript’s bivariant method parameters mean it still compiles — the break surfaces when a handler that answers synchronously reaches it). Callers are unaffected, sinceawaitworks on both.The widening exists because a request the kernel can answer without awaiting must not be wrapped back into a promise:
@hono/node-serverserves a response through its synchronous fast path only when the handler returns a non-promise, so a single eagerly-async link foreclosed that path for every request. An adapter implementing this contract should branch on promise-ness rather than awaiting:const result = this.handler(frameworkRequest); return isPromiseLike(result) ? Promise.resolve(result).then(finish) : finish(result);
Values (runtime exports)
| Export | Kind | Purpose |
|---|---|---|
RuntimePlugin | function | Creates the runtime plugin (registers CAPABILITIES.RUNTIME) |
detectRuntime | function | Detects the current runtime platform ('node' | 'deno' | 'bun' | 'cloudflare-workers') |
createRuntimeServices | function | Creates IRuntimeServices for the detected platform, without an application |
buildNodeHost | function | Builds a NodeHost from injected NodeModules (defaults to real node: built-ins) |
buildBunHost | function | Builds a BunHost from injected BunModules (defaults to node: built-ins) |
createDenoRuntimeServices | function | Creates IRuntimeServices backed by Deno APIs |
createNodeRuntimeServices | function | Creates IRuntimeServices backed by Node.js APIs |
createBunRuntimeServices | function | Creates IRuntimeServices backed by Bun APIs |
createCloudflareRuntimeServices | function | Creates IRuntimeServices backed by Cloudflare Workers APIs (edge-compatible) |
DenoHttpAdapter | class | Deno HTTP server adapter implementing IHttpAdapter |
NodeHttpAdapter | class | Node.js HTTP server adapter implementing IHttpAdapter |
BunHttpAdapter | class | Bun HTTP server adapter implementing IHttpAdapter |
CloudflareWorkersHttpAdapter | class | Cloudflare Workers HTTP adapter implementing IHttpAdapter (fetch-only, no listen) |
createWebWorkerHost | function | Creates an IWorkerHost over the web Worker API (Deno/Bun); throws if Worker absent |
createNodeWorkerHost | function | Creates an IWorkerHost over node:worker_threads |
defineWorkerTask | function | @setu-ts/runtime/worker subpath. Registers a worker module’s task handler |
isDenoHttpServerHandle | function | Type guard for DenoHttpServerHandle |
isNodeHttpServerHandle | function | Type guard for NodeHttpServerHandle |
isBunHttpServerHandle | function | Type guard for BunHttpServerHandle |
WebSocket upgrade support (Milestone 46) — shared primitives:
| Export | Kind | Purpose |
|---|---|---|
isWebSocketUpgradeRequest | function | Whether request headers describe an RFC 6455 upgrade (Upgrade + Connection) |
createWebSocketTransport | function | Wraps a web-API socket as an IWebSocketTransport |
normalizeFrame | function | Normalizes an inbound frame payload to string | Uint8Array |
toReadyState | function | Maps the web WebSocket numeric readyState to WebSocketReadyState |
toTransportError | function | Coerces an error-event payload into a real Error |
Per-runtime upgrade seams:
| Export | Kind | Purpose |
|---|---|---|
bindDenoSocketToSink | function | Binds a Deno.upgradeWebSocket socket’s on* handlers to a WebSocketEventSink |
createBunWebSocketHandlers | function | Builds the serve-time handler object for Bun.serve’s websocket option |
bindCloudflareSocketToSink | function | Accepts a Workers WebSocketPair server half and binds it to a sink |
createDefaultCloudflareWebSocketHost | function | Builds the Workers host from the real WebSocketPair global |
buildUpgradeResponseInit | function | Builds the Workers 101 ResponseInit carrying the client socket |
NodeUpgradeCoordinator | class | Owns the ws server for one Node adapter and performs the raw handshake |
adaptWsModule | function | Narrows an imported ws module to WsModuleLike |
loadWsModule | function | Lazily imports ws; throws with the install command if absent |
bindWsSocketToSink | function | Binds a ws socket’s events to a WebSocketEventSink |
createWsTransport | function | Wraps a ws socket as an IWebSocketTransport |
toWsReadyState | function | Maps a ws numeric ready state to WebSocketReadyState |
createUpgradeRequest | function | Rebuilds a web Request from Node’s upgrade event arguments |
asUpgradeEmitter | function | Probes a Node server handle for the raw upgrade event |
rejectRawUpgrade | function | Writes an HTTP status line to a raw socket and destroys it |
Types
| Export | Kind | Purpose |
|---|---|---|
RuntimeOptions | type | Options for RuntimePlugin (platform, env) |
CreateRuntimeServicesOptions | type | Options for createRuntimeServices (platform, adapters, env) |
RuntimeAdapterFactories | type | Platform → runtime adapter factory map |
GlobalScope | type | Injectable global scope shape for detectRuntime |
DenoHost | type | Host interface for the Deno adapter (extension point) |
DenoFileInfo | type | File info returned by DenoHost.stat() |
DenoDirEntry | type | Directory entry yielded by DenoHost.readDir() (an AsyncIterable) |
NodeHost | type | Host interface for the Node adapter (extension point) |
NodeFsInfo | type | File info returned by NodeHost.stat() |
NodeModules | type | Injectable Node built-ins for buildNodeHost (testing seam) |
BunHost | type | Host interface for the Bun adapter (extension point) |
BunFileInfo | type | File info returned by BunHost.stat() |
BunModules | type | Injectable built-ins for buildBunHost (testing seam) |
DenoHttpServerHandle | type | Internal server handle for DenoHttpAdapter |
NodeHttpServerHandle | type | Internal server handle for NodeHttpAdapter |
BunHttpServerHandle | type | Internal server handle for BunHttpAdapter |
CloudflareWorkersHttpServerHandle | type | Internal server handle for CloudflareWorkersHttpAdapter |
DenoServeHost | type | Injectable host interface for DenoHttpAdapter (extension point) |
NodeServeHost | type | Injectable host interface for NodeHttpAdapter (extension point) |
BunServeHost | type | Injectable host interface for BunHttpAdapter (extension point) |
BunServer | type | Bun server handle returned by Bun.serve |
HttpAdapterFactories | type | Platform→adapter factory map for RuntimePlugin |
WebWorkerGlobals | type | Injectable seam for createWebWorkerHost (Worker + concurrency) |
WebWorkerLike | type | Minimal web Worker shape the host consumes |
NodeWorkerModules | type | Injectable seam for createNodeWorkerHost (Worker + availableParallelism) |
NodeWorkerLike | type | Minimal worker_threads.Worker shape the host consumes |
WebSocketLike | type | Minimal web WebSocket shape the adapters drive |
DenoWebSocketLike | type | A web socket exposing the on* handler properties (Deno) |
DenoWebSocketUpgrade | type | Result shape of Deno.upgradeWebSocket ({ socket, response }) |
BunServerWebSocket | type | Minimal Bun ServerWebSocket shape, carrying data |
BunSocketData | type | Per-socket data Bun carries from upgrade() to its handlers ({ sink }) |
BunWebSocketHandlers | type | The serve-time handler object for Bun.serve’s websocket option |
CloudflareServerSocket | type | Workers server-half socket (accept() + addEventListener) |
CloudflareWebSocketPair | type | A created Workers pair ({ client, server }) |
CloudflareWebSocketHost | type | Injectable seam for the Workers upgrader (extension point) |
CloudflareUpgradeResponseInit | type | The Workers 101 response init, including the webSocket member |
WsModuleLike | type | Structural facade over the ws module ({ WebSocketServer }) |
WsServerLike | type | Structural facade over a noServer ws WebSocketServer |
WsSocketLike | type | Structural facade over a ws socket |
NodeIncomingMessage | type | Minimal node:http.IncomingMessage shape for building an upgrade Request |
RawUpgradeSocket | type | Minimal raw duplex socket shape a refusal writes to |
UpgradeEmitter | type | A server handle that emits the raw upgrade event |
Contract notes:
-
The Bun and Deno host defaults are backed by real APIs (retro review, Part 2).
buildBunHost()builds the defaultBunHostfromnode:fs(sync),node:os, andnode:process— which Bun implements — NOT from members of theBunglobal: Bun’s file API isBun.file()/Bun.write(), and it has noreadFile/stat/readdir/mkdir/rm/realPath/hostname/exit. OnlyBun.versionis read from the global. On the Deno side,DenoHost.readDir()is named and shaped after the realDeno.readDir— anAsyncIterable, consumed withfor await. Both defaults are exercised against their real APIs by the test suite, not only through injected fakes. -
IRuntimeServices.envis a snapshot on Deno and Workers, taken when the services were created (Deno.env.toObject(), the per-invocation Workers bindings object), and a live pass-through ofprocess.envon Node and Bun. Nothing in the framework mutates the environment at runtime. -
M23 replaced M39’s HTTP server adapters. The
IHttpAdaptercontract now exposes the web-standardfetchentry:setHandlerinstalls the framework handler,fetchis the universal entry point callable withoutlisten(Cloudflare Workers),listenbinds a real TCP socket, andclosetears it down. Adapters (NodeHttpAdapter,DenoHttpAdapter,BunHttpAdapter,CloudflareWorkersHttpAdapter) are registered underCAPABILITIES.HTTP_ADAPTERviaRuntimePlugin. -
M46 added a fifth, OPTIONAL member:
setUpgradeRouter?(router). It installs aWebSocketUpgradeRouterconsulted for every inbound WebSocket upgrade before the request is mapped to anIRequestand enters the middleware pipeline. That ordering is a correctness requirement, not an optimization: the shared mapping pre-reads the body viaarrayBuffer(), andDeno.upgradeWebSocketfails once the request body has been disturbed. The router answers accept (with theWebSocketEventSinkthe adapter binds its native socket into), reject (with a status), ornullto fall through — so installing a router never changes the behavior of non-WebSocket traffic. Because the member is optional, adapters written before this seam remain valid implementations; consumers must degrade gracefully when it is absent (seeWebSocketPlugin, which reportsavailable: falseand failsroute()with a typed error). All four first-party adapters implement it. -
M49 added a sixth, OPTIONAL member:
setRpcHandler?(handler). It installs anRpcFetchHandlerconsulted for every inbound request after the WebSocket upgrade short-circuit (where one exists) and before the request is mapped to anIRequest. The ordering is again a correctness requirement: a gRPC exchange needs the raw streaming body and emits trailers, neither of whichIRequest/IResponsecan express, and the shared mapping pre-reads the body. The handler answers aResponse(handled as RPC) ornullto fall through, so installing one never changes the behavior of non-RPC traffic. The handler is consulted exactly once per request, and one returningnullMUST leave the body unread — the adapter maps that sameRequestafterwards, so a consumed body makes the fall-through fail with “Body already consumed”. Decide from method, URL and headers (the gRPC plugin matches on a path prefix alone); a handler that must inspect the body has to readrequest.clone(). Because the member is optional, adapters written before this seam remain valid implementations; consumers must degrade gracefully when it is absent (seeGrpcPlugin, which reportsavailable: falseand throwsGrpcUnavailableErrorfromhandleRequest). All four first-party adapters implement it. -
Socket peer (M113n).
IRequest.ip?: stringis the socket peer on Node, Deno and Bun; the client behind a proxy isCLIENT_IP_STATE_KEY. IPv4-mapped peers are normalized to dotted IPv4. Workers, plainadapter.fetch()and the local diagnostics listener supply no peer.NodeServeHost.serve’s fetch environment addsincoming?: { socket?: { remoteAddress?: string | undefined } }(matching Node’s native member).DenoServeHost.serve’s callback accepts optionalinfo?: { remoteAddr?: { hostname?: string } }.BunServeradds optionalrequestIP?(request: Request): { address: string } | null. Older injected hosts that supply none of these still compile and produce no peer. -
The
RuntimePluginis mandatory in every application. It registers atPLUGIN_PRIORITY.HIGHESTso its services are available to all other plugins during registration. -
Each adapter factory accepts an injectable
*Hostinterface (the documented extension point for custom runtimes). The default host binds to the real runtime global via a single sanctionedascast; no other casts are used. -
detectRuntime()accepts an injectableglobalsparameter (defaultglobalThis) so all detection branches are testable without real runtimes. -
createRuntimeServices(options?)builds services without an application. Use it when the runtime is needed before any plugin exists — reading the environment to decide which plugins to register, which is whatcreateFullStackAppFromConfigdoes.RuntimePlugin.registercalls this same function, so detection, the platform → adapter lookup, and theNo runtime adapter factory for platform: <p>error exist once rather than twice.import { createRuntimeServices } from '@setu-ts/runtime'; import { loadConfig } from '@setu-ts/config-plugin'; const config = await loadConfig(createRuntimeServices());Building a second instance alongside the application’s own is safe: the adapters are stateless facades over platform globals, holding no connection, cache, or handle registry, and nothing compares them by identity. One caveat —
envis a snapshot taken at construction, not a live view, so a variable set between two constructions is visible only to the later instance. -
envon Cloudflare Workers. There is no ambient environment on the edge: bindings and variables arrive as theenvargument of thefetchhandler. BothRuntimePluginandcreateRuntimeServicestherefore take anenvoption, and without itruntime.envis empty on Workers, soConfigPluginand the secretsEnvProviderread nothing. Pass what the platform provides:import { env } from 'cloudflare:workers'; const app = createApplication({ plugins: [RuntimePlugin({ env })] });Only the record’s string entries populate
runtime.env, which is contracted asReadonly<Record<string, string | undefined>>; object bindings are filtered out by the puresplitWorkerEnvincommonand reached throughCAPABILITIES.CLOUDFLAREinstead. Passing them through unfiltered would handConfigPlugina[object Object]for every KV namespace. The option is ignored on Deno, Node, and Bun, which read their own ambient environment.