@fairfox/polly 0.86.0 → 0.87.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.
@@ -159,6 +159,50 @@ function resolveListenPort(app) {
159
159
  function errMessage(err) {
160
160
  return err instanceof Error ? err.message : String(err);
161
161
  }
162
+ function emptyProgress() {
163
+ return { started: [], completed: [] };
164
+ }
165
+ function applyProgress(progress, event) {
166
+ if (event.kind === "plan") {
167
+ progress.planned = event.total;
168
+ return;
169
+ }
170
+ if (event.kind === "start") {
171
+ progress.started.push(event.name);
172
+ return;
173
+ }
174
+ progress.completed.push({ name: event.name, passed: event.passed, error: event.error });
175
+ }
176
+ function describeStall(state, timeoutMs) {
177
+ const prefix = `timed out after ${timeoutMs}ms`;
178
+ if (state === "looping") {
179
+ return `${prefix} — the page never yielded its main thread, so JavaScript is still running. That is a runaway loop in the code under test, not a stalled runner.`;
180
+ }
181
+ if (state === "idle") {
182
+ return `${prefix} — the page still answers, so its main thread is free. The suite never called done(): an awaited promise never settled.`;
183
+ }
184
+ return `${prefix} — the page stopped reporting and its state could not be probed.`;
185
+ }
186
+ function summariseTimeout(progress, timeoutMs, state) {
187
+ const passed = progress.completed.filter((r) => r.passed).length;
188
+ const completedFailed = progress.completed.length - passed;
189
+ const lines = [` ❌ ${describeStall(state, timeoutMs)}`];
190
+ for (const r of progress.completed) {
191
+ lines.push(r.passed ? ` ✅ ${r.name}` : ` ❌ ${r.name}: ${r.error}`);
192
+ }
193
+ const inFlight = progress.started[progress.completed.length];
194
+ if (inFlight !== undefined) {
195
+ lines.push(` ⏳ stalled in: ${inFlight}`);
196
+ }
197
+ if (progress.planned === undefined) {
198
+ lines.push(" ⚠ the page never reported a test count — counting the file as one failure");
199
+ return { tally: { passed, failed: completedFailed + 1 }, lines };
200
+ }
201
+ const neverFinished = Math.max(progress.planned - progress.completed.length, 0);
202
+ lines.push(` ⚠ ${neverFinished} of ${progress.planned} tests never finished (${passed} passed, ${completedFailed} failed before the stall)`);
203
+ const failed = neverFinished === 0 ? completedFailed + 1 : completedFailed + neverFinished;
204
+ return { tally: { passed, failed }, lines };
205
+ }
162
206
  async function runSuite(testFiles, runFile, options = {}) {
163
207
  const label = options.label ?? ((f) => f);
164
208
  const log = options.log ?? console.log;
@@ -180,6 +224,100 @@ async function runSuite(testFiles, runFile, options = {}) {
180
224
  return { passed: totalPassed, failed: totalFailed };
181
225
  }
182
226
 
227
+ // tools/test/src/browser/source-map.ts
228
+ var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
229
+ function decodeSegment(segment) {
230
+ const fields = [];
231
+ let shift = 0;
232
+ let value = 0;
233
+ for (const char of segment) {
234
+ const digit = BASE64_ALPHABET.indexOf(char);
235
+ if (digit === -1)
236
+ return [];
237
+ const hasContinuation = (digit & 32) !== 0;
238
+ value += (digit & 31) << shift;
239
+ if (hasContinuation) {
240
+ shift += 5;
241
+ continue;
242
+ }
243
+ const negative = (value & 1) === 1;
244
+ const magnitude = value >> 1;
245
+ fields.push(negative ? -magnitude : magnitude);
246
+ value = 0;
247
+ shift = 0;
248
+ }
249
+ return fields;
250
+ }
251
+ function decodeMappings(mappings) {
252
+ const lines = [];
253
+ let sourceIndex = 0;
254
+ let sourceLine = 0;
255
+ let sourceColumn = 0;
256
+ for (const lineText of mappings.split(";")) {
257
+ const segments = [];
258
+ let generatedColumn = 0;
259
+ for (const segmentText of lineText.split(",")) {
260
+ if (!segmentText)
261
+ continue;
262
+ const fields = decodeSegment(segmentText);
263
+ const [columnDelta, sourceDelta, lineDelta, sourceColumnDelta] = fields;
264
+ if (columnDelta === undefined)
265
+ continue;
266
+ generatedColumn += columnDelta;
267
+ if (sourceDelta === undefined || lineDelta === undefined || sourceColumnDelta === undefined) {
268
+ continue;
269
+ }
270
+ sourceIndex += sourceDelta;
271
+ sourceLine += lineDelta;
272
+ sourceColumn += sourceColumnDelta;
273
+ segments.push({ generatedColumn, sourceIndex, sourceLine, sourceColumn });
274
+ }
275
+ lines.push(segments);
276
+ }
277
+ return lines;
278
+ }
279
+ function parseInlineSourceMap(bundle) {
280
+ const match = bundle.match(/\/\/# sourceMappingURL=data:application\/json;(?:charset=[^;]+;)?base64,([A-Za-z0-9+/=]+)/);
281
+ const encoded = match?.[1];
282
+ if (!encoded)
283
+ return;
284
+ try {
285
+ const parsed = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
286
+ if (typeof parsed !== "object" || parsed === null)
287
+ return;
288
+ const { sources, mappings } = parsed;
289
+ if (!Array.isArray(sources) || typeof mappings !== "string")
290
+ return;
291
+ return { sources: sources.map(String), mappings };
292
+ } catch {
293
+ return;
294
+ }
295
+ }
296
+ function createSourceMapLookup(bundle, lineOffset = 0) {
297
+ const map = parseInlineSourceMap(bundle);
298
+ if (!map)
299
+ return;
300
+ const lines = decodeMappings(map.mappings);
301
+ return (line, column) => {
302
+ const generatedLine = line - lineOffset;
303
+ if (generatedLine < 0)
304
+ return;
305
+ const segments = lines[generatedLine];
306
+ if (!segments || segments.length === 0)
307
+ return;
308
+ let best = segments[0];
309
+ for (const segment of segments) {
310
+ if (segment.generatedColumn > column)
311
+ break;
312
+ best = segment;
313
+ }
314
+ if (!best)
315
+ return;
316
+ const source = map.sources[best.sourceIndex] ?? "(unknown source)";
317
+ return `${source}:${best.sourceLine + 1}:${best.sourceColumn}`;
318
+ };
319
+ }
320
+
183
321
  // tools/test/src/browser/run.ts
184
322
  var automergeMarker = "@automerge/automerge";
185
323
  var automergeEntry = Bun.resolveSync(automergeMarker, import.meta.dir);
@@ -212,11 +350,115 @@ console.log(`[browser-runner] found ${testFiles.length} test file(s)`);
212
350
  var signalingApp = new Elysia2().use(signalingServer({ path: "/polly/signaling" })).listen(0);
213
351
  var signalingPort = resolveListenPort(signalingApp);
214
352
  console.log(`[browser-runner] signaling server on ws://127.0.0.1:${signalingPort}/polly/signaling`);
215
- var browser = await puppeteer.launch({
216
- headless,
217
- args: ["--no-sandbox", "--disable-setuid-sandbox"]
218
- });
219
- async function runFile(testFile) {
353
+ var launchArgs = ["--no-sandbox", "--disable-setuid-sandbox"];
354
+ var browser = await puppeteer.launch({ headless, args: launchArgs });
355
+ var PROBE_TIMEOUT_MS = 2000;
356
+ var PAUSE_TIMEOUT_MS = 8000;
357
+ var CLOSE_TIMEOUT_MS = 5000;
358
+ var captureStacks = process.env["POLLY_BROWSER_STACK"] === "1";
359
+
360
+ class FileTimeout extends Error {
361
+ }
362
+ async function withDeadline(task, ms) {
363
+ let timer;
364
+ const deadline = new Promise((r) => {
365
+ timer = setTimeout(r, ms);
366
+ });
367
+ try {
368
+ await Promise.race([task.then(noop, noop), deadline]);
369
+ } finally {
370
+ if (timer)
371
+ clearTimeout(timer);
372
+ }
373
+ }
374
+ function noop() {}
375
+ async function ensureBrowser() {
376
+ if (browser.connected)
377
+ return browser;
378
+ console.log("[browser-runner] browser connection lost — relaunching");
379
+ browser = await puppeteer.launch({ headless, args: launchArgs });
380
+ return browser;
381
+ }
382
+ async function probeStallState(page) {
383
+ if (page.isClosed() || !browser.connected)
384
+ return "unknown";
385
+ let timer;
386
+ const deadline = new Promise((r) => {
387
+ timer = setTimeout(() => r(false), PROBE_TIMEOUT_MS);
388
+ });
389
+ try {
390
+ const answered = await Promise.race([
391
+ page.evaluate(() => true).then(() => true, () => false),
392
+ deadline
393
+ ]);
394
+ return answered ? "idle" : "looping";
395
+ } finally {
396
+ if (timer)
397
+ clearTimeout(timer);
398
+ }
399
+ }
400
+ function presentPosition(position) {
401
+ const lastColon = position.lastIndexOf(":");
402
+ const fileEnd = lastColon === -1 ? -1 : position.lastIndexOf(":", lastColon - 1);
403
+ if (fileEnd === -1)
404
+ return position;
405
+ const absolute = resolve(process.cwd(), position.slice(0, fileEnd));
406
+ const prefix = `${process.cwd()}/`;
407
+ const shown = absolute.startsWith(prefix) ? absolute.slice(prefix.length) : absolute;
408
+ return `${shown}${position.slice(fileEnd)}`;
409
+ }
410
+ async function printPausedStack(debug) {
411
+ const session = debug.session;
412
+ if (!session)
413
+ return;
414
+ await session.send("Debugger.pause").catch(noop);
415
+ const deadline = Date.now() + PAUSE_TIMEOUT_MS;
416
+ while (Date.now() < deadline && !debug.frames()) {
417
+ await new Promise((r) => setTimeout(r, 100));
418
+ }
419
+ const frames = debug.frames();
420
+ if (!frames) {
421
+ console.log(" (the page could not be paused — no stack available)");
422
+ return;
423
+ }
424
+ console.log(" the page was executing:");
425
+ for (const frame of frames.callFrames.slice(0, 12)) {
426
+ const { lineNumber, columnNumber } = frame.location;
427
+ const column = columnNumber ?? 0;
428
+ const original = debug.resolve(lineNumber, column);
429
+ const where = original ? presentPosition(original) : `${frame.url || "(inline)"}:${lineNumber + 1}:${column}`;
430
+ console.log(` ${frame.functionName || "(anonymous)"} @ ${where}`);
431
+ }
432
+ }
433
+ async function reportTimeout(page, progress, timeoutMs, debug) {
434
+ const state = await probeStallState(page);
435
+ const { tally, lines } = summariseTimeout(progress, timeoutMs, state);
436
+ for (const line of lines)
437
+ console.log(line);
438
+ if (state !== "looping")
439
+ return tally;
440
+ if (debug.session) {
441
+ await printPausedStack(debug);
442
+ } else {
443
+ console.log(" re-run with POLLY_BROWSER_STACK=1 to print the running stack");
444
+ }
445
+ return tally;
446
+ }
447
+ function printResults(results) {
448
+ let passed = 0;
449
+ let failed = 0;
450
+ for (const r of results) {
451
+ if (r.passed) {
452
+ console.log(` ✅ ${r.name}`);
453
+ passed += 1;
454
+ } else {
455
+ console.log(` ❌ ${r.name}: ${r.error}`);
456
+ failed += 1;
457
+ }
458
+ }
459
+ return { passed, failed };
460
+ }
461
+ async function buildPage(testFile) {
220
462
  const buildResult = await Bun.build({
221
463
  entrypoints: [testFile],
222
464
  target: "browser",
@@ -233,27 +475,42 @@ async function runFile(testFile) {
233
475
  for (const log of buildResult.logs) {
234
476
  console.log(` ${log}`);
235
477
  }
236
- return { passed: 0, failed: 1 };
478
+ return;
237
479
  }
238
480
  const jsText = await buildResult.outputs[0]?.text();
239
481
  if (!jsText) {
240
482
  console.log(" ❌ build produced no output");
241
- return { passed: 0, failed: 1 };
483
+ return;
242
484
  }
243
- const html = `<!DOCTYPE html>
485
+ const htmlPrefix = `<!DOCTYPE html>
244
486
  <html><head><meta charset="utf-8"></head>
245
487
  <body>
246
- <script type="module">${jsText}</script>
247
- </body></html>`;
488
+ <script type="module">
489
+ `;
490
+ return {
491
+ jsText,
492
+ html: `${htmlPrefix}${jsText}
493
+ </script>
494
+ </body></html>`,
495
+ bundleLineOffset: htmlPrefix.split(`
496
+ `).length - 1
497
+ };
498
+ }
499
+ async function runFile(testFile) {
500
+ const built = await buildPage(testFile);
501
+ if (!built)
502
+ return { passed: 0, failed: 1 };
503
+ const { jsText, html, bundleLineOffset } = built;
248
504
  const server = Bun.serve({
249
505
  port: 0,
250
506
  fetch() {
251
507
  return new Response(html, { headers: { "Content-Type": "text/html" } });
252
508
  }
253
509
  });
510
+ const context = await (await ensureBrowser()).createBrowserContext();
254
511
  let page;
255
512
  try {
256
- const newPage = await browser.newPage();
513
+ const newPage = await context.newPage();
257
514
  page = newPage;
258
515
  newPage.on("console", (msg) => {
259
516
  const text = msg.text();
@@ -270,40 +527,65 @@ async function runFile(testFile) {
270
527
  await newPage.exposeFunction("__pollyReport", (results2) => {
271
528
  reportResolve(results2);
272
529
  });
530
+ const progress = emptyProgress();
531
+ await newPage.exposeFunction("__pollyProgress", (event) => {
532
+ applyProgress(progress, event);
533
+ });
273
534
  newPage.on("pageerror", (err) => {
274
535
  reportReject(err instanceof Error ? err : new Error(errMessage(err)));
275
536
  });
276
- await newPage.goto(`http://127.0.0.1:${server.port}/`, { waitUntil: "domcontentloaded" });
537
+ let pausedFrames;
538
+ let debugSession;
539
+ let lookup;
540
+ let lookupBuilt = false;
541
+ const resolvePosition = (line, column) => {
542
+ if (!lookupBuilt) {
543
+ lookup = createSourceMapLookup(jsText, bundleLineOffset);
544
+ lookupBuilt = true;
545
+ }
546
+ return lookup?.(line, column);
547
+ };
548
+ if (captureStacks) {
549
+ debugSession = await newPage.createCDPSession();
550
+ debugSession.on("Debugger.paused", (event) => {
551
+ pausedFrames = event;
552
+ });
553
+ await debugSession.send("Debugger.enable");
554
+ }
277
555
  const timeoutMs = Number(process.env["POLLY_BROWSER_TIMEOUT_MS"] ?? 60000);
556
+ newPage.setDefaultNavigationTimeout(timeoutMs);
557
+ const debug = {
558
+ session: debugSession,
559
+ frames: () => pausedFrames,
560
+ resolve: resolvePosition
561
+ };
562
+ try {
563
+ await newPage.goto(`http://127.0.0.1:${server.port}/`, { waitUntil: "domcontentloaded" });
564
+ } catch (err) {
565
+ if (!(err instanceof Error) || err.name !== "TimeoutError")
566
+ throw err;
567
+ return await reportTimeout(newPage, progress, timeoutMs, debug);
568
+ }
278
569
  let timeoutTimer;
279
570
  const timeout = new Promise((_, reject) => {
280
- timeoutTimer = setTimeout(() => {
281
- reject(new Error(`timed out after ${timeoutMs}ms waiting for __pollyReport — the in-page suite never reported`));
282
- }, timeoutMs);
571
+ timeoutTimer = setTimeout(() => reject(new FileTimeout), timeoutMs);
283
572
  });
284
573
  let results;
285
574
  try {
286
575
  results = await Promise.race([outcome, timeout]);
576
+ } catch (err) {
577
+ if (!(err instanceof FileTimeout))
578
+ throw err;
579
+ return await reportTimeout(newPage, progress, timeoutMs, debug);
287
580
  } finally {
288
581
  if (timeoutTimer)
289
582
  clearTimeout(timeoutTimer);
290
583
  }
291
- let passed = 0;
292
- let failed = 0;
293
- for (const r of results) {
294
- if (r.passed) {
295
- console.log(` ✅ ${r.name}`);
296
- passed += 1;
297
- } else {
298
- console.log(` ❌ ${r.name}: ${r.error}`);
299
- failed += 1;
300
- }
301
- }
302
- return { passed, failed };
584
+ return printResults(results);
303
585
  } finally {
304
- if (page) {
305
- await page.close().catch(() => {});
306
- }
586
+ if (page)
587
+ await withDeadline(page.close(), CLOSE_TIMEOUT_MS);
588
+ await withDeadline(context.close(), CLOSE_TIMEOUT_MS);
307
589
  server.stop();
308
590
  }
309
591
  }
@@ -316,4 +598,4 @@ console.log(`
316
598
  [browser-runner] ${totalPassed} passed, ${totalFailed} failed`);
317
599
  process.exit(totalFailed > 0 ? 1 : 0);
318
600
 
319
- //# debugId=4731421A310B38F664756E2164756E21
601
+ //# debugId=0B420D0CE9973A2C64756E2164756E21
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../tools/test/src/browser/run.ts", "../src/elysia/signaling-server-plugin.ts", "../tools/test/src/e2e-shared/ephemeral-port.ts", "../tools/test/src/browser/runner-core.ts"],
3
+ "sources": ["../tools/test/src/browser/run.ts", "../src/elysia/signaling-server-plugin.ts", "../tools/test/src/e2e-shared/ephemeral-port.ts", "../tools/test/src/browser/runner-core.ts", "../tools/test/src/browser/source-map.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, { type Page } from \"puppeteer\";\nimport { signalingServer } from \"../../../../src/elysia/signaling-server-plugin\";\nimport { resolveListenPort } from \"../e2e-shared/ephemeral-port\";\nimport { errMessage, type FileTally, runSuite } from \"./runner-core\";\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\n// Resolve Automerge relative to polly's own install, not the runner's CWD.\n// `dist/mjs/entrypoints/fullfat_base64.js` is not in the package's `exports`\n// map, so it cannot be resolved by subpath — resolve the package entry and\n// derive the root. A CWD-relative path silently failed to exist when the\n// dependency was hoisted differently in a consumer monorepo (polly#159).\nconst automergeMarker = \"@automerge/automerge\";\nconst automergeEntry = Bun.resolveSync(automergeMarker, import.meta.dir);\nconst automergeBase64Path = resolve(\n automergeEntry.slice(0, automergeEntry.lastIndexOf(automergeMarker) + automergeMarker.length),\n \"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\n// Port 0: the kernel assigns a free port and the server owns it from that\n// moment, so a second runner on the same machine cannot collide (polly#174).\nconst signalingApp = new Elysia().use(signalingServer({ path: \"/polly/signaling\" })).listen(0);\nconst signalingPort = resolveListenPort(signalingApp);\nconsole.log(`[browser-runner] signaling server on ws://127.0.0.1:${signalingPort}/polly/signaling`);\n\n// Launch browser\n//\n// No `protocolTimeout` override: results arrive via `page.exposeFunction`\n// (a push from the page over CDP `Runtime.bindingCalled` events), not via\n// polled `page.evaluate` calls. Without a long-running `Runtime.callFunctionOn`\n// on the hot path there is no protocol round-trip for a busy renderer to\n// stall, so the timeout the previous polling design had to guard against\n// is no longer reachable (polly#138).\n\nconst browser = await puppeteer.launch({\n headless,\n args: [\"--no-sandbox\", \"--disable-setuid-sandbox\"],\n});\n\n/**\n * Build, serve, and run one test file on a fresh page. Returns its\n * pass/fail tally. Build failures are reported here (not thrown);\n * a page-level uncaught error propagates so the suite records the\n * file as failed. The page and server are always cleaned up first.\n */\ninterface TestResult {\n name: string;\n passed: boolean;\n error?: string;\n}\nasync function runFile(testFile: string): Promise<FileTally> {\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 const newPage = await browser.newPage();\n page = newPage;\n newPage.on(\"console\", (msg) => {\n const text = msg.text();\n if (text.includes(\"[test]\")) {\n console.log(` ${text}`);\n }\n });\n\n // Push-based reporting (polly#138): the page calls back into Node via\n // `__pollyReport(results)` when its in-page suite has finished.\n let reportResolve!: (results: TestResult[]) => void;\n let reportReject!: (err: Error) => void;\n const outcome = new Promise<TestResult[]>((resolve, reject) => {\n reportResolve = resolve;\n reportReject = reject;\n });\n\n // Wire the report channel BEFORE navigating. `exposeFunction` is async;\n // if the page were allowed to load first it could call `__pollyReport`\n // before the binding existed, and the result would be lost forever —\n // the deadlock seen when the runner's CWD was outside the test package\n // (polly#159).\n await newPage.exposeFunction(\"__pollyReport\", (results: TestResult[]) => {\n reportResolve(results);\n });\n newPage.on(\"pageerror\", (err: unknown) => {\n reportReject(err instanceof Error ? err : new Error(errMessage(err)));\n });\n\n await newPage.goto(`http://127.0.0.1:${server.port}/`, { waitUntil: \"domcontentloaded\" });\n\n // Bound the wait so a page that never reports (a swallowed error, a hung\n // renderer) fails the file instead of hanging the whole suite forever\n // (polly#159). Override via POLLY_BROWSER_TIMEOUT_MS.\n const timeoutMs = Number(process.env[\"POLLY_BROWSER_TIMEOUT_MS\"] ?? 60000);\n let timeoutTimer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timeoutTimer = setTimeout(() => {\n reject(\n new Error(\n `timed out after ${timeoutMs}ms waiting for __pollyReport — the in-page suite never reported`\n )\n );\n }, timeoutMs);\n });\n\n let results: TestResult[];\n try {\n results = await Promise.race([outcome, timeout]);\n } finally {\n if (timeoutTimer) clearTimeout(timeoutTimer);\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 if (page) {\n await page.close().catch(() => {\n // ignore — page may already be gone after an uncaught error\n });\n }\n server.stop();\n }\n}\n\nconst { passed: totalPassed, failed: totalFailed } = await runSuite(testFiles, runFile, {\n label: (testFile) => testFile.replace(`${testDir}/`, \"\"),\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",
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, and opens a Puppeteer page in its\n * own browser context. The page pushes its results back through bindings the\n * runner exposes (`__pollyReport` for the final tally, `__pollyProgress` per\n * test). Prints pass/fail per test and exits non-zero if any test failed.\n *\n * A file that stops reporting is diagnosed rather than merely timed out: the\n * runner asks the page whether its main thread is still free, so a runaway\n * loop in the code under test reads differently from a suite that never\n * finished, and the tests that never ran are counted (polly#177). Set\n * POLLY_BROWSER_STACK=1 to also print the stack a wedged page is executing.\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 Browser, type CDPSession, type Page } from \"puppeteer\";\nimport { signalingServer } from \"../../../../src/elysia/signaling-server-plugin\";\nimport { resolveListenPort } from \"../e2e-shared/ephemeral-port\";\nimport {\n applyProgress,\n emptyProgress,\n errMessage,\n type FileTally,\n type ProgressEvent,\n runSuite,\n type StallState,\n type SuiteProgress,\n summariseTimeout,\n type TestResult,\n} from \"./runner-core\";\nimport { createSourceMapLookup, type SourceMapLookup } from \"./source-map\";\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\n// Resolve Automerge relative to polly's own install, not the runner's CWD.\n// `dist/mjs/entrypoints/fullfat_base64.js` is not in the package's `exports`\n// map, so it cannot be resolved by subpath — resolve the package entry and\n// derive the root. A CWD-relative path silently failed to exist when the\n// dependency was hoisted differently in a consumer monorepo (polly#159).\nconst automergeMarker = \"@automerge/automerge\";\nconst automergeEntry = Bun.resolveSync(automergeMarker, import.meta.dir);\nconst automergeBase64Path = resolve(\n automergeEntry.slice(0, automergeEntry.lastIndexOf(automergeMarker) + automergeMarker.length),\n \"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\n// Port 0: the kernel assigns a free port and the server owns it from that\n// moment, so a second runner on the same machine cannot collide (polly#174).\nconst signalingApp = new Elysia().use(signalingServer({ path: \"/polly/signaling\" })).listen(0);\nconst signalingPort = resolveListenPort(signalingApp);\nconsole.log(`[browser-runner] signaling server on ws://127.0.0.1:${signalingPort}/polly/signaling`);\n\n// Launch browser\n//\n// No `protocolTimeout` override: results arrive via `page.exposeFunction`\n// (a push from the page over CDP `Runtime.bindingCalled` events), not via\n// polled `page.evaluate` calls. Without a long-running `Runtime.callFunctionOn`\n// on the hot path there is no protocol round-trip for a busy renderer to\n// stall, so the timeout the previous polling design had to guard against\n// is no longer reachable (polly#138).\n\nconst launchArgs = [\"--no-sandbox\", \"--disable-setuid-sandbox\"];\n\nlet browser: Browser = await puppeteer.launch({ headless, args: launchArgs });\n\n/** How long to wait for a page to answer before calling it wedged. */\nconst PROBE_TIMEOUT_MS = 2_000;\n/** How long to wait for a paused stack when POLLY_BROWSER_STACK is set. */\nconst PAUSE_TIMEOUT_MS = 8_000;\n/** How long teardown of one file's context may take before it is abandoned. */\nconst CLOSE_TIMEOUT_MS = 5_000;\n\n/** Set POLLY_BROWSER_STACK=1 to capture the JS stack of a wedged page. */\nconst captureStacks = process.env[\"POLLY_BROWSER_STACK\"] === \"1\";\n\n/** Marks the per-file deadline so a real page error still propagates. */\nclass FileTimeout extends Error {}\n\n/** Await `task`, giving up after `ms`. Never rejects. */\nasync function withDeadline(task: Promise<unknown>, ms: number): Promise<void> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const deadline = new Promise<void>((r) => {\n timer = setTimeout(r, ms);\n });\n try {\n await Promise.race([task.then(noop, noop), deadline]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\nfunction noop(): void {\n /* a settled task's value and error are both uninteresting here */\n}\n\n/**\n * The browser for the next file, relaunched if the connection has gone.\n *\n * A file whose page wedged used to be able to take the whole browser with\n * it, and every later file then failed with `Connection closed.` — one\n * stall turning five healthy files into five failures (polly#177). Each\n * file now checks the connection first, so a dead browser costs one\n * relaunch instead of the rest of the run.\n */\nasync function ensureBrowser(): Promise<Browser> {\n if (browser.connected) return browser;\n console.log(\"[browser-runner] browser connection lost — relaunching\");\n browser = await puppeteer.launch({ headless, args: launchArgs });\n return browser;\n}\n\n/**\n * Ask the page a trivial question to find out whether its main thread is\n * free. A page in a runaway loop never runs the task, so the call never\n * returns; that silence is the reading.\n */\nasync function probeStallState(page: Page): Promise<StallState> {\n if (page.isClosed() || !browser.connected) return \"unknown\";\n let timer: ReturnType<typeof setTimeout> | undefined;\n const deadline = new Promise<boolean>((r) => {\n timer = setTimeout(() => r(false), PROBE_TIMEOUT_MS);\n });\n try {\n const answered = await Promise.race([\n page\n .evaluate(() => true)\n .then(\n () => true,\n () => false\n ),\n deadline,\n ]);\n return answered ? \"idle\" : \"looping\";\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\n/**\n * Present a mapped `path:line:column` relative to the working directory.\n *\n * A bundle's `sources` are relative to the runner's cwd, which for a file\n * outside the package reads as a long climb of `../` segments. Resolve it,\n * then show the shorter of the two forms.\n */\nfunction presentPosition(position: string): string {\n const lastColon = position.lastIndexOf(\":\");\n const fileEnd = lastColon === -1 ? -1 : position.lastIndexOf(\":\", lastColon - 1);\n if (fileEnd === -1) return position;\n const absolute = resolve(process.cwd(), position.slice(0, fileEnd));\n const prefix = `${process.cwd()}/`;\n const shown = absolute.startsWith(prefix) ? absolute.slice(prefix.length) : absolute;\n return `${shown}${position.slice(fileEnd)}`;\n}\n\n/**\n * Interrupt a wedged page and print where its JavaScript is.\n *\n * `Debugger.enable` is itself dispatched to the main thread, so it cannot be\n * sent once that thread is spinning — the domain has to be live before the\n * page starts work. That costs V8 some optimisation on every run, so it is\n * opt-in via POLLY_BROWSER_STACK=1 and the timeout message points at it.\n */\nasync function printPausedStack(debug: DebugAttachment): Promise<void> {\n const session = debug.session;\n if (!session) return;\n await session.send(\"Debugger.pause\").catch(noop);\n const deadline = Date.now() + PAUSE_TIMEOUT_MS;\n while (Date.now() < deadline && !debug.frames()) {\n await new Promise((r) => setTimeout(r, 100));\n }\n const frames = debug.frames();\n if (!frames) {\n console.log(\" (the page could not be paused — no stack available)\");\n return;\n }\n console.log(\" the page was executing:\");\n for (const frame of frames.callFrames.slice(0, 12)) {\n const { lineNumber, columnNumber } = frame.location;\n const column = columnNumber ?? 0;\n const original = debug.resolve(lineNumber, column);\n const where = original\n ? presentPosition(original)\n : `${frame.url || \"(inline)\"}:${lineNumber + 1}:${column}`;\n console.log(` ${frame.functionName || \"(anonymous)\"} @ ${where}`);\n }\n}\n\ninterface PausedFrames {\n callFrames: Array<{\n functionName: string;\n url: string;\n location: { lineNumber: number; columnNumber?: number };\n }>;\n}\n\n/** A live debugger attachment, present only under POLLY_BROWSER_STACK=1. */\ninterface DebugAttachment {\n session?: CDPSession;\n frames: () => PausedFrames | undefined;\n /** Generated document position → the author's own source, when mappable. */\n resolve: (line: number, column: number) => string | undefined;\n}\n\n/**\n * Report a file that ran out of time: what the page was doing, how far the\n * suite got, and how many of its tests never finished.\n */\nasync function reportTimeout(\n page: Page,\n progress: SuiteProgress,\n timeoutMs: number,\n debug: DebugAttachment\n): Promise<FileTally> {\n // Read the page's state before tearing it down: what the renderer was doing\n // decides whether this is a runaway loop in the code under test or a suite\n // that simply never finished (polly#177).\n const state = await probeStallState(page);\n const { tally, lines } = summariseTimeout(progress, timeoutMs, state);\n for (const line of lines) console.log(line);\n if (state !== \"looping\") return tally;\n if (debug.session) {\n await printPausedStack(debug);\n } else {\n console.log(\" re-run with POLLY_BROWSER_STACK=1 to print the running stack\");\n }\n return tally;\n}\n\n/** Print one file's finished results and tally them. */\nfunction printResults(results: TestResult[]): FileTally {\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}\n\n/**\n * Build, serve, and run one test file in its own browser context. Returns\n * its pass/fail tally. Build failures and per-file timeouts are reported\n * here (not thrown); a page-level uncaught error propagates so the suite\n * records the file as failed. The context and server are always cleaned up\n * first, under a deadline so a wedged renderer cannot stall teardown.\n */\n/** The bundle for one test file, wrapped in the document that serves it. */\ninterface BuiltPage {\n jsText: string;\n html: string;\n /** Lines of wrapper above the bundle, so debugger positions can be mapped. */\n bundleLineOffset: number;\n}\n\n/**\n * Bundle one test file for the browser. Reports its own failures and returns\n * undefined, so the caller has nothing to decide.\n */\nasync function buildPage(testFile: string): Promise<BuiltPage | undefined> {\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 undefined;\n }\n\n const jsText = await buildResult.outputs[0]?.text();\n if (!jsText) {\n console.log(\" ❌ build produced no output\");\n return undefined;\n }\n\n // The bundle starts on its own line so a debugger position in the document\n // maps to the bundle by subtracting a fixed line count, with no column\n // adjustment (polly#177). Keep the newline after the opening tag.\n const htmlPrefix = `<!DOCTYPE html>\n<html><head><meta charset=\"utf-8\"></head>\n<body>\n<script type=\"module\">\n`;\n return {\n jsText,\n html: `${htmlPrefix}${jsText}\\n</script>\\n</body></html>`,\n bundleLineOffset: htmlPrefix.split(\"\\n\").length - 1,\n };\n}\n\nasync function runFile(testFile: string): Promise<FileTally> {\n const built = await buildPage(testFile);\n if (!built) return { passed: 0, failed: 1 };\n const { jsText, html, bundleLineOffset } = built;\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 // One context per file. Closing it is a browser-process operation, so a\n // renderer that refuses to yield is still torn down, and nothing a file\n // leaves behind can reach the next one (polly#177).\n const context = await (await ensureBrowser()).createBrowserContext();\n\n let page: Page | undefined;\n try {\n const newPage = await context.newPage();\n page = newPage;\n newPage.on(\"console\", (msg) => {\n const text = msg.text();\n if (text.includes(\"[test]\")) {\n console.log(` ${text}`);\n }\n });\n\n // Push-based reporting (polly#138): the page calls back into Node via\n // `__pollyReport(results)` when its in-page suite has finished.\n let reportResolve!: (results: TestResult[]) => void;\n let reportReject!: (err: Error) => void;\n const outcome = new Promise<TestResult[]>((resolve, reject) => {\n reportResolve = resolve;\n reportReject = reject;\n });\n\n // Wire the report channel BEFORE navigating. `exposeFunction` is async;\n // if the page were allowed to load first it could call `__pollyReport`\n // before the binding existed, and the result would be lost forever —\n // the deadlock seen when the runner's CWD was outside the test package\n // (polly#159).\n await newPage.exposeFunction(\"__pollyReport\", (results: TestResult[]) => {\n reportResolve(results);\n });\n\n // The progress channel carries per-test events as the file runs, so a\n // page that stops part-way still leaves a record of how far it got.\n const progress = emptyProgress();\n await newPage.exposeFunction(\"__pollyProgress\", (event: ProgressEvent) => {\n applyProgress(progress, event);\n });\n\n newPage.on(\"pageerror\", (err: unknown) => {\n reportReject(err instanceof Error ? err : new Error(errMessage(err)));\n });\n\n let pausedFrames: PausedFrames | undefined;\n let debugSession: CDPSession | undefined;\n // Built once and only when a stack is actually printed: decoding a\n // bundle's mappings is wasted work on a healthy run.\n let lookup: SourceMapLookup | undefined;\n let lookupBuilt = false;\n const resolvePosition = (line: number, column: number): string | undefined => {\n if (!lookupBuilt) {\n lookup = createSourceMapLookup(jsText, bundleLineOffset);\n lookupBuilt = true;\n }\n return lookup?.(line, column);\n };\n if (captureStacks) {\n debugSession = await newPage.createCDPSession();\n debugSession.on(\"Debugger.paused\", (event) => {\n pausedFrames = event as unknown as PausedFrames;\n });\n await debugSession.send(\"Debugger.enable\");\n }\n\n // Bound the wait so a page that never reports (a swallowed error, a hung\n // renderer) fails the file instead of hanging the whole suite forever\n // (polly#159). Override via POLLY_BROWSER_TIMEOUT_MS.\n const timeoutMs = Number(process.env[\"POLLY_BROWSER_TIMEOUT_MS\"] ?? 60000);\n\n // A file whose module scope loops never reaches DOMContentLoaded, so it\n // stalls in `goto` rather than in the wait below. Puppeteer's own\n // navigation timeout would report that as \"Navigation timeout of 30000ms\n // exceeded\" — the same silence polly#177 started from. Hold navigation to\n // the file's own deadline and diagnose it the same way.\n newPage.setDefaultNavigationTimeout(timeoutMs);\n const debug: DebugAttachment = {\n session: debugSession,\n frames: () => pausedFrames,\n resolve: resolvePosition,\n };\n try {\n await newPage.goto(`http://127.0.0.1:${server.port}/`, { waitUntil: \"domcontentloaded\" });\n } catch (err) {\n if (!(err instanceof Error) || err.name !== \"TimeoutError\") throw err;\n return await reportTimeout(newPage, progress, timeoutMs, debug);\n }\n\n let timeoutTimer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timeoutTimer = setTimeout(() => reject(new FileTimeout()), timeoutMs);\n });\n\n let results: TestResult[];\n try {\n results = await Promise.race([outcome, timeout]);\n } catch (err) {\n if (!(err instanceof FileTimeout)) throw err;\n return await reportTimeout(newPage, progress, timeoutMs, debug);\n } finally {\n if (timeoutTimer) clearTimeout(timeoutTimer);\n }\n\n return printResults(results);\n } finally {\n // Under a deadline: a page whose main thread never yields can leave both\n // of these pending, and teardown must not become the new hang.\n if (page) await withDeadline(page.close(), CLOSE_TIMEOUT_MS);\n await withDeadline(context.close(), CLOSE_TIMEOUT_MS);\n server.stop();\n }\n}\n\nconst { passed: totalPassed, failed: totalFailed } = await runSuite(testFiles, runFile, {\n label: (testFile) => testFile.replace(`${testDir}/`, \"\"),\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
  "/**\n * @fairfox/polly/test — ephemeral listen ports.\n *\n * A test that draws a listen port at random from a fixed window can collide\n * with itself, with a second copy of the suite, or with any other process\n * holding a port in that window. The integration suite drew 27 ports from a\n * 10,000-port window, which is a 3.45% self-collision probability per run\n * (birthday bound, `1 - exp(-n(n-1)/2N)`) and a lower bound at that. A\n * collision fails `listen()` and reads as flake, so the natural response is a\n * retry — which narrows the window instead of removing it (polly#174).\n *\n * Port 0 asks the kernel for a free port. The server owns that port from the\n * moment it is assigned, so no other process can take it in between. These\n * helpers read the assigned port back off the two server shapes polly binds:\n *\n * - Bun/Elysia — `app.server.port`, after `.listen(0)`\n * - `ws` WebSocketServer — `wss.address().port`, after `{ port: 0 }`\n *\n * `retryOnPortInUse` covers the one case port 0 cannot: a port that must be\n * known before the server starts, such as a restart on the same port.\n */\n\n/** Elysia hangs the underlying Bun server off `.server`. */\ninterface BunServerHost {\n server?: { port?: number | null } | null;\n}\n\n/** `ws`'s WebSocketServer reports its bound address as an AddressInfo. */\ninterface AddressHost {\n address?: () => string | { port?: number | null } | null;\n}\n\n/**\n * Read the port a Bun/Elysia app was actually assigned.\n *\n * @param app An Elysia app (or any Bun server host) already `.listen(0)`-ed.\n * @throws If the app is not listening — a caller reading the port before the\n * listen resolves gets a named failure, not a URL containing `undefined`.\n *\n * @example\n * ```typescript\n * const app = new Elysia().use(signalingServer({ path })).listen(0);\n * const url = `ws://127.0.0.1:${resolveListenPort(app)}${path}`;\n * ```\n */\nexport function resolveListenPort(app: unknown): number {\n const port = (app as unknown as BunServerHost).server?.port;\n if (typeof port !== \"number\" || port <= 0) {\n throw new Error(\n `resolveListenPort: app is not listening (server.port = ${String(port)}). ` +\n \"Call .listen(0) and read the port from the returned app.\"\n );\n }\n return port;\n}\n\n/**\n * Read the port a `ws` WebSocketServer was actually assigned.\n *\n * @param wss A WebSocketServer constructed with `{ port: 0 }` and already\n * listening — `createPeerRepoServer` awaits that before it returns.\n * @throws If the server is not listening on a TCP port.\n *\n * @example\n * ```typescript\n * const server = await createPeerRepoServer({ port: 0, host: \"127.0.0.1\", storagePath });\n * const url = `ws://127.0.0.1:${resolveWebSocketPort(server.webSocketServer)}`;\n * ```\n */\nexport function resolveWebSocketPort(wss: unknown): number {\n const address = (wss as unknown as AddressHost).address?.();\n const port = typeof address === \"object\" && address !== null ? address.port : undefined;\n if (typeof port !== \"number\" || port <= 0) {\n throw new Error(\n `resolveWebSocketPort: server is not listening on a TCP port (address = ${JSON.stringify(address)}).`\n );\n }\n return port;\n}\n\n/** Bun, Node and `ws` all word an occupied-port failure differently. */\nfunction isPortInUse(error: unknown): boolean {\n if (error instanceof Error && \"code\" in error && error.code === \"EADDRINUSE\") return true;\n const message = error instanceof Error ? error.message : String(error);\n return /EADDRINUSE|address already in use|is port \\d+ in use|Failed to start server/i.test(\n message\n );\n}\n\nexport interface RetryOnPortInUseOptions {\n /** How many times to call `start` in total. Defaults to 5. */\n attempts?: number;\n /** Pause between attempts, in milliseconds. Defaults to 100. */\n delayMs?: number;\n}\n\n/**\n * Start a server on a port that is already decided, retrying a bounded number\n * of times while the port is still held.\n *\n * Only for the case port 0 cannot serve: the port must be known before the\n * server starts — a restart on the same port, or a URL handed out in advance.\n * A retry narrows the collision window rather than removing it, so prefer\n * `.listen(0)` plus {@link resolveListenPort} everywhere else.\n *\n * The final failure is the bind error itself, so the test reports \"port in\n * use\" rather than timing out on a connection to a server that never started.\n *\n * @example\n * ```typescript\n * const app = await retryOnPortInUse(() => startSignaling(port));\n * ```\n */\nexport async function retryOnPortInUse<T>(\n start: () => T | Promise<T>,\n options: RetryOnPortInUseOptions = {}\n): Promise<T> {\n const attempts = options.attempts ?? 5;\n const delayMs = options.delayMs ?? 100;\n let lastError: unknown;\n for (let attempt = 1; attempt <= attempts; attempt += 1) {\n try {\n return await start();\n } catch (error) {\n if (!isPortInUse(error)) throw error;\n lastError = error;\n if (attempt < attempts) await new Promise((r) => setTimeout(r, delayMs));\n }\n }\n throw lastError;\n}\n",
8
- "/**\n * Suite orchestration for the browser test runner.\n *\n * Extracted from run.ts so the resilience guarantee — a failure in one\n * file never aborts the whole suite — is a pure function that can be\n * unit-tested without launching a browser. run.ts supplies the real\n * per-file `runFile`; the test suite supplies a fake one.\n */\n\n/** Pass/fail tally for one test file or for a whole suite. */\nexport interface FileTally {\n passed: number;\n failed: number;\n}\n\nexport function errMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * Run every test file, isolating each file's failure.\n *\n * - Any thrown error from `runFile` fails only that file and is logged.\n * - A file that returns a tally with `failed > 0` (a genuine red test)\n * is reported as-is.\n *\n * The loop never rejects: a failure in one file can never abort the\n * suite, so the remaining files always execute and report.\n */\nexport async function runSuite(\n testFiles: string[],\n runFile: (testFile: string) => Promise<FileTally>,\n options: {\n label?: (testFile: string) => string;\n log?: (msg: string) => void;\n } = {}\n): Promise<FileTally> {\n const label = options.label ?? ((f) => f);\n const log = options.log ?? console.log;\n\n let totalPassed = 0;\n let totalFailed = 0;\n\n for (const testFile of testFiles) {\n log(`\\n[browser-runner] running ${label(testFile)}`);\n\n let result: FileTally;\n try {\n result = await runFile(testFile);\n } catch (err) {\n log(` ❌ ${errMessage(err)}`);\n result = { passed: 0, failed: 1 };\n }\n\n totalPassed += result.passed;\n totalFailed += result.failed;\n }\n\n return { passed: totalPassed, failed: totalFailed };\n}\n"
8
+ "/**\n * Suite orchestration for the browser test runner.\n *\n * Extracted from run.ts so the resilience guarantee — a failure in one\n * file never aborts the whole suite — is a pure function that can be\n * unit-tested without launching a browser. run.ts supplies the real\n * per-file `runFile`; the test suite supplies a fake one.\n */\n\n/** Pass/fail tally for one test file or for a whole suite. */\nexport interface FileTally {\n passed: number;\n failed: number;\n}\n\n/** One test's outcome, as the in-page harness reports it. */\nexport interface TestResult {\n name: string;\n passed: boolean;\n error?: string;\n}\n\n/**\n * What the page pushes to the runner as it works through a file.\n *\n * The final `__pollyReport` call carries the whole tally, but it never\n * arrives when the page stops running. These events arrive as the suite\n * goes, so a timed-out file can still say how far it got (polly#177).\n */\nexport type ProgressEvent =\n | { kind: \"plan\"; total: number }\n | { kind: \"start\"; name: string }\n | { kind: \"result\"; name: string; passed: boolean; error?: string };\n\n/** Everything the page managed to say before it went quiet. */\nexport interface SuiteProgress {\n /** Test count the page announced up front; absent if it never got that far. */\n planned?: number;\n /** Names of tests that began, in order. */\n started: string[];\n /** Outcomes of tests that finished. */\n completed: TestResult[];\n}\n\n/**\n * What the renderer was doing when the file timed out.\n *\n * - `looping` — the main thread never yielded, so JavaScript is still\n * running: a runaway loop in the code under test.\n * - `idle` — the page answers, so the thread is free; an awaited\n * promise never settled, or `done()` was never called.\n * - `unknown` — the state could not be probed (the page or the browser\n * had already gone).\n */\nexport type StallState = \"looping\" | \"idle\" | \"unknown\";\n\nexport function errMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Empty progress, for a file whose page said nothing at all. */\nexport function emptyProgress(): SuiteProgress {\n return { started: [], completed: [] };\n}\n\n/** Fold one page-pushed event into the accumulated progress. */\nexport function applyProgress(progress: SuiteProgress, event: ProgressEvent): void {\n if (event.kind === \"plan\") {\n progress.planned = event.total;\n return;\n }\n if (event.kind === \"start\") {\n progress.started.push(event.name);\n return;\n }\n progress.completed.push({ name: event.name, passed: event.passed, error: event.error });\n}\n\nfunction describeStall(state: StallState, timeoutMs: number): string {\n const prefix = `timed out after ${timeoutMs}ms`;\n if (state === \"looping\") {\n return `${prefix} — the page never yielded its main thread, so JavaScript is still running. That is a runaway loop in the code under test, not a stalled runner.`;\n }\n if (state === \"idle\") {\n return `${prefix} — the page still answers, so its main thread is free. The suite never called done(): an awaited promise never settled.`;\n }\n return `${prefix} — the page stopped reporting and its state could not be probed.`;\n}\n\n/**\n * Turn a timed-out file into a tally and the lines to print for it.\n *\n * A timed-out file used to count as a single failure, so the tests it held\n * vanished from the run's totals with no indication — a 9-test file that\n * stalled on its 4th test reported `1 failed` and the tier's count silently\n * lost 9 tests (polly#177). Every test the page announced is now accounted\n * for: the ones that finished keep their real outcome, and the ones that\n * never ran are counted as failures so the tier's total still matches the\n * number of tests in the files.\n */\nexport function summariseTimeout(\n progress: SuiteProgress,\n timeoutMs: number,\n state: StallState\n): { tally: FileTally; lines: string[] } {\n const passed = progress.completed.filter((r) => r.passed).length;\n const completedFailed = progress.completed.length - passed;\n const lines: string[] = [` ❌ ${describeStall(state, timeoutMs)}`];\n\n for (const r of progress.completed) {\n lines.push(r.passed ? ` ✅ ${r.name}` : ` ❌ ${r.name}: ${r.error}`);\n }\n\n const inFlight = progress.started[progress.completed.length];\n if (inFlight !== undefined) {\n lines.push(` ⏳ stalled in: ${inFlight}`);\n }\n\n if (progress.planned === undefined) {\n // The page never announced a plan, so there is no honest test count to\n // report. Fall back to the single-failure tally.\n lines.push(\" ⚠ the page never reported a test count — counting the file as one failure\");\n return { tally: { passed, failed: completedFailed + 1 }, lines };\n }\n\n const neverFinished = Math.max(progress.planned - progress.completed.length, 0);\n lines.push(\n ` ⚠ ${neverFinished} of ${progress.planned} tests never finished (${passed} passed, ${completedFailed} failed before the stall)`\n );\n // A plan that is fully accounted for still has to fail the file: the page\n // owed a report and never sent one.\n const failed = neverFinished === 0 ? completedFailed + 1 : completedFailed + neverFinished;\n return { tally: { passed, failed }, lines };\n}\n\n/**\n * Run every test file, isolating each file's failure.\n *\n * - Any thrown error from `runFile` fails only that file and is logged.\n * - A file that returns a tally with `failed > 0` (a genuine red test)\n * is reported as-is.\n *\n * The loop never rejects: a failure in one file can never abort the\n * suite, so the remaining files always execute and report.\n */\nexport async function runSuite(\n testFiles: string[],\n runFile: (testFile: string) => Promise<FileTally>,\n options: {\n label?: (testFile: string) => string;\n log?: (msg: string) => void;\n } = {}\n): Promise<FileTally> {\n const label = options.label ?? ((f) => f);\n const log = options.log ?? console.log;\n\n let totalPassed = 0;\n let totalFailed = 0;\n\n for (const testFile of testFiles) {\n log(`\\n[browser-runner] running ${label(testFile)}`);\n\n let result: FileTally;\n try {\n result = await runFile(testFile);\n } catch (err) {\n log(` ❌ ${errMessage(err)}`);\n result = { passed: 0, failed: 1 };\n }\n\n totalPassed += result.passed;\n totalFailed += result.failed;\n }\n\n return { passed: totalPassed, failed: totalFailed };\n}\n",
9
+ "/**\n * Minimal source-map lookup for the browser runner's stack reports.\n *\n * When a page wedges, the runner pauses it and prints the frames it was\n * executing. Those positions are in the served bundle, which is one long\n * generated file — \"(inline):199:4\" names the right line of the wrong file.\n * Resolving them through the bundle's own inline source map turns the report\n * into the position in the test or component the author wrote (polly#177).\n *\n * Only what that needs is implemented: decode `mappings`, and answer\n * \"which original position covers this generated one\". No names, no\n * `sourcesContent`, no index maps — Bun.build does not emit those here.\n */\n\nconst BASE64_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n\n/** One decoded mapping segment: where a generated column comes from. */\ninterface Segment {\n generatedColumn: number;\n sourceIndex: number;\n sourceLine: number;\n sourceColumn: number;\n}\n\ninterface SourceMap {\n sources: string[];\n mappings: string;\n}\n\n/** Resolves a generated position to `source:line:column`, or undefined. */\nexport type SourceMapLookup = (line: number, column: number) => string | undefined;\n\n/** Decode one Base64 VLQ segment into its signed fields. */\nfunction decodeSegment(segment: string): number[] {\n const fields: number[] = [];\n let shift = 0;\n let value = 0;\n for (const char of segment) {\n const digit = BASE64_ALPHABET.indexOf(char);\n if (digit === -1) return [];\n const hasContinuation = (digit & 32) !== 0;\n value += (digit & 31) << shift;\n if (hasContinuation) {\n shift += 5;\n continue;\n }\n const negative = (value & 1) === 1;\n const magnitude = value >> 1;\n fields.push(negative ? -magnitude : magnitude);\n value = 0;\n shift = 0;\n }\n return fields;\n}\n\n/** Decode the whole `mappings` string into per-generated-line segments. */\nfunction decodeMappings(mappings: string): Segment[][] {\n const lines: Segment[][] = [];\n // The four fields are deltas carried across the entire map, not per line —\n // except the generated column, which resets on every line.\n let sourceIndex = 0;\n let sourceLine = 0;\n let sourceColumn = 0;\n for (const lineText of mappings.split(\";\")) {\n const segments: Segment[] = [];\n let generatedColumn = 0;\n for (const segmentText of lineText.split(\",\")) {\n if (!segmentText) continue;\n const fields = decodeSegment(segmentText);\n const [columnDelta, sourceDelta, lineDelta, sourceColumnDelta] = fields;\n if (columnDelta === undefined) continue;\n generatedColumn += columnDelta;\n // A one-field segment marks generated code with no origin; skip it.\n if (sourceDelta === undefined || lineDelta === undefined || sourceColumnDelta === undefined) {\n continue;\n }\n sourceIndex += sourceDelta;\n sourceLine += lineDelta;\n sourceColumn += sourceColumnDelta;\n segments.push({ generatedColumn, sourceIndex, sourceLine, sourceColumn });\n }\n lines.push(segments);\n }\n return lines;\n}\n\n/** Pull the inline source map out of a bundle, if it carries one. */\nexport function parseInlineSourceMap(bundle: string): SourceMap | undefined {\n const match = bundle.match(\n /\\/\\/# sourceMappingURL=data:application\\/json;(?:charset=[^;]+;)?base64,([A-Za-z0-9+/=]+)/\n );\n const encoded = match?.[1];\n if (!encoded) return undefined;\n try {\n const parsed: unknown = JSON.parse(Buffer.from(encoded, \"base64\").toString(\"utf8\"));\n if (typeof parsed !== \"object\" || parsed === null) return undefined;\n const { sources, mappings } = parsed as { sources?: unknown; mappings?: unknown };\n if (!Array.isArray(sources) || typeof mappings !== \"string\") return undefined;\n return { sources: sources.map(String), mappings };\n } catch {\n return undefined;\n }\n}\n\n/**\n * Build a lookup over a bundle's inline source map.\n *\n * `lineOffset` is how many lines of wrapper HTML sit above the bundle in the\n * served document, since the debugger reports positions in the document, not\n * in the script. Returns undefined when the bundle carries no usable map, so\n * callers can fall back to the raw generated position.\n */\nexport function createSourceMapLookup(bundle: string, lineOffset = 0): SourceMapLookup | undefined {\n const map = parseInlineSourceMap(bundle);\n if (!map) return undefined;\n const lines = decodeMappings(map.mappings);\n\n return (line: number, column: number): string | undefined => {\n const generatedLine = line - lineOffset;\n if (generatedLine < 0) return undefined;\n const segments = lines[generatedLine];\n if (!segments || segments.length === 0) return undefined;\n // The last segment starting at or before the column owns that position.\n let best = segments[0];\n for (const segment of segments) {\n if (segment.generatedColumn > column) break;\n best = segment;\n }\n if (!best) return undefined;\n const source = map.sources[best.sourceIndex] ?? \"(unknown source)\";\n return `${source}:${best.sourceLine + 1}:${best.sourceColumn}`;\n };\n}\n"
9
10
  ],
10
- "mappings": "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",
11
- "debugId": "4731421A310B38F664756E2164756E21",
11
+ "mappings": "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",
12
+ "debugId": "0B420D0CE9973A2C64756E2164756E21",
12
13
  "names": []
13
14
  }