intellawait

1.0.1 • Public • Published

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

Package Sidebar

Install

npm i intellawait

Weekly Downloads

2

Version

1.0.1

License

LGPL-2.1-or-later

Unpacked Size

23 kB

Total Files

4

Last publish

Collaborators

  • jayk