@tlockney/cdp-kernel 0.5.0 → 0.7.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.
Files changed (74) hide show
  1. package/README.md +27 -6
  2. package/dist/browser/channels.d.ts +27 -0
  3. package/dist/browser/channels.js +90 -0
  4. package/dist/browser/kernel.js +29674 -0
  5. package/dist/browser/kernel.js.map +7 -0
  6. package/dist/browser/mod.d.ts +20 -0
  7. package/dist/browser/mod.js +38 -0
  8. package/dist/browser/target.d.ts +71 -0
  9. package/dist/browser/target.js +206 -0
  10. package/dist/browser/worker.js +1000 -0
  11. package/dist/browser/worker.js.map +7 -0
  12. package/dist/cdp.d.ts +11 -0
  13. package/dist/cdp.js +6 -1
  14. package/dist/celld-target.js +9 -12
  15. package/dist/children.d.ts +25 -3
  16. package/dist/children.js +84 -2
  17. package/dist/debugger.d.ts +2 -0
  18. package/dist/debugger.js +12 -6
  19. package/dist/deno-types.d.ts +30 -0
  20. package/dist/deno-types.js +93 -0
  21. package/dist/kernel-core.d.ts +69 -0
  22. package/dist/kernel-core.js +754 -0
  23. package/dist/kernel.d.ts +12 -1
  24. package/dist/kernel.js +132 -605
  25. package/dist/language-service-worker.d.ts +1 -0
  26. package/dist/language-service-worker.js +88 -0
  27. package/dist/language-service.d.ts +64 -0
  28. package/dist/language-service.js +128 -0
  29. package/dist/main.js +102 -8
  30. package/dist/message.d.ts +34 -0
  31. package/dist/message.js +14 -0
  32. package/dist/mod.d.ts +1 -0
  33. package/dist/mod.js +1 -0
  34. package/dist/notebook-types.d.ts +62 -0
  35. package/dist/notebook-types.js +310 -0
  36. package/dist/package.d.ts +2 -2
  37. package/dist/package.js +2 -2
  38. package/dist/parent.d.ts +12 -0
  39. package/dist/parent.js +39 -0
  40. package/dist/port-holder.d.ts +13 -0
  41. package/dist/port-holder.js +105 -0
  42. package/dist/sourcemap.d.ts +7 -2
  43. package/dist/sourcemap.js +9 -3
  44. package/dist/strip-esbuild.d.ts +2 -0
  45. package/dist/strip-esbuild.js +44 -0
  46. package/dist/strip-sucrase.d.ts +2 -0
  47. package/dist/strip-sucrase.js +78 -0
  48. package/dist/target-api.d.ts +80 -0
  49. package/dist/target-api.js +22 -0
  50. package/dist/target.d.ts +26 -74
  51. package/dist/target.js +232 -58
  52. package/dist/traceback.d.ts +75 -0
  53. package/dist/traceback.js +200 -0
  54. package/dist/transform.d.ts +22 -1
  55. package/dist/transform.js +46 -43
  56. package/dist/wire.d.ts +4 -24
  57. package/dist/wire.js +3 -12
  58. package/dist/workerd-target.js +1 -1
  59. package/dist/workers-project.js +13 -2
  60. package/dist/zmq-channels.d.ts +14 -0
  61. package/dist/zmq-channels.js +59 -0
  62. package/dist/zmtp.d.ts +2 -0
  63. package/dist/zmtp.js +12 -0
  64. package/package.json +15 -5
  65. package/runtime/bootstrap.mjs +113 -33
  66. package/runtime/browser-worker.mjs +119 -0
  67. package/runtime/cdn.d.mts +2 -0
  68. package/runtime/cdn.mjs +17 -0
  69. package/runtime/display.mjs +202 -1
  70. package/runtime/format.d.mts +2 -0
  71. package/runtime/format.mjs +125 -0
  72. package/runtime/introspect.mjs +1 -1
  73. package/runtime/node-hooks.d.mts +6 -0
  74. package/runtime/node-hooks.mjs +11 -0
