@lightpath-dev/daemon 0.0.1 → 0.1.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.
@@ -0,0 +1,19 @@
1
+ import type { ActionRequest, ActionResult, ActionRunner, Trace, TraceId } from '@lightpath-dev/protocol';
2
+ import type { Store } from './store.js';
3
+ /** Longest wait after `traceEnd` for the backend lines of the requests an action sent (D-114). */
4
+ export declare const BACKEND_WAIT_MS = 1500;
5
+ type Tagged = {
6
+ method: string;
7
+ path: string;
8
+ };
9
+ /** The tagged requests that have no backend delta in the trace yet, matched by method and path. */
10
+ export declare function missingBackend(trace: Trace | undefined, tagged: readonly Tagged[]): Tagged[];
11
+ /**
12
+ * One wait for all of an action's requests: ends as soon as each tagged request has its backend
13
+ * delta in the stored trace, or after `waitMs`. Returns the requests still missing.
14
+ */
15
+ export declare function waitForBackend(store: Store, traceId: TraceId, tagged: readonly Tagged[], waitMs?: number): Promise<Tagged[]>;
16
+ /** Runs one `/v1/action` request: the runner's action, then the backend wait (section 4.3). */
17
+ export declare function runAction(runner: ActionRunner | undefined, store: Store, request: ActionRequest, waitMs?: number): Promise<ActionResult>;
18
+ export {};
19
+ //# sourceMappingURL=actions.d.ts.map
@@ -0,0 +1,84 @@
1
+ /** Longest wait after `traceEnd` for the backend lines of the requests an action sent (D-114). */
2
+ export const BACKEND_WAIT_MS = 1500;
3
+ const withoutQuery = (path) => path.split('?')[0] ?? path;
4
+ const key = (r) => `${r.method.toUpperCase()} ${withoutQuery(r.path)}`;
5
+ /** The tagged requests that have no backend delta in the trace yet, matched by method and path. */
6
+ export function missingBackend(trace, tagged) {
7
+ const have = new Map();
8
+ for (const r of trace?.requests ?? []) {
9
+ if (r.collector === 'browser')
10
+ continue;
11
+ have.set(key(r), (have.get(key(r)) ?? 0) + 1);
12
+ }
13
+ const missing = [];
14
+ for (const r of tagged) {
15
+ const n = have.get(key(r)) ?? 0;
16
+ if (n > 0)
17
+ have.set(key(r), n - 1);
18
+ else
19
+ missing.push(r);
20
+ }
21
+ return missing;
22
+ }
23
+ /**
24
+ * One wait for all of an action's requests: ends as soon as each tagged request has its backend
25
+ * delta in the stored trace, or after `waitMs`. Returns the requests still missing.
26
+ */
27
+ export function waitForBackend(store, traceId, tagged, waitMs = BACKEND_WAIT_MS) {
28
+ const initial = missingBackend(store.get(traceId), tagged);
29
+ if (initial.length === 0)
30
+ return Promise.resolve([]);
31
+ return new Promise((resolve) => {
32
+ const finish = () => {
33
+ clearTimeout(timer);
34
+ unsubscribe();
35
+ resolve(missingBackend(store.get(traceId), tagged));
36
+ };
37
+ const timer = setTimeout(finish, waitMs);
38
+ const unsubscribe = store.subscribe((trace) => {
39
+ if (trace.id === traceId && missingBackend(trace, tagged).length === 0)
40
+ finish();
41
+ });
42
+ });
43
+ }
44
+ /** Runs one `/v1/action` request: the runner's action, then the backend wait (section 4.3). */
45
+ export async function runAction(runner, store, request, waitMs = BACKEND_WAIT_MS) {
46
+ if (!runner) {
47
+ return {
48
+ ok: false,
49
+ error: {
50
+ code: 'no_browser',
51
+ message: 'Lightpath has no browser open to act in. Start it with a browser (lightpath dev -- <your dev command>, ' +
52
+ 'without --no-browser) and wait until it prints "watching".',
53
+ },
54
+ };
55
+ }
56
+ const outcome = await runner(request);
57
+ if (!outcome.ok)
58
+ return outcome;
59
+ const missing = await waitForBackend(store, outcome.traceId, outcome.tagged, waitMs);
60
+ const trace = store.get(outcome.traceId);
61
+ if (!trace) {
62
+ return {
63
+ ok: false,
64
+ error: { code: 'no_trace', message: `Trace ${outcome.traceId} was evicted before it could be read.` },
65
+ };
66
+ }
67
+ const warnings = [...outcome.warnings];
68
+ if (missing.length > 0) {
69
+ warnings.push({
70
+ code: 'backend_partial',
71
+ message: `No backend lines arrived within ${waitMs} ms for ${missing.map(key).join(', ')}: a remote API, or a server not started by lightpath dev.`,
72
+ });
73
+ }
74
+ return {
75
+ ok: true,
76
+ trace,
77
+ boundary: outcome.boundary,
78
+ settled: outcome.settled,
79
+ backend: missing.length > 0 ? 'partial' : 'complete',
80
+ pageUrl: outcome.pageUrl,
81
+ warnings,
82
+ };
83
+ }
84
+ //# sourceMappingURL=actions.js.map
@@ -1,5 +1,5 @@
1
1
  import { type ResolveContext } from '@lightpath-dev/core';
