Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sweet-cities-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensadmin": minor
---

Updates `useIndexingStatusWithSwr` to always return current realtime indexing status projection.
5 changes: 5 additions & 0 deletions .changeset/wild-results-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensadmin": minor
---

Includes `ProjectionInfo` component on Indexing Status page.
30 changes: 21 additions & 9 deletions apps/ensadmin/src/app/mock/indexing-stats/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"use client";

import { useNow } from "@namehash/namehash-ui";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";

import {
type IndexingStatusResponse,
CrossChainIndexingStatusSnapshot,
createRealtimeIndexingStatusProjection,
IndexingStatusResponseCodes,
IndexingStatusResponseOk,
OmnichainIndexingStatusIds,
Comment thread
tk-o marked this conversation as resolved.
} from "@ensnode/ensnode-sdk";
Expand All @@ -13,10 +16,7 @@ import { IndexingStats } from "@/components/indexing-status/indexing-stats";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";

import {
indexingStatusResponseError,
indexingStatusResponseOkOmnichain,
} from "../indexing-status-api.mock";
import { indexingStatusResponseOkOmnichain } from "../indexing-status-api.mock";

type LoadingVariant = "Loading" | "Loading Error";
type ResponseOkVariant = keyof typeof indexingStatusResponseOkOmnichain;
Expand All @@ -37,7 +37,7 @@ let loadingTimeoutId: number;

