Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanatkn committed Oct 5, 2024
1 parent 9fea926 commit cf6dfa7
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 11 deletions.
32 changes: 32 additions & 0 deletions src/lib/throttle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,38 @@ test('throttles calls to a function', async () => {
assert.equal(results, ['a_run', 'a_done', 'd_run', 'd_done']);
});

test('throttles calls to a non-async function', async () => {
const results: string[] = [];
const fn = throttle((name: string) => {
results.push(name + '_ran');
});
assert.is('ok', ' o k');

const promise_a = fn('a'); // called immediately
const promise_b = fn('b'); // discarded
const promise_c = fn('c'); // discarded
const promise_d = fn('d'); // called at trailing edge

assert.is(promise_a, undefined);
assert.is(promise_b, undefined);
assert.is(promise_c, undefined);
assert.is(promise_d, undefined);

assert.equal(results, ['a_ran']); // called immediately

await promise_a;

assert.equal(results, ['a_ran']);

await wait();

assert.equal(results, ['a_ran', 'd_ran']);

await promise_b;

assert.equal(results, ['a_ran', 'd_ran']);
});

test('calls functions in sequence', async () => {
const results: string[] = [];
const fn = throttle(async (name: string) => {
Expand Down
30 changes: 19 additions & 11 deletions src/lib/throttle.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {create_deferred, type Deferred} from '$lib/async.js';
import {create_deferred, is_promise, wait, type Deferred} from '$lib/async.js';

export interface Throttle_Options {
/**
Expand All @@ -25,7 +25,8 @@ export interface Throttle_Options {
* It also differs from a queue where every call to the throttled callback eventually runs.
* @returns same as `cb`
*/
export const throttle = <T extends (...args: any[]) => Promise<void>>(
export const throttle = <T extends (...args: any[]) => U, U>(
// TODO BLOCK `Promise<void> | void`, and generic return type?
cb: T,
options?: Throttle_Options,
): T => {
Expand All @@ -34,32 +35,39 @@ export const throttle = <T extends (...args: any[]) => Promise<void>>(

let pending_promise: Promise<void> | null = null;
let next_args: any[] | null = null;
let next_deferred: Deferred<void> | null = null;
let next_deferred: Deferred<U> | null = null;

const defer = (args: any[]): Promise<void> => {
const defer = (args: any[]): Promise<U> => {
next_args = args;
if (!next_deferred) {
next_deferred = create_deferred<void>();
next_deferred = create_deferred<U>();
setTimeout(flush, delay);
}
// TODO BLOCK does it make sense to have `void` be generic?
return next_deferred.promise;
};

const flush = async (): Promise<void> => {
// TODO BLOCK test to ensure it calls a single time for a single call (not trailing unless called twice)
const flush = (): void => {
if (!next_deferred) return;
const result = await call(next_args!);
const result = call(next_args!);
next_args = null;
const {resolve} = next_deferred;
next_deferred = null;
resolve(result); // resolve last to prevent synchronous call issues
if (is_promise(result)) {
void result.then(resolve);
} else {
resolve(result);
} // resolve last to prevent synchronous call issues
};

const call = (args: any[]): Promise<any> => {
pending_promise = cb(...args);
const call = (args: any[]): U => {
const result = cb(...args); // TODO BLOCK for void return value, ` || wait(delay)`, but returning this promise breaks the type of the throttled function
pending_promise = is_promise(result) ? result : wait(delay);
void pending_promise.finally(() => {
pending_promise = null;
});
return pending_promise;
return result;
};

return ((...args) => {
Expand Down

0 comments on commit cf6dfa7

Please sign in to comment.