Skip to content

danielhaim1/Modulator

Repository files navigation

Modulator

npm version Downloads GitHub Build Distribution Deploy Docs TypeScript definitions

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

Modulator Demo

API Documentation

Installation

npm install @danielhaim/modulator # or yarn add @danielhaim/modulator

Usage

ES Modules (Recommended)

import { modulate } from '@danielhaim/modulator'; // or import default Modulator from '@danielhaim/modulator'; // If using the object wrapper (less common now) async function myAsyncFunction(query) { console.log('Executing with:', query); // Simulate work 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)) // Logs 'Success: Result for query1' after 300ms .catch(error => console.error('Caught:', error)); debouncedFunc('fail') .then(result => console.log('Success:', result)) .catch(error => console.error('Caught:', error)); // Logs 'Caught: Error: Failed!' after 300ms // Using async/await 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(/* ... */); // ... usage is the same

Browser (UMD / Direct Script)

Include the UMD build:

<!-- Download dist/modulator.umd.js or use a CDN like jsDelivr/unpkg --> <script src="path/to/modulator.umd.js"></script> <script> // Modulator is available globally 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

Name Type Attributes Default Description
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); // e.g., make API call } // Debounce to run only 500ms after the user stops typing const debouncedHandleInput = modulate(handleInput, 500); searchInput.addEventListener('input', (event) => { debouncedHandleInput(event.target.value) .catch(err => console.error("Input Error:", err)); // Optional: Catch potential errors });

Immediate Execution (Leading Edge)

function handleClick() { console.log('Button clicked!'); // Perform action immediately, but prevent rapid re-clicks } // Trigger immediately, then ignore calls for 1000ms const debouncedClick = modulate(handleClick, 1000, true); myButton.addEventListener('click', () => { debouncedClick().catch(err => { // Only log if it's not a cancellation error, as we don't cancel here if (err.message !== 'Debounced function call was cancelled.') { console.error("Click Error:", err); } }); });

Handling Promise Results & Errors

async function searchAPI(query) { if (!query) return []; // Handle empty query 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'); // Assume element exists const searchInput = document.getElementById('search-input'); // Assume element exists searchInput.addEventListener('input', async (event) => { const query = event.target.value; statusElement.textContent = 'Searching...'; try { // debouncedSearch returns a promise here const results = await debouncedSearch(query); // Check if query is still relevant before updating UI if (query === searchInput.value) { statusElement.textContent = `Found ${results.length} results.`; // Update UI with results } else { console.log("Query changed, ignoring results for:", query); } } catch (error) { // Handle errors from searchAPI OR cancellation errors if (error.message === 'Debounced function call was cancelled.') { console.log('Search cancelled.'); // Status might already be 'Searching...' which is fine } else { console.error('Search failed:', error); statusElement.textContent = `Error: ${error.message}`; } } }); // Example of cancellation (Alternative approach combining input/cancel) let currentQuery = ''; searchInput.addEventListener('input', async (event) => { const query = event.target.value; currentQuery = query; statusElement.textContent = 'Typing...'; // Cancel any previous pending search before starting a new one debouncedSearch.cancel(); // Cancel previous timer/promise if (!query) { // Handle empty input immediately statusElement.textContent = 'Enter search term.'; // Clear results UI return; } // Only proceed if query is not empty after debounce period try { statusElement.textContent = 'Waiting...'; // Indicate waiting for debounce // Start new search (will wait 400ms unless cancelled again) const results = await debouncedSearch(query); // New promise for this call // Re-check if the query changed *after* the await completed if (query === currentQuery) { statusElement.textContent = `Found ${results.length} results.`; // Update UI } else { console.log('Results ignored, query changed.'); // Status might remain 'Typing...' from next input event } } catch (error) { // Handle errors from the awaited promise if (error.message !== 'Debounced function call was cancelled.') { console.error('Search failed:', error); statusElement.textContent = `Error: ${error.message}`; } else { // Ignore cancellation errors here as we trigger cancel often console.log('Search promise cancelled.'); } } });

Using maxWait

function saveData() { console.log('Saving data to server...'); // API call to save return Promise.resolve({ status: 'Saved' }); // Example return } // Debounce saving by 1 second, but ensure it saves // at least once every 5 seconds even if user keeps typing. const debouncedSave = modulate(saveData, 1000, false, null, 0, 5000); // No cache, maxWait 5s const saveStatus = document.getElementById('save-status'); // Assume element exists const textArea = document.getElementById('my-textarea'); // Assume element exists textArea.addEventListener('input', () => { saveStatus.textContent = 'Changes detected, waiting to save...'; debouncedSave() .then(result => { // Check if still relevant (optional) 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."); // Status remains 'waiting...' or might be updated by next input } }); });

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.

About

An advanced debouncing utility designed to optimize high-frequency events in web applications, such as scroll, resize, and input.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published