async function fetchMockedIndexingStatus(
selectedVariant: Variant,
): Promise<IndexingStatusResponseOk> {
): Promise<CrossChainIndexingStatusSnapshot> {
// always try clearing loading timeout when performing a mocked fetch
// this way we get a fresh and very long request to observe the loading state
if (loadingTimeoutId) {
Expand All @@ -48,14 +48,19 @@ async function fetchMockedIndexingStatus(
case OmnichainIndexingStatusIds.Unstarted:
case OmnichainIndexingStatusIds.Backfill:
case OmnichainIndexingStatusIds.Following:
case OmnichainIndexingStatusIds.Completed:
return indexingStatusResponseOkOmnichain[selectedVariant] as IndexingStatusResponseOk;
case OmnichainIndexingStatusIds.Completed: {
const response = indexingStatusResponseOkOmnichain[
selectedVariant
] as IndexingStatusResponseOk;

return response.realtimeProjection.snapshot;
}
Comment thread
tk-o marked this conversation as resolved.
case "Error ResponseCode":
throw new Error(
"Received Indexing Status response with responseCode other than 'ok' which will not be cached.",
);
case "Loading":
return new Promise<IndexingStatusResponseOk>((_resolve, reject) => {
return new Promise<CrossChainIndexingStatusSnapshot>((_resolve, reject) => {
loadingTimeoutId = +setTimeout(reject, 5 * 60 * 1_000);
});
case "Loading Error":
Expand All @@ -67,10 +72,17 @@ export default function MockIndexingStatusPage() {
const [selectedVariant, setSelectedVariant] = useState<Variant>(
OmnichainIndexingStatusIds.Unstarted,
);
const now = useNow();

const mockedIndexingStatus = useQuery({
queryKey: ["mock", "useIndexingStatus", selectedVariant],
queryFn: () => fetchMockedIndexingStatus(selectedVariant),
select: (cachedSnapshot) => {
return {
responseCode: IndexingStatusResponseCodes.Ok,
realtimeProjection: createRealtimeIndexingStatusProjection(cachedSnapshot, now),
} satisfies IndexingStatusResponseOk;
},
retry: false, // allows loading error to be observed immediately
});
Comment thread
tk-o marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.

Expand Down
59 changes: 32 additions & 27 deletions apps/ensadmin/src/components/indexing-status/indexing-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
*/

import { ChainIcon, ChainName } from "@namehash/namehash-ui";
import type { PropsWithChildren, ReactElement } from "react";
import type { PropsWithChildren, ReactElement, ReactNode } from "react";

import type { useIndexingStatus } from "@ensnode/ensnode-react";
import {
ChainIndexingConfigTypeIds,
ChainIndexingStatusIds,
Expand All @@ -24,7 +23,6 @@ import {
sortChainStatusesByStartBlockAsc,
} from "@ensnode/ensnode-sdk";

import { useIndexingStatusWithSwr } from "@/components/indexing-status/use-indexing-status-with-swr";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { formatChainStatus, formatOmnichainIndexingStatus } from "@/lib/indexing-status";
Expand All @@ -33,6 +31,8 @@ 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";
import type { useIndexingStatusWithSwr } from "./use-indexing-status-with-swr";
Comment thread
tk-o marked this conversation as resolved.

interface IndexingStatsForOmnichainStatusSnapshotProps<
OmnichainIndexingStatusSnapshotType extends
Expand Down Expand Up @@ -316,30 +316,20 @@ export function IndexingStatsForSnapshotFollowing({
});
}

interface IndexingStatsShellProps extends PropsWithChildren {
title: ReactNode;
}

/**
* Indexing Stats Shell
*
* UI component for presenting indexing stats UI for specific overall status.
*/
export function IndexingStatsShell({
omnichainStatus,
children,
}: PropsWithChildren<{ omnichainStatus?: OmnichainIndexingStatusId }>) {
export function IndexingStatsShell({ title, children }: IndexingStatsShellProps) {
return (
<Card className="w-full flex flex-col gap-2">
<CardHeader>
<CardTitle className="flex gap-2 items-center">
<span>Indexing Status</span>

{omnichainStatus && (
<Badge
className={cn("uppercase text-xs leading-none")}
title={`Omnichain indexing status: ${formatOmnichainIndexingStatus(omnichainStatus)}`}
>
{formatOmnichainIndexingStatus(omnichainStatus)}
</Badge>
)}
</CardTitle>
<CardTitle className="flex gap-2 items-center">{title}</CardTitle>
</CardHeader>

<CardContent className="flex flex-col gap-8">
Expand Down Expand Up @@ -418,11 +408,30 @@ export function IndexingStatsForRealtimeStatusProjection({
break;
}

const { snapshot, worstCaseDistance } = realtimeProjection;
const { omnichainSnapshot, snapshotTime } = snapshot;
const omnichainStatus = omnichainSnapshot.omnichainStatus;

return (
<section className="flex flex-col gap-6">
{maybeIndexingTimeline}

<IndexingStatsShell omnichainStatus={omnichainStatusSnapshot.omnichainStatus}>
<IndexingStatsShell
title={
<>
<span>Indexing Status</span>

<ProjectionInfo snapshotTime={snapshotTime} worstCaseDistance={worstCaseDistance} />

<Badge
className={cn("uppercase text-xs leading-none")}
title={`Omnichain indexing status: ${formatOmnichainIndexingStatus(omnichainStatus)}`}
>
{formatOmnichainIndexingStatus(omnichainStatus)}
</Badge>
</>
}
>
Comment thread
tk-o marked this conversation as resolved.
{indexingStats}
</IndexingStatsShell>
</section>
Expand All @@ -439,7 +448,7 @@ export function IndexingStats(props: IndexingStatsProps) {

if (indexingStatusQuery.isError) {
return (
<IndexingStatsShell>
<IndexingStatsShell title="Indexing Status Unavailable">
<IndexingStatsForUnavailableSnapshot />
</IndexingStatsShell>
);
Expand All @@ -449,11 +458,7 @@ export function IndexingStats(props: IndexingStatsProps) {
return <IndexingStatusLoading />;
}

const indexingStatus = indexingStatusQuery.data;
const { realtimeProjection } = indexingStatusQuery.data;

return (
<IndexingStatsForRealtimeStatusProjection
realtimeProjection={indexingStatus.realtimeProjection}
/>
);
return <IndexingStatsForRealtimeStatusProjection realtimeProjection={realtimeProjection} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client";

import { RelativeTime, useNow } from "@namehash/namehash-ui";
import { InfoIcon } from "lucide-react";

import type { Duration, UnixTimestamp } from "@ensnode/ensnode-sdk";

import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";

interface ProjectionInfoProps {
snapshotTime: UnixTimestamp;
worstCaseDistance: Duration;
}

/**
* 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({ snapshotTime, worstCaseDistance }: ProjectionInfoProps) {
const now = useNow();

return (
Comment thread
tk-o marked this conversation as resolved.
Outdated
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please optimize the hover state. The way there's a rectangular light-grey panel appearing in the background on hover doesn't feel good.

<button
type="button"
className="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground w-8"
aria-label="Indexing Status Metadata"
>
<InfoIcon className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent
side="right"
className="bg-gray-50 text-sm text-black shadow-md outline-none w-80 p-4"
>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<div className="font-semibold text-xs text-gray-500 uppercase">
Worst-Case Distance*
</div>
<div className="text-sm">
{worstCaseDistance} second{worstCaseDistance !== 1 ? "s" : ""}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you please suggest a nicer way to format this when the value is large? Ex: pick larger units of time than seconds to display. I imagine there's an existing utility function we could use here.

CleanShot 2026-02-10 at 16 48 19

</div>
Comment thread
tk-o marked this conversation as resolved.
</div>

<div className="text-xs text-gray-600 leading-relaxed">
* as of real-time projection generated just now from indexing status snapshot captured{" "}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* as of real-time projection generated just now from indexing status snapshot captured{" "}
* as of the real-time projection generated just now from the indexing status snapshot captured{" "}

<RelativeTime
timestamp={snapshotTime}
relativeTo={now}
includeSeconds
conciseFormatting
/>
.
</div>
</div>
</TooltipContent>
</Tooltip>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client";

import { useNow } from "@namehash/namehash-ui";
import { secondsToMilliseconds } from "date-fns";
import { useCallback, useMemo } from "react";

import {
Expand All @@ -11,16 +13,21 @@ import {
WithSDKConfigParameter,
} from "@ensnode/ensnode-react";
Comment thread
tk-o marked this conversation as resolved.
import {
CrossChainIndexingStatusSnapshotOmnichain,
Comment thread
tk-o marked this conversation as resolved.
createRealtimeIndexingStatusProjection,
Duration,
type IndexingStatusRequest,
IndexingStatusResponseCodes,
IndexingStatusResponseOk,
} from "@ensnode/ensnode-sdk";
Comment thread
tk-o marked this conversation as resolved.

const DEFAULT_REFETCH_INTERVAL = 10 * 1000;
const DEFAULT_REFETCH_INTERVAL = secondsToMilliseconds(10);

const REALTIME_PROJECTION_REFRESH_RATE: Duration = 1;

interface UseIndexingStatusParameters
extends IndexingStatusRequest,
QueryParameter<IndexingStatusResponseOk> {}
QueryParameter<CrossChainIndexingStatusSnapshotOmnichain> {}
Comment thread
tk-o marked this conversation as resolved.

/**
* A proxy hook for {@link useIndexingStatus} which applies
Expand All @@ -31,6 +38,7 @@ export function useIndexingStatusWithSwr(
) {
const { config, query = {} } = parameters;
const _config = useENSNodeSDKConfig(config);
const now = useNow({ timeToRefresh: REALTIME_PROJECTION_REFRESH_RATE });

const queryOptions = useMemo(() => createIndexingStatusQueryOptions(_config), [_config]);
const queryKey = useMemo(() => ["swr", ...queryOptions.queryKey], [queryOptions.queryKey]);
Expand All @@ -46,18 +54,39 @@ export function useIndexingStatusWithSwr(
);
}

// successful response to be cached
return response;
// The indexing status snapshot has been fetched and successfully validated for caching.
// Therefore, return it so that query cache for `queryOptions.queryKey` will:
Comment thread
tk-o marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
// - Replace the currently cached value (if any) with this new value.
// - Return this non-null value.
return response.realtimeProjection.snapshot;
}),
[queryOptions.queryFn],
);

// Call select function to `createRealtimeIndexingStatusProjection` each time
// `now` is updated.
const select = useCallback(
(cachedSnapshot: CrossChainIndexingStatusSnapshotOmnichain): IndexingStatusResponseOk => {
const realtimeProjection = createRealtimeIndexingStatusProjection(cachedSnapshot, now);

Comment thread
tk-o marked this conversation as resolved.
// Maintain the original response shape of `IndexingStatusResponse`
// for the consumers. Creating a new projection from the cached snapshot
// each time `now` is updated should be implementation detail.
return {
responseCode: IndexingStatusResponseCodes.Ok,
realtimeProjection,
} satisfies IndexingStatusResponseOk;
},
[now],
Comment thread
tk-o marked this conversation as resolved.
);
Comment thread
tk-o marked this conversation as resolved.

Comment thread
tk-o marked this conversation as resolved.
return useSwrQuery({
...queryOptions,
refetchInterval: query.refetchInterval ?? DEFAULT_REFETCH_INTERVAL, // Indexing status changes frequently
...query,
refetchInterval: query.refetchInterval ?? DEFAULT_REFETCH_INTERVAL, // Indexing status changes frequently
enabled: query.enabled ?? queryOptions.enabled,
queryKey,
queryFn,
select,
});
Comment thread
tk-o marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useENSNodeConfig, useRegistrarActions } from "@ensnode/ensnode-react";
import {
IndexingStatusResponseCodes,
RegistrarActionsOrders,
RegistrarActionsResponseCodes,
registrarActionsPrerequisites,
Expand Down Expand Up @@ -44,13 +43,10 @@ export function useStatefulRegistrarActions({

let isRegistrarActionsApiSupported = false;

if (
ensNodeConfigQuery.isSuccess &&
indexingStatusQuery.isSuccess &&
indexingStatusQuery.data.responseCode === IndexingStatusResponseCodes.Ok
) {
if (ensNodeConfigQuery.isSuccess && indexingStatusQuery.isSuccess) {
const { ensIndexerPublicConfig } = ensNodeConfigQuery.data;
const { omnichainSnapshot } = indexingStatusQuery.data.realtimeProjection.snapshot;
const { realtimeProjection } = indexingStatusQuery.data;
const { omnichainSnapshot } = realtimeProjection.snapshot;

isRegistrarActionsApiSupported =
hasEnsIndexerConfigSupport(ensIndexerPublicConfig) &&
Expand Down Expand Up @@ -100,7 +96,8 @@ export function useStatefulRegistrarActions({
} satisfies StatefulFetchRegistrarActionsUnsupported;
}

const { omnichainSnapshot } = indexingStatusQuery.data.realtimeProjection.snapshot;
const { realtimeProjection } = indexingStatusQuery.data;
const { omnichainSnapshot } = realtimeProjection.snapshot;

// fetching is temporarily not possible due to indexing status being not advanced enough
if (!hasIndexingStatusSupport(omnichainSnapshot.omnichainStatus)) {
Expand Down
Loading