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

129 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: 014-p2p-presence-hub-ceced8
title: Add the /ws/p2p presence and signalling hub and the holders endpoint
created: 2026-09-29
depends_on: [013-device-identity-and-holdings-3ba493]
est_files: 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):
```js
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):
```js
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:
```bash
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:
```js
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:
```js
// …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
```bash
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.