import 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 { ServerOptions } from 'socket.io'; import { LoggerService } from './logger.service'; const CONTEXT = 'RedisIoAdapter'; /** * Socket.IO adapter backed by Redis pub/sub so WebSocket broadcasts * fan out across every API instance. * * Falls back to the in-memory IoAdapter when Redis cannot be reached, * so local dev without Redis and single-node deployments still work. */ export class RedisIoAdapter extends IoAdapter { private adapterConstructor: ReturnType | null = null; private pubClient: Redis | null = null; private subClient: Redis | null = null; private readonly logger: LoggerService; constructor(app: INestApplicationContext) { super(app); this.logger = app.get(LoggerService); } async connectToRedis(): Promise { const host = process.env['REDIS_HOST'] ?? 'localhost'; const port = Number(process.env['REDIS_PORT'] ?? 6379); const password = process.env['REDIS_PASSWORD'] ?? undefined; const pub = new Redis({ host, port, password, lazyConnect: true, enableReadyCheck: false, maxRetriesPerRequest: 1, retryStrategy: (times) => Math.min(times * 1000, 5000), }); const sub = pub.duplicate(); try { await Promise.all([pub.connect(), sub.connect()]); } catch (error) { this.logger.warn( `Redis pub/sub unavailable — falling back to in-memory adapter: ${ error instanceof Error ? error.message : String(error) }`, CONTEXT, ); await Promise.allSettled([pub.quit(), sub.quit()]); return; } this.pubClient = pub; this.subClient = sub; this.adapterConstructor = createAdapter(pub, sub); this.logger.log( `Redis pub/sub adapter connected (${host}:${port})`, CONTEXT, ); } override createIOServer(port: number, options?: ServerOptions): unknown { const server = super.createIOServer(port, options) as { adapter: (constructor: unknown) => void; }; if (this.adapterConstructor) { server.adapter(this.adapterConstructor); } return server; } override async close(): Promise { await Promise.allSettled([ this.pubClient?.quit(), this.subClient?.quit(), ]); this.pubClient = null; this.subClient = null; this.adapterConstructor = null; } }