Skip to content

DI container for ENSApi#2159

Merged
tk-o merged 20 commits into
mainfrom
feat/2029-di-container-for-ensapi
May 21, 2026
Merged

DI container for ENSApi#2159
tk-o merged 20 commits into
mainfrom
feat/2029-di-container-for-ensapi

Conversation

@tk-o
Copy link
Copy Markdown
Member

@tk-o tk-o commented May 20, 2026

Lite PR

Tip: Review docs on the ENSNode PR process

Summary

  • Introduces a global EnsApiDiContainer (apps/ensapi/src/di.ts) that lazily evaluates config, stack info, caches, and root-chain clients on first access via getters.
  • Replaces ~21 eager import config from "@/config"; reads across ENSApi handlers, middleware, and libraries with lazy import di from "@/di"; reads from di.context (e.g. di.context.ensApiConfig, di.context.ensNamespaceId, di.context.stackInfo).
  • Simplifies EnsApiConfig to only env-based settings; moves previously eager/async-fetched data (ENSIndexer public config, namespace, RPC configs) out of the config object and into the DI container where they are resolved lazily.
  • Simplifies RPC config: ENSApi just needs a single RPC client for the root chain (see apps/ensapi/src/lib/resolution/forward-resolution.ts).

Why

  • Using lazy / lazyProxy calls is challenging when the proxied objects need to be enumerated down the app stack. We need a way to lazy-evaluated certain values / objects, but ideally, without the use of the Proxy primitive.
  • A DI container with lazy-evaluated getters removes that ordering constraint: modules import di safely at the top level and values are computed on first property access, not at import time.
  • This also centralizes lifecycle management (e.g. di.init() / di.destroy() for caches) in index.ts and makes dependencies explicit and testable.

Testing

  • Existing test suites were updated to work with the DI container context (e.g. ensanalytics-api.test.ts, execute-operations.integration.test.ts, find-domains-resolver-helpers.test.ts).
  • Ran pnpm test --project ensapi to verify affected tests pass.
  • Ran pnpm -F ensapi typecheck and pnpm lint to validate TypeScript and formatting.

Notes for Reviewer (Optional)

  • This PR intentionally does not migrate direct singleton imports such as import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton";. Those will be moved into the DI container in a follow-up PR so that all dependencies are lazy and context-bound.
  • The EnsApiDiContainer is designed to support re-initialization (destroy() then init()) which is useful for integration tests and hot-reload scenarios.

Pre-Review Checklist (Blocking)

  • This PR does not introduce significant changes and is low-risk to review quickly.
  • Relevant changesets are included (or are not required)

Copilot AI review requested due to automatic review settings May 20, 2026 19:26
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 20, 2026

🦋 Changeset detected

Latest commit: e4fc2db

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 22 packages
Name Type
@ensnode/ensnode-sdk Minor
ensapi Minor
ensindexer Minor
ensrainbow Minor
fallback-ensapi Minor
@namehash/ens-referrals Minor
@ensnode/ensdb-sdk Minor
@ensnode/ensrainbow-sdk Minor
@ensnode/integration-test-env Minor
@namehash/namehash-ui Minor
ensadmin Minor
@docs/ensnode Minor
@docs/ensrainbow Minor
enssdk Minor
enscli Minor
enskit Minor
ensskills Minor
@ensnode/datasources Minor
@ensnode/ponder-sdk Minor
@ensnode/ponder-subgraph Minor
@ensnode/shared-configs Minor
@ensnode/ensindexer-perf-testing Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
admin.ensnode.io Ready Ready Preview, Comment May 21, 2026 6:14pm
enskit-react-example.ensnode.io Ready Ready Preview, Comment May 21, 2026 6:14pm
ensnode.io Ready Ready Preview, Comment May 21, 2026 6:14pm
ensrainbow.io Ready Ready Preview, Comment May 21, 2026 6:14pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Review Change Stack

Warning

Rate limit exceeded

@tk-o has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 32 minutes and 24 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8f36d138-da17-406e-bf62-11e48e02b90b

📥 Commits

Reviewing files that changed from the base of the PR and between 1a3f8a1 and e4fc2db.

📒 Files selected for processing (2)
  • apps/ensapi/src/cache/referral-program-edition-set.cache.ts
  • apps/ensapi/src/config/config.schema.test.ts
📝 Walkthrough

Walkthrough

Adds a DI container (di) exposing runtime ENSApi/ENSDb config, namespace/root-chain IDs, public client and caches; migrates modules and middleware to read di.context instead of static config; adds subgraph GraphQL middleware; adds SWRCache.peek() with tests.

Changes

Dependency Injection Refactor

Layer / File(s) Summary
DI container and exports
apps/ensapi/src/di.ts
Defines EnsApiDiContext, buildEnsApiDiContext(env), memoized getters for config/db/namespace/root-chain/rpc client/caches/stackInfo, and a default di singleton.
Config schema & helpers
apps/ensapi/src/config/config.schema.ts, apps/ensapi/src/config/config.schema.test.ts
Narrows EnsApiConfigSchema to { port, theGraphApiKey, referralProgramEditionConfigSetUrl }, makes buildConfigFromEnvironment synchronous, adds buildRootChainRpcConfig, and updates tests to match sync semantics.
Cache modules -> DI wiring
apps/ensapi/src/cache/*
Referral-edition snapshots now use di.context.stackInfo.ensIndexer; referral-program-edition-set reads its URL from di.context.ensApiConfig and exports its cache type; stack-info cache dynamically imports DI to build EnsNodeStackInfo.
Subgraph GraphQL middleware
apps/ensapi/src/lib/subgraph/gql.middleware.ts, apps/ensapi/src/handlers/subgraph/subgraph-api.ts
New middleware filters the ENS indexer schema for subgraph_ fields and is configured from di.context.ensDbConfig; subgraph-api mounts the middleware dynamically and uses DI stackInfo prerequisite checks.
Server startup and lifecycle -> DI
apps/ensapi/src/index.ts
Server initialization calls di.init(), reads port from di.context.ensApiConfig.port, warms caches, and delegates teardown to di.destroy() on shutdown.
Handlers and API routes -> DI
apps/ensapi/src/handlers/*, apps/ensapi/src/omnigraph-api/*
Name-tokens builds indexed subregistries per-request using di.context.namespace and plugins; Omnigraph handlers and schema/resolver use di.context.namespace and stackInfo.
Middleware -> DI context
apps/ensapi/src/middleware/*
can-accelerate, ensanalytics, name-tokens, registrar-actions, thegraph-fallback now read namespace and ensIndexer stack info from di.context.
Resolution and protocol-acceleration DI migration
apps/ensapi/src/lib/*
find-name-tokens, find-resolver, get-records-from-index, forward-resolution, multichain-primary-name-resolution, and indexing-status->subgraph-meta use di.context for namespace, rootChainPublicClient, and version/deployment info.
Public client removal & tests
apps/ensapi/src/lib/public-client.ts, tests
Removed getPublicClient; tests adapted to mock di.context.rootChainPublicClient.
Changesets
.changeset/*
Record minor version bumps and describe DI migration and SWRCache.peek addition.

SWRCache Enhancement

Layer / File(s) Summary
SWRCache.peek implementation & tests
packages/ensnode-sdk/src/shared/cache/swr-cache.ts, packages/ensnode-sdk/src/shared/cache/swr-cache.test.ts
Add SWRCache.peek() to synchronously return the in-memory cached ValueType or Error without triggering revalidation; tests cover uninitialized, cached value, cached error, and TTL-expired stale-value scenarios.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #2029: DI container and broad migration plan — this PR implements the DI container and migrates modules to read runtime context from it.

Possibly related PRs

Suggested labels

javascript

"🐇 I hopped through code so spry,
DI roots took root and reached the sky.
Caches peek, the queries play,
Middleware wakes to a brighter day.
A rabbit cheers—hooray, hooray!"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title "DI container for ENSApi" accurately and concisely describes the main change—introduction of a dependency injection container for the ENSApi application.
Description check ✅ Passed The PR description follows the template with all required sections (Summary, Why, Testing, Notes for Reviewer, Pre-Review Checklist) and provides sufficient detail about changes, rationale, and testing performed.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/2029-di-container-for-ensapi

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ensapi/src/config/config.schema.ts (1)

41-46: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale JSDoc: function no longer fetches anything.

The comment says "fetching the EnsIndexerPublicConfig" but the function is now synchronous and only parses environment variables. Update the documentation to reflect the simplified behavior.

📝 Suggested fix
 /**
- * Builds the EnsApiConfig from an EnsApiEnvironment object, fetching the EnsIndexerPublicConfig.
+ * Builds the EnsApiConfig from an EnsApiEnvironment object by parsing environment variables.
  *
  * `@returns` A validated EnsApiConfig object
  * `@throws` Error with formatted validation messages if environment parsing fails
  */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ensapi/src/config/config.schema.ts` around lines 41 - 46, The JSDoc
above the config builder is stale: it claims the function "fetches the
EnsIndexerPublicConfig" but the implementation is now synchronous and only
parses environment variables into an EnsApiConfig; update the comment to
accurately describe the behavior of the function (that it builds/validates an
EnsApiConfig from an EnsApiEnvironment synchronously), remove any mention of
fetching EnsIndexerPublicConfig, and keep the existing notes about returning a
validated EnsApiConfig and throwing an Error with formatted validation messages
on failure; reference the EnsApiConfig and EnsApiEnvironment symbols in the
revised doc so readers can find the relevant types.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ensapi/src/di.ts`:
- Around line 141-163: The three getters indexingStatusCache,
referralProgramEditionConfigSetCache, and stackInfoCache use redundant
memoization around module-level singletons; change each getter to simply return
the imported singleton (i.e., return indexingStatusCache; return
referralProgramEditionConfigSetCache; return stackInfoCache) and remove the
conditional assignment logic; also remove or stop referencing the corresponding
properties on the instances object if they become unused so there is no dead
state left behind.
- Around line 226-230: The di.context getter currently calls
buildEnsApiDiContext(process.env) on every access, which rebuilds the DI
container and loses memoization; change this to compute and cache the context
once at module level (or lazily on first access) so the same frozen
EnsApiDiContext instance is returned on subsequent accesses. Concretely, create
a module-scoped variable (e.g., cachedEnsApiDiContext) and initialize it by
calling buildEnsApiDiContext(process.env) (or set it on first di.context
access), then have the di.context getter return
Object.freeze(cachedEnsApiDiContext) instead of calling buildEnsApiDiContext
each time; ensure references to buildEnsApiDiContext, di.context, and
EnsApiDiContext are used to locate and update the code.

