-
Notifications
You must be signed in to change notification settings - Fork 445
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add debounce and repeating task to utils
This functionality is required in multiple places so add it to the utils module.
- Loading branch information
1 parent
ad5cfd6
commit 101c808
Showing
6 changed files
with
235 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
export interface CancelableFunction { | ||
(): void | ||
stop(): void | ||
} | ||
|
||
/** | ||
* Returns a function wrapper that will only call the passed function once | ||
* | ||
* Important - the passed function should not throw or reject | ||
*/ | ||
export function debounce (func: () => void | Promise<void>, wait: number): CancelableFunction { | ||
let timeout: ReturnType<typeof setTimeout> | undefined | ||
|
||
const output = function (): void { | ||
const later = function (): void { | ||
timeout = undefined | ||
void func() | ||
} | ||
|
||
clearTimeout(timeout) | ||
timeout = setTimeout(later, wait) | ||
} | ||
output.stop = () => { | ||
clearTimeout(timeout) | ||
} | ||
|
||
return output | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { setMaxListeners } from '@libp2p/interface' | ||
import { anySignal } from 'any-signal' | ||
import type { AbortOptions } from '@libp2p/interface' | ||
|
||
export interface RepeatingTask { | ||
start(): void | ||
stop(): void | ||
} | ||
|
||
export interface RepeatingTaskOptions { | ||
/** | ||
* How long the task is allowed to run before the passed AbortSignal fires an | ||
* abort event | ||
*/ | ||
timeout?: number | ||
|
||
/** | ||
* Whether to schedule the task to run immediately | ||
*/ | ||
runImmediately?: boolean | ||
} | ||
|
||
export function repeatingTask (fn: (options?: AbortOptions) => void | Promise<void>, interval: number, options?: RepeatingTaskOptions): RepeatingTask { | ||
let timeout: ReturnType<typeof setTimeout> | ||
let shutdownController: AbortController | ||
|
||
function runTask (): void { | ||
const opts: AbortOptions = { | ||
signal: shutdownController.signal | ||
} | ||
|
||
if (options?.timeout != null) { | ||
const signal = anySignal([shutdownController.signal, AbortSignal.timeout(options.timeout)]) | ||
setMaxListeners(Infinity, signal) | ||
|
||
opts.signal = signal | ||
} | ||
|
||
Promise.resolve().then(async () => { | ||
await fn(opts) | ||
}) | ||
.catch(() => {}) | ||
.finally(() => { | ||
if (shutdownController.signal.aborted) { | ||
// task has been cancelled, bail | ||
return | ||
} | ||
|
||
// reschedule | ||
timeout = setTimeout(runTask, interval) | ||
}) | ||
} | ||
|
||
let started = false | ||
|
||
return { | ||
start: () => { | ||
if (started) { | ||
return | ||
} | ||
|
||
started = true | ||
shutdownController = new AbortController() | ||
setMaxListeners(Infinity, shutdownController.signal) | ||
|
||
// run now | ||
if (options?.runImmediately === true) { | ||
queueMicrotask(() => { | ||
runTask() | ||
}) | ||
} else { | ||
// run later | ||
timeout = setTimeout(runTask, interval) | ||
} | ||
}, | ||
stop: () => { | ||
clearTimeout(timeout) | ||
shutdownController?.abort() | ||
started = false | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { expect } from 'aegir/chai' | ||
import delay from 'delay' | ||
import { debounce } from '../src/debounce.js' | ||
|
||
describe('debounce', () => { | ||
it('should debounce function', async () => { | ||
let invocations = 0 | ||
const fn = (): void => { | ||
invocations++ | ||
} | ||
|
||
const debounced = debounce(fn, 10) | ||
|
||
debounced() | ||
debounced() | ||
debounced() | ||
debounced() | ||
debounced() | ||
|
||
await delay(500) | ||
|
||
expect(invocations).to.equal(1) | ||
}) | ||
|
||
it('should cancel debounced function', async () => { | ||
let invocations = 0 | ||
const fn = (): void => { | ||
invocations++ | ||
} | ||
|
||
const debounced = debounce(fn, 10000) | ||
|
||
debounced() | ||
debounced() | ||
debounced() | ||
debounced() | ||
debounced() | ||
|
||
debounced.stop() | ||
|
||
await delay(500) | ||
|
||
expect(invocations).to.equal(0) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { expect } from 'aegir/chai' | ||
import delay from 'delay' | ||
import pDefer from 'p-defer' | ||
import { repeatingTask } from '../src/repeating-task.js' | ||
|
||
describe('repeating-task', () => { | ||
it('should repeat a task', async () => { | ||
let count = 0 | ||
|
||
const task = repeatingTask(() => { | ||
count++ | ||
}, 100) | ||
task.start() | ||
|
||
await delay(1000) | ||
|
||
task.stop() | ||
|
||
expect(count).to.be.greaterThan(1) | ||
}) | ||
|
||
it('should run a task immediately', async () => { | ||
let count = 0 | ||
|
||
const task = repeatingTask(() => { | ||
count++ | ||
}, 60000, { | ||
runImmediately: true | ||
}) | ||
task.start() | ||
|
||
await delay(10) | ||
|
||
task.stop() | ||
|
||
expect(count).to.equal(1) | ||
}) | ||
|
||
it('should time out a task', async () => { | ||
const deferred = pDefer() | ||
|
||
const task = repeatingTask((opts) => { | ||
opts?.signal?.addEventListener('abort', () => { | ||
deferred.resolve() | ||
}) | ||
}, 100, { | ||
timeout: 10 | ||
}) | ||
task.start() | ||
|
||
await deferred.promise | ||
task.stop() | ||
}) | ||
|
||
it('should repeat a task that throws', async () => { | ||
let count = 0 | ||
|
||
const task = repeatingTask(() => { | ||
count++ | ||
throw new Error('Urk!') | ||
}, 100) | ||
task.start() | ||
|
||
await delay(1000) | ||
|
||
task.stop() | ||
|
||
expect(count).to.be.greaterThan(1) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters