From 1dbbd6da9bfd849b1b99826a6e27f76dec6040e2 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Fri, 1 Sep 2023 20:08:49 +0300 Subject: [PATCH] fix: event emitter listener order --- packages/library/src/environment-decorator.ts | 29 ++++++++++++++- packages/library/src/types/Emitter.ts | 4 +- .../src/utils/emitters/ReadonlyEmitterBase.ts | 37 ++++++++++--------- .../src/utils/emitters/SemiAsyncEmitter.ts | 13 ++++--- 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/packages/library/src/environment-decorator.ts b/packages/library/src/environment-decorator.ts index 70718c4..02e722a 100644 --- a/packages/library/src/environment-decorator.ts +++ b/packages/library/src/environment-decorator.ts @@ -134,7 +134,34 @@ export function WithMetadata( constructor(...args: any[]) { super(...args); onTestEnvironmentCreate(this, args[0], args[1]); - this.testEvents.on('*', ({ event, state }) => onHandleTestEvent(event, state)); + + const handler = ({ event, state }: ForwardedCircusEvent) => onHandleTestEvent(event, state); + + this.testEvents + .on('setup', handler, -1) + .on('include_test_location_in_result', handler, -1) + .on('start_describe_definition', handler, -1) + .on('finish_describe_definition', handler, Number.MAX_SAFE_INTEGER) + .on('add_hook', handler, -1) + .on('add_test', handler, -1) + .on('run_start', handler, -1) + .on('run_describe_start', handler, -1) + .on('hook_failure', handler, Number.MAX_SAFE_INTEGER) + .on('hook_start', handler, -1) + .on('hook_success', handler, Number.MAX_SAFE_INTEGER) + .on('test_start', handler, -1) + .on('test_started', handler, -1) + .on('test_retry', handler, -1) + .on('test_skip', handler, -1) + .on('test_todo', handler, -1) + .on('test_fn_start', handler, -1) + .on('test_fn_failure', handler, Number.MAX_SAFE_INTEGER) + .on('test_fn_success', handler, Number.MAX_SAFE_INTEGER) + .on('test_done', handler, Number.MAX_SAFE_INTEGER) + .on('run_describe_finish', handler, Number.MAX_SAFE_INTEGER) + .on('run_finish', handler, Number.MAX_SAFE_INTEGER) + .on('teardown', handler, Number.MAX_SAFE_INTEGER) + .on('error', handler, -1); } protected get testEvents(): ReadonlyAsyncEmitter { diff --git a/packages/library/src/types/Emitter.ts b/packages/library/src/types/Emitter.ts index 5d77b5a..7928fdf 100644 --- a/packages/library/src/types/Emitter.ts +++ b/packages/library/src/types/Emitter.ts @@ -8,8 +8,8 @@ export interface ReadonlyAsyncEmitter< Event extends { type: string }, EventType = Event['type'] | '*', > { - on(type: EventType, listener: (event: Event) => void | Promise): this; - once(type: EventType, listener: (event: Event) => void | Promise): this; + on(type: EventType, listener: (event: Event) => void | Promise, weight?: number): this; + once(type: EventType, listener: (event: Event) => void | Promise, weight?: number): this; off(type: EventType, listener: (event: Event) => void | Promise): this; } diff --git a/packages/library/src/utils/emitters/ReadonlyEmitterBase.ts b/packages/library/src/utils/emitters/ReadonlyEmitterBase.ts index 0dce4d1..4bf2b5d 100644 --- a/packages/library/src/utils/emitters/ReadonlyEmitterBase.ts +++ b/packages/library/src/utils/emitters/ReadonlyEmitterBase.ts @@ -21,17 +21,16 @@ export abstract class ReadonlyEmitterBase< > implements ReadonlyEmitter { protected readonly _log: typeof logger; - protected readonly _listeners: Map = new Map(); + protected readonly _listeners: Map = new Map(); #listenersCounter = 0; - readonly #listenersOrder = new WeakMap(); constructor(name?: string, shouldLog = true) { this._log = (shouldLog ? logger : nologger).child({ cat: `emitter`, tid: `emitter-${name}` }); this._listeners.set('*', []); } - on(type: EventType, listener: EventListener & { [ONCE]?: true }): this { + on(type: EventType, listener: EventListener & { [ONCE]?: true }, order?: number): this { if (!listener[ONCE]) { this._log.trace(__LISTENERS(listener), `on(${type})`); } @@ -39,13 +38,17 @@ export abstract class ReadonlyEmitterBase< if (!this._listeners.has(type)) { this._listeners.set(type, []); } - this._listeners.get(type)!.push(this.#rememberListener(listener)); + + const listeners = this._listeners.get(type)!; + listeners.push([listener, order ?? this.#listenersCounter++]); + listeners.sort((a, b) => getOrder(a) - getOrder(b)); + return this; } - once(type: EventType, listener: EventListener): this { + once(type: EventType, listener: EventListener, order?: number): this { this._log.trace(__LISTENERS(listener), `once(${type})`); - return this.on(type, this.#createOnceListener(type, listener)); + return this.on(type, this.#createOnceListener(type, listener), order); } off(type: EventType, listener: EventListener & { [ONCE]?: true }): this { @@ -54,22 +57,19 @@ export abstract class ReadonlyEmitterBase< } const listeners = this._listeners.get(type) || []; - const index = listeners.indexOf(listener); + const index = listeners.findIndex(([l]) => l === listener); if (index !== -1) { listeners.splice(index, 1); } return this; } - protected _getListeners(type: EventType): Iterable { - const wildcard = this._listeners.get('*')!; - const named = this._listeners.get(type); - return named ? iterateSorted(this.#getListenerOrder, wildcard, named) : wildcard; - } - - #rememberListener(listener: T): T { - this.#listenersOrder.set(listener, this.#listenersCounter++); - return listener; + protected *_getListeners(type: EventType): Iterable { + const wildcard: [EventListener, number][] = this._listeners.get('*') ?? []; + const named: [EventListener, number][] = this._listeners.get(type) ?? []; + for (const [listener] of iterateSorted<[EventListener, number]>(getOrder, wildcard, named)) { + yield listener; + } } #createOnceListener(type: EventType, listener: EventListener) { @@ -81,7 +81,8 @@ export abstract class ReadonlyEmitterBase< onceListener[ONCE] = true as const; return onceListener; } +} - #getListenerOrder = (listener: EventListener): number => - this.#listenersOrder.get(listener) ?? Number.NaN; +function getOrder([_a, b]: [T, number]): number { + return b; } diff --git a/packages/library/src/utils/emitters/SemiAsyncEmitter.ts b/packages/library/src/utils/emitters/SemiAsyncEmitter.ts index 20ffef9..c84101d 100644 --- a/packages/library/src/utils/emitters/SemiAsyncEmitter.ts +++ b/packages/library/src/utils/emitters/SemiAsyncEmitter.ts @@ -17,12 +17,12 @@ export class SemiAsyncEmitter< this.#syncEvents = new Set(syncEvents); } - on(type: EventType, listener: (event: Event) => unknown): this { - return this.#invoke('on', type, listener); + on(type: EventType, listener: (event: Event) => unknown, order?: number): this { + return this.#invoke('on', type, listener, order); } - once(type: EventType, listener: (event: Event) => unknown): this { - return this.#invoke('once', type, listener); + once(type: EventType, listener: (event: Event) => unknown, order?: number): this { + return this.#invoke('once', type, listener, order); } off(type: EventType, listener: (event: Event) => unknown): this { @@ -39,15 +39,16 @@ export class SemiAsyncEmitter< methodName: 'on' | 'once' | 'off', type: EventType, listener: (event: Event) => unknown, + order?: number, ): this { const isSync = this.#syncEvents.has(type); if (type === '*' || isSync) { - this.#syncEmitter[methodName](type, listener); + this.#syncEmitter[methodName](type, listener, order); } if (type === '*' || !isSync) { - this.#asyncEmitter[methodName](type, listener as (event: Event) => Promise); + this.#asyncEmitter[methodName](type, listener as (event: Event) => Promise, order); } return this;