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

intellawait

v1.0.1

Published

Smart async utilities for deduping, caching, throttling, retrying, and timeout control.

Readme

intellawait

Doing intelligent things with await functions.

intellawait is a utility library designed to help you manage when and how often async operations run, providing ways to deduplicate, throttle and debounce calls, as well as other useful functions.

import { getDeduper } from 'intellawait';

const { dedup, debounce, throttle } = getDeduper({ namespace: 'example' });

// 🚫 Prevent duplicate requests
const user = await dedup(`user:${id}`, () => fetch(`/api/user/${id}`));

// ✋ Throttle API calls to once per second
await throttle('search', () => fetch(`/api/search?q=${query}`), 1000);

// ⏳ Debounce input, show typing immediately, search after pause
debounce('liveSearch', async () => {
    const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
    showResults(results);
}, 300, () => showTypingIndicator());

In other words: ✅ No more racing, duplicate fetches, or overfiring handlers

✨ Highlights

  • 🔁 Prevent duplicate concurrent operations with dedup
  • 💾 Cache async results with dedupWithCache
  • 🚦 Throttle calls per key with throttle
  • ⏳ Debounce repeated triggers with debounce
  • 🔧 Fire-and-forget or fully awaitable semantics
  • 💡 Built-in sleep, retry, and withTimeout helpers

🚀 Getting Started

npm install intellawait
import { getDeduper } from 'intellawait';
const { dedup, throttle, debounce } = getDeduper({ namespace: 'api', debug: true });

⚙️ getDeduper(config) Configuration

Creating a deduper instance is done with getDeduper(config). Each instance is completely independent — no key collisions, no shared state — so you can use multiple dedupers safely across modules or subsystems. All config options are optional.

const {
    dedup,
    dedupWithCache,
    throttle,
    debounce,
    clear,
    clearAll
} = getDeduper(config);

Available config Options:

| Option | Type | Description | |--------------------|--------------------|-------------| | namespace | string | Prefix added to all keys in this instance (useful for logging/debug) | | defaultTimeout | number (ms) | Timeout for dedup() operations (default: 10000) | | defaultTtl | number (ms) | Default TTL for dedupWithCache() results | | defaultThrottle | number (ms) | Default throttle interval for throttle() | | ttl | object { [key]: ms } | Per-key TTL overrides for dedupWithCache() | | throttle | object { [key]: ms } | Per-key throttle overrides | | debug | boolean | Enables internal logging to console |

📦 Example

const { dedupWithCache, throttle, debounce } = getDeduper({
    namespace: 'search',
    debug: true,
    defaultTtl: 2000,           // Cache everything for 2 seconds unless overridden
    defaultThrottle: 1000,      // Throttle all keys by default to 1s
    defaultTimeout: 5000,       // 5s timeout on dedup tasks

    ttl: {
        'search:common': 5000,  // Keep cached results for this key longer
        'search:rare': 0        // Skip caching for this key
    },

    throttle: {
        'submit': 3000          // Prevent spammy form submissions
    }
});

With the above setup:

  • Calls to dedupWithCache('search:common', ...) will cache for 5s
  • Calls to throttle('submit', ...) will only run once every 3 seconds
  • All operations will be logged to console like:
[dedup] cache_hit: search:common
[dedup] throttling: submit

This gives you fine-grained control with sensible defaults — scale from a few deduped actions to full subsystem constraint handling with minimal code.

🔐 dedup(key, fn, [timeoutMs])

Run an async function once per key — suppress concurrent duplicate calls and return the shared result. Used when you don't want to run the same process multiple times simultaneously. Any subsequent request will wait for the original promise.

const result = await dedup('getUser', () => fetch('/user'));

Use cases:

  • API requests where multiple components may request the same resource
  • Expensive database queries
  • Avoiding "double clicks" or repeated submission triggers

💾 dedupWithCache(key, fn, [ttlMs], [timeoutMs])

Same as dedup, but caches the result for a duration. Once resolved, any subsequent requests will be have the same result returned for ttlMs. Useful for when a call is expensive (such as an API call) but the result is good for some period of time.

await dedupWithCache('search:dogs', () => fetchResults('dogs'), 3000);

Use cases:

  • Debouncing search requests without caching at the API level
  • Local memoization of reads from external sources
  • Fine-tuned TTL logic per key

