Skip to content

Instantly share code, notes, and snippets.

@mikearnaldi
Created May 13, 2023 15:10
Show Gist options
  • Save mikearnaldi/4a13fe6f51b28ad0b07fd7bbe3f4c49a to your computer and use it in GitHub Desktop.
Save mikearnaldi/4a13fe6f51b28ad0b07fd7bbe3f4c49a to your computer and use it in GitHub Desktop.
Generic batching & retries
import * as Duration from "@effect/data/Duration"
import { identity, pipe } from "@effect/data/Function"
import * as Effect from "@effect/io/Effect"
import * as Exit from "@effect/io/Exit"
import * as Request from "@effect/io/Request"
import * as RequestResolver from "@effect/io/RequestResolver"
import * as Schedule from "@effect/io/Schedule"
import type { ParseError } from "@effect/schema/ParseResult"
import * as Pretty from "@effect/schema/Pretty"
import * as Schema from "@effect/schema/Schema"
//
// Models a generic json http call
//
const HttpError_ = Schema.struct({ _tag: Schema.literal("HttpError") })
export interface HttpError extends Schema.To<typeof HttpError_> {}
export const HttpError: Schema.Schema<HttpError> = Schema.to(HttpError_)
export const fetch200 = (url: string) =>
Effect.tryCatchPromise(
() => fetch(url).then((res) => res.status === 200 ? res.json() : Promise.reject()),
() => identity<HttpError>({ _tag: "HttpError" })
)
//
// Exponential backoff with a base factor of 10ms for up to a maximum of 10s in total.
//
// Note: we only apply this retry policy on HttpError, not on any other error.
//
export const retryPolicy = pipe(
Schedule.exponential(Duration.millis(10)),
Schedule.compose(Schedule.elapsed()),
Schedule.whileOutput(Duration.lessThanOrEqualTo(Duration.seconds(10))),
Schedule.whileInput(Schema.is(HttpError))
)
//
// Describes the structure of a todo and for an array of Todos
//
const Todo_ = Schema.struct({ id: pipe(Schema.number, Schema.brand("TodoId")) })
export interface Todo extends Schema.To<typeof Todo_> {}
export const Todo: Schema.Schema<Todo> = Schema.to(Todo_)
export const Todos = Schema.array(Todo)
//
// Implements a batchable fetchTodo(id) query by levragng Effect.request
//
const TodoNotFound_ = Schema.struct({ _tag: Schema.literal("TodoNotFound") })
export interface TodoNotFound extends Schema.To<typeof TodoNotFound_> {}
export const TodoNotFound: Schema.Schema<TodoNotFound> = Schema.to(TodoNotFound_)
interface FetchTodo extends Request.Request<HttpError | ParseError | TodoNotFound, Todo> {
_tag: "FetchTodo"
id: Todo["id"]
}
const FetchTodo = Request.tagged<FetchTodo>("FetchTodo")
const fetchTodoResolver = RequestResolver.makeBatched((requests: Array<FetchTodo>) =>
pipe(
Effect.gen(function*($) {
const todos = yield* $(
fetch200(`https://myLovelyApi.example/todos?ids=${requests.map((_) => _.id).join(",")}`),
Effect.flatMap(Schema.parseEffect(Todos)),
Effect.retry(retryPolicy)
)
const foundTodoIds = todos.map((_) => _.id)
const requestsNotFound = requests.filter((_) => !foundTodoIds.includes(_.id))
for (const request of requestsNotFound) {
yield* $(Request.complete(request, Exit.fail<TodoNotFound>({ _tag: "TodoNotFound" })))
}
for (const todo of todos) {
const request = requests.find((_) => _.id === todo.id)!
yield* $(Request.complete(request, Exit.succeed(todo)))
}
}),
Effect.catchAll((error) => Effect.forEach(requests, Request.complete(Exit.fail(error))))
)
)
//
// Now we have a fetchTodo(id) function that is automatically batched and retried
//
export const fetchTodo = (id: Todo["id"]) => Effect.request(FetchTodo({ id }), fetchTodoResolver)
//
// Let's assume to have done the same for the following
//
export interface User {
readonly todos: Array<Todo["id"]>
}
export declare const getAllUsers: Effect.Effect<never, HttpError | ParseError, ReadonlyArray<User>>
//
// Let's write a normal program
//
export const program = Effect.gen(function*($) {
const users = yield* $(getAllUsers)
const todos = yield* $(
Effect.forEachPar(users, (user) => Effect.forEachPar(user.todos, fetchTodo))
)
for (let index = 0; index < users.length; index++) {
const prettyTodos = Pretty.to(Todos)(todos[index]!)
yield* $(Effect.log(`User(${users[index]}): ${prettyTodos}`))
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment