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

fire-forget

v0.1.1

Published

Tiny, zero-dependency in-flight promise tracker with drain-on-shutdown and AbortSignal cancellation.

Readme

fire-forget

npm version npm downloads zero dependencies types included license MIT

Tiny, zero-dependency in-flight promise tracker with drain-on-shutdown and AbortSignal cancellation.

  • Two-call workflow — fire(promise) to dispatch, await drain() on shutdown
  • Drains cleanly on SIGTERM — waits for detached work before the process exits
  • Optional per-task timeout — fire(work, { timeoutMs: 5000 }) rejects on overrun
  • AbortSignal propagation — drain({ abortOnTimeout: true }) cancels stragglers
  • Hookable for observability — onStart / onSettle / onError with task metadata
  • Isolated instances — createTracker() for tests or independent workloads
  • Zero runtime dependencies, ships with TypeScript types
  • Tiny surface area — four functions, one error class
import { fire, drain, inflight } from 'fire-forget';

// Plain promise — fire and forget. No cancellation possible.
fire(fetch('/analytics', { method: 'POST', body }));

// Signal-aware thunk — cancellable on per-task timeout or drain timeout.
fire((signal) => fetch('/slow', { signal }), { timeoutMs: 5000 });

inflight();
// → 2

// On SIGTERM, wait up to 10s; abort what didn't finish.
await drain({ timeoutMs: 10_000, abortOnTimeout: true });
// → { drained: 1, remaining: 1, timedOut: true }

Install

npm install fire-forget
# or
pnpm add fire-forget
# or
yarn add fire-forget
# or
bun add fire-forget

Both ESM and CommonJS are shipped:

import { fire, drain } from 'fire-forget';            // ESM / TypeScript
const { fire, drain } = require('fire-forget');       // CommonJS

Requires Node ≥ 22.

Why

Detached promises are easy to start and easy to lose. When the process receives SIGTERM, every in-flight fetch() you didn't await dies mid-write — half-sent analytics, dropped audit logs, partial cache warms.

API

fire(input, opts?)

Register a detached task. Returns void — by design, so you can't accidentally await and defeat the purpose.

| Input | Cancellable | What it means | |---|---|---| | Promise<T> | No | The work is already running; you just hand off the promise. | | () => Promise<T> | No | fire invokes the thunk synchronously to start the work. | | (signal: AbortSignal) => Promise<T> | Yes | Same as above, plus a per-task AbortSignal for timeouts and drain-abort. |

| Option | Type | Default | Meaning | |---|---|---|---| | timeoutMs | number | none | Reject with TimeoutError after N ms. Aborts the signal if present. | | label | string | none | Surfaced on TaskMeta.label for hook callbacks. |

drain(opts?)

Wait for every in-flight task to settle. Returns Promise<DrainResult>.

| Option | Type | Default | Meaning | |---|---|---|---| | timeoutMs | number | wait forever | Give up waiting after N ms. | | abortOnTimeout | boolean | false | When the drain timer fires, abort the signal on every pending task. Signal-aware tasks settle quickly; plain promises stay in remaining. |

interface DrainResult {
  drained: number;    // tasks that settled within the budget
  remaining: number;  // tasks still pending — only > 0 if timedOut
  timedOut: boolean;
}

inflight()

Returns the current count of pending tasks as a number.

createTracker(opts?)

Returns an isolated Tracker with its own fire / drain / inflight. The module-level exports are bound to a default singleton (no hooks).

| Hook | Signature | Fires when | |---|---|---| | onStart | (meta: TaskMeta) => void | fire() is called, after the task is registered. | | onSettle | (meta: TaskMeta) => void | A task finishes (success or failure). meta.durationMs is set. | | onError | (err: unknown, meta: TaskMeta) => void | A task rejects or its per-task timeout fires. Without onError, errors log to console.error. |

interface TaskMeta {
  id: number;          // monotonic per tracker, starts at 0
  label?: string;      // from fire(..., { label })
  startedAt: number;   // Date.now() when fire() was called
  durationMs?: number; // set in onSettle
  timedOut?: boolean;  // set in onSettle if per-task timeoutMs fired
}

configureDefault(opts)

Install hooks on the default singleton. Must be called before the first fire() / drain() / inflight() — otherwise the default tracker has already been created with no hooks and the call throws. Accepts the same TrackerOptions as createTracker.

import { configureDefault, fire } from 'fire-forget';

configureDefault({
  onError: (err, meta) => logger.error({ err, meta }, 'detached task failed'),
});

fire(somePromise);

TimeoutError

Thrown when a per-task timeoutMs elapses. err.name === 'TimeoutError'.

Patterns

Graceful shutdown with native signals.

import { drain } from 'fire-forget';

const shutdown = async () => {
  const result = await drain({ timeoutMs: 10_000, abortOnTimeout: true });
  if (result.timedOut) {
    console.warn('shutdown drain incomplete', { remaining: result.remaining });
  }
  process.exit(0);
};

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

Isolated tracker for observability.

import { createTracker } from 'fire-forget';

const tracker = createTracker({
  onError:  (err, meta) => logger.error({ err, meta }, 'detached task failed'),
  onStart:  (meta) => metrics.increment('detached.start'),
  onSettle: (meta) => metrics.timing('detached.duration', meta.durationMs),
});

tracker.fire(doWork(), { label: 'cache-warm' });
await tracker.drain();

What's not included

  • ❌ Durable retries across process restarts (use bullmq, pg-boss, graphile-worker)
  • ❌ HTTP keep-alive connection draining (use http-terminator, stoppable)
  • ❌ Auto-installed signal handlers (wire your own — frameworks vary)
  • ❌ Backpressure / max-inflight caps
  • ❌ Browser support (Node-only target)

License

MIT