Skip to content

Components & Services

1. SignalComponent

Source: src/components/signal.component.tsExtends: BaseComponent

Top-level component registered by the Application. Acts as an orchestrator that registers the service, controller, and nested WebSocket component.

Binding phase (binding()):

StepActionType
1Register SignalEventServiceService
2Register WebSocketClientControllerController
3Register NxWebSocketComponentComponent
typescript
// packages/signal/src/components/signal.component.ts
export class SignalComponent extends BaseComponent {
  constructor(
    @inject({ key: CoreBindings.APPLICATION_INSTANCE })
    protected application: BaseApplication,
  ) {
    super({
      scope: SignalComponent.name,
      initDefault: { enable: true, container: application },
      bindings: {},
    });
  }

  override async binding(): Promise<void> {
    this.application.service(SignalEventService);
    this.application.controller(WebSocketClientController);
    this.application.component(NxWebSocketComponent);
  }
}

2. NxWebSocketComponent

Source: src/components/websocket.component.tsExtends: BaseComponent

The core infrastructure component. Initializes Redis, creates ECDH instance, binds all 6 WebSocket handlers, creates WebSocketEmitter, and registers the IGNIS WebSocketComponent.

2.1. Instance Properties

typescript
// packages/signal/src/components/websocket.component.ts
private readonly ecdh = new ECDH({
  algorithm: 'ecdh-p256',
  hkdfInfo: applicationEnvironment.get<string>(EnvironmentKeys.APP_ENV_WEBSOCKET_ECDH_INFO),
});

private readonly _clientAesKeys = new Map<string, CryptoKey>();
PropertyTypePurpose
ecdhECDHIGNIS ECDH helper instance (P-256 algorithm, HKDF info from env)
_clientAesKeysMap<string, CryptoKey>Per-client AES-256-GCM keys, keyed by client ID

2.2. Binding Phase

typescript
override async binding(): Promise<void> {
  const redis = this.initializeRedis();                           // 1. Redis connection
  this.application.bind({ key: WebSocketBindingKeys.REDIS_CONNECTION }).toValue(redis);
  this.application.bind({ key: WebSocketBindingKeys.SERVER_OPTIONS }).toValue({
    path: '/stream',
    requireEncryption: true,
  });

  this.bindHandlers();                                            // 2. All 6 handlers
  await this.bindEmitter({ redis });                              // 3. WebSocketEmitter
  this.application.component(WebSocketComponent);                 // 4. IGNIS WebSocketComponent
}
StepActionBinding Key
1Initialize Redis (single or cluster)WebSocketBindingKeys.REDIS_CONNECTION
1bSet server optionsWebSocketBindingKeys.SERVER_OPTIONS
2Bind 6 handler functionsSee Handler Bindings
3Create and configure WebSocketEmitter@nx/signal/websocket-emitter
4Register IGNIS WebSocketComponent(internal IGNIS bindings)

2.3. Handler Bindings (6 Handlers)

#HandlerBinding KeyTypePurpose
1authenticateWebSocketBindingKeys.AUTHENTICATE_HANDLERTWebSocketAuthenticateFn<ISignalAuthPayload>Verify JWT Bearer token via JWTTokenService, return { userId }
2handshakeWebSocketBindingKeys.HANDSHAKE_HANDLERTWebSocketHandshakeFn<ISignalAuthPayload>ECDH key exchange: generate key pair, derive AES key, store in _clientAesKeys, return { serverPublicKey, salt }
3outboundTransformerWebSocketBindingKeys.OUTBOUND_TRANSFORMERTWebSocketOutboundTransformerEncrypt outgoing messages with client's AES key. Skip connected and error events (return null).
4messageWebSocketBindingKeys.MESSAGE_HANDLERTWebSocketMessageHandlerDecrypt incoming { iv, ct } payloads with client's AES key. Validates payload structure before decryption.
5validateRoomWebSocketBindingKeys.VALIDATE_ROOM_HANDLERTWebSocketValidateRoomFnPassthrough — accepts all requested rooms (({ rooms }) => rooms)
6clientDisconnectedWebSocketBindingKeys.CLIENT_DISCONNECTED_HANDLERTWebSocketClientDisconnectedFnClean up AES key from _clientAesKeys map

2.4. Authentication Payload

typescript
// packages/signal/src/components/websocket.component.ts
interface ISignalAuthPayload extends Record<string, unknown> {
  type: string;            // Authentication type (only "Bearer" supported)
  token: string;           // JWT token from @nx/identity
  clientPublicKey?: string; // Base64-encoded ECDH P-256 raw public key
}

Authenticate handler flow:

  1. Check type and token are present → return false if missing
  2. Switch on type:
    • Authentication.TYPE_BEARER: Resolve JWTTokenService from DI, call verify({ type, token }), return { userId: rs.userId.toString() } on success
    • Default: Log warning, return false
  3. On any error: Log error, return false

2.5. Handshake Handler

Requirements: data.clientPublicKey must be present (rejects if missing).

Flow:

  1. this.ecdh.generateKeyPair(){ keyPair, publicKeyB64: serverPublicKey }
  2. this.ecdh.importPublicKey({ rawKeyB64: clientPubB64 })clientPub
  3. this.ecdh.deriveAESKey({ privateKey: keyPair.privateKey, peerPublicKey: clientPub }){ key: aesKey, salt }
  4. this._clientAesKeys.set(clientId, aesKey) — store for future encrypt/decrypt
  5. Return { serverPublicKey, salt } — sent to client in connected event

2.6. Redis Initialization

Supports two modes based on APP_ENV_WEBSOCKET_REDIS_MODE:

Single mode (default):

