Skip to content

Commit e7c5bad

Browse files
authored
fix: support .stopPropagation() and .stopImmediatePropagation() (#5)
1 parent 7b6bf7f commit e7c5bad

File tree

5 files changed

+260
-92
lines changed

5 files changed

+260
-92
lines changed

src/index.ts

+168-43
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,13 @@ export namespace Emitter {
8787
Target extends Emitter<any>,
8888
Type extends keyof EventMap & string,
8989
EventMap extends DefaultEventMap = InferEventMap<Target>,
90-
> = DataToEvent<Type, EventMap[Type][0]>
90+
> = DataToEvent<Type, Emitter.EventDataType<Target, Type, EventMap>>
91+
92+
export type EventDataType<
93+
Target extends Emitter<any>,
94+
Type extends keyof EventMap & string,
95+
EventMap extends DefaultEventMap = InferEventMap<Target>,
96+
> = EventMap[Type][0]
9197
}
9298

9399
export class Emitter<EventMap extends DefaultEventMap = {}> {
@@ -196,66 +202,124 @@ export class Emitter<EventMap extends DefaultEventMap = {}> {
196202
* @returns {boolean} Returns `true` if the event had any listeners, `false` otherwise.
197203
*/
198204
public emit<Type extends keyof EventMap & string>(
199-
...args: EventMap[Type][0] extends [never]
205+
event: Emitter.EventType<typeof this, Type, EventMap>,
206+
): boolean
207+
public emit<Type extends keyof EventMap & string>(
208+
...args: Emitter.EventDataType<typeof this, Type, EventMap> extends [never]
200209
? [type: Type]
201-
: [type: Type, data: EventMap[Type][0]]
210+
: [type: Type, data: Emitter.EventDataType<typeof this, Type, EventMap>]
211+
): boolean
212+
213+
public emit<Type extends keyof EventMap & string>(
214+
...args:
215+
| [Emitter.EventType<typeof this, Type, EventMap>]
216+
| (Emitter.EventDataType<typeof this, Type, EventMap> extends [never]
217+
? [type: Type]
218+
: [
219+
type: Type,
220+
data: Emitter.EventDataType<typeof this, Type, EventMap>,
221+
])
202222
): boolean {
203-
if (!this.#listeners[args[0]] || this.#listeners[args[0]].length === 0) {
223+
const [eventOrType, data] = args
224+
const isEvent = eventOrType instanceof Event
225+
const type = isEvent ? eventOrType.type : eventOrType
226+
227+
if (!this.#listeners[type] || this.#listeners[type].length === 0) {
204228
return false
205229
}
206230

207-
const event = this.#createEventForData(args[0], args[1])
231+
const event = isEvent
232+
? eventOrType
233+
: this.createEvent.call(this, type, data)
208234

209-
for (const listener of this.#listeners[args[0]]) {
210-
if (this.#listenerOptions.get(listener).signal.aborted) {
211-
continue
235+
for (const listener of this.#listeners[type]) {
236+
if (
237+
event[kPropagationStopped] != null &&
238+
event[kPropagationStopped] !== this
239+
) {
240+
return false
212241
}
213242

214243
if (event[kImmediatePropagationStopped]) {
215244
break
216245
}
217246

247+
if (this.#listenerOptions.get(listener).signal.aborted) {
248+
continue
249+
}
250+
218251
this.#callListener(listener, event)
219252
}
220253

221-
this.#eventsCache.delete([args[0], args[1]])
254+
this.#eventsCache.delete([type, data])
222255

223256
return true
224257
}
225258

226259
/**
227-
* Emits the given event and returns a Promise that resolves
228-
* with the array of listener results, or rejects as soon as any
229-
* of the listeners throw. Listeners are still called synchronously
230-
* to guarantee call order and prevent race conditions.
260+
* Emits the given event and returns a Promise that always resolves
261+
* with the array of listener results (either fulfilled or rejected).
262+
* The listeners are still called synchronously to guarantee call order
263+
* and prevent race conditions.
231264
*/
232-
public async emitAsPromise<Type extends keyof EventMap & string>(
233-
...args: EventMap[Type][0] extends [never]
265+
public emitAsPromise<Type extends keyof EventMap & string>(
266+
event: Emitter.EventType<typeof this, Type, EventMap>,
267+
): Promise<Array<Emitter.ListenerReturnType<typeof this, Type, EventMap>>>
268+
public emitAsPromise<Type extends keyof EventMap & string>(
269+
...args: Emitter.EventDataType<typeof this, Type, EventMap> extends [never]
234270
? [type: Type]
235-
: [type: Type, data: EventMap[Type][0]]
271+
: [type: Type, data: Emitter.EventDataType<typeof this, Type, EventMap>]
272+
): Promise<Array<Emitter.ListenerReturnType<typeof this, Type, EventMap>>>
273+
274+
public async emitAsPromise<Type extends keyof EventMap & string>(
275+
...args:
276+
| [Emitter.EventType<typeof this, Type, EventMap>]
277+
| (Emitter.EventDataType<typeof this, Type, EventMap> extends [never]
278+
? [type: Type]
279+
: [
280+
type: Type,
281+
data: Emitter.EventDataType<typeof this, Type, EventMap>,
282+
])
236283
): Promise<Array<Emitter.ListenerReturnType<typeof this, Type, EventMap>>> {
237-
if (!this.#listeners[args[0]] || this.#listeners[args[0]].length === 0) {
284+
const [eventOrType, data] = args
285+
const isEvent = eventOrType instanceof Event
286+
const type = isEvent ? eventOrType.type : eventOrType
287+
288+
if (!this.#listeners[type] || this.#listeners[type].length === 0) {
238289
return []
239290
}
240291

241-
const event = this.#createEventForData(args[0], args[1])
242-
const pendingListeners: Array<Promise<unknown>> = []
292+
const event = isEvent
293+
? eventOrType
294+
: this.createEvent.call(this, type, data)
243295

244-
for (const listener of this.#listeners[args[0]]) {
245-
if (this.#listenerOptions.get(listener).signal.aborted) {
246-
continue
296+
const pendingListeners: Array<
297+
Promise<Emitter.ListenerReturnType<typeof this, Type, EventMap>>
298+
> = []
299+
300+
for (const listener of this.#listeners[type]) {
301+
if (
302+
event[kPropagationStopped] != null &&
303+
event[kPropagationStopped] !== this
304+
) {
305+
return []
247306
}
248307

249308
if (event[kImmediatePropagationStopped]) {
250309
break
251310
}
252311

312+
if (this.#listenerOptions.get(listener).signal.aborted) {
313+
continue
314+
}
315+
253316
pendingListeners.push(
317+
// Awaiting individual listeners guarantees their call order.
254318
await Promise.resolve(this.#callListener(listener, event)),
255319
)
256320
}
257321

258-
this.#eventsCache.delete([args[0], args[1]])
322+
this.#eventsCache.delete([type, data])
259323

260324
return Promise.allSettled(pendingListeners).then((results) => {
261325
return results.map((result) =>
@@ -266,33 +330,60 @@ export class Emitter<EventMap extends DefaultEventMap = {}> {
266330

267331
/**
268332
* Emits the given event and returns a generator that yields
269-
* the result of each listener. This way, you stop exhausting
333+
* the result of each listener in order. This way, you stop exhausting
270334
* the listeners once you get the expected value.
271335
*/
272-
public *emitAsGenerator<Type extends keyof EventMap & string>(
273-
...args: EventMap[Type][0] extends [never]
336+
public emitAsGenerator<Type extends keyof EventMap & string>(
337+
event: Emitter.EventType<typeof this, Type, EventMap>,
338+
): Generator<Emitter.ListenerReturnType<typeof this, Type, EventMap>>
339+
public emitAsGenerator<Type extends keyof EventMap & string>(
340+
...args: Emitter.EventDataType<typeof this, Type, EventMap> extends [never]
274341
? [type: Type]
275-
: [type: Type, data: EventMap[Type][0]]
276-
): Generator<unknown> {
277-
if (!this.#listeners[args[0]] || this.#listeners[args[0]].length === 0) {
342+
: [type: Type, data: Emitter.EventDataType<typeof this, Type, EventMap>]
343+
): Generator<Emitter.ListenerReturnType<typeof this, Type, EventMap>>
344+
345+
public *emitAsGenerator<Type extends keyof EventMap & string>(
346+
...args:
347+
| [event: Emitter.EventType<typeof this, Type, EventMap>]
348+
| (Emitter.EventDataType<typeof this, Type, EventMap> extends [never]
349+
? [type: Type]
350+
: [
351+
type: Type,
352+
data: Emitter.EventDataType<typeof this, Type, EventMap>,
353+
])
354+
): Generator<Emitter.ListenerReturnType<typeof this, Type, EventMap>> {
355+
const [eventOrType, data] = args
356+
const isEvent = eventOrType instanceof Event
357+
const type = isEvent ? eventOrType.type : eventOrType
358+
359+
if (!this.#listeners[type] || this.#listeners[type].length === 0) {
278360
return
279361
}
280362

281-
const event = this.#createEventForData(args[0], args[1])
363+
const event = isEvent
364+
? eventOrType
365+
: this.createEvent.call(this, type, data)
282366

283-
for (const listener of this.#listeners[args[0]]) {
284-
if (this.#listenerOptions.get(listener).signal.aborted) {
285-
continue
367+
for (const listener of this.#listeners[type]) {
368+
if (
369+
event[kPropagationStopped] != null &&
370+
event[kPropagationStopped] !== this
371+
) {
372+
return
286373
}
287374

288375
if (event[kImmediatePropagationStopped]) {
289376
break
290377
}
291378

379+
if (this.#listenerOptions.get(listener).signal.aborted) {
380+
continue
381+
}
382+
292383
yield this.#callListener(listener, event)
293384
}
294385

295-
this.#eventsCache.delete([args[0], args[1]])
386+
this.#eventsCache.delete([type, data])
296387
}
297388

298389
/**
@@ -377,27 +468,61 @@ export class Emitter<EventMap extends DefaultEventMap = {}> {
377468
this.#listeners[type].push(listener)
378469
}
379470

380-
#createEventForData<Type extends keyof EventMap & string>(
381-
type: Type,
382-
data: EventMap[Type][0],
383-
): Event {
471+
public createEvent<
472+
Type extends keyof EventMap & string,
473+
EmitterEvent extends Emitter.EventType<
474+
typeof this,
475+
Type,
476+
EventMap
477+
> = Emitter.EventType<typeof this, Type, EventMap>,
478+
>(
479+
...args: Emitter.EventDataType<typeof this, Type, EventMap> extends [never]
480+
? [type: Type]
481+
: [type: Type, data: Emitter.EventDataType<typeof this, Type, EventMap>]
482+
): EmitterEvent {
483+
const [type, data] = args
384484
const cachedEvent = this.#eventsCache.get([type, data])
385485

386486
if (cachedEvent) {
387-
return cachedEvent
487+
return cachedEvent as EmitterEvent
388488
}
389489

390490
let event =
391491
data == null
392-
? new Event(type, { cancelable: true })
393-
: new MessageEvent(type, { data, cancelable: true })
492+
? (new Event(type, { cancelable: true }) as EmitterEvent)
493+
: (new MessageEvent(type, { data, cancelable: true }) as EmitterEvent)
394494

395495
Object.defineProperties(event, {
496+
defaultPrevented: {
497+
enumerable: false,
498+
writable: true,
499+
value: false,
500+
},
501+
preventDefault: {
502+
enumerable: false,
503+
value: new Proxy(event.preventDefault, {
504+
apply: (target, thisArg, argArray) => {
505+
/**
506+
* @note Node.js 18 does NOT update the `defaultPrevented` value
507+
* when you call `preventDefault()`. This is a bug in Node.js.
508+
*
509+
* @fixme Remove this hack when Node.js 20 is the minimal version.
510+
*/
511+
Reflect.set(event, 'defaultPrevented', true)
512+
return Reflect.apply(target, thisArg, argArray)
513+
},
514+
}),
515+
},
396516
stopPropagation: {
397517
enumerable: false,
398518
value: new Proxy(event.stopPropagation, {
399-
apply(target, thisArg, argArray) {
400-
event[kPropagationStopped] = true
519+
apply: (target, thisArg, argArray) => {
520+
/**
521+
* @note Propagation is also stopped when the immediate propagation is stopped.
522+
* Because of that, store the reference to the Emitter instance that stopped it.
523+
* (Node.js makes `thisArg` to be the `Event` that stops it).
524+
*/
525+
event[kPropagationStopped] = this
401526
return Reflect.apply(target, thisArg, argArray)
402527
},
403528
}),

0 commit comments

Comments
 (0)