Skip to content

Commit effbc28

Browse files
committed
feat: migrated to redis
1 parent b5ac3f7 commit effbc28

File tree

16 files changed

+588
-2101
lines changed

16 files changed

+588
-2101
lines changed

apps/insights/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
"@pythnetwork/known-publishers": "workspace:*",
2929
"@react-hookz/web": "catalog:",
3030
"@solana/web3.js": "catalog:",
31+
"async-cache-dedupe": "^3.0.0",
3132
"bs58": "catalog:",
3233
"clsx": "catalog:",
3334
"cryptocurrency-icons": "catalog:",
3435
"dnum": "catalog:",
36+
"ioredis": "^5.7.0",
3537
"lightweight-charts": "catalog:",
3638
"motion": "catalog:",
3739
"next": "catalog:",

apps/insights/src/app/price-feeds/[slug]/layout.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Metadata } from "next";
22
import { notFound } from "next/navigation";
33
import type { ReactNode } from "react";
44

5-
import { getFeedsCached } from "../../../server/pyth";
5+
import { getFeeds } from "../../../server/pyth/get-feeds";
66
import { Cluster } from "../../../services/pyth";
77

88
export { PriceFeedLayout as default } from "../../../components/PriceFeed/layout";
@@ -20,7 +20,7 @@ export const generateMetadata = async ({
2020
}: Props): Promise<Metadata> => {
2121
const [{ slug }, feeds] = await Promise.all([
2222
params,
23-
getFeedsCached(Cluster.Pythnet),
23+
getFeeds(Cluster.Pythnet),
2424
]);
2525
const symbol = decodeURIComponent(slug);
2626
const feed = feeds.find((item) => item.symbol === symbol);

apps/insights/src/components/Overview/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import PriceFeedsLight from "./price-feeds-light.svg";
99
import PublishersDark from "./publishers-dark.svg";
1010
import PublishersLight from "./publishers-light.svg";
1111
import { TabList } from "./tab-list";
12-
import { getFeedsCached } from "../../server/pyth";
12+
import { getFeeds } from "../../server/pyth/get-feeds";
1313
import { Cluster } from "../../services/pyth";
1414
import {
1515
totalVolumeTraded,
@@ -24,7 +24,7 @@ import { FormattedDate } from "../FormattedDate";
2424
import { FormattedNumber } from "../FormattedNumber";
2525

2626
export const Overview = async () => {
27-
const priceFeeds = await getFeedsCached(Cluster.Pythnet);
27+
const priceFeeds = await getFeeds(Cluster.Pythnet);
2828
const today = new Date();
2929
const feedCounts = [
3030
...activeFeeds.map(({ date, numFeeds }) => ({

apps/insights/src/components/PriceFeed/get-feed.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { notFound } from "next/navigation";
22

3-
import { getFeedsCached } from "../../server/pyth";
3+
import { getFeeds } from "../../server/pyth/get-feeds";
44
import { Cluster } from "../../services/pyth";
55

66
export const getFeed = async (params: Promise<{ slug: string }>) => {
7-
const [{ slug }, feeds] = await Promise.all([params, getFeedsCached(Cluster.Pythnet)]);
7+
const [{ slug }, feeds] = await Promise.all([params, getFeeds(Cluster.Pythnet)]);
88
const symbol = decodeURIComponent(slug);
99
return {
1010
feeds,

apps/insights/src/components/PriceFeed/publishers.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { lookup as lookupPublisher } from "@pythnetwork/known-publishers";
22
import { notFound } from "next/navigation";
33

44
import { getRankingsBySymbolCached } from '../../server/clickhouse';
5-
import { getFeedsCached, getPublishersForFeedCached } from "../../server/pyth";
5+
import { getPublishersForFeedCached } from "../../server/pyth";
6+
import { getFeeds } from "../../server/pyth/get-feeds";
67
import {
78
Cluster,
89
ClusterToName,
@@ -29,8 +30,8 @@ export const Publishers = async ({ params }: Props) => {
2930
pythnetPublishers,
3031
pythtestConformancePublishers,
3132
] = await Promise.all([
32-
getFeedsCached(Cluster.Pythnet),
33-
getFeedsCached(Cluster.PythtestConformance),
33+
getFeeds(Cluster.Pythnet),
34+
getFeeds(Cluster.PythtestConformance),
3435
getPublishers(Cluster.Pythnet, symbol),
3536
getPublishers(Cluster.PythtestConformance, symbol),
3637
]);
@@ -96,7 +97,7 @@ const getPublishers = async (cluster: Cluster, symbol: string) => {
9697
]);
9798

9899
return (
99-
publishers?.map((publisher) => {
100+
publishers.map((publisher) => {
100101
const ranking = rankings.find(
101102
(ranking) =>
102103
ranking.publisher === publisher &&
@@ -110,6 +111,6 @@ const getPublishers = async (cluster: Cluster, symbol: string) => {
110111
cluster,
111112
knownPublisher: lookupPublisher(publisher),
112113
};
113-
}) ?? []
114+
})
114115
);
115116
};

apps/insights/src/components/PriceFeeds/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { AssetClassTable } from "./asset-class-table";
1919
import { ComingSoonList } from "./coming-soon-list";
2020
import styles from "./index.module.scss";
2121
import { PriceFeedsCard } from "./price-feeds-card";
22-
import { getFeedsCached } from "../../server/pyth";
22+
import { getFeeds } from "../../server/pyth/get-feeds";
2323
import { Cluster } from "../../services/pyth";
2424
import { priceFeeds as priceFeedsStaticConfig } from "../../static-data/price-feeds";
2525
import { activeChains } from "../../static-data/stats";
@@ -290,7 +290,7 @@ const FeaturedFeedsCard = <T extends ElementType>({
290290
);
291291

292292
const getPriceFeeds = async () => {
293-
const priceFeeds = await getFeedsCached(Cluster.Pythnet);
293+
const priceFeeds = await getFeeds(Cluster.Pythnet);
294294
const activeFeeds = priceFeeds.filter((feed) => isActive(feed));
295295
const comingSoon = priceFeeds.filter((feed) => !isActive(feed));
296296
return { activeFeeds, comingSoon };

apps/insights/src/components/Root/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from "../../config/server";
1212
import { LivePriceDataProvider } from "../../hooks/use-live-price-data";
1313
import { getPublishersCached } from '../../server/clickhouse';
14-
import { getFeedsCached } from '../../server/pyth';
14+
import { getFeeds } from '../../server/pyth/get-feeds';
1515
import { Cluster } from "../../services/pyth";
1616
import { PriceFeedIcon } from "../PriceFeedIcon";
1717
import { PublisherIcon } from "../PublisherIcon";
@@ -75,7 +75,7 @@ const getPublishersForSearchDialog = async (cluster: Cluster) => {
7575
};
7676

7777
const getFeedsForSearchDialog = async (cluster: Cluster) => {
78-
const feeds = await getFeedsCached(cluster);
78+
const feeds = await getFeeds(cluster);
7979

8080
return feeds.map((feed) => ({
8181
symbol: feed.symbol,

apps/insights/src/config/server.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// and load all env variables.
33
/* eslint-disable n/no-process-env */
44

5+
import { Redis } from 'ioredis';
56
import "server-only";
67

78
/**
@@ -56,3 +57,21 @@ export const SOLANA_RPC =
5657

5758
export const ENABLE_ACCESSIBILITY_REPORTING =
5859
!IS_PRODUCTION_SERVER && !process.env.DISABLE_ACCESSIBILITY_REPORTING;
60+
61+
62+
export async function getRedis(): Promise<Redis> {
63+
const host = process.env.REDIS_HOST;
64+
const port = process.env.REDIS_PORT;
65+
const password = process.env.REDIS_PASSWORD;
66+
if (!host || !port) {
67+
throw new Error('REDIS_HOST, and REDIS_PORT must be set');
68+
}
69+
const client = new Redis({
70+
username: 'default',
71+
password: password ?? '',
72+
host,
73+
port: Number.parseInt(port),
74+
});
75+
await client.set('test2', Math.random().toString());
76+
return client;
77+
}
Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,45 @@
11

22
import { getPublisherAverageScoreHistory, getPublisherRankingHistory, getPublishers, getRankingsByPublisher, getRankingsBySymbol } from "../services/clickhouse";
33
import { Cluster } from "../services/pyth";
4-
import { createChunkedCacheFetcher, fetchAllChunks, timeFunction } from '../utils/cache';
4+
import { createChunkedCacheFetcher, fetchAllChunks } from '../utils/cache';
55

66

77
const _getRankingsBySymbol = createChunkedCacheFetcher(async (symbol: string) => {
88
return getRankingsBySymbol(symbol);
99
}, 'getRankingsBySymbol');
1010

1111
export const getRankingsBySymbolCached = async (symbol: string) => {
12-
return timeFunction(async () => {
1312
return fetchAllChunks<ReturnType<typeof getRankingsBySymbol>, [string]>(_getRankingsBySymbol, symbol);
14-
}, 'getRankingsBySymbolCached');
1513
};
1614

1715
const _getRankingsByPublisher = createChunkedCacheFetcher(async (publisherKey: string) => {
1816
return getRankingsByPublisher(publisherKey);
1917
}, 'getRankingsByPublisher');
2018

2119
export const getRankingsByPublisherCached = async (publisherKey: string) => {
22-
return timeFunction(async () => {
2320
return fetchAllChunks<ReturnType<typeof getRankingsByPublisher>, [string]>(_getRankingsByPublisher, publisherKey);
24-
}, 'getRankingsByPublisherCached');
2521
};
2622

2723
const _getPublisherAverageScoreHistory = createChunkedCacheFetcher(async (cluster: Cluster, key: string) => {
2824
return getPublisherAverageScoreHistory(cluster, key);
2925
}, 'getPublisherAverageScoreHistory');
3026

3127
export const getPublisherAverageScoreHistoryCached = async (cluster: Cluster, key: string) => {
32-
return timeFunction(async () => {
3328
return fetchAllChunks<ReturnType<typeof getPublisherAverageScoreHistory>, [Cluster, string]>(_getPublisherAverageScoreHistory, cluster, key);
34-
}, 'getPublisherAverageScoreHistoryCached');
3529
};
3630

3731
const _getPublisherRankingHistory = createChunkedCacheFetcher(async (cluster: Cluster, key: string) => {
3832
return getPublisherRankingHistory(cluster, key);
3933
}, 'getPublisherRankingHistory');
4034

4135
export const getPublisherRankingHistoryCached = async (cluster: Cluster, key: string) => {
42-
return timeFunction(async () => {
4336
return fetchAllChunks<ReturnType<typeof getPublisherRankingHistory>, [Cluster, string]>(_getPublisherRankingHistory, cluster, key);
44-
}, 'getPublisherRankingHistoryCached');
4537
};
4638

4739
const _getPublishers = createChunkedCacheFetcher(async (cluster: Cluster) => {
4840
return getPublishers(cluster);
4941
}, 'getPublishers');
5042

5143
export const getPublishersCached = async (cluster: Cluster) => {
52-
return timeFunction(async () => {
5344
return fetchAllChunks<ReturnType<typeof getPublishers>, [Cluster]>(_getPublishers, cluster);
54-
}, 'getPublishersCached');
5545
}

apps/insights/src/server/pyth.ts

Lines changed: 16 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,24 @@
1-
import type { PythHttpClientResult } from '@pythnetwork/client/lib/PythHttpClient';
2-
import type { z } from 'zod';
1+
import { Cluster, priceFeedsSchema } from "../services/pyth";
2+
import { getFeeds } from './pyth/get-feeds';
3+
import { getPublishersForCluster } from './pyth/get-publishers-for-cluster';
34

4-
import { Cluster, clients, priceFeedsSchema } from "../services/pyth";
5-
import { createChunkedCacheFetcher, fetchAllChunks, timeFunction } from '../utils/cache';
6-
7-
8-
type CacheEntry = {
9-
data?: PythHttpClientResult;
10-
timestamp?: number;
11-
promise?: Promise<PythHttpClientResult>;
12-
};
13-
14-
const dataCache = new Map<Cluster, CacheEntry>();
15-
const CACHE_EXPIRY_MS = 24 * 60 * 60; // 1 day in seconds
16-
17-
const getDataCached = async (cluster: Cluster) => {
18-
const now = Date.now();
19-
const cached = dataCache.get(cluster);
20-
21-
// Check if cache exists and is not expired
22-
if (cached?.data && cached.timestamp && (now - cached.timestamp) < CACHE_EXPIRY_MS * 1000) {
23-
return cached.data;
24-
}
25-
26-
// Check if there's already a pending request
27-
if (cached?.promise) {
28-
return cached.promise;
29-
}
30-
31-
// eslint-disable-next-line no-console
32-
console.log('fetching fresh FULL data');
33-
34-
// Create a new promise for the request
35-
const promise = clients[cluster].getData().then((data) => {
36-
// Store the result in cache
37-
dataCache.set(cluster, { data, timestamp: now });
38-
return data;
39-
});
40-
41-
// Store the promise in cache to prevent duplicate requests
42-
dataCache.set(cluster, { promise });
43-
44-
return promise;
45-
};
46-
47-
const fetchFeeds = createChunkedCacheFetcher(async (cluster: Cluster) => {
48-
const unfilteredData = await getDataCached(cluster);
49-
// eslint-disable-next-line no-console
50-
console.log('fetchFeeds called');
51-
const filteredData = unfilteredData.symbols
52-
.filter(
53-
(symbol) =>
54-
unfilteredData.productFromSymbol.get(symbol)?.display_symbol !== undefined
55-
)
56-
.map((symbol) => ({
57-
symbol,
58-
product: unfilteredData.productFromSymbol.get(symbol),
59-
price: {
60-
...unfilteredData.productPrice.get(symbol),
61-
priceComponents:
62-
unfilteredData.productPrice
63-
.get(symbol)
64-
?.priceComponents.map(({ publisher }) => ({
65-
publisher: publisher.toBase58(),
66-
})) ?? [],
67-
},
68-
}));
69-
const parsedData = priceFeedsSchema.parse(filteredData);
70-
return parsedData;
71-
}, 'getFeeds');
72-
73-
const fetchPublishers = createChunkedCacheFetcher(async (cluster: Cluster) => {
74-
const data = await getDataCached(cluster);
75-
// eslint-disable-next-line no-console
76-
console.log('fetchPublishers called');
77-
const result: Record<string, string[]> = {};
78-
for (const key of data.productPrice.keys()) {
79-
const price = data.productPrice.get(key);
80-
result[key] = price?.priceComponents.map(({ publisher }) => publisher.toBase58()) ?? [];
81-
}
82-
return result;
83-
}, 'fetchPublishers');
84-
85-
export const getFeedsCached = async (cluster: Cluster) => {
86-
return timeFunction(async () => {
87-
return fetchAllChunks<z.infer<typeof priceFeedsSchema>, [Cluster]>(fetchFeeds, cluster);
88-
}, 'getFeedsCached');
89-
};
90-
91-
export const getPublishersForFeedCached = async (cluster: Cluster, symbol: string) => {
92-
const data = await timeFunction(async () => {
93-
return fetchAllChunks<Record<string, string[]>, [Cluster]>(fetchPublishers, cluster);
94-
}, 'getPublishersForFeedCached');
95-
return data[symbol]
96-
};
5+
// Convenience helpers matching your previous functions
6+
export async function getPublishersForFeedCached(
7+
cluster: Cluster,
8+
symbol: string
9+
) {
10+
const map = await getPublishersForCluster(cluster);
11+
return map[symbol] ?? [];
12+
}
9713

98-
export const getFeedsForPublisherCached = async (
14+
export async function getFeedsForPublisherCached(
9915
cluster: Cluster,
10016
publisher: string
101-
) => {
102-
const data = await timeFunction(async () => {
103-
return getFeedsCached(cluster);
104-
}, 'getFeedsForPublisherCached');
17+
) {
18+
const data = await getFeeds(cluster);
10519
return priceFeedsSchema.parse(
10620
data.filter(({ price }) =>
107-
price.priceComponents.some(
108-
(component) => component.publisher.toString() === publisher
109-
)
21+
price.priceComponents.some((c) => c.publisher === publisher)
11022
)
11123
);
112-
};
24+
}

0 commit comments

Comments
 (0)