🚦 throttle(key, fn, [throttleMs], [onThrottleReturnValOrFn])

Ensures a function only runs once per throttle window. If called again too soon:

  • If onThrottleReturnValOrFn is given, it’s used instead
  • Otherwise, throws a ThrottleError
await throttle('submit', () => sendForm(), 1000, () => 'wait');

Use cases:

  • Button spam prevention
  • Rate-limiting specific calls per session
  • Smoothing bursty UI events

🧠 debounce(key, fn, delayMs, [eachFunc])

Can be called multiple times, only calls fn() after no new calls are made within delayMs. Useful for keyboard-entry or other actions.
If eachFunc() is provided, triggers eachFunc() immediately on every call (fire-and-forget)

debounce('search', async () => {
    await doSearch();
}, 300, () => {
    console.log('typing...');
});

Use cases:

  • Debounced text input triggers
  • Delayed feedback actions with real-time updates
  • Avoiding excessive API traffic from rapid triggers

🧹 clear(key) and clearAll()

Manually clear any in-flight dedup, cache, throttle, or debounce tracking.

clear('getUser');
clearAll(); // Clears everything in the instance

🔑 Keys Matter: Deduping Is Based on the Key, Not the Function

When using dedup, throttle, debounce, or dedupWithCache, you provide a key string that identifies the operation.

That key — not the function — is what determines whether the operation is allowed, blocked, reused, or cached.

This means that even if you're passing in a new () => {...} function each time, if the key is the same, the system will treat it as the same operation.

🙋 Why?

JavaScript doesn't give you reliable identity on anonymous or inline functions. So intellawait uses your string key as the unique identifier for what's happening.

Think of the key like a label on the operation — it tells the system "Hey, I'm doing this thing again."

🔁 Example

// These two calls are deduplicated — only one fetch will happen
dedup('get:user:123', () => fetch('/api/user/123'));
dedup('get:user:123', () => fetch('/api/user/123')); // shares result of the first
// These two calls are NOT deduplicated — different keys!
dedup('get:user:123', () => fetch('/api/user/123'));
dedup('get:user:456', () => fetch('/api/user/456')); // new fetch happens

🧠 Pro Tip

You can include dynamic values in the key to make deduping more specific:

dedup(`user:${userId}`, () => fetch(`/api/user/${userId}`));

🧰 Additional Utilities

sleep(ms, [valueOrFn])

Pause execution for a number of milliseconds. Optionally return a value or call a function after the delay.

await sleep(1000);
await sleep(300, () => doThing());

withTimeout(promise, ms, fallback)

Runs a promise with a timeout fallback.

await withTimeout(fetchData(), 500, () => getCachedData());

retry(fn, options)

Retries an async function until it succeeds (up to n retries) with optional backoff and error handling.

await retry(() => maybeFails(), {
    retries: 3,
    delay: 200,
    backoff: 1.5,
    onError: (err, attempt) => console.warn(err),
    retriesExhausted: () => 'fallback'
});

📦 Example

import { getDeduper, sleep, retry } from 'intellawait';

const { dedup, throttle, debounce } = getDeduper({ namespace: 'user', debug: true });

async function getUser(id) {
    return await dedup(`user:${id}`, async () => {
        return await fetch(`/api/user/${id}`).then(r => r.json());
    });
}

function onUserInput(input) {
    debounce('search', async () => {
        const results = await fetch(`/api/search?q=${input}`).then(r => r.json());
        showResults(results);
    }, 400, () => showTypingIndicator());
}

🐞 Debugging and Logging

You can enable internal logging by passing a debug option to getDeduper(config).

Simple Mode (Console Logging)

const deduper = getDeduper({ debug: true });

This will print events like cache hits, throttling, or clearing to the console:

[dedup] cache_hit: search:dogs
[dedup] throttling: submit
[dedup] clearing: user:123

Advanced Mode (Custom Logger)

You can pass a function instead of true to receive structured log events:

const deduper = getDeduper({
    debug: ({ event, namespace, key, fullkey, msg, fullmsg }) => {
        myLogger.debug({ event, key: fullkey, msg });
    }
});

This allows you to:

  • Integrate with your own logging or monitoring system
  • Format output differently for dev vs prod
  • Capture logs to a file, buffer, or remote service

--

License

LGPL-2.1-or-later © Jay Kuri