Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/foxytp/stelar-time-real/llms.txt

Use this file to discover all available pages before exploring further.

Hooks let you intercept and react to internal events in both the server and the client. They are plain functions attached at construction time (or updated at runtime) that fire synchronously when an event occurs. The key rule: returning false from any hook that supports cancellation will prevent the default action — such as disconnecting a client, rejecting a room join, or sending a broadcast.
const stelar = new StelarServer({
  port: 3000,
  hooks: {
    onRateLimitExceeded: ({ clientId }) => {
      console.warn('Spammer detected:', clientId);
      return false; // Warn but do NOT disconnect
    },
  },
});

Server Hooks (StelarHooks)

onRateLimitExceeded

Fires when a client exceeds the rate limit (global, event-specific, or per-client).
ParameterTypeDescription
clientIdstringID of the offending client
eventstring | undefinedEvent name that triggered the limit (omitted for global checks)
protocol'ws' | 'tcp'Protocol the client is connected over
Return false to keep the client connected instead of disconnecting them.
onRateLimitExceeded: ({ clientId, event, protocol }) => {
  metrics.increment('rate_limit.exceeded', { protocol });
  if (event === 'typing') return false; // Never disconnect for typing spam
}

onMaxConnectionsReached

Fires when a new connection attempt is rejected because the global maxConnections limit has been reached. The incoming socket is always destroyed — this hook is informational only.
ParameterTypeDescription
activeConnectionsnumberCurrent number of active connections
maxnumberThe configured limit
ipstringIP address of the rejected client
onMaxConnectionsReached: ({ activeConnections, max, ip }) => {
  alertOps(`Server at capacity: ${activeConnections}/${max}. Rejected: ${ip}`);
}

onMaxRoomsReached

Fires when a client tries to create a room but the global maxRooms limit has been reached.
ParameterTypeDescription
clientIdstringClient attempting the join
roomstringRoom name that was requested
totalRoomsnumberCurrent total room count
maxnumberThe configured limit
Return false to allow the room creation despite the limit.

onMaxRoomsPerClientReached

Fires when a client tries to join another room but has already reached maxRoomsPerClient.
ParameterTypeDescription
clientIdstringClient attempting the join
roomstringRoom name that was requested
currentRoomsnumberNumber of rooms the client is already in
maxnumberThe configured per-client limit
Return false to allow the join despite the limit.

onPayloadTooLarge

Fires when an incoming message exceeds maxPayloadSize. The message is always dropped — this hook is informational only.
ParameterTypeDescription
clientIdstringClient that sent the oversized message
eventstring | undefinedEvent name (if available)
sizenumberActual payload size in bytes
maxnumberThe configured limit in bytes
onPayloadTooLarge: ({ clientId, event, size, max }) => {
  console.warn(`Oversized payload from ${clientId}: ${size} bytes (limit ${max})`);
}

onInvalidMessage

Fires when an incoming message cannot be parsed (malformed JSON, invalid binary frame, etc.).
ParameterTypeDescription
clientIdstringClient that sent the invalid message
reasonstringHuman-readable description of the parse failure
protocol'ws' | 'tcp'Protocol

onClientJoinRoom

Fires just before a client is added to a room, after all limit checks have passed.
ParameterTypeDescription
clientIdstringClient requesting the join
roomstringTarget room name
metadataMap<string, unknown>Client’s metadata map
Return false to reject the room join. The client will not be added and will receive no confirmation.
onClientJoinRoom: ({ clientId, room, metadata }) => {
  const role = metadata.get('role');
  if (room.startsWith('admin-') && role !== 'admin') {
    return false; // Block non-admins from admin rooms
  }
  if (room.startsWith('vip-') && !metadata.get('isPremium')) {
    return false; // Block non-premium from VIP rooms
  }
}

onClientLeaveRoom

Fires just before a client is removed from a room.
ParameterTypeDescription
clientIdstringClient requesting the leave
roomstringRoom being left
Return false to prevent the leave operation.

onBeforeBroadcast

Fires before every broadcast (server-initiated or triggered by a client action via ctx.broadcast).
ParameterTypeDescription
eventstringEvent being broadcast
dataunknownPayload
excludeIdstring | undefinedClient ID excluded from broadcast, if any
Return false to cancel the broadcast entirely.
onBeforeBroadcast: ({ event, data }) => {
  if (event === 'system-debug' && process.env.NODE_ENV === 'production') {
    return false; // Never broadcast debug events in production
  }
}

onClientConnect

Fires after a client has been registered and all middleware has run. Informational only — the connection cannot be cancelled here (use middleware for that).
ParameterTypeDescription
clientIdstringNewly assigned client ID
ipstringClient’s IP address
protocol'ws' | 'tcp'Protocol
metadataMap<string, unknown>Client’s metadata map (may already be populated by middleware)

onClientDisconnect

Fires after a client has disconnected and been unregistered. Room memberships have already been cleaned up.
ParameterTypeDescription
clientIdstringDisconnected client’s ID
ipstringClient’s IP address
protocol'ws' | 'tcp'Protocol
roomsSet<string>Snapshot of rooms the client was in at disconnect time

Client Hooks (StelarClientHooks)

Client hooks are configured via the hooks option on StelarClient.

onBeforeEmit

Fires before every outgoing message. Return false to cancel the send.
ParameterTypeDescription
eventstringEvent name
dataunknownPayload
onBeforeEmit: ({ event, data }) => {
  if (event === 'debug' && !isDev) return false; // No debug in production
  console.debug('→', event, data);
}

onMessage

Fires for every incoming message, before any event listener is called.
ParameterTypeDescription
eventstringEvent name
dataunknownPayload (or ArrayBuffer for binary)
isBinarybooleantrue for binary frames

