@tlockney/cdp-kernel 0.1.1 → 0.3.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 +274 -21
- package/dist/bun-target.d.ts +58 -0
- package/dist/bun-target.js +231 -0
- package/dist/celld-target.d.ts +39 -0
- package/dist/celld-target.js +274 -0
- package/dist/deno-config.d.ts +20 -0
- package/dist/deno-config.js +130 -0
- package/dist/globals-deno-compat.d.ts +21 -0
- package/dist/globals-deno-compat.js +10 -0
- package/dist/globals.d.ts +36 -0
- package/dist/globals.js +15 -0
- package/dist/kernel.d.ts +2 -1
- package/dist/kernel.js +126 -11
- package/dist/main.js +5 -5
- package/dist/package.d.ts +13 -5
- package/dist/package.js +33 -8
- package/dist/target.d.ts +78 -7
- package/dist/target.js +67 -12
- package/dist/transform.d.ts +25 -1
- package/dist/transform.js +150 -11
- package/dist/workerd-target.d.ts +123 -0
- package/dist/workerd-target.js +450 -0
- package/dist/workers-project.d.ts +27 -0
- package/dist/workers-project.js +195 -0
- package/dist/ws-client.d.ts +32 -0
- package/dist/ws-client.js +166 -0
- package/package.json +27 -4
- package/runtime/bootstrap.mjs +78 -7
- package/runtime/bun-channel.d.mts +3 -0
- package/runtime/bun-channel.mjs +58 -0
- package/runtime/comm.d.mts +46 -0
- package/runtime/comm.mjs +108 -0
- package/runtime/deno-compat.mjs +1 -0
- package/runtime/display.d.mts +11 -0
- package/runtime/display.mjs +11 -1
- package/runtime/evaluator.d.mts +18 -0
- package/runtime/evaluator.mjs +83 -0
- package/runtime/node-hooks.d.mts +11 -0
- package/runtime/node-hooks.mjs +24 -12
- package/runtime/node-modules-resolve.d.mts +8 -0
- package/runtime/node-modules-resolve.mjs +106 -0
- package/runtime/widget.d.mts +40 -0
- package/runtime/widget.mjs +208 -0
- package/runtime/workerd-worker.mjs +189 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
export declare const OP_CONTINUATION = 0;
|
|
3
|
+
export declare const OP_TEXT = 1;
|
|
4
|
+
export declare const OP_CLOSE = 8;
|
|
5
|
+
export declare const OP_PING = 9;
|
|
6
|
+
export declare const OP_PONG = 10;
|
|
7
|
+
export interface Frame {
|
|
8
|
+
fin: boolean;
|
|
9
|
+
opcode: number;
|
|
10
|
+
payload: Buffer;
|
|
11
|
+
}
|
|
12
|
+
/** One frame; clients must mask what they send. */
|
|
13
|
+
export declare function encodeFrame(opcode: number, payload: Buffer, mask?: Buffer | null): Buffer;
|
|
14
|
+
/** Splits a byte stream into frames, holding back a partial one. */
|
|
15
|
+
export declare class FrameDecoder {
|
|
16
|
+
#private;
|
|
17
|
+
push(chunk: Buffer): Frame[];
|
|
18
|
+
}
|
|
19
|
+
export declare function acceptKey(key: string): string;
|
|
20
|
+
export interface WsChannel {
|
|
21
|
+
send(text: string): void;
|
|
22
|
+
onMessage(listener: (text: string) => void): void;
|
|
23
|
+
onClose(listener: () => void): void;
|
|
24
|
+
close(): void;
|
|
25
|
+
}
|
|
26
|
+
/** Opens a WebSocket over the Unix socket at `socketPath`. */
|
|
27
|
+
export declare function connectUnixWebSocket(socketPath: string, { path, headers }?: {
|
|
28
|
+
path?: string;
|
|
29
|
+
headers?: {
|
|
30
|
+
[name: string]: string;
|
|
31
|
+
};
|
|
32
|
+
}): Promise<WsChannel>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// A minimal WebSocket client (RFC 6455) over a Unix socket, for the workerd
|
|
2
|
+
// target's session with its driver Worker (#26). The WebSocket that Node
|
|
3
|
+
// and Deno provide can't connect to a Unix socket, and a TCP listener would
|
|
4
|
+
// be reachable by every local process. Text messages only: that's all the
|
|
5
|
+
// session carries.
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
8
|
+
import { connect } from "node:net";
|
|
9
|
+
const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
10
|
+
export const OP_CONTINUATION = 0x0;
|
|
11
|
+
export const OP_TEXT = 0x1;
|
|
12
|
+
export const OP_CLOSE = 0x8;
|
|
13
|
+
export const OP_PING = 0x9;
|
|
14
|
+
export const OP_PONG = 0xa;
|
|
15
|
+
/** One frame; clients must mask what they send. */
|
|
16
|
+
export function encodeFrame(opcode, payload, mask = randomBytes(4)) {
|
|
17
|
+
const len = payload.length;
|
|
18
|
+
const head = len < 126 ? 2 : len < 0x10000 ? 4 : 10;
|
|
19
|
+
const out = Buffer.alloc(head + (mask ? 4 : 0) + len);
|
|
20
|
+
out[0] = 0x80 | opcode;
|
|
21
|
+
const maskBit = mask ? 0x80 : 0;
|
|
22
|
+
if (len < 126) {
|
|
23
|
+
out[1] = maskBit | len;
|
|
24
|
+
}
|
|
25
|
+
else if (len < 0x10000) {
|
|
26
|
+
out[1] = maskBit | 126;
|
|
27
|
+
out.writeUInt16BE(len, 2);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
out[1] = maskBit | 127;
|
|
31
|
+
out.writeBigUInt64BE(BigInt(len), 2);
|
|
32
|
+
}
|
|
33
|
+
if (mask) {
|
|
34
|
+
mask.copy(out, head);
|
|
35
|
+
for (let i = 0; i < len; i++)
|
|
36
|
+
out[head + 4 + i] = payload[i] ^ mask[i & 3];
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
payload.copy(out, head);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** Splits a byte stream into frames, holding back a partial one. */
|
|
44
|
+
export class FrameDecoder {
|
|
45
|
+
#buffer = Buffer.alloc(0);
|
|
46
|
+
push(chunk) {
|
|
47
|
+
this.#buffer = Buffer.concat([this.#buffer, chunk]);
|
|
48
|
+
const frames = [];
|
|
49
|
+
for (;;) {
|
|
50
|
+
const b = this.#buffer;
|
|
51
|
+
if (b.length < 2)
|
|
52
|
+
break;
|
|
53
|
+
const masked = (b[1] & 0x80) !== 0;
|
|
54
|
+
let len = b[1] & 0x7f;
|
|
55
|
+
let offset = 2;
|
|
56
|
+
if (len === 126) {
|
|
57
|
+
if (b.length < 4)
|
|
58
|
+
break;
|
|
59
|
+
len = b.readUInt16BE(2);
|
|
60
|
+
offset = 4;
|
|
61
|
+
}
|
|
62
|
+
else if (len === 127) {
|
|
63
|
+
if (b.length < 10)
|
|
64
|
+
break;
|
|
65
|
+
len = Number(b.readBigUInt64BE(2));
|
|
66
|
+
offset = 10;
|
|
67
|
+
}
|
|
68
|
+
const maskAt = offset;
|
|
69
|
+
if (masked)
|
|
70
|
+
offset += 4;
|
|
71
|
+
if (b.length < offset + len)
|
|
72
|
+
break;
|
|
73
|
+
const payload = Buffer.from(b.subarray(offset, offset + len));
|
|
74
|
+
if (masked)
|
|
75
|
+
for (let i = 0; i < len; i++)
|
|
76
|
+
payload[i] ^= b[maskAt + (i & 3)];
|
|
77
|
+
frames.push({ fin: (b[0] & 0x80) !== 0, opcode: b[0] & 0x0f, payload });
|
|
78
|
+
this.#buffer = b.subarray(offset + len);
|
|
79
|
+
}
|
|
80
|
+
return frames;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function acceptKey(key) {
|
|
84
|
+
return createHash("sha1").update(key + GUID).digest("base64");
|
|
85
|
+
}
|
|
86
|
+
/** Opens a WebSocket over the Unix socket at `socketPath`. */
|
|
87
|
+
export function connectUnixWebSocket(socketPath, { path = "/", headers = {} } = {}) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const socket = connect({ path: socketPath });
|
|
90
|
+
const key = randomBytes(16).toString("base64");
|
|
91
|
+
const messageListeners = [];
|
|
92
|
+
const closeListeners = [];
|
|
93
|
+
let open = false;
|
|
94
|
+
let closed = false;
|
|
95
|
+
let head = Buffer.alloc(0);
|
|
96
|
+
const decoder = new FrameDecoder();
|
|
97
|
+
let parts = [];
|
|
98
|
+
const finish = () => {
|
|
99
|
+
if (closed)
|
|
100
|
+
return;
|
|
101
|
+
closed = true;
|
|
102
|
+
if (!open)
|
|
103
|
+
reject(new Error("the WebSocket closed before it opened"));
|
|
104
|
+
for (const listener of closeListeners)
|
|
105
|
+
listener();
|
|
106
|
+
};
|
|
107
|
+
const write = (opcode, payload) => {
|
|
108
|
+
if (!closed)
|
|
109
|
+
socket.write(encodeFrame(opcode, payload));
|
|
110
|
+
};
|
|
111
|
+
const channel = {
|
|
112
|
+
send: (text) => write(OP_TEXT, Buffer.from(text)),
|
|
113
|
+
onMessage: (listener) => void messageListeners.push(listener),
|
|
114
|
+
onClose: (listener) => void closeListeners.push(listener),
|
|
115
|
+
close() {
|
|
116
|
+
write(OP_CLOSE, Buffer.alloc(0));
|
|
117
|
+
socket.end();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
const onFrames = (frames) => {
|
|
121
|
+
for (const frame of frames) {
|
|
122
|
+
if (frame.opcode === OP_TEXT || frame.opcode === OP_CONTINUATION) {
|
|
123
|
+
parts.push(frame.payload);
|
|
124
|
+
if (frame.fin) {
|
|
125
|
+
const text = Buffer.concat(parts).toString("utf8");
|
|
126
|
+
parts = [];
|
|
127
|
+
for (const listener of messageListeners)
|
|
128
|
+
listener(text);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (frame.opcode === OP_PING) {
|
|
132
|
+
write(OP_PONG, frame.payload);
|
|
133
|
+
}
|
|
134
|
+
else if (frame.opcode === OP_CLOSE) {
|
|
135
|
+
socket.end();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
socket.on("data", (chunk) => {
|
|
140
|
+
if (open)
|
|
141
|
+
return onFrames(decoder.push(chunk));
|
|
142
|
+
head = Buffer.concat([head, chunk]);
|
|
143
|
+
const end = head.indexOf("\r\n\r\n");
|
|
144
|
+
if (end === -1)
|
|
145
|
+
return;
|
|
146
|
+
const response = head.subarray(0, end).toString("latin1");
|
|
147
|
+
const accept = /^sec-websocket-accept:\s*(\S+)/im.exec(response)?.[1];
|
|
148
|
+
if (!/^HTTP\/1\.1 101 /.test(response) || accept !== acceptKey(key)) {
|
|
149
|
+
socket.destroy();
|
|
150
|
+
reject(new Error(`the WebSocket handshake failed: ${response.split("\r\n")[0]}`));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
open = true;
|
|
154
|
+
resolve(channel);
|
|
155
|
+
onFrames(decoder.push(head.subarray(end + 4)));
|
|
156
|
+
});
|
|
157
|
+
socket.on("error", (err) => {
|
|
158
|
+
if (!open)
|
|
159
|
+
reject(err);
|
|
160
|
+
});
|
|
161
|
+
socket.on("close", finish);
|
|
162
|
+
const extra = Object.entries(headers).map(([name, value]) => `${name}: ${value}\r\n`).join("");
|
|
163
|
+
socket.write(`GET ${path} HTTP/1.1\r\nHost: cdpk\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n` +
|
|
164
|
+
`Sec-WebSocket-Key: ${key}\r\nSec-WebSocket-Version: 13\r\n${extra}\r\n`);
|
|
165
|
+
});
|
|
166
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tlockney/cdp-kernel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Deno's Jupyter kernel, extracted to run notebooks on any V8 runtime over the Chrome DevTools Protocol",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Thomas Lockney <thomas@lockney.net>",
|
|
@@ -8,7 +8,16 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/tlockney/cdp-kernel.git"
|
|
10
10
|
},
|
|
11
|
-
"keywords": [
|
|
11
|
+
"keywords": [
|
|
12
|
+
"jupyter",
|
|
13
|
+
"kernel",
|
|
14
|
+
"typescript",
|
|
15
|
+
"notebook",
|
|
16
|
+
"deno",
|
|
17
|
+
"node",
|
|
18
|
+
"cdp",
|
|
19
|
+
"devtools-protocol"
|
|
20
|
+
],
|
|
12
21
|
"type": "module",
|
|
13
22
|
"engines": {
|
|
14
23
|
"node": ">=24"
|
|
@@ -20,6 +29,14 @@
|
|
|
20
29
|
".": {
|
|
21
30
|
"types": "./dist/mod.d.ts",
|
|
22
31
|
"default": "./dist/mod.js"
|
|
32
|
+
},
|
|
33
|
+
"./globals": {
|
|
34
|
+
"types": "./dist/globals.d.ts",
|
|
35
|
+
"default": "./dist/globals.js"
|
|
36
|
+
},
|
|
37
|
+
"./globals/deno-compat": {
|
|
38
|
+
"types": "./dist/globals-deno-compat.d.ts",
|
|
39
|
+
"default": "./dist/globals-deno-compat.js"
|
|
23
40
|
}
|
|
24
41
|
},
|
|
25
42
|
"files": [
|
|
@@ -36,9 +53,13 @@
|
|
|
36
53
|
"install:node": "node src/main.ts install --runtime node",
|
|
37
54
|
"install:deno": "node src/main.ts install --runtime deno",
|
|
38
55
|
"install:node-deno": "node src/main.ts install --runtime node --deno-compat",
|
|
56
|
+
"install:bun": "node src/main.ts install --runtime bun",
|
|
57
|
+
"install:workerd": "node src/main.ts install --runtime workerd",
|
|
58
|
+
"install:celld": "node src/main.ts install --runtime celld",
|
|
39
59
|
"prepack": "npm run build",
|
|
40
60
|
"test": "node --test test/*.test.ts",
|
|
41
|
-
"e2e": "uv run --with nbclient --with nbformat --with jupyter_client python test/e2e.py"
|
|
61
|
+
"e2e": "uv run --with nbclient --with nbformat --with jupyter_client python test/e2e.py",
|
|
62
|
+
"test:lab": "uv run --with jupyterlab --with anywidget --with playwright --with nbformat python test/lab_widgets.py"
|
|
42
63
|
},
|
|
43
64
|
"dependencies": {
|
|
44
65
|
"acorn": "^8.15.0",
|
|
@@ -47,6 +68,8 @@
|
|
|
47
68
|
},
|
|
48
69
|
"devDependencies": {
|
|
49
70
|
"@types/node": "^24.19.2",
|
|
50
|
-
"typescript": "^5.9.3"
|
|
71
|
+
"typescript": "^5.9.3",
|
|
72
|
+
"workerd": "1.20261015.1",
|
|
73
|
+
"wrangler": "4.151.0"
|
|
51
74
|
}
|
|
52
75
|
}
|
package/runtime/bootstrap.mjs
CHANGED
|
@@ -4,17 +4,25 @@
|
|
|
4
4
|
// then drives it over CDP. Everything the host needs from inside the
|
|
5
5
|
// runtime lives on globalThis.__cdpk; output to the host goes through the
|
|
6
6
|
// `__cdpk_send` CDP binding (Runtime.addBinding) plus plain stdout/stderr.
|
|
7
|
+
// Bun is launched as `bun runtime/bootstrap.mjs` and driven over a pipe
|
|
8
|
+
// instead; see bun-channel.mjs.
|
|
7
9
|
//
|
|
8
|
-
// Plain JavaScript on purpose: the same file must load unmodified in Node
|
|
9
|
-
// and
|
|
10
|
+
// Plain JavaScript on purpose: the same file must load unmodified in Node,
|
|
11
|
+
// Deno and Bun.
|
|
10
12
|
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
14
|
import { createJupyter } from "./display.mjs";
|
|
12
15
|
|
|
13
16
|
const isDeno = typeof globalThis.Deno?.version?.deno === "string";
|
|
17
|
+
const isBun = typeof globalThis.Bun?.version === "string";
|
|
14
18
|
const ENC = new TextEncoder();
|
|
15
19
|
|
|
16
20
|
const cwd = isDeno ? Deno.cwd() : process.cwd();
|
|
17
|
-
|
|
21
|
+
// pathToFileURL, not string concatenation: a directory name containing
|
|
22
|
+
// "#", "?" or "%" would otherwise cut the path short or turn into a query,
|
|
23
|
+
// breaking relative imports and import.meta (#22). It also handles Windows
|
|
24
|
+
// drive letters.
|
|
25
|
+
const cwdUrl = pathToFileURL(cwd.endsWith("/") ? cwd : cwd + "/");
|
|
18
26
|
// Stand-in referrer for cell code, like Deno's ./$deno$jupyter_repl.mts.
|
|
19
27
|
const replUrl = new URL("$cdpk$repl.mjs", cwdUrl);
|
|
20
28
|
|
|
@@ -72,27 +80,82 @@ function resolveRelative(specifier) {
|
|
|
72
80
|
return /^\.{0,2}\//.test(specifier) ? new URL(specifier, replUrl).href : specifier;
|
|
73
81
|
}
|
|
74
82
|
|
|
75
|
-
if (!isDeno) {
|
|
83
|
+
if (!isDeno && !isBun) {
|
|
76
84
|
// Deno resolves npm:/jsr: natively; Node gets a resolve hook that adds
|
|
77
85
|
// them and re-parents cell imports to the notebook's directory.
|
|
78
86
|
const { installNodeResolveHooks } = await import("./node-hooks.mjs");
|
|
79
87
|
installNodeResolveHooks({ shimUrl: import.meta.url, replUrl: replUrl.href });
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
// In a Deno project (#13), Deno's own resolution (import map, npm:, jsr:)
|
|
91
|
+
// goes first, as in `deno jupyter`. A bare package it can't place, because
|
|
92
|
+
// package.json dependencies resolve relative to *this* module rather than
|
|
93
|
+
// the notebook, is retried from the project's node_modules.
|
|
94
|
+
const inDenoProject = isDeno && Deno.env.get("CDPK_DENO_PROJECT") === "1";
|
|
95
|
+
const { resolveFromNodeModules } = inDenoProject ? await import("./node-modules-resolve.mjs") : {};
|
|
96
|
+
|
|
97
|
+
function isResolutionError(err, specifier) {
|
|
98
|
+
return err instanceof TypeError && err.message.includes(`"${specifier}"`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function denoImport(specifier, options) {
|
|
102
|
+
const resolved = resolveRelative(specifier);
|
|
103
|
+
try {
|
|
104
|
+
return await import(resolved, options);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const fromNodeModules = resolveFromNodeModules && isResolutionError(err, specifier)
|
|
107
|
+
? resolveFromNodeModules(specifier, cwd)
|
|
108
|
+
: null;
|
|
109
|
+
if (fromNodeModules === null) throw err;
|
|
110
|
+
return await import(fromNodeModules, options);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Bun has neither npm: nor jsr:, and would resolve a bare name from this
|
|
115
|
+
// file's directory. npm:/jsr: install into a per-user cache as on Node
|
|
116
|
+
// (with `bun add`); anything else bare resolves from the notebook's
|
|
117
|
+
// directory, so its node_modules, or Bun's auto-install, supplies it.
|
|
118
|
+
const packages = isBun ? await import("./node-hooks.mjs") : null;
|
|
119
|
+
|
|
120
|
+
function bunResolve(specifier) {
|
|
121
|
+
let pkg = null;
|
|
122
|
+
if (specifier.startsWith("npm:")) pkg = packages.parsePackageSpecifier(specifier.slice(4));
|
|
123
|
+
else if (specifier.startsWith("jsr:")) pkg = packages.jsrToNpm(specifier.slice(4));
|
|
124
|
+
if (pkg) {
|
|
125
|
+
const { dir, alias } = packages.ensureInstalled(pkg, "bun");
|
|
126
|
+
return Bun.resolveSync(alias + pkg.subpath, dir);
|
|
127
|
+
}
|
|
128
|
+
// Relative, absolute, or a URL scheme such as node:, bun: or file:.
|
|
129
|
+
if (/^(?:\.{0,2}\/|[a-z][a-z0-9+.-]*:)/i.test(specifier)) return resolveRelative(specifier);
|
|
130
|
+
return Bun.resolveSync(specifier, cwd);
|
|
131
|
+
}
|
|
132
|
+
|
|
82
133
|
globalThis.__cdpk = {
|
|
83
134
|
// Cell code awaits this first; see PRELUDE in src/transform.ts.
|
|
84
135
|
tick: () => new Promise((resolve) => setTimeout(resolve, 0)),
|
|
85
136
|
// Node: the resolve hook re-parents these imports. Deno would resolve
|
|
86
|
-
// relative paths against this file, so
|
|
87
|
-
import: (specifier, options) =>
|
|
137
|
+
// relative paths against this file, so denoImport does it here.
|
|
138
|
+
import: (specifier, options) =>
|
|
139
|
+
isDeno
|
|
140
|
+
? denoImport(specifier, options)
|
|
141
|
+
: isBun
|
|
142
|
+
? import(bunResolve(specifier), options)
|
|
143
|
+
: import(specifier, options),
|
|
88
144
|
meta: {
|
|
89
145
|
url: replUrl.href,
|
|
90
146
|
dirname: cwd,
|
|
91
|
-
filename: replUrl
|
|
147
|
+
filename: fileURLToPath(replUrl),
|
|
92
148
|
resolve: (specifier) => new URL(specifier, replUrl).href,
|
|
93
149
|
},
|
|
94
150
|
barrier,
|
|
95
151
|
broadcastResult: jupyter.broadcastResult,
|
|
152
|
+
// Frontend comm messages (#9), from the host's shell channel. Handlers
|
|
153
|
+
// run after one macrotask, like cells, so interrupt can reach them.
|
|
154
|
+
async commDispatch(msgType, content, metadata, buffers64) {
|
|
155
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
156
|
+
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
157
|
+
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|
|
158
|
+
},
|
|
96
159
|
describeError(value) {
|
|
97
160
|
if (value instanceof Error) {
|
|
98
161
|
return JSON.stringify({ name: value.name, message: value.message, stack: value.stack ?? "" });
|
|
@@ -146,6 +209,14 @@ if (isDeno) {
|
|
|
146
209
|
// Keep the event loop alive between cells.
|
|
147
210
|
setInterval(() => {}, 1 << 30);
|
|
148
211
|
|
|
212
|
+
if (isBun) {
|
|
213
|
+
const { startChannel } = await import("./bun-channel.mjs");
|
|
214
|
+
const { createHandler } = await import("./evaluator.mjs");
|
|
215
|
+
startChannel(createHandler(globalThis.__cdpk), {
|
|
216
|
+
versions: { runtime: Bun.version, engine: `JavaScriptCore (WebKit ${process.versions.webkit.slice(0, 12)})` },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
149
220
|
async function platform() {
|
|
150
221
|
if (isDeno) {
|
|
151
222
|
const stdinBuf = new Uint8Array(1);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The Bun target's side of the host connection (#7), in place of CDP.
|
|
2
|
+
//
|
|
3
|
+
// Bun's --inspect speaks the WebKit Inspector Protocol, which has no
|
|
4
|
+
// replMode, Runtime.addBinding or Runtime.terminateExecution, so the host
|
|
5
|
+
// doesn't drive Bun over an inspector. It sends requests as JSON lines over
|
|
6
|
+
// fd 3, a pipe only the host holds (there is no port to discover), and the
|
|
7
|
+
// cells are evaluated here, in-process, by evaluator.mjs. Replies, and the
|
|
8
|
+
// iopub messages that the CDP binding carries for other targets, go back the
|
|
9
|
+
// same way. stdout and stderr stay ordinary pipes, ordered against those
|
|
10
|
+
// messages by bootstrap.mjs's barrier markers.
|
|
11
|
+
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
|
|
14
|
+
const CHANNEL_FD = 3;
|
|
15
|
+
|
|
16
|
+
/** Synchronous, like a CDP binding call, so output order is kept. */
|
|
17
|
+
function writeAll(fd, text) {
|
|
18
|
+
const bytes = Buffer.from(text);
|
|
19
|
+
for (let offset = 0; offset < bytes.length;) {
|
|
20
|
+
try {
|
|
21
|
+
offset += fs.writeSync(fd, bytes, offset);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
// The reader's side of the pipe is full; the fd may be non-blocking.
|
|
24
|
+
if (err.code !== "EAGAIN") throw err;
|
|
25
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Serves `handle` (see evaluator.mjs) over fd 3 and installs
|
|
32
|
+
* globalThis.__cdpk_send, the function bootstrap.mjs sends iopub messages
|
|
33
|
+
* through. `ready` is sent first, once the target can take requests.
|
|
34
|
+
*/
|
|
35
|
+
export function startChannel(handle, ready, fd = CHANNEL_FD) {
|
|
36
|
+
const send = (message) => writeAll(fd, JSON.stringify(message) + "\n");
|
|
37
|
+
Object.defineProperty(globalThis, "__cdpk_send", {
|
|
38
|
+
value: (payload) => writeAll(fd, payload + "\n"),
|
|
39
|
+
enumerable: false,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
let buffer = "";
|
|
43
|
+
const input = fs.createReadStream(null, { fd, autoClose: false, encoding: "utf8" });
|
|
44
|
+
input.on("data", (chunk) => {
|
|
45
|
+
buffer += chunk;
|
|
46
|
+
for (let nl = buffer.indexOf("\n"); nl !== -1; nl = buffer.indexOf("\n")) {
|
|
47
|
+
const request = JSON.parse(buffer.slice(0, nl));
|
|
48
|
+
buffer = buffer.slice(nl + 1);
|
|
49
|
+
handle(request).then(
|
|
50
|
+
(reply) => send({ kind: "reply", id: request.id, ...reply }),
|
|
51
|
+
(err) => send({ kind: "reply", id: request.id, failure: String(err?.stack ?? err) }),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// The host is gone; don't linger as an orphan.
|
|
56
|
+
input.on("end", () => process.exit(0));
|
|
57
|
+
send({ kind: "ready", ...ready });
|
|
58
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** A comm message as a handler receives it. */
|
|
2
|
+
export interface CommMessage {
|
|
3
|
+
data: { [key: string]: unknown };
|
|
4
|
+
metadata: { [key: string]: unknown };
|
|
5
|
+
buffers: Uint8Array[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CommSendOptions {
|
|
9
|
+
metadata?: { [key: string]: unknown };
|
|
10
|
+
buffers?: Uint8Array[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** One end of a Jupyter comm channel. */
|
|
14
|
+
export interface Comm {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly targetName: string;
|
|
17
|
+
readonly closed: boolean;
|
|
18
|
+
send(data?: { [key: string]: unknown }, options?: CommSendOptions): void;
|
|
19
|
+
close(data?: { [key: string]: unknown }, options?: CommSendOptions): void;
|
|
20
|
+
onMsg(handler: (msg: CommMessage) => void | Promise<void>): void;
|
|
21
|
+
onClose(handler: (msg: CommMessage) => void | Promise<void>): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `jupyter.comm`: open comms, and accept comms the frontend opens. */
|
|
25
|
+
export interface CommApi {
|
|
26
|
+
open(targetName: string, data?: { [key: string]: unknown }, options?: CommSendOptions): Comm;
|
|
27
|
+
registerTarget(targetName: string, handler: (comm: Comm, msg: CommMessage) => void | Promise<void>): void;
|
|
28
|
+
unregisterTarget(targetName: string): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Comms {
|
|
32
|
+
api: CommApi;
|
|
33
|
+
/** Routes a frontend comm message; resolves to whether a comm or target took it. */
|
|
34
|
+
dispatch(
|
|
35
|
+
msgType: string,
|
|
36
|
+
content: { [key: string]: unknown },
|
|
37
|
+
metadata: { [key: string]: unknown },
|
|
38
|
+
buffers: Uint8Array[],
|
|
39
|
+
): Promise<boolean>;
|
|
40
|
+
list(): { commId: string; targetName: string }[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createComms(
|
|
44
|
+
send: (msgType: string, content: object, metadata: object, buffers: Uint8Array[]) => void,
|
|
45
|
+
newId?: () => string,
|
|
46
|
+
): Comms;
|
package/runtime/comm.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// @ts-self-types="./comm.d.mts"
|
|
2
|
+
// Jupyter comms: bidirectional channels between kernel and frontend, the
|
|
3
|
+
// transport ipywidgets and anywidget are built on (#9). Runs inside the
|
|
4
|
+
// target runtime.
|
|
5
|
+
//
|
|
6
|
+
// Kernel-to-frontend messages (comm_open, comm_msg, comm_close) go out
|
|
7
|
+
// through `send`, the same ordered path as display messages. Frontend
|
|
8
|
+
// messages arrive from the host through `dispatch`, which returns whether
|
|
9
|
+
// anything in the kernel handled them, so the host can answer an unknown
|
|
10
|
+
// target's comm_open with a comm_close, as the protocol asks.
|
|
11
|
+
|
|
12
|
+
class Comm {
|
|
13
|
+
#send;
|
|
14
|
+
#onClosed;
|
|
15
|
+
#msgHandlers = [];
|
|
16
|
+
#closeHandlers = [];
|
|
17
|
+
closed = false;
|
|
18
|
+
|
|
19
|
+
constructor(id, targetName, send, onClosed) {
|
|
20
|
+
this.id = id;
|
|
21
|
+
this.targetName = targetName;
|
|
22
|
+
this.#send = send;
|
|
23
|
+
this.#onClosed = onClosed;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
send(data = {}, { metadata = {}, buffers = [] } = {}) {
|
|
27
|
+
if (this.closed) return;
|
|
28
|
+
this.#send("comm_msg", { comm_id: this.id, data }, metadata, buffers);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
close(data = {}, { metadata = {}, buffers = [] } = {}) {
|
|
32
|
+
if (this.closed) return;
|
|
33
|
+
this.#send("comm_close", { comm_id: this.id, data }, metadata, buffers);
|
|
34
|
+
// close() is synchronous for callers; its close handlers run detached.
|
|
35
|
+
void this._closed({ data, metadata, buffers });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onMsg(handler) {
|
|
39
|
+
this.#msgHandlers.push(handler);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onClose(handler) {
|
|
43
|
+
this.#closeHandlers.push(handler);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async _receive(msg) {
|
|
47
|
+
for (const handler of this.#msgHandlers) await handler(msg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async _closed(msg) {
|
|
51
|
+
if (this.closed) return;
|
|
52
|
+
this.closed = true;
|
|
53
|
+
this.#onClosed(this.id);
|
|
54
|
+
for (const handler of this.#closeHandlers) await handler(msg);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {(msgType: string, content: object, metadata: object, buffers: Uint8Array[]) => void} send
|
|
60
|
+
* @param {() => string} [newId]
|
|
61
|
+
*/
|
|
62
|
+
export function createComms(send, newId = () => crypto.randomUUID()) {
|
|
63
|
+
const comms = new Map(); // comm_id -> Comm
|
|
64
|
+
const targets = new Map(); // target_name -> handler
|
|
65
|
+
const forget = (id) => comms.delete(id);
|
|
66
|
+
|
|
67
|
+
function track(id, targetName) {
|
|
68
|
+
const comm = new Comm(id, targetName, send, forget);
|
|
69
|
+
comms.set(id, comm);
|
|
70
|
+
return comm;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const api = {
|
|
74
|
+
open(targetName, data = {}, { metadata = {}, buffers = [] } = {}) {
|
|
75
|
+
const comm = track(newId(), targetName);
|
|
76
|
+
send("comm_open", { comm_id: comm.id, target_name: targetName, data }, metadata, buffers);
|
|
77
|
+
return comm;
|
|
78
|
+
},
|
|
79
|
+
registerTarget(targetName, handler) {
|
|
80
|
+
targets.set(targetName, handler);
|
|
81
|
+
},
|
|
82
|
+
unregisterTarget(targetName) {
|
|
83
|
+
targets.delete(targetName);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
async function dispatch(msgType, content, metadata, buffers) {
|
|
88
|
+
const msg = { data: content.data ?? {}, metadata: metadata ?? {}, buffers: buffers ?? [] };
|
|
89
|
+
if (msgType === "comm_open") {
|
|
90
|
+
const handler = targets.get(content.target_name);
|
|
91
|
+
if (!handler) return false;
|
|
92
|
+
await handler(track(content.comm_id, content.target_name), msg);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const comm = comms.get(content.comm_id);
|
|
96
|
+
if (!comm) return false;
|
|
97
|
+
if (msgType === "comm_msg") await comm._receive(msg);
|
|
98
|
+
else if (msgType === "comm_close") await comm._closed(msg);
|
|
99
|
+
else return false;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function list() {
|
|
104
|
+
return [...comms.values()].map((comm) => ({ commId: comm.id, targetName: comm.targetName }));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { api, dispatch, list };
|
|
108
|
+
}
|
package/runtime/deno-compat.mjs
CHANGED
package/runtime/display.d.mts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import type { CommApi, Comms } from "./comm.d.mts";
|
|
2
|
+
import type { Widget, WidgetOptions } from "./widget.d.mts";
|
|
3
|
+
|
|
4
|
+
export type { CommApi, Widget, WidgetOptions };
|
|
5
|
+
|
|
1
6
|
export const $display: unique symbol;
|
|
2
7
|
|
|
3
8
|
export type MediaBundle = { [mediaType: string]: unknown };
|
|
@@ -33,6 +38,10 @@ export interface JupyterNamespace {
|
|
|
33
38
|
svg(strings: TemplateStringsArray, ...values: unknown[]): Displayable;
|
|
34
39
|
image(obj: string | Uint8Array): Displayable;
|
|
35
40
|
$display: typeof $display;
|
|
41
|
+
/** Jupyter comms: bidirectional channels to the frontend. */
|
|
42
|
+
comm: CommApi;
|
|
43
|
+
/** An anywidget-compatible widget; display the result to show it. */
|
|
44
|
+
widget(options: WidgetOptions): Widget;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
export interface Jupyter {
|
|
@@ -40,6 +49,8 @@ export interface Jupyter {
|
|
|
40
49
|
broadcastResult(executionCount: number, result: unknown): Promise<void>;
|
|
41
50
|
confirm(message?: string): boolean;
|
|
42
51
|
prompt(message?: string, defaultValue?: string, options?: { password?: boolean }): string | null;
|
|
52
|
+
/** Routes a frontend comm message into the kernel's comms. */
|
|
53
|
+
commDispatch: Comms["dispatch"];
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
export function escapeHTML(str: string): string;
|