Skip to content

Commit 2959f7c

Browse files
committed
fix: Properly cache all updates for 45 minutes.
1 parent d37e833 commit 2959f7c

File tree

3 files changed

+228
-35
lines changed

3 files changed

+228
-35
lines changed

apps/www/src/app/api/checkupdates/route.ts

Lines changed: 220 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,121 @@
11
import { NextResponse } from 'next/server';
22
import { FetchPlugins } from '../v1/plugins/GetPlugins';
33
import { GraphQLUpdates } from '../v2/GraphQLHandler';
4-
import { GithubGraphQL } from '../v2/GraphQLInterop';
5-
import { GetPluginMetadata } from '../v1/plugins/GetPluginMetadata';
4+
import { Database, firebaseAdmin } from '../Firebase';
65

76
export const revalidate = 1800;
7+
const CACHE_DURATION_MS = 30 * 60 * 1000;
8+
9+
const createSafeCacheKey = (owner: string, repo: string): string => {
10+
const combined = `${owner}__${repo}`;
11+
const sanitized = combined.replace(/[^a-zA-Z0-9_-]/g, '_');
12+
const withoutLeadingDot = sanitized.replace(/^\.+/, '_');
13+
14+
const maxLength = 1500;
15+
const truncated = withoutLeadingDot.length > maxLength ? withoutLeadingDot.substring(0, maxLength) : withoutLeadingDot;
16+
17+
const cleaned = truncated || 'unknown_repo';
18+
return cleaned.replace(/__+/g, '__');
19+
};
20+
21+
const isCacheValid = (timestamp: FirebaseFirestore.Timestamp): boolean => {
22+
const now = new Date();
23+
const cacheTime = timestamp.toDate();
24+
return now.getTime() - cacheTime.getTime() < CACHE_DURATION_MS;
25+
};
26+
27+
const getCachedRepositoryData = async (owner: string, repo: string): Promise<CachedRepositoryData | null> => {
28+
try {
29+
const cacheKey = createSafeCacheKey(owner, repo);
30+
const docRef = Database.collection('UpdateCache').doc(cacheKey);
31+
const doc = await docRef.get();
32+
33+
if (!doc.exists) {
34+
return null;
35+
}
36+
37+
const data = doc.data() as UpdateCacheEntry;
38+
if (data && isCacheValid(data.timestamp)) {
39+
console.log(`Found valid cached data for ${owner}/${repo} (key: ${cacheKey})`);
40+
return data.data;
41+
} else if (data) {
42+
// Remove expired entry
43+
await docRef.delete();
44+
console.log(`Removed expired cache entry for ${owner}/${repo} (key: ${cacheKey})`);
45+
}
46+
47+
return null;
48+
} catch (error) {
49+
console.error(`Error retrieving cached data for ${owner}/${repo}:`, error);
50+
return null;
51+
}
52+
};
53+
54+
const setCachedRepositoryData = async (owner: string, repo: string, data: CachedRepositoryData): Promise<void> => {
55+
try {
56+
const cacheKey = createSafeCacheKey(owner, repo);
57+
const docRef = Database.collection('UpdateCache').doc(cacheKey);
58+
const now = new Date();
59+
60+
const cacheEntry: UpdateCacheEntry = {
61+
owner,
62+
repo,
63+
data,
64+
timestamp: firebaseAdmin.firestore.Timestamp.fromDate(now),
65+
expiresAt: firebaseAdmin.firestore.Timestamp.fromDate(new Date(now.getTime() + CACHE_DURATION_MS)),
66+
};
67+
68+
await docRef.set(cacheEntry);
69+
console.log(`Cached repository data for ${owner}/${repo} (key: ${cacheKey}) for ${CACHE_DURATION_MS / (1000 * 60)} minutes`);
70+
} catch (error) {
71+
console.error(`Error caching data for ${owner}/${repo}:`, error);
72+
}
73+
};
74+
75+
const getCachedPluginData = async (type: 'allPlugins'): Promise<any | null> => {
76+
try {
77+
const docRef = Database.collection('PluginCache').doc(type);
78+
const doc = await docRef.get();
79+
80+
if (!doc.exists) {
81+
return null;
82+
}
83+
84+
const data = doc.data() as PluginCacheEntry;
85+
if (data && isCacheValid(data.timestamp)) {
86+
console.log(`Found valid cached ${type} data`);
87+
return data.data;
88+
} else if (data) {
89+
// Remove expired entry
90+
await docRef.delete();
91+
console.log(`Removed expired ${type} cache entry`);
92+
}
93+
94+
return null;
95+
} catch (error) {
96+
console.error(`Error retrieving cached ${type} data:`, error);
97+
return null;
98+
}
99+
};
100+
101+
const setCachedPluginData = async (type: 'allPlugins', data: any): Promise<void> => {
102+
try {
103+
const docRef = Database.collection('PluginCache').doc(type);
104+
const now = new Date();
105+
106+
const cacheEntry: PluginCacheEntry = {
107+
type,
108+
data,
109+
timestamp: firebaseAdmin.firestore.Timestamp.fromDate(now),
110+
expiresAt: firebaseAdmin.firestore.Timestamp.fromDate(new Date(now.getTime() + CACHE_DURATION_MS)),
111+
};
112+
113+
await docRef.set(cacheEntry);
114+
console.log(`Cached ${type} data for ${CACHE_DURATION_MS / (1000 * 60)} minutes`);
115+
} catch (error) {
116+
console.error(`Error caching ${type} data:`, error);
117+
}
118+
};
8119

