Skip to content

Commit 9d20176

Browse files
authored
feat: allow chaining of listener methods (#16)
1 parent 140d4a6 commit 9d20176

File tree

6 files changed

+79
-145
lines changed

6 files changed

+79
-145
lines changed

src/index.ts

Lines changed: 50 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,12 @@ type InternalListenersMap<
8484
>
8585

8686
export type TypedListenerOptions = {
87+
once?: boolean
8788
signal?: AbortSignal
8889
}
8990

91+
const kListenerOptions = Symbol('kListenerOptions')
92+
9093
export namespace Emitter {
9194
/**
9295
* Returns an appropriate `Event` type for the given event type.
@@ -145,13 +148,9 @@ export namespace Emitter {
145148

146149
export class Emitter<EventMap extends DefaultEventMap> {
147150
#listeners: InternalListenersMap<typeof this, EventMap>
148-
#listenerOptions: WeakMap<Function, AddEventListenerOptions>
149-
#abortControllers: WeakMap<Function, AbortController>
150151

151152
constructor() {
152153
this.#listeners = {} as InternalListenersMap<typeof this, EventMap>
153-
this.#listenerOptions = new WeakMap()
154-
this.#abortControllers = new WeakMap()
155154
}
156155

157156
/**
@@ -163,17 +162,8 @@ export class Emitter<EventMap extends DefaultEventMap> {
163162
type: EventType,
164163
listener: Emitter.ListenerType<typeof this, EventType, EventMap>,
165164
options?: TypedListenerOptions,
166-
): AbortController {
167-
this.#addListener(type, listener)
168-
169-
const abortController = this.#createAbortController(type, listener)
170-
this.#listenerOptions.set(listener, {
171-
signal: options?.signal
172-
? AbortSignal.any([abortController.signal, options.signal])
173-
: abortController.signal,
174-
})
175-
176-
return abortController
165+
): typeof this {
166+
return this.#addListener(type, listener, options)
177167
}
178168

179169
/**
@@ -184,19 +174,9 @@ export class Emitter<EventMap extends DefaultEventMap> {
184174
public once<EventType extends keyof EventMap & string>(
185175
type: EventType,
186176
listener: Emitter.ListenerType<typeof this, EventType, EventMap>,
187-
options?: TypedListenerOptions,
188-
): AbortController {
189-
this.#addListener(type, listener)
190-
191-
const abortController = this.#createAbortController(type, listener)
192-
this.#listenerOptions.set(listener, {
193-
once: true,
194-
signal: options?.signal
195-
? AbortSignal.any([abortController.signal, options.signal])
196-
: abortController.signal,
197-
})
198-
199-
return abortController
177+
options?: Omit<TypedListenerOptions, 'once'>,
178+
): typeof this {
179+
return this.on(type, listener, { ...(options || {}), once: true })
200180
}
201181

202182
/**
@@ -208,21 +188,8 @@ export class Emitter<EventMap extends DefaultEventMap> {
208188
type: EventType,
209189
listener: Emitter.ListenerType<typeof this, EventType, EventMap>,
210190
options?: TypedListenerOptions,
211-
): AbortController {
212-
if (!this.#listeners[type]) {
213-
this.#listeners[type] = []
214-
}
215-
216-
this.#listeners[type].unshift(listener)
217-
218-
const abortController = this.#createAbortController(type, listener)
219-
this.#listenerOptions.set(listener, {
220-
signal: options?.signal
221-
? AbortSignal.any([abortController.signal, options.signal])
222-
: abortController.signal,
223-
})
224-
225-
return abortController
191+
): typeof this {
192+
return this.#addListener(type, listener, options, 'prepend')
226193
}
227194

228195
/**
@@ -231,19 +198,9 @@ export class Emitter<EventMap extends DefaultEventMap> {
231198
public earlyOnce<EventType extends keyof EventMap & string>(
232199
type: EventType,
233200
listener: Emitter.ListenerType<typeof this, EventType, EventMap>,
234-
options?: TypedListenerOptions,
235-
): AbortController {
236-
this.earlyOn(type, listener)
237-
238-
const abortController = this.#createAbortController(type, listener)
239-
this.#listenerOptions.set(listener, {
240-
once: true,
241-
signal: options?.signal
242-
? AbortSignal.any([abortController.signal, options.signal])
243-
: abortController.signal,
244-
})
245-
246-
return abortController
201+
options?: Omit<TypedListenerOptions, 'once'>,
202+
): typeof this {
203+
return this.earlyOn(type, listener, { ...(options || {}), once: true })
247204
}
248205

249206
/**
@@ -272,11 +229,7 @@ export class Emitter<EventMap extends DefaultEventMap> {
272229
break
273230
}
274231

275-
if (this.#listenerOptions.get(listener).signal.aborted) {
276-
continue
277-
}
278-
279-
this.#callListener(listener, proxiedEvent.event)
232+
this.#callListener(proxiedEvent.event, listener)
280233
}
281234

282235
proxiedEvent.revoke()
@@ -318,13 +271,9 @@ export class Emitter<EventMap extends DefaultEventMap> {
318271
break
319272
}
320273

321-
if (this.#listenerOptions.get(listener)?.signal?.aborted) {
322-
continue
323-
}
324-
325274
pendingListeners.push(
326275
// Awaiting individual listeners guarantees their call order.
327-
await Promise.resolve(this.#callListener(listener, proxiedEvent.event)),
276+
await Promise.resolve(this.#callListener(proxiedEvent.event, listener)),
328277
)
329278
}
330279

@@ -363,11 +312,7 @@ export class Emitter<EventMap extends DefaultEventMap> {
363312
break
364313
}
365314

366-
if (this.#listenerOptions.get(listener)?.signal?.aborted) {
367-
continue
368-
}
369-
370-
yield this.#callListener(listener, proxiedEvent.event)
315+
yield this.#callListener(proxiedEvent.event, listener)
371316
}
372317

373318
proxiedEvent.revoke()
@@ -389,13 +334,9 @@ export class Emitter<EventMap extends DefaultEventMap> {
389334
> = []
390335

391336
for (const existingListener of this.#listeners[type]) {
392-
if (existingListener === listener) {
393-
this.#listenerOptions.delete(existingListener)
394-
this.#abortControllers.delete(existingListener)
395-
continue
337+
if (existingListener !== listener) {
338+
nextListeners.push(existingListener)
396339
}
397-
398-
nextListeners.push(existingListener)
399340
}
400341

401342
this.#listeners[type] = nextListeners
@@ -410,8 +351,6 @@ export class Emitter<EventMap extends DefaultEventMap> {
410351
): void {
411352
if (type == null) {
412353
this.#listeners = {} as InternalListenersMap<typeof this>
413-
this.#listenerOptions = new WeakMap()
414-
this.#abortControllers = new WeakMap()
415354
return
416355
}
417356

@@ -445,12 +384,36 @@ export class Emitter<EventMap extends DefaultEventMap> {
445384
#addListener<EventType extends keyof EventMap & string>(
446385
type: EventType,
447386
listener: Emitter.ListenerType<typeof this, EventType, EventMap>,
448-
) {
449-
if (!this.#listeners[type]) {
450-
this.#listeners[type] = []
387+
options: TypedListenerOptions | undefined,
388+
insertMode: 'append' | 'prepend' = 'append',
389+
): typeof this {
390+
this.#listeners[type] ??= []
391+
392+
if (insertMode === 'prepend') {
393+
this.#listeners[type].unshift(listener)
394+
} else {
395+
this.#listeners[type].push(listener)
396+
}
397+
398+
if (options) {
399+
Object.defineProperty(listener, kListenerOptions, {
400+
value: options,
401+
enumerable: false,
402+
writable: false,
403+
})
404+
405+
if (options.signal) {
406+
options.signal.addEventListener(
407+
'abort',
408+
() => {
409+
this.removeListener(type, listener)
410+
},
411+
{ once: true },
412+
)
413+
}
451414
}
452415

453-
this.#listeners[type].push(listener)
416+
return this
454417
}
455418

456419
#proxyEvent<Event extends TypedEvent>(
@@ -474,31 +437,17 @@ export class Emitter<EventMap extends DefaultEventMap> {
474437
}
475438

476439
#callListener<EventType extends keyof EventMap & string>(
477-
listener: Emitter.ListenerType<typeof this, EventType, EventMap>,
478440
event: Event,
441+
listener: Emitter.ListenerType<typeof this, EventType, EventMap> & {
442+
[kListenerOptions]?: TypedListenerOptions
443+
},
479444
) {
480445
const returnValue = listener.call(this, event)
481446

482-
if (this.#listenerOptions.get(listener)?.once) {
447+
if (listener[kListenerOptions]?.once) {
483448
this.removeListener(event.type, listener)
484449
}
485450

486451
return returnValue
487452
}
488-
489-
#createAbortController<EventType extends keyof EventMap & string>(
490-
type: EventType,
491-
listener: Emitter.ListenerType<typeof this, EventType, EventMap>,
492-
): AbortController {
493-
const abortController = new AbortController()
494-
495-
// Since we are emitting events manually, aborting the controller
496-
// won't do anything by itself. We need to teach the class what to do.
497-
abortController.signal.addEventListener('abort', () => {
498-
this.removeListener(type, listener)
499-
})
500-
501-
this.#abortControllers.set(listener, abortController)
502-
return abortController
503-
}
504453
}

test/event-abort.test.ts

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,11 @@
11
import { Emitter, TypedEvent } from '#src/index.js'
22

3-
it('(on) supports aborting a listener by calling `abort()` on its controller', () => {
4-
const listener = vi.fn()
5-
const emitter = new Emitter<{ greeting: TypedEvent<string> }>()
6-
const controller = emitter.on('greeting', listener)
7-
8-
controller.abort()
9-
emitter.emit(new TypedEvent('greeting', { data: 'John' }))
10-
11-
expect(listener).not.toHaveBeenCalled()
12-
})
13-
143
it('(on) supports aborting a listener by providing it a custom `AbortController`', () => {
154
const listener = vi.fn()
165
const emitter = new Emitter<{ greeting: TypedEvent<string> }>()
176
const controller = new AbortController()
18-
emitter.on('greeting', listener, { signal: controller.signal })
197

20-
controller.abort()
21-
emitter.emit(new TypedEvent('greeting', { data: 'John' }))
22-
23-
expect(listener).not.toHaveBeenCalled()
24-
})
25-
26-
it('(once) supports aborting a listener by calling `abort()` on its controller', () => {
27-
const listener = vi.fn()
28-
const emitter = new Emitter<{ greeting: TypedEvent<string> }>()
29-
const controller = emitter.once('greeting', listener)
8+
emitter.on('greeting', listener, { signal: controller.signal })
309

3110
controller.abort()
3211
emitter.emit(new TypedEvent('greeting', { data: 'John' }))
@@ -46,17 +25,6 @@ it('(once) supports aborting a listener by providing it a custom `AbortControlle
4625
expect(listener).not.toHaveBeenCalled()
4726
})
4827

49-
it('(earlyOn) supports aborting a listener by calling `abort()` on its controller', () => {
50-
const listener = vi.fn()
51-
const emitter = new Emitter<{ greeting: TypedEvent<string> }>()
52-
const controller = emitter.earlyOn('greeting', listener)
53-
54-
controller.abort()
55-
emitter.emit(new TypedEvent('greeting', { data: 'John' }))
56-
57-
expect(listener).not.toHaveBeenCalled()
58-
})
59-
6028
it('(earlyOn) supports aborting a listener by providing it a custom `AbortController`', () => {
6129
const listener = vi.fn()
6230
const emitter = new Emitter<{ greeting: TypedEvent<string> }>()
@@ -69,17 +37,6 @@ it('(earlyOn) supports aborting a listener by providing it a custom `AbortContro
6937
expect(listener).not.toHaveBeenCalled()
7038
})
7139

72-
it('(earlyOnce) supports aborting a listener by calling `abort()` on its controller', () => {
73-
const listener = vi.fn()
74-
const emitter = new Emitter<{ greeting: TypedEvent<string> }>()
75-
const controller = emitter.earlyOnce('greeting', listener)
76-
77-
controller.abort()
78-
emitter.emit(new TypedEvent('greeting', { data: 'John' }))
79-
80-
expect(listener).not.toHaveBeenCalled()
81-
})
82-
8340
it('(earlyOnce) supports aborting a listener by providing it a custom `AbortController`', () => {
8441
const listener = vi.fn()
8542
const emitter = new Emitter<{ greeting: TypedEvent<string> }>()

test/typings/early-on.test-d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,10 @@ it('infers custom event type with a custom data type', () => {
7373
expectTypeOf(event.id).toBeString()
7474
})
7575
})
76+
77+
it('returns the emitter reference', () => {
78+
const emitter = new Emitter<{ greeting: TypedEvent }>()
79+
const returnValue = emitter.earlyOn('greeting', () => void 0)
80+
81+
expectTypeOf(returnValue).toEqualTypeOf<typeof emitter>()
82+
})

test/typings/early-once.test-d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,10 @@ it('infers custom event type with a custom data type', () => {
7373
expectTypeOf(event.id).toBeString()
7474
})
7575
})
76+
77+
it('returns the emitter reference', () => {
78+
const emitter = new Emitter<{ greeting: TypedEvent }>()
79+
const returnValue = emitter.earlyOnce('greeting', () => void 0)
80+
81+
expectTypeOf(returnValue).toEqualTypeOf<typeof emitter>()
82+
})

test/typings/on.test-d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,10 @@ it('infers custom event type with a custom data type', () => {
7373
expectTypeOf(event.id).toBeString()
7474
})
7575
})
76+
77+
it('returns the emitter reference', () => {
78+
const emitter = new Emitter<{ greeting: TypedEvent }>()
79+
const returnValue = emitter.on('greeting', () => void 0)
80+
81+
expectTypeOf(returnValue).toEqualTypeOf<typeof emitter>()
82+
})

test/typings/once.test-d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,10 @@ it('infers custom event type with a custom data type', () => {
7373
expectTypeOf(event.id).toBeString()
7474
})
7575
})
76+
77+
it('returns the emitter reference', () => {
78+
const emitter = new Emitter<{ greeting: TypedEvent }>()
79+
const returnValue = emitter.once('greeting', () => void 0)
80+
81+
expectTypeOf(returnValue).toEqualTypeOf<typeof emitter>()
82+
})

0 commit comments

Comments
 (0)