-
-
Save mikearnaldi/4a13fe6f51b28ad0b07fd7bbe3f4c49a to your computer and use it in GitHub Desktop.
Generic batching & retries
This file contains 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
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