Last active
August 17, 2023 09:24
-
-
Save schickling/d20a265c6b1a5593a66e7d06d17db8f3 to your computer and use it in GitHub Desktop.
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
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 } | |
}) |
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 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