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

@efficimo/deferred-promise

v0.3.0

Published

Extend native Promises with external resolve/reject, status tracking, timeout, abort signal and deferred maps. Fully typed. Directly awaitable.

Readme

@efficimo/deferred-promise

npm version license types

Extend native Promises with external resolve/reject, status tracking, timeout, abort signal, progress reporting, lazy execution and deferred maps. Fully typed. Directly awaitable.

Why

The native Promise constructor forces you to resolve or reject from inside the executor. This is inconvenient for patterns like RPC, event correlation, or any case where the resolution happens in a completely different part of your code.

@efficimo/deferred-promise solves this by extending the native Promise class — so every deferred is directly awaitable with no wrappers — and adding lifecycle controls: external resolve/reject, status tracking, timeout management, abort signal integration, progress reporting, and a deferred map for key-based correlation.

Installation

npm install @efficimo/deferred-promise

Quick start

import { DeferredPromise } from '@efficimo/deferred-promise';

const deferred = new DeferredPromise<string>();

// resolve from anywhere
deferred.resolve('hello');

// directly awaitable
const result = await deferred; // 'hello'

API

DeferredPromise<T>

Base class. Extends native Promise<T>.

new DeferredPromise<T>(executor?)

| Member | Type | Description | |---|---|---| | status | 'pending' \| 'fulfilled' \| 'rejected' | Current lifecycle state | | isPending | boolean | Shorthand for status === 'pending' | | resolve(value) | void | Resolves the promise. No-op if already settled. | | reject(reason?) | void | Rejects the promise. No-op if already settled. |

const deferred = new DeferredPromise<number>();

console.log(deferred.status);   // 'pending'
console.log(deferred.isPending); // true

deferred.resolve(42);

console.log(deferred.status);   // 'fulfilled'
console.log(deferred.isPending); // false

// calling resolve/reject after settlement is a no-op
deferred.reject(new Error('too late')); // ignored

const value = await deferred; // 42

The optional executor runs immediately, just like a native Promise:

const deferred = new DeferredPromise<number>((resolve) => {
  someEmitter.once('data', resolve);
});

TimeoutDeferredPromise<T>

Extends DeferredPromise<T>. Automatically rejects after a given delay. The timer is cleared when the promise is resolved or rejected before it fires.

new TimeoutDeferredPromise<T>(timeoutMs, message?, executor?)

| Parameter | Type | Description | |---|---|---| | timeoutMs | number | Delay in milliseconds before auto-rejection | | message | string \| (ms: number) => string | Custom error message or factory | | executor | function | Optional executor, runs alongside the timer |

| Member | Description | |---|---| | extendTimeout(timeoutMs?) | Resets the timer. Uses original timeoutMs if omitted. No-op if already settled. |

import { TimeoutDeferredPromise } from '@efficimo/deferred-promise';

// default message
const deferred = new TimeoutDeferredPromise<string>(5000);

// custom static message
const deferred = new TimeoutDeferredPromise<string>(5000, 'Request timed out');

// dynamic message
const deferred = new TimeoutDeferredPromise<string>(5000, ms => `Timed out after ${ms}ms`);

// extend the timer (e.g. on activity)
deferred.extendTimeout();        // reset to original 5000ms
deferred.extendTimeout(10_000);  // reset to a new duration

// resolve before timeout — timer is cleared automatically
deferred.resolve('ok');

AbortDeferredPromise<T>

Extends DeferredPromise<T>. Automatically rejects when an AbortSignal is triggered. Cleans up the event listener when resolved or rejected before the signal fires.

new AbortDeferredPromise<T>(signal, executor?)

| Parameter | Type | Description | |---|---|---| | signal | AbortSignal | Signal that triggers automatic rejection | | executor | function | Optional executor |

import { AbortDeferredPromise } from '@efficimo/deferred-promise';

const controller = new AbortController();

const deferred = new AbortDeferredPromise<string>(controller.signal);

// abort from anywhere
controller.abort(); // deferred rejects with signal.reason or DOMException('Aborted', 'AbortError')

// already-aborted signal rejects immediately
const aborted = new AbortDeferredPromise<string>(AbortSignal.abort());

DeferredMap<K, T>

A Map-like structure of DeferredPromise instances, auto-created on first access by key. Designed for request/response correlation patterns (RPC, WebSocket, message queues).

new DeferredMap<K, T>()

| Member | Description | |---|---| | size | Number of pending entries | | has(key) | Returns true if an entry exists for this key | | get(key) | Returns the deferred for this key, creating it if absent | | resolve(key, value) | Resolves the entry and removes it from the map | | reject(key, reason?) | Rejects the entry and removes it from the map | | delete(key) | Rejects the entry with a deletion error and removes it | | clear() | Rejects all pending entries and empties the map |

import { DeferredMap } from '@efficimo/deferred-promise';

const pending = new DeferredMap<string, ApiResponse>();

// caller side — get or create a deferred and await it
const response = await pending.get(callId);

// responder side — resolve by key
pending.resolve(callId, data);   // auto-removes the entry

// error handling
pending.reject(callId, new Error('not found'));

// teardown (e.g. on disconnect)
pending.clear(); // rejects all pending entries

ProgressDeferredPromise<T, P>

Extends DeferredPromise<T>. Adds progress notifications before final resolution. The second type parameter P defines the shape of progress updates.

new ProgressDeferredPromise<T, P>(executor?)

| Member | Description | |---|---| | onProgress(listener) | Registers a progress listener. Returns this for chaining. | | progress(value) | Notifies all listeners. No-op if already settled. |

import { ProgressDeferredPromise } from '@efficimo/deferred-promise';

const deferred = new ProgressDeferredPromise<string, { percent: number }>();

// register one or more listeners (chainable)
deferred
  .onProgress(p => updateProgressBar(p.percent))
  .onProgress(p => console.log(`${p.percent}%`));

// notify from anywhere
deferred.progress({ percent: 25 });
deferred.progress({ percent: 75 });

deferred.resolve('done');

// progress after settlement is a no-op
deferred.progress({ percent: 99 }); // ignored

const result = await deferred; // 'done'

LazyPromise<T>

Extends Promise<T>. Defers execution of the executor until the first then, catch, finally, or await. Useful when the side effect (network request, file read, heavy computation) should only start when a consumer is actually ready.

new LazyPromise<T>(executor)

| Member | Type | Description | |---|---|---| | status | 'pending' \| 'fulfilled' \| 'rejected' | Current lifecycle state | | isPending | boolean | Shorthand for status === 'pending' | | isStarted | boolean | true once the executor has been triggered |

import { LazyPromise } from '@efficimo/deferred-promise';

const lazy = new LazyPromise<string>((resolve) => {
  console.log('executor running');
  resolve('hello');
});

// nothing has run yet — executor is still dormant
console.log(lazy.isStarted); // false
console.log(lazy.status);    // 'pending'

// first await triggers the executor
const result = await lazy;
// logs: 'executor running'

console.log(result);         // 'hello'
console.log(lazy.isStarted); // true
console.log(lazy.status);    // 'fulfilled'

Multiple then/await calls are safe — the executor runs exactly once:

const lazy = new LazyPromise<number>((resolve) => resolve(42));

const [a, b] = await Promise.all([lazy, lazy]);
// executor ran once, both consumers receive 42

Real-world example: RPC over WebSocket

import { DeferredMap, TimeoutDeferredPromise } from '@efficimo/deferred-promise';

type RpcResponse = { result: unknown };

const pending = new DeferredMap<string, RpcResponse>();
let callCounter = 0;

function call(method: string, params: unknown): Promise<RpcResponse> {
  const callId = `${method}-${++callCounter}`;
  const deferred = new TimeoutDeferredPromise<RpcResponse>(10_000, `RPC "${method}" timed out`);

  pending.get(callId); // register the key
  socket.send(JSON.stringify({ callId, method, params }));

  return deferred;
}

socket.on('message', (raw: string) => {
  const { callId, result, error } = JSON.parse(raw);
  error ? pending.reject(callId, error) : pending.resolve(callId, { result });
});

socket.on('close', () => pending.clear());

// usage
const { result } = await call('getUser', { id: 42 });

Contributing

Issues and PRs are welcome at github.com/efficimo/@efficimo/deferred-promise.

License

MIT