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. diff --git a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts index 365be365c..9a8493408 100644 --- a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts +++ b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts @@ -15,16 +15,14 @@ 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(); - -// 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,10 +40,7 @@ app.get("/indexing-status", async (c) => { let omnichainSnapshot: OmnichainIndexingStatusSnapshot | undefined; try { - const localPonderClient = await getLocalPonderClient(); - 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/ponder-client.cache.ts b/apps/ensindexer/ponder/src/api/lib/cache/ponder-client.cache.ts new file mode 100644 index 000000000..206959101 --- /dev/null +++ b/apps/ensindexer/ponder/src/api/lib/cache/ponder-client.cache.ts @@ -0,0 +1,66 @@ +import config from "@/config"; + +import { type Duration, SWRCache } from "@ensnode/ensnode-sdk"; +import { + PonderClient, + type PonderIndexingMetrics, + type 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 { + const [ponderIndexingMetrics, ponderIndexingStatus] = await Promise.all([ + ponderClient.metrics(), + ponderClient.status(), + ]); + + 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, +}) satisfies PonderClientCache; 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) 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..90fb97510 --- /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) { + 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/chains-indexing-metadata-immutable.ts b/apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-immutable.ts new file mode 100644 index 000000000..c6ddefc20 --- /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) { + 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..b5e16ddbb 100644 --- a/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts +++ b/apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts @@ -1,280 +1,150 @@ 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 { 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 { ponderClientCache } from "./cache/ponder-client.cache"; import { buildChainsBlockrange } from "./chains-config-blockrange"; -import { fetchBlockRef } from "./fetch-block-ref"; +import { buildChainsIndexingMetadataDynamic } from "./chains-indexing-metadata-dynamic"; /** - * 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, - ]), - ); -} - -/** - * 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. + * LocalPonderClient for interacting with the local Ponder app and its data. */ -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}`); - } +export class LocalPonderClient extends PonderClient { + // Configuration + #indexedChainIds: Set; - if (!chainConfigBlockrange.startBlock) { - throw new Error( - `No start block found in chain config blockrange for indexed chain ID ${chainId}`, - ); - } + // Values based on Ponder config and inter-process APIs + #chainsConfigBlockrange?: Map; + #chainsIndexingMetadataImmutable?: Promise>; + #publicClients?: Map; - if (!chainIndexingMetrics) { - throw new Error(`No indexing metrics found for indexed chain ID ${chainId}`); - } + constructor(ponderAppUrl: URL, indexedChainIds: Set) { + super(ponderAppUrl); - 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}"`, - ); - } + this.#indexedChainIds = indexedChainIds; + } - if (!chainIndexingStatus) { - throw new Error(`No indexing status found for indexed chain ID ${chainId}`); + /** + * Map of chain ID to its configured blockrange (start and end blocks) + * based on the Ponder config. + * + * @throws Error if invariants are violated. + */ + private get chainsConfigBlockrange(): Map { + if (this.#chainsConfigBlockrange) { + return this.#chainsConfigBlockrange; } - 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); + this.#chainsConfigBlockrange = buildChainsBlockrange(ponderConfig); - const metadataImmutable = { - backfillScope: { - startBlock, - endBlock: backfillEndBlock, - }, - indexingConfig: chainIndexingConfig, - } satisfies ChainIndexingMetadataImmutable; - - // Cache the immutable metadata for this chain ID - chainsIndexingMetadataImmutable.set(chainId, metadataImmutable); + return this.#chainsConfigBlockrange; } - console.log("ChainIndexingMetadataImmutable built successfully"); - - return chainsIndexingMetadataImmutable; -} - -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}`); + /** + * 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. + */ + private get publicClients(): Map { + if (this.#publicClients) { + return this.#publicClients; } - const metadataDynamic = { - indexingMetrics: chainIndexingMetrics, - indexingStatus: chainIndexingStatus, - } satisfies ChainIndexingMetadataDynamic; - - // Cache the dynamic metadata for this chain ID - chainsIndexingMetadataDynamic.set(chainId, metadataDynamic); - } + const result = new Map(); - return chainsIndexingMetadataDynamic; -} + for (const [chainId, publicClient] of Object.entries(ponderPublicClients)) { + result.set(deserializeChainId(chainId), publicClient); + } -/** - * LocalPonderClient for interacting with the local Ponder app and its data. - */ -export class LocalPonderClient { - #ponderClient: PonderClient; - #publicClients: Map; - #indexedChainIds: Set; - #chainIndexingMetadataImmutable: Map; + this.#publicClients = result; - private constructor( - ponderClient: PonderClient, - publicClients: Map, - indexedChainIds: Set, - chainIndexingMetadataImmutable: Map, - ) { - this.#ponderClient = ponderClient; - this.#publicClients = publicClients; - this.#indexedChainIds = indexedChainIds; - this.#chainIndexingMetadataImmutable = chainIndexingMetadataImmutable; + return this.#publicClients; } /** - * Initialize a LocalPonderClient instance by connecting to - * the local Ponder app and fetching the necessary data to build - * the client's state. + * Get {@link OmnichainIndexingStatusSnapshot} for the indexed chains. * - * @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. + * This method fetches the necessary data from the Ponder Client cache and + * builds the indexing status snapshot for all indexed chains. * - * @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. + * @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. */ - static async init(ponderAppUrl: URL, indexedChainIds: Set): Promise { - const ponderClient = new PonderClient(ponderAppUrl); - const publicClients = buildPublicClientsMap(); - const chainIndexingMetadataImmutable = await buildChainsIndexingMetadataImmutable( - publicClients, - ponderClient, - ); + public async getOmnichainIndexingStatusSnapshot(): Promise { + const chainsIndexingMetadata = new Map(); - const client = new LocalPonderClient( - ponderClient, - publicClients, - indexedChainIds, - chainIndexingMetadataImmutable, - ); + const ponderClientCacheResult = await ponderClientCache.read(); - return client; - } + // 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}`, + ); + } - /** - * Complete Ponder config object. - */ - get ponderConfig() { - return ponderConfig; - } + const { ponderIndexingMetrics, ponderIndexingStatus } = ponderClientCacheResult; - /** - * List of indexed chain IDs. - */ - get indexedChainIds() { - return this.#indexedChainIds; - } + // Build and cache immutable metadata for indexed chains if not already cached. + if (this.#chainsIndexingMetadataImmutable === undefined) { + this.#chainsIndexingMetadataImmutable = buildChainsIndexingMetadataImmutable( + this.#indexedChainIds, + this.chainsConfigBlockrange, + this.publicClients, + ponderIndexingMetrics, + ); + } - /** - * Ponder client instance connected to the local Ponder app. - */ - get ponderClient(): PonderClient { - return this.#ponderClient; - } + let chainsIndexingMetadataImmutable: Map; - /** - * Public client for a given chain ID. - * - * @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. - */ - publicClient(chainId: ChainId): PublicClient { - const publicClient = this.#publicClients.get(chainId); + 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; - // Invariant: public client must exist for indexed chain - if (!publicClient) { - throw new Error(`No public client found for chain ID ${chainId}`); + 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}`, + ); } - return publicClient; - } - - /** - * Chain indexing metadata. - * - * @returns Chain indexing metadata 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 chainsIndexingMetadataDynamic = await buildChainsIndexingMetadataDynamic( - this.indexedChainIds, - this.#ponderClient, + // 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 = this.#chainIndexingMetadataImmutable.get(chainId); + 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 = { @@ -285,6 +155,6 @@ export class LocalPonderClient { chainsIndexingMetadata.set(chainId, metadata); } - return chainsIndexingMetadata; + return buildOmnichainIndexingStatusSnapshot(chainsIndexingMetadata); } } diff --git a/apps/ensindexer/src/lib/ponder-api-client.ts b/apps/ensindexer/src/lib/ponder-api-client.ts index 5390bc1fa..194e545dc 100644 --- a/apps/ensindexer/src/lib/ponder-api-client.ts +++ b/apps/ensindexer/src/lib/ponder-api-client.ts @@ -1,62 +1,24 @@ import config from "@/config"; -import pRetry from "p-retry"; - import { LocalPonderClient } from "@/ponder/api/lib/local-ponder-client"; -let localPonderClientPromise: Promise; +let localPonderClient: LocalPonderClient | 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. * * @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; +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); } - // 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}`, - ); - - // Signal termination of the process with a non-zero exit code to indicate failure. - process.exitCode = 1; - - throw error; - }); - - return localPonderClientPromise; + return localPonderClient; }