onStateChange

Fires whenever the connection state changes.
ParameterTypeDescription
fromConnectionStatePrevious state
toConnectionStateNew state
Possible states: 'disconnected''connecting''connected''reconnecting' → …
onStateChange: ({ from, to }) => {
  if (to === 'reconnecting') showOfflineBanner();
  if (to === 'connected')    hideOfflineBanner();
  analytics.track('connection_state_change', { from, to });
}

onReconnectDelay

Fires before each reconnection attempt. Return a number to override the calculated delay (in milliseconds), or return nothing to use the default exponential backoff.
ParameterTypeDescription
attemptnumberCurrent reconnection attempt number (1-based)
defaultDelaynumberThe delay that would be used without the hook
onReconnectDelay: ({ attempt, defaultDelay }) => {
  // During business hours: reconnect fast
  const hour = new Date().getHours();
  if (hour >= 9 && hour <= 18) return 500;
  return defaultDelay; // Off-hours: normal exponential backoff
}

onMessageQueued

Fires when a message is queued because the client is currently disconnected. Useful for showing a “pending” indicator in your UI.
ParameterTypeDescription
eventstringEvent name of the queued message
dataunknownPayload
queueSizenumberCurrent queue length after this addition

onQueueDrained

Fires after all queued messages have been successfully flushed following a reconnection.
ParameterTypeDescription
countnumberNumber of messages that were sent

onError

Fires when a connection or protocol error occurs.
ParameterTypeDescription
errorErrorThe error object
contextstringWhere the error originated: 'emit', 'node-ws', 'browser-ws', or 'tcp'

Full Server Hooks Example

const stelar = new StelarServer({
  port: 3000,
  maxConnections: 5000,
  rateLimit: { maxPoints: 100, windowMs: 1000 },

  hooks: {
    // Access control for admin rooms
    onClientJoinRoom: ({ clientId, room, metadata }) => {
      const role = metadata.get('role');
      if (room.startsWith('admin-') && role !== 'admin') {
        console.warn(`[ACCESS DENIED] ${clientId} tried to join ${room}`);
        return false;
      }
    },

    // Warn on rate limit, auto-ban repeat offenders
    onRateLimitExceeded: ({ clientId, event, protocol }) => {
      const count = incrementAbuse(clientId);
      console.warn(`[RATE LIMIT] ${clientId} on "${event}" (offense #${count})`);

      if (count >= 10) {
        banlist.add(clientId);
        return; // Disconnect on 10th offense
      }
      return false; // Warn only for first 9 offenses
    },

    // Metrics on connection/disconnection
    onClientConnect: ({ clientId, ip, protocol }) => {
      metrics.gauge('active_connections', stelar.getStats().activeConnections);
      console.log(`[+] ${clientId} (${protocol}) from ${ip}`);
    },

    onClientDisconnect: ({ clientId, rooms }) => {
      metrics.gauge('active_connections', stelar.getStats().activeConnections);
      console.log(`[-] ${clientId} left ${rooms.size} room(s)`);
    },

    // Alert when server approaches capacity
    onMaxConnectionsReached: ({ activeConnections, max, ip }) => {
      notifyOps(`Server full (${activeConnections}/${max}). Rejected: ${ip}`);
    },

    // Log oversized payloads
    onPayloadTooLarge: ({ clientId, event, size, max }) => {
      console.warn(`[PAYLOAD] ${clientId} sent ${size} bytes on "${event}" (limit ${max})`);
    },

    // Intercept certain broadcasts
    onBeforeBroadcast: ({ event }) => {
      if (event === 'internal-debug') return false;
    },
  },
});

Full Client Hooks Example

const client = new StelarClient('wss://app.example.com', {
  hooks: {
    // UI feedback on state changes
    onStateChange: ({ from, to }) => {
      console.log(`State: ${from}${to}`);
      updateStatusIndicator(to);

      if (to === 'reconnecting') showReconnectingOverlay();
      if (to === 'connected')    hideReconnectingOverlay();
    },

    // Track all received messages
    onMessage: ({ event, data, isBinary }) => {
      metrics.increment('messages.received');
      if (isBinary) metrics.increment('messages.binary');
    },

    // Suppress debug events in production
    onBeforeEmit: ({ event }) => {
      if (event.startsWith('debug:') && !isDev) return false;
    },

    // Adaptive reconnection delay based on time of day
    onReconnectDelay: ({ attempt, defaultDelay }) => {
      const hour = new Date().getHours();
      return (hour >= 9 && hour <= 18) ? 500 : defaultDelay;
    },

    // Notify user that messages are pending
    onMessageQueued: ({ event, queueSize }) => {
      if (queueSize === 1) showPendingBadge();
    },

    // Hide badge once messages are sent
    onQueueDrained: ({ count }) => {
      hidePendingBadge();
      console.log(`Flushed ${count} pending message(s)`);
    },

    // Report errors
    onError: ({ error, context }) => {
      errorReporter.capture(error, { context, user: getUser() });
    },
  },
});

Updating Hooks at Runtime

Both server and client hooks can be replaced at runtime without restarting. Server:
stelar.updateConfig({
  hooks: {
    onRateLimitExceeded: ({ clientId }) => {
      // Replace the original hook — now auto-bans instead of warning
      banUser(clientId);
      return false;
    },
  },
});
// Note: updateConfig merges hooks — existing hooks not in the update are preserved
Client:
client.updateOptions({
  hooks: {
    onBeforeEmit: ({ event }) => {
      // After feature flag change: start suppressing analytics events
      if (event.startsWith('analytics:')) return false;
    },
  },
});

Build docs developers (and LLMs) love