Skip to content

Commit 5faf824

Browse files
Merge pull request AOSSIE-Org#80 from Ri1tik/improvement/rate-limit-live-updates
feat(rate-limit): update quota banner from response headers and persi…
2 parents c261826 + 06b9f30 commit 5faf824

3 files changed

Lines changed: 80 additions & 23 deletions

File tree

src/components/RateLimitBanner.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ export default function RateLimitBanner() {
3333
</span>
3434
)}
3535
</span>
36-
<span style={{ fontSize: 11, color: 'var(--text2)' }}>RESETS HOURLY</span>
36+
<span style={{ fontSize: 11, color: 'var(--text2)' }}>
37+
Used: {rateLimit.used} • Reset at{' '}
38+
{new Date(rateLimit.reset * 1000).toLocaleTimeString()}
39+
</span>
3740
</div>
3841
)
3942
}

src/context/AppContext.jsx

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,63 @@
1-
import { createContext, useContext, useState, useCallback } from 'react'
2-
import { fetchOrg, fetchRepos, fetchContributors, fetchIssues, fetchRateLimit } from '../services/github'
1+
import { createContext, useContext, useState, useCallback, useEffect } from 'react'
2+
import { fetchOrg, fetchRepos, fetchContributors, fetchIssues, } from '../services/github'
33
import { buildAnalyticalModel } from '../services/analytics'
44

55
const Ctx = createContext(null)
66

7+
function getStoredRateLimit() {
8+
const stored = localStorage.getItem('oe_rate_limit')
9+
10+
if (!stored) return null
11+
12+
try {
13+
const data = JSON.parse(stored)
14+
15+
if (Date.now() > data.reset * 1000) {
16+
localStorage.removeItem('oe_rate_limit')
17+
return null
18+
}
19+
20+
return data
21+
} catch {
22+
localStorage.removeItem('oe_rate_limit')
23+
return null
24+
}
25+
}
26+
727
export function AppProvider({ children }) {
8-
const [pat, setPat] = useState(() => localStorage.getItem('oe_pat') || '')
9-
const [orgs, setOrgs] = useState([])
10-
const [model, setModel] = useState(null)
28+
const [pat, setPat] = useState(() => localStorage.getItem('oe_pat') || '')
29+
const [orgs, setOrgs] = useState([])
30+
const [model, setModel] = useState(null)
1131
const [issuesData, setIssuesData] = useState({})
12-
const [rateLimit, setRateLimit] = useState(null)
13-
const [loading, setLoading] = useState(false)
14-
const [loadMsg, setLoadMsg] = useState('')
32+
const [rateLimit, setRateLimit] = useState(getStoredRateLimit)
33+
const [loading, setLoading] = useState(false)
34+
const [loadMsg, setLoadMsg] = useState('')
1535
const [govLoading, setGovLoading] = useState(false)
16-
const [error, setError] = useState('')
36+
const [error, setError] = useState('')
37+
38+
useEffect(() => {
39+
const handler = e => {
40+
setRateLimit(e.detail)
41+
localStorage.setItem('oe_rate_limit', JSON.stringify(e.detail))
42+
}
43+
44+
window.addEventListener('rate-limit-update', handler)
45+
46+
return () => {
47+
window.removeEventListener('rate-limit-update', handler)
48+
}
49+
}, [])
50+
51+
useEffect(() => {
52+
if (!rateLimit?.reset) return
53+
54+
const timeout = setTimeout(() => {
55+
localStorage.removeItem('oe_rate_limit')
56+
setRateLimit(null)
57+
}, Math.max(0, rateLimit.reset * 1000 - Date.now()))
58+
59+
return () => clearTimeout(timeout)
60+
}, [rateLimit])
1761

1862
const savePat = useCallback(token => {
1963
setPat(token)
@@ -25,7 +69,7 @@ export function AppProvider({ children }) {
2569
setLoading(true); setError(''); setModel(null); setOrgs([]); setIssuesData({})
2670
try {
2771
setLoadMsg('Fetching organization metadata...')
28-
const orgRes = await Promise.allSettled(orgNames.map(n => fetchOrg(n, pat)))
72+
const orgRes = await Promise.allSettled(orgNames.map(n => fetchOrg(n, pat)))
2973
const validOrgs = orgRes.filter(r => r.status === 'fulfilled').map(r => r.value)
3074
if (!validOrgs.length) throw new Error('No valid organizations found. Check the names and try again.')
3175
setOrgs(validOrgs)
@@ -50,11 +94,9 @@ export function AppProvider({ children }) {
5094
setLoadMsg('Building analytical data model...')
5195
setModel(buildAnalyticalModel(validOrgs, reposPerOrg, contribsPerRepo))
5296

53-
const rl = await fetchRateLimit(pat)
54-
if (rl) setRateLimit(rl)
5597

5698
// Save to recent searches
57-
const prev = JSON.parse(localStorage.getItem('oe_recent') || '[]')
99+
const prev = JSON.parse(localStorage.getItem('oe_recent') || '[]')
58100
const entry = orgNames.join(', ')
59101
localStorage.setItem('oe_recent', JSON.stringify([...new Set([entry, ...prev])].slice(0, 6)))
60102
return true
@@ -72,7 +114,7 @@ export function AppProvider({ children }) {
72114
const runAudit = useCallback(async () => {
73115
if (!model || govLoading) return
74116
setGovLoading(true)
75-
const map = {}
117+
const map = {}
76118
const repos = model.allRepos.slice(0, 15)
77119

78120
// Batches of 5 using Promise.allSettled

src/services/github.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
// IndexedDB Cache (L2)
22
const DB_NAME = 'orgexplorer_cache'
3-
const STORE = 'cache'
4-
const TTL_MS = 3_600_000 // 1 hour
3+
const STORE = 'cache'
4+
const TTL_MS = 3_600_000 // 1 hour
55

66
function openDB() {
77
return new Promise((resolve, reject) => {
88
const req = indexedDB.open(DB_NAME, 1)
99
req.onupgradeneeded = e => e.target.result.createObjectStore(STORE, { keyPath: 'k' })
10-
req.onsuccess = e => resolve(e.target.result)
11-
req.onerror = () => reject(req.error)
10+
req.onsuccess = e => resolve(e.target.result)
11+
req.onerror = () => reject(req.error)
1212
})
1313
}
1414

@@ -34,7 +34,7 @@ export async function cacheSet(key, value) {
3434
const tx = db.transaction(STORE, 'readwrite')
3535
tx.objectStore(STORE).put({ k: key, v: value, ts: Date.now() })
3636
tx.oncomplete = () => res(true)
37-
tx.onerror = () => res(false)
37+
tx.onerror = () => res(false)
3838
})
3939
} catch { return false }
4040
}
@@ -46,7 +46,7 @@ export async function cacheClear() {
4646
const tx = db.transaction(STORE, 'readwrite')
4747
tx.objectStore(STORE).clear()
4848
tx.oncomplete = () => res(true)
49-
tx.onerror = () => res(false)
49+
tx.onerror = () => res(false)
5050
})
5151
} catch { return false }
5252
}
@@ -61,6 +61,18 @@ async function fetchWithCache(url, pat) {
6161
if (pat) headers.Authorization = `token ${pat}`
6262

6363
const res = await fetch(url, { headers })
64+
65+
window.dispatchEvent(
66+
new CustomEvent('rate-limit-update', {
67+
detail: {
68+
limit: Number(res.headers.get('x-ratelimit-limit')),
69+
remaining: Number(res.headers.get('x-ratelimit-remaining')),
70+
used: Number(res.headers.get('x-ratelimit-used')),
71+
reset: Number(res.headers.get('x-ratelimit-reset'))
72+
}
73+
})
74+
)
75+
6476
if (res.status === 403) throw new Error('RATE_LIMIT')
6577
if (res.status === 404) throw new Error('NOT_FOUND')
6678
if (!res.ok) throw new Error(`HTTP_${res.status}`)
@@ -77,7 +89,7 @@ export const fetchOrg = (org, pat) =>
7789
export async function fetchRepos(org, pat) {
7890
const all = []
7991
for (let page = 1; page <= 5; page++) {
80-
const url = `https://api.github.com/orgs/${org}/repos?per_page=100&page=${page}&sort=updated`
92+
const url = `https://api.github.com/orgs/${org}/repos?per_page=100&page=${page}&sort=updated`
8193
const data = await fetchWithCache(url, pat)
8294
all.push(...data)
8395
if (data.length < 100) break
@@ -105,7 +117,7 @@ export async function fetchRateLimit(pat) {
105117
try {
106118
const headers = { Accept: 'application/vnd.github.v3+json' }
107119
if (pat) headers.Authorization = `token ${pat}`
108-
const res = await fetch('https://api.github.com/rate_limit', { headers })
120+
const res = await fetch('https://api.github.com/rate_limit', { headers })
109121
const data = await res.json()
110122
return data.rate
111123
} catch { return null }

0 commit comments

Comments
 (0)