Created
April 15, 2026 04:21
-
-
Save yimity/59aa1a90ba5f33e9cd9618f952b2dee9 to your computer and use it in GitHub Desktop.
step-runner-demo.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| type StepCallback<T = unknown> = () => T | Promise<T>; | |
| interface StepRunner { | |
| run<T>(id: string, callback: StepCallback<T>): Promise<T>; | |
| } | |
| interface PendingStep { | |
| id: string; | |
| callback: StepCallback; | |
| } | |
| async function execute( | |
| fn: (step: StepRunner) => Promise<void> | void, | |
| stepState: Map<string, unknown> | |
| ): Promise<boolean> { | |
| let newStep: PendingStep | null = null; | |
| // Run the user function in the background. It will hang at the new step | |
| void fn({ | |
| run: async <T>(id: string, callback: StepCallback<T>): Promise<T> => { | |
| // If this step already ran, return the memoized result | |
| if (stepState.has(id)) { | |
| return stepState.get(id) as T; | |
| } | |
| // This is a new step. Report it | |
| newStep = { id, callback }; | |
| // Hang forever | |
| return new Promise<T>(() => {}); | |
| }, | |
| }); | |
| // Schedule a macrotask. All pending microtasks (the resolved awaits from | |
| // memoized steps) will drain before this runs, giving the workflow function | |
| // time to advance through already-completed steps and reach the next new one. | |
| await new Promise((r) => setTimeout(r, 0)); | |
| const pendingStep = newStep as PendingStep | null; | |
| if (pendingStep !== null) { | |
| // A new step was found. Execute it and save the result | |
| const result = await pendingStep.callback(); | |
| stepState.set(pendingStep.id, result); | |
| // Function is not done | |
| return false; | |
| } | |
| // Function is done | |
| return true; | |
| } | |
| // User-defined workflow function | |
| async function myWorkflow(step: StepRunner): Promise<void> { | |
| console.log(' Workflow: top'); | |
| const data = await step.run('fetch', () => { | |
| console.log(' Step: fetch'); | |
| return [1, 2, 3]; | |
| }); | |
| console.log(' Workflow: fetch data: ', data); | |
| const processed = await step.run('process', () => { | |
| console.log(' Step: process'); | |
| return data.map((n) => n * 2); | |
| }); | |
| console.log(' Workflow: process data: ', processed); | |
| console.log(' Workflow: complete', processed); | |
| } | |
| async function main() { | |
| // In-memory store of completed step results | |
| const stepState = new Map<string, unknown>(); | |
| // Keep entering the workflow function until it's done | |
| let done = false; | |
| let i = 0; | |
| while (!done) { | |
| console.log(`Run ${i}:`); | |
| done = await execute(myWorkflow, stepState); | |
| console.log('--------------------------------'); | |
| i++; | |
| } | |
| } | |
| main(); | |
| ref:https://www.inngest.com/blog/hanging-promises-for-control-flow |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment