Skip to content

Make ENSDb Writer Worker to use builders for ENSIndexer Public Config and Indexing Status objects#1715

Merged
lightwalker-eth merged 13 commits intomainfrom
feat/ensdb-to-power-ensindexer-http-handlers
Mar 4, 2026
Merged

Make ENSDb Writer Worker to use builders for ENSIndexer Public Config and Indexing Status objects#1715
lightwalker-eth merged 13 commits intomainfrom
feat/ensdb-to-power-ensindexer-http-handlers

Conversation

@tk-o
Copy link
Contributor

@tk-o tk-o commented Mar 4, 2026

Lite PR

Tip: Review docs on the ENSNode PR process

Summary

  • Created PublicConfigBuilder class to cover all abstractions required for building EnsIndexerPublicConfig object within ENSIndexer runtime.
  • UpdatedEnsDbWriterWorker to build ENSIndexer Public Config object with PublicConfigBuilder, instead of reading the config from EnsIndexerClient.
  • Updated ENSIndexer HTTP handlers to read ENSIndexer Public Config and Indexing Status from ENSDb Client, instead of building those objects at the time of a HTTP request.

Why

  • We need to detach ENSIndexer from ENSApi. Indexing Status API for both of these services must be powered solely by ENSDb Client when it comes to reading ENSIndexer Public Config or the current Indexing Status.

Testing

  • Ran lint, and typecheck.
  • Extended testing suite, all tests are in green.
  • Tested HTTP APIs of a local ENSIndexer instance.

Notes for Reviewer (Optional)

  • Review commit-by-commit is highly encouraged.
  • ENSIndexer might not need HTTP APIs anymore...

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 March 4, 2026 10:56
@vercel
Copy link
Contributor

vercel bot commented Mar 4, 2026

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

3 Skipped Deployments
Project Deployment Actions Updated (UTC)
admin.ensnode.io Skipped Skipped Mar 4, 2026 0:15am
ensnode.io Skipped Skipped Mar 4, 2026 0:15am
ensrainbow.io Skipped Skipped Mar 4, 2026 0:15am

@changeset-bot
Copy link

changeset-bot bot commented Mar 4, 2026

🦋 Changeset detected

Latest commit: 5fa7ba0

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

This PR includes changesets to release 18 packages
Name Type
@ensnode/ensnode-sdk Major
ensindexer Major
ensadmin Major
ensapi Major
ensrainbow Major
fallback-ensapi Major
@namehash/ens-referrals Major
@ensnode/ensnode-react Major
@ensnode/ensrainbow-sdk Major
@namehash/namehash-ui Major
@ensnode/datasources Major
@ensnode/ensnode-schema Major
@ensnode/ponder-sdk Major
@ensnode/ponder-subgraph Major
@ensnode/shared-configs Major
@docs/ensnode Major
@docs/ensrainbow Major
@docs/mintlify Major

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6ff8e888-e11e-4735-a020-98dc8859bb62

📥 Commits

Reviewing files that changed from the base of the PR and between 8e53487 and 5fa7ba0.

📒 Files selected for processing (1)
  • apps/ensindexer/src/lib/version-info.ts

📝 Walkthrough

Walkthrough

Replaces ad-hoc public-config construction with a cached PublicConfigBuilder, shifts EnsDbWriterWorker to depend on that builder, adds SDK validators validateEnsIndexerPublicConfig and validateEnsIndexerVersionInfo, and adds tests, mocks, singletons, and ENSDB/API handler wiring to consume stored config and snapshots.

Changes

