@tlockney/cdp-kernel 0.7.2 → 0.9.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.
- package/README.md +4 -2
- package/dist/browser/kernel.js +211 -42
- package/dist/browser/kernel.js.map +2 -2
- package/dist/browser/target.d.ts +2 -1
- package/dist/browser/target.js +29 -7
- package/dist/browser/worker.js +47 -4
- package/dist/browser/worker.js.map +2 -2
- package/dist/bun-target.d.ts +5 -1
- package/dist/bun-target.js +32 -7
- package/dist/crash-reports.d.ts +47 -0
- package/dist/crash-reports.js +155 -0
- package/dist/globals.d.ts +35 -4
- package/dist/kernel-core.d.ts +7 -0
- package/dist/kernel-core.js +215 -35
- package/dist/kernel.js +10 -0
- package/dist/main.js +31 -14
- package/dist/message.d.ts +1 -1
- package/dist/message.js +1 -1
- package/dist/package.d.ts +2 -2
- package/dist/package.js +2 -2
- package/dist/target-api.d.ts +11 -0
- package/dist/target.d.ts +9 -2
- package/dist/target.js +66 -12
- package/dist/transform.d.ts +10 -1
- package/dist/transform.js +23 -3
- package/dist/usage.d.ts +25 -0
- package/dist/usage.js +197 -0
- package/dist/workerd-target.d.ts +13 -2
- package/dist/workerd-target.js +36 -6
- package/dist/workers-project.js +2 -0
- package/package.json +1 -1
- package/runtime/bootstrap.mjs +65 -7
- package/runtime/browser-worker.mjs +6 -2
- package/runtime/comm.d.mts +87 -1
- package/runtime/display.d.mts +261 -4
- package/runtime/display.mjs +42 -1
- package/runtime/evaluator.d.mts +5 -1
- package/runtime/evaluator.mjs +12 -1
- package/runtime/widget.d.mts +205 -6
- package/runtime/workerd-worker.mjs +6 -2
package/runtime/bootstrap.mjs
CHANGED
|
@@ -97,7 +97,9 @@ const jupyter = createJupyter({
|
|
|
97
97
|
const reply = JSON.parse(line);
|
|
98
98
|
if (reply.interrupted === true) {
|
|
99
99
|
// Interrupted while waiting for the answer: unwind the cell, as
|
|
100
|
-
// KeyboardInterrupt does in Python's input() (#121).
|
|
100
|
+
// KeyboardInterrupt does in Python's input() (#121). The host
|
|
101
|
+
// doesn't interrupt the target then, so abort jupyter.signal here.
|
|
102
|
+
jupyter.abortSignal(reply.cell !== false);
|
|
101
103
|
const err = new Error("Execution interrupted");
|
|
102
104
|
err.name = "Interrupted";
|
|
103
105
|
throw err;
|
|
@@ -217,6 +219,10 @@ globalThis.__cdpk = {
|
|
|
217
219
|
broadcastResult: jupyter.broadcastResult,
|
|
218
220
|
// execute_request's user_expressions (#340).
|
|
219
221
|
formatJson: async (value) => JSON.stringify(await jupyter.namespace.format(value)),
|
|
222
|
+
// jupyter.signal (#418, #421): a new one as each cell starts; the host
|
|
223
|
+
// aborts the running ones on interrupt.
|
|
224
|
+
startCell: jupyter.startCell,
|
|
225
|
+
abortSignal: jupyter.abortSignal,
|
|
220
226
|
// The debugger's rich variable viewer (#18), synchronously: it runs while
|
|
221
227
|
// the target is paused. A value whose $display is async shows as text.
|
|
222
228
|
richBundle(value) {
|
|
@@ -225,10 +231,10 @@ globalThis.__cdpk = {
|
|
|
225
231
|
},
|
|
226
232
|
// Frontend comm messages (#9), from the host's shell channel. Handlers
|
|
227
233
|
// run after one macrotask, like cells, so interrupt can reach them.
|
|
228
|
-
async commDispatch(msgType, content, metadata, buffers64) {
|
|
234
|
+
async commDispatch(msgType, content, metadata, buffers64, ownSignal) {
|
|
229
235
|
await yieldTask();
|
|
230
236
|
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
231
|
-
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|
|
237
|
+
return await jupyter.commDispatch(msgType, content, metadata, buffers, ownSignal);
|
|
232
238
|
},
|
|
233
239
|
// The same, for a message the host wrote to stdin (Node and Deno). A
|
|
234
240
|
// value passed as a CDP call argument is compiled as source, and V8's
|
|
@@ -236,8 +242,23 @@ globalThis.__cdpk = {
|
|
|
236
242
|
// each message's data would stay in memory (#287). Read before the first
|
|
237
243
|
// await, so messages are taken in the order the host wrote them.
|
|
238
244
|
commDispatchFromStdin() {
|
|
239
|
-
const
|
|
240
|
-
|
|
245
|
+
const dispatch = () => {
|
|
246
|
+
const { msgType, content, metadata, buffers, ownSignal } = readCommRecord();
|
|
247
|
+
return globalThis.__cdpk.commDispatch(msgType, content, metadata, buffers, ownSignal === true);
|
|
248
|
+
};
|
|
249
|
+
// Node's inspector can run this call in the middle of a blocking stdin
|
|
250
|
+
// read, e.g. a prompt's, when the message came on a kernel subshell
|
|
251
|
+
// (#417): reading then would take bytes from the middle of that read.
|
|
252
|
+
if (stdinReading) {
|
|
253
|
+
const resumed = new Promise((resolve) => afterStdinRead.push(resolve));
|
|
254
|
+
// A read is synchronous, so by the next task it has ended. If the flag
|
|
255
|
+
// is still set, an interrupt terminated the read before its finally.
|
|
256
|
+
yieldTask().then(() => {
|
|
257
|
+
if (stdinReading) endStdinRead();
|
|
258
|
+
});
|
|
259
|
+
return resumed.then(dispatch);
|
|
260
|
+
}
|
|
261
|
+
return dispatch();
|
|
241
262
|
},
|
|
242
263
|
describeError(value) {
|
|
243
264
|
if (value instanceof Error) {
|
|
@@ -445,18 +466,55 @@ function readLine() {
|
|
|
445
466
|
}
|
|
446
467
|
}
|
|
447
468
|
|
|
448
|
-
/**
|
|
469
|
+
/**
|
|
470
|
+
* Comm records read while waiting for a prompt's reply. A comm message on
|
|
471
|
+
* a kernel subshell (#417) can be written while a cell waits in input();
|
|
472
|
+
* its dispatch runs after, and takes it from here.
|
|
473
|
+
*/
|
|
474
|
+
const commRecords = [];
|
|
475
|
+
|
|
476
|
+
/** Whether a read from stdin is in progress, and what waits for it to end. */
|
|
477
|
+
let stdinReading = false;
|
|
478
|
+
const afterStdinRead = [];
|
|
479
|
+
|
|
480
|
+
function readingStdin(read) {
|
|
481
|
+
stdinReading = true;
|
|
482
|
+
try {
|
|
483
|
+
return read();
|
|
484
|
+
} finally {
|
|
485
|
+
endStdinRead();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function endStdinRead() {
|
|
490
|
+
stdinReading = false;
|
|
491
|
+
// In order, so comm records are taken as the host wrote them.
|
|
492
|
+
for (const resume of afterStdinRead.splice(0)) resume();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** The next prompt reply's line; comm records before it are kept. */
|
|
449
496
|
function readInputReply() {
|
|
497
|
+
return readingStdin(readInputReplyNow);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function readInputReplyNow() {
|
|
450
501
|
for (;;) {
|
|
451
502
|
const line = readLine();
|
|
452
503
|
const comm = line === null ? null : COMM_HEADER.exec(line);
|
|
453
504
|
if (comm === null) return line;
|
|
454
|
-
readExactly(Number(comm[1]));
|
|
505
|
+
const body = readExactly(Number(comm[1]));
|
|
506
|
+
if (body !== null) commRecords.push(body);
|
|
455
507
|
}
|
|
456
508
|
}
|
|
457
509
|
|
|
458
510
|
/** The next comm message, past any prompt reply left unread. */
|
|
459
511
|
function readCommRecord() {
|
|
512
|
+
return readingStdin(readCommRecordNow);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function readCommRecordNow() {
|
|
516
|
+
const kept = commRecords.shift();
|
|
517
|
+
if (kept !== undefined) return JSON.parse(new TextDecoder().decode(kept));
|
|
460
518
|
for (;;) {
|
|
461
519
|
const line = readLine();
|
|
462
520
|
if (line === null) throw new Error("stdin closed before a comm message arrived");
|
|
@@ -164,9 +164,13 @@ function install({ base, cdn: cdnBase }) {
|
|
|
164
164
|
broadcastResult: jupyter.broadcastResult,
|
|
165
165
|
// execute_request's user_expressions (#340).
|
|
166
166
|
formatJson: async (value) => JSON.stringify(await jupyter.namespace.format(value)),
|
|
167
|
-
|
|
167
|
+
// jupyter.signal (#418, #421): a new one as each cell starts; the host
|
|
168
|
+
// aborts the running ones on interrupt.
|
|
169
|
+
startCell: jupyter.startCell,
|
|
170
|
+
abortSignal: jupyter.abortSignal,
|
|
171
|
+
async commDispatch(msgType, content, metadata, buffers64, ownSignal) {
|
|
168
172
|
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
169
|
-
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|
|
173
|
+
return await jupyter.commDispatch(msgType, content, metadata, buffers, ownSignal);
|
|
170
174
|
},
|
|
171
175
|
describeError(value) {
|
|
172
176
|
if (value instanceof Error) {
|
package/runtime/comm.d.mts
CHANGED
|
@@ -1,30 +1,116 @@
|
|
|
1
1
|
/** A comm message as a handler receives it. */
|
|
2
2
|
export interface CommMessage {
|
|
3
|
+
/** The message's JSON data. */
|
|
3
4
|
data: { [key: string]: unknown };
|
|
5
|
+
/** The message's metadata. */
|
|
4
6
|
metadata: { [key: string]: unknown };
|
|
7
|
+
/** Binary buffers sent with the message. */
|
|
5
8
|
buffers: Uint8Array[];
|
|
6
9
|
}
|
|
7
10
|
|
|
11
|
+
/** Options for sending, opening or closing a comm. */
|
|
8
12
|
export interface CommSendOptions {
|
|
13
|
+
/** Metadata for the message. Default `{}`. */
|
|
9
14
|
metadata?: { [key: string]: unknown };
|
|
15
|
+
/** Binary buffers sent with the message. Default `[]`. */
|
|
10
16
|
buffers?: Uint8Array[];
|
|
11
17
|
}
|
|
12
18
|
|
|
13
|
-
/** One end of a Jupyter comm channel. */
|
|
19
|
+
/** One end of a Jupyter comm channel, from `jupyter.comm.open` or a target handler. */
|
|
14
20
|
export interface Comm {
|
|
21
|
+
/** The comm id, shared with the frontend's end. */
|
|
15
22
|
readonly id: string;
|
|
23
|
+
/** The target the comm was opened on. */
|
|
16
24
|
readonly targetName: string;
|
|
25
|
+
/** Whether either end has closed the comm. */
|
|
17
26
|
readonly closed: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Send a message to the frontend's end. Does nothing once closed.
|
|
29
|
+
*
|
|
30
|
+
* @param data The message's JSON data. Default `{}`.
|
|
31
|
+
* @param options Metadata and binary buffers.
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const comm = jupyter.comm.open("my.target");
|
|
35
|
+
* comm.send({ ping: true }, { buffers: [new Uint8Array([1, 2, 3])] });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
18
38
|
send(data?: { [key: string]: unknown }, options?: CommSendOptions): void;
|
|
39
|
+
/**
|
|
40
|
+
* Close the comm and tell the frontend. Its `onClose` handlers start
|
|
41
|
+
* right away, without being awaited. Does nothing once closed.
|
|
42
|
+
*
|
|
43
|
+
* @param data Data sent with the close message. Default `{}`.
|
|
44
|
+
* @param options Metadata and binary buffers.
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const comm = jupyter.comm.open("my.target");
|
|
48
|
+
* comm.close({ reason: "done" });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
19
51
|
close(data?: { [key: string]: unknown }, options?: CommSendOptions): void;
|
|
52
|
+
/**
|
|
53
|
+
* Handle messages from the frontend's end. A handler runs like a cell:
|
|
54
|
+
* its output goes to the notebook, and an exception shows on stderr.
|
|
55
|
+
*
|
|
56
|
+
* @param handler Called with each message; may be async.
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* const comm = jupyter.comm.open("my.target");
|
|
60
|
+
* comm.onMsg(({ data, buffers }) => console.log(data, buffers.length));
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
20
63
|
onMsg(handler: (msg: CommMessage) => void | Promise<void>): void;
|
|
64
|
+
/**
|
|
65
|
+
* Handle the comm closing, from either end.
|
|
66
|
+
*
|
|
67
|
+
* @param handler Called with the close message; may be async.
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* const comm = jupyter.comm.open("my.target");
|
|
71
|
+
* comm.onClose(() => console.log("closed"));
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
21
74
|
onClose(handler: (msg: CommMessage) => void | Promise<void>): void;
|
|
22
75
|
}
|
|
23
76
|
|
|
24
77
|
/** `jupyter.comm`: open comms, and accept comms the frontend opens. */
|
|
25
78
|
export interface CommApi {
|
|
79
|
+
/**
|
|
80
|
+
* Open a comm to a target the frontend registered.
|
|
81
|
+
*
|
|
82
|
+
* @param targetName The frontend's target name.
|
|
83
|
+
* @param data Data sent with the open message. Default `{}`.
|
|
84
|
+
* @param options Metadata and binary buffers.
|
|
85
|
+
* @returns The kernel's end of the comm.
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* const comm = jupyter.comm.open("my.target", { hello: 1 });
|
|
89
|
+
* comm.onMsg((m) => console.log(m.data));
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
26
92
|
open(targetName: string, data?: { [key: string]: unknown }, options?: CommSendOptions): Comm;
|
|
93
|
+
/**
|
|
94
|
+
* Accept comms the frontend opens on `targetName`. Registering the same
|
|
95
|
+
* name again replaces the handler.
|
|
96
|
+
*
|
|
97
|
+
* @param targetName The target name the frontend opens comms on.
|
|
98
|
+
* @param handler Called with the new comm and its open message.
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* jupyter.comm.registerTarget("echo", (comm) => comm.onMsg((m) => comm.send(m.data)));
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
27
104
|
registerTarget(targetName: string, handler: (comm: Comm, msg: CommMessage) => void | Promise<void>): void;
|
|
105
|
+
/**
|
|
106
|
+
* Stop accepting new comms on `targetName`. Comms already open stay open.
|
|
107
|
+
*
|
|
108
|
+
* @param targetName The target to remove.
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* jupyter.comm.unregisterTarget("echo");
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
28
114
|
unregisterTarget(targetName: string): void;
|
|
29
115
|
}
|
|
30
116
|
|
package/runtime/display.d.mts
CHANGED
|
@@ -3,8 +3,18 @@ import type { Widget, WidgetOptions } from "./widget.d.mts";
|
|
|
3
3
|
|
|
4
4
|
export type { CommApi, Widget, WidgetOptions };
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The symbol a class implements to choose how its instances appear. At
|
|
8
|
+
* runtime it is `Symbol.for("Jupyter.display")`, so a library can use that
|
|
9
|
+
* form without the `jupyter` global.
|
|
10
|
+
*/
|
|
6
11
|
export const $display: unique symbol;
|
|
7
12
|
|
|
13
|
+
/**
|
|
14
|
+
* A Jupyter MIME bundle: media type to content, such as
|
|
15
|
+
* `{ "text/plain": "42", "text/html": "<b>42</b>" }`. Binary images are
|
|
16
|
+
* base64 strings; JSON types (`application/json`, Vega-Lite) are objects.
|
|
17
|
+
*/
|
|
8
18
|
export type MediaBundle = { [mediaType: string]: unknown };
|
|
9
19
|
|
|
10
20
|
export interface DisplayHost {
|
|
@@ -15,33 +25,264 @@ export interface DisplayHost {
|
|
|
15
25
|
input: (prompt: string, password: boolean) => string | null;
|
|
16
26
|
}
|
|
17
27
|
|
|
28
|
+
/** Options for `jupyter.display`. */
|
|
18
29
|
export interface DisplayOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Send a plain object as the MIME bundle itself instead of formatting
|
|
32
|
+
* it. Default `false`.
|
|
33
|
+
*/
|
|
19
34
|
raw?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Replace every earlier output that has the same `display_id`, including
|
|
37
|
+
* one shown by an earlier cell, instead of adding an output. Needs
|
|
38
|
+
* `display_id`. Default `false`.
|
|
39
|
+
*/
|
|
20
40
|
update?: boolean;
|
|
41
|
+
/** An id that a later display with `update: true` can replace. */
|
|
21
42
|
display_id?: string;
|
|
22
43
|
}
|
|
23
44
|
|
|
45
|
+
/** A value that renders as rich output: it has a `[jupyter.$display]` method. */
|
|
24
46
|
export interface Displayable {
|
|
47
|
+
/** Returns the value's MIME bundle, or a promise of one. */
|
|
25
48
|
[$display]: () => MediaBundle | Promise<MediaBundle>;
|
|
26
49
|
}
|
|
27
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The `jupyter` global: how values appear in the notebook. The same object
|
|
53
|
+
* as `Deno.jupyter` in `deno jupyter`.
|
|
54
|
+
*/
|
|
28
55
|
export interface JupyterNamespace {
|
|
56
|
+
/**
|
|
57
|
+
* Send a raw Jupyter message on the kernel's output channel (IOPub), as a
|
|
58
|
+
* reply to the running cell. The other helpers are built on it. It does
|
|
59
|
+
* not check the message: a wrong field is ignored or mishandled by the
|
|
60
|
+
* frontend.
|
|
61
|
+
*
|
|
62
|
+
* @param msgType An IOPub message type, such as `"clear_output"` or
|
|
63
|
+
* `"display_data"`.
|
|
64
|
+
* @param content The message content, as the messaging spec defines it
|
|
65
|
+
* for `msgType`.
|
|
66
|
+
* @param options `metadata` for the message, and binary `buffers`.
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* // Count down in place: clear_output with wait avoids a flicker.
|
|
70
|
+
* for (const n of [3, 2, 1]) {
|
|
71
|
+
* jupyter.broadcast("clear_output", { wait: true });
|
|
72
|
+
* await jupyter.display(jupyter.md`Launching in **${n}**…`);
|
|
73
|
+
* await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
* @see jupyter-api.md#raw-messages-with-broadcast
|
|
77
|
+
*/
|
|
29
78
|
broadcast(
|
|
30
79
|
msgType: string,
|
|
31
80
|
content: object,
|
|
32
81
|
options?: { metadata?: object; buffers?: Uint8Array[] },
|
|
33
82
|
): void;
|
|
83
|
+
/**
|
|
84
|
+
* Show a value now, in the middle of a cell, or replace one shown
|
|
85
|
+
* earlier. The value can be anything a cell could end with: a template
|
|
86
|
+
* from `md`, `html` or `svg`, an image, a widget, an object with a
|
|
87
|
+
* `[jupyter.$display]` method, or a plain value, shown as text.
|
|
88
|
+
*
|
|
89
|
+
* @param obj The value to show.
|
|
90
|
+
* @param options `display_id` and `update` to replace an earlier output;
|
|
91
|
+
* `raw` to send a plain object as the MIME bundle itself.
|
|
92
|
+
* @returns Resolves once the output is sent. Without `await` the output
|
|
93
|
+
* still appears, but later output in the cell can appear before it.
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* const bar = (pct: number) => jupyter.html`<progress value="${pct}" max="100"></progress>`;
|
|
97
|
+
* await jupyter.display(bar(0), { display_id: "download" });
|
|
98
|
+
* for (let pct = 25; pct <= 100; pct += 25) {
|
|
99
|
+
* await new Promise((resolve) => setTimeout(resolve, 500));
|
|
100
|
+
* await jupyter.display(bar(pct), { display_id: "download", update: true });
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* // A MIME bundle sent as is: JupyterLab renders it as a JSON tree.
|
|
106
|
+
* const user = { name: "Ada", active: true };
|
|
107
|
+
* await jupyter.display(
|
|
108
|
+
* { "application/json": user, "text/plain": JSON.stringify(user) },
|
|
109
|
+
* { raw: true },
|
|
110
|
+
* );
|
|
111
|
+
* ```
|
|
112
|
+
* @see jupyter-api.md#showing-output-with-display
|
|
113
|
+
*/
|
|
34
114
|
display(obj: unknown, options?: DisplayOptions): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* The MIME bundle that displaying a value would send, without showing
|
|
117
|
+
* anything. For plain values `text/plain` holds terminal color codes,
|
|
118
|
+
* as the notebook shows them.
|
|
119
|
+
*
|
|
120
|
+
* @param obj The value to format.
|
|
121
|
+
* @returns The bundle, for example `{ "text/plain": "\x1b[33m42\x1b[39m" }`
|
|
122
|
+
* for `42`.
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* const bundle = await jupyter.format(jupyter.md`# Weekly report`);
|
|
126
|
+
* bundle["text/markdown"]; // "# Weekly report"
|
|
127
|
+
* ```
|
|
128
|
+
* @see jupyter-api.md#getting-a-bundle-with-format
|
|
129
|
+
*/
|
|
35
130
|
format(obj: unknown): Promise<MediaBundle>;
|
|
131
|
+
/**
|
|
132
|
+
* A tagged template that renders as Markdown. Interpolated values are
|
|
133
|
+
* inserted as is, without escaping.
|
|
134
|
+
*
|
|
135
|
+
* @returns A value that shows as `text/markdown` as a cell's result or
|
|
136
|
+
* through `display`.
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* const scores = { Ada: 92, Grace: 88 };
|
|
140
|
+
* const rows = Object.entries(scores).map(([n, s]) => `| ${n} | ${s} |`).join("\n");
|
|
141
|
+
* jupyter.md`| Name | Score |
|
|
142
|
+
* | --- | --- |
|
|
143
|
+
* ${rows}`;
|
|
144
|
+
* ```
|
|
145
|
+
* @see jupyter-api.md#markdown
|
|
146
|
+
*/
|
|
36
147
|
md(strings: TemplateStringsArray, ...values: unknown[]): Displayable;
|
|
148
|
+
/**
|
|
149
|
+
* A tagged template that renders as HTML. Interpolated values are
|
|
150
|
+
* inserted as is, without escaping: escape text from users, files or
|
|
151
|
+
* the network yourself.
|
|
152
|
+
*
|
|
153
|
+
* @returns A value that shows as `text/html`.
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* const escape = (s: string) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
157
|
+
* const comment = "<script>alert(1)</script>";
|
|
158
|
+
* jupyter.html`<p>${escape(comment)}</p>`;
|
|
159
|
+
* ```
|
|
160
|
+
* @see jupyter-api.md#html
|
|
161
|
+
*/
|
|
37
162
|
html(strings: TemplateStringsArray, ...values: unknown[]): Displayable;
|
|
163
|
+
/**
|
|
164
|
+
* A tagged template that renders as SVG. Include the `xmlns` attribute.
|
|
165
|
+
* Interpolated values are inserted as is, without escaping.
|
|
166
|
+
*
|
|
167
|
+
* @returns A value that shows as `image/svg+xml`.
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* const data = [4, 8, 15, 16];
|
|
171
|
+
* const bars = data.map((v, i) => `<rect x="${i * 20}" y="${40 - v * 2}" width="16" height="${v * 2}"/>`).join("");
|
|
172
|
+
* jupyter.svg`<svg xmlns="http://www.w3.org/2000/svg" width="80" height="40">${bars}</svg>`;
|
|
173
|
+
* ```
|
|
174
|
+
* @see jupyter-api.md#svg
|
|
175
|
+
*/
|
|
38
176
|
svg(strings: TemplateStringsArray, ...values: unknown[]): Displayable;
|
|
177
|
+
/**
|
|
178
|
+
* Show a PNG or JPEG. The format is read from the bytes, not the file
|
|
179
|
+
* name.
|
|
180
|
+
*
|
|
181
|
+
* @param obj A file path, relative to the notebook's directory, or the
|
|
182
|
+
* image's bytes.
|
|
183
|
+
* @returns A value that shows as `image/png` or `image/jpeg`.
|
|
184
|
+
* @throws {TypeError} The bytes are not PNG or JPEG, or the path can't
|
|
185
|
+
* be read.
|
|
186
|
+
* @availability Paths need a file system: on workerd and celld pass
|
|
187
|
+
* bytes.
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* jupyter.image("./chart.png");
|
|
191
|
+
* ```
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* const response = await fetch("https://www.python.org/static/img/python-logo.png");
|
|
195
|
+
* jupyter.image(new Uint8Array(await response.arrayBuffer()));
|
|
196
|
+
* ```
|
|
197
|
+
* @see jupyter-api.md#images
|
|
198
|
+
*/
|
|
39
199
|
image(obj: string | Uint8Array): Displayable;
|
|
200
|
+
/**
|
|
201
|
+
* The symbol a class implements to choose how its instances appear. The
|
|
202
|
+
* method returns a MIME bundle, or a promise of one. It is used for a
|
|
203
|
+
* cell's result, `display` and `format`, but not for objects nested in
|
|
204
|
+
* other values. At runtime it is `Symbol.for("Jupyter.display")`.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```ts
|
|
208
|
+
* class Temperature {
|
|
209
|
+
* celsius: number;
|
|
210
|
+
* constructor(celsius: number) {
|
|
211
|
+
* this.celsius = celsius;
|
|
212
|
+
* }
|
|
213
|
+
* [jupyter.$display]() {
|
|
214
|
+
* return { "text/plain": `${this.celsius}°C`, "text/html": `<b>${this.celsius}°C</b>` };
|
|
215
|
+
* }
|
|
216
|
+
* }
|
|
217
|
+
* new Temperature(21);
|
|
218
|
+
* ```
|
|
219
|
+
* @see jupyter-api.md#custom-display-for-your-own-classes
|
|
220
|
+
*/
|
|
40
221
|
$display: typeof $display;
|
|
41
|
-
/**
|
|
222
|
+
/**
|
|
223
|
+
* Jupyter comms: message channels to the frontend outside cell
|
|
224
|
+
* execution. Its methods are in the widgets and comms reference.
|
|
225
|
+
*
|
|
226
|
+
* @see widgets.md#comms
|
|
227
|
+
*/
|
|
42
228
|
comm: CommApi;
|
|
43
|
-
/**
|
|
229
|
+
/**
|
|
230
|
+
* Create an anywidget-compatible widget model. Display it (or end the
|
|
231
|
+
* cell with it) to show a view.
|
|
232
|
+
*
|
|
233
|
+
* @param options The view module, its CSS, and the initial state.
|
|
234
|
+
* @returns The model, synced with every view of it.
|
|
235
|
+
* @throws {TypeError} `options.esm` is not a string.
|
|
236
|
+
* @availability The frontend needs the anywidget extension.
|
|
237
|
+
* @example
|
|
238
|
+
* ```ts
|
|
239
|
+
* const counter = jupyter.widget({
|
|
240
|
+
* esm: `export default {
|
|
241
|
+
* render({ model, el }) {
|
|
242
|
+
* const button = document.createElement("button");
|
|
243
|
+
* const update = () => {
|
|
244
|
+
* button.textContent = "count is " + model.get("count");
|
|
245
|
+
* };
|
|
246
|
+
* button.onclick = () => {
|
|
247
|
+
* model.set("count", model.get("count") + 1);
|
|
248
|
+
* model.save_changes();
|
|
249
|
+
* };
|
|
250
|
+
* model.on("change:count", update);
|
|
251
|
+
* update();
|
|
252
|
+
* el.append(button);
|
|
253
|
+
* },
|
|
254
|
+
* };`,
|
|
255
|
+
* state: { count: 0 },
|
|
256
|
+
* });
|
|
257
|
+
* counter; // the cell's result: displays the widget
|
|
258
|
+
* ```
|
|
259
|
+
* 
|
|
260
|
+
* @see widgets.md#a-first-widget-a-counter
|
|
261
|
+
*/
|
|
44
262
|
widget(options: WidgetOptions): Widget;
|
|
263
|
+
/**
|
|
264
|
+
* An `AbortSignal` that an interrupt aborts, so code waiting on something
|
|
265
|
+
* only it can end can stop and clean up. Each cell gets its own, as does
|
|
266
|
+
* a comm handler that starts while no cell runs. Read it once when the
|
|
267
|
+
* work starts: a later read gets the signal of whatever started last.
|
|
268
|
+
* The abort reason is a `DOMException` named `AbortError`.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* const response = await fetch("https://example.com/large.csv", { signal: jupyter.signal });
|
|
273
|
+
* ```
|
|
274
|
+
* @example
|
|
275
|
+
* ```ts
|
|
276
|
+
* const signal = jupyter.signal;
|
|
277
|
+
* const ask = jupyter.comm.open("my.app", { question: "ready?" });
|
|
278
|
+
* const answer = await new Promise((resolve, reject) => {
|
|
279
|
+
* ask.onMsg((m) => resolve(m.data));
|
|
280
|
+
* signal.addEventListener("abort", () => { ask.close(); reject(signal.reason); }, { once: true });
|
|
281
|
+
* });
|
|
282
|
+
* ```
|
|
283
|
+
* @see jupyter-api.md#stopping-on-interrupt-jupyter-signal
|
|
284
|
+
*/
|
|
285
|
+
readonly signal: AbortSignal;
|
|
45
286
|
}
|
|
46
287
|
|
|
47
288
|
export interface Jupyter {
|
|
@@ -51,8 +292,24 @@ export interface Jupyter {
|
|
|
51
292
|
formatNow(obj: unknown): unknown;
|
|
52
293
|
confirm(message?: string): boolean;
|
|
53
294
|
prompt(message?: string, defaultValue?: string, options?: { password?: boolean }): string | null;
|
|
54
|
-
/**
|
|
55
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Routes a frontend comm message into the kernel's comms. `ownSignal`
|
|
297
|
+
* (no cell is running) gives its handlers their own jupyter.signal.
|
|
298
|
+
*/
|
|
299
|
+
commDispatch(
|
|
300
|
+
msgType: string,
|
|
301
|
+
content: { [key: string]: unknown },
|
|
302
|
+
metadata: { [key: string]: unknown },
|
|
303
|
+
buffers: Uint8Array[],
|
|
304
|
+
ownSignal?: boolean,
|
|
305
|
+
): Promise<boolean>;
|
|
306
|
+
/** A new jupyter.signal: a cell is starting (#421). */
|
|
307
|
+
startCell(): void;
|
|
308
|
+
/**
|
|
309
|
+
* Aborts the running comm handlers' jupyter.signal, and the last cell's
|
|
310
|
+
* unless `cellRunning` is false: the kernel was interrupted.
|
|
311
|
+
*/
|
|
312
|
+
abortSignal(cellRunning?: boolean): void;
|
|
56
313
|
}
|
|
57
314
|
|
|
58
315
|
export function escapeHTML(str: string): string;
|
package/runtime/display.mjs
CHANGED
|
@@ -441,8 +441,47 @@ export function createJupyter(host) {
|
|
|
441
441
|
|
|
442
442
|
const comms = createComms((msgType, content, metadata, buffers) => host.send(msgType, content, metadata, buffers));
|
|
443
443
|
|
|
444
|
+
// jupyter.signal (#418): aborted when the kernel is interrupted, so cell
|
|
445
|
+
// code can stop waiting on what the interrupt can't reach, such as a comm
|
|
446
|
+
// reply. Each cell gets its own, as does a comm handler that starts while
|
|
447
|
+
// no cell runs; a handler during a cell shares the cell's. An interrupt
|
|
448
|
+
// aborts only those of the cell and handlers running (#421).
|
|
449
|
+
/** The last cell's controller. */
|
|
450
|
+
let cell = new AbortController();
|
|
451
|
+
/** What jupyter.signal returns: that of whatever started last. */
|
|
452
|
+
let current = cell;
|
|
453
|
+
/** Controllers of running comm handlers, with how many handlers use each. */
|
|
454
|
+
const handlers = new Map();
|
|
455
|
+
function startCell() {
|
|
456
|
+
current = cell = new AbortController();
|
|
457
|
+
}
|
|
458
|
+
/** `cellRunning`: the host has a cell in flight, so the last cell's signal is aborted too. */
|
|
459
|
+
function abortSignal(cellRunning = true) {
|
|
460
|
+
const reason = new DOMException("Execution interrupted", "AbortError");
|
|
461
|
+
if (cellRunning) cell.abort(reason);
|
|
462
|
+
for (const controller of handlers.keys()) controller.abort(reason);
|
|
463
|
+
handlers.clear();
|
|
464
|
+
}
|
|
465
|
+
async function commDispatch(msgType, content, metadata, buffers, ownSignal = false) {
|
|
466
|
+
// A handler during a cell shares the cell's signal, and is tracked
|
|
467
|
+
// apart from it: it can outlive the cell.
|
|
468
|
+
const controller = ownSignal ? new AbortController() : cell;
|
|
469
|
+
if (ownSignal) current = controller;
|
|
470
|
+
handlers.set(controller, (handlers.get(controller) ?? 0) + 1);
|
|
471
|
+
try {
|
|
472
|
+
return await comms.dispatch(msgType, content, metadata, buffers);
|
|
473
|
+
} finally {
|
|
474
|
+
const left = (handlers.get(controller) ?? 0) - 1;
|
|
475
|
+
if (left > 0) handlers.set(controller, left);
|
|
476
|
+
else handlers.delete(controller);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
444
480
|
return {
|
|
445
481
|
namespace: {
|
|
482
|
+
get signal() {
|
|
483
|
+
return current.signal;
|
|
484
|
+
},
|
|
446
485
|
broadcast,
|
|
447
486
|
display,
|
|
448
487
|
format,
|
|
@@ -458,6 +497,8 @@ export function createJupyter(host) {
|
|
|
458
497
|
formatNow,
|
|
459
498
|
confirm,
|
|
460
499
|
prompt,
|
|
461
|
-
commDispatch
|
|
500
|
+
commDispatch,
|
|
501
|
+
startCell,
|
|
502
|
+
abortSignal,
|
|
462
503
|
};
|
|
463
504
|
}
|
package/runtime/evaluator.d.mts
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
export interface EvaluatorShim {
|
|
3
3
|
broadcastResult(executionCount: number, value: unknown): void | Promise<void>;
|
|
4
4
|
describeError(value: unknown): string;
|
|
5
|
-
commDispatch(msgType: string, content: object, metadata: object, buffers64: string[]): Promise<unknown>;
|
|
5
|
+
commDispatch(msgType: string, content: object, metadata: object, buffers64: string[], ownSignal?: boolean): Promise<unknown>;
|
|
6
6
|
barrier(): number;
|
|
7
7
|
/** inspect_request's text for a dotted path, or null (#10). */
|
|
8
8
|
inspectPath(path: string, detail: number): string | null;
|
|
9
9
|
/** Resolves a dotted path without running getters; see introspect.mjs. */
|
|
10
10
|
resolvePath?(path: string): import("./introspect.mjs").Resolved;
|
|
11
|
+
/** A new jupyter.signal, as a cell starts (#421). */
|
|
12
|
+
startCell?(): void;
|
|
13
|
+
/** Aborts the running cell's and comm handlers' jupyter.signal, after an interrupt (#418). */
|
|
14
|
+
abortSignal?(cellRunning?: boolean): void;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
/** A request from the host: an `op` plus its parameters. */
|
package/runtime/evaluator.mjs
CHANGED
|
@@ -44,6 +44,7 @@ export function createHandler(shim, evaluate = (code) => (0, eval)(code)) {
|
|
|
44
44
|
case "eval": {
|
|
45
45
|
// The host uses a result's handle before its next cell.
|
|
46
46
|
handles.clear();
|
|
47
|
+
shim.startCell?.();
|
|
47
48
|
try {
|
|
48
49
|
const box = await evaluate(request.code);
|
|
49
50
|
return { result: box === undefined ? { type: "undefined" } : remote(box[0]) };
|
|
@@ -62,7 +63,13 @@ export function createHandler(shim, evaluate = (code) => (0, eval)(code)) {
|
|
|
62
63
|
return { value: shim.describeError(take(request.handle)) };
|
|
63
64
|
case "comm":
|
|
64
65
|
try {
|
|
65
|
-
const handled = await shim.commDispatch(
|
|
66
|
+
const handled = await shim.commDispatch(
|
|
67
|
+
request.msgType,
|
|
68
|
+
request.content,
|
|
69
|
+
request.metadata,
|
|
70
|
+
request.buffers,
|
|
71
|
+
request.ownSignal === true,
|
|
72
|
+
);
|
|
66
73
|
return { handled: handled === true };
|
|
67
74
|
} catch (err) {
|
|
68
75
|
return { error: JSON.parse(shim.describeError(err)) };
|
|
@@ -86,6 +93,10 @@ export function createHandler(shim, evaluate = (code) => (0, eval)(code)) {
|
|
|
86
93
|
return { text: shim.inspectPath(request.path, request.detail ?? 0) };
|
|
87
94
|
case "barrier":
|
|
88
95
|
return { value: shim.barrier() };
|
|
96
|
+
case "abort":
|
|
97
|
+
// After an interrupt: jupyter.signal (#418).
|
|
98
|
+
shim.abortSignal?.(request.cell !== false);
|
|
99
|
+
return {};
|
|
89
100
|
default:
|
|
90
101
|
throw new Error(`unknown request ${JSON.stringify(request.op)}`);
|
|
91
102
|
}
|