Practical Use Cases for Effect-TS: Polling AI APIs

Practical Use Cases for Effect-TS: Polling AI APIs

·

3 min read

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:

Polling Flow

And here's the breakdown in plain English:

  1. Send a POST request to the API to generate an image based on a text prompt.
  2. Receive a generation ID as the response.
  3. 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".
  4. 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.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:

  1. We create a Deferred effect called deferredUrl representing the image url of the image that is being created.
  2. 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.
  3. Once we reach a state where the status is completed we assign the url to deferredUrl with Deferred.succeed(deferredUrl, url)
  4. 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, meaning Effect.forkScoped will complete once the Deferred is unblocked.

Conclusion

shib army

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 .