Skip to content

Commit

Permalink
fix: add "once", "off", and "removeAllListeners" to the Interceptor c…
Browse files Browse the repository at this point in the history
…lass (#427)
  • Loading branch information
kettanaito authored Sep 18, 2023
1 parent e0dc5a8 commit 04152ed
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 11 deletions.
132 changes: 132 additions & 0 deletions src/BatchInterceptor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,135 @@ it('disposes of child interceptors', async () => {
expect(primaryDisposeSpy).toHaveBeenCalledTimes(1)
expect(secondaryDisposeSpy).toHaveBeenCalledTimes(1)
})

it('forwards listeners added via "on()"', () => {
class FirstInterceptor extends Interceptor<any> {
constructor() {
super(Symbol('first'))
}
}
class SecondaryInterceptor extends Interceptor<any> {
constructor() {
super(Symbol('second'))
}
}

const firstInterceptor = new FirstInterceptor()
const secondInterceptor = new SecondaryInterceptor()

const interceptor = new BatchInterceptor({
name: 'batch',
interceptors: [firstInterceptor, secondInterceptor],
})

const listener = vi.fn()
interceptor.on('foo', listener)

expect(firstInterceptor['emitter'].listenerCount('foo')).toBe(1)
expect(secondInterceptor['emitter'].listenerCount('foo')).toBe(1)
expect(interceptor['emitter'].listenerCount('foo')).toBe(0)
})

it('forwards listeners removal via "off()"', () => {
class FirstInterceptor extends Interceptor<any> {
constructor() {
super(Symbol('first'))
}
}
class SecondaryInterceptor extends Interceptor<any> {
constructor() {
super(Symbol('second'))
}
}

const firstInterceptor = new FirstInterceptor()
const secondInterceptor = new SecondaryInterceptor()

const interceptor = new BatchInterceptor({
name: 'batch',
interceptors: [firstInterceptor, secondInterceptor],
})

const listener = vi.fn()
interceptor.on('foo', listener)
interceptor.off('foo', listener)

expect(firstInterceptor['emitter'].listenerCount('foo')).toBe(0)
expect(secondInterceptor['emitter'].listenerCount('foo')).toBe(0)
})

it('forwards removal of all listeners by name via ".removeAllListeners()"', () => {
class FirstInterceptor extends Interceptor<any> {
constructor() {
super(Symbol('first'))
}
}
class SecondaryInterceptor extends Interceptor<any> {
constructor() {
super(Symbol('second'))
}
}

const firstInterceptor = new FirstInterceptor()
const secondInterceptor = new SecondaryInterceptor()

const interceptor = new BatchInterceptor({
name: 'batch',
interceptors: [firstInterceptor, secondInterceptor],
})

const listener = vi.fn()
interceptor.on('foo', listener)
interceptor.on('foo', listener)
interceptor.on('bar', listener)

expect(firstInterceptor['emitter'].listenerCount('foo')).toBe(2)
expect(secondInterceptor['emitter'].listenerCount('foo')).toBe(2)
expect(firstInterceptor['emitter'].listenerCount('bar')).toBe(1)
expect(secondInterceptor['emitter'].listenerCount('bar')).toBe(1)

interceptor.removeAllListeners('foo')

expect(firstInterceptor['emitter'].listenerCount('foo')).toBe(0)
expect(secondInterceptor['emitter'].listenerCount('foo')).toBe(0)
expect(firstInterceptor['emitter'].listenerCount('bar')).toBe(1)
expect(secondInterceptor['emitter'].listenerCount('bar')).toBe(1)
})

it('forwards removal of all listeners via ".removeAllListeners()"', () => {
class FirstInterceptor extends Interceptor<any> {
constructor() {
super(Symbol('first'))
}
}
class SecondaryInterceptor extends Interceptor<any> {
constructor() {
super(Symbol('second'))
}
}

const firstInterceptor = new FirstInterceptor()
const secondInterceptor = new SecondaryInterceptor()

const interceptor = new BatchInterceptor({
name: 'batch',
interceptors: [firstInterceptor, secondInterceptor],
})

const listener = vi.fn()
interceptor.on('foo', listener)
interceptor.on('foo', listener)
interceptor.on('bar', listener)

expect(firstInterceptor['emitter'].listenerCount('foo')).toBe(2)
expect(secondInterceptor['emitter'].listenerCount('foo')).toBe(2)
expect(firstInterceptor['emitter'].listenerCount('bar')).toBe(1)
expect(secondInterceptor['emitter'].listenerCount('bar')).toBe(1)

interceptor.removeAllListeners()

expect(firstInterceptor['emitter'].listenerCount('foo')).toBe(0)
expect(secondInterceptor['emitter'].listenerCount('foo')).toBe(0)
expect(firstInterceptor['emitter'].listenerCount('bar')).toBe(0)
expect(secondInterceptor['emitter'].listenerCount('bar')).toBe(0)
})
40 changes: 37 additions & 3 deletions src/BatchInterceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,45 @@ export class BatchInterceptor<
public on<EventName extends ExtractEventNames<Events>>(
event: EventName,
listener: Listener<Events[EventName]>
) {
): this {
// Instead of adding a listener to the batch interceptor,
// propagate the listener to each of the individual interceptors.
this.interceptors.forEach((interceptor) => {
for (const interceptor of this.interceptors) {
interceptor.on(event, listener)
})
}

return this
}

public once<EventName extends ExtractEventNames<Events>>(
event: EventName,
listener: Listener<Events[EventName]>
): this {
for (const interceptor of this.interceptors) {
interceptor.once(event, listener)
}

return this
}

public off<EventName extends ExtractEventNames<Events>>(
event: EventName,
listener: Listener<Events[EventName]>
): this {
for (const interceptor of this.interceptors) {
interceptor.off(event, listener)
}

return this
}

public removeAllListeners<EventName extends ExtractEventNames<Events>>(
event?: EventName | undefined
): this {
for (const interceptors of this.interceptors) {
interceptors.removeAllListeners(event)
}

return this
}
}
25 changes: 25 additions & 0 deletions src/Interceptor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,31 @@ it('does not set a maximum listeners limit', () => {
expect(interceptor['emitter'].getMaxListeners()).toBe(0)
})