Cohort / File(s) Summary
Changelog Entries
\.changeset/flat-flowers-shave.md, \.changeset/violet-tires-sell.md
Added minor-release changelog entries for @ensnode/ensnode-sdk and ensindexer.
PublicConfigBuilder
apps/ensindexer/src/lib/public-config-builder/public-config-builder.ts, apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts, apps/ensindexer/src/lib/public-config-builder/index.ts, apps/ensindexer/src/lib/public-config-builder/singleton.ts
New cached PublicConfigBuilder class that fetches ENSRainbow config, assembles/validates version info, caches result; comprehensive tests and singleton export added.
Worker & Wiring
apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts, apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts, apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts, apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts
EnsDbWriterWorker now depends on PublicConfigBuilder (replacing EnsIndexerClient), constructor/signature and singleton wiring updated, added typed mocks and tests, and added run() error handling in worker startup.
Removed builder function
apps/ensindexer/src/config/public.ts
Deleted legacy buildENSIndexerPublicConfig(); its responsibilities migrated into PublicConfigBuilder.
API handler and ENSDB usage
apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts
Handlers now use EnsDbClient getters (getEnsIndexerPublicConfig, getIndexingStatusSnapshot) with guards and unified error handling; removed local config construction logic.
Version utilities
apps/ensindexer/src/lib/version-info.ts
Added getEnsIndexerVersion() and getNodeJsVersion() exports; removed prior combined helper.
SDK Validator Exports
packages/ensnode-sdk/src/ensindexer/config/validate/ensindexer-public-config.ts, packages/ensnode-sdk/src/ensindexer/config/validate/ensindexer-version-info.ts, packages/ensnode-sdk/src/ensindexer/config/index.ts
Added and exported validateEnsIndexerPublicConfig and validateEnsIndexerVersionInfo using Zod schemas; they throw formatted errors on validation failure and return typed objects on success.
Tests & Mocks
apps/ensindexer/src/lib/ensdb-writer-worker/*.test.ts, *.mock.ts, public-config-builder.test.ts
New and updated tests cover PublicConfigBuilder caching, validation/error handling, worker initialization paths, snapshot upserts, and typed mock helpers for EnsDb interactions.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Worker as EnsDbWriterWorker
  participant Builder as PublicConfigBuilder
  participant ENSRainbow as ENSRainbowApiClient
  participant Validator as SDKValidators
  participant EnsDB as EnsDbClient

  Worker->>Builder: getPublicConfig()
  Builder->>ENSRainbow: config() (fetch ENSRainbowPublicConfig)
  ENSRainbow-->>Builder: ENSRainbowPublicConfig
  Builder->>Validator: validateEnsIndexerVersionInfo(...)
  Validator-->>Builder: ValidatedVersionInfo
  Builder->>Validator: validateEnsIndexerPublicConfig(assembledConfig)
  Validator-->>Builder: ValidatedPublicConfig
  Builder-->>Worker: EnsIndexerPublicConfig (cached)
  Worker->>EnsDB: upsertEnsIndexerPublicConfig / upsertIndexingStatusSnapshot(...)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped to fetch the rainbow's hue,

Cached it snug so workers know what to do,
Validators nodded, versions in sight,
Snapshots saved through day and night,
A happy hop — configs cozy and bright! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Make ENSDb Writer Worker to use builders for ENSIndexer Public Config and Indexing Status objects' clearly and specifically describes the main change in the PR—refactoring the ENSDb Writer Worker to use builder patterns instead of direct configuration reading.
Description check ✅ Passed The pull request description follows the template structure with all required sections (Summary, Why, Testing, Notes for Reviewer, Pre-Review Checklist) filled out appropriately with substantive content.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ensdb-to-power-ensindexer-http-handlers

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.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 4, 2026

Greptile Summary

This PR successfully decouples ENSIndexer's HTTP handlers from config-building by introducing a PublicConfigBuilder class and routing all reads of EnsIndexerPublicConfig and Indexing Status through the EnsDbClient. The refactor is well-structured and thoroughly tested.

Key changes:

  • New PublicConfigBuilder: Lazy-initialized singleton that fetches ENSRainbow config and version info, validates the result, and caches it immutably.
  • EnsDbWriterWorker refactored: Now accepts a PublicConfigBuilder dependency; fetches stored and in-memory configs concurrently with Promise.all and retries via p-retry.
  • HTTP handlers simplified: /config and /indexing-status both delegate to ensDbClient — no more per-request config builds.
  • New SDK validators: validateEnsIndexerPublicConfig and validateEnsIndexerVersionInfo added as clean Zod-backed validators.

The core logic — moving config reads to ENSDb and writes to the worker — is correct and well-tested. Error handling is appropriate for each endpoint.

Confidence Score: 5/5

  • PR is safe to merge; the refactor is well-structured, thoroughly tested, and introduces no new risks.
  • The refactor is clean, well-tested, and logically sound. All test cases pass. The new PublicConfigBuilder class is properly cached and validated. The EnsDbWriterWorker correctly fetches both stored and in-memory configs concurrently with retry logic. HTTP handlers appropriately delegate to EnsDbClient with documented invariants. No functional issues or regressions detected.
  • No files require special attention.

Sequence Diagram

sequenceDiagram
    participant App as ENSIndexer App
    participant Worker as EnsDbWriterWorker
    participant PCB as PublicConfigBuilder
    participant ENSRainbow as ENSRainbow Client
    participant ENSDb as ENSDb Client
    participant HTTP as HTTP Handler (ensnode-api)

    App->>Worker: startEnsDbWriterWorker()
    note over App,Worker: run() is NOT awaited

    par Concurrent fetch during startup
        Worker->>PCB: getPublicConfig() [with pRetry]
        PCB->>ENSRainbow: config()
        ENSRainbow-->>PCB: EnsRainbowPublicConfig
        PCB->>PCB: validateEnsIndexerPublicConfig(...)
        PCB-->>Worker: EnsIndexerPublicConfig (cached)
    and
        Worker->>ENSDb: getEnsIndexerPublicConfig()
        ENSDb-->>Worker: storedConfig (or undefined)
    end

    opt storedConfig exists
        Worker->>Worker: validateEnsIndexerPublicConfigCompatibility(stored, inMemory)
    end

    Worker->>ENSDb: upsertEnsDbVersion(versionInfo.ensDb)
    Worker->>ENSDb: upsertEnsIndexerPublicConfig(inMemoryConfig)

    loop Every 1 second
        Worker->>Worker: getValidatedIndexingStatusSnapshot()
        Worker->>ENSDb: upsertIndexingStatusSnapshot(crossChainSnapshot)
    end

    HTTP->>ENSDb: GET /config → getEnsIndexerPublicConfig()
    ENSDb-->>HTTP: EnsIndexerPublicConfig
    HTTP-->>HTTP: serializeENSIndexerPublicConfig(config)

    HTTP->>ENSDb: GET /indexing-status → getIndexingStatusSnapshot()
    ENSDb-->>HTTP: CrossChainIndexingStatusSnapshot
    HTTP-->>HTTP: createRealtimeIndexingStatusProjection(snapshot, now)
Loading

Last reviewed commit: 8e53487

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 4, 2026

Additional Comments (2)

apps/ensindexer/src/lib/version-info.ts
Dead code: getENSIndexerVersionInfo is no longer used

Now that PublicConfigBuilder builds version info internally using the individual helper functions (getEnsIndexerVersion, getNodeJsVersion, getPackageVersion), the top-level getENSIndexerVersionInfo function in this file is unreferenced anywhere in the codebase. Keeping it risks future callers using the old function and bypassing validation introduced in PublicConfigBuilder, and the two paths use slightly different Zod validation mechanisms (makeENSIndexerVersionInfoSchema directly here vs. validateEnsIndexerVersionInfo in the builder).

Consider removing getENSIndexerVersionInfo to keep the module lean and avoid confusion about which code path is authoritative.


apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts
storedConfig is assigned the module object instead of the config value

ensDbClientMock is the entire module namespace (import * as ensDbClientMock from "@/lib/ensdb-client/ensdb-client.mock"), not an EnsIndexerPublicConfig. Assigning the module itself as storedConfig means ensDbClient.getEnsIndexerPublicConfig will resolve to the module object, which is then passed to validateEnsIndexerPublicConfigCompatibility(storedConfig, inMemoryConfig). The test only passes because validateEnsIndexerPublicConfigCompatibility is mocked to pass silently in this test case.

If the mock is ever removed or the real validator is invoked, this would produce a confusing type mismatch. It appears the intent was:

      const storedConfig = ensDbClientMock.publicConfig;
      const inMemoryConfig = ensDbClientMock.publicConfig;

Copy link
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 continues the ENSIndexer/ENSApi decoupling by making ENSDb the single source of truth for ENSIndexer’s public config and indexing status in HTTP handlers, and by moving public-config construction into a dedicated builder used during startup/worker execution.

Changes:

  • Added SDK validation helpers for EnsIndexerPublicConfig and EnsIndexerVersionInfo and exported them from @ensnode/ensnode-sdk.
  • Introduced PublicConfigBuilder in ENSIndexer to build/cache the public config (incl. ENSRainbow config + runtime version info) for ENSDb writes.
  • Updated ENSDb writer worker and HTTP endpoints to read public config and indexing status from EnsDbClient instead of building them per-request.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/ensnode-sdk/src/ensindexer/config/validate/ensindexer-version-info.ts New validator for EnsIndexerVersionInfo.
packages/ensnode-sdk/src/ensindexer/config/validate/ensindexer-public-config.ts New validator for EnsIndexerPublicConfig.
packages/ensnode-sdk/src/ensindexer/config/index.ts Re-exports the new validation helpers.
apps/ensindexer/src/lib/version-info.ts Adds helpers to get ENSIndexer + Node.js versions for building version info.
apps/ensindexer/src/lib/public-config-builder/public-config-builder.ts New builder responsible for building/caching validated public config (incl. ENSRainbow fetch).
apps/ensindexer/src/lib/public-config-builder/singleton.ts Singleton wiring for PublicConfigBuilder.
apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts Unit tests for builder caching/error behavior.
apps/ensindexer/src/lib/public-config-builder/index.ts Barrel export for builder.
apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts Worker now uses PublicConfigBuilder instead of EnsIndexerClient for config.
apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts Updates singleton wiring for new worker constructor signature.
apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts Updates worker tests for the new builder dependency.
apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts Adds mock helpers for the updated worker tests.
apps/ensindexer/src/config/public.ts Removes the old per-request public-config building helper.
apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts HTTP handlers now read public config and indexing status snapshot from ENSDb.
.changeset/violet-tires-sell.md Changeset for ensindexer release.
.changeset/flat-flowers-shave.md Changeset for @ensnode/ensnode-sdk release.
Comments suppressed due to low confidence (1)

apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts:160

  • pRetry callback is returning the getPublicConfig function instead of invoking it. This will cause pRetry to resolve to a function value, and inMemoryConfig won’t be an EnsIndexerPublicConfig (and will likely break when accessing versionInfo). Call getPublicConfig() inside the retry callback.
    const inMemoryConfigPromise = pRetry(() => this.publicConfigBuilder.getPublicConfig(), {
      retries: 3,
      onFailedAttempt: ({ error, attemptNumber, retriesLeft }) => {
        console.warn(
          `ENSIndexer Config fetch attempt ${attemptNumber} failed (${error.message}). ${retriesLeft} retries left.`,
        );
      },
    });

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

You can also share your feedback on Copilot code review. Take the survey.

// Invariant: the public config is guaranteed to be available in ENSDb after
// application startup.
if (typeof publicConfig === "undefined") {
throw new Error("Unreachable: ENSIndexer Public Config is not available in ENSDb");
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

The "Unreachable" branch is reachable: startEnsDbWriterWorker() is kicked off without awaiting .run(), so /config can be called before the worker has upserted the public config to ENSDb. Instead of throwing (which becomes a 500), return a controlled 503/500 response indicating config isn’t ready yet, or await worker initialization before serving routes.

Suggested change
throw new Error("Unreachable: ENSIndexer Public Config is not available in ENSDb");
// During startup there is a window where the public config may not yet
// be available in ENSDb. In that case, return a controlled error rather
// than throwing and causing an unstructured 500 response.
return c.json(
{
error: "ENSIndexer Public Config is not yet available in ENSDb",
},
503,
);

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In practice, it won't be rechable. ENSIndexer might not need any HTTP API whatsoever.

Copy link
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/violet-tires-sell.md:
- Line 5: The changeset description line "Improved HTTP handlers with ENSDb
Client being the only data provider that is needed." is a bit verbose—update the
sentence to a clearer, more concise wording such as "Refactored HTTP handlers to
rely solely on ENSDb Client for data" (edit the content of
.changeset/violet-tires-sell.md to replace the existing line), leaving the
frontmatter intact and ignoring MD041 as noted.

In `@apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts`:
- Around line 159-180: The test assigns storedConfig to the module namespace
ensDbClientMock instead of the actual stored config object, letting an invalid
shape slip through; change storedConfig to use the real public config (e.g.,
storedConfig = ensDbClientMock.publicConfig) and pass that into
createMockEnsDbClient's getEnsIndexerPublicConfig mock so the worker.run()
concurrency test uses a properly shaped stored config (references: storedConfig
variable, ensDbClientMock, createMockEnsDbClient, and
getEnsIndexerPublicConfig).

In `@apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts`:
- Around line 131-168: Add a new test that verifies concurrent-call caching by
starting two (or more) getPublicConfig() calls before the first one resolves and
asserting that only a single fetch/build occurs; to implement, mock
ensRainbowClientMock.config to return a controllable (deferred) Promise so you
can call builder.getPublicConfig() twice, then resolve the deferred promise and
await both results, and assert ensRainbowClientMock.config was called once,
getEnsIndexerVersion/getNodeJsVersion/getPackageVersion/validateEnsIndexerVersionInfo/validateEnsIndexerPublicConfig
were each called only once (or expected counts), and both returned values
strictly equal the same cached mockPublicConfig; use
PublicConfigBuilder.getPublicConfig and ensRainbowClientMock.config as the
unique symbols to locate where to modify/add the test.

In `@apps/ensindexer/src/lib/public-config-builder/public-config-builder.ts`:
- Around line 28-29: Concurrent callers of getPublicConfig() can race and build
multiple times; introduce an in-flight promise to dedupe builds by adding a
private field (e.g., buildingPromise: Promise<EnsIndexerPublicConfig> | null)
and update getPublicConfig() to: return immutablePublicConfig if set; otherwise
if buildingPromise exists await and return it; otherwise set buildingPromise to
the async build/validate flow that sets immutablePublicConfig on success and
clears buildingPromise in a finally block (propagate errors so callers see
failures and ensure buildingPromise is cleared on error).

In `@apps/ensindexer/src/lib/version-info.ts`:
- Around line 22-24: getNodeJsVersion() currently returns process.versions.node
(e.g., "20.0.0") but tests/mocks expect a "v" prefixed string; update
getNodeJsVersion() to return `v` + process.versions.node (ensuring no double
prefix if already present) and also update getENSIndexerVersionInfo() where it
directly uses process.versions.node to use getNodeJsVersion() (or prepend "v"
similarly) so the Node version in the version info object matches the "vX.Y.Z"
format used in tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cd5e06cc-e156-4d3b-a6f0-b048975a4cfe

📥 Commits

Reviewing files that changed from the base of the PR and between f786fc6 and e28d4d2.

📒 Files selected for processing (16)
  • .changeset/flat-flowers-shave.md
  • .changeset/violet-tires-sell.md
  • apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts
  • apps/ensindexer/src/config/public.ts
  • apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts
  • apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts
  • apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts
  • apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts
  • apps/ensindexer/src/lib/public-config-builder/index.ts
  • apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts
  • apps/ensindexer/src/lib/public-config-builder/public-config-builder.ts
  • apps/ensindexer/src/lib/public-config-builder/singleton.ts
  • apps/ensindexer/src/lib/version-info.ts
  • packages/ensnode-sdk/src/ensindexer/config/index.ts
  • packages/ensnode-sdk/src/ensindexer/config/validate/ensindexer-public-config.ts
  • packages/ensnode-sdk/src/ensindexer/config/validate/ensindexer-version-info.ts
💤 Files with no reviewable changes (1)
  • apps/ensindexer/src/config/public.ts

@tk-o
Copy link
Contributor Author

tk-o commented Mar 4, 2026

@greptile review

Copy link
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: 3

♻️ Duplicate comments (2)
apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts (1)

157-165: ⚠️ Potential issue | 🟡 Minor

Use the actual stored config object in this concurrency test.

storedConfig is currently assigned to the module namespace, not an EnsIndexerPublicConfig object, so the test can pass with an invalid shape.

✅ Suggested fix
-      const storedConfig = ensDbClientMock;
+      const storedConfig = ensDbClientMock.publicConfig;
       const inMemoryConfig = ensDbClientMock.publicConfig;
@@
       expect(ensDbClient.getEnsIndexerPublicConfig).toHaveBeenCalledTimes(1);
       expect(publicConfigBuilder.getPublicConfig).toHaveBeenCalledTimes(1);
+      expect(validateEnsIndexerPublicConfigCompatibility).toHaveBeenCalledWith(
+        storedConfig,
+        inMemoryConfig,
+      );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts`
around lines 157 - 165, The test assigns storedConfig to the module namespace
(ensDbClientMock) instead of a real EnsIndexerPublicConfig, allowing an invalid
shape to slip through; update storedConfig to use the actual public config
object (e.g., ensDbClientMock.publicConfig or an explicit EnsIndexerPublicConfig
fixture) and ensure the mocked getEnsIndexerPublicConfig returns that real
config; check references in the test to storedConfig, inMemoryConfig,
createMockEnsDbClient, getEnsIndexerPublicConfig, and
validateEnsIndexerPublicConfigCompatibility to confirm the shape matches the
expected EnsIndexerPublicConfig.
apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts (1)

141-172: 🧹 Nitpick | 🔵 Trivial

Add an overlapping-call cache test for getPublicConfig().

This currently validates sequential caching only; it doesn’t verify that concurrent callers are deduplicated into a single fetch/build path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts`
around lines 141 - 172, Add a new test that verifies getPublicConfig()
deduplicates concurrent calls: mock ensRainbowClientMock.config to return a
single deferred Promise (e.g., create a manual resolver or use a delay) so
multiple simultaneous calls can be issued before it resolves, instantiate
PublicConfigBuilder, call Promise.all on three concurrent
builder.getPublicConfig() invocations, then resolve the deferred response and
await the Promise.all result; assert ensRainbowClientMock.config was called
exactly once, validateEnsIndexerPublicConfig and other validators were called
once as in the existing test, and assert all three results are strictly the same
object (===) to confirm deduplication. Reference PublicConfigBuilder,
getPublicConfig, ensRainbowClientMock.config, and validateEnsIndexerPublicConfig
when implementing the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts`:
- Around line 21-25: The current guard in the /config handler (where
getEnsIndexerPublicConfig() result is stored in publicConfig) treats undefined
as unreachable and throws, causing startup race 500s; change this to handle the
legitimate undefined case by returning a transient response (e.g., 503 Service
Unavailable or an empty/fallback config) instead of throwing. Locate the check
around publicConfig in ensnode-api.ts (the code that calls
getEnsIndexerPublicConfig()), remove the throw, and implement a graceful early
return that sets appropriate HTTP status and message or a safe default payload
so callers don’t get a hard 500 during initial metadata writes.

In
`@packages/ensnode-sdk/src/ensindexer/config/validate/ensindexer-public-config.ts`:
- Around line 18-22: Replace the non-fail-fast safeParse usage with zod's parse
so validation throws immediately: change schema.safeParse(unvalidatedConfig) +
the result.success branch to simply schema.parse(unvalidatedConfig) and remove
the manual throw/pretty-error branch; apply the same change in the other
occurrence in ensindexer-version-info.ts (the schema.safeParse call at line 18)
so both validators use schema.parse() for immediate failure handling.

In
`@packages/ensnode-sdk/src/ensindexer/config/validate/ensindexer-version-info.ts`:
- Around line 18-22: Replace the safeParse-based validation with zod.parse() so
validation is fail-fast: call schema.parse(unvalidatedVersionInfo) inside a
try/catch, and in the catch detect ZodError (imported from zod) and rethrow a
formatted error like new Error(`Invalid EnsIndexerVersionInfo:
${prettifyError(e)}`); rethrow non-Zod errors unchanged; update the code around
schema.safeParse/unvalidatedVersionInfo to use schema.parse and reference
prettifyError and ZodError accordingly.

---

Duplicate comments:
In `@apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts`:
- Around line 157-165: The test assigns storedConfig to the module namespace
(ensDbClientMock) instead of a real EnsIndexerPublicConfig, allowing an invalid
shape to slip through; update storedConfig to use the actual public config
object (e.g., ensDbClientMock.publicConfig or an explicit EnsIndexerPublicConfig
fixture) and ensure the mocked getEnsIndexerPublicConfig returns that real
config; check references in the test to storedConfig, inMemoryConfig,
createMockEnsDbClient, getEnsIndexerPublicConfig, and
validateEnsIndexerPublicConfigCompatibility to confirm the shape matches the
expected EnsIndexerPublicConfig.

In `@apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts`:
- Around line 141-172: Add a new test that verifies getPublicConfig()
deduplicates concurrent calls: mock ensRainbowClientMock.config to return a
single deferred Promise (e.g., create a manual resolver or use a delay) so
multiple simultaneous calls can be issued before it resolves, instantiate
PublicConfigBuilder, call Promise.all on three concurrent
builder.getPublicConfig() invocations, then resolve the deferred response and
await the Promise.all result; assert ensRainbowClientMock.config was called
exactly once, validateEnsIndexerPublicConfig and other validators were called
once as in the existing test, and assert all three results are strictly the same
object (===) to confirm deduplication. Reference PublicConfigBuilder,
getPublicConfig, ensRainbowClientMock.config, and validateEnsIndexerPublicConfig
when implementing the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8ec57794-adac-4670-8b79-fee8903d2010

📥 Commits

Reviewing files that changed from the base of the PR and between e28d4d2 and 4dc3b7c.

📒 Files selected for processing (7)
  • .changeset/violet-tires-sell.md
  • apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts
  • apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts
  • apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts
  • apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts
  • packages/ensnode-sdk/src/ensindexer/config/validate/ensindexer-public-config.ts
  • packages/ensnode-sdk/src/ensindexer/config/validate/ensindexer-version-info.ts

Comment on lines +21 to +25
// Invariant: the public config is guaranteed to be available in ENSDb after
// application startup.
if (typeof publicConfig === "undefined") {
throw new Error("Unreachable: ENSIndexer Public Config is not available in ENSDb");
}
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 4, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

/config can hit a real startup race; this guard is reachable.

getEnsIndexerPublicConfig() legitimately returns undefined before metadata is written. Treating it as unreachable will produce transient 500s right after startup.

✅ Suggested handling
   if (typeof publicConfig === "undefined") {
-    throw new Error("Unreachable: ENSIndexer Public Config is not available in ENSDb");
+    return c.json(
+      { message: "ENSIndexer Public Config is not available yet." },
+      503,
+    );
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Invariant: the public config is guaranteed to be available in ENSDb after
// application startup.
if (typeof publicConfig === "undefined") {
throw new Error("Unreachable: ENSIndexer Public Config is not available in ENSDb");
}
// Invariant: the public config is guaranteed to be available in ENSDb after
// application startup.
if (typeof publicConfig === "undefined") {
return c.json(
{ message: "ENSIndexer Public Config is not available yet." },
503,
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts` around lines 21 - 25,
The current guard in the /config handler (where getEnsIndexerPublicConfig()
result is stored in publicConfig) treats undefined as unreachable and throws,
causing startup race 500s; change this to handle the legitimate undefined case
by returning a transient response (e.g., 503 Service Unavailable or an
empty/fallback config) instead of throwing. Locate the check around publicConfig
in ensnode-api.ts (the code that calls getEnsIndexerPublicConfig()), remove the
throw, and implement a graceful early return that sets appropriate HTTP status
and message or a safe default payload so callers don’t get a hard 500 during
initial metadata writes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In practice, it won't be rechable. ENSIndexer might not need any HTTP API whatsoever.

Copy link
Contributor

Choose a reason for hiding this comment

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

@tk-o, understood — the EnsDbWriterWorker populates the public config at startup (fail-fast), so by the time the HTTP layer accepts requests, the config is guaranteed to be present in ENSDb. My concern about the startup race was incorrect. The throw pattern is consistent with the existing design philosophy.

I'll record this for future reviews.


✏️ Learnings added
Learnt from: tk-o
Repo: namehash/ensnode PR: 1715
File: apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts:21-25
Timestamp: 2026-03-04T11:40:29.081Z
Learning: In `apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts`, the guard `if (typeof publicConfig === "undefined") { throw new Error("Unreachable: ...") }` in the `/config` handler is intentionally unreachable. The `EnsDbWriterWorker` populates the ENSIndexer public config into ENSDb at startup (fail-fast), so the HTTP layer is guaranteed to only serve requests after the config is available. The `throw` is a defensive invariant, not a real error path.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: namehash/ensnode PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T05:28:59.644Z
Learning: Applies to apps/ensindexer/**/*.ts : Use Ponder as the indexer framework for blockchain indexing

