@tlockney/cdp-kernel 0.7.0 → 0.7.2

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/README.md CHANGED
@@ -5,7 +5,9 @@ into Deno 2.9.6 (`deno jupyter`), that runs notebooks on several JavaScript runt
5
5
  host runs on Node or Deno; the code you type runs in a separate **target**
6
6
  runtime: Node or Deno over the Chrome DevTools Protocol, or Bun, workerd and
7
7
  celld, which evaluate cells in-process. In principle, anything else whose
8
- inspector speaks CDP works too.
8
+ inspector speaks CDP works too. The kernel can also run inside a web page,
9
+ with cells in a Web Worker and no server
10
+ ([The browser](https://cdp-kernel.org/docs/integration-browser), experimental).
9
11
 
10
12
  **Website:** <https://cdp-kernel.org/> ·
11
13
  **Docs:** [cdp-kernel.org/docs](https://cdp-kernel.org/docs/)
@@ -33,7 +35,7 @@ runtime versions and frontends are supported, and what may change before
33
35
  - Deno projects (`deno.json` import maps, JSX settings) and Workers
34
36
  projects (`wrangler` bindings in `env`).
35
37
  - JupyterLab's visual debugger on Node and Deno targets.
36
- - anywidget-style widgets and Jupyter comms.
38
+ - [anywidget](https://github.com/manzt/anywidget)-style widgets and Jupyter comms.
37
39
  - `prompt()` and `confirm()`, interrupt that keeps state, Shift-Tab
38
40
  inspection, completion and history.
39
41
 
@@ -28401,6 +28401,17 @@ function makeHeader(session, msgType) {
28401
28401
  }
28402
28402
 
28403
28403
  // src/target-api.ts
28404
+ function parseBundle(text) {
28405
+ const bundle = typeof text === "string" ? parseJson(text) : void 0;
28406
+ return typeof bundle === "object" && bundle !== null && !Array.isArray(bundle) ? bundle : {};
28407
+ }
28408
+ function parseJson(text) {
28409
+ try {
28410
+ return JSON.parse(text);
28411
+ } catch {
28412
+ return void 0;
28413
+ }
28414
+ }
28404
28415
  function isTargetMessage(value) {
28405
28416
  if (typeof value !== "object" || value === null) return false;
28406
28417
  const kind = value.kind;
@@ -28746,6 +28757,7 @@ function clip(frames) {
28746
28757
  }
28747
28758
 
28748
28759
  // src/kernel-core.ts
28760
+ var TYPESCRIPT_VERSION = "6";
28749
28761
  function fromBase64(text) {
28750
28762
  return Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
28751
28763
  }
@@ -28814,6 +28826,8 @@ var KernelCore = class {
28814
28826
  #input = null;
28815
28827
  /** An interrupt is waiting on the target. */
28816
28828
  #interrupting = false;
28829
+ /** Ends the cell waiting for the target to start, while one is (#336). */
28830
+ #interruptStart = null;
28817
28831
  /** The shell message being handled, and since when. */
28818
28832
  #handling = null;
28819
28833
  // Open comms, for comm_info_request: comm_id -> target_name (#9).
@@ -28932,6 +28946,21 @@ var KernelCore = class {
28932
28946
  });
28933
28947
  return this.#targetStarting;
28934
28948
  }
28949
+ /**
28950
+ * The target for a cell. An interrupt while it starts ends the cell; the
28951
+ * start goes on, bounded by the target's own deadline, for the next one (#336).
28952
+ */
28953
+ async #targetForCell() {
28954
+ if (this.#target) return this.#target;
28955
+ const interrupted = new Promise((_, reject) => {
28956
+ this.#interruptStart = () => reject(new Interrupted("Execution interrupted"));
28957
+ });
28958
+ try {
28959
+ return await Promise.race([this.#ensureTarget(), interrupted]);
28960
+ } finally {
28961
+ this.#interruptStart = null;
28962
+ }
28963
+ }
28935
28964
  #targetEvents() {
28936
28965
  return {
28937
28966
  stream: (name, text) => this.#publish("stream", { name, text }),
@@ -28974,6 +29003,10 @@ ${text}
28974
29003
  input2.settle({ value: null, interrupted: true });
28975
29004
  return;
28976
29005
  }
29006
+ if (this.#interruptStart !== null) {
29007
+ this.#interruptStart();
29008
+ return;
29009
+ }
28977
29010
  if (!this.#target || !this.#executing) return;
28978
29011
  this.#interrupting = true;
28979
29012
  try {
@@ -29029,7 +29062,7 @@ ${text}
29029
29062
  implementation_version: this.#host.version,
29030
29063
  language_info: {
29031
29064
  name: "typescript",
29032
- version: "5",
29065
+ version: this.#host.languageVersion ?? TYPESCRIPT_VERSION,
29033
29066
  mimetype: "text/x.typescript",
29034
29067
  file_extension: ".ts",
29035
29068
  pygments_lexer: "typescript",
@@ -29042,7 +29075,8 @@ ${text}
29042
29075
  debugger: this.#debugger !== null,
29043
29076
  // What current ipykernel advertises; JupyterLab itself goes by the
29044
29077
  // kernelspec's metadata.debugger.
29045
- supported_features: this.#debugger !== null ? ["debugger"] : []
29078
+ supported_features: this.#debugger !== null ? ["debugger"] : [],
29079
+ ...this.#host.capabilities ? { capabilities: this.#host.capabilities } : {}
29046
29080
  };
29047
29081
  }
29048
29082
  // --- control channel ------------------------------------------------------
@@ -29281,6 +29315,39 @@ ${withoutSignature(runtime)}`;
29281
29315
  }
29282
29316
  return target.evaluate(transform2(code));
29283
29317
  }
29318
+ /** execute_request's user_expressions, each as its value's MIME bundle or the error it threw (#340). */
29319
+ async #userExpressions(target, expressions) {
29320
+ const results = {};
29321
+ if (typeof expressions !== "object" || expressions === null) return results;
29322
+ for (const [name, expression] of Object.entries(expressions)) {
29323
+ results[name] = await this.#userExpression(target, String(expression));
29324
+ }
29325
+ return results;
29326
+ }
29327
+ async #userExpression(target, expression) {
29328
+ try {
29329
+ const source = `(
29330
+ ${expression}
29331
+ )`;
29332
+ const result = await target.evaluate(`${target.transform(source, this.#transformOptions())}
29333
+ //# sourceURL=$cdpk$user-expression.ts`);
29334
+ if (result.exceptionDetails) {
29335
+ const exception = result.exceptionDetails.exception;
29336
+ const described = exception ? await target.describeError(exception) : { name: "Error", message: result.exceptionDetails.text, stack: "" };
29337
+ return {
29338
+ status: "error",
29339
+ ename: described.name || "Error",
29340
+ evalue: described.message,
29341
+ traceback: (described.stack || `${described.name}: ${described.message}`).split("\n")
29342
+ };
29343
+ }
29344
+ return { status: "ok", data: await target.format(result.result), metadata: {} };
29345
+ } catch (err) {
29346
+ const ename = err instanceof CellSyntaxError ? "SyntaxError" : err instanceof Interrupted ? "Interrupted" : "Error";
29347
+ const evalue = err instanceof Error ? err.message : String(err);
29348
+ return { status: "error", ename, evalue, traceback: [`${ename}: ${evalue}`] };
29349
+ }
29350
+ }
29284
29351
  async #execute(msg, peer) {
29285
29352
  const parent = msg.header;
29286
29353
  if (this.#aborting) {
@@ -29306,10 +29373,11 @@ ${withoutSignature(runtime)}`;
29306
29373
  this.#projectWarning = null;
29307
29374
  }
29308
29375
  let error = null;
29376
+ let userExpressions = {};
29309
29377
  let structured = null;
29310
29378
  this.#executing = true;
29311
29379
  try {
29312
- const target = await this.#ensureTarget();
29380
+ const target = await this.#targetForCell();
29313
29381
  if (target.notice !== void 0 && this.#noticeShownFor !== target) {
29314
29382
  this.#noticeShownFor = target;
29315
29383
  this.#publish("stream", { name: "stderr", text: `${target.notice}
@@ -29334,6 +29402,7 @@ ${withoutSignature(runtime)}`;
29334
29402
  } else if (!silent && result.result.type !== "undefined") {
29335
29403
  await target.broadcastResult(count, result.result);
29336
29404
  }
29405
+ if (!result.exceptionDetails) userExpressions = await this.#userExpressions(target, msg.content.user_expressions);
29337
29406
  await target.flush();
29338
29407
  } catch (err) {
29339
29408
  if (err instanceof CellSyntaxError) {
@@ -29362,7 +29431,7 @@ ${withoutSignature(runtime)}`;
29362
29431
  status: "ok",
29363
29432
  execution_count: count,
29364
29433
  payload: [],
29365
- user_expressions: {}
29434
+ user_expressions: userExpressions
29366
29435
  });
29367
29436
  }
29368
29437
  this.#status("idle", parent);
@@ -29370,7 +29439,7 @@ ${withoutSignature(runtime)}`;
29370
29439
  };
29371
29440
 
29372
29441
  // stub:package
29373
- var VERSION = "0.7.0";
29442
+ var VERSION = "0.7.2";
29374
29443
 
29375
29444
  // src/browser/channels.ts
29376
29445
  var PEER = "page";
@@ -29485,13 +29554,17 @@ var BrowserTarget = class _BrowserTarget {
29485
29554
  #pending = /* @__PURE__ */ new Map();
29486
29555
  /** Requests that interrupt() abandons when the driver can still answer. */
29487
29556
  #interruptible = /* @__PURE__ */ new Set();
29557
+ /** Host requests in flight, by the Worker's id (#384). */
29558
+ #hostCalls = /* @__PURE__ */ new Map();
29559
+ #host;
29488
29560
  #nextId = 0;
29489
29561
  #interrupted = false;
29490
29562
  #stopNote;
29491
29563
  #exited = false;
29492
- constructor(worker, events) {
29564
+ constructor(worker, events, host) {
29493
29565
  this.#worker = worker;
29494
29566
  this.#events = events;
29567
+ this.#host = host;
29495
29568
  worker.addEventListener("message", (event) => this.#receive(event.data));
29496
29569
  worker.addEventListener("error", (event) => {
29497
29570
  this.#events.stream("stderr", `[cdp-kernel] browser Worker error: ${event.message ?? "unknown"}
@@ -29500,10 +29573,10 @@ var BrowserTarget = class _BrowserTarget {
29500
29573
  });
29501
29574
  }
29502
29575
  static async launch(events, options) {
29503
- const target = new _BrowserTarget(options.createWorker(), events);
29576
+ const target = new _BrowserTarget(options.createWorker(), events, options.host);
29504
29577
  const base2 = options.base ?? globalThis.location?.href ?? "file:///";
29505
29578
  try {
29506
- const { userAgent } = await target.#request("info", { base: base2, cdn: options.cdn });
29579
+ const { userAgent } = await target.#request("info", { base: base2, cdn: options.cdn, preload: options.preload ?? [] });
29507
29580
  if (typeof userAgent === "string") target.versions.runtime = browserName(userAgent);
29508
29581
  target.versions.engine = engineName(typeof userAgent === "string" ? userAgent : "");
29509
29582
  } catch (err) {
@@ -29515,6 +29588,8 @@ var BrowserTarget = class _BrowserTarget {
29515
29588
  #receive(msg) {
29516
29589
  if (isRecord(msg) && msg.kind === "stream") {
29517
29590
  this.#events.stream(msg.name === "stderr" ? "stderr" : "stdout", String(msg.text));
29591
+ } else if (isRecord(msg) && msg.kind === "host") {
29592
+ this.#hostRequest(msg);
29518
29593
  } else if (isTargetMessage(msg)) {
29519
29594
  this.#events.message(msg);
29520
29595
  } else if (isRecord(msg) && msg.kind === "reply") {
@@ -29527,10 +29602,52 @@ var BrowserTarget = class _BrowserTarget {
29527
29602
  else pending.resolve(msg);
29528
29603
  }
29529
29604
  }
29605
+ #hostRequest(msg) {
29606
+ const id = Number(msg.id);
29607
+ if (msg.cancel === true) {
29608
+ this.#hostCalls.get(id)?.abort();
29609
+ this.#hostCalls.delete(id);
29610
+ return;
29611
+ }
29612
+ const controller = new AbortController();
29613
+ this.#hostCalls.set(id, controller);
29614
+ const answer = (reply) => {
29615
+ if (this.#hostCalls.get(id) !== controller) return;
29616
+ this.#hostCalls.delete(id);
29617
+ try {
29618
+ this.#worker.postMessage({ op: "host", id, ...reply });
29619
+ } catch (err) {
29620
+ this.#worker.postMessage({ op: "host", id, error: describeHostError(err) });
29621
+ }
29622
+ };
29623
+ const host = this.#host;
29624
+ new Promise((resolve) => {
29625
+ if (!host) throw new Error("this page offers no host requests");
29626
+ resolve(host({ method: String(msg.method), params: msg.params }, controller.signal));
29627
+ }).then(
29628
+ (result) => answer({ result }),
29629
+ (err) => answer({ error: describeHostError(err) })
29630
+ );
29631
+ }
29632
+ /** Aborts each host request in flight and rejects it in the Worker. */
29633
+ #cancelHostCalls() {
29634
+ const calls = [...this.#hostCalls];
29635
+ this.#hostCalls.clear();
29636
+ for (const [id, controller] of calls) {
29637
+ controller.abort();
29638
+ this.#worker.postMessage({ op: "host", id, error: { name: "AbortError", message: "interrupted" } });
29639
+ }
29640
+ }
29641
+ /** Sends `host.onEvent` listeners in the Worker an event (#384). */
29642
+ notifyHost(event, value) {
29643
+ if (!this.#exited) this.#worker.postMessage({ op: "host", event, value });
29644
+ }
29530
29645
  #exit(code) {
29531
29646
  if (this.#exited) return;
29532
29647
  this.#exited = true;
29533
29648
  this.#worker.terminate();
29649
+ for (const controller of this.#hostCalls.values()) controller.abort();
29650
+ this.#hostCalls.clear();
29534
29651
  this.#events.exit(code, this.#stopNote);
29535
29652
  for (const pending of this.#pending.values()) pending.reject(new Abandoned("the browser Worker stopped"));
29536
29653
  this.#pending.clear();
@@ -29558,9 +29675,10 @@ var BrowserTarget = class _BrowserTarget {
29558
29675
  }
29559
29676
  /**
29560
29677
  * A cell that's waiting on something leaves the Worker free: it answers a
29561
- * ping, and the cell is abandoned with its state intact. A busy cell
29562
- * can't be reached, so the Worker is terminated, and a new one starts on
29563
- * the next cell.
29678
+ * ping, and the cell is abandoned with its state intact; host requests
29679
+ * in flight are cancelled on the page, and reject in the Worker with an
29680
+ * AbortError (#388). A busy cell can't be reached, so the Worker is
29681
+ * terminated, and a new one starts on the next cell.
29564
29682
  */
29565
29683
  async interrupt() {
29566
29684
  this.#interrupted = true;
@@ -29575,6 +29693,7 @@ var BrowserTarget = class _BrowserTarget {
29575
29693
  this.#pending.delete(id);
29576
29694
  this.#interruptible.delete(id);
29577
29695
  }
29696
+ this.#cancelHostCalls();
29578
29697
  return;
29579
29698
  }
29580
29699
  this.#stopNote = RESTART_NOTE;
@@ -29596,6 +29715,10 @@ var BrowserTarget = class _BrowserTarget {
29596
29715
  }
29597
29716
  return { handled: reply.handled === true };
29598
29717
  }
29718
+ async format(value) {
29719
+ const reply = await this.#request("format", { handle: value.objectId });
29720
+ return parseBundle(reply.value);
29721
+ }
29599
29722
  async describeError(exception) {
29600
29723
  if (exception.objectId === void 0) return { name: "Error", message: exception.description ?? "", stack: "" };
29601
29724
  const { value } = await this.#request("describe", { handle: exception.objectId });
@@ -29621,6 +29744,17 @@ var BrowserTarget = class _BrowserTarget {
29621
29744
  this.#exit(null);
29622
29745
  }
29623
29746
  };
29747
+ function describeHostError(err) {
29748
+ if (isRecord(err)) {
29749
+ const { name, message, code } = err;
29750
+ return {
29751
+ name: typeof name === "string" ? name : "Error",
29752
+ message: typeof message === "string" ? message : String(err),
29753
+ ...code === void 0 ? {} : { code }
29754
+ };
29755
+ }
29756
+ return { name: "Error", message: String(err) };
29757
+ }
29624
29758
  function toBase64(bytes2) {
29625
29759
  let binary = "";
29626
29760
  for (let i = 0; i < bytes2.length; i += 32768) binary += String.fromCharCode(...bytes2.subarray(i, i + 32768));
@@ -29651,12 +29785,14 @@ function startBrowserKernel(options) {
29651
29785
  const log = options.log ?? ((text) => console.debug(`[cdp-kernel] ${text}`));
29652
29786
  let closed;
29653
29787
  const closedPromise = new Promise((resolve) => closed = resolve);
29788
+ let target = null;
29654
29789
  const core = new KernelCore({
29655
29790
  runtime: "browser",
29656
29791
  version: VERSION,
29657
29792
  channels,
29658
- launchTarget: (events) => BrowserTarget.launch(events, options),
29793
+ launchTarget: async (events) => target = await BrowserTarget.launch(events, options),
29659
29794
  typeStrip: options.typeStrip ?? "sucrase",
29795
+ capabilities: options.capabilities,
29660
29796
  log,
29661
29797
  exit: () => closed()
29662
29798
  });
@@ -29664,7 +29800,8 @@ function startBrowserKernel(options) {
29664
29800
  return {
29665
29801
  send: (channel, msg) => channels.deliver(channel, msg),
29666
29802
  onMessage: (listener) => channels.onMessage(listener),
29667
- closed: closedPromise
29803
+ closed: closedPromise,
29804
+ notifyHost: (event, value) => target?.notifyHost(event, value)
29668
29805
  };
29669
29806
  }
29670
29807
  export {