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:

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();