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

@xemahq/managed-fetch

v0.6.1

Published

Layer-1 managed outbound-fetch / polite-crawl HTTP client. A domain-agnostic wrapper any ingestion/monitor/scraper calls instead of bare `fetch`: per-host token-bucket rate limiting, exponential backoff+jitter with Retry-After, a per-host circuit breaker,

Readme

@xemahq/managed-fetch

Polite outbound HTTP client with rate limiting, retries and circuit breaking

Overview

ManagedFetch wraps fetch for ingestion, monitoring and scraping callers: per-host rate limiting, retry with exponential backoff and jitter (honoring Retry-After), a per-host circuit breaker, conditional requests (a 304 is a success, result.notModified), a configurable User-Agent, per-request timeouts and a health probe. It has no runtime dependencies. Every terminal failure throws a typed error that extends ManagedFetchError.

When to use it

  • Use it instead of bare fetch for any outbound crawl or polling.
  • State (token buckets, breakers) is per instance and in memory; each pod keeps its own, so a shared cross-pod budget must be enforced upstream.

Installation

pnpm add @xemahq/managed-fetch

Usage

import { ManagedFetch } from '@xemahq/managed-fetch';

const client = new ManagedFetch({
  userAgent: 'AcmeCrawler/2.0 (+https://acme.example)',
  rateLimit: { requestsPerSecond: 1, burst: 3 },
  retry: { maxRetries: 4, baseDelayMs: 250, maxDelayMs: 15_000 },
  circuitBreaker: { failureThreshold: 5, cooldownMs: 30_000 },
});

const res = await client.fetch('https://example.com/feed.json', {
  conditional: { etag: previousEtag },
});
if (!res.notModified) {
  const body = await res.json();
}

Configuration

new ManagedFetch(config); every field is optional.

| Option | Meaning | | --- | --- | | userAgent | User-Agent sent unless overridden per request. | | rateLimit | Default token bucket: requestsPerSecond, burst, maxQueueMs?. | | perHostRateLimit | Per-hostname overrides of rateLimit. | | retry | maxRetries, baseDelayMs, maxDelayMs, retryableStatuses?, respectRetryAfter?. | | circuitBreaker | failureThreshold, cooldownMs, halfOpenMaxProbes?. | | timeoutMs | Default per-request timeout. | | fetchImpl | Transport override (defaults to global fetch). | | clock | Override now, sleep, random. | | egress | Optional proxy and browser egress (below). |

Errors: CircuitOpenError, RateLimitError, RequestTimeoutError, RetriesExhaustedError, EgressExhaustedError. Other methods: probe(url), getCircuitState(host), getHealthMatrix().

Optional egress

Without egress, every request goes direct. With it, hosts can be routed through an ordered list of strategies (a proxy pool or a browser service) with failover, exit-IP rotation (maxRotations) and opt-in soft-block detection (detectSoftBlock). Build the resolver with createEgressResolver({ strategies, policy }) using ProxyStrategy and BrowserStrategy. A strategy selected without its transport wired fails fast; it never falls back to an unproxied request.

The undici proxy transport is the @xemahq/managed-fetch/egress-undici subpath: createUndiciDispatcherFactory({ ssrf: 'public-web' }) returns a dispatcherFactory for egress, and its direct agent refuses private, loopback and metadata addresses on every hop.

Guarded public-web fetch

createGuardedFetch (from @xemahq/managed-fetch/egress-undici; needs the optional undici peer) fetches a public-web URL and refuses to reach anything internal. It never reads proxy environment variables and never uses or sets undici's global dispatcher; all configuration is passed as typed options.

import {
  createGuardedFetch,
  GuardedFetchError,
  GuardedFetchFailureReason,
} from '@xemahq/managed-fetch/egress-undici';

const guarded = createGuardedFetch();
try {
  const res = await guarded.fetch('https://example.com/feed.json');
} catch (err) {
  if (err instanceof GuardedFetchError) {
    // err.reason: EGRESS_REFUSED | HOST_UNRESOLVABLE | REDIRECT_REFUSED
  }
}
await guarded.close(); // on shutdown
  • Pre-flight assertUrlAllowed: http(s) only, no non-public literal IP, no host with any non-public DNS answer (guarded.assertAllowed(url) runs it alone).
  • Connect-time makeSsrfLookup on a private Agent: a host that rebinds to a private, loopback, link-local or metadata address after the pre-flight is refused at connect.
  • redirect: 'manual' is forced; any 3xx throws REDIRECT_REFUSED.
  • resolveHost and connectLookup options replace name resolution for tests. The transport is not injectable: it is always undici with the guarded Agent.

Peer requirements

undici ^7.29.0, optional: only for the egress-undici subpath.

License

Apache-2.0 © Xema — xema.dev