Modulator

Modulator is an advanced debouncing utility, now written in TypeScript, designed to optimize high-frequency events in web applications (e.g., scroll, resize, input). This standalone solution offers enhanced performance and flexibility compared to basic debouncing functions.
Key features include:
- Promise-based Return: Always returns a Promise that resolves with the result of your function or rejects on error/cancellation.
- Configurable Caching: Optional result caching based on arguments with controllable
maxCacheSize
.
- Immediate Execution: Option (
immediate: true
) to trigger the function on the leading edge.
- Maximum Wait Time: Optional
maxWait
parameter to guarantee execution after a certain period, even with continuous calls.
- Cancellation: A
.cancel()
method to abort pending debounced calls and reject their associated Promise.
- TypeScript Support: Ships with built-in type definitions for a better developer experience.
Demo

API Documentation
Installation
npm install @danielhaim/modulator
yarn add @danielhaim/modulator
Usage
ES Modules (Recommended)
import { modulate } from '@danielhaim/modulator';
async function myAsyncFunction(query) {
console.log('Executing with:', query);
await new Promise(res => setTimeout(res, 50));
if (query === 'fail') throw new Error('Failed!');
return `Result for ${query}`;
}
const debouncedFunc = modulate(myAsyncFunction, 300);
debouncedFunc('query1')
.then(result => console.log('Success:', result))
.catch(error => console.error('Caught:', error));
debouncedFunc('fail')
.then(result => console.log('Success:', result))
.catch(error => console.error('Caught:', error));
async function run() {
try {
const result = await debouncedFunc('query2');
console.log('Async Success:', result);
} catch (error) {
console.error('Async Error:', error);
}
}
run();
CommonJS
const { modulate } = require('@danielhaim/modulator');
const debouncedFunc = modulate();
Browser (UMD / Direct Script)
Include the UMD build:
<script src="path/to/modulator.umd.js"></script>
<script>
const debouncedFunc = Modulator.modulate(myFunction, 200);
myButton.addEventListener('click', async () => {
try {
const result = await debouncedFunc('data');
console.log('Got:', result);
} catch (e) {
console.error('Error:', e);
}
});
</script>
AMD
requirejs(['path/to/modulator.amd'], function(Modulator) {
const debouncedFunc = Modulator.modulate(myFunction, 200);
});
modulate(func, wait, immediate?, context?, maxCacheSize?, maxWait?)
Creates a debounced function that delays invoking func
until after wait
milliseconds have elapsed since the last time the debounced function was invoked.
Returns: DebouncedFunction
- A new function that returns a Promise
. This promise resolves with the return value of the original func
or rejects if func
throws an error, returns a rejected promise, or if the debounced call is cancelled via .cancel()
.
Parameters
func | Function | | | The function to debounce. Can be synchronous or asynchronous (return a Promise). |
wait | number | | | The debouncing wait time in milliseconds. Must be non-negative. |
immediate? | boolean | <optional> | false | If true , triggers func on the leading edge instead of the trailing edge. Subsequent calls within the wait period are ignored until the cooldown finishes. |
context? | object | <optional> | null | The context (this ) to apply when invoking func . Defaults to the context the debounced function is called with. |
maxCacheSize? | number | <optional> | 100 | The maximum number of results to cache based on arguments. Uses JSON.stringify for keys. Set to 0 to disable caching. Must be non-negative. |
maxWait? | number | null | <optional> | null | The maximum time (in ms) func is allowed to be delayed before it's invoked, even if calls keep occurring. Must be >= wait if set. |
Enhanced Functionality
The returned debounced function has an additional method:
debouncedFunc.cancel()
: Cancels any pending invocation of the debounced function. If a call was pending, the Promise
returned by that call will be rejected with an error indicating cancellation. This does not clear the result cache.
Caching
- When
maxCacheSize > 0
, Modulator caches the results (resolved values) of successful func
invocations.
- The cache key is generated using
JSON.stringify(arguments)
. This works well for primitive arguments but may have limitations with complex objects, functions, or circular references.
- If a subsequent call is made with the same arguments (generating the same cache key) while the result is in the cache, the cached result is returned immediately via a resolved Promise, and
func
is not invoked.
- The cache uses a simple Least Recently Used (LRU) eviction strategy: when the cache exceeds
maxCacheSize
, the oldest entry is removed. Accessing a cached item marks it as recently used.
Examples
Basic Debounce (Trailing Edge)
function handleInput(value) {
console.log('Processing input:', value);
}
const debouncedHandleInput = modulate(handleInput, 500);
searchInput.addEventListener('input', (event) => {
debouncedHandleInput(event.target.value)
.catch(err => console.error("Input Error:", err));
});
Immediate Execution (Leading Edge)
function handleClick() {
console.log('Button clicked!');
}
const debouncedClick = modulate(handleClick, 1000, true);
myButton.addEventListener('click', () => {
debouncedClick().catch(err => {
if (err.message !== 'Debounced function call was cancelled.') {
console.error("Click Error:", err);
}
});
});
Handling Promise Results & Errors
async function searchAPI(query) {
if (!query) return [];
console.log(`Searching API for: ${query}`);
const response = await fetch(`/api/search?q=${query}`);
if (!response.ok) throw new Error(`API Error: ${response.statusText}`);
return response.json();
}
const debouncedSearch = modulate(searchAPI, 400);
const statusElement = document.getElementById('search-status');
const searchInput = document.getElementById('search-input');
searchInput.addEventListener('input', async (event) => {
const query = event.target.value;
statusElement.textContent = 'Searching...';
try {
const results = await debouncedSearch(query);
if (query === searchInput.value) {
statusElement.textContent = `Found ${results.length} results.`;
} else {
console.log("Query changed, ignoring results for:", query);
}
} catch (error) {
if (error.message === 'Debounced function call was cancelled.') {
console.log('Search cancelled.');
} else {
console.error('Search failed:', error);
statusElement.textContent = `Error: ${error.message}`;
}
}
});
let currentQuery = '';
searchInput.addEventListener('input', async (event) => {
const query = event.target.value;
currentQuery = query;
statusElement.textContent = 'Typing...';
debouncedSearch.cancel();
if (!query) {
statusElement.textContent = 'Enter search term.';
return;
}
try {
statusElement.textContent = 'Waiting...';
const results = await debouncedSearch(query);
if (query === currentQuery) {
statusElement.textContent = `Found ${results.length} results.`;
} else {
console.log('Results ignored, query changed.');
}
} catch (error) {
if (error.message !== 'Debounced function call was cancelled.') {
console.error('Search failed:', error);
statusElement.textContent = `Error: ${error.message}`;
} else {
console.log('Search promise cancelled.');
}
}
});
Using maxWait
function saveData() {
console.log('Saving data to server...');
return Promise.resolve({ status: 'Saved' });
}
const debouncedSave = modulate(saveData, 1000, false, null, 0, 5000);
const saveStatus = document.getElementById('save-status');
const textArea = document.getElementById('my-textarea');
textArea.addEventListener('input', () => {
saveStatus.textContent = 'Changes detected, waiting to save...';
debouncedSave()
.then(result => {
saveStatus.textContent = `Saved successfully at ${new Date().toLocaleTimeString()}`;
console.log('Save result:', result);
})
.catch(err => {
if (err.message !== 'Debounced function call was cancelled.') {
console.error("Save Error:", err);
saveStatus.textContent = `Save failed: ${err.message}`;
} else {
console.log("Save cancelled.");
}
});
});
Resources
Report Bugs
If you encounter any bugs while using Modulator, please report them to the GitHub issue tracker.
When submitting a bug report, please include as much information as possible, such as:
- Version of Modulator used.
- Browser/Node.js environment and version.
- Steps to reproduce the bug.
- Expected behavior vs. actual behavior.
- Any relevant code snippets.