2
- import type { IngestMessage } from '@lightpath-dev/protocol';
2
+ import { type IngestMessage } from '@lightpath-dev/protocol';
3
3
  import type { Store } from './store.js';
4
4
  /**
5
5
  * Turns ingest messages into mapped traces (docs/ARCHITECTURE.md sections 3, 4, and 6).
@@ -1,4 +1,5 @@
1
- import { decodeDataUrlSourceMap, mapScript, mergeLineCounts, newTraceId, PeriodicFunctions, resolveFile, } from '@lightpath-dev/core';
1
+ import { decodeDataUrlSourceMap, mapFunctions, mapScript, mergeLineCounts, newTraceId, originalLocation, PeriodicFunctions, resolveFile, } from '@lightpath-dev/core';
2
+ import { MAX_TRACE_PROBLEMS, } from '@lightpath-dev/protocol';
2
3
  /**
3
4
  * Turns ingest messages into mapped traces (docs/ARCHITECTURE.md sections 3, 4, and 6).
4
5
  * Pure in-memory logic with no I/O, so it's tested without sockets.
@@ -68,7 +69,62 @@ export class Correlator {
68
69
  case 'lines':
69
70
  this.#onLines(agentId, message);
70
71
  return;
72
+ case 'problem':
73
+ this.#onProblem(agentId, message);
74
+ return;
75
+ }
76
+ }
77
+ /** Attaches an error or failed request to its trace (D-121). Without an open trace, dropped. */
78
+ #onProblem(agentId, msg) {
79
+ if (!msg.traceId)
80
+ return;
81
+ const agent = this.#agent(agentId);
82
+ // A backend crash during run_request comes before any delta for its trace: it opens one.
83
+ const pending = this.#pending.get(msg.traceId) ??
84
+ this.#fromStore(msg.traceId) ??
85
+ (agent.kind === 'browser' ? undefined : this.#pendingFor(msg.traceId, 'uncaught error', msg.at));
86
+ if (!pending)
87
+ return;
88
+ pending.trace.problems ??= [];
89
+ const problems = pending.trace.problems;
90
+ if (problems.length >= MAX_TRACE_PROBLEMS) {
91
+ pending.trace.problemsDropped = (pending.trace.problemsDropped ?? 0) + 1;
92
+ this.#publish(pending);
93
+ return;
94
+ }
95
+ const where = msg.location ? this.#locate(agent, msg.location) : undefined;
96
+ problems.push({
97
+ collector: agent.kind,
98
+ kind: msg.kind,
99
+ message: msg.message,
100
+ at: msg.at,
101
+ ...(where ? { path: where.path, line: where.line } : {}),
102
+ ...(msg.request ? { request: msg.request } : {}),
103
+ });
104
+ this.#publish(pending);
105
+ }
106
+ /** A problem's generated location as a workspace path and original line, if it maps there. */
107
+ #locate(agent, loc) {
108
+ if (loc.line < 1)
109
+ return undefined;
110
+ const info = [...agent.scripts.values()].reverse().find((s) => s.url === loc.url);
111
+ let source = loc.url;
112
+ let line = loc.line;
113
+ if (info?.sourceMapURL) {
114
+ try {
115
+ const map = decodeDataUrlSourceMap(info.sourceMapURL);
116
+ const orig = map ? originalLocation(map, info.url, loc.line, loc.column) : undefined;
117
+ if (!orig)
118
+ return undefined;
119
+ source = orig.source;
120
+ line = orig.line;
121
+ }
122
+ catch {
123
+ return undefined;
124
+ }
71
125
  }
