Skip to content

Commit 0c95d01

Browse files
committed
fix: Fix NextJS caching issues. Closes #56
1 parent 72f15c4 commit 0c95d01

10 files changed

Lines changed: 2802 additions & 321 deletions

File tree

apps/docs/src/plugins/lua/http.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ end
273273

274274
### Error handling
275275

276-
```/dev/null/examples/error_handling.lua#L1-12
276+
```lua
277277
local http = require("http")
278278
local res, err = http.get("https://api.example.com/data")
279279
if not res then
@@ -292,7 +292,7 @@ end
292292

293293
### JSON APIs
294294

295-
```/dev/null/examples/json_api.lua#L1-16
295+
```lua
296296
local http = require("http")
297297
local cjson = require("json")
298298

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

Lines changed: 1 addition & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,8 @@
11
import { NextResponse } from 'next/server';
22
import { FetchPlugins } from '../v1/plugins/GetPlugins';
33
import { FetchThemes } from '../v2/FetchThemes';
4-
import { Database, firebaseAdmin } from '../Firebase';
54

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

877
interface PluginUpdateCheck {
888
id: string;

apps/www/src/app/api/v1/plugin/[slug]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { StorageBucket } from '../../../Firebase';
22
import { FetchPlugins } from '../../plugins/GetPlugins';
33

4-
export const revalidate = 60;
4+
export const dynamic = 'force-dynamic';
55

66
const FindPlugin = async (id: string) => {
77
const plugin = (await FetchPlugins()).pluginData.find((plugin) => plugin.id === id);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FetchPlugins } from './GetPlugins';
22

3-
export const revalidate = 60;
3+
export const dynamic = 'force-dynamic';
44

55
export async function GET(request: Request) {
66
return Response.json((await FetchPlugins()).pluginData);

apps/www/src/app/api/v2/GraphQLInterop.ts

Lines changed: 25 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -1,229 +1,34 @@
1-
// Cache duration: 30 minutes (in milliseconds)
2-
const CACHE_DURATION_MS = 30 * 60 * 1000;
3-
4-
const simpleHash = (str: string): string => {
5-
let hash = 5381;
6-
for (let i = 0; i < str.length; i++) {
7-
hash = (hash << 5) + hash + str.charCodeAt(i);
8-
}
9-
10-
return Math.abs(hash).toString(16).padStart(8, '0');
11-
};
12-
13-
const createCacheKey = (query: string): string => {
14-
const normalizedQuery = query.replace(/\s+/g, ' ').trim();
15-
const hash = simpleHash(normalizedQuery);
16-
17-
const queryTypePrefix = normalizedQuery.includes('repository(owner:') ? 'single_' : normalizedQuery.includes('_0: repository') ? 'batch_' : 'other_';
18-
const cacheKey = `${queryTypePrefix}${hash}`;
19-
return cacheKey;
20-
};
21-
22-
const getDatabase = () => {
23-
if (typeof global !== 'undefined' && global.Database) {
24-
return global.Database;
25-
}
26-
return null;
27-
};
28-
29-
const isCacheEntryValid = (timestamp: any): boolean => {
30-
if (!timestamp || !timestamp.toDate) return false;
31-
const now = new Date();
32-
const cacheTime = timestamp.toDate();
33-
return now.getTime() - cacheTime.getTime() < CACHE_DURATION_MS;
34-
};
35-
36-
const getCachedData = async (cacheKey: string) => {
37-
try {
38-
const db = getDatabase();
39-
if (!db) return null;
40-
41-
const docRef = db.collection('GraphQLCache').doc(cacheKey);
42-
const doc = await docRef.get();
43-
44-
if (!doc.exists) {
45-
return null;
46-
}
47-
48-
const data = doc.data();
49-
if (data && isCacheEntryValid(data.timestamp)) {
50-
return data.response;
51-
} else if (data) {
52-
await docRef.delete();
53-
}
54-
55-
return null;
56-
} catch (error) {
57-
console.error('Error retrieving cached data:', error);
58-
return null;
59-
}
60-
};
61-
62-
const setCachedData = async (cacheKey: string, response: any, query: string) => {
63-
try {
64-
const db = getDatabase();
65-
if (!db) return;
66-
67-
const docRef = db.collection('GraphQLCache').doc(cacheKey);
68-
await docRef.set({
69-
response: response,
70-
timestamp: new Date(),
71-
expiresAt: new Date(Date.now() + CACHE_DURATION_MS),
72-
});
73-
} catch (error) {
74-
console.error('Error caching data:', error);
75-
}
76-
};
77-
781
const GithubGraphQL = {
79-
cleanupExpiredEntries: async (): Promise<void> => {
80-
try {
81-
const db = getDatabase();
82-
if (!db) return;
83-
84-
const now = new Date();
85-
const expiredQuery = db.collection('GraphQLCache').where('expiresAt', '<', now);
86-
87-
const snapshot = await expiredQuery.get();
88-
89-
if (snapshot.empty) {
90-
return;
2+
Post: async (body: string): Promise<{ data: Record<string, any> }> => {
3+
const rateLimitFields = `
4+
rateLimit {
5+
limit
6+
cost
7+
remaining
8+
resetAt
919
}
10+
`;
11+
12+
const queryWithRateLimit = body.replace(/}(\s*)$/, `${rateLimitFields}}$1`);
13+
14+
const response = await fetch('https://api.github.com/graphql', {
15+
method: 'POST',
16+
headers: {
17+
'Content-Type': 'application/json',
18+
...(process.env.BEARER ? { Authorization: process.env.BEARER } : {}),
19+
},
20+
body: JSON.stringify({ query: queryWithRateLimit }),
21+
cache: 'no-store',
22+
});
9223

93-
const batch = db.batch();
94-
snapshot.docs.forEach((doc) => {
95-
batch.delete(doc.ref);
96-
});
97-
98-
await batch.commit();
99-
} catch (error) {
100-
console.error('Error cleaning up expired cache:', error);
101-
}
102-
},
103-
104-
clearCache: async (): Promise<void> => {
105-
try {
106-
const db = getDatabase();
107-
if (!db) {
108-
return;
109-
}
110-
111-
const batch = db.batch();
112-
const snapshot = await db.collection('GraphQLCache').get();
113-
114-
snapshot.docs.forEach((doc) => {
115-
batch.delete(doc.ref);
116-
});
117-
118-
await batch.commit();
119-
} catch (error) {
120-
console.error('Error clearing GraphQL cache:', error);
121-
}
122-
},
123-
124-
getCacheStats: async () => {
125-
try {
126-
const db = getDatabase();
127-
if (!db) {
128-
return {
129-
totalEntries: 0,
130-
validEntries: 0,
131-
expiredEntries: 0,
132-
cacheDurationMinutes: CACHE_DURATION_MS / (1000 * 60),
133-
cacheAvailable: false,
134-
};
135-
}
136-
137-
const snapshot = await db.collection('GraphQLCache').get();
138-
let validEntries = 0;
139-
let expiredEntries = 0;
140-
141-
snapshot.docs.forEach((doc) => {
142-
const data = doc.data();
143-
if (isCacheEntryValid(data.timestamp)) {
144-
validEntries++;
145-
} else {
146-
expiredEntries++;
147-
}
148-
});
24+
const json = await response.json();
14925

150-
return {
151-
totalEntries: snapshot.size,
152-
validEntries,
153-
expiredEntries,
154-
cacheDurationMinutes: CACHE_DURATION_MS / (1000 * 60),
155-
cacheAvailable: true,
156-
};
157-
} catch (error) {
158-
console.error('Error getting cache stats:', error);
159-
return {
160-
totalEntries: 0,
161-
validEntries: 0,
162-
expiredEntries: 0,
163-
cacheDurationMinutes: CACHE_DURATION_MS / (1000 * 60),
164-
cacheAvailable: false,
165-
error: error.message,
166-
};
26+
if (json.data && json.data.rateLimit) {
27+
const { rateLimit, ...dataWithoutRateLimit } = json.data;
28+
json.data = dataWithoutRateLimit;
16729
}
168-
},
169-
170-
Post: async (body: string) => {
171-
return new Promise<{ data: Record<string, any> }>(async (resolve, reject) => {
172-
try {
173-
const cacheKey = createCacheKey(body);
174-
175-
const cachedResponse = await getCachedData(cacheKey);
176-
if (cachedResponse) {
177-
if (!cachedResponse.data) {
178-
console.warn('Evicting bad cached response for key:', cacheKey);
179-
const db = getDatabase();
180-
if (db) await db.collection('GraphQLCache').doc(cacheKey).delete();
181-
} else {
182-
resolve(cachedResponse);
183-
return;
184-
}
185-
}
186-
187-
const rateLimitFields = `
188-
rateLimit {
189-
limit
190-
cost
191-
remaining
192-
resetAt
193-
}
194-
`;
195-
196-
const queryWithRateLimit = body.replace(/}(\s*)$/, `${rateLimitFields}}$1`);
19730

198-
const response = await fetch('https://api.github.com/graphql', {
199-
method: 'POST',
200-
headers: {
201-
'Content-Type': 'application/json',
202-
...(process.env.BEARER ? { Authorization: process.env.BEARER } : {}),
203-
},
204-
body: JSON.stringify({ query: queryWithRateLimit }),
205-
next: { revalidate: 1800 },
206-
});
207-
208-
const json = await response.json();
209-
210-
if (json.data && json.data.rateLimit) {
211-
const { rateLimit, ...dataWithoutRateLimit } = json.data;
212-
json.data = dataWithoutRateLimit;
213-
}
214-
215-
if (json.data) {
216-
setCachedData(cacheKey, json, body).catch((error) => {
217-
console.error('Failed to cache response:', error);
218-
});
219-
}
220-
221-
resolve(json);
222-
} catch (error) {
223-
console.error('GraphQL request failed:', error);
224-
reject(error);
225-
}
226-
});
31+
return json;
22732
},
22833
};
22934

apps/www/src/app/api/v2/details/[slug]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FetchThemes, ThemeData } from '../../FetchThemes';
22

3-
export const revalidate = 60;
3+
export const dynamic = 'force-dynamic';
44

55
interface DiscordInfo {
66
name?: string;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FetchThemes } from './FetchThemes';
22

3-
export const revalidate = 60;
3+
export const dynamic = 'force-dynamic';
44

55
export async function GET(request: Request) {
66
const themes = await FetchThemes();

0 commit comments

Comments
 (0)