Skip to content
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
2e2f8bb
feat: add cached methods
Aug 4, 2025
6e33dd2
feat: add more cached methods
alexcambose Aug 4, 2025
bc390f6
feat(insights): add more cached methods 2
alexcambose Aug 4, 2025
70ba6f9
feat(insights): testing in memory data
alexcambose Aug 4, 2025
80a0429
chore: test with partial cache
alexcambose Aug 5, 2025
a3d71ee
perf: reduce partial cache
alexcambose Aug 5, 2025
092cdd5
chore: test with max memory setting
alexcambose Aug 5, 2025
5ca1c24
chore: bump next version
alexcambose Aug 5, 2025
7c69871
chore: add logs
alexcambose Aug 5, 2025
5a0694a
chore: add cache test function
alexcambose Aug 5, 2025
c8f9edb
chore: test with unstable cache
alexcambose Aug 6, 2025
970fec1
chore: test with function tags
alexcambose Aug 6, 2025
90b3668
perf: chunk cache
alexcambose Aug 7, 2025
ca16ed8
fix: add cache buffer
alexcambose Aug 7, 2025
f7c4e0d
fix: lint
alexcambose Aug 7, 2025
f226d34
feat: chunk cache
alexcambose Aug 8, 2025
f6ffcf7
feat: replaced more cached functions
alexcambose Aug 8, 2025
022793b
fix: use in memory cache
alexcambose Aug 8, 2025
3823e64
fix: cache key issue
alexcambose Aug 8, 2025
b06ac66
chore: added logging
alexcambose Aug 8, 2025
b5ac3f7
chore: revalidate false
alexcambose Aug 8, 2025
effbc28
feat: migrated to redis
alexcambose Aug 11, 2025
0682c73
feat: added fetch routes
alexcambose Aug 12, 2025
8b2719c
feat: added all endpoints
alexcambose Aug 12, 2025
fcbda8a
feat: more endpoint fetching
alexcambose Aug 13, 2025
8daffbb
fix: types
alexcambose Aug 13, 2025
9e29e54
refactor: cleanup
alexcambose Aug 13, 2025
b684cbe
fix: publishers metadata
alexcambose Aug 13, 2025
2744ee0
fix: imports
alexcambose Aug 14, 2025
a3cfcda
refactor: minor fixes
alexcambose Aug 14, 2025
d9456ed
Merge branch 'main' into feat/perf-caching
alexcambose Aug 14, 2025
5da02aa
Merge branch 'main' into feat/perf-caching
alexcambose Aug 14, 2025
038d370
chore: updated pnpm
alexcambose Aug 14, 2025
1872162
chore: updated pnpm
alexcambose Aug 14, 2025
70e5bd4
chore: revert with fetch again
alexcambose Aug 14, 2025
43ff345
refactor: addressed PR comments
alexcambose Aug 15, 2025
9966636
feat: use zod for validation
alexcambose Aug 15, 2025
498dbd0
fix: use origin headers
alexcambose Aug 18, 2025
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
1 change: 0 additions & 1 deletion apps/insights/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ const config = {
useCache: true,
reactCompiler: true,
},

reactStrictMode: true,

pageExtensions: ["ts", "tsx", "mdx"],
Expand Down
2 changes: 2 additions & 0 deletions apps/insights/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@
"@pythnetwork/known-publishers": "workspace:*",
"@react-hookz/web": "catalog:",
"@solana/web3.js": "catalog:",
"async-cache-dedupe": "^3.0.0",
"bs58": "catalog:",
"clsx": "catalog:",
"cryptocurrency-icons": "catalog:",
"dnum": "catalog:",
"ioredis": "^5.7.0",
"lightweight-charts": "catalog:",
"motion": "catalog:",
"next": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextResponse } from "next/server";
import { stringify } from "superjson";

import { getFeedsCached } from '../../../../../server/pyth/get-feeds';
import { Cluster } from "../../../../../services/pyth";

export const GET = async (request: Request, { params }: { params: Promise<{ publisher: string }> }) => {
const { publisher } = await params;
const { searchParams } = new URL(request.url);
const cluster = Number.parseInt(searchParams.get("cluster") ?? Cluster.Pythnet.toString()) as Cluster;

// check if cluster is valid
if (cluster && !Object.values(Cluster).includes(cluster)) {
return NextResponse.json({ error: "Invalid cluster" }, { status: 400 });
}

if (!publisher) {
return NextResponse.json({ error: "Publisher is required" }, { status: 400 });
}

const feeds = await getFeedsCached(cluster);

const filteredFeeds = feeds.filter((feed) => feed.price.priceComponents.some((c) => c.publisher === publisher));

return new Response(stringify(filteredFeeds), {
headers: {
'Content-Type': 'application/json',
},
});
};
25 changes: 25 additions & 0 deletions apps/insights/src/app/api/pyth/get-feeds/[symbol]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { stringify } from 'superjson';

