Skip to content

Instantly share code, notes, and snippets.

@schickling
Last active August 17, 2023 09:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save schickling/d20a265c6b1a5593a66e7d06d17db8f3 to your computer and use it in GitHub Desktop.
Save schickling/d20a265c6b1a5593a66e7d06d17db8f3 to your computer and use it in GitHub Desktop.
const makeResourceLoaders = Effect.gen(function* ($) {
// NOTE we're taking a slightly larger timeout here (default is 10ms) to improve API call utilization
const batchTimeoutMs = 30
const tracks = yield* $(
ResourceLoader.make({
tag: 'loadTrack',
fetchResources: (trackIds) => callSpotifyApi((spotify) => spotify.tracks.getTracks(trackIds as string[])),
mapResult: (_) => (_ === null ? Either.left(new SpotifyApiError({ error: 'Track not found' })) : Either.right(_)),
batchTimeoutMs,
batchCapacity: 50,
}),
)
const artists = yield* $(
ResourceLoader.make({
tag: 'loadArtist',
fetchResources: (artistIds) => callSpotifyApi((spotify) => spotify.artists.getArtists(artistIds as string[])),
batchTimeoutMs,
batchCapacity: 50,
}),
)
const albums = yield* $(
ResourceLoader.make({
tag: 'loadAlbum',
fetchResources: (albumIds) => callSpotifyApi((spotify) => spotify.albums.getAlbums(albumIds as string[])),
mapResult: (_) => (_ === null ? Either.left(new SpotifyApiError({ error: 'Album not found' })) : Either.right(_)),
batchTimeoutMs,
batchCapacity: 20,
}),
)
const albumImages = pipe(
albums,
ResourceLoader.map((album) => album.images),
)
const artistImages = pipe(
artists,
ResourceLoader.map((artist) => artist.images),
)
return { tracks, artists, albums, albumImages, artistImages }
})
import type { Otel, Scope } from '@overtone/utils/effect'
import { Duration, Effect, Either, pipe, Request, RequestResolver } from '@overtone/utils/effect'
export type MakeArgs<TTag extends string, C, E, A, E2, A2> = {
tag: TTag
fetchResources: (resourceIds: ReadonlyArray<string>) => Effect.Effect<C, E, A[]>
mapResult?: (fetchedResources: A) => Either.Either<E2, A2>
batchTimeoutMs?: number
batchCapacity: number
cache?: {
capacity?: number
timeToLive?: Duration.DurationInput
}
}
export type ResourceLoader<TTag extends string, E, A> = {
_tag: TTag
loadOne: (resourceId: string) => Effect.Effect<never, E, A>
loadMany: (resourceIds: ReadonlyArray<string>) => Effect.Effect<never, E, ReadonlyArray<A>>
}
export const make = <TTag extends string, C, E, A, E2 = never, A2 = A>({
tag,
fetchResources,
mapResult,
batchTimeoutMs = 30,
batchCapacity,
cache,
}: MakeArgs<TTag, C, E, A, E2, A2>): Effect.Effect<
Otel.Tracer | C | Scope.Scope,
never,
ResourceLoader<TTag, E | E2, A2>
> =>
Effect.gen(function* ($) {
interface ResourceRequest extends Request.Request<E | E2, A2> {
_tag: TTag
id: string
// TODO add span info
}
const ResourceRequest = Request.tagged<ResourceRequest>(tag)
const ctx = yield* $(Effect.context<C | Otel.Tracer>())
const resourceCache = yield* $(
Request.makeCache({ capacity: cache?.capacity ?? 1000, timeToLive: cache?.timeToLive ?? Duration.hours(1) }),
)
const mapResults = mapResult
? (fetchedResources: ReadonlyArray<A>) => fetchedResources.map(mapResult)
: (fetchedResources: ReadonlyArray<A>) =>
fetchedResources.map(Either.right) as any as ReadonlyArray<Either.Either<E2, A2>>
const getResourceRequestResolver = RequestResolver.makeBatched((requests: ReadonlyArray<ResourceRequest>) => {
const resourceIds = requests.map((_) => _.id)
return pipe(
fetchResources(resourceIds),
// Otel.withSpan('fetchResources', { attributes: { resourceIds: JSON.stringify(resourceIds) } }),
Effect.map(mapResults),
Effect.flatMap(
Effect.forEach((resourceEither, index) =>
resourceEither._tag === 'Left'
? Request.fail(requests[index]!, resourceEither.left)
: Request.succeed(requests[index]!, resourceEither.right),
),
),
Effect.catchAll((err) => Effect.forEach(requests, (request) => Request.fail(request, err))),
)
}).pipe(RequestResolver.provideContext(ctx))
const getResources = yield* $(
RequestResolver.dataLoader(getResourceRequestResolver, { maxBatchSize: batchCapacity, window: batchTimeoutMs }),
)
const loadMany = (resourceIds: ReadonlyArray<string>) =>
pipe(
Effect.forEach(resourceIds, (id) => Effect.request(ResourceRequest({ id }), getResources), {
batching: true,
}),
Effect.withRequestCaching(true),
Effect.withRequestCache(resourceCache),
)
const loadOne = (resourceId: string) =>
pipe(
Effect.request(ResourceRequest({ id: resourceId }), getResources),
Effect.withRequestCaching(true),
Effect.withRequestCache(resourceCache),
)
return { _tag: tag, loadMany, loadOne }
})
export const map =
<TTag extends string, E, A1, A2>(mapResult: (a: A1) => A2) =>
(resouces: ResourceLoader<TTag, E, A1>): ResourceLoader<TTag, E, A2> => ({
_tag: resouces._tag,
loadOne: (resourceId: string) => resouces.loadOne(resourceId).pipe(Effect.map(mapResult)),
loadMany: (resourceIds: ReadonlyArray<string>) => resouces.loadMany(resourceIds).pipe(Effect.mapArray(mapResult)),
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment