Skip to content

MeshService ​

MeshService provides Redis-backed membership, leader-driven cleanup, and best-effort broadcast fan-out for a group of service instances. It is not an RPC transport.

Cross-service requests, responses, client forwarding, and byte streams use the authenticated mesh WebSocket link exposed by MeshSrpcServer. Redis is limited to mesh membership, the client registry, cleanup obligations, and optional broadcasts. Protect those Redis key prefixes with your Redis ACL or network policy.

Broadcasts ​

Declare the messages that may be broadcast, register handlers before starting the mesh, then publish when needed:

ts
import { MeshService, type MeshBroadcastMap } from '@zyno-io/ts-server-foundation';

interface Broadcasts extends MeshBroadcastMap {
    cacheInvalidated: { key: string };
}

const mesh = new MeshService<Broadcasts>('my-app');

mesh.registerBroadcastHandler('cacheInvalidated', async ({ key }, senderInstanceId, { signal }) => {
    if (signal.aborted) return;
    await invalidateLocalCache(key);
    console.log(`received from mesh node ${senderInstanceId}`);
});

await mesh.start();
await mesh.broadcast('cacheInvalidated', { key: 'users:42' });

Broadcast data is JSON-encoded once, before local delivery and Redis publish. That means local and remote receivers observe the same representation (for example, Date becomes an ISO string and NaN becomes null). Use { skipSelf: true } to publish only to other instances.

broadcast() rejects with MeshBroadcastIndeterminateDeliveryError if Redis publish fails after local delivery has become possible. Broadcasts are best-effort; handlers must tolerate duplicates and missed messages.

Membership and cleanup ​

start() assigns an instance ID, records heartbeat and metadata in Redis, and subscribes to the broadcast channel. getNodes() and getNode() return only instances whose heartbeat is live. Node metadata is updated atomically with the liveness check through updateNodeMetadata().

One leader drains expired membership records. Register setNodeCleanedUpCallback() for cleanup work associated with a vanished node. The callback obligation remains in Redis until it succeeds, so a later leader can retry it.

Mesh membership is a lease. assertLeaseSafe() synchronously fences work once the latest provable lease expires; setLeaseLostCallback() lets dependants close local resources immediately when that happens.

API ​

new MeshService<B extends MeshBroadcastMap = {}>(key, options?) ​

OptionDefaultMeaning
heartbeatIntervalMs5000Membership renewal interval.
nodeTtlMs15000Membership lease duration. Must be at least the heartbeat interval.
leaderOptions—Options for the cleanup leader election.
nodeMetadata{}Initial metadata advertised with this node.
maxMessageBytes1048576Maximum JSON-encoded broadcast size.
maxActiveHandlers4096Maximum concurrent broadcast handlers.
maxActiveHandlerBytes67108864Maximum bytes retained by active broadcast handlers.
cleanupBatchSize100Maximum cleanup records handled in one Lua pass.

Methods ​

  • start() / stop() — join or leave membership.
  • getNodes() / getNode(instanceId) — read live nodes.
  • updateNodeMetadata(metadata) — atomically update metadata for a live node.
  • registerBroadcastHandler(type, handler) — register a local broadcast receiver. The handler receives (data, senderInstanceId, { signal }).
  • broadcast(type, data, { skipSelf? }) — fan out an optional Redis broadcast.
  • setNodeCleanedUpCallback(callback) — handle expired node cleanup.
  • setLeaseLostCallback(callback) / assertLeaseSafe() — react to membership fencing.

For request/response calls, create a MeshSrpcServer and use its mesh-link transport instead of MeshService.

Released under the MIT License.