From 94b95573419086e81452f06c9ec019fae80bb5b6 Mon Sep 17 00:00:00 2001 From: orebams Date: Sat, 26 Sep 2026 22:20:53 +0100 Subject: [PATCH] fix: configure status event listener limit --- .env.example | 3 ++ .../events/status-events.service.spec.ts | 29 +++++++++++++++++++ src/common/events/status-events.service.ts | 12 ++++++-- 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 src/common/events/status-events.service.spec.ts diff --git a/.env.example b/.env.example index 728ad0a..d2e8633 100644 --- a/.env.example +++ b/.env.example @@ -120,6 +120,9 @@ AVIATIONSTACK_API_KEY= # optional — leave blank to use mock data # Port the API server listens on PORT=3001 +# Maximum listeners per policy status event emitter. Default: 1000. +# STATUS_EVENTS_MAX_LISTENERS=1000 + # Environment: "development", "staging", or "production" NODE_ENV="development" diff --git a/src/common/events/status-events.service.spec.ts b/src/common/events/status-events.service.spec.ts new file mode 100644 index 0000000..7a3a1b1 --- /dev/null +++ b/src/common/events/status-events.service.spec.ts @@ -0,0 +1,29 @@ +import { StatusEventsService } from './status-events.service'; +import { ConfigService } from '@nestjs/config'; +import { EventEmitter } from 'events'; + +describe('StatusEventsService', () => { + function build(configuredMaxListeners?: string) { + const config = { + get: jest.fn((key: string) => key === 'STATUS_EVENTS_MAX_LISTENERS' ? configuredMaxListeners : undefined), + }; + const service = new StatusEventsService(undefined, undefined, config as unknown as ConfigService); + + return { service, config }; + } + + it('uses the configured maximum listener count', () => { + const { service } = build('250'); + + expect((service as unknown as { emitter: EventEmitter }).emitter.getMaxListeners()).toBe(250); + }); + + it.each([undefined, '', '0', '-1', '1.5', 'not-a-number'])( + 'uses the default maximum listener count for invalid configuration (%s)', + (configuredMaxListeners) => { + const { service } = build(configuredMaxListeners); + + expect((service as unknown as { emitter: EventEmitter }).emitter.getMaxListeners()).toBe(1000); + }, + ); +}); \ No newline at end of file diff --git a/src/common/events/status-events.service.ts b/src/common/events/status-events.service.ts index d53df00..4485871 100644 --- a/src/common/events/status-events.service.ts +++ b/src/common/events/status-events.service.ts @@ -9,6 +9,7 @@ import { } from '@nestjs/common'; import { EventEmitter } from 'events'; import * as crypto from 'crypto'; +import { ConfigService } from '@nestjs/config'; import type Redis from 'ioredis'; import { PrismaService } from '../../prisma/prisma.service'; @@ -19,6 +20,7 @@ export interface PolicyStatusEvent { } const STATUS_EVENTS_CHANNEL = 'policy:status:events'; +const DEFAULT_MAX_LISTENERS = 1000; /** * StatusEventsService — pub/sub for policy status changes (#349). @@ -42,10 +44,14 @@ export class StatusEventsService implements OnModuleInit, OnModuleDestroy { private readonly redis?: Redis, @Optional() private readonly prisma?: PrismaService, + @Optional() + private readonly config?: ConfigService, ) { - // Default is 10 — a popular policy with many open SSE connections - // shouldn't trigger Node's "possible memory leak" warning. - this.emitter.setMaxListeners(1000); + const configuredMaxListeners = Number(this.config?.get('STATUS_EVENTS_MAX_LISTENERS')); + const maxListeners = Number.isSafeInteger(configuredMaxListeners) && configuredMaxListeners > 0 + ? configuredMaxListeners + : DEFAULT_MAX_LISTENERS; + this.emitter.setMaxListeners(maxListeners); } setPolicyOwner(policyId: string, owner: string): void {