Practical Use Cases for Effect-TS: Polling AI APIs
Intro
If you're new to Effect-TS, this is the start of a mini series where we'll explore practical use cases for it. Looking back at fp-ts, Effect-TS is a significant improvement because it enables true functional programming in a concurrent environment.
Let's dive into a real-world example of using Effect-TS in a production setting. Imagine you're developing an application that uses image generation APIs. You need to wait for the images to render before proceeding, but you want to avoid the complexity of implementing webhooks. Instead, you'll use polling.
Here's a simplified diagram of the flow:
And here's the breakdown in plain English:
Send a POST request to the API to generate an image based on a text prompt.
Receive a generation ID as the response.
Spawn a new thread to periodically check the status of the image generation using the provided ID. Keep looping until the status is "Complete" or "Failed".
Suspend the main thread until the image generation task completes.
Effect-TS shines in scenarios like this, where you need to perform concurrent operations and handle asynchronous results in a functional way. It provides the tools to write clean, composable code that's easy to reason about, even in the presence of concurrency.
Data Modelling
Lets write some interfaces to model what our AI APIs would look like
export type ImageGeneration = {
id: string;
} & (
| {
status: 'completed';
url: string;
}
| { status: 'failed'; error: string }
| { status: 'pending' }
);
export type AiImageSDK = {
generateImage(prompt: string): Promise<string>;
getGenerationById(id: string): Promise<ImageGeneration>;
};
And now the code
declare const client: AiImageSDK;
const getImageGeneration = (id: string) =>
Effect.Do.pipe(
Effect.bind('deferredUrl', () => Deferred.make<string, Error>()),
Effect.tap(({ deferredUrl }) =>
Effect.tryPromise(async () => {
const response = await client.getGenerationById(id);
if (response.status === 'completed') {
return response.url;
}
if (response.status === 'failed') {
throw new Error(`Generation ${id} failed.`);
}
}).pipe(
Effect.tap((url) =>
url ? Deferred.succeed(deferredUrl, url) : Effect.none
),
Effect.catchAllCause((c) => Deferred.failCause(deferredUrl, c)),
Effect.repeat(Schedule.spaced('5 seconds')),
Effect.forkScoped
)
),
Effect.flatMap(({ deferredUrl }) => Deferred.await(deferredUrl))
).pipe(Effect.scoped);
await Effect.Do.pipe(
Effect.bind('id', () =>
Effect.tryPromise(async () => client.generateImage('shib army'))
),
Effect.bind('url', ({ id }) => getImageGeneration(id)),
Effect.map(({ url }) => url),
Effect.tap(Console.log)
).pipe(Effect.runPromise);
Lets break down the getImageGeneration
function:
We create a
Deferred
effect calleddeferredUrl
representing the image url of the image that is being created.We create a fork aka spawn a new non-blocking promise, to get the image generation. if the status is "completed" we return the url, if its "failed" we throw an error, if its "pending" we return
undefined
.Once we reach a state where the status is completed successfully we assign the url to deferredUrl with
Deferred.succeed(deferredUrl, url)
. If the operation fails or encounters a defect, we useEffect.catchAllCause((c) => Deferred.failCause(deferredUrl, c))
to propogate it back to theDeferred
object.This process repeats every 5 seconds until the function succeeds or fails. Once the URL is assigned,
Deferred.await(deferredUrl)
is unblocked and the effect is finished. Also take note of how the fork is scoped. The entire function is wrapped in Effect.scoped, meaningEffect.forkScoped
will complete once theDeferred
is unblocked.
Conclusion
And thats how you do polling with effect-ts. Hope you leanred something!
If you have any questions or want to discuss this further, feel out to reach out. I'm available on Twitter or you can email me at ryanleecode@gmail.com.
Subscribe to my newsletter
Read articles from Ryan Lee directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Ryan Lee
Ryan Lee
Full Stack Developer writing about functional programming, AI, and blockchain.