9120
interface PluginUpdateCheck {
10121
id: string;
@@ -25,53 +136,130 @@ interface PluginUpdateStatus {
25136
pluginInfo: any;
26137
}
27138

28-
async function CheckForThemeUpdates(requestBody) {
29-
// Check if themes are provided
139+
interface CachedRepositoryData {
140+
download: string;
141+
name: string;
142+
commit: string;
143+
url: string;
144+
date: string;
145+
message: string;
146+
}
147+
148+
interface UpdateCacheEntry {
149+
owner: string;
150+
repo: string;
151+
data: CachedRepositoryData;
152+
timestamp: FirebaseFirestore.Timestamp;
153+
expiresAt: FirebaseFirestore.Timestamp;
154+
}
155+
156+
interface ThemeUpdateRequest {
157+
owner: string;
158+
repo: string;
159+
}
160+
161+
interface PluginCacheEntry {
162+
type: 'allPlugins';
163+
data: any;
164+
timestamp: FirebaseFirestore.Timestamp;
165+
expiresAt: FirebaseFirestore.Timestamp;
166+
}
167+
168+
async function CheckForThemeUpdates(requestBody: ThemeUpdateRequest[]) {
30169
if (!requestBody || requestBody.length === 0) {
31170
return [];
32171
}
33172

34-
const graphQLHandler = new GraphQLUpdates();
35-
requestBody.forEach((item) => graphQLHandler.add(item.owner, item.repo));
36-
37-
return new Promise(async (resolve) => {
38-
resolve(
39-
Object.values((await GithubGraphQL.Post(graphQLHandler.get())).data).map((repository, i) => ({
40-
download: `https://codeload.github.com/${requestBody[i]?.owner}/${requestBody[i]?.repo}/zip/refs/heads/${repository?.default_branch?.name}`,
41-
name: repository?.name ?? null,
42-
commit: repository?.defaultBranchRef?.target?.oid ?? null,
43-
url: repository?.defaultBranchRef?.target?.commitUrl ?? null,
44-
date: repository?.defaultBranchRef?.target?.committedDate ?? null,
45-
message: repository?.defaultBranchRef?.target?.history?.edges[0]?.node.message ?? null,
46-
})),
47-
);
173+
const results: CachedRepositoryData[] = [];
174+
const itemsToFetch: ThemeUpdateRequest[] = [];
175+
const cachePromises: Promise<void>[] = [];
176+
177+
for (const item of requestBody) {
178+
const cachedData = await getCachedRepositoryData(item.owner, item.repo);
179+
180+
if (cachedData) {
181+
results.push(cachedData);
182+
} else {
183+
itemsToFetch.push(item);
184+
}
185+
}
186+
187+
if (itemsToFetch.length > 0) {
188+
const graphQLHandler = new GraphQLUpdates();
189+
itemsToFetch.forEach((item) => graphQLHandler.add(item.owner, item.repo));
190+
191+
try {
192+
// Manual fetch to GitHub GraphQL API to avoid caching conflicts
193+
const response = await fetch('https://api.github.com/graphql', {
194+
method: 'POST',
195+
headers: {
196+
'Content-Type': 'application/json',
197+
...(process.env.BEARER ? { Authorization: process.env.BEARER } : {}),
198+
},
199+
body: JSON.stringify({ query: graphQLHandler.get() }),
200+
});
201+
202+
const json = await response.json();
203+
const fetchedData = Object.values(json.data).map((repository: any, i: number) => {
204+
const item = itemsToFetch[i];
205+
const repositoryData: CachedRepositoryData = {
206+
download: `https://codeload.github.com/${item?.owner}/${item?.repo}/zip/refs/heads/${repository?.default_branch?.name}`,
207+
name: repository?.name ?? null,
208+
commit: repository?.defaultBranchRef?.target?.oid ?? null,
209+
url: repository?.defaultBranchRef?.target?.commitUrl ?? null,
210+
date: repository?.defaultBranchRef?.target?.committedDate ?? null,
211+
message: repository?.defaultBranchRef?.target?.history?.edges[0]?.node.message ?? null,
212+
};
213+
214+
cachePromises.push(setCachedRepositoryData(item.owner, item.repo, repositoryData));
215+
216+
return repositoryData;
217+
});
218+
219+
results.push(...fetchedData);
220+
} catch (error) {
221+
console.error('Error fetching missing repositories from GitHub:', error);
222+
throw error;
223+
}
224+
}
225+
226+
Promise.all(cachePromises).catch((error) => {
227+
console.error('Error updating cache entries:', error);
48228
});
229+
230+
const orderedResults: CachedRepositoryData[] = [];
231+
for (const item of requestBody) {
232+
const result = results.find((r) => r.name === item.repo);
233+
if (result) {
234+
orderedResults.push(result);
235+
}
236+
}
237+
238+
return orderedResults;
49239
}
50240

51241
async function CheckForPluginUpdates(plugins: PluginUpdateCheck[]) {
52-
// Check if plugins are provided
53242
if (!plugins || plugins.length === 0) {
54243
return [];
55244
}
56245

57-
// Try to get cached data
58-
const cachedAllPlugins = global.requestCache.get('allPlugins');
59-
const cachedMetadata = global.requestCache.get('pluginMetadata');
246+
let cachedData = await getCachedPluginData('allPlugins');
247+
let allPlugins: any;
248+
let metadata: any;
60249

61-
let allPlugins, metadata;
250+
if (!cachedData) {
251+
const fetchResult = await FetchPlugins();
252+
allPlugins = fetchResult.pluginData;
253+
metadata = fetchResult.metadata;
62254

63-
if (cachedAllPlugins && cachedMetadata) {
64-
allPlugins = cachedAllPlugins;
65-
metadata = cachedMetadata;
255+
setCachedPluginData('allPlugins', fetchResult).catch((error) => {
256+
console.error('Error caching plugin data:', error);
257+
});
66258
} else {
67-
[allPlugins, metadata] = await Promise.all([FetchPlugins(), GetPluginMetadata()]);
68-
69-
// Cache the results for 1 hour
70-
global.requestCache.set('allPlugins', allPlugins, 3600);
71-
global.requestCache.set('pluginMetadata', metadata, 3600);
259+
allPlugins = cachedData.pluginData;
260+
metadata = cachedData.metadata;
72261
}
73262

74-
// Check update status for all plugins
75263
const pluginStatuses: PluginUpdateStatus[] = plugins.map((plugin) => {
76264
const metadataEntry = metadata.find((m) => m.id === plugin.id);
77265
const pluginInfo = allPlugins.find((p) => p.initCommitId === plugin.id);

apps/www/src/app/api/v1/plugins/GetPluginData.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ export interface PluginDataProps {
3636
downloadUrl?: string;
3737
}
3838

39+
export interface PluginDataTable {
40+
pluginData: PluginDataProps[];
41+
metadata: { id: string; commitId: string }[];
42+
}
43+
3944
const GetPluginData = (pluginList) => {
4045
return new Promise<PluginDataProps[]>(async (resolve, reject) => {
4146
const query = `

apps/www/src/app/api/v1/plugins/GetPlugins.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Database, StorageBucket } from '../../Firebase';
2-
import { GetPluginData, PluginDataProps } from './GetPluginData';
2+
import { GetPluginData, PluginDataProps, PluginDataTable } from './GetPluginData';
33
import { GetPluginMetadata } from './GetPluginMetadata';
44
import { RetrievePluginList } from './GetPluginList';
55

@@ -11,7 +11,7 @@ const FormatBytes = (bytes: number, decimals = 2) => {
1111
};
1212

1313
export const FetchPlugins = async () => {
14-
return new Promise<PluginDataProps[]>(async (resolve, reject) => {
14+
return new Promise<PluginDataTable>(async (resolve, reject) => {
1515
try {
1616
const pluginList = await RetrievePluginList();
1717

@@ -51,7 +51,7 @@ export const FetchPlugins = async () => {
5151
}
5252
}
5353

54-
resolve(pluginData);
54+
resolve({ pluginData, metadata });
5555
} catch (error) {
5656
console.error('An error occurred while processing plugins:', error);
5757
reject(error);

0 commit comments

Comments
 (0)