@tlockney/cdp-kernel 0.9.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/dist/browser/kernel.js +584 -425
- 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/kernel-core.js +8 -0
- package/dist/package.d.ts +3 -3
- package/dist/package.js +3 -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/display.d.mts +75 -1
- package/runtime/display.mjs +63 -0
- package/runtime/host.d.mts +246 -0
- package/runtime/host.mjs +215 -0
- package/runtime/workerd-worker.mjs +5 -1
package/dist/package.js
CHANGED
|
@@ -14,13 +14,13 @@ import { join, sep } from "node:path";
|
|
|
14
14
|
import process from "node:process";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
/** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
|
|
17
|
-
export const VERSION = "0.
|
|
17
|
+
export const VERSION = "0.10.0";
|
|
18
18
|
/**
|
|
19
19
|
* The UTC day this version was released, set with VERSION. The publish
|
|
20
20
|
* workflow refuses a tag unless it's today or yesterday, so it can't go
|
|
21
21
|
* stale: see dependencyCutoff.
|
|
22
22
|
*/
|
|
23
|
-
export const RELEASE_DATE = "2026-11-
|
|
23
|
+
export const RELEASE_DATE = "2026-11-11";
|
|
24
24
|
/**
|
|
25
25
|
* Deno blocks packages less than 24 hours old (minimum dependency age), so
|
|
26
26
|
* a kernelspec relaunching a release from JSR would fail on release day. It
|
|
@@ -43,6 +43,7 @@ export const RUNTIME_FILES = [
|
|
|
43
43
|
"workerd-worker.mjs",
|
|
44
44
|
"display.mjs",
|
|
45
45
|
"comm.mjs",
|
|
46
|
+
"host.mjs",
|
|
46
47
|
"widget.mjs",
|
|
47
48
|
"node-hooks.mjs",
|
|
48
49
|
"deno-compat.mjs",
|
package/dist/workerd-target.js
CHANGED
|
@@ -35,7 +35,7 @@ import { connectUnixWebSocket } from "./ws-client.js";
|
|
|
35
35
|
import { findWranglerConfig, startMiniflare } from "./workers-project.js";
|
|
36
36
|
import { ensureInstalledAsync, findLockFile, jsrToNpm, parsePackageSpecifier } from "../runtime/node-hooks.mjs";
|
|
37
37
|
/** The driver Worker's modules; the first is its main module. */
|
|
38
|
-
export const WORKER_MODULES = ["workerd-worker.mjs", "display.mjs", "comm.mjs", "widget.mjs", "evaluator.mjs", "introspect.mjs"];
|
|
38
|
+
export const WORKER_MODULES = ["workerd-worker.mjs", "display.mjs", "comm.mjs", "host.mjs", "widget.mjs", "evaluator.mjs", "introspect.mjs"];
|
|
39
39
|
/** See BunTarget: how long an interrupted cell has to show it isn't busy. */
|
|
40
40
|
export const INTERRUPT_GRACE_MS = 500;
|
|
41
41
|
export function restartNote(runtime) {
|
|
@@ -71,6 +71,7 @@ const driver :Workerd.Worker = (
|
|
|
71
71
|
(name = "UNSAFE", unsafeEval = void),
|
|
72
72
|
(name = "TOKEN", fromEnvironment = "CDPK_TOKEN"),
|
|
73
73
|
(name = "META", fromEnvironment = "CDPK_META"),
|
|
74
|
+
(name = "CDPK_PARAMS", fromEnvironment = "CDPK_PARAMS"),
|
|
74
75
|
],
|
|
75
76
|
);
|
|
76
77
|
`;
|
|
@@ -332,7 +333,10 @@ export class RawWorkerd {
|
|
|
332
333
|
copyFileSync(join(source, name), join(dir, name));
|
|
333
334
|
writeFileSync(join(dir, "config.capnp"), workerdConfig({ socket, compatibilityDate }));
|
|
334
335
|
const token = randomBytes(32).toString("hex");
|
|
335
|
-
const env = { ...process.env, CDPK_TOKEN: token, CDPK_META: JSON.stringify(cellMeta(cwd))
|
|
336
|
+
const env = { ...process.env, CDPK_TOKEN: token, CDPK_META: JSON.stringify(cellMeta(cwd)),
|
|
337
|
+
// jupyter.params (#456); always set, so the binding exists.
|
|
338
|
+
CDPK_PARAMS: process.env.GANYMEDE_PARAMS ?? "",
|
|
339
|
+
};
|
|
336
340
|
// --experimental: the UnsafeEval binding.
|
|
337
341
|
const child = spawn(workerd, ["serve", "config.capnp", "--experimental"], { cwd: dir, stdio: ["ignore", "pipe", "pipe"], env });
|
|
338
342
|
child.once("exit", () => rmSync(dir, { recursive: true, force: true }));
|
package/dist/workers-project.js
CHANGED
|
@@ -185,7 +185,7 @@ export async function startMiniflare(configPath, cwd, startTimeoutMs) {
|
|
|
185
185
|
unsafeEvalBinding: "UNSAFE",
|
|
186
186
|
compatibilityDate: date,
|
|
187
187
|
compatibilityFlags: compatibilityFlagsFor(workerOptions.compatibilityFlags ?? []),
|
|
188
|
-
bindings: { ...workerOptions.bindings, TOKEN: token, META: JSON.stringify(cellMeta(cwd)) },
|
|
188
|
+
bindings: { ...workerOptions.bindings, TOKEN: token, META: JSON.stringify(cellMeta(cwd)), CDPK_PARAMS: process.env.GANYMEDE_PARAMS ?? "" },
|
|
189
189
|
// Miniflare 5 renamed Miniflare 4's defaultPersistRoot.
|
|
190
190
|
...(v5 ? { resourcePersistencePath: state } : { defaultPersistRoot: state }),
|
|
191
191
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tlockney/cdp-kernel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Deno's Jupyter kernel, extracted to run notebooks on Node, Deno, Bun, workerd and celld",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Thomas Lockney <thomas@lockney.net>",
|
|
@@ -77,11 +77,11 @@
|
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"acorn": "^8.15.0",
|
|
79
79
|
"acorn-walk": "^8.3.4",
|
|
80
|
-
"esbuild": "^0.
|
|
80
|
+
"esbuild": "^0.28.2",
|
|
81
81
|
"typescript": "^6.0.3"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@types/node": "^
|
|
84
|
+
"@types/node": "^26.6.6",
|
|
85
85
|
"medium-zoom": "^1.1.0",
|
|
86
86
|
"vitepress": "^1.6.4",
|
|
87
87
|
"workerd": "1.20261015.1",
|
package/runtime/bootstrap.mjs
CHANGED
|
@@ -83,6 +83,8 @@ function send(payload) {
|
|
|
83
83
|
|
|
84
84
|
const jupyter = createJupyter({
|
|
85
85
|
inspect: (value) => inspect(value),
|
|
86
|
+
// jupyter.params (#456): the kernel's environment reaches the target.
|
|
87
|
+
params: () => (isDeno ? Deno.env.get("GANYMEDE_PARAMS") : process.env.GANYMEDE_PARAMS),
|
|
86
88
|
base64,
|
|
87
89
|
readFile,
|
|
88
90
|
send(msgType, content, metadata, buffers) {
|
|
@@ -65,7 +65,10 @@ const jupyter = createJupyter({
|
|
|
65
65
|
// A synchronous prompt would need SharedArrayBuffer and Atomics.wait, so
|
|
66
66
|
// a cross-origin isolated page; until then, as with no stdin.
|
|
67
67
|
input: () => null,
|
|
68
|
+
// jupyter.params (#456): the target's `params` option, sent with info.
|
|
69
|
+
params: () => paramsText,
|
|
68
70
|
});
|
|
71
|
+
let paramsText;
|
|
69
72
|
|
|
70
73
|
// --- Modules ----------------------------------------------------------------
|
|
71
74
|
let cdn = DEFAULT_CDN;
|
|
@@ -150,7 +153,8 @@ async function preload(urls, base) {
|
|
|
150
153
|
// --- The shim cells and the evaluator use -----------------------------------
|
|
151
154
|
let handle = null;
|
|
152
155
|
|
|
153
|
-
function install({ base, cdn: cdnBase }) {
|
|
156
|
+
function install({ base, cdn: cdnBase, params }) {
|
|
157
|
+
if (typeof params === "string") paramsText = params;
|
|
154
158
|
if (typeof cdnBase === "string") cdn = cdnBase.endsWith("/") ? cdnBase : cdnBase + "/";
|
|
155
159
|
const url = new URL("$cdpk$repl.mjs", base).href;
|
|
156
160
|
globalThis.__cdpk = {
|
package/runtime/display.d.mts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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
|
|
|
6
7
|
/**
|
|
7
8
|
* The symbol a class implements to choose how its instances appear. At
|
|
@@ -23,8 +24,16 @@ export interface DisplayHost {
|
|
|
23
24
|
readFile: (path: string) => Uint8Array;
|
|
24
25
|
send: (msgType: string, content: object, metadata: object, buffers: Uint8Array[]) => void;
|
|
25
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;
|
|
26
29
|
}
|
|
27
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
|
+
|
|
28
37
|
/** Options for `jupyter.display`. */
|
|
29
38
|
export interface DisplayOptions {
|
|
30
39
|
/**
|
|
@@ -283,6 +292,71 @@ export interface JupyterNamespace {
|
|
|
283
292
|
* @see jupyter-api.md#stopping-on-interrupt-jupyter-signal
|
|
284
293
|
*/
|
|
285
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>;
|
|
286
360
|
}
|
|
287
361
|
|
|
288
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,
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { CommApi } from "./comm.d.mts";
|
|
2
|
+
|
|
3
|
+
/** The target name of the comm a Ganymede server opens to the kernel. */
|
|
4
|
+
export const HOST_TARGET: "ganymede.host.v1";
|
|
5
|
+
|
|
6
|
+
/** Why a host call failed. The codes are ganymede-device's. */
|
|
7
|
+
export type HostErrorCode = "unavailable" | "denied" | "cancelled" | "timeout" | "failed" | "disconnected";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A failed host call. Branch on `code` (or `name === "HostError"`): the
|
|
11
|
+
* class isn't a global, so `instanceof` isn't available in a cell.
|
|
12
|
+
*
|
|
13
|
+
* A failed host call: `unavailable` (no such method), `denied` (not
|
|
14
|
+
* granted to this notebook), `cancelled` (its signal aborted), `timeout`,
|
|
15
|
+
* `failed` (the server couldn't do it), `disconnected` (the host comm
|
|
16
|
+
* closed, such as on a server restart).
|
|
17
|
+
*/
|
|
18
|
+
export class HostError extends Error {
|
|
19
|
+
readonly code: HostErrorCode;
|
|
20
|
+
constructor(code: HostErrorCode, message: string);
|
|
21
|
+
static from(err: unknown): HostError;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Options for a host call. */
|
|
25
|
+
export interface HostCallOptions {
|
|
26
|
+
/** Aborting it cancels the call. Default `jupyter.signal`. */
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Parameters and results are JSON; the host API's design note lists each method's. */
|
|
31
|
+
export type HostParams = { [key: string]: unknown };
|
|
32
|
+
|
|
33
|
+
/** An entry `notebooks.list` returns: a folder, or a notebook this notebook may read. */
|
|
34
|
+
export interface NotebookEntry {
|
|
35
|
+
path: string;
|
|
36
|
+
type: "directory" | "notebook";
|
|
37
|
+
/** Its first heading, for a notebook that has one. */
|
|
38
|
+
title?: string | null;
|
|
39
|
+
/** Its kernelspec name. */
|
|
40
|
+
kernel?: string | null;
|
|
41
|
+
/** ISO 8601. */
|
|
42
|
+
modified: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A cell `notebooks.search` matched. */
|
|
46
|
+
export interface NotebookHit {
|
|
47
|
+
path: string;
|
|
48
|
+
title: string | null;
|
|
49
|
+
/** The cell's position in the notebook. */
|
|
50
|
+
index: number;
|
|
51
|
+
id: string | null;
|
|
52
|
+
cell_type: "code" | "markdown" | "raw";
|
|
53
|
+
/** Where the first word matched. */
|
|
54
|
+
in: "source" | "output";
|
|
55
|
+
excerpt: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** What `notebooks.search` returns. */
|
|
59
|
+
export interface NotebookSearch {
|
|
60
|
+
hits: NotebookHit[];
|
|
61
|
+
/** Matching cells, beyond `limit` too. */
|
|
62
|
+
total: number;
|
|
63
|
+
truncated: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The Ganymede server that runs this kernel, from `jupyter.host()`. */
|
|
67
|
+
export interface Host {
|
|
68
|
+
/** The host API's protocol version. */
|
|
69
|
+
readonly protocol: number;
|
|
70
|
+
/** The server's version. */
|
|
71
|
+
readonly server: string;
|
|
72
|
+
/** This notebook's path under the server's root, or null when the kernel has none. */
|
|
73
|
+
readonly notebook: string | null;
|
|
74
|
+
/** The methods this notebook may call. */
|
|
75
|
+
readonly methods: readonly string[];
|
|
76
|
+
/**
|
|
77
|
+
* Whether this notebook may call `method`.
|
|
78
|
+
*
|
|
79
|
+
* @param method The method, such as `"notebooks.run"`.
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* const host = await jupyter.host();
|
|
83
|
+
* host?.has("notebooks.run"); // false unless the notebook is granted runs
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
has(method: string): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Call a method by name. The shortcuts under `notebooks` and `self` call
|
|
89
|
+
* this.
|
|
90
|
+
*
|
|
91
|
+
* @param method The method, such as `"notebooks.read"`.
|
|
92
|
+
* @param params Its parameters, a JSON object. Default `{}`.
|
|
93
|
+
* @param options `signal` cancels the call.
|
|
94
|
+
* @returns What the server answered, as JSON.
|
|
95
|
+
* @throws {HostError} The call failed; `code` says why.
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* const host = await jupyter.host();
|
|
99
|
+
* const { notebook } = await host!.call("notebooks.read", { path: "journal/a.ipynb" });
|
|
100
|
+
* notebook.cells.length;
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
call(method: string, params?: HostParams, options?: HostCallOptions): Promise<any>;
|
|
104
|
+
/** Other notebooks under the server's root, as far as this notebook's grant reaches. */
|
|
105
|
+
readonly notebooks: HostNotebooks;
|
|
106
|
+
/** This notebook's own cells. Not on a server yet: they reject `unavailable`. */
|
|
107
|
+
readonly self: HostSelf;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** `host.notebooks`: other notebooks under the server's root. */
|
|
111
|
+
export interface HostNotebooks {
|
|
112
|
+
/**
|
|
113
|
+
* The notebooks this notebook may read, and the folders, in a folder.
|
|
114
|
+
*
|
|
115
|
+
* @param params `dir`: the folder, relative to the root. Default the root.
|
|
116
|
+
* @param options `signal` cancels the call.
|
|
117
|
+
* @returns One entry per notebook or folder.
|
|
118
|
+
* @throws {HostError} `denied` when the grant doesn't reach `dir`.
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* const host = await jupyter.host();
|
|
122
|
+
* const entries = await host!.notebooks.list({ dir: "journal" });
|
|
123
|
+
* entries.filter((e) => e.type === "notebook").map((e) => e.title ?? e.path);
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
list(params?: { dir?: string }, options?: HostCallOptions): Promise<NotebookEntry[]>;
|
|
127
|
+
/**
|
|
128
|
+
* A notebook as nbformat JSON (from its open document when someone has
|
|
129
|
+
* it open), and each cell's source hash by cell id.
|
|
130
|
+
*
|
|
131
|
+
* @param params `path`: the notebook, relative to the root.
|
|
132
|
+
* @param options `signal` cancels the call.
|
|
133
|
+
* @returns The notebook and its hashes.
|
|
134
|
+
* @throws {HostError} `denied` when the grant doesn't reach `path`.
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* const host = await jupyter.host();
|
|
138
|
+
* const { notebook } = await host!.notebooks.read({ path: "journal/a.ipynb" });
|
|
139
|
+
* notebook.cells.length;
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
read(params: { path: string }, options?: HostCallOptions): Promise<{ notebook: any; hashes: { [cellId: string]: string } }>;
|
|
143
|
+
/**
|
|
144
|
+
* Cells that hold every word of `query`, in the notebooks this notebook
|
|
145
|
+
* may read: in sources, and in outputs too with `outputs: true`.
|
|
146
|
+
*
|
|
147
|
+
* @param params `query`, and optionally `dir`, `outputs`, `cell_type`
|
|
148
|
+
* (`code`, `markdown` or `raw`) and `limit` (default 20, at most 200).
|
|
149
|
+
* @param options `signal` cancels the call.
|
|
150
|
+
* @returns The matches, how many there are in all, and whether `limit` cut them.
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* const host = await jupyter.host();
|
|
154
|
+
* const { hits } = await host!.notebooks.search({ query: "weekday drop", dir: "journal" });
|
|
155
|
+
* hits.map((h) => `${h.path}: ${h.excerpt}`);
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
search(
|
|
159
|
+
params: { query: string; dir?: string; outputs?: boolean; cell_type?: "code" | "markdown" | "raw"; limit?: number },
|
|
160
|
+
options?: HostCallOptions,
|
|
161
|
+
): Promise<NotebookSearch>;
|
|
162
|
+
/**
|
|
163
|
+
* A notebook's named results. Not on a server yet: rejects `unavailable`.
|
|
164
|
+
*
|
|
165
|
+
* @param params `path`: the notebook.
|
|
166
|
+
* @param options `signal` cancels the call.
|
|
167
|
+
* @example
|
|
168
|
+
* ```ts
|
|
169
|
+
* const host = await jupyter.host();
|
|
170
|
+
* const results = await host!.notebooks.results({ path: "journal/fremont.ipynb" });
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
results(params: { path: string }, options?: HostCallOptions): Promise<any>;
|
|
174
|
+
/**
|
|
175
|
+
* Run a notebook with parameters and return its results. Not on a
|
|
176
|
+
* server yet: rejects `unavailable`.
|
|
177
|
+
*
|
|
178
|
+
* @param params `path`: the notebook; `params`: its parameters.
|
|
179
|
+
* @param options `signal` cancels the run.
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* const host = await jupyter.host();
|
|
183
|
+
* const run = await host!.notebooks.run({ path: "journal/fremont.ipynb", params: { counter: "2nd Ave" } });
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
run(params: { path: string; params?: HostParams }, options?: HostCallOptions): Promise<any>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** `host.self`: this notebook's own cells. Not on a server yet: each rejects `unavailable`. */
|
|
190
|
+
export interface HostSelf {
|
|
191
|
+
/**
|
|
192
|
+
* Add a cell to this notebook.
|
|
193
|
+
*
|
|
194
|
+
* @param params The cell, as the host API's design note describes.
|
|
195
|
+
* @param options `signal` cancels the call.
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* const host = await jupyter.host();
|
|
199
|
+
* await host!.self.add({ cell_type: "markdown", source: "## Index" });
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
add(params?: HostParams, options?: HostCallOptions): Promise<any>;
|
|
203
|
+
/**
|
|
204
|
+
* Change a cell's source, given the hash of the source it replaces.
|
|
205
|
+
*
|
|
206
|
+
* @param params The cell id, its new source and the old source's hash.
|
|
207
|
+
* @param options `signal` cancels the call.
|
|
208
|
+
* @example
|
|
209
|
+
* ```ts
|
|
210
|
+
* const host = await jupyter.host();
|
|
211
|
+
* const { notebook, hashes } = await host!.notebooks.read({ path: host!.notebook! });
|
|
212
|
+
* const id = notebook.cells[0].id;
|
|
213
|
+
* await host!.self.edit({ id, source: "# New title", source_hash: hashes[id] });
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
edit(params?: HostParams, options?: HostCallOptions): Promise<any>;
|
|
217
|
+
/**
|
|
218
|
+
* Delete a cell, given its source's hash.
|
|
219
|
+
*
|
|
220
|
+
* @param params The cell id and its source's hash.
|
|
221
|
+
* @param options `signal` cancels the call.
|
|
222
|
+
* @example
|
|
223
|
+
* ```ts
|
|
224
|
+
* const host = await jupyter.host();
|
|
225
|
+
* const { notebook, hashes } = await host!.notebooks.read({ path: host!.notebook! });
|
|
226
|
+
* const id = notebook.cells.at(-1).id;
|
|
227
|
+
* await host!.self.delete({ id, source_hash: hashes[id] });
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
delete(params?: HostParams, options?: HostCallOptions): Promise<any>;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Options for `jupyter.host()`. */
|
|
234
|
+
export interface HostOptions {
|
|
235
|
+
/**
|
|
236
|
+
* How long to wait for a server that hasn't opened the comm, in ms.
|
|
237
|
+
* Default 1000. Once a wait goes unanswered, later calls answer `null`
|
|
238
|
+
* at once.
|
|
239
|
+
*/
|
|
240
|
+
timeout?: number;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function createHost(
|
|
244
|
+
comms: CommApi,
|
|
245
|
+
defaultSignal?: () => AbortSignal | undefined,
|
|
246
|
+
): { host(options?: HostOptions): Promise<Host | null> };
|