@tlockney/cdp-kernel 0.8.0 → 0.10.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 +592 -426
- package/dist/browser/kernel.js.map +3 -3
- package/dist/browser/target.d.ts +7 -0
- package/dist/browser/target.js +2 -1
- package/dist/browser/worker.js +224 -6
- package/dist/browser/worker.js.map +4 -4
- package/dist/celld-target.d.ts +2 -1
- package/dist/celld-target.js +5 -3
- package/dist/globals.d.ts +35 -4
- package/dist/kernel-core.js +11 -0
- package/dist/package.d.ts +3 -3
- package/dist/package.js +3 -2
- package/dist/transform.d.ts +10 -1
- package/dist/transform.js +20 -2
- package/dist/workerd-target.js +6 -2
- package/dist/workers-project.js +1 -1
- package/package.json +3 -3
- package/runtime/bootstrap.mjs +2 -0
- package/runtime/browser-worker.mjs +5 -1
- package/runtime/comm.d.mts +87 -1
- package/runtime/display.d.mts +315 -6
- package/runtime/display.mjs +63 -0
- package/runtime/host.d.mts +246 -0
- package/runtime/host.mjs +215 -0
- package/runtime/widget.d.mts +205 -6
- package/runtime/workerd-worker.mjs +5 -1
package/runtime/display.d.mts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type { CommApi, Comms } from "./comm.d.mts";
|
|
2
|
+
import type { Host, HostCallOptions, HostError, HostErrorCode, HostNotebooks, HostOptions, HostSelf, NotebookEntry, NotebookHit, NotebookSearch } from "./host.d.mts";
|
|
2
3
|
import type { Widget, WidgetOptions } from "./widget.d.mts";
|
|
3
4
|
|
|
4
|
-
export type { CommApi, Widget, WidgetOptions };
|
|
5
|
+
export type { CommApi, Host, HostCallOptions, HostError, HostErrorCode, HostNotebooks, HostOptions, HostSelf, NotebookEntry, NotebookHit, NotebookSearch, Widget, WidgetOptions };
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The symbol a class implements to choose how its instances appear. At
|
|
9
|
+
* runtime it is `Symbol.for("Jupyter.display")`, so a library can use that
|
|
10
|
+
* form without the `jupyter` global.
|
|
11
|
+
*/
|
|
6
12
|
export const $display: unique symbol;
|
|
7
13
|
|
|
14
|
+
/**
|
|
15
|
+
* A Jupyter MIME bundle: media type to content, such as
|
|
16
|
+
* `{ "text/plain": "42", "text/html": "<b>42</b>" }`. Binary images are
|
|
17
|
+
* base64 strings; JSON types (`application/json`, Vega-Lite) are objects.
|
|
18
|
+
*/
|
|
8
19
|
export type MediaBundle = { [mediaType: string]: unknown };
|
|
9
20
|
|
|
10
21
|
export interface DisplayHost {
|
|
@@ -13,41 +24,339 @@ export interface DisplayHost {
|
|
|
13
24
|
readFile: (path: string) => Uint8Array;
|
|
14
25
|
send: (msgType: string, content: object, metadata: object, buffers: Uint8Array[]) => void;
|
|
15
26
|
input: (prompt: string, password: boolean) => string | null;
|
|
27
|
+
/** The run's parameters as JSON text, or `undefined` when there are none. */
|
|
28
|
+
params?: () => string | undefined;
|
|
16
29
|
}
|
|
17
30
|
|
|
31
|
+
/** The MIME type of a named result: `{ name, value }`. */
|
|
32
|
+
export const RESULT_MIME: "application/vnd.ganymede.result+json";
|
|
33
|
+
|
|
34
|
+
/** A JSON value, as `JSON.parse` returns it. */
|
|
35
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
36
|
+
|
|
37
|
+
/** Options for `jupyter.display`. */
|
|
18
38
|
export interface DisplayOptions {
|
|
39
|
+
/**
|
|
40
|
+
* Send a plain object as the MIME bundle itself instead of formatting
|
|
41
|
+
* it. Default `false`.
|
|
42
|
+
*/
|
|
19
43
|
raw?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Replace every earlier output that has the same `display_id`, including
|
|
46
|
+
* one shown by an earlier cell, instead of adding an output. Needs
|
|
47
|
+
* `display_id`. Default `false`.
|
|
48
|
+
*/
|
|
20
49
|
update?: boolean;
|
|
50
|
+
/** An id that a later display with `update: true` can replace. */
|
|
21
51
|
display_id?: string;
|
|
22
52
|
}
|
|
23
53
|
|
|
54
|
+
/** A value that renders as rich output: it has a `[jupyter.$display]` method. */
|
|
24
55
|
export interface Displayable {
|
|
56
|
+
/** Returns the value's MIME bundle, or a promise of one. */
|
|
25
57
|
[$display]: () => MediaBundle | Promise<MediaBundle>;
|
|
26
58
|
}
|
|
27
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The `jupyter` global: how values appear in the notebook. The same object
|
|
62
|
+
* as `Deno.jupyter` in `deno jupyter`.
|
|
63
|
+
*/
|
|
28
64
|
export interface JupyterNamespace {
|
|
65
|
+
/**
|
|
66
|
+
* Send a raw Jupyter message on the kernel's output channel (IOPub), as a
|
|
67
|
+
* reply to the running cell. The other helpers are built on it. It does
|
|
68
|
+
* not check the message: a wrong field is ignored or mishandled by the
|
|
69
|
+
* frontend.
|
|
70
|
+
*
|
|
71
|
+
* @param msgType An IOPub message type, such as `"clear_output"` or
|
|
72
|
+
* `"display_data"`.
|
|
73
|
+
* @param content The message content, as the messaging spec defines it
|
|
74
|
+
* for `msgType`.
|
|
75
|
+
* @param options `metadata` for the message, and binary `buffers`.
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* // Count down in place: clear_output with wait avoids a flicker.
|
|
79
|
+
* for (const n of [3, 2, 1]) {
|
|
80
|
+
* jupyter.broadcast("clear_output", { wait: true });
|
|
81
|
+
* await jupyter.display(jupyter.md`Launching in **${n}**…`);
|
|
82
|
+
* await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
83
|
+
* }
|
|
84
|
+
* ```
|
|
85
|
+
* @see jupyter-api.md#raw-messages-with-broadcast
|
|
86
|
+
*/
|
|
29
87
|
broadcast(
|
|
30
88
|
msgType: string,
|
|
31
89
|
content: object,
|
|
32
90
|
options?: { metadata?: object; buffers?: Uint8Array[] },
|
|
33
91
|
): void;
|
|
92
|
+
/**
|
|
93
|
+
* Show a value now, in the middle of a cell, or replace one shown
|
|
94
|
+
* earlier. The value can be anything a cell could end with: a template
|
|
95
|
+
* from `md`, `html` or `svg`, an image, a widget, an object with a
|
|
96
|
+
* `[jupyter.$display]` method, or a plain value, shown as text.
|
|
97
|
+
*
|
|
98
|
+
* @param obj The value to show.
|
|
99
|
+
* @param options `display_id` and `update` to replace an earlier output;
|
|
100
|
+
* `raw` to send a plain object as the MIME bundle itself.
|
|
101
|
+
* @returns Resolves once the output is sent. Without `await` the output
|
|
102
|
+
* still appears, but later output in the cell can appear before it.
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* const bar = (pct: number) => jupyter.html`<progress value="${pct}" max="100"></progress>`;
|
|
106
|
+
* await jupyter.display(bar(0), { display_id: "download" });
|
|
107
|
+
* for (let pct = 25; pct <= 100; pct += 25) {
|
|
108
|
+
* await new Promise((resolve) => setTimeout(resolve, 500));
|
|
109
|
+
* await jupyter.display(bar(pct), { display_id: "download", update: true });
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* // A MIME bundle sent as is: JupyterLab renders it as a JSON tree.
|
|
115
|
+
* const user = { name: "Ada", active: true };
|
|
116
|
+
* await jupyter.display(
|
|
117
|
+
* { "application/json": user, "text/plain": JSON.stringify(user) },
|
|
118
|
+
* { raw: true },
|
|
119
|
+
* );
|
|
120
|
+
* ```
|
|
121
|
+
* @see jupyter-api.md#showing-output-with-display
|
|
122
|
+
*/
|
|
34
123
|
display(obj: unknown, options?: DisplayOptions): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* The MIME bundle that displaying a value would send, without showing
|
|
126
|
+
* anything. For plain values `text/plain` holds terminal color codes,
|
|
127
|
+
* as the notebook shows them.
|
|
128
|
+
*
|
|
129
|
+
* @param obj The value to format.
|
|
130
|
+
* @returns The bundle, for example `{ "text/plain": "\x1b[33m42\x1b[39m" }`
|
|
131
|
+
* for `42`.
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const bundle = await jupyter.format(jupyter.md`# Weekly report`);
|
|
135
|
+
* bundle["text/markdown"]; // "# Weekly report"
|
|
136
|
+
* ```
|
|
137
|
+
* @see jupyter-api.md#getting-a-bundle-with-format
|
|
138
|
+
*/
|
|
35
139
|
format(obj: unknown): Promise<MediaBundle>;
|
|
140
|
+
/**
|
|
141
|
+
* A tagged template that renders as Markdown. Interpolated values are
|
|
142
|
+
* inserted as is, without escaping.
|
|
143
|
+
*
|
|
144
|
+
* @returns A value that shows as `text/markdown` as a cell's result or
|
|
145
|
+
* through `display`.
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* const scores = { Ada: 92, Grace: 88 };
|
|
149
|
+
* const rows = Object.entries(scores).map(([n, s]) => `| ${n} | ${s} |`).join("\n");
|
|
150
|
+
* jupyter.md`| Name | Score |
|
|
151
|
+
* | --- | --- |
|
|
152
|
+
* ${rows}`;
|
|
153
|
+
* ```
|
|
154
|
+
* @see jupyter-api.md#markdown
|
|
155
|
+
*/
|
|
36
156
|
md(strings: TemplateStringsArray, ...values: unknown[]): Displayable;
|
|
157
|
+
/**
|
|
158
|
+
* A tagged template that renders as HTML. Interpolated values are
|
|
159
|
+
* inserted as is, without escaping: escape text from users, files or
|
|
160
|
+
* the network yourself.
|
|
161
|
+
*
|
|
162
|
+
* @returns A value that shows as `text/html`.
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* const escape = (s: string) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
166
|
+
* const comment = "<script>alert(1)</script>";
|
|
167
|
+
* jupyter.html`<p>${escape(comment)}</p>`;
|
|
168
|
+
* ```
|
|
169
|
+
* @see jupyter-api.md#html
|
|
170
|
+
*/
|
|
37
171
|
html(strings: TemplateStringsArray, ...values: unknown[]): Displayable;
|
|
172
|
+
/**
|
|
173
|
+
* A tagged template that renders as SVG. Include the `xmlns` attribute.
|
|
174
|
+
* Interpolated values are inserted as is, without escaping.
|
|
175
|
+
*
|
|
176
|
+
* @returns A value that shows as `image/svg+xml`.
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* const data = [4, 8, 15, 16];
|
|
180
|
+
* const bars = data.map((v, i) => `<rect x="${i * 20}" y="${40 - v * 2}" width="16" height="${v * 2}"/>`).join("");
|
|
181
|
+
* jupyter.svg`<svg xmlns="http://www.w3.org/2000/svg" width="80" height="40">${bars}</svg>`;
|
|
182
|
+
* ```
|
|
183
|
+
* @see jupyter-api.md#svg
|
|
184
|
+
*/
|
|
38
185
|
svg(strings: TemplateStringsArray, ...values: unknown[]): Displayable;
|
|
186
|
+
/**
|
|
187
|
+
* Show a PNG or JPEG. The format is read from the bytes, not the file
|
|
188
|
+
* name.
|
|
189
|
+
*
|
|
190
|
+
* @param obj A file path, relative to the notebook's directory, or the
|
|
191
|
+
* image's bytes.
|
|
192
|
+
* @returns A value that shows as `image/png` or `image/jpeg`.
|
|
193
|
+
* @throws {TypeError} The bytes are not PNG or JPEG, or the path can't
|
|
194
|
+
* be read.
|
|
195
|
+
* @availability Paths need a file system: on workerd and celld pass
|
|
196
|
+
* bytes.
|
|
197
|
+
* @example
|
|
198
|
+
* ```ts
|
|
199
|
+
* jupyter.image("./chart.png");
|
|
200
|
+
* ```
|
|
201
|
+
* @example
|
|
202
|
+
* ```ts
|
|
203
|
+
* const response = await fetch("https://www.python.org/static/img/python-logo.png");
|
|
204
|
+
* jupyter.image(new Uint8Array(await response.arrayBuffer()));
|
|
205
|
+
* ```
|
|
206
|
+
* @see jupyter-api.md#images
|
|
207
|
+
*/
|
|
39
208
|
image(obj: string | Uint8Array): Displayable;
|
|
209
|
+
/**
|
|
210
|
+
* The symbol a class implements to choose how its instances appear. The
|
|
211
|
+
* method returns a MIME bundle, or a promise of one. It is used for a
|
|
212
|
+
* cell's result, `display` and `format`, but not for objects nested in
|
|
213
|
+
* other values. At runtime it is `Symbol.for("Jupyter.display")`.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* class Temperature {
|
|
218
|
+
* celsius: number;
|
|
219
|
+
* constructor(celsius: number) {
|
|
220
|
+
* this.celsius = celsius;
|
|
221
|
+
* }
|
|
222
|
+
* [jupyter.$display]() {
|
|
223
|
+
* return { "text/plain": `${this.celsius}°C`, "text/html": `<b>${this.celsius}°C</b>` };
|
|
224
|
+
* }
|
|
225
|
+
* }
|
|
226
|
+
* new Temperature(21);
|
|
227
|
+
* ```
|
|
228
|
+
* @see jupyter-api.md#custom-display-for-your-own-classes
|
|
229
|
+
*/
|
|
40
230
|
$display: typeof $display;
|
|
41
|
-
/**
|
|
231
|
+
/**
|
|
232
|
+
* Jupyter comms: message channels to the frontend outside cell
|
|
233
|
+
* execution. Its methods are in the widgets and comms reference.
|
|
234
|
+
*
|
|
235
|
+
* @see widgets.md#comms
|
|
236
|
+
*/
|
|
42
237
|
comm: CommApi;
|
|
43
|
-
/**
|
|
238
|
+
/**
|
|
239
|
+
* Create an anywidget-compatible widget model. Display it (or end the
|
|
240
|
+
* cell with it) to show a view.
|
|
241
|
+
*
|
|
242
|
+
* @param options The view module, its CSS, and the initial state.
|
|
243
|
+
* @returns The model, synced with every view of it.
|
|
244
|
+
* @throws {TypeError} `options.esm` is not a string.
|
|
245
|
+
* @availability The frontend needs the anywidget extension.
|
|
246
|
+
* @example
|
|
247
|
+
* ```ts
|
|
248
|
+
* const counter = jupyter.widget({
|
|
249
|
+
* esm: `export default {
|
|
250
|
+
* render({ model, el }) {
|
|
251
|
+
* const button = document.createElement("button");
|
|
252
|
+
* const update = () => {
|
|
253
|
+
* button.textContent = "count is " + model.get("count");
|
|
254
|
+
* };
|
|
255
|
+
* button.onclick = () => {
|
|
256
|
+
* model.set("count", model.get("count") + 1);
|
|
257
|
+
* model.save_changes();
|
|
258
|
+
* };
|
|
259
|
+
* model.on("change:count", update);
|
|
260
|
+
* update();
|
|
261
|
+
* el.append(button);
|
|
262
|
+
* },
|
|
263
|
+
* };`,
|
|
264
|
+
* state: { count: 0 },
|
|
265
|
+
* });
|
|
266
|
+
* counter; // the cell's result: displays the widget
|
|
267
|
+
* ```
|
|
268
|
+
* 
|
|
269
|
+
* @see widgets.md#a-first-widget-a-counter
|
|
270
|
+
*/
|
|
44
271
|
widget(options: WidgetOptions): Widget;
|
|
45
272
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
273
|
+
* An `AbortSignal` that an interrupt aborts, so code waiting on something
|
|
274
|
+
* only it can end can stop and clean up. Each cell gets its own, as does
|
|
275
|
+
* a comm handler that starts while no cell runs. Read it once when the
|
|
276
|
+
* work starts: a later read gets the signal of whatever started last.
|
|
277
|
+
* The abort reason is a `DOMException` named `AbortError`.
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* ```ts
|
|
281
|
+
* const response = await fetch("https://example.com/large.csv", { signal: jupyter.signal });
|
|
282
|
+
* ```
|
|
283
|
+
* @example
|
|
284
|
+
* ```ts
|
|
285
|
+
* const signal = jupyter.signal;
|
|
286
|
+
* const ask = jupyter.comm.open("my.app", { question: "ready?" });
|
|
287
|
+
* const answer = await new Promise((resolve, reject) => {
|
|
288
|
+
* ask.onMsg((m) => resolve(m.data));
|
|
289
|
+
* signal.addEventListener("abort", () => { ask.close(); reject(signal.reason); }, { once: true });
|
|
290
|
+
* });
|
|
291
|
+
* ```
|
|
292
|
+
* @see jupyter-api.md#stopping-on-interrupt-jupyter-signal
|
|
49
293
|
*/
|
|
50
294
|
readonly signal: AbortSignal;
|
|
295
|
+
/**
|
|
296
|
+
* The parameters of this run: a frozen object, empty unless whoever
|
|
297
|
+
* started the kernel passed some, such as `ganymede execute -p
|
|
298
|
+
* counter="2nd Ave"`. They arrive as a JSON object in the kernel's
|
|
299
|
+
* `GANYMEDE_PARAMS` environment variable. Take defaults with
|
|
300
|
+
* destructuring, so the notebook also runs on its own.
|
|
301
|
+
*
|
|
302
|
+
* @throws {TypeError} `GANYMEDE_PARAMS` is set but isn't a JSON object.
|
|
303
|
+
* @availability Node, Deno, Bun, workerd and celld read the kernel's
|
|
304
|
+
* environment; the browser target takes `params` from its options.
|
|
305
|
+
* @example
|
|
306
|
+
* ```ts
|
|
307
|
+
* const { counter = "Fremont", days = 30 } = jupyter.params as { counter?: string; days?: number };
|
|
308
|
+
* `${counter}, last ${days} days`;
|
|
309
|
+
* ```
|
|
310
|
+
* @see jupyter-api.md#parameters-and-named-results
|
|
311
|
+
*/
|
|
312
|
+
readonly params: { readonly [name: string]: JsonValue };
|
|
313
|
+
/**
|
|
314
|
+
* Publish a value under a name, as an output that a run's caller reads
|
|
315
|
+
* by name instead of parsing outputs: `ganymede execute --results`, or
|
|
316
|
+
* another notebook. The output has the MIME type
|
|
317
|
+
* `application/vnd.ganymede.result+json` holding `{ name, value }`, and
|
|
318
|
+
* a `text/plain` line `name = value` for frontends that don't know the
|
|
319
|
+
* type. When a name is published twice, readers take the last one.
|
|
320
|
+
*
|
|
321
|
+
* @param name The result's name.
|
|
322
|
+
* @param value Any value `JSON.stringify` accepts; it is stored as JSON,
|
|
323
|
+
* so a `Date` becomes its ISO string.
|
|
324
|
+
* @throws {TypeError} `name` is empty, or `value` isn't JSON (a
|
|
325
|
+
* function, `undefined`, a `BigInt` or a cycle).
|
|
326
|
+
* @example
|
|
327
|
+
* ```ts
|
|
328
|
+
* const counts = [3120, 2890, 2410];
|
|
329
|
+
* jupyter.result("weekday_drop", (counts[2] - counts[0]) / counts[0]);
|
|
330
|
+
* jupyter.result("counts", counts);
|
|
331
|
+
* ```
|
|
332
|
+
* @see jupyter-api.md#parameters-and-named-results
|
|
333
|
+
*/
|
|
334
|
+
result(name: string, value: unknown): void;
|
|
335
|
+
/**
|
|
336
|
+
* The Ganymede server running this kernel, through its host API: read,
|
|
337
|
+
* search and (when granted) run other notebooks under its root. The
|
|
338
|
+
* server opens a comm to the kernel when it starts; this resolves to
|
|
339
|
+
* `null` when none answers, as under plain Jupyter.
|
|
340
|
+
*
|
|
341
|
+
* Each call returns a promise and takes `{ signal }`, by default
|
|
342
|
+
* `jupyter.signal`, so an interrupt cancels it. A failed call rejects
|
|
343
|
+
* with a `HostError` whose `code` says why: `denied` for a notebook or
|
|
344
|
+
* method this notebook isn't granted. Branch on `code`: `HostError`
|
|
345
|
+
* isn't a global, so `instanceof` isn't available.
|
|
346
|
+
*
|
|
347
|
+
* @param options `timeout`: how long to wait for a server, in ms.
|
|
348
|
+
* @throws {HostError} `cancelled` when an interrupt ends the wait.
|
|
349
|
+
* @example
|
|
350
|
+
* ```ts
|
|
351
|
+
* const host = await jupyter.host();
|
|
352
|
+
* if (host) {
|
|
353
|
+
* const { hits } = await host.notebooks.search({ query: "weekday drop", dir: "journal" });
|
|
354
|
+
* hits.map((h) => `${h.path}: ${h.excerpt}`);
|
|
355
|
+
* }
|
|
356
|
+
* ```
|
|
357
|
+
* @see jupyter-api.md#other-notebooks-jupyter-host
|
|
358
|
+
*/
|
|
359
|
+
host(options?: HostOptions): Promise<Host | null>;
|
|
51
360
|
}
|
|
52
361
|
|
|
53
362
|
export interface Jupyter {
|
package/runtime/display.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// Deno doesn't have; see comm.mjs and widget.mjs.
|
|
16
16
|
|
|
17
17
|
import { createComms } from "./comm.mjs";
|
|
18
|
+
import { createHost } from "./host.mjs";
|
|
18
19
|
import { createWidgetFactory } from "./widget.mjs";
|
|
19
20
|
|
|
20
21
|
export const $display = Symbol.for("Jupyter.display");
|
|
@@ -343,8 +344,20 @@ function isMediaBundle(obj) {
|
|
|
343
344
|
* readFile: (path: string) => Uint8Array,
|
|
344
345
|
* send: (msgType: string, content: object, metadata: object, buffers: Uint8Array[]) => void,
|
|
345
346
|
* input: (prompt: string, password: boolean) => string | null,
|
|
347
|
+
* params?: () => string | undefined,
|
|
346
348
|
* }} host
|
|
347
349
|
*/
|
|
350
|
+
/** The MIME type of a named result (cdp-kernel#456): `{ name, value }`. */
|
|
351
|
+
export const RESULT_MIME = "application/vnd.ganymede.result+json";
|
|
352
|
+
|
|
353
|
+
function deepFreeze(value) {
|
|
354
|
+
if (typeof value === "object" && value !== null) {
|
|
355
|
+
for (const v of Object.values(value)) deepFreeze(v);
|
|
356
|
+
Object.freeze(value);
|
|
357
|
+
}
|
|
358
|
+
return value;
|
|
359
|
+
}
|
|
360
|
+
|
|
348
361
|
export function createJupyter(host) {
|
|
349
362
|
async function format(obj) {
|
|
350
363
|
return await formatNow(obj);
|
|
@@ -439,6 +452,46 @@ export function createJupyter(host) {
|
|
|
439
452
|
return answer === "" ? defaultValue : answer;
|
|
440
453
|
}
|
|
441
454
|
|
|
455
|
+
// jupyter.params (cdp-kernel#456): the run's parameters, which the host
|
|
456
|
+
// (ganymede execute -p) passes as JSON in GANYMEDE_PARAMS. Parsed on first
|
|
457
|
+
// read, so a bad value fails the cell that reads it, not the kernel.
|
|
458
|
+
let params;
|
|
459
|
+
function readParams() {
|
|
460
|
+
if (params !== undefined) return params;
|
|
461
|
+
const text = host.params?.();
|
|
462
|
+
if (text === undefined || text === "") return (params = Object.freeze({}));
|
|
463
|
+
let value;
|
|
464
|
+
try {
|
|
465
|
+
value = JSON.parse(text);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
throw new TypeError(`GANYMEDE_PARAMS is not JSON: ${err instanceof Error ? err.message : err}`);
|
|
468
|
+
}
|
|
469
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
470
|
+
throw new TypeError("GANYMEDE_PARAMS must be a JSON object, such as {\"counter\": \"Fremont\"}");
|
|
471
|
+
}
|
|
472
|
+
return (params = deepFreeze(value));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// jupyter.result (cdp-kernel#456): a named value as an output of
|
|
476
|
+
// Ganymede's result type, which a run's caller reads without parsing
|
|
477
|
+
// outputs. JSON only, so every reader sees the same value.
|
|
478
|
+
function result(name, value) {
|
|
479
|
+
if (typeof name !== "string" || name === "") throw new TypeError("jupyter.result: name must be a non-empty string");
|
|
480
|
+
let text;
|
|
481
|
+
try {
|
|
482
|
+
text = JSON.stringify(value);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
throw new TypeError(`jupyter.result: ${name} is not JSON: ${err instanceof Error ? err.message : err}`);
|
|
485
|
+
}
|
|
486
|
+
if (text === undefined) throw new TypeError(`jupyter.result: ${name} is not JSON (${typeof value})`);
|
|
487
|
+
const json = JSON.parse(text);
|
|
488
|
+
broadcast("display_data", {
|
|
489
|
+
data: { [RESULT_MIME]: { name, value: json }, "text/plain": `${name} = ${text}` },
|
|
490
|
+
metadata: {},
|
|
491
|
+
transient: {},
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
442
495
|
const comms = createComms((msgType, content, metadata, buffers) => host.send(msgType, content, metadata, buffers));
|
|
443
496
|
|
|
444
497
|
// jupyter.signal (#418): aborted when the kernel is interrupted, so cell
|
|
@@ -462,6 +515,11 @@ export function createJupyter(host) {
|
|
|
462
515
|
for (const controller of handlers.keys()) controller.abort(reason);
|
|
463
516
|
handlers.clear();
|
|
464
517
|
}
|
|
518
|
+
// jupyter.host() (#461): Ganymede's host API. Registers its comm target
|
|
519
|
+
// now, so the comm the server opens at kernel start finds it. A call
|
|
520
|
+
// without a signal watches the jupyter.signal of whatever started it.
|
|
521
|
+
const hostApi = createHost(comms.api, () => current.signal);
|
|
522
|
+
|
|
465
523
|
async function commDispatch(msgType, content, metadata, buffers, ownSignal = false) {
|
|
466
524
|
// A handler during a cell shares the cell's signal, and is tracked
|
|
467
525
|
// apart from it: it can outlive the cell.
|
|
@@ -484,6 +542,10 @@ export function createJupyter(host) {
|
|
|
484
542
|
},
|
|
485
543
|
broadcast,
|
|
486
544
|
display,
|
|
545
|
+
get params() {
|
|
546
|
+
return readParams();
|
|
547
|
+
},
|
|
548
|
+
result,
|
|
487
549
|
format,
|
|
488
550
|
md: taggedTemplate("text/markdown"),
|
|
489
551
|
html: taggedTemplate("text/html"),
|
|
@@ -491,6 +553,7 @@ export function createJupyter(host) {
|
|
|
491
553
|
image,
|
|
492
554
|
$display,
|
|
493
555
|
comm: comms.api,
|
|
556
|
+
host: hostApi.host,
|
|
494
557
|
widget: createWidgetFactory(comms.api, $display),
|
|
495
558
|
},
|
|
496
559
|
broadcastResult,
|