Discovery
Discovery answers one question, once: which peers does a fresh node try to reach at boot? A node resolves a seed list, dials those seeds to join their cluster, and never calls discovery again. Every topology change after that, a node joining, leaving, failing, or changing metadata, is reported by membership, not by discovery.
An empty seed list is not an error: while an environment is still coming up (no pod of a headless service has registered yet), the provider returns empty and the boot sequence retries until a seed appears or the bootstrap deadline passes, at which point the node anchors a new cluster as its first member.
The discovery provider is the one part of clustering you plug in yourself. Two implementations ship in the zero-dependency core, and you can write your own.
DNS discovery
DnsDiscovery resolves a hostname to its addresses, ideal for a headless service in a container orchestrator, where one DNS name resolves to every pod's IP.
import { DnsDiscovery, DnsRecordType } from "@tochemey/nodeakt";
new DnsDiscovery({
hostname: "orders.default.svc.cluster.local",
recordType: DnsRecordType.address, // A records; each IP + the given gossip port
port: 7946,
});| Option | Meaning |
|---|---|
hostname | The name to resolve. |
recordType | DnsRecordType.srv (the default) reads SRV records and takes host and port from each; DnsRecordType.address (A) and DnsRecordType.address6 (AAAA) read IPs and attach port. |
port | The gossip port to attach to each address in address/address6 mode; ignored for srv, which carries its own. |
resolver | An optional resolver to query; defaults to the platform's node:dns. |
With SRV records the service registers each node's gossip host and port, so no port is needed. With A records every node shares one gossip port, which is what the example uses: a Compose network alias resolves to all three containers. The Kubernetes example runs the same configuration against a headless service, whose name resolves to every pod behind it.
These options are the DnsDiscoveryOptions type, and recordType is a DnsRecordTypeValue. To point discovery at a non-default DNS source, pass a custom resolver implementing DnsResolver, whose SRV answers are DnsSrvRecords.
Static discovery
StaticDiscovery resolves a fixed list, for a known topology or a test.
import { StaticDiscovery } from "@tochemey/nodeakt";
new StaticDiscovery(["10.0.0.1:7946", "10.0.0.2:7946", "10.0.0.3:7946"]);The list may be empty to start a node that anchors its own cluster and waits for others to find it. Each entry is a host:port string; a blank entry is a TypeError at construction.
A custom provider
Discovery is deliberately pluggable, so the core takes on no cloud or orchestration dependency. Implement DiscoveryProvider to pull seeds from a Kubernetes API query, a Consul or NATS registry, or a cloud instance lookup:
import type { DiscoveryProvider } from "@tochemey/nodeakt";
class ConsulDiscovery implements DiscoveryProvider {
async resolve(): Promise<readonly string[]> {
// Query your registry and return each peer's gossip endpoint as "host:port".
// Return [] (not a rejection) while the source is simply not populated yet.
return await lookupGossipEndpoints();
}
}resolve()is called once, at boot, and never again. Do not poll it for topology.- Return an empty array while the source is still coming up; the boot sequence treats empty as "not ready" and retries. Reject only on a genuine failure to query the source at all.