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

fetch-coalescer

v1.0.0

Published

Coalesce duplicate GETs to the same URL at the fetch layer (in-flight merging + short TTL cache). Ships with defaults for Auth.js / NextAuth session; works for any fetch-based duplicate-request pattern via path/match.

Readme

fetch-coalescer

window.fetch wrapper: merge overlapping GETs to the same URL into one in-flight request (coalescing) and serve a short TTL in-memory replay so duplicate callers don’t multiply network work. Intended for small JSON responses; not NextAuth-specific code — it never imports NextAuth.

Defaults target Auth.js / NextAuth: path is /api/auth/session. The most common motivation is NextAuth v5 SessionProvider firing two session fetches on mount (BroadcastChannel echo); this fixes that without monkey-patching NextAuth. Any duplicate fetch pattern to a configured URL benefits — use path, paths, or match for your routes.

  • Zero dependencies
  • Works with [email protected] / @auth/* session clients and other libraries that duplicate GETs to the same URL
  • Tiny (~2 KB min+gzip)
  • Safe with AbortSignal: one caller's abort never kills the shared request or other joined callers
  • Supports cross-origin auth endpoints and custom matchers
  • Honors explicit cache-bypass (cache: "no-store" / "reload" / "no-cache", Cache-Control: no-cache/no-store)
  • Explicit clearCoalescerCache() for post-signOut() / signIn() invalidation
  • Optional paths: string[] and getCoalescerStats()
  • SSR-safe: no-op on the server; never patches globalThis.fetch in Node
  • ESM + CJS, with full TypeScript types
  • Install once at module scope; forget about it

Migrating from nextauth-session-dedupe

Published as nextauth-session-dedupe through v0.3.0. fetch-coalescer v1.0.0 changes the package name and uses generic API identifiers (same behavior).

1. Package

npm uninstall nextauth-session-dedupe
npm install fetch-coalescer

2. Renames (replace in your codebase):

| Before | After | | --- | --- | | installSessionFetchDedupe | installFetchCoalescer | | clearSessionCache | clearCoalescerCache | | getSessionFetchDedupeStats | getCoalescerStats | | SessionFetchDedupeOptions | FetchCoalescerOptions | | SessionFetchDedupeStats | CoalescerStats | | SessionMatchContext | CoalescerMatchContext |

After you publish fetch-coalescer, deprecate the old name (from an account that owns the package):

npm deprecate nextauth-session-dedupe@latest "Renamed to fetch-coalescer. Use: npm install fetch-coalescer"

Why this exists (NextAuth / Auth.js)

NextAuth v5's SessionProvider does this on mount:

  1. Calls getSession() → fetches /api/auth/session (expected).
  2. When that fetch resolves, getSession() posts a message on a fresh BroadcastChannel("next-auth") so other tabs can sync.
  3. SessionProvider listens on a separate cached instance of the same channel. Because they're different instances, the message delivers into the same tab and triggers a second /api/auth/session fetch a few ms later.

That second request is redundant in the single-tab case. This package intercepts it.

Install

npm install fetch-coalescer

Usage

Option A — auto-install (simplest)

// Top of a "use client" module, e.g. app/providers.tsx in Next.js
import "fetch-coalescer/auto";

That's it. The wrapper is installed at module load, before any React component mounts, with default options.

Option B — explicit install (configurable)

"use client";

import { installFetchCoalescer } from "fetch-coalescer";

// Run at module scope so it's ready before SessionProvider mounts.
if (typeof window !== "undefined") {
	installFetchCoalescer({
		// path: "/api/auth/session",  // default
		// ttlMs: 1500,                 // default
		// debug: false,                // default
	});
}

Full Next.js App Router example

// app/providers.tsx
"use client";

import { SessionProvider } from "next-auth/react";
import "fetch-coalescer/auto";

export function Providers({ children }: { children: React.ReactNode }) {
	return (
		<SessionProvider
			refetchOnWindowFocus={false}
			refetchInterval={0}
		>
			{children}
		</SessionProvider>
	);
}
// app/layout.tsx
import { Providers } from "./providers";

export default function RootLayout({ children }: { children: React.ReactNode }) {
	return (
		<html lang="en">
			<body>
				<Providers>{children}</Providers>
			</body>
		</html>
	);
}

Combine the dedupe with refetchOnWindowFocus={false} and refetchInterval={0} for a fully quiet /api/auth/session — one request on first load, zero after.

Next.js Pages Router example

// pages/_app.tsx
import "fetch-coalescer/auto";
import { SessionProvider } from "next-auth/react";
import type { AppProps } from "next/app";

export default function App({
	Component,
	pageProps: { session, ...pageProps },
}: AppProps) {
	return (
		<SessionProvider
			session={session}
			refetchOnWindowFocus={false}
			refetchInterval={0}
		>
			<Component {...pageProps} />
		</SessionProvider>
	);
}

Plain React (Vite / CRA / any SPA)

Anywhere in your client entry — main.tsx, index.tsx, or the module that renders your app root — add the import before rendering:

import "fetch-coalescer/auto";
import { createRoot } from "react-dom/client";
import { App } from "./App";

createRoot(document.getElementById("root")!).render(<App />);

Verifying the install

Enable debug temporarily to confirm the wrapper is intercepting. You should see a console.debug each time the second session fetch is served from cache or coalesced onto the first:

"use client";
import { installFetchCoalescer } from "fetch-coalescer";

if (typeof window !== "undefined") {
	installFetchCoalescer({ debug: true });
}

In DevTools → Network, with throttling off, you should see exactly one GET /api/auth/session on initial page load instead of two.

Options

| Option | Type | Default | Notes | | --------- | ------------------------------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | path | string | /api/auth/session | Pathname matched with endsWith — apps with a basePath (e.g. /my-app/api/auth/session) work without extra config. Pass an absolute URL (https://auth.example.com/api/auth/session) to dedupe a cross-origin session endpoint. Ignored when match is provided, or when paths is non-empty. | | paths | string[] | — | Multiple patterns with the same rules as path. When non-empty, takes precedence over path. Each distinct request URL gets its own TTL cache and in-flight slot — responses are not shared across URLs. | | match | (ctx) => boolean | — | Custom matcher. Receives { url: URL, method: string, sameOrigin: boolean } and returns true to dedupe. Fully replaces path. Non-GET requests are always passed through regardless. | | ttlMs | number | 1500 | How long a successful response is reused. Short on purpose — just long enough to absorb the BroadcastChannel echo and React Strict Mode double-mounts. | | debug | boolean | false | When true, logs a console.debug every time a request is served from cache, joined onto an in-flight promise, or bypassed because the caller opted out of caching. |

Observability

getCoalescerStats() returns a snapshot of { hits, misses, coalesced, cacheClears }:

  • hits — responses served from the in-memory TTL cache
  • misses — new upstream fetches started (not joins)
  • coalesced — callers that awaited an existing in-flight request
  • cacheClears — how many times clearCoalescerCache() ran while installed

Returns all zeros on the server or when the wrapper is not installed.

How it works

On window.fetch(input, init):

  1. If the request isn't a GET matched by path / paths / match, pass through untouched.
  2. If the caller opted out of caching (cache: "no-store" | "reload" | "no-cache" or a Cache-Control: no-cache/no-store request header), pass through untouched.
  3. If the caller's AbortSignal is already aborted, reject immediately with an AbortError (matching native fetch).
  4. If a successful response for this exact request URL (origin + pathname + search) is in the cache and younger than ttlMs, return a fresh Response built from the cached body.
  5. If a network request is already in flight for that same URL key, await the shared promise and return a .clone(). Each caller's AbortSignal is honored individually — aborting one caller does not cancel the shared upstream request or affect other joined callers.
  6. Otherwise, issue the real request, store the shared promise in the per-URL in-flight map, buffer the body on success, and return the response.

Errors (response.ok === false) are not cached, so transient failures re-hit the network on the next attempt.

AbortSignal semantics

The shared upstream network request is deliberately detached from every caller's AbortSignal. Each caller's returned promise rejects with an AbortError when their own signal fires, but the network request itself continues so that other concurrent and future callers (and the TTL cache) still benefit.

This is the safe choice for a session endpoint: the request is tiny, always needed, and losing the ability for a single caller to cancel it is vastly better than letting one caller's cancellation cascade into failures for every other consumer on the page.

FAQ

Does this break cross-tab sign-in / sign-out sync?

No. The cache TTL is 1.5 seconds by default. A user signing in or out in another tab happens on human timescales (at minimum many seconds), far outside the window. When the other tab broadcasts its session change, this tab's SessionProvider refetches, the cache has expired, and the new session propagates normally.

What if I run my app under a basePath?

The default path is matched with endsWith, so /my-app/api/auth/session matches automatically. If you need a different match rule, pass path explicitly, or use match for full control.

What if my auth endpoint is on a different origin?

Pass an absolute URL as path:

installFetchCoalescer({
	path: "https://auth.example.com/api/auth/session",
});

The wrapper will dedupe requests whose origin matches auth.example.com and whose pathname ends with /api/auth/session. Same-origin requests to unrelated paths are untouched.

Do I need this if I only use auth() on the server?

No. The duplicate request is a behavior of the client-side SessionProvider. If your app doesn't use SessionProvider (or useSession, getSession, etc.), the client never hits /api/auth/session and there's nothing to dedupe. Importing this package in that case is harmless — every entry point is guarded by typeof window !== "undefined" and does nothing on the server — but it's also unnecessary.

How does this interact with AbortController / AbortSignal?

The upstream network request is detached from every caller's signal, so aborting one caller never cancels the shared request or affects other joined callers. Each caller's returned promise still honors its own signal and rejects with an AbortError when aborted, matching native fetch semantics. In practice NextAuth's internal getSession() doesn't pass a signal, but this keeps the wrapper safe for any library that might (React Query, SWR, custom code).

How do I force a fresh network request?

Pass cache: "no-store" (or "reload" / "no-cache"), or a Cache-Control: no-cache / no-store header. These requests bypass the dedupe entirely and hit the network directly.

What about signOut() / signIn()?

The cache TTL is 1.5 seconds by default. If your app calls getSession() (or hits /api/auth/session directly) within that window after sign-out or sign-in, it may be answered from the stale pre-signout response.

This is a UX consistency window, not a security issue — the server has already revoked the session, and the stale read expires within ttlMs. For apps that want zero such window, call clearCoalescerCache() explicitly:

import { signOut, signIn } from "next-auth/react";
import { clearCoalescerCache } from "fetch-coalescer";

await signOut({ redirect: false });
clearCoalescerCache();

await signIn("credentials", { redirect: false, /* ... */ });
clearCoalescerCache();

