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
- 🔁 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
, andwithTimeout
helpers
npm install intellawait
import { getDeduper } from 'intellawait';
const { dedup, throttle, debounce } = getDeduper({ namespace: 'api', debug: true });
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);
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 |
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.
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'));
- API requests where multiple components may request the same resource
- Expensive database queries
- Avoiding "double clicks" or repeated submission triggers
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);
- Debouncing search requests without caching at the API level
- Local memoization of reads from external sources
- Fine-tuned TTL logic per key
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');
- Button spam prevention
- Rate-limiting specific calls per session
- Smoothing bursty UI events
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...');
});
- Debounced text input triggers
- Delayed feedback actions with real-time updates
- Avoiding excessive API traffic from rapid triggers
Manually clear any in-flight dedup, cache, throttle, or debounce tracking.
clear('getUser');
clearAll(); // Clears everything in the instance
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.
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."
// 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
You can include dynamic values in the key to make deduping more specific:
dedup(`user:${userId}`, () => fetch(`/api/user/${userId}`));
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());
Runs a promise with a timeout fallback.
await withTimeout(fetchData(), 500, () => getCachedData());
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'
});
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());
}
You can enable internal logging by passing a debug
option to getDeduper(config)
.
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
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
--
LGPL-2.1-or-later © Jay Kuri