npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ee-first-modern

v0.1.5

Published

A modern, dependency-free, typed first-event primitive compatible with the ee-first callback API, with Promise and AbortSignal support.

Readme

ee-first-modern

CI npm version npm downloads

A tiny, dependency-free, TypeScript-first first-event primitive for Node.js.

A modern, maintained implementation of the ee-first pattern: wait for the first event across multiple events and emitters, return the winner, and clean up all losing listeners.

Why this exists

Node.js provides useful lower-level event primitives such as EventEmitter.once() and events.once(), but they wait for a particular event on a particular emitter. The operation this package focuses on is different:

Wait for whichever of several events fires first, potentially across multiple emitters, then remove all of the other listeners.

That is the small abstraction ee-first provides, and it is still useful when coordinating things such as request completion, socket closure, abort/error races, and competing lifecycle events.

ee-first-modern preserves the established callback API while adding a modern package/tooling surface:

  • ESM and CommonJS
  • first-party TypeScript types
  • Promise API
  • AbortSignal cancellation
  • zero runtime dependencies
  • cleanup and registration-safety checks

Why use this instead of ee-first?

The original ee-first package is a long-established dependency. This project is intended for code that wants the same small primitive with modern packaging and APIs without pulling in the old implementation.

| Capability | ee-first 1.1.x | ee-first-modern | | --- | ---: | ---: | | Callback API | Yes | Yes | | Callable waiter | Yes | Yes | | .cancel() | Yes | Yes | | ESM | No | Yes | | CommonJS | Yes | Yes | | First-party TypeScript | No | Yes | | Promise API | No | Yes | | AbortSignal | No | Yes | | Runtime dependencies | 0 | 0 |

The compatibility target is the public callback behavior and callable waiter shape. The modern APIs are additive.

Build architecture

src/index.ts is the single source of truth. The build generates both ESM and CommonJS artifacts from that same TypeScript implementation. The CommonJS entrypoint is a generated compatibility shim that preserves the historical callable require('ee-first-modern') shape.

The test suite loads both dist/index.js and dist/index.cjs and runs the same behavior suite against each format, so packaging drift between ESM and CommonJS is caught by CI.

Install

npm install ee-first-modern

Callback API

import first from 'ee-first-modern'

const waiter = first([
  [request, 'close', 'error'],
  [response, 'finish'],
], (error, emitter, event, args) => {
  // Cleanup has been attempted before this callback runs.
})

waiter.cancel()

The callback receives:

(error, emitter, event, args)

For a non-error event, error is null. For an error event, it is the event's first argument.

The returned waiter is callable for compatibility with the historical API, so a pending callback can be replaced:

waiter(nextCallback)

Promise API

import { firstAsync } from 'ee-first-modern'

const result = await firstAsync([
  [request, 'close', 'error'],
  [response, 'finish'],
])

console.log(result.event, result.emitter, result.args)

By default, an error event rejects the Promise. Use rejectOnError: false to receive an error event as a normal result.

AbortSignal

import { firstAsync } from 'ee-first-modern'

const controller = new AbortController()

const promise = firstAsync(
  [[socket, 'close', 'error']],
  { signal: controller.signal },
)

controller.abort('request cancelled')

Aborting removes the registered listeners and rejects with FirstAbortedError. A non-undefined abort reason is available as error.cause.

The callback API also accepts { signal }; aborting cancels the waiter without invoking the callback.

Cleanup and failure semantics

Cleanup is attempted before the callback or Promise settles.

For the callback API, the callback still runs if listener cleanup fails; the cleanup error is then thrown synchronously. Calling .cancel() again retries any listener removals that previously failed.

For the Promise API, a cleanup failure rejects the Promise. When both the winning error event and cleanup fail, the rejection is an AggregateError containing both failures. Aborting rejects with FirstAbortedError; a simultaneous cleanup failure is combined with it in an AggregateError.

Cleanup includes the package's internal AbortSignal listener. If a non-standard signal throws while removing that listener, the failure is surfaced alongside any event-listener cleanup failure, and the callback waiter's later .cancel() can retry it.

An 'error' event with no arguments preserves the historical callback behavior: error is undefined. The public callback and result types therefore allow unknown | null | undefined.

Behavior notes

Event listeners may be invoked synchronously by a custom emitter during .on(). In that case the winning callback can run before first() returns, so the returned waiter is not available from inside that callback yet.

When an emitter provides both .removeListener() and .off(), cleanup uses .removeListener() first and .off() as the fallback.

Supported emitter shape

An emitter must provide:

  • .on(event, listener)
  • .removeListener(event, listener) or .off(event, listener)

String and symbol event names are supported.

Quality

The repository includes:

  • compatibility and edge-case tests
  • registration rollback and cleanup checks
  • ESM/CommonJS parity coverage
  • type-level API tests
  • benchmark harness
  • Node 18/20/22/24/26 CI across Linux, Windows, and macOS (ARM64 primary; Intel smoke test)
  • public API type checks
  • zero runtime dependencies

Run locally:

npm install
npm run check
npm run bench

Links

License

MIT