--- /dev/null +++ b/frontend/p2p-core.js @@ -0,0 +1,55 @@ +/* ============================================================================ + * p2p-core.js — pure helpers for peer-to-peer UI (window.P2PCore / node) + * + * formatAvailability(payload, now) turns GET /api/p2p/holders into the line + * under the now-playing title, e.g. + * "📡 On 3 devices · 1 online now · last checked 2d ago" + * Holders are persistent (docs/p2p-architecture.md): they are never hidden + * for age; when every holder is older than the server's staleDays the line + * says so and is marked stale. + * ========================================================================== */ +(function (root) { + 'use strict'; + + function ago(ms, now) { + const s = Math.max(0, Math.round((now - ms) / 1000)); + if (s < 90) return 'just now'; + const m = Math.round(s / 60); + if (m < 90) return m + 'm ago'; + const h = Math.round(m / 60); + if (h < 48) return h + 'h ago'; + return Math.round(h / 24) + 'd ago'; + } + + function formatAvailability(payload, now) { + if (!payload || !payload.ok || !Array.isArray(payload.cids)) return null; + const holders = []; + for (const c of payload.cids) for (const h of c.holders || []) holders.push(h); + if (!holders.length) return null; + const online = holders.filter((h) => h.online).length; + const fresh = holders.filter((h) => !h.stale).length; + const newest = Math.max(...holders.map((h) => Number(h.lastVerifiedAt) || 0)); + const n = holders.length; + const allStale = fresh === 0; + const text = `📡 On ${n} device${n === 1 ? '' : 's'} · ${online ? online + ' online now' : 'none online'}` + + ` · last checked ${ago(newest, now)}` + (allStale ? ' (not checked recently)' : ''); + const title = holders.map((h) => + `${h.peer} · ${h.online ? 'online' : 'offline'} · checked ${ago(Number(h.lastVerifiedAt) || 0, now)}` + + `${h.stale ? ' (stale)' : ''}${h.trust === 'challenged' ? ' · spot-checked' : ''}`).join('\n'); + return { text, title, stale: allStale, online, holders: n }; + } + + // Holders worth trying for a download: online first, then freshest check. + function pickPeers(payload, cid) { + if (!payload || !Array.isArray(payload.cids)) return []; + const c = payload.cids.find((x) => x.cid === cid) || payload.cids[0]; + if (!c) return []; + return (c.holders || []).filter((h) => h.online) + .sort((a, b) => (b.trust === 'challenged') - (a.trust === 'challenged') || b.lastVerifiedAt - a.lastVerifiedAt) + .map((h) => ({ peer: h.peer, cid: c.cid, size: c.size })); + } + + const P2PCore = { ago, formatAvailability, pickPeers }; + if (typeof module !== 'undefined' && module.exports) module.exports = P2PCore; + else root.P2PCore = P2PCore; +})(typeof globalThis !== 'undefined' ? globalThis : this); --- /dev/null +++ b/frontend/p2p-core.test.js @@ -0,0 +1,46 @@ +'use strict'; +const { test } = require('node:test'); +const assert = require('node:assert'); +const C = require('./p2p-core'); + +const NOW = Date.UTC(2026, 8, 29, 12); +const H = 3600_000; +const payload = (holders, extra = {}) => ({ ok: true, staleDays: 7, cids: [{ cid: 'a'.repeat(64), size: 10, holders, ...extra }] }); + +test('ago buckets', () => { + assert.strictEqual(C.ago(NOW - 30_000, NOW), 'just now'); + assert.strictEqual(C.ago(NOW - 20 * 60_000, NOW), '20m ago'); + assert.strictEqual(C.ago(NOW - 5 * H, NOW), '5h ago'); + assert.strictEqual(C.ago(NOW - 72 * H, NOW), '3d ago'); +}); + +test('no holders → nothing to show', () => { + assert.strictEqual(C.formatAvailability(payload([]), NOW), null); + assert.strictEqual(C.formatAvailability(null, NOW), null); +}); + +test('mixed holders: counts, newest check, not stale', () => { + const f = C.formatAvailability(payload([ + { peer: 'p1', online: true, lastVerifiedAt: NOW - 30 * 24 * H, stale: true, trust: 'reported' }, + { peer: 'p2', online: false, lastVerifiedAt: NOW - 48 * H, stale: false, trust: 'challenged' }, + ]), NOW); + assert.strictEqual(f.text, '📡 On 2 devices · 1 online now · last checked 2d ago'); + assert.strictEqual(f.stale, false); + assert.match(f.title, /p1 · online · checked 30d ago \(stale\)/); + assert.match(f.title, /p2 · offline · checked 2d ago · spot-checked/); +}); + +test('every holder stale → still listed, flagged', () => { + const f = C.formatAvailability(payload([{ peer: 'p1', online: false, lastVerifiedAt: NOW - 20 * 24 * H, stale: true }]), NOW); + assert.strictEqual(f.text, '📡 On 1 device · none online · last checked 20d ago (not checked recently)'); + assert.strictEqual(f.stale, true); +}); + +test('pickPeers: online only, spot-checked first', () => { + const p = payload([ + { peer: 'off', online: false, lastVerifiedAt: NOW, trust: 'challenged' }, + { peer: 'on1', online: true, lastVerifiedAt: NOW, trust: 'reported' }, + { peer: 'on2', online: true, lastVerifiedAt: NOW - H, trust: 'challenged' }, + ]); + assert.deepStrictEqual(C.pickPeers(p).map((x) => x.peer), ['on2', 'on1']); +});