import { getFeedsCached } from "../../../../../server/pyth/get-feeds";
import { Cluster } from "../../../../../services/pyth";

export const GET = async (request: Request, { params }: { params: Promise<{ symbol: string }> }) => {
const { symbol } = await params;
const { searchParams } = new URL(request.url);
const cluster = Number.parseInt(searchParams.get("cluster") ?? Cluster.Pythnet.toString()) as Cluster;

// check if cluster is valid
if (cluster && !Object.values(Cluster).includes(cluster)) {
return NextResponse.json({ error: "Invalid cluster" }, { status: 400 });
}

const feeds = await getFeedsCached(cluster);
const feed = feeds.find((feed) => feed.symbol === symbol);

return new Response(stringify(feed), {
headers: {
'Content-Type': 'application/json',
}
});
};
33 changes: 33 additions & 0 deletions apps/insights/src/app/api/pyth/get-feeds/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { NextResponse } from "next/server";
import { stringify } from 'superjson';
import type { z } from 'zod';

import { getFeedsCached } from "../../../../server/pyth/get-feeds";
import { Cluster, priceFeedsSchema } from "../../../../services/pyth";

export const GET = async (request: Request) => {
// get cluster from query params
const { searchParams } = new URL(request.url);
const excludePriceComponents = searchParams.get("excludePriceComponents") === "true";
const cluster = Number.parseInt(searchParams.get("cluster") ?? Cluster.Pythnet.toString()) as Cluster;

// check if cluster is valid
if (cluster && !Object.values(Cluster).includes(cluster)) {
return NextResponse.json({ error: "Invalid cluster" }, { status: 400 });
}

let feeds = await getFeedsCached(cluster) as Omit<z.infer<typeof priceFeedsSchema>[number], 'price'>[];
Copy link
Collaborator

@cprussin cprussin Aug 14, 2025

Choose a reason for hiding this comment

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

We should not typecast here, the typecast will be unnecessary if you just do this like this:

const feeds = await getFeedsCached(cluster);
return NextResponse.json(stringify(
  excludePriceComponents ? feeds.map(({ price, ...feed }) => feed) : feeds
));


if(excludePriceComponents) {
feeds = feeds.map((feed) => ({
...feed,
price: undefined,
}));
}

return new Response(stringify(feeds), {
headers: {
'Content-Type': 'application/json',
},
});
};
24 changes: 24 additions & 0 deletions apps/insights/src/app/api/pyth/get-publishers/[symbol]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextResponse } from "next/server";

import { getPublishersForClusterCached } from "../../../../../server/pyth/get-publishers-for-cluster";
import { Cluster } from "../../../../../services/pyth";

export const GET = async (request: Request, { params }: { params: Promise<{ symbol: string }> }) => {
const { symbol } = await params;
// get cluster from query params
const { searchParams } = new URL(request.url);
const cluster = Number.parseInt(searchParams.get("cluster") ?? Cluster.Pythnet.toString()) as Cluster;

// check if cluster is valid
if (cluster && !Object.values(Cluster).includes(cluster)) {
return NextResponse.json({ error: "Invalid cluster" }, { status: 400 });
}

if (!symbol) {
return NextResponse.json({ error: "Symbol is required" }, { status: 400 });
}

const map = await getPublishersForClusterCached(cluster);

return NextResponse.json(map[symbol] ?? []);
};
5 changes: 3 additions & 2 deletions apps/insights/src/app/price-feeds/[slug]/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation";
import type { ReactNode } from "react";

import { Cluster, getFeeds } from "../../../services/pyth";
import { getFeedsCached } from "../../../server/pyth/get-feeds";
import { Cluster } from "../../../services/pyth";

export { PriceFeedLayout as default } from "../../../components/PriceFeed/layout";

