count.js (2862B)
1 // api/count.js 2 const REPO = 'NodeX-AR/Pytml'; 3 const PATH = 'count.json'; 4 const FLUSH_MS = 60_000; 5 const FLUSH_THRESHOLD = 25; 6 7 let buffer = { total: 0, origins: {} }; 8 let cachedTotal = 0; 9 let lastFlush = 0; 10 let flushing = null; 11 12 async function readCount(token) { 13 const r = await fetch(`https://api.github.com/repos/${REPO}/contents/${PATH}`, { 14 headers: { Authorization: `token ${token}`, Accept: 'application/vnd.github.v3+json' } 15 }); 16 if (!r.ok) throw new Error('read failed'); 17 const data = await r.json(); 18 const text = Buffer.from(data.content, 'base64').toString('utf-8'); 19 let parsed; 20 try { parsed = JSON.parse(text); } 21 catch { parsed = { total: parseInt(text) || 0, origins: {} }; } 22 return { data: parsed, sha: data.sha }; 23 } 24 25 async function writeCount(token, payload, sha) { 26 const r = await fetch(`https://api.github.com/repos/${REPO}/contents/${PATH}`, { 27 method: 'PUT', 28 headers: { 29 Authorization: `token ${token}`, 30 Accept: 'application/vnd.github.v3+json', 31 'Content-Type': 'application/json' 32 }, 33 body: JSON.stringify({ 34 message: `Count: ${payload.total}`, 35 content: Buffer.from(JSON.stringify(payload, null, 2)).toString('base64'), 36 sha 37 }) 38 }); 39 if (!r.ok) throw new Error('write failed'); 40 } 41 42 function flush() { 43 if (flushing || buffer.total === 0) return flushing; 44 const token = process.env.GITHUB_TOKEN; 45 const snapshot = buffer; 46 buffer = { total: 0, origins: {} }; 47 lastFlush = Date.now(); 48 49 flushing = (async () => { 50 try { 51 const { data, sha } = await readCount(token); 52 data.total = (data.total || 0) + snapshot.total; 53 data.origins = data.origins || {}; 54 for (const [o, n] of Object.entries(snapshot.origins)) { 55 data.origins[o] = (data.origins[o] || 0) + n; 56 } 57 data.lastUpdated = new Date().toISOString(); 58 await writeCount(token, data, sha); 59 cachedTotal = data.total; 60 } catch (err) { 61 buffer.total += snapshot.total; 62 for (const [o, n] of Object.entries(snapshot.origins)) { 63 buffer.origins[o] = (buffer.origins[o] || 0) + n; 64 } 65 console.error('flush failed:', err); 66 } finally { 67 flushing = null; 68 } 69 })(); 70 71 return flushing; 72 } 73 74 export default async function handler(req, res) { 75 res.setHeader('Access-Control-Allow-Origin', '*'); 76 77 let origin = req.headers.origin || ''; 78 if (!origin && req.headers.referer) { 79 try { origin = new URL(req.headers.referer).origin; } catch {} 80 } 81 if (!origin) origin = 'unknown'; 82 83 buffer.total += 1; 84 buffer.origins[origin] = (buffer.origins[origin] || 0) + 1; 85 86 if (buffer.total >= FLUSH_THRESHOLD || Date.now() - lastFlush >= FLUSH_MS) { 87 flush().catch(() => {}); 88 } 89 90 res.status(200).json({ 91 schemaVersion: 1, 92 label: 'pytml loads', 93 message: String(cachedTotal + buffer.total), 94 color: 'blue' 95 }); 96 }