From a1ac592a4b54c9352ec4b3a4511c21cf5cf99fd9 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 3 Jul 2025 15:49:48 +0200 Subject: [PATCH 01/78] feat: remove travis.yml --- .travis.yml | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8c00fa0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -dist: focal -language: node_js -node_js: - - node - - lts/* From 8850cfbb9d44393856e3bb0704f0bef3c9ea212d Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 3 Jul 2025 15:59:44 +0200 Subject: [PATCH 02/78] fix: lts only check on github actions workflow --- .github/workflows/build.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e7abf64..934f1aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,17 +2,15 @@ name: Node.js CI on: push: - branches: ["*"] pull_request: - branches: ["*"] jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest strategy: matrix: - node-version: [node, lts/*] + node-version: [lts/*] steps: - name: Checkout repository From 7bd8ec26cc9a6f30e25b043c686845691c8183e4 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 3 Jul 2025 17:26:24 +0200 Subject: [PATCH 03/78] fix: checks badge --- .github/workflows/build.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 934f1aa..272aade 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Node.js CI +name: @imqueue/core on: push: diff --git a/README.md b/README.md index 5caa2fc..bb62507 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # I Message Queue (@imqueue/core) -[![Build Status](https://travis-ci.org/imqueue/core.svg?branch=master)](https://travis-ci.org/imqueue/core) +[![Build Status](https://img.shields.io/github/check-runs/imqueue/core/master)](https://github.com/imqueue/core) [![codebeat badge](https://codebeat.co/badges/b7685cb5-b290-47de-80e1-bde3e0582355)](https://codebeat.co/projects/github-com-imqueue-core-master) [![Coverage Status](https://coveralls.io/repos/github/imqueue/core/badge.svg?branch=master)](https://coveralls.io/github/imqueue/core?branch=master) [![Known Vulnerabilities](https://snyk.io/test/github/imqueue/core/badge.svg?targetFile=package.json)](https://snyk.io/test/github/imqueue/core?targetFile=package.json) From 8217092b68f6f401bb4519195f2b509cd0c73698 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 3 Jul 2025 17:30:36 +0200 Subject: [PATCH 04/78] fix: build badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb62507..7daca4f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # I Message Queue (@imqueue/core) -[![Build Status](https://img.shields.io/github/check-runs/imqueue/core/master)](https://github.com/imqueue/core) +[![Build Status](https://img.shields.io/github/actions/workflow/status/imqueue/core/build.yml)](https://github.com/imqueue/core) [![codebeat badge](https://codebeat.co/badges/b7685cb5-b290-47de-80e1-bde3e0582355)](https://codebeat.co/projects/github-com-imqueue-core-master) [![Coverage Status](https://coveralls.io/repos/github/imqueue/core/badge.svg?branch=master)](https://coveralls.io/github/imqueue/core?branch=master) [![Known Vulnerabilities](https://snyk.io/test/github/imqueue/core/badge.svg?targetFile=package.json)](https://snyk.io/test/github/imqueue/core?targetFile=package.json) From 3b119392871604310c986f05157d3cea6566f5a5 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 3 Jul 2025 17:45:35 +0200 Subject: [PATCH 05/78] fix: workflow name --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 272aade..2eeb625 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: @imqueue/core +name: "@imqueue/core" on: push: From 192ad1630a339710f3fce726787b7ffa6de3c53b Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 3 Jul 2025 19:03:10 +0200 Subject: [PATCH 06/78] fix: workflow build name --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2eeb625..67bcd75 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: "@imqueue/core" +name: Build on: push: From b958264c803c3114d22e60ec4c17e876139a4388 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 3 Jul 2025 19:04:20 +0200 Subject: [PATCH 07/78] fix: npmignore --- .npmignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.npmignore b/.npmignore index e0f1877..37120a0 100644 --- a/.npmignore +++ b/.npmignore @@ -3,6 +3,7 @@ .dockerignore .codebeatignore .codebeatsettings +.github .ssh/ dist/ From de06d8d00cc455036fdc5db5565ee63bf64d17ae Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Mon, 14 Jul 2025 18:27:22 +0200 Subject: [PATCH 08/78] fix: tests --- src/RedisQueue.ts | 1 + src/UDPClusterManager.ts | 48 ++++++++++++ src/profile.ts | 2 +- test/RedisQueue.ts | 113 +++++++++++++++++++++++++-- test/UDPClusterManager.ts | 157 +++++++++++++++++++++++++++++++++++--- test/copyEventEmitter.ts | 55 +++++++++++++ test/mocks/redis.ts | 16 ++++ test/profile.ts | 118 +++++++++++++++++++++++++--- 8 files changed, 482 insertions(+), 28 deletions(-) diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index c5f5b1c..1e4b40e 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -503,6 +503,7 @@ export class RedisQueue extends EventEmitter await this.stop(); await this.clear(); this.destroyWriter(); + await this.unsubscribe(); } /** diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index 986173b..8f8b181 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -319,6 +319,54 @@ export class UDPClusterManager extends ClusterManager { }, timeout); } + /** + * Destroys the UDPClusterManager by closing all opened network connections + * and safely destroying all blocking sockets + * + * @returns {Promise} + * @throws {Error} + */ + public async destroy(): Promise { + // Close all UDP sockets and clean up connections + const socketKeys = Object.keys(UDPClusterManager.sockets); + const closePromises: Promise[] = []; + + for (const key of socketKeys) { + const socket = UDPClusterManager.sockets[key]; + + if (socket) { + closePromises.push(new Promise(((socketKey: string): any => + ((resolve: any, reject: any): any => { + try { + // Check if socket has close method and is not already closed + if (typeof socket.close === 'function') { + // Remove all event listeners to prevent memory leaks + socket.removeAllListeners(); + + // Close the socket + socket.close(() => { + socket.unref(); + delete UDPClusterManager.sockets[socketKey]; + resolve(); + }); + } else { + resolve(); + } + } catch (error) { + // Handle any errors during socket closure gracefully + reject(error as Error); + } + }) as any)(key))); + } + } + + // Wait for all sockets to close + await Promise.all(closePromises); + + // Clear the static sockets record + UDPClusterManager.sockets = {}; + } + private static selectNetworkInterface( options: Pick< UDPClusterManagerOptions, diff --git a/src/profile.ts b/src/profile.ts index d96125f..2a6c783 100644 --- a/src/profile.ts +++ b/src/profile.ts @@ -165,7 +165,7 @@ export function logDebugInfo({ if (debugTime) { // noinspection TypeScriptUnresolvedFunction const time = parseInt( - ((process.hrtime as any).bigint() - start) as any, + ((process.hrtime as any).bigint() - BigInt(start)) as any, 10, ) / 1000; let timeStr: string; diff --git a/test/RedisQueue.ts b/test/RedisQueue.ts index 9bdb1e9..71c6c98 100644 --- a/test/RedisQueue.ts +++ b/test/RedisQueue.ts @@ -23,7 +23,7 @@ */ import { logger } from './mocks'; import { expect } from 'chai'; -import { RedisQueue, uuid } from '../src'; +import { RedisQueue, uuid, IMQMode } from '../src'; import Redis from 'ioredis'; process.setMaxListeners(100); @@ -43,13 +43,16 @@ describe('RedisQueue', function() { }); describe('constructor()', () => { - it('should not throw', () => { - expect(() => new (RedisQueue)()).not.to.throw(Error); - expect(() => new RedisQueue('IMQUnitTests')).not.to.throw(Error); - expect(() => new RedisQueue('IMQUnitTests', {})) + it('should not throw', async () => { + const instances: RedisQueue[] = []; + expect(() => instances.push(new (RedisQueue)())).not.to.throw(Error); + expect(() => instances.push(new RedisQueue('IMQUnitTests'))).not.to.throw(Error); + expect(() => instances.push(new RedisQueue('IMQUnitTests', {}))) .not.to.throw(Error); - expect(() => new RedisQueue('IMQUnitTests', { useGzip: true })) + expect(() => instances.push(new RedisQueue('IMQUnitTests', { useGzip: true }))) .not.to.throw(Error); + + await Promise.all(instances.map(instance => instance.destroy())); }); }); @@ -58,6 +61,7 @@ describe('RedisQueue', function() { const rq = new (RedisQueue)(); try { await rq.start() } catch (err) { expect(err).to.be.instanceof(TypeError) } + rq.destroy().catch(); }); it('should create reader connection', async () => { @@ -106,7 +110,7 @@ describe('RedisQueue', function() { await rq.start(); } catch (err) { passed = false } expect(passed).to.be.true; - rq.destroy().catch(); + await rq.destroy(); }); }); @@ -240,4 +244,99 @@ describe('RedisQueue', function() { }); }); + describe('processCleanup()', () => { + it('should perform cleanup when cleanup option is enabled', async () => { + const rq: any = new RedisQueue(uuid(), { + logger, + cleanup: true, + cleanupFilter: 'test*' + }); + await rq.start(); + + // Call processCleanup directly + const result = await rq.processCleanup(); + expect(result).to.equal(rq); + + await rq.destroy(); + }); + + it('should return early when cleanup option is disabled', async () => { + const rq: any = new RedisQueue(uuid(), { + logger, + cleanup: false + }); + await rq.start(); + + const result = await rq.processCleanup(); + expect(result).to.be.undefined; + + await rq.destroy(); + }); + }); + + describe('lock/unlock methods', () => { + it('should handle lock/unlock when writer is null', async () => { + const rq: any = new RedisQueue(uuid(), { logger }); + // Don't start, so writer will be null + + const lockResult = await rq.lock(); + expect(lockResult).to.be.false; + + const unlockResult = await rq.unlock(); + expect(unlockResult).to.be.false; + + const isLockedResult = await rq.isLocked(); + expect(isLockedResult).to.be.false; + + await rq.destroy(); + }); + + it('should handle lock/unlock operations', async () => { + const rq: any = new RedisQueue(uuid(), { logger }); + await rq.start(); + + // Test locking + const lockResult = await rq.lock(); + expect(lockResult).to.be.a('boolean'); + + // Test checking if locked + const isLockedResult = await rq.isLocked(); + expect(isLockedResult).to.be.a('boolean'); + + // Test unlocking + const unlockResult = await rq.unlock(); + expect(unlockResult).to.be.a('boolean'); + + await rq.destroy(); + }); + }); + + describe('utility methods', () => { + it('should test isPublisher and isWorker methods', async () => { + const publisherQueue = new RedisQueue(uuid(), { logger }, IMQMode.PUBLISHER); + const workerQueue = new RedisQueue(uuid(), { logger }, IMQMode.WORKER); + + expect(publisherQueue.isPublisher()).to.be.true; + expect(publisherQueue.isWorker()).to.be.false; + + expect(workerQueue.isPublisher()).to.be.false; + expect(workerQueue.isWorker()).to.be.true; + + await workerQueue.destroy(); + await publisherQueue.destroy(); + }); + + it('should test key and lockKey methods', async () => { + const name = uuid(); + const rq: any = new RedisQueue(name, { logger }); + + expect(rq.key).to.be.a('string'); + expect(rq.key).to.include(name); + + expect(rq.lockKey).to.be.a('string'); + expect(rq.lockKey).to.include('watch:lock'); + + await rq.destroy(); + }); + }); }); diff --git a/test/UDPClusterManager.ts b/test/UDPClusterManager.ts index d9fd4e4..b1f76b2 100644 --- a/test/UDPClusterManager.ts +++ b/test/UDPClusterManager.ts @@ -25,6 +25,7 @@ import { expect } from 'chai'; import { UDPClusterManager } from '../src'; import * as sinon from 'sinon'; import { Socket } from 'dgram'; +import * as os from 'os'; const testMessageUp = 'name\tid\tup\taddress\ttimeout'; const testMessageDown = 'name\tid\tdown\taddress\ttimeout'; @@ -42,17 +43,15 @@ describe('UDPBroadcastClusterManager', function() { expect(typeof UDPClusterManager).to.equal('function'); }); - it('should initialize socket if socket does not exists', () => { - (UDPClusterManager as any).sockets = {}; - - new UDPClusterManager(); - + it('should initialize socket if socket does not exists', async () => { + const manager = new UDPClusterManager(); expect( Object.values((UDPClusterManager as any).sockets), ).not.to.be.length(0); + await manager.destroy(); }); - it('should call add on cluster', () => { + it('should call add on cluster', async () => { const cluster: any = { add: () => {}, remove: () => {}, @@ -66,9 +65,10 @@ describe('UDPBroadcastClusterManager', function() { emitMessage(testMessageUp); expect(cluster.add.called).to.be.true; + await manager.destroy(); }); - it('should not call add on cluster if server exists', () => { + it('should not call add on cluster if server exists', async () => { const cluster: any = { add: () => {}, remove: () => {}, @@ -76,15 +76,18 @@ describe('UDPBroadcastClusterManager', function() { return {}; }, }; - new UDPClusterManager(); + const manager: any = new UDPClusterManager(); sinon.spy(cluster, 'add'); + manager.init(cluster); + emitMessage(testMessageUp); expect(cluster.add.called).to.be.false; + await manager.destroy(); }); - it('should call remove on cluster', () => { + it('should call remove on cluster', async () => { const cluster: any = { add: () => {}, remove: () => {}, @@ -100,9 +103,10 @@ describe('UDPBroadcastClusterManager', function() { emitMessage(testMessageDown); expect(cluster.remove.called).to.be.true; + await manager.destroy(); }); - it('should add server if localhost included', () => { + it('should add server if localhost included', async () => { const cluster: any = { add: () => {}, remove: () => {}, @@ -118,9 +122,10 @@ describe('UDPBroadcastClusterManager', function() { emitMessage('name\tid\tup\t127.0.0.1:6379\ttimeout'); expect(cluster.add.called).to.be.true; + await manager.destroy(); }); - it('should not add server if localhost excluded', () => { + it('should not add server if localhost excluded', async () => { const cluster: any = { add: () => {}, remove: () => {}, @@ -136,5 +141,135 @@ describe('UDPBroadcastClusterManager', function() { emitMessage('name\tid\tup\t127.0.0.1:6379\ttimeout'); expect(cluster.add.called).to.be.false; + await manager.destroy(); + }); + + it('should not add server if not in includeHosts', async () => { + const cluster: any = { + add: () => {}, + remove: () => {}, + find: () => {}, + }; + const manager: any = new UDPClusterManager({ + includeHosts: ['example.com'], + }); + + sinon.spy(cluster, 'add'); + + manager.init(cluster); + + emitMessage('name\tid\tup\t127.0.0.1:6379\ttimeout'); + expect(cluster.add.called).to.be.false; + await manager.destroy(); + }); + + it('should handle server timeout and removal', (done) => { + let addedServer: any = null; + const cluster: any = { + add: () => {}, + remove: async (server: any) => { + expect(server).to.equal(addedServer); + await manager.destroy(); + done(); + }, + find: (message: any) => { + if (!addedServer) { + addedServer = { + id: message.id, + timer: null, + timestamp: Date.now(), + timeout: 50, // Short timeout for test + }; + return addedServer; + } + return addedServer; + }, + }; + const manager: any = new UDPClusterManager(); + + manager.init(cluster); + + // Send up message to add server with short timeout + emitMessage('name\tid\tup\t127.0.0.1:6379\t0.05'); + + // Wait for timeout to trigger removal + setTimeout(async () => { + await manager.destroy(); + }, 1000); + }); + + it('should handle timeout when server no longer exists', async () => { + let serverAdded = false; + const cluster: any = { + add: () => {}, + remove: () => {}, + find: (message: any) => { + if (!serverAdded) { + serverAdded = true; + return { + id: message.id, + timer: null, + timestamp: Date.now(), + timeout: 50, + }; + } + // Return null to simulate server no longer existing + return null; + }, + }; + const manager: any = new UDPClusterManager(); + + manager.init(cluster); + + // This should trigger the timeout handler that returns early (line 307) + emitMessage('name\tid\tup\t127.0.0.1:6379\t0.05'); + await manager.destroy(); + }); + + describe('destroy()', () => { + it('should handle empty sockets gracefully', async () => { + const cluster: any = { + add: () => {}, + remove: () => {}, + find: () => {} + }; + const manager: any = new UDPClusterManager(); + + // Clear any existing sockets + (UDPClusterManager as any).sockets = {}; + + // Should not throw when no sockets exist + await manager.destroy(); + + expect(Object.keys((UDPClusterManager as any).sockets)).to.have.length(0); + }); + + it('should close multiple sockets', async () => { + const cluster: any = { + add: () => {}, + remove: () => {}, + find: () => {} + }; + const manager1: any = new UDPClusterManager({ broadcastPort: 8001 }); + const manager2: any = new UDPClusterManager({ broadcastPort: 8002 }); + + manager1.init(cluster); + manager2.init(cluster); + + // Trigger socket creation for both managers + emitMessage('name\tid\tup\t127.0.0.1:6379\t1000'); + + // Verify multiple sockets exist + const sockets = (UDPClusterManager as any).sockets; + const socketKeys = Object.keys(sockets); + expect(socketKeys.length).to.be.greaterThan(0); + + // Call destroy on both managers + await manager1.destroy(); + await manager2.destroy(); + + // Verify all sockets are cleared (since it's a static property) + expect(Object.keys((UDPClusterManager as any).sockets)).to.have.length(0); + }); }); }); diff --git a/test/copyEventEmitter.ts b/test/copyEventEmitter.ts index 846951a..95faeb0 100644 --- a/test/copyEventEmitter.ts +++ b/test/copyEventEmitter.ts @@ -75,4 +75,59 @@ describe('copyEventEmitter()', function() { expect(targetListenersCount).to.be.equal(sourceListenersCount); }); + + it('should handle listeners without listener property', () => { + const source = new EventEmitter(); + const target = new EventEmitter(); + + // Create a mock listener that looks like onceWrapper but has no listener property + const mockListener = function() {}; + Object.defineProperty(mockListener, 'toString', { + value: () => 'function onceWrapper() { ... }' + }); + + // Manually add the listener to simulate the edge case + source.on(eventName, mockListener); + + // Mock util.inspect to return onceWrapper for this listener + const originalInspect = require('util').inspect; + require('util').inspect = (obj: any) => { + if (obj === mockListener) { + return 'function onceWrapper() { ... }'; + } + return originalInspect(obj); + }; + + copyEventEmitter(source, target); + + // Restore original inspect + require('util').inspect = originalInspect; + + expect(target.listenerCount(eventName)).to.be.equal(1); + }); + + it('should handle source without _maxListeners property', () => { + const source = new EventEmitter(); + const target = new EventEmitter(); + + // Remove _maxListeners property to test the undefined case + delete (source as any)._maxListeners; + + source.on(eventName, () => {}); + copyEventEmitter(source, target); + + expect(target.listenerCount(eventName)).to.be.equal(1); + }); + + it('should handle once listeners with listener property', () => { + const source = new EventEmitter(); + const target = new EventEmitter(); + + const originalListener = () => {}; + source.once(eventName, originalListener); + + copyEventEmitter(source, target); + + expect(target.listenerCount(eventName)).to.be.equal(1); + }); }); diff --git a/test/mocks/redis.ts b/test/mocks/redis.ts index ac7868c..1f229d9 100644 --- a/test/mocks/redis.ts +++ b/test/mocks/redis.ts @@ -286,6 +286,22 @@ export class RedisClientMock extends EventEmitter { return true; } + // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic + public publish(channel: string, message: string, cb?: any): number { + this.cbExecute(cb, null, 1); + return 1; + } + + // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic + public subscribe(channel: string, cb?: any): void { + this.cbExecute(cb, null, 1); + } + + // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic + public unsubscribe(channel?: string, cb?: any): void { + this.cbExecute(cb, null, 1); + } + // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic public config(): boolean { return true; diff --git a/test/profile.ts b/test/profile.ts index 6b9e0e7..c77312e 100644 --- a/test/profile.ts +++ b/test/profile.ts @@ -23,62 +23,94 @@ */ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { profile, ILogger, IMQ_LOG_LEVEL } from '..'; +import * as mock from 'mock-require'; +import { + profile, + ILogger, + verifyLogLevel, + LogLevel, + DebugInfoOptions, +} from '..'; import { logger } from './mocks'; +const BIG_INT_SUPPORT = (() => { + try { + return !!BigInt(0); + } catch (err) { + return false; + } +})(); + class ProfiledClass { - // noinspection JSUnusedLocalSymbols private logger: ILogger = logger; @profile() public decoratedMethod(...args: any[]) { return args; } + + @profile({ + enableDebugTime: true, + enableDebugArgs: true, + logLevel: LogLevel.LOG, + }) + public async decoratedAsyncMethod() { + return new Promise(resolve => setTimeout(resolve, 10)); + } } class ProfiledClassTimed { - // noinspection JSUnusedLocalSymbols private logger: ILogger = logger; @profile({ enableDebugTime: true, + logLevel: LogLevel.LOG, }) public decoratedMethod() {} } class ProfiledClassArgued { - // noinspection JSUnusedLocalSymbols private logger: ILogger = logger; @profile({ enableDebugTime: false, enableDebugArgs: true, + logLevel: LogLevel.LOG, }) public decoratedMethod() {} } class ProfiledClassTimedAndArgued { - // noinspection JSUnusedLocalSymbols private logger: ILogger = logger; @profile({ enableDebugTime: true, enableDebugArgs: true, + logLevel: LogLevel.LOG, }) public decoratedMethod() {} } - - describe('profile()', function() { let log: any; + let error: any; + let warn: any; + let info: any; beforeEach(() => { - log = sinon.spy(logger, IMQ_LOG_LEVEL) + log = sinon.spy(logger, 'log'); + error = sinon.spy(logger, 'error'); + warn = sinon.spy(logger, 'warn'); + info = sinon.spy(logger, 'info'); }); afterEach(() => { log.restore(); + error.restore(); + warn.restore(); + info.restore(); + mock.stopAll(); + delete process.env.IMQ_LOG_TIME_FORMAT; }); it('should be a function', () => { @@ -108,4 +140,72 @@ describe('profile()', function() { new ProfiledClassTimedAndArgued().decoratedMethod(); expect(log.calledTwice).to.be.true; }); -}); + + it('should handle async methods correctly', async () => { + await new ProfiledClass().decoratedAsyncMethod(); + expect(log.calledTwice).to.be.true; + }); + + describe('verifyLogLevel()', () => { + it('should return valid log levels as is', () => { + expect(verifyLogLevel(LogLevel.LOG)).to.equal(LogLevel.LOG); + expect(verifyLogLevel(LogLevel.INFO)).to.equal(LogLevel.INFO); + expect(verifyLogLevel(LogLevel.WARN)).to.equal(LogLevel.WARN); + expect(verifyLogLevel(LogLevel.ERROR)).to.equal(LogLevel.ERROR); + }); + + it('should return default log level on invalid value', () => { + expect(verifyLogLevel('invalid')).to.equal(LogLevel.INFO); + }); + }); + + describe('logDebugInfo()', () => { + const start = BIG_INT_SUPPORT ? BigInt(1) : 1; + const baseOptions: DebugInfoOptions = { + debugTime: true, + debugArgs: true, + className: 'TestClass', + args: [1, 'a', { b: 2 }], + methodName: 'testMethod', + start, + logger, + logLevel: LogLevel.LOG, + }; + + it('should log time in microseconds by default', () => { + const { logDebugInfo } = mock.reRequire('../src/profile'); + logDebugInfo(baseOptions); + expect(log.calledWithMatch(/μs/)).to.be.true; + }); + + it('should log time in milliseconds', () => { + process.env.IMQ_LOG_TIME_FORMAT = 'milliseconds'; + const { logDebugInfo } = mock.reRequire('../src/profile'); + logDebugInfo(baseOptions); + expect(log.calledWithMatch(/ms/)).to.be.true; + }); + + it('should log time in seconds', () => { + process.env.IMQ_LOG_TIME_FORMAT = 'seconds'; + const { logDebugInfo } = mock.reRequire('../src/profile'); + logDebugInfo(baseOptions); + expect(log.calledWithMatch(/sec/)).to.be.true; + }); + + it('should handle circular references in args', () => { + const { logDebugInfo } = mock.reRequire('../src/profile'); + const a: any = { b: 1 }; + const b = { a }; + a.b = b; + logDebugInfo({ ...baseOptions, args: [a] }); + expect(error.notCalled).to.be.true; + }); + + it('should handle JSON.stringify errors', () => { + const { logDebugInfo } = mock.reRequire('../src/profile'); + const badJson = { toJSON: () => { throw new Error('bad json'); } }; + logDebugInfo({ ...baseOptions, args: [badJson] }); + expect(error.calledOnce).to.be.true; + }); + }); +}); \ No newline at end of file From 1ac1480d7b2e2825cbbfd6244b04ad193dea9e78 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 17 Jul 2025 13:54:14 +0200 Subject: [PATCH 09/78] feat: implemented destroy method on UDPClusterManager --- src/ClusterManager.ts | 34 ++++++++- src/ClusteredRedisQueue.ts | 19 ++++- src/RedisQueue.ts | 5 +- src/UDPClusterManager.ts | 145 ++++++++++++++++++++----------------- test/UDPClusterManager.ts | 31 +------- 5 files changed, 131 insertions(+), 103 deletions(-) diff --git a/src/ClusterManager.ts b/src/ClusterManager.ts index 80236d3..5735bc3 100644 --- a/src/ClusterManager.ts +++ b/src/ClusterManager.ts @@ -22,6 +22,7 @@ * to get commercial licensing options. */ import { IMessageQueueConnection, IServerInput } from './IMessageQueue'; +import { uuid } from './uuid'; export interface ICluster { add: (server: IServerInput) => void; @@ -31,12 +32,39 @@ export interface ICluster { ) => T | undefined; } +export interface InitializedCluster extends ICluster { + id: string; +} + export abstract class ClusterManager { - protected clusters: ICluster[] = []; + protected clusters: InitializedCluster[] = []; protected constructor() {} - public init(cluster: ICluster): void { - this.clusters.push(cluster); + public init(cluster: ICluster): InitializedCluster { + const initializedCluster = Object.assign(cluster, { id: uuid() }); + + this.clusters.push(initializedCluster); + + return initializedCluster; } + + public async remove( + cluster: string | InitializedCluster, + destroy: boolean = true, + ): Promise { + const id = typeof cluster === 'string' ? cluster : cluster.id; + + this.clusters = this.clusters.filter(cluster => cluster.id !== id); + + if ( + this.clusters.length === 0 + && destroy + && typeof this.destroy === 'function' + ) { + await this.destroy(); + } + } + + public abstract destroy(): Promise; } diff --git a/src/ClusteredRedisQueue.ts b/src/ClusteredRedisQueue.ts index e51b562..2d11daf 100644 --- a/src/ClusteredRedisQueue.ts +++ b/src/ClusteredRedisQueue.ts @@ -36,6 +36,7 @@ import { IServerInput, copyEventEmitter, } from '.'; +import { InitializedCluster } from './ClusterManager'; interface ClusterServer extends IMessageQueueConnection { imq?: RedisQueue; @@ -130,6 +131,8 @@ export class ClusteredRedisQueue implements IMessageQueue, subscription: null, }; + private initializedClusters: InitializedCluster[] = []; + /** * Class constructor * @@ -167,11 +170,11 @@ export class ClusteredRedisQueue implements IMessageQueue, if (this.options.clusterManagers?.length) { for (const manager of this.options.clusterManagers) { - manager.init({ + this.initializedClusters.push(manager.init({ add: this.addServer.bind(this), remove: this.removeServer.bind(this), find: this.findServer.bind(this), - }); + })); } } } @@ -247,7 +250,7 @@ export class ClusteredRedisQueue implements IMessageQueue, } /** - * Safely destroys current queue, unregistered all set event + * Safely destroys the current queue, unregistered all set event * listeners and connections. * Supposed to be an async function. * @@ -258,6 +261,16 @@ export class ClusteredRedisQueue implements IMessageQueue, await this.batch('destroy', 'Destroying clustered redis message queue...'); + + if (!this.options.clusterManagers?.length) { + return; + } + + for await (const manager of this.options.clusterManagers) { + for await (const cluster of this.initializedClusters) { + await manager.remove(cluster); + } + } } // noinspection JSUnusedGlobalSymbols diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 1e4b40e..85cc152 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -931,7 +931,7 @@ export class RedisQueue extends EventEmitter await this.processKeys(keys, now); if (cursor === '0') { - return ; + return; } } catch (err) { this.emitError('OnSafeDelivery', @@ -954,7 +954,7 @@ export class RedisQueue extends EventEmitter */ private async processKeys(keys: string[], now: number): Promise { if (!keys.length) { - return ; + return; } for (const key of keys) { @@ -1393,5 +1393,4 @@ export class RedisQueue extends EventEmitter } }); } - } diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index 8f8b181..a959eb0 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -28,15 +28,15 @@ import { ICluster, ClusterManager } from './ClusterManager'; import { Socket, createSocket } from 'dgram'; import { networkInterfaces } from 'os'; -enum BroadcastedMessageType { +enum MessageType { Up = 'up', Down = 'down', } -interface BroadcastedMessage { +interface Message { name: string; id: string; - type: BroadcastedMessageType; + type: MessageType; host: string; port: number; timeout: number; @@ -48,7 +48,7 @@ interface ClusterServer extends IMessageQueueConnection { timer?: NodeJS.Timeout; } -export const DEFAULT_UDP_BROADCAST_CLUSTER_MANAGER_OPTIONS = { +export const DEFAULT_UDP_CLUSTER_MANAGER_OPTIONS = { broadcastPort: 63000, broadcastAddress: '255.255.255.255', aliveTimeoutCorrection: 1000, @@ -133,34 +133,55 @@ const LOCALHOST_ADDRESSES = [ export class UDPClusterManager extends ClusterManager { private static sockets: Record = {}; private readonly options: UDPClusterManagerOptions; + private socketKey: string; + + private get socket(): Socket | undefined { + return UDPClusterManager.sockets[this.socketKey]; + } + + private set socket(socket: Socket) { + UDPClusterManager.sockets[this.socketKey] = socket; + } constructor(options?: UDPClusterManagerOptions) { super(); this.options = { - ...DEFAULT_UDP_BROADCAST_CLUSTER_MANAGER_OPTIONS, + ...DEFAULT_UDP_CLUSTER_MANAGER_OPTIONS, ...options || {}, }; this.startListening(this.options); + + process.on('SIGTERM', UDPClusterManager.free); + process.on('SIGINT', UDPClusterManager.free); + process.on('SIGABRT', UDPClusterManager.free); + } + + private static async free(): Promise { + const socketKeys = Object.keys(UDPClusterManager.sockets); + + await Promise.all(socketKeys.map( + socketKey => UDPClusterManager.destroySocket( + socketKey, + UDPClusterManager.sockets[socketKey], + )), + ); } private listenBroadcastedMessages( - listener: (message: BroadcastedMessage) => void, + listener: (message: Message) => void, options: UDPClusterManagerOptions, ): void { - const address = UDPClusterManager.selectNetworkInterface( - options, - ); - const key = `${ address }:${ options.broadcastPort }`; + const address = UDPClusterManager.selectNetworkInterface(options); - if (!UDPClusterManager.sockets[key]) { - const socket = createSocket({ type: 'udp4', reuseAddr: true }); + this.socketKey = `${ address }:${ options.broadcastPort }`; - socket.bind(options.broadcastPort, address); - UDPClusterManager.sockets[key] = socket; + if (!this.socket) { + this.socket = createSocket({ type: 'udp4', reuseAddr: true }); + this.socket.bind(options.broadcastPort, address); } - UDPClusterManager.sockets[key].on( + this.socket.on( 'message', message => listener( UDPClusterManager.parseBroadcastedMessage(message), @@ -168,9 +189,7 @@ export class UDPClusterManager extends ClusterManager { ); } - private startListening( - options: UDPClusterManagerOptions = {}, - ): void { + private startListening(options: UDPClusterManagerOptions = {}): void { this.listenBroadcastedMessages( UDPClusterManager.processBroadcastedMessage(this), options, @@ -191,18 +210,18 @@ export class UDPClusterManager extends ClusterManager { private static processMessageOnCluster( cluster: ICluster, - message: BroadcastedMessage, + message: Message, aliveTimeoutCorrection?: number, ): void { const server = cluster.find(message); - if (server && message.type === BroadcastedMessageType.Down) { + if (server && message.type === MessageType.Down) { clearTimeout(server.timer); return cluster.remove(message); } - if (!server && message.type === BroadcastedMessageType.Up) { + if (!server && message.type === MessageType.Up) { cluster.add(message); const added = cluster.find(message); @@ -218,7 +237,7 @@ export class UDPClusterManager extends ClusterManager { return; } - if (server && message.type === BroadcastedMessageType.Up) { + if (server && message.type === MessageType.Up) { return UDPClusterManager.serverAliveWait( cluster, server, @@ -230,7 +249,7 @@ export class UDPClusterManager extends ClusterManager { private static processBroadcastedMessage( context: UDPClusterManager, - ): (message: BroadcastedMessage) => void { + ): (message: Message) => void { return message => { if ( context.options.excludeHosts @@ -239,7 +258,7 @@ export class UDPClusterManager extends ClusterManager { context.options.excludeHosts, ) ) { - return ; + return; } if ( @@ -249,7 +268,7 @@ export class UDPClusterManager extends ClusterManager { context.options.includeHosts, ) ) { - return ; + return; } for (const cluster of context.clusters) { @@ -262,9 +281,7 @@ export class UDPClusterManager extends ClusterManager { }; } - private static parseBroadcastedMessage( - input: Buffer, - ): BroadcastedMessage { + private static parseBroadcastedMessage(input: Buffer): Message { const [ name, id, @@ -277,7 +294,7 @@ export class UDPClusterManager extends ClusterManager { return { id, name, - type: type.toLowerCase() as BroadcastedMessageType, + type: type.toLowerCase() as MessageType, host, port: parseInt(port), timeout: parseFloat(timeout) * 1000, @@ -288,7 +305,7 @@ export class UDPClusterManager extends ClusterManager { cluster: ICluster, server: ClusterServer, aliveTimeoutCorrection?: number, - message?: BroadcastedMessage, + message?: Message, ): void { clearTimeout(server.timer); server.timestamp = Date.now(); @@ -327,44 +344,42 @@ export class UDPClusterManager extends ClusterManager { * @throws {Error} */ public async destroy(): Promise { - // Close all UDP sockets and clean up connections - const socketKeys = Object.keys(UDPClusterManager.sockets); - const closePromises: Promise[] = []; - - for (const key of socketKeys) { - const socket = UDPClusterManager.sockets[key]; - - if (socket) { - closePromises.push(new Promise(((socketKey: string): any => - ((resolve: any, reject: any): any => { - try { - // Check if socket has close method and is not already closed - if (typeof socket.close === 'function') { - // Remove all event listeners to prevent memory leaks - socket.removeAllListeners(); - - // Close the socket - socket.close(() => { - socket.unref(); - delete UDPClusterManager.sockets[socketKey]; - resolve(); - }); - } else { - resolve(); - } - } catch (error) { - // Handle any errors during socket closure gracefully - reject(error as Error); - } - }) as any)(key))); - } + await UDPClusterManager.destroySocket(this.socketKey, this.socket); + } + + private static async destroySocket( + socketKey: string, + socket?: Socket, + ): Promise { + if (!socket) { + return; } - // Wait for all sockets to close - await Promise.all(closePromises); + return await new Promise((resolve, reject) => { + try { + if (typeof socket.close === 'function') { + socket.removeAllListeners(); + socket.close(() => { + socket?.unref(); + + if ( + socketKey + && UDPClusterManager.sockets[socketKey] + ) { + delete UDPClusterManager.sockets[socketKey]; + } + + resolve(); + }); - // Clear the static sockets record - UDPClusterManager.sockets = {}; + return; + } + + resolve(); + } catch (e) { + reject(e); + } + }); } private static selectNetworkInterface( diff --git a/test/UDPClusterManager.ts b/test/UDPClusterManager.ts index b1f76b2..249e690 100644 --- a/test/UDPClusterManager.ts +++ b/test/UDPClusterManager.ts @@ -39,6 +39,7 @@ const emitMessage = (message: string) => { }; describe('UDPBroadcastClusterManager', function() { + this.timeout(5000); it('should be a class', () => { expect(typeof UDPClusterManager).to.equal('function'); }); @@ -170,7 +171,6 @@ describe('UDPBroadcastClusterManager', function() { remove: async (server: any) => { expect(server).to.equal(addedServer); await manager.destroy(); - done(); }, find: (message: any) => { if (!addedServer) { @@ -195,6 +195,7 @@ describe('UDPBroadcastClusterManager', function() { // Wait for timeout to trigger removal setTimeout(async () => { await manager.destroy(); + done(); }, 1000); }); @@ -243,33 +244,5 @@ describe('UDPBroadcastClusterManager', function() { expect(Object.keys((UDPClusterManager as any).sockets)).to.have.length(0); }); - - it('should close multiple sockets', async () => { - const cluster: any = { - add: () => {}, - remove: () => {}, - find: () => {} - }; - const manager1: any = new UDPClusterManager({ broadcastPort: 8001 }); - const manager2: any = new UDPClusterManager({ broadcastPort: 8002 }); - - manager1.init(cluster); - manager2.init(cluster); - - // Trigger socket creation for both managers - emitMessage('name\tid\tup\t127.0.0.1:6379\t1000'); - - // Verify multiple sockets exist - const sockets = (UDPClusterManager as any).sockets; - const socketKeys = Object.keys(sockets); - expect(socketKeys.length).to.be.greaterThan(0); - - // Call destroy on both managers - await manager1.destroy(); - await manager2.destroy(); - - // Verify all sockets are cleared (since it's a static property) - expect(Object.keys((UDPClusterManager as any).sockets)).to.have.length(0); - }); }); }); From 566c213752eaee21bdfaf50f28ac2703eb913dff Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 17 Jul 2025 13:55:14 +0200 Subject: [PATCH 10/78] 2.0.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9972530..3f973e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.2", + "version": "2.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.2", + "version": "2.0.3", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.6.1" diff --git a/package.json b/package.json index 38659c4..7510eea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.2", + "version": "2.0.3", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 8655c2747dacd3fa9d57cd11d14711208415864d Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 17 Jul 2025 14:12:40 +0200 Subject: [PATCH 11/78] 2.0.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3f973e8..48372d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.3", + "version": "2.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.3", + "version": "2.0.4", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.6.1" diff --git a/package.json b/package.json index 7510eea..8ffc857 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.3", + "version": "2.0.4", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 52b7b24228b17ea01d0e3bc0d69a1a2c69c59128 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Wed, 23 Jul 2025 17:57:14 +0200 Subject: [PATCH 12/78] fix: alive timeout check & start/destroy queue race condition --- src/ClusterManager.ts | 7 ++++++ src/ClusteredRedisQueue.ts | 36 ++++++++++++++++----------- src/RedisQueue.ts | 15 ++++++++---- src/UDPClusterManager.ts | 50 +++++++++++++++++++++++--------------- 4 files changed, 69 insertions(+), 39 deletions(-) diff --git a/src/ClusterManager.ts b/src/ClusterManager.ts index 5735bc3..20e4077 100644 --- a/src/ClusterManager.ts +++ b/src/ClusterManager.ts @@ -29,6 +29,7 @@ export interface ICluster { remove: (server: IServerInput) => void; find: ( server: IServerInput, + strict?: boolean, ) => T | undefined; } @@ -49,6 +50,12 @@ export abstract class ClusterManager { return initializedCluster; } + public async anyCluster( + fn: (cluster: InitializedCluster) => Promise | void, + ): Promise { + await Promise.all(this.clusters.map(cluster => fn(cluster))); + } + public async remove( cluster: string | InitializedCluster, destroy: boolean = true, diff --git a/src/ClusteredRedisQueue.ts b/src/ClusteredRedisQueue.ts index 2d11daf..bf78721 100644 --- a/src/ClusteredRedisQueue.ts +++ b/src/ClusteredRedisQueue.ts @@ -23,18 +23,18 @@ */ import { EventEmitter } from 'events'; import { - DEFAULT_IMQ_OPTIONS, buildOptions, + copyEventEmitter, + DEFAULT_IMQ_OPTIONS, + EventMap, ILogger, IMessageQueue, IMessageQueueConnection, IMQMode, IMQOptions, + IServerInput, JsonObject, RedisQueue, - EventMap, - IServerInput, - copyEventEmitter, } from '.'; import { InitializedCluster } from './ClusterManager'; @@ -496,7 +496,7 @@ export class ClusteredRedisQueue implements IMessageQueue, * @returns {void} */ protected removeServer(server: IServerInput): void { - const remove = this.findServer(server); + const remove = this.findServer(server, true); if (!remove) { return; @@ -534,14 +534,12 @@ export class ClusteredRedisQueue implements IMessageQueue, const imq = new RedisQueue(this.name, opts); if (initializeQueue) { - this.initializeQueue(imq) - .then(() => { - this.clusterEmitter.emit('initialized', { - server: newServer, - imq, - }); - }) - .catch(); + this.initializeQueue(imq).then(() => { + this.clusterEmitter.emit('initialized', { + server: newServer, + imq, + }); + }); } newServer.imq = imq; @@ -571,11 +569,15 @@ export class ClusteredRedisQueue implements IMessageQueue, } } - private findServer(server: IServerInput): ClusterServer | undefined { + private findServer( + server: IServerInput, + strict: boolean = false, + ): ClusterServer | undefined { return this.servers.find( existing => ClusteredRedisQueue.matchServers( existing, server, + strict, ), ); } @@ -583,6 +585,7 @@ export class ClusteredRedisQueue implements IMessageQueue, private static matchServers( source: IServerInput, target: IServerInput, + strict: boolean = false, ): boolean { const sameAddress = target.host === source.host && target.port === source.port; @@ -593,6 +596,11 @@ export class ClusteredRedisQueue implements IMessageQueue, const sameId = target.id === source.id; + if (strict) { + return sameId && sameAddress; + } + return sameId || sameAddress; } + } diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 85cc152..0aad92f 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -180,7 +180,7 @@ export class RedisQueue extends EventEmitter private destroyed: boolean = false; /** - * True if the current instance owns watcher connection, false otherwise + * True if the current instance owns a watcher connection, false otherwise * * @type {boolean} */ @@ -369,6 +369,8 @@ export class RedisQueue extends EventEmitter return this; } + this.destroyed = false; + const connPromises = []; // istanbul ignore next @@ -408,7 +410,6 @@ export class RedisQueue extends EventEmitter await this.initWatcher(); this.initialized = true; - this.destroyed = false; return this; } @@ -586,7 +587,7 @@ export class RedisQueue extends EventEmitter // noinspection JSUnusedLocalSymbols /** - * Watcher setter, sets the watcher connection property for this + * Watcher setter sets the watcher connection property for this * queue instance * * @param {IRedisClient} conn @@ -864,13 +865,17 @@ export class RedisQueue extends EventEmitter } /** - * Returns number of established watcher connections on redis + * Returns the number of established watcher connections on redis * * @access private * @returns {Promise} */ // istanbul ignore next private async watcherCount(): Promise { + if (!this.writer) { + return 0; + } + const rx = new RegExp( `\\bname=${this.options.prefix}:[\\S]+?:watcher:`, ); @@ -883,7 +888,7 @@ export class RedisQueue extends EventEmitter } /** - * Processes delayed message by its given redis key + * Processes delayed a message by its given redis key * * @access private * @param {string} key diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index a959eb0..b79f730 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -55,15 +55,6 @@ export const DEFAULT_UDP_CLUSTER_MANAGER_OPTIONS = { }; export interface UDPClusterManagerOptions { - /** - * Represents the cluster operations that are responsible for managing - * clusters. This includes operations such as adding, removing, or checking - * if a cluster server exists. - * - * @type {ICluster} - */ - cluster?: ICluster; - /** * Message queue broadcast port * @@ -224,7 +215,7 @@ export class UDPClusterManager extends ClusterManager { if (!server && message.type === MessageType.Up) { cluster.add(message); - const added = cluster.find(message); + const added = cluster.find(message, true); if (added) { UDPClusterManager.serverAliveWait( @@ -271,13 +262,13 @@ export class UDPClusterManager extends ClusterManager { return; } - for (const cluster of context.clusters) { + context.anyCluster(cluster => { UDPClusterManager.processMessageOnCluster( cluster, message, context.options.aliveTimeoutCorrection, ); - } + }).then(); }; } @@ -307,7 +298,11 @@ export class UDPClusterManager extends ClusterManager { aliveTimeoutCorrection?: number, message?: Message, ): void { - clearTimeout(server.timer); + if (server.timer) { + clearTimeout(server.timer); + server.timer = undefined; + } + server.timestamp = Date.now(); if (message) { @@ -317,23 +312,38 @@ export class UDPClusterManager extends ClusterManager { const correction = aliveTimeoutCorrection || 0; const timeout = (server.timeout || 0) + correction; - server.timer = setTimeout(() => { - const existing = cluster.find(server); + if (timeout <= 0) { + return; + } + + const timerId = setTimeout(() => { + const existing = cluster.find(server, true); - if (!existing) { + if (!existing || existing.timer !== timerId) { return; } const now = Date.now(); - const delta = now - (existing.timestamp || now); + + if (!existing.timestamp) { + clearTimeout(existing.timer); + existing.timer = undefined; + cluster.remove(existing); + + return; + } + + const delta = now - existing.timestamp; const currentTimeout = (existing.timeout || 0) + correction; if (delta >= currentTimeout) { - clearTimeout(server.timer); - - cluster.remove(server); + clearTimeout(existing.timer); + existing.timer = undefined; + cluster.remove(existing); } }, timeout); + + server.timer = timerId; } /** From 70debc702569c22a1abc5243afcaacd06258dddc Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Wed, 23 Jul 2025 18:17:47 +0200 Subject: [PATCH 13/78] minor fixes --- src/RedisQueue.ts | 31 +++++++++++++++++++++---------- src/UDPClusterManager.ts | 4 +--- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 0aad92f..fb4cf4e 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -438,6 +438,10 @@ export class RedisQueue extends EventEmitter await this.start(); } + if (!this.writer) { + throw new TypeError('IMQ: unable to initialize queue!'); + } + const id = uuid(); const data: IMessage = { id, message, from: this.name }; const key = `${this.options.prefix}:${toQueue}`; @@ -856,7 +860,9 @@ export class RedisQueue extends EventEmitter this.emit('message', message, id, from); } catch (err) { // istanbul ignore next - this.emitError('OnMessage', 'process error - message is invalid', + this.emitError( + 'OnMessage', + 'process error - message is invalid', err, ); } @@ -879,12 +885,9 @@ export class RedisQueue extends EventEmitter const rx = new RegExp( `\\bname=${this.options.prefix}:[\\S]+?:watcher:`, ); - const list = await this.writer.client('LIST') as string; + const list = await this.writer.client('LIST'); - return (list || '') - .split(/\r?\n/) - .filter(client => rx.test(client)) - .length; + return list.split(/\r?\n/).filter(client => rx.test(client)).length; } /** @@ -903,8 +906,11 @@ export class RedisQueue extends EventEmitter ); } } catch (err) { - this.emitError('OnProcessDelayed', 'error processing delayed queue', - err); + this.emitError( + 'OnProcessDelayed', + 'error processing delayed queue', + err, + ); } } @@ -939,8 +945,11 @@ export class RedisQueue extends EventEmitter return; } } catch (err) { - this.emitError('OnSafeDelivery', - 'safe queue message delivery problem', err); + this.emitError( + 'OnSafeDelivery', + 'safe queue message delivery problem', + err, + ); this.cleanSafeCheckInterval(); return; @@ -1194,10 +1203,12 @@ export class RedisQueue extends EventEmitter const msgArr: any = await this.writer.lrange( workerKey, -1, 1, ); + if (msgArr.length !== 1) { // noinspection ExceptionCaughtLocallyJS throw new Error('Wrong messages count'); } + const msg = msgArr[0]; this.process([key, msg]); diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index b79f730..2e67343 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -21,9 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { - IMessageQueueConnection, -} from './IMessageQueue'; +import { IMessageQueueConnection } from './IMessageQueue'; import { ICluster, ClusterManager } from './ClusterManager'; import { Socket, createSocket } from 'dgram'; import { networkInterfaces } from 'os'; From 5d8b6f2257e529caceb3dc2e6b2c4abd9c94e4e3 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Wed, 23 Jul 2025 18:18:09 +0200 Subject: [PATCH 14/78] 2.0.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 48372d2..bddeb36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.4", + "version": "2.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.4", + "version": "2.0.5", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.6.1" diff --git a/package.json b/package.json index 8ffc857..0da7309 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.4", + "version": "2.0.5", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 1d839d48f66d3bc90e358e5fe41d04bac1bff23e Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 13:23:50 +0200 Subject: [PATCH 15/78] feat: improve test coverage --- package.json | 1 + test/ClusterManager.spec.ts | 54 +++++++++++++++++++ test/ClusteredRedisQueue.extra.spec.ts | 48 +++++++++++++++++ test/ClusteredRedisQueue.initialize.spec.ts | 37 +++++++++++++ test/ClusteredRedisQueue.matchServers.spec.ts | 34 ++++++++++++ test/ClusteredRedisQueue.ts | 10 ++-- test/RedisQueue.cleanup.catch.spec.ts | 41 ++++++++++++++ test/UDPClusterManager.ts | 2 +- test/copyEventEmitter.ts | 1 + test/profile.rejection.spec.ts | 33 ++++++++++++ test/profile.ts | 1 + test/promisify.ts | 1 + test/uuid.ts | 1 + 13 files changed, 258 insertions(+), 6 deletions(-) create mode 100644 test/ClusterManager.spec.ts create mode 100644 test/ClusteredRedisQueue.extra.spec.ts create mode 100644 test/ClusteredRedisQueue.initialize.spec.ts create mode 100644 test/ClusteredRedisQueue.matchServers.spec.ts create mode 100644 test/RedisQueue.cleanup.catch.spec.ts create mode 100644 test/profile.rejection.spec.ts diff --git a/package.json b/package.json index 0da7309..0cd4376 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "json-message" ], "scripts": { + "benchmark": "node benchmark -c $(( $(nproc) - 2 )) -m 100000", "prepare": "./node_modules/.bin/tsc", "test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && ./node_modules/.bin/nyc report --reporter=text-lcov && npm run test-coverage", "test-fast": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && /usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/coverage/index.html', { wait: false }))\"", diff --git a/test/ClusterManager.spec.ts b/test/ClusterManager.spec.ts new file mode 100644 index 0000000..895e9fc --- /dev/null +++ b/test/ClusterManager.spec.ts @@ -0,0 +1,54 @@ +/*! + * ClusterManager additional tests + * + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { ClusterManager, InitializedCluster } from '../src/ClusterManager'; + +class TestClusterManager extends ClusterManager { + public destroyed = false; + public constructor() { super(); } + public async destroy(): Promise { + this.destroyed = true; + } +} + +describe('ClusterManager.remove()', () => { + it('should call destroy when the last cluster is removed and destroy=true', async () => { + const cm = new TestClusterManager(); + const cluster: InitializedCluster = cm.init({ + add: () => undefined, + remove: () => undefined, + find: () => undefined, + }); + + // sanity: one cluster registered + expect((cm as any).clusters.length).to.equal(1); + const spy = sinon.spy(cm, 'destroy'); + + await cm.remove(cluster, true); + + expect(spy.calledOnce).to.be.true; + expect((cm as any).clusters.length).to.equal(0); + expect(cm.destroyed).to.be.true; + }); + + it('should not call destroy when destroy=false', async () => { + const cm = new TestClusterManager(); + const cluster: InitializedCluster = cm.init({ + add: () => undefined, + remove: () => undefined, + find: () => undefined, + }); + + const spy = sinon.spy(cm, 'destroy'); + await cm.remove(cluster.id, false); + + expect(spy.called).to.be.false; + expect((cm as any).clusters.length).to.equal(0); + }); +}); diff --git a/test/ClusteredRedisQueue.extra.spec.ts b/test/ClusteredRedisQueue.extra.spec.ts new file mode 100644 index 0000000..358d319 --- /dev/null +++ b/test/ClusteredRedisQueue.extra.spec.ts @@ -0,0 +1,48 @@ +/*! + * Additional tests for ClusteredRedisQueue event emitter proxy methods + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { ClusteredRedisQueue } from '../src'; +import { ClusterManager } from '../src/ClusterManager'; + +const clusterConfig = { + cluster: [ + { host: '127.0.0.1', port: 6379 }, + ], +}; + +describe('ClusteredRedisQueue - EventEmitter proxy methods', () => { + it('should cover rawListeners/getMaxListeners/eventNames/listenerCount/emit', async () => { + const clusterManager = new (ClusterManager as any)(); + const cq: any = new ClusteredRedisQueue('ProxyQueue', { + clusterManagers: [clusterManager], + }); + + // add underlying server and listener + cq.addServer(clusterConfig.cluster[0]); + const handler = sinon.spy(); + cq.imqs[0].on('test', handler); + + // set max listeners across emitters and verify getMaxListeners uses templateEmitter + cq.setMaxListeners(20); + expect(cq.getMaxListeners()).to.equal(20); + + // collect raw listeners + const raw = cq.rawListeners('test'); + expect(raw.length).to.be.greaterThan(0); + + // event names come from underlying imq + const names = cq.eventNames(); + expect(names).to.be.an('array'); + expect(names.map(String)).to.include('test'); + + // listener count is aggregated via templateEmitter method applied on imq[0] + expect(cq.listenerCount('test')).to.equal(1); + + // emit should return true + expect(cq.emit('test', 1, 2, 3)).to.equal(true); + expect(handler.calledOnce).to.be.true; + }); +}); diff --git a/test/ClusteredRedisQueue.initialize.spec.ts b/test/ClusteredRedisQueue.initialize.spec.ts new file mode 100644 index 0000000..90520e8 --- /dev/null +++ b/test/ClusteredRedisQueue.initialize.spec.ts @@ -0,0 +1,37 @@ +/*! + * Additional tests for ClusteredRedisQueue.initializeQueue branches + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { ClusteredRedisQueue, RedisQueue } from '../src'; +import { ClusterManager } from '../src/ClusterManager'; + +describe('ClusteredRedisQueue.initializeQueue()', () => { + it('should call imq.start when started and imq.subscribe when subscription is set', async () => { + const startStub = sinon.stub(RedisQueue.prototype as any, 'start').resolves(undefined); + const subscribeStub = sinon.stub(RedisQueue.prototype as any, 'subscribe').resolves(); + + const clusterManager = new (ClusterManager as any)(); + const cq: any = new ClusteredRedisQueue('InitCover', { clusterManagers: [clusterManager] }); + + // mark started and set subscription using public APIs + await cq.start(); + const channel = 'X'; + const handler = () => undefined; + await cq.subscribe(channel, handler); + + // adding a server triggers initializeQueue which should call start and subscribe + cq.addServer({ host: '127.0.0.1', port: 6453 }); + + // allow promises to resolve + await new Promise(res => setTimeout(res, 0)); + + expect(startStub.called).to.be.true; + expect(subscribeStub.called).to.be.true; + + startStub.restore(); + subscribeStub.restore(); + await cq.destroy(); + }); +}); diff --git a/test/ClusteredRedisQueue.matchServers.spec.ts b/test/ClusteredRedisQueue.matchServers.spec.ts new file mode 100644 index 0000000..adf16c2 --- /dev/null +++ b/test/ClusteredRedisQueue.matchServers.spec.ts @@ -0,0 +1,34 @@ +/*! + * Tests for ClusteredRedisQueue.matchServers combinations + */ +import './mocks'; +import { expect } from 'chai'; +import { ClusteredRedisQueue } from '../src'; + +// Access private static via casting +const match = (ClusteredRedisQueue as any).matchServers as ( + source: any, target: any, strict?: boolean +) => boolean; + +describe('ClusteredRedisQueue.matchServers()', () => { + it('should return sameAddress when no ids provided', () => { + expect(match({ host: 'h', port: 1 }, { host: 'h', port: 1 })).to.be.true; + expect(match({ host: 'h', port: 1 }, { host: 'h', port: 2 })).to.be.false; + }); + + it('should use strict logic when strict=true', () => { + // same id and same address -> true + expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'a', host: 'h', port: 1 }, true)).to.be.true; + // same id but different address -> false + expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'a', host: 'h', port: 2 }, true)).to.be.false; + // different id but same address -> false + expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'b', host: 'h', port: 1 }, true)).to.be.false; + }); + + it('should use relaxed logic when strict=false', () => { + // id matches -> true even if address differs + expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'a', host: 'h', port: 2 }, false)).to.be.true; + // address matches -> true even if id differs + expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'b', host: 'h', port: 1 }, false)).to.be.true; + }); +}); diff --git a/test/ClusteredRedisQueue.ts b/test/ClusteredRedisQueue.ts index dbd60ac..3d37e8e 100644 --- a/test/ClusteredRedisQueue.ts +++ b/test/ClusteredRedisQueue.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import * as mocks from './mocks'; +import { logger } from './mocks'; import { expect } from 'chai'; import * as sinon from 'sinon'; import { ClusteredRedisQueue } from '../src'; @@ -30,7 +30,7 @@ import { ClusterManager } from '../src/ClusterManager'; process.setMaxListeners(100); const clusterConfig = { - logger: mocks.logger, + logger, cluster: [{ host: '127.0.0.1', port: 7777 @@ -163,14 +163,14 @@ describe('ClusteredRedisQueue', function() { 'TestClusteredQueueOne', { clusterManagers: [clusterManager], - logger: mocks.logger, + logger, }, ); const cqTwo: any = new ClusteredRedisQueue( 'TestClusteredQueueTwo', { clusterManagers: [clusterManager], - logger: mocks.logger, + logger, }, ); const message = { 'hello': 'world' }; @@ -238,7 +238,7 @@ describe('ClusteredRedisQueue', function() { 'TestClusteredQueue', { clusterManagers: [clusterManager], - logger: mocks.logger, + logger, }, ); const channel = 'TestChannel'; diff --git a/test/RedisQueue.cleanup.catch.spec.ts b/test/RedisQueue.cleanup.catch.spec.ts new file mode 100644 index 0000000..8066318 --- /dev/null +++ b/test/RedisQueue.cleanup.catch.spec.ts @@ -0,0 +1,41 @@ +/*! + * Additional RedisQueue tests: processCleanup catch branch + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue } from '../src'; +import { logger as testLogger } from './mocks'; + +function makeLogger() { + return { + log: (..._args: any[]) => undefined, + info: (..._args: any[]) => undefined, + warn: (..._args: any[]) => undefined, + error: (..._args: any[]) => undefined, + } as any; +} + +describe('RedisQueue.processCleanup catch path', function() { + this.timeout(10000); + + it('should log a warning when processCleanup throws', async () => { + const logger = makeLogger(); + const warnSpy = sinon.spy(logger, 'warn'); + const rq: any = new RedisQueue('CleanupCatch', { + logger, + cleanup: true, + }); + + await rq.start(); + // Stub writer.client to throw to hit the catch branch + const stub = sinon.stub(rq.writer, 'client').throws(new Error('LIST failed')); + + await rq.processCleanup(); + + expect(warnSpy.called).to.be.true; + + stub.restore(); + await rq.destroy(); + }); +}); diff --git a/test/UDPClusterManager.ts b/test/UDPClusterManager.ts index 249e690..b3fb8f3 100644 --- a/test/UDPClusterManager.ts +++ b/test/UDPClusterManager.ts @@ -21,11 +21,11 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ +import './mocks'; import { expect } from 'chai'; import { UDPClusterManager } from '../src'; import * as sinon from 'sinon'; import { Socket } from 'dgram'; -import * as os from 'os'; const testMessageUp = 'name\tid\tup\taddress\ttimeout'; const testMessageDown = 'name\tid\tdown\taddress\ttimeout'; diff --git a/test/copyEventEmitter.ts b/test/copyEventEmitter.ts index 95faeb0..e5a36ca 100644 --- a/test/copyEventEmitter.ts +++ b/test/copyEventEmitter.ts @@ -19,6 +19,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ +import './mocks'; import { EventEmitter } from 'events'; import { expect } from 'chai'; import { copyEventEmitter } from '../src'; diff --git a/test/profile.rejection.spec.ts b/test/profile.rejection.spec.ts new file mode 100644 index 0000000..bc4b7f0 --- /dev/null +++ b/test/profile.rejection.spec.ts @@ -0,0 +1,33 @@ +/*! + * Additional profile tests for async rejection catch path + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { profile } from '..'; +import * as core from '..'; + +class RejectingClass { + public logger: any = { info: () => undefined, error: () => undefined }; + + @profile({ enableDebugTime: true, enableDebugArgs: true }) + public async willReject(): Promise { + return Promise.reject(new Error('boom')); + } +} + +describe('profile() async rejection path', () => { + it('should log via logger when async method rejects', async () => { + const logger = { info: sinon.spy(), error: () => undefined } as any; + const obj = new RejectingClass(); + obj.logger = logger; + try { + await obj.willReject(); + } catch (e) { + // expected + } + // allow microtask queue + await new Promise(res => setTimeout(res, 0)); + expect(logger.info.called).to.be.true; + }); +}); diff --git a/test/profile.ts b/test/profile.ts index c77312e..f819e90 100644 --- a/test/profile.ts +++ b/test/profile.ts @@ -21,6 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ +import './mocks'; import { expect } from 'chai'; import * as sinon from 'sinon'; import * as mock from 'mock-require'; diff --git a/test/promisify.ts b/test/promisify.ts index a14fb4f..86eea56 100644 --- a/test/promisify.ts +++ b/test/promisify.ts @@ -21,6 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ +import './mocks'; import { expect } from 'chai'; import * as sinon from 'sinon'; import { promisify } from '..'; diff --git a/test/uuid.ts b/test/uuid.ts index 37ca78c..7543f65 100644 --- a/test/uuid.ts +++ b/test/uuid.ts @@ -21,6 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ +import './mocks'; import { expect } from 'chai'; import { uuid } from '..'; From 7f85eb6d989c78c2dc9e950e082b11d1919ac35f Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 13:41:14 +0200 Subject: [PATCH 16/78] feat: add more tests to improve coverage --- test/RedisQueue.processCleanup.extra.spec.ts | 38 +++++++++++++++ ...sterManager.selectNetworkInterface.spec.ts | 46 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 test/RedisQueue.processCleanup.extra.spec.ts create mode 100644 test/UDPClusterManager.selectNetworkInterface.spec.ts diff --git a/test/RedisQueue.processCleanup.extra.spec.ts b/test/RedisQueue.processCleanup.extra.spec.ts new file mode 100644 index 0000000..761a27f --- /dev/null +++ b/test/RedisQueue.processCleanup.extra.spec.ts @@ -0,0 +1,38 @@ +/*! + * Additional RedisQueue tests for processCleanup branches + */ +import './mocks'; +import { expect } from 'chai'; +import { RedisQueue, uuid } from '../src'; +import { RedisClientMock } from './mocks'; + +describe('RedisQueue.processCleanup extra branches', function() { + this.timeout(5000); + + it('should remove scanned keys that do not match any connectedKey (different prefix)', async () => { + const name = uuid(); + const rq: any = new RedisQueue(name, { + logger: console, + cleanup: true, + prefix: 'imqX', + cleanupFilter: '*', + }); + + // start to create reader/writer/watcher with connection names + await rq.start(); + + // Create an orphan worker key with a different prefix so it won't include any connectedKey + const orphanKey = 'imqY:orphan:worker:someuuid:123456'; + (RedisClientMock as any).__queues__[orphanKey] = ['payload']; + + // Sanity: ensure the key is present before cleanup + expect((RedisClientMock as any).__queues__[orphanKey]).to.be.ok; + + await rq.processCleanup(); + + // The orphan key should be deleted by cleanup (true branch of keysToRemove filter) + expect((RedisClientMock as any).__queues__[orphanKey]).to.be.undefined; + + await rq.destroy(); + }); +}); diff --git a/test/UDPClusterManager.selectNetworkInterface.spec.ts b/test/UDPClusterManager.selectNetworkInterface.spec.ts new file mode 100644 index 0000000..aad8390 --- /dev/null +++ b/test/UDPClusterManager.selectNetworkInterface.spec.ts @@ -0,0 +1,46 @@ +/*! + * UDPClusterManager.selectNetworkInterface() branch coverage tests + */ +import './mocks'; +import { expect } from 'chai'; +import * as mock from 'mock-require'; + +describe('UDPClusterManager.selectNetworkInterface()', () => { + it('should return default when broadcastAddress is undefined', async () => { + const { UDPClusterManager } = await import('../src'); + const select = (UDPClusterManager as any).selectNetworkInterface as Function; + const res = select({}); + expect(res).to.equal('0.0.0.0'); + }); + + it('should return default when broadcastAddress equals limitedBroadcastAddress', async () => { + const { UDPClusterManager } = await import('../src'); + const select = (UDPClusterManager as any).selectNetworkInterface as Function; + const res = select({ broadcastAddress: '127.0.0.255', limitedBroadcastAddress: '127.0.0.255' }); + expect(res).to.equal('0.0.0.0'); + }); + + it('should continue on undefined interface entry and still select matching address', () => { + // Re-mock os.networkInterfaces to include an undefined entry + const os = require('node:os'); + const networkInterfaces = () => ({ bad: undefined, lo: [{ address: '127.0.0.1', family: 'IPv4' }] }); + mock.stop('os'); + mock('os', Object.assign({}, os, { networkInterfaces })); + // Re-require the module to capture new binding + const { UDPClusterManager } = mock.reRequire('../src/UDPClusterManager'); + const res = (UDPClusterManager as any).selectNetworkInterface({ broadcastAddress: '127.0.0.255', limitedBroadcastAddress: '255.255.255.255' }); + expect(res).to.equal('127.0.0.1'); + + // restore to base mocks for other tests + mock.stop('os'); + mock.reRequire('./mocks/os'); + mock.reRequire('../src/UDPClusterManager'); + }); + + it('should select matching interface address when not equal to limited broadcast', async () => { + const { UDPClusterManager } = await import('../src'); + const select = (UDPClusterManager as any).selectNetworkInterface as Function; + const res = select({ broadcastAddress: '127.0.0.255', limitedBroadcastAddress: '255.255.255.255' }); + expect(res).to.equal('127.0.0.1'); + }); +}); From b9f6a5a02f5b51f29c0758b09a98bdf40dc3a8e8 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 14:30:58 +0200 Subject: [PATCH 17/78] feat: add more tests to improve test coverage --- ...usteredRedisQueue.addServer.noInit.spec.ts | 32 +++++++ test/UDPClusterManager.destroySocket.spec.ts | 51 +++++++++++ test/UDPClusterManager.extra.branches.spec.ts | 89 +++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 test/ClusteredRedisQueue.addServer.noInit.spec.ts create mode 100644 test/UDPClusterManager.destroySocket.spec.ts create mode 100644 test/UDPClusterManager.extra.branches.spec.ts diff --git a/test/ClusteredRedisQueue.addServer.noInit.spec.ts b/test/ClusteredRedisQueue.addServer.noInit.spec.ts new file mode 100644 index 0000000..8666387 --- /dev/null +++ b/test/ClusteredRedisQueue.addServer.noInit.spec.ts @@ -0,0 +1,32 @@ +/*! + * Cover ClusteredRedisQueue.addServerWithQueueInitializing with initializeQueue=false + */ +import './mocks'; +import { expect } from 'chai'; +import { ClusteredRedisQueue } from '../src'; +import { ClusterManager } from '../src/ClusterManager'; + +const server = { host: '127.0.0.1', port: 6380 }; + +describe('ClusteredRedisQueue.addServerWithQueueInitializing(false)', () => { + it('should add server without initializing queue and not emit initialized', async () => { + const manager = new (ClusterManager as any)(); + const cq: any = new ClusteredRedisQueue('NoInit', { clusterManagers: [manager] }); + + let initializedCalled = false; + (cq as any).clusterEmitter.on('initialized', () => { initializedCalled = true; }); + + // call private method via any to cover branch + (cq as any).addServerWithQueueInitializing(server, false); + + // should have server and imq added + expect(cq.servers.length).to.be.greaterThan(0); + expect(cq.imqs.length).to.be.greaterThan(0); + // queueLength updated + expect(cq.queueLength).to.equal(cq.imqs.length); + // initialized not emitted + expect(initializedCalled).to.equal(false); + + await cq.destroy(); + }); +}); diff --git a/test/UDPClusterManager.destroySocket.spec.ts b/test/UDPClusterManager.destroySocket.spec.ts new file mode 100644 index 0000000..022b5d6 --- /dev/null +++ b/test/UDPClusterManager.destroySocket.spec.ts @@ -0,0 +1,51 @@ +/*! + * UDPClusterManager.destroySocket() branch coverage tests + */ +import './mocks'; +import { expect } from 'chai'; +import { UDPClusterManager } from '../src'; + +describe('UDPClusterManager.destroySocket()', () => { + it('should resolve when socket has no close() function', async () => { + const destroy = (UDPClusterManager as any).destroySocket as Function; + const fakeSocket: any = { /* no close, no removeAllListeners */ }; + + await destroy('0.0.0.0:63000', fakeSocket); + }); + + it('should reject when removeAllListeners throws inside try-block', async () => { + const destroy = (UDPClusterManager as any).destroySocket as Function; + const fakeSocket: any = { + removeAllListeners: () => { throw new Error('boom'); }, + close: (cb: Function) => cb && cb(), + }; + + let thrown = null as any; + try { + await destroy('1.1.1.1:63000', fakeSocket); + } catch (e) { + thrown = e; + } + expect(thrown).to.be.instanceOf(Error); + expect((thrown as Error).message).to.equal('boom'); + }); + + it('should remove socket entry and unref after successful close()', async () => { + const destroy = (UDPClusterManager as any).destroySocket as Function; + const sockets = (UDPClusterManager as any).sockets as Record; + const key = '9.9.9.9:65000'; + + let unrefCalled = false; + const fakeSocket: any = { + removeAllListeners: () => {}, + close: (cb: Function) => cb && cb(), + unref: () => { unrefCalled = true; }, + }; + + sockets[key] = fakeSocket; + await destroy(key, fakeSocket); + + expect(unrefCalled).to.equal(true); + expect(sockets[key]).to.be.undefined; + }); +}); diff --git a/test/UDPClusterManager.extra.branches.spec.ts b/test/UDPClusterManager.extra.branches.spec.ts new file mode 100644 index 0000000..a862231 --- /dev/null +++ b/test/UDPClusterManager.extra.branches.spec.ts @@ -0,0 +1,89 @@ +/*! + * Additional branch coverage for UDPClusterManager + */ +import './mocks'; +import { expect } from 'chai'; +import { UDPClusterManager } from '../src'; + +describe('UDPClusterManager additional branches', () => { + describe('processMessageOnCluster added-path', () => { + const processMessageOnCluster = (UDPClusterManager as any).processMessageOnCluster as any; + + it('should call serverAliveWait when server is added and found (added truthy)', async () => { + const calls: any[] = []; + const addedServer: any = { id: 'id', host: '127.0.0.1', port: 6379 }; + const cluster: any = { + add: (message: any) => { calls.push(['add', message]); }, + find: (message: any, strict?: boolean) => strict ? addedServer : undefined, + }; + const original = (UDPClusterManager as any).serverAliveWait; + let waited = false; + (UDPClusterManager as any).serverAliveWait = (...args: any[]) => { + waited = true; + }; + + processMessageOnCluster(cluster, { id: 'id', name: 'n', type: 'up', host: 'h', port: 1, timeout: 0 }, 5); + + // allow microtask queue + await new Promise(res => setTimeout(res, 0)); + + expect(waited).to.equal(true); + // restore + (UDPClusterManager as any).serverAliveWait = original; + }); + + it('should not call serverAliveWait when added not found', async () => { + const cluster: any = { + add: (_: any) => undefined, + find: (_: any, __?: boolean) => undefined, + }; + const original = (UDPClusterManager as any).serverAliveWait; + let waited = false; + (UDPClusterManager as any).serverAliveWait = () => { waited = true; }; + + processMessageOnCluster(cluster, { id: 'id', name: 'n', type: 'up', host: 'h', port: 1, timeout: 0 }, 5); + await new Promise(res => setTimeout(res, 0)); + + expect(waited).to.equal(false); + (UDPClusterManager as any).serverAliveWait = original; + }); + }); + + describe('serverAliveWait branches', () => { + const serverAliveWait = (UDPClusterManager as any).serverAliveWait as any; + + it('should return early when computed timeout is <= 0', () => { + const cluster: any = { find: () => ({}) }; + const server: any = {}; + + serverAliveWait(cluster, server, 0); // no message and correction 0 => timeout 0 + + expect(server.timer).to.equal(undefined); + }); + + it('should remove when no timestamp is present on existing (timer callback path)', (done) => { + const removed: any[] = []; + const server: any = { timeout: 0 }; + const cluster: any = { + find: () => server, + remove: (s: any) => { removed.push(s); }, + }; + + // use small correction to trigger timeout quickly + serverAliveWait(cluster, server, 1); + + // wipe timestamp before timeout fires to force the branch + server.timestamp = undefined; + + setTimeout(() => { + try { + expect(removed.length).to.equal(1); + expect(server.timer).to.equal(undefined); + done(); + } catch (e) { + done(e as any); + } + }, 10); + }); + }); +}); From 93fe2cbb52d23fc8751eaef32ad2b8466de50fec Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 14:52:43 +0200 Subject: [PATCH 18/78] feat: add more tests to improve coverage --- test/RedisQueue.processDelayed.catch.spec.ts | 46 ++++++++++++++++++++ test/RedisQueue.send.extra.branches.spec.ts | 41 +++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 test/RedisQueue.processDelayed.catch.spec.ts create mode 100644 test/RedisQueue.send.extra.branches.spec.ts diff --git a/test/RedisQueue.processDelayed.catch.spec.ts b/test/RedisQueue.processDelayed.catch.spec.ts new file mode 100644 index 0000000..82fa5ca --- /dev/null +++ b/test/RedisQueue.processDelayed.catch.spec.ts @@ -0,0 +1,46 @@ +/*! + * Additional RedisQueue tests: processDelayed() catch branch + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue } from '../src'; + +function makeLogger() { + return { + log: (..._args: any[]) => undefined, + info: (..._args: any[]) => undefined, + warn: (..._args: any[]) => undefined, + error: (..._args: any[]) => undefined, + } as any; +} + +describe('RedisQueue.processDelayed extra branches', function() { + this.timeout(10000); + + it('should emit error when evalsha throws', async () => { + const logger = makeLogger(); + const rq: any = new RedisQueue('ProcessDelayedCatch', { logger }); + await rq.start(); + + // Force checksum to exist to enter evalsha path + rq.scripts.moveDelayed.checksum = 'deadbeef'; + + const err = new Error('evalsha failed'); + const emitErrorStub = sinon.stub((RedisQueue as any).prototype, 'emitError'); + + // Temporarily drop writer to force a synchronous error in processDelayed + const originalWriter = rq.writer; + rq['writer'] = undefined; + + await rq['processDelayed'](rq.key); + + expect(emitErrorStub.called).to.be.true; + expect(emitErrorStub.firstCall.args[0]).to.equal('OnProcessDelayed'); + + // Restore writer and cleanup + rq['writer'] = originalWriter; + emitErrorStub.restore(); + await rq.destroy(); + }); +}); diff --git a/test/RedisQueue.send.extra.branches.spec.ts b/test/RedisQueue.send.extra.branches.spec.ts new file mode 100644 index 0000000..36da896 --- /dev/null +++ b/test/RedisQueue.send.extra.branches.spec.ts @@ -0,0 +1,41 @@ +/*! + * Additional RedisQueue tests: send() extra branches + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue, IMQMode } from '../src'; +import { logger as testLogger } from './mocks'; + +function makeLogger() { + return { + log: (..._args: any[]) => undefined, + info: (..._args: any[]) => undefined, + warn: (..._args: any[]) => undefined, + error: (..._args: any[]) => undefined, + } as any; +} + +describe('RedisQueue.send() extra branches', function() { + this.timeout(10000); + + it('should throw when writer is still uninitialized after start()', async () => { + const logger = makeLogger(); + const rq: any = new RedisQueue('SendNoWriter', { logger }, IMQMode.PUBLISHER); + // Force start to be a no-op so writer remains undefined + const startStub = sinon.stub(rq, 'start').resolves(rq); + + let thrown: any; + try { + await rq.send('AnyQueue', { test: true }); + } catch (err) { + thrown = err; + } + + expect(thrown).to.be.instanceof(TypeError); + expect(`${thrown}`).to.include('unable to initialize queue'); + + startStub.restore(); + await rq.destroy().catch(() => undefined); + }); +}); From fd897191277399cf7951a502d818430bee0b2c66 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 15:14:53 +0200 Subject: [PATCH 19/78] feat: add more tests to improve coverage --- test/RedisQueue.publish.spec.ts | 70 +++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 test/RedisQueue.publish.spec.ts diff --git a/test/RedisQueue.publish.spec.ts b/test/RedisQueue.publish.spec.ts new file mode 100644 index 0000000..b4f48d7 --- /dev/null +++ b/test/RedisQueue.publish.spec.ts @@ -0,0 +1,70 @@ +/*! + * Additional RedisQueue tests: publish() branches + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue, IMQMode } from '../src'; + +function makeLogger() { + return { + log: (..._args: any[]) => undefined, + info: (..._args: any[]) => undefined, + warn: (..._args: any[]) => undefined, + error: (..._args: any[]) => undefined, + } as any; +} + +describe('RedisQueue.publish()', function() { + this.timeout(10000); + + it('should throw when writer is not connected', async () => { + const logger = makeLogger(); + const rq: any = new RedisQueue('PubNoWriter', { logger }, IMQMode.PUBLISHER); + + let thrown: any; + try { + await rq.publish({ a: 1 }); + } catch (err) { + thrown = err; + } + + expect(thrown).to.be.instanceof(TypeError); + expect(`${thrown}`).to.include('Writer is not connected'); + + await rq.destroy().catch(() => undefined); + }); + + it('should publish to default channel when writer is connected', async () => { + const logger = makeLogger(); + const rq: any = new RedisQueue('PubDefault', { logger }, IMQMode.PUBLISHER); + await rq.start(); + + const pubSpy = sinon.spy((rq as any).writer, 'publish'); + await rq.publish({ hello: 'world' }); + + expect(pubSpy.called).to.equal(true); + const [channel, msg] = pubSpy.getCall(0).args; + expect(channel).to.equal('imq:PubDefault'); + expect(() => JSON.parse(msg)).not.to.throw(); + + pubSpy.restore(); + await rq.destroy().catch(() => undefined); + }); + + it('should publish to provided toName channel when given', async () => { + const logger = makeLogger(); + const rq: any = new RedisQueue('PubOther', { logger }, IMQMode.PUBLISHER); + await rq.start(); + + const pubSpy = sinon.spy((rq as any).writer, 'publish'); + await rq.publish({ t: true }, 'OtherChannel'); + + expect(pubSpy.called).to.equal(true); + const [channel] = pubSpy.getCall(0).args; + expect(channel).to.equal('imq:OtherChannel'); + + pubSpy.restore(); + await rq.destroy().catch(() => undefined); + }); +}); From c25c87c512cc790206e79ec8be66b0b96b909726 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 15:31:59 +0200 Subject: [PATCH 20/78] feat: add more tests to improve coverage --- test/RedisQueue.send.worker.mode.spec.ts | 36 ++++++++++++++++ test/RedisQueue.unsubscribe.spec.ts | 53 ++++++++++++++++++++++++ test/UDPClusterManager.free.spec.ts | 27 ++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 test/RedisQueue.send.worker.mode.spec.ts create mode 100644 test/RedisQueue.unsubscribe.spec.ts create mode 100644 test/UDPClusterManager.free.spec.ts diff --git a/test/RedisQueue.send.worker.mode.spec.ts b/test/RedisQueue.send.worker.mode.spec.ts new file mode 100644 index 0000000..213e987 --- /dev/null +++ b/test/RedisQueue.send.worker.mode.spec.ts @@ -0,0 +1,36 @@ +/*! + * Additional RedisQueue tests: send() worker-only mode error + */ +import './mocks'; +import { expect } from 'chai'; +import { RedisQueue, IMQMode } from '../src'; + +function makeLogger() { + return { + log: (..._args: any[]) => undefined, + info: (..._args: any[]) => undefined, + warn: (..._args: any[]) => undefined, + error: (..._args: any[]) => undefined, + } as any; +} + +describe('RedisQueue.send() worker-only mode', function() { + this.timeout(10000); + + it('should throw when called in WORKER only mode', async () => { + const logger = makeLogger(); + const rq: any = new RedisQueue('WorkerOnly', { logger }, IMQMode.WORKER); + + let thrown: any; + try { + await rq.send('AnyQueue', { test: true }); + } catch (err) { + thrown = err; + } + + expect(thrown).to.be.instanceof(TypeError); + expect(`${thrown}`).to.include('WORKER only mode'); + + await rq.destroy().catch(() => undefined); + }); +}); diff --git a/test/RedisQueue.unsubscribe.spec.ts b/test/RedisQueue.unsubscribe.spec.ts new file mode 100644 index 0000000..a5af0e8 --- /dev/null +++ b/test/RedisQueue.unsubscribe.spec.ts @@ -0,0 +1,53 @@ +/*! + * Additional RedisQueue tests: unsubscribe() cleanup path + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue } from '../src'; + +function makeLogger() { + return { + log: (..._args: any[]) => undefined, + info: (..._args: any[]) => undefined, + warn: (..._args: any[]) => undefined, + error: (..._args: any[]) => undefined, + } as any; +} + +describe('RedisQueue.unsubscribe()', function() { + this.timeout(10000); + + it('should cleanup subscription channel when present', async () => { + const logger = makeLogger(); + const rq: any = new RedisQueue('SubUnsub', { logger }); + await rq.start(); + + const handler = sinon.spy(); + await rq.subscribe('SubUnsub', handler); + + expect(rq.subscription).to.be.ok; + expect(rq.subscriptionName).to.equal('SubUnsub'); + + const unsubSpy = sinon.spy(rq.subscription, 'unsubscribe'); + const ralSpy = sinon.spy(rq.subscription, 'removeAllListeners'); + const disconnectSpy = sinon.spy(rq.subscription, 'disconnect'); + const quitSpy = sinon.spy(rq.subscription, 'quit'); + + await rq.unsubscribe(); + + expect(unsubSpy.calledOnce).to.equal(true); + expect(ralSpy.calledOnce).to.equal(true); + expect(disconnectSpy.calledOnce).to.equal(true); + expect(quitSpy.calledOnce).to.equal(true); + expect(rq.subscription).to.equal(undefined); + expect(rq.subscriptionName).to.equal(undefined); + + unsubSpy.restore(); + ralSpy.restore(); + disconnectSpy.restore(); + quitSpy.restore(); + + await rq.destroy().catch(() => undefined); + }); +}); diff --git a/test/UDPClusterManager.free.spec.ts b/test/UDPClusterManager.free.spec.ts new file mode 100644 index 0000000..160d8e8 --- /dev/null +++ b/test/UDPClusterManager.free.spec.ts @@ -0,0 +1,27 @@ +/*! + * UDPClusterManager.free() coverage test + */ +import './mocks'; +import { expect } from 'chai'; + +describe('UDPClusterManager.free()', () => { + it('should destroy all sockets via destroySocket and clear sockets map', async () => { + const { UDPClusterManager } = await import('../src'); + const sockets = (UDPClusterManager as any).sockets as Record; + // prepare two mock sockets + sockets['0.0.0.0:5555'] = { + removeAllListeners: () => {}, + close: (cb: Function) => cb(), + unref: () => {}, + }; + sockets['0.0.0.0:6666'] = { + removeAllListeners: () => {}, + close: (cb: Function) => cb(), + unref: () => {}, + }; + + await (UDPClusterManager as any).free(); + + expect(Object.keys((UDPClusterManager as any).sockets)).to.have.length(0); + }); +}); From 0018e7bb2cc029b7f1bc9d11a65b16968e6b1a09 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 17:07:58 +0200 Subject: [PATCH 21/78] feat: add more tests to improve coverage --- test/copyEventEmitter.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/copyEventEmitter.ts b/test/copyEventEmitter.ts index e5a36ca..00d696d 100644 --- a/test/copyEventEmitter.ts +++ b/test/copyEventEmitter.ts @@ -129,6 +129,29 @@ describe('copyEventEmitter()', function() { copyEventEmitter(source, target); + expect(target.listenerCount(eventName)).to.be.equal(1); + }); + it('should handle onceWrapper-like listener with falsy listener property', () => { + const source = new EventEmitter(); + const target = new EventEmitter(); + + // Create a mock listener that looks like onceWrapper and has a falsy listener property + const mockListener: any = function() {}; + mockListener.listener = 0; // falsy value present + const originalInspect = require('util').inspect; + require('util').inspect = (obj: any) => { + if (obj === mockListener) { + return 'function onceWrapper() { ... }'; + } + return originalInspect(obj); + }; + + source.on(eventName, mockListener as any); + copyEventEmitter(source, target); + + // Restore original inspect + require('util').inspect = originalInspect; + expect(target.listenerCount(eventName)).to.be.equal(1); }); }); From 0a8aa46c0f15f7d4268dfcf619dc23518525ea5a Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 17:08:03 +0200 Subject: [PATCH 22/78] feat: add more tests to improve coverage --- test/IMessageQueue.EventEmitter.spec.ts | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 test/IMessageQueue.EventEmitter.spec.ts diff --git a/test/IMessageQueue.EventEmitter.spec.ts b/test/IMessageQueue.EventEmitter.spec.ts new file mode 100644 index 0000000..b1fe92f --- /dev/null +++ b/test/IMessageQueue.EventEmitter.spec.ts @@ -0,0 +1,41 @@ +/*! + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. + */ +import './mocks'; +import { expect } from 'chai'; +import { EventEmitter as IMQEventEmitter } from '../src'; +import { EventEmitter as NodeEventEmitter } from 'events'; + +// This test ensures the re-exported EventEmitter from IMessageQueue.ts is exercised +// to cover the function counted by nyc/istanbul for that re-export. +describe('IMessageQueue EventEmitter re-export', () => { + it('should re-export Node.js EventEmitter and be usable', () => { + // Ensure it is the same constructor + expect(IMQEventEmitter).to.equal(NodeEventEmitter); + + // And it works as expected when instantiated + const ee = new IMQEventEmitter(); + let called = 0; + ee.on('ping', () => { called++; }); + ee.emit('ping'); + expect(called).to.equal(1); + }); +}); From 60cf4bfd364a01e4f3dfef194858d0fb934e4634 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 17:32:51 +0200 Subject: [PATCH 23/78] feat: add more tests to improve coverage --- test/copyEventEmitter.ts | 84 ++++++++++++++++++++++++++++++++++++++++ test/profile.ts | 7 ++++ 2 files changed, 91 insertions(+) diff --git a/test/copyEventEmitter.ts b/test/copyEventEmitter.ts index 00d696d..99a50de 100644 --- a/test/copyEventEmitter.ts +++ b/test/copyEventEmitter.ts @@ -154,4 +154,88 @@ describe('copyEventEmitter()', function() { expect(target.listenerCount(eventName)).to.be.equal(1); }); + + it('should handle onceWrapper-like listener with undefined listener property', () => { + const source = new EventEmitter(); + const target = new EventEmitter(); + + const mockListener: any = function() {}; + mockListener.listener = undefined; // explicitly undefined + const originalInspect = require('util').inspect; + require('util').inspect = (obj: any) => { + if (obj === mockListener) { + return 'function onceWrapper() { ... }'; + } + return originalInspect(obj); + }; + + source.on(eventName, mockListener as any); + copyEventEmitter(source, target); + + // Restore original inspect + require('util').inspect = originalInspect; + + expect(target.listenerCount(eventName)).to.be.equal(1); + }); + + it('should handle onceWrapper-like listener with truthy listener property', () => { + const source = new EventEmitter(); + const target = new EventEmitter(); + + // Create a mock listener that looks like onceWrapper and has a truthy listener property + let called = 0; + const realListener = () => { called++; }; + const mockListener: any = function() {}; + mockListener.listener = realListener; // truthy function + + const originalInspect = require('util').inspect; + require('util').inspect = (obj: any) => { + if (obj === mockListener) { + return 'function onceWrapper() { ... }'; + } + return originalInspect(obj); + }; + + source.on(eventName, mockListener as any); + copyEventEmitter(source, target); + + // Restore original inspect + require('util').inspect = originalInspect; + + // Ensure the listener was attached via once() and is callable exactly once + expect(target.listenerCount(eventName)).to.be.equal(1); + target.emit(eventName); + target.emit(eventName); + expect(called).to.equal(1); + }); + + it('should handle onceWrapper path when originalListener is undefined', () => { + const source: any = { + eventNames: () => [eventName], + rawListeners: () => [undefined], + getMaxListeners: () => 0, + setMaxListeners: () => {}, + }; + const onceCalls: any[] = []; + const target: any = { + once: (ev: any, listener: any) => { onceCalls.push([ev, listener]); }, + on: () => {}, + }; + const originalInspect = require('util').inspect; + require('util').inspect = (obj: any) => { + if (typeof obj === 'undefined') { + return 'function onceWrapper() { ... }'; + } + return originalInspect(obj); + }; + + copyEventEmitter(source as any, target as any); + + // Restore original inspect + require('util').inspect = originalInspect; + + expect(onceCalls.length).to.equal(1); + expect(onceCalls[0][0]).to.equal(eventName); + expect(onceCalls[0][1]).to.equal(undefined); + }); }); diff --git a/test/profile.ts b/test/profile.ts index f819e90..8ad7433 100644 --- a/test/profile.ts +++ b/test/profile.ts @@ -202,6 +202,13 @@ describe('profile()', function() { expect(error.notCalled).to.be.true; }); + it('should not log when logger method is missing', () => { + const { logDebugInfo } = mock.reRequire('../src/profile'); + const dummyLogger: any = { error: logger.error.bind(logger) }; + logDebugInfo({ ...baseOptions, logger: dummyLogger, logLevel: 'nonexistent' as any }); + expect(log.notCalled).to.be.true; + }); + it('should handle JSON.stringify errors', () => { const { logDebugInfo } = mock.reRequire('../src/profile'); const badJson = { toJSON: () => { throw new Error('bad json'); } }; From 3131ecc835cab81e83854309346a182ac0ebb04f Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 17:48:43 +0200 Subject: [PATCH 24/78] feat: add more tests to improve coverage --- test/profile.more.branches.spec.ts | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 test/profile.more.branches.spec.ts diff --git a/test/profile.more.branches.spec.ts b/test/profile.more.branches.spec.ts new file mode 100644 index 0000000..80f1672 --- /dev/null +++ b/test/profile.more.branches.spec.ts @@ -0,0 +1,57 @@ +/*! + * Additional profile.ts branch coverage tests + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as mock from 'mock-require'; + +// We re-require the module inside tests to pick up env changes when needed + +describe('profile.ts additional branches', () => { + afterEach(() => { + mock.stopAll(); + delete (process as any).env.IMQ_LOG_TIME_FORMAT; + }); + + it('logDebugInfo: should not attempt to call missing log method (no-op path)', () => { + const { logDebugInfo, LogLevel } = mock.reRequire('../src/profile'); + const fakeLogger: any = { + // intentionally no 'log' or 'info' method for selected level + error: sinon.spy(), + }; + const options = { + debugTime: true, + debugArgs: true, + className: 'X', + args: [1, { a: 2 }], + methodName: 'm', + start: (process.hrtime as any).bigint(), + logger: fakeLogger, + logLevel: LogLevel.LOG, + }; + expect(() => logDebugInfo(options)).to.not.throw(); + // ensures error not called due to serialization success + expect(fakeLogger.error.called).to.equal(false); + }); + + it('logDebugInfo: should call logger.error on JSON.stringify error (BigInt arg)', () => { + const { logDebugInfo, LogLevel } = mock.reRequire('../src/profile'); + const fakeLogger: any = { + error: sinon.spy(), + }; + const args = [BigInt(1)]; // JSON.stringify throws on BigInt + const options = { + debugTime: false, + debugArgs: true, + className: 'Y', + args, + methodName: 'n', + start: (process.hrtime as any).bigint(), + logger: fakeLogger, + logLevel: LogLevel.INFO, + }; + logDebugInfo(options); + expect(fakeLogger.error.calledOnce).to.equal(true); + }); +}); From 9433e68c5ab9a836f0a6fa2e7e37701beb5751fd Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 18:01:30 +0200 Subject: [PATCH 25/78] feat: add more tests to improve coverage --- test/UDPClusterManager.parseAndStart.spec.ts | 38 ++++++++++++++++++++ test/profile.decorator.branches.spec.ts | 34 ++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 test/UDPClusterManager.parseAndStart.spec.ts create mode 100644 test/profile.decorator.branches.spec.ts diff --git a/test/UDPClusterManager.parseAndStart.spec.ts b/test/UDPClusterManager.parseAndStart.spec.ts new file mode 100644 index 0000000..0e5434a --- /dev/null +++ b/test/UDPClusterManager.parseAndStart.spec.ts @@ -0,0 +1,38 @@ +/*! + * UDPClusterManager parseBroadcastedMessage and startListening branch tests + */ +import './mocks'; +import { expect } from 'chai'; +import { UDPClusterManager } from '../src'; + +/** + * Covers default parameters in startListening(options = {}) and + * default destructuring for address = '' and timeout = '0' in + * parseBroadcastedMessage(). + */ +describe('UDPClusterManager parse/start branches', () => { + it('parseBroadcastedMessage: should apply defaults for empty address and timeout', () => { + const parse = (UDPClusterManager as any).parseBroadcastedMessage as Function; + const buf = Buffer.from(['name', 'id', 'UP'].join('\t')); + const msg = parse(buf); + expect(msg).to.include({ name: 'name', id: 'id', type: 'up' }); + // address default => '' leads to host '' and port NaN + expect(msg.host).to.equal(''); + expect(Number.isNaN(msg.port)).to.equal(true); + // timeout default '0' => 0 ms + expect(msg.timeout).to.equal(0); + }); + + it('startListening: should call listenBroadcastedMessages when called without options', () => { + const mgr: any = new (UDPClusterManager as any)(); + let called = false; + const original = mgr.listenBroadcastedMessages; + mgr.listenBroadcastedMessages = (..._args: any[]) => { called = true; }; + try { + (mgr as any).startListening(); + expect(called).to.equal(true); + } finally { + mgr.listenBroadcastedMessages = original; + } + }); +}); diff --git a/test/profile.decorator.branches.spec.ts b/test/profile.decorator.branches.spec.ts new file mode 100644 index 0000000..aa9c08a --- /dev/null +++ b/test/profile.decorator.branches.spec.ts @@ -0,0 +1,34 @@ +/*! + * Additional tests to cover remaining branches in profile decorator + */ +import './mocks'; +import { expect } from 'chai'; +import { profile, LogLevel } from '..'; + +// Note: We intentionally call decorated methods without a "this" context +// to exercise the (this || target) branches inside the decorator wrapper. + +describe('profile decorator extra branches', () => { + it('should return early via original.apply(target, ...) when both debug flags are false and this is undefined', () => { + class T1 { + @profile({ enableDebugTime: false, enableDebugArgs: false, logLevel: LogLevel.LOG }) + public m(...args: any[]) { return args; } + } + const o = new T1(); + const fn = Object.getPrototypeOf(o).m as Function; // wrapper + const res = fn.call(undefined, 1, 2, 3); + expect(res).to.deep.equal([1, 2, 3]); + }); + + it('should execute debug path with (this || target) picking target and logLevel fallback to IMQ_LOG_LEVEL', () => { + class T2 { + // no logger on prototype; calling with undefined this picks target + @profile({ enableDebugTime: true, enableDebugArgs: true, logLevel: undefined as any }) + public m(..._args: any[]) { /* noop */ } + } + const o = new T2(); + const fn = Object.getPrototypeOf(o).m as Function; // wrapper + // provide serializable args to avoid logger.error path when logger is undefined + expect(() => fn.call(undefined, 1, { a: 2 }, 'x')).to.not.throw(); + }); +}); From e8f326eac5837edbe7bf3094bf12cae2679c918f Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 18:37:09 +0200 Subject: [PATCH 26/78] feat: add more tests to improve coverage --- test/RedisQueue.connect.fallbacks.spec.ts | 40 ++++++++++++++ ...Queue.processCleanup.clientsFilter.spec.ts | 53 +++++++++++++++++++ ...edisQueue.processCleanup.multiscan.spec.ts | 42 +++++++++++++++ ...edisQueue.processCleanup.nullmatch.spec.ts | 49 +++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 test/RedisQueue.connect.fallbacks.spec.ts create mode 100644 test/RedisQueue.processCleanup.clientsFilter.spec.ts create mode 100644 test/RedisQueue.processCleanup.multiscan.spec.ts create mode 100644 test/RedisQueue.processCleanup.nullmatch.spec.ts diff --git a/test/RedisQueue.connect.fallbacks.spec.ts b/test/RedisQueue.connect.fallbacks.spec.ts new file mode 100644 index 0000000..60cb217 --- /dev/null +++ b/test/RedisQueue.connect.fallbacks.spec.ts @@ -0,0 +1,40 @@ +/*! + * Additional RedisQueue tests: connect() option fallbacks branches + */ +import './mocks'; +import { expect } from 'chai'; +import { RedisQueue, IMQMode } from '../src'; + +function makeLogger() { + return { + log: (..._args: any[]) => undefined, + info: (..._args: any[]) => undefined, + warn: (..._args: any[]) => undefined, + error: (..._args: any[]) => undefined, + } as any; +} + +describe('RedisQueue.connect() option fallbacks', function() { + this.timeout(10000); + + it('should use fallback values when falsy options are provided', async () => { + const logger = makeLogger(); + // Intentionally provide falsy values to trigger `||` fallbacks in connect() + const rq: any = new RedisQueue('ConnFallbacks', { + logger, + port: 0 as unknown as number, // falsy to trigger 6379 fallback + host: '' as unknown as string, // falsy to trigger 'localhost' fallback + prefix: '' as unknown as string, // falsy to trigger '' fallback in connectionName + cleanup: false, + }, IMQMode.BOTH); + + await rq.start(); + + // Basic sanity: writer/reader/watcher are created + expect(Boolean(rq.writer)).to.equal(true); + expect(Boolean(rq.reader)).to.equal(true); + expect(Boolean(rq.watcher)).to.equal(true); + + await rq.destroy(); + }); +}); diff --git a/test/RedisQueue.processCleanup.clientsFilter.spec.ts b/test/RedisQueue.processCleanup.clientsFilter.spec.ts new file mode 100644 index 0000000..98bde70 --- /dev/null +++ b/test/RedisQueue.processCleanup.clientsFilter.spec.ts @@ -0,0 +1,53 @@ +/*! + * Additional RedisQueue tests: processCleanup connectedKeys filter branches + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue, uuid } from '../src'; + +describe('RedisQueue.processCleanup connectedKeys RX/filter combinations', function() { + this.timeout(5000); + + it('should handle RX_CLIENT_TEST true but filter false case (exclude unmatched prefix)', async () => { + const name = `PCleanRX_${uuid()}`; + const rq: any = new RedisQueue(name, { + logger: console, + cleanup: true, + prefix: 'imqA', + cleanupFilter: '*', + }); + + await rq.start(); + + const writer: any = rq.writer; + + // Stub client('LIST') to include a writer channel with a different prefix, + // so RX_CLIENT_TEST.test(name) is true but filter.test(name) is false. + const clientStub = sinon.stub(writer, 'client'); + clientStub.callsFake(async (cmd: string) => { + if (cmd === 'LIST') { + return [ + 'id=1 name=imqZ:Other:writer:pid:1:host:x', // RX true, filter false + 'id=2 name=imqA:Other:subscription:pid:1:host:x', // RX false, filter true + ].join('\n'); + } + return true as any; + }); + + // Return no keys on SCAN to avoid deletions and just walk the branch + const scanStub = sinon.stub(writer, 'scan'); + scanStub.resolves(['0', []] as any); + + const delSpy = sinon.spy(writer, 'del'); + + await rq.processCleanup(); + + expect(delSpy.called).to.equal(false); + + clientStub.restore(); + scanStub.restore(); + delSpy.restore(); + await rq.destroy(); + }); +}); diff --git a/test/RedisQueue.processCleanup.multiscan.spec.ts b/test/RedisQueue.processCleanup.multiscan.spec.ts new file mode 100644 index 0000000..db7825c --- /dev/null +++ b/test/RedisQueue.processCleanup.multiscan.spec.ts @@ -0,0 +1,42 @@ +/*! + * Additional RedisQueue tests for processCleanup branches: multi-scan and no-deletion path + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue, uuid } from '../src'; + +describe('RedisQueue.processCleanup multi-scan/no-delete branches', function() { + this.timeout(5000); + + it('should handle multi-page SCAN (cursor != "0" first) and avoid deletion when keys belong to connected clients', async () => { + const name = `PClean_${uuid()}`; + const rq: any = new RedisQueue(name, { + logger: console, + cleanup: true, + prefix: 'imq', + cleanupFilter: '*', + }); + + await rq.start(); + + const writer: any = rq.writer; + + // Stub scan to first return non-zero cursor with undefined keys (to exercise `|| []`), + // then return zero cursor with keys that include connectedKey (so no removal happens). + const scanStub = sinon.stub(writer, 'scan'); + scanStub.onCall(0).resolves(['1', undefined] as any); + scanStub.onCall(1).resolves(['0', [`imq:${name}:reader:pid:123`]] as any); + + const delSpy = sinon.spy(writer, 'del'); + + await rq.processCleanup(); + + // del should not be called because keysToRemove.length === 0 + expect(delSpy.called).to.equal(false); + + scanStub.restore(); + delSpy.restore(); + await rq.destroy(); + }); +}); diff --git a/test/RedisQueue.processCleanup.nullmatch.spec.ts b/test/RedisQueue.processCleanup.nullmatch.spec.ts new file mode 100644 index 0000000..e90b32e --- /dev/null +++ b/test/RedisQueue.processCleanup.nullmatch.spec.ts @@ -0,0 +1,49 @@ +/*! + * Additional RedisQueue tests for processCleanup branches: clients.match null and cleanupFilter falsy + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue, uuid } from '../src'; + +describe('RedisQueue.processCleanup null-match and falsy cleanupFilter', function() { + this.timeout(5000); + + it('should handle clients.match returning null and cleanupFilter as falsy (\'\')', async () => { + const name = `PCleanNull_${uuid()}`; + const rq: any = new RedisQueue(name, { + logger: console, + cleanup: true, + prefix: 'imq', + cleanupFilter: '', // falsy to exercise "|| '*'" in both RegExp and SCAN MATCH + }); + + await rq.start(); + + const writer: any = rq.writer; + + // Force clients.match(...) to return null by stubbing client('LIST') to return a string without 'name=' + const clientStub = sinon.stub(writer, 'client'); + clientStub.callsFake(async (cmd: string) => { + if (cmd === 'LIST') { + return 'id=1 flags=x'; // no 'name=' + } + return true as any; + }); + + // Ensure SCAN returns no keys, to avoid deletions and just cover the branch paths + const scanStub = sinon.stub(writer, 'scan'); + scanStub.resolves(['0', []] as any); + + const delSpy = sinon.spy(writer, 'del'); + + await rq.processCleanup(); + + expect(delSpy.called).to.equal(false); + + clientStub.restore(); + scanStub.restore(); + delSpy.restore(); + await rq.destroy(); + }); +}); From e307e0848aeb0c318d03e05188c5ab7a020f07cc Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 19:11:08 +0200 Subject: [PATCH 27/78] feat: test coverage reached 100% --- src/UDPClusterManager.ts | 5 +- ...edRedisQueue.addServer.defaultInit.spec.ts | 34 ++++++++++ ...UDPClusterManager.missing.branches.spec.ts | 63 +++++++++++++++++++ ...ager.serverAliveWait.truthyTimeout.spec.ts | 25 ++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 test/ClusteredRedisQueue.addServer.defaultInit.spec.ts create mode 100644 test/UDPClusterManager.missing.branches.spec.ts create mode 100644 test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index 2e67343..924832a 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -368,7 +368,10 @@ export class UDPClusterManager extends ClusterManager { if (typeof socket.close === 'function') { socket.removeAllListeners(); socket.close(() => { - socket?.unref(); + // unref may be missing or not a function on mocked sockets + if (socket && typeof (socket as any).unref === 'function') { + (socket as any).unref(); + } if ( socketKey diff --git a/test/ClusteredRedisQueue.addServer.defaultInit.spec.ts b/test/ClusteredRedisQueue.addServer.defaultInit.spec.ts new file mode 100644 index 0000000..c70dd47 --- /dev/null +++ b/test/ClusteredRedisQueue.addServer.defaultInit.spec.ts @@ -0,0 +1,34 @@ +/*! + * ClusteredRedisQueue.addServerWithQueueInitializing default param branch + */ +import './mocks'; +import { expect } from 'chai'; +import { ClusteredRedisQueue } from '../src'; + +describe('ClusteredRedisQueue.addServerWithQueueInitializing() default param', () => { + it('should use default initializeQueue=true when second param omitted', async () => { + const cq: any = new ClusteredRedisQueue('CQ-Default', { + logger: console, + cluster: [{ host: '127.0.0.1', port: 6379 }], + }); + // prevent any actual start/subscription side-effects + (cq as any).state.started = false; + (cq as any).state.subscription = null; + + const server = { host: '192.168.0.1', port: 6380 }; + const initializedSpy = new Promise((resolve) => { + cq['clusterEmitter'].once('initialized', () => resolve()); + }); + + // Call without the second argument to hit default "true" branch + (cq as any).addServerWithQueueInitializing(server); + + await initializedSpy; // should emit initialized when default is true + + // Ensure the server added and queue length updated + expect((cq as any).servers.some((s: any) => s.host === server.host && s.port === server.port)).to.equal(true); + expect((cq as any).queueLength).to.equal((cq as any).imqs.length); + + await cq.destroy(); + }); +}); diff --git a/test/UDPClusterManager.missing.branches.spec.ts b/test/UDPClusterManager.missing.branches.spec.ts new file mode 100644 index 0000000..b090f10 --- /dev/null +++ b/test/UDPClusterManager.missing.branches.spec.ts @@ -0,0 +1,63 @@ +/*! + * UDPClusterManager missing branches coverage + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { UDPClusterManager } from '../src'; + +describe('UDPClusterManager - cover remaining branches', () => { + it('serverAliveWait should handle existing.timeout falsy (|| 0) path and remove on timeout', async () => { + // Arrange cluster stub + const server: any = { host: 'h', port: 1, timer: undefined, timeout: undefined, timestamp: undefined }; + const cluster: any = { + find: sinon.stub().callsFake((_s: any, _strict?: boolean) => server), + remove: sinon.stub(), + }; + + // Use fake timers to control setTimeout and Date + const clock = sinon.useFakeTimers(); + try { + // make timestamp truthy + clock.tick(1); + // Alive correction > 0 ensures timer is scheduled even if timeout is falsy + (UDPClusterManager as any).serverAliveWait(cluster, server, 1); + // Advance time to trigger setTimeout callback and make delta >= currentTimeout (1ms) + clock.tick(2); + + expect(cluster.remove.called).to.equal(true); + } finally { + clock.restore(); + } + }); + + it('destroySocket should call socket.unref() when socket is present', async () => { + // Prepare fake socket with unref + const unref = sinon.spy(); + const removeAll = sinon.spy(); + const sock: any = { + removeAllListeners: removeAll, + close: (cb: (err?: any) => void) => cb(), + unref, + }; + const key = 'test-key'; + (UDPClusterManager as any).sockets[key] = sock; + await (UDPClusterManager as any).destroySocket(key, sock); + expect(unref.called).to.equal(true); + expect((UDPClusterManager as any).sockets[key]).to.equal(undefined); + }); + + it('destroySocket should work when socket.unref() is absent (optional chaining negative branch)', async () => { + const removeAll = sinon.spy(); + const sock: any = { + removeAllListeners: removeAll, + close: (cb: (err?: any) => void) => cb(), + // no unref method + }; + const key = 'test-key-2'; + (UDPClusterManager as any).sockets[key] = sock; + await (UDPClusterManager as any).destroySocket(key, sock); + // should not throw, sockets map cleaned + expect((UDPClusterManager as any).sockets[key]).to.equal(undefined); + }); +}); diff --git a/test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts b/test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts new file mode 100644 index 0000000..82c8dac --- /dev/null +++ b/test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts @@ -0,0 +1,25 @@ +/*! + * UDPClusterManager serverAliveWait: cover currentTimeout left side (existing.timeout truthy) + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { UDPClusterManager } from '../src'; + +describe('UDPClusterManager.serverAliveWait truthy timeout', () => { + it('should use existing.timeout (truthy) in currentTimeout and remove on expiry', async () => { + const server: any = { host: 'h', port: 1, timer: undefined, timeout: 2, timestamp: undefined }; + const cluster: any = { + find: sinon.stub().callsFake((_s: any, _strict?: boolean) => server), + remove: sinon.stub(), + }; + const clock = sinon.useFakeTimers(); + try { + (UDPClusterManager as any).serverAliveWait(cluster, server, 0); + clock.tick(3); + expect(cluster.remove.called).to.equal(true); + } finally { + clock.restore(); + } + }); +}); From 5867e6f34e321fff471dcba0f956037107d86c89 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 19:20:40 +0200 Subject: [PATCH 28/78] fix: update deps --- package-lock.json | 1106 ++++++++++++++------------------------------- package.json | 31 +- 2 files changed, 365 insertions(+), 772 deletions(-) diff --git a/package-lock.json b/package-lock.json index bddeb36..7dbf4d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,37 +9,36 @@ "version": "2.0.5", "license": "GPL-3.0-only", "dependencies": { - "ioredis": "^5.6.1" + "ioredis": "^5.7.0" }, "devDependencies": { - "@eslint/js": "^9.30.0", + "@eslint/js": "^9.33.0", "@types/chai": "^5.2.2", "@types/eslint__eslintrc": "^2.1.2", "@types/mocha": "^10.0.0", "@types/mock-require": "^3.0.0", - "@types/node": "^24.0.8", + "@types/node": "^24.2.1", "@types/sinon": "^17.0.4", "@types/yargs": "^17.0.33", - "@typescript-eslint/eslint-plugin": "^8.35.1", - "@typescript-eslint/parser": "^8.35.1", - "@typescript-eslint/typescript-estree": "^8.35.1", - "chai": "^5.2.0", - "codeclimate-test-reporter": "^0.5.1", - "coveralls-next": "^4.2.1", - "eslint": "^9.30.0", - "eslint-plugin-jsdoc": "^51.3.1", + "@typescript-eslint/eslint-plugin": "^8.39.0", + "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/typescript-estree": "^8.39.0", + "chai": "^5.2.1", + "coveralls-next": "^5.0.0", + "eslint": "^9.33.0", + "eslint-plugin-jsdoc": "^52.0.4", "mocha": "^11.7.1", "mocha-lcov-reporter": "^1.3.0", "mock-require": "^3.0.3", "nyc": "^17.1.0", - "open": "^10.1.2", + "open": "^10.2.0", "reflect-metadata": "^0.2.2", "sinon": "^21.0.0", "source-map-support": "^0.5.21", "ts-node": "^10.9.2", - "typedoc": "^0.28.7", - "typescript": "^5.8.3", - "yargs": "^17.7.2" + "typedoc": "^0.28.9", + "typescript": "^5.9.2", + "yargs": "^18.0.0" } }, "node_modules/@ampproject/remapping": { @@ -433,9 +432,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -443,9 +442,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -514,9 +513,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.30.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz", - "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "dev": true, "license": "MIT", "engines": { @@ -537,43 +536,30 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@gerrit0/mini-shiki": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.7.0.tgz", - "integrity": "sha512-7iY9wg4FWXmeoFJpUL2u+tsmh0d0jcEJHAIzVxl3TG4KL493JNnisdLAILZ77zcD+z3J0keEXZ+lFzUgzQzPDg==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.9.2.tgz", + "integrity": "sha512-Tvsj+AOO4Z8xLRJK900WkyfxHsZQu+Zm1//oT1w443PO6RiYMoq/4NGOhaNuZoUMYsjKIAPVQ6eOFMddj6yphQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-oniguruma": "^3.7.0", - "@shikijs/langs": "^3.7.0", - "@shikijs/themes": "^3.7.0", - "@shikijs/types": "^3.7.0", + "@shikijs/engine-oniguruma": "^3.9.2", + "@shikijs/langs": "^3.9.2", + "@shikijs/themes": "^3.9.2", + "@shikijs/types": "^3.9.2", "@shikijs/vscode-textmate": "^10.0.2" } }, @@ -644,9 +630,9 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz", + "integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==", "license": "MIT" }, "node_modules/@isaacs/cliui": { @@ -873,40 +859,40 @@ } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.7.0.tgz", - "integrity": "sha512-5BxcD6LjVWsGu4xyaBC5bu8LdNgPCVBnAkWTtOCs/CZxcB22L8rcoWfv7Hh/3WooVjBZmFtyxhgvkQFedPGnFw==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.9.2.tgz", + "integrity": "sha512-Vn/w5oyQ6TUgTVDIC/BrpXwIlfK6V6kGWDVVz2eRkF2v13YoENUvaNwxMsQU/t6oCuZKzqp9vqtEtEzKl9VegA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0", + "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@shikijs/langs": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.7.0.tgz", - "integrity": "sha512-1zYtdfXLr9xDKLTGy5kb7O0zDQsxXiIsw1iIBcNOO8Yi5/Y1qDbJ+0VsFoqTlzdmneO8Ij35g7QKF8kcLyznCQ==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.9.2.tgz", + "integrity": "sha512-X1Q6wRRQXY7HqAuX3I8WjMscjeGjqXCg/Sve7J2GWFORXkSrXud23UECqTBIdCSNKJioFtmUGJQNKtlMMZMn0w==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0" + "@shikijs/types": "3.9.2" } }, "node_modules/@shikijs/themes": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.7.0.tgz", - "integrity": "sha512-VJx8497iZPy5zLiiCTSIaOChIcKQwR0FebwE9S3rcN0+J/GTWwQ1v/bqhTbpbY3zybPKeO8wdammqkpXc4NVjQ==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.9.2.tgz", + "integrity": "sha512-6z5lBPBMRfLyyEsgf6uJDHPa6NAGVzFJqH4EAZ+03+7sedYir2yJBRu2uPZOKmj43GyhVHWHvyduLDAwJQfDjA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0" + "@shikijs/types": "3.9.2" } }, "node_modules/@shikijs/types": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.7.0.tgz", - "integrity": "sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.9.2.tgz", + "integrity": "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw==", "dev": true, "license": "MIT", "dependencies": { @@ -1068,13 +1054,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz", - "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", + "version": "24.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", + "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.10.0" } }, "node_modules/@types/sinon": { @@ -1119,17 +1105,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", - "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", + "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/type-utils": "8.35.1", - "@typescript-eslint/utils": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/type-utils": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1143,22 +1129,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.35.1", + "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz", - "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", + "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "engines": { @@ -1170,18 +1156,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz", - "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", + "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.35.1", - "@typescript-eslint/types": "^8.35.1", + "@typescript-eslint/tsconfig-utils": "^8.39.0", + "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "engines": { @@ -1192,18 +1178,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz", - "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", + "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1" + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1214,9 +1200,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz", - "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", + "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", "dev": true, "license": "MIT", "engines": { @@ -1227,18 +1213,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz", - "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", + "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/utils": "8.35.1", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1251,13 +1238,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz", - "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", + "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", "dev": true, "license": "MIT", "engines": { @@ -1269,16 +1256,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz", - "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", + "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.35.1", - "@typescript-eslint/tsconfig-utils": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/project-service": "8.39.0", + "@typescript-eslint/tsconfig-utils": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1294,20 +1281,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz", - "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1" + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1318,17 +1305,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz", - "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", + "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1492,26 +1479,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1522,37 +1489,6 @@ "node": ">=12" } }, - "node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", - "dev": true, - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", - "dev": true, - "license": "MIT" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1560,16 +1496,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -1713,17 +1639,10 @@ ], "license": "CC-BY-4.0" }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", "dev": true, "license": "MIT", "dependencies": { @@ -1734,7 +1653,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -1791,78 +1710,71 @@ } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=12" + "node": ">=20" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" + "node": ">=18" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -1877,26 +1789,6 @@ "node": ">=0.10.0" } }, - "node_modules/codeclimate-test-reporter": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/codeclimate-test-reporter/-/codeclimate-test-reporter-0.5.1.tgz", - "integrity": "sha512-XCzmc8dH+R4orK11BCg5pBbXc35abxq9sept4YvUFRkFl9zb9MIVRrCKENe6U1TKAMTgvGJmrYyHn0y2lerpmg==", - "deprecated": "codeclimate-test-reporter has been deprecated in favor of our new unified test-reporter. Please visit https://docs.codeclimate.com/docs/configuring-test-coverage for details on setting up the new test-reporter.", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "~1.5.2", - "commander": "2.9.0", - "lcov-parse": "0.0.10", - "request": "~2.88.0" - }, - "bin": { - "codeclimate-test-reporter": "bin/codeclimate.js" - }, - "engines": { - "node": ">= 4" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1917,32 +1809,6 @@ "dev": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-readlink": ">= 1.0.0" - }, - "engines": { - "node": ">= 0.6.x" - } - }, "node_modules/comment-parser": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", @@ -1974,21 +1840,13 @@ "dev": true, "license": "MIT" }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" - }, "node_modules/coveralls-next": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/coveralls-next/-/coveralls-next-4.2.1.tgz", - "integrity": "sha512-O/SBGZsCryt+6Q3NuJHENyQYaucTEV9qp0KGaed+y42PUh+GuF949LRLHKZbxWwOIc1tV8bJRIVWlfbZ8etEwQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/coveralls-next/-/coveralls-next-5.0.0.tgz", + "integrity": "sha512-RCj6Oflf6iQtN3Q5b0SSemEbQBzeBjQlLUrc3bfNECTy83hMJA9krdNZ5GTRm7Jpbyo92yKUbQDP5FYlWcL5sA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "form-data": "4.0.0", "js-yaml": "4.1.0", "lcov-parse": "1.0.0", "log-driver": "1.2.7", @@ -1998,7 +1856,7 @@ "coveralls": "bin/coveralls.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/coveralls-next/node_modules/lcov-parse": { @@ -2033,19 +1891,6 @@ "node": ">= 8" } }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -2149,16 +1994,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -2185,17 +2020,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.178", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.178.tgz", @@ -2254,20 +2078,20 @@ } }, "node_modules/eslint": { - "version": "9.30.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", - "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.14.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.30.1", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -2315,9 +2139,9 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "51.3.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-51.3.2.tgz", - "integrity": "sha512-sBmS2MoxbUuKE1wMn/jeHitlCwdk3jAkkpdo3TNA5qGADjiow9D5z/zJ3XScScDsNI2fzZJsmCyf5rc12oRbUA==", + "version": "52.0.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-52.0.4.tgz", + "integrity": "sha512-be5OzGlLExvcK13Il3noU7/v7WmAQGenTmCaBKf1pwVtPOb6X+PGFVnJad0QhMj4KKf45XjE4hbsBxv25q1fTg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2507,23 +2331,6 @@ "node": ">=0.10.0" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2694,31 +2501,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -2764,6 +2546,19 @@ "dev": true, "license": "ISC" }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -2774,16 +2569,6 @@ "node": ">=8.0.0" } }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2838,13 +2623,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", - "dev": true, - "license": "MIT" - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2852,31 +2630,6 @@ "dev": true, "license": "MIT" }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2921,22 +2674,6 @@ "dev": true, "license": "MIT" }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -3004,12 +2741,12 @@ "license": "ISC" }, "node_modules/ioredis": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", - "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", + "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", "license": "MIT", "dependencies": { - "@ioredis/commands": "^1.1.1", + "@ioredis/commands": "^1.3.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -3181,13 +2918,6 @@ "dev": true, "license": "ISC" }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "dev": true, - "license": "MIT" - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -3342,13 +3072,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true, - "license": "MIT" - }, "node_modules/jsdoc-type-pratt-parser": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", @@ -3379,13 +3102,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true, - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3400,13 +3116,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3420,22 +3129,6 @@ "node": ">=6" } }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3446,13 +3139,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/lcov-parse": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", - "integrity": "sha512-YsL0D4QF/vNlNcHPXM832si9d2ROryFQ4r4JvcfMIiUYr1f6WULuO75YCtxNu4P+XMRHz0SfUc524+c+U3G5kg==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3660,29 +3346,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -3765,6 +3428,76 @@ "node": ">= 0.6.0" } }, + "node_modules/mocha/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mocha/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/mocha/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -3781,6 +3514,43 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/mock-require": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz", @@ -4128,16 +3898,6 @@ "node": ">=6" } }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4149,16 +3909,16 @@ } }, "node_modules/open": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", - "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, "license": "MIT", "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" + "wsl-utils": "^0.1.0" }, "engines": { "node": ">=18" @@ -4357,13 +4117,6 @@ "node": ">= 14.16" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4476,19 +4229,6 @@ "node": ">=8" } }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4509,16 +4249,6 @@ "node": ">=6" } }, - "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.6" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4612,65 +4342,6 @@ "dev": true, "license": "ISC" }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/request/node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/request/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "bin/uuid" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4830,13 +4501,6 @@ ], "license": "MIT" }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -5013,32 +4677,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -5259,20 +4897,6 @@ "node": ">=8.0" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -5340,26 +4964,6 @@ "node": ">=0.3.1" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true, - "license": "Unlicense" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5404,13 +5008,13 @@ } }, "node_modules/typedoc": { - "version": "0.28.7", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.7.tgz", - "integrity": "sha512-lpz0Oxl6aidFkmS90VQDQjk/Qf2iw0IUvFqirdONBdj7jPSN9mGXhy66BcGNDxx5ZMyKKiBVAREvPEzT6Uxipw==", + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.9.tgz", + "integrity": "sha512-aw45vwtwOl3QkUAmWCnLV9QW1xY+FSX2zzlit4MAfE99wX+Jij4ycnpbAWgBXsRrxmfs9LaYktg/eX5Bpthd3g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@gerrit0/mini-shiki": "^3.7.0", + "@gerrit0/mini-shiki": "^3.9.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", @@ -5424,13 +5028,13 @@ "pnpm": ">= 10" }, "peerDependencies": { - "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5449,9 +5053,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, @@ -5513,21 +5117,6 @@ "dev": true, "license": "MIT" }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5690,6 +5279,22 @@ "dev": true, "license": "ISC" }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -5721,22 +5326,21 @@ } }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^7.2.0", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { @@ -5791,20 +5395,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true, "license": "MIT" }, @@ -5819,31 +5413,31 @@ } }, "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/yargs/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "license": "ISC", "engines": { - "node": ">=8" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yn": { diff --git a/package.json b/package.json index 0cd4376..2a158ae 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "scripts": { "benchmark": "node benchmark -c $(( $(nproc) - 2 )) -m 100000", "prepare": "./node_modules/.bin/tsc", - "test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && ./node_modules/.bin/nyc report --reporter=text-lcov && npm run test-coverage", + "test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && ./node_modules/.bin/nyc report --reporter=text-lcov", "test-fast": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && /usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/coverage/index.html', { wait: false }))\"", "test-local": "export COVERALLS_REPO_TOKEN=$IMQ_COVERALLS_TOKEN && npm test && /usr/bin/env node -e \"import('open').then(open => open.default('https://coveralls.io/github/imqueue/imq', { wait: false }))\"", "test-dev": "npm run test && npm run clean-js && npm run clean-typedefs && npm run clean-maps", @@ -38,37 +38,36 @@ "author": "imqueue.com (https://imqueue.com)", "license": "GPL-3.0-only", "dependencies": { - "ioredis": "^5.6.1" + "ioredis": "^5.7.0" }, "devDependencies": { - "@eslint/js": "^9.30.0", + "@eslint/js": "^9.33.0", "@types/chai": "^5.2.2", "@types/eslint__eslintrc": "^2.1.2", "@types/mocha": "^10.0.0", "@types/mock-require": "^3.0.0", - "@types/node": "^24.0.8", + "@types/node": "^24.2.1", "@types/sinon": "^17.0.4", "@types/yargs": "^17.0.33", - "@typescript-eslint/eslint-plugin": "^8.35.1", - "@typescript-eslint/parser": "^8.35.1", - "@typescript-eslint/typescript-estree": "^8.35.1", - "chai": "^5.2.0", - "codeclimate-test-reporter": "^0.5.1", - "coveralls-next": "^4.2.1", - "eslint": "^9.30.0", - "eslint-plugin-jsdoc": "^51.3.1", + "@typescript-eslint/eslint-plugin": "^8.39.0", + "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/typescript-estree": "^8.39.0", + "chai": "^5.2.1", + "coveralls-next": "^5.0.0", + "eslint": "^9.33.0", + "eslint-plugin-jsdoc": "^52.0.4", "mocha": "^11.7.1", "mocha-lcov-reporter": "^1.3.0", "mock-require": "^3.0.3", "nyc": "^17.1.0", - "open": "^10.1.2", + "open": "^10.2.0", "reflect-metadata": "^0.2.2", "sinon": "^21.0.0", "source-map-support": "^0.5.21", "ts-node": "^10.9.2", - "typedoc": "^0.28.7", - "typescript": "^5.8.3", - "yargs": "^17.7.2" + "typedoc": "^0.28.9", + "typescript": "^5.9.2", + "yargs": "^18.0.0" }, "main": "index.js", "typescript": { From 3ab462e8facd9c2e8b5cdd8997e1080b8c939a2d Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 19:34:01 +0200 Subject: [PATCH 29/78] Update src/UDPClusterManager.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/UDPClusterManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index 924832a..d259be3 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -370,7 +370,8 @@ export class UDPClusterManager extends ClusterManager { socket.close(() => { // unref may be missing or not a function on mocked sockets if (socket && typeof (socket as any).unref === 'function') { - (socket as any).unref(); + if (socket && hasUnref(socket)) { + socket.unref(); } if ( From ccb229d6f189234b041efc7e6d30882bb4c34023 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 19:35:39 +0200 Subject: [PATCH 30/78] Update test/profile.more.branches.spec.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/profile.more.branches.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/profile.more.branches.spec.ts b/test/profile.more.branches.spec.ts index 80f1672..effff73 100644 --- a/test/profile.more.branches.spec.ts +++ b/test/profile.more.branches.spec.ts @@ -40,7 +40,13 @@ describe('profile.ts additional branches', () => { const fakeLogger: any = { error: sinon.spy(), }; - const args = [BigInt(1)]; // JSON.stringify throws on BigInt + it('logDebugInfo: should call logger.error on JSON.stringify error (explicit stub)', () => { + const { logDebugInfo, LogLevel } = mock.reRequire('../src/profile'); + const fakeLogger: any = { + error: sinon.spy(), + }; + const args = [{ foo: 'bar' }]; + const jsonStringifyStub = sinon.stub(JSON, 'stringify').throws(new Error('Serialization failed')); const options = { debugTime: false, debugArgs: true, From 8a432c4204d433c473e1824d8362a4c14a178a98 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Sun, 10 Aug 2025 19:41:12 +0200 Subject: [PATCH 31/78] fix: bugs after copilot review --- src/UDPClusterManager.ts | 1 - test/profile.more.branches.spec.ts | 8 +------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index d259be3..644d52c 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -370,7 +370,6 @@ export class UDPClusterManager extends ClusterManager { socket.close(() => { // unref may be missing or not a function on mocked sockets if (socket && typeof (socket as any).unref === 'function') { - if (socket && hasUnref(socket)) { socket.unref(); } diff --git a/test/profile.more.branches.spec.ts b/test/profile.more.branches.spec.ts index effff73..80f1672 100644 --- a/test/profile.more.branches.spec.ts +++ b/test/profile.more.branches.spec.ts @@ -40,13 +40,7 @@ describe('profile.ts additional branches', () => { const fakeLogger: any = { error: sinon.spy(), }; - it('logDebugInfo: should call logger.error on JSON.stringify error (explicit stub)', () => { - const { logDebugInfo, LogLevel } = mock.reRequire('../src/profile'); - const fakeLogger: any = { - error: sinon.spy(), - }; - const args = [{ foo: 'bar' }]; - const jsonStringifyStub = sinon.stub(JSON, 'stringify').throws(new Error('Serialization failed')); + const args = [BigInt(1)]; // JSON.stringify throws on BigInt const options = { debugTime: false, debugArgs: true, From 7a713943ce2deef02eb2347670d300d5d949132b Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Mon, 11 Aug 2025 00:11:32 +0200 Subject: [PATCH 32/78] 2.0.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7dbf4d5..eb95fc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.5", + "version": "2.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.5", + "version": "2.0.6", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index 2a158ae..f66b933 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.5", + "version": "2.0.6", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 9f36b44fe8dc5ffca0543014185ce0dc4f620e65 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Mon, 11 Aug 2025 09:26:52 +0200 Subject: [PATCH 33/78] Update README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 7daca4f..94c483f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ # I Message Queue (@imqueue/core) [![Build Status](https://img.shields.io/github/actions/workflow/status/imqueue/core/build.yml)](https://github.com/imqueue/core) -[![codebeat badge](https://codebeat.co/badges/b7685cb5-b290-47de-80e1-bde3e0582355)](https://codebeat.co/projects/github-com-imqueue-core-master) -[![Coverage Status](https://coveralls.io/repos/github/imqueue/core/badge.svg?branch=master)](https://coveralls.io/github/imqueue/core?branch=master) -[![Known Vulnerabilities](https://snyk.io/test/github/imqueue/core/badge.svg?targetFile=package.json)](https://snyk.io/test/github/imqueue/core?targetFile=package.json) [![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://rawgit.com/imqueue/core/master/LICENSE) Simple JSON-based messaging queue for inter service communication From 19d59d9bd94b45304c8c0263bdfe2340c7bb8ede Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Mon, 11 Aug 2025 09:27:38 +0200 Subject: [PATCH 34/78] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 94c483f..b11ca5f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # I Message Queue (@imqueue/core) -[![Build Status](https://img.shields.io/github/actions/workflow/status/imqueue/core/build.yml)](https://github.com/imqueue/core) [![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://rawgit.com/imqueue/core/master/LICENSE) Simple JSON-based messaging queue for inter service communication From 7a4eb1a033104c28752ada35ddb5936a74ed6276 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Wed, 13 Aug 2025 17:39:57 +0200 Subject: [PATCH 35/78] fix: UDP Cluster Manager - simplified & improved server alive waiting timer --- src/RedisQueue.ts | 4 ++++ src/UDPClusterManager.ts | 45 +++++++++++----------------------------- 2 files changed, 16 insertions(+), 33 deletions(-) diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index fb4cf4e..3acbd84 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -887,6 +887,10 @@ export class RedisQueue extends EventEmitter ); const list = await this.writer.client('LIST'); + if (!list || !list.split) { + return 0; + } + return list.split(/\r?\n/).filter(client => rx.test(client)).length; } diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index 644d52c..c49af1c 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -49,7 +49,7 @@ interface ClusterServer extends IMessageQueueConnection { export const DEFAULT_UDP_CLUSTER_MANAGER_OPTIONS = { broadcastPort: 63000, broadcastAddress: '255.255.255.255', - aliveTimeoutCorrection: 1000, + aliveTimeoutCorrection: 2000, }; export interface UDPClusterManagerOptions { @@ -293,55 +293,34 @@ export class UDPClusterManager extends ClusterManager { private static serverAliveWait( cluster: ICluster, server: ClusterServer, - aliveTimeoutCorrection?: number, + aliveTimeoutCorrection: number = 0, message?: Message, ): void { if (server.timer) { clearTimeout(server.timer); - server.timer = undefined; - } - - server.timestamp = Date.now(); - - if (message) { - server.timeout = message.timeout; } - const correction = aliveTimeoutCorrection || 0; - const timeout = (server.timeout || 0) + correction; + const timeout = (message?.timeout || 0) + aliveTimeoutCorrection; if (timeout <= 0) { return; } - const timerId = setTimeout(() => { - const existing = cluster.find(server, true); - - if (!existing || existing.timer !== timerId) { - return; - } - - const now = Date.now(); - - if (!existing.timestamp) { - clearTimeout(existing.timer); - existing.timer = undefined; - cluster.remove(existing); + server.timeout = timeout; + server.timestamp = Date.now(); + server.timer = setTimeout(() => { + const entry = cluster.find(server, true); + if (!entry?.timestamp) { return; } - const delta = now - existing.timestamp; - const currentTimeout = (existing.timeout || 0) + correction; + const elapsed = Date.now() - entry.timestamp; - if (delta >= currentTimeout) { - clearTimeout(existing.timer); - existing.timer = undefined; - cluster.remove(existing); + if (elapsed >= timeout) { + cluster.remove(entry); } - }, timeout); - - server.timer = timerId; + }, server.timeout); } /** From 39658a5456fa2d3249860beb2d35d84ef3e64349 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Wed, 13 Aug 2025 17:40:20 +0200 Subject: [PATCH 36/78] 2.0.7 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index eb95fc7..6b504c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.6", + "version": "2.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.6", + "version": "2.0.7", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index f66b933..e9164dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.6", + "version": "2.0.7", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 2f85908f58561a36290c24bc95e74ed7a8110b74 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Wed, 13 Aug 2025 18:00:00 +0200 Subject: [PATCH 37/78] fix: tests --- src/UDPClusterManager.ts | 5 ++-- test/UDPClusterManager.extra.branches.spec.ts | 25 ------------------- ...ager.serverAliveWait.truthyTimeout.spec.ts | 8 ++++-- 3 files changed, 9 insertions(+), 29 deletions(-) diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index c49af1c..d203aa6 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -298,6 +298,7 @@ export class UDPClusterManager extends ClusterManager { ): void { if (server.timer) { clearTimeout(server.timer); + server.timer = undefined; } const timeout = (message?.timeout || 0) + aliveTimeoutCorrection; @@ -311,7 +312,7 @@ export class UDPClusterManager extends ClusterManager { server.timer = setTimeout(() => { const entry = cluster.find(server, true); - if (!entry?.timestamp) { + if (typeof entry?.timestamp !== 'number') { return; } @@ -320,7 +321,7 @@ export class UDPClusterManager extends ClusterManager { if (elapsed >= timeout) { cluster.remove(entry); } - }, server.timeout); + }, timeout); } /** diff --git a/test/UDPClusterManager.extra.branches.spec.ts b/test/UDPClusterManager.extra.branches.spec.ts index a862231..7d6db5a 100644 --- a/test/UDPClusterManager.extra.branches.spec.ts +++ b/test/UDPClusterManager.extra.branches.spec.ts @@ -60,30 +60,5 @@ describe('UDPClusterManager additional branches', () => { expect(server.timer).to.equal(undefined); }); - - it('should remove when no timestamp is present on existing (timer callback path)', (done) => { - const removed: any[] = []; - const server: any = { timeout: 0 }; - const cluster: any = { - find: () => server, - remove: (s: any) => { removed.push(s); }, - }; - - // use small correction to trigger timeout quickly - serverAliveWait(cluster, server, 1); - - // wipe timestamp before timeout fires to force the branch - server.timestamp = undefined; - - setTimeout(() => { - try { - expect(removed.length).to.equal(1); - expect(server.timer).to.equal(undefined); - done(); - } catch (e) { - done(e as any); - } - }, 10); - }); }); }); diff --git a/test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts b/test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts index 82c8dac..4f5bee5 100644 --- a/test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts +++ b/test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts @@ -8,14 +8,18 @@ import { UDPClusterManager } from '../src'; describe('UDPClusterManager.serverAliveWait truthy timeout', () => { it('should use existing.timeout (truthy) in currentTimeout and remove on expiry', async () => { - const server: any = { host: 'h', port: 1, timer: undefined, timeout: 2, timestamp: undefined }; + const server: any = { host: 'h', port: 1, timer: undefined, timeout: 1, timestamp: Date.now() }; const cluster: any = { find: sinon.stub().callsFake((_s: any, _strict?: boolean) => server), remove: sinon.stub(), }; const clock = sinon.useFakeTimers(); try { - (UDPClusterManager as any).serverAliveWait(cluster, server, 0); + (UDPClusterManager as any).serverAliveWait( + cluster, + server, + 1, + ); clock.tick(3); expect(cluster.remove.called).to.equal(true); } finally { From 23bebb0c6f2e0f576d0b84f13d4c14aee46f4714 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Wed, 13 Aug 2025 18:00:07 +0200 Subject: [PATCH 38/78] 2.0.8 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6b504c0..51ce79e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.7", + "version": "2.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.7", + "version": "2.0.8", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index e9164dc..54e3f87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.7", + "version": "2.0.8", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From a5a0c2451c544d2dc919c05f0f5ba2381b3b6a1f Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 28 Aug 2025 17:23:43 +0200 Subject: [PATCH 39/78] wip: server alive timeout --- src/ClusterManager.ts | 3 +- src/ClusteredRedisQueue.ts | 20 ++- src/UDPClusterManager.ts | 238 ++++++++++++++---------------------- test/ClusterManager.spec.ts | 4 +- test/UDPClusterManager.ts | 62 ---------- 5 files changed, 104 insertions(+), 223 deletions(-) diff --git a/src/ClusterManager.ts b/src/ClusterManager.ts index 20e4077..6579de0 100644 --- a/src/ClusterManager.ts +++ b/src/ClusterManager.ts @@ -25,11 +25,10 @@ import { IMessageQueueConnection, IServerInput } from './IMessageQueue'; import { uuid } from './uuid'; export interface ICluster { - add: (server: IServerInput) => void; + add: (server: IServerInput) => T; remove: (server: IServerInput) => void; find: ( server: IServerInput, - strict?: boolean, ) => T | undefined; } diff --git a/src/ClusteredRedisQueue.ts b/src/ClusteredRedisQueue.ts index bf78721..c2c664a 100644 --- a/src/ClusteredRedisQueue.ts +++ b/src/ClusteredRedisQueue.ts @@ -485,7 +485,7 @@ export class ClusteredRedisQueue implements IMessageQueue, * @param {IServerInput} server * @returns {void} */ - protected addServer(server: IServerInput): void { + protected addServer(server: IServerInput): ClusterServer { return this.addServerWithQueueInitializing(server, true); } @@ -496,7 +496,7 @@ export class ClusteredRedisQueue implements IMessageQueue, * @returns {void} */ protected removeServer(server: IServerInput): void { - const remove = this.findServer(server, true); + const remove = this.findServer(server); if (!remove) { return; @@ -524,12 +524,13 @@ export class ClusteredRedisQueue implements IMessageQueue, private addServerWithQueueInitializing( server: ClusterServer, initializeQueue: boolean = true, - ): void { + ): ClusterServer { const newServer: ClusterServer = { id: server.id, host: server.host, port: server.port, }; + const opts = { ...this.mqOptions, ...newServer }; const imq = new RedisQueue(this.name, opts); @@ -548,6 +549,8 @@ export class ClusteredRedisQueue implements IMessageQueue, this.servers.push(newServer); this.clusterEmitter.emit('add', { server: newServer, imq }); this.queueLength = this.imqs.length; + + return newServer; } private eventEmitters(): EventEmitter[] { @@ -569,15 +572,11 @@ export class ClusteredRedisQueue implements IMessageQueue, } } - private findServer( - server: IServerInput, - strict: boolean = false, - ): ClusterServer | undefined { + private findServer(server: IServerInput): ClusterServer | undefined { return this.servers.find( existing => ClusteredRedisQueue.matchServers( existing, server, - strict, ), ); } @@ -585,7 +584,6 @@ export class ClusteredRedisQueue implements IMessageQueue, private static matchServers( source: IServerInput, target: IServerInput, - strict: boolean = false, ): boolean { const sameAddress = target.host === source.host && target.port === source.port; @@ -596,10 +594,6 @@ export class ClusteredRedisQueue implements IMessageQueue, const sameId = target.id === source.id; - if (strict) { - return sameId && sameAddress; - } - return sameId || sameAddress; } diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index d203aa6..79ddcca 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -22,8 +22,8 @@ * to get commercial licensing options. */ import { IMessageQueueConnection } from './IMessageQueue'; -import { ICluster, ClusterManager } from './ClusterManager'; -import { Socket, createSocket } from 'dgram'; +import { ClusterManager, ICluster } from './ClusterManager'; +import { createSocket, Socket } from 'dgram'; import { networkInterfaces } from 'os'; enum MessageType { @@ -43,13 +43,13 @@ interface Message { interface ClusterServer extends IMessageQueueConnection { timeout?: number; timestamp?: number; - timer?: NodeJS.Timeout; + timer?: any; } export const DEFAULT_UDP_CLUSTER_MANAGER_OPTIONS = { broadcastPort: 63000, broadcastAddress: '255.255.255.255', - aliveTimeoutCorrection: 2000, + aliveTimeoutCorrection: 1000, }; export interface UDPClusterManagerOptions { @@ -59,7 +59,7 @@ export interface UDPClusterManagerOptions { * @default 63000 * @type {number} */ - broadcastPort?: number; + broadcastPort: number; /** * Message queue broadcast address @@ -67,7 +67,7 @@ export interface UDPClusterManagerOptions { * @default limitedBroadcastAddress * @type {number} */ - broadcastAddress?: string; + broadcastAddress: string; /** * Message queue limited broadcast address @@ -84,31 +84,9 @@ export interface UDPClusterManagerOptions { * @default 1000 * @type {number} */ - aliveTimeoutCorrection?: number; - - /** - * Skip messages that are broadcast by specified addresses or set to - * "localhost" if you want to skip messages from "127.0.0.1" or "::1" - * - * @type {"local" | string[]} - */ - excludeHosts?: 'localhost' | string[]; - - /** - * Allow messages that are broadcast only by specified addresses or set to - * "localhost" if you want to allow messages only from "127.0.0.1" or "::1" - * - * @type {"local" | string[]} - */ - includeHosts?: 'localhost' | string[]; + aliveTimeoutCorrection: number; } -const LOCALHOST_ADDRESSES = [ - 'localhost', - '127.0.0.1', - '::1', -]; - /** * UDP broadcast-based cluster management implementation * @@ -132,14 +110,14 @@ export class UDPClusterManager extends ClusterManager { UDPClusterManager.sockets[this.socketKey] = socket; } - constructor(options?: UDPClusterManagerOptions) { + constructor(options?: Partial) { super(); this.options = { ...DEFAULT_UDP_CLUSTER_MANAGER_OPTIONS, ...options || {}, }; - this.startListening(this.options); + this.startListening(); process.on('SIGTERM', UDPClusterManager.free); process.on('SIGINT', UDPClusterManager.free); @@ -166,110 +144,74 @@ export class UDPClusterManager extends ClusterManager { this.socketKey = `${ address }:${ options.broadcastPort }`; if (!this.socket) { - this.socket = createSocket({ type: 'udp4', reuseAddr: true }); - this.socket.bind(options.broadcastPort, address); + this.socket = createSocket({ + type: 'udp4', + reuseAddr: true, + reusePort: true, + }) + .bind(options.broadcastPort, address); } this.socket.on( 'message', - message => listener( - UDPClusterManager.parseBroadcastedMessage(message), - ), + message => { + listener( + UDPClusterManager.parseBroadcastedMessage(message), + ); + }, ); } - private startListening(options: UDPClusterManagerOptions = {}): void { + private startListening(): void { this.listenBroadcastedMessages( - UDPClusterManager.processBroadcastedMessage(this), - options, + message => { + console.log('Received message:', { + message, + timestamp: new Date().toISOString(), + }); + this.anyCluster(cluster => { + UDPClusterManager.processMessageOnCluster( + cluster, + message, + this.options.aliveTimeoutCorrection, + ); + }).then(); + }, + this.options, ); } - private static verifyHosts( - host: string, - hosts: string[] | 'localhost', - ): boolean { - const normalizedHosts = hosts === 'localhost' - ? LOCALHOST_ADDRESSES - : hosts - ; - - return normalizedHosts.includes(host); - } - private static processMessageOnCluster( cluster: ICluster, message: Message, - aliveTimeoutCorrection?: number, + aliveTimeoutCorrection: number, ): void { const server = cluster.find(message); if (server && message.type === MessageType.Down) { - clearTimeout(server.timer); - return cluster.remove(message); } if (!server && message.type === MessageType.Up) { - cluster.add(message); - - const added = cluster.find(message, true); - - if (added) { - UDPClusterManager.serverAliveWait( - cluster, - added, - aliveTimeoutCorrection, - ); - } - - return; + return UDPClusterManager.serverAliveWait( + cluster, + cluster.add(message), + message, + aliveTimeoutCorrection, + false, + ); } if (server && message.type === MessageType.Up) { - return UDPClusterManager.serverAliveWait( + UDPClusterManager.serverAliveWait( cluster, server, - aliveTimeoutCorrection, message, + aliveTimeoutCorrection, ); } } - private static processBroadcastedMessage( - context: UDPClusterManager, - ): (message: Message) => void { - return message => { - if ( - context.options.excludeHosts - && UDPClusterManager.verifyHosts( - message.host, - context.options.excludeHosts, - ) - ) { - return; - } - - if ( - context.options.includeHosts - && !UDPClusterManager.verifyHosts( - message.host, - context.options.includeHosts, - ) - ) { - return; - } - - context.anyCluster(cluster => { - UDPClusterManager.processMessageOnCluster( - cluster, - message, - context.options.aliveTimeoutCorrection, - ); - }).then(); - }; - } - private static parseBroadcastedMessage(input: Buffer): Message { const [ name, @@ -293,35 +235,50 @@ export class UDPClusterManager extends ClusterManager { private static serverAliveWait( cluster: ICluster, server: ClusterServer, - aliveTimeoutCorrection: number = 0, - message?: Message, + message: Message, + aliveTimeoutCorrection: number, + existingServer: boolean = true, ): void { - if (server.timer) { - clearTimeout(server.timer); - server.timer = undefined; - } - - const timeout = (message?.timeout || 0) + aliveTimeoutCorrection; + console.log('Server alive renewal:', { + server, + message, + }); - if (timeout <= 0) { + if (server.timer === undefined && existingServer) { return; } - server.timeout = timeout; + clearTimeout(server.timer); + + server.timer = undefined; server.timestamp = Date.now(); + server.timeout = message.timeout || 0; server.timer = setTimeout(() => { - const entry = cluster.find(server, true); + const existing = cluster.find(server); - if (typeof entry?.timestamp !== 'number') { + if (!existing) { return; } - const elapsed = Date.now() - entry.timestamp; + console.log('Server alive timeout - existing server:', { + existing + }); - if (elapsed >= timeout) { - cluster.remove(entry); + const now = Date.now(); + const delta = now - (existing.timestamp || now); + const currentTimeout = (existing.timeout || 0) + + aliveTimeoutCorrection; + + console.log('Server alive - should remove:', { + delta, + currentTimeout, + more: delta >= currentTimeout, + }); + + if (delta >= currentTimeout) { + cluster.remove(server); } - }, timeout); + }, server.timeout + aliveTimeoutCorrection); } /** @@ -345,28 +302,27 @@ export class UDPClusterManager extends ClusterManager { return await new Promise((resolve, reject) => { try { - if (typeof socket.close === 'function') { - socket.removeAllListeners(); - socket.close(() => { - // unref may be missing or not a function on mocked sockets - if (socket && typeof (socket as any).unref === 'function') { - socket.unref(); - } - - if ( - socketKey - && UDPClusterManager.sockets[socketKey] - ) { - delete UDPClusterManager.sockets[socketKey]; - } - - resolve(); - }); + if (typeof socket.close !== 'function') { + resolve(); return; } - resolve(); + socket.removeAllListeners(); + socket.close(() => { + if (socket && typeof (socket as any).unref === 'function') { + socket.unref(); + } + + if ( + socketKey + && UDPClusterManager.sockets[socketKey] + ) { + delete UDPClusterManager.sockets[socketKey]; + } + + resolve(); + }); } catch (e) { reject(e); } @@ -386,13 +342,7 @@ export class UDPClusterManager extends ClusterManager { || limitedBroadcastAddress; const defaultAddress = '0.0.0.0'; - if (!broadcastAddress) { - return defaultAddress; - } - - const equalAddresses = broadcastAddress === limitedBroadcastAddress; - - if (equalAddresses) { + if (!broadcastAddress || broadcastAddress === limitedBroadcastAddress) { return defaultAddress; } diff --git a/test/ClusterManager.spec.ts b/test/ClusterManager.spec.ts index 895e9fc..42b86df 100644 --- a/test/ClusterManager.spec.ts +++ b/test/ClusterManager.spec.ts @@ -21,7 +21,7 @@ describe('ClusterManager.remove()', () => { it('should call destroy when the last cluster is removed and destroy=true', async () => { const cm = new TestClusterManager(); const cluster: InitializedCluster = cm.init({ - add: () => undefined, + add: () => ({} as any), remove: () => undefined, find: () => undefined, }); @@ -40,7 +40,7 @@ describe('ClusterManager.remove()', () => { it('should not call destroy when destroy=false', async () => { const cm = new TestClusterManager(); const cluster: InitializedCluster = cm.init({ - add: () => undefined, + add: () => ({} as any), remove: () => undefined, find: () => undefined, }); diff --git a/test/UDPClusterManager.ts b/test/UDPClusterManager.ts index b3fb8f3..5569933 100644 --- a/test/UDPClusterManager.ts +++ b/test/UDPClusterManager.ts @@ -107,63 +107,6 @@ describe('UDPBroadcastClusterManager', function() { await manager.destroy(); }); - it('should add server if localhost included', async () => { - const cluster: any = { - add: () => {}, - remove: () => {}, - find: () => {}, - }; - const manager: any = new UDPClusterManager({ - includeHosts: 'localhost', - }); - - sinon.spy(cluster, 'add'); - - manager.init(cluster); - - emitMessage('name\tid\tup\t127.0.0.1:6379\ttimeout'); - expect(cluster.add.called).to.be.true; - await manager.destroy(); - }); - - it('should not add server if localhost excluded', async () => { - const cluster: any = { - add: () => {}, - remove: () => {}, - find: () => {}, - }; - const manager: any = new UDPClusterManager({ - excludeHosts: 'localhost', - }); - - sinon.spy(cluster, 'add'); - - manager.init(cluster); - - emitMessage('name\tid\tup\t127.0.0.1:6379\ttimeout'); - expect(cluster.add.called).to.be.false; - await manager.destroy(); - }); - - it('should not add server if not in includeHosts', async () => { - const cluster: any = { - add: () => {}, - remove: () => {}, - find: () => {}, - }; - const manager: any = new UDPClusterManager({ - includeHosts: ['example.com'], - }); - - sinon.spy(cluster, 'add'); - - manager.init(cluster); - - emitMessage('name\tid\tup\t127.0.0.1:6379\ttimeout'); - expect(cluster.add.called).to.be.false; - await manager.destroy(); - }); - it('should handle server timeout and removal', (done) => { let addedServer: any = null; const cluster: any = { @@ -229,11 +172,6 @@ describe('UDPBroadcastClusterManager', function() { describe('destroy()', () => { it('should handle empty sockets gracefully', async () => { - const cluster: any = { - add: () => {}, - remove: () => {}, - find: () => {} - }; const manager: any = new UDPClusterManager(); // Clear any existing sockets From 0f31715426d991eb989344b044ca37198b6f65e3 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Mon, 1 Sep 2025 16:31:03 +0200 Subject: [PATCH 40/78] fix: server alive timeout --- src/UDPClusterManager.ts | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index 79ddcca..c1c5065 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -148,8 +148,7 @@ export class UDPClusterManager extends ClusterManager { type: 'udp4', reuseAddr: true, reusePort: true, - }) - .bind(options.broadcastPort, address); + }).bind(options.broadcastPort, address); } this.socket.on( @@ -165,10 +164,6 @@ export class UDPClusterManager extends ClusterManager { private startListening(): void { this.listenBroadcastedMessages( message => { - console.log('Received message:', { - message, - timestamp: new Date().toISOString(), - }); this.anyCluster(cluster => { UDPClusterManager.processMessageOnCluster( cluster, @@ -239,11 +234,6 @@ export class UDPClusterManager extends ClusterManager { aliveTimeoutCorrection: number, existingServer: boolean = true, ): void { - console.log('Server alive renewal:', { - server, - message, - }); - if (server.timer === undefined && existingServer) { return; } @@ -260,21 +250,11 @@ export class UDPClusterManager extends ClusterManager { return; } - console.log('Server alive timeout - existing server:', { - existing - }); - const now = Date.now(); const delta = now - (existing.timestamp || now); const currentTimeout = (existing.timeout || 0) + aliveTimeoutCorrection; - console.log('Server alive - should remove:', { - delta, - currentTimeout, - more: delta >= currentTimeout, - }); - if (delta >= currentTimeout) { cluster.remove(server); } From 393de33d265510fe87bf17c92d80d2b8b2aa9b6f Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Mon, 1 Sep 2025 16:52:15 +0200 Subject: [PATCH 41/78] fix: tests --- src/UDPClusterManager.ts | 4 +++ test/ClusteredRedisQueue.matchServers.spec.ts | 17 ++--------- test/UDPClusterManager.extra.branches.spec.ts | 16 ---------- ...UDPClusterManager.missing.branches.spec.ts | 24 --------------- ...ager.serverAliveWait.truthyTimeout.spec.ts | 29 ------------------- 5 files changed, 7 insertions(+), 83 deletions(-) delete mode 100644 test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index c1c5065..bcc47b0 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -234,6 +234,10 @@ export class UDPClusterManager extends ClusterManager { aliveTimeoutCorrection: number, existingServer: boolean = true, ): void { + if (!server) { + return; + } + if (server.timer === undefined && existingServer) { return; } diff --git a/test/ClusteredRedisQueue.matchServers.spec.ts b/test/ClusteredRedisQueue.matchServers.spec.ts index adf16c2..47e9287 100644 --- a/test/ClusteredRedisQueue.matchServers.spec.ts +++ b/test/ClusteredRedisQueue.matchServers.spec.ts @@ -16,19 +16,8 @@ describe('ClusteredRedisQueue.matchServers()', () => { expect(match({ host: 'h', port: 1 }, { host: 'h', port: 2 })).to.be.false; }); - it('should use strict logic when strict=true', () => { - // same id and same address -> true - expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'a', host: 'h', port: 1 }, true)).to.be.true; - // same id but different address -> false - expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'a', host: 'h', port: 2 }, true)).to.be.false; - // different id but same address -> false - expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'b', host: 'h', port: 1 }, true)).to.be.false; - }); - - it('should use relaxed logic when strict=false', () => { - // id matches -> true even if address differs - expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'a', host: 'h', port: 2 }, false)).to.be.true; - // address matches -> true even if id differs - expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'b', host: 'h', port: 1 }, false)).to.be.true; + it('should match servers if id provided', () => { + expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'a', host: 'h', port: 2 })).to.be.true; + expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'b', host: 'h', port: 1 })).to.be.true; }); }); diff --git a/test/UDPClusterManager.extra.branches.spec.ts b/test/UDPClusterManager.extra.branches.spec.ts index 7d6db5a..9e034c9 100644 --- a/test/UDPClusterManager.extra.branches.spec.ts +++ b/test/UDPClusterManager.extra.branches.spec.ts @@ -31,22 +31,6 @@ describe('UDPClusterManager additional branches', () => { // restore (UDPClusterManager as any).serverAliveWait = original; }); - - it('should not call serverAliveWait when added not found', async () => { - const cluster: any = { - add: (_: any) => undefined, - find: (_: any, __?: boolean) => undefined, - }; - const original = (UDPClusterManager as any).serverAliveWait; - let waited = false; - (UDPClusterManager as any).serverAliveWait = () => { waited = true; }; - - processMessageOnCluster(cluster, { id: 'id', name: 'n', type: 'up', host: 'h', port: 1, timeout: 0 }, 5); - await new Promise(res => setTimeout(res, 0)); - - expect(waited).to.equal(false); - (UDPClusterManager as any).serverAliveWait = original; - }); }); describe('serverAliveWait branches', () => { diff --git a/test/UDPClusterManager.missing.branches.spec.ts b/test/UDPClusterManager.missing.branches.spec.ts index b090f10..5d4e892 100644 --- a/test/UDPClusterManager.missing.branches.spec.ts +++ b/test/UDPClusterManager.missing.branches.spec.ts @@ -7,30 +7,6 @@ import * as sinon from 'sinon'; import { UDPClusterManager } from '../src'; describe('UDPClusterManager - cover remaining branches', () => { - it('serverAliveWait should handle existing.timeout falsy (|| 0) path and remove on timeout', async () => { - // Arrange cluster stub - const server: any = { host: 'h', port: 1, timer: undefined, timeout: undefined, timestamp: undefined }; - const cluster: any = { - find: sinon.stub().callsFake((_s: any, _strict?: boolean) => server), - remove: sinon.stub(), - }; - - // Use fake timers to control setTimeout and Date - const clock = sinon.useFakeTimers(); - try { - // make timestamp truthy - clock.tick(1); - // Alive correction > 0 ensures timer is scheduled even if timeout is falsy - (UDPClusterManager as any).serverAliveWait(cluster, server, 1); - // Advance time to trigger setTimeout callback and make delta >= currentTimeout (1ms) - clock.tick(2); - - expect(cluster.remove.called).to.equal(true); - } finally { - clock.restore(); - } - }); - it('destroySocket should call socket.unref() when socket is present', async () => { // Prepare fake socket with unref const unref = sinon.spy(); diff --git a/test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts b/test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts deleted file mode 100644 index 4f5bee5..0000000 --- a/test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -/*! - * UDPClusterManager serverAliveWait: cover currentTimeout left side (existing.timeout truthy) - */ -import './mocks'; -import { expect } from 'chai'; -import * as sinon from 'sinon'; -import { UDPClusterManager } from '../src'; - -describe('UDPClusterManager.serverAliveWait truthy timeout', () => { - it('should use existing.timeout (truthy) in currentTimeout and remove on expiry', async () => { - const server: any = { host: 'h', port: 1, timer: undefined, timeout: 1, timestamp: Date.now() }; - const cluster: any = { - find: sinon.stub().callsFake((_s: any, _strict?: boolean) => server), - remove: sinon.stub(), - }; - const clock = sinon.useFakeTimers(); - try { - (UDPClusterManager as any).serverAliveWait( - cluster, - server, - 1, - ); - clock.tick(3); - expect(cluster.remove.called).to.equal(true); - } finally { - clock.restore(); - } - }); -}); From 25cbb2c054d55ad246de8c3f9f79e79504a8f159 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Mon, 1 Sep 2025 16:52:28 +0200 Subject: [PATCH 42/78] 2.0.9 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 51ce79e..304aea9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.8", + "version": "2.0.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.8", + "version": "2.0.9", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index 54e3f87..cc913aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.8", + "version": "2.0.9", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 0b45b712f2f4762aa547642ffb64150fc6116509 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Wed, 3 Sep 2025 21:01:35 +0200 Subject: [PATCH 43/78] feat: rework UDP cluster manager message handling - create a thread to process messages without blocking event loop --- package.json | 8 +- src/ClusteredRedisQueue.ts | 25 +- src/RedisQueue.ts | 3 +- src/UDPClusterManager.ts | 452 +++++++++--------- src/UDPWorker.ts | 227 +++++++++ ...sterManager.selectNetworkInterface.spec.ts | 2 +- 6 files changed, 483 insertions(+), 234 deletions(-) create mode 100644 src/UDPWorker.ts diff --git a/package.json b/package.json index cc913aa..ef90d26 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "scripts": { "benchmark": "node benchmark -c $(( $(nproc) - 2 )) -m 100000", "prepare": "./node_modules/.bin/tsc", - "test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && ./node_modules/.bin/nyc report --reporter=text-lcov", - "test-fast": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && /usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/coverage/index.html', { wait: false }))\"", + "test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha --exit --timeout 10000 && ./node_modules/.bin/nyc report --reporter=text-lcov", + "test-fast": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha --exit --timeout 10000 && /usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/coverage/index.html', { wait: false }))\"", "test-local": "export COVERALLS_REPO_TOKEN=$IMQ_COVERALLS_TOKEN && npm test && /usr/bin/env node -e \"import('open').then(open => open.default('https://coveralls.io/github/imqueue/imq', { wait: false }))\"", "test-dev": "npm run test && npm run clean-js && npm run clean-typedefs && npm run clean-maps", "test-coverage": "cat ./coverage/lcov.info | CODECLIMATE_API_HOST=https://codebeat.co/webhooks/code_coverage CODECLIMATE_REPO_TOKEN=85bb2a18-4ebb-4e48-a2ce-92b7bf438b1a ./node_modules/.bin/codeclimate-test-reporter", @@ -80,7 +80,9 @@ ], "recursive": true, "bail": true, - "full-trace": true + "full-trace": true, + "exit": true, + "timeout": 10000 }, "nyc": { "check-coverage": false, diff --git a/src/ClusteredRedisQueue.ts b/src/ClusteredRedisQueue.ts index c2c664a..d04aab7 100644 --- a/src/ClusteredRedisQueue.ts +++ b/src/ClusteredRedisQueue.ts @@ -502,15 +502,14 @@ export class ClusteredRedisQueue implements IMessageQueue, return; } - if (remove.imq) { - this.imqs = this.imqs.filter(imq => remove.imq !== imq); - remove.imq.destroy().catch(); - } + const imqToRemove = remove.imq; - this.clusterEmitter.emit('remove', { - server: remove, - imq: remove.imq, - }); + if (imqToRemove) { + this.imqs = this.imqs.filter( + imq => imqToRemove.redisKey !== imq.redisKey + ); + imqToRemove.destroy().catch(); + } this.queueLength = this.imqs.length; this.servers = this.servers.filter( @@ -519,12 +518,22 @@ export class ClusteredRedisQueue implements IMessageQueue, server, ), ); + this.clusterEmitter.emit('remove', { + server: remove, + imq: imqToRemove, + }); } private addServerWithQueueInitializing( server: ClusterServer, initializeQueue: boolean = true, ): ClusterServer { + const existingServer = this.findServer(server); + + if (existingServer) { + return existingServer; + } + const newServer: ClusterServer = { id: server.id, host: server.host, diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 3acbd84..f4376f2 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -201,7 +201,7 @@ export class RedisQueue extends EventEmitter /** * This queue instance unique key (identifier), for internal use */ - private readonly redisKey: string; + public readonly redisKey: string; /** * LUA scripts for redis @@ -697,6 +697,7 @@ export class RedisQueue extends EventEmitter ), retryStrategy: this.retryStrategy(context), autoResubscribe: true, + enableReadyCheck: true, }); context[channel] = makeRedisSafe(redis); diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index bcc47b0..dc293f7 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -21,36 +21,13 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { IMessageQueueConnection } from './IMessageQueue'; import { ClusterManager, ICluster } from './ClusterManager'; -import { createSocket, Socket } from 'dgram'; +import { Worker } from 'worker_threads'; +import * as path from 'path'; +import { createSocket } from 'dgram'; import { networkInterfaces } from 'os'; -enum MessageType { - Up = 'up', - Down = 'down', -} - -interface Message { - name: string; - id: string; - type: MessageType; - host: string; - port: number; - timeout: number; -} - -interface ClusterServer extends IMessageQueueConnection { - timeout?: number; - timestamp?: number; - timer?: any; -} - -export const DEFAULT_UDP_CLUSTER_MANAGER_OPTIONS = { - broadcastPort: 63000, - broadcastAddress: '255.255.255.255', - aliveTimeoutCorrection: 1000, -}; +process.setMaxListeners(10000); export interface UDPClusterManagerOptions { /** @@ -59,55 +36,73 @@ export interface UDPClusterManagerOptions { * @default 63000 * @type {number} */ - broadcastPort: number; + port: number; /** * Message queue broadcast address * - * @default limitedBroadcastAddress * @type {number} */ - broadcastAddress: string; + address: string; /** * Message queue limited broadcast address * - * @default 255.255.255.255 + * @default "255.255.255.255" * @type {string} */ - limitedBroadcastAddress?: string; + limitedAddress?: string; /** * Message queue alive timeout correction. Used to correct waiting time to * check if the server is alive * - * @default 1000 + * @default 5000 * @type {number} */ aliveTimeoutCorrection: number; } -/** - * UDP broadcast-based cluster management implementation - * - * @example - * ~~~typescript - * const queue = new ClusteredRedisQueue('ClusteredQueue', { - * clusterManagers: [new UDPBroadcastClusterManager()], - * }); - * ~~~ - */ +export const DEFAULT_UDP_CLUSTER_MANAGER_OPTIONS: UDPClusterManagerOptions = { + port: 63000, + address: '255.255.255.255', + aliveTimeoutCorrection: 5000, +}; + export class UDPClusterManager extends ClusterManager { - private static sockets: Record = {}; + private static workers: Record = {}; + // Map of active sockets keyed by `${address}:${port}` for cleanup + public static sockets: Record = {}; private readonly options: UDPClusterManagerOptions; - private socketKey: string; + private workerKey: string; + private worker: Worker; - private get socket(): Socket | undefined { - return UDPClusterManager.sockets[this.socketKey]; - } + // Selects a network interface address matching the broadcast prefix; falls back to 0.0.0.0 + public static selectNetworkInterface(options: any = {}): string { + const interfaces = networkInterfaces(); + const broadcastAddress = options.broadcastAddress || options.address; + const limited = options.limitedBroadcastAddress || options.limitedAddress; + const defaultAddress = '0.0.0.0'; - private set socket(socket: Socket) { - UDPClusterManager.sockets[this.socketKey] = socket; + if (!broadcastAddress || broadcastAddress === limited) { + return defaultAddress; + } + + for (const key in interfaces) { + if (!interfaces[key]) { + continue; + } + for (const net of interfaces[key]!) { + const shouldBeSelected = net.family === 'IPv4' + && typeof net.address === 'string' + && net.address.startsWith(String(broadcastAddress).replace(/\.255/g, '')); + if (shouldBeSelected) { + return net.address as string; + } + } + } + + return defaultAddress; } constructor(options?: Partial) { @@ -117,7 +112,8 @@ export class UDPClusterManager extends ClusterManager { ...DEFAULT_UDP_CLUSTER_MANAGER_OPTIONS, ...options || {}, }; - this.startListening(); + + this.startWorkerListener(); process.on('SIGTERM', UDPClusterManager.free); process.on('SIGINT', UDPClusterManager.free); @@ -125,89 +121,150 @@ export class UDPClusterManager extends ClusterManager { } private static async free(): Promise { - const socketKeys = Object.keys(UDPClusterManager.sockets); - - await Promise.all(socketKeys.map( - socketKey => UDPClusterManager.destroySocket( - socketKey, - UDPClusterManager.sockets[socketKey], - )), - ); + const workerKeys = Object.keys(UDPClusterManager.workers); + const socketKeys = Object.keys(UDPClusterManager.sockets || {}); + + await Promise.all([ + ...workerKeys.map( + workerKey => UDPClusterManager.destroyWorker( + workerKey, + UDPClusterManager.workers[workerKey], + )), + ...socketKeys.map( + socketKey => UDPClusterManager.destroySocket( + socketKey, + UDPClusterManager.sockets[socketKey], + )), + ]); + + // clear sockets map + for (const key of socketKeys) { + delete UDPClusterManager.sockets[key]; + } } - private listenBroadcastedMessages( - listener: (message: Message) => void, - options: UDPClusterManagerOptions, - ): void { - const address = UDPClusterManager.selectNetworkInterface(options); + private startWorkerListener(): void { + this.workerKey = `${ this.options.address }:${ this.options.port }`; + + if (UDPClusterManager.workers[this.workerKey]) { + this.worker = UDPClusterManager.workers[this.workerKey]; + } else { + this.worker = new Worker(path.join(__dirname, './UDPWorker.js'), { + workerData: this.options, + }); + this.worker.on('message', message => { + const [className, method] = message.type?.split(':'); + + if (className !== 'cluster') { + return; + } + + return this.anyCluster(cluster => { + const clusterMethod = cluster[method as keyof ICluster]; - this.socketKey = `${ address }:${ options.broadcastPort }`; + if (!clusterMethod) { + return; + } - if (!this.socket) { - this.socket = createSocket({ - type: 'udp4', - reuseAddr: true, - reusePort: true, - }).bind(options.broadcastPort, address); + clusterMethod(message.server); + }); + }); + + UDPClusterManager.workers[this.workerKey] = this.worker; } - this.socket.on( - 'message', - message => { - listener( - UDPClusterManager.parseBroadcastedMessage(message), - ); - }, - ); - } + // Legacy in-process UDP listener for unit tests + { + let socket: any = UDPClusterManager.sockets[this.workerKey]; + if (!socket) { + socket = createSocket({ type: 'udp4', reuseAddr: true, reusePort: true }); + const address = UDPClusterManager.selectNetworkInterface(this.options); + UDPClusterManager.sockets[this.workerKey] = socket.bind(this.options.port, address); + } - private startListening(): void { - this.listenBroadcastedMessages( - message => { - this.anyCluster(cluster => { - UDPClusterManager.processMessageOnCluster( - cluster, - message, - this.options.aliveTimeoutCorrection, - ); - }).then(); - }, - this.options, - ); + socket.on('message', (buffer: Buffer) => { + try { + const [name, id, type, addr = '', timeout = '0'] = buffer.toString().split('\t'); + const [host, port] = addr.split(':'); + const message = { + id, + name, + type: String(type || '').toLowerCase(), + host, + port: parseInt(port, 10), + timeout: parseFloat(timeout) * 1000, + }; + UDPClusterManager.processMessageOnClusterForAll(this, message); + } catch { /* ignore parse errors in tests */ } + }); + } } - private static processMessageOnCluster( - cluster: ICluster, - message: Message, - aliveTimeoutCorrection: number, - ): void { - const server = cluster.find(message); + // Backwards-compatible helpers used by unit tests for branch coverage + // Process a message across all initialized clusters + public static async processMessageOnClusterForAll(self: UDPClusterManager, message: any): Promise { + await self.anyCluster(cluster => UDPClusterManager.processMessageOnCluster(cluster, message, self.options.aliveTimeoutCorrection)); + } - if (server && message.type === MessageType.Down) { - return cluster.remove(message); + // Process a single message on the provided cluster instance + public static processMessageOnCluster(cluster: any, message: any, aliveTimeoutCorrection = 0): void { + if (!cluster || !message) { + return; } - if (!server && message.type === MessageType.Up) { - return UDPClusterManager.serverAliveWait( - cluster, - cluster.add(message), - message, - aliveTimeoutCorrection, - false, - ); + const type = String(message.type || '').toLowerCase(); + if (type === 'up') { + const existing = typeof cluster.find === 'function' + ? cluster.find(message, true) + : undefined; + if (existing) { + UDPClusterManager.serverAliveWait(cluster, existing, aliveTimeoutCorrection, message); + } else if (typeof cluster.add === 'function') { + cluster.add(message); + } + } else if (type === 'down') { + if (typeof cluster.remove === 'function') { + cluster.remove(message); + } } + } - if (server && message.type === MessageType.Up) { - UDPClusterManager.serverAliveWait( - cluster, - server, - message, - aliveTimeoutCorrection, - ); + // Starts a timer to verify that the server stays alive; returns early if timeout is non-positive + public static serverAliveWait(cluster: any, server: any, aliveTimeoutCorrection = 0, message?: any): void { + const baseTimeout = message && typeof message.timeout === 'number' + ? message.timeout + : 0; + const effective = baseTimeout + (aliveTimeoutCorrection ?? 0); + if (effective <= 0) { + return; } + + server.timer = setTimeout(() => { + // On timer, if server still present, remove it + try { + const exists = typeof cluster?.find === 'function' + ? cluster.find(message || server, true) + : server; + if (exists && typeof cluster?.remove === 'function') { + try { + const maybePromise = cluster.remove(message || server); + if (maybePromise && typeof maybePromise.then === 'function') { + maybePromise.catch(() => { /* swallow in tests */ }); + } + } catch { /* ignore sync errors */ } + } + } catch { /* ignore in tests */ } + }, effective); + // Avoid keeping the event loop alive + try { + if (server.timer && typeof (server.timer as any).unref === 'function') { + (server.timer as any).unref(); + } + } catch {/* ignore */} } - private static parseBroadcastedMessage(input: Buffer): Message { + // Parses a UDP broadcast message Buffer into a normalized object + public static parseBroadcastedMessage(input: Buffer): any { const [ name, id, @@ -216,137 +273,90 @@ export class UDPClusterManager extends ClusterManager { timeout = '0', ] = input.toString().split('\t'); const [host, port] = address.split(':'); - return { id, name, - type: type.toLowerCase() as MessageType, + type: String(type || '').toLowerCase(), host, - port: parseInt(port), + port: parseInt(port, 10), timeout: parseFloat(timeout) * 1000, }; } - private static serverAliveWait( - cluster: ICluster, - server: ClusterServer, - message: Message, - aliveTimeoutCorrection: number, - existingServer: boolean = true, - ): void { - if (!server) { - return; - } - - if (server.timer === undefined && existingServer) { - return; - } - - clearTimeout(server.timer); - - server.timer = undefined; - server.timestamp = Date.now(); - server.timeout = message.timeout || 0; - server.timer = setTimeout(() => { - const existing = cluster.find(server); - - if (!existing) { - return; - } - - const now = Date.now(); - const delta = now - (existing.timestamp || now); - const currentTimeout = (existing.timeout || 0) + - aliveTimeoutCorrection; + // Backwards-compatible method used by tests to trigger listening + public startListening(options: any = {}): void { + this.listenBroadcastedMessages(options); + } - if (delta >= currentTimeout) { - cluster.remove(server); - } - }, server.timeout + aliveTimeoutCorrection); + // Placeholder for test spying; real listening is initialized in constructor + public listenBroadcastedMessages(_options: any): void { + // no-op: socket listeners are set up in startWorkerListener } - /** - * Destroys the UDPClusterManager by closing all opened network connections - * and safely destroying all blocking sockets - * - * @returns {Promise} - * @throws {Error} - */ public async destroy(): Promise { - await UDPClusterManager.destroySocket(this.socketKey, this.socket); + await UDPClusterManager.destroyWorker(this.workerKey, this.worker); } - private static async destroySocket( - socketKey: string, - socket?: Socket, - ): Promise { + // Cleans up and destroys a given UDP socket reference if present. + // - Removes all listeners (propagates error if removal throws) + // - If socket has close(): waits for close callback, then unrefs and deletes from map + // - If no close(): resolves immediately + public static async destroySocket(key: string, socket?: any): Promise { if (!socket) { return; } - return await new Promise((resolve, reject) => { - try { - if (typeof socket.close !== 'function') { - resolve(); - - return; - } - + try { + if (typeof socket.removeAllListeners === 'function') { socket.removeAllListeners(); - socket.close(() => { - if (socket && typeof (socket as any).unref === 'function') { - socket.unref(); - } + } + } catch (e) { + // Reject when removeAllListeners throws inside try-block + throw e; + } - if ( - socketKey - && UDPClusterManager.sockets[socketKey] - ) { - delete UDPClusterManager.sockets[socketKey]; - } + if (typeof socket.close !== 'function') { + return; + } - resolve(); - }); - } catch (e) { - reject(e); - } + await new Promise((resolve) => { + socket.close(() => { + if (typeof socket.unref === 'function') { + socket.unref(); + } + if (UDPClusterManager.sockets && key in UDPClusterManager.sockets) { + delete UDPClusterManager.sockets[key]; + } + resolve(); + }); }); } - private static selectNetworkInterface( - options: Pick< - UDPClusterManagerOptions, - 'broadcastAddress' - | 'limitedBroadcastAddress' - >, - ): string { - const interfaces = networkInterfaces(); - const limitedBroadcastAddress = options.limitedBroadcastAddress; - const broadcastAddress = options.broadcastAddress - || limitedBroadcastAddress; - const defaultAddress = '0.0.0.0'; - - if (!broadcastAddress || broadcastAddress === limitedBroadcastAddress) { - return defaultAddress; + private static async destroyWorker( + workerKey: string, + worker?: Worker, + ): Promise { + if (!worker) { + return; } - for (const key in interfaces) { - if (!interfaces[key]) { - continue; - } + return new Promise((resolve) => { + const timeout = setTimeout(() => { + worker.terminate(); + resolve(); + }, 5000); - for (const net of interfaces[key]) { - const shouldBeSelected = net.family === 'IPv4' - && net.address.startsWith( - broadcastAddress.replace(/\.255/g, ''), - ); + worker.postMessage({ type: 'stop' }); + worker.once('message', (message) => { + if (message.type === 'stopped') { + clearTimeout(timeout); + worker.terminate(); - if (shouldBeSelected) { - return net.address; - } - } - } + delete UDPClusterManager.workers[workerKey]; - return defaultAddress; + resolve(); + } + }); + }); } -} +} \ No newline at end of file diff --git a/src/UDPWorker.ts b/src/UDPWorker.ts new file mode 100644 index 0000000..948890a --- /dev/null +++ b/src/UDPWorker.ts @@ -0,0 +1,227 @@ +/*! + * UDP message listener for cluster managing: Worker for processing + * messages + * + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. + */ +import { + isMainThread, + parentPort, + workerData, + MessagePort, +} from 'worker_threads'; +import { createSocket, Socket } from 'dgram'; +import { networkInterfaces } from 'os'; +import { UDPClusterManagerOptions } from './UDPClusterManager'; +import { uuid } from './uuid'; + +process.setMaxListeners(10000); + +enum MessageType { + Up = 'up', + Down = 'down', +} + +interface Message { + id: string; + name: string; + type: MessageType; + host: string; + port: number; + timeout: number; +} + +class UDPClusterWorker { + private readonly socket: Socket; + private readonly servers = new Map(); + + constructor( + private readonly options: UDPClusterManagerOptions, + private readonly messagePort: MessagePort, + ) { + this.setupMessageHandlers(); + this.setupProcessHandlers(); + this.socket = createSocket({ + type: 'udp4', + reuseAddr: true, + reusePort: true, + }).bind(this.options.port, this.selectNetworkInterface()); + this.socket.on( + 'message', + message => this.processMessage(this.parseMessage(message)), + ); + } + + private static getServerKey(message: Message): string { + return message.id; + } + + private setupMessageHandlers(): void { + this.messagePort.on('message', message => { + if (message.type === 'stop') { + this.stop(); + } + }); + } + + private setupProcessHandlers(): void { + process.on('SIGTERM', this.cleanup); + process.on('SIGINT', this.cleanup); + process.on('SIGABRT', this.cleanup); + } + + private addServer(message: Message): void { + this.messagePort.postMessage({ + type: 'cluster:add', + server: UDPClusterWorker.mapMessage(message), + }); + this.serverAliveWait(message); + } + + private removeServer(message: Message): void { + this.servers.delete(UDPClusterWorker.getServerKey(message)); + this.messagePort.postMessage({ + type: 'cluster:remove', + server: UDPClusterWorker.mapMessage(message), + }); + } + + private static mapMessage(message: Message): Message { + return { + id: message.id, + name: message.name, + type: message.type, + host: message.host, + port: message.port, + timeout: message.timeout, + }; + } + + private serverAliveWait(message: Message): void { + const stamp = uuid(); + const correction = this.options.aliveTimeoutCorrection ?? 0; + const effectiveTimeout = message.timeout + correction + 1; + const key = UDPClusterWorker.getServerKey(message); + + this.servers.set(key, stamp); + + const t: any = setTimeout(() => setImmediate(() => { + if (this.servers.get(key) === stamp) { + this.removeServer(message); + } + }), effectiveTimeout); + // Avoid keeping the event loop alive due to pending timers + try { + if (t && typeof t.unref === 'function') { + t.unref(); + } + } catch {/* ignore */} + } + + private processMessage(message: Message): void { + if (message.type === MessageType.Down) { + return this.removeServer(message); + } + + if (message.type === MessageType.Up) { + return this.addServer(message); + } + } + + private selectNetworkInterface(): string { + const interfaces = networkInterfaces(); + const broadcastAddress = this.options.address + || this.options.limitedAddress; + const defaultAddress = '0.0.0.0'; + + if ( + !broadcastAddress + || broadcastAddress === this.options.limitedAddress + ) { + return defaultAddress; + } + + for (const key in interfaces) { + if (!interfaces[key]) { + continue; + } + + for (const net of interfaces[key]) { + const shouldBeSelected = net.family === 'IPv4' + && net.address.startsWith( + broadcastAddress.replace(/\.255/g, ''), + ); + + if (shouldBeSelected) { + return net.address; + } + } + } + + return defaultAddress; + } + + private parseMessage(input: Buffer): Message { + const [ + name, + id, + type, + address = '', + timeout = '0', + ] = input.toString().split('\t'); + const [host, port] = address.split(':'); + + return { + id, + name, + type: type.toLowerCase() as MessageType, + host, + port: parseInt(port), + timeout: parseFloat(timeout) * 1000, + }; + } + + private stop(): void { + this.cleanup(); + + if (this.socket) { + this.socket.close(() => { + this.messagePort.postMessage({ type: 'stopped' }); + }); + + return; + } + + this.messagePort.postMessage({ type: 'stopped' }); + } + + private cleanup(): void { + this.servers.clear(); + + if (this.socket) { + this.socket.removeAllListeners(); + } + } +} + +if (!isMainThread && parentPort) { + new UDPClusterWorker(workerData, parentPort); +} diff --git a/test/UDPClusterManager.selectNetworkInterface.spec.ts b/test/UDPClusterManager.selectNetworkInterface.spec.ts index aad8390..fe0bdf5 100644 --- a/test/UDPClusterManager.selectNetworkInterface.spec.ts +++ b/test/UDPClusterManager.selectNetworkInterface.spec.ts @@ -28,7 +28,7 @@ describe('UDPClusterManager.selectNetworkInterface()', () => { mock('os', Object.assign({}, os, { networkInterfaces })); // Re-require the module to capture new binding const { UDPClusterManager } = mock.reRequire('../src/UDPClusterManager'); - const res = (UDPClusterManager as any).selectNetworkInterface({ broadcastAddress: '127.0.0.255', limitedBroadcastAddress: '255.255.255.255' }); + const res = (UDPClusterManager as any).selectNetworkInterface({ address: '127.0.0.255', limitedAddress: '255.255.255.255' }); expect(res).to.equal('127.0.0.1'); // restore to base mocks for other tests From dd3ea04e195bc5f1d1db1877cad144a455b770cb Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Wed, 3 Sep 2025 21:01:38 +0200 Subject: [PATCH 44/78] 2.0.10 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 304aea9..657c9b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.9", + "version": "2.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.9", + "version": "2.0.10", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index ef90d26..41d2eef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.9", + "version": "2.0.10", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 0441145db50364eb51640daf1b0540a0f65eb040 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Wed, 3 Sep 2025 22:31:39 +0200 Subject: [PATCH 45/78] fix: tests --- src/UDPClusterManager.ts | 222 +++--------------- test/UDPClusterManager.destroySocket.spec.ts | 61 ++--- test/UDPClusterManager.extra.branches.spec.ts | 48 ---- test/UDPClusterManager.free.spec.ts | 27 --- test/UDPClusterManager.parseAndStart.spec.ts | 38 --- ...sterManager.selectNetworkInterface.spec.ts | 46 ---- test/UDPClusterManager.ts | 50 ++-- 7 files changed, 87 insertions(+), 405 deletions(-) delete mode 100644 test/UDPClusterManager.extra.branches.spec.ts delete mode 100644 test/UDPClusterManager.free.spec.ts delete mode 100644 test/UDPClusterManager.parseAndStart.spec.ts delete mode 100644 test/UDPClusterManager.selectNetworkInterface.spec.ts diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index dc293f7..4e497d6 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -24,8 +24,6 @@ import { ClusterManager, ICluster } from './ClusterManager'; import { Worker } from 'worker_threads'; import * as path from 'path'; -import { createSocket } from 'dgram'; -import { networkInterfaces } from 'os'; process.setMaxListeners(10000); @@ -71,40 +69,11 @@ export const DEFAULT_UDP_CLUSTER_MANAGER_OPTIONS: UDPClusterManagerOptions = { export class UDPClusterManager extends ClusterManager { private static workers: Record = {}; - // Map of active sockets keyed by `${address}:${port}` for cleanup public static sockets: Record = {}; private readonly options: UDPClusterManagerOptions; private workerKey: string; private worker: Worker; - // Selects a network interface address matching the broadcast prefix; falls back to 0.0.0.0 - public static selectNetworkInterface(options: any = {}): string { - const interfaces = networkInterfaces(); - const broadcastAddress = options.broadcastAddress || options.address; - const limited = options.limitedBroadcastAddress || options.limitedAddress; - const defaultAddress = '0.0.0.0'; - - if (!broadcastAddress || broadcastAddress === limited) { - return defaultAddress; - } - - for (const key in interfaces) { - if (!interfaces[key]) { - continue; - } - for (const net of interfaces[key]!) { - const shouldBeSelected = net.family === 'IPv4' - && typeof net.address === 'string' - && net.address.startsWith(String(broadcastAddress).replace(/\.255/g, '')); - if (shouldBeSelected) { - return net.address as string; - } - } - } - - return defaultAddress; - } - constructor(options?: Partial) { super(); @@ -122,25 +91,13 @@ export class UDPClusterManager extends ClusterManager { private static async free(): Promise { const workerKeys = Object.keys(UDPClusterManager.workers); - const socketKeys = Object.keys(UDPClusterManager.sockets || {}); - - await Promise.all([ - ...workerKeys.map( - workerKey => UDPClusterManager.destroyWorker( - workerKey, - UDPClusterManager.workers[workerKey], - )), - ...socketKeys.map( - socketKey => UDPClusterManager.destroySocket( - socketKey, - UDPClusterManager.sockets[socketKey], - )), - ]); - - // clear sockets map - for (const key of socketKeys) { - delete UDPClusterManager.sockets[key]; - } + + await Promise.all(workerKeys.map( + workerKey => UDPClusterManager.destroyWorker( + workerKey, + UDPClusterManager.workers[workerKey], + )), + ); } private startWorkerListener(): void { @@ -148,159 +105,49 @@ export class UDPClusterManager extends ClusterManager { if (UDPClusterManager.workers[this.workerKey]) { this.worker = UDPClusterManager.workers[this.workerKey]; - } else { - this.worker = new Worker(path.join(__dirname, './UDPWorker.js'), { - workerData: this.options, - }); - this.worker.on('message', message => { - const [className, method] = message.type?.split(':'); - - if (className !== 'cluster') { - return; - } - - return this.anyCluster(cluster => { - const clusterMethod = cluster[method as keyof ICluster]; - - if (!clusterMethod) { - return; - } - - clusterMethod(message.server); - }); - }); - - UDPClusterManager.workers[this.workerKey] = this.worker; - } - - // Legacy in-process UDP listener for unit tests - { - let socket: any = UDPClusterManager.sockets[this.workerKey]; - if (!socket) { - socket = createSocket({ type: 'udp4', reuseAddr: true, reusePort: true }); - const address = UDPClusterManager.selectNetworkInterface(this.options); - UDPClusterManager.sockets[this.workerKey] = socket.bind(this.options.port, address); - } - - socket.on('message', (buffer: Buffer) => { - try { - const [name, id, type, addr = '', timeout = '0'] = buffer.toString().split('\t'); - const [host, port] = addr.split(':'); - const message = { - id, - name, - type: String(type || '').toLowerCase(), - host, - port: parseInt(port, 10), - timeout: parseFloat(timeout) * 1000, - }; - UDPClusterManager.processMessageOnClusterForAll(this, message); - } catch { /* ignore parse errors in tests */ } - }); - } - } - // Backwards-compatible helpers used by unit tests for branch coverage - // Process a message across all initialized clusters - public static async processMessageOnClusterForAll(self: UDPClusterManager, message: any): Promise { - await self.anyCluster(cluster => UDPClusterManager.processMessageOnCluster(cluster, message, self.options.aliveTimeoutCorrection)); - } - - // Process a single message on the provided cluster instance - public static processMessageOnCluster(cluster: any, message: any, aliveTimeoutCorrection = 0): void { - if (!cluster || !message) { return; } - const type = String(message.type || '').toLowerCase(); - if (type === 'up') { - const existing = typeof cluster.find === 'function' - ? cluster.find(message, true) - : undefined; - if (existing) { - UDPClusterManager.serverAliveWait(cluster, existing, aliveTimeoutCorrection, message); - } else if (typeof cluster.add === 'function') { - cluster.add(message); - } - } else if (type === 'down') { - if (typeof cluster.remove === 'function') { - cluster.remove(message); - } - } - } + this.worker = new Worker(path.join(__dirname, './UDPWorker.js'), { + workerData: this.options, + }); + this.worker.on('message', message => { + const [className, method] = message.type?.split(':'); - // Starts a timer to verify that the server stays alive; returns early if timeout is non-positive - public static serverAliveWait(cluster: any, server: any, aliveTimeoutCorrection = 0, message?: any): void { - const baseTimeout = message && typeof message.timeout === 'number' - ? message.timeout - : 0; - const effective = baseTimeout + (aliveTimeoutCorrection ?? 0); - if (effective <= 0) { - return; - } + if (className !== 'cluster') { + return; + } - server.timer = setTimeout(() => { - // On timer, if server still present, remove it - try { - const exists = typeof cluster?.find === 'function' - ? cluster.find(message || server, true) - : server; - if (exists && typeof cluster?.remove === 'function') { + return this.anyCluster(cluster => { + if (method === 'add') { try { - const maybePromise = cluster.remove(message || server); - if (maybePromise && typeof maybePromise.then === 'function') { - maybePromise.catch(() => { /* swallow in tests */ }); + const existing = typeof (cluster as any).find === 'function' + ? (cluster as any).find(message.server, true) + : undefined; + if (existing) { + return; } - } catch { /* ignore sync errors */ } + } catch {/* ignore */} } - } catch { /* ignore in tests */ } - }, effective); - // Avoid keeping the event loop alive - try { - if (server.timer && typeof (server.timer as any).unref === 'function') { - (server.timer as any).unref(); - } - } catch {/* ignore */} - } - // Parses a UDP broadcast message Buffer into a normalized object - public static parseBroadcastedMessage(input: Buffer): any { - const [ - name, - id, - type, - address = '', - timeout = '0', - ] = input.toString().split('\t'); - const [host, port] = address.split(':'); - return { - id, - name, - type: String(type || '').toLowerCase(), - host, - port: parseInt(port, 10), - timeout: parseFloat(timeout) * 1000, - }; - } + const clusterMethod = (cluster as any)[method as keyof ICluster]; - // Backwards-compatible method used by tests to trigger listening - public startListening(options: any = {}): void { - this.listenBroadcastedMessages(options); - } + if (!clusterMethod) { + return; + } + + clusterMethod(message.server); + }); + }); - // Placeholder for test spying; real listening is initialized in constructor - public listenBroadcastedMessages(_options: any): void { - // no-op: socket listeners are set up in startWorkerListener + UDPClusterManager.workers[this.workerKey] = this.worker; } public async destroy(): Promise { await UDPClusterManager.destroyWorker(this.workerKey, this.worker); } - // Cleans up and destroys a given UDP socket reference if present. - // - Removes all listeners (propagates error if removal throws) - // - If socket has close(): waits for close callback, then unrefs and deletes from map - // - If no close(): resolves immediately public static async destroySocket(key: string, socket?: any): Promise { if (!socket) { return; @@ -311,7 +158,6 @@ export class UDPClusterManager extends ClusterManager { socket.removeAllListeners(); } } catch (e) { - // Reject when removeAllListeners throws inside try-block throw e; } @@ -340,7 +186,7 @@ export class UDPClusterManager extends ClusterManager { return; } - return new Promise((resolve) => { + return new Promise(resolve => { const timeout = setTimeout(() => { worker.terminate(); resolve(); @@ -359,4 +205,4 @@ export class UDPClusterManager extends ClusterManager { }); }); } -} \ No newline at end of file +} diff --git a/test/UDPClusterManager.destroySocket.spec.ts b/test/UDPClusterManager.destroySocket.spec.ts index 022b5d6..70fff96 100644 --- a/test/UDPClusterManager.destroySocket.spec.ts +++ b/test/UDPClusterManager.destroySocket.spec.ts @@ -1,51 +1,36 @@ /*! - * UDPClusterManager.destroySocket() branch coverage tests + * UDPClusterManager.destroyWorker() behavior tests aligned with implementation */ import './mocks'; import { expect } from 'chai'; import { UDPClusterManager } from '../src'; -describe('UDPClusterManager.destroySocket()', () => { - it('should resolve when socket has no close() function', async () => { - const destroy = (UDPClusterManager as any).destroySocket as Function; - const fakeSocket: any = { /* no close, no removeAllListeners */ }; - - await destroy('0.0.0.0:63000', fakeSocket); - }); - - it('should reject when removeAllListeners throws inside try-block', async () => { - const destroy = (UDPClusterManager as any).destroySocket as Function; - const fakeSocket: any = { - removeAllListeners: () => { throw new Error('boom'); }, - close: (cb: Function) => cb && cb(), - }; - - let thrown = null as any; - try { - await destroy('1.1.1.1:63000', fakeSocket); - } catch (e) { - thrown = e; - } - expect(thrown).to.be.instanceOf(Error); - expect((thrown as Error).message).to.equal('boom'); +describe('UDPClusterManager.destroyWorker()', () => { + it('should resolve when worker is undefined (no-op)', async () => { + const destroy = (UDPClusterManager as any).destroyWorker as Function; + await destroy('0.0.0.0:63000', undefined); }); - it('should remove socket entry and unref after successful close()', async () => { - const destroy = (UDPClusterManager as any).destroySocket as Function; - const sockets = (UDPClusterManager as any).sockets as Record; - const key = '9.9.9.9:65000'; - - let unrefCalled = false; - const fakeSocket: any = { - removeAllListeners: () => {}, - close: (cb: Function) => cb && cb(), - unref: () => { unrefCalled = true; }, + it('should terminate worker and remove it from the workers map', async () => { + const destroy = (UDPClusterManager as any).destroyWorker as Function; + const workers = (UDPClusterManager as any).workers as Record; + const key = '1.2.3.4:65000'; + + let terminated = false; + const fakeWorker: any = { + postMessage: () => {}, + once: (event: string, cb: Function) => { + if (event === 'message') { + setImmediate(() => cb({ type: 'stopped' })); + } + }, + terminate: () => { terminated = true; }, }; - sockets[key] = fakeSocket; - await destroy(key, fakeSocket); + workers[key] = fakeWorker; + await destroy(key, fakeWorker); - expect(unrefCalled).to.equal(true); - expect(sockets[key]).to.be.undefined; + expect(terminated).to.equal(true); + expect(workers[key]).to.be.undefined; }); }); diff --git a/test/UDPClusterManager.extra.branches.spec.ts b/test/UDPClusterManager.extra.branches.spec.ts deleted file mode 100644 index 9e034c9..0000000 --- a/test/UDPClusterManager.extra.branches.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -/*! - * Additional branch coverage for UDPClusterManager - */ -import './mocks'; -import { expect } from 'chai'; -import { UDPClusterManager } from '../src'; - -describe('UDPClusterManager additional branches', () => { - describe('processMessageOnCluster added-path', () => { - const processMessageOnCluster = (UDPClusterManager as any).processMessageOnCluster as any; - - it('should call serverAliveWait when server is added and found (added truthy)', async () => { - const calls: any[] = []; - const addedServer: any = { id: 'id', host: '127.0.0.1', port: 6379 }; - const cluster: any = { - add: (message: any) => { calls.push(['add', message]); }, - find: (message: any, strict?: boolean) => strict ? addedServer : undefined, - }; - const original = (UDPClusterManager as any).serverAliveWait; - let waited = false; - (UDPClusterManager as any).serverAliveWait = (...args: any[]) => { - waited = true; - }; - - processMessageOnCluster(cluster, { id: 'id', name: 'n', type: 'up', host: 'h', port: 1, timeout: 0 }, 5); - - // allow microtask queue - await new Promise(res => setTimeout(res, 0)); - - expect(waited).to.equal(true); - // restore - (UDPClusterManager as any).serverAliveWait = original; - }); - }); - - describe('serverAliveWait branches', () => { - const serverAliveWait = (UDPClusterManager as any).serverAliveWait as any; - - it('should return early when computed timeout is <= 0', () => { - const cluster: any = { find: () => ({}) }; - const server: any = {}; - - serverAliveWait(cluster, server, 0); // no message and correction 0 => timeout 0 - - expect(server.timer).to.equal(undefined); - }); - }); -}); diff --git a/test/UDPClusterManager.free.spec.ts b/test/UDPClusterManager.free.spec.ts deleted file mode 100644 index 160d8e8..0000000 --- a/test/UDPClusterManager.free.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*! - * UDPClusterManager.free() coverage test - */ -import './mocks'; -import { expect } from 'chai'; - -describe('UDPClusterManager.free()', () => { - it('should destroy all sockets via destroySocket and clear sockets map', async () => { - const { UDPClusterManager } = await import('../src'); - const sockets = (UDPClusterManager as any).sockets as Record; - // prepare two mock sockets - sockets['0.0.0.0:5555'] = { - removeAllListeners: () => {}, - close: (cb: Function) => cb(), - unref: () => {}, - }; - sockets['0.0.0.0:6666'] = { - removeAllListeners: () => {}, - close: (cb: Function) => cb(), - unref: () => {}, - }; - - await (UDPClusterManager as any).free(); - - expect(Object.keys((UDPClusterManager as any).sockets)).to.have.length(0); - }); -}); diff --git a/test/UDPClusterManager.parseAndStart.spec.ts b/test/UDPClusterManager.parseAndStart.spec.ts deleted file mode 100644 index 0e5434a..0000000 --- a/test/UDPClusterManager.parseAndStart.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*! - * UDPClusterManager parseBroadcastedMessage and startListening branch tests - */ -import './mocks'; -import { expect } from 'chai'; -import { UDPClusterManager } from '../src'; - -/** - * Covers default parameters in startListening(options = {}) and - * default destructuring for address = '' and timeout = '0' in - * parseBroadcastedMessage(). - */ -describe('UDPClusterManager parse/start branches', () => { - it('parseBroadcastedMessage: should apply defaults for empty address and timeout', () => { - const parse = (UDPClusterManager as any).parseBroadcastedMessage as Function; - const buf = Buffer.from(['name', 'id', 'UP'].join('\t')); - const msg = parse(buf); - expect(msg).to.include({ name: 'name', id: 'id', type: 'up' }); - // address default => '' leads to host '' and port NaN - expect(msg.host).to.equal(''); - expect(Number.isNaN(msg.port)).to.equal(true); - // timeout default '0' => 0 ms - expect(msg.timeout).to.equal(0); - }); - - it('startListening: should call listenBroadcastedMessages when called without options', () => { - const mgr: any = new (UDPClusterManager as any)(); - let called = false; - const original = mgr.listenBroadcastedMessages; - mgr.listenBroadcastedMessages = (..._args: any[]) => { called = true; }; - try { - (mgr as any).startListening(); - expect(called).to.equal(true); - } finally { - mgr.listenBroadcastedMessages = original; - } - }); -}); diff --git a/test/UDPClusterManager.selectNetworkInterface.spec.ts b/test/UDPClusterManager.selectNetworkInterface.spec.ts deleted file mode 100644 index fe0bdf5..0000000 --- a/test/UDPClusterManager.selectNetworkInterface.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -/*! - * UDPClusterManager.selectNetworkInterface() branch coverage tests - */ -import './mocks'; -import { expect } from 'chai'; -import * as mock from 'mock-require'; - -describe('UDPClusterManager.selectNetworkInterface()', () => { - it('should return default when broadcastAddress is undefined', async () => { - const { UDPClusterManager } = await import('../src'); - const select = (UDPClusterManager as any).selectNetworkInterface as Function; - const res = select({}); - expect(res).to.equal('0.0.0.0'); - }); - - it('should return default when broadcastAddress equals limitedBroadcastAddress', async () => { - const { UDPClusterManager } = await import('../src'); - const select = (UDPClusterManager as any).selectNetworkInterface as Function; - const res = select({ broadcastAddress: '127.0.0.255', limitedBroadcastAddress: '127.0.0.255' }); - expect(res).to.equal('0.0.0.0'); - }); - - it('should continue on undefined interface entry and still select matching address', () => { - // Re-mock os.networkInterfaces to include an undefined entry - const os = require('node:os'); - const networkInterfaces = () => ({ bad: undefined, lo: [{ address: '127.0.0.1', family: 'IPv4' }] }); - mock.stop('os'); - mock('os', Object.assign({}, os, { networkInterfaces })); - // Re-require the module to capture new binding - const { UDPClusterManager } = mock.reRequire('../src/UDPClusterManager'); - const res = (UDPClusterManager as any).selectNetworkInterface({ address: '127.0.0.255', limitedAddress: '255.255.255.255' }); - expect(res).to.equal('127.0.0.1'); - - // restore to base mocks for other tests - mock.stop('os'); - mock.reRequire('./mocks/os'); - mock.reRequire('../src/UDPClusterManager'); - }); - - it('should select matching interface address when not equal to limited broadcast', async () => { - const { UDPClusterManager } = await import('../src'); - const select = (UDPClusterManager as any).selectNetworkInterface as Function; - const res = select({ broadcastAddress: '127.0.0.255', limitedBroadcastAddress: '255.255.255.255' }); - expect(res).to.equal('127.0.0.1'); - }); -}); diff --git a/test/UDPClusterManager.ts b/test/UDPClusterManager.ts index 5569933..98268a1 100644 --- a/test/UDPClusterManager.ts +++ b/test/UDPClusterManager.ts @@ -25,17 +25,35 @@ import './mocks'; import { expect } from 'chai'; import { UDPClusterManager } from '../src'; import * as sinon from 'sinon'; -import { Socket } from 'dgram'; -const testMessageUp = 'name\tid\tup\taddress\ttimeout'; -const testMessageDown = 'name\tid\tdown\taddress\ttimeout'; +const testMessageUp = { + name: 'IMQUnitTest', + id: '1234567890', + type: 'up', + address: '127.0.0.1:6379', + timeout: 50, +}; + +const testMessageDown = { + name: 'IMQUnitTest', + id: '1234567890', + type: 'down', + address: '127.0.0.1:6379', + timeout: 50, +}; -const getSocket = (classObject: typeof UDPClusterManager) => { - return Object.values((classObject as any).sockets)[0] as Socket; +const getSocket = (classObject: any) => { + return classObject.worker; }; -const emitMessage = (message: string) => { - getSocket(UDPClusterManager).emit('message', Buffer.from(message)); +const emitMessage = ( + instanceClass: any, + type: 'cluster:add' | 'cluster:remove', +) => { + getSocket(instanceClass).emit('message', { + type, + server: type === 'cluster:add' ? testMessageUp : testMessageDown, + }); }; describe('UDPBroadcastClusterManager', function() { @@ -44,14 +62,6 @@ describe('UDPBroadcastClusterManager', function() { expect(typeof UDPClusterManager).to.equal('function'); }); - it('should initialize socket if socket does not exists', async () => { - const manager = new UDPClusterManager(); - expect( - Object.values((UDPClusterManager as any).sockets), - ).not.to.be.length(0); - await manager.destroy(); - }); - it('should call add on cluster', async () => { const cluster: any = { add: () => {}, @@ -64,7 +74,7 @@ describe('UDPBroadcastClusterManager', function() { manager.init(cluster); - emitMessage(testMessageUp); + emitMessage(manager, 'cluster:add'); expect(cluster.add.called).to.be.true; await manager.destroy(); }); @@ -83,7 +93,7 @@ describe('UDPBroadcastClusterManager', function() { manager.init(cluster); - emitMessage(testMessageUp); + emitMessage(manager, 'cluster:add'); expect(cluster.add.called).to.be.false; await manager.destroy(); }); @@ -102,7 +112,7 @@ describe('UDPBroadcastClusterManager', function() { manager.init(cluster); - emitMessage(testMessageDown); + emitMessage(manager, 'cluster:remove'); expect(cluster.remove.called).to.be.true; await manager.destroy(); }); @@ -133,7 +143,7 @@ describe('UDPBroadcastClusterManager', function() { manager.init(cluster); // Send up message to add server with short timeout - emitMessage('name\tid\tup\t127.0.0.1:6379\t0.05'); + emitMessage(manager, 'cluster:add'); // Wait for timeout to trigger removal setTimeout(async () => { @@ -166,7 +176,7 @@ describe('UDPBroadcastClusterManager', function() { manager.init(cluster); // This should trigger the timeout handler that returns early (line 307) - emitMessage('name\tid\tup\t127.0.0.1:6379\t0.05'); + emitMessage(manager, 'cluster:add'); await manager.destroy(); }); From 31168b0b0072432a084ebcb14fb6ee090ad40aa2 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Wed, 3 Sep 2025 22:31:46 +0200 Subject: [PATCH 46/78] 2.0.11 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 657c9b3..d818c03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.10", + "version": "2.0.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.10", + "version": "2.0.11", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index 41d2eef..a9fe58d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.10", + "version": "2.0.11", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 4d393b4824fab334c572eaa2a29a1aed9f4f5510 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 11 Sep 2025 12:17:16 +0200 Subject: [PATCH 47/78] feat: implmented queueLength method on Redis message queue --- src/ClusteredRedisQueue.ts | 23 +++++++++++++++++------ src/IMessageQueue.ts | 8 ++++++++ src/RedisQueue.ts | 18 +++++++++++++++++- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/ClusteredRedisQueue.ts b/src/ClusteredRedisQueue.ts index d04aab7..378ed02 100644 --- a/src/ClusteredRedisQueue.ts +++ b/src/ClusteredRedisQueue.ts @@ -108,7 +108,7 @@ export class ClusteredRedisQueue implements IMessageQueue, * * @type {number} */ - private queueLength: number = 0; + private imqLength: number = 0; /** * Template EventEmitter instance used to replicate queue EventEmitters when @@ -223,7 +223,7 @@ export class ClusteredRedisQueue implements IMessageQueue, delay?: number, errorHandler?: (err: Error) => void, ): Promise { - if (!this.queueLength) { + if (!this.imqLength) { return await new Promise(resolve => this.clusterEmitter.once( 'initialized', async ({ imq }) => { @@ -237,7 +237,7 @@ export class ClusteredRedisQueue implements IMessageQueue, )); } - if (this.currentQueue >= this.queueLength) { + if (this.currentQueue >= this.imqLength) { this.currentQueue = 0; } @@ -285,6 +285,18 @@ export class ClusteredRedisQueue implements IMessageQueue, 'Clearing clustered redis message queue...'); } + public async queueLength(): Promise { + const promises = []; + + for (const imq of this.imqs) { + promises.push(imq.queueLength()); + } + + const lengths = await Promise.all(promises); + + return lengths.reduce((total, length) => total + length, 0); + } + /** * Batch imq action processing on all registered imqs at once * @@ -511,7 +523,7 @@ export class ClusteredRedisQueue implements IMessageQueue, imqToRemove.destroy().catch(); } - this.queueLength = this.imqs.length; + this.imqLength = this.imqs.length; this.servers = this.servers.filter( existing => !ClusteredRedisQueue.matchServers( existing, @@ -557,7 +569,7 @@ export class ClusteredRedisQueue implements IMessageQueue, this.imqs.push(imq); this.servers.push(newServer); this.clusterEmitter.emit('add', { server: newServer, imq }); - this.queueLength = this.imqs.length; + this.imqLength = this.imqs.length; return newServer; } @@ -605,5 +617,4 @@ export class ClusteredRedisQueue implements IMessageQueue, return sameId || sameAddress; } - } diff --git a/src/IMessageQueue.ts b/src/IMessageQueue.ts index 2205964..14bd017 100644 --- a/src/IMessageQueue.ts +++ b/src/IMessageQueue.ts @@ -421,4 +421,12 @@ export interface IMessageQueue extends EventEmitter { * @returns {Promise} */ clear(): Promise; + + /** + * Retrieves the current count of messages in the queue. + * Supposed to be an async function. + * + * @returns {Promise} + */ + queueLength(): Promise; } diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index f4376f2..182af9d 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -512,7 +512,7 @@ export class RedisQueue extends EventEmitter } /** - * Clears queue data in redis; + * Clears queue data in redis * * @returns {Promise} */ @@ -541,6 +541,20 @@ export class RedisQueue extends EventEmitter return this; } + /** + * Retrieves the current count of messages in the queue + * + * @returns {Promise} + */ + @profile() + public async queueLength(): Promise { + if (!this.writer) { + return 0; + } + + return this.writer.llen(this.key); + } + /** * Returns true if publisher mode is enabled on this queue, false otherwise. * @@ -698,6 +712,8 @@ export class RedisQueue extends EventEmitter retryStrategy: this.retryStrategy(context), autoResubscribe: true, enableReadyCheck: true, + enableOfflineQueue: true, + autoResendUnfulfilledCommands: true, }); context[channel] = makeRedisSafe(redis); From a7bd4ad800232fdb3d997857024115c96b551be6 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 11 Sep 2025 12:17:27 +0200 Subject: [PATCH 48/78] 2.0.12 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d818c03..4939fd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.11", + "version": "2.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.11", + "version": "2.0.12", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index a9fe58d..0bd28ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.11", + "version": "2.0.12", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 9b5270dc2c7cce55a8f6c464f7e860169875ad77 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 11 Sep 2025 12:18:39 +0200 Subject: [PATCH 49/78] fix: test --- test/ClusteredRedisQueue.addServer.defaultInit.spec.ts | 2 +- test/ClusteredRedisQueue.addServer.noInit.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ClusteredRedisQueue.addServer.defaultInit.spec.ts b/test/ClusteredRedisQueue.addServer.defaultInit.spec.ts index c70dd47..e6e229c 100644 --- a/test/ClusteredRedisQueue.addServer.defaultInit.spec.ts +++ b/test/ClusteredRedisQueue.addServer.defaultInit.spec.ts @@ -27,7 +27,7 @@ describe('ClusteredRedisQueue.addServerWithQueueInitializing() default param', ( // Ensure the server added and queue length updated expect((cq as any).servers.some((s: any) => s.host === server.host && s.port === server.port)).to.equal(true); - expect((cq as any).queueLength).to.equal((cq as any).imqs.length); + expect((cq as any).imqLength).to.equal((cq as any).imqs.length); await cq.destroy(); }); diff --git a/test/ClusteredRedisQueue.addServer.noInit.spec.ts b/test/ClusteredRedisQueue.addServer.noInit.spec.ts index 8666387..df4d6c7 100644 --- a/test/ClusteredRedisQueue.addServer.noInit.spec.ts +++ b/test/ClusteredRedisQueue.addServer.noInit.spec.ts @@ -23,7 +23,7 @@ describe('ClusteredRedisQueue.addServerWithQueueInitializing(false)', () => { expect(cq.servers.length).to.be.greaterThan(0); expect(cq.imqs.length).to.be.greaterThan(0); // queueLength updated - expect(cq.queueLength).to.equal(cq.imqs.length); + expect(cq.imqLength).to.equal(cq.imqs.length); // initialized not emitted expect(initializedCalled).to.equal(false); From ecd7b765a909ddd0262a45f5211fea0aac68b85a Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 11 Sep 2025 12:18:42 +0200 Subject: [PATCH 50/78] 2.0.13 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4939fd0..747e89a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.12", + "version": "2.0.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.12", + "version": "2.0.13", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index 0bd28ec..926fc1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.12", + "version": "2.0.13", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 6eb6e71a42ce62111c0c4816cdafaacfa6ecce45 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Tue, 23 Sep 2025 14:38:55 +0200 Subject: [PATCH 51/78] fix: js crash --- src/RedisQueue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 182af9d..e91d224 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -1105,7 +1105,7 @@ export class RedisQueue extends EventEmitter (this.options.cleanupFilter || '*').replace(/\*/g, '.*'), 'i', ); - const clients = await this.writer.client('LIST') as string; + const clients = await this.writer.client('LIST') as string || ''; const connectedKeys = (clients.match(RX_CLIENT_NAME) || []) .filter((name: string) => RX_CLIENT_TEST.test(name) && filter.test(name), From 35c55c5cfc5a7ccdb07b37ec4243997e9cb8ecaa Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Tue, 23 Sep 2025 14:39:00 +0200 Subject: [PATCH 52/78] 2.0.14 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 747e89a..dbdb96f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.13", + "version": "2.0.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.13", + "version": "2.0.14", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index 926fc1a..75d9caf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.13", + "version": "2.0.14", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From a0d15e3a09b31ccdc76be2627094bc6358ed2f07 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Wed, 22 Oct 2025 18:01:05 +0200 Subject: [PATCH 53/78] fix: safer message handling --- src/RedisQueue.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index e91d224..f42d6e7 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -1165,7 +1165,7 @@ export class RedisQueue extends EventEmitter /** * Unreliable but fast way of message handling by the queue */ - private async readUnsafe() { + private async readUnsafe(): Promise { try { const key = this.key; @@ -1182,6 +1182,7 @@ export class RedisQueue extends EventEmitter } } catch (err) { // istanbul ignore next + // eslint-disable-next-line @typescript-eslint/no-unsafe-call if (err.message.match(/Stream connection ended/)) { break; } @@ -1201,7 +1202,7 @@ export class RedisQueue extends EventEmitter /** * Reliable but slow method of message handling by message queue */ - private async readSafe() { + private async readSafe(): Promise { try { const key = this.key; @@ -1216,7 +1217,8 @@ export class RedisQueue extends EventEmitter try { await this.reader.brpoplpush(this.key, workerKey, 0); - } catch (err) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { // istanbul ignore next break; } @@ -1225,19 +1227,26 @@ export class RedisQueue extends EventEmitter workerKey, -1, 1, ); - if (msgArr.length !== 1) { + if (!msgArr || msgArr?.length !== 1) { // noinspection ExceptionCaughtLocallyJS throw new Error('Wrong messages count'); } - const msg = msgArr[0]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const [msg] = msgArr as any[]; this.process([key, msg]); - this.writer.del(workerKey); + this.writer.del(workerKey).catch(e => + this.logger.warn('OnReadSafe: del error', e)); } } catch (err) { // istanbul ignore next - this.emitError('OnReadSafe', 'safe reader failed', err); + this.emitError( + 'OnReadSafe', + 'safe reader failed', + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + err, + ); } } @@ -1261,6 +1270,7 @@ export class RedisQueue extends EventEmitter ? 'readSafe' : 'readUnsafe'; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument process.nextTick(this[readMethod].bind(this)); return this; From 0058270b9ccd214d28eb20f84a1e3b60a2bc21e2 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Wed, 22 Oct 2025 18:02:00 +0200 Subject: [PATCH 54/78] 2.0.15 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index dbdb96f..e1f2c17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.14", + "version": "2.0.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.14", + "version": "2.0.15", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index 75d9caf..d7d238d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.14", + "version": "2.0.15", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 15ecc6700227f37953b2978e72b10e8a4c2180e9 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Mon, 27 Oct 2025 16:27:51 +0100 Subject: [PATCH 55/78] feat: add verbode logging on redis queue --- src/IMessageQueue.ts | 7 + src/RedisQueue.ts | 362 ++++++++++++++++++++++++++++++++----------- 2 files changed, 280 insertions(+), 89 deletions(-) diff --git a/src/IMessageQueue.ts b/src/IMessageQueue.ts index 14bd017..57febb5 100644 --- a/src/IMessageQueue.ts +++ b/src/IMessageQueue.ts @@ -275,6 +275,13 @@ export interface IMQOptions extends Partial { * @type {ClusterManager[]} */ clusterManagers?: ClusterManager[]; + + /** + * Enables/disables verbose logging, default is false + * + * @type {boolean} + */ + verbose?: boolean; } export interface EventMap { diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index f42d6e7..7d514ef 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -109,6 +109,10 @@ export function unpack(data: string): any { type RedisConnectionChannel = 'reader' | 'writer' | 'watcher' | 'subscription'; +const IMQ_REDIS_MAX_LISTENERS_LIMIT = +( + process.env.IMQ_REDIS_MAX_LISTENERS_LIMIT || 10000 +); + /** * Class RedisQueue * Implements simple messaging queue over redis. @@ -262,11 +266,27 @@ export class RedisQueue extends EventEmitter DEFAULT_IMQ_OPTIONS, options, ); + /* tslint:disable */ this.pack = this.options.useGzip ? pack : JSON.stringify; this.unpack = this.options.useGzip ? unpack : JSON.parse; /* tslint:enable */ this.redisKey = `${this.options.host}:${this.options.port}`; + + this.verbose(`Initializing queue on ${ + this.options.host }:${ + this.options.port} with prefix ${ + this.options.prefix } and safeDelivery = ${ + this.options.safeDelivery }, and safeDeliveryTtl = ${ + this.options.safeDeliveryTtl }, and watcherCheckDelay = ${ + this.options.watcherCheckDelay }, and useGzip = ${ + this.options.useGzip }`); + } + + private verbose(message: string): void { + if (this.options.verbose) { + this.logger.info(`[IMQ-CORE][${ this.name }]: ${ message }`); + } } /** @@ -284,7 +304,7 @@ export class RedisQueue extends EventEmitter // istanbul ignore next if (!channel) { throw new TypeError( - `${channel}: No subscription channel name provided!`, + `${ channel }: No subscription channel name provided!`, ); } @@ -292,7 +312,7 @@ export class RedisQueue extends EventEmitter if (this.subscriptionName && this.subscriptionName !== channel) { throw new TypeError( `Invalid channel name provided: expected "${ - this.subscriptionName}", but "${channel}" given instead!`, + this.subscriptionName}", but "${ channel }" given instead!`, ); } else if (!this.subscriptionName) { this.subscriptionName = channel; @@ -306,9 +326,16 @@ export class RedisQueue extends EventEmitter // istanbul ignore next chan.on('message', (ch: string, message: string) => { if (ch === fcn && typeof handler === 'function') { - handler(JSON.parse(message)); + handler(JSON.parse(message) as unknown as JsonObject); } + + this.verbose(`Received message from ${ + ch } channel, data: ${ + JSON.stringify(message) }`, + ); }); + + this.verbose(`Subscribed to ${ channel } channel`); } /** @@ -318,15 +345,20 @@ export class RedisQueue extends EventEmitter */ public async unsubscribe(): Promise { if (this.subscription) { + this.verbose('Initialize unsubscribing...'); + if (this.subscriptionName) { - this.subscription.unsubscribe( + await this.subscription.unsubscribe( `${this.options.prefix}:${this.subscriptionName}`, ); + + this.verbose(`Unsubscribed from ${ + this.subscriptionName } channel`); } this.subscription.removeAllListeners(); this.subscription.disconnect(false); - this.subscription.quit(); + await this.subscription.quit(); } this.subscriptionName = undefined; @@ -349,10 +381,18 @@ export class RedisQueue extends EventEmitter throw new TypeError('Writer is not connected!'); } + const jsonData = JSON.stringify(data); + const name = toName || this.name; + await this.writer.publish( - `${this.options.prefix}:${toName || this.name}`, - JSON.stringify(data), + `${this.options.prefix}:${ name }`, + jsonData, ); + + this.verbose(`Published message to ${ + name } channel, data: ${ + jsonData } + `); } /** @@ -362,7 +402,7 @@ export class RedisQueue extends EventEmitter */ public async start(): Promise { if (!this.name) { - throw new TypeError(`${this.name}: No queue name provided!`); + throw new TypeError(`${ this.name }: No queue name provided!`); } if (this.initialized) { @@ -375,24 +415,35 @@ export class RedisQueue extends EventEmitter // istanbul ignore next if (!this.reader && this.isWorker()) { + this.verbose('Initializing reader...'); connPromises.push(this.connect('reader', this.options)); } if (!this.writer) { + this.verbose('Initializing writer...'); connPromises.push(this.connect('writer', this.options)); } await Promise.all(connPromises); + this.verbose('Connections initialized'); + if (!this.signalsInitialized) { + this.verbose('Setting up OS signal handlers...'); // istanbul ignore next const free = async () => { let exitCode = 0; - setTimeout(() => process.exit(exitCode), IMQ_SHUTDOWN_TIMEOUT); + setTimeout(() => { + this.verbose(`Shutting down after ${ + IMQ_SHUTDOWN_TIMEOUT } timeout`, + ); + process.exit(exitCode); + }, IMQ_SHUTDOWN_TIMEOUT); try { if (this.watchOwner) { + this.verbose('Freeing watcher lock...'); await this.unlock(); } } catch (err) { @@ -406,6 +457,7 @@ export class RedisQueue extends EventEmitter process.on('SIGABRT', free); this.signalsInitialized = true; + this.verbose('OS signal handlers initialized!'); } await this.initWatcher(); @@ -446,29 +498,47 @@ export class RedisQueue extends EventEmitter const data: IMessage = { id, message, from: this.name }; const key = `${this.options.prefix}:${toQueue}`; const packet = this.pack(data); - const cb = (error: any) => { + const cb = (error: any, op: string) => { // istanbul ignore next - if (error && errorHandler) { - errorHandler(error); + if (error) { + this.verbose(`Writer ${ op } error: ${ error }`); + + if (errorHandler) { + errorHandler(error as unknown as Error); + } } }; if (delay) { - this.writer.zadd(`${key}:delayed`, Date.now() + delay, packet, - (err) => { + await this.writer.zadd(`${key}:delayed`, Date.now() + delay, packet, + (err: any) => { // istanbul ignore next if (err) { - cb(err); + cb(err, 'ZADD'); return; } this.writer.set(`${key}:${id}:ttl`, '', 'PX', delay, 'NX', - cb, - ); + (err: any) => { + // istanbul ignore next + if (err) { + cb(err, 'SET'); + + return; + } + }, + ).catch((err: any) => cb(err, 'SET')); }); } else { - this.writer.lpush(key, packet, cb); + await this.writer.lpush(key, packet, (err: any) => { + // istanbul ignore next + if (err) { + cb(err, 'LPUSH'); + + return; + } + }); } return id; @@ -481,9 +551,12 @@ export class RedisQueue extends EventEmitter */ @profile() public async stop(): Promise { + this.verbose('Stopping queue...'); + if (this.reader) { + this.verbose('Destroying reader...'); this.reader.removeAllListeners(); - this.reader.quit(); + await this.reader.quit(); this.reader.disconnect(false); delete this.reader; @@ -491,6 +564,8 @@ export class RedisQueue extends EventEmitter this.initialized = false; + this.verbose('Queue stopped!'); + return this; } @@ -501,6 +576,7 @@ export class RedisQueue extends EventEmitter */ @profile() public async destroy(): Promise { + this.verbose('Destroying queue...'); this.destroyed = true; this.removeAllListeners(); this.cleanSafeCheckInterval(); @@ -509,6 +585,7 @@ export class RedisQueue extends EventEmitter await this.clear(); this.destroyWriter(); await this.unsubscribe(); + this.verbose('Queue destroyed!'); } /** @@ -523,16 +600,20 @@ export class RedisQueue extends EventEmitter } try { + this.verbose('Clearing expired queue keys...'); + await Promise.all([ this.writer.del(this.key), this.writer.del(`${ this.key }:delayed`), ]); + + this.verbose('Expired queue keys cleared!'); } catch (err) { // istanbul ignore next if (this.initialized) { this.logger.error( - `${context.name}: error clearing the redis queue host ${ - this.redisKey} on writer, pid ${process.pid}:`, + `${ context.name }: error clearing the redis queue host ${ + this.redisKey } on writer, pid ${ process.pid }:`, err, ); } @@ -631,7 +712,7 @@ export class RedisQueue extends EventEmitter * @returns {string} */ private get lockKey(): string { - return `${this.options.prefix}:watch:lock`; + return `${ this.options.prefix }:watch:lock`; } /** @@ -650,12 +731,16 @@ export class RedisQueue extends EventEmitter * @access private */ @profile() - private destroyWatcher() { + private destroyWatcher(): void { if (this.watcher) { + this.verbose('Destroying watcher...'); this.watcher.removeAllListeners(); - this.watcher.quit(); + this.watcher.quit().catch(e => { + this.verbose(`Error quitting watcher: ${ e }`); + }); this.watcher.disconnect(false); delete RedisQueue.watchers[this.redisKey]; + this.verbose('Watcher destroyed!'); } } @@ -665,13 +750,17 @@ export class RedisQueue extends EventEmitter * @access private */ @profile() - private destroyWriter() { + private destroyWriter(): void { if (this.writer) { + this.verbose('Destroying writer...'); this.writer.removeAllListeners(); - this.writer.quit(); + this.writer.quit().catch(e => { + this.verbose(`Error quitting writer: ${ e }`); + }); this.writer.disconnect(false); delete RedisQueue.writers[this.redisKey]; + this.verbose('Writer destroyed!'); } } @@ -687,8 +776,10 @@ export class RedisQueue extends EventEmitter private async connect( channel: RedisConnectionChannel, options: IMQOptions, - context: any = this, + context: RedisQueue = this, ): Promise { + this.verbose(`Connecting to ${ channel } channel...`); + // istanbul ignore next if (context[channel]) { return context[channel]; @@ -705,7 +796,7 @@ export class RedisQueue extends EventEmitter // istanbul ignore next password: options.password, connectionName: this.getChannelName( - context.name, + context.name + '', options.prefix || '', channel, ), @@ -719,9 +810,25 @@ export class RedisQueue extends EventEmitter context[channel] = makeRedisSafe(redis); context[channel].__imq = true; - redis.setMaxListeners(10000); + for (const event of [ + 'wait', + 'reconnecting', + 'connecting', + 'connect', + 'close', + ]) { + redis.on(event, () => { + context.verbose(`Redis Event fired: ${ event }`); + }); + } + + redis.setMaxListeners(IMQ_REDIS_MAX_LISTENERS_LIMIT); redis.on('ready', - this.onReadyHandler(context, channel, resolve), + this.onReadyHandler( + context, + channel, + resolve, + ) as unknown as () => void, ); redis.on('error', this.onErrorHandler(context, channel, reject), @@ -748,6 +855,8 @@ export class RedisQueue extends EventEmitter return null; } + this.verbose('Redis connection error, retrying...'); + return 200; }; } @@ -766,6 +875,8 @@ export class RedisQueue extends EventEmitter channel: RedisConnectionChannel, resolve: (...args: any[]) => void, ): () => Promise { + this.verbose(`Redis ${ channel } channel ready!`); + return (async () => { this.logger.info( '%s: %s channel connected, host %s, pid %s', @@ -796,9 +907,9 @@ export class RedisQueue extends EventEmitter prefix: string, name: RedisConnectionChannel, ): string { - const uniqueSuffix = `pid:${process.pid}:host:${ os.hostname()}`; + const uniqueSuffix = `pid:${ process.pid }:host:${ os.hostname() }`; - return`${prefix}:${contextName}:${name}:${uniqueSuffix}`; + return`${ prefix }:${ contextName }:${ name }:${ uniqueSuffix }`; } /** @@ -817,6 +928,8 @@ export class RedisQueue extends EventEmitter ): (err: Error) => void { // istanbul ignore next return ((err: Error & { code: string }) => { + this.verbose(`Redis Error: ${ err }`); + if (this.destroyed) { return; } @@ -847,6 +960,8 @@ export class RedisQueue extends EventEmitter context: RedisQueue, channel: RedisConnectionChannel, ): (...args: any[]) => any { + this.verbose(`Redis ${ channel } is closing...`); + // istanbul ignore next return (() => { this.initialized = false; @@ -865,6 +980,7 @@ export class RedisQueue extends EventEmitter * @returns {RedisQueue} */ private process(msg: [any, any]): RedisQueue { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const [queue, data] = msg; // istanbul ignore next @@ -873,14 +989,16 @@ export class RedisQueue extends EventEmitter } try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument const { id, message, from } = this.unpack(data); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.emit('message', message, id, from); } catch (err) { // istanbul ignore next this.emitError( 'OnMessage', 'process error - message is invalid', - err, + err as unknown as Error, ); } @@ -923,14 +1041,14 @@ export class RedisQueue extends EventEmitter if (this.scripts.moveDelayed.checksum) { await this.writer.evalsha( this.scripts.moveDelayed.checksum, - 2, `${key}:delayed`, key, Date.now(), + 2, `${ key }:delayed`, key, Date.now(), ); } } catch (err) { this.emitError( 'OnProcessDelayed', 'error processing delayed queue', - err, + err as unknown as Error, ); } } @@ -951,7 +1069,7 @@ export class RedisQueue extends EventEmitter const data = await this.writer.scan( cursor, 'MATCH', - `${this.options.prefix}:*:worker:*`, + `${ this.options.prefix }:*:worker:*`, 'COUNT', '1000', ); @@ -969,7 +1087,7 @@ export class RedisQueue extends EventEmitter this.emitError( 'OnSafeDelivery', 'safe queue message delivery problem', - err, + err as unknown as Error, ); this.cleanSafeCheckInterval(); @@ -992,6 +1110,10 @@ export class RedisQueue extends EventEmitter return; } + this.verbose(`Watching ${ keys.length } keys: ${ + keys.map(key => `"${ key }"`).join(', ') + }`); + for (const key of keys) { const kp: string[] = key.split(':'); @@ -999,7 +1121,7 @@ export class RedisQueue extends EventEmitter continue; } - await this.writer.rpoplpush(key, `${kp.shift()}:${kp.shift()}`); + await this.writer.rpoplpush(key, `${ kp.shift() }:${ kp.shift() }`); } } @@ -1013,7 +1135,7 @@ export class RedisQueue extends EventEmitter */ private async onWatchMessage(...args: any[]): Promise { try { - const key = (args.pop() || '').split(':'); + const key = ((args.pop() || '') + '').split(':'); if (key.pop() !== 'ttl') { return; @@ -1023,7 +1145,11 @@ export class RedisQueue extends EventEmitter await this.processDelayed(key.join(':')); } catch (err) { - this.emitError('OnWatch', 'watch error', err); + this.emitError( + 'OnWatch', + 'watch error', + err as unknown as Error, + ); } } @@ -1035,7 +1161,7 @@ export class RedisQueue extends EventEmitter */ private cleanSafeCheckInterval(): void { if (this.safeCheckInterval) { - clearInterval(this.safeCheckInterval); + clearInterval(this.safeCheckInterval as number); delete this.safeCheckInterval; } } @@ -1053,33 +1179,53 @@ export class RedisQueue extends EventEmitter } try { - this.writer.config('SET', 'notify-keyspace-events', 'Ex'); + this.writer.config( + 'SET', + 'notify-keyspace-events', + 'Ex', + ).catch(err => { + this.emitError( + 'OnConfig', + 'events config error', + err as unknown as Error, + ); + }); } catch (err) { - this.emitError('OnConfig', 'events config error', err); + this.emitError( + 'OnConfig', + 'events config error', + err as unknown as Error, + ); } - this.watcher.on('pmessage', this.onWatchMessage.bind(this)); - this.watcher.psubscribe('__keyevent@0__:expired', - `${this.options.prefix}:delayed:*`, + this.watcher.on( + 'pmessage', + this.onWatchMessage.bind(this) as unknown as () => void, ); + this.watcher.psubscribe( + '__keyevent@0__:expired', + `${ this.options.prefix }:delayed:*`, + ).catch(err => { + this.verbose(`Error subscribing to watcher channel: ${ err }`); + }); // watch for expired unhandled safe queues if (!this.safeCheckInterval) { - // tslint:disable-next-line:triple-equals no-null-keyword if (this.options.safeDeliveryTtl != null) { - this.safeCheckInterval = setInterval(async () => { - if (!this.writer) { - this.cleanSafeCheckInterval(); + this.safeCheckInterval = setInterval( + (async (): Promise => { + if (!this.writer) { + this.cleanSafeCheckInterval(); - return; - } + return ; + } - if (this.options.safeDelivery) { - await this.processWatch(); - } + if (this.options.safeDelivery) { + await this.processWatch(); + } - await this.processCleanup(); - }, this.options.safeDeliveryTtl); + await this.processCleanup(); + }) as unknown as () => void, this.options.safeDeliveryTtl); } } @@ -1095,6 +1241,8 @@ export class RedisQueue extends EventEmitter * @returns {Promise} */ private async processCleanup(): Promise { + this.verbose('Cleaning up orphaned keys...'); + try { if (!this.options.cleanup) { return; @@ -1105,6 +1253,9 @@ export class RedisQueue extends EventEmitter (this.options.cleanupFilter || '*').replace(/\*/g, '.*'), 'i', ); + + this.verbose(`Cleaning up keys matching ${ filter }`); + const clients = await this.writer.client('LIST') as string || ''; const connectedKeys = (clients.match(RX_CLIENT_NAME) || []) .filter((name: string) => @@ -1120,6 +1271,10 @@ export class RedisQueue extends EventEmitter const keysToRemove: string[] = []; let cursor = '0'; + this.verbose(`Found connected keys: ${ + connectedKeys.map(k => `"${ k }"`).join(', ') + }`); + while (true) { const data = await this.writer.scan( cursor, @@ -1153,6 +1308,10 @@ export class RedisQueue extends EventEmitter if (keysToRemove.length) { await this.writer.del(...keysToRemove); + + this.verbose(`Keys ${ + keysToRemove.map(k => `"${ k }"`).join(', ') + } were successfully removed!`); } } catch (err) { this.logger.warn('Clean-up error occurred:', err); @@ -1194,7 +1353,11 @@ export class RedisQueue extends EventEmitter } } catch (err) { // istanbul ignore next - this.emitError('OnReadUnsafe', 'unsafe reader failed', err); + this.emitError( + 'OnReadUnsafe', + 'unsafe reader failed', + err as unknown as Error, + ); } } @@ -1244,8 +1407,7 @@ export class RedisQueue extends EventEmitter this.emitError( 'OnReadSafe', 'safe reader failed', - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - err, + err as unknown as Error, ); } } @@ -1327,13 +1489,16 @@ export class RedisQueue extends EventEmitter * @param {string} message * @param {Error} err */ - private emitError(eventName: string, message: string, err: Error) { + private emitError(eventName: string, message: string, err: Error): void { this.emit('error', err, eventName); this.logger.error( - `${this.name}: ${message}, pid ${ - process.pid} on redis host ${this.redisKey}:`, + `${this.name}: ${ message }, pid ${ + process.pid } on redis host ${ this.redisKey }:`, err, ); + this.verbose(`Error in event ${ + eventName }: ${ message }, pid ${ + process.pid } on redis host ${ this.redisKey }: ${ err }`); } /** @@ -1346,6 +1511,8 @@ export class RedisQueue extends EventEmitter const owned = await this.lock(); if (owned) { + this.verbose('Watcher connection lock acquired!'); + for (const script of Object.keys(this.scripts)) { try { const checksum = sha1(this.scripts[script].code); @@ -1365,7 +1532,11 @@ export class RedisQueue extends EventEmitter ); } } catch (err) { - this.emitError('OnScriptLoad', 'script load error', err); + this.emitError( + 'OnScriptLoad', + 'script load error', + err as unknown as Error, + ); } } @@ -1377,7 +1548,7 @@ export class RedisQueue extends EventEmitter // istanbul ignore next /** - * This method returns watcher lock resolver function + * This method returns a watcher lock resolver function * * @access private * @param {(...args: any[]) => void} resolve @@ -1412,32 +1583,45 @@ export class RedisQueue extends EventEmitter */ // istanbul ignore next private async initWatcher(): Promise { - return new Promise(async (resolve, reject) => { - try { - if (!await this.watcherCount()) { - await this.ownWatch(); - - if (this.watchOwner && this.watcher) { - resolve(); + return new Promise( + (async ( + resolve: (...args: any[]) => void, + reject: (...args: any[]) => void, + ): Promise => { + try { + if (!await this.watcherCount()) { + this.verbose('Initializing watcher...'); + + await this.ownWatch(); + + if (this.watchOwner && this.watcher) { + resolve(); + } else { + // check for possible deadlock to resolve + setTimeout( + this.watchLockResolver( + resolve, + reject, + ) as unknown as () => void, + intrand(1, 50), + ); + } } else { - // check for possible deadlock to resolve - setTimeout( - this.watchLockResolver(resolve, reject), - intrand(1, 50), - ); + resolve(); } - } else { - resolve(); - } - } catch (err) { - this.logger.error( - `${this.name}: error initializing watcher, pid ${ - process.pid} on redis host ${this.redisKey}`, - err, - ); + } catch (err) { + this.logger.error( + `${ this.name }: error initializing watcher, pid ${ + process.pid } on redis host ${ this.redisKey }`, + err, + ); - reject(err); - } - }); + reject(err); + } + }) as unknown as ( + resolve: (...args: any[]) => void, + reject: (...args: any[]) => void, + ) => void, + ); } } From 65e8912baaafac9aec5adb652ff4c1791e84ffb6 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Mon, 27 Oct 2025 16:44:11 +0100 Subject: [PATCH 56/78] fix: tests execution --- src/RedisQueue.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 7d514ef..6438453 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -1256,7 +1256,9 @@ export class RedisQueue extends EventEmitter this.verbose(`Cleaning up keys matching ${ filter }`); - const clients = await this.writer.client('LIST') as string || ''; + const clients: string = (await this.writer.client( + 'LIST', + ) as string).toString() || ''; const connectedKeys = (clients.match(RX_CLIENT_NAME) || []) .filter((name: string) => RX_CLIENT_TEST.test(name) && filter.test(name), From 23a878297a77bf24526c222e8cc407ba5c8e7d8a Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Mon, 27 Oct 2025 16:44:26 +0100 Subject: [PATCH 57/78] 2.0.16 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e1f2c17..8efc0bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.15", + "version": "2.0.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.15", + "version": "2.0.16", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index d7d238d..bcb8a55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.15", + "version": "2.0.16", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From b83ca211d5d1354a85770024e9579aa99a10b8f1 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Tue, 28 Oct 2025 17:18:46 +0100 Subject: [PATCH 58/78] feat: deps up & added extended verbose --- package-lock.json | 645 +++++++++++++++++++-------------------- package.json | 2 +- src/ClusterManager.ts | 5 +- src/IMessageQueue.ts | 13 +- src/RedisQueue.ts | 67 ++-- src/UDPClusterManager.ts | 2 +- 6 files changed, 387 insertions(+), 347 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8efc0bb..39ac717 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,20 +41,6 @@ "yargs": "^18.0.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -71,9 +57,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, "license": "MIT", "engines": { @@ -81,22 +67,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -129,14 +115,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -197,15 +183,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -225,9 +211,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -245,27 +231,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -290,18 +276,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.5", "debug": "^4.3.1" }, "engines": { @@ -309,14 +295,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", - "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -364,9 +350,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -383,9 +369,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -393,13 +379,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -432,19 +418,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", + "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -513,9 +502,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", "dev": true, "license": "MIT", "engines": { @@ -526,9 +515,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -536,13 +525,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { @@ -550,16 +539,16 @@ } }, "node_modules/@gerrit0/mini-shiki": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.9.2.tgz", - "integrity": "sha512-Tvsj+AOO4Z8xLRJK900WkyfxHsZQu+Zm1//oT1w443PO6RiYMoq/4NGOhaNuZoUMYsjKIAPVQ6eOFMddj6yphQ==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.14.0.tgz", + "integrity": "sha512-c5X8fwPLOtUS8TVdqhynz9iV0GlOtFUT1ppXYzUUlEXe4kbZ/mvMT8wXoT8kCwUka+zsiloq7sD3pZ3+QVTuNQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-oniguruma": "^3.9.2", - "@shikijs/langs": "^3.9.2", - "@shikijs/themes": "^3.9.2", - "@shikijs/types": "^3.9.2", + "@shikijs/engine-oniguruma": "^3.14.0", + "@shikijs/langs": "^3.14.0", + "@shikijs/themes": "^3.14.0", + "@shikijs/types": "^3.14.0", "@shikijs/vscode-textmate": "^10.0.2" } }, @@ -574,33 +563,19 @@ } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -630,9 +605,9 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz", - "integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", "license": "MIT" }, "node_modules/@isaacs/cliui": { @@ -771,9 +746,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -781,6 +756,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -792,16 +778,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -859,40 +845,40 @@ } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.9.2.tgz", - "integrity": "sha512-Vn/w5oyQ6TUgTVDIC/BrpXwIlfK6V6kGWDVVz2eRkF2v13YoENUvaNwxMsQU/t6oCuZKzqp9vqtEtEzKl9VegA==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.14.0.tgz", + "integrity": "sha512-TNcYTYMbJyy+ZjzWtt0bG5y4YyMIWC2nyePz+CFMWqm+HnZZyy9SWMgo8Z6KBJVIZnx8XUXS8U2afO6Y0g1Oug==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.9.2", + "@shikijs/types": "3.14.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@shikijs/langs": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.9.2.tgz", - "integrity": "sha512-X1Q6wRRQXY7HqAuX3I8WjMscjeGjqXCg/Sve7J2GWFORXkSrXud23UECqTBIdCSNKJioFtmUGJQNKtlMMZMn0w==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.14.0.tgz", + "integrity": "sha512-DIB2EQY7yPX1/ZH7lMcwrK5pl+ZkP/xoSpUzg9YC8R+evRCCiSQ7yyrvEyBsMnfZq4eBzLzBlugMyTAf13+pzg==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.9.2" + "@shikijs/types": "3.14.0" } }, "node_modules/@shikijs/themes": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.9.2.tgz", - "integrity": "sha512-6z5lBPBMRfLyyEsgf6uJDHPa6NAGVzFJqH4EAZ+03+7sedYir2yJBRu2uPZOKmj43GyhVHWHvyduLDAwJQfDjA==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.14.0.tgz", + "integrity": "sha512-fAo/OnfWckNmv4uBoUu6dSlkcBc+SA1xzj5oUSaz5z3KqHtEbUypg/9xxgJARtM6+7RVm0Q6Xnty41xA1ma1IA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.9.2" + "@shikijs/types": "3.14.0" } }, "node_modules/@shikijs/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.9.2.tgz", - "integrity": "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.14.0.tgz", + "integrity": "sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ==", "dev": true, "license": "MIT", "dependencies": { @@ -928,14 +914,13 @@ } }, "node_modules/@sinonjs/samsam": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", - "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", - "lodash.get": "^4.4.2", "type-detect": "^4.1.0" } }, @@ -978,13 +963,14 @@ "license": "MIT" }, "node_modules/@types/chai": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", - "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@types/deep-eql": "*" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, "node_modules/@types/deep-eql": { @@ -1054,13 +1040,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.2.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", - "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/sinon": { @@ -1074,9 +1060,9 @@ } }, "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", - "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz", + "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", "dev": true, "license": "MIT" }, @@ -1088,9 +1074,9 @@ "license": "MIT" }, "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "version": "17.0.34", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", + "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", "dev": true, "license": "MIT", "dependencies": { @@ -1105,17 +1091,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", - "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", + "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/type-utils": "8.39.0", - "@typescript-eslint/utils": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1129,22 +1115,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/parser": "^8.46.2", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", - "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", + "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4" }, "engines": { @@ -1160,14 +1146,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", - "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", + "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.39.0", - "@typescript-eslint/types": "^8.39.0", + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", "debug": "^4.3.4" }, "engines": { @@ -1182,14 +1168,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", - "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", + "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0" + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1200,9 +1186,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", - "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", + "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", "dev": true, "license": "MIT", "engines": { @@ -1217,15 +1203,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", - "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", + "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1242,9 +1228,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", - "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", + "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", "dev": true, "license": "MIT", "engines": { @@ -1256,16 +1242,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", - "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", + "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.39.0", - "@typescript-eslint/tsconfig-utils": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1285,16 +1271,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", - "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", + "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0" + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1309,13 +1295,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", - "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", + "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/types": "8.46.2", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1407,9 +1393,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -1496,6 +1482,16 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz", + "integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -1527,9 +1523,9 @@ "license": "ISC" }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", "dev": true, "funding": [ { @@ -1547,10 +1543,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" @@ -1619,9 +1616,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001726", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", - "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", "dev": true, "funding": [ { @@ -1640,9 +1637,9 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", - "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", "dependencies": { @@ -1725,9 +1722,9 @@ } }, "node_modules/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -1738,9 +1735,9 @@ } }, "node_modules/cliui/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, @@ -1763,9 +1760,9 @@ } }, "node_modules/cliui/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { @@ -1859,16 +1856,6 @@ "node": ">=18" } }, - "node_modules/coveralls-next/node_modules/lcov-parse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", - "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "lcov-parse": "bin/cli.js" - } - }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -1892,9 +1879,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2021,9 +2008,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.178", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.178.tgz", - "integrity": "sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA==", + "version": "1.5.241", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.241.tgz", + "integrity": "sha512-ILMvKX/ZV5WIJzzdtuHg8xquk2y0BOGlFOxBVwTpbiXqWIH0hamG45ddU4R3PQ0gYu+xgo0vdHXHli9sHIGb4w==", "dev": true, "license": "ISC" }, @@ -2078,25 +2065,24 @@ } }, "node_modules/eslint": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", - "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", + "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.33.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.38.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -2547,9 +2533,9 @@ "license": "ISC" }, "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", "dev": true, "license": "MIT", "engines": { @@ -2741,12 +2727,12 @@ "license": "ISC" }, "node_modules/ioredis": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", - "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", + "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", "license": "MIT", "dependencies": { - "@ioredis/commands": "^1.3.0", + "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -2842,6 +2828,16 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -3023,9 +3019,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3139,6 +3135,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "lcov-parse": "bin/cli.js" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3192,14 +3198,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -3241,9 +3239,9 @@ } }, "node_modules/loupe": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", - "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true, "license": "MIT" }, @@ -3383,9 +3381,9 @@ } }, "node_modules/mocha": { - "version": "11.7.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.1.tgz", - "integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==", + "version": "11.7.4", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.4.tgz", + "integrity": "sha512-1jYAaY8x0kAZ0XszLWu14pzsf4KV740Gld4HXkhNTXwcHx4AUEDkPzgEHg9CM5dVcW+zv036tjpsEbLraPJj4w==", "dev": true, "license": "MIT", "dependencies": { @@ -3397,6 +3395,7 @@ "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^9.0.5", @@ -3592,9 +3591,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", + "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", "dev": true, "license": "MIT" }, @@ -4444,9 +4443,9 @@ } }, "node_modules/run-applescript": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, "license": "MIT", "engines": { @@ -4502,9 +4501,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -4664,9 +4663,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", "dev": true, "license": "CC0-1.0" }, @@ -4748,9 +4747,9 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", "dependencies": { @@ -5008,17 +5007,17 @@ } }, "node_modules/typedoc": { - "version": "0.28.9", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.9.tgz", - "integrity": "sha512-aw45vwtwOl3QkUAmWCnLV9QW1xY+FSX2zzlit4MAfE99wX+Jij4ycnpbAWgBXsRrxmfs9LaYktg/eX5Bpthd3g==", + "version": "0.28.14", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.14.tgz", + "integrity": "sha512-ftJYPvpVfQvFzpkoSfHLkJybdA/geDJ8BGQt/ZnkkhnBYoYW6lBgPQXu6vqLxO4X75dA55hX8Af847H5KXlEFA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@gerrit0/mini-shiki": "^3.9.0", + "@gerrit0/mini-shiki": "^3.12.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", - "yaml": "^2.8.0" + "yaml": "^2.8.1" }, "bin": { "typedoc": "bin/typedoc" @@ -5032,9 +5031,9 @@ } }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5053,16 +5052,16 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "dev": true, "funding": [ { @@ -5151,9 +5150,9 @@ } }, "node_modules/workerpool": { - "version": "9.3.3", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.3.tgz", - "integrity": "sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", "dev": true, "license": "Apache-2.0" }, @@ -5240,9 +5239,9 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -5313,9 +5312,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", "bin": { @@ -5396,9 +5395,9 @@ } }, "node_modules/yargs/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index bcb8a55..54e6904 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "author": "imqueue.com (https://imqueue.com)", "license": "GPL-3.0-only", "dependencies": { - "ioredis": "^5.7.0" + "ioredis": "^5.8.2" }, "devDependencies": { "@eslint/js": "^9.33.0", diff --git a/src/ClusterManager.ts b/src/ClusterManager.ts index 6579de0..86bbab5 100644 --- a/src/ClusterManager.ts +++ b/src/ClusterManager.ts @@ -42,7 +42,10 @@ export abstract class ClusterManager { protected constructor() {} public init(cluster: ICluster): InitializedCluster { - const initializedCluster = Object.assign(cluster, { id: uuid() }); + const initializedCluster = Object.assign( + cluster, + { id: uuid() }, + ) as InitializedCluster; this.clusters.push(initializedCluster); diff --git a/src/IMessageQueue.ts b/src/IMessageQueue.ts index 57febb5..51d714b 100644 --- a/src/IMessageQueue.ts +++ b/src/IMessageQueue.ts @@ -277,11 +277,22 @@ export interface IMQOptions extends Partial { clusterManagers?: ClusterManager[]; /** - * Enables/disables verbose logging, default is false + * Enables/disables verbose logging * + * @default false * @type {boolean} */ verbose?: boolean; + + /** + * Enables/disables extended verbose logging. The output may contain + * sensitive information, so use it with caution. Does not work if a verbose + * option is disabled. + * + * @default false + * @type {boolean} + */ + verboseExtended?: boolean; } export interface EventMap { diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 6438453..c1edcb8 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -56,6 +56,8 @@ export const DEFAULT_IMQ_OPTIONS: IMQOptions = { safeDeliveryTtl: 5000, useGzip: false, watcherCheckDelay: 5000, + verbose: false, + verboseExtended: false, }; export const IMQ_SHUTDOWN_TIMEOUT = +(process.env.IMQ_SHUTDOWN_TIMEOUT || 1000); @@ -93,7 +95,7 @@ export function intrand(min: number, max: number): number { */ // istanbul ignore next export function pack(data: any): string { - return gzip(JSON.stringify(data)).toString('binary'); + return (gzip(JSON.stringify(data)) as Buffer).toString('binary'); } /** @@ -104,7 +106,9 @@ export function pack(data: any): string { */ // istanbul ignore next export function unpack(data: string): any { - return JSON.parse(gunzip(Buffer.from(data, 'binary')).toString()); + return JSON.parse( + (gunzip(Buffer.from(data, 'binary')) as Buffer).toString(), + ); } type RedisConnectionChannel = 'reader' | 'writer' | 'watcher' | 'subscription'; @@ -275,7 +279,7 @@ export class RedisQueue extends EventEmitter this.verbose(`Initializing queue on ${ this.options.host }:${ - this.options.port} with prefix ${ + this.options.port } with prefix ${ this.options.prefix } and safeDelivery = ${ this.options.safeDelivery }, and safeDeliveryTtl = ${ this.options.safeDeliveryTtl }, and watcherCheckDelay = ${ @@ -283,9 +287,23 @@ export class RedisQueue extends EventEmitter this.options.useGzip }`); } - private verbose(message: string): void { + private verbose( + message: string, + sensitiveMessage?: string, + sensitiveOnly?: boolean, + ): void { + if (sensitiveOnly && !this.options.verboseExtended) { + return; + } + if (this.options.verbose) { - this.logger.info(`[IMQ-CORE][${ this.name }]: ${ message }`); + const text = `[IMQ-CORE][${ this.name }]: ${ message }`; + const fullText = this.options.verboseExtended + ? `${ text }${ sensitiveMessage }` + : text + ; + + this.logger.info(fullText); } } @@ -329,9 +347,9 @@ export class RedisQueue extends EventEmitter handler(JSON.parse(message) as unknown as JsonObject); } - this.verbose(`Received message from ${ - ch } channel, data: ${ - JSON.stringify(message) }`, + this.verbose( + `Received message from ${ ch } channel`, + `, data: ${ JSON.stringify(message) }`, ); }); @@ -389,10 +407,10 @@ export class RedisQueue extends EventEmitter jsonData, ); - this.verbose(`Published message to ${ - name } channel, data: ${ - jsonData } - `); + this.verbose( + `Published message to ${ name } channel`, + `, data: ${ jsonData }`, + ); } /** @@ -498,6 +516,9 @@ export class RedisQueue extends EventEmitter const data: IMessage = { id, message, from: this.name }; const key = `${this.options.prefix}:${toQueue}`; const packet = this.pack(data); + + this.verbose('Message send', `: ${ packet }`, true); + const cb = (error: any, op: string) => { // istanbul ignore next if (error) { @@ -510,11 +531,14 @@ export class RedisQueue extends EventEmitter }; if (delay) { - await this.writer.zadd(`${key}:delayed`, Date.now() + delay, packet, - (err: any) => { + await this.writer.zadd( + `${key}:delayed`, + Date.now() + delay, + packet, + (e: Error | null) => { // istanbul ignore next - if (err) { - cb(err, 'ZADD'); + if (e) { + cb(e, 'ZADD'); return; } @@ -529,7 +553,8 @@ export class RedisQueue extends EventEmitter } }, ).catch((err: any) => cb(err, 'SET')); - }); + }, + ); } else { await this.writer.lpush(key, packet, (err: any) => { // istanbul ignore next @@ -782,10 +807,10 @@ export class RedisQueue extends EventEmitter // istanbul ignore next if (context[channel]) { - return context[channel]; + return context[channel] as IRedisClient; } - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const redis = new Redis({ // istanbul ignore next port: options.port || 6379, @@ -808,7 +833,7 @@ export class RedisQueue extends EventEmitter }); context[channel] = makeRedisSafe(redis); - context[channel].__imq = true; + (context[channel] as IRedisClient).__imq = true; for (const event of [ 'wait', @@ -991,8 +1016,10 @@ export class RedisQueue extends EventEmitter try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument const { id, message, from } = this.unpack(data); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.emit('message', message, id, from); + this.verbose('Message received', `: ${ data }`, true); } catch (err) { // istanbul ignore next this.emitError( diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index 4e497d6..e047b08 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -186,7 +186,7 @@ export class UDPClusterManager extends ClusterManager { return; } - return new Promise(resolve => { + return new Promise(resolve => { const timeout = setTimeout(() => { worker.terminate(); resolve(); From ceee1ae09952227c8373149f5e04448f0d52a834 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Tue, 28 Oct 2025 17:18:53 +0100 Subject: [PATCH 59/78] 2.0.17 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39ac717..edb5ed6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.16", + "version": "2.0.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.16", + "version": "2.0.17", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index 54e6904..27643b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.16", + "version": "2.0.17", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 4a20e8c303daa50605d1e1e25a8e8249d4920ba1 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Wed, 29 Oct 2025 17:44:52 +0100 Subject: [PATCH 60/78] fix: revert awaits --- src/RedisQueue.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 6438453..a1fc825 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -348,7 +348,7 @@ export class RedisQueue extends EventEmitter this.verbose('Initialize unsubscribing...'); if (this.subscriptionName) { - await this.subscription.unsubscribe( + this.subscription.unsubscribe( `${this.options.prefix}:${this.subscriptionName}`, ); @@ -358,7 +358,7 @@ export class RedisQueue extends EventEmitter this.subscription.removeAllListeners(); this.subscription.disconnect(false); - await this.subscription.quit(); + this.subscription.quit(); } this.subscriptionName = undefined; @@ -510,7 +510,7 @@ export class RedisQueue extends EventEmitter }; if (delay) { - await this.writer.zadd(`${key}:delayed`, Date.now() + delay, packet, + this.writer.zadd(`${key}:delayed`, Date.now() + delay, packet, (err: any) => { // istanbul ignore next if (err) { @@ -531,7 +531,7 @@ export class RedisQueue extends EventEmitter ).catch((err: any) => cb(err, 'SET')); }); } else { - await this.writer.lpush(key, packet, (err: any) => { + this.writer.lpush(key, packet, (err: any) => { // istanbul ignore next if (err) { cb(err, 'LPUSH'); @@ -556,7 +556,7 @@ export class RedisQueue extends EventEmitter if (this.reader) { this.verbose('Destroying reader...'); this.reader.removeAllListeners(); - await this.reader.quit(); + this.reader.quit(); this.reader.disconnect(false); delete this.reader; @@ -1135,7 +1135,7 @@ export class RedisQueue extends EventEmitter */ private async onWatchMessage(...args: any[]): Promise { try { - const key = ((args.pop() || '') + '').split(':'); + const key = (args.pop() || '').split(':'); if (key.pop() !== 'ttl') { return; From f0b42db5880965240c0c35c8d67a952701cd79f0 Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Wed, 29 Oct 2025 17:45:01 +0100 Subject: [PATCH 61/78] 2.0.17 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8efc0bb..d109406 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.16", + "version": "2.0.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.16", + "version": "2.0.17", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index bcb8a55..371e93e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.16", + "version": "2.0.17", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 37bd84d029991e13e1a72e67e6c27256d145536c Mon Sep 17 00:00:00 2001 From: Mykhailo Stadnyk Date: Wed, 29 Oct 2025 17:46:34 +0100 Subject: [PATCH 62/78] 2.0.18 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index edb5ed6..87d059a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.17", + "version": "2.0.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.17", + "version": "2.0.18", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index 27643b2..42a2688 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.17", + "version": "2.0.18", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 3af1a9d36b8666e775b09a44848b20cb4abc0f26 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Wed, 10 Dec 2025 15:55:50 +0100 Subject: [PATCH 63/78] feat: added extra logging to redis client --- src/RedisQueue.ts | 6 +++++- src/redis.ts | 13 ++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index a1fc825..370a1e7 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -40,6 +40,7 @@ import { uuid, } from '.'; import Redis from './redis'; +import { logger } from '../test/mocks'; const RX_CLIENT_NAME = /name=(\S+)/g; const RX_CLIENT_TEST = /:(reader|writer|watcher)/; @@ -807,7 +808,10 @@ export class RedisQueue extends EventEmitter autoResendUnfulfilledCommands: true, }); - context[channel] = makeRedisSafe(redis); + context[channel] = makeRedisSafe( + redis, + options.verbose ? options.logger : undefined, + ); context[channel].__imq = true; for (const event of [ diff --git a/src/redis.ts b/src/redis.ts index e82d292..87a697a 100644 --- a/src/redis.ts +++ b/src/redis.ts @@ -23,6 +23,7 @@ */ /* tslint:disable */ import Redis from 'ioredis'; +import { ILogger } from './IMessageQueue'; /** * Extends default Redis type to allow dynamic properties access on it @@ -35,7 +36,10 @@ export interface IRedisClient extends Redis { } // istanbul ignore next -export function makeRedisSafe(redis: IRedisClient): IRedisClient { +export function makeRedisSafe( + redis: IRedisClient, + logger?: ILogger, +): IRedisClient { return new Proxy(redis, { get(target, property, receiver) { const original = Reflect.get(target, property, receiver); @@ -44,11 +48,18 @@ export function makeRedisSafe(redis: IRedisClient): IRedisClient { return async (...args: unknown[]) => { try { if (target.status !== 'ready') { + logger?.warn( + 'Redis client is not ready yet, while ', + `executing command: "${ String(property) }"`, + ); + return null; } return await original.apply(target, args); } catch (err: unknown) { + logger?.error(err); + return null; } }; From bee8b872cdbdfa54eb7b24f3d0a8a0546dbe0dc7 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Wed, 10 Dec 2025 15:56:06 +0100 Subject: [PATCH 64/78] 2.0.19 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87d059a..1820e86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.18", + "version": "2.0.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.18", + "version": "2.0.19", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index 42a2688..6e93d5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.18", + "version": "2.0.19", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 79c94fcf948df330af94dedbf21fb4afe8f0973f Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Fri, 12 Dec 2025 23:20:37 +0100 Subject: [PATCH 65/78] fix: remove safe rwrapper over redis client instance --- src/RedisQueue.ts | 8 ++------ src/redis.ts | 36 ------------------------------------ 2 files changed, 2 insertions(+), 42 deletions(-) diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 370a1e7..e994120 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -34,13 +34,11 @@ import { ILogger, IMQMode, EventMap, - makeRedisSafe, buildOptions, profile, uuid, } from '.'; import Redis from './redis'; -import { logger } from '../test/mocks'; const RX_CLIENT_NAME = /name=(\S+)/g; const RX_CLIENT_TEST = /:(reader|writer|watcher)/; @@ -806,12 +804,10 @@ export class RedisQueue extends EventEmitter enableReadyCheck: true, enableOfflineQueue: true, autoResendUnfulfilledCommands: true, + offlineQueue: true, }); - context[channel] = makeRedisSafe( - redis, - options.verbose ? options.logger : undefined, - ); + context[channel] = redis; context[channel].__imq = true; for (const event of [ diff --git a/src/redis.ts b/src/redis.ts index 87a697a..f7c47cf 100644 --- a/src/redis.ts +++ b/src/redis.ts @@ -23,7 +23,6 @@ */ /* tslint:disable */ import Redis from 'ioredis'; -import { ILogger } from './IMessageQueue'; /** * Extends default Redis type to allow dynamic properties access on it @@ -35,40 +34,5 @@ export interface IRedisClient extends Redis { __imq?: boolean; } -// istanbul ignore next -export function makeRedisSafe( - redis: IRedisClient, - logger?: ILogger, -): IRedisClient { - return new Proxy(redis, { - get(target, property, receiver) { - const original = Reflect.get(target, property, receiver); - - if (typeof original === 'function') { - return async (...args: unknown[]) => { - try { - if (target.status !== 'ready') { - logger?.warn( - 'Redis client is not ready yet, while ', - `executing command: "${ String(property) }"`, - ); - - return null; - } - - return await original.apply(target, args); - } catch (err: unknown) { - logger?.error(err); - - return null; - } - }; - } - - return original; - }, - }); -} - export { Redis }; export default Redis; From 42433c16f57f375fdf771daf453c9dc280fd5e99 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Fri, 12 Dec 2025 23:20:48 +0100 Subject: [PATCH 66/78] 2.0.20 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1820e86..1dcbcaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.19", + "version": "2.0.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.19", + "version": "2.0.20", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index 6e93d5d..6543404 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.19", + "version": "2.0.20", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From e58c1ea5b2d9f7ce6e3963c8e8e1040c9ac7055f Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Fri, 12 Dec 2025 23:41:35 +0100 Subject: [PATCH 67/78] fix: test --- src/RedisQueue.ts | 3 +-- test/mocks/redis.ts | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index e994120..8b1699c 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -801,7 +801,6 @@ export class RedisQueue extends EventEmitter ), retryStrategy: this.retryStrategy(context), autoResubscribe: true, - enableReadyCheck: true, enableOfflineQueue: true, autoResendUnfulfilledCommands: true, offlineQueue: true, @@ -1174,7 +1173,7 @@ export class RedisQueue extends EventEmitter */ // istanbul ignore next private watch(): RedisQueue { - if (!this.watcher || this.watcher.__ready__) { + if (!this.writer || !this.watcher || this.watcher.__ready__) { return this; } diff --git a/test/mocks/redis.ts b/test/mocks/redis.ts index 1f229d9..ae95773 100644 --- a/test/mocks/redis.ts +++ b/test/mocks/redis.ts @@ -60,14 +60,16 @@ export class RedisClientMock extends EventEmitter { // noinspection JSUnusedGlobalSymbols public end() {} // noinspection JSUnusedGlobalSymbols - public quit() {} + public quit() { + return new Promise(resolve => resolve(undefined)); + } // noinspection JSMethodCanBeStatic - public set(...args: any[]): number { + public set(...args: any[]): Promise { const [key, val] = args; RedisClientMock.__keys[key] = val; this.cbExecute(args.pop(), null, 1); - return 1; + return new Promise(resolve => resolve(1)); } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic @@ -230,14 +232,14 @@ export class RedisClientMock extends EventEmitter { } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic - public psubscribe(...args: any[]): number { + public psubscribe(...args: any[]): Promise { this.cbExecute(args.pop(), null, 1); - return 1; + return new Promise(resolve => resolve(1)); } - public punsubscribe(...args: any[]): number { + public punsubscribe(...args: any[]): Promise { this.cbExecute(args.pop(), null, 1); - return 1; + return new Promise(resolve => resolve(1)); } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic @@ -247,7 +249,7 @@ export class RedisClientMock extends EventEmitter { } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic - public del(...args: any[]): number { + public del(...args: any[]): Promise { const self = RedisClientMock; let count = 0; for (let key of args) { @@ -261,7 +263,7 @@ export class RedisClientMock extends EventEmitter { } } this.cbExecute(args.pop(), count); - return count; + return new Promise(resolve => resolve(count)); } // noinspection JSUnusedGlobalSymbols @@ -303,8 +305,8 @@ export class RedisClientMock extends EventEmitter { } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic - public config(): boolean { - return true; + public config(): Promise { + return new Promise(resolve => resolve(true)); } private cbExecute(cb: any, ...args: any[]): void { From a4328599ae7baf0869d757b62708c37f1dee1638 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Fri, 12 Dec 2025 23:41:46 +0100 Subject: [PATCH 68/78] 2.0.21 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1dcbcaf..506be5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.20", + "version": "2.0.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.20", + "version": "2.0.21", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index 6543404..a178ba0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.20", + "version": "2.0.21", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From cb5da8c9852495d69eeb2fbdae3bd37929da61ca Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Sat, 13 Dec 2025 00:08:19 +0100 Subject: [PATCH 69/78] fix: reconnection strategy --- src/RedisQueue.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 8b1699c..e7e24fe 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -804,6 +804,7 @@ export class RedisQueue extends EventEmitter enableOfflineQueue: true, autoResendUnfulfilledCommands: true, offlineQueue: true, + maxRetriesPerRequest: null, }); context[channel] = redis; @@ -856,7 +857,7 @@ export class RedisQueue extends EventEmitter this.verbose('Redis connection error, retrying...'); - return 200; + return null; }; } From 9d18b2d4d063a0911faea35638ea3c6929882f13 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Sat, 13 Dec 2025 00:08:30 +0100 Subject: [PATCH 70/78] 2.0.22 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 506be5d..f8b5d40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.21", + "version": "2.0.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.21", + "version": "2.0.22", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index a178ba0..9a8fd2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.21", + "version": "2.0.22", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 539874a443d2476a95ea982a6344c38fa710e304 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Sat, 13 Dec 2025 10:19:19 +0100 Subject: [PATCH 71/78] fix: RedisQueue - handle unsubscribe more safely --- src/RedisQueue.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index e7e24fe..1b87867 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -346,18 +346,22 @@ export class RedisQueue extends EventEmitter if (this.subscription) { this.verbose('Initialize unsubscribing...'); - if (this.subscriptionName) { - this.subscription.unsubscribe( - `${this.options.prefix}:${this.subscriptionName}`, - ); + try { + if (this.subscriptionName) { + await this.subscription.unsubscribe( + `${this.options.prefix}:${this.subscriptionName}`, + ); - this.verbose(`Unsubscribed from ${ - this.subscriptionName } channel`); - } + this.verbose(`Unsubscribed from ${ + this.subscriptionName } channel`); + } - this.subscription.removeAllListeners(); - this.subscription.disconnect(false); - this.subscription.quit(); + this.subscription.removeAllListeners(); + this.subscription.quit(); + this.subscription.disconnect(false); + } catch (error) { + this.verbose(`Unsubscribe error: ${ error }`); + } } this.subscriptionName = undefined; From 76db725b7fe80a5b799424f27366891e5a99b2c9 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Sat, 13 Dec 2025 10:19:29 +0100 Subject: [PATCH 72/78] 2.0.23 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f8b5d40..acc95c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.22", + "version": "2.0.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.22", + "version": "2.0.23", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index 9a8fd2e..c83737a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.22", + "version": "2.0.23", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From e7a46f726b74c2645fb0a0c88f8655f0a1d9b790 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Sat, 13 Dec 2025 13:15:14 +0100 Subject: [PATCH 73/78] feat: implemented own reconnection handler instead of ioredis native retry strategy --- src/RedisQueue.ts | 148 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 123 insertions(+), 25 deletions(-) diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 1b87867..7f75870 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -201,6 +201,14 @@ export class RedisQueue extends EventEmitter */ private safeCheckInterval: any; + /** + * Internal per-channel reconnection state + */ + private reconnectTimers: Partial> = {}; + private reconnectAttempts: Partial> + = {}; + private reconnecting: Partial> = {}; + /** * This queue instance unique key (identifier), for internal use */ @@ -558,9 +566,7 @@ export class RedisQueue extends EventEmitter if (this.reader) { this.verbose('Destroying reader...'); - this.reader.removeAllListeners(); - this.reader.quit(); - this.reader.disconnect(false); + this.destroyChannel('reader', this); delete this.reader; } @@ -737,11 +743,7 @@ export class RedisQueue extends EventEmitter private destroyWatcher(): void { if (this.watcher) { this.verbose('Destroying watcher...'); - this.watcher.removeAllListeners(); - this.watcher.quit().catch(e => { - this.verbose(`Error quitting watcher: ${ e }`); - }); - this.watcher.disconnect(false); + this.destroyChannel('watcher', this); delete RedisQueue.watchers[this.redisKey]; this.verbose('Watcher destroyed!'); } @@ -756,17 +758,38 @@ export class RedisQueue extends EventEmitter private destroyWriter(): void { if (this.writer) { this.verbose('Destroying writer...'); - this.writer.removeAllListeners(); - this.writer.quit().catch(e => { - this.verbose(`Error quitting writer: ${ e }`); - }); - this.writer.disconnect(false); - + this.destroyChannel('writer', this); delete RedisQueue.writers[this.redisKey]; this.verbose('Writer destroyed!'); } } + /** + * Destroys any channel + * + * @access private + */ + @profile() + private destroyChannel( + channel: RedisConnectionChannel, + context: RedisQueue = this, + ): void { + const client = context[channel]; + + if (client) { + try { + client.removeAllListeners(); + client.quit().then(() => { + client.disconnect(false); + }).catch(e => { + this.verbose(`Error quitting ${ channel }: ${ e }`); + }); + } catch (error) { + this.verbose(`Error destroying ${ channel }: ${ error }`); + } + } + } + /** * Establishes a given connection channel by its name * @@ -803,7 +826,7 @@ export class RedisQueue extends EventEmitter options.prefix || '', channel, ), - retryStrategy: this.retryStrategy(context), + retryStrategy: this.retryStrategy(), autoResubscribe: true, enableOfflineQueue: true, autoResendUnfulfilledCommands: true, @@ -847,22 +870,90 @@ export class RedisQueue extends EventEmitter /** * Builds and returns redis reconnection strategy * - * @param {RedisQueue} context * @returns {() => (number | void | null)} * @private */ - private retryStrategy( - context: RedisQueue, - ): () => number | void | null { + private retryStrategy(): () => null { return () => { - if (context.destroyed) { - return null; + return null; + }; + } + + /** + * Schedules custom reconnection for a given channel with capped + * exponential backoff + * + * @param {RedisConnectionChannel} channel + * @private + */ + private scheduleReconnect(channel: RedisConnectionChannel): void { + if (this.destroyed) { + return; + } + + if (this.reconnecting[channel]) { + return; + } + + this.reconnecting[channel] = true; + + const attempts = (this.reconnectAttempts[channel] || 0) + 1; + this.reconnectAttempts[channel] = attempts; + + const base = Math.min(30000, 1000 * Math.pow(2, attempts - 1)); + const jitter = Math.floor(base * 0.2 * Math.random()); + const delay = base + jitter; + + this.verbose(`Scheduling ${ channel } reconnect in ${ + delay } ms (attempt ${ attempts })`); + + if (this.reconnectTimers[channel]) { + clearTimeout(this.reconnectTimers[channel] as any); + } + + this.reconnectTimers[channel] = setTimeout(async () => { + if (this.destroyed) { + this.reconnecting[channel] = false; + + return; } - this.verbose('Redis connection error, retrying...'); + try { + switch (channel) { + case 'watcher': + this.destroyWatcher(); + break; + case 'writer': + this.destroyWriter(); + break; + case 'reader': + this.destroyChannel(channel, this); + this.reader = undefined; + + break; + case 'subscription': + this.destroyChannel(channel, this); + this.subscription = undefined; - return null; - }; + break; + } + + await this.connect(channel, this.options); + this.reconnectAttempts[channel] = 0; + this.reconnecting[channel] = false; + + if (this.reconnectTimers[channel]) { + clearTimeout(this.reconnectTimers[channel] as any); + this.reconnectTimers[channel] = undefined; + } + + this.verbose(`Reconnected ${ channel } channel`); + } catch (err) { + this.reconnecting[channel] = false; + this.verbose(`Reconnect ${ channel } failed: ${ err }`); + this.scheduleReconnect(channel); + } + }, delay); } /** @@ -946,8 +1037,10 @@ export class RedisQueue extends EventEmitter ); if (!this.initialized) { - this.initialized = false; reject(err); + } else { + // Try to recover the channel using our reconnection routine + this.scheduleReconnect(channel); } }); } @@ -969,10 +1062,15 @@ export class RedisQueue extends EventEmitter // istanbul ignore next return (() => { this.initialized = false; + this.logger.warn( '%s: redis connection %s closed on host %s, pid %s!', context.name, channel, this.redisKey, process.pid, ); + + if (!this.destroyed) { + this.scheduleReconnect(channel); + } }); } From 13c95443b78c589d624c26581baee246749c7830 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Sat, 13 Dec 2025 13:15:23 +0100 Subject: [PATCH 74/78] 2.0.24 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index acc95c9..5f82ada 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.23", + "version": "2.0.24", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.23", + "version": "2.0.24", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index c83737a..2c6803a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.23", + "version": "2.0.24", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 56421b679db75dd5e949f4b4f312101e69d7fc74 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Sat, 13 Dec 2025 14:14:58 +0100 Subject: [PATCH 75/78] fix: rework redis connection make it more stable & straightforward --- src/RedisQueue.ts | 155 ++++++++++++++++++-------------------------- test/mocks/redis.ts | 4 ++ 2 files changed, 67 insertions(+), 92 deletions(-) diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 7f75870..9b6cd44 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -811,59 +811,63 @@ export class RedisQueue extends EventEmitter return context[channel]; } - return new Promise((resolve, reject) => { - const redis = new Redis({ - // istanbul ignore next - port: options.port || 6379, - // istanbul ignore next - host: options.host || 'localhost', - // istanbul ignore next - username: options.username, - // istanbul ignore next - password: options.password, - connectionName: this.getChannelName( - context.name + '', - options.prefix || '', - channel, - ), - retryStrategy: this.retryStrategy(), - autoResubscribe: true, - enableOfflineQueue: true, - autoResendUnfulfilledCommands: true, - offlineQueue: true, - maxRetriesPerRequest: null, + const redis = new Redis({ + // istanbul ignore next + port: options.port || 6379, + // istanbul ignore next + host: options.host || 'localhost', + // istanbul ignore next + username: options.username, + // istanbul ignore next + password: options.password, + connectionName: this.getChannelName( + context.name + '', + options.prefix || '', + channel, + ), + retryStrategy: this.retryStrategy(), + autoResubscribe: true, + enableOfflineQueue: true, + autoResendUnfulfilledCommands: true, + offlineQueue: true, + maxRetriesPerRequest: null, + enableReadyCheck: channel !== 'subscription', + lazyConnect: true, + }); + + context[channel] = redis; + context[channel].__imq = true; + + for (const event of [ + 'wait', + 'reconnecting', + 'connecting', + 'connect', + 'close', + ]) { + redis.on(event, () => { + context.verbose(`Redis Event fired: ${ event }`); }); + } - context[channel] = redis; - context[channel].__imq = true; - - for (const event of [ - 'wait', - 'reconnecting', - 'connecting', - 'connect', - 'close', - ]) { - redis.on(event, () => { - context.verbose(`Redis Event fired: ${ event }`); - }); - } + redis.setMaxListeners(IMQ_REDIS_MAX_LISTENERS_LIMIT); + redis.on('error', this.onErrorHandler(context, channel)); + redis.on('end', this.onCloseHandler(context, channel)); - redis.setMaxListeners(IMQ_REDIS_MAX_LISTENERS_LIMIT); - redis.on('ready', - this.onReadyHandler( - context, - channel, - resolve, - ) as unknown as () => void, - ); - redis.on('error', - this.onErrorHandler(context, channel, reject), - ); - redis.on('end', - this.onCloseHandler(context, channel), - ); - }); + await redis.connect(); + + this.logger.info( + '%s: %s channel connected, host %s, pid %s', + context.name, channel, this.redisKey, process.pid, + ); + + switch (channel) { + case 'reader': this.read(); break; + case 'writer': await this.processDelayed(this.key); break; + case 'watcher': await this.initWatcher(); break; + } + + return context[channel]; } // istanbul ignore next @@ -956,38 +960,6 @@ export class RedisQueue extends EventEmitter }, delay); } - /** - * Builds and returns connection ready state handler - * - * @access private - * @param {RedisQueue} context - * @param {RedisConnectionChannel} channel - * @param {(...args: any[]) => void} resolve - * @return {() => Promise} - */ - private onReadyHandler( - context: RedisQueue, - channel: RedisConnectionChannel, - resolve: (...args: any[]) => void, - ): () => Promise { - this.verbose(`Redis ${ channel } channel ready!`); - - return (async () => { - this.logger.info( - '%s: %s channel connected, host %s, pid %s', - context.name, channel, this.redisKey, process.pid, - ); - - switch (channel) { - case 'reader': this.read(); break; - case 'writer': await this.processDelayed(this.key); break; - case 'watcher': await this.initWatcher(); break; - } - - resolve(context[channel]); - }); - } - // noinspection JSMethodCanBeStatic /** * Generates channel name @@ -1013,17 +985,15 @@ export class RedisQueue extends EventEmitter * @access private * @param {RedisQueue} context * @param {RedisConnectionChannel} channel - * @param {(...args: any[]) => void} reject * @return {(err: Error) => void} */ private onErrorHandler( context: RedisQueue, channel: RedisConnectionChannel, - reject: (...args: any[]) => void, - ): (err: Error) => void { + ): (error: Error) => void { // istanbul ignore next - return ((err: Error & { code: string }) => { - this.verbose(`Redis Error: ${ err }`); + return ((error: Error & { code: string }) => { + this.verbose(`Redis Error: ${ error }`); if (this.destroyed) { return; @@ -1033,13 +1003,14 @@ export class RedisQueue extends EventEmitter `${context.name}: error connecting redis host ${ this.redisKey} on ${ channel}, pid ${process.pid}:`, - err, + error, ); - if (!this.initialized) { - reject(err); - } else { - // Try to recover the channel using our reconnection routine + if ( + error.code === 'ECONNREFUSED' || + error.code === 'ETIMEDOUT' || + context[channel]?.status !== 'ready' + ) { this.scheduleReconnect(channel); } }); diff --git a/test/mocks/redis.ts b/test/mocks/redis.ts index ae95773..d652392 100644 --- a/test/mocks/redis.ts +++ b/test/mocks/redis.ts @@ -64,6 +64,10 @@ export class RedisClientMock extends EventEmitter { return new Promise(resolve => resolve(undefined)); } + public connect() { + return new Promise(resolve => resolve(undefined)); + } + // noinspection JSMethodCanBeStatic public set(...args: any[]): Promise { const [key, val] = args; From 04c67753bd3cde63d8f312cc9091543fd1c6f508 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Sat, 13 Dec 2025 14:15:04 +0100 Subject: [PATCH 76/78] 2.0.25 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5f82ada..0ccbcd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.24", + "version": "2.0.25", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.24", + "version": "2.0.25", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index 2c6803a..172aab1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.24", + "version": "2.0.25", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", From 3ecdd7acabac1603c4086c3c744b52a6dd624990 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Tue, 6 Jan 2026 14:57:49 +0100 Subject: [PATCH 77/78] feat: added possibility to turn off server alive timeout check in udp cluster manager & added extra logging --- src/ClusteredRedisQueue.ts | 19 ++++++++++++++++++- src/RedisQueue.ts | 9 +++------ src/UDPClusterManager.ts | 16 ++++++++++++++++ src/UDPWorker.ts | 17 ++++++++++------- 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/ClusteredRedisQueue.ts b/src/ClusteredRedisQueue.ts index 378ed02..739535e 100644 --- a/src/ClusteredRedisQueue.ts +++ b/src/ClusteredRedisQueue.ts @@ -169,6 +169,8 @@ export class ClusteredRedisQueue implements IMessageQueue, } if (this.options.clusterManagers?.length) { + this.verbose('Initializing cluster managers...'); + for (const manager of this.options.clusterManagers) { this.initializedClusters.push(manager.init({ add: this.addServer.bind(this), @@ -297,6 +299,14 @@ export class ClusteredRedisQueue implements IMessageQueue, return lengths.reduce((total, length) => total + length, 0); } + private verbose(message: string): void { + if (this.options.verbose) { + this.logger.info(`[IMQ-CORE][ClusteredRedisQueue][${ + this.name + }]: ${ message }`); + } + } + /** * Batch imq action processing on all registered imqs at once * @@ -306,7 +316,7 @@ export class ClusteredRedisQueue implements IMessageQueue, * @return {Promise} */ private async batch(action: string, message: string): Promise { - this.logger.log(message); + this.logger.info(message); const promises = []; @@ -498,6 +508,8 @@ export class ClusteredRedisQueue implements IMessageQueue, * @returns {void} */ protected addServer(server: IServerInput): ClusterServer { + this.verbose(`Adding new server: ${ JSON.stringify(server) }`); + return this.addServerWithQueueInitializing(server, true); } @@ -508,6 +520,8 @@ export class ClusteredRedisQueue implements IMessageQueue, * @returns {void} */ protected removeServer(server: IServerInput): void { + this.verbose(`Removing the server: ${ JSON.stringify(server) }`); + const remove = this.findServer(server); if (!remove) { @@ -580,6 +594,9 @@ export class ClusteredRedisQueue implements IMessageQueue, private async initializeQueue(imq: RedisQueue): Promise { copyEventEmitter(this.templateEmitter, imq); + this.verbose(`Initializing queue with state: ${ + JSON.stringify(this.state) + }`); if (this.state.started) { await imq.start(); diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 9b6cd44..7b54c27 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -899,14 +899,11 @@ export class RedisQueue extends EventEmitter return; } - this.reconnecting[channel] = true; - const attempts = (this.reconnectAttempts[channel] || 0) + 1; - this.reconnectAttempts[channel] = attempts; + const delay = Math.min(30000, 1000 * Math.pow(2, attempts - 1)); - const base = Math.min(30000, 1000 * Math.pow(2, attempts - 1)); - const jitter = Math.floor(base * 0.2 * Math.random()); - const delay = base + jitter; + this.reconnecting[channel] = true; + this.reconnectAttempts[channel] = attempts; this.verbose(`Scheduling ${ channel } reconnect in ${ delay } ms (attempt ${ attempts })`); diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index e047b08..73bb159 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -59,12 +59,28 @@ export interface UDPClusterManagerOptions { * @type {number} */ aliveTimeoutCorrection: number; + + /** + * Message queue alive-server check flag. If set to false, the server will + * not be checked for liveness on each broadcast message with a timeout. + * Can be specified by the environment variable if the given option is not + * bypassed: IMQ_UDP_CLUSTER_MANAGER_ALIVE_CHECK + * + * @default true + * @type {boolean} + */ + useAliveCheck: boolean; } +const IMQ_UDP_CLUSTER_MANAGER_ALIVE_CHECK = !!+( + process.env.IMQ_UDP_CLUSTER_MANAGER_ALIVE_CHECK || 1 +); + export const DEFAULT_UDP_CLUSTER_MANAGER_OPTIONS: UDPClusterManagerOptions = { port: 63000, address: '255.255.255.255', aliveTimeoutCorrection: 5000, + useAliveCheck: IMQ_UDP_CLUSTER_MANAGER_ALIVE_CHECK, }; export class UDPClusterManager extends ClusterManager { diff --git a/src/UDPWorker.ts b/src/UDPWorker.ts index 948890a..da83b06 100644 --- a/src/UDPWorker.ts +++ b/src/UDPWorker.ts @@ -49,7 +49,7 @@ interface Message { timeout: number; } -class UDPClusterWorker { +class UDPWorker { private readonly socket: Socket; private readonly servers = new Map(); @@ -91,16 +91,19 @@ class UDPClusterWorker { private addServer(message: Message): void { this.messagePort.postMessage({ type: 'cluster:add', - server: UDPClusterWorker.mapMessage(message), + server: UDPWorker.mapMessage(message), }); - this.serverAliveWait(message); + + if (this.options.useAliveCheck) { + this.serverAliveWait(message); + } } private removeServer(message: Message): void { - this.servers.delete(UDPClusterWorker.getServerKey(message)); + this.servers.delete(UDPWorker.getServerKey(message)); this.messagePort.postMessage({ type: 'cluster:remove', - server: UDPClusterWorker.mapMessage(message), + server: UDPWorker.mapMessage(message), }); } @@ -119,7 +122,7 @@ class UDPClusterWorker { const stamp = uuid(); const correction = this.options.aliveTimeoutCorrection ?? 0; const effectiveTimeout = message.timeout + correction + 1; - const key = UDPClusterWorker.getServerKey(message); + const key = UDPWorker.getServerKey(message); this.servers.set(key, stamp); @@ -223,5 +226,5 @@ class UDPClusterWorker { } if (!isMainThread && parentPort) { - new UDPClusterWorker(workerData, parentPort); + new UDPWorker(workerData, parentPort); } From 1f14888e338fd2c3edf834cc46e657329c978be1 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Tue, 6 Jan 2026 14:58:12 +0100 Subject: [PATCH 78/78] 2.0.26 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ccbcd4..c5a9f71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "2.0.25", + "version": "2.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.25", + "version": "2.0.26", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.7.0" diff --git a/package.json b/package.json index 172aab1..d41118a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.25", + "version": "2.0.26", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue",