Skip to content
Source
Sign inSign up
Open History to see the last commit

30_batching.ts

118 lines4.1 KB

1/**
2 * @title Batching lookups with GraphQLResolver
3 *
4 * Collect concurrent lookups by id into one `nodes(ids:)` request with
5 * `GraphQLResolver`, instead of one request per id.
6 */
7import { type Cause, Context, Effect, Layer, RequestResolver, Schema } from "effect"
8import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol, GraphQLResolver } from "effect/graphql"
9import { FetchHttpClient } from "effect/http"
10import { GitHubAuth } from "./10_basics.ts"
11​
12// The generator's output for `repo-nodes.graphql`:
13//
14// query RepoNodes($ids: [ID!]!) {
15// nodes(ids: $ids) { __typename ... on Repository { nameWithOwner } }
16// }
17//
18// Generated modules import shared codecs and the possible `__typename`s of
19// each interface from the module the generator writes next to the schema
20// (`import * as Shared from "../schema.graphql.ts"`); here `Shared` is
21// declared inline.
22const Shared = { ID: Schema.String }
23declare namespace Shared {
24 export namespace Typename {
25 export type Node = "Issue" | "PullRequest" | "Repository" | "User"
26 }
28​
29export const Repository = Schema.Struct({
30 __typename: Schema.Literal("Repository"),
31 nameWithOwner: Schema.String
32})
33export interface Repository extends Schema.Struct.Type<typeof Repository.fields> {}
34​
35// Unselected node types are represented by `otherTypename`.
36export const NodeOther = Schema.Struct({
37 __typename: GraphQL.otherTypename<Shared.Typename.Node>()(["Repository"])
38})
39export interface NodeOther extends Schema.Struct.Type<typeof NodeOther.fields> {}
40​
41/** `query RepoNodes` */
42export const RepoNodes = GraphQL.query("RepoNodes", {
43 document: {
44 variables: [
45 { name: "ids", body: "[ID!]!" }
46 ],
47 selections: [
48 { name: "nodes", body: ["(ids:$", "ids", "){__typename...on Repository{nameWithOwner}}"] }
49 ],
50 fragments: []
51 },
52 variables: {
53 ids: Schema.Array(Shared.ID)
54 },
55 result: Schema.Struct({
56 nodes: Schema.Array(Schema.NullOr(Schema.Union([
57 Repository,
58 NodeOther
59 ])))
60 })
61})
62export declare namespace RepoNodes {
63 export type Variables = typeof RepoNodes.variables.Type
64 export type Result = typeof RepoNodes.result.Type
66​
67/** Every operation in repo-nodes.graphql. */
68export const RepoNodesGraphQLGroup = GraphQLGroup.make(RepoNodes)
69​
70// Your code: attach the middleware to the generated group.
71export const GitHubGraphQLGroup = RepoNodesGraphQLGroup.middleware(GitHubAuth)
72​
73export class GitHub extends Context.Service<
74 GitHub,
75 GraphQLClient.FromGroup<typeof GitHubGraphQLGroup> & {
76 getRepo(id: string): Effect.Effect<
77 Repository,
78 GraphQL.Error<typeof RepoNodes> | Cause.NoSuchElementError
79 >
80 }
81>()("app/GitHub") {
82 static readonly layer = Layer.effect(
83 GitHub,
84 Effect.gen(function*() {
85 const client = yield* GraphQLClient.make(GitHubGraphQLGroup)
86​
87 // Null entries and non-Repository nodes fail with NoSuchElementError.
88 // GitHub accepts at most 100 ids per request.
89 //
90 // GitHubAuth reads one token from config, so every lookup can share a
91 // batch. If a middleware `requires` a per-request service, such as the
92 // signed-in user, `batchKey` is required and keeps lookups for different
93 // users in separate calls:
94 //
95 // batchKey: (context) => Context.get(context, CurrentUser).id
96 const RepoById = GraphQLResolver.ordered(client.RepoNodes, {
97 variables: (ids: ReadonlyArray<string>) => ({ ids }),
98 path: ["nodes"],
99 typename: "Repository"
100 }).pipe(RequestResolver.batchN(100))
101 return { ...client, getRepo: GraphQLResolver.request(RepoById) }
102 })
103 ).pipe(
104 Layer.provide(GitHubAuth.layer),
105 Layer.provide(GraphQLProtocol.layerHttp({ url: "https://api.github.com/graphql" })),
106 Layer.provide(FetchHttpClient.layer)
107 )
109​
110export const repoNames = Effect.fn("repoNames")(function*(ids: ReadonlyArray<string>) {
111 const github = yield* GitHub
112 const repos = yield* Effect.forEach(ids, github.getRepo, { concurrency: "unbounded" })
113 return repos.map((repo) => repo.nameWithOwner)
114})
115​
116export const program = repoNames(["R_1", "R_2"]).pipe(
117 Effect.provide(GitHub.layer)