This is the same pub/sub primitive as Event Emitter, with one change to the API. Instead of on/off, there is a single subscribe(eventName, callback) that returns a handle — a small object with a release() method. Calling release() removes that one subscription. There is no off(eventName, callback). The handle is the only way to unsubscribe.
The handle pattern shows up in RxJS, in DOM AbortController, and in most modern bus libraries because it sidesteps a real footgun in the on/off design: if the caller passes an inline arrow function to on, they no longer hold a reference equal to it and can never call off for it. A handle is unique by construction.
class EventEmitter {
subscribe(eventName, callback)
// returns { release: () => void }
emit(eventName, ...args)
// calls each live subscriber for eventName, in subscription order, with args
}
eventName is a string. callback is a function. release() returns nothing.
Basic subscribe + emit:
const bus = new EventEmitter();
const sub = bus.subscribe('login', (user) => console.log('hi', user));
bus.emit('login', 'ada'); // logs: hi ada
sub.release();
bus.emit('login', 'ada'); // no listeners — silent
Release one of several subscribers:
const bus = new EventEmitter();
const a = bus.subscribe('tick', () => console.log('a'));
const b = bus.subscribe('tick', () => console.log('b'));
const c = bus.subscribe('tick', () => console.log('c'));
b.release();
bus.emit('tick'); // logs: a then c
Double release is a safe no-op:
const sub = bus.subscribe('x', () => {});
sub.release();
sub.release(); // does NOT throw, does NOT remove anything else
subscribe with the same function twice creates two independent handles; releasing one leaves the other firing.emit on an event with no subscribers is a silent no-op, not an error.release() more than once is safe and must not affect other subscribers.emit forwards args verbatim — whatever you pass after the event name reaches each callback as positional arguments.emit returns. Don't worry about async fan-out, once, or wildcards.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.