@tlockney/cdp-kernel 0.11.0 → 0.12.1
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 +16 -2
- package/dist/browser/kernel.js.map +2 -2
- package/dist/browser/mod.d.ts +27 -0
- package/dist/browser/mod.js +27 -0
- package/dist/browser/target.d.ts +10 -0
- package/dist/browser/target.js +1 -1
- package/dist/browser/worker.js +182 -14
- package/dist/browser/worker.js.map +4 -4
- package/dist/package.d.ts +3 -3
- package/dist/package.js +3 -2
- package/dist/workerd-target.js +1 -1
- package/package.json +1 -1
- package/runtime/browser-worker.mjs +9 -3
- package/runtime/cdn.d.mts +6 -1
- package/runtime/cdn.mjs +7 -2
- package/runtime/display.d.mts +136 -135
- package/runtime/display.mjs +15 -4
- package/runtime/sql.d.mts +14 -0
- package/runtime/sql.mjs +37 -6
- package/runtime/table-paging.d.mts +19 -0
- package/runtime/table-paging.mjs +183 -0
- package/runtime/table.d.mts +14 -0
- package/runtime/table.mjs +31 -2
- package/runtime/widget.d.mts +3 -3
package/dist/package.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.12.1";
|
|
3
3
|
/**
|
|
4
4
|
* The UTC day this version was released, set with VERSION. The publish
|
|
5
5
|
* workflow refuses a tag unless it's today or yesterday, so it can't go
|
|
6
6
|
* stale: see dependencyCutoff.
|
|
7
7
|
*/
|
|
8
|
-
export declare const RELEASE_DATE = "2026-11-
|
|
8
|
+
export declare const RELEASE_DATE = "2026-11-14";
|
|
9
9
|
/**
|
|
10
10
|
* Deno blocks packages less than 24 hours old (minimum dependency age), so
|
|
11
11
|
* a kernelspec relaunching a release from JSR would fail on release day. It
|
|
@@ -14,7 +14,7 @@ export declare const RELEASE_DATE = "2026-11-13";
|
|
|
14
14
|
* late in the day its tag was pushed), and nothing newer ever is.
|
|
15
15
|
*/
|
|
16
16
|
export declare function dependencyCutoff(releaseDate?: string): string;
|
|
17
|
-
export declare const RUNTIME_FILES: readonly ["bootstrap.mjs", "bun-channel.mjs", "evaluator.mjs", "introspect.mjs", "workerd-worker.mjs", "display.mjs", "comm.mjs", "host.mjs", "widget.mjs", "table.mjs", "sql.mjs", "chart.mjs", "node-hooks.mjs", "deno-compat.mjs", "node-modules-resolve.mjs"];
|
|
17
|
+
export declare const RUNTIME_FILES: readonly ["bootstrap.mjs", "bun-channel.mjs", "evaluator.mjs", "introspect.mjs", "workerd-worker.mjs", "display.mjs", "comm.mjs", "host.mjs", "widget.mjs", "table.mjs", "table-paging.mjs", "sql.mjs", "chart.mjs", "node-hooks.mjs", "deno-compat.mjs", "node-modules-resolve.mjs"];
|
|
18
18
|
export type HostRuntime = "node" | "deno";
|
|
19
19
|
export declare function hostRuntime(): HostRuntime;
|
|
20
20
|
/** Where the CLI module was loaded from. */
|
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.12.1";
|
|
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-14";
|
|
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
|
|
@@ -46,6 +46,7 @@ export const RUNTIME_FILES = [
|
|
|
46
46
|
"host.mjs",
|
|
47
47
|
"widget.mjs",
|
|
48
48
|
"table.mjs",
|
|
49
|
+
"table-paging.mjs",
|
|
49
50
|
"sql.mjs",
|
|
50
51
|
"chart.mjs",
|
|
51
52
|
"node-hooks.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", "host.mjs", "widget.mjs", "table.mjs", "sql.mjs", "chart.mjs", "evaluator.mjs", "introspect.mjs"];
|
|
38
|
+
export const WORKER_MODULES = ["workerd-worker.mjs", "display.mjs", "comm.mjs", "host.mjs", "widget.mjs", "table.mjs", "table-paging.mjs", "sql.mjs", "chart.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) {
|
package/package.json
CHANGED
|
@@ -72,6 +72,8 @@ let paramsText;
|
|
|
72
72
|
|
|
73
73
|
// --- Modules ----------------------------------------------------------------
|
|
74
74
|
let cdn = DEFAULT_CDN;
|
|
75
|
+
/** Specifiers the page serves itself, such as jupyter.chart's libraries in the Ganymede apps. */
|
|
76
|
+
let modules;
|
|
75
77
|
|
|
76
78
|
|
|
77
79
|
// --- The host page (#384) -----------------------------------------------------
|
|
@@ -153,13 +155,17 @@ async function preload(urls, base) {
|
|
|
153
155
|
// --- The shim cells and the evaluator use -----------------------------------
|
|
154
156
|
let handle = null;
|
|
155
157
|
|
|
156
|
-
function install({ base, cdn: cdnBase, params }) {
|
|
158
|
+
function install({ base, cdn: cdnBase, modules: moduleMap, params }) {
|
|
157
159
|
if (typeof params === "string") paramsText = params;
|
|
158
160
|
if (typeof cdnBase === "string") cdn = cdnBase.endsWith("/") ? cdnBase : cdnBase + "/";
|
|
161
|
+
if (moduleMap !== null && typeof moduleMap === "object") modules = { ...moduleMap };
|
|
159
162
|
const url = new URL("$cdpk$repl.mjs", base).href;
|
|
160
163
|
globalThis.__cdpk = {
|
|
161
|
-
import: (specifier, options) => import(resolveSpecifier(specifier, url, cdn), options),
|
|
162
|
-
meta: { url, resolve: (specifier) => resolveSpecifier(specifier, url, cdn) },
|
|
164
|
+
import: (specifier, options) => import(resolveSpecifier(specifier, url, cdn, modules), options),
|
|
165
|
+
meta: { url, resolve: (specifier) => resolveSpecifier(specifier, url, cdn, modules) },
|
|
166
|
+
// The URL `modules` maps a specifier to, or null: jupyter.sql's DuckDB
|
|
167
|
+
// files load from the page this way.
|
|
168
|
+
mapped: (specifier) => (modules && Object.hasOwn(modules, specifier) ? new URL(modules[specifier], url).href : null),
|
|
163
169
|
// inspect_request (#10): by descriptor, so nothing runs.
|
|
164
170
|
describeForInspect: (value, detail) => describeValue(value, { inspect: (v) => inspect(v, { depth: 2 }), detail }),
|
|
165
171
|
inspectPath: (path, detail) => inspectPath(path, { inspect: (v) => inspect(v, { depth: 2 }), detail }),
|
package/runtime/cdn.d.mts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
export const DEFAULT_CDN: string;
|
|
2
|
-
export function resolveSpecifier(
|
|
2
|
+
export function resolveSpecifier(
|
|
3
|
+
specifier: string,
|
|
4
|
+
base: string,
|
|
5
|
+
cdn?: string,
|
|
6
|
+
modules?: { readonly [specifier: string]: string },
|
|
7
|
+
): string;
|
package/runtime/cdn.mjs
CHANGED
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
export const DEFAULT_CDN = "https://esm.sh/";
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The URL `specifier`, imported from `base`, loads from. `modules` maps
|
|
11
|
+
* specifiers, as written, to URLs (resolved against `base`) that win over
|
|
12
|
+
* the CDN: an app ships the packages it needs offline this way.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveSpecifier(specifier, base, cdn = DEFAULT_CDN, modules = undefined) {
|
|
15
|
+
if (modules && Object.hasOwn(modules, specifier)) return new URL(modules[specifier], base).href;
|
|
11
16
|
if (/^(?:https?|data|blob):/.test(specifier)) return specifier;
|
|
12
17
|
if (/^\.{0,2}\//.test(specifier)) return new URL(specifier, base).href;
|
|
13
18
|
if (specifier.startsWith("npm:")) return cdn + specifier.slice(4);
|
package/runtime/display.d.mts
CHANGED
|
@@ -83,33 +83,6 @@ export interface Displayable {
|
|
|
83
83
|
* as `Deno.jupyter` in `deno jupyter`.
|
|
84
84
|
*/
|
|
85
85
|
export interface JupyterNamespace {
|
|
86
|
-
/**
|
|
87
|
-
* Send a raw Jupyter message on the kernel's output channel (IOPub), as a
|
|
88
|
-
* reply to the running cell. The other helpers are built on it. It does
|
|
89
|
-
* not check the message: a wrong field is ignored or mishandled by the
|
|
90
|
-
* frontend.
|
|
91
|
-
*
|
|
92
|
-
* @param msgType An IOPub message type, such as `"clear_output"` or
|
|
93
|
-
* `"display_data"`.
|
|
94
|
-
* @param content The message content, as the messaging spec defines it
|
|
95
|
-
* for `msgType`.
|
|
96
|
-
* @param options `metadata` for the message, and binary `buffers`.
|
|
97
|
-
* @example
|
|
98
|
-
* ```ts
|
|
99
|
-
* // Count down in place: clear_output with wait avoids a flicker.
|
|
100
|
-
* for (const n of [3, 2, 1]) {
|
|
101
|
-
* jupyter.broadcast("clear_output", { wait: true });
|
|
102
|
-
* await jupyter.display(jupyter.md`Launching in **${n}**…`);
|
|
103
|
-
* await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
104
|
-
* }
|
|
105
|
-
* ```
|
|
106
|
-
* @see jupyter-api.md#raw-messages-with-broadcast
|
|
107
|
-
*/
|
|
108
|
-
broadcast(
|
|
109
|
-
msgType: string,
|
|
110
|
-
content: object,
|
|
111
|
-
options?: { metadata?: object; buffers?: Uint8Array[] },
|
|
112
|
-
): void;
|
|
113
86
|
/**
|
|
114
87
|
* Show a value now, in the middle of a cell, or replace one shown
|
|
115
88
|
* earlier. The value can be anything a cell could end with: a template
|
|
@@ -142,22 +115,6 @@ export interface JupyterNamespace {
|
|
|
142
115
|
* @see jupyter-api.md#showing-output-with-display
|
|
143
116
|
*/
|
|
144
117
|
display(obj: unknown, options?: DisplayOptions): Promise<void>;
|
|
145
|
-
/**
|
|
146
|
-
* The MIME bundle that displaying a value would send, without showing
|
|
147
|
-
* anything. For plain values `text/plain` holds terminal color codes,
|
|
148
|
-
* as the notebook shows them.
|
|
149
|
-
*
|
|
150
|
-
* @param obj The value to format.
|
|
151
|
-
* @returns The bundle, for example `{ "text/plain": "\x1b[33m42\x1b[39m" }`
|
|
152
|
-
* for `42`.
|
|
153
|
-
* @example
|
|
154
|
-
* ```ts
|
|
155
|
-
* const bundle = await jupyter.format(jupyter.md`# Weekly report`);
|
|
156
|
-
* bundle["text/markdown"]; // "# Weekly report"
|
|
157
|
-
* ```
|
|
158
|
-
* @see jupyter-api.md#getting-a-bundle-with-format
|
|
159
|
-
*/
|
|
160
|
-
format(obj: unknown): Promise<MediaBundle>;
|
|
161
118
|
/**
|
|
162
119
|
* A tagged template that renders as Markdown. Interpolated values are
|
|
163
120
|
* inserted as is, without escaping.
|
|
@@ -250,109 +207,48 @@ export interface JupyterNamespace {
|
|
|
250
207
|
*/
|
|
251
208
|
$display: typeof $display;
|
|
252
209
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* @see widgets.md#comms
|
|
257
|
-
*/
|
|
258
|
-
comm: CommApi;
|
|
259
|
-
/**
|
|
260
|
-
* Create an anywidget-compatible widget model. Display it (or end the
|
|
261
|
-
* cell with it) to show a view.
|
|
262
|
-
*
|
|
263
|
-
* @param options The view module, its CSS, and the initial state.
|
|
264
|
-
* @returns The model, synced with every view of it.
|
|
265
|
-
* @throws {TypeError} `options.esm` is not a string.
|
|
266
|
-
* @availability The frontend needs the anywidget extension.
|
|
267
|
-
* @example
|
|
268
|
-
* ```ts
|
|
269
|
-
* const counter = jupyter.widget({
|
|
270
|
-
* esm: `export default {
|
|
271
|
-
* render({ model, el }) {
|
|
272
|
-
* const button = document.createElement("button");
|
|
273
|
-
* const update = () => {
|
|
274
|
-
* button.textContent = "count is " + model.get("count");
|
|
275
|
-
* };
|
|
276
|
-
* button.onclick = () => {
|
|
277
|
-
* model.set("count", model.get("count") + 1);
|
|
278
|
-
* model.save_changes();
|
|
279
|
-
* };
|
|
280
|
-
* model.on("change:count", update);
|
|
281
|
-
* update();
|
|
282
|
-
* el.append(button);
|
|
283
|
-
* },
|
|
284
|
-
* };`,
|
|
285
|
-
* state: { count: 0 },
|
|
286
|
-
* });
|
|
287
|
-
* counter; // the cell's result: displays the widget
|
|
288
|
-
* ```
|
|
289
|
-
* 
|
|
290
|
-
* @see widgets.md#a-first-widget-a-counter
|
|
291
|
-
*/
|
|
292
|
-
widget(options: WidgetOptions): Widget;
|
|
293
|
-
/**
|
|
294
|
-
* An `AbortSignal` that an interrupt aborts, so code waiting on something
|
|
295
|
-
* only it can end can stop and clean up. Each cell gets its own, as does
|
|
296
|
-
* a comm handler that starts while no cell runs. Read it once when the
|
|
297
|
-
* work starts: a later read gets the signal of whatever started last.
|
|
298
|
-
* The abort reason is a `DOMException` named `AbortError`.
|
|
299
|
-
*
|
|
300
|
-
* @example
|
|
301
|
-
* ```ts
|
|
302
|
-
* const response = await fetch("https://example.com/large.csv", { signal: jupyter.signal });
|
|
303
|
-
* ```
|
|
304
|
-
* @example
|
|
305
|
-
* ```ts
|
|
306
|
-
* const signal = jupyter.signal;
|
|
307
|
-
* const ask = jupyter.comm.open("my.app", { question: "ready?" });
|
|
308
|
-
* const answer = await new Promise((resolve, reject) => {
|
|
309
|
-
* ask.onMsg((m) => resolve(m.data));
|
|
310
|
-
* signal.addEventListener("abort", () => { ask.close(); reject(signal.reason); }, { once: true });
|
|
311
|
-
* });
|
|
312
|
-
* ```
|
|
313
|
-
* @see jupyter-api.md#stopping-on-interrupt-jupyter-signal
|
|
314
|
-
*/
|
|
315
|
-
readonly signal: AbortSignal;
|
|
316
|
-
/**
|
|
317
|
-
* The parameters of this run: a frozen object, empty unless whoever
|
|
318
|
-
* started the kernel passed some, such as `ganymede execute -p
|
|
319
|
-
* counter="2nd Ave"`. They arrive as a JSON object in the kernel's
|
|
320
|
-
* `GANYMEDE_PARAMS` environment variable. Take defaults with
|
|
321
|
-
* destructuring, so the notebook also runs on its own.
|
|
210
|
+
* The MIME bundle that displaying a value would send, without showing
|
|
211
|
+
* anything. For plain values `text/plain` holds terminal color codes,
|
|
212
|
+
* as the notebook shows them.
|
|
322
213
|
*
|
|
323
|
-
* @
|
|
324
|
-
* @
|
|
325
|
-
*
|
|
214
|
+
* @param obj The value to format.
|
|
215
|
+
* @returns The bundle, for example `{ "text/plain": "\x1b[33m42\x1b[39m" }`
|
|
216
|
+
* for `42`.
|
|
326
217
|
* @example
|
|
327
218
|
* ```ts
|
|
328
|
-
* const
|
|
329
|
-
*
|
|
219
|
+
* const bundle = await jupyter.format(jupyter.md`# Weekly report`);
|
|
220
|
+
* bundle["text/markdown"]; // "# Weekly report"
|
|
330
221
|
* ```
|
|
331
|
-
* @see jupyter-api.md#
|
|
222
|
+
* @see jupyter-api.md#getting-a-bundle-with-format
|
|
332
223
|
*/
|
|
333
|
-
|
|
224
|
+
format(obj: unknown): Promise<MediaBundle>;
|
|
334
225
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* a `text/plain` line `name = value` for frontends that don't know the
|
|
340
|
-
* type. When a name is published twice, readers take the last one.
|
|
226
|
+
* Send a raw Jupyter message on the kernel's output channel (IOPub), as a
|
|
227
|
+
* reply to the running cell. The other helpers are built on it. It does
|
|
228
|
+
* not check the message: a wrong field is ignored or mishandled by the
|
|
229
|
+
* frontend.
|
|
341
230
|
*
|
|
342
|
-
* @param
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
231
|
+
* @param msgType An IOPub message type, such as `"clear_output"` or
|
|
232
|
+
* `"display_data"`.
|
|
233
|
+
* @param content The message content, as the messaging spec defines it
|
|
234
|
+
* for `msgType`.
|
|
235
|
+
* @param options `metadata` for the message, and binary `buffers`.
|
|
347
236
|
* @example
|
|
348
237
|
* ```ts
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
238
|
+
* // Count down in place: clear_output with wait avoids a flicker.
|
|
239
|
+
* for (const n of [3, 2, 1]) {
|
|
240
|
+
* jupyter.broadcast("clear_output", { wait: true });
|
|
241
|
+
* await jupyter.display(jupyter.md`Launching in **${n}**…`);
|
|
242
|
+
* await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
243
|
+
* }
|
|
352
244
|
* ```
|
|
353
|
-
* @see jupyter-api.md#
|
|
245
|
+
* @see jupyter-api.md#raw-messages-with-broadcast
|
|
354
246
|
*/
|
|
355
|
-
|
|
247
|
+
broadcast(
|
|
248
|
+
msgType: string,
|
|
249
|
+
content: object,
|
|
250
|
+
options?: { metadata?: object; buffers?: Uint8Array[] },
|
|
251
|
+
): void;
|
|
356
252
|
/**
|
|
357
253
|
* Show rows and columns as a table. The output has Ganymede's table
|
|
358
254
|
* type, `application/vnd.dataresource+json` (Table Schema fields, up to
|
|
@@ -456,6 +352,111 @@ export interface JupyterNamespace {
|
|
|
456
352
|
* @see jupyter-api.md#charts
|
|
457
353
|
*/
|
|
458
354
|
chart(chart: object, options?: ChartOptions | Record<string, unknown>): Displayable;
|
|
355
|
+
/**
|
|
356
|
+
* An `AbortSignal` that an interrupt aborts, so code waiting on something
|
|
357
|
+
* only it can end can stop and clean up. Each cell gets its own, as does
|
|
358
|
+
* a comm handler that starts while no cell runs. Read it once when the
|
|
359
|
+
* work starts: a later read gets the signal of whatever started last.
|
|
360
|
+
* The abort reason is a `DOMException` named `AbortError`.
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* ```ts
|
|
364
|
+
* const response = await fetch("https://example.com/large.csv", { signal: jupyter.signal });
|
|
365
|
+
* ```
|
|
366
|
+
* @example
|
|
367
|
+
* ```ts
|
|
368
|
+
* const signal = jupyter.signal;
|
|
369
|
+
* const ask = jupyter.comm.open("my.app", { question: "ready?" });
|
|
370
|
+
* const answer = await new Promise((resolve, reject) => {
|
|
371
|
+
* ask.onMsg((m) => resolve(m.data));
|
|
372
|
+
* signal.addEventListener("abort", () => { ask.close(); reject(signal.reason); }, { once: true });
|
|
373
|
+
* });
|
|
374
|
+
* ```
|
|
375
|
+
* @see jupyter-api.md#stopping-on-interrupt-jupyter-signal
|
|
376
|
+
*/
|
|
377
|
+
readonly signal: AbortSignal;
|
|
378
|
+
/**
|
|
379
|
+
* Jupyter comms: message channels to the frontend outside cell
|
|
380
|
+
* execution. Its methods are in the widgets and comms reference.
|
|
381
|
+
*
|
|
382
|
+
* @see widgets.md#comms
|
|
383
|
+
*/
|
|
384
|
+
comm: CommApi;
|
|
385
|
+
/**
|
|
386
|
+
* Create an [anywidget](https://anywidget.dev)-compatible [widget
|
|
387
|
+
* model](widgets-reference.md#widget-model). Display it (or end the
|
|
388
|
+
* cell with it) to show a view.
|
|
389
|
+
*
|
|
390
|
+
* @param options The view module, its CSS, and the initial state.
|
|
391
|
+
* @returns The model, synced with every view of it.
|
|
392
|
+
* @throws {TypeError} `options.esm` is not a string.
|
|
393
|
+
* @availability The frontend needs the [anywidget](https://anywidget.dev) extension.
|
|
394
|
+
* @example
|
|
395
|
+
* ```ts
|
|
396
|
+
* const counter = jupyter.widget({
|
|
397
|
+
* esm: `export default {
|
|
398
|
+
* render({ model, el }) {
|
|
399
|
+
* const button = document.createElement("button");
|
|
400
|
+
* const update = () => {
|
|
401
|
+
* button.textContent = "count is " + model.get("count");
|
|
402
|
+
* };
|
|
403
|
+
* button.onclick = () => {
|
|
404
|
+
* model.set("count", model.get("count") + 1);
|
|
405
|
+
* model.save_changes();
|
|
406
|
+
* };
|
|
407
|
+
* model.on("change:count", update);
|
|
408
|
+
* update();
|
|
409
|
+
* el.append(button);
|
|
410
|
+
* },
|
|
411
|
+
* };`,
|
|
412
|
+
* state: { count: 0 },
|
|
413
|
+
* });
|
|
414
|
+
* counter; // the cell's result: displays the widget
|
|
415
|
+
* ```
|
|
416
|
+
* 
|
|
417
|
+
* @see widgets.md#a-first-widget-a-counter
|
|
418
|
+
*/
|
|
419
|
+
widget(options: WidgetOptions): Widget;
|
|
420
|
+
/**
|
|
421
|
+
* The parameters of this run: a frozen object, empty unless whoever
|
|
422
|
+
* started the kernel passed some, such as `ganymede execute -p
|
|
423
|
+
* counter="2nd Ave"`. They arrive as a JSON object in the kernel's
|
|
424
|
+
* `GANYMEDE_PARAMS` environment variable. Take defaults with
|
|
425
|
+
* destructuring, so the notebook also runs on its own.
|
|
426
|
+
*
|
|
427
|
+
* @throws {TypeError} `GANYMEDE_PARAMS` is set but isn't a JSON object.
|
|
428
|
+
* @availability Node, Deno, Bun, workerd and celld read the kernel's
|
|
429
|
+
* environment; the browser target takes `params` from its options.
|
|
430
|
+
* @example
|
|
431
|
+
* ```ts
|
|
432
|
+
* const { counter = "Fremont", days = 30 } = jupyter.params as { counter?: string; days?: number };
|
|
433
|
+
* `${counter}, last ${days} days`;
|
|
434
|
+
* ```
|
|
435
|
+
* @see jupyter-api.md#parameters-and-named-results
|
|
436
|
+
*/
|
|
437
|
+
readonly params: { readonly [name: string]: JsonValue };
|
|
438
|
+
/**
|
|
439
|
+
* Publish a value under a name, as an output that a run's caller reads
|
|
440
|
+
* by name instead of parsing outputs: `ganymede execute --results`, or
|
|
441
|
+
* another notebook. The output has the MIME type
|
|
442
|
+
* `application/vnd.ganymede.result+json` holding `{ name, value }`, and
|
|
443
|
+
* a `text/plain` line `name = value` for frontends that don't know the
|
|
444
|
+
* type. When a name is published twice, readers take the last one.
|
|
445
|
+
*
|
|
446
|
+
* @param name The result's name.
|
|
447
|
+
* @param value Any value `JSON.stringify` accepts; it is stored as JSON,
|
|
448
|
+
* so a `Date` becomes its ISO string.
|
|
449
|
+
* @throws {TypeError} `name` is empty, or `value` isn't JSON (a
|
|
450
|
+
* function, `undefined`, a `BigInt` or a cycle).
|
|
451
|
+
* @example
|
|
452
|
+
* ```ts
|
|
453
|
+
* const counts = [3120, 2890, 2410];
|
|
454
|
+
* jupyter.result("weekday_drop", (counts[2] - counts[0]) / counts[0]);
|
|
455
|
+
* jupyter.result("counts", counts);
|
|
456
|
+
* ```
|
|
457
|
+
* @see jupyter-api.md#parameters-and-named-results
|
|
458
|
+
*/
|
|
459
|
+
result(name: string, value: unknown): void;
|
|
459
460
|
/**
|
|
460
461
|
* The Ganymede server running this kernel, through its host API: read,
|
|
461
462
|
* search and (when granted) run other notebooks under its root. The
|
package/runtime/display.mjs
CHANGED
|
@@ -23,7 +23,8 @@ import { createHost } from "./host.mjs";
|
|
|
23
23
|
import { createWidgetFactory } from "./widget.mjs";
|
|
24
24
|
import { createSql } from "./sql.mjs";
|
|
25
25
|
import { createChart } from "./chart.mjs";
|
|
26
|
-
import { isTableLike, tableBundle, tableSource } from "./table.mjs";
|
|
26
|
+
import { isTableLike, pageTable, tableBundle, tableSource } from "./table.mjs";
|
|
27
|
+
import { createTablePager } from "./table-paging.mjs";
|
|
27
28
|
|
|
28
29
|
export const $display = Symbol.for("Jupyter.display");
|
|
29
30
|
|
|
@@ -368,10 +369,14 @@ export function createJupyter(host) {
|
|
|
368
369
|
host.send(msgType, content, metadata, buffers);
|
|
369
370
|
}
|
|
370
371
|
|
|
372
|
+
// Set below, once the comms are: a table cut by the cap is kept for
|
|
373
|
+
// paging when it is sent as an output, not when it is formatted.
|
|
374
|
+
let pager;
|
|
375
|
+
|
|
371
376
|
async function display(obj, options = { raw: false, update: false }) {
|
|
372
377
|
const bundle = options.raw && isMediaBundle(obj) ? obj : await format(obj);
|
|
373
378
|
broadcast(options.update ? "update_display_data" : "display_data", {
|
|
374
|
-
data: bundle,
|
|
379
|
+
data: pageTable(bundle, pager, options.display_id),
|
|
375
380
|
metadata: {},
|
|
376
381
|
transient: options.display_id ? { display_id: options.display_id } : {},
|
|
377
382
|
});
|
|
@@ -382,7 +387,7 @@ export function createJupyter(host) {
|
|
|
382
387
|
try {
|
|
383
388
|
broadcast("execute_result", {
|
|
384
389
|
execution_count: executionCount,
|
|
385
|
-
data: await format(result),
|
|
390
|
+
data: pageTable(await format(result), pager),
|
|
386
391
|
metadata: {},
|
|
387
392
|
// nbclient crashes on a null `transient`; Deno learned this too.
|
|
388
393
|
transient: {},
|
|
@@ -457,12 +462,18 @@ export function createJupyter(host) {
|
|
|
457
462
|
|
|
458
463
|
// jupyter.sql (#477): DuckDB, loaded on the first query; an interrupt
|
|
459
464
|
// (jupyter.signal) interrupts the query.
|
|
460
|
-
const sql = createSql({
|
|
465
|
+
const sql = createSql({
|
|
466
|
+
importModule: cellImport("jupyter.sql"),
|
|
467
|
+
mapped: (specifier) => globalThis.__cdpk?.mapped?.(specifier) ?? null,
|
|
468
|
+
signal: () => current.signal,
|
|
469
|
+
});
|
|
461
470
|
|
|
462
471
|
// jupyter.chart (#479): vega, vega-lite or linkedom, loaded on first use.
|
|
463
472
|
const chart = createChart({ importModule: cellImport("jupyter.chart"), summarizeSpec: summarizeChart });
|
|
464
473
|
|
|
465
474
|
const comms = createComms((msgType, content, metadata, buffers) => host.send(msgType, content, metadata, buffers));
|
|
475
|
+
// Tables cut by the cap page through the kernel over a comm (ganymede#747).
|
|
476
|
+
pager = createTablePager(comms.api);
|
|
466
477
|
|
|
467
478
|
// jupyter.signal (#418): aborted when the kernel is interrupted, so cell
|
|
468
479
|
// code can stop waiting on what the interrupt can't reach, such as a comm
|
package/runtime/sql.d.mts
CHANGED
|
@@ -2,6 +2,19 @@ import type { Displayable } from "./display.d.mts";
|
|
|
2
2
|
|
|
3
3
|
export const NODE_API: string;
|
|
4
4
|
export const DUCKDB_WASM: string;
|
|
5
|
+
export interface DuckDBBundle {
|
|
6
|
+
mainModule: string;
|
|
7
|
+
mainWorker: string;
|
|
8
|
+
pthreadWorker?: string | null;
|
|
9
|
+
}
|
|
10
|
+
export const DUCKDB_FILES: {
|
|
11
|
+
readonly mvp: { readonly mainModule: string; readonly mainWorker: string };
|
|
12
|
+
readonly eh: { readonly mainModule: string; readonly mainWorker: string };
|
|
13
|
+
};
|
|
14
|
+
export function duckdbBundles(
|
|
15
|
+
jsDelivr: Record<string, DuckDBBundle>,
|
|
16
|
+
mapped?: (specifier: string) => string | null | undefined,
|
|
17
|
+
): Record<string, DuckDBBundle>;
|
|
5
18
|
|
|
6
19
|
/** One column of a query's result. */
|
|
7
20
|
export interface SqlColumn {
|
|
@@ -142,6 +155,7 @@ export interface SqlEngine {
|
|
|
142
155
|
|
|
143
156
|
export function createSql(options: {
|
|
144
157
|
importModule: (specifier: string) => Promise<any>;
|
|
158
|
+
mapped?: (specifier: string) => string | null | undefined;
|
|
145
159
|
signal?: () => AbortSignal | undefined;
|
|
146
160
|
openEngine?: () => Promise<SqlEngine>;
|
|
147
161
|
}): Sql;
|
package/runtime/sql.mjs
CHANGED
|
@@ -21,6 +21,35 @@ export { timeText, uniqueNames };
|
|
|
21
21
|
export const NODE_API = "npm:@duckdb/node-api@1.5.6-r.2";
|
|
22
22
|
export const DUCKDB_WASM = "npm:@duckdb/duckdb-wasm@1.29.0";
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The files duckdb-wasm runs from besides its module, by the specifier a
|
|
26
|
+
* browser kernel's `modules` option maps to a URL the page serves. A
|
|
27
|
+
* variant whose wasm and worker are both mapped loads from those URLs;
|
|
28
|
+
* the others load from jsDelivr, as duckdb-wasm's own list has them.
|
|
29
|
+
*/
|
|
30
|
+
export const DUCKDB_FILES = {
|
|
31
|
+
mvp: { mainModule: `${DUCKDB_WASM}/dist/duckdb-mvp.wasm`, mainWorker: `${DUCKDB_WASM}/dist/duckdb-browser-mvp.worker.js` },
|
|
32
|
+
eh: { mainModule: `${DUCKDB_WASM}/dist/duckdb-eh.wasm`, mainWorker: `${DUCKDB_WASM}/dist/duckdb-browser-eh.worker.js` },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* duckdb-wasm's bundles: jsDelivr's, with each variant `mapped` gives
|
|
37
|
+
* both files for in their place.
|
|
38
|
+
*
|
|
39
|
+
* @param {Record<string, { mainModule: string, mainWorker: string, pthreadWorker?: string | null }>} jsDelivr
|
|
40
|
+
* @param {(specifier: string) => string | null | undefined} [mapped]
|
|
41
|
+
*/
|
|
42
|
+
export function duckdbBundles(jsDelivr, mapped) {
|
|
43
|
+
const bundles = { ...jsDelivr };
|
|
44
|
+
if (!mapped) return bundles;
|
|
45
|
+
for (const [variant, files] of Object.entries(DUCKDB_FILES)) {
|
|
46
|
+
const mainModule = mapped(files.mainModule);
|
|
47
|
+
const mainWorker = mapped(files.mainWorker);
|
|
48
|
+
if (mainModule && mainWorker) bundles[variant] = { mainModule, mainWorker, pthreadWorker: null };
|
|
49
|
+
}
|
|
50
|
+
return bundles;
|
|
51
|
+
}
|
|
52
|
+
|
|
24
53
|
const $display = Symbol.for("Jupyter.display");
|
|
25
54
|
const RAW = Symbol("cdpk.sql.raw");
|
|
26
55
|
|
|
@@ -190,6 +219,7 @@ const DUCK_TYPES = {
|
|
|
190
219
|
/**
|
|
191
220
|
* @param {{
|
|
192
221
|
* importModule: (specifier: string) => Promise<any>,
|
|
222
|
+
* mapped?: (specifier: string) => string | null | undefined,
|
|
193
223
|
* signal?: () => AbortSignal | undefined,
|
|
194
224
|
* openEngine?: () => Promise<Engine>,
|
|
195
225
|
* }} options
|
|
@@ -201,9 +231,9 @@ const DUCK_TYPES = {
|
|
|
201
231
|
* connection: unknown,
|
|
202
232
|
* }} Engine
|
|
203
233
|
*/
|
|
204
|
-
export function createSql({ importModule, signal, openEngine }) {
|
|
234
|
+
export function createSql({ importModule, mapped, signal, openEngine }) {
|
|
205
235
|
let engine = null;
|
|
206
|
-
const open = () => (engine ??= (openEngine ?? (() => defaultEngine(importModule)))().catch((err) => {
|
|
236
|
+
const open = () => (engine ??= (openEngine ?? (() => defaultEngine(importModule, mapped)))().catch((err) => {
|
|
207
237
|
engine = null;
|
|
208
238
|
throw err;
|
|
209
239
|
}));
|
|
@@ -321,9 +351,9 @@ function hasNativeRuntime() {
|
|
|
321
351
|
typeof globalThis.process?.versions?.node === "string" && !inWorker();
|
|
322
352
|
}
|
|
323
353
|
|
|
324
|
-
async function defaultEngine(importModule) {
|
|
354
|
+
async function defaultEngine(importModule, mapped) {
|
|
325
355
|
if (hasNativeRuntime()) return nativeEngine(importModule);
|
|
326
|
-
if (inWorker() && typeof Worker === "function") return wasmEngine(importModule);
|
|
356
|
+
if (inWorker() && typeof Worker === "function") return wasmEngine(importModule, mapped);
|
|
327
357
|
throw new Error("jupyter.sql needs Node, Deno, Bun or the browser target; workerd and celld have no DuckDB");
|
|
328
358
|
}
|
|
329
359
|
|
|
@@ -355,9 +385,10 @@ async function nativeEngine(importModule) {
|
|
|
355
385
|
};
|
|
356
386
|
}
|
|
357
387
|
|
|
358
|
-
async function wasmEngine(importModule) {
|
|
388
|
+
async function wasmEngine(importModule, mapped) {
|
|
359
389
|
const duckdb = await importModule(DUCKDB_WASM);
|
|
360
|
-
|
|
390
|
+
// An app that serves the files (DUCKDB_FILES) runs SQL offline.
|
|
391
|
+
const bundle = await duckdb.selectBundle(duckdbBundles(duckdb.getJsDelivrBundles(), mapped));
|
|
361
392
|
// A Worker can only start from its own origin's scripts, so a Blob
|
|
362
393
|
// script loads DuckDB's.
|
|
363
394
|
const loader = URL.createObjectURL(new Blob([`importScripts(${JSON.stringify(bundle.mainWorker)});`], { type: "text/javascript" }));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CommApi } from "./comm.d.mts";
|
|
2
|
+
import type { TablePager } from "./table.d.mts";
|
|
3
|
+
|
|
4
|
+
/** The comm target a frontend opens, with `{ id }`, to page through a table. */
|
|
5
|
+
export const PAGE_TARGET: "ganymede.table.v1";
|
|
6
|
+
/** How many tables the kernel keeps for paging, the newest. */
|
|
7
|
+
export const KEEP: number;
|
|
8
|
+
/** The most rows one request returns. */
|
|
9
|
+
export const MAX_PAGE: number;
|
|
10
|
+
/** The most rows a table can have to be sorted in the kernel. */
|
|
11
|
+
export const MAX_SORT: number;
|
|
12
|
+
|
|
13
|
+
/** Compares two encoded values of a column as the Ganymede apps' table does. */
|
|
14
|
+
export function compareEncoded(a: unknown, b: unknown, type: string | undefined, descending?: boolean): number;
|
|
15
|
+
|
|
16
|
+
export function createTablePager(
|
|
17
|
+
commApi: CommApi,
|
|
18
|
+
options?: { keep?: number; newId?: () => string },
|
|
19
|
+
): TablePager & { size(): number };
|