Learnt from: tk-o
Repo: namehash/ensnode PR: 1639
File: packages/ensnode-sdk/src/ensapi/api/indexing-status/zod-schemas.ts:21-76
Timestamp: 2026-02-16T17:53:46.139Z
Learning: In the ENSNode SDK (`packages/ensnode-sdk`), schema builder functions exported from `zod-schemas.ts` files (e.g., `makeEnsApiIndexingStatusResponseSchema`) are considered internal API, not public API. These can have breaking changes without requiring deprecated aliases, even when exported via the `internal` entry point.

Learnt from: tk-o
Repo: namehash/ensnode PR: 1705
File: apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts:25-36
Timestamp: 2026-03-02T20:10:05.060Z
Learning: Ensure the ENS label set validation (validateSupportedLabelSetAndVersion) is performed at startup by the ENSDb Writer Worker during application startup for ENSIndexer. If validation fails, the worker should crash the process (fail-fast), so that runtime /config endpoints do not need to raise or return error responses. This enforces configuration correctness at deploy/startup time rather than at runtime for the file apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts.

Learnt from: tk-o
Repo: namehash/ensnode PR: 1705
File: apps/ensapi/src/config/validations.ts:37-43
Timestamp: 2026-03-02T19:03:50.730Z
Learning: In `apps/ensapi/src/config/validations.ts`, the ENSRainbow version equality invariant (ensIndexerPublicConfig.versionInfo.ensRainbowPublicConfig.version === packageJson.version) is intentionally kept. Even though ensRainbowPublicConfig represents a connected ENSRainbow service, strict version parity with ENSApi is enforced as a deployment requirement.

Learnt from: notrab
Repo: namehash/ensnode PR: 1631
File: apps/ensapi/src/handlers/ensnode-api.ts:23-27
Timestamp: 2026-02-18T16:11:09.421Z
Learning: In the ensapi application, dynamic `import("@/config")` inside request handlers is an acceptable pattern because Node.js caches modules after the first import, making subsequent calls resolve from cache with negligible overhead (just promise resolution).

Learnt from: tk-o
Repo: namehash/ensnode PR: 1614
File: apps/ensindexer/src/lib/ponder-api-client.ts:7-7
Timestamp: 2026-02-18T15:26:09.067Z
Learning: In apps/ensindexer/src/lib/ponder-api-client.ts, the localPonderClientPromise is intentionally left as a permanently rejected promise when LocalPonderClient.init fails after retries. This is expected behavior because process.exitCode = 1 signals the process should terminate, and if it continues running, all subsequent calls should fail immediately with the cached rejection rather than retrying initialization.

Learnt from: tk-o
Repo: namehash/ensnode PR: 1615
File: packages/ensnode-sdk/src/api/indexing-status/deserialize.ts:38-40
Timestamp: 2026-02-07T12:22:32.900Z
Learning: In `packages/ensnode-sdk/src/api/indexing-status/deserialize.ts`, the pattern of using `z.preprocess()` with `buildUnvalidatedIndexingStatusResponse` (which returns `unknown`) is intentional. This enforces a "parse serialized → preprocess to unvalidated → validate final schema" flow, where `z.preprocess` is semantically correct because it runs before final validation. Using `.transform()` would be incorrect here as it runs after parsing and receives a typed input.

Learnt from: CR
Repo: namehash/ensnode PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T05:28:59.644Z
Learning: Applies to apps/ensapi/src/**/*.ts : Use the shared `errorResponse` helper from `apps/ensapi/src/lib/handlers/error-response.ts` for all error responses in ENSApi

Learnt from: tk-o
Repo: namehash/ensnode PR: 1615
File: packages/ensnode-sdk/src/ensindexer/indexing-status/chain-indexing-status-snapshot.ts:314-322
Timestamp: 2026-02-07T11:54:52.607Z
Learning: In the ENSNode SDK, `ChainIndexingStatusSnapshot[]` parameters in functions like `getTimestampForLowestOmnichainStartBlock` are guaranteed not to be empty arrays by design.

Learnt from: tk-o
Repo: namehash/ensnode PR: 1617
File: packages/ensnode-sdk/src/ensindexer/indexing-status/validate/chain-indexing-status-snapshot.ts:9-12
Timestamp: 2026-02-09T10:19:29.575Z
Learning: In ensnode-sdk validation functions (e.g., `validateChainIndexingStatusSnapshot` in `packages/ensnode-sdk/src/ensindexer/indexing-status/validate/chain-indexing-status-snapshot.ts`), the pattern of using `ChainIndexingStatusSnapshot | unknown` (even though it collapses to `unknown` in TypeScript) is intentionally kept for semantic clarity and documentation purposes.

Learnt from: Goader
Repo: namehash/ensnode PR: 1663
File: packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts:74-96
Timestamp: 2026-02-24T15:53:06.633Z
Learning: In TypeScript code reviews, prefer placing invariants on type aliases only when the invariant is context-independent or reused across multiple fields. If an invariant depends on surrounding rules or object semantics (e.g., field-specific metrics), keep the invariant as a field JSDoc instead. This guideline applies to TS files broadly (e.g., the repo's v1/award-models and similar modules).

Copilot AI review requested due to automatic review settings March 4, 2026 11:39
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io March 4, 2026 11:39 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io March 4, 2026 11:39 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io March 4, 2026 11:39 Inactive
@tk-o
Copy link
Contributor Author

tk-o commented Mar 4, 2026

@greptile review

@tk-o tk-o changed the title Use ENSDb Client to power ENSIndexer HTTP handlers Make ENSDb Writer Worker to use builders for ENSIndexer Public Config and Indexing Status objects Mar 4, 2026
Copy link
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 16 out of 16 changed files in this pull request and generated 4 comments.


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

You can also share your feedback on Copilot code review. Take the survey.

@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io March 4, 2026 12:15 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io March 4, 2026 12:15 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io March 4, 2026 12:15 Inactive
Copy link
Contributor

@vercel vercel bot left a comment

Choose a reason for hiding this comment

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

Additional Suggestion:

Unused imports left in version-info.ts after getENSIndexerVersionInfo() function was removed

Fix on Vercel

Copy link
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

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

@tk-o Looks good! 😄

@lightwalker-eth lightwalker-eth merged commit 1f8a05b into main Mar 4, 2026
20 of 21 checks passed
@lightwalker-eth lightwalker-eth deleted the feat/ensdb-to-power-ensindexer-http-handlers branch March 4, 2026 12:44
@github-actions github-actions bot mentioned this pull request Mar 4, 2026
@coderabbitai coderabbitai bot mentioned this pull request Mar 4, 2026
2 tasks
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