fix(core): Add retry mechanism to tools (#16667)

This commit is contained in:
Benjamin Schroth
2025-06-26 13:11:41 +02:00
committed by GitHub
parent f690ed5e97
commit 9e61d0b9c0
7 changed files with 1056 additions and 88 deletions

View File

@@ -31,6 +31,7 @@ export {
jsonStringify,
replaceCircularReferences,
sleep,
sleepWithAbort,
fileTypeFromMimeType,
assert,
removeCircularRefs,

View File

@@ -9,6 +9,7 @@ import merge from 'lodash/merge';
import { ALPHABET } from './constants';
import { ApplicationError } from './errors/application.error';
import { ExecutionCancelledError } from './errors/execution-cancelled.error';
import type { BinaryFileType, IDisplayOptions, INodeProperties, JsonObject } from './interfaces';
const readStreamClasses = new Set(['ReadStream', 'Readable', 'ReadableStream']);
@@ -199,6 +200,23 @@ export const sleep = async (ms: number): Promise<void> =>
setTimeout(resolve, ms);
});
export const sleepWithAbort = async (ms: number, abortSignal?: AbortSignal): Promise<void> =>
await new Promise((resolve, reject) => {
if (abortSignal?.aborted) {
reject(new ExecutionCancelledError(''));
return;
}
const timeout = setTimeout(resolve, ms);
const abortHandler = () => {
clearTimeout(timeout);
reject(new ExecutionCancelledError(''));
};
abortSignal?.addEventListener('abort', abortHandler, { once: true });
});
export function fileTypeFromMimeType(mimeType: string): BinaryFileType | undefined {
if (mimeType.startsWith('application/json')) return 'json';
if (mimeType.startsWith('text/html')) return 'html';

View File

@@ -1,5 +1,6 @@
import { ALPHABET } from '@/constants';
import { ApplicationError } from '@/errors/application.error';
import { ExecutionCancelledError } from '@/errors/execution-cancelled.error';
import {
jsonParse,
jsonStringify,
@@ -11,6 +12,7 @@ import {
hasKey,
isSafeObjectProperty,
setSafeObjectProperty,
sleepWithAbort,
} from '@/utils';
describe('isObjectEmpty', () => {
@@ -394,3 +396,68 @@ describe('setSafeObjectProperty', () => {
expect(obj).toEqual(expected);
});
});
describe('sleepWithAbort', () => {
it('should resolve after the specified time when not aborted', async () => {
const start = Date.now();
await sleepWithAbort(100);
const end = Date.now();
const elapsed = end - start;
// Allow some tolerance for timing
expect(elapsed).toBeGreaterThanOrEqual(90);
expect(elapsed).toBeLessThan(200);
});
it('should reject immediately if abort signal is already aborted', async () => {
const abortController = new AbortController();
abortController.abort();
await expect(sleepWithAbort(1000, abortController.signal)).rejects.toThrow(
ExecutionCancelledError,
);
});
it('should reject when abort signal is triggered during sleep', async () => {
const abortController = new AbortController();
// Start the sleep and abort after 50ms
setTimeout(() => abortController.abort(), 50);
const start = Date.now();
await expect(sleepWithAbort(1000, abortController.signal)).rejects.toThrow(
ExecutionCancelledError,
);
const end = Date.now();
const elapsed = end - start;
// Should have been aborted after ~50ms, not the full 1000ms
expect(elapsed).toBeLessThan(200);
});
it('should work without abort signal', async () => {
const start = Date.now();
await sleepWithAbort(100, undefined);
const end = Date.now();
const elapsed = end - start;
expect(elapsed).toBeGreaterThanOrEqual(90);
expect(elapsed).toBeLessThan(200);
});
it('should clean up timeout when aborted during sleep', async () => {
const abortController = new AbortController();
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
// Start the sleep and abort after 50ms
const sleepPromise = sleepWithAbort(1000, abortController.signal);
setTimeout(() => abortController.abort(), 50);
await expect(sleepPromise).rejects.toThrow(ExecutionCancelledError);
// clearTimeout should have been called to clean up
expect(clearTimeoutSpy).toHaveBeenCalled();
clearTimeoutSpy.mockRestore();
});
});