Initial TrueGrowth source import

This commit is contained in:
2026-07-07 09:36:36 +08:00
commit 3b6781d695
2283 changed files with 691996 additions and 0 deletions

View File

@@ -0,0 +1,204 @@
import { describe, it, expect, vi } from 'vitest';
import { isPromiseLike, composeEventHandlers, yieldToMain, processBatched } from './index';
describe('isPromiseLike', () => {
it('should return true for Promise instances', () => {
const promise = Promise.resolve(42);
expect(isPromiseLike(promise)).toBe(true);
});
it('should return true for objects with then/catch/finally methods', () => {
const promiseLike = {
then: () => {},
catch: () => {},
finally: () => {},
};
expect(isPromiseLike(promiseLike)).toBe(true);
});
it('should return false for non-promise objects', () => {
expect(isPromiseLike({})).toBe(false);
expect(isPromiseLike({ then: () => {} })).toBe(false);
expect(isPromiseLike({ then: () => {}, catch: () => {} })).toBe(false);
});
it('should return false for primitives', () => {
expect(isPromiseLike(null)).toBe(false);
expect(isPromiseLike(undefined)).toBe(false);
expect(isPromiseLike(42)).toBe(false);
expect(isPromiseLike('string')).toBe(false);
});
it('should type guard correctly', async () => {
const value: unknown = Promise.resolve(42);
if (isPromiseLike(value)) {
// TypeScript should know this is a Promise
const result = await value;
expect(result).toBe(42);
}
});
});
describe('composeEventHandlers', () => {
it('should call both handlers in order', () => {
const calls: string[] = [];
const handler1 = () => calls.push('handler1');
const handler2 = () => calls.push('handler2');
const composed = composeEventHandlers(handler1, handler2);
composed({} as any);
expect(calls).toEqual(['handler1', 'handler2']);
});
it('should pass event to both handlers', () => {
const event = { type: 'click', target: 'button' };
const handler1 = vi.fn();
const handler2 = vi.fn();
const composed = composeEventHandlers(handler1, handler2);
composed(event);
expect(handler1).toHaveBeenCalledWith(event);
expect(handler2).toHaveBeenCalledWith(event);
});
it('should skip second handler if defaultPrevented is true', () => {
const event = {
defaultPrevented: true,
} as Event;
const handler1 = vi.fn();
const handler2 = vi.fn();
const composed = composeEventHandlers(handler1, handler2);
composed(event);
expect(handler1).toHaveBeenCalled();
expect(handler2).not.toHaveBeenCalled();
});
it('should call second handler when checkForDefaultPrevented is false', () => {
const event = {
defaultPrevented: true,
} as Event;
const handler1 = vi.fn();
const handler2 = vi.fn();
const composed = composeEventHandlers(handler1, handler2, {
checkForDefaultPrevented: false,
});
composed(event);
expect(handler1).toHaveBeenCalled();
expect(handler2).toHaveBeenCalled();
});
it('should handle undefined handlers', () => {
const handler = vi.fn();
const composed1 = composeEventHandlers(undefined, handler);
composed1({} as any);
expect(handler).toHaveBeenCalled();
const composed2 = composeEventHandlers(handler, undefined);
composed2({} as any);
expect(handler).toHaveBeenCalledTimes(2);
});
it('should handle both handlers undefined', () => {
const composed = composeEventHandlers(undefined, undefined);
expect(() => composed({} as any)).not.toThrow();
});
it('should return value from second handler', () => {
const handler1 = () => 'first';
const handler2 = () => 'second';
const composed = composeEventHandlers(handler1, handler2);
const result = composed({} as any);
expect(result).toBe('second');
});
});
describe('yieldToMain', () => {
it('should return a Promise', () => {
const result = yieldToMain();
expect(result).toBeInstanceOf(Promise);
});
it('should resolve immediately on next tick', async () => {
const start = Date.now();
await yieldToMain();
const duration = Date.now() - start;
// Should be nearly instant (< 50ms accounting for test overhead)
expect(duration).toBeLessThan(50);
});
it('should allow other tasks to run', async () => {
const order: number[] = [];
// Schedule a microtask
Promise.resolve().then(() => order.push(2));
order.push(1);
await yieldToMain(); // This uses setTimeout, so microtasks run first
order.push(3);
expect(order).toEqual([1, 2, 3]);
});
});
describe('processBatched', () => {
it('should process all items', async () => {
const items = [1, 2, 3, 4, 5];
const results = await processBatched(
items,
async (item) => item * 2,
2
);
expect(results).toEqual([2, 4, 6, 8, 10]);
});
it('should call processor with item and index', async () => {
const items = ['a', 'b', 'c'];
const calls: [string, number][] = [];
await processBatched(
items,
async (item, index) => {
calls.push([item, index]);
return item;
},
5
);
expect(calls).toEqual([['a', 0], ['b', 1], ['c', 2]]);
});
it('should yield after batchSize items', async () => {
const yieldSpy = vi.spyOn(global, 'setTimeout');
const items = [1, 2, 3, 4, 5, 6];
await processBatched(items, async (x) => x, 2);
// Should yield 3 times (after item 2, 4, 6)
// Note: We check that setTimeout was called, which indicates yielding
expect(yieldSpy).toHaveBeenCalled();
yieldSpy.mockRestore();
});
it('should handle empty array', async () => {
const results = await processBatched([], async (x) => x, 5);
expect(results).toEqual([]);
});
it('should use default batchSize of 5', async () => {
const items = Array.from({ length: 10 }, (_, i) => i);
const results = await processBatched(items, async (x) => x * 2);
expect(results).toEqual([0, 2, 4, 6, 8, 10, 12, 14, 16, 18]);
});
});

