@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.
@@ -58,9 +58,11 @@ export declare class BrowserTarget implements KernelTarget {
58
58
  evaluate(script: string): Promise<EvaluateResult>;
59
59
  /**
60
60
  * A cell that's waiting on something leaves the Worker free: it answers a
61
- * ping, and the cell is abandoned with its state intact. A busy cell
62
- * can't be reached, so the Worker is terminated, and a new one starts on
63
- * the next cell.
61
+ * ping, and the cell is abandoned with its state intact; host requests
62
+ * in flight are cancelled on the page, and reject in the Worker with an
63
+ * AbortError (#388), and jupyter.signal is aborted (#418). A busy cell
64
+ * can't be reached, so the Worker is
65
+ * terminated, and a new one starts on the next cell.
64
66
  */
65
67
  interrupt(): Promise<void>;
66
68
  /** Output arrives before the reply it precedes. */
@@ -27,7 +27,10 @@ export class BrowserTarget {
27
27
  #hostCalls = new Map();
28
28
  #host;
29
29
  #nextId = 0;
30
- #interrupted = false;
30
+ /** Counts interrupt() calls: a call that saw one start during it was interrupted (#417). */
31
+ #interrupts = 0;
32
+ /** Cells running: a comm handler that starts with none gets its own jupyter.signal (#421). */
33
+ #cells = 0;
31
34
  #stopNote;
32
35
  #exited = false;
33
36
  constructor(worker, events, host) {
@@ -107,6 +110,15 @@ export class BrowserTarget {
107
110
  resolve(host({ method: String(msg.method), params: msg.params }, controller.signal));
108
111
  }).then((result) => answer({ result }), (err) => answer({ error: describeHostError(err) }));
109
112
  }
113
+ /** Aborts each host request in flight and rejects it in the Worker. */
114
+ #cancelHostCalls() {
115
+ const calls = [...this.#hostCalls];
116
+ this.#hostCalls.clear();
117
+ for (const [id, controller] of calls) {
118
+ controller.abort();
119
+ this.#worker.postMessage({ op: "host", id, error: { name: "AbortError", message: "interrupted" } });
120
+ }
121
+ }
110
122
  /** Sends `host.onEvent` listeners in the Worker an event (#384). */
111
123
  notifyHost(event, value) {
112
124
  if (!this.#exited)
@@ -139,27 +151,37 @@ export class BrowserTarget {
139
151
  });
140
152
  }
141
153
  async #interruptibly(op, params, what) {
142
- this.#interrupted = false;
154
+ const interrupts = this.#interrupts;
143
155
  try {
144
156
  return await this.#request(op, params, true);
145
157
  }
146
158
  catch (err) {
147
- if (err instanceof Abandoned || this.#interrupted)
159
+ if (err instanceof Abandoned || this.#interrupts !== interrupts)
148
160
  throw new Interrupted(`${what} interrupted`);
149
161
  throw err;
150
162
  }
151
163
  }
152
164
  async evaluate(script) {
153
- return asEvaluateResult(await this.#interruptibly("eval", { code: script }, "Execution"));
165
+ this.#cells++;
166
+ try {
167
+ return asEvaluateResult(await this.#interruptibly("eval", { code: script }, "Execution"));
168
+ }
169
+ finally {
170
+ this.#cells--;
171
+ }
154
172
  }
155
173
  /**
156
174
  * A cell that's waiting on something leaves the Worker free: it answers a
157
- * ping, and the cell is abandoned with its state intact. A busy cell
158
- * can't be reached, so the Worker is terminated, and a new one starts on
159
- * the next cell.
175
+ * ping, and the cell is abandoned with its state intact; host requests
176
+ * in flight are cancelled on the page, and reject in the Worker with an
177
+ * AbortError (#388), and jupyter.signal is aborted (#418). A busy cell
178
+ * can't be reached, so the Worker is
179
+ * terminated, and a new one starts on the next cell.
160
180
  */
161
181
  async interrupt() {
162
- this.#interrupted = true;
182
+ this.#interrupts++;
183
+ // Before abandoning the cell's request, which ends the count.
184
+ const cellRunning = this.#cells > 0;
163
185
  const running = [...this.#interruptible];
164
186
  const answered = await Promise.race([
165
187
  this.#request("ping").then(() => true, () => false),
@@ -171,6 +193,11 @@ export class BrowserTarget {
171
193
  this.#pending.delete(id);
172
194
  this.#interruptible.delete(id);
173
195
  }
196
+ this.#cancelHostCalls();
197
+ // After the cell is abandoned, so its reply can't end it first
198
+ // (#418). Not awaited: requests are answered in order, and an abort
199
+ // listener that never returns mustn't hold up the interrupt.
200
+ void this.#request("abort", { cell: cellRunning }).catch(() => { });
174
201
  return;
175
202
  }
176
203
  this.#stopNote = RESTART_NOTE;
@@ -186,7 +213,13 @@ export class BrowserTarget {
186
213
  await this.#request("broadcast", { count: executionCount, handle: result.objectId });
187
214
  }
188
215
  async dispatchComm(msgType, content, metadata, buffers) {
189
- const reply = await this.#interruptibly("comm", { msgType, content, metadata, buffers: buffers.map(toBase64) }, "Comm handler");
216
+ const reply = await this.#interruptibly("comm", {
217
+ msgType,
218
+ content,
219
+ metadata,
220
+ buffers: buffers.map(toBase64),
221
+ ownSignal: this.#cells === 0,
222
+ }, "Comm handler");
190
223
  if (isRecord(reply.error)) {
191
224
  const { name, message, stack } = reply.error;
192
225
  return { error: { name: String(name), message: String(message), stack: String(stack) } };
@@ -604,8 +604,35 @@ function createJupyter(host2) {
604
604
  return answer2 === "" ? defaultValue : answer2;
605
605
  }
606
606
  const comms = createComms((msgType, content, metadata, buffers) => host2.send(msgType, content, metadata, buffers));
607
+ let cell = new AbortController();
608
+ let current = cell;
609
+ const handlers = /* @__PURE__ */ new Map();
610
+ function startCell() {
611
+ current = cell = new AbortController();
612
+ }
613
+ function abortSignal(cellRunning = true) {
614
+ const reason = new DOMException("Execution interrupted", "AbortError");
615
+ if (cellRunning) cell.abort(reason);
616
+ for (const controller of handlers.keys()) controller.abort(reason);
617
+ handlers.clear();
618
+ }
619
+ async function commDispatch(msgType, content, metadata, buffers, ownSignal = false) {
620
+ const controller = ownSignal ? new AbortController() : cell;
621
+ if (ownSignal) current = controller;
622
+ handlers.set(controller, (handlers.get(controller) ?? 0) + 1);
623
+ try {
624
+ return await comms.dispatch(msgType, content, metadata, buffers);
625
+ } finally {
626
+ const left = (handlers.get(controller) ?? 0) - 1;
627
+ if (left > 0) handlers.set(controller, left);
628
+ else handlers.delete(controller);
629
+ }
630
+ }
607
631
  return {
608
632
  namespace: {
633
+ get signal() {
634
+ return current.signal;
635
+ },
609
636
  broadcast,
610
637
  display,
611
638
  format: format2,
@@ -621,7 +648,9 @@ function createJupyter(host2) {
621
648
  formatNow,
622
649
  confirm,
623
650
  prompt,
624
- commDispatch: comms.dispatch
651
+ commDispatch,
652
+ startCell,
653
+ abortSignal
625
654
  };
626
655
  }
627
656
 
@@ -653,6 +682,7 @@ function createHandler(shim, evaluate = (code) => (0, eval)(code)) {
653
682
  return {};
654
683
  case "eval": {
655
684
  handles.clear();
685
+ shim.startCell?.();
656
686
  try {
657
687
  const box = await evaluate(request.code);
658
688
  return { result: box === void 0 ? { type: "undefined" } : remote(box[0]) };
@@ -671,7 +701,13 @@ function createHandler(shim, evaluate = (code) => (0, eval)(code)) {
671
701
  return { value: shim.describeError(take(request.handle)) };
672
702
  case "comm":
673
703
  try {
674
- const handled = await shim.commDispatch(request.msgType, request.content, request.metadata, request.buffers);
704
+ const handled = await shim.commDispatch(
705
+ request.msgType,
706
+ request.content,
707
+ request.metadata,
708
+ request.buffers,
709
+ request.ownSignal === true
710
+ );
675
711
  return { handled: handled === true };
676
712
  } catch (err) {
677
713
  return { error: JSON.parse(shim.describeError(err)) };
@@ -693,6 +729,9 @@ function createHandler(shim, evaluate = (code) => (0, eval)(code)) {
693
729
  return { text: shim.inspectPath(request.path, request.detail ?? 0) };
694
730
  case "barrier":
695
731
  return { value: shim.barrier() };
732
+ case "abort":
733
+ shim.abortSignal?.(request.cell !== false);
734
+ return {};
696
735
  default:
697
736
  throw new Error(`unknown request ${JSON.stringify(request.op)}`);
698
737
  }
@@ -1034,9 +1073,13 @@ function install({ base, cdn: cdnBase }) {
1034
1073
  broadcastResult: jupyter.broadcastResult,
1035
1074
  // execute_request's user_expressions (#340).
1036
1075
  formatJson: async (value) => JSON.stringify(await jupyter.namespace.format(value)),
1037
- async commDispatch(msgType, content, metadata, buffers64) {
1076
+ // jupyter.signal (#418, #421): a new one as each cell starts; the host
1077
+ // aborts the running ones on interrupt.
1078
+ startCell: jupyter.startCell,
1079
+ abortSignal: jupyter.abortSignal,
1080
+ async commDispatch(msgType, content, metadata, buffers64, ownSignal) {
1038
1081
  const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
1039
- return await jupyter.commDispatch(msgType, content, metadata, buffers);
1082
+ return await jupyter.commDispatch(msgType, content, metadata, buffers, ownSignal);
1040
1083
  },
1041
1084
  describeError(value) {
1042
1085
  if (value instanceof Error) {