Skip to content

feat(ensindexer): leverage SWR caches to achieve goals related to LocalPonderClient#1652

Draft
tk-o wants to merge 12 commits intofeat/indexing-status-builder-3from
feat/indexing-status-builder-3-with-cache
Draft

feat(ensindexer): leverage SWR caches to achieve goals related to LocalPonderClient#1652
tk-o wants to merge 12 commits intofeat/indexing-status-builder-3from
feat/indexing-status-builder-3-with-cache

Conversation

@tk-o
Copy link
Contributor

@tk-o tk-o commented Feb 19, 2026

Lite PR

Tip: Review docs on the ENSNode PR process

Summary

  • Updates SWRCache to allow reading "cache context" from the data loader function. Also, introduces stopProactiveRevalidation() method so that proactive validation could be turned off for a given cache instance after some condition is met.
  • Creates two SWR caches: one for fetching data from PonderClient, and other for fetching data from RPCs (via publicClients).
  • Updates LocalPonderClient to read data from aforementioned SWR caches to simplify how possible data loading failures are handled.
  • Simplifies the getLocalPonderClient() function that returns a LocalPonderClient singleton instance. Now, there's no need to await anything.

Why

  • Fetching data may fail, and we need to manage the complexity that stems from these possible failures in a simple way. SWRCache is a great abstraction for that goal, and LocalPonderClient should leverage it.

Testing

  • I ran static code analysis (lint, typecheck) and testing suite
  • I ran ENSIndexer service locally and tested it /api/indexing-status endpoint (including logs it produces, see below)
Logs
[ChainsIndexingMetadataImmutableCache]: loading data...
[ChainsIndexingMetadataImmutableCache]: an error occurred while loading data: Ponder Indexing Metrics must be available in cache to build chains indexing metadata immutable: PonderClientCache context must be set to load Ponder Indexing Metrics and Status
[PonderClientCache]: loading data...
[PonderClientCache]: Successfully loaded data
[ChainsIndexingMetadataImmutableCache]: loading data...
[PonderClientCache]: loading data...
[PonderClientCache]: Successfully loaded data
[ChainsIndexingMetadataImmutableCache]: Successfully loaded data
[PonderClientCache]: loading data...
[PonderClientCache]: Successfully loaded data
[PonderClientCache]: loading data...
[PonderClientCache]: Successfully loaded data
[PonderClientCache]: loading data...
[PonderClientCache]: Successfully loaded data
[PonderClientCache]: loading data...
[PonderClientCache]: Successfully loaded data
[PonderClientCache]: loading data...
[PonderClientCache]: Successfully loaded data
[PonderClientCache]: loading data...
[PonderClientCache]: Successfully loaded data
^C%                                             

Notes for Reviewer (Optional)


Pre-Review Checklist (Blocking)

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

tk-o added 11 commits February 19, 2026 16:09
Allow setting "context" object for cache. This object can be accessed from function that fetches data into cache. The "context" object may include values, such as, API clients, configuration, etc.
This will not be needed as we will use SWRCache with proactive initialization to achieve similar result in a much cleaner way.
For data coherence, it is optimal to fetch from `/status` and `/metrics` endpoints of Ponder app at the same time. Therefore, we will use a single SWRCache to load Ponder Status and Metrics.
With this cachange, the LocalPonderClient reads external data only from SWR caches. This allows managing possible network request failuers easily within each individual cache implementation.
Copilot AI review requested due to automatic review settings February 19, 2026 16:23
@vercel
Copy link
Contributor

vercel bot commented Feb 19, 2026

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

Project Deployment Actions Updated (UTC)
admin.ensnode.io Ready Ready Preview, Comment Feb 19, 2026 4:24pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
ensnode.io Skipped Skipped Feb 19, 2026 4:24pm
ensrainbow.io Skipped Skipped Feb 19, 2026 4:24pm

@changeset-bot
Copy link

changeset-bot bot commented Feb 19, 2026

⚠️ No Changeset found

Latest commit: 0999394

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors ENSIndexer’s LocalPonderClient initialization to rely on SWR caches (with proactive revalidation) and extends the shared SWRCache utility to support a context object for cache loaders.

Changes:

  • Extend SWRCache to accept an optional context parameter in fn() and add a setContext() API.
  • Refactor LocalPonderClient to use new SWR-backed caches for Ponder indexing metrics/status (dynamic) and derived immutable chain metadata.
  • Update the Ponder API handler to use a singleton LocalPonderClient instance (no per-request initialization).

Reviewed changes

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

Show a summary per file
File Description
packages/ensnode-sdk/src/shared/cache/swr-cache.ts Adds context support and proactive revalidation stop API to the shared SWR cache implementation.
packages/ensnode-sdk/src/shared/cache/swr-cache.test.ts Updates tests for the new (cachedResult, context) callback signature and adds context coverage.
apps/ensindexer/src/lib/ponder-api-client.ts Switches singleton getter to synchronous construction and injects SWR caches into LocalPonderClient.
apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts Reworks LocalPonderClient to extend PonderClient and source metadata via SWR caches.
apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-immutable.ts Introduces builder for immutable chain indexing metadata.
apps/ensindexer/ponder/src/api/lib/chains-indexing-metadata-dynamic.ts Introduces builder for dynamic chain indexing metadata from metrics/status.
apps/ensindexer/ponder/src/api/lib/cache/ponder-client.cache.ts Adds SWR cache for Ponder metrics/status with proactive revalidation.
apps/ensindexer/ponder/src/api/lib/cache/chains-indexing-metadata-immutable.cache.ts Adds SWR cache for immutable chain metadata and stops revalidation after success.
apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts Uses a module-level singleton LocalPonderClient for request handling.

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

indexingStatus: chainIndexingStatus,
} satisfies ChainIndexingMetadataDynamic;

// Cache the dynamic metadata for this chain ID
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

Comment says "Cache the dynamic metadata for this chain ID", but this function is only building and returning a Map (no caching side effects). Please adjust the comment to avoid implying additional behavior.

