@fairfox/polly 0.70.0 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/client/index.js +2 -2
- package/dist/src/client/index.js.map +2 -2
- package/dist/src/mesh.js +407 -156
- package/dist/src/mesh.js.map +9 -8
- package/dist/src/peer.js +92 -5
- package/dist/src/peer.js.map +4 -4
- package/dist/src/polly-ui/ActionInput.d.ts +10 -1
- package/dist/src/polly-ui/ActionSelect.d.ts +35 -0
- package/dist/src/polly-ui/Cluster.d.ts +35 -0
- package/dist/src/polly-ui/Code.d.ts +17 -0
- package/dist/src/polly-ui/Text.d.ts +31 -0
- package/dist/src/polly-ui/index.css +278 -185
- package/dist/src/polly-ui/index.d.ts +5 -1
- package/dist/src/polly-ui/index.js +480 -284
- package/dist/src/polly-ui/index.js.map +11 -6
- package/dist/src/polly-ui/internal/dispatch-action.d.ts +13 -0
- package/dist/src/polly-ui/markdown.js +3 -3
- package/dist/src/polly-ui/markdown.js.map +2 -2
- package/dist/src/polly-ui/styles.css +288 -185
- package/dist/src/shared/lib/mesh-client.d.ts +29 -0
- package/dist/src/shared/lib/mesh-diagnostics.d.ts +31 -0
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +91 -0
- package/dist/src/shared/lib/revocation-summary.d.ts +54 -0
- package/dist/tools/quality/src/cli.js +6 -2
- package/dist/tools/quality/src/cli.js.map +3 -3
- package/dist/tools/quality/src/index.js +6 -2
- package/dist/tools/quality/src/index.js.map +3 -3
- package/dist/tools/test/src/browser/run.js +75 -44
- package/dist/tools/test/src/browser/run.js.map +3 -3
- package/dist/tools/test/src/e2e-mesh/index.d.ts +1 -1
- package/dist/tools/test/src/e2e-mesh/index.js +95 -1
- package/dist/tools/test/src/e2e-mesh/index.js.map +4 -4
- package/dist/tools/test/src/e2e-mesh/launch-peer.d.ts +29 -1
- package/dist/tools/test/src/visual/index.js +24 -24
- package/dist/tools/test/src/visual/index.js.map +2 -2
- package/dist/tools/verify/src/cli.js +82 -51
- package/dist/tools/verify/src/cli.js.map +7 -6
- package/package.json +2 -2
|
@@ -178,14 +178,18 @@ var signalingApp = new Elysia2().use(signalingServer({ path: "/polly/signaling"
|
|
|
178
178
|
console.log(`[browser-runner] signaling server on ws://127.0.0.1:${signalingPort}/polly/signaling`);
|
|
179
179
|
var browser = await puppeteer.launch({
|
|
180
180
|
headless,
|
|
181
|
-
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
181
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
182
|
+
protocolTimeout: 30000
|
|
182
183
|
});
|
|
183
184
|
var totalPassed = 0;
|
|
184
185
|
var totalFailed = 0;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
function isProtocolError(err) {
|
|
187
|
+
return err instanceof Error && err.name === "ProtocolError";
|
|
188
|
+
}
|
|
189
|
+
function errMessage(err) {
|
|
190
|
+
return err instanceof Error ? err.message : String(err);
|
|
191
|
+
}
|
|
192
|
+
async function runFile(testFile) {
|
|
189
193
|
const buildResult = await Bun.build({
|
|
190
194
|
entrypoints: [testFile],
|
|
191
195
|
target: "browser",
|
|
@@ -202,14 +206,12 @@ for (const testFile of testFiles) {
|
|
|
202
206
|
for (const log of buildResult.logs) {
|
|
203
207
|
console.log(` ${log}`);
|
|
204
208
|
}
|
|
205
|
-
|
|
206
|
-
continue;
|
|
209
|
+
return { passed: 0, failed: 1 };
|
|
207
210
|
}
|
|
208
211
|
const jsText = await buildResult.outputs[0]?.text();
|
|
209
212
|
if (!jsText) {
|
|
210
213
|
console.log(" ❌ build produced no output");
|
|
211
|
-
|
|
212
|
-
continue;
|
|
214
|
+
return { passed: 0, failed: 1 };
|
|
213
215
|
}
|
|
214
216
|
const html = `<!DOCTYPE html>
|
|
215
217
|
<html><head><meta charset="utf-8"></head>
|
|
@@ -222,46 +224,75 @@ for (const testFile of testFiles) {
|
|
|
222
224
|
return new Response(html, { headers: { "Content-Type": "text/html" } });
|
|
223
225
|
}
|
|
224
226
|
});
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
let page;
|
|
228
|
+
try {
|
|
229
|
+
page = await browser.newPage();
|
|
230
|
+
page.on("console", (msg) => {
|
|
231
|
+
const text = msg.text();
|
|
232
|
+
if (text.includes("[test]")) {
|
|
233
|
+
console.log(` ${text}`);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
page.on("pageerror", (err) => {
|
|
237
|
+
console.log(` ❌ page error: ${errMessage(err)}`);
|
|
238
|
+
});
|
|
239
|
+
await page.goto(`http://127.0.0.1:${server.port}/`, { waitUntil: "domcontentloaded" });
|
|
240
|
+
const timeout = 15000;
|
|
241
|
+
const deadline = Date.now() + timeout;
|
|
242
|
+
let finished = false;
|
|
243
|
+
while (Date.now() < deadline) {
|
|
244
|
+
finished = await page.evaluate(() => window["__done"] === true);
|
|
245
|
+
if (finished)
|
|
246
|
+
break;
|
|
247
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
248
|
+
}
|
|
249
|
+
if (!finished) {
|
|
250
|
+
console.log(` ❌ timed out after ${timeout}ms`);
|
|
251
|
+
return { passed: 0, failed: 1 };
|
|
252
|
+
}
|
|
253
|
+
const results = await page.evaluate(() => window["__testResults"]);
|
|
254
|
+
let passed = 0;
|
|
255
|
+
let failed = 0;
|
|
256
|
+
for (const r of results ?? []) {
|
|
257
|
+
if (r.passed) {
|
|
258
|
+
console.log(` ✅ ${r.name}`);
|
|
259
|
+
passed += 1;
|
|
260
|
+
} else {
|
|
261
|
+
console.log(` ❌ ${r.name}: ${r.error}`);
|
|
262
|
+
failed += 1;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { passed, failed };
|
|
266
|
+
} finally {
|
|
267
|
+
if (page) {
|
|
268
|
+
await page.close().catch(() => {});
|
|
230
269
|
}
|
|
231
|
-
});
|
|
232
|
-
page.on("pageerror", (err) => {
|
|
233
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
234
|
-
console.log(` ❌ page error: ${msg}`);
|
|
235
|
-
});
|
|
236
|
-
await page.goto(`http://127.0.0.1:${server.port}/`, { waitUntil: "domcontentloaded" });
|
|
237
|
-
const timeout = 15000;
|
|
238
|
-
const deadline = Date.now() + timeout;
|
|
239
|
-
let finished = false;
|
|
240
|
-
while (Date.now() < deadline) {
|
|
241
|
-
finished = await page.evaluate(() => window["__done"] === true);
|
|
242
|
-
if (finished)
|
|
243
|
-
break;
|
|
244
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
245
|
-
}
|
|
246
|
-
if (!finished) {
|
|
247
|
-
console.log(` ❌ timed out after ${timeout}ms`);
|
|
248
|
-
totalFailed += 1;
|
|
249
|
-
await page.close();
|
|
250
270
|
server.stop();
|
|
251
|
-
continue;
|
|
252
271
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
272
|
+
}
|
|
273
|
+
for (const testFile of testFiles) {
|
|
274
|
+
const shortName = testFile.replace(`${testDir}/`, "");
|
|
275
|
+
console.log(`
|
|
276
|
+
[browser-runner] running ${shortName}`);
|
|
277
|
+
let result;
|
|
278
|
+
try {
|
|
279
|
+
result = await runFile(testFile);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
if (isProtocolError(err)) {
|
|
282
|
+
console.log(` ⚠️ protocol error (${errMessage(err)}) — retrying once on a fresh page`);
|
|
283
|
+
try {
|
|
284
|
+
result = await runFile(testFile);
|
|
285
|
+
} catch (retryErr) {
|
|
286
|
+
console.log(` ❌ retry failed: ${errMessage(retryErr)}`);
|
|
287
|
+
result = { passed: 0, failed: 1 };
|
|
288
|
+
}
|
|
258
289
|
} else {
|
|
259
|
-
console.log(` ❌ ${
|
|
260
|
-
|
|
290
|
+
console.log(` ❌ ${errMessage(err)}`);
|
|
291
|
+
result = { passed: 0, failed: 1 };
|
|
261
292
|
}
|
|
262
293
|
}
|
|
263
|
-
|
|
264
|
-
|
|
294
|
+
totalPassed += result.passed;
|
|
295
|
+
totalFailed += result.failed;
|
|
265
296
|
}
|
|
266
297
|
await browser.close();
|
|
267
298
|
signalingApp.server?.stop?.(true);
|
|
@@ -269,4 +300,4 @@ console.log(`
|
|
|
269
300
|
[browser-runner] ${totalPassed} passed, ${totalFailed} failed`);
|
|
270
301
|
process.exit(totalFailed > 0 ? 1 : 0);
|
|
271
302
|
|
|
272
|
-
//# debugId=
|
|
303
|
+
//# debugId=995635FD6B287CDC64756E2164756E21
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../tools/test/src/browser/run.ts", "../src/elysia/signaling-server-plugin.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"#!/usr/bin/env bun\n\n/**\n * Browser test runner for Polly applications.\n *\n * Finds all *.browser.ts files in a given directory, bundles each with\n * Bun.build for the browser target (with an internal Automerge WASM fix),\n * serves the bundle on an ephemeral port, opens a Puppeteer page, and\n * polls window.__done for results. Prints pass/fail per test and exits\n * non-zero if any test failed.\n *\n * A signalling server for WebRTC tests starts automatically on a random\n * port. The URL is injected into the bundle via process.env.SIGNALING_URL.\n *\n * Usage (from project root):\n *\n * bun tools/test/src/browser/run.ts [testDir] [filter]\n *\n * Examples:\n *\n * bun tools/test/src/browser/run.ts tests/browser\n * bun tools/test/src/browser/run.ts tests/browser mesh-webrtc\n * HEADLESS=false bun tools/test/src/browser/run.ts tests/browser\n *\n * When invoked without a testDir, defaults to tests/browser relative to cwd.\n */\n\nimport { resolve } from \"node:path\";\nimport { type BunPlugin, Glob } from \"bun\";\nimport { Elysia } from \"elysia\";\nimport puppeteer from \"puppeteer\";\nimport { signalingServer } from \"../../../../src/elysia/signaling-server-plugin\";\n\n// Automerge WASM fix\n// Bun.build's target: \"browser\" picks Automerge's fullfat_bundler.js which\n// does a static .wasm import that Bun can't wire up. Redirect to the\n// base64 variant which embeds the WASM as a string and self-initialises.\n\nconst automergeBase64Path = resolve(\n process.cwd(),\n \"node_modules/@automerge/automerge/dist/mjs/entrypoints/fullfat_base64.js\"\n);\n\nconst automergeBase64Plugin: BunPlugin = {\n name: \"automerge-base64\",\n setup(build) {\n build.onResolve({ filter: /^@automerge\\/automerge(\\/slim)?$/ }, () => {\n return { path: automergeBase64Path };\n });\n },\n};\n\n// Argument parsing\n\nconst testDir = resolve(process.cwd(), process.argv[2] ?? \"tests/browser\");\nconst filter = process.argv[3] ?? \"\";\nconst headless = process.env[\"HEADLESS\"] !== \"false\";\n\nconst glob = new Glob(\"**/*.browser.{ts,tsx}\");\nconst testFiles: string[] = [];\nfor await (const file of glob.scan({ cwd: testDir, absolute: true })) {\n if (file.includes(\"harness\")) continue;\n if (filter && !file.includes(filter)) continue;\n testFiles.push(file);\n}\n\nif (testFiles.length === 0) {\n console.log(`[browser-runner] no test files found${filter ? ` matching \"${filter}\"` : \"\"}`);\n process.exit(0);\n}\n\nconsole.log(`[browser-runner] found ${testFiles.length} test file(s)`);\n\n// Start server-side infrastructure\n\nconst signalingPort = 39000 + Math.floor(Math.random() * 1000);\nconst signalingApp = new Elysia()\n .use(signalingServer({ path: \"/polly/signaling\" }))\n .listen(signalingPort);\nconsole.log(`[browser-runner] signaling server on ws://127.0.0.1:${signalingPort}/polly/signaling`);\n\n// Launch browser\n\nconst browser = await puppeteer.launch({\n headless,\n args: [\"--no-sandbox\", \"--disable-setuid-sandbox\"],\n});\n\nlet totalPassed = 0;\nlet totalFailed = 0;\n\
|
|
5
|
+
"#!/usr/bin/env bun\n\n/**\n * Browser test runner for Polly applications.\n *\n * Finds all *.browser.ts files in a given directory, bundles each with\n * Bun.build for the browser target (with an internal Automerge WASM fix),\n * serves the bundle on an ephemeral port, opens a Puppeteer page, and\n * polls window.__done for results. Prints pass/fail per test and exits\n * non-zero if any test failed.\n *\n * A signalling server for WebRTC tests starts automatically on a random\n * port. The URL is injected into the bundle via process.env.SIGNALING_URL.\n *\n * Usage (from project root):\n *\n * bun tools/test/src/browser/run.ts [testDir] [filter]\n *\n * Examples:\n *\n * bun tools/test/src/browser/run.ts tests/browser\n * bun tools/test/src/browser/run.ts tests/browser mesh-webrtc\n * HEADLESS=false bun tools/test/src/browser/run.ts tests/browser\n *\n * When invoked without a testDir, defaults to tests/browser relative to cwd.\n */\n\nimport { resolve } from \"node:path\";\nimport { type BunPlugin, Glob } from \"bun\";\nimport { Elysia } from \"elysia\";\nimport puppeteer, { type Page } from \"puppeteer\";\nimport { signalingServer } from \"../../../../src/elysia/signaling-server-plugin\";\n\n// Automerge WASM fix\n// Bun.build's target: \"browser\" picks Automerge's fullfat_bundler.js which\n// does a static .wasm import that Bun can't wire up. Redirect to the\n// base64 variant which embeds the WASM as a string and self-initialises.\n\nconst automergeBase64Path = resolve(\n process.cwd(),\n \"node_modules/@automerge/automerge/dist/mjs/entrypoints/fullfat_base64.js\"\n);\n\nconst automergeBase64Plugin: BunPlugin = {\n name: \"automerge-base64\",\n setup(build) {\n build.onResolve({ filter: /^@automerge\\/automerge(\\/slim)?$/ }, () => {\n return { path: automergeBase64Path };\n });\n },\n};\n\n// Argument parsing\n\nconst testDir = resolve(process.cwd(), process.argv[2] ?? \"tests/browser\");\nconst filter = process.argv[3] ?? \"\";\nconst headless = process.env[\"HEADLESS\"] !== \"false\";\n\nconst glob = new Glob(\"**/*.browser.{ts,tsx}\");\nconst testFiles: string[] = [];\nfor await (const file of glob.scan({ cwd: testDir, absolute: true })) {\n if (file.includes(\"harness\")) continue;\n if (filter && !file.includes(filter)) continue;\n testFiles.push(file);\n}\n\nif (testFiles.length === 0) {\n console.log(`[browser-runner] no test files found${filter ? ` matching \"${filter}\"` : \"\"}`);\n process.exit(0);\n}\n\nconsole.log(`[browser-runner] found ${testFiles.length} test file(s)`);\n\n// Start server-side infrastructure\n\nconst signalingPort = 39000 + Math.floor(Math.random() * 1000);\nconst signalingApp = new Elysia()\n .use(signalingServer({ path: \"/polly/signaling\" }))\n .listen(signalingPort);\nconsole.log(`[browser-runner] signaling server on ws://127.0.0.1:${signalingPort}/polly/signaling`);\n\n// Launch browser\n//\n// protocolTimeout caps how long any single CDP call (e.g. page.evaluate)\n// waits before throwing. Puppeteer's default is 180s, so a stalled\n// renderer hangs the runner for three minutes. 30s fails fast while\n// still tolerating a slow-but-healthy initial sync.\n\nconst browser = await puppeteer.launch({\n headless,\n args: [\"--no-sandbox\", \"--disable-setuid-sandbox\"],\n protocolTimeout: 30_000,\n});\n\nlet totalPassed = 0;\nlet totalFailed = 0;\n\n/** A transient CDP timeout (renderer stall) — retryable, unlike a red test. */\nfunction isProtocolError(err: unknown): boolean {\n return err instanceof Error && err.name === \"ProtocolError\";\n}\n\nfunction errMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * Build, serve, and run one test file on a fresh page. Returns its\n * pass/fail tally. Build failures and test timeouts are reported here\n * (not thrown). A thrown error (e.g. a ProtocolError from a stalled\n * renderer) propagates to the caller so it can retry the file; the\n * page and server are always cleaned up first.\n */\nasync function runFile(testFile: string): Promise<{ passed: number; failed: number }> {\n const buildResult = await Bun.build({\n entrypoints: [testFile],\n target: \"browser\",\n format: \"esm\",\n minify: false,\n sourcemap: \"inline\",\n plugins: [automergeBase64Plugin],\n define: {\n \"process.env.SIGNALING_URL\": JSON.stringify(\n `ws://127.0.0.1:${signalingPort}/polly/signaling`\n ),\n },\n });\n\n if (!buildResult.success) {\n console.log(\" ❌ build failed:\");\n for (const log of buildResult.logs) {\n console.log(` ${log}`);\n }\n return { passed: 0, failed: 1 };\n }\n\n const jsText = await buildResult.outputs[0]?.text();\n if (!jsText) {\n console.log(\" ❌ build produced no output\");\n return { passed: 0, failed: 1 };\n }\n\n const html = `<!DOCTYPE html>\n<html><head><meta charset=\"utf-8\"></head>\n<body>\n<script type=\"module\">${jsText}</script>\n</body></html>`;\n\n const server = Bun.serve({\n port: 0,\n fetch() {\n return new Response(html, { headers: { \"Content-Type\": \"text/html\" } });\n },\n });\n\n let page: Page | undefined;\n try {\n page = await browser.newPage();\n page.on(\"console\", (msg) => {\n const text = msg.text();\n if (text.includes(\"[test]\")) {\n console.log(` ${text}`);\n }\n });\n page.on(\"pageerror\", (err: unknown) => {\n console.log(` ❌ page error: ${errMessage(err)}`);\n });\n\n await page.goto(`http://127.0.0.1:${server.port}/`, { waitUntil: \"domcontentloaded\" });\n\n const timeout = 15_000;\n const deadline = Date.now() + timeout;\n let finished = false;\n while (Date.now() < deadline) {\n finished = await page.evaluate(\n () => (window as unknown as Record<string, unknown>)[\"__done\"] === true\n );\n if (finished) break;\n await new Promise((r) => setTimeout(r, 100));\n }\n\n if (!finished) {\n console.log(` ❌ timed out after ${timeout}ms`);\n return { passed: 0, failed: 1 };\n }\n\n const results = await page.evaluate(\n () =>\n (window as unknown as Record<string, unknown>)[\"__testResults\"] as unknown as Array<{\n name: string;\n passed: boolean;\n error?: string;\n }>\n );\n\n let passed = 0;\n let failed = 0;\n for (const r of results ?? []) {\n if (r.passed) {\n console.log(` ✅ ${r.name}`);\n passed += 1;\n } else {\n console.log(` ❌ ${r.name}: ${r.error}`);\n failed += 1;\n }\n }\n return { passed, failed };\n } finally {\n // Always run, even on the error path. close() can itself throw a\n // ProtocolError under a stall — swallow it so cleanup completes.\n if (page) {\n await page.close().catch(() => {\n // ignore — page may already be gone after a stall\n });\n }\n server.stop();\n }\n}\n\nfor (const testFile of testFiles) {\n const shortName = testFile.replace(`${testDir}/`, \"\");\n console.log(`\\n[browser-runner] running ${shortName}`);\n\n let result: { passed: number; failed: number };\n try {\n result = await runFile(testFile);\n } catch (err) {\n if (isProtocolError(err)) {\n console.log(` ⚠️ protocol error (${errMessage(err)}) — retrying once on a fresh page`);\n try {\n result = await runFile(testFile);\n } catch (retryErr) {\n console.log(` ❌ retry failed: ${errMessage(retryErr)}`);\n result = { passed: 0, failed: 1 };\n }\n } else {\n // A non-protocol error: record the file as failed and keep going,\n // never abort the whole suite.\n console.log(` ❌ ${errMessage(err)}`);\n result = { passed: 0, failed: 1 };\n }\n }\n\n totalPassed += result.passed;\n totalFailed += result.failed;\n}\n\nawait browser.close();\n(signalingApp as unknown as { server?: { stop?: (f?: boolean) => void } }).server?.stop?.(true);\n\nconsole.log(`\\n[browser-runner] ${totalPassed} passed, ${totalFailed} failed`);\nprocess.exit(totalFailed > 0 ? 1 : 0);\n",
|
|
6
6
|
"// @ts-nocheck - Optional peer dependencies (elysia, @elysiajs/eden)\n/**\n * signalingServer — Phase 2 Elysia plugin that exposes a stateless\n * WebSocket route for SDP/ICE relay between $meshState peers.\n *\n * The mesh transport is a star-of-data-channels: peers establish direct\n * WebRTC connections to each other and exchange document operations\n * peer-to-peer once those channels are open. WebRTC connection setup\n * needs an out-of-band channel for SDP offer/answer and ICE candidate\n * exchange, and that channel is what this plugin provides. The plugin\n * does not own any document state, does not hold any encryption keys,\n * and never inspects the contents of the messages it relays. It is a\n * pure pub-sub by peer id.\n *\n * Once two peers have completed the SDP exchange and opened a direct\n * data channel, the signalling server is no longer on the critical\n * path — the peers talk directly. The signalling server's role is\n * therefore intermittent: peers connect to it only during the brief\n * windows when they are establishing or re-establishing connections.\n *\n * Wire protocol:\n *\n * Client → server (join):\n * { type: \"join\", peerId: \"peer-alice\" }\n *\n * Client → server (signal to another peer):\n * { type: \"signal\", peerId: \"peer-alice\", targetPeerId: \"peer-bob\",\n * payload: { ... } }\n *\n * Server → client (delivered signal):\n * { type: \"signal\", peerId: \"peer-alice\", targetPeerId: \"peer-bob\",\n * payload: { ... } }\n *\n * Server → client (notification of unknown target):\n * { type: \"error\", reason: \"unknown-target\", targetPeerId: \"...\" }\n *\n * The `payload` is opaque to the signalling server — typically it\n * carries an SDP offer, SDP answer, or ICE candidate. Applications can\n * also use the relay for any other peer-to-peer message that needs an\n * intermediary, such as the initial handshake of a pairing flow.\n *\n * @example\n * ```ts\n * import { Elysia } from \"elysia\";\n * import { signalingServer } from \"@fairfox/polly/elysia\";\n *\n * const app = new Elysia()\n * .use(signalingServer({ path: \"/polly/signaling\" }))\n * .listen(8080);\n * ```\n */\n\nimport { Elysia } from \"elysia\";\n\n/** A signalling message. The `type` discriminates between client-to-server\n * requests (join, signal), the error envelope, and the server-to-client\n * discovery frames (peers-present, peer-joined, peer-left) that let\n * peers learn about each other without polling. */\nexport type SignalingMessage =\n | {\n type: \"join\";\n /** The peer registering itself with the signalling server. */\n peerId: string;\n }\n | {\n type: \"signal\";\n /** The peer sending the signal. */\n peerId: string;\n /** The peer the signal is being relayed to. */\n targetPeerId: string;\n /** Opaque payload, typically SDP or ICE. */\n payload: unknown;\n }\n | {\n type: \"error\";\n reason: \"unknown-target\" | \"not-joined\" | \"malformed\";\n targetPeerId?: string;\n }\n | {\n /** Sent to a newcomer immediately after it joins, listing every\n * peer that was already joined at that moment. Empty for a lone\n * newcomer. */\n type: \"peers-present\";\n peerIds: string[];\n }\n | {\n /** Broadcast to every incumbent when a new peer joins. */\n type: \"peer-joined\";\n peerId: string;\n }\n | {\n /** Broadcast to every remaining incumbent when a joined peer\n * closes its socket. Never emitted for a connection that never\n * sent a join frame. */\n type: \"peer-left\";\n peerId: string;\n };\n\n/** A frame whose `type` is outside the built-in signalling vocabulary.\n * Consumers who pass an {@link SignalingServerOptions.onCustomFrame}\n * handler receive these on the server side; everything else — including\n * routing them to a specific peer, storing a session, or rejecting the\n * frame — is the consumer's call. Polly does not touch the body. */\nexport interface CustomSignalingFrame {\n type: string;\n [key: string]: unknown;\n}\n\n/** Minimal surface the custom-frame handler receives in place of the\n * Elysia-specific `ws` object so the plugin stays portable. Exposes the\n * `data` bag (used to stash the authenticated peerId under the existing\n * join handshake) and a `send` method. */\nexport interface CustomFrameSocket {\n data: Record<string, unknown>;\n send: (msg: unknown) => void;\n}\n\nexport interface SignalingServerOptions {\n /** WebSocket route path. Defaults to \"/polly/signaling\". */\n path?: string;\n /** Optional hook for frames whose `type` is outside the built-in\n * vocabulary. The plugin invokes it in place of returning a\n * `malformed` error, so consumers can layer their own application\n * protocol (pairing return tokens, presence pings, etc.) on the\n * existing socket. The `peerId` argument is the sender's\n * authenticated peer id from their prior `join` frame, or\n * `undefined` if they haven't joined yet. */\n onCustomFrame?: (\n socket: CustomFrameSocket,\n frame: CustomSignalingFrame,\n peerId: string | undefined\n ) => void;\n}\n\n/**\n * Construct the signalling-server Elysia plugin. The plugin keeps a\n * per-instance map of peer id → WebSocket connection so that incoming\n * \"signal\" messages can be routed to the right target socket. The map\n * is local to the plugin instance, not shared across processes; for\n * multi-instance deployments behind a load balancer, applications need\n * sticky connection routing or a shared backplane (Redis pub-sub or\n * similar). That is a follow-up.\n */\nexport function signalingServer(options: SignalingServerOptions = {}) {\n const path = options.path ?? \"/polly/signaling\";\n const onCustomFrame = options.onCustomFrame;\n // Per-peer-id map of joined sockets. The inverse mapping is stored\n // directly on ws.data (a mutable property bag that Elysia preserves\n // across message callbacks for a given connection); the webrtc-p2p-chat\n // example in examples/ confirms this pattern is stable under Bun.\n const peerSockets = new Map<string, { send: (msg: unknown) => void }>();\n\n // Intentionally unnamed — Elysia deduplicates plugins by name, and each\n // signalingServer() call needs its own closure-captured peer map.\n const parseMessage = (raw: unknown): SignalingMessage | undefined => {\n try {\n return typeof raw === \"string\" ? JSON.parse(raw) : (raw as unknown as SignalingMessage);\n } catch {\n return undefined;\n }\n };\n\n const handleJoin = (ws: unknown, peerId: string): void => {\n const newcomer = ws as unknown as { send: (m: unknown) => void };\n // Collect the peers that were already joined so we can (a) tell the\n // newcomer who is present and (b) tell each of them about the\n // newcomer. A rejoin with the same peerId replaces the prior entry\n // but is otherwise treated as a fresh arrival.\n const incumbents: Array<{ peerId: string; socket: { send: (m: unknown) => void } }> = [];\n for (const [existingPeerId, existingSocket] of peerSockets) {\n if (existingPeerId === peerId) continue;\n incumbents.push({ peerId: existingPeerId, socket: existingSocket });\n }\n peerSockets.set(peerId, newcomer);\n const wsWithData = ws as unknown as { data: Record<string, unknown> };\n wsWithData.data.peerId = peerId;\n\n newcomer.send({\n type: \"peers-present\",\n peerIds: incumbents.map((i) => i.peerId),\n } as unknown as SignalingMessage);\n\n for (const incumbent of incumbents) {\n try {\n incumbent.socket.send({ type: \"peer-joined\", peerId } as unknown as SignalingMessage);\n } catch {\n // If a send fails we leave the stale socket to its own close\n // handler to evict. Dropping here would open a window where\n // the next signal to this peer still thinks it's alive.\n }\n }\n };\n\n const sendUnknownTarget = (ws: unknown, targetPeerId: string): void => {\n (ws as unknown as { send: (m: unknown) => void }).send({\n type: \"error\",\n reason: \"unknown-target\",\n targetPeerId,\n } as unknown as SignalingMessage);\n };\n\n /** Look up a target socket and confirm it is still open. */\n const findOpenTarget = (targetPeerId: string): { send: (msg: unknown) => void } | undefined => {\n const target = peerSockets.get(targetPeerId);\n if (!target) return undefined;\n const readyState = (target as unknown as { readyState?: number }).readyState;\n const OPEN = 1;\n if (readyState !== undefined && readyState !== OPEN) {\n peerSockets.delete(targetPeerId);\n return undefined;\n }\n return target;\n };\n\n const handleSignal = (ws: unknown, msg: Extract<SignalingMessage, { type: \"signal\" }>): void => {\n const wsWithData = ws as unknown as {\n data: Record<string, unknown>;\n send: (m: unknown) => void;\n };\n const senderId = wsWithData.data.peerId as unknown as string | undefined;\n if (!senderId) {\n wsWithData.send({ type: \"error\", reason: \"not-joined\" } as unknown as SignalingMessage);\n return;\n }\n const target = findOpenTarget(msg.targetPeerId);\n if (!target) {\n sendUnknownTarget(ws, msg.targetPeerId);\n return;\n }\n const relayed: SignalingMessage = {\n type: \"signal\",\n peerId: senderId,\n targetPeerId: msg.targetPeerId,\n payload: msg.payload,\n };\n try {\n target.send(relayed);\n } catch {\n peerSockets.delete(msg.targetPeerId);\n sendUnknownTarget(ws, msg.targetPeerId);\n }\n };\n\n return new Elysia().ws(path, {\n message(ws, raw) {\n const msg = parseMessage(raw);\n if (!msg) {\n ws.send({ type: \"error\", reason: \"malformed\" } as unknown as SignalingMessage);\n return;\n }\n if (msg.type === \"join\") {\n handleJoin(ws, msg.peerId);\n return;\n }\n if (msg.type === \"signal\") {\n handleSignal(ws, msg);\n return;\n }\n // Unknown types route to the consumer's custom-frame hook when\n // one is configured. Without a hook they still fall through to\n // the `malformed` error — same behaviour as before this branch\n // existed.\n if (onCustomFrame !== undefined) {\n const wsWithData = ws as unknown as CustomFrameSocket;\n const senderId = wsWithData.data[\"peerId\"];\n const peerId = typeof senderId === \"string\" ? senderId : undefined;\n onCustomFrame(wsWithData, msg as unknown as CustomSignalingFrame, peerId);\n return;\n }\n ws.send({ type: \"error\", reason: \"malformed\" } as unknown as SignalingMessage);\n },\n\n close(ws) {\n const peerId = (ws.data as unknown as Record<string, unknown>).peerId as unknown as\n | string\n | undefined;\n if (!peerId) {\n // Connection that never sent a join — nothing to broadcast and\n // nothing to evict. A bystander coming and going leaves no trace.\n return;\n }\n // Only evict if the map still points at *this* socket. A stale\n // close after the same peerId rejoined on a new socket should not\n // take the fresh entry with it. The comparison uses the `data` bag\n // Elysia attaches to each connection because it is preserved across\n // message and close callbacks, unlike the `ws` wrapper object which\n // Elysia may or may not reuse.\n const mapped = peerSockets.get(peerId);\n const wsData = (ws as unknown as { data: Record<string, unknown> }).data;\n const mappedData = (mapped as unknown as { data?: Record<string, unknown> } | undefined)\n ?.data;\n if (mapped === undefined || mappedData !== wsData) {\n return;\n }\n peerSockets.delete(peerId);\n for (const [_incumbentId, incumbentSocket] of peerSockets) {\n try {\n incumbentSocket.send({ type: \"peer-left\", peerId } as unknown as SignalingMessage);\n } catch {\n // Incumbent socket is gone; its own close handler will tidy.\n }\n }\n },\n });\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "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
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "995635FD6B287CDC64756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
export { type ConsolePattern, isAllowedConsoleLine, MESH_CONSOLE_ALLOWLIST, } from "./console-allowlist";
|
|
22
22
|
export { knownPeersFor, type PrebakedKeyringPair, type PrebakedKeyringSet, type PrebakedPeer, prebakeKeyringPair, prebakeKeyringSet, } from "./keys";
|
|
23
|
-
export { type CapturedConsoleLine, type LaunchedPeer, type LaunchPeerOptions, launchPeer, } from "./launch-peer";
|
|
23
|
+
export { type CapturedConsoleLine, type LaunchedPeer, type LaunchPeerOptions, type LaunchSecondTabOptions, launchPeer, launchSecondTab, } from "./launch-peer";
|
|
24
24
|
export { type DiagnosticRecorder, MeshAssertionError, type MeshAssertionFailure, startDiagnosticRecorder, } from "./mesh-assertions";
|
|
25
25
|
export { type ServeConsumerOptions, type ServeConsumerResult, serveConsumer, } from "./serve-consumer";
|
|
26
26
|
export { type ConvergencePredicate, type PeerSnapshot, type WaitForConvergenceOptions, waitForConvergence, waitForMeshConnected, } from "./wait-for-convergence";
|
|
@@ -806,6 +806,7 @@ ${summary}
|
|
|
806
806
|
return {
|
|
807
807
|
peerId,
|
|
808
808
|
page,
|
|
809
|
+
browser,
|
|
809
810
|
console: consoleLines,
|
|
810
811
|
pageErrors,
|
|
811
812
|
assertNoUnexpectedConsole,
|
|
@@ -827,6 +828,98 @@ ${summary}
|
|
|
827
828
|
}
|
|
828
829
|
};
|
|
829
830
|
}
|
|
831
|
+
async function launchSecondTab(parent, options) {
|
|
832
|
+
const {
|
|
833
|
+
consumerUrl,
|
|
834
|
+
consoleAllowlist = MESH_CONSOLE_ALLOWLIST,
|
|
835
|
+
readyTimeoutMs = 15000
|
|
836
|
+
} = options;
|
|
837
|
+
const page = await parent.browser.newPage();
|
|
838
|
+
const consoleLines = [];
|
|
839
|
+
const pageErrors = [];
|
|
840
|
+
page.on("console", (msg) => {
|
|
841
|
+
const level = msg.type();
|
|
842
|
+
const text = msg.text();
|
|
843
|
+
const allowed = isAllowedConsoleLine({ level, text }, consoleAllowlist);
|
|
844
|
+
consoleLines.push({ level, text, allowed });
|
|
845
|
+
});
|
|
846
|
+
page.on("pageerror", (err) => {
|
|
847
|
+
pageErrors.push(err instanceof Error ? err.message : String(err));
|
|
848
|
+
});
|
|
849
|
+
await page.goto(consumerUrl, { waitUntil: "domcontentloaded" });
|
|
850
|
+
const deadline = Date.now() + readyTimeoutMs;
|
|
851
|
+
let ready = false;
|
|
852
|
+
let lastStatus = "";
|
|
853
|
+
while (Date.now() < deadline) {
|
|
854
|
+
lastStatus = await page.evaluate(() => document.querySelector("[data-e2e='status']")?.textContent ?? "");
|
|
855
|
+
if (lastStatus === "ready") {
|
|
856
|
+
ready = true;
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
if (lastStatus.startsWith("error") || lastStatus.startsWith("bootstrap-failed")) {
|
|
860
|
+
await page.close();
|
|
861
|
+
throw new Error(`launchSecondTab(${parent.peerId}): consumer reported "${lastStatus}"`);
|
|
862
|
+
}
|
|
863
|
+
await new Promise((r) => setTimeout(r, READY_POLL_MS));
|
|
864
|
+
}
|
|
865
|
+
if (!ready) {
|
|
866
|
+
await page.close();
|
|
867
|
+
throw new Error(`launchSecondTab(${parent.peerId}): consumer did not reach "ready" within ${readyTimeoutMs}ms (last status: "${lastStatus}")`);
|
|
868
|
+
}
|
|
869
|
+
function assertNoUnexpectedConsole() {
|
|
870
|
+
const bad = consoleLines.filter((line) => !line.allowed && (line.level === "error" || line.level === "warn" || line.level === "warning"));
|
|
871
|
+
if (bad.length > 0) {
|
|
872
|
+
const summary = bad.map((l) => ` [${l.level}] ${l.text}`).join(`
|
|
873
|
+
`);
|
|
874
|
+
throw new Error(`launchSecondTab(${parent.peerId}): unexpected console output:
|
|
875
|
+
${summary}`);
|
|
876
|
+
}
|
|
877
|
+
if (pageErrors.length > 0) {
|
|
878
|
+
throw new Error(`launchSecondTab(${parent.peerId}): page errors:
|
|
879
|
+
${pageErrors.map((e) => ` ${e}`).join(`
|
|
880
|
+
`)}`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async function collectDiagnostics() {
|
|
884
|
+
return page.evaluate(() => {
|
|
885
|
+
const w = window;
|
|
886
|
+
return w.__pollyE2eDiagnostics ? [...w.__pollyE2eDiagnostics] : [];
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
async function assertNoSilentDrops(allow = []) {
|
|
890
|
+
const allowed = new Set(allow);
|
|
891
|
+
const events = await collectDiagnostics();
|
|
892
|
+
const unexpected = events.filter((event) => event.kind.startsWith("drop:") && !allowed.has(event.kind));
|
|
893
|
+
if (unexpected.length === 0)
|
|
894
|
+
return;
|
|
895
|
+
const summary = unexpected.map((event) => {
|
|
896
|
+
const { kind, timestamp: _ts, ...rest } = event;
|
|
897
|
+
return ` ${kind} ${JSON.stringify(rest)}`;
|
|
898
|
+
}).join(`
|
|
899
|
+
`);
|
|
900
|
+
throw new MeshAssertionError(`launchSecondTab(${parent.peerId}): unexpected silent-drop diagnostics fired during the e2e run.
|
|
901
|
+
${summary}`, unexpected);
|
|
902
|
+
}
|
|
903
|
+
let closed = false;
|
|
904
|
+
return {
|
|
905
|
+
peerId: parent.peerId,
|
|
906
|
+
page,
|
|
907
|
+
browser: parent.browser,
|
|
908
|
+
console: consoleLines,
|
|
909
|
+
pageErrors,
|
|
910
|
+
assertNoUnexpectedConsole,
|
|
911
|
+
collectDiagnostics,
|
|
912
|
+
assertNoSilentDrops,
|
|
913
|
+
close: async () => {
|
|
914
|
+
if (closed)
|
|
915
|
+
return;
|
|
916
|
+
closed = true;
|
|
917
|
+
try {
|
|
918
|
+
await page.close();
|
|
919
|
+
} catch {}
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
}
|
|
830
923
|
// tools/test/src/e2e-mesh/serve-consumer.ts
|
|
831
924
|
var {readFileSync} = (() => ({}));
|
|
832
925
|
var __dirname = "/Users/AJT/projects/polly/packages/polly/tools/test/src/e2e-mesh";
|
|
@@ -1079,6 +1172,7 @@ export {
|
|
|
1079
1172
|
serveConsumer,
|
|
1080
1173
|
prebakeKeyringSet,
|
|
1081
1174
|
prebakeKeyringPair,
|
|
1175
|
+
launchSecondTab,
|
|
1082
1176
|
launchPeer,
|
|
1083
1177
|
knownPeersFor,
|
|
1084
1178
|
isAllowedConsoleLine,
|
|
@@ -1086,4 +1180,4 @@ export {
|
|
|
1086
1180
|
MESH_CONSOLE_ALLOWLIST
|
|
1087
1181
|
};
|
|
1088
1182
|
|
|
1089
|
-
//# debugId=
|
|
1183
|
+
//# debugId=2D797840A9246AEE64756E2164756E21
|