From c7b6db4c0a8dcc13113ac5e5629689c77efcc372 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Feb 2026 12:00:01 +0100 Subject: [PATCH 01/14] Extract business-layer for `ChainIndexingMetadataImmutable` Move logic from `LocalPonderClient` into a separate file. --- .../lib/chains-indexing-metadata-immutable.ts | 114 ++++++++++++++++++ .../ponder/src/api/lib/local-ponder-client.ts | 98 ++------------- 2 files changed, 123 insertions(+), 89 deletions(-) create mode 100644 apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-immutable.ts diff --git a/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-immutable.ts b/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-immutable.ts new file mode 100644 index 000000000..6a0dd54b5 --- /dev/null +++ b/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-immutable.ts @@ -0,0 +1,114 @@ +import type { PublicClient } from "viem"; + +import { createIndexingConfig } from "@ensnode/ensnode-sdk"; +import { + type BlockRef, + type BlockrangeWithStartBlock, + type ChainId, + ChainIndexingStates, + type PonderIndexingMetrics, +} from "@ensnode/ponder-sdk"; + +import type { ChainIndexingMetadataImmutable } from "@/lib/indexing-status-builder/chain-indexing-metadata"; + +import { fetchBlockRef } from "./fetch-block-ref"; + +/** + * Build an immutable indexing metadata for a chain. + * + * Some of the metadata fields are based on RPC calls to fetch block references. + * + * @param startBlock Chain's start block. + * @param endBlock Chain's end block (optional). + * @param backfillEndBlock Chain's backfill end block. + * + * @returns The immutable indexing metadata for the chain. + */ +function buildChainIndexingMetadataImmutable( + startBlock: BlockRef, + endBlock: BlockRef | null, + backfillEndBlock: BlockRef, +): ChainIndexingMetadataImmutable { + const chainIndexingConfig = createIndexingConfig(startBlock, endBlock); + + return { + backfillScope: { + startBlock, + endBlock: backfillEndBlock, + }, + indexingConfig: chainIndexingConfig, + }; +} + +/** + * Map of chain ID to its immutable indexing metadata. + */ +export type ChainsIndexingMetadataImmutable = Map; + +/** + * Build a map of chain ID to its immutable indexing metadata. + * + * @param indexedChainIds Set of indexed chain IDs. + * @param chainsConfigBlockrange Map of chain ID to its configured blockrange. + * @param publicClients Map of chain ID to its cached public client. + * @param ponderIndexingMetrics Ponder indexing metrics for each chain. + * @returns A map of chain ID to its immutable indexing metadata. + * @throws Error if any of the required data cannot be fetched or is invalid, + * or if invariants are violated. + */ +export async function buildChainsIndexingMetadataImmutable( + indexedChainIds: Set, + chainsConfigBlockrange: Map, + publicClients: Map, + ponderIndexingMetrics: PonderIndexingMetrics, +): Promise> { + const chainsIndexingMetadataImmutable = new Map(); + + for (const chainId of indexedChainIds.values()) { + const chainConfigBlockrange = chainsConfigBlockrange.get(chainId); + const chainIndexingMetrics = ponderIndexingMetrics.chains.get(chainId); + const publicClient = publicClients.get(chainId); + + // Invariants: chain config blockrange, indexing metrics, and public client + // must exist in proper state for the indexed chain. + if (!chainConfigBlockrange) { + throw new Error(`Chain config blockrange must be available for indexed chain ID ${chainId}`); + } + + if (!chainIndexingMetrics) { + throw new Error(`Indexing metrics must be available for indexed chain ID ${chainId}`); + } + + if (chainIndexingMetrics.state !== ChainIndexingStates.Historical) { + throw new Error( + `Chain indexing state must be "historical" for indexed chain ID ${chainId}, but got "${chainIndexingMetrics.state}"`, + ); + } + + if (!publicClient) { + throw new Error(`Public client must be available for indexed chain ID ${chainId}`); + } + + const backfillEndBlockNumber = + chainConfigBlockrange.startBlock + chainIndexingMetrics.historicalTotalBlocks - 1; + + // Fetch required block references in parallel. + const [startBlock, endBlock, backfillEndBlock] = await Promise.all([ + fetchBlockRef(publicClient, chainConfigBlockrange.startBlock), + chainConfigBlockrange.endBlock + ? fetchBlockRef(publicClient, chainConfigBlockrange.endBlock) + : null, + fetchBlockRef(publicClient, backfillEndBlockNumber), + ]); + + const metadataImmutable = buildChainIndexingMetadataImmutable( + startBlock, + endBlock, + backfillEndBlock, + ); + + chainsIndexingMetadataImmutable.set(chainId, metadataImmutable); + } + + return chainsIndexingMetadataImmutable; +} diff --git a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts index 804c7f9bd..9e45f3389 100644 --- a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts +++ b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts @@ -1,18 +1,18 @@ import { publicClients as ponderPublicClients } from "ponder:api"; import type { PublicClient } from "viem"; -import { type Blockrange, createIndexingConfig, deserializeChainId } from "@ensnode/ensnode-sdk"; -import { type ChainId, ChainIndexingStates, PonderClient } from "@ensnode/ponder-sdk"; +import { type Blockrange, deserializeChainId } from "@ensnode/ensnode-sdk"; +import { type BlockrangeWithStartBlock, type ChainId, PonderClient } from "@ensnode/ponder-sdk"; import type { ChainIndexingMetadata, ChainIndexingMetadataDynamic, ChainIndexingMetadataImmutable, } from "@/lib/indexing-status-builder/chain-indexing-metadata"; +import { buildChainsIndexingMetadataImmutable } from "@/ponder/api/lib/chains-indexing-metadata-immutable"; import ponderConfig from "@/ponder/config"; import { buildChainsBlockrange } from "./chains-config-blockrange"; -import { fetchBlockRef } from "./fetch-block-ref"; /** * Build a map of chain ID to its configured blockrange (start and end blocks) @@ -20,7 +20,7 @@ import { fetchBlockRef } from "./fetch-block-ref"; * * @throws Error if invariants are violated. */ -function buildChainsConfigBlockrange(): Map { +function buildChainsConfigBlockrange(): Map { return buildChainsBlockrange(ponderConfig); } @@ -39,90 +39,6 @@ function buildPublicClientsMap(): Map { ); } -/** - * Build a map of chain ID to its fixed indexing metadata. - * - * @param publicClients A map of chain ID to its corresponding public client, - * used to fetch block references for chain's blockrange. - * @param ponderClient The Ponder client used to fetch indexing metrics and status. - * @returns A map of chain ID to its fixed indexing metadata. - * @throws Error if any of the required data cannot be fetched or is invalid, - * or if invariants are violated. - */ -async function buildChainsIndexingMetadataImmutable( - publicClients: Map, - ponderClient: PonderClient, -): Promise> { - console.log("Building ChainIndexingMetadataImmutable..."); - const chainsIndexingMetadataImmutable = new Map(); - - const chainsConfigBlockrange = buildChainsConfigBlockrange(); - const [ponderIndexingMetrics, ponderIndexingStatus] = await Promise.all([ - ponderClient.metrics(), - ponderClient.status(), - ]); - - for (const [chainId, publicClient] of publicClients.entries()) { - const chainConfigBlockrange = chainsConfigBlockrange.get(chainId); - const chainIndexingMetrics = ponderIndexingMetrics.chains.get(chainId); - const chainIndexingStatus = ponderIndexingStatus.chains.get(chainId); - - // Invariants: chain config blockrange, indexing metrics, and indexing status - // must exist in proper state for the indexed chain. - if (!chainConfigBlockrange) { - throw new Error(`No chain config blockrange found for indexed chain ID ${chainId}`); - } - - if (!chainConfigBlockrange.startBlock) { - throw new Error( - `No start block found in chain config blockrange for indexed chain ID ${chainId}`, - ); - } - - if (!chainIndexingMetrics) { - throw new Error(`No indexing metrics found for indexed chain ID ${chainId}`); - } - - if (chainIndexingMetrics.state !== ChainIndexingStates.Historical) { - throw new Error( - `In order to build 'ChainsIndexingMetadataFixed', chain indexing state must be "historical" for indexed chain ID ${chainId}, but got "${chainIndexingMetrics.state}"`, - ); - } - - if (!chainIndexingStatus) { - throw new Error(`No indexing status found for indexed chain ID ${chainId}`); - } - - const backfillEndBlockNumber = - chainConfigBlockrange.startBlock + chainIndexingMetrics.historicalTotalBlocks - 1; - - const [startBlock, endBlock, backfillEndBlock] = await Promise.all([ - fetchBlockRef(publicClient, chainConfigBlockrange.startBlock), - chainConfigBlockrange.endBlock - ? fetchBlockRef(publicClient, chainConfigBlockrange.endBlock) - : null, - fetchBlockRef(publicClient, backfillEndBlockNumber), - ]); - - const chainIndexingConfig = createIndexingConfig(startBlock, endBlock); - - const metadataImmutable = { - backfillScope: { - startBlock, - endBlock: backfillEndBlock, - }, - indexingConfig: chainIndexingConfig, - } satisfies ChainIndexingMetadataImmutable; - - // Cache the immutable metadata for this chain ID - chainsIndexingMetadataImmutable.set(chainId, metadataImmutable); - } - - console.log("ChainIndexingMetadataImmutable built successfully"); - - return chainsIndexingMetadataImmutable; -} - async function buildChainsIndexingMetadataDynamic( indexedChainIds: Set, ponderClient: PonderClient, @@ -195,9 +111,13 @@ export class LocalPonderClient { static async init(ponderAppUrl: URL, indexedChainIds: Set): Promise { const ponderClient = new PonderClient(ponderAppUrl); const publicClients = buildPublicClientsMap(); + const chainsConfigBlockrange = buildChainsConfigBlockrange(); + const ponderIndexingMetrics = await ponderClient.metrics(); const chainIndexingMetadataImmutable = await buildChainsIndexingMetadataImmutable( + indexedChainIds, + chainsConfigBlockrange, publicClients, - ponderClient, + ponderIndexingMetrics, ); const client = new LocalPonderClient( From 4cf211ab8264ffd0e36c639b8782b23d4bf15994 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Feb 2026 12:03:30 +0100 Subject: [PATCH 02/14] Extract business-layer for `ChainIndexingMetadataDynamic` Move logic from `LocalPonderClient` into a separate file. --- .../lib/chains-indexing-metadata-dynamic.ts | 48 +++++++++++++++++ .../ponder/src/api/lib/local-ponder-client.ts | 51 ++++--------------- 2 files changed, 59 insertions(+), 40 deletions(-) create mode 100644 apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-dynamic.ts diff --git a/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-dynamic.ts b/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-dynamic.ts new file mode 100644 index 000000000..efb772f89 --- /dev/null +++ b/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-dynamic.ts @@ -0,0 +1,48 @@ +import type { ChainId, PonderIndexingMetrics, PonderIndexingStatus } from "@ensnode/ponder-sdk"; + +import type { ChainIndexingMetadataDynamic } from "@/lib/indexing-status-builder/chain-indexing-metadata"; + +/** + * Build a map of chain ID to its dynamic indexing metadata. + * + * The dynamic metadata is based on the current indexing metrics and status + * of the chain, which can change over time as the chain is being indexed. + * + * @param indexedChainIds A set of chain IDs that are being indexed. + * @param ponderIndexingMetrics The current indexing metrics for all chains. + * @param ponderIndexingStatus The current indexing status for all chains. + * + * @returns A map of chain ID to its dynamic indexing metadata. + * + * @throws Error if any invariants are violated. + */ +export function buildChainsIndexingMetadataDynamic( + indexedChainIds: Set, + ponderIndexingMetrics: PonderIndexingMetrics, + ponderIndexingStatus: PonderIndexingStatus, +): Map { + const chainsIndexingMetadataDynamic = new Map(); + + for (const chainId of indexedChainIds.values()) { + const chainIndexingMetrics = ponderIndexingMetrics.chains.get(chainId); + const chainIndexingStatus = ponderIndexingStatus.chains.get(chainId); + + // Invariants: indexing metrics and indexing status must exist in proper state for the indexed chain. + if (!chainIndexingMetrics) { + throw new Error(`Indexing metrics must be available for indexed chain ID ${chainId}`); + } + + if (!chainIndexingStatus) { + throw new Error(`Indexing status must be available for indexed chain ID ${chainId}`); + } + + const metadataDynamic = { + indexingMetrics: chainIndexingMetrics, + indexingStatus: chainIndexingStatus, + } satisfies ChainIndexingMetadataDynamic; + + chainsIndexingMetadataDynamic.set(chainId, metadataDynamic); + } + + return chainsIndexingMetadataDynamic; +} diff --git a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts index 9e45f3389..daeacf1b6 100644 --- a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts +++ b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts @@ -1,18 +1,18 @@ import { publicClients as ponderPublicClients } from "ponder:api"; import type { PublicClient } from "viem"; -import { type Blockrange, deserializeChainId } from "@ensnode/ensnode-sdk"; +import { deserializeChainId } from "@ensnode/ensnode-sdk"; import { type BlockrangeWithStartBlock, type ChainId, PonderClient } from "@ensnode/ponder-sdk"; import type { ChainIndexingMetadata, - ChainIndexingMetadataDynamic, ChainIndexingMetadataImmutable, } from "@/lib/indexing-status-builder/chain-indexing-metadata"; import { buildChainsIndexingMetadataImmutable } from "@/ponder/api/lib/chains-indexing-metadata-immutable"; import ponderConfig from "@/ponder/config"; import { buildChainsBlockrange } from "./chains-config-blockrange"; +import { buildChainsIndexingMetadataDynamic } from "./chains-indexing-metadata-dynamic"; /** * Build a map of chain ID to its configured blockrange (start and end blocks) @@ -39,42 +39,6 @@ function buildPublicClientsMap(): Map { ); } -async function buildChainsIndexingMetadataDynamic( - indexedChainIds: Set, - ponderClient: PonderClient, -): Promise> { - const chainsIndexingMetadataDynamic = new Map(); - - const [ponderIndexingMetrics, ponderIndexingStatus] = await Promise.all([ - ponderClient.metrics(), - ponderClient.status(), - ]); - - for (const chainId of indexedChainIds) { - const chainIndexingMetrics = ponderIndexingMetrics.chains.get(chainId); - const chainIndexingStatus = ponderIndexingStatus.chains.get(chainId); - - // Invariants: indexing metrics and indexing status must exist in proper state for the indexed chain. - if (!chainIndexingMetrics) { - throw new Error(`No indexing metrics found for indexed chain ID ${chainId}`); - } - - if (!chainIndexingStatus) { - throw new Error(`No indexing status found for indexed chain ID ${chainId}`); - } - - const metadataDynamic = { - indexingMetrics: chainIndexingMetrics, - indexingStatus: chainIndexingStatus, - } satisfies ChainIndexingMetadataDynamic; - - // Cache the dynamic metadata for this chain ID - chainsIndexingMetadataDynamic.set(chainId, metadataDynamic); - } - - return chainsIndexingMetadataDynamic; -} - /** * LocalPonderClient for interacting with the local Ponder app and its data. */ @@ -179,9 +143,16 @@ export class LocalPonderClient { */ async chainsIndexingMetadata(): Promise> { const chainsIndexingMetadata = new Map(); - const chainsIndexingMetadataDynamic = await buildChainsIndexingMetadataDynamic( + + const [ponderIndexingMetrics, ponderIndexingStatus] = await Promise.all([ + this.#ponderClient.metrics(), + this.#ponderClient.status(), + ]); + + const chainsIndexingMetadataDynamic = buildChainsIndexingMetadataDynamic( this.indexedChainIds, - this.#ponderClient, + ponderIndexingMetrics, + ponderIndexingStatus, ); for (const chainId of this.indexedChainIds) { From 748ce5de4f992793c5a82ad06cd86e64daebeb99 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Feb 2026 12:21:36 +0100 Subject: [PATCH 03/14] Create `PonderClientCache` This SWR cache is desinged to load data for `PonderIndexingMetrics` and `PonderIndexingStatus` with `PonderClient` and have data cached indefinitely, but also proactively refreshed on recurring basisc. --- .../src/api/lib/cache/ponder-client.cache.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 apps/ensindexer/ponder/src/api/lib/cache/ponder-client.cache.ts diff --git a/apps/ensindexer/ponder/src/api/lib/cache/ponder-client.cache.ts b/apps/ensindexer/ponder/src/api/lib/cache/ponder-client.cache.ts new file mode 100644 index 000000000..228e1b6ae --- /dev/null +++ b/apps/ensindexer/ponder/src/api/lib/cache/ponder-client.cache.ts @@ -0,0 +1,68 @@ +import config from "@/config"; +import { type Duration, SWRCache } from "@ensnode/ensnode-sdk"; +import { + PonderClient, + PonderIndexingMetrics, + PonderIndexingStatus, +} from "@ensnode/ponder-sdk"; + +/** + * Result of the Ponder Client cache. + */ +export interface PonderClientCacheResult { + ponderIndexingMetrics: PonderIndexingMetrics; + ponderIndexingStatus: PonderIndexingStatus; +} + +/** + * SWR Cache for Ponder Client data + */ +export type PonderClientCache = SWRCache; + +const ponderClient = new PonderClient(config.ensIndexerUrl); + +/** + * Cache for Ponder Client data + * + * In case of using multiple Ponder API endpoints, it is optimal for data + * consistency to call all endpoints at once. This cache loads both + * Ponder Indexing Metrics and Ponder Indexing Status together, and provides + * them as a single cached result. This way, we ensure that the metrics and + * status data are always from the same point in time, and avoid potential + * inconsistencies that could arise if they were loaded separately. + * + * Ponder Client may sometimes fail to load data, i.e. due to network issues. + * The cache is designed to be resilient to loading failures, and will keep data + * in the cache indefinitely until it can be successfully reloaded. + * See `ttl` option below. + * + * Ponder Indexing Metrics and Ponder Indexing Status can both change frequently, + * so the cache is designed to proactively revalidate data to ensure freshness. + * See `proactiveRevalidationInterval` option below. + * + * Note, that Ponder app needs a while at startup to populate indexing metrics, + * and indexing status, so a few of the initial attempts to load this cache may + * fail until required data is made available by the Ponder app. + */ +export const ponderClientCache = new SWRCache({ + fn: async function loadPonderClientCache() { + try { + console.info(`[PonderClientCache]: loading data...`); + const [ponderIndexingMetrics, ponderIndexingStatus] = await Promise.all([ + ponderClient.metrics(), + ponderClient.status(), + ]); + console.info(`[PonderClientCache]: Successfully loaded data`); + + return { ponderIndexingMetrics, ponderIndexingStatus }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[PonderClientCache]: an error occurred while loading data: ${errorMessage}`); + + throw new Error(`Failed to load Ponder Client cache: ${errorMessage}`); + } + }, + ttl: Number.POSITIVE_INFINITY, + proactiveRevalidationInterval: 10 satisfies Duration, + proactivelyInitialize: true, +}) satisfies PonderClientCache; From 05f512c3d99db54acb1fe7e5dff6a651cb70af6a Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Feb 2026 12:30:36 +0100 Subject: [PATCH 04/14] Update `LocalPonderClient` data fetching approach Replace `PonderClient` calls with `PonderClientCache` calls while fetching data. --- .../ponder/src/api/lib/local-ponder-client.ts | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts index daeacf1b6..a2848c0ae 100644 --- a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts +++ b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts @@ -8,11 +8,12 @@ import type { ChainIndexingMetadata, ChainIndexingMetadataImmutable, } from "@/lib/indexing-status-builder/chain-indexing-metadata"; -import { buildChainsIndexingMetadataImmutable } from "@/ponder/api/lib/chains-indexing-metadata-immutable"; import ponderConfig from "@/ponder/config"; +import { ponderClientCache, type PonderClientCache } from "./cache/ponder-client.cache"; import { buildChainsBlockrange } from "./chains-config-blockrange"; import { buildChainsIndexingMetadataDynamic } from "./chains-indexing-metadata-dynamic"; +import { buildChainsIndexingMetadataImmutable } from "./chains-indexing-metadata-immutable"; /** * Build a map of chain ID to its configured blockrange (start and end blocks) @@ -43,21 +44,21 @@ function buildPublicClientsMap(): Map { * LocalPonderClient for interacting with the local Ponder app and its data. */ export class LocalPonderClient { - #ponderClient: PonderClient; - #publicClients: Map; #indexedChainIds: Set; #chainIndexingMetadataImmutable: Map; + #ponderClientCache: PonderClientCache; + #publicClients: Map; private constructor( - ponderClient: PonderClient, - publicClients: Map, indexedChainIds: Set, chainIndexingMetadataImmutable: Map, + ponderClientCache: PonderClientCache, + publicClients: Map, ) { - this.#ponderClient = ponderClient; - this.#publicClients = publicClients; this.#indexedChainIds = indexedChainIds; this.#chainIndexingMetadataImmutable = chainIndexingMetadataImmutable; + this.#ponderClientCache = ponderClientCache; + this.#publicClients = publicClients; } /** @@ -74,9 +75,11 @@ export class LocalPonderClient { */ static async init(ponderAppUrl: URL, indexedChainIds: Set): Promise { const ponderClient = new PonderClient(ponderAppUrl); + ponderClientCache.setContext({ ponderClient }); + + const ponderIndexingMetrics = await ponderClient.metrics(); const publicClients = buildPublicClientsMap(); const chainsConfigBlockrange = buildChainsConfigBlockrange(); - const ponderIndexingMetrics = await ponderClient.metrics(); const chainIndexingMetadataImmutable = await buildChainsIndexingMetadataImmutable( indexedChainIds, chainsConfigBlockrange, @@ -85,10 +88,10 @@ export class LocalPonderClient { ); const client = new LocalPonderClient( - ponderClient, - publicClients, indexedChainIds, chainIndexingMetadataImmutable, + ponderClientCache, + publicClients, ); return client; @@ -108,13 +111,6 @@ export class LocalPonderClient { return this.#indexedChainIds; } - /** - * Ponder client instance connected to the local Ponder app. - */ - get ponderClient(): PonderClient { - return this.#ponderClient; - } - /** * Public client for a given chain ID. * @@ -144,10 +140,17 @@ export class LocalPonderClient { async chainsIndexingMetadata(): Promise> { const chainsIndexingMetadata = new Map(); - const [ponderIndexingMetrics, ponderIndexingStatus] = await Promise.all([ - this.#ponderClient.metrics(), - this.#ponderClient.status(), - ]); + const ponderClientCacheResult = await this.#ponderClientCache.read(); + + // Invariants: both indexing metrics and indexing status must be available + // in cache + if (ponderClientCacheResult instanceof Error) { + throw new Error( + `Ponder Client cache must be available to build chains indexing metadata dynamic: ${ponderClientCacheResult.message}`, + ); + } + + const { ponderIndexingMetrics, ponderIndexingStatus } = ponderClientCacheResult; const chainsIndexingMetadataDynamic = buildChainsIndexingMetadataDynamic( this.indexedChainIds, From 9b274d7e9adfad42b6ee8970af35f3e5e5c889c9 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Feb 2026 12:34:08 +0100 Subject: [PATCH 05/14] Create `ChainsIndexingMetadataImmutableCache` This SWR cache is desinged to load data for `ChainsIndexingMetadataImmutable` with `PonderClientCache` and RPC calls and have data cached indefinitely, with proactive revalidation on until data is loaded into cache successfully (then, proactive revalidation is stopped for the cache). --- ...hains-indexing-metadata-immutable.cache.ts | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 apps/ensindexer/ponder/src/api/lib/cache/chains-indexing-metadata-immutable.cache.ts diff --git a/apps/ensindexer/ponder/src/api/lib/cache/chains-indexing-metadata-immutable.cache.ts b/apps/ensindexer/ponder/src/api/lib/cache/chains-indexing-metadata-immutable.cache.ts new file mode 100644 index 000000000..ce02a1fcf --- /dev/null +++ b/apps/ensindexer/ponder/src/api/lib/cache/chains-indexing-metadata-immutable.cache.ts @@ -0,0 +1,89 @@ +import type { PublicClient } from "viem"; + +import { SWRCache } from "@ensnode/ensnode-sdk"; +import type { BlockrangeWithStartBlock, ChainId, PonderIndexingMetrics } from "@ensnode/ponder-sdk"; + +import { + buildChainsIndexingMetadataImmutable, + type ChainsIndexingMetadataImmutable, +} from "../chains-indexing-metadata-immutable"; +import type { PonderClientCache } from "./ponder-client.cache"; + +/** + * Context required to load the chains indexing metadata immutable cache. + */ +export interface ChainsIndexingMetadataImmutableCacheContext { + indexedChainIds: Set; + chainsConfigBlockrange: Map; + publicClients: Map; + ponderClientCache: PonderClientCache; +} + +/** + * Type of the cache for the immutable metadata of the indexed chains. + */ +export type ChainsIndexingMetadataImmutableCache = SWRCache< + ChainsIndexingMetadataImmutable, + ChainsIndexingMetadataImmutableCacheContext +>; + +/** + * Cache for the immutable metadata of the indexed chains. + * + * This cache is designed to store metadata that is expected to remain constant + * throughout the indexing process. The metadata is built based on + * {@link PonderIndexingMetrics} value cached in {@link PonderClientCache}. + * There may be a few failed attempts to load this cache at the startup of + * the Ponder app until the metrics become available. Once the data is + * successfully loaded, the cache stops proactive revalidation since the data + * is expected to be immutable. + */ +export const chainsIndexingMetadataImmutableCache = new SWRCache({ + fn: async function loadChainsIndexingMetadataImmutable(_cachedValue, context) { + if (!context) { + throw new Error( + `ChainsIndexingMetadataImmutableCache context must be set to load Chains Indexing Metadata Immutable`, + ); + } + + const { indexedChainIds, chainsConfigBlockrange, publicClients, ponderClientCache } = context; + + try { + console.info(`[ChainsIndexingMetadataImmutableCache]: loading data...`); + const ponderClientCacheResult = await ponderClientCache.read(); + + // Invariant: indexing metrics must be available in cache + if (ponderClientCacheResult instanceof Error) { + throw new Error( + `Ponder Indexing Metrics must be available in cache to build chains indexing metadata immutable: ${ponderClientCacheResult.message}`, + ); + } + + const { ponderIndexingMetrics } = ponderClientCacheResult; + + const chainsIndexingMetadataImmutable = await buildChainsIndexingMetadataImmutable( + indexedChainIds, + chainsConfigBlockrange, + publicClients, + ponderIndexingMetrics, + ); + + console.info(`[ChainsIndexingMetadataImmutableCache]: Successfully loaded data`); + + // Stop the proactive revalidation of this cache since we have + // successfully loaded the data and initialized the client state. + chainsIndexingMetadataImmutableCache.stopProactiveRevalidation(); + + return chainsIndexingMetadataImmutable; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error( + `[ChainsIndexingMetadataImmutableCache]: an error occurred while loading data: ${errorMessage}`, + ); + + throw new Error(`Failed to load Chains Indexing Metadata Immutable: ${errorMessage}`); + } + }, + ttl: Number.POSITIVE_INFINITY, + proactiveRevalidationInterval: 5, +}) satisfies ChainsIndexingMetadataImmutableCache; From 84005d7f2a9ce688be330becec87f4f03b1a2c85 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Feb 2026 12:53:16 +0100 Subject: [PATCH 06/14] Update `LocalPonderClient` data fetching approach Replace `buildChainsIndexingMetadataImmutable` calls with `ChainsIndexingMetadataImmutableCache` reads while fetching `ChainsIndexingMetadataImmutable` data. --- .../ponder/src/api/lib/local-ponder-client.ts | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts index a2848c0ae..733ca2103 100644 --- a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts +++ b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts @@ -6,7 +6,6 @@ import { type BlockrangeWithStartBlock, type ChainId, PonderClient } from "@ensn import type { ChainIndexingMetadata, - ChainIndexingMetadataImmutable, } from "@/lib/indexing-status-builder/chain-indexing-metadata"; import ponderConfig from "@/ponder/config"; @@ -14,6 +13,7 @@ import { ponderClientCache, type PonderClientCache } from "./cache/ponder-client import { buildChainsBlockrange } from "./chains-config-blockrange"; import { buildChainsIndexingMetadataDynamic } from "./chains-indexing-metadata-dynamic"; import { buildChainsIndexingMetadataImmutable } from "./chains-indexing-metadata-immutable"; +import { chainsIndexingMetadataImmutableCache, ChainsIndexingMetadataImmutableCache } from "./cache/chains-indexing-metadata-immutable.cache"; /** * Build a map of chain ID to its configured blockrange (start and end blocks) @@ -45,18 +45,18 @@ function buildPublicClientsMap(): Map { */ export class LocalPonderClient { #indexedChainIds: Set; - #chainIndexingMetadataImmutable: Map; + #chainsIndexingMetadataImmutableCache: ChainsIndexingMetadataImmutableCache; #ponderClientCache: PonderClientCache; #publicClients: Map; private constructor( indexedChainIds: Set, - chainIndexingMetadataImmutable: Map, + chainsIndexingMetadataImmutableCache: ChainsIndexingMetadataImmutableCache, ponderClientCache: PonderClientCache, publicClients: Map, ) { this.#indexedChainIds = indexedChainIds; - this.#chainIndexingMetadataImmutable = chainIndexingMetadataImmutable; + this.#chainsIndexingMetadataImmutableCache = chainsIndexingMetadataImmutableCache; this.#ponderClientCache = ponderClientCache; this.#publicClients = publicClients; } @@ -77,19 +77,19 @@ export class LocalPonderClient { const ponderClient = new PonderClient(ponderAppUrl); ponderClientCache.setContext({ ponderClient }); - const ponderIndexingMetrics = await ponderClient.metrics(); const publicClients = buildPublicClientsMap(); const chainsConfigBlockrange = buildChainsConfigBlockrange(); - const chainIndexingMetadataImmutable = await buildChainsIndexingMetadataImmutable( + + chainsIndexingMetadataImmutableCache.setContext({ indexedChainIds, chainsConfigBlockrange, publicClients, - ponderIndexingMetrics, - ); + ponderClientCache, + }); const client = new LocalPonderClient( indexedChainIds, - chainIndexingMetadataImmutable, + chainsIndexingMetadataImmutableCache, ponderClientCache, publicClients, ); @@ -150,6 +150,15 @@ export class LocalPonderClient { ); } + const chainsIndexingMetadataImmutable = await this.#chainsIndexingMetadataImmutableCache.read(); + + // Invariant: indexing metrics must be available in cache + if (chainsIndexingMetadataImmutable instanceof Error) { + throw new Error( + `Chains Indexing Metadata Immutable must be available in cache to build chains indexing metadata immutable: ${chainsIndexingMetadataImmutable.message}`, + ); + } + const { ponderIndexingMetrics, ponderIndexingStatus } = ponderClientCacheResult; const chainsIndexingMetadataDynamic = buildChainsIndexingMetadataDynamic( @@ -159,7 +168,7 @@ export class LocalPonderClient { ); for (const chainId of this.indexedChainIds) { - const chainIndexingMetadataImmutable = this.#chainIndexingMetadataImmutable.get(chainId); + const chainIndexingMetadataImmutable = chainsIndexingMetadataImmutable.get(chainId); const chainIndexingMetadataDynamic = chainsIndexingMetadataDynamic.get(chainId); // Invariant: immutable and dynamic metadata must exist for indexed chain From 37f4b44e502fc5077eb383f17026b216fe13cd5e Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Feb 2026 13:01:42 +0100 Subject: [PATCH 07/14] Refactor `LocalPonderClient` into simplicity `LocalPonderClient` extends `PonderClient` now. This change allows setting up proper cache context for `this.#chainsIndexingMetadataImmutableCache`. Also, `LocalPonderClient` does not require any async initialization. The reremote data required by `LocalPonderClient` is now managed by two SWR cache instances: 1) `PonderClientCache`, and 2) `ChainsIndexingMetadataImmutableCache`. Ponder configuration and APIs are made avaialble via `LocalPonderClient` fields: `chainsConfigBlockrange`, `indexedChainIds`, `publicClients`; and methods: `getPublicClient`, `chainsIndexingMetadata`, plus all methods provided by `PonderClient` implementation. --- .../ponder/src/api/lib/local-ponder-client.ts | 235 ++++++++++-------- 1 file changed, 135 insertions(+), 100 deletions(-) diff --git a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts index 733ca2103..d7bad4ab0 100644 --- a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts +++ b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts @@ -6,95 +6,74 @@ import { type BlockrangeWithStartBlock, type ChainId, PonderClient } from "@ensn import type { ChainIndexingMetadata, + ChainIndexingMetadataDynamic, + ChainIndexingMetadataImmutable, } from "@/lib/indexing-status-builder/chain-indexing-metadata"; import ponderConfig from "@/ponder/config"; -import { ponderClientCache, type PonderClientCache } from "./cache/ponder-client.cache"; +import type { ChainsIndexingMetadataImmutableCache } from "./cache/chains-indexing-metadata-immutable.cache"; +import type { PonderClientCache } from "./cache/ponder-client.cache"; import { buildChainsBlockrange } from "./chains-config-blockrange"; import { buildChainsIndexingMetadataDynamic } from "./chains-indexing-metadata-dynamic"; -import { buildChainsIndexingMetadataImmutable } from "./chains-indexing-metadata-immutable"; -import { chainsIndexingMetadataImmutableCache, ChainsIndexingMetadataImmutableCache } from "./cache/chains-indexing-metadata-immutable.cache"; - -/** - * Build a map of chain ID to its configured blockrange (start and end blocks) - * based on the Ponder config. - * - * @throws Error if invariants are violated. - */ -function buildChainsConfigBlockrange(): Map { - return buildChainsBlockrange(ponderConfig); -} - -/** - * Build a map of cached RPC clients for each indexed chain. - * - * @returns A map where the keys are chain IDs and the values are the corresponding public clients. - * @throws Error if any of chain ID keys cannot be deserialized. - */ -function buildPublicClientsMap(): Map { - return new Map( - Object.entries(ponderPublicClients).map(([chainId, publicClient]) => [ - deserializeChainId(chainId), - publicClient, - ]), - ); -} /** * LocalPonderClient for interacting with the local Ponder app and its data. */ -export class LocalPonderClient { +export class LocalPonderClient extends PonderClient { + // Configuration #indexedChainIds: Set; + + // Caches #chainsIndexingMetadataImmutableCache: ChainsIndexingMetadataImmutableCache; #ponderClientCache: PonderClientCache; - #publicClients: Map; - private constructor( + // Values based on Ponder config and APIs + #chainsConfigBlockrange?: Map; + #publicClients?: Map; + + constructor( + ponderAppUrl: URL, indexedChainIds: Set, chainsIndexingMetadataImmutableCache: ChainsIndexingMetadataImmutableCache, ponderClientCache: PonderClientCache, - publicClients: Map, ) { + super(ponderAppUrl); + this.#indexedChainIds = indexedChainIds; - this.#chainsIndexingMetadataImmutableCache = chainsIndexingMetadataImmutableCache; + this.#ponderClientCache = ponderClientCache; - this.#publicClients = publicClients; + this.#ponderClientCache.setContext({ ponderClient: this }); + + this.#chainsIndexingMetadataImmutableCache = chainsIndexingMetadataImmutableCache; + this.#chainsIndexingMetadataImmutableCache.setContext({ + indexedChainIds: this.indexedChainIds, + chainsConfigBlockrange: this.chainsConfigBlockrange, + publicClients: this.publicClients, + ponderClientCache: this.#ponderClientCache, + }); } /** - * Initialize a LocalPonderClient instance by connecting to - * the local Ponder app and fetching the necessary data to build - * the client's state. + * Map of chain ID to its configured blockrange (start and end blocks) + * based on the Ponder config. * - * @param ponderAppUrl The URL of the local Ponder app to connect to. - * @returns An initialized LocalPonderClient instance ready to be - * used to access the local Ponder app and its data. - * - * @throws Error if the client fails to connect to the local Ponder app or - * if any of the required data cannot be fetched or is invalid. + * @throws Error if invariants are violated. */ - static async init(ponderAppUrl: URL, indexedChainIds: Set): Promise { - const ponderClient = new PonderClient(ponderAppUrl); - ponderClientCache.setContext({ ponderClient }); - - const publicClients = buildPublicClientsMap(); - const chainsConfigBlockrange = buildChainsConfigBlockrange(); - - chainsIndexingMetadataImmutableCache.setContext({ - indexedChainIds, - chainsConfigBlockrange, - publicClients, - ponderClientCache, - }); + get chainsConfigBlockrange(): Map { + if (this.#chainsConfigBlockrange) { + return this.#chainsConfigBlockrange; + } - const client = new LocalPonderClient( - indexedChainIds, - chainsIndexingMetadataImmutableCache, - ponderClientCache, - publicClients, - ); + this.#chainsConfigBlockrange = buildChainsBlockrange(this.ponderConfig); - return client; + return this.#chainsConfigBlockrange; + } + + /** + * List of indexed chain IDs. + */ + get indexedChainIds(): Set { + return this.#indexedChainIds; } /** @@ -105,10 +84,28 @@ export class LocalPonderClient { } /** - * List of indexed chain IDs. + * Map of chain ID to its RPC public client. + * + * Each RPC public client is cached by Ponder app. + * + * @returns Map where the keys are chain IDs and the values are + * the corresponding public clients. + * @throws Error if any of chain ID keys cannot be deserialized. */ - get indexedChainIds() { - return this.#indexedChainIds; + get publicClients(): Map { + if (this.#publicClients) { + return this.#publicClients; + } + + const result = new Map(); + + for (const [chainId, publicClient] of Object.entries(ponderPublicClients)) { + result.set(deserializeChainId(chainId), publicClient); + } + + this.#publicClients = result; + + return this.#publicClients; } /** @@ -119,65 +116,48 @@ export class LocalPonderClient { * @returns The public client for the specified chain ID. * @throws Error if no public client is found for the specified chain ID. */ - publicClient(chainId: ChainId): PublicClient { - const publicClient = this.#publicClients.get(chainId); + getPublicClient(chainId: ChainId): PublicClient { + const publicClient = this.publicClients.get(chainId); // Invariant: public client must exist for indexed chain if (!publicClient) { - throw new Error(`No public client found for chain ID ${chainId}`); + throw new Error(`Public client must be available for chain ID ${chainId}`); } return publicClient; } /** - * Chain indexing metadata. + * Chain Indexing Metadata. + * + * This method combines both {@link ChainIndexingMetadataImmutable} and + * {@link ChainIndexingMetadataDynamic} metadata for each indexed + * chain ID. * - * @returns Chain indexing metadata for each chain. + * The combined metadata gives a comprehensive view of the indexing status, + * indexing metrics, and configuration for each chain. + * + * @returns A {@link ChainIndexingMetadata} for each chain. * @throws Error if dynamic metadata could not be fetched, or if any of * the required metadata is missing or invalid for any indexed chain. */ async chainsIndexingMetadata(): Promise> { const chainsIndexingMetadata = new Map(); - const ponderClientCacheResult = await this.#ponderClientCache.read(); - - // Invariants: both indexing metrics and indexing status must be available - // in cache - if (ponderClientCacheResult instanceof Error) { - throw new Error( - `Ponder Client cache must be available to build chains indexing metadata dynamic: ${ponderClientCacheResult.message}`, - ); - } - - const chainsIndexingMetadataImmutable = await this.#chainsIndexingMetadataImmutableCache.read(); - - // Invariant: indexing metrics must be available in cache - if (chainsIndexingMetadataImmutable instanceof Error) { - throw new Error( - `Chains Indexing Metadata Immutable must be available in cache to build chains indexing metadata immutable: ${chainsIndexingMetadataImmutable.message}`, - ); - } - - const { ponderIndexingMetrics, ponderIndexingStatus } = ponderClientCacheResult; - - const chainsIndexingMetadataDynamic = buildChainsIndexingMetadataDynamic( - this.indexedChainIds, - ponderIndexingMetrics, - ponderIndexingStatus, - ); + const chainsIndexingMetadataImmutable = await this.getChainsIndexingMetadataImmutable(); + const chainsIndexingMetadataDynamic = await this.getChainsIndexingMetadataDynamic(); for (const chainId of this.indexedChainIds) { const chainIndexingMetadataImmutable = chainsIndexingMetadataImmutable.get(chainId); const chainIndexingMetadataDynamic = chainsIndexingMetadataDynamic.get(chainId); - // Invariant: immutable and dynamic metadata must exist for indexed chain + // Invariant: both, immutable and dynamic metadata must exist for indexed chain if (!chainIndexingMetadataImmutable) { - throw new Error(`No immutable indexing metadata found for chain ID ${chainId}`); + throw new Error(`Immutable indexing metadata must be available for chain ID ${chainId}`); } if (!chainIndexingMetadataDynamic) { - throw new Error(`No dynamic indexing metadata found for chain ID ${chainId}`); + throw new Error(`Dynamic indexing metadata must be available for chain ID ${chainId}`); } const metadata = { @@ -190,4 +170,59 @@ export class LocalPonderClient { return chainsIndexingMetadata; } + + /** + * Get the immutable part of the chains indexing metadata, which includes + * the metadata fields that are expected to remain constant for a chain + * during the indexing process. + * + * @returns A {@link ChainIndexingMetadataImmutable} for each indexed chain. + * @throws Error if the required metadata is not available in cache or if + * any invariants are violated. + */ + private async getChainsIndexingMetadataImmutable(): Promise< + Map + > { + const chainsIndexingMetadataImmutable = await this.#chainsIndexingMetadataImmutableCache.read(); + + // Invariant: indexing metrics must be available in cache + if (chainsIndexingMetadataImmutable instanceof Error) { + throw new Error( + `Chains Indexing Metadata Immutable must be available in cache to build chains indexing metadata immutable: ${chainsIndexingMetadataImmutable.message}`, + ); + } + + return chainsIndexingMetadataImmutable; + } + + /** + * Get the dynamic part of the chains indexing metadata, which includes + * the metadata fields that can change for a chain during the indexing + * process. + * + * @returns A {@link ChainIndexingMetadataDynamic} for each indexed chain. + * @throws Error if the required metadata is not available in cache or if + * any invariants are violated. + */ + private async getChainsIndexingMetadataDynamic(): Promise< + Map + > { + const ponderClientCacheResult = await this.#ponderClientCache.read(); + + // Invariants: both indexing metrics and indexing status must be available + // in cache + if (ponderClientCacheResult instanceof Error) { + throw new Error( + `Ponder Client cache must be available to build chains indexing metadata dynamic: ${ponderClientCacheResult.message}`, + ); + } + + const { ponderIndexingMetrics, ponderIndexingStatus } = ponderClientCacheResult; + + return buildChainsIndexingMetadataDynamic( + this.indexedChainIds, + ponderIndexingMetrics, + ponderIndexingStatus, + ); + } } From f3414c1754484522f190f1ea89125170997b1443 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Feb 2026 13:05:59 +0100 Subject: [PATCH 08/14] Make `getLocalPonderClient` sync function Now, that async data fetching has been delegated to SWR caches, creating a singleton instnace for `LocalPonderClient` is way simpler, _and synchronous_! --- apps/ensindexer/src/lib/ponder-api-client.ts | 65 +++++--------------- 1 file changed, 17 insertions(+), 48 deletions(-) diff --git a/apps/ensindexer/src/lib/ponder-api-client.ts b/apps/ensindexer/src/lib/ponder-api-client.ts index 5390bc1fa..1d8f6b2cc 100644 --- a/apps/ensindexer/src/lib/ponder-api-client.ts +++ b/apps/ensindexer/src/lib/ponder-api-client.ts @@ -1,62 +1,31 @@ import config from "@/config"; -import pRetry from "p-retry"; - +import { chainsIndexingMetadataImmutableCache } from "@/ponder/api/lib/cache/chains-indexing-metadata-immutable.cache"; +import { ponderClientCache } from "@/ponder/api/lib/cache/ponder-client.cache"; import { LocalPonderClient } from "@/ponder/api/lib/local-ponder-client"; -let localPonderClientPromise: Promise; +let localPonderClient: LocalPonderClient | undefined = undefined; /** * Get the singleton LocalPonderClient instance for the ENSIndexer app. * - * This function initializes the LocalPonderClient on the first call by - * connecting to the local Ponder app and fetching the necessary data to - * build the client's state. The initialized client is cached and returned - * on subsequent calls. + * This function relies on SWR caches with proactive revalidation to load + * necessary data for the client state, allowing the client to be initialized + * in a non-blocking way. The function will return the cached client instance. * * @returns The singleton LocalPonderClient instance. - * @throws Error if the client fails to initialize after - * the specified number of retries. */ -export async function getLocalPonderClient(): Promise { - // Return the cached client instance if it has already been initialized. - if (localPonderClientPromise) { - return localPonderClientPromise; - } - - // Initialize the LocalPonderClient in a non-blocking way. - // Apply retries in case of failure, for example, if the Ponder app is - // not yet ready to accept connections. - /** - * Initialize the LocalPonderClient by connecting to the local Ponder app and - * fetching necessary data to build the client's state. This operation is - * retried up to 3 times in case of failure, with a warning logged on each - * failed attempt. - * - * @returns The initialized LocalPonderClient instance. - * @throws Error if the client fails to initialize after the specified number of retries. - */ - localPonderClientPromise = pRetry( - () => LocalPonderClient.init(config.ensIndexerUrl, config.indexedChainIds), - { - retries: 3, - - onFailedAttempt: ({ error, attemptNumber, retriesLeft }) => { - console.warn( - `Initializing local Ponder client attempt ${attemptNumber} failed (${error.message}). ${retriesLeft} retries left.`, - ); - }, - }, - ).catch((error) => { - console.error( - `Failed to initialize LocalPonderClient after multiple attempts: ${error.message}`, +export function getLocalPonderClient(): LocalPonderClient { + // Initialize the singleton LocalPonderClient instance if it hasn't been + // initialized yet. + if (localPonderClient === undefined) { + localPonderClient = new LocalPonderClient( + config.ensIndexerUrl, + config.indexedChainIds, + chainsIndexingMetadataImmutableCache, + ponderClientCache, ); + } - // Signal termination of the process with a non-zero exit code to indicate failure. - process.exitCode = 1; - - throw error; - }); - - return localPonderClientPromise; + return localPonderClient; } From 2264126d32c7bc4fad356f1c0cd8e2ebefa51d98 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Feb 2026 13:08:37 +0100 Subject: [PATCH 09/14] Use `LocalPonderClient` in Indexer Status API handler Declares a top-level `localPonderClient` var that references the singleton `LocalPonderClient` instance. It is important to initialize singleton this way, as it also initializes SWR caches to perform data fetching in the background. --- .../ponder/src/api/handlers/ensnode-api.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts index 365be365c..9fb811fa2 100644 --- a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts +++ b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts @@ -19,12 +19,11 @@ import { buildOmnichainIndexingStatusSnapshot } from "@/lib/indexing-status-buil import { getLocalPonderClient } from "@/lib/ponder-api-client"; const app = new Hono(); - -// Calling `getLocalPonderClient` at the top level to initialize -// the singleton client instance on app startup. -// This ensures that the client is ready to use when handling requests, -// and allows us to catch initialization errors early. -getLocalPonderClient(); +// Get the local Ponder Client instance +// Note that the client initialization is designed to be non-blocking, +// by implementing internal SWR caches with proactive revalidation to +// load necessary data for the client state. +const localPonderClient = getLocalPonderClient(); // include ENSIndexer Public Config endpoint app.get("/config", async (c) => { @@ -42,7 +41,6 @@ app.get("/indexing-status", async (c) => { let omnichainSnapshot: OmnichainIndexingStatusSnapshot | undefined; try { - const localPonderClient = await getLocalPonderClient(); const chainsIndexingMetadata = await localPonderClient.chainsIndexingMetadata(); omnichainSnapshot = buildOmnichainIndexingStatusSnapshot(chainsIndexingMetadata); From 65ee9792660d6126973c63fa330f97d84ab065ef Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Feb 2026 13:12:30 +0100 Subject: [PATCH 10/14] Fix typo --- apps/ensindexer/ponder/src/api/lib/chains-config-blockrange.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ensindexer/ponder/src/api/lib/chains-config-blockrange.ts b/apps/ensindexer/ponder/src/api/lib/chains-config-blockrange.ts index ad89ab2ae..22f2f0deb 100644 --- a/apps/ensindexer/ponder/src/api/lib/chains-config-blockrange.ts +++ b/apps/ensindexer/ponder/src/api/lib/chains-config-blockrange.ts @@ -149,7 +149,7 @@ export function buildChainsBlockrange( // ponderSource for that chain has its respective `endBlock` defined. const isEndBlockForChainAllowed = chainEndBlocks.length === chainStartBlocks.length; - // 3.b) Get the highest endBLock for the chain. + // 3.b) Get the highest endBlock for the chain. const chainHighestEndBlock = isEndBlockForChainAllowed && chainEndBlocks.length > 0 ? Math.max(...chainEndBlocks) From 817f1cce46fc5767e9d9354e55f220ead5ce7cef Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Feb 2026 13:53:55 +0100 Subject: [PATCH 11/14] docs(changeset): Implements `LocalPonderClient` allowing interactions with Ponder app configuration and data. --- .changeset/purple-glasses-shave.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/purple-glasses-shave.md diff --git a/.changeset/purple-glasses-shave.md b/.changeset/purple-glasses-shave.md new file mode 100644 index 000000000..661f7a311 --- /dev/null +++ b/.changeset/purple-glasses-shave.md @@ -0,0 +1,5 @@ +--- +"ensindexer": minor +--- + +Implements `LocalPonderClient` allowing interactions with Ponder app configuration and data. From 7559fb80f102bca31fc667a9bc051d10d64269b0 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Feb 2026 18:28:12 +0100 Subject: [PATCH 12/14] Further simplify `LocalPonderClient` implementation 1) `SWRCache` implementation has been reset to the current `main` branch version. 2) Caching `ChainsIndexingMetadataImmutable` value, which includes relevant block refs, has been moved from SWRCache implementation into the `chainsIndexingMetadataImmutable` field on `LocalPonderClient` class. The SWRCache for `ChainsIndexingMetadataImmutable` has been removed. 3) The `chainsIndexingMetadata()` method was replaced by more straightforward `getOmnichainIndexingStatusSnapshot` method, which can be called with no arguments from a caller's side. 4) SWRCache for `PonderClient`, called `PonderClientCache`, creates `PonderClient` instance directly now, so no cache context object is needed. 5) `PonderClientCache` is now initiated lazily. Its revalidation interval is set to 10 seconds. This gives time for Ponder app to load data needed for Ponder Client. 6) `LocalPonderClient` imports `PonderClientCache` directly, so there is no need to inject it with constructor params. 7) The Indexing Status API endpoint can now just call `omnichainSnapshot = await localPonderClient.getOmnichainIndexingStatusSnapshot();` to get the complete, fresh value for current omnichain indexing status snapshot. --- .../ponder/src/api/handlers/ensnode-api.ts | 5 +- ...hains-indexing-metadata-immutable.cache.ts | 89 --------- .../src/api/lib/cache/ponder-client.cache.ts | 8 +- .../ponder/src/api/lib/local-ponder-client.ts | 180 +++++------------- apps/ensindexer/src/lib/ponder-api-client.ts | 11 +- 5 files changed, 55 insertions(+), 238 deletions(-) delete mode 100644 apps/ensindexer/ponder/src/api/lib/cache/chains-indexing-metadata-immutable.cache.ts diff --git a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts index 9fb811fa2..9a8493408 100644 --- a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts +++ b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts @@ -15,7 +15,6 @@ import { import { buildENSIndexerPublicConfig } from "@/config/public"; import { createCrossChainIndexingStatusSnapshotOmnichain } from "@/lib/indexing-status/build-index-status"; -import { buildOmnichainIndexingStatusSnapshot } from "@/lib/indexing-status-builder/omnichain-indexing-status-snapshot"; import { getLocalPonderClient } from "@/lib/ponder-api-client"; const app = new Hono(); @@ -41,9 +40,7 @@ app.get("/indexing-status", async (c) => { let omnichainSnapshot: OmnichainIndexingStatusSnapshot | undefined; try { - const chainsIndexingMetadata = await localPonderClient.chainsIndexingMetadata(); - - omnichainSnapshot = buildOmnichainIndexingStatusSnapshot(chainsIndexingMetadata); + omnichainSnapshot = await localPonderClient.getOmnichainIndexingStatusSnapshot(); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; console.error( diff --git a/apps/ensindexer/ponder/src/api/lib/cache/chains-indexing-metadata-immutable.cache.ts b/apps/ensindexer/ponder/src/api/lib/cache/chains-indexing-metadata-immutable.cache.ts deleted file mode 100644 index ce02a1fcf..000000000 --- a/apps/ensindexer/ponder/src/api/lib/cache/chains-indexing-metadata-immutable.cache.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { PublicClient } from "viem"; - -import { SWRCache } from "@ensnode/ensnode-sdk"; -import type { BlockrangeWithStartBlock, ChainId, PonderIndexingMetrics } from "@ensnode/ponder-sdk"; - -import { - buildChainsIndexingMetadataImmutable, - type ChainsIndexingMetadataImmutable, -} from "../chains-indexing-metadata-immutable"; -import type { PonderClientCache } from "./ponder-client.cache"; - -/** - * Context required to load the chains indexing metadata immutable cache. - */ -export interface ChainsIndexingMetadataImmutableCacheContext { - indexedChainIds: Set; - chainsConfigBlockrange: Map; - publicClients: Map; - ponderClientCache: PonderClientCache; -} - -/** - * Type of the cache for the immutable metadata of the indexed chains. - */ -export type ChainsIndexingMetadataImmutableCache = SWRCache< - ChainsIndexingMetadataImmutable, - ChainsIndexingMetadataImmutableCacheContext ->; - -/** - * Cache for the immutable metadata of the indexed chains. - * - * This cache is designed to store metadata that is expected to remain constant - * throughout the indexing process. The metadata is built based on - * {@link PonderIndexingMetrics} value cached in {@link PonderClientCache}. - * There may be a few failed attempts to load this cache at the startup of - * the Ponder app until the metrics become available. Once the data is - * successfully loaded, the cache stops proactive revalidation since the data - * is expected to be immutable. - */ -export const chainsIndexingMetadataImmutableCache = new SWRCache({ - fn: async function loadChainsIndexingMetadataImmutable(_cachedValue, context) { - if (!context) { - throw new Error( - `ChainsIndexingMetadataImmutableCache context must be set to load Chains Indexing Metadata Immutable`, - ); - } - - const { indexedChainIds, chainsConfigBlockrange, publicClients, ponderClientCache } = context; - - try { - console.info(`[ChainsIndexingMetadataImmutableCache]: loading data...`); - const ponderClientCacheResult = await ponderClientCache.read(); - - // Invariant: indexing metrics must be available in cache - if (ponderClientCacheResult instanceof Error) { - throw new Error( - `Ponder Indexing Metrics must be available in cache to build chains indexing metadata immutable: ${ponderClientCacheResult.message}`, - ); - } - - const { ponderIndexingMetrics } = ponderClientCacheResult; - - const chainsIndexingMetadataImmutable = await buildChainsIndexingMetadataImmutable( - indexedChainIds, - chainsConfigBlockrange, - publicClients, - ponderIndexingMetrics, - ); - - console.info(`[ChainsIndexingMetadataImmutableCache]: Successfully loaded data`); - - // Stop the proactive revalidation of this cache since we have - // successfully loaded the data and initialized the client state. - chainsIndexingMetadataImmutableCache.stopProactiveRevalidation(); - - return chainsIndexingMetadataImmutable; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error( - `[ChainsIndexingMetadataImmutableCache]: an error occurred while loading data: ${errorMessage}`, - ); - - throw new Error(`Failed to load Chains Indexing Metadata Immutable: ${errorMessage}`); - } - }, - ttl: Number.POSITIVE_INFINITY, - proactiveRevalidationInterval: 5, -}) satisfies ChainsIndexingMetadataImmutableCache; diff --git a/apps/ensindexer/ponder/src/api/lib/cache/ponder-client.cache.ts b/apps/ensindexer/ponder/src/api/lib/cache/ponder-client.cache.ts index 228e1b6ae..206959101 100644 --- a/apps/ensindexer/ponder/src/api/lib/cache/ponder-client.cache.ts +++ b/apps/ensindexer/ponder/src/api/lib/cache/ponder-client.cache.ts @@ -1,9 +1,10 @@ import config from "@/config"; + import { type Duration, SWRCache } from "@ensnode/ensnode-sdk"; import { PonderClient, - PonderIndexingMetrics, - PonderIndexingStatus, + type PonderIndexingMetrics, + type PonderIndexingStatus, } from "@ensnode/ponder-sdk"; /** @@ -47,12 +48,10 @@ const ponderClient = new PonderClient(config.ensIndexerUrl); export const ponderClientCache = new SWRCache({ fn: async function loadPonderClientCache() { try { - console.info(`[PonderClientCache]: loading data...`); const [ponderIndexingMetrics, ponderIndexingStatus] = await Promise.all([ ponderClient.metrics(), ponderClient.status(), ]); - console.info(`[PonderClientCache]: Successfully loaded data`); return { ponderIndexingMetrics, ponderIndexingStatus }; } catch (error) { @@ -64,5 +63,4 @@ export const ponderClientCache = new SWRCache({ }, ttl: Number.POSITIVE_INFINITY, proactiveRevalidationInterval: 10 satisfies Duration, - proactivelyInitialize: true, }) satisfies PonderClientCache; diff --git a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts index d7bad4ab0..7315f3923 100644 --- a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts +++ b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts @@ -1,18 +1,18 @@ import { publicClients as ponderPublicClients } from "ponder:api"; import type { PublicClient } from "viem"; -import { deserializeChainId } from "@ensnode/ensnode-sdk"; +import { deserializeChainId, type OmnichainIndexingStatusSnapshot } from "@ensnode/ensnode-sdk"; import { type BlockrangeWithStartBlock, type ChainId, PonderClient } from "@ensnode/ponder-sdk"; import type { ChainIndexingMetadata, - ChainIndexingMetadataDynamic, ChainIndexingMetadataImmutable, } from "@/lib/indexing-status-builder/chain-indexing-metadata"; +import { buildOmnichainIndexingStatusSnapshot } from "@/lib/indexing-status-builder/omnichain-indexing-status-snapshot"; +import { buildChainsIndexingMetadataImmutable } from "@/ponder/api/lib/chains-indexing-metadata-immutable"; import ponderConfig from "@/ponder/config"; -import type { ChainsIndexingMetadataImmutableCache } from "./cache/chains-indexing-metadata-immutable.cache"; -import type { PonderClientCache } from "./cache/ponder-client.cache"; +import { ponderClientCache } from "./cache/ponder-client.cache"; import { buildChainsBlockrange } from "./chains-config-blockrange"; import { buildChainsIndexingMetadataDynamic } from "./chains-indexing-metadata-dynamic"; @@ -23,34 +23,15 @@ export class LocalPonderClient extends PonderClient { // Configuration #indexedChainIds: Set; - // Caches - #chainsIndexingMetadataImmutableCache: ChainsIndexingMetadataImmutableCache; - #ponderClientCache: PonderClientCache; - - // Values based on Ponder config and APIs + // Values based on Ponder config and inter-process APIs #chainsConfigBlockrange?: Map; + #chainsIndexingMetadataImmutable?: Map; #publicClients?: Map; - constructor( - ponderAppUrl: URL, - indexedChainIds: Set, - chainsIndexingMetadataImmutableCache: ChainsIndexingMetadataImmutableCache, - ponderClientCache: PonderClientCache, - ) { + constructor(ponderAppUrl: URL, indexedChainIds: Set) { super(ponderAppUrl); this.#indexedChainIds = indexedChainIds; - - this.#ponderClientCache = ponderClientCache; - this.#ponderClientCache.setContext({ ponderClient: this }); - - this.#chainsIndexingMetadataImmutableCache = chainsIndexingMetadataImmutableCache; - this.#chainsIndexingMetadataImmutableCache.setContext({ - indexedChainIds: this.indexedChainIds, - chainsConfigBlockrange: this.chainsConfigBlockrange, - publicClients: this.publicClients, - ponderClientCache: this.#ponderClientCache, - }); } /** @@ -59,30 +40,16 @@ export class LocalPonderClient extends PonderClient { * * @throws Error if invariants are violated. */ - get chainsConfigBlockrange(): Map { + private get chainsConfigBlockrange(): Map { if (this.#chainsConfigBlockrange) { return this.#chainsConfigBlockrange; } - this.#chainsConfigBlockrange = buildChainsBlockrange(this.ponderConfig); + this.#chainsConfigBlockrange = buildChainsBlockrange(ponderConfig); return this.#chainsConfigBlockrange; } - /** - * List of indexed chain IDs. - */ - get indexedChainIds(): Set { - return this.#indexedChainIds; - } - - /** - * Complete Ponder config object. - */ - get ponderConfig() { - return ponderConfig; - } - /** * Map of chain ID to its RPC public client. * @@ -92,7 +59,7 @@ export class LocalPonderClient extends PonderClient { * the corresponding public clients. * @throws Error if any of chain ID keys cannot be deserialized. */ - get publicClients(): Map { + private get publicClients(): Map { if (this.#publicClients) { return this.#publicClients; } @@ -109,46 +76,52 @@ export class LocalPonderClient extends PonderClient { } /** - * Public client for a given chain ID. + * Get {@link OmnichainIndexingStatusSnapshot} for the indexed chains. + * + * This method fetches the necessary data from the Ponder Client cache and + * builds the indexing status snapshot for all indexed chains. * - * @param chainId The chain ID for which to get the public client. - * Must be one of the indexed chain IDs. - * @returns The public client for the specified chain ID. - * @throws Error if no public client is found for the specified chain ID. + * @returns A {@link OmnichainIndexingStatusSnapshot} for the indexed chains. + * @throws Error if required data is not available in cache or if any of + * the invariants are violated. For example, if indexing metrics are not + * available in cache, or if the metadata for any indexed chain cannot be + * built due to missing or invalid data. */ - getPublicClient(chainId: ChainId): PublicClient { - const publicClient = this.publicClients.get(chainId); + public async getOmnichainIndexingStatusSnapshot(): Promise { + const chainsIndexingMetadata = new Map(); + + const ponderClientCacheResult = await ponderClientCache.read(); - // Invariant: public client must exist for indexed chain - if (!publicClient) { - throw new Error(`Public client must be available for chain ID ${chainId}`); + // Invariant: indexing metrics must be available in cache + if (ponderClientCacheResult instanceof Error) { + throw new Error( + `Ponder Indexing Metrics must be available in cache to build chains indexing metadata immutable: ${ponderClientCacheResult.message}`, + ); } - return publicClient; - } + const { ponderIndexingMetrics, ponderIndexingStatus } = ponderClientCacheResult; - /** - * Chain Indexing Metadata. - * - * This method combines both {@link ChainIndexingMetadataImmutable} and - * {@link ChainIndexingMetadataDynamic} metadata for each indexed - * chain ID. - * - * The combined metadata gives a comprehensive view of the indexing status, - * indexing metrics, and configuration for each chain. - * - * @returns A {@link ChainIndexingMetadata} for each chain. - * @throws Error if dynamic metadata could not be fetched, or if any of - * the required metadata is missing or invalid for any indexed chain. - */ - async chainsIndexingMetadata(): Promise> { - const chainsIndexingMetadata = new Map(); + // Build and cache immutable metadata for indexed chains if not already + // cached. + if (this.#chainsIndexingMetadataImmutable === undefined) { + this.#chainsIndexingMetadataImmutable = await buildChainsIndexingMetadataImmutable( + this.#indexedChainIds, + this.chainsConfigBlockrange, + this.publicClients, + ponderIndexingMetrics, + ); + } - const chainsIndexingMetadataImmutable = await this.getChainsIndexingMetadataImmutable(); - const chainsIndexingMetadataDynamic = await this.getChainsIndexingMetadataDynamic(); + // Build dynamic metadata for indexed chains on each request since + // it may change frequently based on the indexing status and metrics. + const chainsIndexingMetadataDynamic = buildChainsIndexingMetadataDynamic( + this.#indexedChainIds, + ponderIndexingMetrics, + ponderIndexingStatus, + ); - for (const chainId of this.indexedChainIds) { - const chainIndexingMetadataImmutable = chainsIndexingMetadataImmutable.get(chainId); + for (const chainId of this.#indexedChainIds) { + const chainIndexingMetadataImmutable = this.#chainsIndexingMetadataImmutable.get(chainId); const chainIndexingMetadataDynamic = chainsIndexingMetadataDynamic.get(chainId); // Invariant: both, immutable and dynamic metadata must exist for indexed chain @@ -168,61 +141,6 @@ export class LocalPonderClient extends PonderClient { chainsIndexingMetadata.set(chainId, metadata); } - return chainsIndexingMetadata; - } - - /** - * Get the immutable part of the chains indexing metadata, which includes - * the metadata fields that are expected to remain constant for a chain - * during the indexing process. - * - * @returns A {@link ChainIndexingMetadataImmutable} for each indexed chain. - * @throws Error if the required metadata is not available in cache or if - * any invariants are violated. - */ - private async getChainsIndexingMetadataImmutable(): Promise< - Map - > { - const chainsIndexingMetadataImmutable = await this.#chainsIndexingMetadataImmutableCache.read(); - - // Invariant: indexing metrics must be available in cache - if (chainsIndexingMetadataImmutable instanceof Error) { - throw new Error( - `Chains Indexing Metadata Immutable must be available in cache to build chains indexing metadata immutable: ${chainsIndexingMetadataImmutable.message}`, - ); - } - - return chainsIndexingMetadataImmutable; - } - - /** - * Get the dynamic part of the chains indexing metadata, which includes - * the metadata fields that can change for a chain during the indexing - * process. - * - * @returns A {@link ChainIndexingMetadataDynamic} for each indexed chain. - * @throws Error if the required metadata is not available in cache or if - * any invariants are violated. - */ - private async getChainsIndexingMetadataDynamic(): Promise< - Map - > { - const ponderClientCacheResult = await this.#ponderClientCache.read(); - - // Invariants: both indexing metrics and indexing status must be available - // in cache - if (ponderClientCacheResult instanceof Error) { - throw new Error( - `Ponder Client cache must be available to build chains indexing metadata dynamic: ${ponderClientCacheResult.message}`, - ); - } - - const { ponderIndexingMetrics, ponderIndexingStatus } = ponderClientCacheResult; - - return buildChainsIndexingMetadataDynamic( - this.indexedChainIds, - ponderIndexingMetrics, - ponderIndexingStatus, - ); + return buildOmnichainIndexingStatusSnapshot(chainsIndexingMetadata); } } diff --git a/apps/ensindexer/src/lib/ponder-api-client.ts b/apps/ensindexer/src/lib/ponder-api-client.ts index 1d8f6b2cc..7ac6d91f0 100644 --- a/apps/ensindexer/src/lib/ponder-api-client.ts +++ b/apps/ensindexer/src/lib/ponder-api-client.ts @@ -1,7 +1,5 @@ import config from "@/config"; -import { chainsIndexingMetadataImmutableCache } from "@/ponder/api/lib/cache/chains-indexing-metadata-immutable.cache"; -import { ponderClientCache } from "@/ponder/api/lib/cache/ponder-client.cache"; import { LocalPonderClient } from "@/ponder/api/lib/local-ponder-client"; let localPonderClient: LocalPonderClient | undefined = undefined; @@ -11,7 +9,7 @@ let localPonderClient: LocalPonderClient | undefined = undefined; * * This function relies on SWR caches with proactive revalidation to load * necessary data for the client state, allowing the client to be initialized - * in a non-blocking way. The function will return the cached client instance. + * in a non-blocking way. * * @returns The singleton LocalPonderClient instance. */ @@ -19,12 +17,7 @@ export function getLocalPonderClient(): LocalPonderClient { // Initialize the singleton LocalPonderClient instance if it hasn't been // initialized yet. if (localPonderClient === undefined) { - localPonderClient = new LocalPonderClient( - config.ensIndexerUrl, - config.indexedChainIds, - chainsIndexingMetadataImmutableCache, - ponderClientCache, - ); + localPonderClient = new LocalPonderClient(config.ensIndexerUrl, config.indexedChainIds); } return localPonderClient; From 6fbed6440ad4740aa75587558ad76a090fe0ca1f Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Feb 2026 20:28:54 +0100 Subject: [PATCH 13/14] Apply AI PR feedback --- .../lib/chains-indexing-metadata-dynamic.ts | 2 +- .../lib/chains-indexing-metadata-immutable.ts | 2 +- .../ponder/src/api/lib/local-ponder-client.ts | 22 ++++++++++++++----- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-dynamic.ts b/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-dynamic.ts index efb772f89..90fb97510 100644 --- a/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-dynamic.ts +++ b/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-dynamic.ts @@ -23,7 +23,7 @@ export function buildChainsIndexingMetadataDynamic( ): Map { const chainsIndexingMetadataDynamic = new Map(); - for (const chainId of indexedChainIds.values()) { + for (const chainId of indexedChainIds) { const chainIndexingMetrics = ponderIndexingMetrics.chains.get(chainId); const chainIndexingStatus = ponderIndexingStatus.chains.get(chainId); diff --git a/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-immutable.ts b/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-immutable.ts index 6a0dd54b5..c6ddefc20 100644 --- a/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-immutable.ts +++ b/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-immutable.ts @@ -64,7 +64,7 @@ export async function buildChainsIndexingMetadataImmutable( ): Promise> { const chainsIndexingMetadataImmutable = new Map(); - for (const chainId of indexedChainIds.values()) { + for (const chainId of indexedChainIds) { const chainConfigBlockrange = chainsConfigBlockrange.get(chainId); const chainIndexingMetrics = ponderIndexingMetrics.chains.get(chainId); const publicClient = publicClients.get(chainId); diff --git a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts index 7315f3923..aa4ddd8b6 100644 --- a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts +++ b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts @@ -25,7 +25,7 @@ export class LocalPonderClient extends PonderClient { // Values based on Ponder config and inter-process APIs #chainsConfigBlockrange?: Map; - #chainsIndexingMetadataImmutable?: Map; + #chainsIndexingMetadataImmutable?: Promise>; #publicClients?: Map; constructor(ponderAppUrl: URL, indexedChainIds: Set) { @@ -101,10 +101,9 @@ export class LocalPonderClient extends PonderClient { const { ponderIndexingMetrics, ponderIndexingStatus } = ponderClientCacheResult; - // Build and cache immutable metadata for indexed chains if not already - // cached. + // Build and cache immutable metadata for indexed chains if not already cached. if (this.#chainsIndexingMetadataImmutable === undefined) { - this.#chainsIndexingMetadataImmutable = await buildChainsIndexingMetadataImmutable( + this.#chainsIndexingMetadataImmutable = buildChainsIndexingMetadataImmutable( this.#indexedChainIds, this.chainsConfigBlockrange, this.publicClients, @@ -112,6 +111,19 @@ export class LocalPonderClient extends PonderClient { ); } + let chainsIndexingMetadataImmutable: Map; + + try { + chainsIndexingMetadataImmutable = await this.#chainsIndexingMetadataImmutable; + } catch (error) { + // Reset the cached promise if it is rejected to allow retrying on + // the next request, since the error may be transient. + this.#chainsIndexingMetadataImmutable = undefined; + + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new Error(`Chains Indexing Metadata Immutable must be available to build omnichain indexing status snapshot: ${errorMessage}`); + } + // Build dynamic metadata for indexed chains on each request since // it may change frequently based on the indexing status and metrics. const chainsIndexingMetadataDynamic = buildChainsIndexingMetadataDynamic( @@ -121,7 +133,7 @@ export class LocalPonderClient extends PonderClient { ); for (const chainId of this.#indexedChainIds) { - const chainIndexingMetadataImmutable = this.#chainsIndexingMetadataImmutable.get(chainId); + const chainIndexingMetadataImmutable = chainsIndexingMetadataImmutable.get(chainId); const chainIndexingMetadataDynamic = chainsIndexingMetadataDynamic.get(chainId); // Invariant: both, immutable and dynamic metadata must exist for indexed chain From f3ab1274623cc20b3d530918369b5d71c6a07b43 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Feb 2026 21:30:26 +0100 Subject: [PATCH 14/14] Apply code formatting --- apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts | 8 +++++--- apps/ensindexer/src/lib/ponder-api-client.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts index aa4ddd8b6..b5e16ddbb 100644 --- a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts +++ b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts @@ -112,7 +112,7 @@ export class LocalPonderClient extends PonderClient { } let chainsIndexingMetadataImmutable: Map; - + try { chainsIndexingMetadataImmutable = await this.#chainsIndexingMetadataImmutable; } catch (error) { @@ -120,8 +120,10 @@ export class LocalPonderClient extends PonderClient { // the next request, since the error may be transient. this.#chainsIndexingMetadataImmutable = undefined; - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw new Error(`Chains Indexing Metadata Immutable must be available to build omnichain indexing status snapshot: ${errorMessage}`); + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + throw new Error( + `Chains Indexing Metadata Immutable must be available to build omnichain indexing status snapshot: ${errorMessage}`, + ); } // Build dynamic metadata for indexed chains on each request since diff --git a/apps/ensindexer/src/lib/ponder-api-client.ts b/apps/ensindexer/src/lib/ponder-api-client.ts index 7ac6d91f0..194e545dc 100644 --- a/apps/ensindexer/src/lib/ponder-api-client.ts +++ b/apps/ensindexer/src/lib/ponder-api-client.ts @@ -2,7 +2,7 @@ import config from "@/config"; import { LocalPonderClient } from "@/ponder/api/lib/local-ponder-client"; -let localPonderClient: LocalPonderClient | undefined = undefined; +let localPonderClient: LocalPonderClient | undefined; /** * Get the singleton LocalPonderClient instance for the ENSIndexer app.