{"id":51009914,"url":"https://github.com/gmac/graphql-breadth-js","last_synced_at":"2026-06-21T01:07:45.766Z","repository":{"id":359130790,"uuid":"1241926730","full_name":"gmac/graphql-breadth-js","owner":"gmac","description":"A reference implementation of breadth-first GraphQL execution for JavaScript","archived":false,"fork":false,"pushed_at":"2026-05-20T16:00:02.000Z","size":124,"stargazers_count":3,"open_issues_count":1,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-20T18:37:45.588Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gmac.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-18T01:15:34.000Z","updated_at":"2026-05-20T16:00:42.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/gmac/graphql-breadth-js","commit_stats":null,"previous_names":["gmac/graphql-breadth-js"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/gmac/graphql-breadth-js","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gmac%2Fgraphql-breadth-js","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gmac%2Fgraphql-breadth-js/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gmac%2Fgraphql-breadth-js/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gmac%2Fgraphql-breadth-js/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gmac","download_url":"https://codeload.github.com/gmac/graphql-breadth-js/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gmac%2Fgraphql-breadth-js/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34590373,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-21T01:07:45.096Z","updated_at":"2026-06-21T01:07:45.760Z","avatar_url":"https://github.com/gmac.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# graphql-breadth\n\nA basic breadth-first GraphQL executor based on [Shopify's Cardinal engine](https://shopify.engineering/faster-breadth-first-graphql-execution). Written in TypeScript, built on top of [graphql-js](https://github.com/graphql/graphql-js) for parsing, type system, and input coercion.\n\nUnlike graphql-js depth traversal, this executor operates breadth-first: every object at a given depth resolves the same field together. This allows per-field overhead to amortize across the entire breadth of a level, and lets lazy loads batch using one Promise _per selection_ rather than _per field instance_.\n\nNote that breadth traversal does still eagerly drill into nested field selections, so this execution model is no more blocking than standard (non-deferred) depth-based execution using Dataloader for batching.\n\n_JavaScript implementation is experimental. No support for subscriptions, defer, and stream in this basic build._\n\n## Benchmarks\n\n**Speed:** single-machine numbers from `pnpm run bench:*` on an M2 MacBook Air, Node 22.\n\n**GC pressure:** numbers from `pnpm run mem:*`. The metric is wall-clock time V8 spent in GC during the run, divided by iterations — a direct proxy for allocation volume. Lower is better. (Heap bytes/iter would be more direct, but V8 triggers GC mid-run on workloads this size, so GC time is the more reliable signal.)\n\n_Some comparisons drop rows due to weak signal._\n\n### Flat list\n\n```graphql\nquery { widgets(first: N) { id } }\n```\n\n**Speed**\n\n| size  | graphql-js | graphql-breadth | ratio                   |\n| ----- | ---------- | --------------- | ----------------------- |\n| 1     | 603k i/s   | 453k i/s        | graphql-js 1.33× faster |\n| 10    | 157k i/s   | 236k i/s        | breadth 1.50× faster    |\n| 100   | 20k i/s    | 42k i/s         | breadth 2.13× faster    |\n| 1000  | 2.1k i/s   | 4.4k i/s        | breadth 2.13× faster    |\n| 10000 | 202 i/s    | 449 i/s         | breadth 2.23× faster    |\n\n**GC pressure**\n\n| size  | graphql-js | graphql-breadth | ratio                   |\n| ----- | ---------- | --------------- | ----------------------- |\n| 100   | 1.9µs/iter | 0.7µs/iter      | breadth 2.80× less GC   |\n| 1000  | 9.0µs/iter | 2.1µs/iter      | breadth 4.39× less GC   |\n| 10000 | 239µs/iter | 31µs/iter       | breadth 7.71× less GC   |\n\n### Tree within list\n\n```graphql\n# inner tree depth D\nquery { widgets(first: N) { widget { widget { id } id } id } }\n```\n\n**Speed**\n\n| D × N    | graphql-js | graphql-breadth | ratio                |\n| -------- | ---------- | --------------- | -------------------- |\n| 1 × 10   | 87k i/s    | 162k i/s        | breadth 1.86× faster |\n| 1 × 100  | 10k i/s    | 29k i/s         | breadth 2.79× faster |\n| 1 × 1000 | 1.1k i/s   | 3.1k i/s        | breadth 2.90× faster |\n| 5 × 10   | 25k i/s    | 45k i/s         | breadth 1.79× faster |\n| 5 × 100  | 2.7k i/s   | 6.9k i/s        | breadth 2.56× faster |\n| 5 × 1000 | 273 i/s    | 740 i/s         | breadth 2.71× faster |\n\n**GC pressure**\n\n| D × N    | graphql-js | graphql-breadth | ratio                 |\n| -------- | ---------- | --------------- | --------------------- |\n| 1 × 100  | 3.2µs/iter | 0.6µs/iter      | breadth 5.07× less GC |\n| 1 × 1000 | 14µs/iter  | 3.7µs/iter      | breadth 3.94× less GC |\n| 5 × 100  | 6.1µs/iter | 1.1µs/iter      | breadth 5.64× less GC |\n| 5 × 1000 | 52µs/iter  | 12µs/iter       | breadth 4.20× less GC |\n\n### List with batched lazy field (DataLoader promises)\n\n```graphql\nquery { widgets(first: N) { id lazy } }  # N promises\n```\n\n**Speed**\n\n| size  | graphql-js | graphql-breadth | ratio                |\n| ----- | ---------- | --------------- | -------------------- |\n| 1     | 285k i/s   | 290k i/s        | breadth 1.02× faster |\n| 10    | 66k i/s    | 137k i/s        | breadth 2.10× faster |\n| 100   | 7.9k i/s   | 22k i/s         | breadth 2.82× faster |\n| 1000  | 758 i/s    | 2.4k i/s        | breadth 3.20× faster |\n| 10000 | 47 i/s     | 244 i/s         | breadth 5.18× faster |\n\n**GC pressure**\n\n| size  | graphql-js   | graphql-breadth | ratio                 |\n| ----- | ------------ | --------------- | --------------------- |\n| 100   | 6.2µs/iter   | 0.3µs/iter      | breadth 18.7× less GC |\n| 1000  | 110µs/iter   | 2.3µs/iter      | breadth 47.1× less GC |\n| 10000 | 5247µs/iter  | 44µs/iter       | breadth 119× less GC  |\n\n### Deep flat tree (no breadth)\n\n```graphql\nquery { widget { widget { widget { id } id } id } }  # depth D\n```\n\n**Speed**\n\n| depth | graphql-js | graphql-breadth | ratio                   |\n| ----- | ---------- | --------------- | ----------------------- |\n| 1     | 866k i/s   | 552k i/s        | graphql-js 1.57× faster |\n| 5     | 235k i/s   | 120k i/s        | graphql-js 1.95× faster |\n| 10    | 125k i/s   | 62k i/s         | graphql-js 2.02× faster |\n| 18    | 69k i/s    | 34k i/s         | graphql-js 2.00× faster |\n\n**GC pressure**\n\n| depth | graphql-js | graphql-breadth | ratio                 |\n| ----- | ---------- | --------------- | --------------------- |\n| 1     | 0.3µs/iter | 0.2µs/iter      | breadth 1.22× less GC |\n| 5     | 1.1µs/iter | 0.5µs/iter      | breadth 2.37× less GC |\n| 10    | 1.2µs/iter | 0.4µs/iter      | breadth 2.89× less GC |\n| 18    | 2.3µs/iter | 0.6µs/iter      | breadth 4.13× less GC |\n\n### Where each executor wins\n\n- **graphql-js wins on speed for deep, narrow queries** — every level holds one object, so breadth-first never engages, and graphql-js's tight inner loop runs unopposed. However, lacking repetition means the disadvantage doesn't scale, so is negligible (~15µs vs ~30µs as a one-time cost per query, even at depth 18).\n- **graphql-breadth wins on speed once a level holds multiple objects**. The win grows with breadth (2–2.5× at 100+) because per-field work amortizes across the level instead of repeating per object.\n- **graphql-breadth wins on GC pressure in every shape tested**, even the deep-narrow case where graphql-js wins on speed. One long-lived `ExecutionField` per level allocates less than one short-lived frame per resolution.\n- **The lazy field case is the headline.** graphql-js + DataLoader pays a Promise per value per leaf; the breadth-first lazy queue drains synchronously inside the executor — no Promise allocations on the hot path. At 10k objects the GC gap is 119× (5247µs vs 44µs/iter), and graphql-js spends ~25% of its wall-clock in GC (5247µs of 21277µs/iter at 47 i/s).\n\nRun the benchmarks yourself:\n\n```bash\npnpm install\nSIZES=1,10,100,1000,10000 pnpm run bench:list\nFIELDS=lazy SIZES=1,10,100,1000,10000 pnpm run bench:list\nDEPTHS=1,5,10,18 pnpm run bench:tree\nDEPTHS=1,5 BREADTHS=10,100,1000 pnpm run bench:tree-list\npnpm run mem:tree\npnpm run mem:list\nFIELDS=lazy pnpm run mem:list\npnpm run mem:tree-list\n```\n\n## Install\n\n```bash\nnpm install graphql-breadth graphql\n```\n\n## Quick start\n\n```ts\nimport { buildSchema } from \"graphql\";\nimport { Executor, ObjectKeyResolver } from \"graphql-breadth\";\n\nconst schema = buildSchema(`type Query { hello: String }`);\n\nconst { result } = Executor.build({\n  schema,\n  document: `{ hello }`,\n  resolvers: {\n    Query: {\n      hello: new ObjectKeyResolver(\"hello\"),\n    },\n  },\n  rootObject: { hello: \"world\" },\n});\n\nconsole.log(result); // { data: { hello: \"world\" } }\n```\n\nAn executor is built with a resolver map that keys `{ TypeName =\u003e { fieldName =\u003e new FieldResolver() } }` to provide all schema field resolvers. Additional options:\n\n```ts\nExecutor.build({\n  schema,           // GraphQLSchema (from graphql-js)\n  document,         // string | DocumentNode\n  resolvers,        // ResolverMap\n  rootObject,       // unknown, optional\n  context,          // unknown, optional - passed to resolvers\n  variables,        // Record\u003cstring, unknown\u003e, optional\n  operationName,    // string | null, optional\n  validateDocument, // boolean, default true - skip if pre-validated\n});\n```\n\n## Resolvers\n\nResolvers receive an `execField` with all field state, including `objects`, `arguments`, and `context`. A resolver must return a mapped set of results derived from `execField.objects`. Returning results with unmatched cardinality is a programming error.\n\n```ts\nimport { FieldResolver } from \"graphql-breadth\";\nimport type { ExecutionField } from \"graphql-breadth\";\n\nclass FullName extends FieldResolver {\n  resolve(execField: ExecutionField) {\n    if (!execField.context.authorized) return execField.resolveAll(null);\n\n    return execField.mapObjects((user) =\u003e `${user.firstName} ${user.lastName}`);\n  }\n}\n```\n\nBuilt-in resolvers are provided to cover common cases:\n\n```ts\nimport {\n  ObjectKeyResolver, // obj[key]\n  MethodResolver,  // obj.method() or obj.a.b.c\n  SelfResolver,    // returns the object itself\n  ValueResolver,   // returns a constant\n} from \"graphql-breadth\";\n\nconst resolvers = {\n  User: {\n    id: new ObjectKeyResolver(\"id\"),\n    fullName: new FullName(),\n    age: new MethodResolver(\"computeAge\"),\n    self: new SelfResolver(),\n    apiVersion: new ValueResolver(\"v2\"),\n  },\n};\n```\n\n## Errors\n\nMap error instances into resolver results, or throw an `ExecutionError` within a `mapObjects` loop:\n\n```ts\nimport { ExecutionError } from \"graphql-breadth\";\n\nclass SecretField extends FieldResolver {\n  resolve(execField) {\n    if (!execField.context.authenticated) throw new ExecutionError(\"Not authorized\");\n\n    return execField.mapObjects(\n      (obj) =\u003e obj.allow() ? obj.secret : new ExecutionError(\"Not authorized\"),\n    );\n  }\n}\n```\n\nRaising an `ExecutionError` outside of `mapObjects` will fail the field across all objects. Unhandled exceptions will terminate all execution.\n\n## Lazy batching\n\nBreadth-based fields receive all `objects` at once, so are implicitly batched. However, lazy batching is still useful when pooling I/O across separate field selections. A `LazyLoader` can pool an entire key set into a single lazy promise. That means only one promise is built _per document selection_, versus _per field instance_ in graphql-js.\n\n```ts\nimport { LazyLoader, FieldResolver } from \"graphql-breadth\";\n\nclass UserById extends LazyLoader {\n  map = true; // perform returns results 1:1 with keys\n  perform(ids: string[]): User[] {\n    return db.usersWhereIdIn(ids); // one query for the entire level\n  }\n}\n\nclass Author extends FieldResolver {\n  resolve(execField) {\n    return execField.lazy({\n      loaderClass: UserById,\n      keys: execField.mapObjects((post) =\u003e post.authorId),\n    });\n  }\n}\n```\n\nTwo orthogonal flags configure how a loader delivers its results:\n\n- `map` — when `true`, `perform`'s return value IS the mapped result array. When `false` (the default), the return value is ignored and the implementation calls `fulfillKey(key, result)` (or `fulfillIdentity`) for each key it resolves.\n- `async` — when `true`, the loader implements `performAsync` instead of `perform`, and the executor awaits the returned Promise before resolving any waiting fields. When `false` (the default), `perform` runs synchronously and the executor drains the lazy queue without yielding to the microtask queue.\n\nAsync loaders are the canonical way to plug remote I/O into the breadth model. Because `performAsync` is called once per document selection — not per field instance — a list of N objects produces a single Promise, regardless of N:\n\n```ts\nclass UserByIdAsync extends LazyLoader {\n  async = true;\n  map = true;\n  async performAsync(ids: string[]): Promise\u003cUser[]\u003e {\n    return await db.usersWhereIdIn(ids); // one round-trip for the whole level\n  }\n}\n```\n\nWhen passing in lazy keys, null keys may be submitted to hold a results position. These will get dropped from the loader set and pass through as null results. Chain a post-load callback with `.then(...)`:\n\n```ts\nclass AuthorName extends FieldResolver {\n  resolve(execField) {\n    return execField\n      .lazy({ loaderClass: UserById, keys: ... })\n      .then((users) =\u003e users.map((u) =\u003e u.name));\n  }\n}\n```\n\nAwaiting and chaining is also supported:\n\n```ts\nclass FancyLazy extends FieldResolver {\n  resolve(execField) {\n    pendingPosts = execField\n      .lazy({ loaderClass: UserById, keys: ... })\n      .then((users) =\u003e execField.lazy({ loaderClass: PostsById, keys: users }));\n\n    pendingPromos = execField\n      .lazy({ loaderClass: PromosById, keys: ... })\n\n    return execField.awaitAll([pendingPosts, pendingPromos])\n      .then((posts, promos) =\u003e posts.zip(promos));\n  }\n}\n```\n\n## Abstract types\n\nFor interfaces and unions, attach a `__type__` resolver that maps an object to its concrete type:\n\n```ts\nconst resolvers = {\n  Character: {\n    __type__: (obj) =\u003e obj.kind === \"droid\" ? schema.getType(\"Droid\") : schema.getType(\"Human\"),\n    id: new ObjectKeyResolver(\"id\"),\n  },\n};\n```\n\nWithout `__type__`, the executor falls back to reading `__typename` off the object.\n\n## Planning phase\n\nBefore execution begins, the planner walks the tree bottom-up and calls `plan()` on every field's resolver. A field's children plan first, so they can annotate their ancestors with dependency information. Both execution scopes and fields have an `attributes` Map for sharing state between resolvers in the same planning pass.\n\n```ts\n// Field resolver for `Widget.sprockets`.\n// Plans first and annotates the scope's parent field to include sprockets.\nclass Sprockets extends FieldResolver {\n  plan(execField) {\n    // Tell the scope above to join sprockets.\n    execField.scope.parentField.attributes.set(\"includeSprockets\", true);\n  }\n\n  resolve(execField) {\n    return execField.mapObjects((widget) =\u003e widget.sprockets);\n  }\n}\n\n// Field resolver for `Query.widgets`.\n// Can check itself for annotations passed upwards by children.\nclass Widgets extends FieldResolver {\n  resolve(execField) {\n    const includeSprockets = execField.attributes.get(\"includeSprockets\") === true;\n    return db.widgets({ join: includeSprockets ? [\"sprockets\"] : [] });\n  }\n}\n```\n\n## GraphQL JS resolvers\n\nFor schemas built with graphql-js's executable schema pattern (where each field carries a `resolve` function with the `(source, args, context, info)` signature), `interpretSchema` walks the schema and produces a `ResolverMap` whose entries delegate to those resolvers. Pass the result to `Executor.build` to run an existing graphql-js schema through the breadth executor unchanged:\n\n```ts\nimport { Executor, interpretSchema } from \"graphql-breadth\";\nimport { schema } from \"./my-graphql-js-schema\";\n\nconst { result } = Executor.build({\n  schema,\n  document: `{ hero { name } }`,\n  resolvers: interpretSchema(schema),\n});\n```\n\nMix interpreted and native resolvers by passing a breadth-native `ResolverMap` as the second argument. Entries are merged field-by-field over the interpreted defaults, so native resolvers retain their breadth-first advantages (single invocation per level, lazy batching, planning) while the rest of the schema runs through the per-object interpreter:\n\n```ts\nconst resolvers = interpretSchema(schema, {\n  User: {\n    posts: new PostsLoader(), // batched native resolver\n  },\n});\n```\n\n**Support notes:**\n\n- Resolvers that return native Promises are awaited together as one breadth-loader cycle via `InterpretedPromiseLoader`, so a list of N async resolvers still yields once, not N times. This is generally compatible though may produce different results for situations designed around a depth-based execution flow.\n- Accessing resolver `info.path` is not supported. Breadth has no concept of runtime subtrees (though this gap is possible to fill with overhead).\n- No support for lazy abstract type resolution. `resolveType` and `isTypeOf` returning a `Promise` throw an `ImplementationError`.\n\n## Development\n\n```bash\npnpm install\npnpm test      # jest\npnpm run build # emits dist/\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgmac%2Fgraphql-breadth-js","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgmac%2Fgraphql-breadth-js","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgmac%2Fgraphql-breadth-js/lists"}