Skip to content

Commit e600e47

Browse files
authored
refactor: update licenses page loading (#30)
* refactor: update licenses page loading * fix: serialize cache
1 parent b025b47 commit e600e47

File tree

5 files changed

+161
-168
lines changed

5 files changed

+161
-168
lines changed

app/licenses/page.tsx

Lines changed: 89 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,97 @@
11
import ErrorComponent from '@/app/server-components/shared/ErrorComponent';
2-
import { getAllLicenseTokenIds } from '@/lib/api/blockchain';
3-
import { LicenseItem } from '@/typedefs/general';
2+
import { PAGE_SIZE } from '@/config';
3+
import { getLicensesPage } from '@/lib/api/blockchain';
4+
import { LicenseListItem } from '@/typedefs/general';
45
import { unstable_cache } from 'next/cache';
56
import List from '../server-components/Licenses/List';
67
import { BorderedCard } from '../server-components/shared/cards/BorderedCard';
78
import { CardHorizontal } from '../server-components/shared/cards/CardHorizontal';
8-
9-
export async function generateMetadata({ searchParams }: { searchParams?: Promise<{ page?: string }> }) {
10-
const resolvedSearchParams = await searchParams;
11-
const pageParam = Number.parseInt(resolvedSearchParams?.page ?? '', 10);
12-
const canonical = Number.isFinite(pageParam) && pageParam > 1 ? `/licenses?page=${pageParam}` : '/licenses';
13-
14-
return {
15-
title: 'Licenses',
16-
openGraph: {
17-
title: 'Licenses',
18-
},
19-
alternates: {
20-
canonical,
21-
},
22-
};
23-
}
24-
25-
const getCachedLicenseTokenIds = unstable_cache(getAllLicenseTokenIds, ['licenses-token-ids'], { revalidate: 300 });
26-
27-
export default async function LicensesPage(props: {
28-
searchParams?: Promise<{
29-
page?: string;
30-
}>;
31-
}) {
32-
const searchParams = await props.searchParams;
33-
const currentPage = Number(searchParams?.page) || 1;
34-
35-
let ndTotalSupply: number, mndTotalSupply: number;
36-
let licenses: LicenseItem[];
9+
10+
export async function generateMetadata({ searchParams }: { searchParams?: Promise<{ page?: string }> }) {
11+
const resolvedSearchParams = await searchParams;
12+
const pageParam = Number.parseInt(resolvedSearchParams?.page ?? '', 10);
13+
const canonical = Number.isFinite(pageParam) && pageParam > 1 ? `/licenses?page=${pageParam}` : '/licenses';
14+
15+
return {
16+
title: 'Licenses',
17+
openGraph: {
18+
title: 'Licenses',
19+
},
20+
alternates: {
21+
canonical,
22+
},
23+
};
24+
}
25+
26+
const getCachedLicensesPage = unstable_cache(
27+
async (currentPage: number) => {
28+
const page = currentPage > 0 ? currentPage : 1;
29+
const offset = (page - 1) * PAGE_SIZE;
30+
const data = await getLicensesPage(offset, PAGE_SIZE);
31+
32+
return {
33+
...data,
34+
ndTotalSupply: data.ndTotalSupply.toString(),
35+
mndTotalSupply: data.mndTotalSupply.toString(),
36+
};
37+
},
38+
['licenses-page'],
39+
{ revalidate: 300 },
40+
);
41+
42+
export default async function LicensesPage(props: {
43+
searchParams?: Promise<{
44+
page?: string;
45+
}>;
46+
}) {
47+
const searchParams = await props.searchParams;
48+
const currentPage = Number(searchParams?.page) || 1;
49+
50+
let ndTotalSupply: bigint, mndTotalSupply: bigint;
51+
let licensesCount: number;
52+
let licenses: LicenseListItem[];
3753

3854
try {
39-
const { ndLicenseIds, mndLicenseIds } = await getCachedLicenseTokenIds();
40-
ndTotalSupply = ndLicenseIds.length;
41-
mndTotalSupply = mndLicenseIds.length;
55+
const {
56+
ndTotalSupply: ndSupply,
57+
mndTotalSupply: mndSupply,
58+
licenses: pageLicenses,
59+
} = await getCachedLicensesPage(currentPage);
60+
61+
ndTotalSupply = BigInt(ndSupply);
62+
mndTotalSupply = BigInt(mndSupply);
63+
licenses = pageLicenses;
64+
licensesCount = Number(ndTotalSupply + mndTotalSupply);
65+
} catch (error) {
66+
console.error(error);
67+
console.log('[Licenses Page] Failed to fetch license data');
68+
return <NotFound />;
69+
}
70+
71+
return (
72+
<>
73+
<div className="w-full">
74+
<BorderedCard>
75+
<div className="card-title-big font-bold">Licenses</div>
76+
77+
<div className="flexible-row">
78+
<CardHorizontal
79+
label="Total"
80+
value={licensesCount.toString()}
81+
isFlexible
82+
widthClasses="min-w-[192px]"
83+
/>
84+
<CardHorizontal label="ND" value={ndTotalSupply.toString()} isFlexible widthClasses="min-w-[192px]" />
85+
<CardHorizontal label="MND" value={mndTotalSupply.toString()} isFlexible widthClasses="min-w-[192px]" />
86+
</div>
87+
</BorderedCard>
88+
</div>
89+
90+
<List licenses={licenses} currentPage={currentPage} totalLicenses={licensesCount} />
91+
</>
92+
);
93+
}
4294

43-
licenses = [
44-
...mndLicenseIds.map((licenseId) => ({
45-
licenseId,
46-
licenseType: licenseId === 1 ? ('GND' as const) : ('MND' as const),
47-
})),
48-
...ndLicenseIds.map((licenseId) => ({
49-
licenseId,
50-
licenseType: 'ND' as const,
51-
})),
52-
];
53-
} catch (error) {
54-
console.error(error);
55-
console.log('[Licenses Page] Failed to fetch license data');
56-
return <NotFound />;
57-
}
58-
59-
return (
60-
<>
61-
<div className="w-full">
62-
<BorderedCard>
63-
<div className="card-title-big font-bold">Licenses</div>
64-
65-
<div className="flexible-row">
66-
<CardHorizontal
67-
label="Total"
68-
value={ndTotalSupply + mndTotalSupply}
69-
isFlexible
70-
widthClasses="min-w-[192px]"
71-
/>
72-
<CardHorizontal label="ND" value={ndTotalSupply} isFlexible widthClasses="min-w-[192px]" />
73-
<CardHorizontal label="MND" value={mndTotalSupply} isFlexible widthClasses="min-w-[192px]" />
74-
</div>
75-
</BorderedCard>
76-
</div>
77-
78-
<List licenses={licenses} currentPage={currentPage} />
79-
</>
80-
);
81-
}
82-
83-
function NotFound() {
84-
return <ErrorComponent title="Error" description="The licenses data could not be loaded. Please try again later." />;
85-
}
95+
function NotFound() {
96+
return <ErrorComponent title="Error" description="The licenses data could not be loaded. Please try again later." />;
97+
}