In `@apps/ensapi/src/handlers/api/explore/name-tokens-api.ts`:
- Around line 80-83: The code computes indexedSubregistries on every request by
calling getIndexedSubregistries(di.context.ensNamespaceId,
di.context.stackInfo.ensIndexer.plugins as PluginName[]); move this into a
memoized or lazily-initialized location (e.g., module-level lazyProxy or store
it on di/context during app startup) so the result is computed once instead of
per-request, and update call sites to read the cached value; also remove the
unchecked cast of di.context.stackInfo.ensIndexer.plugins to PluginName[]—either
validate/filter plugins to the PluginName set before passing them to
getIndexedSubregistries or change getIndexedSubregistries to accept the actual
plugin type and internally narrow/validate entries to avoid silent type errors.

In `@apps/ensapi/src/lib/resolution/multichain-primary-name-resolution.ts`:
- Around line 17-37: The current getENSIP19SupportedChainIds builds an array
that always includes mainnet.id and then appends datasource-derived chain ids
which may include mainnet again; change getENSIP19SupportedChainIds so the final
returned list is deduplicated (e.g., apply uniq or convert to a Set) across the
entire array before it is used by resolveReverse fan-out to avoid duplicate
reverse lookups; keep references to
maybeGetDatasource(di.context.ensNamespaceId, DatasourceNames.*) and mainnet.id
intact and dedupe the combined result returned by getENSIP19SupportedChainIds.

In `@packages/ensnode-sdk/src/shared/cache/swr-cache.ts`:
- Around line 176-180: The JSDoc for peak() repeats the summary in an
unnecessary `@returns` tag; remove the redundant `@returns` line from the peak()
comment block while keeping the summary and the `@throws` tag intact so the doc
follows the repo rule about not restating the method summary.

---

Outside diff comments:
In `@apps/ensapi/src/config/config.schema.ts`:
- Around line 41-46: The JSDoc above the config builder is stale: it claims the
function "fetches the EnsIndexerPublicConfig" but the implementation is now
synchronous and only parses environment variables into an EnsApiConfig; update
the comment to accurately describe the behavior of the function (that it
builds/validates an EnsApiConfig from an EnsApiEnvironment synchronously),
remove any mention of fetching EnsIndexerPublicConfig, and keep the existing
notes about returning a validated EnsApiConfig and throwing an Error with
formatted validation messages on failure; reference the EnsApiConfig and
EnsApiEnvironment symbols in the revised doc so readers can find the relevant
types.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f6c095f7-651a-434f-96cd-2e8951278c2f

📥 Commits

Reviewing files that changed from the base of the PR and between 9f4bb10 and 00defa3.

