From e41434d32e8c95243da5b85fd7345110e6d3b2de Mon Sep 17 00:00:00 2001 From: Zahra Bayat Date: Thu, 18 Sep 2025 18:29:33 +0330 Subject: [PATCH 1/7] implement event filtering --- src/flare.ts | 8 ++++++-- src/types.ts | 3 ++- test/flare.test.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/flare.ts b/src/flare.ts index b6cbbcf..5b9159f 100644 --- a/src/flare.ts +++ b/src/flare.ts @@ -26,7 +26,7 @@ export class Flare> { async fire( event: K, payload: E[K], - options: FlareFireOptions = {}, + options: FlareFireOptions = {}, ): Promise { const beforeResult = this.handleBeforeInterceptors(event, payload); if (beforeResult) return beforeResult; @@ -39,7 +39,11 @@ export class Flare> { const handlers = this.handlers[event]; if (!handlers) return Promise.resolve(`No handlers found for event "${String(event)}".`); - const { strategy = FlareFireStrategy.Parallel, timeout, haltOnError = false } = options; + const { strategy = FlareFireStrategy.Parallel, timeout, haltOnError = false, fireIf } = options; + + if (fireIf && !fireIf(newPayload)) { + return Promise.resolve(`Event "${String(event)}" skipped by fireIf condition.`); + } if (strategy === FlareFireStrategy.Parallel) { await Promise.allSettled( diff --git a/src/types.ts b/src/types.ts index f7e2ed9..98309dd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,10 +7,11 @@ export enum FlareFireStrategy { Serial = 'serial' }; -export type FlareFireOptions = { +export type FlareFireOptions = { timeout?: number; haltOnError?: boolean; strategy?: FlareFireStrategy; + fireIf?: (payload: E[K]) => boolean; }; export type FlareCatchOptions = { diff --git a/test/flare.test.ts b/test/flare.test.ts index 7853bd5..d9e7957 100644 --- a/test/flare.test.ts +++ b/test/flare.test.ts @@ -121,4 +121,46 @@ describe('Flare class', () => { // Act & Assert expect(() => flare.release(EVENT_NAME, handler)).not.toThrow(); }); + + it('skips handlers when fireIf returns false', async () => { + // Arrange + const handler = jest.fn(); + flare.catch(EVENT_NAME, handler); + + // Act + await flare.fire(EVENT_NAME, PAYLOAD, { + fireIf: () => false, + }); + + // Assert + expect(handler).not.toHaveBeenCalled(); + }); + + it('executes handlers when fireIf returns true', async () => { + // Arrange + const handler = jest.fn(); + flare.catch(EVENT_NAME, handler); + + // Act + await flare.fire(EVENT_NAME, PAYLOAD, { + fireIf: () => true, + }); + + // Assert + expect(handler).toHaveBeenCalledWith(PAYLOAD); + }); + + it('receives payload in fireIf predicate', async () => { + // Arrange + const handler = jest.fn(); + flare.catch(EVENT_NAME, handler); + const fireIf = jest.fn(() => true); + + // Act + await flare.fire(EVENT_NAME, PAYLOAD, { fireIf }); + + // Assert + expect(fireIf).toHaveBeenCalledWith(PAYLOAD); + expect(handler).toHaveBeenCalledWith(PAYLOAD); + }); }); From 640b8e33bb6bd5061f4d4f3783fcd698d6c6585b Mon Sep 17 00:00:00 2001 From: Zahra Bayat Date: Thu, 18 Sep 2025 18:34:48 +0330 Subject: [PATCH 2/7] add flare npm badge in readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index db55ce8..ae5cb1d 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ **Blazing fast event aggregator for JavaScript & TypeScript** A lightweight, flexible, and type-safe alternative to traditional event emitters. - -[![License](https://img.shields.io/github/license/xafans/flare)](./LICENSE)  [![Build Status](https://github.com/xafans/flare/actions/workflows/ci.yml/badge.svg)](https://github.com/xafans/flare/actions) +[![npm version](https://img.shields.io/npm/v/@xafans/flare.svg)](https://www.npmjs.com/package/@xafans/flare)  [![License](https://img.shields.io/github/license/xafans/flare)](./LICENSE)  [![Build Status](https://github.com/xafans/flare/actions/workflows/ci.yml/badge.svg)](https://github.com/xafans/flare/actions) --- From b8826e358031bccf4cea40c791e156be361b0e76 Mon Sep 17 00:00:00 2001 From: Zahra Bayat Date: Sat, 20 Sep 2025 00:41:36 +0330 Subject: [PATCH 3/7] add unit tests --- src/flare.ts | 10 +++------- src/types.ts | 6 +++--- test/flare.test.ts | 49 ++++++++++++++++++++++++++++++---------------- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/flare.ts b/src/flare.ts index 5b9159f..f4ee4be 100644 --- a/src/flare.ts +++ b/src/flare.ts @@ -26,7 +26,7 @@ export class Flare> { async fire( event: K, payload: E[K], - options: FlareFireOptions = {}, + options: FlareFireOptions = {}, ): Promise { const beforeResult = this.handleBeforeInterceptors(event, payload); if (beforeResult) return beforeResult; @@ -39,11 +39,7 @@ export class Flare> { const handlers = this.handlers[event]; if (!handlers) return Promise.resolve(`No handlers found for event "${String(event)}".`); - const { strategy = FlareFireStrategy.Parallel, timeout, haltOnError = false, fireIf } = options; - - if (fireIf && !fireIf(newPayload)) { - return Promise.resolve(`Event "${String(event)}" skipped by fireIf condition.`); - } + const { strategy = FlareFireStrategy.Parallel, timeout, haltOnError = false } = options; if (strategy === FlareFireStrategy.Parallel) { await Promise.allSettled( @@ -73,7 +69,7 @@ export class Flare> { catch( event: K, handler: FlareHandler, - options: FlareCatchOptions = {}, + options: FlareCatchOptions = {} ): () => void { if (!this.handlers[event]) { this.handlers[event] = new Set(); diff --git a/src/types.ts b/src/types.ts index 98309dd..4439a8e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,15 +7,15 @@ export enum FlareFireStrategy { Serial = 'serial' }; -export type FlareFireOptions = { +export type FlareFireOptions = { timeout?: number; haltOnError?: boolean; strategy?: FlareFireStrategy; - fireIf?: (payload: E[K]) => boolean; }; -export type FlareCatchOptions = { +export type FlareCatchOptions = { once?: boolean; + when?: (payload: E[K]) => boolean; }; export type FlareInterceptor = { diff --git a/test/flare.test.ts b/test/flare.test.ts index d9e7957..5863755 100644 --- a/test/flare.test.ts +++ b/test/flare.test.ts @@ -122,45 +122,60 @@ describe('Flare class', () => { expect(() => flare.release(EVENT_NAME, handler)).not.toThrow(); }); - it('skips handlers when fireIf returns false', async () => { + test('release works for once: true handlers before firing', async () => { // Arrange const handler = jest.fn(); - flare.catch(EVENT_NAME, handler); + flare.catch(EVENT_NAME, handler, { once: true }); // Act - await flare.fire(EVENT_NAME, PAYLOAD, { - fireIf: () => false, - }); + flare.release(EVENT_NAME, handler); // release before fire + await flare.fire(EVENT_NAME, PAYLOAD); // Assert expect(handler).not.toHaveBeenCalled(); }); - it('executes handlers when fireIf returns true', async () => { + + test('handler runs only when when() returns true', async () => { // Arrange const handler = jest.fn(); - flare.catch(EVENT_NAME, handler); + flare.catch(EVENT_NAME, handler, { when: (payload) => payload === 'run' }); // Act - await flare.fire(EVENT_NAME, PAYLOAD, { - fireIf: () => true, - }); + await flare.fire(EVENT_NAME, 'skip'); + await flare.fire(EVENT_NAME, 'run'); // Assert - expect(handler).toHaveBeenCalledWith(PAYLOAD); + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith('run'); }); - it('receives payload in fireIf predicate', async () => { + test('handler can dynamically run and skip across multiple fires', async () => { // Arrange const handler = jest.fn(); - flare.catch(EVENT_NAME, handler); - const fireIf = jest.fn(() => true); + flare.catch(EVENT_NAME, handler, { when: (payload) => payload.active }); // Act - await flare.fire(EVENT_NAME, PAYLOAD, { fireIf }); + await flare.fire(EVENT_NAME, { active: false }); + await flare.fire(EVENT_NAME, { active: true }); + await flare.fire(EVENT_NAME, { active: false }); + await flare.fire(EVENT_NAME, { active: true }); // Assert - expect(fireIf).toHaveBeenCalledWith(PAYLOAD); - expect(handler).toHaveBeenCalledWith(PAYLOAD); + expect(handler).toHaveBeenCalledTimes(2); + }); + + test('once handlers with when only removed if executed', async () => { + // Arrange + const handler = jest.fn(); + flare.catch(EVENT_NAME, handler, { once: true, when: (payload) => payload.active }); + + // Act + await flare.fire(EVENT_NAME, { active: false }); + await flare.fire(EVENT_NAME, { active: true }); + await flare.fire(EVENT_NAME, { active: true }); + + // Assert + expect(handler).toHaveBeenCalledTimes(1); }); }); From dd2bbaea8652204895584f3e2f193c3e60226b6a Mon Sep 17 00:00:00 2001 From: Zahra Bayat Date: Sat, 20 Sep 2025 00:50:22 +0330 Subject: [PATCH 4/7] implement event handler filtering --- src/flare.ts | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/flare.ts b/src/flare.ts index f4ee4be..05a14fa 100644 --- a/src/flare.ts +++ b/src/flare.ts @@ -11,6 +11,9 @@ export class Flare> { private handlers: { [K in keyof E]?: Set>; } = {}; + private handlerMap: { + [K in keyof E]?: WeakMap, FlareHandler>; + } = {}; private interceptors: FlareInterceptor[] = []; private middlewares: FlareMiddleware[] = []; @@ -74,16 +77,19 @@ export class Flare> { if (!this.handlers[event]) { this.handlers[event] = new Set(); } + if (!this.handlerMap[event]) { + this.handlerMap[event] = new WeakMap(); + } - if (options.once) { - const wrappedHandler = (payload: E[K]) => { + const wrappedHandler = (payload: E[K]) => { + if (!options.when || options.when(payload)) { this.call(handler, payload); - this.release(event, wrappedHandler); - }; - this.handlers[event]!.add(wrappedHandler); - } else { - this.handlers[event]!.add(handler); - } + if (options.once) this.release(event, handler); + } + }; + + this.handlers[event]!.add(wrappedHandler); + this.handlerMap[event]!.set(handler, wrappedHandler); return () => this.release(event, handler); } @@ -92,7 +98,14 @@ export class Flare> { event: K, handler: FlareHandler, ): void { - this.handlers[event]?.delete(handler); + const map = this.handlerMap[event]; + if (!map) return; + + const wrappedHandler = map.get(handler); + if (!wrappedHandler) return; + + this.handlers[event]?.delete(wrappedHandler); + map.delete(handler); } releaseAll(): void { From f41baf8ae8fe16a949a1ddf61ed229aca136af9e Mon Sep 17 00:00:00 2001 From: Zahra Bayat Date: Mon, 22 Sep 2025 15:47:26 +0330 Subject: [PATCH 5/7] resolve conflict --- src/flare.ts | 141 +++++++++++++++++++++++++-------------------- test/flare.test.ts | 16 ++++- 2 files changed, 93 insertions(+), 64 deletions(-) diff --git a/src/flare.ts b/src/flare.ts index 05a14fa..967a247 100644 --- a/src/flare.ts +++ b/src/flare.ts @@ -8,11 +8,8 @@ import { } from './types'; export class Flare> { - private handlers: { - [K in keyof E]?: Set>; - } = {}; - private handlerMap: { - [K in keyof E]?: WeakMap, FlareHandler>; + private handlerOptionsStore: { + [K in keyof E]?: Set>; } = {}; private interceptors: FlareInterceptor[] = []; @@ -39,32 +36,10 @@ export class Flare> { return Promise.resolve(`Event "${String(event)}" was stopped by middleware.`); } - const handlers = this.handlers[event]; - if (!handlers) return Promise.resolve(`No handlers found for event "${String(event)}".`); + const handlerOptionsSet = this.handlerOptionsStore[event]; + if (!handlerOptionsSet) return Promise.resolve(`No handlers found for event "${String(event)}".`); - const { strategy = FlareFireStrategy.Parallel, timeout, haltOnError = false } = options; - - if (strategy === FlareFireStrategy.Parallel) { - await Promise.allSettled( - Array.from(handlers).map(async (handler) => { - try { - await this.execute(handler, newPayload, timeout); - } catch (err) { - // TODO: handle or log exception - if (haltOnError) throw err; - } - }) - ); - } else { - for (const handler of handlers) { - try { - await this.execute(handler, newPayload, timeout); - } catch (err) { - // TODO: handle or log exception - if (haltOnError) break; - } - } - } + await this.handleExecute(event, newPayload, options, handlerOptionsSet); this.handleAfterInterceptors(event, newPayload); } @@ -74,22 +49,14 @@ export class Flare> { handler: FlareHandler, options: FlareCatchOptions = {} ): () => void { - if (!this.handlers[event]) { - this.handlers[event] = new Set(); + if (!this.handlerOptionsStore[event]) { + this.handlerOptionsStore[event] = new Set(); } - if (!this.handlerMap[event]) { - this.handlerMap[event] = new WeakMap(); - } - - const wrappedHandler = (payload: E[K]) => { - if (!options.when || options.when(payload)) { - this.call(handler, payload); - if (options.once) this.release(event, handler); - } - }; - this.handlers[event]!.add(wrappedHandler); - this.handlerMap[event]!.set(handler, wrappedHandler); + this.handlerOptionsStore[event].add({ + handler, + options + }) return () => this.release(event, handler); } @@ -98,26 +65,26 @@ export class Flare> { event: K, handler: FlareHandler, ): void { - const map = this.handlerMap[event]; - if (!map) return; - - const wrappedHandler = map.get(handler); - if (!wrappedHandler) return; + const handlerOptionsSet = this.handlerOptionsStore[event]; + if (!handlerOptionsSet) return; - this.handlers[event]?.delete(wrappedHandler); - map.delete(handler); + for (const handlerOptions of handlerOptionsSet) { + if (handlerOptions.handler === handler) { + handlerOptionsSet.delete(handlerOptions); + break; + } + } } releaseAll(): void { - for (const event in this.handlers) { - if (!Object.prototype.hasOwnProperty.call(this.handlers, event)) continue; - const handlers = this.handlers[event]; - handlers?.clear(); + for (const event in this.handlerOptionsStore) { + if (!Object.prototype.hasOwnProperty.call(this.handlerOptionsStore, event)) continue; + this.handlerOptionsStore[event]?.clear(); } - this.handlers = {}; + this.handlerOptionsStore = {}; } - // ==================== internals ==================== + // ==================== handle methods ==================== private handleBeforeInterceptors(event: K, payload: E[K]) { for (const interceptor of this.interceptors) { @@ -167,14 +134,59 @@ export class Flare> { }; } + private async handleExecute( + event: K, + newPayload: E[K], + fireOptions: FlareFireOptions, + handlerOptionsSet: Set> + ) { + const { strategy = FlareFireStrategy.Parallel, timeout, haltOnError = false } = fireOptions; + + if (strategy === FlareFireStrategy.Parallel) { + await Promise.allSettled( + Array.from(handlerOptionsSet).map(async (handlerOptions) => { + try { + await this.execute(event, newPayload, timeout, handlerOptions); + } catch (err) { + // TODO: handle or log exception + if (haltOnError) throw err; + } + }) + ); + } else { + for (const handlerOptions of handlerOptionsSet) { + try { + await this.execute(event, newPayload, timeout, handlerOptions); + } catch (err) { + // TODO: handle or log exception + if (haltOnError) break; + } + } + } + } + + // ==================== internal methods ==================== + private async execute( - handler: FlareHandler, + event: K, payload: E[K], - timeout?: number): Promise { - if (!timeout) return this.call(handler, payload); + timeout: number | undefined, + handlerOptions: HandlerOptionsPair + ): Promise { + const shouldRun = !handlerOptions.options.when || handlerOptions.options.when(payload); + if (!shouldRun) return; + + try { + if (!timeout) return this.call(handlerOptions.handler, payload); + + const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('FlareHandler timeout')), timeout)); + return Promise.race([this.call(handlerOptions.handler, payload), timeoutPromise]); - const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('FlareHandler timeout')), timeout)); - return Promise.race([this.call(handler, payload), timeoutPromise]); + } finally { + if (handlerOptions.options.once) { + this.handlerOptionsStore[event]?.delete(handlerOptions); + } + } }; private call( @@ -191,3 +203,8 @@ export class Flare> { } } } + +interface HandlerOptionsPair { + handler: FlareHandler; + options: FlareCatchOptions; +}; diff --git a/test/flare.test.ts b/test/flare.test.ts index 5863755..8a118c8 100644 --- a/test/flare.test.ts +++ b/test/flare.test.ts @@ -77,6 +77,19 @@ describe('Flare class', () => { expect(handler).toHaveBeenCalledTimes(1); }); + test('callback of catch with once: true', async () => { + // Arrange + const handler = jest.fn(); + const release = flare.catch(EVENT_NAME, handler, { once: true }); + + // Act + release(); + await flare.fire(EVENT_NAME, PAYLOAD); + + // Assert + expect(handler).toHaveBeenCalledTimes(0); + }); + test('releaseAll', async () => { // Arrange const handler = jest.fn(); @@ -128,14 +141,13 @@ describe('Flare class', () => { flare.catch(EVENT_NAME, handler, { once: true }); // Act - flare.release(EVENT_NAME, handler); // release before fire + flare.release(EVENT_NAME, handler); await flare.fire(EVENT_NAME, PAYLOAD); // Assert expect(handler).not.toHaveBeenCalled(); }); - test('handler runs only when when() returns true', async () => { // Arrange const handler = jest.fn(); From dd86973a3ddb4c0b156f9f0c3232fc948b5a7010 Mon Sep 17 00:00:00 2001 From: Zahra Bayat Date: Mon, 22 Sep 2025 16:00:44 +0330 Subject: [PATCH 6/7] revert unrelated changes --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ae5cb1d..f0be6d2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ **Blazing fast event aggregator for JavaScript & TypeScript** A lightweight, flexible, and type-safe alternative to traditional event emitters. -[![npm version](https://img.shields.io/npm/v/@xafans/flare.svg)](https://www.npmjs.com/package/@xafans/flare)  [![License](https://img.shields.io/github/license/xafans/flare)](./LICENSE)  [![Build Status](https://github.com/xafans/flare/actions/workflows/ci.yml/badge.svg)](https://github.com/xafans/flare/actions) + +[![License](https://img.shields.io/github/license/xafans/flare)](./LICENSE)  [![Build Status](https://github.com/xafans/flare/actions/workflows/ci.yml/badge.svg)](https://github.com/xafans/flare/actions) --- From a455abaa6c71892b864bab70acbba48058d5334b Mon Sep 17 00:00:00 2001 From: Zahra Bayat Date: Mon, 22 Sep 2025 16:01:55 +0330 Subject: [PATCH 7/7] remove unrelated changes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f0be6d2..db55ce8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A lightweight, flexible, and type-safe alternative to traditional event emitters. -[![License](https://img.shields.io/github/license/xafans/flare)](./LICENSE)  [![Build Status](https://github.com/xafans/flare/actions/workflows/ci.yml/badge.svg)](https://github.com/xafans/flare/actions) +[![License](https://img.shields.io/github/license/xafans/flare)](./LICENSE)  [![Build Status](https://github.com/xafans/flare/actions/workflows/ci.yml/badge.svg)](https://github.com/xafans/flare/actions) ---