VOLT
Open Source Ecosystem

DaemonClusterClient

TypeScript WebSocket client for cluster daemon communication with the VOLT server.

Overview

@voltstack/daemon-cluster-client is the TypeScript client that cluster daemons use to communicate with the VOLT server over WebSocket (socket.io). It handles enrollment, heartbeat, bidirectional command dispatch, and real-time messaging.

This package is used internally by the VOLT cluster daemon. If you are building a custom daemon or integrating cluster-level services, this is the client you need.

Installation

npm install @voltstack/daemon-cluster-client

Peer dependency: socket.io-client >= 4.0.0

Quick Start

import { ClusterDaemonClient } from '@voltstack/daemon-cluster-client';

const client = new ClusterDaemonClient({
  serverUrl: 'https://api.voltcloud.dev',
  controlSocketUrl: 'wss://api.voltcloud.dev/control',
  credentials: {
    teamClusterId: 'cluster-abc123',
    daemonPassword: 'password',
  },
});

await client.connect();

client.registerHandler('execute-plugin', async (payload) => {
  // Handle inbound command from server
  return { success: true };
});

Constructor

class ClusterDaemonClient {
  constructor(options: ClusterDaemonClientOptions)
}

Options

interface ClusterDaemonClientOptions {
  // Base URL of the VOLT server
  serverUrl: string;
  // socket.io control namespace URL
  controlSocketUrl: string;
  // Authentication credentials
  credentials: DaemonCredentials;
  // Optional: enrollment configuration
  enrollment?: EnrollmentOptions;
  // Optional: heartbeat configuration
  heartbeat?: HeartbeatOptions;
  // Optional: socket.io options
  socket?: SocketOptions;
  // Optional: default command timeout in ms (default: 30000)
  commandTimeout?: number;
}

interface DaemonCredentials {
  teamClusterId: string;
  daemonPassword: string;
  // Used during first-time enrollment
  enrollmentToken?: string;
}

interface HeartbeatOptions {
  // Interval in ms (default: 30000)
  interval?: number;
}

Connection Lifecycle

┌─────────────┐     ┌──────────────┐     ┌───────────────┐
│  Enrollment  │ ──► │ Socket.io    │ ──► │  Heartbeat    │
│  (HTTP POST) │     │  Connect     │     │  Loop Start   │
└─────────────┘     └──────────────┘     └───────────────┘

connect(): Promise<void>

Performs the full connection sequence:

  1. Enrollment (if enrollmentToken is provided): Registers the daemon with the VOLT server via HTTP. The server may rotate the daemon password.
  2. Socket connection: Establishes a socket.io connection to the control namespace and completes registration.
  3. Heartbeat: Starts a periodic heartbeat loop that sends runtime.heartbeat commands.
await client.connect();

disconnect(): void

Gracefully disconnects the socket and stops the heartbeat loop.

client.disconnect();

isReady(): boolean

Returns true if the socket is connected and registration is complete.

if (client.isReady()) {
  await client.sendCommand('status');
}

Sending Commands

sendCommand<T>(command, payload?, timeout?): Promise<T | undefined>

Sends a request to the VOLT server and waits for a response. Uses a UUID-keyed request/response pattern with timeout.

// Send a command and wait for response
const result = await client.sendCommand('get-container-stats', {
  containerId: 'container-123',
});

// With custom timeout (ms)
const result = await client.sendCommand('long-operation', payload, 60000);

Throws DaemonClientError with codes:

CodeMeaning
SOCKET_NOT_READYSocket is not connected or registration incomplete
COMMAND_TIMEOUTServer did not respond within the timeout
COMMAND_REJECTEDServer explicitly rejected the command

emit(message): void

Sends a fire-and-forget message on the control socket. Used for notifications that do not require a response.

client.emit({
  type: 'exposure-snapshot',
  data: { analysisId, timestep, status: 'complete' },
});

Handling Inbound Commands

The VOLT server can send commands to the daemon (e.g., "run this plugin", "list container files"). Register handlers to respond to these commands.

registerHandler(command, handler): this

Registers a handler for a specific inbound command. Handlers survive reconnections.

client.registerHandler('execute-plugin', async (payload) => {
  const result = await runPlugin(payload.pluginId, payload.params);
  return result;
});

client.registerHandler('list-files', async (payload) => {
  return fs.readdirSync(payload.path);
});

unregisterHandler(command): this

Removes a previously registered handler.

client.unregisterHandler('execute-plugin');

onMessage(callback): this

Subscribes to all inbound non-command messages (e.g., session input, tunnel events, exposure snapshots).

client.onMessage((message) => {
  console.log(message.type, message.data);
});

Event Listeners

onConnected(callback): this

Fires when the socket connection is established and registration is complete.

client.onConnected(() => {
  console.log('Daemon connected and registered');
});

onDisconnected(callback): this

Fires when the socket disconnects.

client.onDisconnected((reason) => {
  console.log('Disconnected:', reason);
});

onError(callback): this

Fires on client errors (heartbeat failures, handler errors, socket errors).

client.onError((err) => {
  console.error(err.code, err.message);
});

Credential Accessors

getTeamClusterId(): string

Returns the current team cluster ID.

getDaemonPassword(): string

Returns the current daemon password. This may differ from the initial value if the password was rotated during enrollment.

Error Handling

import { DaemonClientError } from '@voltstack/daemon-cluster-client';

try {
  await client.sendCommand('some-command');
} catch (err) {
  if (err instanceof DaemonClientError) {
    switch (err.code) {
      case 'COMMAND_TIMEOUT':
        // Handle timeout
        break;
      case 'SOCKET_NOT_READY':
        // Handle not connected
        break;
      case 'COMMAND_REJECTED':
        // Handle rejection
        break;
    }
  }
}

Error Codes

CodeDescription
EnrollmentFailedHTTP enrollment request failed
SocketConnectionFailedCould not establish socket.io connection
SocketRegistrationFailedSocket connected but registration was rejected
CommandTimeoutServer did not respond within the timeout
CommandRejectedServer explicitly rejected the command
SocketNotReadyAttempted to send before connection was ready
HandlerErrorAn inbound command handler threw an error
EmitFailedFire-and-forget emit failed

Architecture

The client orchestrates four internal components:

ComponentResponsibility
EnrollmentClientHTTP-based daemon registration and password rotation
ControlSocketManagersocket.io connection, command send/receive, registration
HeartbeatManagerPeriodic runtime.heartbeat commands to keep the connection alive
ReverseChannelBridgeInbound command dispatch and non-command message routing

On this page