Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add async utils: callbackify, promisify, debounce, throttle #276

Open
wants to merge 2 commits into
base: async-utils
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## [Unreleased][unreleased]

- Added `throttle(fn, interval, ...presetArgs): Function`
- Executes a given function at most once per specified interval, even if it's called multiple times.
- Added `debounce(fn, delay, ...args): Function`
- Delays the execution of a function until a specified delay has elapsed since the last time it was invoked.
- Added `callbackify(asyncFn): Function`
- Converts an async function into a callback-style function (Node.js-style).
- Added `asyncify(fn): Function`
- Converts a synchronous function into a callback-style asynchronous function.
- Added `promisify(fn): Function`
- Converts a callback-style function into a promise-based function.

## [5.2.4][] - 2024-09-12

- Update eslint/prettier/metarhia configs
Expand Down
95 changes: 86 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,92 @@

## Async utilities

- `toBool = [() => true, () => false]`
- Example: `const created = await mkdir(path).then(...toBool);`
- `timeout(msec: number, signal?: AbortSignal): Promise<void>`
- `delay(msec: number, signal?: AbortSignal): Promise<void>`
- `timeoutify(promise: Promise<unknown>, msec: number): Promise<unknown>`
- `collect(keys: Array<string>, options?: CollectorOptions): Collector`
- `options.exact?: boolean`
- `options.timeout?: number`
- `options.reassign?: boolean`
- **toBool = [() => true, () => false]**

- Example:
```javascript
const created = await mkdir(path).then(...toBool);
```

- **timeout(msec: number, signal?: AbortSignal): Promise<void>**

- Creates a promise that resolves after `msec` milliseconds or rejects if `signal` is aborted.

- **delay(msec: number, signal?: AbortSignal): Promise<void>**

- Delays the execution of a promise for a specified number of milliseconds, optionally cancelable via `signal`.

- **timeoutify(promise: Promise<unknown>, msec: number): Promise<unknown>**

- Adds a timeout to an existing promise. If the promise doesn't resolve or reject within the given `msec`, it rejects with a timeout error.

- **throttle(fn: Function, interval: number, ...presetArgs: Array<unknown>): Function**

- Executes a given function at most once per specified interval, even if it's called multiple times.
- Example:

```javascript
const log = (msg) => console.log(msg);
const throttledLog = throttle(log, 2000);

throttledLog('Hello'); // Logs: "Hello"
throttledLog('World'); // Ignored if called within 2 seconds of the last call
```

- **debounce(fn: Function, delay: number, ...args: Array<unknown>): Function**

- Delays the execution of a function until a specified delay has elapsed since the last time it was invoked.
- Example:

```javascript
const log = (msg) => console.log(msg);
const debouncedLog = debounce(log, 1000);

debouncedLog('Hello'); // Logs: "Hello" after 1 second
debouncedLog('World'); // Resets the timer, only "World" is logged after 1 second
```

- **callbackify(asyncFn: Function): Function**

- Converts an async function into a callback-style function (Node.js-style).
- Example:

```javascript
const asyncAdd = async (a, b) => a + b;
const callbackAdd = callbackify(asyncAdd);

callbackAdd(2, 3, (err, result) => {
if (err) console.error(err);
else console.log(result); // Logs: 5
});
```

- **asyncify(fn: Function): Function**

- Converts a synchronous function into a callback-style asynchronous function.
- Example:

```javascript
const syncAdd = (a, b) => a + b;
const asyncAdd = asyncify(syncAdd);

asyncAdd(2, 3, (err, result) => {
if (err) console.error(err);
else console.log(result); // Logs: 5
});
```

- **promisify(fn: Function): Function**

- Converts a callback-style function into a promise-based function.
- Example:

```javascript
const callbackAdd = (a, b, callback) => callback(null, a + b);
const promiseAdd = promisify(callbackAdd);

promiseAdd(2, 3).then(console.log).catch(console.error); // Logs: 5
```

## Class `Collector`

Expand Down
71 changes: 29 additions & 42 deletions lib/async.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,69 +46,56 @@ const timeoutify = (promise, msec) =>
);
});

const throttle = (timeout, fn, ...args) => {
let timer;
let wait = false;

const execute = args
? (...pars) => (pars ? fn(...args, ...pars) : fn(...args))
: (...pars) => (pars ? fn(...pars) : fn());

const delayed = (...pars) => {
timer = undefined;
if (wait) execute(...pars);
};

const throttled = (...pars) => {
if (!timer) {
timer = setTimeout(delayed, timeout, ...pars);
wait = false;
execute(...pars);
const throttle = (fn, interval, ...presetArgs) => {
if (typeof interval !== 'number' || interval < 0) {
throw new Error('Interval must be greater then 0');
}
let lastTime = 0;
return (...args) => {
const now = Date.now();
if (now - lastTime < interval) return;
try {
fn(...presetArgs, ...args);
} finally {
lastTime = now;
}
wait = true;
};

return throttled;
};

const debounce = (timeout, fn, ...args) => {
let timer;

const debounced = () => (args ? fn(...args) : fn());

const wrapped = () => {
const debounce = (fn, delay, ...args) => {
let timer = null;
return () => {
if (timer) clearTimeout(timer);
timer = setTimeout(debounced, timeout);
timer = setTimeout(() => {
fn(...args);
timer = null;
}, delay);
};
Comment on lines +67 to 73
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use named functions as in prev implementation, it is much more readable than unnamed arrows


return wrapped;
};

const callbackify =
(fn) =>
(asyncFn) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer fn

(...args) => {
const callback = args.pop();
fn(...args)
.then((value) => {
callback(null, value);
})
.catch((reason) => {
callback(reason);
});
if (typeof callback !== 'function' || callback.length !== 2) {
throw new Error('Last argument should be a function with 2 parameters');
}
Comment on lines +80 to +82
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not need such a check, callback may handle just first argument error and will be thrown while it is ok

asyncFn(...args)
.then((res) => callback(null, res))
.catch((err) => callback(err));
Comment on lines +83 to +85
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
asyncFn(...args)
.then((res) => callback(null, res))
.catch((err) => callback(err));
fn(...args)
.then((res) => void callback(null, res), (err) => void callback(err));

};

const asyncify =
(fn) =>
(...args) => {
const callback = args.pop();
setTimeout(() => {
let result;
try {
result = fn(...args);
const result = fn(...args);
callback(null, result);
} catch (error) {
return void callback(error);
callback(error);
}
callback(null, result);
}, 0);
};

Expand Down
17 changes: 17 additions & 0 deletions metautil.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ export function timeoutify(
msec: number,
): Promise<unknown>;

type Callback = (...args: Array<unknown>) => unknown;
export function callbackify(asyncFunction: AsyncFunction): Callback;
export function asyncify(
syncFunction: Function,
): (...args: Array<unknown>) => void;
export function promisify(asyncFunction: AsyncFunction): Promise<unknown>;
export function debounce(
fn: Function,
delay: number,
...args: Array<unknown>
): void;
export function throttle(
fn: Function,
interval: number,
...args: Array<unknown>
): Function;

// Submodule: crypto

export function cryptoRandom(min?: number, max?: number): number;
Expand Down
Loading