Skip to content

Refactor once() to use withResolvers directly instead of on() + signal #1172

Description

@cowboyd

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-49once() 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions