diff --git a/src/common.ts b/src/common.ts index 70ba648..0d3573d 100644 --- a/src/common.ts +++ b/src/common.ts @@ -30,6 +30,29 @@ export interface TinypoolWorker { threadId: number } +type SliceFirst = // + T extends [] | [any?] | [any] + ? T + : T extends [any, any, ...any[]] + ? never + : Required extends [infer A, ...any[]] + ? [A?] + : never + +export type TinypoolRunner = { + [K in keyof T as T[K] extends (...args: any[]) => any + ? K + : never]: T[K] extends (...args: infer A) => infer R + ? [R] extends [Promise] + ? true extends R & 'any' + ? (...args: SliceFirst) => Promise + : A['length'] extends 0 | 1 + ? T[K] + : (...args: SliceFirst) => Promise + : (...args: SliceFirst) => Promise + : never +} + /** * Tinypool's internal messaging between main thread and workers. * - Utilizers can use `__tinypool_worker_message__` property to identify diff --git a/src/index.ts b/src/index.ts index 2309ed8..a4b8a7b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,7 @@ import { type TinypoolData, type TinypoolWorker, type TinypoolChannel, + type TinypoolRunner, } from './common' import ThreadWorker from './runtime/thread-worker' import ProcessWorker from './runtime/process-worker' @@ -1159,6 +1160,22 @@ class Tinypool extends EventEmitterAsyncResource { }) } + withRunner(): this & { runner: TinypoolRunner } { + return this as any + } + + get runner(): TinypoolRunner<{ [K in never]: never }> { + return new Proxy({} as TinypoolRunner<{ [K in never]: never }>, { + get: + (_target: any, p: string) => + (...a: any[]) => { + if (a.length > 1) + throw new Error('TinypoolRunner doesn’t support args array') + return this.run(a[0], { name: p }) + }, + }) + } + async destroy() { await this.#pool.destroy() this.emitDestroy() diff --git a/test/fixtures/multiple.d.ts b/test/fixtures/multiple.d.ts new file mode 100644 index 0000000..5037296 --- /dev/null +++ b/test/fixtures/multiple.d.ts @@ -0,0 +1,21 @@ +export function a(): 'a' + +export function b(): 'b' + +export default a + +export function identity(v: V): V +export function identityAsync(v: V): Promise + +export function foobar(o: { foobar: V }): V +export function foobarAsync(o: { foobar: V }): Promise + +export function args(...args: A[]): A +export function argsAsync(...args: A): Promise + +export function firstArg(a: 1, b?: 2): 1 +export function firstArgAsync(a: 2, b?: 3): Promise<2> + +export const digit: 4 + +export function returnsAny(): any diff --git a/test/fixtures/multiple.js b/test/fixtures/multiple.js index 916871f..02f7016 100644 --- a/test/fixtures/multiple.js +++ b/test/fixtures/multiple.js @@ -9,3 +9,38 @@ export function b() { } export default a + +export function identity(v) { + return v +} +export function identityAsync(v) { + return v +} + +export function foobar({ foobar }) { + return foobar +} +export function foobarAsync({ foobar }) { + return foobar +} + +export function args(...args) { + return args +} +// eslint-disable-next-line @typescript-eslint/require-await +export async function argsAsync(...args) { + return args +} +export function firstArg(a) { + return a +} +// eslint-disable-next-line @typescript-eslint/require-await +export async function firstArgAsync(a) { + return a +} + +export const digit = 4 + +export function returnsAny() { + return 'any' +} diff --git a/test/runner.test.ts b/test/runner.test.ts new file mode 100644 index 0000000..bec47ac --- /dev/null +++ b/test/runner.test.ts @@ -0,0 +1,61 @@ +import { dirname, resolve } from 'node:path' +import Tinypool from 'tinypool' +import { fileURLToPath } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +test('runner worker_threads test', async () => { + const { runner } = new Tinypool({ + filename: resolve(__dirname, 'fixtures/multiple.js'), + runtime: 'worker_threads', + }).withRunner() + + expect((await runner.a()) satisfies 'a').toBe('a') + expect((await runner.b()) satisfies 'b').toBe('b') + + expect(await runner.identity(123)).toBe(123) + expect((await runner.identityAsync(123)) satisfies 123).toBe(123) + + expect(await runner.foobar({ foobar: 1 })).toBe(1) + expect((await runner.foobarAsync({ foobar: 1 })) satisfies 1).toBe(1) + + expect((await runner.firstArg(1)) satisfies 1).toBe(1) + expect((await runner.firstArgAsync(2)) satisfies 2).toBe(2) + + // @ts-expect-error should throw + expect(() => runner.firstArg(1, 2)).toThrow('doesn’t support args array') + // @ts-expect-error should throw + expect(() => runner.firstArgAsync(2, 3)).toThrow('doesn’t support args array') + // @ts-expect-error should throw + expect(() => runner.args(1, 2, 3)).toThrow('doesn’t support args array') + // @ts-expect-error should throw + expect(() => runner.argsAsync(1, 2, 3)).toThrow('doesn’t support args array') +}) + +test('runner child_process test', async () => { + const { runner } = new Tinypool({ + filename: resolve(__dirname, 'fixtures/multiple.js'), + runtime: 'child_process', + }).withRunner() + + expect((await runner.a()) satisfies 'a').toBe('a') + expect((await runner.b()) satisfies 'b').toBe('b') + + expect(await runner.identity(123)).toBe(123) + expect((await runner.identityAsync(123)) satisfies 123).toBe(123) + + expect(await runner.foobar({ foobar: 1 })).toBe(1) + expect((await runner.foobarAsync({ foobar: 1 })) satisfies 1).toBe(1) + + expect((await runner.firstArg(1)) satisfies 1).toBe(1) + expect((await runner.firstArgAsync(2)) satisfies 2).toBe(2) + + // @ts-expect-error should throw + expect(() => runner.firstArg(1, 2)).toThrow('doesn’t support args array') + // @ts-expect-error should throw + expect(() => runner.firstArgAsync(2, 3)).toThrow('doesn’t support args array') + // @ts-expect-error should throw + expect(() => runner.args(1, 2, 3)).toThrow('doesn’t support args array') + // @ts-expect-error should throw + expect(() => runner.argsAsync(1, 2, 3)).toThrow('doesn’t support args array') +})