package/dist/target.js CHANGED
@@ -7,11 +7,20 @@ import { spawn } from "node:child_process";
7
7
  import { createRequire } from "node:module";
8
8
  import { join } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
- import { asEvaluateResult, CdpAbandoned, CdpSession } from "./cdp.js";
10
+ import { asEvaluateResult, CdpAbandoned, CdpError, CdpSession } from "./cdp.js";
11
11
  import { SHIM, transformCell } from "./transform.js";
12
12
  import { hostRuntime, isInsideNodeModules, runtimeDir } from "./package.js";
13
13
  import { killOnExit } from "./children.js";
14
- const BINDING = "__cdpk_send";
14
+ import { Interrupted, isTargetMessage, parseJson } from "./target-api.js";
15
+ export { Interrupted, isTargetMessage, parseJson };
16
+ /**
17
+ * The inspector keeps every object it returns a remote reference to alive
18
+ * until it is released, so a cell's result would never be collected (#286).
19
+ * Cells and lookups put theirs in these groups, which are released once the
20
+ * kernel is done with them.
21
+ */
22
+ const CELL_GROUP = "cdpk-cell";
23
+ const LOOKUP_GROUP = "cdpk-lookup";
15
24
  /** How long a target may take from spawn to a usable session. */
16
25
  const START_TIMEOUT_MS = 60_000;
17
26
  const MARKER_RE = /\x1b\[cdpk:(\d+)(m?)\]\x1b/;
@@ -27,7 +36,9 @@ const INSPECTOR_CHATTER = [
27
36
  "Visit chrome://inspect",
28
37
  "Waiting for the debugger to disconnect",
29
38
  ];
