Queue executable speed and peer-to-peer sharing plans with tested patches and harnesses
This commit is contained in:
122
plans/queue/014-p2p-presence-hub-ceced8.md
Normal file
122
plans/queue/014-p2p-presence-hub-ceced8.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user