clearCoalescerCache() clears the in-memory cache (all URL keys) without uninstalling the wrapper. The next matching request hits the network and repopulates the cache with the new state. It's a safe no-op on the server and when the wrapper is not installed.

Note: if a session fetch was already in flight at the moment you call clearCoalescerCache(), any response that lands from it is delivered to its existing callers but is not written to the cache. New callers that arrive after invalidation start a fresh upstream request rather than joining onto the soon-to-be-discarded one. This means you can temporarily see multiple concurrent session fetches immediately after clearCoalescerCache() — this is intentional and the tradeoff for guaranteed freshness.

Is it safe to call installFetchCoalescer() more than once?

Yes. The second and subsequent calls only refresh path, paths, match, ttlMs, and debug on the existing wrapper. They do not re-wrap window.fetch.

Can I uninstall?

installFetchCoalescer() returns an uninstall function that restores the original fetch. Most apps never need it; the wrapper is designed to live for the page's lifetime.

Does it work with SWR / React Query / any other library that calls useSession internally?

Yes. This library dedupes at the fetch level, not at the NextAuth level, so any caller that hits /api/auth/session benefits — including libraries you don't control.

Why not just set SessionProvider session={...} to pre-hydrate?

You can, but it forces your root layout to be dynamic (await auth() there calls cookies()), which disables static rendering for every page in the tree. This package is the escape hatch when you want to keep the layout static.

License

MIT