30
- const DENO_CHATTER = [...INSPECTOR_CHATTER, "Download http://", "Download https://"];
39
+ // Deno prints "Debugger session started." for each session the host opens
40
+ // (#281).
41
+ const DENO_CHATTER = [...INSPECTOR_CHATTER, "Debugger session started.", "Download http://", "Download https://"];
31
42
  const ANSI_SGR = /\x1b\[[0-9;]*m/g;
32
43
  /** A colour code cut off at the end of a chunk. */
33
44
  const PARTIAL_SGR = /\x1b(?:\[[0-9;]*)?$/;
@@ -75,23 +86,11 @@ export function esbuildBinary(resolve = import.meta.resolve, platform = process.
75
86
  return null;
76
87
  }
77
88
  }
78
- /** The parsed value, or undefined for text that isn't JSON. */
79
- export function parseJson(text) {
80
- try {
81
- return JSON.parse(text);
82
- }
83
- catch {
84
- return undefined;
85
- }
86
- }
87
- export function isTargetMessage(value) {
88
- if (typeof value !== "object" || value === null)
89
- return false;
90
- const kind = value.kind;
91
- return (kind === "iopub" || kind === "input_request") &&
92
- typeof value.barrier === "number";
93
- }
94
89
  const MARKER_PREFIX = "\x1b[cdpk:";
90
+ // A message record: the message's JSON, which never holds a raw ESC, between
91
+ // "\x1b[cdpk:<" and "\x1b]" (#271).
92
+ const RECORD_PREFIX = "\x1b[cdpk:<";
93
+ const RECORD_RE = /\x1b\[cdpk:<([^\x1b]*)\x1b\]/;
95
94
  function isPartialMarker(tail) {
96
95
  if (tail.length >= MAX_MARKER_LEN)
97
96
  return false;
@@ -99,10 +98,13 @@ function isPartialMarker(tail) {
99
98
  }
100
99
  /**
101
100
  * Splits one output pipe into text and barrier markers, and releases them
102
- * in order. A gated marker (one that announces a binding message) pauses
101
+ * in order. A gated marker (one that announces a message) pauses
103
102
  * the pipe until `gate(n)` resolves, i.e. until that message has been
104
103
  * delivered, so later output can't overtake it. A possible partial marker
105
104
  * at the end of a chunk is held back until the next chunk decides it.
105
+ *
106
+ * With `onMessage`, the pipe also carries message records. A record stands
107
+ * where its gated marker would, and pauses the pipe the same way.
106
108
  */
107
109
  export class PipeDemux {
108
110
  #buffer = "";
@@ -111,28 +113,49 @@ export class PipeDemux {
111
113
  #closed = false;
112
114
  #reached = 0;
113
115
  #waiters = [];
116
+ /** The gated marker the pipe is paused on, if any. */
117
+ #gatedOn = null;
114
118
  onText;
115
119
  gate;
116
- constructor(onText, gate = () => Promise.resolve()) {
120
+ onMessage;
121
+ constructor(onText, gate = () => Promise.resolve(), onMessage = null) {
117
122
  this.onText = onText;
118
123
  this.gate = gate;
124
+ this.onMessage = onMessage;
119
125
  }
120
126
  push(chunk) {
121
127
  this.#buffer += chunk;
122
128
  for (;;) {
123
129
  const m = MARKER_RE.exec(this.#buffer);
124
- if (!m)
130
+ const r = this.onMessage ? RECORD_RE.exec(this.#buffer) : null;
131
+ const first = m && r ? (m.index < r.index ? m : r) : m ?? r;
132
+ if (!first)
125
133
  break;
126
- if (m.index > 0)
127
- this.#items.push({ text: this.#buffer.slice(0, m.index) });
128
- this.#items.push({ barrier: Number(m[1]), gated: m[2] === "m" });
129
- this.#buffer = this.#buffer.slice(m.index + m[0].length);
134
+ if (first.index > 0)
135
+ this.#items.push({ text: this.#buffer.slice(0, first.index) });
136
+ if (first === m) {
137
+ this.#items.push({ barrier: Number(m[1]), gated: m[2] === "m" });
138
+ }
139
+ else {
140
+ // Anything else a cell prints in this shape is dropped (#122).
141
+ const msg = parseJson(first[1]);
142
+ if (isTargetMessage(msg))
143
+ this.#items.push({ message: msg });
144
+ }
145
+ this.#buffer = this.#buffer.slice(first.index + first[0].length);
130
146
  }
131
147
  // Hold back a tail that could still grow into a marker, including one
132
- // cut inside the "\x1b[cdpk:" prefix itself.
148
+ // cut inside the "\x1b[cdpk:" prefix itself, or into a record: one
149
+ // whose only ESC after its prefix is a final one.
133
150
  const esc = this.#buffer.lastIndexOf("\x1b");
134
151
  const tail = esc === -1 ? "" : this.#buffer.slice(esc);
135
- const holdFrom = esc !== -1 && isPartialMarker(tail) ? esc : this.#buffer.length;
152
+ let holdFrom = esc !== -1 && isPartialMarker(tail) ? esc : this.#buffer.length;
153
+ const record = this.onMessage ? this.#buffer.lastIndexOf(RECORD_PREFIX) : -1;
154
+ if (record !== -1) {
155
+ const next = this.#buffer.indexOf("\x1b", record + RECORD_PREFIX.length);
156
+ if (next === -1 || next === this.#buffer.length - 1)
157
+ holdFrom = Math.min(holdFrom, record);
158
+ }
136
159
  if (holdFrom > 0) {
137
160
  this.#items.push({ text: this.#buffer.slice(0, holdFrom) });
138
161
  this.#buffer = this.#buffer.slice(holdFrom);
@@ -149,9 +172,22 @@ export class PipeDemux {
149
172
  this.onText(item.text);
150
173
  continue;
151
174
  }
175
+ if ("message" in item) {
176
+ this.#reach(item.message.barrier);
177
+ this.onMessage?.(item.message);
178
+ if (!this.#closed) {
179
+ this.#gatedOn = item.message.barrier;
180
+ await this.gate(item.message.barrier);
181
+ this.#gatedOn = null;
182
+ }
183
+ continue;
184
+ }
152
185
  this.#reach(item.barrier);
153
- if (item.gated && !this.#closed)
186
+ if (item.gated && !this.#closed) {
187
+ this.#gatedOn = item.barrier;
154
188
  await this.gate(item.barrier);
189
+ this.#gatedOn = null;
190
+ }
155
191
  }
156
192
  }
157
193
  finally {
@@ -172,6 +208,16 @@ export class PipeDemux {
172
208
  return Promise.resolve();
173
209
  return new Promise((resolve) => this.#waiters.push({ n, resolve }));
174
210
  }
211
+ /** Where the pipe stands, for a state dump (#237). */
212
+ state() {
213
+ return {
214
+ reached: this.#reached,
215
+ waitingFor: this.#waiters.map((w) => w.n),
216
+ gatedOn: this.#gatedOn,
217
+ queued: this.#items.length,
218
+ held: this.#buffer.length,
219
+ };
220
+ }
175
221
  /** Release everything (the process died). */
176
222
  close() {
177
223
  this.#closed = true;
@@ -267,9 +313,6 @@ export class Gates {
267
313
  this.#map.clear();
268
314
  }
269
315
  }
270
- export class Interrupted extends Error {
271
- name = "Interrupted";
272
- }
273
316
  /** A catch handler: a call interrupt() abandoned becomes Interrupted. */
274
317
  function interrupted(what) {
275
318
  return (err) => {
@@ -287,8 +330,13 @@ export class Target {
287
330
  #gates;
288
331
  #contextId;
289
332
  #messages = Promise.resolve();
333
+ /** Messages received but not yet delivered. */
334
+ #undelivered = 0;
290
335
  #interrupted = false;
291
336
  #paused = false;
337
+ /** What the debugger asked to pause on (#307), and how many kernel calls hold it off. */
338
+ #pauseOnExceptions = "none";
339
+ #unpausable = 0;
292
340
  /** Set once the main context is gone: the runtime is exiting (#130). */
293
341
  #exiting = false;
294
342
  #exited;
@@ -332,6 +380,9 @@ export class Target {
332
380
  delete env.NO_COLOR;
333
381
  const child = spawn(cmd, args, { cwd: options.cwd, stdio: ["pipe", "pipe", "pipe"], env });
334
382
  killOnExit(child);
383
+ // Prompt replies and comm messages go to stdin; one written as the
384
+ // runtime exits fails with EPIPE, and the exit is handled on its own.
385
+ child.stdin.on("error", () => { });
335
386
  // A start that fails or stalls must not leave the runtime running: its
336
387
  // pipes and inspector sockets would keep this process alive (#52).
337
388
  const sessions = [];
@@ -357,7 +408,12 @@ export class Target {
357
408
  child.stdout.setEncoding("utf8");
358
409
  child.stderr.setEncoding("utf8");
359
410
  const gates = new Gates();
360
- const stdout = new PipeDemux((text) => events.stream("stdout", text), (n) => gates.wait(n));
411
+ // Messages come only after attach(), so the target exists by then.
412
+ let target;
413
+ const stdout = new PipeDemux((text) => events.stream("stdout", text), (n) => gates.wait(n), (msg) => {
414
+ if (target)
415
+ target.#deliver(msg, events);
416
+ });
361
417
  const stderr = new PipeDemux((text) => events.stream("stderr", text), (n) => gates.wait(n));
362
418
  const filter = new StderrFilter(runtime, (text) => stderr.push(text));
363
419
  const pushStderr = (chunk) => filter.push(chunk);
@@ -398,25 +454,22 @@ export class Target {
398
454
  });
399
455
  });
400
456
  await shell.send("Runtime.enable");
401
- await shell.send("Runtime.addBinding", { name: BINDING });
402
457
  const contextId = await contextReady;
403
- const target = new Target(runtime, child, shell, control, stdout, stderr, gates, contextId, { runtime: "", engine: "" });
458
+ // The inspector socket leaves Nagle's algorithm on, so an event written
459
+ // just before an evaluate's reply holds that reply until the event is
460
+ // acknowledged, ~40 ms on Linux (#271). So the shell session, which runs
461
+ // the cells, takes no Runtime events (console calls among them), and
462
+ // messages come over stdout. The control session watches for exit.
463
+ await shell.send("Runtime.disable");
464
+ await control.send("Runtime.enable");
465
+ target = new Target(runtime, child, shell, control, stdout, stderr, gates, contextId, { runtime: "", engine: "" });
404
466
  // process.exit() or Deno.exit() destroys the context, then the runtime
405
467
  // waits for its inspector clients to disconnect before it exits. Let go,
406
468
  // so it exits and the kernel starts a fresh one (#130).
407
- shell.on("Runtime.executionContextDestroyed", (params) => {
408
- if (params.executionContextId === contextId)
469
+ control.on("Runtime.executionContextDestroyed", (params) => {
470
+ if (params.executionContextId === contextId && target)
409
471
  target.#onExit();
410
472
  });
411
- shell.on("Runtime.bindingCalled", (params) => {
412
- if (params.name !== BINDING || typeof params.payload !== "string")
413
- return;
414
- // __cdpk_send is an ordinary global, so a cell can call it with
415
- // anything; that mustn't take the host down (#122).
416
- const msg = parseJson(params.payload);
417
- if (isTargetMessage(msg))
418
- target.#deliver(msg, events);
419
- });
420
473
  // Wait for the bootstrap's top-level await to finish installing the
421
474
  // shim. There is no cap here: launch's start timeout bounds the wait and
422
475
  // kills the child, so a slow bootstrap never yields a target without
@@ -427,6 +480,7 @@ export class Target {
427
480
  break;
428
481
  await new Promise((r) => setTimeout(r, 25));
429
482
  }
483
+ await target.#evaluateSync(`${SHIM}.attach()`);
430
484
  const versions = await target.#evaluateSync(`JSON.stringify(typeof Deno !== "undefined" ? { runtime: Deno.version.deno, engine: "V8 " + Deno.version.v8 } : { runtime: process.versions.node, engine: "V8 " + process.versions.v8 })`);
431
485
  Object.assign(target.versions, JSON.parse(String(versions.result.value)));
432
486
  // The runtime's own enumerable globals, before any cell: the debugger's
@@ -443,21 +497,102 @@ export class Target {
443
497
  /** The Debugger domain on this target's session, for src/debugger.ts. */
444
498
  get debug() {
445
499
  return {
446
- send: (method, params = {}) => this.#shell.send(method, params),
500
+ send: (method, params = {}) => {
501
+ if (method === "Debugger.setPauseOnExceptions") {
502
+ this.#pauseOnExceptions = String(params.state ?? "none");
503
+ // Applied when the kernel's own call in flight finishes.
504
+ if (this.#unpausable > 0)
505
+ return Promise.resolve({});
506
+ }
507
+ if (method === "Debugger.disable")
508
+ this.#pauseOnExceptions = "none";
509
+ return this.#shell.send(method, params);
510
+ },
447
511
  on: (method, listener) => this.#shell.on(method, listener),
448
512
  contextId: this.#contextId,
449
513
  baselineGlobals: this.#baselineGlobals,
514
+ withoutPausing: (call) => this.#withoutPausing(call),
450
515
  };
451
516
  }
452
- // Binding messages wait until both pipes have caught up to their marker,
517
+ /**
518
+ * Runs the kernel's own inspector call with pausing on exceptions off. A
519
+ * pause inside a call the inspector dispatches holds that call (Node), or
520
+ * resumes by itself and leaves Deno's event loop stopped for good, so no
521
+ * later cell runs (#307). A cell that throws meanwhile doesn't pause.
522
+ */
523
+ async #withoutPausing(call) {
524
+ if (this.#pauseOnExceptions === "none" || this.#paused)
525
+ return call();
526
+ if (this.#unpausable++ === 0)
527
+ await this.#shell.send("Debugger.setPauseOnExceptions", { state: "none" });
528
+ try {
529
+ return await call();
530
+ }
531
+ finally {
532
+ if (--this.#unpausable === 0 && !this.#exiting) {
533
+ await this.#shell.send("Debugger.setPauseOnExceptions", { state: this.#pauseOnExceptions }).catch(() => { });
534
+ }
535
+ }
536
+ }
537
+ // Messages wait until both pipes have caught up to their marker,
453
538
  // and are delivered strictly in order.
454
539
  #deliver(msg, events) {
540
+ this.#undelivered++;
455
541
  this.#messages = this.#messages.then(async () => {
456
542
  await Promise.all([this.#stdout.waitFor(msg.barrier), this.#stderr.waitFor(msg.barrier)]);
543
+ this.#undelivered--;
457
544
  events.message(msg);
458
545
  this.#gates.open(msg.barrier);
459
546
  });
460
547
  }
548
+ /**
549
+ * What the host is waiting on, whether the runtime still evaluates, runs
550
+ * immediates and runs timers, and where its JavaScript is if it is
551
+ * running any, for a kernel that has stopped answering (#237). The probes
552
+ * go over the control session, which no cell holds.
553
+ */
554
+ async diagnose() {
555
+ const within = (promise, fallback) => Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve(fallback), 2000).unref())]);
556
+ const probe = (expression, awaitPromise) => within(this.#control.send("Runtime.evaluate", { expression, contextId: this.#contextId, awaitPromise, returnByValue: true })
557
+ .then((res) => res.exceptionDetails ? `threw: ${JSON.stringify(res.exceptionDetails)}` : String(res.result?.value), (err) => `failed: ${err}`), "no answer in 2 s");
558
+ const [evaluates, immediates, timers] = await Promise.all([
559
+ probe("'yes'", false),
560
+ probe("new Promise((resolve) => setImmediate(() => resolve('yes')))", true),
561
+ probe("new Promise((resolve) => setTimeout(() => resolve('yes'), 0))", true),
562
+ ]);
563
+ // A runtime whose event loop is stuck may be stuck in JavaScript: say
564
+ // where. (An idle one would take the pause in whatever runs next.)
565
+ let running = "event loop runs";
566
+ if (immediates !== "yes") {
567
+ try {
568
+ const paused = new Promise((resolve) => this.#control.on("Debugger.paused", resolve));
569
+ await this.#control.send("Debugger.enable");
570
+ await this.#control.send("Debugger.pause");
571
+ const event = await within(paused, null);
572
+ running = event === null ? "no" : event.callFrames
573
+ .slice(0, 10)
574
+ .map((f) => `${f.functionName || "(anonymous)"} ${f.url || "(no url)"}:${f.location.lineNumber + 1}`);
575
+ await this.#control.send("Debugger.resume").catch(() => { });
576
+ await this.#control.send("Debugger.disable");
577
+ }
578
+ catch (err) {
579
+ running = `failed: ${err}`;
580
+ }
581
+ }
582
+ return {
583
+ pid: this.#child.pid ?? null,
584
+ calls: { shell: this.#shell.pendingCalls(), control: this.#control.pendingCalls() },
585
+ stdout: this.#stdout.state(),
586
+ stderr: this.#stderr.state(),
587
+ undelivered: this.#undelivered,
588
+ paused: this.#paused,
589
+ exiting: this.#exiting,
590
+ evaluates,
591
+ immediates,
592
+ timers,
593
+ running,
594
+ };
595
+ }
461
596
  /** Resolves once all output and messages sent so far have been delivered. */
462
597
  async flush() {
463
598
  const n = await this.#evaluateSync(`${SHIM}.barrier()`).catch(interrupted("Execution"));
@@ -479,7 +614,13 @@ export class Target {
479
614
  }
480
615
  #evaluateRaw(expression, session = this.#shell) {
481
616
  return session
482
- .send("Runtime.evaluate", { expression, contextId: this.#contextId, replMode: true, awaitPromise: true })
617
+ .send("Runtime.evaluate", {
618
+ expression,
619
+ contextId: this.#contextId,
620
+ replMode: true,
621
+ awaitPromise: true,
622
+ objectGroup: CELL_GROUP,
623
+ })
483
624
  .then(asEvaluateResult);
484
625
  }
485
626
  /** Runs transformed cell code. Rejects with Interrupted after interrupt(). */
@@ -491,6 +632,10 @@ export class Target {
491
632
  catch (err) {
492
633
  if (err instanceof CdpAbandoned || this.#interrupted)
493
634
  throw new Interrupted("Execution interrupted");
635
+ // The control session's executionContextDestroyed event can arrive
636
+ // after the shell session's error reply (#271 moved it there).
637
+ if (err instanceof CdpError && err.message.includes("Execution context was destroyed"))
638
+ this.#onExit();
494
639
  if (this.#exiting) {
495
640
  // The child's exit, which drops this target in the kernel, comes
496
641
  // first, so the next cell starts a fresh one.
@@ -500,6 +645,11 @@ export class Target {
500
645
  throw err;
501
646
  }
502
647
  }
648
+ async releaseResults() {
649
+ if (this.#exiting)
650
+ return;
651
+ await this.#shell.send("Runtime.releaseObjectGroup", { objectGroup: CELL_GROUP }).catch(() => { });
652
+ }
503
653
  #onExit() {
504
654
  if (this.#exiting)
505
655
  return;
@@ -532,12 +682,12 @@ export class Target {
532
682
  * even a plain return value in a promise it holds only weakly (#55).
533
683
  */
534
684
  callFunctionOn(functionDeclaration, args) {
535
- return this.#shell.send("Runtime.callFunctionOn", {
685
+ return this.#withoutPausing(() => this.#shell.send("Runtime.callFunctionOn", {
536
686
  functionDeclaration,
537
687
  arguments: args,
538
688
  executionContextId: this.#contextId,
539
689
  returnByValue: true,
540
- });
690
+ }));
541
691
  }
542
692
  /**
543
693
  * For functions that return a promise. The target holds it until after
@@ -573,14 +723,19 @@ export class Target {
573
723
  */
574
724
  async dispatchComm(msgType, content, metadata, buffers) {
575
725
  this.#interrupted = false;
726
+ // Over stdin, not as call arguments: V8 caches an argument's source,
727
+ // so the data would stay in the target's memory (#287).
728
+ const record = Buffer.from(JSON.stringify({
729
+ msgType,
730
+ content,
731
+ metadata,
732
+ buffers: buffers.map((b) => Buffer.from(b).toString("base64")),
733
+ }));
734
+ this.#child.stdin.write(`comm ${record.length}\n`);
735
+ this.#child.stdin.write(record);
576
736
  let res;
577
737
  try {
578
- res = await this.#callAsyncFunctionOn(`function (t, c, m, b) { return globalThis.${SHIM}.commDispatch(t, c, m, b); }`, [
579
- { value: msgType },
580
- { value: content },
581
- { value: metadata },
582
- { value: buffers.map((b) => Buffer.from(b).toString("base64")) },
583
- ]);
738
+ res = await this.#callAsyncFunctionOn(`function () { return globalThis.${SHIM}.commDispatchFromStdin(); }`, []);
584
739
  }
585
740
  catch (err) {
586
741
  if (err instanceof CdpAbandoned || this.#interrupted)
@@ -611,11 +766,22 @@ export class Target {
611
766
  */
612
767
  #evaluatePure(expression) {
613
768
  return this.#shell
614
- .send("Runtime.evaluate", { expression, contextId: this.#contextId, throwOnSideEffect: true })
769
+ .send("Runtime.evaluate", { expression, contextId: this.#contextId, throwOnSideEffect: true, objectGroup: LOOKUP_GROUP })
615
770
  .then(asEvaluateResult)
616
771
  .catch(() => null);
617
772
  }
773
+ #releaseLookup() {
774
+ return this.#shell.send("Runtime.releaseObjectGroup", { objectGroup: LOOKUP_GROUP }).then(() => { }, () => { });
775
+ }
618
776
  async propertyNames(expression) {
777
+ try {
778
+ return await this.#propertyNames(expression);
779
+ }
780
+ finally {
781
+ await this.#releaseLookup();
782
+ }
783
+ }
784
+ async #propertyNames(expression) {
619
785
  const evaluated = await this.#evaluatePure(`(${expression})`);
620
786
  const objectId = evaluated?.result.objectId;
621
787
  if (!objectId || evaluated?.exceptionDetails)
@@ -625,6 +791,14 @@ export class Target {
625
791
  return (list ?? []).map((p) => p.name);
626
792
  }
627
793
  async inspect(path, detail) {
794
+ try {
795
+ return await this.#inspect(path, detail);
796
+ }
797
+ finally {
798
+ await this.#releaseLookup();
799
+ }
800
+ }
801
+ async #inspect(path, detail) {
628
802
  const found = await this.#evaluatePure(path);
629
803
  if (!found || found.exceptionDetails)
630
804
  return null;
@@ -0,0 +1,75 @@
1
+ import type { LineMap } from "./sourcemap.ts";
2
+ export declare const TRACEBACK_MIME = "application/vnd.nteract.traceback+json";
3
+ /** The //# sourceURL a cell's script runs under while not debugging. */
4
+ export declare function cellName(code: string): string;
5
+ /** A cell that ran: its code, and where the notebook knows it from. */
6
+ export interface CellRecord {
7
+ code: string;
8
+ executionCount: number | null;
9
+ cellId: string | null;
10
+ }
11
+ export interface SourceLine extends Record<string, string | number | boolean> {
12
+ lineno: number;
13
+ source: string;
14
+ }
15
+ export interface TracebackFrame extends Record<string, unknown> {
16
+ filename: string;
17
+ lineno: number;
18
+ colno?: number;
19
+ name: string;
20
+ library: boolean;
21
+ lines: SourceLine[];
22
+ execution_count?: number;
23
+ cell_id?: string;
24
+ }
25
+ /** A frame of CDP's exceptionDetails.stackTrace (0-based positions). */
26
+ export interface CallFrame {
27
+ url: string;
28
+ lineNumber: number;
29
+ columnNumber: number;
30
+ }
31
+ /** A frame as parsed from one line of a stack. */
32
+ export interface StackFrame {
33
+ name: string;
34
+ file: string;
35
+ line: number | null;
36
+ column: number | null;
37
+ }
38
+ /** One line of a stack as a frame, or null for a line that isn't one (the message). */
39
+ export declare function parseFrame(text: string): StackFrame | null;
40
+ /**
41
+ * Frames from packages, runtimes and the kernel itself, which a frontend
42
+ * can dim or fold: a hint, not a boundary.
43
+ */
44
+ export declare function isLibraryFile(file: string, kernelFiles?: readonly string[]): boolean;
45
+ export interface TracebackSources {
46
+ /** The cell a script name belongs to, if it is one. */
47
+ cell(file: string): (CellRecord & {
48
+ lines: LineMap | null;
49
+ }) | undefined;
50
+ /** Another file's source, when it can be read. */
51
+ readFile?(file: string): string | undefined;
52
+ /** Path or URL prefixes of the kernel's own runtime files. */
53
+ kernelFiles?: readonly string[];
54
+ }
55
+ export interface Traceback {
56
+ /** The classic traceback lines, with cell frames pointing at the cell. */
57
+ lines: string[];
58
+ /** The structured payload. */
59
+ payload: Record<string, unknown>;
60
+ }
61
+ /**
62
+ * `stack` is the error's stack as the target describes it (src/target.ts's
63
+ * describeError), message lines included. `callFrames`, from CDP, name the
64
+ * scripts of frames the stack shows as "<anonymous>": Deno formats stacks
65
+ * itself and doesn't use a script's //# sourceURL.
66
+ */
67
+ export declare function buildTraceback(error: {
68
+ ename: string;
69
+ evalue: string;
70
+ stack: string;
71
+ callFrames?: readonly CallFrame[];
72
+ }, sources: TracebackSources, execution: {
73
+ executionCount: number | null;
74
+ cellId: string | null;
75
+ }): Traceback;