secondaries

Changed files
+63 -16
atproto-notifications
server
+1 -1
atproto-notifications/index.html
··· 3 3 <head> 4 4 <meta charset="UTF-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 - <title>atproto notifications</title> 6 + <title>spacedust notifications</title> 7 7 </head> 8 8 <body> 9 9 <div id="root"></div>
+30 -6
atproto-notifications/src/components/Feed.tsx
··· 1 1 import { useEffect, useState } from 'react'; 2 - import { getNotifications } from '../db'; 2 + import { getNotifications, getSecondary } from '../db'; 3 + 4 + function Asdf({ inc, secondary }) { 5 + const [secondaries, setSecondaries] = useState([]); 6 + useEffect(() => { 7 + (async () => { 8 + const secondaries = await getSecondary(secondary); 9 + secondaries.sort((a, b) => b.unread - a.unread); 10 + setSecondaries(secondaries); 11 + })(); 12 + }, [inc, secondary]); 13 + 14 + return ( 15 + <div> 16 + <p>secondaries: ({secondaries.length})</p> 17 + {secondaries.map(a => ( 18 + <p key={a.k}>asdf {a.k} ({a.unread}/{a.total})</p> 19 + ))} 20 + </div> 21 + ); 22 + } 3 23 4 24 export function Feed() { 5 25 ··· 16 36 // this could be combined with the broadcast thing above, but for now just chain deps 17 37 const [feed, setFeed] = useState([]); 18 38 useEffect(() => { 19 - (async () => setFeed((await getNotifications())))(); 39 + (async () => setFeed(await getNotifications()))(); 20 40 }, [inc]); 21 41 22 42 if (feed.length === 0) { 23 43 return 'no notifications loaded'; 24 44 } 25 - return feed.map(([k, n]) => ( 26 - <p key={k}>{k}: {n.source} ({n.source_record}) <code>{JSON.stringify(n)}</code></p> 27 - )); 28 - 45 + return ( 46 + <div className="feed"> 47 + <Asdf inc={inc} secondary='source' /> 48 + {feed.map(([k, n]) => ( 49 + <p key={k}>{k}: {n.source} ({n.source_record}) <code>{JSON.stringify(n)}</code></p> 50 + ))} 51 + </div> 52 + ); 29 53 }
+22 -6
atproto-notifications/src/db.ts
··· 18 18 // primary store for notifications 19 19 try { 20 20 // upgrade is a reset: entirely remove the store (ignore errors if it didn't exist) 21 - db.deleteObjectStore('notifs'); 21 + db.deleteObjectStore(NOTIFICATIONS); 22 22 } catch (e) {} 23 23 const notifStore = db.createObjectStore(NOTIFICATIONS, { 24 - key: 'id', 24 + keyPath: 'id', 25 25 autoIncrement: true, 26 26 }); 27 27 // subject prob doesn't need an index, could just query constellation ··· 44 44 db.deleteObjectStore(secondary); 45 45 } catch (e) {} 46 46 const store = db.createObjectStore(secondary, { 47 - key: 'k', 47 + keyPath: 'k', 48 48 }); 49 49 store.createIndex('total', 'total', { unique: false }); 50 50 store.createIndex('unread', 'unread', { unique: false }); 51 51 } 52 52 53 - }, 3); 53 + }, 4); 54 54 55 55 export async function insertNotification(notif: { 56 56 subject: String, ··· 71 71 const store = tx.objectStore(secondary); 72 72 const key = secondary === 'all' ? 'all' : notif[secondary]; 73 73 store.get(key).onsuccess = ev => { 74 - let count = ev.target.result ?? { total: 0, unread: 0 }; 74 + let count = ev.target.result ?? { 75 + k: key, 76 + total: 0, 77 + unread: 0, 78 + }; 75 79 count.total += 1; 76 80 count.unread += 1; 77 - store.put(count, key); 81 + store.put(count); 78 82 }; 79 83 } 80 84 ··· 104 108 } 105 109 }); 106 110 } 111 + 112 + export async function getSecondary(secondary) { 113 + const db = await getDB(); 114 + const obj = db 115 + .transaction([secondary]) 116 + .objectStore(secondary) 117 + .getAll(); 118 + return new Promise((resolve, reject) => { 119 + obj.onerror = () => reject(obj.error); 120 + obj.onsuccess = ev => resolve(ev.target.result); 121 + }); 122 + }
-1
atproto-notifications/src/service-worker.ts
··· 36 36 // TODO: user pref for alt client -> prefer that client's icon 37 37 const lex = lexicons[appPrefix]; 38 38 const icon = lex?.clients[0]?.icon; 39 - console.log('app', app, 'lex', lex, lexicons); 40 39 const title = lex?.known_sources[source.slice(app.length + 1)] ?? source; 41 40 const body = `from @${handle} on ${lex?.name ?? app}`; 42 41
+1 -1
atproto-notifications/vite.config.ts
··· 8 8 enforce: 'pre', 9 9 transformIndexHtml() { 10 10 buildSync({ 11 - minify: true, 11 + minify: forProd, 12 12 bundle: true, 13 13 entryPoints: [join(process.cwd(), 'src', 'service-worker.ts')], 14 14 outfile: join(process.cwd(), forProd ? 'dist' : 'public', 'service-worker.js'),
+9 -1
server/index.js
··· 25 25 if (!subs.has(did)) { 26 26 subs.set(did, []); 27 27 } 28 + sub.t = new Date(); 28 29 subs.get(did).push(sub); 29 30 updateSubs(); 30 31 }; ··· 70 71 } 71 72 72 73 const expiredSubs = []; 73 - for (const sub of subs.get(did) ?? []) { try { 74 + const now = new Date(); 75 + for (const sub of subs.get(did) ?? []) { 76 + try { 77 + if (now - sub.t < 1500) { 78 + console.warn('skipping for rate limit'); 79 + continue; 80 + } 81 + sub.t = now; 74 82 await webpush.sendNotification(sub, JSON.stringify({ subject, source, source_record })); 75 83 } catch (err) { 76 84 if (400 <= err.statusCode && err.statusCode < 500) {