Skip to content

Commit f226d34

Browse files
committed
feat: chunk cache
1 parent f7c4e0d commit f226d34

File tree

4 files changed

+42
-140
lines changed

4 files changed

+42
-140
lines changed

apps/insights/src/app/actions.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

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

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { getStatus } from "../../status";
1111
import { PublisherIcon } from "../PublisherIcon";
1212
import { PublisherTag } from "../PublisherTag";
1313
import { PublishersCard } from "./publishers-card";
14-
import { funcA, funcAUnstableCache } from '../../app/actions';
1514

1615
type Props = {
1716
params: Promise<{
@@ -20,24 +19,6 @@ type Props = {
2019
};
2120

2221

23-
const funcB = async () => {
24-
const start = performance.now();
25-
const res = await funcA();
26-
const end = performance.now();
27-
// eslint-disable-next-line no-console, @typescript-eslint/restrict-template-expressions
28-
console.log(`funcB: ${end - start}ms`);
29-
return res;
30-
}
31-
32-
const funcBUnstableCache = async () => {
33-
const start = performance.now();
34-
const res = await funcAUnstableCache();
35-
const end = performance.now();
36-
// eslint-disable-next-line no-console, @typescript-eslint/restrict-template-expressions
37-
console.log(`funcBUnstableCache: ${end - start}ms`);
38-
return res;
39-
}
40-
4122
export const Publishers = async ({ params }: Props) => {
4223
const { slug } = await params;
4324
const symbol = decodeURIComponent(slug);
@@ -46,19 +27,12 @@ export const Publishers = async ({ params }: Props) => {
4627
pythtestConformanceFeeds,
4728
pythnetPublishers,
4829
pythtestConformancePublishers,
49-
funcRes,
50-
funcResUnstableCache,
5130
] = await Promise.all([
5231
getFeedsCached(Cluster.Pythnet),
5332
getFeedsCached(Cluster.PythtestConformance),
5433
getPublishers(Cluster.Pythnet, symbol),
5534
getPublishers(Cluster.PythtestConformance, symbol),
56-
funcB(),
57-
funcBUnstableCache(),
5835
]);
59-
60-
// eslint-disable-next-line no-console, @typescript-eslint/restrict-template-expressions
61-
console.log(funcRes, funcResUnstableCache);
6236
const feed = pythnetFeeds.find((feed) => feed.symbol === symbol);
6337
const testFeed = pythtestConformanceFeeds.find(
6438
(feed) => feed.symbol === symbol,

apps/insights/src/server/pyth.ts

Lines changed: 35 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,67 @@
1-
import { unstable_cache } from "next/cache";
21
import { cache } from 'react';
3-
import superjson from "superjson";
4-
import { z } from "zod";
2+
import type { z } from 'zod';
53

6-
// Your imports
74
import { Cluster, clients, priceFeedsSchema } from "../services/pyth";
5+
import { createChunkedCacheFetcher, fetchAllChunks } from '../utils/cache';
86

97
const getDataCached = cache(async (cluster: Cluster) => {
108
return clients[cluster].getData();
119
});
12-
const MAX_CACHE_SIZE_STRING = 2 * 1024 * 1024 - 510_000; // is the buffer for the returned object and the metadata from next
1310

14-
const getPublishersForFeed = unstable_cache(async (cluster: Cluster, chunk?: number) => {
11+
const fetchFeeds = createChunkedCacheFetcher(async (cluster: Cluster) => {
12+
const unfilteredData = await getDataCached(cluster);
13+
const filteredData = unfilteredData.symbols
14+
.filter(
15+
(symbol) =>
16+
unfilteredData.productFromSymbol.get(symbol)?.display_symbol !== undefined
17+
)
18+
.map((symbol) => ({
19+
symbol,
20+
product: unfilteredData.productFromSymbol.get(symbol),
21+
price: {
22+
...unfilteredData.productPrice.get(symbol),
23+
priceComponents:
24+
unfilteredData.productPrice
25+
.get(symbol)
26+
?.priceComponents.map(({ publisher }) => ({
27+
publisher: publisher.toBase58(),
28+
})) ?? [],
29+
},
30+
}));
31+
const parsedData = priceFeedsSchema.parse(filteredData);
32+
return parsedData;
33+
}, 'getfeeds');
34+
35+
36+
const fetchPublishers = createChunkedCacheFetcher(async (cluster: Cluster) => {
1537
const data = await getDataCached(cluster);
1638
const result: Record<string, string[]> = {};
1739
for (const key of data.productPrice.keys()) {
1840
const price = data.productPrice.get(key);
1941
result[key] = price?.priceComponents.map(({ publisher }) => publisher.toBase58()) ?? [];
2042
}
21-
const stringifiedResult = superjson.stringify(result);
22-
23-
const chunksNumber = Math.ceil(stringifiedResult.length / MAX_CACHE_SIZE_STRING);
24-
const chunks = [];
25-
for(let i = 0; i < chunksNumber; i++) {
26-
chunks.push(stringifiedResult.slice(i * MAX_CACHE_SIZE_STRING, (i + 1) * MAX_CACHE_SIZE_STRING));
27-
}
28-
return {
29-
chunk: chunks[chunk ?? 0],
30-
chunksNumber,
31-
};
32-
}, [], { revalidate: false });
33-
34-
const _getFeeds = unstable_cache(async (cluster: Cluster, chunk?: number) => {
35-
// eslint-disable-next-line no-console
36-
console.log('getFeeds', cluster, chunk);
37-
const data = await getDataCached(cluster);
38-
const parsedData = priceFeedsSchema.parse(
39-
data.symbols
40-
.filter(
41-
(symbol) =>
42-
data.productFromSymbol.get(symbol)?.display_symbol !== undefined
43-
)
44-
.map((symbol) => ({
45-
symbol,
46-
product: data.productFromSymbol.get(symbol),
47-
price: {
48-
...data.productPrice.get(symbol),
49-
priceComponents:
50-
data.productPrice
51-
.get(symbol)
52-
?.priceComponents.map(({ publisher }) => ({
53-
publisher: publisher.toBase58(),
54-
})) ?? [],
55-
},
56-
}))
57-
)
58-
const result = superjson.stringify(
59-
parsedData
60-
);
61-
const chunksNumber = Math.ceil(result.length / MAX_CACHE_SIZE_STRING);
62-
const chunks = [];
63-
for(let i = 0; i < chunksNumber; i++) {
64-
chunks.push(result.slice(i * MAX_CACHE_SIZE_STRING, (i + 1) * MAX_CACHE_SIZE_STRING));
65-
}
66-
return {
67-
chunk: chunks[chunk ?? 0],
68-
chunksNumber,
69-
};
70-
}, [], { revalidate: false });
43+
return result;
44+
}, 'getpublishers');
7145

7246
export const getFeedsCached = async (cluster: Cluster) => {
73-
const start = performance.now();
74-
const { chunk, chunksNumber } = await _getFeeds(cluster); // uses cache
75-
const rawResults = await Promise.all(Array.from({ length: chunksNumber-1 }, (_, i) => _getFeeds(cluster, i+1)));
76-
const rawJson = [chunk, ...rawResults.map(({ chunk }) => chunk)].join('');
77-
const data = superjson.parse<z.infer<typeof priceFeedsSchema>>(rawJson);
78-
const end = performance.now();
79-
// eslint-disable-next-line no-console, @typescript-eslint/restrict-template-expressions
80-
console.log(`getFeedsCached: ${end - start}ms`);
81-
return data;
47+
return fetchAllChunks<z.infer<typeof priceFeedsSchema>, [Cluster]>(fetchFeeds, cluster)
8248
};
8349

8450
export const getPublishersForFeedCached = async (cluster: Cluster, symbol: string) => {
85-
const start = performance.now();
86-
const { chunk, chunksNumber } = await getPublishersForFeed(cluster); // uses cache
87-
const rawResults = await Promise.all(Array.from({ length: chunksNumber-1 }, (_, i) => getPublishersForFeed(cluster, i+1)));
88-
const rawJson = [chunk, ...rawResults.map(({ chunk }) => chunk)].join('');
89-
const data = superjson.parse<Record<string, string[]>>(rawJson);
90-
const end = performance.now();
91-
// eslint-disable-next-line no-console, @typescript-eslint/restrict-template-expressions
92-
console.log(`getPublishersForFeedCached: ${end - start}ms`);
93-
return data[symbol];
51+
const data = await fetchAllChunks<Record<string, string[]>, [Cluster]>(fetchPublishers, cluster);
52+
return data[symbol]
9453
};
9554

9655
export const getFeedsForPublisherCached = async (
9756
cluster: Cluster,
9857
publisher: string
9958
) => {
100-
const start = performance.now();
101-
const data = await getFeedsCached(cluster); // uses cache
102-
const end = performance.now();
103-
// eslint-disable-next-line no-console, @typescript-eslint/restrict-template-expressions
104-
console.log(`getFeedsForPublisherCached: ${end - start}ms.`);
59+
const data = await getFeedsCached(cluster);
10560
return priceFeedsSchema.parse(
10661
data.filter(({ price }) =>
10762
price.priceComponents.some(
10863
(component) => component.publisher.toString() === publisher
10964
)
11065
)
11166
);
112-
};
67+
};

apps/insights/src/utils/cache.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { unstable_cache } from "next/cache";
22
import superjson from "superjson";
33

4-
const MAX_CACHE_SIZE_STRING = 2 * 1024 * 1024 - 510_000; // your buffer size
4+
const MAX_CACHE_SIZE_STRING = 2 * 1024 * 1024 - 510_000; // buffer size, subtracted some of the nextjs overhead added to each cache entry
5+
const REVALIDATE_TIME = 60 * 60 * 24; // 24 hours
6+
57

68
type ChunkedCacheResult = {
79
chunk: string;
@@ -17,9 +19,9 @@ type CacheFetcher<Args extends unknown[]> = (...args: [...Args, number?]) => Pro
1719
* @returns a new async function that fetches chunked cached data with the same args plus optional chunk number
1820
*/
1921
export function createChunkedCacheFetcher<T, Args extends unknown[]>(
20-
fetchFullData: (...args: Args) => Promise<T>
22+
fetchFullData: (...args: Args) => Promise<T>,
23+
key: string,
2124
): CacheFetcher<Args> {
22-
// chunk number default 0 = first chunk
2325
return unstable_cache(
2426
async (...argsWithChunk: [...Args, number?]) => {
2527
const [args, chunk] = (() => {
@@ -29,8 +31,6 @@ export function createChunkedCacheFetcher<T, Args extends unknown[]>(
2931
return [argsWithChunk.slice(0, -1) as Args, argsWithChunk.at(-1) ?? 0] as [Args, number];
3032
})();
3133

32-
// Fetch fresh full data and serialize it
33-
// Could memoize inside this function but using Next's cache instead
3434
const fullData = await fetchFullData(...args);
3535
const serialized = superjson.stringify(fullData);
3636

@@ -48,8 +48,8 @@ export function createChunkedCacheFetcher<T, Args extends unknown[]>(
4848
chunksNumber,
4949
};
5050
},
51-
[],
52-
{ revalidate: false }
51+
[key],
52+
{ revalidate: REVALIDATE_TIME }
5353
);
5454
}
5555

@@ -67,13 +67,11 @@ export async function fetchAllChunks<T, Args extends unknown[]>(
6767
if (chunksNumber <= 1) {
6868
return superjson.parse(firstChunkData.chunk);
6969
}
70-
7170
const otherChunks = await Promise.all(
7271
Array.from({ length: chunksNumber - 1 }, (_, i) => fetcher(...args, i + 1))
7372
);
7473

7574
const fullString =
7675
firstChunkData.chunk + otherChunks.map(({ chunk }) => chunk).join("");
77-
7876
return superjson.parse(fullString);
7977
}

0 commit comments

Comments
 (0)