ParameterSourceDefault
nameHardcodedwebsocket-redis
hostAPP_ENV_WEBSOCKET_REDIS_HOSTlocalhost
portAPP_ENV_WEBSOCKET_REDIS_PORT6379
passwordAPP_ENV_WEBSOCKET_REDIS_PASSWORD
databaseAPP_ENV_WEBSOCKET_REDIS_DB0
maxRetryAPP_ENV_WEBSOCKET_REDIS_MAX_RETRY5
autoConnectHardcodedfalse

Cluster mode:

ParameterSourceDefault
nameHardcodedwebsocket-redis-cluster
nodesAPP_ENV_WEBSOCKET_REDIS_CLUSTER_NODES (comma-separated host:port)Required
passwordAPP_ENV_WEBSOCKET_REDIS_PASSWORD
enableOfflineQueueHardcodedtrue

Invalid mode throws an error with message: [NxWebSocketComponent] Invalid Redis Mode: ${mode} | Valid: [single, cluster]

2.7. WebSocketEmitter

A WebSocketEmitter instance is created and bound to @nx/signal/websocket-emitter. This enables cross-instance and cross-service messaging:

typescript
// packages/signal/src/components/websocket.component.ts — bindEmitter()
private async bindEmitter(opts: { redis: DefaultRedisHelper }) {
  const emitter = new WebSocketEmitter({
    identifier: 'signal-ws-emitter',
    redisConnection: opts.redis,
  });
  await emitter.configure();
  this.application.bind({ key: NxBindingKeys.WEBSOCKET_EMITTER }).toValue(emitter);
}

3. SignalEventService

Source: src/services/signal-event.service.tsExtends: BaseServiceDI Dependencies: BaseApplication (for lazy resolution of WebSocket helpers)

Core service for WebSocket messaging. Uses lazy getters because WebSocketServerHelper is bound via a post-start hook (not available during DI construction).

3.1. Lazy Getter Pattern

typescript
// packages/signal/src/services/signal-event.service.ts
private _ws: WebSocketServerHelper | null = null;
private _emitter: WebSocketEmitter | null = null;

private get ws(): WebSocketServerHelper {
  if (!this._ws) {
    this._ws = this._application.get<WebSocketServerHelper>({
      key: WebSocketBindingKeys.WEBSOCKET_INSTANCE,
      isOptional: true,
    }) ?? null;
  }
  if (!this._ws) {
    throw getError({ message: '[SignalEventService] WebSocket not initialized' });
  }
  return this._ws;
}

private get emitter(): WebSocketEmitter {
  if (!this._emitter) {
    this._emitter = this._application.get<WebSocketEmitter>({
      key: NxBindingKeys.WEBSOCKET_EMITTER,
      isOptional: true,
    }) ?? null;
  }
  if (!this._emitter) {
    throw getError({ message: '[SignalEventService] WebSocketEmitter not initialized' });
  }
  return this._emitter;
}

3.2. Query Methods

MethodParametersReturnsDescription
isReady()booleanCheck if WebSocket server is initialized (resolves WebSocketBindingKeys.WEBSOCKET_INSTANCE with isOptional: true)
getClientCount()numberGet count of connected clients via this.ws.getClients() as Map<string, IWebSocketClient>
getClient(opts){ clientId: string }IWebSocketClient | undefinedGet raw WebSocket client object by ID
getClientInfo(opts){ clientId: string }TWebSocketClientInfo | undefinedGet serialized client info (safe for API responses)
getConnectedClientsInfo()TWebSocketClientInfo[]List all connected clients (serialized)

3.3. Messaging Methods

MethodParametersReturnsDescription
broadcast(opts){ topic: string, data: T }Promise<void>Send to all clients (cross-instance via Redis pub/sub). Delegates to this.ws.send({ payload }).
sendToRoom(opts){ room: string, topic: string, data: T }Promise<void>Send to all clients in a room (cross-instance). Delegates to this.ws.send({ destination: room, payload }).
sendToClient(opts){ clientId: string, topic: string, data: T }Promise<void>Send to a specific client. Local delivery if on this instance, otherwise via WebSocketEmitter.
disconnectClient(opts){ clientId: string }booleanForce-close client connection via client.socket.close(). Returns false if client not found.

Cross-instance routing in sendToClient:

typescript
async sendToClient<T>(opts: { clientId: string; topic: string; data: T }) {
  const client = this.getClient({ clientId: opts.clientId });
  if (client) {
    // Client is on this instance — deliver locally
    this.ws.sendToClient({ clientId: opts.clientId, event: opts.topic, data: opts.data });
  } else {
    // Client is on another instance — publish via Redis
    await this.emitter.toClient({ clientId: opts.clientId, event: opts.topic, data: opts.data });
  }
}

3.4. Client Serialization

typescript
// packages/signal/src/services/signal-event.service.ts
private serializeClient(client: IWebSocketClient): TWebSocketClientInfo {
  return {
    id: client.id,
    state: client.state,
    encrypted: client.encrypted,
    rooms: Array.from(client.rooms),
    connectedAt: client.connectedAt,
    lastActivity: client.lastActivity,
    metadata: {},
  };
}

3.5. TWebSocketClientInfo Schema

typescript
// packages/signal/src/common/types.ts
const WebSocketClientInfoSchema = z.object({
  id: z.string(),
  state: z.string(),
  encrypted: z.boolean(),
  rooms: z.array(z.string()),
  connectedAt: z.number(),
  lastActivity: z.number(),
  metadata: z.record(z.string(), z.unknown()).optional(),
});

type TWebSocketClientInfo = z.infer<typeof WebSocketClientInfoSchema>;

NOTE

The metadata field is always set to {} in serializeClient(). It exists in the schema for future extensibility.

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.