From d21549a368c54f143966f92293ef3d23f4859d8a Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 4 Nov 2025 18:19:43 +0000 Subject: [PATCH 01/20] enable cached snapshots --- .../src/hooks/useIndexingStatus.ts | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index c56f3d692..de8b7dd13 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -1,6 +1,13 @@ import { useQuery } from "@tanstack/react-query"; +import { useRef } from "react"; -import type { IndexingStatusRequest, IndexingStatusResponse } from "@ensnode/ensnode-sdk"; +import { + type CrossChainIndexingStatusSnapshotOmnichain, + createRealtimeIndexingStatusProjection, + type IndexingStatusRequest, + type IndexingStatusResponse, + IndexingStatusResponseCodes, +} from "@ensnode/ensnode-sdk"; import type { QueryParameter, WithSDKConfigParameter } from "../types"; import { createIndexingStatusQueryOptions } from "../utils/query"; @@ -18,6 +25,8 @@ export function useIndexingStatus( const queryOptions = createIndexingStatusQueryOptions(_config); + const cachedSnapshotRef = useRef(null); + const options = { ...queryOptions, refetchInterval: 10 * 1000, // 10 seconds - indexing status changes frequently @@ -25,5 +34,38 @@ export function useIndexingStatus( enabled: query.enabled ?? queryOptions.enabled, }; - return useQuery(options); + const queryResult = useQuery(options); + + if (queryResult.data && queryResult.data.responseCode === IndexingStatusResponseCodes.Ok) { + cachedSnapshotRef.current = queryResult.data.realtimeProjection.snapshot; + } + + // If we have a cached snapshot and either: + // 1. The query resulted in a network/fetch error, or + // 2. The API returned an Error responseCode + // Then generate a fresh realtime projection from the cached snapshot + // instead of exposing the error to the UI. + const shouldUseCachedSnapshot = + cachedSnapshotRef.current && + (queryResult.isError || queryResult.data?.responseCode === IndexingStatusResponseCodes.Error); + + if (shouldUseCachedSnapshot) { + const now = Math.floor(Date.now() / 1000); + const syntheticRealtimeProjection = createRealtimeIndexingStatusProjection( + cachedSnapshotRef.current!, + now, + ); + + return { + ...queryResult, + data: { + responseCode: IndexingStatusResponseCodes.Ok, + realtimeProjection: syntheticRealtimeProjection, + } as IndexingStatusResponse, + isError: false, + error: null, + }; + } + + return queryResult; } From 7e55416a9c859d65ade1134fa3174f407cc29341 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Wed, 5 Nov 2025 15:31:11 +0000 Subject: [PATCH 02/20] update fallback error --- .../src/components/recent-registrations/registrations.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/ensadmin/src/components/recent-registrations/registrations.tsx b/apps/ensadmin/src/components/recent-registrations/registrations.tsx index dbc5b366b..7ff77df70 100644 --- a/apps/ensadmin/src/components/recent-registrations/registrations.tsx +++ b/apps/ensadmin/src/components/recent-registrations/registrations.tsx @@ -14,7 +14,12 @@ export function Registrations() { if (status === "error") { return ( - + ); } From 281a0441a9f99b40f70f0448e957ed71175430d3 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Wed, 5 Nov 2025 15:53:04 +0000 Subject: [PATCH 03/20] use fresh projection --- .../src/hooks/useIndexingStatus.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index de8b7dd13..57079aefb 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -36,23 +36,18 @@ export function useIndexingStatus( const queryResult = useQuery(options); + // Update cached snapshot whenever we get a successful response if (queryResult.data && queryResult.data.responseCode === IndexingStatusResponseCodes.Ok) { cachedSnapshotRef.current = queryResult.data.realtimeProjection.snapshot; } - // If we have a cached snapshot and either: - // 1. The query resulted in a network/fetch error, or - // 2. The API returned an Error responseCode - // Then generate a fresh realtime projection from the cached snapshot - // instead of exposing the error to the UI. - const shouldUseCachedSnapshot = - cachedSnapshotRef.current && - (queryResult.isError || queryResult.data?.responseCode === IndexingStatusResponseCodes.Error); - - if (shouldUseCachedSnapshot) { + // If we have a cached snapshot, always build a fresh projection from it + // using the current time. This allows the client to rebuild projections + // as frequently as needed without waiting for API responses. + if (cachedSnapshotRef.current) { const now = Math.floor(Date.now() / 1000); const syntheticRealtimeProjection = createRealtimeIndexingStatusProjection( - cachedSnapshotRef.current!, + cachedSnapshotRef.current, now, ); From add646cfc7002aa59cd5f3335063d3e1c09dfee3 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Wed, 5 Nov 2025 18:29:04 +0000 Subject: [PATCH 04/20] use date-fns --- packages/ensnode-react/package.json | 3 ++- packages/ensnode-react/src/hooks/useIndexingStatus.ts | 5 +++-- pnpm-lock.yaml | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/ensnode-react/package.json b/packages/ensnode-react/package.json index 99195e3a1..0bd7d019c 100644 --- a/packages/ensnode-react/package.json +++ b/packages/ensnode-react/package.json @@ -57,6 +57,7 @@ "vitest": "catalog:" }, "dependencies": { - "@ensnode/ensnode-sdk": "workspace:*" + "@ensnode/ensnode-sdk": "workspace:*", + "date-fns": "catalog:" } } diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index 57079aefb..644d4c1b0 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query"; +import { getUnixTime } from "date-fns"; import { useRef } from "react"; import { @@ -45,10 +46,10 @@ export function useIndexingStatus( // using the current time. This allows the client to rebuild projections // as frequently as needed without waiting for API responses. if (cachedSnapshotRef.current) { - const now = Math.floor(Date.now() / 1000); + const projectedAt = getUnixTime(new Date()); const syntheticRealtimeProjection = createRealtimeIndexingStatusProjection( cachedSnapshotRef.current, - now, + projectedAt, ); return { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c804a931..22f5c73b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -659,6 +659,9 @@ importers: '@ensnode/ensnode-sdk': specifier: workspace:* version: link:../ensnode-sdk + date-fns: + specifier: 'catalog:' + version: 4.1.0 react: specifier: ^18.0.0 || ^19.0.0 version: 19.2.0 From a398f1c99a0994b9f2dece4e89fa63495b8bcae7 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Thu, 6 Nov 2025 21:52:41 +0000 Subject: [PATCH 05/20] improve comments --- .../src/hooks/useIndexingStatus.ts | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index 644d4c1b0..7b7bfa13d 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -37,17 +37,32 @@ export function useIndexingStatus( const queryResult = useQuery(options); - // Update cached snapshot whenever we get a successful response + // Store the latest snapshot in memory whenever we get a successful response. + // Each incremental snapshot will have >= indexing progress than previous snapshots. if (queryResult.data && queryResult.data.responseCode === IndexingStatusResponseCodes.Ok) { cachedSnapshotRef.current = queryResult.data.realtimeProjection.snapshot; } - // If we have a cached snapshot, always build a fresh projection from it - // using the current time. This allows the client to rebuild projections - // as frequently as needed without waiting for API responses. + // If we have a cached snapshot, always build a fresh projection from it. + // + // Clients often need frequently updated worst-case distance for their logic, + // but calling the API every second would be inefficient. Instead, we fetch an + // updated snapshot every X seconds (here X=10) and keep it in memory. + // + // From that cached snapshot, we can continuously generate new projections — + // entirely in memory — that stay up to date as time moves forward. Each + // projection recalculates worst-case distance based on: + // The current time + // The snapshot's absolute timestamps and recorded indexing progress + // + // This works reliably because indexing progress is always non-decreasing over + // time (never goes backward). Clients can safely assume that a snapshot from a + // few seconds ago is still valid for building new projections. Since snapshots + // use absolute timestamps, we can compute accurate projections and worst-case + // distances without additional API calls. if (cachedSnapshotRef.current) { const projectedAt = getUnixTime(new Date()); - const syntheticRealtimeProjection = createRealtimeIndexingStatusProjection( + const realtimeProjection = createRealtimeIndexingStatusProjection( cachedSnapshotRef.current, projectedAt, ); @@ -56,7 +71,7 @@ export function useIndexingStatus( ...queryResult, data: { responseCode: IndexingStatusResponseCodes.Ok, - realtimeProjection: syntheticRealtimeProjection, + realtimeProjection, } as IndexingStatusResponse, isError: false, error: null, From 010e11fdf37915ccb3f8ca92a73a944461bc069f Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Thu, 6 Nov 2025 21:57:56 +0000 Subject: [PATCH 06/20] move comment to jsdoc --- .../src/hooks/useIndexingStatus.ts | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index 7b7bfa13d..e443ff371 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -18,6 +18,30 @@ interface UseIndexingStatusParameters extends IndexingStatusRequest, QueryParameter {} +/** + * Hook for fetching and tracking indexing status with client-side projection updates. + * + * Clients often need frequently updated worst-case distance for their logic, + * but calling the API every second would be inefficient. Instead, we fetch an + * updated snapshot every 10 seconds and keep it in memory. + * + * From that cached snapshot, we continuously generate new projections — + * entirely in memory — that stay up to date as time moves forward. Each + * projection recalculates worst-case distance based on: + * • The current time + * • The snapshot's absolute timestamps and recorded indexing progress + * + * This works reliably because indexing progress is always non-decreasing over + * time (never goes backward). Clients can safely assume that a snapshot from a + * few seconds ago is still valid for building new projections. Since snapshots + * use absolute timestamps, we can compute accurate projections and worst-case + * distances without additional API calls. + * + * @param parameters - Configuration options + * @param parameters.config - ENSNode SDK configuration (optional, uses context if not provided) + * @param parameters.query - TanStack Query options for customizing query behavior (refetchInterval, enabled, etc.) + * @returns TanStack Query result containing indexing status data with real-time projections + */ export function useIndexingStatus( parameters: WithSDKConfigParameter & UseIndexingStatusParameters = {}, ) { @@ -43,23 +67,8 @@ export function useIndexingStatus( cachedSnapshotRef.current = queryResult.data.realtimeProjection.snapshot; } - // If we have a cached snapshot, always build a fresh projection from it. - // - // Clients often need frequently updated worst-case distance for their logic, - // but calling the API every second would be inefficient. Instead, we fetch an - // updated snapshot every X seconds (here X=10) and keep it in memory. - // - // From that cached snapshot, we can continuously generate new projections — - // entirely in memory — that stay up to date as time moves forward. Each - // projection recalculates worst-case distance based on: - // The current time - // The snapshot's absolute timestamps and recorded indexing progress - // - // This works reliably because indexing progress is always non-decreasing over - // time (never goes backward). Clients can safely assume that a snapshot from a - // few seconds ago is still valid for building new projections. Since snapshots - // use absolute timestamps, we can compute accurate projections and worst-case - // distances without additional API calls. + // If we have a cached snapshot, always build a fresh projection from it + // using the current time. This happens on every render. if (cachedSnapshotRef.current) { const projectedAt = getUnixTime(new Date()); const realtimeProjection = createRealtimeIndexingStatusProjection( From 7d3367a5da26dd00dce1d9ce24cd9d81564e3f71 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Fri, 7 Nov 2025 09:04:04 +0000 Subject: [PATCH 07/20] docs(changeset): Enhance useIndexingStatus with in-memory snapshot caching and real-time projection updates for smoother, continuously refreshed indexing status between API fetches --- .changeset/spotty-meals-glow.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/spotty-meals-glow.md diff --git a/.changeset/spotty-meals-glow.md b/.changeset/spotty-meals-glow.md new file mode 100644 index 000000000..39fb18a50 --- /dev/null +++ b/.changeset/spotty-meals-glow.md @@ -0,0 +1,6 @@ +--- +"@ensnode/ensnode-react": minor +"ensadmin": minor +--- + +Enhance useIndexingStatus with in-memory snapshot caching and real-time projection updates for smoother, continuously refreshed indexing status between API fetches From c4265aade0be0bed0cb8efbbf4c2fd5d09b5ee0f Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 11 Nov 2025 13:47:46 +0000 Subject: [PATCH 08/20] Update packages/ensnode-react/src/hooks/useIndexingStatus.ts Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- packages/ensnode-react/src/hooks/useIndexingStatus.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index e443ff371..4183dc153 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -22,8 +22,8 @@ interface UseIndexingStatusParameters * Hook for fetching and tracking indexing status with client-side projection updates. * * Clients often need frequently updated worst-case distance for their logic, - * but calling the API every second would be inefficient. Instead, we fetch an - * updated snapshot every 10 seconds and keep it in memory. + * but calling the API every second would be inefficient. Instead, we fetch a + * snapshot and keep it in memory. We then asynchronously attempt to update it every 10 seconds. * * From that cached snapshot, we continuously generate new projections — * entirely in memory — that stay up to date as time moves forward. Each From 9d5b542a042b125de66e77d4428e05bbc2ab1dd1 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 11 Nov 2025 13:47:54 +0000 Subject: [PATCH 09/20] Update packages/ensnode-react/src/hooks/useIndexingStatus.ts Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- packages/ensnode-react/src/hooks/useIndexingStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index 4183dc153..242d2d9db 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -25,7 +25,7 @@ interface UseIndexingStatusParameters * but calling the API every second would be inefficient. Instead, we fetch a * snapshot and keep it in memory. We then asynchronously attempt to update it every 10 seconds. * - * From that cached snapshot, we continuously generate new projections — + * From the most recently cached snapshot, this hook instantly generates new projections — * entirely in memory — that stay up to date as time moves forward. Each * projection recalculates worst-case distance based on: * • The current time From 83f20a665ce003c10cc22a3bb096585fd7bfb81e Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 11 Nov 2025 13:48:25 +0000 Subject: [PATCH 10/20] Update packages/ensnode-react/src/hooks/useIndexingStatus.ts Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- packages/ensnode-react/src/hooks/useIndexingStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index 242d2d9db..5815dddea 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -27,7 +27,7 @@ interface UseIndexingStatusParameters * * From the most recently cached snapshot, this hook instantly generates new projections — * entirely in memory — that stay up to date as time moves forward. Each - * projection recalculates worst-case distance based on: + * projection provides a recalculation of worst-case distance based on: * • The current time * • The snapshot's absolute timestamps and recorded indexing progress * From 523d475f4169f4d506fee791675b3e27cb6099cf Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 11 Nov 2025 13:48:42 +0000 Subject: [PATCH 11/20] Update packages/ensnode-react/src/hooks/useIndexingStatus.ts Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- packages/ensnode-react/src/hooks/useIndexingStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index 5815dddea..36620f2ef 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -29,7 +29,7 @@ interface UseIndexingStatusParameters * entirely in memory — that stay up to date as time moves forward. Each * projection provides a recalculation of worst-case distance based on: * • The current time - * • The snapshot's absolute timestamps and recorded indexing progress + * • The snapshot's absolute timestamps of recorded indexing progress * * This works reliably because indexing progress is always non-decreasing over * time (never goes backward). Clients can safely assume that a snapshot from a From cce10374ada236ae89658c722aab502af765b959 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 11 Nov 2025 13:49:04 +0000 Subject: [PATCH 12/20] Update packages/ensnode-react/src/hooks/useIndexingStatus.ts Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- packages/ensnode-react/src/hooks/useIndexingStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index 36620f2ef..abe886c9f 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -28,7 +28,7 @@ interface UseIndexingStatusParameters * From the most recently cached snapshot, this hook instantly generates new projections — * entirely in memory — that stay up to date as time moves forward. Each * projection provides a recalculation of worst-case distance based on: - * • The current time + * • The current time (when the projection was generated) * • The snapshot's absolute timestamps of recorded indexing progress * * This works reliably because indexing progress is always non-decreasing over From 21707a1bfaf8074c832b9dfc813d62d6ae5c1f9e Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 11 Nov 2025 13:49:13 +0000 Subject: [PATCH 13/20] Update packages/ensnode-react/src/hooks/useIndexingStatus.ts Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- packages/ensnode-react/src/hooks/useIndexingStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index abe886c9f..328adb826 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -40,7 +40,7 @@ interface UseIndexingStatusParameters * @param parameters - Configuration options * @param parameters.config - ENSNode SDK configuration (optional, uses context if not provided) * @param parameters.query - TanStack Query options for customizing query behavior (refetchInterval, enabled, etc.) - * @returns TanStack Query result containing indexing status data with real-time projections + * @returns TanStack Query result containing a new indexing status projection based on the current time */ export function useIndexingStatus( parameters: WithSDKConfigParameter & UseIndexingStatusParameters = {}, From f90d6ab8a1c5664735ec1b78a791feecfde6e19f Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 11 Nov 2025 13:49:24 +0000 Subject: [PATCH 14/20] Update packages/ensnode-react/src/hooks/useIndexingStatus.ts Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- packages/ensnode-react/src/hooks/useIndexingStatus.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index 328adb826..0b1c726a6 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -31,8 +31,8 @@ interface UseIndexingStatusParameters * • The current time (when the projection was generated) * • The snapshot's absolute timestamps of recorded indexing progress * - * This works reliably because indexing progress is always non-decreasing over - * time (never goes backward). Clients can safely assume that a snapshot from a + * This works reliably because indexing progress is virtually always non-decreasing over + * time (virtually never goes backward). Clients can safely assume that a snapshot from a * few seconds ago is still valid for building new projections. Since snapshots * use absolute timestamps, we can compute accurate projections and worst-case * distances without additional API calls. From c2c8831d84557517c0e17943cecf78f73d0312ad Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 11 Nov 2025 13:49:33 +0000 Subject: [PATCH 15/20] Update packages/ensnode-react/src/hooks/useIndexingStatus.ts Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- packages/ensnode-react/src/hooks/useIndexingStatus.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index 0b1c726a6..39ba11091 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -34,8 +34,7 @@ interface UseIndexingStatusParameters * This works reliably because indexing progress is virtually always non-decreasing over * time (virtually never goes backward). Clients can safely assume that a snapshot from a * few seconds ago is still valid for building new projections. Since snapshots - * use absolute timestamps, we can compute accurate projections and worst-case - * distances without additional API calls. + * exclusively contain absolute timestamps, we can reuse a snapshot across time to continuously compute updated worst-case projections without additional API calls. * * @param parameters - Configuration options * @param parameters.config - ENSNode SDK configuration (optional, uses context if not provided) From b10ef048913ab27f2c807947e4e72f0fa33b15b2 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 11 Nov 2025 20:03:56 +0000 Subject: [PATCH 16/20] Update packages/ensnode-react/src/hooks/useIndexingStatus.ts Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- packages/ensnode-react/src/hooks/useIndexingStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index 39ba11091..15d344370 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -26,7 +26,7 @@ interface UseIndexingStatusParameters * snapshot and keep it in memory. We then asynchronously attempt to update it every 10 seconds. * * From the most recently cached snapshot, this hook instantly generates new projections — - * entirely in memory — that stay up to date as time moves forward. Each + * entirely in memory. Each * projection provides a recalculation of worst-case distance based on: * • The current time (when the projection was generated) * • The snapshot's absolute timestamps of recorded indexing progress From 55a6a87281dd65d905b354748a1db9491d3e62b6 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Thu, 13 Nov 2025 16:19:13 +0000 Subject: [PATCH 17/20] projection sync --- .../indexing-status/indexing-stats.tsx | 38 +++++++---- .../indexing-status/projection-info.tsx | 57 ++++++++++++++++ .../src/hooks/useIndexingStatus.ts | 57 +++++++++------- .../ensnode-react/src/utils/projectionSync.ts | 65 +++++++++++++++++++ 4 files changed, 181 insertions(+), 36 deletions(-) create mode 100644 apps/ensadmin/src/components/indexing-status/projection-info.tsx create mode 100644 packages/ensnode-react/src/utils/projectionSync.ts diff --git a/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx b/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx index 63a487f53..9e76d8e16 100644 --- a/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx +++ b/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx @@ -33,6 +33,7 @@ import { cn } from "@/lib/utils"; import { BackfillStatus } from "./backfill-status"; import { BlockStats } from "./block-refs"; import { IndexingStatusLoading } from "./indexing-status-loading"; +import { ProjectionInfo } from "./projection-info"; interface IndexingStatsForOmnichainStatusSnapshotProps< OmnichainIndexingStatusSnapshotType extends @@ -323,22 +324,32 @@ export function IndexingStatsForSnapshotFollowing({ */ export function IndexingStatsShell({ omnichainStatus, + realtimeProjection, children, -}: PropsWithChildren<{ omnichainStatus?: OmnichainIndexingStatusId }>) { +}: PropsWithChildren<{ + omnichainStatus?: OmnichainIndexingStatusId; + realtimeProjection?: RealtimeIndexingStatusProjection; +}>) { return ( - - Indexing Status + +
+ Indexing Status + + {omnichainStatus && ( + + {formatOmnichainIndexingStatus(omnichainStatus)} + + )} +
- {omnichainStatus && ( - - {formatOmnichainIndexingStatus(omnichainStatus)} - - )} + {realtimeProjection && }
@@ -422,7 +433,10 @@ export function IndexingStatsForRealtimeStatusProjection({
{maybeIndexingTimeline} - + {indexingStats}
diff --git a/apps/ensadmin/src/components/indexing-status/projection-info.tsx b/apps/ensadmin/src/components/indexing-status/projection-info.tsx new file mode 100644 index 000000000..8acc9b2f0 --- /dev/null +++ b/apps/ensadmin/src/components/indexing-status/projection-info.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { InfoIcon } from "lucide-react"; + +import type { RealtimeIndexingStatusProjection } from "@ensnode/ensnode-sdk"; + +import { RelativeTime } from "@/components/datetime-utils"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; + +interface ProjectionInfoProps { + realtimeProjection: RealtimeIndexingStatusProjection; +} + +/** + * Displays metadata about the current indexing status projection in a tooltip. + * Shows when the projection was generated, when the snapshot was taken, and worst-case distance. + */ +export function ProjectionInfo({ realtimeProjection }: ProjectionInfoProps) { + const { projectedAt, snapshot, worstCaseDistance } = realtimeProjection; + const { snapshotTime } = snapshot; + + return ( + + + + + +
+
+
+ Worst-Case Distance* +
+
+ {worstCaseDistance !== null ? `${worstCaseDistance} seconds` : "N/A"} +
+
+ +
+ * as of real-time projection generated{" "} + from indexing + status snapshot captured{" "} + . +
+
+
+
+ ); +} diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index 15d344370..354570481 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -1,9 +1,7 @@ import { useQuery } from "@tanstack/react-query"; -import { getUnixTime } from "date-fns"; -import { useRef } from "react"; +import { useMemo, useSyncExternalStore } from "react"; import { - type CrossChainIndexingStatusSnapshotOmnichain, createRealtimeIndexingStatusProjection, type IndexingStatusRequest, type IndexingStatusResponse, @@ -11,6 +9,7 @@ import { } from "@ensnode/ensnode-sdk"; import type { QueryParameter, WithSDKConfigParameter } from "../types"; +import { projectionSync } from "../utils/projectionSync"; import { createIndexingStatusQueryOptions } from "../utils/query"; import { useENSNodeSDKConfig } from "./useENSNodeSDKConfig"; @@ -26,8 +25,7 @@ interface UseIndexingStatusParameters * snapshot and keep it in memory. We then asynchronously attempt to update it every 10 seconds. * * From the most recently cached snapshot, this hook instantly generates new projections — - * entirely in memory. Each - * projection provides a recalculation of worst-case distance based on: + * entirely in memory. Each projection provides a recalculation of worst-case distance based on: * • The current time (when the projection was generated) * • The snapshot's absolute timestamps of recorded indexing progress * @@ -49,8 +47,6 @@ export function useIndexingStatus( const queryOptions = createIndexingStatusQueryOptions(_config); - const cachedSnapshotRef = useRef(null); - const options = { ...queryOptions, refetchInterval: 10 * 1000, // 10 seconds - indexing status changes frequently @@ -60,27 +56,40 @@ export function useIndexingStatus( const queryResult = useQuery(options); - // Store the latest snapshot in memory whenever we get a successful response. - // Each incremental snapshot will have >= indexing progress than previous snapshots. - if (queryResult.data && queryResult.data.responseCode === IndexingStatusResponseCodes.Ok) { - cachedSnapshotRef.current = queryResult.data.realtimeProjection.snapshot; - } + // Extract the current snapshot from the query result. + // This will be null until we get a successful response. + const currentSnapshot = + queryResult.data?.responseCode === IndexingStatusResponseCodes.Ok + ? queryResult.data.realtimeProjection.snapshot + : null; + + // Subscribe to synchronized timestamp updates (ticks every second). + // All components using this hook will receive the same timestamp value, + // ensuring consistent projections across the entire UI. + const projectedAt = useSyncExternalStore( + projectionSync.subscribe.bind(projectionSync), + projectionSync.getTimestamp.bind(projectionSync), + projectionSync.getTimestamp.bind(projectionSync), // Server-side snapshot + ); - // If we have a cached snapshot, always build a fresh projection from it - // using the current time. This happens on every render. - if (cachedSnapshotRef.current) { - const projectedAt = getUnixTime(new Date()); - const realtimeProjection = createRealtimeIndexingStatusProjection( - cachedSnapshotRef.current, - projectedAt, - ); + // Generate projection from cached snapshot using the synchronized timestamp. + // useMemo ensures we only create a new projection object when values actually change, + // maintaining referential equality for unchanged data (prevents unnecessary re-renders). + const projectedData = useMemo(() => { + if (!currentSnapshot) return null; + + const realtimeProjection = createRealtimeIndexingStatusProjection(currentSnapshot, projectedAt); + + return { + responseCode: IndexingStatusResponseCodes.Ok, + realtimeProjection, + } as IndexingStatusResponse; + }, [currentSnapshot, projectedAt]); + if (projectedData) { return { ...queryResult, - data: { - responseCode: IndexingStatusResponseCodes.Ok, - realtimeProjection, - } as IndexingStatusResponse, + data: projectedData, isError: false, error: null, }; diff --git a/packages/ensnode-react/src/utils/projectionSync.ts b/packages/ensnode-react/src/utils/projectionSync.ts new file mode 100644 index 000000000..b0bf34c55 --- /dev/null +++ b/packages/ensnode-react/src/utils/projectionSync.ts @@ -0,0 +1,65 @@ +import { getUnixTime } from "date-fns"; + +/** + * Sync mechanism to ensure all components + * using useIndexingStatus share the same projection timestamp. + * + * This is a minimal store that only manages: + * - A synchronized "tick" (updated every second) + * - Notifying subscribers when the tick changes + * + * React Query handles all the actual data fetching and caching. + * This store just ensures all components project from the same moment in time. + */ +class ProjectionSync { + private currentTimestamp: number = getUnixTime(new Date()); + private intervalId: ReturnType | null = null; + private listeners = new Set<() => void>(); + private subscriberCount = 0; + + getTimestamp(): number { + return this.currentTimestamp; + } + + subscribe(listener: () => void): () => void { + this.listeners.add(listener); + this.subscriberCount++; + + if (this.subscriberCount === 1) { + this.start(); + } + + return () => { + this.listeners.delete(listener); + this.subscriberCount--; + + if (this.subscriberCount === 0) { + this.stop(); + } + }; + } + + private start(): void { + if (this.intervalId !== null) return; + + this.intervalId = setInterval(() => { + this.currentTimestamp = getUnixTime(new Date()); + this.notifyListeners(); + }, 1000); + } + + private stop(): void { + if (this.intervalId !== null) { + clearInterval(this.intervalId); + this.intervalId = null; + } + } + + private notifyListeners(): void { + this.listeners.forEach((listener) => { + listener(); + }); + } +} + +export const projectionSync = new ProjectionSync(); From 467d1594b1078abe07e3ec597ba88680d11a1275 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Thu, 13 Nov 2025 20:50:12 +0000 Subject: [PATCH 18/20] remove project sync idea --- packages/ensnode-react/src/hooks/index.ts | 1 + .../src/hooks/useIndexingStatus.ts | 39 ++++++----- packages/ensnode-react/src/hooks/useNow.ts | 31 +++++++++ .../ensnode-react/src/utils/projectionSync.ts | 65 ------------------- packages/ensnode-react/src/utils/query.ts | 13 +++- 5 files changed, 67 insertions(+), 82 deletions(-) create mode 100644 packages/ensnode-react/src/hooks/useNow.ts delete mode 100644 packages/ensnode-react/src/utils/projectionSync.ts diff --git a/packages/ensnode-react/src/hooks/index.ts b/packages/ensnode-react/src/hooks/index.ts index 0d5997070..df506b3d4 100644 --- a/packages/ensnode-react/src/hooks/index.ts +++ b/packages/ensnode-react/src/hooks/index.ts @@ -1,6 +1,7 @@ export * from "./useENSNodeConfig"; export * from "./useENSNodeSDKConfig"; export * from "./useIndexingStatus"; +export * from "./useNow"; export * from "./usePrimaryName"; export * from "./usePrimaryNames"; export * from "./useRecords"; diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index 354570481..328be35db 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -1,5 +1,5 @@ -import { useQuery } from "@tanstack/react-query"; -import { useMemo, useSyncExternalStore } from "react"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { useMemo } from "react"; import { createRealtimeIndexingStatusProjection, @@ -9,9 +9,9 @@ import { } from "@ensnode/ensnode-sdk"; import type { QueryParameter, WithSDKConfigParameter } from "../types"; -import { projectionSync } from "../utils/projectionSync"; import { createIndexingStatusQueryOptions } from "../utils/query"; import { useENSNodeSDKConfig } from "./useENSNodeSDKConfig"; +import { useNow } from "./useNow"; interface UseIndexingStatusParameters extends IndexingStatusRequest, @@ -34,6 +34,12 @@ interface UseIndexingStatusParameters * few seconds ago is still valid for building new projections. Since snapshots * exclusively contain absolute timestamps, we can reuse a snapshot across time to continuously compute updated worst-case projections without additional API calls. * + * **Error Handling:** + * When the indexing status API returns an error response, this hook continues to display + * the last successful snapshot while projecting forward in time. This provides a graceful + * degradation experience - the UI shows slightly stale but still useful data rather than + * breaking completely. + * * @param parameters - Configuration options * @param parameters.config - ENSNode SDK configuration (optional, uses context if not provided) * @param parameters.query - TanStack Query options for customizing query behavior (refetchInterval, enabled, etc.) @@ -50,6 +56,7 @@ export function useIndexingStatus( const options = { ...queryOptions, refetchInterval: 10 * 1000, // 10 seconds - indexing status changes frequently + placeholderData: keepPreviousData, // Keep showing previous data during refetch and on error ...query, enabled: query.enabled ?? queryOptions.enabled, }; @@ -57,20 +64,21 @@ export function useIndexingStatus( const queryResult = useQuery(options); // Extract the current snapshot from the query result. - // This will be null until we get a successful response. + // Thanks to placeholderData: keepPreviousData, this will continue showing + // the last successful snapshot even when subsequent fetches fail. + // Note: queryFn now throws on error responses, so data will only contain valid responses + + // debug const currentSnapshot = queryResult.data?.responseCode === IndexingStatusResponseCodes.Ok ? queryResult.data.realtimeProjection.snapshot : null; - // Subscribe to synchronized timestamp updates (ticks every second). - // All components using this hook will receive the same timestamp value, - // ensuring consistent projections across the entire UI. - const projectedAt = useSyncExternalStore( - projectionSync.subscribe.bind(projectionSync), - projectionSync.getTimestamp.bind(projectionSync), - projectionSync.getTimestamp.bind(projectionSync), // Server-side snapshot - ); + // / worstCaseDistance is measured in seconds + + // Get current timestamp that updates every second. + // Each component instance gets its own timestamp + const projectedAt = useNow(1000); // Generate projection from cached snapshot using the synchronized timestamp. // useMemo ensures we only create a new projection object when values actually change, @@ -81,17 +89,16 @@ export function useIndexingStatus( const realtimeProjection = createRealtimeIndexingStatusProjection(currentSnapshot, projectedAt); return { - responseCode: IndexingStatusResponseCodes.Ok, + // debugging + responseCode: "ok" as const, realtimeProjection, - } as IndexingStatusResponse; + } satisfies IndexingStatusResponse; }, [currentSnapshot, projectedAt]); if (projectedData) { return { ...queryResult, data: projectedData, - isError: false, - error: null, }; } diff --git a/packages/ensnode-react/src/hooks/useNow.ts b/packages/ensnode-react/src/hooks/useNow.ts new file mode 100644 index 000000000..2458c0f36 --- /dev/null +++ b/packages/ensnode-react/src/hooks/useNow.ts @@ -0,0 +1,31 @@ +import { getUnixTime } from "date-fns"; +import { useEffect, useState } from "react"; + +/** + * Hook that returns the current Unix timestamp, updated at a specified interval. + * + * @param refreshRate - How often to update the timestamp in milliseconds (default: 1000ms) + * @returns Current Unix timestamp that updates every refreshRate milliseconds + * + * @example + * ```tsx + * // Updates every second + * const now = useNow(1000); + * + * // Updates every 5 seconds + * const now = useNow(5000); + * ``` + */ +export function useNow(refreshRate = 1000): number { + const [now, setNow] = useState(() => getUnixTime(new Date())); + + useEffect(() => { + const interval = setInterval(() => { + setNow(getUnixTime(new Date())); + }, refreshRate); + + return () => clearInterval(interval); + }, [refreshRate]); + + return now; +} diff --git a/packages/ensnode-react/src/utils/projectionSync.ts b/packages/ensnode-react/src/utils/projectionSync.ts deleted file mode 100644 index b0bf34c55..000000000 --- a/packages/ensnode-react/src/utils/projectionSync.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { getUnixTime } from "date-fns"; - -/** - * Sync mechanism to ensure all components - * using useIndexingStatus share the same projection timestamp. - * - * This is a minimal store that only manages: - * - A synchronized "tick" (updated every second) - * - Notifying subscribers when the tick changes - * - * React Query handles all the actual data fetching and caching. - * This store just ensures all components project from the same moment in time. - */ -class ProjectionSync { - private currentTimestamp: number = getUnixTime(new Date()); - private intervalId: ReturnType | null = null; - private listeners = new Set<() => void>(); - private subscriberCount = 0; - - getTimestamp(): number { - return this.currentTimestamp; - } - - subscribe(listener: () => void): () => void { - this.listeners.add(listener); - this.subscriberCount++; - - if (this.subscriberCount === 1) { - this.start(); - } - - return () => { - this.listeners.delete(listener); - this.subscriberCount--; - - if (this.subscriberCount === 0) { - this.stop(); - } - }; - } - - private start(): void { - if (this.intervalId !== null) return; - - this.intervalId = setInterval(() => { - this.currentTimestamp = getUnixTime(new Date()); - this.notifyListeners(); - }, 1000); - } - - private stop(): void { - if (this.intervalId !== null) { - clearInterval(this.intervalId); - this.intervalId = null; - } - } - - private notifyListeners(): void { - this.listeners.forEach((listener) => { - listener(); - }); - } -} - -export const projectionSync = new ProjectionSync(); diff --git a/packages/ensnode-react/src/utils/query.ts b/packages/ensnode-react/src/utils/query.ts index 6e77b25df..49ae17954 100644 --- a/packages/ensnode-react/src/utils/query.ts +++ b/packages/ensnode-react/src/utils/query.ts @@ -4,6 +4,7 @@ import type { UndefinedInitialDataOptions } from "@tanstack/react-query"; import { ENSNodeClient, + IndexingStatusResponseCodes, type ResolvePrimaryNameRequest, type ResolvePrimaryNamesRequest, type ResolveRecordsRequest, @@ -124,6 +125,9 @@ export function createConfigQueryOptions(config: ENSNodeSDKConfig) { /** * Creates query options for ENSNode Indexing Status API + * + * Note: This query throws when the response indicates an error status, + * ensuring React Query treats it as a failed fetch and maintains previous data. */ export function createIndexingStatusQueryOptions(config: ENSNodeSDKConfig) { return { @@ -131,7 +135,14 @@ export function createIndexingStatusQueryOptions(config: ENSNodeSDKConfig) { queryKey: queryKeys.indexingStatus(config.client.url.href), queryFn: async () => { const client = new ENSNodeClient(config.client); - return client.indexingStatus(); + const response = await client.indexingStatus(); + + // debug: use placeholderData to keep showing the last valid data + if (response.responseCode === IndexingStatusResponseCodes.Error) { + throw new Error("Indexing status is currently unavailable"); + } + + return response; }, }; } From 393626e7f41ee9dd77996c7f01ef3d7cce2ac322 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Thu, 13 Nov 2025 20:58:54 +0000 Subject: [PATCH 19/20] update relative time when seconds --- .../src/components/datetime-utils/index.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/ensadmin/src/components/datetime-utils/index.tsx b/apps/ensadmin/src/components/datetime-utils/index.tsx index 851c9b290..224863181 100644 --- a/apps/ensadmin/src/components/datetime-utils/index.tsx +++ b/apps/ensadmin/src/components/datetime-utils/index.tsx @@ -92,9 +92,18 @@ export function RelativeTime({ const [relativeTime, setRelativeTime] = useState(""); useEffect(() => { - setRelativeTime( - formatRelativeTime(timestamp, enforcePast, includeSeconds, conciseFormatting, relativeTo), - ); + const updateTime = () => { + setRelativeTime( + formatRelativeTime(timestamp, enforcePast, includeSeconds, conciseFormatting, relativeTo), + ); + }; + + updateTime(); + + if (includeSeconds) { + const interval = setInterval(updateTime, 1000); + return () => clearInterval(interval); + } }, [timestamp, conciseFormatting, enforcePast, includeSeconds, relativeTo]); return ( From 13209c1d9d0a062a97fe719c62b6075d3a9bb16e Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Fri, 14 Nov 2025 16:43:54 +0000 Subject: [PATCH 20/20] Update apps/ensadmin/src/components/indexing-status/projection-info.tsx Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- .../ensadmin/src/components/indexing-status/projection-info.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ensadmin/src/components/indexing-status/projection-info.tsx b/apps/ensadmin/src/components/indexing-status/projection-info.tsx index 8acc9b2f0..1e135d65b 100644 --- a/apps/ensadmin/src/components/indexing-status/projection-info.tsx +++ b/apps/ensadmin/src/components/indexing-status/projection-info.tsx @@ -25,7 +25,7 @@ export function ProjectionInfo({ realtimeProjection }: ProjectionInfoProps) {