@saptools/cf-inspector 0.3.15 → 0.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ var init_types = __esm({
29
29
  }
30
30
  });
31
31
 
32
- // src/wsTransport.ts
32
+ // src/cdp/wsTransport.ts
33
33
  var wsTransport_exports = {};
34
34
  __export(wsTransport_exports, {
35
35
  wsTransportFactory: () => wsTransportFactory
@@ -37,6 +37,32 @@ __export(wsTransport_exports, {
37
37
  import { WebSocket } from "ws";
38
38
  async function wsTransportFactory(url) {
39
39
  const socket = new WebSocket(url, { perMessageDeflate: false });
40
+ await waitForOpen(socket, url);
41
+ const wrappers = /* @__PURE__ */ new WeakMap();
42
+ return {
43
+ send(payload) {
44
+ socket.send(payload);
45
+ },
46
+ close() {
47
+ socket.close();
48
+ },
49
+ get readyState() {
50
+ return socket.readyState;
51
+ },
52
+ on(event, listener) {
53
+ const wrapped = wrapListener(event, listener, wrappers);
54
+ socket.on(event, wrapped);
55
+ },
56
+ off(event, listener) {
57
+ const wrapped = wrappers.get(listener);
58
+ if (!wrapped) {
59
+ return;
60
+ }
61
+ socket.off(event, wrapped);
62
+ }
63
+ };
64
+ }
65
+ async function waitForOpen(socket, url) {
40
66
  await new Promise((resolve, reject) => {
41
67
  const onOpen = () => {
42
68
  socket.off("error", onError);
@@ -54,64 +80,30 @@ async function wsTransportFactory(url) {
54
80
  socket.once("open", onOpen);
55
81
  socket.once("error", onError);
56
82
  });
57
- const wrappers = /* @__PURE__ */ new WeakMap();
58
- const wrapMessage = (listener) => {
59
- const wrapped = (data) => {
83
+ }
84
+ function wrapListener(event, listener, wrappers) {
85
+ if (event === "message") {
86
+ const wrapped2 = (data) => {
60
87
  listener(data.toString("utf8"));
61
88
  };
62
- wrappers.set(listener, wrapped);
63
- return wrapped;
64
- };
65
- const wrapClose = (listener) => {
66
- const wrapped = () => {
89
+ wrappers.set(listener, wrapped2);
90
+ return wrapped2;
91
+ }
92
+ if (event === "close") {
93
+ const wrapped2 = () => {
67
94
  listener();
68
95
  };
69
- wrappers.set(listener, wrapped);
70
- return wrapped;
71
- };
72
- const wrapError = (listener) => {
73
- const wrapped = (err) => {
74
- listener(err);
75
- };
76
- wrappers.set(listener, wrapped);
77
- return wrapped;
78
- };
79
- return {
80
- send(payload) {
81
- socket.send(payload);
82
- },
83
- close() {
84
- socket.close();
85
- },
86
- get readyState() {
87
- return socket.readyState;
88
- },
89
- on(event, listener) {
90
- if (event === "message") {
91
- socket.on("message", wrapMessage(listener));
92
- } else if (event === "close") {
93
- socket.on("close", wrapClose(listener));
94
- } else {
95
- socket.on("error", wrapError(listener));
96
- }
97
- },
98
- off(event, listener) {
99
- const wrapped = wrappers.get(listener);
100
- if (!wrapped) {
101
- return;
102
- }
103
- if (event === "message") {
104
- socket.off("message", wrapped);
105
- } else if (event === "close") {
106
- socket.off("close", wrapped);
107
- } else {
108
- socket.off("error", wrapped);
109
- }
110
- }
96
+ wrappers.set(listener, wrapped2);
97
+ return wrapped2;
98
+ }
99
+ const wrapped = (err) => {
100
+ listener(err);
111
101
  };
102
+ wrappers.set(listener, wrapped);
103
+ return wrapped;
112
104
  }
113
105
  var init_wsTransport = __esm({
114
- "src/wsTransport.ts"() {
106
+ "src/cdp/wsTransport.ts"() {
115
107
  "use strict";
116
108
  init_types();
117
109
  }
@@ -259,252 +251,155 @@ function buildBreakpointUrlRegex(input) {
259
251
  }
260
252
  }
261
253
 
262
- // src/inspector.ts
263
- import { request } from "http";
264
- import { performance } from "perf_hooks";
265
-
266
- // src/cdp.ts
254
+ // src/inspector/breakpoints.ts
267
255
  init_types();
268
- import { EventEmitter } from "events";
269
- var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
270
- function parseMessage(raw) {
271
- try {
272
- const value = JSON.parse(raw);
273
- if (typeof value !== "object" || value === null) {
274
- return void 0;
275
- }
276
- return value;
277
- } catch {
278
- return void 0;
279
- }
256
+
257
+ // src/inspector/conversions.ts
258
+ function asString(value, fallback = "") {
259
+ return typeof value === "string" ? value : fallback;
280
260
  }
281
- async function loadDefaultFactory() {
282
- const mod = await Promise.resolve().then(() => (init_wsTransport(), wsTransport_exports));
283
- return mod.wsTransportFactory;
261
+ function asNumber(value, fallback = 0) {
262
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
284
263
  }
285
- var CdpClient = class _CdpClient {
286
- transport;
287
- requestTimeoutMs;
288
- pending = /* @__PURE__ */ new Map();
289
- emitter = new EventEmitter();
290
- nextId = 1;
291
- closed = false;
292
- closeReason;
293
- handleMessage = (raw) => {
294
- const parsed = parseMessage(raw);
295
- if (!parsed) {
296
- return;
264
+ function toResolvedLocations(value) {
265
+ if (!Array.isArray(value)) {
266
+ return [];
267
+ }
268
+ return value.flatMap((entry) => {
269
+ if (typeof entry !== "object" || entry === null) {
270
+ return [];
297
271
  }
298
- if (typeof parsed.id === "number") {
299
- const pending = this.pending.get(parsed.id);
300
- if (!pending) {
301
- return;
302
- }
303
- this.pending.delete(parsed.id);
304
- clearTimeout(pending.timer);
305
- if (parsed.error) {
306
- pending.reject(
307
- new CfInspectorError(
308
- "CDP_REQUEST_FAILED",
309
- `CDP request ${parsed.id.toString()} failed: ${parsed.error.message}`,
310
- JSON.stringify(parsed.error)
311
- )
312
- );
313
- return;
314
- }
315
- pending.resolve(parsed.result);
316
- return;
272
+ const candidate = entry;
273
+ const scriptId = asString(candidate.scriptId);
274
+ if (scriptId.length === 0) {
275
+ return [];
317
276
  }
318
- if (typeof parsed.method === "string") {
319
- this.emitter.emit(parsed.method, parsed.params);
320
- this.emitter.emit("event", { method: parsed.method, params: parsed.params });
277
+ const url = typeof candidate.url === "string" ? candidate.url : void 0;
278
+ const lineNumber = asNumber(candidate.lineNumber);
279
+ const result = url === void 0 ? { scriptId, lineNumber, columnNumber: asNumber(candidate.columnNumber) } : { scriptId, url, lineNumber, columnNumber: asNumber(candidate.columnNumber) };
280
+ return [result];
281
+ });
282
+ }
283
+ function toScopeChain(value) {
284
+ if (!Array.isArray(value)) {
285
+ return [];
286
+ }
287
+ return value.flatMap((entry) => {
288
+ if (typeof entry !== "object" || entry === null) {
289
+ return [];
321
290
  }
291
+ const candidate = entry;
292
+ const type = asString(candidate.type);
293
+ if (type.length === 0) {
294
+ return [];
295
+ }
296
+ const objectId = typeof candidate.object?.objectId === "string" ? candidate.object.objectId : void 0;
297
+ const name = typeof candidate.name === "string" ? candidate.name : void 0;
298
+ const base = name === void 0 ? { type } : { type, name };
299
+ return [objectId === void 0 ? base : { ...base, objectId }];
300
+ });
301
+ }
302
+ function toCallFrames(value) {
303
+ if (!Array.isArray(value)) {
304
+ return [];
305
+ }
306
+ return value.flatMap((entry) => {
307
+ if (typeof entry !== "object" || entry === null) {
308
+ return [];
309
+ }
310
+ const candidate = entry;
311
+ const callFrameId = asString(candidate.callFrameId);
312
+ if (callFrameId.length === 0) {
313
+ return [];
314
+ }
315
+ const url = typeof candidate.url === "string" ? candidate.url : void 0;
316
+ const base = {
317
+ callFrameId,
318
+ functionName: asString(candidate.functionName),
319
+ lineNumber: asNumber(candidate.location?.lineNumber),
320
+ columnNumber: asNumber(candidate.location?.columnNumber),
321
+ scopeChain: toScopeChain(candidate.scopeChain)
322
+ };
323
+ return [url === void 0 ? base : { ...base, url }];
324
+ });
325
+ }
326
+ function toPauseEvent(params, receivedAtMs) {
327
+ return {
328
+ reason: asString(params.reason),
329
+ hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
330
+ callFrames: toCallFrames(params.callFrames),
331
+ receivedAtMs
322
332
  };
323
- handleClose = () => {
324
- this.markClosed(new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Inspector connection closed"));
325
- };
326
- handleError = (err) => {
327
- this.markClosed(
328
- err instanceof CfInspectorError ? err : new CfInspectorError("INSPECTOR_CONNECTION_FAILED", err.message)
329
- );
333
+ }
334
+ function topFrameLocation(pause) {
335
+ const top = pause.callFrames[0];
336
+ if (top === void 0) {
337
+ return "(no call frame)";
338
+ }
339
+ const url = top.url !== void 0 && top.url.length > 0 ? top.url : "(unknown)";
340
+ return `${url}:${(top.lineNumber + 1).toString()}:${(top.columnNumber + 1).toString()}`;
341
+ }
342
+ function pauseDetail(pause) {
343
+ return JSON.stringify({
344
+ reason: pause.reason,
345
+ hitBreakpoints: pause.hitBreakpoints,
346
+ topFrame: topFrameLocation(pause)
347
+ });
348
+ }
349
+
350
+ // src/inspector/breakpoints.ts
351
+ async function setBreakpoint(session, input) {
352
+ const remoteRoot = input.remoteRoot ?? { kind: "none" };
353
+ const urlRegex = buildBreakpointUrlRegex({ file: input.file, remoteRoot });
354
+ const params = {
355
+ lineNumber: input.line - 1,
356
+ urlRegex
330
357
  };
331
- constructor(transport, requestTimeoutMs) {
332
- this.transport = transport;
333
- this.requestTimeoutMs = requestTimeoutMs;
334
- transport.on("message", this.handleMessage);
335
- transport.on("close", this.handleClose);
336
- transport.on("error", this.handleError);
358
+ if (input.condition !== void 0 && input.condition.length > 0) {
359
+ params["condition"] = input.condition;
337
360
  }
338
- static async connect(options) {
339
- const factory = options.transportFactory ?? await loadDefaultFactory();
340
- const transport = await factory(options.url);
341
- return new _CdpClient(transport, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
361
+ const result = await session.client.send(
362
+ "Debugger.setBreakpointByUrl",
363
+ params
364
+ );
365
+ const breakpointId = asString(result.breakpointId);
366
+ if (breakpointId.length === 0) {
367
+ throw new CfInspectorError(
368
+ "CDP_REQUEST_FAILED",
369
+ `setBreakpointByUrl did not return a breakpointId for ${input.file}:${input.line.toString()}`
370
+ );
342
371
  }
343
- async send(method, params = {}) {
344
- if (this.closed) {
345
- throw this.closeReason ?? new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Connection closed");
346
- }
347
- const id = this.nextId++;
348
- const payload = JSON.stringify({ id, method, params });
349
- return await new Promise((resolve, reject) => {
350
- const timer = setTimeout(() => {
351
- this.pending.delete(id);
352
- reject(
353
- new CfInspectorError(
354
- "CDP_REQUEST_FAILED",
355
- `CDP method ${method} timed out after ${this.requestTimeoutMs.toString()}ms`
356
- )
357
- );
358
- }, this.requestTimeoutMs);
359
- this.pending.set(id, {
360
- resolve: (value) => {
361
- resolve(value);
362
- },
363
- reject,
364
- timer
365
- });
366
- try {
367
- this.transport.send(payload);
368
- } catch (err) {
369
- clearTimeout(timer);
370
- this.pending.delete(id);
371
- const message = err instanceof Error ? err.message : String(err);
372
- reject(new CfInspectorError("CDP_REQUEST_FAILED", `Failed to send ${method}: ${message}`));
373
- }
374
- });
375
- }
376
- on(method, listener) {
377
- this.emitter.on(method, listener);
378
- return () => {
379
- this.emitter.off(method, listener);
380
- };
381
- }
382
- async waitFor(method, options = {
383
- timeoutMs: this.requestTimeoutMs
384
- }) {
385
- if (this.closed) {
386
- throw this.closeReason ?? new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Connection closed");
387
- }
388
- return await new Promise((resolve, reject) => {
389
- let settled = false;
390
- const cleanup = () => {
391
- clearTimeout(timer);
392
- offEvent();
393
- offClose();
394
- };
395
- const offEvent = this.on(method, (raw) => {
396
- if (settled) {
397
- return;
398
- }
399
- const params = raw;
400
- if (options.predicate && !options.predicate(params)) {
401
- return;
402
- }
403
- settled = true;
404
- cleanup();
405
- resolve(params);
406
- });
407
- const offClose = this.onClose((err) => {
408
- if (settled) {
409
- return;
410
- }
411
- settled = true;
412
- cleanup();
413
- reject(err);
414
- });
415
- const timer = setTimeout(() => {
416
- if (settled) {
417
- return;
418
- }
419
- settled = true;
420
- cleanup();
421
- reject(
422
- new CfInspectorError(
423
- "BREAKPOINT_NOT_HIT",
424
- `Timed out waiting for ${method} after ${options.timeoutMs.toString()}ms`
425
- )
426
- );
427
- }, options.timeoutMs);
428
- });
429
- }
430
- onClose(listener) {
431
- if (this.closed) {
432
- const reason = this.closeReason ?? new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Connection closed");
433
- queueMicrotask(() => {
434
- listener(reason);
435
- });
436
- return () => {
437
- };
438
- }
439
- this.emitter.on("__close__", listener);
440
- return () => {
441
- this.emitter.off("__close__", listener);
442
- };
443
- }
444
- dispose() {
445
- if (this.closed) {
446
- return;
447
- }
448
- this.transport.off("message", this.handleMessage);
449
- this.transport.off("close", this.handleClose);
450
- this.transport.off("error", this.handleError);
451
- try {
452
- this.transport.close();
453
- } catch {
454
- }
455
- this.markClosed(new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Connection disposed"));
456
- }
457
- get isClosed() {
458
- return this.closed;
459
- }
460
- markClosed(reason) {
461
- if (this.closed) {
462
- return;
463
- }
464
- this.closed = true;
465
- this.closeReason = reason;
466
- for (const [, pending] of this.pending) {
467
- clearTimeout(pending.timer);
468
- pending.reject(reason);
469
- }
470
- this.pending.clear();
471
- this.emitter.emit("__close__", reason);
472
- this.emitter.removeAllListeners();
473
- }
474
- };
475
-
476
- // src/inspector.ts
477
- init_types();
478
- var DEFAULT_CONNECT_TIMEOUT_MS = 5e3;
479
- var DEFAULT_HOST = "127.0.0.1";
480
- async function fetchJson(url, timeoutMs) {
481
- return await new Promise((resolve, reject) => {
482
- const req = request(url, { method: "GET" }, (res) => {
483
- const chunks = [];
484
- res.on("data", (chunk) => {
485
- chunks.push(chunk);
486
- });
487
- res.on("end", () => {
488
- try {
489
- const text = Buffer.concat(chunks).toString("utf8");
490
- resolve(JSON.parse(text));
491
- } catch (err) {
492
- const message = err instanceof Error ? err.message : String(err);
493
- reject(
494
- new CfInspectorError(
495
- "INSPECTOR_DISCOVERY_FAILED",
496
- `Failed to parse inspector discovery response from ${url}: ${message}`
497
- )
498
- );
499
- }
500
- });
501
- res.on("error", (err) => {
502
- reject(
503
- new CfInspectorError(
504
- "INSPECTOR_DISCOVERY_FAILED",
505
- `Inspector discovery response error: ${err.message}`
506
- )
507
- );
372
+ return {
373
+ breakpointId,
374
+ file: input.file,
375
+ line: input.line,
376
+ urlRegex,
377
+ resolvedLocations: toResolvedLocations(result.locations)
378
+ };
379
+ }
380
+ async function removeBreakpoint(session, breakpointId) {
381
+ await session.client.send("Debugger.removeBreakpoint", { breakpointId });
382
+ }
383
+
384
+ // src/inspector/discovery.ts
385
+ init_types();
386
+ import { request } from "http";
387
+ async function fetchJson(url, timeoutMs) {
388
+ return await new Promise((resolve, reject) => {
389
+ const req = request(url, { method: "GET" }, (res) => {
390
+ const chunks = [];
391
+ res.on("data", (chunk) => {
392
+ chunks.push(chunk);
393
+ });
394
+ res.on("end", () => {
395
+ try {
396
+ resolve(parseJsonResponse(chunks));
397
+ } catch (err) {
398
+ reject(parseDiscoveryError(url, err));
399
+ }
400
+ });
401
+ res.on("error", (err) => {
402
+ reject(newDiscoveryError(`Inspector discovery response error: ${err.message}`));
508
403
  });
509
404
  });
510
405
  req.setTimeout(timeoutMs, () => {
@@ -526,6 +421,17 @@ async function fetchJson(url, timeoutMs) {
526
421
  req.end();
527
422
  });
528
423
  }
424
+ function parseJsonResponse(chunks) {
425
+ const text = Buffer.concat(chunks).toString("utf8");
426
+ return JSON.parse(text);
427
+ }
428
+ function parseDiscoveryError(url, err) {
429
+ const message = err instanceof Error ? err.message : String(err);
430
+ return newDiscoveryError(`Failed to parse inspector discovery response from ${url}: ${message}`);
431
+ }
432
+ function newDiscoveryError(message) {
433
+ return new CfInspectorError("INSPECTOR_DISCOVERY_FAILED", message);
434
+ }
529
435
  function toInspectorTarget(value, source) {
530
436
  if (typeof value !== "object" || value === null) {
531
437
  throw new CfInspectorError(
@@ -592,240 +498,61 @@ async function fetchInspectorVersion(host, port, timeoutMs) {
592
498
  }
593
499
  return { browser, protocolVersion };
594
500
  }
595
- async function connectInspector(options) {
596
- const host = options.host ?? DEFAULT_HOST;
597
- const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
598
- const targets = await discoverInspectorTargets(host, options.port, connectTimeoutMs);
599
- const target = targets[0];
600
- if (!target) {
601
- throw new CfInspectorError(
602
- "INSPECTOR_DISCOVERY_FAILED",
603
- `No inspector targets available on ${host}:${options.port.toString()}`
604
- );
501
+
502
+ // src/inspector/pause.ts
503
+ init_types();
504
+ import { performance } from "perf_hooks";
505
+ function pauseMatches(pause, breakpointIds) {
506
+ if (breakpointIds === void 0 || breakpointIds.length === 0) {
507
+ return true;
605
508
  }
606
- const client = await CdpClient.connect({ url: target.webSocketDebuggerUrl });
607
- const scripts = /* @__PURE__ */ new Map();
608
- client.on("Debugger.scriptParsed", (raw) => {
609
- const params = raw;
610
- const scriptId = asString(params.scriptId);
611
- const url = asString(params.url);
612
- if (scriptId.length === 0) {
613
- return;
614
- }
615
- scripts.set(scriptId, { scriptId, url });
616
- });
617
- const PAUSE_BUFFER_LIMIT = 32;
618
- const pauseBuffer = [];
619
- const pauseWaitGate = { active: false };
620
- const debuggerState = {};
621
- client.on("Debugger.paused", (raw) => {
622
- if (pauseWaitGate.active) {
623
- return;
624
- }
625
- const params = raw;
626
- const event = toPauseEvent(params, performance.now());
627
- if (pauseBuffer.length >= PAUSE_BUFFER_LIMIT) {
628
- pauseBuffer.shift();
629
- }
630
- pauseBuffer.push(event);
631
- });
632
- client.on("Debugger.resumed", () => {
633
- debuggerState.lastResumedAtMs = performance.now();
634
- });
635
- await client.send("Runtime.enable");
636
- await client.send("Debugger.enable");
637
- return {
638
- client,
639
- target,
640
- scripts,
641
- pauseBuffer,
642
- pauseWaitGate,
643
- debuggerState,
644
- dispose: async () => {
645
- try {
646
- await client.send("Debugger.disable");
647
- } catch {
648
- }
649
- client.dispose();
650
- }
651
- };
509
+ return pause.hitBreakpoints.some((id) => breakpointIds.includes(id));
652
510
  }
653
- function asString(value, fallback = "") {
654
- return typeof value === "string" ? value : fallback;
511
+ function remainingUntil(deadlineMs) {
512
+ return Math.max(0, deadlineMs - performance.now());
655
513
  }
656
- function asNumber(value, fallback = 0) {
657
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
514
+ function hasResumedSincePause(session, pause) {
515
+ const pauseAt = pause.receivedAtMs;
516
+ const resumedAt = session.debuggerState.lastResumedAtMs;
517
+ return pauseAt !== void 0 && resumedAt !== void 0 && resumedAt >= pauseAt;
658
518
  }
659
- function toResolvedLocations(value) {
660
- if (!Array.isArray(value)) {
661
- return [];
519
+ function throwBreakpointTimeout(timeoutMs) {
520
+ throw new CfInspectorError(
521
+ "BREAKPOINT_NOT_HIT",
522
+ `Timed out waiting for matching Debugger.paused after ${timeoutMs.toString()}ms`
523
+ );
524
+ }
525
+ function throwUnrelatedPauseTimeout(pause, timeoutMs) {
526
+ throw new CfInspectorError(
527
+ "UNRELATED_PAUSE_TIMEOUT",
528
+ `Target stayed paused by another debugger event before this command's breakpoint could hit within ${timeoutMs.toString()}ms`,
529
+ pauseDetail(pause)
530
+ );
531
+ }
532
+ async function waitForUnmatchedPauseToResume(session, pause, deadlineMs, timeoutMs) {
533
+ if (hasResumedSincePause(session, pause)) {
534
+ return;
662
535
  }
663
- return value.flatMap((entry) => {
664
- if (typeof entry !== "object" || entry === null) {
665
- return [];
666
- }
667
- const candidate = entry;
668
- const scriptId = asString(candidate.scriptId);
669
- if (scriptId.length === 0) {
670
- return [];
536
+ const remainingMs = remainingUntil(deadlineMs);
537
+ if (remainingMs <= 0) {
538
+ throwUnrelatedPauseTimeout(pause, timeoutMs);
539
+ }
540
+ try {
541
+ await session.client.waitFor("Debugger.resumed", { timeoutMs: remainingMs });
542
+ session.debuggerState.lastResumedAtMs = performance.now();
543
+ } catch (err) {
544
+ if (err instanceof CfInspectorError && err.code === "BREAKPOINT_NOT_HIT") {
545
+ throwUnrelatedPauseTimeout(pause, timeoutMs);
671
546
  }
672
- const url = typeof candidate.url === "string" ? candidate.url : void 0;
673
- const lineNumber = asNumber(candidate.lineNumber);
674
- const result = url === void 0 ? { scriptId, lineNumber, columnNumber: asNumber(candidate.columnNumber) } : { scriptId, url, lineNumber, columnNumber: asNumber(candidate.columnNumber) };
675
- return [result];
676
- });
677
- }
678
- async function setBreakpoint(session, input) {
679
- const remoteRoot = input.remoteRoot ?? { kind: "none" };
680
- const urlRegex = buildBreakpointUrlRegex({ file: input.file, remoteRoot });
681
- const params = {
682
- lineNumber: input.line - 1,
683
- urlRegex
684
- };
685
- if (input.condition !== void 0 && input.condition.length > 0) {
686
- params["condition"] = input.condition;
547
+ throw err;
687
548
  }
688
- const result = await session.client.send(
689
- "Debugger.setBreakpointByUrl",
690
- params
691
- );
692
- const breakpointId = asString(result.breakpointId);
693
- if (breakpointId.length === 0) {
549
+ }
550
+ async function handleUnmatchedPause(session, pause, options, deadlineMs) {
551
+ if (options.unmatchedPausePolicy === "fail") {
694
552
  throw new CfInspectorError(
695
- "CDP_REQUEST_FAILED",
696
- `setBreakpointByUrl did not return a breakpointId for ${input.file}:${input.line.toString()}`
697
- );
698
- }
699
- return {
700
- breakpointId,
701
- file: input.file,
702
- line: input.line,
703
- urlRegex,
704
- resolvedLocations: toResolvedLocations(result.locations)
705
- };
706
- }
707
- async function removeBreakpoint(session, breakpointId) {
708
- await session.client.send("Debugger.removeBreakpoint", { breakpointId });
709
- }
710
- function toScopeChain(value) {
711
- if (!Array.isArray(value)) {
712
- return [];
713
- }
714
- return value.flatMap((entry) => {
715
- if (typeof entry !== "object" || entry === null) {
716
- return [];
717
- }
718
- const candidate = entry;
719
- const type = asString(candidate.type);
720
- if (type.length === 0) {
721
- return [];
722
- }
723
- const objectId = typeof candidate.object?.objectId === "string" ? candidate.object.objectId : void 0;
724
- const name = typeof candidate.name === "string" ? candidate.name : void 0;
725
- const base = name === void 0 ? { type } : { type, name };
726
- return [objectId === void 0 ? base : { ...base, objectId }];
727
- });
728
- }
729
- function toCallFrames(value) {
730
- if (!Array.isArray(value)) {
731
- return [];
732
- }
733
- return value.flatMap((entry) => {
734
- if (typeof entry !== "object" || entry === null) {
735
- return [];
736
- }
737
- const candidate = entry;
738
- const callFrameId = asString(candidate.callFrameId);
739
- if (callFrameId.length === 0) {
740
- return [];
741
- }
742
- const url = typeof candidate.url === "string" ? candidate.url : void 0;
743
- const lineNumber = asNumber(candidate.location?.lineNumber);
744
- const columnNumber = asNumber(candidate.location?.columnNumber);
745
- const base = {
746
- callFrameId,
747
- functionName: asString(candidate.functionName),
748
- lineNumber,
749
- columnNumber,
750
- scopeChain: toScopeChain(candidate.scopeChain)
751
- };
752
- return [url === void 0 ? base : { ...base, url }];
753
- });
754
- }
755
- function pauseMatches(pause, breakpointIds) {
756
- if (breakpointIds === void 0 || breakpointIds.length === 0) {
757
- return true;
758
- }
759
- return pause.hitBreakpoints.some((id) => breakpointIds.includes(id));
760
- }
761
- function toPauseEvent(params, receivedAtMs) {
762
- return {
763
- reason: asString(params.reason),
764
- hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
765
- callFrames: toCallFrames(params.callFrames),
766
- receivedAtMs
767
- };
768
- }
769
- function remainingUntil(deadlineMs) {
770
- return Math.max(0, deadlineMs - performance.now());
771
- }
772
- function topFrameLocation(pause) {
773
- const top = pause.callFrames[0];
774
- if (top === void 0) {
775
- return "(no call frame)";
776
- }
777
- const url = top.url !== void 0 && top.url.length > 0 ? top.url : "(unknown)";
778
- return `${url}:${(top.lineNumber + 1).toString()}:${(top.columnNumber + 1).toString()}`;
779
- }
780
- function pauseDetail(pause) {
781
- return JSON.stringify({
782
- reason: pause.reason,
783
- hitBreakpoints: pause.hitBreakpoints,
784
- topFrame: topFrameLocation(pause)
785
- });
786
- }
787
- function hasResumedSincePause(session, pause) {
788
- const pauseAt = pause.receivedAtMs;
789
- const resumedAt = session.debuggerState.lastResumedAtMs;
790
- return pauseAt !== void 0 && resumedAt !== void 0 && resumedAt >= pauseAt;
791
- }
792
- function throwBreakpointTimeout(timeoutMs) {
793
- throw new CfInspectorError(
794
- "BREAKPOINT_NOT_HIT",
795
- `Timed out waiting for matching Debugger.paused after ${timeoutMs.toString()}ms`
796
- );
797
- }
798
- function throwUnrelatedPauseTimeout(pause, timeoutMs) {
799
- throw new CfInspectorError(
800
- "UNRELATED_PAUSE_TIMEOUT",
801
- `Target stayed paused by another debugger event before this command's breakpoint could hit within ${timeoutMs.toString()}ms`,
802
- pauseDetail(pause)
803
- );
804
- }
805
- async function waitForUnmatchedPauseToResume(session, pause, deadlineMs, timeoutMs) {
806
- if (hasResumedSincePause(session, pause)) {
807
- return;
808
- }
809
- const remainingMs = remainingUntil(deadlineMs);
810
- if (remainingMs <= 0) {
811
- throwUnrelatedPauseTimeout(pause, timeoutMs);
812
- }
813
- try {
814
- await session.client.waitFor("Debugger.resumed", { timeoutMs: remainingMs });
815
- session.debuggerState.lastResumedAtMs = performance.now();
816
- } catch (err) {
817
- if (err instanceof CfInspectorError && err.code === "BREAKPOINT_NOT_HIT") {
818
- throwUnrelatedPauseTimeout(pause, timeoutMs);
819
- }
820
- throw err;
821
- }
822
- }
823
- async function handleUnmatchedPause(session, pause, options, deadlineMs) {
824
- if (options.unmatchedPausePolicy === "fail") {
825
- throw new CfInspectorError(
826
- "UNRELATED_PAUSE",
827
- "Target paused before this command's breakpoint was reached",
828
- pauseDetail(pause)
553
+ "UNRELATED_PAUSE",
554
+ "Target paused before this command's breakpoint was reached",
555
+ pauseDetail(pause)
829
556
  );
830
557
  }
831
558
  if (hasResumedSincePause(session, pause)) {
@@ -848,25 +575,7 @@ async function waitForPause(session, options) {
848
575
  }
849
576
  await handleUnmatchedPause(session, buffered, options, deadlineMs);
850
577
  }
851
- const remainingMs = remainingUntil(deadlineMs);
852
- if (remainingMs <= 0) {
853
- throwBreakpointTimeout(options.timeoutMs);
854
- }
855
- session.pauseWaitGate.active = true;
856
- let receivedAtMs;
857
- let params;
858
- try {
859
- params = await session.client.waitFor("Debugger.paused", {
860
- timeoutMs: remainingMs,
861
- predicate: () => {
862
- receivedAtMs = performance.now();
863
- return true;
864
- }
865
- });
866
- } finally {
867
- session.pauseWaitGate.active = false;
868
- }
869
- const pause = toPauseEvent(params, receivedAtMs ?? performance.now());
578
+ const pause = await waitForLivePause(session, options, deadlineMs);
870
579
  if (pauseMatches(pause, options.breakpointIds)) {
871
580
  return pause;
872
581
  }
@@ -874,6 +583,30 @@ async function waitForPause(session, options) {
874
583
  }
875
584
  throwBreakpointTimeout(options.timeoutMs);
876
585
  }
586
+ async function waitForLivePause(session, options, deadlineMs) {
587
+ const remainingMs = remainingUntil(deadlineMs);
588
+ if (remainingMs <= 0) {
589
+ throwBreakpointTimeout(options.timeoutMs);
590
+ }
591
+ session.pauseWaitGate.active = true;
592
+ let receivedAtMs;
593
+ let params;
594
+ try {
595
+ params = await session.client.waitFor("Debugger.paused", {
596
+ timeoutMs: remainingMs,
597
+ predicate: () => {
598
+ receivedAtMs = performance.now();
599
+ return true;
600
+ }
601
+ });
602
+ } finally {
603
+ session.pauseWaitGate.active = false;
604
+ }
605
+ return toPauseEvent(params, receivedAtMs ?? performance.now());
606
+ }
607
+
608
+ // src/inspector/runtime.ts
609
+ init_types();
877
610
  async function resume(session) {
878
611
  await session.client.send("Debugger.resume");
879
612
  }
@@ -922,194 +655,328 @@ async function getProperties(session, objectId) {
922
655
  return result.result;
923
656
  }
924
657
 
925
- // src/snapshot.ts
658
+ // src/inspector/session.ts
659
+ import { performance as performance2 } from "perf_hooks";
660
+
661
+ // src/cdp/client.ts
926
662
  init_types();
927
- var MAX_SCOPES = 3;
928
- var MAX_SCOPE_VARIABLES = 20;
929
- var MAX_CHILD_VARIABLES = 8;
930
- var MAX_VARIABLE_DEPTH = 2;
931
- var DEFAULT_MAX_VALUE_LENGTH = 4096;
932
- var PRIORITY_BY_TYPE = {
933
- local: 0,
934
- arguments: 1,
935
- block: 2,
936
- closure: 3,
937
- catch: 4,
938
- with: 5,
939
- module: 6,
940
- script: 7
941
- };
942
- function buildDescribed(value, type, objectId) {
943
- const base = { value };
944
- if (type !== void 0) {
945
- base.type = type;
946
- }
947
- if (objectId !== void 0) {
948
- base.objectId = objectId;
663
+ import { EventEmitter } from "events";
664
+ var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
665
+ function parseMessage(raw) {
666
+ try {
667
+ const value = JSON.parse(raw);
668
+ if (typeof value !== "object" || value === null) {
669
+ return void 0;
670
+ }
671
+ return value;
672
+ } catch {
673
+ return void 0;
949
674
  }
950
- return base;
951
675
  }
952
- function describeProperty(prop) {
953
- const value = prop.value;
954
- if (value === void 0) {
955
- return { value: "undefined" };
676
+ async function loadDefaultFactory() {
677
+ const mod = await Promise.resolve().then(() => (init_wsTransport(), wsTransport_exports));
678
+ return mod.wsTransportFactory;
679
+ }
680
+ var CdpClient = class _CdpClient {
681
+ transport;
682
+ requestTimeoutMs;
683
+ pending = /* @__PURE__ */ new Map();
684
+ emitter = new EventEmitter();
685
+ nextId = 1;
686
+ closed = false;
687
+ closeReason;
688
+ handleMessage = (raw) => {
689
+ const parsed = parseMessage(raw);
690
+ if (!parsed) {
691
+ return;
692
+ }
693
+ if (typeof parsed.id === "number") {
694
+ this.handleResponse(parsed);
695
+ return;
696
+ }
697
+ if (typeof parsed.method === "string") {
698
+ this.emitter.emit(parsed.method, parsed.params);
699
+ this.emitter.emit("event", { method: parsed.method, params: parsed.params });
700
+ }
701
+ };
702
+ handleClose = () => {
703
+ this.markClosed(new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Inspector connection closed"));
704
+ };
705
+ handleError = (err) => {
706
+ this.markClosed(
707
+ err instanceof CfInspectorError ? err : new CfInspectorError("INSPECTOR_CONNECTION_FAILED", err.message)
708
+ );
709
+ };
710
+ constructor(transport, requestTimeoutMs) {
711
+ this.transport = transport;
712
+ this.requestTimeoutMs = requestTimeoutMs;
713
+ transport.on("message", this.handleMessage);
714
+ transport.on("close", this.handleClose);
715
+ transport.on("error", this.handleError);
956
716
  }
957
- const type = typeof value.type === "string" ? value.type : void 0;
958
- const objectId = typeof value.objectId === "string" ? value.objectId : void 0;
959
- if (type === "undefined") {
960
- return buildDescribed("undefined", type);
717
+ static async connect(options) {
718
+ const factory = options.transportFactory ?? await loadDefaultFactory();
719
+ const transport = await factory(options.url);
720
+ return new _CdpClient(transport, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
961
721
  }
962
- if (type === "string" && typeof value.value === "string") {
963
- return buildDescribed(JSON.stringify(value.value), type);
722
+ async send(method, params = {}) {
723
+ if (this.closed) {
724
+ throw this.closeReason ?? new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Connection closed");
725
+ }
726
+ const id = this.nextId++;
727
+ const payload = JSON.stringify({ id, method, params });
728
+ return await new Promise((resolve, reject) => {
729
+ const timer = this.createRequestTimer(id, method, reject);
730
+ this.pending.set(id, {
731
+ resolve: (value) => {
732
+ resolve(value);
733
+ },
734
+ reject,
735
+ timer
736
+ });
737
+ this.sendPayload(id, method, payload, timer, reject);
738
+ });
964
739
  }
965
- if ((type === "number" || type === "boolean" || type === "bigint" || type === "symbol") && isPrimitive(value.value)) {
966
- return buildDescribed(formatPrimitive(value.value), type);
740
+ on(method, listener) {
741
+ this.emitter.on(method, listener);
742
+ return () => {
743
+ this.emitter.off(method, listener);
744
+ };
967
745
  }
968
- if (typeof value.description === "string") {
969
- return buildDescribed(value.description, type, objectId);
746
+ async waitFor(method, options = {
747
+ timeoutMs: this.requestTimeoutMs
748
+ }) {
749
+ if (this.closed) {
750
+ throw this.closeReason ?? new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Connection closed");
751
+ }
752
+ return await new Promise((resolve, reject) => {
753
+ let settled = false;
754
+ const cleanup = () => {
755
+ clearTimeout(timer);
756
+ offEvent();
757
+ offClose();
758
+ };
759
+ const finish = (value) => {
760
+ settled = true;
761
+ cleanup();
762
+ resolve(value);
763
+ };
764
+ const offEvent = this.on(method, (raw) => {
765
+ if (settled) {
766
+ return;
767
+ }
768
+ const params = raw;
769
+ if (options.predicate && !options.predicate(params)) {
770
+ return;
771
+ }
772
+ finish(params);
773
+ });
774
+ const offClose = this.onClose((err) => {
775
+ if (settled) {
776
+ return;
777
+ }
778
+ settled = true;
779
+ cleanup();
780
+ reject(err);
781
+ });
782
+ const timer = setTimeout(() => {
783
+ if (settled) {
784
+ return;
785
+ }
786
+ settled = true;
787
+ cleanup();
788
+ reject(this.createWaitTimeoutError(method, options.timeoutMs));
789
+ }, options.timeoutMs);
790
+ });
970
791
  }
971
- if (isPrimitive(value.value)) {
972
- return buildDescribed(formatPrimitive(value.value), type);
792
+ onClose(listener) {
793
+ if (this.closed) {
794
+ const reason = this.closeReason ?? new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Connection closed");
795
+ queueMicrotask(() => {
796
+ listener(reason);
797
+ });
798
+ return () => {
799
+ };
800
+ }
801
+ this.emitter.on("__close__", listener);
802
+ return () => {
803
+ this.emitter.off("__close__", listener);
804
+ };
973
805
  }
974
- if (objectId === void 0) {
975
- return buildDescribed("undefined", type);
806
+ dispose() {
807
+ if (this.closed) {
808
+ return;
809
+ }
810
+ this.transport.off("message", this.handleMessage);
811
+ this.transport.off("close", this.handleClose);
812
+ this.transport.off("error", this.handleError);
813
+ try {
814
+ this.transport.close();
815
+ } catch {
816
+ }
817
+ this.markClosed(new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Connection disposed"));
976
818
  }
977
- return buildDescribed("[object]", type, objectId);
978
- }
979
- function isPrimitive(value) {
980
- const t = typeof value;
981
- return t === "string" || t === "number" || t === "boolean" || t === "bigint" || t === "symbol";
982
- }
983
- function formatPrimitive(value) {
984
- if (typeof value === "symbol") {
985
- return value.toString();
819
+ get isClosed() {
820
+ return this.closed;
986
821
  }
987
- if (typeof value === "bigint") {
988
- return `${value.toString()}n`;
822
+ handleResponse(parsed) {
823
+ const pending = this.pending.get(parsed.id ?? -1);
824
+ if (!pending) {
825
+ return;
826
+ }
827
+ this.pending.delete(parsed.id ?? -1);
828
+ clearTimeout(pending.timer);
829
+ if (parsed.error) {
830
+ pending.reject(
831
+ new CfInspectorError(
832
+ "CDP_REQUEST_FAILED",
833
+ `CDP request ${(parsed.id ?? -1).toString()} failed: ${parsed.error.message}`,
834
+ JSON.stringify(parsed.error)
835
+ )
836
+ );
837
+ return;
838
+ }
839
+ pending.resolve(parsed.result);
989
840
  }
990
- return String(value);
991
- }
992
- function resolveMaxValueLength(value) {
993
- if (value === void 0) {
994
- return DEFAULT_MAX_VALUE_LENGTH;
841
+ createRequestTimer(id, method, reject) {
842
+ return setTimeout(() => {
843
+ this.pending.delete(id);
844
+ reject(
845
+ new CfInspectorError(
846
+ "CDP_REQUEST_FAILED",
847
+ `CDP method ${method} timed out after ${this.requestTimeoutMs.toString()}ms`
848
+ )
849
+ );
850
+ }, this.requestTimeoutMs);
995
851
  }
996
- if (!Number.isInteger(value) || value <= 0) {
997
- throw new CfInspectorError(
998
- "INVALID_ARGUMENT",
999
- `Invalid maxValueLength: ${value.toString()} \u2014 expected a positive integer`
852
+ createWaitTimeoutError(method, timeoutMs) {
853
+ return new CfInspectorError(
854
+ "BREAKPOINT_NOT_HIT",
855
+ `Timed out waiting for ${method} after ${timeoutMs.toString()}ms`
1000
856
  );
1001
857
  }
1002
- return value;
1003
- }
1004
- function limitValueLength(raw, maxValueLength = DEFAULT_MAX_VALUE_LENGTH) {
1005
- if (raw.length <= maxValueLength) {
1006
- return raw;
858
+ sendPayload(id, method, payload, timer, reject) {
859
+ try {
860
+ this.transport.send(payload);
861
+ } catch (err) {
862
+ clearTimeout(timer);
863
+ this.pending.delete(id);
864
+ const message = err instanceof Error ? err.message : String(err);
865
+ reject(new CfInspectorError("CDP_REQUEST_FAILED", `Failed to send ${method}: ${message}`));
866
+ }
1007
867
  }
1008
- return `${raw.slice(0, maxValueLength)}...`;
1009
- }
1010
- function isExpandable(type) {
1011
- return type === "object" || type === "function";
1012
- }
1013
- async function captureProperties(session, objectId, limit, depth, maxValueLength) {
1014
- const properties = await getProperties(session, objectId);
1015
- const limited = properties.slice(0, limit);
1016
- const variables = await Promise.all(
1017
- limited.map(async (prop) => {
1018
- const name = typeof prop.name === "string" ? prop.name : "?";
1019
- const described = describeProperty(prop);
1020
- let children;
1021
- if (depth > 0 && described.objectId !== void 0 && isExpandable(described.type)) {
1022
- try {
1023
- const nested = await captureProperties(
1024
- session,
1025
- described.objectId,
1026
- MAX_CHILD_VARIABLES,
1027
- depth - 1,
1028
- maxValueLength
1029
- );
1030
- if (nested.length > 0) {
1031
- children = nested;
1032
- }
1033
- } catch {
1034
- }
1035
- }
1036
- const sanitizedValue = limitValueLength(described.value, maxValueLength);
1037
- const base = { name, value: sanitizedValue };
1038
- const withType = described.type === void 0 ? base : { ...base, type: described.type };
1039
- return children === void 0 ? withType : { ...withType, children };
1040
- })
1041
- );
1042
- return variables;
1043
- }
1044
- function selectScopes(scopeChain) {
1045
- const eligible = scopeChain.filter((scope) => scope.objectId !== void 0 && scope.type !== "global");
1046
- return [...eligible].sort((a, b) => priorityOf(a.type) - priorityOf(b.type)).slice(0, MAX_SCOPES);
1047
- }
1048
- function priorityOf(type) {
1049
- return PRIORITY_BY_TYPE[type] ?? Number.MAX_SAFE_INTEGER;
1050
- }
1051
- async function captureScopes(session, frame, maxValueLength) {
1052
- const scopes = selectScopes(frame.scopeChain);
1053
- return await Promise.all(
1054
- scopes.map(async (scope) => {
1055
- const objectId = scope.objectId;
1056
- if (objectId === void 0) {
1057
- return { type: scope.type, variables: [] };
1058
- }
1059
- try {
1060
- const variables = await captureProperties(
1061
- session,
1062
- objectId,
1063
- MAX_SCOPE_VARIABLES,
1064
- MAX_VARIABLE_DEPTH,
1065
- maxValueLength
1066
- );
1067
- return { type: scope.type, variables };
868
+ markClosed(reason) {
869
+ if (this.closed) {
870
+ return;
871
+ }
872
+ this.closed = true;
873
+ this.closeReason = reason;
874
+ for (const [, pending] of this.pending) {
875
+ clearTimeout(pending.timer);
876
+ pending.reject(reason);
877
+ }
878
+ this.pending.clear();
879
+ this.emitter.emit("__close__", reason);
880
+ this.emitter.removeAllListeners();
881
+ }
882
+ };
883
+
884
+ // src/inspector/session.ts
885
+ init_types();
886
+ var DEFAULT_CONNECT_TIMEOUT_MS = 5e3;
887
+ var DEFAULT_HOST = "127.0.0.1";
888
+ var PAUSE_BUFFER_LIMIT = 32;
889
+ async function connectInspector(options) {
890
+ const host = options.host ?? DEFAULT_HOST;
891
+ const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
892
+ const targets = await discoverInspectorTargets(host, options.port, connectTimeoutMs);
893
+ const target = targets[0];
894
+ if (!target) {
895
+ throw new CfInspectorError(
896
+ "INSPECTOR_DISCOVERY_FAILED",
897
+ `No inspector targets available on ${host}:${options.port.toString()}`
898
+ );
899
+ }
900
+ const client = await CdpClient.connect({ url: target.webSocketDebuggerUrl });
901
+ const scripts = /* @__PURE__ */ new Map();
902
+ client.on("Debugger.scriptParsed", (raw) => {
903
+ const params = raw;
904
+ const scriptId = asString(params.scriptId);
905
+ const url = asString(params.url);
906
+ if (scriptId.length === 0) {
907
+ return;
908
+ }
909
+ scripts.set(scriptId, { scriptId, url });
910
+ });
911
+ const pauseBuffer = [];
912
+ const pauseWaitGate = { active: false };
913
+ const debuggerState = {};
914
+ client.on("Debugger.paused", (raw) => {
915
+ if (pauseWaitGate.active) {
916
+ return;
917
+ }
918
+ const params = raw;
919
+ const event = toPauseEvent(params, performance2.now());
920
+ if (pauseBuffer.length >= PAUSE_BUFFER_LIMIT) {
921
+ pauseBuffer.shift();
922
+ }
923
+ pauseBuffer.push(event);
924
+ });
925
+ client.on("Debugger.resumed", () => {
926
+ debuggerState.lastResumedAtMs = performance2.now();
927
+ });
928
+ await client.send("Runtime.enable");
929
+ await client.send("Debugger.enable");
930
+ return {
931
+ client,
932
+ target,
933
+ scripts,
934
+ pauseBuffer,
935
+ pauseWaitGate,
936
+ debuggerState,
937
+ dispose: async () => {
938
+ try {
939
+ await client.send("Debugger.disable");
1068
940
  } catch {
1069
- return { type: scope.type, variables: [] };
1070
941
  }
1071
- })
1072
- );
1073
- }
1074
- function evalResultToCaptured(expression, result, maxValueLength = DEFAULT_MAX_VALUE_LENGTH) {
1075
- if (result.exceptionDetails !== void 0) {
1076
- const text = typeof result.exceptionDetails.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails.text === "string" ? result.exceptionDetails.text : "evaluation failed";
1077
- return { expression, error: limitValueLength(text, maxValueLength) };
1078
- }
1079
- const inner = result.result;
1080
- if (!inner) {
1081
- return { expression, error: "no result returned" };
1082
- }
1083
- const type = typeof inner.type === "string" ? inner.type : void 0;
1084
- const buildCaptured = (rendered) => {
1085
- const sanitized = limitValueLength(rendered, maxValueLength);
1086
- const base = { expression, value: sanitized };
1087
- return type === void 0 ? base : { ...base, type };
942
+ client.dispose();
943
+ }
1088
944
  };
1089
- if (type === "string" && typeof inner.value === "string") {
1090
- return buildCaptured(JSON.stringify(inner.value));
945
+ }
946
+
947
+ // src/snapshot/values.ts
948
+ init_types();
949
+ var DEFAULT_MAX_VALUE_LENGTH = 4096;
950
+ function isPrimitive(value) {
951
+ const t = typeof value;
952
+ return t === "string" || t === "number" || t === "boolean" || t === "bigint" || t === "symbol";
953
+ }
954
+ function formatPrimitive(value) {
955
+ if (typeof value === "symbol") {
956
+ return value.toString();
1091
957
  }
1092
- if ((type === "number" || type === "boolean" || type === "bigint") && isPrimitive(inner.value)) {
1093
- return buildCaptured(formatPrimitive(inner.value));
958
+ if (typeof value === "bigint") {
959
+ return `${value.toString()}n`;
1094
960
  }
1095
- if (typeof inner.description === "string") {
1096
- return buildCaptured(inner.description);
961
+ return String(value);
962
+ }
963
+ function resolveMaxValueLength(value) {
964
+ if (value === void 0) {
965
+ return DEFAULT_MAX_VALUE_LENGTH;
1097
966
  }
1098
- if (isPrimitive(inner.value)) {
1099
- return buildCaptured(formatPrimitive(inner.value));
967
+ if (!Number.isInteger(value) || value <= 0) {
968
+ throw new CfInspectorError(
969
+ "INVALID_ARGUMENT",
970
+ `Invalid maxValueLength: ${value.toString()} \u2014 expected a positive integer`
971
+ );
1100
972
  }
1101
- return buildCaptured("undefined");
973
+ return value;
1102
974
  }
1103
- function objectIdFromEvalResult(result) {
1104
- const inner = result.result;
1105
- if (inner?.type !== "object") {
1106
- return void 0;
1107
- }
1108
- const objectId = inner.objectId;
1109
- if (typeof objectId !== "string" || objectId.length === 0) {
1110
- return void 0;
975
+ function limitValueLength(raw, maxValueLength = DEFAULT_MAX_VALUE_LENGTH) {
976
+ if (raw.length <= maxValueLength) {
977
+ return raw;
1111
978
  }
1112
- return objectId;
979
+ return `${raw.slice(0, maxValueLength)}...`;
1113
980
  }
1114
981
  function parseQuotedString(value) {
1115
982
  try {
@@ -1177,6 +1044,134 @@ function toStructuredValue(variable) {
1177
1044
  }
1178
1045
  return out;
1179
1046
  }
1047
+
1048
+ // src/snapshot/evaluation.ts
1049
+ function evalResultToCaptured(expression, result, maxValueLength = DEFAULT_MAX_VALUE_LENGTH) {
1050
+ if (result.exceptionDetails !== void 0) {
1051
+ return { expression, error: readEvalError(result, maxValueLength) };
1052
+ }
1053
+ const inner = result.result;
1054
+ if (!inner) {
1055
+ return { expression, error: "no result returned" };
1056
+ }
1057
+ const type = typeof inner.type === "string" ? inner.type : void 0;
1058
+ const buildCaptured = (rendered) => {
1059
+ const sanitized = limitValueLength(rendered, maxValueLength);
1060
+ const base = { expression, value: sanitized };
1061
+ return type === void 0 ? base : { ...base, type };
1062
+ };
1063
+ if (type === "string" && typeof inner.value === "string") {
1064
+ return buildCaptured(JSON.stringify(inner.value));
1065
+ }
1066
+ if ((type === "number" || type === "boolean" || type === "bigint") && isPrimitive(inner.value)) {
1067
+ return buildCaptured(formatPrimitive(inner.value));
1068
+ }
1069
+ if (typeof inner.description === "string") {
1070
+ return buildCaptured(inner.description);
1071
+ }
1072
+ if (isPrimitive(inner.value)) {
1073
+ return buildCaptured(formatPrimitive(inner.value));
1074
+ }
1075
+ return buildCaptured("undefined");
1076
+ }
1077
+ function readEvalError(result, maxValueLength) {
1078
+ const text = typeof result.exceptionDetails?.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails?.text === "string" ? result.exceptionDetails.text : "evaluation failed";
1079
+ return limitValueLength(text, maxValueLength);
1080
+ }
1081
+
1082
+ // src/snapshot/properties.ts
1083
+ var MAX_SCOPE_VARIABLES = 20;
1084
+ var MAX_CHILD_VARIABLES = 8;
1085
+ var MAX_VARIABLE_DEPTH = 2;
1086
+ function buildDescribed(value, type, objectId) {
1087
+ const base = { value };
1088
+ if (type !== void 0) {
1089
+ base.type = type;
1090
+ }
1091
+ if (objectId !== void 0) {
1092
+ base.objectId = objectId;
1093
+ }
1094
+ return base;
1095
+ }
1096
+ function describeProperty(prop) {
1097
+ const value = prop.value;
1098
+ if (value === void 0) {
1099
+ return { value: "undefined" };
1100
+ }
1101
+ const type = typeof value.type === "string" ? value.type : void 0;
1102
+ const objectId = typeof value.objectId === "string" ? value.objectId : void 0;
1103
+ if (type === "undefined") {
1104
+ return buildDescribed("undefined", type);
1105
+ }
1106
+ if (type === "string" && typeof value.value === "string") {
1107
+ return buildDescribed(JSON.stringify(value.value), type);
1108
+ }
1109
+ if ((type === "number" || type === "boolean" || type === "bigint" || type === "symbol") && isPrimitive(value.value)) {
1110
+ return buildDescribed(formatPrimitive(value.value), type);
1111
+ }
1112
+ if (typeof value.description === "string") {
1113
+ return buildDescribed(value.description, type, objectId);
1114
+ }
1115
+ if (isPrimitive(value.value)) {
1116
+ return buildDescribed(formatPrimitive(value.value), type);
1117
+ }
1118
+ if (objectId === void 0) {
1119
+ return buildDescribed("undefined", type);
1120
+ }
1121
+ return buildDescribed("[object]", type, objectId);
1122
+ }
1123
+ function isExpandable(type) {
1124
+ return type === "object" || type === "function";
1125
+ }
1126
+ async function captureProperties(session, objectId, limit, depth, maxValueLength) {
1127
+ const properties = await getProperties(session, objectId);
1128
+ const limited = properties.slice(0, limit);
1129
+ const variables = await Promise.all(
1130
+ limited.map(async (prop) => {
1131
+ return await captureProperty(session, prop, depth, maxValueLength);
1132
+ })
1133
+ );
1134
+ return variables;
1135
+ }
1136
+ async function captureProperty(session, prop, depth, maxValueLength) {
1137
+ const name = typeof prop.name === "string" ? prop.name : "?";
1138
+ const described = describeProperty(prop);
1139
+ const children = await capturePropertyChildren(session, described, depth, maxValueLength);
1140
+ const sanitizedValue = limitValueLength(described.value, maxValueLength);
1141
+ const base = { name, value: sanitizedValue };
1142
+ const withType = described.type === void 0 ? base : { ...base, type: described.type };
1143
+ return children === void 0 ? withType : { ...withType, children };
1144
+ }
1145
+ async function capturePropertyChildren(session, described, depth, maxValueLength) {
1146
+ if (depth <= 0 || described.objectId === void 0 || !isExpandable(described.type)) {
1147
+ return void 0;
1148
+ }
1149
+ try {
1150
+ const nested = await captureProperties(
1151
+ session,
1152
+ described.objectId,
1153
+ MAX_CHILD_VARIABLES,
1154
+ depth - 1,
1155
+ maxValueLength
1156
+ );
1157
+ return nested.length > 0 ? nested : void 0;
1158
+ } catch {
1159
+ return void 0;
1160
+ }
1161
+ }
1162
+
1163
+ // src/snapshot/objects.ts
1164
+ function objectIdFromEvalResult(result) {
1165
+ const inner = result.result;
1166
+ if (inner?.type !== "object") {
1167
+ return void 0;
1168
+ }
1169
+ const objectId = inner.objectId;
1170
+ if (typeof objectId !== "string" || objectId.length === 0) {
1171
+ return void 0;
1172
+ }
1173
+ return objectId;
1174
+ }
1180
1175
  async function renderObjectCapture(session, objectId, maxValueLength) {
1181
1176
  try {
1182
1177
  const properties = await captureProperties(
@@ -1223,6 +1218,51 @@ async function withSerializedObjectCapture(session, expression, evalResult, capt
1223
1218
  const value = limitValueLength(normalized, maxValueLength);
1224
1219
  return captured.type === void 0 ? { expression, value } : { expression, value, type: captured.type };
1225
1220
  }
1221
+
1222
+ // src/snapshot/scopes.ts
1223
+ var MAX_SCOPES = 3;
1224
+ var PRIORITY_BY_TYPE = {
1225
+ local: 0,
1226
+ arguments: 1,
1227
+ block: 2,
1228
+ closure: 3,
1229
+ catch: 4,
1230
+ with: 5,
1231
+ module: 6,
1232
+ script: 7
1233
+ };
1234
+ function selectScopes(scopeChain) {
1235
+ const eligible = scopeChain.filter((scope) => scope.objectId !== void 0 && scope.type !== "global");
1236
+ return [...eligible].sort((a, b) => priorityOf(a.type) - priorityOf(b.type)).slice(0, MAX_SCOPES);
1237
+ }
1238
+ function priorityOf(type) {
1239
+ return PRIORITY_BY_TYPE[type] ?? Number.MAX_SAFE_INTEGER;
1240
+ }
1241
+ async function captureScopes(session, frame, maxValueLength) {
1242
+ const scopes = selectScopes(frame.scopeChain);
1243
+ return await Promise.all(
1244
+ scopes.map(async (scope) => {
1245
+ const objectId = scope.objectId;
1246
+ if (objectId === void 0) {
1247
+ return { type: scope.type, variables: [] };
1248
+ }
1249
+ try {
1250
+ const variables = await captureProperties(
1251
+ session,
1252
+ objectId,
1253
+ MAX_SCOPE_VARIABLES,
1254
+ MAX_VARIABLE_DEPTH,
1255
+ maxValueLength
1256
+ );
1257
+ return { type: scope.type, variables };
1258
+ } catch {
1259
+ return { type: scope.type, variables: [] };
1260
+ }
1261
+ })
1262
+ );
1263
+ }
1264
+
1265
+ // src/snapshot/capture.ts
1226
1266
  async function captureSnapshot(session, pause, options = {}) {
1227
1267
  const maxValueLength = resolveMaxValueLength(options.maxValueLength);
1228
1268
  const top = pause.callFrames[0];
@@ -1239,26 +1279,7 @@ async function captureSnapshot(session, pause, options = {}) {
1239
1279
  const scopes = await captureScopes(session, top, maxValueLength);
1240
1280
  topFrame = { ...topFrame, scopes };
1241
1281
  }
1242
- if (options.captures !== void 0 && options.captures.length > 0) {
1243
- captures = await Promise.all(
1244
- options.captures.map(async (expression) => {
1245
- try {
1246
- const result = await evaluateOnFrame(session, top.callFrameId, expression);
1247
- const captured = evalResultToCaptured(expression, result, maxValueLength);
1248
- return await withSerializedObjectCapture(
1249
- session,
1250
- expression,
1251
- result,
1252
- captured,
1253
- maxValueLength
1254
- );
1255
- } catch (err) {
1256
- const message = err instanceof Error ? err.message : String(err);
1257
- return { expression, error: limitValueLength(message, maxValueLength) };
1258
- }
1259
- })
1260
- );
1261
- }
1282
+ captures = await captureExpressions(session, top.callFrameId, options.captures, maxValueLength);
1262
1283
  }
1263
1284
  return {
1264
1285
  reason: pause.reason,
@@ -1268,8 +1289,28 @@ async function captureSnapshot(session, pause, options = {}) {
1268
1289
  captures
1269
1290
  };
1270
1291
  }
1292
+ async function captureExpressions(session, callFrameId, captures, maxValueLength) {
1293
+ if (captures === void 0 || captures.length === 0) {
1294
+ return [];
1295
+ }
1296
+ return await Promise.all(
1297
+ captures.map(async (expression) => {
1298
+ return await captureExpression(session, callFrameId, expression, maxValueLength);
1299
+ })
1300
+ );
1301
+ }
1302
+ async function captureExpression(session, callFrameId, expression, maxValueLength) {
1303
+ try {
1304
+ const result = await evaluateOnFrame(session, callFrameId, expression);
1305
+ const captured = evalResultToCaptured(expression, result, maxValueLength);
1306
+ return await withSerializedObjectCapture(session, expression, result, captured, maxValueLength);
1307
+ } catch (err) {
1308
+ const message = err instanceof Error ? err.message : String(err);
1309
+ return { expression, error: limitValueLength(message, maxValueLength) };
1310
+ }
1311
+ }
1271
1312
 
1272
- // src/logpoint.ts
1313
+ // src/logpoint/condition.ts
1273
1314
  import { randomBytes } from "crypto";
1274
1315
  var SENTINEL_PREFIX = "__CFI_LOG_";
1275
1316
  var SENTINEL_SUFFIX = "__";
@@ -1288,6 +1329,11 @@ function buildLogpointCondition(sentinel, expression) {
1288
1329
  "})()"
1289
1330
  ].join("");
1290
1331
  }
1332
+ function generateSentinel() {
1333
+ return `${SENTINEL_PREFIX}${randomBytes(8).toString("hex")}${SENTINEL_SUFFIX}`;
1334
+ }
1335
+
1336
+ // src/logpoint/events.ts
1291
1337
  function asString2(value) {
1292
1338
  return typeof value === "string" ? value : void 0;
1293
1339
  }
@@ -1320,6 +1366,9 @@ function parseLogEvent(rawArgs, sentinel, location, timestamp) {
1320
1366
  if (payload.startsWith("!err:")) {
1321
1367
  return { ts, at, error: payload.slice("!err:".length) };
1322
1368
  }
1369
+ return parsePayload(ts, at, payload);
1370
+ }
1371
+ function parsePayload(ts, at, payload) {
1323
1372
  try {
1324
1373
  const parsed = JSON.parse(payload);
1325
1374
  if (typeof parsed === "string") {
@@ -1330,20 +1379,14 @@ function parseLogEvent(rawArgs, sentinel, location, timestamp) {
1330
1379
  return { ts, at, value: payload, raw: payload };
1331
1380
  }
1332
1381
  }
1333
- function generateSentinel() {
1334
- return `${SENTINEL_PREFIX}${randomBytes(8).toString("hex")}${SENTINEL_SUFFIX}`;
1335
- }
1382
+
1383
+ // src/logpoint/stream.ts
1336
1384
  async function streamLogpoint(session, options) {
1337
1385
  const sentinel = generateSentinel();
1338
1386
  const condition = buildLogpointCondition(sentinel, options.expression);
1339
1387
  let emitted = 0;
1340
1388
  const offEvent = session.client.on("Runtime.consoleAPICalled", (raw) => {
1341
- const params = raw;
1342
- if (asString2(params.type) !== "log") {
1343
- return;
1344
- }
1345
- const ts = typeof params.timestamp === "number" ? params.timestamp : void 0;
1346
- const event = parseLogEvent(params.args, sentinel, options.location, ts);
1389
+ const event = toLogpointEvent(raw, sentinel, options.location);
1347
1390
  if (event === void 0) {
1348
1391
  return;
1349
1392
  }
@@ -1363,18 +1406,26 @@ async function streamLogpoint(session, options) {
1363
1406
  throw err;
1364
1407
  }
1365
1408
  options.onBreakpointSet?.(handle);
1366
- const cleanup = async () => {
1367
- offEvent();
1368
- try {
1369
- await removeBreakpoint(session, handle.breakpointId);
1370
- } catch {
1371
- }
1372
- };
1373
1409
  try {
1374
1410
  const reason = await waitForStop(session, options);
1375
1411
  return { handle, sentinel, emitted, stoppedReason: reason };
1376
1412
  } finally {
1377
- await cleanup();
1413
+ offEvent();
1414
+ await removeBreakpointBestEffort(session, handle.breakpointId);
1415
+ }
1416
+ }
1417
+ function toLogpointEvent(raw, sentinel, location) {
1418
+ const params = raw;
1419
+ if (asString2(params.type) !== "log") {
1420
+ return void 0;
1421
+ }
1422
+ const ts = typeof params.timestamp === "number" ? params.timestamp : void 0;
1423
+ return parseLogEvent(params.args, sentinel, location, ts);
1424
+ }
1425
+ async function removeBreakpointBestEffort(session, breakpointId) {
1426
+ try {
1427
+ await removeBreakpoint(session, breakpointId);
1428
+ } catch {
1378
1429
  }
1379
1430
  }
1380
1431
  async function waitForStop(session, options) {
@@ -1411,7 +1462,7 @@ async function waitForStop(session, options) {
1411
1462
  });
1412
1463
  }
1413
1464
 
1414
- // src/tunnel.ts
1465
+ // src/cf/tunnel.ts
1415
1466
  import { startDebugger } from "@saptools/cf-debugger";
1416
1467
  async function openCfTunnel(target) {
1417
1468
  const opts = {