5.7 KiB
5.7 KiB
id, title, created, depends_on, est_files
| id | title | created | depends_on | est_files | |
|---|---|---|---|---|---|
| 014-p2p-presence-hub-ceced8 | Add the /ws/p2p presence and signalling hub and the holders endpoint | 2026-09-29 |
|
6 |
014 — Presence + signalling hub, holders endpoint
Objective
Implements flows 4–5 of docs/p2p-architecture.md. After this plan:
/ws/p2pis a websocket hub (server/p2p-hub.js). A device authenticates with its FIRST message{type:'auth', device, secret}(never in the URL — proxies log URLs), gets{type:'hello', peer}, and is online while the socket is open (memory only). It relays{type:'signal', to, data}between two authenticated online devices.GET /api/p2p/holders?v=<videoId>(or?cid=) lists each verified cid with its holders: opaquepeer,online,lastVerifiedAt,stale,trust, plusserverHasand counts. Holder rows are NEVER hidden for age —staleonly flags them. Devices with sharing off are not listed. Device ids never appear in the payload.P2PClientkeeps the socket open while sharing or receiving is on (both default ON), with a 50 s keepalive (the server closes idle sockets after 120 s), and exposesonMessage(type, fn),signal(to, data),peer(),isConnected().
Pre-tested: hub/payload 3/3 (bun:test); two real browsers exchanged a signal through a
real Bun server (plans/harness/presence-*.js).
Context the executor must NOT rediscover
plans/patches/014-p2p-hub-new.diffcreatesserver/p2p-hub.js(exportscreateP2pHub,holdersPayload,peerIdOf) andserver/p2p-hub.test.js.plans/patches/014-p2p-client-presence.diffpatchesfrontend/p2p-client.js(from plan 013).server/server.js(~line 1778-1790):const remote = createRemoteHub({ requireSameNetwork: process.env.REMOTE_SAME_NETWORK === '1' }); const party = createPartyHub(); // Bun allows ONE websocket handler per server: party sockets are tagged // (ws.data.hub === 'party'), everything else belongs to the remote relay. const pickHub = (ws) => (ws.data && ws.data.hub === 'party' ? party.websocket : remote.websocket);server/server.jsBun.serve({ … fetch(req, server) {(~line 1926):const path = new URL(req.url).pathname; if (path === '/ws/remote') return remote.upgrade(req, server, clientIpOf(req, server)); if (path === '/ws/party') return party.upgrade(req, server, clientIpOf(req, server)); return app.fetch(req, server);- Plan 013 added in server.js:
async function fileForCid(cid)andconst p2p = registerP2pRoutes(app, { cfg: P2P, p2pDb, fileForCid, sha256Range });(p2p.gateis a Hono middleware answering 404 when P2P is disabled).
Steps
- From the repo root:
STOP and report if either fails.
git apply plans/patches/014-p2p-hub-new.diff git apply plans/patches/014-p2p-client-presence.diff server/package.json"test" script — append&& bun test ./p2p-hub.test.js.server/server.js— add importimport { createP2pHub, holdersPayload } from './p2p-hub.js';server/server.js— directly afterconst p2p = registerP2pRoutes(app, { … });add:const p2pHub = createP2pHub({ getDevice: p2pDb.getDevice, enabled: () => P2P.enabled }); // GET /api/p2p/holders?v=<videoId>|cid=<sha256> — who holds a copy. Rows are // persistent; `stale` flags a holder not re-verified for P2P_STALE_DAYS. app.get('/api/p2p/holders', p2p.gate, async (c) => { const v = (c.req.query('v') || '').trim(); const cid = (c.req.query('cid') || '').trim().toLowerCase(); const hasCid = /^[0-9a-f]{64}$/.test(cid); if (!hasCid && !/^[A-Za-z0-9_-]{6,64}$/.test(v)) return c.json({ ok: false, error: 'missing v or cid' }, 400); const payload = await holdersPayload({ videoId: v, cid: hasCid ? cid : null, p2pDb, isOnline: p2pHub.isOnline, staleDays: P2P.staleDays, serverHas: fileForCid, }); return c.json(payload, 200, { 'Cache-Control': 'no-store' }); });server/server.js— replace thepickHubline with:(// …and P2P sockets are tagged ws.data.hub === 'p2p' (p2p-hub.js). const pickHub = (ws) => (ws.data && ws.data.hub === 'party' ? party.websocket : ws.data && ws.data.hub === 'p2p' ? p2pHub.websocket : remote.websocket);p2pHubis declared later in the file; that is fine —pickHubonly runs once sockets exist.)server/server.jsBun.servefetch — after the/ws/partyline add:if (path === '/ws/p2p') return p2pHub.upgrade(req, server);- Copy nothing else;
frontend/index.html/sw.jsneed no change (no new frontend files).
Out of scope / do NOT touch
remote.js,party.jsand their behaviour. No UI (plan 015). No file transfer (plan 017).
Verification
cd /home/user/ytplayer/server && bun install >/dev/null 2>&1
bun test ./p2p-hub.test.js 2>&1 | tail -4
bun run test 2>&1 | grep -E "^ *[0-9]+ (pass|fail)"
bun build server.js --target=bun --outdir=/tmp/ytp-check >/dev/null && echo SERVER_OK
cd .. && node --check frontend/p2p-client.js && echo FRONT_OK
cd server
bun ../plans/harness/presence-server.js >/tmp/ytp014.log 2>&1 & SRV=$!; sleep 3
cd ../plans/harness && (npm ls playwright >/dev/null 2>&1 || npm i --no-save playwright >/dev/null 2>&1); timeout 90 node presence-check.mjs
kill $SRV; true
Expected: hub 3 pass; every file 0 fail; SERVER_OK; FRONT_OK; browser JSON
{"peers":[true,true],"distinct":true,"sent":true,"got":[{"from":true,"data":{"hi":1}}]}.
Report format (executor: follow exactly)
Output ONLY the following, no other prose:
git diff(unified) of all changes.- Raw output of the Verification commands.
Findings:— max 10 lines.
Do not commit. Do not push. Do not touch files outside the Steps.