Expand All @@ -19,7 +20,7 @@ export const generateMetadata = async ({
}: Props): Promise<Metadata> => {
const [{ slug }, feeds] = await Promise.all([
params,
getFeeds(Cluster.Pythnet),
getFeedsCached(Cluster.Pythnet),
]);
const symbol = decodeURIComponent(slug);
const feed = feeds.find((item) => item.symbol === symbol);
Expand Down
7 changes: 5 additions & 2 deletions apps/insights/src/components/Overview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import PriceFeedsLight from "./price-feeds-light.svg";
import PublishersDark from "./publishers-dark.svg";
import PublishersLight from "./publishers-light.svg";
import { TabList } from "./tab-list";
import { Cluster, getFeeds } from "../../services/pyth";
import { getFeedsCached } from "../../server/pyth/get-feeds";
import { Cluster } from "../../services/pyth";
import {
totalVolumeTraded,
activeChains,
Expand All @@ -23,8 +24,9 @@ import { FormattedDate } from "../FormattedDate";
import { FormattedNumber } from "../FormattedNumber";

export const Overview = async () => {
const priceFeeds = await getFeeds(Cluster.Pythnet);
const priceFeeds = await getFeedsCached(Cluster.Pythnet);
const today = new Date();

const feedCounts = [
...activeFeeds.map(({ date, numFeeds }) => ({
x: date,
Expand All @@ -37,6 +39,7 @@ export const Overview = async () => {
y: priceFeeds.length,
},
];

return (
<div className={styles.overview}>
<h1 className={styles.header}>Overview</h1>
Expand Down
29 changes: 20 additions & 9 deletions apps/insights/src/components/PriceFeed/get-feed.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import { notFound } from "next/navigation";
import { parse } from "superjson";
import { z } from "zod";

import { Cluster, getFeeds } from "../../services/pyth";
import { PUBLIC_URL, VERCEL_AUTOMATION_BYPASS_SECRET } from '../../config/server';
import { getFeedForSymbolCached } from '../../server/pyth';
import { Cluster, priceFeedsSchema } from "../../services/pyth";
import { DEFAULT_CACHE_TTL } from '../../utils/cache';

export const getFeed = async (params: Promise<{ slug: string }>) => {
"use cache";
const data = await fetch(`${PUBLIC_URL}/api/pyth/get-feeds?cluster=${Cluster.Pythnet.toString()}&excludePriceComponents=true`, {
next: {
revalidate: DEFAULT_CACHE_TTL,
},
headers: {
'x-vercel-protection-bypass': VERCEL_AUTOMATION_BYPASS_SECRET,
},
});
const dataJson = await data.text();
const feeds: z.infer<typeof priceFeedsSchema> = parse(dataJson);

const [{ slug }, feeds] = await Promise.all([params, getPythnetFeeds()]);
const { slug } = await params;
const symbol = decodeURIComponent(slug);
const feed = await getFeedForSymbolCached({symbol, cluster: Cluster.Pythnet});

return {
feeds,
feed: feeds.find((item) => item.symbol === symbol) ?? notFound(),
feed: feed ?? notFound(),
symbol,
} as const;
};

const getPythnetFeeds = async () => {
"use cache";
return getFeeds(Cluster.Pythnet);
};
29 changes: 14 additions & 15 deletions apps/insights/src/components/PriceFeed/publishers.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { lookup as lookupPublisher } from "@pythnetwork/known-publishers";
import { notFound } from "next/navigation";

import { getRankingsBySymbol } from "../../services/clickhouse";
import { getRankingsBySymbolCached } from '../../server/clickhouse';
import { getFeedForSymbolCached, getPublishersForFeedCached } from "../../server/pyth";
import {
Cluster,
ClusterToName,
getFeeds,
getPublishersForFeed,
} from "../../services/pyth";
import { getStatus } from "../../status";
import { PublisherIcon } from "../PublisherIcon";
Expand All @@ -19,24 +18,23 @@ type Props = {
}>;
};


export const Publishers = async ({ params }: Props) => {
const { slug } = await params;
const symbol = decodeURIComponent(slug);

const [
pythnetFeeds,
pythtestConformanceFeeds,
feed,
testFeed,
pythnetPublishers,
pythtestConformancePublishers,
] = await Promise.all([
getFeeds(Cluster.Pythnet),
getFeeds(Cluster.PythtestConformance),
getFeedForSymbolCached({symbol, cluster: Cluster.Pythnet}),
getFeedForSymbolCached({symbol, cluster: Cluster.PythtestConformance}),
getPublishers(Cluster.Pythnet, symbol),
getPublishers(Cluster.PythtestConformance, symbol),
]);
const feed = pythnetFeeds.find((feed) => feed.symbol === symbol);
const testFeed = pythtestConformanceFeeds.find(
(feed) => feed.symbol === symbol,
);

const publishers = [...pythnetPublishers, ...pythtestConformancePublishers];
const metricsTime = pythnetPublishers.find(
(publisher) => publisher.ranking !== undefined,
Expand Down Expand Up @@ -86,13 +84,14 @@ export const Publishers = async ({ params }: Props) => {
export const PublishersLoading = () => <PublishersCard isLoading />;

const getPublishers = async (cluster: Cluster, symbol: string) => {

const [publishers, rankings] = await Promise.all([
getPublishersForFeed(cluster, symbol),
getRankingsBySymbol(symbol),
getPublishersForFeedCached(cluster, symbol),
getRankingsBySymbolCached(symbol),
]);

return (
publishers?.map((publisher) => {
publishers.map((publisher) => {
const ranking = rankings.find(
(ranking) =>
ranking.publisher === publisher &&
Expand All @@ -106,6 +105,6 @@ const getPublishers = async (cluster: Cluster, symbol: string) => {
cluster,
knownPublisher: lookupPublisher(publisher),
};
}) ?? []
})
);
};
5 changes: 3 additions & 2 deletions apps/insights/src/components/PriceFeeds/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import { AssetClassTable } from "./asset-class-table";
import { ComingSoonList } from "./coming-soon-list";
import styles from "./index.module.scss";
import { PriceFeedsCard } from "./price-feeds-card";
import { Cluster, getFeeds } from "../../services/pyth";
import { getFeedsCached } from "../../server/pyth/get-feeds";
import { Cluster } from "../../services/pyth";
import { priceFeeds as priceFeedsStaticConfig } from "../../static-data/price-feeds";
import { activeChains } from "../../static-data/stats";
import { Cards } from "../Cards";
Expand Down Expand Up @@ -289,7 +290,7 @@ const FeaturedFeedsCard = <T extends ElementType>({
);

const getPriceFeeds = async () => {
const priceFeeds = await getFeeds(Cluster.Pythnet);
const priceFeeds = await getFeedsCached(Cluster.Pythnet);
const activeFeeds = priceFeeds.filter((feed) => isActive(feed));
const comingSoon = priceFeeds.filter((feed) => !isActive(feed));
return { activeFeeds, comingSoon };
Expand Down
10 changes: 5 additions & 5 deletions apps/insights/src/components/Publisher/get-price-feeds.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { getRankingsByPublisher } from "../../services/clickhouse";
import type { Cluster } from "../../services/pyth";
import { ClusterToName, getFeedsForPublisher } from "../../services/pyth";
import { getRankingsByPublisherCached } from '../../server/clickhouse';
import { getFeedsForPublisherCached } from "../../server/pyth";
import { Cluster, ClusterToName } from "../../services/pyth";
import { getStatus } from "../../status";

export const getPriceFeeds = async (cluster: Cluster, key: string) => {
const [feeds, rankings] = await Promise.all([
getFeedsForPublisher(cluster, key),
getRankingsByPublisher(key),
getFeedsForPublisherCached(cluster, key),
getRankingsByPublisherCached(key),
]);
return feeds.map((feed) => {
const ranking = rankings.find(
Expand Down
21 changes: 8 additions & 13 deletions apps/insights/src/components/Publisher/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import type { ReactNode } from "react";
import { Suspense } from "react";

import {
getPublisherRankingHistory,
getPublisherAverageScoreHistory,
getPublishers,
} from "../../services/clickhouse";
getPublishersCached,
getPublisherRankingHistoryCached,
getPublisherAverageScoreHistoryCached,
} from "../../server/clickhouse";
import { getPublisherCaps } from "../../services/hermes";
import { ClusterToName, parseCluster, Cluster } from "../../services/pyth";
import { getPublisherPoolData } from "../../services/staking";
Expand Down Expand Up @@ -150,10 +150,7 @@ const RankingCard = async ({
cluster: Cluster;
publisherKey: string;
}) => {
const rankingHistory = await getPublisherRankingHistory(
cluster,
publisherKey,
);
const rankingHistory = await getPublisherRankingHistoryCached({ cluster, key: publisherKey });
return <RankingCardImpl rankingHistory={rankingHistory} />;
};

Expand Down Expand Up @@ -234,10 +231,8 @@ const ScoreCard = async ({
cluster: Cluster;
publisherKey: string;
}) => {
const averageScoreHistory = await getPublisherAverageScoreHistory(
cluster,
publisherKey,
);
const averageScoreHistory = await getPublisherAverageScoreHistoryCached({ cluster, key: publisherKey });

return <ScoreCardImpl averageScoreHistory={averageScoreHistory} />;
};

Expand Down Expand Up @@ -338,7 +333,7 @@ const ActiveFeedsCard = async ({
publisherKey: string;
}) => {
const [publishers, priceFeeds] = await Promise.all([
getPublishers(cluster),
getPublishersCached(cluster),
getPriceFeeds(cluster, publisherKey),
]);
const publisher = publishers.find(
Expand Down
Loading
Loading