Suggested change
// Cache the dynamic metadata for this chain ID
// Store the dynamic metadata for this chain ID in the map

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +53
this.#chainsIndexingMetadataImmutableCache = chainsIndexingMetadataImmutableCache;
this.#chainsIndexingMetadataImmutableCache.setContext({
indexedChainIds: this.indexedChainIds,
chainsConfigBlockrange: this.chainsConfigBlockrange,
publicClients: this.publicClients,
ponderClientCache: this.#ponderClientCache,
});
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The comment/typing suggests these are "Lazily initialized" (#chainsConfigBlockrange?, #publicClients?), but the constructor immediately forces initialization by reading this.chainsConfigBlockrange and this.publicClients to set the cache context. Either update the comment/field optionality to reflect eager initialization, or defer computing these values until needed (e.g., pass suppliers/getters in the cache context).

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +53
* @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.
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The JSDoc for buildChainsIndexingMetadataImmutable lists a ponderClient param (and describes fetching metrics/status), but the function signature now takes ponderIndexingMetrics directly and doesn’t accept a client. Please update the @param docs to match the current parameters/behavior.

Suggested change
* @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.
* @param indexedChainIds Set of chain IDs that are being indexed.
* @param chainsConfigBlockrange Map of chain ID to its configured blockrange.
* @param publicClients Map of chain ID to its corresponding public client,
* used to fetch block references for the chain's blockrange.
* @param ponderIndexingMetrics Indexing metrics for all chains, used to derive
* historical/backfill ranges for each indexed chain.

Copilot uses AI. Check for mistakes.
Comment on lines +207 to +209
public destroy(): void {
this.stopProactiveRevalidation();
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

destroy() calls stopProactiveRevalidation() even though it returns a Promise (because the method is async). This creates a floating Promise and will likely trip Biome's suspicious/noFloatingPromises, and it also makes destroy() appear synchronous while doing async work. Consider making stopProactiveRevalidation() synchronous (no async, return void) since it only clears an interval, or make destroy() async and properly await/handle the Promise.

Copilot uses AI. Check for mistakes.
Comment on lines 17 to 18
* @throws Error if the client fails to initialize after
* the specified number of retries.
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The JSDoc still mentions retries ("fails to initialize after the specified number of retries"), but the retry logic was removed and this function no longer performs async initialization. Please update/remove this @throws text so it matches the current behavior.

Suggested change
* @throws Error if the client fails to initialize after
* the specified number of retries.
* @throws Error if the client cannot be instantiated.

Copilot uses AI. Check for mistakes.
},
ttl: Number.POSITIVE_INFINITY,
proactiveRevalidationInterval: 5, // indexing metrics and status can change frequently, so proactively revalidate every 5 seconds to ensure data is fresh
proactivelyInitialize: true,
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

This cache is proactivelyInitialize: true but fn hard-requires context to be set. Because the cache instance is created at module load time, the first proactive revalidation will run before LocalPonderClient calls setContext(), caching an Error until the next interval tick. To avoid a guaranteed initial failure (and up-to-5s delay after context is set), consider setting proactivelyInitialize: false and triggering an initial read() after setContext(), and/or configuring a finite errorTtl so reads can also schedule retries.

Suggested change
proactivelyInitialize: true,
errorTtl: 5, // on errors, allow retries after 5 seconds instead of caching failures indefinitely
proactivelyInitialize: false,

Copilot uses AI. Check for mistakes.

// Stop the proactive revalidation of this cache since we have
// successfully loaded the data and initialized the client state.
chainsIndexingMetadataImmutableCache.destroy();
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

After successfully loading immutable data, this calls chainsIndexingMetadataImmutableCache.destroy(). Now that SWRCache has stopProactiveRevalidation(), using that method here would better express the intent (stop the interval) without implying the cache is being torn down entirely.

Suggested change
chainsIndexingMetadataImmutableCache.destroy();
chainsIndexingMetadataImmutableCache.stopProactiveRevalidation();

Copilot uses AI. Check for mistakes.
// Cache the fixed metadata for this chain ID
chainsIndexingMetadataFixed.set(chainId, metadataFixed);
this.#chainsIndexingMetadataImmutableCache = chainsIndexingMetadataImmutableCache;
this.#chainsIndexingMetadataImmutableCache.setContext({
Copy link
Contributor

@vercel vercel bot Feb 19, 2026

Choose a reason for hiding this comment

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

Cache initialization errors are cached indefinitely because errorTtl is not configured, causing failures to never be retried

Fix on Vercel

Copy link
Contributor Author

@tk-o tk-o left a comment

Choose a reason for hiding this comment

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

Self-review completed.

* such as API clients or configuration.
*/
fn: (cachedResult?: CachedResult<ValueType>) => Promise<ValueType>;
fn: (cachedResult?: CachedResult<ValueType>, context?: ContextType) => Promise<ValueType>;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In my opinion, this has been a missing feature. Without that, all dependencies must be linked directly in the file containing the SWR cache definition. However, with the context variable being available for the fn function, we can model data loading functionality in a flexible way.

* such as when caching immutable data.
*/
public destroy(): void {
public async stopProactiveRevalidation(): Promise<void> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding this function as it reads less scary than .destroy(). This function is handy to stop proactive validation for a given cache programatically.

// 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();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There should be need to call getLocalPonderClient in order to initialize the singleton client instance. We'll manage that initialization via SWR caches which LocalPonderClient will use directly.

@@ -0,0 +1,57 @@
import { SWRCache } from "@ensnode/ensnode-sdk";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I initially thought that creating separate cache for Ponder Indexing Metrics and Ponder Indexing Status was a good idea. Then, I figured that fetching those two data objects is optimal when done at the same time. The reason is that both data points will describe states that were captured at the same moment of indexing progress.

@@ -0,0 +1,55 @@
import { SWRCache } from "@ensnode/ensnode-sdk";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I initially thought that creating separate cache for Ponder Indexing Metrics and Ponder Indexing Status was a good idea. Then, I figured that fetching those two data objects is optimal when done at the same time. The reason is that both data points will describe states that were captured at the same moment of indexing progress.

@@ -0,0 +1,80 @@
import type { PublicClient } from "viem";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need to fetch ChainIndexingMetadataImmutable object successfully just once, and then keep using it in the application use cases.

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/indexing-status-builder-3-with-cache

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

❤️ Share

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments