108 lines
5.0 KiB
Diff
108 lines
5.0 KiB
Diff
--- /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']);
|
|
+});
|