|
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 | | - |
78 | 1 | 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 |
91 | 9 | } |
| 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 | + }); |
92 | 23 |
|
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(); |
149 | 25 |
|
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; |
167 | 29 | } |
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`); |
197 | 30 |
|
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; |
227 | 32 | }, |
228 | 33 | }; |
229 | 34 |
|
|
0 commit comments