📒 Files selected for processing (28)
  • apps/ensapi/src/cache/referral-edition-snapshots.cache.ts
  • apps/ensapi/src/cache/referral-program-edition-set.cache.ts
  • apps/ensapi/src/cache/stack-info.cache.ts
  • apps/ensapi/src/config/config.schema.ts
  • apps/ensapi/src/config/index.ts
  • apps/ensapi/src/config/redact.ts
  • apps/ensapi/src/config/validations.ts
  • apps/ensapi/src/di.ts
  • apps/ensapi/src/handlers/api/explore/name-tokens-api.ts
  • apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.ts
  • apps/ensapi/src/handlers/subgraph/subgraph-api.ts
  • apps/ensapi/src/index.ts
  • apps/ensapi/src/lib/name-tokens/find-name-tokens-for-domain.ts
  • apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts
  • apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts
  • apps/ensapi/src/lib/resolution/forward-resolution.ts
  • apps/ensapi/src/lib/resolution/multichain-primary-name-resolution.ts
  • apps/ensapi/src/lib/subgraph/indexing-status-to-subgraph-meta.ts
  • apps/ensapi/src/middleware/can-accelerate.middleware.ts
  • apps/ensapi/src/middleware/ensanalytics.middleware.ts
  • apps/ensapi/src/middleware/name-tokens.middleware.ts
  • apps/ensapi/src/middleware/registrar-actions.middleware.ts
  • apps/ensapi/src/middleware/thegraph-fallback.middleware.ts
  • apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts
  • apps/ensapi/src/omnigraph-api/schema/query.ts
  • apps/ensapi/src/omnigraph-api/schema/resolver.ts
  • packages/ensnode-sdk/src/shared/cache/swr-cache.test.ts
  • packages/ensnode-sdk/src/shared/cache/swr-cache.ts
💤 Files with no reviewable changes (3)
  • apps/ensapi/src/config/index.ts
  • apps/ensapi/src/config/redact.ts
  • apps/ensapi/src/config/validations.ts

Comment thread apps/ensapi/src/di.ts
Comment thread apps/ensapi/src/di.ts Outdated
Comment thread apps/ensapi/src/handlers/api/explore/name-tokens-api.ts
Comment thread packages/ensnode-sdk/src/shared/cache/swr-cache.ts
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new DI container for ENSApi and migrates many modules off the previous global config singleton, while also extending the SDK’s SWR cache to support synchronous reads.

Changes:

  • Add SWRCache.peak() (sync access to cached value) plus tests.
  • Add apps/ensapi/src/di.ts and refactor ENSApi codepaths to read config/stack info/caches via di.context.
  • Remove the old ENSApi config singleton and related validation/redaction helpers, updating handlers/middleware accordingly.

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
packages/ensnode-sdk/src/shared/cache/swr-cache.ts Adds new synchronous cache accessor (peak).
packages/ensnode-sdk/src/shared/cache/swr-cache.test.ts Adds test coverage for the new cache accessor.
apps/ensapi/src/omnigraph-api/schema/resolver.ts Switches namespace sourcing from config to DI.
apps/ensapi/src/omnigraph-api/schema/query.ts Switches namespace sourcing from config to DI.
apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts Switches namespace sourcing from config to DI.
apps/ensapi/src/middleware/thegraph-fallback.middleware.ts Reads namespace/TheGraph key/subgraph compatibility from DI.
apps/ensapi/src/middleware/registrar-actions.middleware.ts Reads ENSIndexer capability info from DI stackInfo.
apps/ensapi/src/middleware/name-tokens.middleware.ts Reads ENSIndexer capability info from DI stackInfo.
apps/ensapi/src/middleware/ensanalytics.middleware.ts Reads ENSIndexer capability info from DI stackInfo.
apps/ensapi/src/middleware/can-accelerate.middleware.ts Reads namespace/plugins from DI stackInfo.
apps/ensapi/src/lib/subgraph/indexing-status-to-subgraph-meta.ts Uses DI-derived root chain + version info for _meta.
apps/ensapi/src/lib/resolution/multichain-primary-name-resolution.ts Switches namespace sourcing to DI; removes prior lazy memoization.
apps/ensapi/src/lib/resolution/forward-resolution.ts Switches namespace sourcing to DI; changes public client wiring.
apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts Switches namespace sourcing to DI.
apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts Switches namespace sourcing to DI (and leaves an error message typo).
apps/ensapi/src/lib/name-tokens/find-name-tokens-for-domain.ts Switches namespace sourcing to DI.
apps/ensapi/src/index.ts Boots server using DI config and warms caches on startup; updates shutdown hooks.
apps/ensapi/src/handlers/subgraph/subgraph-api.ts Uses DI-provided subgraph GraphQL middleware.
apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.ts Reads ENSIndexer capability info from DI stackInfo.
apps/ensapi/src/handlers/api/explore/name-tokens-api.ts Switches namespace/plugins sourcing to DI stackInfo.
apps/ensapi/src/di.ts Adds new ENSApi DI container and wiring for config/db/stackInfo/rpc/subgraph middleware.
apps/ensapi/src/config/validations.ts Removes previous config validation checks.
apps/ensapi/src/config/redact.ts Removes prior config redaction helper.
apps/ensapi/src/config/index.ts Removes the old global config singleton.
apps/ensapi/src/config/config.schema.ts Simplifies EnsApiConfig parsing (no longer fetches indexer public config).
apps/ensapi/src/cache/stack-info.cache.ts Updates stack-info cache to build public config via DI.
apps/ensapi/src/cache/referral-program-edition-set.cache.ts Reads referral editions URL from DI config; exports cache type.
apps/ensapi/src/cache/referral-edition-snapshots.cache.ts Reads ENSIndexer capability info from DI stackInfo.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/ensnode-sdk/src/shared/cache/swr-cache.ts
Comment thread packages/ensnode-sdk/src/shared/cache/swr-cache.ts
Comment thread apps/ensapi/src/di.ts Outdated
Comment thread apps/ensapi/src/di.ts
Comment thread apps/ensapi/src/lib/resolution/forward-resolution.ts
Comment thread apps/ensapi/src/index.ts
Comment thread apps/ensapi/src/config/config.schema.ts
Comment thread apps/ensapi/src/cache/stack-info.cache.ts Outdated
Comment thread apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts Outdated
Comment thread apps/ensapi/src/index.ts
Comment thread apps/ensapi/src/di.ts Outdated
Comment thread apps/ensapi/src/di.ts Outdated
Comment thread packages/ensnode-sdk/src/shared/cache/swr-cache.ts
Comment thread apps/ensapi/src/index.ts Outdated
@vercel vercel Bot temporarily deployed to Preview – ensnode.io May 20, 2026 19:50 Inactive
@vercel vercel Bot temporarily deployed to Preview – admin.ensnode.io May 20, 2026 19:50 Inactive
@vercel vercel Bot temporarily deployed to Preview – ensrainbow.io May 20, 2026 19:50 Inactive
Enables sync access to the currently cached value.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 8 comments.

