@tlockney/cdp-kernel 0.7.1 → 0.8.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.
@@ -28376,7 +28376,7 @@ var init_transform = __esm({
28376
28376
  SHIM = "__cdpk";
28377
28377
  YIELD = `await ${SHIM}.tick();
28378
28378
  `;
28379
- PRELUDE = `'use strict'; ${YIELD}`;
28379
+ PRELUDE = `'use strict'; ${SHIM}.startCell(); ${YIELD}`;
28380
28380
  SETTLE = `
28381
28381
  ;let ${SHIM}_settled = await ${SHIM}.tick();`;
28382
28382
  CellSyntaxError = class extends Error {
@@ -28388,7 +28388,7 @@ var init_transform = __esm({
28388
28388
  });
28389
28389
 
28390
28390
  // src/message.ts
28391
- var PROTOCOL_VERSION = "5.3";
28391
+ var PROTOCOL_VERSION = "5.5";
28392
28392
  function makeHeader(session, msgType) {
28393
28393
  return {
28394
28394
  msg_id: crypto.randomUUID(),
@@ -28758,12 +28758,43 @@ function clip(frames) {
28758
28758
 
28759
28759
  // src/kernel-core.ts
28760
28760
  var TYPESCRIPT_VERSION = "6";
28761
+ var launchFailures = /* @__PURE__ */ new WeakSet();
28761
28762
  function fromBase64(text) {
28762
28763
  return Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
28763
28764
  }
28764
28765
  var INTERRUPT_REPLY_WAIT_MS = 1e3;
28765
28766
  var NO_INPUT = { value: null, interrupted: false };
28767
+ var ShellQueue = class {
28768
+ items = [];
28769
+ #wake = null;
28770
+ #closed = false;
28771
+ push(item) {
28772
+ if (this.#closed) return;
28773
+ this.items.push(item);
28774
+ this.#wake?.();
28775
+ }
28776
+ /** The next message; null once closed. */
28777
+ async next() {
28778
+ for (; ; ) {
28779
+ if (this.#closed) return null;
28780
+ const item = this.items.shift();
28781
+ if (item !== void 0) return item;
28782
+ await new Promise((resolve) => this.#wake = resolve);
28783
+ this.#wake = null;
28784
+ }
28785
+ }
28786
+ /** Stops taking messages; returns those still queued. */
28787
+ close() {
28788
+ this.#closed = true;
28789
+ const dropped = this.items.splice(0);
28790
+ this.#wake?.();
28791
+ return dropped;
28792
+ }
28793
+ };
28766
28794
  var MAX_CELLS = 1e3;
28795
+ function subshellNotFound(evalue) {
28796
+ return { status: "error", ename: "SubshellNotFound", evalue, traceback: [`SubshellNotFound: ${evalue}`] };
28797
+ }
28767
28798
  var SHELL_REPLIES = {
28768
28799
  kernel_info_request: "kernel_info_reply",
28769
28800
  complete_request: "complete_reply",
@@ -28815,13 +28846,16 @@ var KernelCore = class {
28815
28846
  #executionCount = 0;
28816
28847
  #parent = {};
28817
28848
  #allowStdin = false;
28818
- #executing = false;
28819
- /**
28820
- * stop_on_error (#123): how many of the shell messages queued when a cell
28821
- * failed are still to come, and whether the one being handled is one.
28822
- */
28823
- #abortQueued = 0;
28849
+ /** Cells and comm handlers running in the target. */
28850
+ #running = 0;
28851
+ /** A cell is running: its parent header and allow_stdin hold until it ends (#417). */
28852
+ #cellRunning = false;
28853
+ /** stop_on_error (#123): the message being handled was queued behind a failed cell. */
28824
28854
  #aborting = false;
28855
+ /** Messages without a subshell_id, in the order they came. */
28856
+ #mainShell = new ShellQueue();
28857
+ /** Kernel subshells (JEP 91, #417): subshell_id -> its queue. */
28858
+ #subshells = /* @__PURE__ */ new Map();
28825
28859
  /** The input_request waiting for its input_reply, if any. */
28826
28860
  #input = null;
28827
28861
  /** An interrupt is waiting on the target. */
@@ -28846,7 +28880,8 @@ var KernelCore = class {
28846
28880
  this.#ensureTarget().catch((err) => this.#log(`target failed to start: ${err}`));
28847
28881
  if (typeof this.#typeStrip === "string") await loadTypeStripper(this.#typeStrip);
28848
28882
  await Promise.all([
28849
- this.#loop("shell", (m, p) => this.#handleShell(m, p)),
28883
+ this.#readShell(),
28884
+ this.#runShell(this.#mainShell),
28850
28885
  this.#loop("control", (m, p) => this.#handleControl(m, p)),
28851
28886
  this.#loop("stdin", (m) => this.#handleStdin(m))
28852
28887
  ]);
@@ -28870,8 +28905,9 @@ var KernelCore = class {
28870
28905
  if (target?.diagnose) targetState = await target.diagnose().catch((err) => `diagnose failed: ${err}`);
28871
28906
  this.#log(`state: ${JSON.stringify({
28872
28907
  shell: handling && { msgType: handling.msgType, msgId: handling.msgId, ms: Date.now() - handling.since },
28873
- queued: this.#channels.shellQueued(),
28874
- executing: this.#executing,
28908
+ queued: this.#mainShell.items.length + this.#channels.shellQueued(),
28909
+ subshells: this.#subshells.size,
28910
+ running: this.#running,
28875
28911
  interrupting: this.#interrupting,
28876
28912
  awaitingInput: this.#input !== null,
28877
28913
  target: targetState
@@ -28900,25 +28936,28 @@ var KernelCore = class {
28900
28936
  const type = msg.header.msg_type;
28901
28937
  const commId = String(msg.content.comm_id ?? "");
28902
28938
  const target = await this.#ensureTarget();
28903
- this.#parent = msg.header;
28904
- this.#allowStdin = false;
28905
- this.#executing = true;
28939
+ if (!this.#cellRunning) {
28940
+ this.#parent = msg.header;
28941
+ this.#allowStdin = false;
28942
+ }
28943
+ const parent = msg.header;
28906
28944
  let handled = false;
28945
+ this.#running++;
28907
28946
  try {
28908
28947
  const result = await target.dispatchComm(type, msg.content, msg.metadata, msg.buffers);
28909
28948
  if ("error" in result) {
28910
28949
  const { name, message, stack } = result.error;
28911
28950
  this.#publish("stream", { name: "stderr", text: `Error in ${type} handler: ${stack || `${name}: ${message}`}
28912
- ` });
28951
+ ` }, { parent });
28913
28952
  } else {
28914
28953
  handled = result.handled;
28915
28954
  }
28916
28955
  } catch (err) {
28917
28956
  if (!(err instanceof Interrupted)) throw err;
28918
28957
  this.#publish("stream", { name: "stderr", text: `${type} handler interrupted
28919
- ` });
28958
+ ` }, { parent });
28920
28959
  } finally {
28921
- this.#executing = false;
28960
+ this.#running--;
28922
28961
  }
28923
28962
  await target.flush().catch(() => {
28924
28963
  });
@@ -28926,7 +28965,7 @@ var KernelCore = class {
28926
28965
  if (handled) {
28927
28966
  this.#comms.set(commId, String(msg.content.target_name ?? ""));
28928
28967
  } else {
28929
- this.#publish("comm_close", { comm_id: commId, data: {} });
28968
+ this.#publish("comm_close", { comm_id: commId, data: {} }, { parent });
28930
28969
  }
28931
28970
  } else if (type === "comm_close") {
28932
28971
  this.#comms.delete(commId);
@@ -28942,6 +28981,7 @@ var KernelCore = class {
28942
28981
  return target;
28943
28982
  }, (err) => {
28944
28983
  this.#targetStarting = null;
28984
+ if (typeof err === "object" && err !== null) launchFailures.add(err);
28945
28985
  throw err;
28946
28986
  });
28947
28987
  return this.#targetStarting;
@@ -28961,10 +29001,18 @@ var KernelCore = class {
28961
29001
  this.#interruptStart = null;
28962
29002
  }
28963
29003
  }
29004
+ /** Tells the host of a fault in the kernel's own code; a target that failed to launch isn't one (#422). */
29005
+ #fault(err, where) {
29006
+ if (typeof err === "object" && err !== null && launchFailures.has(err)) return;
29007
+ this.#host.fault?.(err, where);
29008
+ }
28964
29009
  #targetEvents() {
28965
29010
  return {
28966
29011
  stream: (name, text) => this.#publish("stream", { name, text }),
28967
- message: (msg) => void this.#onTargetMessage(msg).catch((err) => this.#log(`error handling target message: ${err}`)),
29012
+ message: (msg) => void this.#onTargetMessage(msg).catch((err) => {
29013
+ this.#log(`error handling target message: ${err}`);
29014
+ this.#fault(err, "target message");
29015
+ }),
28968
29016
  exit: (code, note) => {
28969
29017
  this.#target = null;
28970
29018
  this.#types?.reset();
@@ -29007,7 +29055,7 @@ ${text}
29007
29055
  this.#interruptStart();
29008
29056
  return;
29009
29057
  }
29010
- if (!this.#target || !this.#executing) return;
29058
+ if (!this.#target || this.#running === 0) return;
29011
29059
  this.#interrupting = true;
29012
29060
  try {
29013
29061
  await this.#target.interrupt();
@@ -29037,19 +29085,70 @@ ${text}
29037
29085
  async #loop(channel, handle) {
29038
29086
  while (!this.#shuttingDown) {
29039
29087
  const received = await this.#channels.recv(channel);
29040
- if (channel === "shell") {
29041
- this.#aborting = this.#abortQueued > 0;
29042
- if (this.#aborting) this.#abortQueued--;
29088
+ if (received === null) continue;
29089
+ try {
29090
+ await handle(received.msg, received.peer);
29091
+ } catch (err) {
29092
+ this.#log(`error handling message: ${err instanceof Error ? err.stack : err}`);
29093
+ this.#fault(err, `${channel} handler`);
29043
29094
  }
29095
+ }
29096
+ }
29097
+ /**
29098
+ * Takes every shell message as it comes and queues it for the main shell
29099
+ * or its subshell, so a subshell's message isn't stuck behind a running
29100
+ * cell (#417).
29101
+ */
29102
+ async #readShell() {
29103
+ while (!this.#shuttingDown) {
29104
+ const received = await this.#channels.recv("shell");
29044
29105
  if (received === null) continue;
29045
- const { peer, msg } = received;
29106
+ const subshellId = received.msg.header.subshell_id;
29107
+ if (typeof subshellId !== "string") {
29108
+ this.#mainShell.push({ ...received, abort: false });
29109
+ continue;
29110
+ }
29111
+ const queue = this.#subshells.get(subshellId);
29112
+ if (queue === void 0) {
29113
+ this.#refuseShell(received, `No subshell with id ${subshellId}`);
29114
+ } else if (received.msg.header.msg_type === "execute_request") {
29115
+ this.#mainShell.push({ ...received, abort: false });
29116
+ } else {
29117
+ queue.push({ ...received, abort: false });
29118
+ }
29119
+ }
29120
+ }
29121
+ /**
29122
+ * Answers a shell message for a subshell that doesn't exist, or was
29123
+ * deleted with the message queued, so the frontend doesn't wait on it.
29124
+ */
29125
+ #refuseShell({ peer, msg }, evalue) {
29126
+ const type = msg.header.msg_type;
29127
+ this.#log(`refusing ${type}: ${evalue}`);
29128
+ this.#status("busy", msg.header);
29129
+ const replyType = type === "execute_request" ? "execute_reply" : SHELL_REPLIES[type];
29130
+ if (replyType !== void 0) this.#reply("shell", peer, msg, replyType, subshellNotFound(evalue));
29131
+ if (type === "comm_open") this.#publish("comm_close", { comm_id: String(msg.content.comm_id ?? ""), data: {} }, { parent: msg.header });
29132
+ this.#status("idle", msg.header);
29133
+ }
29134
+ /** Handles one queue's messages in turn. */
29135
+ async #runShell(queue) {
29136
+ const main = queue === this.#mainShell;
29137
+ while (!this.#shuttingDown) {
29138
+ const queued = await queue.next();
29139
+ if (queued === null) return;
29140
+ const { peer, msg } = queued;
29046
29141
  try {
29047
- if (channel === "shell") this.#handling = { msgType: msg.header.msg_type, msgId: msg.header.msg_id, since: Date.now() };
29048
- await handle(msg, peer);
29142
+ if (main) {
29143
+ this.#aborting = queued.abort;
29144
+ this.#handling = { msgType: msg.header.msg_type, msgId: msg.header.msg_id, since: Date.now() };
29145
+ }
29146
+ await this.#handleShell(msg, peer);
29049
29147
  } catch (err) {
29050
29148
  this.#log(`error handling message: ${err instanceof Error ? err.stack : err}`);
29149
+ this.#fault(err, "shell handler");
29051
29150
  } finally {
29052
- if (channel === "shell") this.#handling = null;
29151
+ if (main) this.#handling = null;
29053
29152
  }
29054
29153
  }
29055
29154
  }
@@ -29063,7 +29162,12 @@ ${text}
29063
29162
  language_info: {
29064
29163
  name: "typescript",
29065
29164
  version: this.#host.languageVersion ?? TYPESCRIPT_VERSION,
29066
- mimetype: "text/x.typescript",
29165
+ // JupyterLab's LSP adapter picks the language server by a language
29166
+ // it derives from this MIME type, not from name: it drops the type
29167
+ // and an "x-" prefix, so "text/x.typescript" became "x.typescript"
29168
+ // and matched no server (#400). This is CodeMirror's MIME type for
29169
+ // TypeScript and in jupyter-lsp's typescript-language-server spec.
29170
+ mimetype: "application/typescript",
29067
29171
  file_extension: ".ts",
29068
29172
  pygments_lexer: "typescript",
29069
29173
  codemirror_mode: "typescript",
@@ -29075,12 +29179,19 @@ ${text}
29075
29179
  debugger: this.#debugger !== null,
29076
29180
  // What current ipykernel advertises; JupyterLab itself goes by the
29077
29181
  // kernelspec's metadata.debugger.
29078
- supported_features: this.#debugger !== null ? ["debugger"] : [],
29182
+ supported_features: [...this.#debugger !== null ? ["debugger"] : [], "kernel subshells"],
29079
29183
  ...this.#host.capabilities ? { capabilities: this.#host.capabilities } : {}
29080
29184
  };
29081
29185
  }
29082
29186
  // --- control channel ------------------------------------------------------
29083
29187
  async #handleControl(msg, peer) {
29188
+ if (msg.header.msg_type === "usage_request") {
29189
+ void this.#handleControlRequest(msg, peer).catch((err) => this.#log(`usage_request failed: ${err}`));
29190
+ return;
29191
+ }
29192
+ return this.#handleControlRequest(msg, peer);
29193
+ }
29194
+ async #handleControlRequest(msg, peer) {
29084
29195
  this.#status("busy", msg.header);
29085
29196
  try {
29086
29197
  await this.#controlRequest("control", msg, peer);
@@ -29101,6 +29212,37 @@ ${text}
29101
29212
  ]);
29102
29213
  this.#reply(channel, peer, msg, "interrupt_reply", { status: "ok" });
29103
29214
  break;
29215
+ case "create_subshell_request": {
29216
+ const subshellId = crypto.randomUUID();
29217
+ const queue = new ShellQueue();
29218
+ this.#subshells.set(subshellId, queue);
29219
+ void this.#runShell(queue);
29220
+ this.#reply(channel, peer, msg, "create_subshell_reply", { status: "ok", subshell_id: subshellId });
29221
+ break;
29222
+ }
29223
+ case "delete_subshell_request": {
29224
+ const subshellId = String(msg.content.subshell_id ?? "");
29225
+ const queue = this.#subshells.get(subshellId);
29226
+ if (queue === void 0) {
29227
+ this.#reply(channel, peer, msg, "delete_subshell_reply", subshellNotFound(`No subshell with id ${subshellId}`));
29228
+ break;
29229
+ }
29230
+ this.#subshells.delete(subshellId);
29231
+ for (const dropped of queue.close()) this.#refuseShell(dropped, `Subshell ${subshellId} was deleted`);
29232
+ this.#reply(channel, peer, msg, "delete_subshell_reply", { status: "ok" });
29233
+ break;
29234
+ }
29235
+ case "list_subshell_request":
29236
+ this.#reply(channel, peer, msg, "list_subshell_reply", { status: "ok", subshell_id: [...this.#subshells.keys()] });
29237
+ break;
29238
+ case "usage_request": {
29239
+ const usage = this.#host.usage?.(this.#target).catch((err) => {
29240
+ this.#log(`usage_request failed: ${err}`);
29241
+ return {};
29242
+ });
29243
+ this.#reply(channel, peer, msg, "usage_reply", await usage ?? {});
29244
+ break;
29245
+ }
29104
29246
  case "debug_request": {
29105
29247
  let reply;
29106
29248
  if (this.#debugger === null) {
@@ -29375,7 +29517,8 @@ ${expression}
29375
29517
  let error = null;
29376
29518
  let userExpressions = {};
29377
29519
  let structured = null;
29378
- this.#executing = true;
29520
+ this.#running++;
29521
+ this.#cellRunning = true;
29379
29522
  try {
29380
29523
  const target = await this.#targetForCell();
29381
29524
  if (target.notice !== void 0 && this.#noticeShownFor !== target) {
@@ -29416,15 +29559,16 @@ ${expression}
29416
29559
  error = { ename: "KernelError", evalue: message, traceback: [message] };
29417
29560
  }
29418
29561
  } finally {
29419
- this.#executing = false;
29562
+ this.#running--;
29563
+ this.#cellRunning = false;
29420
29564
  }
29421
29565
  await this.#target?.releaseResults?.();
29422
29566
  if (error) {
29423
29567
  if (!silent && msg.content.stop_on_error !== false) {
29424
29568
  await new Promise((r) => setTimeout(r, 0));
29425
- this.#abortQueued = this.#channels.shellQueued();
29569
+ for (const queued of this.#mainShell.items) queued.abort = true;
29426
29570
  }
29427
- this.#publish("error", error, structured ? { metadata: { [TRACEBACK_MIME]: structured } } : {});
29571
+ this.#publish("error", error, { parent, ...structured ? { metadata: { [TRACEBACK_MIME]: structured } } : {} });
29428
29572
  this.#reply("shell", peer, msg, "execute_reply", { status: "error", execution_count: count, ...error });
29429
29573
  } else {
29430
29574
  this.#reply("shell", peer, msg, "execute_reply", {
@@ -29439,7 +29583,7 @@ ${expression}
29439
29583
  };
29440
29584
 
29441
29585
  // stub:package
29442
- var VERSION = "0.7.1";
29586
+ var VERSION = "0.8.0";
29443
29587
 
29444
29588
  // src/browser/channels.ts
29445
29589
  var PEER = "page";
@@ -29558,7 +29702,10 @@ var BrowserTarget = class _BrowserTarget {
29558
29702
  #hostCalls = /* @__PURE__ */ new Map();
29559
29703
  #host;
29560
29704
  #nextId = 0;
29561
- #interrupted = false;
29705
+ /** Counts interrupt() calls: a call that saw one start during it was interrupted (#417). */
29706
+ #interrupts = 0;
29707
+ /** Cells running: a comm handler that starts with none gets its own jupyter.signal (#421). */
29708
+ #cells = 0;
29562
29709
  #stopNote;
29563
29710
  #exited = false;
29564
29711
  constructor(worker, events, host) {
@@ -29629,6 +29776,15 @@ var BrowserTarget = class _BrowserTarget {
29629
29776
  (err) => answer({ error: describeHostError(err) })
29630
29777
  );
29631
29778
  }
29779
+ /** Aborts each host request in flight and rejects it in the Worker. */
29780
+ #cancelHostCalls() {
29781
+ const calls = [...this.#hostCalls];
29782
+ this.#hostCalls.clear();
29783
+ for (const [id, controller] of calls) {
29784
+ controller.abort();
29785
+ this.#worker.postMessage({ op: "host", id, error: { name: "AbortError", message: "interrupted" } });
29786
+ }
29787
+ }
29632
29788
  /** Sends `host.onEvent` listeners in the Worker an event (#384). */
29633
29789
  notifyHost(event, value) {
29634
29790
  if (!this.#exited) this.#worker.postMessage({ op: "host", event, value });
@@ -29653,25 +29809,33 @@ var BrowserTarget = class _BrowserTarget {
29653
29809
  });
29654
29810
  }
29655
29811
  async #interruptibly(op, params, what) {
29656
- this.#interrupted = false;
29812
+ const interrupts = this.#interrupts;
29657
29813
  try {
29658
29814
  return await this.#request(op, params, true);
29659
29815
  } catch (err) {
29660
- if (err instanceof Abandoned || this.#interrupted) throw new Interrupted(`${what} interrupted`);
29816
+ if (err instanceof Abandoned || this.#interrupts !== interrupts) throw new Interrupted(`${what} interrupted`);
29661
29817
  throw err;
29662
29818
  }
29663
29819
  }
29664
29820
  async evaluate(script) {
29665
- return asEvaluateResult(await this.#interruptibly("eval", { code: script }, "Execution"));
29821
+ this.#cells++;
29822
+ try {
29823
+ return asEvaluateResult(await this.#interruptibly("eval", { code: script }, "Execution"));
29824
+ } finally {
29825
+ this.#cells--;
29826
+ }
29666
29827
  }
29667
29828
  /**
29668
29829
  * A cell that's waiting on something leaves the Worker free: it answers a
29669
- * ping, and the cell is abandoned with its state intact. A busy cell
29670
- * can't be reached, so the Worker is terminated, and a new one starts on
29671
- * the next cell.
29830
+ * ping, and the cell is abandoned with its state intact; host requests
29831
+ * in flight are cancelled on the page, and reject in the Worker with an
29832
+ * AbortError (#388), and jupyter.signal is aborted (#418). A busy cell
29833
+ * can't be reached, so the Worker is
29834
+ * terminated, and a new one starts on the next cell.
29672
29835
  */
29673
29836
  async interrupt() {
29674
- this.#interrupted = true;
29837
+ this.#interrupts++;
29838
+ const cellRunning = this.#cells > 0;
29675
29839
  const running = [...this.#interruptible];
29676
29840
  const answered = await Promise.race([
29677
29841
  this.#request("ping").then(() => true, () => false),
@@ -29683,6 +29847,9 @@ var BrowserTarget = class _BrowserTarget {
29683
29847
  this.#pending.delete(id);
29684
29848
  this.#interruptible.delete(id);
29685
29849
  }
29850
+ this.#cancelHostCalls();
29851
+ void this.#request("abort", { cell: cellRunning }).catch(() => {
29852
+ });
29686
29853
  return;
29687
29854
  }
29688
29855
  this.#stopNote = RESTART_NOTE;
@@ -29697,7 +29864,13 @@ var BrowserTarget = class _BrowserTarget {
29697
29864
  await this.#request("broadcast", { count: executionCount, handle: result.objectId });
29698
29865
  }
29699
29866
  async dispatchComm(msgType, content, metadata, buffers) {
29700
- const reply = await this.#interruptibly("comm", { msgType, content, metadata, buffers: buffers.map(toBase64) }, "Comm handler");
29867
+ const reply = await this.#interruptibly("comm", {
29868
+ msgType,
29869
+ content,
29870
+ metadata,
29871
+ buffers: buffers.map(toBase64),
29872
+ ownSignal: this.#cells === 0
29873
+ }, "Comm handler");
29701
29874
  if (isRecord(reply.error)) {
29702
29875
  const { name, message, stack } = reply.error;
29703
29876
  return { error: { name: String(name), message: String(message), stack: String(stack) } };