diff --git a/README.md b/README.md index b9930ba..3230034 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ another event emiiter. features: - can listen async/conjoined event. -- support to mixed async/sync handlers. +- support to mixed async/sync handlers +- conditional event handlers. (onDual) - return unscriber when you subscribe an event. [![Coverage Status](https://coveralls.io/repos/github/lisez/xevt/badge.svg)](https://coveralls.io/github/lisez/xevt) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) @@ -48,6 +49,45 @@ emitter.on("event", async () => { await emitter.emit("event"); ``` +### Conditional event handlers. + +IMPORTANT: conditional handlers not supported to conjoined event. + +```typescript +const emitter = new Xevt(); +const result: any[] = []; +emitter.on("event", (arg: number) => { + result.push(arg); + return arg % 2 === 0; +}); + +emitter.onDual("event", { + true: async () => { + result.push("first"); + }, +}); + +emitter.onDual("event", { + false: async () => { + result.push("fail"); + }, +}); + +emitter.onDual("event", { + true: () => { + result.push(100); + }, + false: () => { + result.push(99); + }, +}); + +emitter.emit("event", 1); +emitter.emit("event", 2); +await delay(10); +// [1, "fail", 99, 2, "first", 100] +``` + ### Conjoined event IMPORTANT: conjoined events are not supported any arguments in handlers. diff --git a/modules/conjoin_emitter.ts b/modules/conjoin_emitter.ts index 5ccf2f2..18eda3a 100644 --- a/modules/conjoin_emitter.ts +++ b/modules/conjoin_emitter.ts @@ -123,15 +123,18 @@ export class ConjoinEmitter extends CoreEmitter if (!this.hasEvent(event)) return; const executing = this.consume(event); - if (executing.length) { - if (this.debug) this.logger.debug("conjoined", executing); - if (this.prevEvents) { - this.prevEvents = this.prevEvents.then(() => this.exec(executing)); - } else { - this.prevEvents = this.exec(executing); - } + if (!executing.length) return; + if (this.debug) this.logger.debug("conjoined", executing); + + const next = () => { + this.prevEvents = this.exec(executing); + return this.prevEvents; + }; + + if (this.prevEvents instanceof Promise) { + return Promise.resolve(this.prevEvents).then(next); } - return this.prevEvents; + return next(); } off(event: ConjoinEvents, handler?: EventHandler): void { diff --git a/modules/core_emitter.ts b/modules/core_emitter.ts index 4b9ca29..5a236df 100644 --- a/modules/core_emitter.ts +++ b/modules/core_emitter.ts @@ -1,4 +1,5 @@ import type { + DualEventHandler, ErrorHandler, EventHandler, EventHandlerSignature, @@ -59,7 +60,10 @@ export abstract class CoreEmitter implements XCoreEmitter { this.handlers.delete(event); } - protected offByHandler(event: EventName, handler: EventHandler): void { + protected offByHandler( + event: EventName, + handler: EventHandler | DualEventHandler, + ): void { const handlers = this.handlers.get(event); if (!handlers?.length) return; const idx = handlers.findIndex((h) => h.handler === handler); diff --git a/modules/emitter.ts b/modules/emitter.ts index 4a73e3e..e9b183f 100644 --- a/modules/emitter.ts +++ b/modules/emitter.ts @@ -1,4 +1,6 @@ import type { + DualEventHandler, + DualEventHandlerSignature, ErrorHandler, EventHandler, EventName, @@ -7,7 +9,7 @@ import type { } from "modules/types.ts"; import { CoreEmitter } from "modules/core_emitter.ts"; -import { SequenceRunner } from "modules/runners/sequence.ts"; +import { StepRunner } from "modules/runners/step.ts"; export const EmitDone = Symbol("emit_done"); @@ -26,6 +28,22 @@ export class Emitter extends CoreEmitter implements XevtEmitter { return this.onBySignature(event, signature); } + onDual( + event: EventName, + handler: DualEventHandler, + options?: Partial, + ) { + const signature: DualEventHandlerSignature = { + name: event, + handler, + options: { + once: options?.once || event === EmitDone, + dual: true, + }, + }; + return this.onBySignature(event, signature); + } + get addEventListener() { return this.on; } @@ -37,18 +55,13 @@ export class Emitter extends CoreEmitter implements XevtEmitter { emit(event: EventName, ...args: any[]): any { if (this.debug) this.logger.debug("emit", event, args); - const handlers = this.handlers.get(event)?.slice() || []; - for (const e of handlers.filter((e) => e.options?.once)) { - this.offByHandler(event, e.handler); - } - try { - if (this.prevEvents) { + if (this.prevEvents instanceof Promise) { this.prevEvents = this.prevEvents.then(() => - new SequenceRunner(handlers).exec(0, ...args) + this.prevEvents = new StepRunner(this.handlers).exec(event, args) ); } else { - this.prevEvents = new SequenceRunner(handlers).exec(0, ...args); + this.prevEvents = new StepRunner(this.handlers).exec(event, args); } return this.prevEvents; } catch (err) { diff --git a/modules/helpers.ts b/modules/helpers.ts index 98a0ae1..1972793 100644 --- a/modules/helpers.ts +++ b/modules/helpers.ts @@ -1,7 +1,18 @@ -import type { ConjoinEvents, EventName } from "modules/types.ts"; +import type { + ConjoinEvents, + DualEventHandlerSignature, + EventHandlerSignature, + EventName, +} from "modules/types.ts"; export function isConjoinEvents( event: EventName | EventName[] | ConjoinEvents, ): event is ConjoinEvents { return Array.isArray(event) && event.length > 1; } + +export function isDualHandler( + handler: EventHandlerSignature, +): handler is DualEventHandlerSignature { + return !!handler.options && "dual" in handler.options; +} diff --git a/modules/runners/dual.ts b/modules/runners/dual.ts new file mode 100644 index 0000000..10f5592 --- /dev/null +++ b/modules/runners/dual.ts @@ -0,0 +1,61 @@ +import type { + DualEventHandlerSignature, + GeneralEventHandlerSignature, +} from "modules/types.ts"; + +import { SequenceRunner } from "modules/runners/sequence.ts"; + +/** + * Run a dual event handler. + */ +export class DualRunner { + /** + * Create a new instance of the DualRunner. + * @param handlers The dual handler profile. + */ + constructor( + private handlers: DualEventHandlerSignature[], + ) { + this.handlers = handlers; + } + + /** + * Conditionally filter the handlers. + * @param condition The condition to filter the handlers. + * @returns The filtered handlers. + */ + private filterHandlers( + condition: boolean, + ): GeneralEventHandlerSignature[] { + const handlers: GeneralEventHandlerSignature[] = []; + for (const p of this.handlers) { + // @ts-ignore TS2538 + if (typeof p.handler[condition] === "function") { + // @ts-ignore TS2538 + handlers.push({ ...p, handler: p.handler[condition] }); + } + } + return handlers; + } + + /** + * Execute the dual handler that corresponds to the result. + * @param result The result of the handler. + */ + private dualExec(result: any) { + const handlers = this.filterHandlers(!!result); + if (!handlers.length) return; + return new SequenceRunner(handlers).exec([result]); + } + + /** + * Execute the dual handler. + * @param args The arguments to pass to the dual handler. + */ + exec(result: any) { + if (result instanceof Promise) { + return Promise.resolve(result).then((res) => this.dualExec(res)); + } + return this.dualExec(result); + } +} diff --git a/modules/runners/sequence.ts b/modules/runners/sequence.ts index ab23969..f048ade 100644 --- a/modules/runners/sequence.ts +++ b/modules/runners/sequence.ts @@ -1,4 +1,4 @@ -import type { EventHandlerSignature } from "modules/types.ts"; +import type { GeneralEventHandlerSignature } from "modules/types.ts"; import { SingleRunner } from "modules/runners/single.ts"; @@ -6,40 +6,38 @@ import { SingleRunner } from "modules/runners/single.ts"; * Run handlers in sequence. */ export class SequenceRunner< - T extends EventHandlerSignature = EventHandlerSignature, + N extends GeneralEventHandlerSignature = GeneralEventHandlerSignature< + any + >, > { /** * Create a new instance of the SequenceRunner. * @param handlers The handlers to run. */ - constructor(private handlers: T[]) { + constructor(private handlers: N[]) { this.handlers = handlers; } /** * Execute the handlers in sequence. - * @param pointer The current handler index. * @param args The arguments to pass to the handlers. + * @param index The current handler index. */ exec( - pointer: number = 0, - ...args: Parameters + args: Parameters, + index: number = 0, ): void | Promise { - const profile = this.handlers[pointer]; + const profile = this.handlers[index]; if (!profile) return; - const result = new SingleRunner(profile).exec( - ...args, - ); + const result = new SingleRunner(profile).exec(args) as any; /** * Wait for the handler to finish before moving to the next handler. */ - if (profile.options?.async) { - return Promise.resolve(result).then(() => - this.exec(pointer + 1, ...args) - ); + if (profile.options?.async || result instanceof Promise) { + return Promise.resolve(result).then(() => this.exec(args, index + 1)); } - return this.exec(pointer + 1, ...args); + return this.exec(args, index + 1); } } diff --git a/modules/runners/series.ts b/modules/runners/series.ts index 6c9ae92..489da3c 100644 --- a/modules/runners/series.ts +++ b/modules/runners/series.ts @@ -1,10 +1,6 @@ -import type { - EventHandlerSignature, - EventName, - RegisteredHandlers, -} from "modules/types.ts"; +import type { EventName, RegisteredHandlers } from "modules/types.ts"; -import { SequenceRunner } from "modules/runners/sequence.ts"; +import { StepRunner } from "modules/runners/step.ts"; /** * Run handlers each in series. @@ -18,18 +14,6 @@ export class SeriesRunner { this.handlers = handlers; } - /** - * Remove a handler. - * @param key The event name. - * @param profile The handler profile. - */ - private remove(key: EventName, profile: EventHandlerSignature): void { - const handlers = this.handlers.get(key); - if (!handlers) return; - const idx = handlers.findIndex((h) => h.handler === profile.handler); - if (idx !== -1) handlers.splice(idx, 1); - } - /** * Execute the handlers in series. * @param series The series of event names. @@ -39,15 +23,9 @@ export class SeriesRunner { const key = series[idx]; if (!key) return; - const handlers = this.handlers.get(key)?.slice() || []; - for (const p of handlers.filter((p) => !!p.options?.once)) { - this.remove(key, p); - } - if (!handlers.length) return; - - const result = new SequenceRunner(handlers).exec(0); - if (result) { - return result.then(() => this.exec(series, idx + 1)); + const step = new StepRunner(this.handlers).exec(key); + if (step instanceof Promise) { + return Promise.resolve(step).then(() => this.exec(series, idx + 1)); } return this.exec(series, idx + 1); } diff --git a/modules/runners/single.ts b/modules/runners/single.ts index 8a8498a..c1e2256 100644 --- a/modules/runners/single.ts +++ b/modules/runners/single.ts @@ -1,4 +1,4 @@ -import type { EventHandlerSignature } from "modules/types.ts"; +import type { GeneralEventHandlerSignature } from "modules/types.ts"; /** * The result of a single runner. @@ -11,7 +11,9 @@ export type SingleRunnerResult any> = ReturnType< * Run a handler. */ export class SingleRunner< - T extends EventHandlerSignature = EventHandlerSignature, + T extends GeneralEventHandlerSignature = GeneralEventHandlerSignature< + any + >, > { /** * Create a new instance of the SingleRunner. @@ -25,9 +27,7 @@ export class SingleRunner< * Execute the handler. * @param args The arguments to pass to the handler. */ - exec( - ...args: Parameters - ): SingleRunnerResult { + exec(args: Parameters): SingleRunnerResult { return this.profile.handler(...args); } } diff --git a/modules/runners/step.ts b/modules/runners/step.ts new file mode 100644 index 0000000..e870b3d --- /dev/null +++ b/modules/runners/step.ts @@ -0,0 +1,137 @@ +import type { + DualEventHandlerSignature, + EventHandlerSignature, + EventName, + GeneralEventHandlerSignature, + RegisteredHandlers, +} from "modules/types.ts"; + +import { DualRunner } from "modules/runners/dual.ts"; +import { SingleRunner } from "modules/runners/single.ts"; +import { SequenceRunner } from "modules/runners/sequence.ts"; +import * as helpers from "modules/helpers.ts"; + +/** + * Run handlers step by step. + */ +export class StepRunner { + /** + * Create a new instance of the StepRunner. + * @param handlers The all handlers. + */ + constructor(private handlers: RegisteredHandlers) { + this.handlers = handlers; + } + + /** + * Remove a handler. + * @param key The event name. + * @param profile The handler profile. + */ + private remove(key: EventName, profile: EventHandlerSignature): void { + const handlers = this.handlers.get(key); + if (!handlers) return; + const idx = handlers.findIndex((h) => h.handler === profile.handler); + if (idx !== -1) handlers.splice(idx, 1); + } + + /** + * Execute the handlers step by step by index. + */ + private execByIndex( + handlers: GeneralEventHandlerSignature[] = [], + duals: DualEventHandlerSignature[] = [], + args: any[] = [], + idx = 0, + ): void | Promise { + const handler = handlers[idx]; + if (!handler) return; + + const result = new SingleRunner(handler).exec(args); + + const next = (result: any) => { + const dualResult = new DualRunner(duals).exec(!!result); + if (dualResult instanceof Promise) { + return dualResult.then(() => + this.execByIndex(handlers, duals, args, idx + 1) + ); + } + }; + + if (handler.options?.async) { + return Promise.resolve(result).then(() => next(result)); + } + return next(result); + } + + /** + * Execute the handlers in sequence. + */ + private execInSequence( + step: EventName, + handlers: GeneralEventHandlerSignature[], + args: any[] = [], + ) { + for (const p of handlers.filter((e) => !!e.options?.once)) { + this.remove(step, p); + } + return new SequenceRunner(handlers as GeneralEventHandlerSignature[]) + .exec(args); + } + + /** + * Execute the handlers step by step. + */ + private execByStep( + step: EventName, + handlers: EventHandlerSignature[], + args?: any[], + ) { + const categories = handlers.reduce((y, x) => { + if (x.options?.once) { + y.once.push(x); + } + if (helpers.isDualHandler(x)) { + y.duals.push(x); + } else { + y.handlers.push(x); + } + return y; + }, { + handlers: [] as GeneralEventHandlerSignature[], + duals: [] as DualEventHandlerSignature[], + once: [] as EventHandlerSignature[], + }); + if (!categories.handlers.length) return; + + for (const p of categories.once) { + this.remove(step, p); + } + + return this.execByIndex(categories.handlers, categories.duals, args, 0); + } + + /** + * Execute the handlers by step. + * @param step The event name. + * @param args The arguments to pass to the handlers. + */ + exec(step: EventName, args?: any[]): void | Promise { + const handlers = this.handlers.get(step)?.slice() || []; + if (!handlers.length) return; + + const hasDual = handlers.some(helpers.isDualHandler); + if (!hasDual) { + return this.execInSequence( + step, + handlers as GeneralEventHandlerSignature[], + args, + ); + } + return this.execByStep( + step, + handlers as EventHandlerSignature[], + args, + ); + } +} diff --git a/modules/types.ts b/modules/types.ts index 8383fe6..fa67c5e 100644 --- a/modules/types.ts +++ b/modules/types.ts @@ -10,7 +10,24 @@ export type EventOptions = { once: boolean; }; -export type EventHandlerSignature = { +export type DualEventHandler = + | { true: EventHandler; false: EventHandler | ErrorHandler } + | { true: EventHandler } + | { false: EventHandler | ErrorHandler }; + +export type DualEventHandlerSignature = { + name: T; + handler: DualEventHandler; + options: + & Partial< + EventOptions & { + async: boolean; + } + > + & { dual: true }; +}; + +export type GeneralEventHandlerSignature = { name: T; handler: EventHandler; options?: Partial< @@ -20,6 +37,10 @@ export type EventHandlerSignature = { >; }; +export type EventHandlerSignature = + | GeneralEventHandlerSignature + | DualEventHandlerSignature; + export type EventUnscriber = () => void; export type EventRegister = ( @@ -28,6 +49,12 @@ export type EventRegister = ( options?: Partial, ) => EventUnscriber; +export type DualEventRegister = ( + event: EventName, + handlers: DualEventHandler, + options?: Partial, +) => EventUnscriber; + export type ConjoinEventsRegister = ( events: ConjoinEvents, handler: EventHandler, @@ -65,6 +92,9 @@ export type XCoreEmitter = & EventUnregister; export type XevtEmitter = + & { + onDual: DualEventRegister; + } & XCoreEmitter & Record< | "addEventListener" diff --git a/modules/xevt.ts b/modules/xevt.ts index 44d25bb..187e654 100644 --- a/modules/xevt.ts +++ b/modules/xevt.ts @@ -51,6 +51,10 @@ export class Xevt extends CoreEmitter } } + get onDual() { + return this.emitter.onDual.bind(this.emitter); + } + get addEventListener() { return this.on; } diff --git a/tests/deno/single_event_test.ts b/tests/deno/single_event_test.ts index 67627cf..cd588b1 100644 --- a/tests/deno/single_event_test.ts +++ b/tests/deno/single_event_test.ts @@ -155,3 +155,133 @@ describe("Xevt - unscriber", () => { assertEquals(result, [1]); }); }); + +describe("Xevt - onDual", () => { + it('should listen event with "onDual"', () => { + const emitter = new Xevt(); + const result: number[] = []; + emitter.on("event", (arg: number) => { + result.push(arg); + return arg % 2 === 0; + }); + + emitter.onDual("event", { + true: () => { + result.push(100); + }, + false: () => { + result.push(99); + }, + }); + + emitter.emit("event", 1); + emitter.emit("event", 2); + assertEquals(result, [1, 99, 2, 100]); + }); + + it("should listen multiple onDual events", () => { + const emitter = new Xevt(); + const result: any[] = []; + emitter.on("event", (arg: number) => { + result.push(arg); + return arg % 2 === 0; + }); + + emitter.onDual("event", { + true: () => { + result.push("first"); + }, + }); + + emitter.onDual("event", { + false: () => { + result.push("fail"); + }, + }); + + emitter.onDual("event", { + true: () => { + result.push(100); + }, + false: () => { + result.push(99); + }, + }); + + emitter.emit("event", 1); + emitter.emit("event", 2); + assertEquals(result, [1, "fail", 99, 2, "first", 100]); + }); + + it("should listen once onDual event", () => { + const emitter = new Xevt(); + const result: any[] = []; + emitter.on("event", (arg: number) => { + result.push(arg); + return arg % 2 === 0; + }); + + emitter.onDual("event", { + true: () => { + result.push("first"); + }, + }); + + emitter.onDual("event", { + false: () => { + result.push("fail"); + }, + }); + + emitter.onDual("event", { + true: () => { + result.push(100); + }, + false: () => { + result.push(99); + }, + }, { once: true }); + + emitter.emit("event", 1); + emitter.emit("event", 2); + emitter.emit("event", 1); + emitter.emit("event", 2); + + assertEquals(result, [1, "fail", 99, 2, "first", 1, "fail", 2, "first"]); + }); + + it("should listen async onDual events", async () => { + const emitter = new Xevt(); + const result: any[] = []; + emitter.on("event", (arg: number) => { + result.push(arg); + return arg % 2 === 0; + }); + + emitter.onDual("event", { + true: async () => { + result.push("first"); + }, + }); + + emitter.onDual("event", { + false: async () => { + result.push("fail"); + }, + }); + + emitter.onDual("event", { + true: () => { + result.push(100); + }, + false: () => { + result.push(99); + }, + }); + + emitter.emit("event", 1); + emitter.emit("event", 2); + await delay(10); + assertEquals(result, [1, "fail", 99, 2, "first", 100]); + }); +});