Comment thread apps/ensapi/src/di.ts Outdated
Comment thread apps/ensapi/src/di.ts
Comment thread apps/ensapi/src/lib/resolution/forward-resolution.ts
Comment thread apps/ensapi/src/config/config.schema.ts
Comment thread apps/ensapi/src/config/config.schema.ts Outdated
Comment thread apps/ensapi/src/cache/stack-info.cache.ts Outdated
Comment thread apps/ensapi/src/handlers/subgraph/subgraph-api.ts Outdated
tk-o added 2 commits May 20, 2026 22:18
We use `import di from @/di;` now to allow for lazy initialization upon config access.
@tk-o tk-o force-pushed the feat/2029-di-container-for-ensapi branch from a960bc4 to 95c0285 Compare May 20, 2026 20:18
@vercel vercel Bot temporarily deployed to Preview – ensnode.io May 20, 2026 20:18 Inactive
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (3)

apps/ensapi/src/lib/resolution/forward-resolution.ts:165

  • publicClient is always di.context.rootChainPublicClient, but _resolveForward can recurse into a bridged resolver path (registry: bridged.targetRegistry) where chainId changes to the target registry’s chain. Using a root-chain-only client will send RPC reads to the wrong network for bridged registries (Basenames/Lineanames shadow registries). This should select a PublicClient based on the current chainId (e.g., DI exposes a getPublicClient(chainId) backed by per-chain RPC configs).
          const publicClient = di.context.rootChainPublicClient;

          ////////////////////////////
          /// 0. Temporary ENSv2 Bailout
          ////////////////////////////
          // TODO: re-enable protocol acceleration for ENSv2

apps/ensapi/src/handlers/subgraph/subgraph-api.ts:27

  • hasSubgraphApiConfigSupport(di.context.stackInfo.ensIndexer) can throw if stackInfoCache.peek() is not ready (or errored), which would bypass the intended 503 response logic in this guard. Consider using an awaited stack-info value (e.g., await di.context.stackInfoCache.read() and handle Error) or reusing the existing stackInfoMiddleware pattern for request-scoped gating.
