chore: 清理macOS同步产生的重复文件
详细说明: - 删除了352个带数字后缀的重复文件 - 更新.gitignore防止未来产生此类文件 - 这些文件是由iCloud或其他同步服务冲突产生的 - 不影响项目功能,仅清理冗余文件
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Asserts given condition
|
||||
*/
|
||||
export function assert(condition: unknown, message?: string): asserts condition {
|
||||
if (!condition) {
|
||||
// eslint-disable-next-line n8n-local-rules/no-plain-errors
|
||||
throw new Error(message ?? 'Assertion failed');
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type CallbackFn = (...args: any[]) => any;
|
||||
|
||||
type Payloads<ListenerMap> = {
|
||||
[E in keyof ListenerMap]: unknown;
|
||||
};
|
||||
|
||||
type Listener<Payload> = (payload: Payload) => void;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export interface EventBus<ListenerMap extends Payloads<ListenerMap> = Record<string, any>> {
|
||||
on<EventName extends keyof ListenerMap & string>(
|
||||
eventName: EventName,
|
||||
fn: Listener<ListenerMap[EventName]>,
|
||||
): void;
|
||||
|
||||
once<EventName extends keyof ListenerMap & string>(
|
||||
eventName: EventName,
|
||||
fn: Listener<ListenerMap[EventName]>,
|
||||
): void;
|
||||
|
||||
off<EventName extends keyof ListenerMap & string>(
|
||||
eventName: EventName,
|
||||
fn: Listener<ListenerMap[EventName]>,
|
||||
): void;
|
||||
|
||||
emit<EventName extends keyof ListenerMap & string>(
|
||||
eventName: EventName,
|
||||
event?: ListenerMap[EventName],
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event bus with the given listener map.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const eventBus = createEventBus<{
|
||||
* 'user-logged-in': { username: string };
|
||||
* 'user-logged-out': never;
|
||||
* }>();
|
||||
*/
|
||||
export function createEventBus<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ListenerMap extends Payloads<ListenerMap> = Record<string, any>,
|
||||
>(): EventBus<ListenerMap> {
|
||||
const handlers = new Map<string, CallbackFn[]>();
|
||||
|
||||
return {
|
||||
on(eventName, fn) {
|
||||
let eventFns = handlers.get(eventName);
|
||||
if (!eventFns) {
|
||||
eventFns = [fn];
|
||||
} else {
|
||||
eventFns.push(fn);
|
||||
}
|
||||
handlers.set(eventName, eventFns);
|
||||
},
|
||||
|
||||
once(eventName, fn) {
|
||||
const handler: typeof fn = (payload) => {
|
||||
this.off(eventName, handler);
|
||||
fn(payload);
|
||||
};
|
||||
this.on(eventName, handler);
|
||||
},
|
||||
|
||||
off(eventName, fn) {
|
||||
const eventFns = handlers.get(eventName);
|
||||
if (eventFns) {
|
||||
eventFns.splice(eventFns.indexOf(fn) >>> 0, 1);
|
||||
}
|
||||
},
|
||||
|
||||
emit(eventName, event) {
|
||||
const eventFns = handlers.get(eventName);
|
||||
if (eventFns) {
|
||||
eventFns.slice().forEach((handler) => {
|
||||
handler(event);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { createEventBus } from './event-bus';
|
||||
|
||||
describe('createEventBus()', () => {
|
||||
const eventBus = createEventBus();
|
||||
|
||||
describe('on()', () => {
|
||||
it('should register event handler', () => {
|
||||
const handler = vi.fn();
|
||||
const eventName = 'test';
|
||||
|
||||
eventBus.on(eventName, handler);
|
||||
|
||||
eventBus.emit(eventName, {});
|
||||
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('once()', () => {
|
||||
it('should register event handler', () => {
|
||||
const handler = vi.fn();
|
||||
const eventName = 'test';
|
||||
|
||||
eventBus.once(eventName, handler);
|
||||
|
||||
eventBus.emit(eventName, {});
|
||||
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should unregister event handler after first call', () => {
|
||||
const handler = vi.fn();
|
||||
const eventName = 'test';
|
||||
|
||||
eventBus.once(eventName, handler);
|
||||
|
||||
eventBus.emit(eventName, {});
|
||||
eventBus.emit(eventName, {});
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('off()', () => {
|
||||
it('should register event handler', () => {
|
||||
const handler = vi.fn();
|
||||
const eventName = 'test';
|
||||
|
||||
eventBus.on(eventName, handler);
|
||||
eventBus.off(eventName, handler);
|
||||
|
||||
eventBus.emit(eventName, {});
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('emit()', () => {
|
||||
it('should call handlers with given event', () => {
|
||||
const handlerA = vi.fn();
|
||||
const handlerB = vi.fn();
|
||||
const eventName = 'test';
|
||||
const event = new Event(eventName);
|
||||
|
||||
eventBus.on(eventName, handlerA);
|
||||
eventBus.on(eventName, handlerB);
|
||||
|
||||
eventBus.emit(eventName, event);
|
||||
|
||||
expect(handlerA).toHaveBeenCalledWith(event);
|
||||
expect(handlerB).toHaveBeenCalledWith(event);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* Create an event queue that processes events sequentially.
|
||||
*
|
||||
* @param processEvent - Async function that processes a single event.
|
||||
* @returns A function that enqueues events for processing.
|
||||
*/
|
||||
export function createEventQueue<T>(processEvent: (event: T) => Promise<void>) {
|
||||
// The internal queue holding events.
|
||||
const queue: T[] = [];
|
||||
|
||||
// Flag to indicate whether an event is currently being processed.
|
||||
let processing = false;
|
||||
|
||||
/**
|
||||
* Process the next event in the queue (if not already processing).
|
||||
*/
|
||||
async function processNext(): Promise<void> {
|
||||
if (processing || queue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
processing = true;
|
||||
const currentEvent = queue.shift();
|
||||
|
||||
if (currentEvent !== undefined) {
|
||||
try {
|
||||
await processEvent(currentEvent);
|
||||
} catch (error) {
|
||||
console.error('Error processing event:', error);
|
||||
}
|
||||
}
|
||||
|
||||
processing = false;
|
||||
|
||||
// Recursively process the next event.
|
||||
await processNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue an event and trigger processing.
|
||||
*
|
||||
* @param event - The event to enqueue.
|
||||
*/
|
||||
function enqueue(event: T): void {
|
||||
queue.push(event);
|
||||
void processNext();
|
||||
}
|
||||
|
||||
return { enqueue };
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { createEventQueue } from './event-queue';
|
||||
|
||||
describe('createEventQueue', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should process events in order', async () => {
|
||||
const processedEvents: string[] = [];
|
||||
|
||||
// Create an async handler that pushes events into the processedEvents array.
|
||||
const processEvent = vi.fn(async (event: string) => {
|
||||
processedEvents.push(event);
|
||||
// Simulate asynchronous delay of 10ms.
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
});
|
||||
|
||||
// Create the event queue.
|
||||
const { enqueue } = createEventQueue<string>(processEvent);
|
||||
|
||||
// Enqueue events in a specific order.
|
||||
enqueue('Event 1');
|
||||
enqueue('Event 2');
|
||||
enqueue('Event 3');
|
||||
|
||||
// Advance the timers enough to process all events.
|
||||
// runAllTimersAsync() will run all pending timers and wait for any pending promise resolution.
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(processEvent).toHaveBeenCalledTimes(3);
|
||||
expect(processedEvents).toEqual(['Event 1', 'Event 2', 'Event 3']);
|
||||
});
|
||||
|
||||
it('should handle errors and continue processing', async () => {
|
||||
const processedEvents: string[] = [];
|
||||
const processEvent = vi.fn(async (event: string) => {
|
||||
if (event === 'fail') {
|
||||
throw new Error('Processing error'); // eslint-disable-line n8n-local-rules/no-plain-errors
|
||||
}
|
||||
processedEvents.push(event);
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
});
|
||||
const { enqueue } = createEventQueue<string>(processEvent);
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
enqueue('Event A');
|
||||
enqueue('fail');
|
||||
enqueue('Event B');
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(processEvent).toHaveBeenCalledTimes(3);
|
||||
// 'fail' should cause an error but processing continues.
|
||||
expect(processedEvents).toEqual(['Event A', 'Event B']);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Error processing event:', expect.any(Error));
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not process any events if none are enqueued', async () => {
|
||||
const processEvent = vi.fn(async (_event: string) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
});
|
||||
|
||||
createEventQueue<string>(processEvent);
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
// Did not enqueue any event.
|
||||
expect(processEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should ensure no concurrent processing of events', async () => {
|
||||
let processingCounter = 0;
|
||||
let maxConcurrent = 0;
|
||||
|
||||
const processEvent = vi.fn(async (_event: string) => {
|
||||
processingCounter++;
|
||||
maxConcurrent = Math.max(maxConcurrent, processingCounter);
|
||||
// Simulate asynchronous delay.
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
processingCounter--;
|
||||
});
|
||||
|
||||
const { enqueue } = createEventQueue<string>(processEvent);
|
||||
|
||||
enqueue('A');
|
||||
enqueue('B');
|
||||
enqueue('C');
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
// Throughout processing, maxConcurrent should remain 1.
|
||||
expect(maxConcurrent).toEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
type RetryFn = () => boolean | Promise<boolean>;
|
||||
|
||||
/**
|
||||
* A utility that retries a function every `interval` milliseconds
|
||||
* until the function returns true or the maximum number of retries is reached.
|
||||
*
|
||||
* @param fn - A function that returns a boolean or a Promise resolving to a boolean.
|
||||
* @param interval - The time interval (in milliseconds) between each retry. Defaults to 1000.
|
||||
* @param maxRetries - The maximum number of retry attempts. Defaults to 3.
|
||||
* @param backoff - The backoff strategy to use: 'linear', 'exponential', or null.
|
||||
* @returns {Promise<boolean>} - A promise that resolves to:
|
||||
* - true: If the function returns true before reaching maxRetries.
|
||||
* - false: If the function never returns true or if an error occurs.
|
||||
*/
|
||||
export async function retry(
|
||||
fn: RetryFn,
|
||||
interval: number = 1000,
|
||||
maxRetries: number = 3,
|
||||
backoff: 'exponential' | 'linear' | null = 'linear',
|
||||
): Promise<boolean> {
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
attempt++;
|
||||
try {
|
||||
const result = await fn();
|
||||
if (result) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during retry:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Wait for the specified interval before the next attempt, if any attempts remain.
|
||||
if (attempt < maxRetries) {
|
||||
let computedInterval = interval;
|
||||
|
||||
if (backoff === 'linear') {
|
||||
computedInterval = interval * attempt;
|
||||
} else if (backoff === 'exponential') {
|
||||
computedInterval = Math.pow(2, attempt - 1) * interval;
|
||||
computedInterval = Math.min(computedInterval, 30000); // Cap the maximum interval to 30 seconds
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, computedInterval));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import { retry } from './retry';
|
||||
|
||||
describe('retry', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.clearAllTimers();
|
||||
});
|
||||
|
||||
it('should resolve true when the function eventually returns true', async () => {
|
||||
let callCount = 0;
|
||||
const fn = vi.fn(async () => {
|
||||
callCount++;
|
||||
// Return true on the second attempt.
|
||||
return callCount === 2;
|
||||
});
|
||||
|
||||
const promise = retry(fn, 1000, 2, null);
|
||||
|
||||
// The first call happens immediately.
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Advance timers by 1000ms asynchronously to allow the waiting period to complete.
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
// After advancing, the second attempt should have occurred.
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
|
||||
// The promise should now resolve with true.
|
||||
const result = await promise;
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should resolve false if maximum retries are reached with no success', async () => {
|
||||
let callCount = 0;
|
||||
const fn = vi.fn(async () => {
|
||||
callCount++;
|
||||
return false;
|
||||
});
|
||||
|
||||
const promise = retry(fn, 1000, 3, null);
|
||||
|
||||
// The first attempt fires immediately.
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Advance timers for the delay after the first attempt.
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Advance timers for the delay after the second attempt.
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
expect(fn).toHaveBeenCalledTimes(3);
|
||||
|
||||
// With maxRetries reached (3 calls), promise should resolve to false.
|
||||
const result = await promise;
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject if the function throws an error', async () => {
|
||||
const fn = vi.fn(async () => {
|
||||
throw new Error('Test error'); // eslint-disable-line n8n-local-rules/no-plain-errors
|
||||
});
|
||||
|
||||
// Since the error is thrown on the first call, no timer advancement is needed.
|
||||
await expect(retry(fn, 1000, 3, null)).rejects.toThrow('Test error');
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should use linear backoff strategy', async () => {
|
||||
let callCount = 0;
|
||||
const fn = vi.fn(async () => {
|
||||
callCount++;
|
||||
return callCount === 4; // Return true on the fourth attempt.
|
||||
});
|
||||
|
||||
const promise = retry(fn, 1000, 4, 'linear');
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1000); // First backoff
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2000); // Second backoff
|
||||
expect(fn).toHaveBeenCalledTimes(3);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(3000); // Third backoff
|
||||
expect(fn).toHaveBeenCalledTimes(4);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should use exponential backoff strategy', async () => {
|
||||
let callCount = 0;
|
||||
const fn = vi.fn(async () => {
|
||||
callCount++;
|
||||
return callCount === 5; // Return true on the fifth attempt.
|
||||
});
|
||||
|
||||
const promise = retry(fn, 1000, 5, 'exponential');
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1000); // First backoff
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2000); // Second backoff
|
||||
expect(fn).toHaveBeenCalledTimes(3);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(4000); // Third backoff
|
||||
expect(fn).toHaveBeenCalledTimes(4);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(8000); // Fourth backoff
|
||||
expect(fn).toHaveBeenCalledTimes(5);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user