1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48class Emitter {
#listeners = new Map();
on(event, fn) {
if (typeof fn !== 'function') throw new TypeError('Listener must be a function.');
if (!this.#listeners.has(event)) this.#listeners.set(event, new Set());
this.#listeners.get(event).add(fn);
return () => this.off(event, fn);
}
off(event, fn) {
this.#listeners.get(event)?.delete(fn);
return this;
}
emit(event, payload) {
for (const fn of [...(this.#listeners.get(event) ?? [])]) {
try { fn(payload); } catch (err) { console.log('listener threw:', err.message); }
}
return this;
}
get eventNames() { return [...this.#listeners.keys()]; }
}
class Trip extends Emitter {
#sold = new Set();
constructor(route, capacity) { super(); this.route = route; this.capacity = capacity; }
book(seat) {
this.#sold.add(seat);
this.emit('booked', { seat, remaining: this.capacity - this.#sold.size });
if (this.#sold.size === this.capacity) this.emit('soldout', { route: this.route });
return this;
}
}
const trip = new Trip('Lagos - Abuja', 3);
const stopAudit = trip.on('booked', d => console.log('audit:', d.seat));
trip.on('booked', d => console.log('display:', d.remaining, 'left'));
trip.on('soldout', d => console.log('FULLY BOOKED:', d.route));
trip.on('booked', () => { throw new Error('a badly written listener'); });
trip.book('D11');
stopAudit();
trip.book('D12');
trip.book('D13');
console.log('events registered:', trip.eventNames);
Copying the listener set before iterating, and catching listener errors, are the two details that make an emitter safe in real use.