diff --git a/packages/trampoline/src/trampoline.js b/packages/trampoline/src/trampoline.js index 9bfc76f078..98a6b8ab1c 100644 --- a/packages/trampoline/src/trampoline.js +++ b/packages/trampoline/src/trampoline.js @@ -1,21 +1,20 @@ /* eslint-disable @jessie.js/safe-await-separator */ /** - * @import {ThunkFn, SyncTrampolineGeneratorFn, TrampolineGeneratorFn, AsyncTrampolineGeneratorFn, SyncThunkFn } from './types.js' + * @import {AnyGenerator, SyncTrampolineGeneratorFn, AsyncTrampolineGeneratorFn } from './types.js' */ /** - * Trampoline on {@link TrampolineGeneratorFn generatorFn} with a synchronous {@link SyncThunkFn thunk}. + * Trampoline on {@link TrampolineGeneratorFn generatorFn} synchronously. * - * @template TInitial Type of the initial value passed to the `generatorFn` - * @template [TArg=TInitial] Type of the argument passed to the `thunkFn` - * @template [TResult=TArg] Result of the `thunkFn` _and_ the return value of the `generatorFn` - * @param {SyncTrampolineGeneratorFn} generatorFn Generator-returning function accepting a thunk and optionally an initial value - * @param {SyncThunkFn} thunk Synchronous thunk which `generatorFn` should call - * @param {TInitial} initial Initial value + * @template {readonly any[]} [TArgs=unknown[]] Parameters for `generatorFn` + * @template [TResult=unknown] Type of the return value of the `generatorFn` + * @template {Generator} [TGenerator=Generator] Type of the generator function + * @param {SyncTrampolineGeneratorFn} generatorFn Generator-returning function accepting a thunk and optionally an initial value + * @param {TArgs} args Initial args * @returns {TResult} */ -export function syncTrampoline(generatorFn, thunk, initial) { - const iterator = generatorFn(thunk, initial); +export function syncTrampoline(generatorFn, ...args) { + const iterator = generatorFn(...args); let result = iterator.next(); while (!result.done) { result = iterator.next(result.value); @@ -24,18 +23,17 @@ export function syncTrampoline(generatorFn, thunk, initial) { } /** - * Trampoline on {@link TrampolineGeneratorFn generatorFn} with a synchronous _or_ asynchronous {@link ThunkFn thunk}. + * Trampoline on {@link TrampolineGeneratorFn generatorFn} asynchronously. * - * @template TInitial Type of the initial value passed to the `generatorFn` - * @template [TArg=TInitial] Type of the argument passed to the `thunkFn` - * @template [TResult=TArg] Result of `thunkFn` _and_ the return value of the `generatorFn` - * @param {TrampolineGeneratorFn} generatorFn Generator-returning function accepting a thunk and optionally an initial value - * @param {ThunkFn} thunk Thunk function - * @param {TInitial} initial Initial value passed to `generatorFn` - * @returns {Promise>} Final value of generator + * @template {readonly any[]} [TArgs=unknown[]] Parameters for `generatorFn` + * @template [TResult=unknown] Type of the return value of the `generatorFn` + * @template {Generator} [TGenerator=Generator] Type of the generator function + * @param {AsyncTrampolineGeneratorFn} generatorFn Generator-returning function accepting a thunk and optionally an initial value + * @param {TArgs} args Initial args + * @returns {Promise} */ -export async function trampoline(generatorFn, thunk, initial) { - const iterator = generatorFn(thunk, initial); +export async function trampoline(generatorFn, ...args) { + const iterator = generatorFn(...args); let result = iterator.next(); while (!result.done) { // eslint-disable-next-line no-await-in-loop diff --git a/packages/trampoline/src/types.ts b/packages/trampoline/src/types.ts index e74e3e735e..2a7a497db3 100644 --- a/packages/trampoline/src/types.ts +++ b/packages/trampoline/src/types.ts @@ -1,16 +1,4 @@ -/** - * A {@link TrampolineGeneratorFn} will yield the result of calling this - * function - */ -export type ThunkFn = (arg: TArg) => TResult; - -/** - * A {@link SyncTrampolineGeneratorFn} or {@link TrampolineGeneratorFn} will - * yield the result of calling this function - */ -export type SyncThunkFn = - TResult extends Promise ? never : (arg: TArg) => TResult; - +export type AnyGenerator = Generator; /** * A function type that represents a generator function for trampolining. * @@ -23,10 +11,14 @@ export type SyncThunkFn = * @param initial - The initial value to start the generator. * @returns A generator that yields results of type `TResult`. */ -export type TrampolineGeneratorFn = ( - thunk: ThunkFn, - initial: TInitial, -) => Generator, Awaited>; +export type AsyncTrampolineGeneratorFn< + TGenerator extends AnyGenerator, + TArgs extends readonly any[] = unknown[], + TResult = unknown, +> = + TGenerator extends Generator + ? (...args: TArgs) => Generator, Awaited> + : never; /** * A function type that represents a synchronous generator function for @@ -35,20 +27,18 @@ export type TrampolineGeneratorFn = ( * This type ensures that the result type (`TResult`) is not a `Promise`. If * `TResult` extends `Promise`, the type resolves to `never`. * - * @template TInitial - The type of the initial value. - * @template TArg - The type of the argument passed to the thunk function. - * Defaults to `TInitial`. - * @template TResult - The type of the result produced by the thunk function. - * Defaults to `TArg`. - * @param thunk - The thunk function to be used in the generator. - * @param initial - The initial value to start the generator. + * @template TArgs - The type of the arguments passed to the generator function. + * Defaults to `unknown[]`. + * @template TResult - The type of the result produced by the generator function. + * Defaults to `unknown`. + * @param args - The arguments passed to the generator function. * @returns A generator that yields results of type `TResult`. */ export type SyncTrampolineGeneratorFn< - TInitial, - TArg = TInitial, - TResult = TArg, -> = ( - thunk: ThunkFn, - initial: TInitial, -) => Generator; + TGenerator extends AnyGenerator, + TArgs extends readonly any[] = unknown[], + TResult = unknown, +> = + TGenerator extends Generator + ? (...args: TArgs) => Generator + : never; diff --git a/packages/trampoline/test/trampoline-example.test.js b/packages/trampoline/test/trampoline-example.test.js index 82845dfc55..61a6b8f4f7 100644 --- a/packages/trampoline/test/trampoline-example.test.js +++ b/packages/trampoline/test/trampoline-example.test.js @@ -6,10 +6,6 @@ import test from 'ava'; import { syncTrampoline, trampoline } from '../src/trampoline.js'; -/** - * @import {ThunkFn} from '../src/types.js' - */ - /** * Mapping of filesnames to import specifiers. Trust me */ @@ -51,7 +47,7 @@ const findImportsAsync = async filepath => findImportsInSource(filepath); * Recursively crawls a dependency tree to find all dependencies * * @template {string[]|Promise} TResult - * @param {ThunkFn} thunk + * @param {(arg: string) => TResult} thunk * @param {string} filename * @returns {Generator} */ @@ -67,17 +63,17 @@ function* loadRecursive(thunk, filename) { const expected = ['b', 'c', 'c', 'd', 'e', 'f', 'g', 'e', 'f', 'g']; -test('asynchronous execution', async t => { +test('asynchronous execution - example code', async t => { const asyncResult = await trampoline(loadRecursive, findImportsAsync, 'a'); t.deepEqual(asyncResult, expected); }); -test('asynchronous execution w/ sync thunk', async t => { +test('asynchronous execution w/ sync thunk - example code', async t => { const asyncResult = await trampoline(loadRecursive, findImportsSync, 'a'); t.deepEqual(asyncResult, expected); }); -test('synchronous execution', t => { +test('synchronous execution - example code', t => { const syncResult = syncTrampoline(loadRecursive, findImportsSync, 'a'); t.deepEqual(syncResult, expected); }); diff --git a/packages/trampoline/test/trampoline.test-d.ts b/packages/trampoline/test/trampoline.test-d.ts index 6eab6cf0cf..893f472c20 100644 --- a/packages/trampoline/test/trampoline.test-d.ts +++ b/packages/trampoline/test/trampoline.test-d.ts @@ -1,11 +1,9 @@ /* eslint-disable no-redeclare */ -import { expectAssignable, expectNever, expectNotType, expectType } from 'tsd'; +import { expectAssignable, expectType } from 'tsd'; import { trampoline, syncTrampoline } from '../src/trampoline.js'; import { - SyncThunkFn, SyncTrampolineGeneratorFn, - ThunkFn, - TrampolineGeneratorFn, + AsyncTrampolineGeneratorFn, } from '../src/types.js'; function syncHook(x: number): number { @@ -17,21 +15,29 @@ async function asyncHook(x: number): Promise { return `${x}`; } -expectAssignable>(syncHook); - -expectAssignable>>(asyncHook); - -function* simple< - TResult extends string | Promise, - Thunk extends ThunkFn, ->(thunk: Thunk, initial: string): Generator { +function* simple>( + thunk: (arg: string) => TResult, + initial: string, +): Generator { const hello = yield thunk(initial); return `${hello} world`; } -expectAssignable>(simple); - -expectAssignable>(simple); +expectAssignable< + AsyncTrampolineGeneratorFn< + ReturnType, + [(arg: string) => Promise, string], + Promise + > +>(simple); + +expectAssignable< + SyncTrampolineGeneratorFn< + ReturnType, + [(arg: string) => string, string], + string + > +>(simple); expectType( syncTrampoline(simple, (str: string) => `${str} cruel`, 'goodbye'), diff --git a/packages/trampoline/test/trampoline.test.js b/packages/trampoline/test/trampoline.test.js index cb6647f2bf..98fe698606 100644 --- a/packages/trampoline/test/trampoline.test.js +++ b/packages/trampoline/test/trampoline.test.js @@ -8,13 +8,9 @@ import test from 'ava'; import { setTimeout } from 'node:timers'; import { syncTrampoline, trampoline } from '../src/trampoline.js'; -/** - * @import {ThunkFn} from '../src/types.js' - */ - /** * @template {number|Promise} TResult - * @param {ThunkFn} thunk + * @param {(arg: number) => TResult} thunk * @param {number} [input] * @returns {Generator} */ @@ -29,6 +25,23 @@ function* operationsWithThunk(thunk, input = 0) { return result; } +/** + * @template {number|Promise} TResult + * @param {(arg: number) => TResult} thunk + * @param {number} [input] + * @returns {Generator} + */ +function* operations(thunk, input = 0) { + let result = input * 2; // First operation + result = yield thunk(result); // Call the hook, which can be sync or async + result *= 2; // Operation on the hook's result + // Check if the result is divisible by N, and if so, recurse + while (result < 1000) { + result *= 2; + } + return result; +} + /** * Synchronous thunk * @param {number} x @@ -48,22 +61,35 @@ async function asyncThunk(x = 0) { return x + 10; } -const expectedResult = 2980; +const expectedRecursionResult = 2980; +const expectedResult = 1280; -test('synchronous execution', t => { +test('synchronous execution - recursion', t => { const syncResult = syncTrampoline(operationsWithThunk, syncThunk, 5); + t.is(syncResult, expectedRecursionResult); +}); + +test('asynchronous execution w/ sync thunk - recursion', async t => { + const asyncResult = await trampoline(operationsWithThunk, syncThunk, 5); + t.is(asyncResult, expectedRecursionResult); +}); + +test('asynchronous execution - recursion', async t => { + const asyncResult = await trampoline(operationsWithThunk, asyncThunk, 5); + t.is(asyncResult, expectedRecursionResult); +}); + +test('synchronous execution', t => { + const syncResult = syncTrampoline(operations, syncThunk, 5); t.is(syncResult, expectedResult); - t.pass(); }); test('asynchronous execution w/ sync thunk', async t => { - const asyncResult = await trampoline(operationsWithThunk, syncThunk, 5); + const asyncResult = await trampoline(operations, syncThunk, 5); t.is(asyncResult, expectedResult); - t.pass(); }); test('asynchronous execution', async t => { - const asyncResult = await trampoline(operationsWithThunk, asyncThunk, 5); + const asyncResult = await trampoline(operations, syncThunk, 5); t.is(asyncResult, expectedResult); - t.pass(); });