Summary
once() currently goes through on() (which constructs a Signal + subscription) just to read the first event and tear down. The same behavior can be expressed directly with addEventListener + withResolvers, skipping the signal machinery.
Current implementation
lib/events.ts:32-49 — once() yields the next item from an on() subscription and relies on resource teardown to remove the listener.
Proposed sketch
export function once<
T extends EventTarget,
K extends EventList<T> | (string & {}),
>(target: T, name: K): Operation<EventTypeFromEventTarget<T, K>> {
return {
*[Symbol.iterator]() {
let happened = withResolvers<EventTypeFromEventTarget<T, K>>();
let listener = (event: Yielded<typeof happened.operation>) =>
happened.resolve(event);
try {
target.addEventListener(name, listener);
return yield* happened.operation;
} finally {
target.removeEventListener(name, listener);
}
},
};
}
Motivation
- Drops a layer of indirection — no
Signal allocation, no subscription bookkeeping for the one-shot case.
- Listener lifetime becomes explicit in the
try/finally, easier to reason about than implicit resource teardown.
Open questions / risk
- Verify nothing observable changes vs. the
on() path (event coercion, ordering when the event fires synchronously during addEventListener).
- Confirm type inference stays equivalent — the sketch uses
Yielded<typeof happened.operation> for the listener parameter.
- Worth measuring against the events scenarios in
bench/codspeed.bench.ts to see if there's a measurable win.
Notes
Sketched locally and then thrown away — capturing here so it doesn't get lost.
Summary
once()currently goes throughon()(which constructs aSignal+ subscription) just to read the first event and tear down. The same behavior can be expressed directly withaddEventListener+withResolvers, skipping the signal machinery.Current implementation
lib/events.ts:32-49—once()yields the next item from anon()subscription and relies on resource teardown to remove the listener.Proposed sketch
Motivation
Signalallocation, no subscription bookkeeping for the one-shot case.try/finally, easier to reason about than implicit resource teardown.Open questions / risk
on()path (event coercion, ordering when the event fires synchronously duringaddEventListener).Yielded<typeof happened.operation>for the listener parameter.bench/codspeed.bench.tsto see if there's a measurable win.Notes
Sketched locally and then thrown away — capturing here so it doesn't get lost.