diff --git a/app/internal-explorer/blocks/page.tsx b/app/internal-explorer/blocks/page.tsx
index d90d7c3..2ef1ad7 100644
--- a/app/internal-explorer/blocks/page.tsx
+++ b/app/internal-explorer/blocks/page.tsx
@@ -13,6 +13,7 @@ import { ActiveBlockButton } from '../components/ActiveBlockButton';
import { explorerApi } from '../library/client';
import { formatInteger } from '../library/explorer-format';
import { explorerHref } from '../library/links';
+import { nextShadowCursor } from '../library/shadow-pagination';
import type { BlocksResponse, ShadowBlockSummary } from '../library/types';
import { useExplorerChain } from '../library/useExplorerChain';
import { useShadowDelta } from '../library/useShadowDelta';
@@ -76,13 +77,13 @@ function BlocksContent() {
setShadowCandidates({});
explorerApi
- .recentShadowBlocks(chain, { limit: PAGE_LIMIT }, controller.signal)
+ .recentShadowBlocks(chain, { limit: PAGE_LIMIT, before: cursor }, controller.signal)
.then(async (shadows) => {
if (cancelled) return;
if (shadows.length === 0) {
setData({
blocks: [],
- page: { cursor: null, limit: PAGE_LIMIT, latestBlockNumber: 0, nextCursor: null, hasMore: false },
+ page: { cursor: cursor ?? null, limit: PAGE_LIMIT, latestBlockNumber: 0, nextCursor: null, hasMore: false },
});
return;
}
@@ -101,15 +102,17 @@ function BlocksContent() {
.map((shadow) => [shadow.canonicalHash!.toLowerCase(), [shadow]]),
);
+ const nextCursor = nextShadowCursor(shadows, PAGE_LIMIT);
+
setShadowCandidates(candidates);
setData({
blocks: orderedBlocks,
page: {
- cursor: null,
+ cursor: cursor ?? null,
limit: PAGE_LIMIT,
latestBlockNumber: orderedBlocks[0]?.number ?? 0,
- nextCursor: null,
- hasMore: false,
+ nextCursor,
+ hasMore: nextCursor !== null,
},
});
})
@@ -125,7 +128,7 @@ function BlocksContent() {
cancelled = true;
controller.abort();
};
- }, [chain, showShadowDelta]);
+ }, [chain, cursor, showShadowDelta]);
return (
diff --git a/app/internal-explorer/library/shadow-pagination.test.ts b/app/internal-explorer/library/shadow-pagination.test.ts
new file mode 100644
index 0000000..34fc6e4
--- /dev/null
+++ b/app/internal-explorer/library/shadow-pagination.test.ts
@@ -0,0 +1,31 @@
+import { describe, expect, it } from 'vitest';
+
+import { nextShadowCursor } from './shadow-pagination';
+
+const PAGE_LIMIT = 25;
+
+function shadowsEndingAt(oldest: number, count: number): { number: number }[] {
+ return Array.from({ length: count }, (_, index) => ({ number: oldest + count - 1 - index }));
+}
+
+describe('nextShadowCursor', () => {
+ it('returns the oldest block number when a full page is returned', () => {
+ const shadows = shadowsEndingAt(100, PAGE_LIMIT);
+ expect(shadows.at(-1)?.number).toBe(100);
+ expect(nextShadowCursor(shadows, PAGE_LIMIT)).toBe(100);
+ });
+
+ it('returns null on a short page (source exhausted)', () => {
+ const shadows = shadowsEndingAt(100, PAGE_LIMIT - 1);
+ expect(nextShadowCursor(shadows, PAGE_LIMIT)).toBeNull();
+ });
+
+ it('returns null when the oldest block is genesis (0)', () => {
+ const shadows = shadowsEndingAt(0, PAGE_LIMIT);
+ expect(nextShadowCursor(shadows, PAGE_LIMIT)).toBeNull();
+ });
+
+ it('returns null for an empty page', () => {
+ expect(nextShadowCursor([], PAGE_LIMIT)).toBeNull();
+ });
+});
diff --git a/app/internal-explorer/library/shadow-pagination.ts b/app/internal-explorer/library/shadow-pagination.ts
new file mode 100644
index 0000000..16b0c5d
--- /dev/null
+++ b/app/internal-explorer/library/shadow-pagination.ts
@@ -0,0 +1,14 @@
+// Pagination cursor for the shadow-delta block view. Shadow blocks come back
+// newest-first; the API's `before` param is an *exclusive* block-number cutoff,
+// so the next page starts at the oldest block returned (no overlap, no gap). A
+// short page means the source is exhausted.
+import type { ShadowBlockSummary } from './types';
+
+export function nextShadowCursor(
+ shadows: Pick[],
+ pageLimit: number,
+): number | null {
+ if (shadows.length < pageLimit) return null;
+ const oldest = shadows.at(-1)?.number;
+ return oldest === undefined || oldest <= 0 ? null : oldest;
+}