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' });
const user = await dedup(`user:${id}`, () => fetch(`/api/user/${id}`));
await throttle('search', () => fetch(`/api/search?q=${query}`), 1000);
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:
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,
defaultThrottle: 1000,
defaultTimeout: 5000,
ttl: {
'search:common': 5000,
'search:rare': 0
},
throttle: {
'submit': 3000
}
});
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();
🔑 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
dedup('get:user:123', () => fetch('/api/user/123'));
dedup('get:user:123', () => fetch('/api/user/123'));
dedup('get:user:123', () => fetch('/api/user/123'));
dedup('get:user:456', () => fetch('/api/user/456'));
🧠 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