app/server-components/Licenses/License.tsx

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import ClientWrapper from '@/components/shared/ClientWrapper';
22
import { CopyableAddress } from '@/components/shared/CopyableValue';
3-
import { cachedGetLicense } from '@/lib/api/cache';
43
import { routePath } from '@/lib/routes';
54
import { isZeroAddress } from '@/lib/utils';
6-
import * as types from '@/typedefs/blockchain';
5+
import { LicenseListItem } from '@/typedefs/general';
76
import { Skeleton } from '@heroui/skeleton';
87
import clsx from 'clsx';
98
import { Suspense } from 'react';
@@ -14,36 +13,19 @@ import { SmallTag } from '../shared/SmallTag';
1413
import NodeSmallCard from './NodeSmallCard';
1514

1615
interface Props {
17-
licenseType: 'ND' | 'MND' | 'GND';
18-
licenseId: string;
16+
license: LicenseListItem;
1917
}
2018

21-
export default async function License({ licenseType, licenseId }: Props) {
22-
let owner: types.EthAddress;
23-
let nodeAddress: types.EthAddress;
24-
let totalAssignedAmount: string;
25-
let totalClaimedAmount: string;
26-
let assignTimestamp: string;
27-
let isBanned: boolean;
28-
29-
try {
30-
({ owner, nodeAddress, totalAssignedAmount, totalClaimedAmount, assignTimestamp, isBanned } = await cachedGetLicense(
31-
licenseType,
32-
licenseId,
33-
));
34-
} catch (error: any) {
35-
if (!error.message.includes('ERC721: invalid token ID')) {
36-
console.error({ licenseType, licenseId }, error);
37-
}
38-
return null;
39-
}
19+
export default function License({ license }: Props) {
20+
const { licenseType, licenseId, owner, nodeAddress, totalAssignedAmount, totalClaimedAmount, assignTimestamp, isBanned } =
21+
license;
4022

4123
return (
4224
<BorderedCard useCustomWrapper useFixedWidthLarge>
4325
<div className="row justify-between gap-3 py-2 md:py-3 lg:gap-6">
4426
{/* License */}
4527
<LicenseSmallCard
46-
licenseId={Number(licenseId)}
28+
licenseId={licenseId}
4729
licenseType={licenseType}
4830
totalAssignedAmount={BigInt(totalAssignedAmount)}
4931
totalClaimedAmount={BigInt(totalClaimedAmount)}
Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import ParamsPagination from '@/components/shared/ParamsPagination';
22
import { PAGE_SIZE } from '@/config';
3-
import { LicenseItem } from '@/typedefs/general';
3+
import { LicenseListItem } from '@/typedefs/general';
44
import { Skeleton } from '@heroui/skeleton';
55
import { Suspense } from 'react';
66
import ListHeader from '../shared/ListHeader';
77
import License from './License';
88

9-
export default async function List({ licenses, currentPage }: { licenses: LicenseItem[]; currentPage: number }) {
10-
const getPage = () => {
11-
const startIndex = (currentPage - 1) * PAGE_SIZE;
12-
const endIndex = startIndex + PAGE_SIZE;
13-
return licenses.slice(startIndex, endIndex);
14-
};
15-
9+
export default async function List({
10+
licenses,
11+
currentPage,
12+
totalLicenses,
13+
}: {
14+
licenses: LicenseListItem[];
15+
currentPage: number;
16+
totalLicenses: number;
17+
}) {
1618
return (
1719
<div className="list-wrapper">
1820
<div id="list" className="list">
@@ -24,17 +26,17 @@ export default async function List({ licenses, currentPage }: { licenses: Licens
2426
<div className="min-w-[256px]">Node</div>
2527
</ListHeader>
2628

27-
{getPage().map((license) => (
29+
{licenses.map((license) => (
2830
<Suspense
2931
key={`${currentPage}-${license.licenseType}-${license.licenseId}`}
3032
fallback={<Skeleton className="min-h-[92px] w-full rounded-2xl" />}
3133
>
32-
<License licenseType={license.licenseType} licenseId={license.licenseId.toString()} />
34+
<License license={license} />
3335
</Suspense>
3436
))}
3537
</div>
3638

37-
<ParamsPagination total={Math.ceil(licenses.length / PAGE_SIZE)} />
39+
<ParamsPagination total={Math.max(1, Math.ceil(totalLicenses / PAGE_SIZE))} />
3840
</div>
3941
);
4042
}

lib/api/blockchain.ts

Lines changed: 35 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ReaderAbi } from '@/blockchain/Reader';
77
import { AdoptionOracleAbi } from '@/blockchain/AdoptionOracle';
88
import config, { getCurrentEpoch, getEpochStartTimestamp, getNextEpochTimestamp } from '@/config';
99
import * as types from '@/typedefs/blockchain';
10+
import { LicenseListItem } from '@/typedefs/general';
1011
import console from 'console';
1112
import { differenceInSeconds } from 'date-fns';
1213
import Moralis from 'moralis';
@@ -371,66 +372,45 @@ export async function getLicensesTotalSupply(): Promise<{
371372
};
372373
}
373374

374-
const LICENSE_ENUMERATION_CHUNK_SIZE = 200;
375-
376-
const getEnumerableLicenseIds = async (
377-
publicClient: Awaited<ReturnType<typeof getPublicClient>>,
378-
address: types.EthAddress,
379-
abi: typeof NDContractAbi | typeof MNDContractAbi,
380-
totalSupply: bigint,
381-
): Promise<number[]> => {
382-
if (totalSupply === 0n) {
383-
return [];
384-
}
385-
386-
const total = Number(totalSupply);
387-
const licenseIds: number[] = [];
388-
389-
for (let start = 0; start < total; start += LICENSE_ENUMERATION_CHUNK_SIZE) {
390-
const end = Math.min(start + LICENSE_ENUMERATION_CHUNK_SIZE, total);
391-
const tokenIds = await publicClient.multicall({
392-
contracts: Array.from({ length: end - start }, (_, index) => ({
393-
address,
394-
abi,
395-
functionName: 'tokenByIndex' as const,
396-
args: [BigInt(start + index)],
397-
})),
398-
allowFailure: false,
399-
});
400-
401-
licenseIds.push(...tokenIds.map((tokenId) => Number(tokenId)));
402-
}
403-
404-
return licenseIds.sort((a, b) => a - b);
405-
};
406-
407-
export async function getAllLicenseTokenIds(): Promise<{
408-
mndLicenseIds: number[];
409-
ndLicenseIds: number[];
375+
export async function getLicensesPage(offset: number, limit: number): Promise<{
376+
mndTotalSupply: bigint;
377+
ndTotalSupply: bigint;
378+
licenses: LicenseListItem[];
410379
}> {
411380
const publicClient = await getPublicClient();
412381

413-
const [mndTotalSupply, ndTotalSupply] = await Promise.all([
414-
publicClient.readContract({
415-
address: config.mndContractAddress,
416-
abi: MNDContractAbi,
417-
functionName: 'totalSupply',
418-
}),
419-
publicClient.readContract({
420-
address: config.ndContractAddress,
421-
abi: NDContractAbi,
422-
functionName: 'totalSupply',
423-
}),
424-
]);
425-
426-
const [mndLicenseIds, ndLicenseIds] = await Promise.all([
427-
getEnumerableLicenseIds(publicClient, config.mndContractAddress, MNDContractAbi, mndTotalSupply),
428-
getEnumerableLicenseIds(publicClient, config.ndContractAddress, NDContractAbi, ndTotalSupply),
429-
]);
382+
const [mndTotalSupply, ndTotalSupply, licenseRows] = await publicClient.readContract({
383+
address: config.readerContractAddress,
384+
abi: ReaderAbi,
385+
functionName: 'getLicensesPage',
386+
args: [BigInt(offset), BigInt(limit)],
387+
});
430388

431389
return {
432-
mndLicenseIds,
433-
ndLicenseIds,
390+
mndTotalSupply,
391+
ndTotalSupply,
392+
licenses: licenseRows.map((license) => {
393+
const licenseType = [undefined, 'ND', 'MND', 'GND'][Number(license.licenseType)] as
394+
| 'ND'
395+
| 'MND'
396+
| 'GND'
397+
| undefined;
398+
399+
if (!licenseType) {
400+
throw new Error(`Invalid license type returned by reader for license #${license.licenseId}`);
401+
}
402+
403+
return {
404+
licenseType,
405+
licenseId: Number(license.licenseId),
406+
owner: license.owner,
407+
nodeAddress: license.nodeAddress,
408+
totalAssignedAmount: license.totalAssignedAmount.toString(),
409+
totalClaimedAmount: license.totalClaimedAmount.toString(),
410+
assignTimestamp: license.assignTimestamp.toString(),
411+
isBanned: license.isBanned,
412+
};
413+
}),
434414
};
435415
}
436416

0 commit comments

Comments
 (0)