Skip to content

Commit

Permalink
fix: event emitter listener order (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
noomorph authored Sep 13, 2023
1 parent 127d49c commit c770855
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 27 deletions.
29 changes: 28 additions & 1 deletion packages/library/src/environment-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,34 @@ export function WithMetadata<E extends JestEnvironmentLike>(
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<ForwardedCircusEvent> {
Expand Down
4 changes: 2 additions & 2 deletions packages/library/src/types/Emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export interface ReadonlyAsyncEmitter<
Event extends { type: string },
EventType = Event['type'] | '*',
> {
on(type: EventType, listener: (event: Event) => void | Promise<void>): this;
once(type: EventType, listener: (event: Event) => void | Promise<void>): this;
on(type: EventType, listener: (event: Event) => void | Promise<void>, weight?: number): this;
once(type: EventType, listener: (event: Event) => void | Promise<void>, weight?: number): this;
off(type: EventType, listener: (event: Event) => void | Promise<void>): this;
}

Expand Down
37 changes: 19 additions & 18 deletions packages/library/src/utils/emitters/ReadonlyEmitterBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,34 @@ export abstract class ReadonlyEmitterBase<
> implements ReadonlyEmitter<Event, EventType | '*'>
{
protected readonly _log: typeof logger;
protected readonly _listeners: Map<EventType | '*', EventListener[]> = new Map();
protected readonly _listeners: Map<EventType | '*', [EventListener, number][]> = new Map();

#listenersCounter = 0;
readonly #listenersOrder = new WeakMap<EventListener, number>();

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})`);
}

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 {
Expand All @@ -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<EventListener> {
const wildcard = this._listeners.get('*')!;
const named = this._listeners.get(type);
return named ? iterateSorted(this.#getListenerOrder, wildcard, named) : wildcard;
}

#rememberListener<T extends EventListener>(listener: T): T {
this.#listenersOrder.set(listener, this.#listenersCounter++);
return listener;
protected *_getListeners(type: EventType): Iterable<EventListener> {
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) {
Expand All @@ -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<T>([_a, b]: [T, number]): number {
return b;
}
13 changes: 7 additions & 6 deletions packages/library/src/utils/emitters/SemiAsyncEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<void>);
this.#asyncEmitter[methodName](type, listener as (event: Event) => Promise<void>, order);
}

return this;
Expand Down

0 comments on commit c770855

Please sign in to comment.