cluster
js/cluster.ts
fino:cluster - public API for cluster participation.
Cluster transport uses WebTransport over HTTP/3, with realm port payloads modeled after HTML channel messaging where the cluster serializer supports the same value shape.
Learn more:
- WebTransport: https://www.w3.org/TR/webtransport/
- HTML channel messaging: https://html.spec.whatwg.org/multipage/web-messaging.html#channel-messaging
A node joins the cluster in one of two roles:
startCluster({ port }) - Start a seed server on the given port AND
participate as a worker. The calling node becomes both the coordinator
and an execution target. This is the entry point for the first node.
joinCluster({ seed }) - Connect to an existing seed node. The calling
node becomes a worker: it accepts realm spawns and hosts them locally.
It can also spawn remote realms onto other workers.
After either call, new Realm({ ..., remote: true }) routes through the
active cluster client to spawn onto a remote worker.
Only one cluster connection per process is supported. Calling either function while connected or while a previous connection is closing throws.
Current release scope uses one trusted seed node. Seed election and cluster
authentication are not implemented in this release. Direct peer-to-peer
PORT_MSG delivery remains deferred; control-plane and data-plane messages
route through the seed-backed WebTransport cluster.
Remote realm messaging conformance:
| Topic | Current behavior |
|---|---|
| Trust model | A single trusted seed routes membership, spawn, and port traffic. Hostile-peer handling, authentication failures, seed election, and peer authorization are unsupported. |
| Spawn routing | Realm({ remote: true }) sends SPAWN through the seed, which selects a worker and returns SPAWN_ACK; failures reject the pending spawn. |
| Port routing | PORT_MSG frames route through the seed by destination port ID. Direct peer-to-peer port delivery and transport negotiation are not part of the public protocol. |
| Ordering | Messages sent over one routed port pair are delivered in send order by the reliable WebTransport stream path used for each PORT_MSG; the API does not promise global ordering across unrelated ports. |
| Transfers | ArrayBuffer transfer stores are preserved through the serializer and cluster payload. MessagePort and other live handle transfers are rejected for remote realms. |
| Close and errors | Local port close unregisters the parent-side port, remote realm exit rejects or resolves the waiting run() / call(), and connection close rejects pending spawns and active remote calls. |
import { startCluster, leaveCluster } from 'fino:cluster';
import { Realm } from 'fino:realm';
await startCluster({ port: 9999, nodeId: 'seed-a', tls: { cert: './cert.pem', key: './key.pem' } });
const realm = new Realm({ entry: './worker.ts', remote: true });
await realm.call('healthcheck');
await leaveCluster();Interfaces
interface StartClusterOptions {
Options for starting the first cluster seed node.
import { startCluster, type StartClusterOptions } from 'fino:cluster';
const opts: StartClusterOptions = { port: 9999, nodeId: 'seed-a' };
await startCluster(opts);Properties
port: number
TCP/UDP port the seed HTTP/3 WebTransport server will listen on.
The port must be available on the local host. Pass 0 to let the operating
system select an ephemeral port; startCluster() returns the concrete port.
import { startCluster } from 'fino:cluster';
await startCluster({ port: 9999, tls: { cert: './cert.pem', key: './key.pem' } });hostname?: string
Interface address the seed server binds.
Defaults to all interfaces (0.0.0.0, or :: when an IPv6 hostname
selects the IPv6 family). The seed also joins itself as a worker; for
that self-connection wildcard binds (0.0.0.0, ::) are mapped to the
matching loopback address, and bare IPv6 literals are bracketed.
nodeId?: string
Optional node identifier.
When omitted, the seed uses seed-{port}. Choose a stable ID if logs or
cluster diagnostics need to correlate restarts.
import { startCluster } from 'fino:cluster';
await startCluster({ port: 9999, nodeId: 'primary-seed' });tls: {
cert: string;
key: string;
}
TLS certificate and key files for the seed's HTTPS/HTTP/3 server.
cert and key are filesystem paths to PEM files, not inline PEM text.
TLS is required — WebTransport rides on HTTP/3 over QUIC, so there is no
plaintext mode. For certificates not signed by a system-trusted CA,
joining workers should trust the issuing CA via tls.ca or pin the
certificate via serverCertificateHashes.
import { startCluster } from 'fino:cluster';
await startCluster({ port: 9999, tls: { cert: './cert.pem', key: './key.pem' } });path?: string
URL path of the cluster WebTransport endpoint on the seed server.
Defaults to /__fino_cluster. A missing leading slash is added. Workers
must connect to this exact path; WebTransport sessions requested on any
other path are rejected with a 404, and plain HTTP requests receive a
placeholder response.
h3?: true | { quic?: Record<string, unknown> }
HTTP/3 listener configuration forwarded to the underlying server.
HTTP/3 is always enabled — the cluster transport requires it — so the
only useful form is an object whose quic field tunes the QUIC
transport (timeouts, flow control, and similar low-level knobs).
Omitting this or passing true uses the defaults.
interface JoinClusterOptions {
Options for joining an existing cluster seed.
import { joinCluster, type JoinClusterOptions } from 'fino:cluster';
const opts: JoinClusterOptions = { seed: 'https://127.0.0.1:9999/__fino_cluster' };
await joinCluster(opts);Properties
seed: string | URL
HTTPS WebTransport URL of the seed node.
The URL must include the https:// scheme and a reachable host and port.
When the path is omitted, /__fino_cluster is used.
import { joinCluster } from 'fino:cluster';
await joinCluster({ seed: 'https://seed.example.test:9999/__fino_cluster' });nodeId?: string
Optional worker node identifier.
When omitted, a short random worker-* ID is generated. Provide a stable
value for predictable logs or cluster placement diagnostics.
import { joinCluster } from 'fino:cluster';
await joinCluster({ seed: 'https://127.0.0.1:9999/__fino_cluster', nodeId: 'worker-a' });tls?: { ca?: string; rejectUnauthorized?: boolean }
TLS verification settings for the connection to the seed.
ca is a filesystem path to a PEM CA bundle trusted for the seed's
certificate — use it when the seed's certificate is not signed by a
system-trusted CA. rejectUnauthorized: false skips verification
entirely; avoid it outside local development, since it permits
man-in-the-middle attacks — prefer ca or serverCertificateHashes.
import { joinCluster } from 'fino:cluster';
await joinCluster({
seed: 'https://seed.example.test:9999/__fino_cluster',
tls: { ca: './cluster-ca.pem' },
});quic?: Record<string, unknown>
QUIC transport tuning forwarded to the WebTransport session.
Low-level knobs (timeouts, flow control, and similar) applied to the QUIC connection under the WebTransport session. Most deployments should omit this and use the defaults.
serverCertificateHashes?: readonly WebTransportHash[]
Certificate pinning hashes for the seed's certificate.
Follows the WebTransport serverCertificateHashes model: the connection
is accepted when the seed's certificate matches one of the given hashes,
bypassing CA-based validation. Useful for self-signed deployments where
distributing a CA bundle is impractical.
import { joinCluster } from 'fino:cluster';
const certSha256 = new Uint8Array(32); // SHA-256 digest of the seed certificate
await joinCluster({
seed: 'https://127.0.0.1:9999/__fino_cluster',
serverCertificateHashes: [{ algorithm: 'sha-256', value: certSha256 }],
});Functions
async function startCluster(opts: StartClusterOptions): Promise<number>
Start the cluster seed server and participate as a worker on this node.
The seed is the current routing hub: it routes SPAWN requests, tracks realm ownership, propagates deaths, and forwards PORT_MSG frames to the node that owns the destination port. Direct peer-to-peer delivery is deferred.
This call returns immediately after the seed starts listening. The event loop keeps the server alive as long as there are connected peers.
Throws if this process is already connected to a cluster. The function
resolves with the concrete bound port after the seed transport is listening
and the local worker client has connected to it. This equals opts.port
unless the caller passed 0 to request an ephemeral port.
import { startCluster, leaveCluster } from 'fino:cluster';
const port = await startCluster({
port: 0,
nodeId: 'seed-a',
tls: { cert: './cert.pem', key: './key.pem' },
});
console.log(`cluster seed listening on ${port}`);
await leaveCluster();async function joinCluster(opts: JoinClusterOptions): Promise<void>
Connect to an existing seed and register this node as a worker.
After this call the node accepts remote realm spawns and can spawn realms onto other nodes in the cluster.
Throws if this process is already connected or if the seed URL cannot be
reached, and throws a TypeError when the seed URL does not use the
https: scheme. The returned promise resolves once the worker transport
is connected and the client loop has started.
import { joinCluster, leaveCluster } from 'fino:cluster';
await joinCluster({ seed: 'https://127.0.0.1:9999/__fino_cluster', nodeId: 'worker-a' });
await leaveCluster();function leaveCluster(): Promise<void>
Disconnect from the cluster. Active remote realms are not terminated.
The call is asynchronous and idempotent. It makes the active cluster unavailable immediately, then resolves after the worker client, seed server, WebTransport streams, and underlying HTTP/3/QUIC resources have closed. Concurrent calls share the same shutdown. Remote realm users should terminate or await their realms separately. The promise rejects if an underlying transport cannot complete its cleanup.
import { startCluster, leaveCluster } from 'fino:cluster';
await startCluster({ port: 9999 });
await leaveCluster();