import { Logger, type INestApplicationContext } from '@nestjs/common'; import { IoAdapter } from '@nestjs/platform-socket.io'; import { createAdapter } from '@socket.io/redis-adapter'; import { Redis } from 'ioredis'; import type { Server, ServerOptions } from 'socket.io'; /** * Socket.io adapter backed by Redis pub/sub (P9 scaling). With the default in-memory * adapter, `server.to(room).emit()` only reaches sockets on the SAME instance; behind a * load balancer a client on replica B never sees a message emitted by replica A. This * adapter fans room emits across every replica through Redis, so realtime works at N>1. * Wired only when REDIS_URL is set (single-instance dev keeps the in-memory adapter). */ export class RedisIoAdapter extends IoAdapter { private readonly logger = new Logger(RedisIoAdapter.name); private adapterConstructor?: ReturnType; private clients: Redis[] = []; constructor( app: INestApplicationContext, private readonly url: string, ) { super(app); } /** Establish the pub/sub clients + the socket.io Redis adapter factory. */ async connect(): Promise { const pubClient = new Redis(this.url, { maxRetriesPerRequest: null }); const subClient = pubClient.duplicate(); this.clients = [pubClient, subClient]; pubClient.on('error', (err) => this.logger.error(`redis pub error: ${err.message}`)); subClient.on('error', (err) => this.logger.error(`redis sub error: ${err.message}`)); this.adapterConstructor = createAdapter(pubClient, subClient); this.logger.log('socket.io Redis adapter connected — realtime fans out across replicas'); } createIOServer(port: number, options?: ServerOptions): Server { const server = super.createIOServer(port, options) as Server; if (this.adapterConstructor) server.adapter(this.adapterConstructor); return server; } async dispose(): Promise { await Promise.all(this.clients.map((c) => c.quit().catch(() => undefined))); } }