anproto personal data server

more notifications code

Changed files
+48 -13
+48 -13
serve.js
··· 113 113 let flushTimer = undefined 114 114 const queuedPushSigs = new Set() 115 115 116 - const broadcastPush = async (payloadObj) => { 116 + const redactEndpoint = (endpoint) => { 117 + try { 118 + const u = new URL(endpoint) 119 + const p = u.pathname || '' 120 + const suffix = p.length > 14 ? p.slice(-14) : p 121 + return `${u.origin}${suffix ? '/…' + suffix : ''}` 122 + } catch { 123 + const s = String(endpoint || '') 124 + return s.length > 32 ? s.slice(0, 32) + '…' : s 125 + } 126 + } 127 + 128 + const broadcastPushWithResults = async (payloadObj) => { 117 129 const store = await readSubscriptions() 118 130 const endpoints = Object.keys(store.subscriptions ?? {}) 119 - if (endpoints.length === 0) return 120 131 121 132 const dead = new Set() 122 - const results = await Promise.allSettled( 133 + const results = await Promise.all( 123 134 endpoints.map(async (endpoint) => { 124 - const entry = store.subscriptions[endpoint] 125 - if (!entry?.subscription) return 135 + const entry = store.subscriptions?.[endpoint] 136 + const subscription = entry?.subscription 137 + if (!subscription) { 138 + return { endpoint: redactEndpoint(endpoint), skipped: true } 139 + } 140 + 126 141 try { 127 - const res = await sendWebPush(entry.subscription, payloadObj) 128 - if (res.status === 404 || res.status === 410) dead.add(endpoint) 129 - } catch { 130 - // Best-effort. 142 + const res = await sendWebPush(subscription, payloadObj) 143 + const status = res.status 144 + if (status === 404 || status === 410) dead.add(endpoint) 145 + if (!res.ok) { 146 + console.log('webpush send failed', { endpoint: redactEndpoint(endpoint), status }) 147 + } 148 + return { endpoint: redactEndpoint(endpoint), status, ok: res.ok } 149 + } catch (err) { 150 + console.log('webpush send error', { endpoint: redactEndpoint(endpoint), err: String(err?.message || err) }) 151 + return { endpoint: redactEndpoint(endpoint), error: String(err?.message || err) } 131 152 } 132 153 }) 133 154 ) 134 - void results 135 155 136 156 if (dead.size > 0) { 137 157 for (const endpoint of dead) delete store.subscriptions[endpoint] 138 158 store.updatedAt = Date.now() 139 159 await writeSubscriptions(store) 140 160 } 161 + 162 + const okCount = results.filter(r => r.ok).length 163 + const failCount = results.length - okCount 164 + return { 165 + stored: endpoints.length, 166 + ok: okCount, 167 + failed: failCount, 168 + pruned: dead.size, 169 + results 170 + } 171 + } 172 + 173 + const broadcastPush = async (payloadObj) => { 174 + await broadcastPushWithResults(payloadObj) 141 175 } 142 176 143 177 const scheduleFlush = (delayMs = 0) => { ··· 444 478 store.updatedAt = Date.now() 445 479 await writeSubscriptions(store) 446 480 447 - return new Response(JSON.stringify({ ok: true }), { status: 201, headers: header }) 481 + return new Response(JSON.stringify({ ok: true, stored: Object.keys(store.subscriptions).length }), { status: 201, headers: header }) 448 482 } 449 483 450 484 if (url.pathname === '/push/vapidPublicKey') { ··· 460 494 if (r.method !== 'POST') { 461 495 return new Response(JSON.stringify({ error: 'Method Not Allowed' }), { status: 405, headers: header }) 462 496 } 463 - await broadcastPush({ type: 'test', ts: Date.now() }) 464 - return new Response(JSON.stringify({ ok: true }), { status: 200, headers: header }) 497 + const now = Date.now() 498 + const report = await broadcastPushWithResults({ type: 'test', ts: now }) 499 + return new Response(JSON.stringify({ ok: true, ts: now, ...report }), { status: 200, headers: header }) 465 500 } 466 501 467 502 const key = url.pathname.substring(1)