View File

@@ -0,0 +1,139 @@
/**
* Async Utilities
*
* Utilities for working with Promises and asynchronous event handling.
* All functions are pure and framework-agnostic.
*/
import type { ResolutionType } from '../types';
/**
* Type guard to check if a value is Promise-like (has then/catch/finally methods)
*
* @param value - Value to check
* @returns True if value implements Promise interface
*
* @example
* ```typescript
* const promise = Promise.resolve(42);
* const notPromise = { value: 42 };
*
* if (isPromiseLike(promise)) {
* promise.then(console.log); // TypeScript knows it's a Promise
* }
*
* isPromiseLike(notPromise); // false
* ```
*/
export const isPromiseLike = (
value: any
): value is Promise<ResolutionType<typeof value>> => {
return (
!!value &&
typeof value === 'object' &&
'then' in value &&
'catch' in value &&
'finally' in value
);
};
/**
* Compose multiple event handlers into a single handler
*
* Useful for merging user-provided event handlers with library handlers.
* Taken from Radix UI primitives.
*
* @param originalEventHandler - User's event handler (optional)
* @param ourEventHandler - Library's event handler (optional)
* @param options - Configuration options
* @param options.checkForDefaultPrevented - If true, skip ourEventHandler when defaultPrevented
* @returns Composed event handler function
*
* @example
* ```typescript
* const userOnClick = (e) => console.log('User clicked');
* const libraryOnClick = (e) => console.log('Library action');
*
* const composedHandler = composeEventHandlers(
* userOnClick,
* libraryOnClick,
* { checkForDefaultPrevented: true }
* );
*
* button.addEventListener('click', composedHandler);
* // Calls both handlers unless defaultPrevented
* ```
*
* @see https://github.com/radix-ui/primitives/blob/main/packages/core/primitive/src/primitive.tsx
*/
export const composeEventHandlers = <E>(
originalEventHandler?: (event: E) => void,
ourEventHandler?: (event: E) => void,
{ checkForDefaultPrevented = true } = {}
) => {
return function handleEvent(event: E) {
originalEventHandler?.(event);
if (
!checkForDefaultPrevented ||
!(event as unknown as Event)?.defaultPrevented
) {
return ourEventHandler?.(event);
}
};
};
/**
* Yield to the main thread to prevent UI blocking
*
* Use this in CPU-intensive loops to allow the browser to process
* pending events (user input, rendering, etc.) between iterations.
*
* @example
* ```typescript
* for (let i = 0; i < largeArray.length; i++) {
* processItem(largeArray[i]);
* if (i % 10 === 0) {
* await yieldToMain(); // Yield every 10 items
* }
* }
* ```
*/
export const yieldToMain = (): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, 0));
};
/**
* Process items in batches with yielding to prevent UI blocking
*
* @param items - Array of items to process
* @param processor - Async function to process each item
* @param batchSize - Number of items to process before yielding (default: 5)
* @returns Array of results from processor
*
* @example
* ```typescript
* const results = await processBatched(
* boards,
* async (board) => {
* const json = JSON.stringify(board);
* return await encrypt(json);
* },
* 5 // Process 5 boards before yielding
* );
* ```
*/
export const processBatched = async <T, R>(
items: T[],
processor: (item: T, index: number) => Promise<R>,
batchSize = 5
): Promise<R[]> => {
const results: R[] = [];
for (let i = 0; i < items.length; i++) {
results.push(await processor(items[i]!, i));
if ((i + 1) % batchSize === 0) {
await yieldToMain();
}
}
return results;
};