126
+ const r = resolveFile(source, this.#ctx);
127
+ return r.kind === 'workspace' ? { path: r.path, line } : undefined;
72
128
  }
73
129
  #onDelta(agentId, delta) {
74
130
  const agent = this.#agent(agentId);
@@ -78,6 +134,20 @@ export class Correlator {
78
134
  return;
79
135
  }
80
136
  const pending = this.#pendingFor(delta.traceId, delta.request ? `${delta.request.method} ${delta.request.path}` : 'action', delta.startedAt);
137
+ // Which phase this delta's functions belong to (D-116). A backend phase is made below, once
138
+ // the request has its index.
139
+ let phase;
140
+ if (delta.beforeRequest) {
141
+ phase = { kind: 'before-request', at: delta.endedAt, leaving: delta.beforeRequest };
142
+ pending.phases.push(phase);
143
+ }
144
+ else if (!delta.request) {
145
+ phase = this.#closingPhase(pending, delta.endedAt);
146
+ }
147
+ else {
148
+ phase = { kind: 'backend', at: delta.startedAt, request: pending.trace.requests.length };
149
+ pending.phases.push(phase);
150
+ }
81
151
  for (const coverage of delta.coverage) {
82
152
  const info = agent.scripts.get(coverage.scriptId);
83
153
  const source = info ? this.#sources.get(info.hash) : undefined;
@@ -108,6 +178,9 @@ export class Correlator {
108
178
  const foreground = fgLines !== undefined && [...fgLines.values()].some((c) => c > 0);
109
179
  this.#addFile(pending, path, agent.kind, lines, delta.startedAt, foreground);
110
180
  }
181
+ const fns = mapFunctions({ coverage, source, ...(sourceMap ? { sourceMap } : {}) }, result.files, this.#ctx);
182
+ for (const fn of fns)
183
+ this.#addFunction(pending, phase, agent.kind, fn);
111
184
  }
112
185
  if (delta.request) {
113
186
  pending.trace.requests.push({
@@ -126,10 +199,56 @@ export class Correlator {
126
199
  pending.trace.endedAt = Math.max(pending.trace.endedAt ?? 0, delta.endedAt);
127
200
  this.#publish(pending);
128
201
  }
202
+ /** Adds one function to its phase, summing calls and keeping lines not run in every delta. */
203
+ #addFunction(p, phase, collector, fn) {
204
+ const key = `${p.phases.indexOf(phase)}|${phase.kind}|${fn.path}|${fn.name}|${fn.startLine}`;
205
+ const prev = p.functions.get(key);
206
+ if (!prev) {
207
+ p.functions.set(key, {
208
+ phase,
209
+ hit: {
210
+ path: fn.path,
211
+ collector,
212
+ name: fn.name,
213
+ startLine: fn.startLine,
214
+ endLine: fn.endLine,
215
+ count: fn.count,
216
+ ...(fn.notRun.length ? { notRun: fn.notRun } : {}),
217
+ ...(fn.notRunSpans.length ? { notRunSpans: fn.notRunSpans } : {}),
218
+ },
219
+ });
220
+ return;
221
+ }
222
+ const hit = prev.hit;
223
+ hit.count += fn.count;
224
+ hit.endLine = Math.max(hit.endLine, fn.endLine);
225
+ const notRun = (hit.notRun ?? []).filter((l) => fn.notRun.includes(l));
226
+ const spans = (hit.notRunSpans ?? []).filter((s) => fn.notRunSpans.some((t) => t.line === s.line && t.startColumn === s.startColumn));
227
+ if (notRun.length)
228
+ hit.notRun = notRun;
229
+ else
230
+ delete hit.notRun;
231
+ if (spans.length)
232
+ hit.notRunSpans = spans;
233
+ else
234
+ delete hit.notRunSpans;
235
+ }
236
+ /** The one browser phase for reads not taken before a request (D-116), made on first use. */
237
+ #closingPhase(p, at) {
238
+ if (!p.closing) {
239
+ p.closing = { kind: 'browser', at };
240
+ p.phases.push(p.closing);
241
+ }
242
+ p.closing.at = Math.max(p.closing.at, at);
243
+ return p.closing;
244
+ }
129
245
  #onLines(agentId, msg) {
130
246
  const agent = this.#agent(agentId);
131
247
  const label = msg.request ? `${msg.request.method} ${msg.request.path}` : 'action';
132
248
  const pending = this.#pendingFor(msg.traceId, label, msg.startedAt);
249
+ // Python files have no function list; the summary lists them by line range in this phase.
250
+ if (msg.request)
251
+ pending.phases.push({ kind: 'backend', at: msg.startedAt, request: pending.trace.requests.length });
133
252
  for (const file of msg.files) {
134
253
  const r = resolveFile(file.path, this.#ctx);
135
254
  if (r.kind === 'dropped') {
@@ -182,7 +301,37 @@ export class Correlator {
182
301
  const trace = this.#store.get(traceId);
183
302
  if (!trace)
184
303
  return undefined;
185
- const p = { trace: structuredClone(trace), files: new Map(), dropped: new Map() };
304
+ const p = {
305
+ trace: structuredClone(trace),
306
+ files: new Map(),
307
+ dropped: new Map(),
308
+ phases: [],
309
+ functions: new Map(),
310
+ };
311
+ // Phases keep their published order: their old sort keys are their positions, which sort
312
+ // before any new read's time. The closing browser phase stays last.
313
+ for (const [i, ph] of (trace.phases ?? []).entries()) {
314
+ const open = {
315
+ kind: ph.kind,
316
+ at: i,
317
+ ...(ph.request !== undefined ? { request: ph.request } : {}),
318
+ };
319
+ if (ph.kind === 'before-request' && ph.request !== undefined) {
320
+ const r = trace.requests[ph.request];
321
+ if (r)
322
+ open.leaving = { method: r.method, path: r.path };
323
+ }
324
+ if (ph.kind === 'browser' || ph.kind === 'after-response')
325
+ p.closing = open;
326
+ p.phases.push(open);
327
+ }
328
+ for (const f of trace.functions ?? []) {
329
+ const phase = p.phases[f.phase];
330
+ if (!phase)
331
+ continue;
332
+ const { phase: _, ...hit } = f;
333
+ p.functions.set(`${f.phase}|${phase.kind}|${f.path}|${f.name}|${f.startLine}`, { phase, hit });
334
+ }
186
335
  for (const f of trace.files) {
187
336
  p.files.set(f.path, {
188
337
  collector: f.collector,
@@ -201,8 +350,48 @@ export class Correlator {
201
350
  trace: { id, label, origin, startedAt: at, files: [], requests: [], dropped: [] },
202
351
  files: new Map(),
203
352
  dropped: new Map(),
353
+ phases: [],
354
+ functions: new Map(),
204
355
  };
205
356
  }
357
+ /**
358
+ * Orders the phases and numbers them (design section 5.3): reads and requests in time order, the
359
+ * closing browser phase last. It is `after-response` when any request left the page before it,
360
+ * else `browser`. Each before-request phase is tied to the first unmatched backend request with
361
+ * the same method and path.
362
+ */
363
+ #phasesOf(p, published) {
364
+ if (p.phases.length === 0)
365
+ return undefined;
366
+ const ordered = p.phases.filter((ph) => ph !== p.closing).sort((a, b) => a.at - b.at);
367
+ if (p.closing)
368
+ ordered.push(p.closing);
369
+ const hasBefore = ordered.some((ph) => ph.kind === 'before-request');
370
+ const matched = new Set();
371
+ const phases = ordered.map((ph) => {
372
+ if (ph === p.closing)
373
+ return { kind: hasBefore ? 'after-response' : 'browser' };
374
+ if (ph.kind === 'backend')
375
+ return { kind: 'backend', ...(ph.request !== undefined ? { request: ph.request } : {}) };
376
+ if (ph.kind === 'before-request' && ph.leaving) {
377
+ const { method, path } = ph.leaving;
378
+ const i = p.trace.requests.findIndex((r, j) => !matched.has(j) && r.collector !== 'browser' && r.method === method && r.path === path);
379
+ if (i >= 0) {
380
+ matched.add(i);
381
+ return { kind: 'before-request', request: i };
382
+ }
383
+ }
384
+ return { kind: ph.kind };
385
+ });
386
+ const functions = [];
387
+ for (const { phase, hit } of p.functions.values()) {
388
+ if (!published.has(hit.path))
389
+ continue; // its file has no lit line, so it isn't in the trace
390
+ functions.push({ ...hit, phase: ordered.indexOf(phase) });
391
+ }
392
+ functions.sort((a, b) => a.phase - b.phase || a.path.localeCompare(b.path) || a.startLine - b.startLine);
393
+ return { phases, functions };
394
+ }
206
395
  #addFile(p, path, collector, lines, at, foreground) {
207
396
  const existing = p.files.get(path);
208
397
  if (existing) {
@@ -235,6 +424,11 @@ export class Correlator {
235
424
  files.sort((a, b) => a.firstSeenAt - b.firstSeenAt || a.path.localeCompare(b.path));
236
425
  p.trace.files = files;
237
426
  p.trace.dropped = [...p.dropped].map(([reason, count]) => ({ reason, count }));
427
+ const shaped = this.#phasesOf(p, new Set(files.map((f) => f.path)));
428
+ if (shaped) {
429
+ p.trace.phases = shaped.phases;
430
+ p.trace.functions = shaped.functions;
431
+ }
238
432
  this.#store.put(structuredClone(p.trace));
239
433
  // Keep at most 50 traces open for late backend deltas; older ones are re-read from the store.
240
434
  if (this.#pending.size > 50) {
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { BACKEND_WAIT_MS, missingBackend, runAction, waitForBackend } from './actions.js';
1
2
  export { authorize, newToken, tokensEqual } from './auth.js';
2
3
  export { Correlator } from './correlator.js';
3
4
  export { findDiscovery, readDiscovery, removeDiscovery, writeDiscovery } from './discovery.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { BACKEND_WAIT_MS, missingBackend, runAction, waitForBackend } from './actions.js';
1
2
  export { authorize, newToken, tokensEqual } from './auth.js';
2
3
  export { Correlator } from './correlator.js';
3
4
  export { findDiscovery, readDiscovery, removeDiscovery, writeDiscovery } from './discovery.js';
package/dist/server.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ResolveContext } from '@lightpath-dev/core';
2
+ import { type ActionRunner } from '@lightpath-dev/protocol';
2
3
  import { Correlator } from './correlator.js';
3
4
  import { type Store } from './store.js';
4
5
  export interface DaemonOptions {
@@ -13,6 +14,8 @@ export interface DaemonOptions {
13
14
  store?: Store;
14
15
  writeDiscoveryFile?: boolean;
15
16
  log?: (msg: string) => void;
17
+ /** Longest wait after an action's `traceEnd` for its backend lines (default 1,500 ms). */
18
+ backendWaitMs?: number;
16
19
  }
17
20
  export interface RunningDaemon {
18
21
  port: number;
@@ -21,6 +24,11 @@ export interface RunningDaemon {
21
24
  startedAt: number;
22
25
  store: Store;
23
26
  correlator: Correlator;
27
+ /**
28
+ * Registers what runs `POST /v1/action` (the browser collector, once its page is open), or
29
+ * removes it. Without one, every action answers `no_browser` (D-115).
30
+ */
31
+ setActionRunner(runner: ActionRunner | undefined): void;
24
32
  close(): Promise<void>;
25
33
  }
26
34
  /** Starts the daemon on 127.0.0.1 (docs/ARCHITECTURE.md section 7). */
package/dist/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import http, {} from 'node:http';
2
- import { ClientMessage, CloseCode, IngestBatch, MAX_MESSAGE_BYTES, PROTOCOL_VERSION, } from '@lightpath-dev/protocol';
2
+ import { ActionRequest, ClientMessage, CloseCode, IngestBatch, MAX_ACTION_BYTES, MAX_MESSAGE_BYTES, PROTOCOL_VERSION, } from '@lightpath-dev/protocol';
3
3
  import { WebSocketServer } from 'ws';
4
+ import { BACKEND_WAIT_MS, runAction } from './actions.js';
4
5
  import { authorize, newToken } from './auth.js';
5
6
  import { Correlator } from './correlator.js';
6
7
  import { removeDiscovery, writeDiscovery } from './discovery.js';
@@ -11,6 +12,8 @@ export async function startDaemon(options) {
11
12
  const store = options.store ?? new MemoryStore();
12
13
  const log = options.log ?? (() => { });
13
14
  const startedAt = Date.now();
15
+ const backendWaitMs = options.backendWaitMs ?? BACKEND_WAIT_MS;
16
+ let actionRunner;
14
17
  const correlator = new Correlator(store, {
15
18
  workspaceRoots: options.workspaceRoots,
16
19
  ...(options.devServers ? { devServers: options.devServers } : {}),
@@ -52,6 +55,32 @@ export async function startDaemon(options) {
52
55
  res.writeHead(204, { 'x-lightpath-started-at': String(startedAt) }).end();
53
56
  return;
54
57
  }
58
+ if (req.method === 'POST' && path === '/v1/action') {
59
+ // Same rules as /v1/ingest (D-115). The body holds `fill` values, which are secrets: it is
60
+ // never logged, and a schema error reply names fields, never values.
61
+ const auth = authorize(req, token);
62
+ if (auth !== 'ok')
63
+ return json(res, auth === 'forbidden' ? 403 : 401, { error: auth });
64
+ // Drain an oversized body instead of dropping the connection, so the caller sees the 413.
65
+ const body = await readBody(req, MAX_ACTION_BYTES, true);
66
+ if (body === undefined)
67
+ return json(res, 413, { error: 'body too large' });
68
+ let parsed;
69
+ try {
70
+ parsed = JSON.parse(body);
71
+ }
72
+ catch {
73
+ return json(res, 400, { error: 'invalid JSON' });
74
+ }
75
+ const request = ActionRequest.safeParse(parsed);
76
+ if (!request.success) {
77
+ const issues = request.error.issues.slice(0, 5).map((i) => ({ path: i.path, message: i.message }));
78
+ return json(res, 400, { error: issues });
79
+ }
80
+ if (request.data.protocolVersion !== PROTOCOL_VERSION)
81
+ return json(res, 426, { error: 'protocol version mismatch' });
82
+ return json(res, 200, await runAction(actionRunner, store, request.data, backendWaitMs));
83
+ }
55
84
  json(res, 404, { error: 'not found' });
56
85
  }
57
86
  // ── Client feed ───────────────────────────────────────────────────────────
@@ -119,6 +148,9 @@ export async function startDaemon(options) {
119
148
  startedAt,
120
149
  store,
121
150
  correlator,
151
+ setActionRunner(runner) {
152
+ actionRunner = runner;
153
+ },
122
154
  async close() {
123
155
  for (const client of wss.clients)
124
156
  client.close();
@@ -132,21 +164,27 @@ export async function startDaemon(options) {
132
164
  function json(res, status, body) {
133
165
  res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify(body));
134
166
  }
135
- /** Reads a request body as UTF-8, or returns undefined if it exceeds `limit` bytes. */
136
- function readBody(req, limit) {
167
+ /**
168
+ * Reads a request body as UTF-8, or returns undefined if it exceeds `limit` bytes. Past the limit
169
+ * it drops the connection, or with `drain` reads the rest without keeping it.
170
+ */
171
+ function readBody(req, limit, drain = false) {
137
172
  return new Promise((resolve, reject) => {
138
173
  const chunks = [];
139
174
  let size = 0;
140
175
  req.on('data', (chunk) => {
141
176
  size += chunk.length;
142
177
  if (size > limit) {
178
+ chunks.length = 0;
179
+ if (drain)
180
+ return;
143
181
  resolve(undefined);
144
182
  req.destroy();
145
183
  return;
146
184
  }
147
185
  chunks.push(chunk);
148
186
  });
149
- req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
187
+ req.on('end', () => resolve(size > limit ? undefined : Buffer.concat(chunks).toString('utf8')));
150
188
  req.on('error', reject);
151
189
  });
152
190
  }
package/dist/store.js CHANGED
@@ -28,7 +28,14 @@ export class MemoryStore {
28
28
  for (const t of [...this.#traces.values()].reverse()) {
29
29
  if (needle && !t.label.toLowerCase().includes(needle))
30
30
  continue;
31
- out.push({ id: t.id, label: t.label, startedAt: t.startedAt, fileCount: t.files.length });
31
+ const problems = (t.problems?.length ?? 0) + (t.problemsDropped ?? 0);
32
+ out.push({
33
+ id: t.id,
34
+ label: t.label,
35
+ startedAt: t.startedAt,
36
+ fileCount: t.files.length,
37
+ ...(problems > 0 ? { problemCount: problems } : {}),
38
+ });
32
39
  if (query.limit && out.length >= query.limit)
33
40
  break;
34
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightpath-dev/daemon",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Local Lightpath daemon: receives coverage from agents, maps it to source lines, and serves traces",
5
5
  "keywords": [
6
6
  "coverage",
@@ -35,8 +35,8 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "@lightpath-dev/core": "0.0.1",
39
- "@lightpath-dev/protocol": "0.0.1",
38
+ "@lightpath-dev/core": "0.1.0",
39
+ "@lightpath-dev/protocol": "0.1.0",
40
40
  "ws": "8.22.0"
41
41
  },
42
42
  "devDependencies": {