@reporters/web 1.3.0 → 1.4.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.1](https://github.com/MoLow/reporters/compare/web-v1.4.0...web-v1.4.1) (2026-07-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **web:** true live durations via writer stamps on the wire ([#237](https://github.com/MoLow/reporters/issues/237)) ([42d5478](https://github.com/MoLow/reporters/commit/42d5478d153de83d8f457fb8783794a5aed54408))
9
+
10
+ ## [1.4.0](https://github.com/MoLow/reporters/compare/web-v1.3.0...web-v1.4.0) (2026-07-06)
11
+
12
+
13
+ ### Features
14
+
15
+ * **tree-core:** report a passing todo as passed, reserving todo for failing ones ([#227](https://github.com/MoLow/reporters/issues/227)) ([46a9c87](https://github.com/MoLow/reporters/commit/46a9c874d88b2ae68aff80b50b286ea5e11e2a45))
16
+ * **web:** bounded log viewer — capped panels, full-log modal, named affordances ([#233](https://github.com/MoLow/reporters/issues/233)) ([f30d492](https://github.com/MoLow/reporters/commit/f30d49254394fb97f820b9202c055cd78b815000))
17
+ * **web:** clickable header status chips filter the tree ([#231](https://github.com/MoLow/reporters/issues/231)) ([bdf4f4e](https://github.com/MoLow/reporters/commit/bdf4f4e8efd3e8c03fe2c458a83d1d637b592f5f))
18
+ * **web:** distinct loading state before the first log fetch resolves ([#235](https://github.com/MoLow/reporters/issues/235)) ([5d1eb6c](https://github.com/MoLow/reporters/commit/5d1eb6ca77b3447a3304d8fc655eaf3a33d1c27f))
19
+ * **web:** richer diagnostics rendering in the viewer ([#232](https://github.com/MoLow/reporters/issues/232)) ([b7eb565](https://github.com/MoLow/reporters/commit/b7eb56506e5776a4231aef631a857512800df3ea))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * **web:** keep the http server alive until the viewer reads the whole log ([#230](https://github.com/MoLow/reporters/issues/230)) ([ba7de4d](https://github.com/MoLow/reporters/commit/ba7de4d8035afb5ec4bece56b737cce607d0fa2c))
25
+ * **web:** show real wall-clock durations instead of summing concurrent tests ([#225](https://github.com/MoLow/reporters/issues/225)) ([e80ad8b](https://github.com/MoLow/reporters/commit/e80ad8b55a9c3b3aa2317763487f4185a0a5d9cf))
26
+
3
27
  ## [1.3.0](https://github.com/MoLow/reporters/compare/web-v1.2.0...web-v1.3.0) (2026-07-02)
4
28
 
5
29
 
@@ -9,17 +9,27 @@ function viewerPage() {
9
9
  return '<!doctype html><meta charset="utf-8"><p>viewer bundle missing \u2014 run the package build</p>';
10
10
  }
11
11
  }
12
- function startViewerServer(host = "127.0.0.1", pollMs = 250) {
12
+ var DRAIN_TIMEOUT_MS = 5e3;
13
+ function startViewerServer(host = "127.0.0.1", pollMs = 250, drainTimeoutMs = DRAIN_TIMEOUT_MS) {
13
14
  const page = viewerPage();
14
15
  let buffer = Buffer.alloc(0);
16
+ let sawViewer = false;
17
+ let servedUpTo = 0;
18
+ let onCaughtUp;
19
+ function served(upTo) {
20
+ servedUpTo = Math.max(servedUpTo, upTo);
21
+ if (servedUpTo >= buffer.length) onCaughtUp?.();
22
+ }
15
23
  return new Promise((resolve) => {
16
24
  const server = createServer((req, res) => {
17
25
  const path = req.url.split("?")[0];
18
26
  if (path === "/run.ndjson") {
27
+ sawViewer = true;
19
28
  const range = /^bytes=(\d+)-/.exec(req.headers.range ?? "");
20
29
  if (range) {
21
30
  const startByte = Number(range[1]);
22
31
  if (startByte >= buffer.length) {
32
+ served(startByte);
23
33
  res.writeHead(416).end();
24
34
  return;
25
35
  }
@@ -29,10 +39,12 @@ function startViewerServer(host = "127.0.0.1", pollMs = 250) {
29
39
  "content-range": `bytes ${startByte}-${buffer.length - 1}/${buffer.length}`
30
40
  });
31
41
  res.end(buffer.subarray(startByte));
42
+ served(buffer.length);
32
43
  return;
33
44
  }
34
45
  res.writeHead(200, { "content-type": "application/x-ndjson", "accept-ranges": "bytes" });
35
46
  res.end(buffer);
47
+ served(buffer.length);
36
48
  return;
37
49
  }
38
50
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
@@ -47,8 +59,17 @@ function startViewerServer(host = "127.0.0.1", pollMs = 250) {
47
59
  push(chunk) {
48
60
  buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
49
61
  },
50
- close() {
51
- return new Promise((res) => {
62
+ async close() {
63
+ if (sawViewer && servedUpTo < buffer.length) {
64
+ await new Promise((res) => {
65
+ const timer = setTimeout(res, drainTimeoutMs);
66
+ onCaughtUp = () => {
67
+ clearTimeout(timer);
68
+ res();
69
+ };
70
+ });
71
+ }
72
+ await new Promise((res) => {
52
73
  server.close(() => res());
53
74
  });
54
75
  }
package/dist/index.d.ts CHANGED
@@ -27,6 +27,8 @@ interface TestEventData {
27
27
  }
28
28
  interface TestEvent {
29
29
  type: string;
30
+ /** Writer wall-clock (epoch ms), stamped when the event is serialized. */
31
+ t?: number;
30
32
  data: TestEventData;
31
33
  }
32
34
 
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  startViewerServer
3
- } from "./chunk-X6OHEYIN.js";
3
+ } from "./chunk-KDWFRAWN.js";
4
4
 
5
5
  // ../tree-core/src/wire.ts
6
6
  function flattenError(raw) {
@@ -16,6 +16,7 @@ function flattenError(raw) {
16
16
  function toWireEvent(event) {
17
17
  const d = event.data ?? {};
18
18
  const data = {};
19
+ const t = event.t;
19
20
  if (d.name != null) data.name = d.name;
20
21
  if (d.nesting != null) data.nesting = d.nesting;
21
22
  if (d.file != null) data.file = d.file;
@@ -41,10 +42,11 @@ function toWireEvent(event) {
41
42
  error: flattenError(d.details.error)
42
43
  };
43
44
  }
44
- return { type: event.type, data };
45
+ return t != null ? { type: event.type, t, data } : { type: event.type, data };
45
46
  }
46
47
  function serializeWireLine(event) {
47
- return `${JSON.stringify(toWireEvent(event))}
48
+ const stamped = event.t != null ? event : { ...event, t: Date.now() };
49
+ return `${JSON.stringify(toWireEvent(stamped))}
48
50
  `;
49
51
  }
50
52
 
package/dist/sink.d.ts CHANGED
@@ -19,12 +19,15 @@ interface HttpServerOptions {
19
19
  host?: string;
20
20
  /** Viewer polling cadence in ms (default 250; the viewer clamps to 100–10000). */
21
21
  pollMs?: number;
22
+ /** How long close() keeps serving a lagging viewer before giving up (default 5000). */
23
+ drainTimeoutMs?: number;
22
24
  }
23
25
  /**
24
26
  * A sink that serves the run locally: the viewer page at `/` and the growing
25
27
  * NDJSON at `/run.ndjson` (HTTP Range aware, so the viewer polls appended bytes
26
28
  * — no `file://`/CORS limits). Stays alive after the run while attached to an
27
- * interactive terminal; otherwise shuts down so the process can exit.
29
+ * interactive terminal; otherwise drains a lagging viewer (bounded by
30
+ * `drainTimeoutMs`) and shuts down so the process can exit.
28
31
  */
29
32
  declare function httpServer(opts?: HttpServerOptions): Sink;
30
33
 
package/dist/sink.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  startViewerServer
3
- } from "./chunk-X6OHEYIN.js";
3
+ } from "./chunk-KDWFRAWN.js";
4
4
 
5
5
  // src/sink.ts
6
6
  function httpServer(opts = {}) {
7
7
  let server;
8
8
  return {
9
9
  async start() {
10
- server = await startViewerServer(opts.host, opts.pollMs);
10
+ server = await startViewerServer(opts.host, opts.pollMs, opts.drainTimeoutMs);
11
11
  },
12
12
  write(chunk) {
13
13
  server?.push(chunk);