implement callAllAsync and callAllOnAsync utilities
This commit is contained in:
parent
c777b79431
commit
6387b36dc3
2 changed files with 185 additions and 4 deletions
|
|
@ -3,8 +3,10 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { promiseTry } from '@/misc/promise-try.js';
|
||||
|
||||
/**
|
||||
* Calls a group of functions with the given parameters.
|
||||
* Calls a group of synchronous functions with the given parameters.
|
||||
* Errors are suppressed and aggregated, ensuring that nothing is thrown until all calls have completed.
|
||||
* This ensures that an error in one callback does not prevent later callbacks from completing.
|
||||
* @param funcs Callback functions to execute
|
||||
|
|
@ -27,7 +29,33 @@ export function callAll<T extends unknown[]>(funcs: Iterable<(...args: T) => voi
|
|||
}
|
||||
|
||||
/**
|
||||
* Calls a single method across a group of object, passing the given parameters as values.
|
||||
* Calls a group of async functions with the given parameters.
|
||||
* Errors are suppressed and aggregated, ensuring that nothing is thrown until all calls have completed.
|
||||
* This ensures that an error in one callback does not prevent later callbacks from completing.
|
||||
* Callbacks are executed in parallel using Promise.allSettled().
|
||||
* @param funcs Callback functions to execute
|
||||
* @param args Arguments to pass to each callback
|
||||
*/
|
||||
export async function callAllAsync<T extends unknown[]>(funcs: Iterable<(...args: T) => Promise<void> | void>, ...args: T): Promise<void> {
|
||||
// Start all the tasks
|
||||
const promises = Array.from(funcs)
|
||||
.map(func => {
|
||||
// Handle errors thrown synchronously
|
||||
return promiseTry(() => func(...args));
|
||||
});
|
||||
|
||||
// Wait for all to finish
|
||||
const results = await Promise.allSettled(promises);
|
||||
|
||||
// Check for errors
|
||||
const errors = results.filter(r => r.status === 'rejected').map(r => r.reason as unknown);
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a single synchronous method across a group of object, passing the given parameters as values.
|
||||
* Errors are suppressed and aggregated, ensuring that nothing is thrown until all calls have completed.
|
||||
* This ensures that an error in one callback does not prevent later callbacks from completing.
|
||||
* @param objects Objects to execute methods on
|
||||
|
|
@ -51,6 +79,36 @@ export function callAllOn<TObject, TMethod extends MethodKeys<TObject>>(objects:
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a single asynchronous method across a group of object, passing the given parameters as values.
|
||||
* Errors are suppressed and aggregated, ensuring that nothing is thrown until all calls have completed.
|
||||
* This ensures that an error in one callback does not prevent later callbacks from completing.
|
||||
* Callbacks are executed in parallel using Promise.allSettled().
|
||||
* @param objects Objects to execute methods on
|
||||
* @param method Name (property key) of the method to execute
|
||||
* @param args Arguments to pass
|
||||
*/
|
||||
export async function callAllOnAsync<TObject, TMethod extends MethodKeys<TObject>>(objects: Iterable<TObject>, method: TMethod, ...args: MethodParams<TObject, TMethod>): Promise<void> {
|
||||
// Start all the tasks
|
||||
const promises = Array.from(objects)
|
||||
.map(object => {
|
||||
// Handle errors thrown synchronously
|
||||
return promiseTry(() => {
|
||||
// @ts-expect-error Our generic constraints ensure this is safe, but TS can't infer that much context.
|
||||
return object[method](...args);
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for all to finish
|
||||
const results = await Promise.allSettled(promises);
|
||||
|
||||
// Check for errors
|
||||
const errors = results.filter(r => r.status === 'rejected').map(r => r.reason as unknown);
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(errors);
|
||||
}
|
||||
}
|
||||
|
||||
type AnyFunc = (...args: unknown[]) => unknown;
|
||||
type Methods<TObject> = {
|
||||
[Key in keyof TObject]: TObject[Key] extends AnyFunc ? TObject[Key] : never;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import { callAll, callAllOn } from '@/misc/call-all.js';
|
||||
import * as assert from '../../misc/custom-assertions.js';
|
||||
import { callAll, callAllOn, callAllAsync, callAllOnAsync } from '@/misc/call-all.js';
|
||||
|
||||
describe(callAll, () => {
|
||||
it('should call all functions when all succeed', () => {
|
||||
|
|
@ -56,7 +57,9 @@ describe(callAll, () => {
|
|||
jest.fn(() => {}),
|
||||
];
|
||||
|
||||
expect(() => callAll(funcs)).toThrow();
|
||||
assert.throws(AggregateError, () => {
|
||||
callAll(funcs);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not throw when input is empty', () => {
|
||||
|
|
@ -64,6 +67,66 @@ describe(callAll, () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe(callAllAsync, () => {
|
||||
it('should call all functions when all succeed', async () => {
|
||||
const funcs = [
|
||||
jest.fn(() => Promise.resolve()),
|
||||
jest.fn(() => Promise.resolve()),
|
||||
jest.fn(() => Promise.resolve()),
|
||||
];
|
||||
|
||||
await callAllAsync(funcs);
|
||||
|
||||
for (const func of funcs) {
|
||||
expect(func).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should pass parameters to all functions', async () => {
|
||||
const funcs = [
|
||||
jest.fn((num: number) => expect(num).toBe(1)),
|
||||
jest.fn((num: number) => expect(num).toBe(1)),
|
||||
jest.fn((num: number) => expect(num).toBe(1)),
|
||||
];
|
||||
|
||||
await callAllAsync(funcs, 1);
|
||||
});
|
||||
|
||||
it('should call all functions when some fail', async () => {
|
||||
const funcs = [
|
||||
jest.fn(() => Promise.reject(new Error())),
|
||||
jest.fn(() => Promise.resolve()),
|
||||
jest.fn(() => Promise.resolve()),
|
||||
];
|
||||
|
||||
try {
|
||||
await callAllAsync(funcs);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
for (const func of funcs) {
|
||||
expect(func).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw when some functions fail', async () => {
|
||||
const funcs = [
|
||||
jest.fn(() => Promise.reject(new Error())),
|
||||
jest.fn(() => Promise.resolve()),
|
||||
jest.fn(() => Promise.resolve()),
|
||||
];
|
||||
|
||||
await assert.throwsAsync(AggregateError, async () => {
|
||||
await callAllAsync(funcs);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not throw when input is empty', async () => {
|
||||
await callAllAsync([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(callAllOn, () => {
|
||||
it('should call all methods when all succeed', () => {
|
||||
const objects = [
|
||||
|
|
@ -121,3 +184,63 @@ describe(callAllOn, () => {
|
|||
expect(() => callAllOn([] as { foo: () => void }[], 'foo')).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe(callAllOnAsync, () => {
|
||||
it('should call all methods when all succeed', async () => {
|
||||
const objects = [
|
||||
{ foo: jest.fn(() => Promise.resolve()) },
|
||||
{ foo: jest.fn(() => Promise.resolve()) },
|
||||
{ foo: jest.fn(() => Promise.resolve()) },
|
||||
];
|
||||
|
||||
await callAllOnAsync(objects, 'foo');
|
||||
|
||||
for (const object of objects) {
|
||||
expect(object.foo).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should pass parameters to all methods', async () => {
|
||||
const objects = [
|
||||
{ foo: jest.fn((num: number) => expect(num).toBe(1)) },
|
||||
{ foo: jest.fn((num: number) => expect(num).toBe(1)) },
|
||||
{ foo: jest.fn((num: number) => expect(num).toBe(1)) },
|
||||
];
|
||||
|
||||
await callAllOnAsync(objects, 'foo', 1);
|
||||
});
|
||||
|
||||
it('should call all methods when some fail', async () => {
|
||||
const objects = [
|
||||
{ foo: jest.fn(() => Promise.resolve()) },
|
||||
{ foo: jest.fn(() => Promise.resolve()) },
|
||||
{ foo: jest.fn(() => Promise.resolve()) },
|
||||
];
|
||||
|
||||
try {
|
||||
await callAllOnAsync(objects, 'foo');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
for (const object of objects) {
|
||||
expect(object.foo).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw when some methods fail', async () => {
|
||||
const objects = [
|
||||
{ foo: jest.fn(() => Promise.reject(new Error())) },
|
||||
{ foo: jest.fn(() => Promise.resolve()) },
|
||||
{ foo: jest.fn(() => Promise.resolve()) },
|
||||
];
|
||||
|
||||
await assert.throwsAsync(AggregateError, async () => {
|
||||
await callAllOnAsync(objects, 'foo');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not throw when input is empty', async () => {
|
||||
await callAllOnAsync([] as { foo: () => void }[], 'foo');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue