-
Notifications
You must be signed in to change notification settings - Fork 17
Make ENSDb Writer Worker to use builders for ENSIndexer Public Config and Indexing Status objects #1715
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make ENSDb Writer Worker to use builders for ENSIndexer Public Config and Indexing Status objects #1715
Changes from 9 commits
2a44669
9a62c0e
64bbc0b
f95146b
7a73da2
ab000eb
a655c23
d775259
e28d4d2
66f17c5
4dc3b7c
8e53487
5fa7ba0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@ensnode/ensnode-sdk": minor | ||
| --- | ||
|
|
||
| Added `validateEnsIndexerPublicConfig` and `validateEnsIndexerVersionInfo` functions. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "ensindexer": minor | ||
| --- | ||
|
|
||
| Improved HTTP handlers with ENSDb Client being the only data provider that is needed. | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,10 +1,7 @@ | ||||||||||||||||||||||||||||
| import config from "@/config"; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import { getUnixTime } from "date-fns"; | ||||||||||||||||||||||||||||
| import { Hono } from "hono"; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||
| buildCrossChainIndexingStatusSnapshotOmnichain, | ||||||||||||||||||||||||||||
| createRealtimeIndexingStatusProjection, | ||||||||||||||||||||||||||||
| IndexingStatusResponseCodes, | ||||||||||||||||||||||||||||
| type IndexingStatusResponseError, | ||||||||||||||||||||||||||||
|
|
@@ -13,31 +10,38 @@ import { | |||||||||||||||||||||||||||
| serializeIndexingStatusResponse, | ||||||||||||||||||||||||||||
| } from "@ensnode/ensnode-sdk"; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import { buildENSIndexerPublicConfig } from "@/config/public"; | ||||||||||||||||||||||||||||
| import { indexingStatusBuilder } from "@/lib/indexing-status-builder/singleton"; | ||||||||||||||||||||||||||||
| import { ensDbClient } from "@/lib/ensdb-client/singleton"; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const app = new Hono(); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // include ENSIndexer Public Config endpoint | ||||||||||||||||||||||||||||
| app.get("/config", async (c) => { | ||||||||||||||||||||||||||||
| // prepare the public config object, including dependency info | ||||||||||||||||||||||||||||
| const publicConfig = await buildENSIndexerPublicConfig(config); | ||||||||||||||||||||||||||||
| const publicConfig = await ensDbClient.getEnsIndexerPublicConfig(); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // 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"); | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
| 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, | |
| ); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/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.
| // 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { vi } from "vitest"; | ||
|
|
||
| import { | ||
| type CrossChainIndexingStatusSnapshot, | ||
| CrossChainIndexingStrategyIds, | ||
| type EnsIndexerPublicConfig, | ||
| OmnichainIndexingStatusIds, | ||
| type OmnichainIndexingStatusSnapshot, | ||
| } from "@ensnode/ensnode-sdk"; | ||
|
|
||
| import type { EnsDbClient } from "@/lib/ensdb-client/ensdb-client"; | ||
| import * as ensDbClientMock from "@/lib/ensdb-client/ensdb-client.mock"; | ||
| import type { IndexingStatusBuilder } from "@/lib/indexing-status-builder"; | ||
| import type { PublicConfigBuilder } from "@/lib/public-config-builder"; | ||
|
|
||
| // Helper to create mock objects with consistent typing | ||
| export function createMockEnsDbClient( | ||
| overrides: Partial<ReturnType<typeof baseEnsDbClient>> = {}, | ||
| ): EnsDbClient { | ||
| return { | ||
| ...baseEnsDbClient(), | ||
| ...overrides, | ||
| } as unknown as EnsDbClient; | ||
| } | ||
|
|
||
| export function baseEnsDbClient() { | ||
| return { | ||
| getEnsIndexerPublicConfig: vi.fn().mockResolvedValue(undefined), | ||
| upsertEnsDbVersion: vi.fn().mockResolvedValue(undefined), | ||
| upsertEnsIndexerPublicConfig: vi.fn().mockResolvedValue(undefined), | ||
| upsertIndexingStatusSnapshot: vi.fn().mockResolvedValue(undefined), | ||
| }; | ||
| } | ||
|
|
||
| export function createMockPublicConfigBuilder( | ||
| resolvedConfig: EnsIndexerPublicConfig = ensDbClientMock.publicConfig, | ||
| ): PublicConfigBuilder { | ||
| return { | ||
| getPublicConfig: vi.fn().mockResolvedValue(resolvedConfig), | ||
| } as unknown as PublicConfigBuilder; | ||
| } | ||
|
|
||
| export function createMockIndexingStatusBuilder( | ||
| resolvedSnapshot: OmnichainIndexingStatusSnapshot = createMockOmnichainSnapshot(), | ||
| ): IndexingStatusBuilder { | ||
| return { | ||
| getOmnichainIndexingStatusSnapshot: vi.fn().mockResolvedValue(resolvedSnapshot), | ||
| } as unknown as IndexingStatusBuilder; | ||
| } | ||
|
|
||
| export function createMockOmnichainSnapshot( | ||
| overrides: Partial<OmnichainIndexingStatusSnapshot> = {}, | ||
| ): OmnichainIndexingStatusSnapshot { | ||
| return { | ||
| omnichainStatus: OmnichainIndexingStatusIds.Following, | ||
| omnichainIndexingCursor: 100, | ||
| chains: new Map(), | ||
| ...overrides, | ||
| }; | ||
| } | ||
|
|
||
| export function createMockCrossChainSnapshot( | ||
| overrides: Partial<CrossChainIndexingStatusSnapshot> = {}, | ||
| ): CrossChainIndexingStatusSnapshot { | ||
| return { | ||
| strategy: CrossChainIndexingStrategyIds.Omnichain, | ||
| slowestChainIndexingCursor: 100, | ||
| snapshotTime: 200, | ||
| omnichainSnapshot: createMockOmnichainSnapshot(), | ||
| ...overrides, | ||
| }; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.