describe('on()', () => {
it('adds a new listener using "on()"', () => {
const interceptor = new Interceptor(symbol)
expect(interceptor['emitter'].listenerCount('event')).toBe(0)

const listener = vi.fn()
interceptor.on('event', listener)
expect(interceptor['emitter'].listenerCount('event')).toBe(1)
})
})

describe('off()', () => {
it('removes a listener using "off()"', () => {
const interceptor = new Interceptor(symbol)
expect(interceptor['emitter'].listenerCount('event')).toBe(0)

const listener = vi.fn()
interceptor.on('event', listener)
expect(interceptor['emitter'].listenerCount('event')).toBe(1)

interceptor.off('event', listener)
expect(interceptor['emitter'].listenerCount('event')).toBe(0)
})
})

describe('persistence', () => {
it('stores global reference to the applied interceptor', () => {
const interceptor = new Interceptor(symbol)
Expand Down
42 changes: 37 additions & 5 deletions src/Interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export class Interceptor<Events extends InterceptorEventMap> {
runningInstance.emitter.removeListener(event, listener)
logger.info('removed proxied "%s" listener!', event)
})

return this
}

this.readyState = InterceptorReadyState.APPLIED
Expand Down Expand Up @@ -141,22 +143,52 @@ export class Interceptor<Events extends InterceptorEventMap> {
* Listen to the interceptor's public events.
*/
public on<EventName extends ExtractEventNames<Events>>(
eventName: EventName,
event: EventName,
listener: Listener<Events[EventName]>
): void {
): this {
const logger = this.logger.extend('on')

if (
this.readyState === InterceptorReadyState.DISPOSING ||
this.readyState === InterceptorReadyState.DISPOSED
) {
logger.info('cannot listen to events, already disposed!')
return
return this
}

logger.info('adding "%s" event listener:', eventName, listener.name)
logger.info('adding "%s" event listener:', event, listener.name)

this.emitter.on(event, listener)
return this
}

public once<EventName extends ExtractEventNames<Events>>(
event: EventName,
listener: Listener<Events[EventName]>
): this {
const logger = this.logger.extend('once')
logger.info('adding a one-time "%s" event listener:', event, listener.name)

this.emitter.once(event, listener)
return this
}

public off<EventName extends ExtractEventNames<Events>>(
event: EventName,
listener: Listener<Events[EventName]>
): this {
const logger = this.logger.extend('off')
logger.info('removing "%s" event listener:', event, listener.name)

this.emitter.off(event, listener)
return this
}

this.emitter.on(eventName, listener)
public removeAllListeners<EventName extends ExtractEventNames<Events>>(
event?: EventName
): this {
this.emitter.removeAllListeners(event)
return this
}

/**
Expand Down
29 changes: 26 additions & 3 deletions src/utils/AsyncEventEmitter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Logger } from '@open-draft/logger'
import { Emitter, EventMap, Listener } from 'strict-event-emitter'
import { nextTick } from './nextTick'
import { invariant } from 'outvariant'

export interface QueueItem<Args extends Array<unknown>> {
args: Args
Expand All @@ -17,13 +18,16 @@ export class AsyncEventEmitter<
> extends Emitter<Events> {
public readyState: AsyncEventEmitterReadyState

private logger: Logger
protected logger: Logger

protected wrappedListeners: Map<Function, Listener<any>>
protected queue: Map<keyof Events, Array<QueueItem<Events[any]>>>

constructor() {
super()

this.logger = new Logger('async-event-emitter')
this.wrappedListeners = new Map()
this.queue = new Map()

this.readyState = AsyncEventEmitterReadyState.ACTIVE
Expand All @@ -42,7 +46,7 @@ export class AsyncEventEmitter<
return this
}

return super.on(eventName, async (...args) => {
const wrappedListener: Listener<Events[EventName]> = async (...args) => {
// Event queue is always established when calling ".emit()".
const queue = this.openListenerQueue(eventName)

Expand All @@ -66,7 +70,13 @@ export class AsyncEventEmitter<
}
}),
})
})
}

// Associate the raw listener function with the wrapped listener
// to be able to remove this listener by the raw function reference.
this.wrappedListeners.set(listener, wrappedListener)

return super.on(eventName, wrappedListener)
}

public emit<EventName extends keyof Events>(
Expand Down Expand Up @@ -107,6 +117,19 @@ export class AsyncEventEmitter<
return super.emit(eventName, ...data)
}

public removeListener<EventName extends keyof Events>(
eventName: EventName,
listener: Listener<any>
): this {
const wrappedListener = this.wrappedListeners.get(listener)

if (!wrappedListener) {
return this
}

return super.removeListener(eventName, wrappedListener)
}

/**
* Returns a promise that resolves when all the listeners for the given event
* has been called. Awaits asynchronous listeners.
Expand Down

0 comments on commit 04152ed

Please sign in to comment.