app.use(async (c, next) => {
  const configPrerequisite = hasSubgraphApiConfigSupport(di.context.stackInfo.ensIndexer);
  // 503 if Subgraph API is not available due to config prerequisites not met
  if (!configPrerequisite.supported) {
    return c.text(`Service Unavailable: ${configPrerequisite.reason}`, 503);
  }

.changeset/clever-coins-sell.md:6

  • Typo in changeset text: "granual" → "granular".

Replaced all eagerly-evaluated reads from `import config from "@/config";` with lazy-evaluated reads from `import di from "@/di";`. This change allows more granual control over internal resources in ENSApi.

Comment thread apps/ensapi/src/di.ts
Comment thread apps/ensapi/src/di.ts
Comment thread apps/ensapi/src/middleware/thegraph-fallback.middleware.ts
Comment thread apps/ensapi/src/handlers/subgraph/subgraph-api.ts
Comment thread apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts Outdated
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 21, 2026

Greptile Summary

This PR replaces the eager @/config singleton with a lazy EnsApiDiContainer (di.ts) that resolves configuration, cache instances, and the root-chain viem client on first property access. It also simplifies EnsApiConfig to env-only fields and moves ENSIndexer-derived values (namespace, RPC config) into the DI context, eliminating the async pRetry startup block.

  • EnsApiDiContainer wraps all lazily-computed values behind typed getters; di.init() / di.destroy() manage the lifecycle and fire off cache warm-up as a fire-and-forget Promise.all.
  • buildConfigFromEnvironment is simplified to a synchronous function; buildRootChainRpcConfig is extracted as a separate helper called lazily from the container.
  • Circular-dependency breakage between stack-info.cache.ts and di.ts is handled with an async dynamic import inside the cache loader, with a comment that a factory-function refactor will remove it later.

Confidence Score: 5/5

Safe to merge; the DI container correctly gates all stackInfo-dependent paths at request time, and the intentional simplification to a single root-chain RPC client is consistent throughout the change.

The core refactor is mechanical (swapping eager config reads for lazy DI context reads) and is well-covered by existing tests. The fire-and-forget cache warm-up in init() is safe because routes are gated on cache availability. The only issues found are a destroy→init re-initialization edge case where cached data is not flushed and the proactive-revalidation interval is not re-armed, and a fragile load-order assumption in gql.middleware.ts — neither affects the normal single-lifecycle production path.

packages/ensnode-sdk/src/shared/cache/swr-cache.ts (destroy semantics) and apps/ensapi/src/lib/subgraph/gql.middleware.ts (implicit init dependency at module level)

Important Files Changed

Filename Overview
apps/ensapi/src/di.ts New DI container; lazy getters are well-structured, but destroy() on SWRCache singletons clears background revalidation intervals without resetting cache data, making destroy→init cycles not fully clean.
apps/ensapi/src/index.ts Startup/shutdown wiring updated to use DI container; closeServer is now correctly called first in graceful shutdown before resource cleanup.
apps/ensapi/src/config/config.schema.ts EnsApiConfig simplified to env-only fields; pRetry startup block removed; buildRootChainRpcConfig extracted as a new helper with correct error-exit handling.
apps/ensapi/src/lib/subgraph/gql.middleware.ts New module extracted from subgraph-api.ts; accesses di.context at module-evaluation time, which is safe only because it is always loaded via a dynamic import at request time after di.init() has run.
packages/ensnode-sdk/src/shared/cache/swr-cache.ts New peek() method added with tests; destroy() still only clears the background interval without resetting this.cache, meaning cached data and the proactive-revalidation interval are not restored after a destroy→init cycle.
apps/ensapi/src/lib/resolution/forward-resolution.ts Replaces getPublicClient(chainId) with di.context.rootChainPublicClient, consistent with the PR's intentional simplification to a single root-chain RPC client; lazy getters from DI are safe at call time.
apps/ensapi/src/cache/stack-info.cache.ts Circular dependency with di.ts broken via async dynamic import inside the loader; comment acknowledges this is temporary pending a factory-function refactor.
apps/ensapi/src/handlers/subgraph/subgraph-api.ts lazy() wrapper replaced by a dynamic import of gql.middleware.ts on first request; simpler and more idiomatic, correctly defers DI context access to request time.

Sequence Diagram

sequenceDiagram
    participant idx as index.ts
    participant di as EnsApiDiContainer
    participant ctx as EnsApiDiContext
    participant sc as stackInfoCache
    participant ic as indexingStatusCache
    participant req as HTTP Request

    idx->>di: di.init()
    di->>di: loadContext() builds EnsApiDiContext from process.env
    di->>sc: void stackInfoCache.read() [fire-and-forget]
    di->>ic: void indexingStatusCache.read() [fire-and-forget]
    di-->>idx: returns synchronously

    idx->>idx: serve(app, port from di.context.ensApiConfig.port)

    req->>ctx: di.context.namespace
    ctx->>sc: stackInfoCache.peek()
    sc-->>ctx: EnsNodeStackInfo or throws if not ready
    ctx-->>req: ENSNamespaceId

    req->>ctx: di.context.rootChainPublicClient
    ctx->>ctx: buildRootChainRpcConfig cached on first access
    ctx-->>req: PublicClient for root chain

    idx->>di: di.destroy() on SIGTERM or SIGINT
    di->>sc: stackInfoCache.destroy()
    di->>ic: indexingStatusCache.destroy()
    di->>di: set _context to undefined
Loading

Reviews (5): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment thread apps/ensapi/src/di.ts
Comment thread apps/ensapi/src/di.ts
@vercel vercel Bot temporarily deployed to Preview – admin.ensnode.io May 21, 2026 12:57 Inactive
@vercel vercel Bot temporarily deployed to Preview – ensrainbow.io May 21, 2026 12:57 Inactive
@vercel vercel Bot temporarily deployed to Preview – ensnode.io May 21, 2026 12:57 Inactive
@tk-o
Copy link
Copy Markdown
Member Author

tk-o commented May 21, 2026

@greptile review

Copy link
Copy Markdown
Member

@shrugs shrugs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some questions but otherwise LGTM! can we get rid of the lazy proxy entirely now?

Comment thread .changeset/clever-coins-sell.md Outdated
"ensapi": minor
---

Replaced all eagerly-evaluated reads from `import config from "@/config";` with lazy-evaluated reads from `import di from "@/di";`. This change allows more granual control over internal resources in ENSApi.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo internal change needs no changeset

// biome-ignore lint/style/noNonNullAssertion: domain.name guaranteed to exist
const name = asInterpretedName(record.domains.name!);
const ownership = getNameTokenOwnership(config.namespace, name, owner);
const ownership = getNameTokenOwnership(di.context.ensNamespaceId, name, owner);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i realize ensNamespaceId is most correct, but what about maintaining namespace? di.context.namespace reads cleaner ¯\_(ツ)_/¯

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, will update that one 👍

Comment thread apps/ensapi/src/di.ts
* Initializes the DI container by loading the context and initializing
* necessary resources.
*/
init(): void {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm certainly not an expert in this pattern, but my intuition was that the init function would have to be async because of the async dependencies, but then after we await init, anything further can run synchronously. and that we'd do something like await di.init() somewhere in the ensapi startup sequence.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea here is that we need the init to lazy-access all cache instances which are configured withproactivelyInitialize: true. The idea here is not to wait for the cache instances to actually have cached result ready, but rather to have the cache instances to be instantiated.

The goal of the di.init() is not to block the HTTP server from becoming open ASAP. The HTTP server needs to be available as fast as possible, and the HTTP route handlers have to be able to handle situations when relevant cache result is not available at the request time.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we don't really need the async dependencies inside di.init() to actually return any value to the caller scope, so not need to wait for this step really.

Comment thread apps/ensapi/src/index.ts
// start ENSNode API OpenTelemetry SDK
sdk.start();
// initialize DI container and its resources
di.init();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like right here I'm surprised that we don't need to await

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I shared the explanation here. In short, we don't want the HTTP server start to become blocked on caches being populated from I/O.

@tk-o
Copy link
Copy Markdown
Member Author

tk-o commented May 21, 2026

@shrugs about this one:

can we get rid of the lazy proxy entirely now?

I'll need two PRs more to make the lazyProxy gone from the codebase.

Copilot AI review requested due to automatic review settings May 21, 2026 17:48
@vercel vercel Bot temporarily deployed to Preview – ensrainbow.io May 21, 2026 17:48 Inactive
@vercel vercel Bot temporarily deployed to Preview – admin.ensnode.io May 21, 2026 17:48 Inactive
@vercel vercel Bot temporarily deployed to Preview – ensnode.io May 21, 2026 17:48 Inactive
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated 7 comments.

Comment thread apps/ensapi/src/di.ts
Comment thread apps/ensapi/src/di.ts
Comment thread apps/ensapi/src/cache/referral-program-edition-set.cache.ts
Comment thread apps/ensapi/src/lib/resolution/forward-resolution.ts
Comment thread apps/ensapi/src/config/config.schema.test.ts Outdated
Comment thread .changeset/clever-coins-sell.md Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ensapi/src/di.ts (1)

149-156: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't collapse RPC access to a root-chain-only client.

This container now exposes only rootChainPublicClient, but forward resolution still hops into bridged.targetRegistry and then runs resolver RPCs there. Once the active resolver lives on Base/Linea/etc, isExtendedResolver() / executeOperations() will still hit the root-chain RPC through this client and query the wrong network. DI still needs a client lookup keyed by the active registry/resolver chain for bridged resolution to remain correct.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ensapi/src/di.ts` around lines 149 - 156, The DI currently exposes a
single rootChainPublicClient (instances.rootChainPublicClient created from
context.rootChainRpcConfig) which causes bridged resolution paths
(bridged.targetRegistry -> isExtendedResolver / executeOperations) to query the
wrong network; change the DI to provide a lookup/registry of PublicClient
instances keyed by chain/registry/resolver identity (e.g., map from chainId or
targetRegistry id to PublicClient) instead of collapsing to only
rootChainPublicClient, populate that map from the configured RPC lists (similar
to how rootChainPublicClient is created from context.rootChainRpcConfig), and
update callers that resolve bridged.targetRegistry, isExtendedResolver, and
executeOperations to fetch the correct client from the new lookup by the active
registry/chain key.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/clever-coins-sell.md:
- Line 5: Update the changeset description to fix the typo: replace "granual"
with "granular" in the sentence that reads "This change allows more granual
control over internal resources in ENSApi." Ensure the updated text references
the same symbols/imports (import config, import di, ENSApi) so the meaning
remains unchanged.

In `@apps/ensapi/src/di.ts`:
- Around line 126-146: The namespace/getters currently read context.stackInfo
synchronously which can fail before stackInfoCache.read() warms up; change the
implementation so di.context.namespace, rootChainId, and rootChainRpcConfig do
not block on stackInfoCache: either (A) initialize and store a minimal fallback
namespace synchronously during init() (e.g., set instances.namespace in init
prior to starting stackInfoCache.read()) and have namespace return that stored
value, then compute rootChainId via getENSRootChainId(instances.namespace) and
rootChainRpcConfig via buildRootChainRpcConfig(instances.namespace), or (B) make
these getters throw a clear NotReady/503-style error and expose a readiness
check (e.g., context.isReady or stackReady) that init() flips once
stackInfoCache.read() completes; update di.context.namespace, rootChainId,
rootChainRpcConfig, and init() accordingly and ensure route handlers use the
readiness check if you choose option B.

---

Outside diff comments:
In `@apps/ensapi/src/di.ts`:
- Around line 149-156: The DI currently exposes a single rootChainPublicClient
(instances.rootChainPublicClient created from context.rootChainRpcConfig) which
causes bridged resolution paths (bridged.targetRegistry -> isExtendedResolver /
executeOperations) to query the wrong network; change the DI to provide a
lookup/registry of PublicClient instances keyed by chain/registry/resolver
identity (e.g., map from chainId or targetRegistry id to PublicClient) instead
of collapsing to only rootChainPublicClient, populate that map from the
configured RPC lists (similar to how rootChainPublicClient is created from
context.rootChainRpcConfig), and update callers that resolve
bridged.targetRegistry, isExtendedResolver, and executeOperations to fetch the
correct client from the new lookup by the active registry/chain key.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 71a42f8e-bfd5-4df6-8806-78a2d222b12d

📥 Commits

Reviewing files that changed from the base of the PR and between 868ac54 and 1a3f8a1.

📒 Files selected for processing (17)
  • .changeset/clever-coins-sell.md
  • .changeset/thirty-bees-divide.md
  • apps/ensapi/src/config/config.schema.test.ts
  • apps/ensapi/src/config/config.schema.ts
  • apps/ensapi/src/di.ts
  • apps/ensapi/src/handlers/api/explore/name-tokens-api.ts
  • apps/ensapi/src/index.ts
  • apps/ensapi/src/lib/name-tokens/find-name-tokens-for-domain.ts
  • apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts
  • apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts
  • apps/ensapi/src/lib/resolution/forward-resolution.ts
  • apps/ensapi/src/lib/resolution/multichain-primary-name-resolution.ts
  • apps/ensapi/src/middleware/can-accelerate.middleware.ts
  • apps/ensapi/src/middleware/thegraph-fallback.middleware.ts
  • apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts
  • apps/ensapi/src/omnigraph-api/schema/query.ts
  • apps/ensapi/src/omnigraph-api/schema/resolver.ts

Comment thread .changeset/clever-coins-sell.md Outdated
Comment thread apps/ensapi/src/di.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants