Files
ytplayer/plans/done/014-p2p-presence-hub-ceced8.md

6.5 KiB
Raw Permalink Blame History

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
013-device-identity-and-holdings-3ba493
6

014 — Presence + signalling hub, holders endpoint

Objective

Implements flows 4–5 of docs/p2p-architecture.md. After this plan:

  • /ws/p2p is 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: opaque peer, online, lastVerifiedAt, stale, trust, plus serverHas and counts. Holder rows are NEVER hidden for age — stale only flags them. Devices with sharing off are not listed. Device ids never appear in the payload.
  • P2PClient keeps 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 exposes onMessage(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.diff creates server/p2p-hub.js (exports createP2pHub, holdersPayload, peerIdOf) and server/p2p-hub.test.js.
  • plans/patches/014-p2p-client-presence.diff patches frontend/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.js Bun.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) and const p2p = registerP2pRoutes(app, { cfg: P2P, p2pDb, fileForCid, sha256Range }); (p2p.gate is a Hono middleware answering 404 when P2P is disabled).

Steps

  1. From the repo root:
    git apply plans/patches/014-p2p-hub-new.diff
    git apply plans/patches/014-p2p-client-presence.diff
    
    STOP and report if either fails.
  2. server/package.json "test" script — append && bun test ./p2p-hub.test.js.
  3. server/server.js — add import import { createP2pHub, holdersPayload } from './p2p-hub.js';
  4. server/server.js — directly after const 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' });
    });
    
  5. server/server.js — replace the pickHub line 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);
    
    (p2pHub is declared later in the file; that is fine — pickHub only runs once sockets exist.)
  6. server/server.js Bun.serve fetch — after the /ws/party line add: if (path === '/ws/p2p') return p2pHub.upgrade(req, server);
  7. Copy nothing else; frontend/index.html / sw.js need no change (no new frontend files).

Out of scope / do NOT touch

  • remote.js, party.js and 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:

  1. git diff (unified) of all changes.
  2. Raw output of the Verification commands.
  3. Findings: — max 10 lines.

Do not commit. Do not push. Do not touch files outside the Steps.

Execution log

  • Executor: in-session Agent (haiku). Attempts: 1. Fix rounds: 0.
  • Orchestrator re-ran Verification: p2p-hub.js, p2p-hub.test.js, p2p-client.js byte-identical to the pre-tested versions; hub tests 3 pass; all 13 server test files 0 fail; SERVER_OK; FRONT_OK; two-browser presence check relayed the signal ({"peers":[true,true],"distinct":true,"sent":true,"got":[{"from":true,"data":{"hi":1}}]}). No leftover processes.
  • Executor Findings (verbatim): Both git apply patches applied successfully without errors. All 13 test files (24 total test groups) passed with 0 failures. Server builds successfully. Frontend syntax check passes. Browser presence test matches expected output exactly. All modifications applied according to plan steps 1-7. No files touched outside the specified changes.