188 lines
6.0 KiB
TypeScript
188 lines
6.0 KiB
TypeScript
/**
|
|
* Creates a debounced version of an async function.
|
|
*
|
|
* A debounced function will only execute after a specified delay has passed
|
|
* without the function being called again. This is particularly useful for
|
|
* functions that make API calls in response to user input, to avoid making
|
|
* too many calls when a user is actively typing or interacting.
|
|
*
|
|
* @param fn The async function to debounce
|
|
* @param wait The time to wait in milliseconds before the function is called
|
|
* @returns A debounced version of the input function
|
|
*
|
|
* @example
|
|
* // Create a debounced version of an API call function
|
|
* const debouncedFetch = asyncDebounce(fetchFromAPI, 300);
|
|
*
|
|
* // Use the debounced function in an input handler
|
|
* const handleInputChange = (e) => {
|
|
* debouncedFetch(e.target.value)
|
|
* .then(result => setData(result))
|
|
* .catch(error => setError(error));
|
|
* };
|
|
*/
|
|
export function asyncDebounce<A extends unknown[], R>(
|
|
fn: (...args: A) => Promise<R>,
|
|
wait: number
|
|
): (...args: A) => Promise<R> {
|
|
let lastTimeoutId: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
|
|
return (...args: A): Promise<R> => {
|
|
// Clear any existing timeout to cancel pending executions
|
|
clearTimeout(lastTimeoutId);
|
|
|
|
// Return a promise that will resolve with the function's result
|
|
return new Promise((resolve, reject) => {
|
|
// Create a new timeout
|
|
const currentTimeoutId = setTimeout(async () => {
|
|
try {
|
|
// Only execute if this is still the most recent timeout
|
|
if (currentTimeoutId === lastTimeoutId) {
|
|
const result = await fn(...args);
|
|
resolve(result);
|
|
}
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
}, wait);
|
|
|
|
// Store the current timeout ID
|
|
lastTimeoutId = currentTimeoutId;
|
|
});
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Throttles an async function to be called at most once per specified period.
|
|
*
|
|
* Unlike debounce which resets the timer on each call, throttle will ensure the
|
|
* function is called at most once in the specified period, regardless of how many
|
|
* times the throttled function is called.
|
|
*
|
|
* @param fn The async function to throttle
|
|
* @param limit The minimum time in milliseconds between function executions
|
|
* @returns A throttled version of the input function
|
|
*
|
|
* @example
|
|
* // Create a throttled version of an API call function
|
|
* const throttledSave = asyncThrottle(saveToAPI, 1000);
|
|
*
|
|
* // Use the throttled function in an input handler
|
|
* const handleInputChange = (e) => {
|
|
* throttledSave(e.target.value)
|
|
* .then(() => setSaveStatus('Saved'))
|
|
* .catch(error => setSaveStatus('Error saving'));
|
|
* };
|
|
*/
|
|
export function asyncThrottle<A extends unknown[], R>(
|
|
fn: (...args: A) => Promise<R>,
|
|
limit: number
|
|
): (...args: A) => Promise<R> {
|
|
let lastRun = 0;
|
|
let lastPromise: Promise<R> | null = null;
|
|
let pending = false;
|
|
let lastArgs: A | null = null;
|
|
|
|
const execute = async (...args: A): Promise<R> => {
|
|
lastRun = Date.now();
|
|
pending = false;
|
|
return await fn(...args);
|
|
};
|
|
|
|
return (...args: A): Promise<R> => {
|
|
lastArgs = args;
|
|
|
|
// If we're not pending and it's been longer than the limit since the last run,
|
|
// execute immediately
|
|
if (!pending && Date.now() - lastRun >= limit) {
|
|
return execute(...args);
|
|
}
|
|
|
|
// If we don't have a promise or we're not pending, create a new promise
|
|
if (!lastPromise || !pending) {
|
|
pending = true;
|
|
lastPromise = new Promise<R>((resolve, reject) => {
|
|
setTimeout(async () => {
|
|
try {
|
|
// Make sure we're using the most recent args
|
|
if (lastArgs) {
|
|
const result = await execute(...lastArgs);
|
|
resolve(result);
|
|
}
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
}, limit - (Date.now() - lastRun));
|
|
});
|
|
}
|
|
|
|
return lastPromise;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extracts a search parameter from a URL and removes it from the URL.
|
|
*
|
|
* Useful for handling one-time parameters like auth tokens or invite codes.
|
|
*
|
|
* @param url The URL object
|
|
* @param param The parameter name to extract
|
|
* @returns The parameter value or null if not found
|
|
*
|
|
* @example
|
|
* // Extract an invite code from the current URL
|
|
* const url = new URL(window.location.href);
|
|
* const inviteCode = extractSearchParam(url, 'invite');
|
|
* // The parameter is now removed from the URL
|
|
*/
|
|
export const extractSearchParam = (url: URL, param: string): string | null => {
|
|
// Get the parameter value
|
|
const val = url.searchParams.get(param);
|
|
|
|
// Remove the parameter from the URL
|
|
url.searchParams.delete(param);
|
|
|
|
// Update the browser history to reflect the URL change without reloading
|
|
if (typeof history !== 'undefined') {
|
|
history.replaceState(null, document.title, url.toString());
|
|
}
|
|
|
|
return val;
|
|
};
|
|
|
|
/**
|
|
* Checks if a function execution is taking too long and returns a timeout result if so.
|
|
*
|
|
* @param fn The async function to execute with timeout
|
|
* @param timeout The maximum time in milliseconds to wait
|
|
* @param timeoutResult The result to return if timeout occurs
|
|
* @returns The function result or timeout result
|
|
*
|
|
* @example
|
|
* // Execute a function with a 5-second timeout
|
|
* const result = await withTimeout(
|
|
* fetchDataFromSlowAPI,
|
|
* 5000,
|
|
* { error: 'Request timed out' }
|
|
* );
|
|
*/
|
|
export async function withTimeout<T, R>(
|
|
fn: () => Promise<T>,
|
|
timeout: number,
|
|
timeoutResult: R
|
|
): Promise<T | R> {
|
|
let timeoutId: ReturnType<typeof setTimeout>;
|
|
|
|
const timeoutPromise = new Promise<R>((resolve) => {
|
|
timeoutId = setTimeout(() => resolve(timeoutResult), timeout);
|
|
});
|
|
|
|
try {
|
|
const result = await Promise.race([fn(), timeoutPromise]);
|
|
clearTimeout(timeoutId);
|
|
return result;
|
|
} catch (error) {
|
|
clearTimeout(timeoutId);
|
|
throw error;
|
|
}
|
|
} |