@tlockney/cdp-kernel 0.3.0 → 0.5.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 +60 -404
- package/THIRD-PARTY-NOTICES +62 -0
- package/dist/bun-target.d.ts +4 -2
- package/dist/bun-target.js +35 -8
- package/dist/cdp.d.ts +6 -1
- package/dist/cdp.js +25 -10
- package/dist/celld-target.d.ts +1 -1
- package/dist/celld-target.js +46 -8
- package/dist/children.d.ts +7 -0
- package/dist/children.js +32 -0
- package/dist/debugger.d.ts +42 -0
- package/dist/debugger.js +545 -0
- package/dist/globals.d.ts +3 -3
- package/dist/globals.js +3 -3
- package/dist/history.d.ts +18 -0
- package/dist/history.js +45 -0
- package/dist/kernel.js +211 -32
- package/dist/main.js +3 -1
- package/dist/mod.d.ts +5 -3
- package/dist/mod.js +5 -3
- package/dist/murmur2.d.ts +2 -0
- package/dist/murmur2.js +41 -0
- package/dist/package.d.ts +16 -2
- package/dist/package.js +26 -3
- package/dist/sourcemap.d.ts +22 -0
- package/dist/sourcemap.js +86 -0
- package/dist/target.d.ts +39 -3
- package/dist/target.js +281 -30
- package/dist/text.d.ts +11 -1
- package/dist/text.js +72 -2
- package/dist/transform.d.ts +19 -0
- package/dist/transform.js +74 -13
- package/dist/wire.d.ts +8 -1
- package/dist/wire.js +3 -1
- package/dist/workerd-target.d.ts +9 -1
- package/dist/workerd-target.js +108 -9
- package/dist/ws-client.d.ts +5 -1
- package/dist/ws-client.js +46 -13
- package/dist/zmtp.d.ts +7 -1
- package/dist/zmtp.js +30 -1
- package/package.json +10 -4
- package/runtime/bootstrap.mjs +138 -12
- package/runtime/bun-channel.d.mts +3 -0
- package/runtime/bun-channel.mjs +2 -2
- package/runtime/comm.mjs +11 -1
- package/runtime/deno-compat.d.mts +2 -0
- package/runtime/deno-compat.mjs +17 -2
- package/runtime/display.d.mts +2 -0
- package/runtime/display.mjs +17 -2
- package/runtime/evaluator.d.mts +4 -0
- package/runtime/evaluator.mjs +8 -0
- package/runtime/introspect.d.mts +13 -0
- package/runtime/introspect.mjs +120 -0
- package/runtime/node-hooks.d.mts +61 -1
- package/runtime/node-hooks.mjs +354 -21
- package/runtime/workerd-worker.mjs +82 -6
package/dist/ws-client.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
// target's session with its driver Worker (#26). The WebSocket that Node
|
|
3
3
|
// and Deno provide can't connect to a Unix socket, and a TCP listener would
|
|
4
4
|
// be reachable by every local process. Text messages only: that's all the
|
|
5
|
-
// session carries.
|
|
5
|
+
// session carries, so binary messages are dropped.
|
|
6
6
|
import { Buffer } from "node:buffer";
|
|
7
7
|
import { createHash, randomBytes } from "node:crypto";
|
|
8
8
|
import { connect } from "node:net";
|
|
9
9
|
const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
10
10
|
export const OP_CONTINUATION = 0x0;
|
|
11
11
|
export const OP_TEXT = 0x1;
|
|
12
|
+
export const OP_BINARY = 0x2;
|
|
12
13
|
export const OP_CLOSE = 0x8;
|
|
13
14
|
export const OP_PING = 0x9;
|
|
14
15
|
export const OP_PONG = 0xa;
|
|
@@ -40,14 +41,24 @@ export function encodeFrame(opcode, payload, mask = randomBytes(4)) {
|
|
|
40
41
|
}
|
|
41
42
|
return out;
|
|
42
43
|
}
|
|
43
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Splits a byte stream into frames, holding back a partial one. Chunks of a
|
|
46
|
+
* large frame are only joined once the whole frame has arrived.
|
|
47
|
+
*/
|
|
44
48
|
export class FrameDecoder {
|
|
45
|
-
#
|
|
49
|
+
#chunks = [];
|
|
50
|
+
#length = 0;
|
|
51
|
+
/** Bytes the frame at the front needs, once its header is known. */
|
|
52
|
+
#needed = 0;
|
|
46
53
|
push(chunk) {
|
|
47
|
-
this.#
|
|
54
|
+
this.#chunks.push(chunk);
|
|
55
|
+
this.#length += chunk.length;
|
|
56
|
+
if (this.#length < this.#needed)
|
|
57
|
+
return [];
|
|
58
|
+
let b = this.#chunks.length === 1 ? this.#chunks[0] : Buffer.concat(this.#chunks, this.#length);
|
|
59
|
+
this.#needed = 0;
|
|
48
60
|
const frames = [];
|
|
49
61
|
for (;;) {
|
|
50
|
-
const b = this.#buffer;
|
|
51
62
|
if (b.length < 2)
|
|
52
63
|
break;
|
|
53
64
|
const masked = (b[1] & 0x80) !== 0;
|
|
@@ -68,15 +79,19 @@ export class FrameDecoder {
|
|
|
68
79
|
const maskAt = offset;
|
|
69
80
|
if (masked)
|
|
70
81
|
offset += 4;
|
|
71
|
-
if (b.length < offset + len)
|
|
82
|
+
if (b.length < offset + len) {
|
|
83
|
+
this.#needed = offset + len;
|
|
72
84
|
break;
|
|
85
|
+
}
|
|
73
86
|
const payload = Buffer.from(b.subarray(offset, offset + len));
|
|
74
87
|
if (masked)
|
|
75
88
|
for (let i = 0; i < len; i++)
|
|
76
89
|
payload[i] ^= b[maskAt + (i & 3)];
|
|
77
90
|
frames.push({ fin: (b[0] & 0x80) !== 0, opcode: b[0] & 0x0f, payload });
|
|
78
|
-
|
|
91
|
+
b = b.subarray(offset + len);
|
|
79
92
|
}
|
|
93
|
+
this.#chunks = b.length ? [b] : [];
|
|
94
|
+
this.#length = b.length;
|
|
80
95
|
return frames;
|
|
81
96
|
}
|
|
82
97
|
}
|
|
@@ -95,6 +110,9 @@ export function connectUnixWebSocket(socketPath, { path = "/", headers = {} } =
|
|
|
95
110
|
let head = Buffer.alloc(0);
|
|
96
111
|
const decoder = new FrameDecoder();
|
|
97
112
|
let parts = [];
|
|
113
|
+
// The opcode of the message whose fragments are arriving.
|
|
114
|
+
let messageOpcode = null;
|
|
115
|
+
let closeSent = false;
|
|
98
116
|
const finish = () => {
|
|
99
117
|
if (closed)
|
|
100
118
|
return;
|
|
@@ -108,30 +126,45 @@ export function connectUnixWebSocket(socketPath, { path = "/", headers = {} } =
|
|
|
108
126
|
if (!closed)
|
|
109
127
|
socket.write(encodeFrame(opcode, payload));
|
|
110
128
|
};
|
|
129
|
+
const sendClose = (payload) => {
|
|
130
|
+
if (closeSent)
|
|
131
|
+
return;
|
|
132
|
+
closeSent = true;
|
|
133
|
+
write(OP_CLOSE, payload);
|
|
134
|
+
};
|
|
111
135
|
const channel = {
|
|
112
136
|
send: (text) => write(OP_TEXT, Buffer.from(text)),
|
|
113
137
|
onMessage: (listener) => void messageListeners.push(listener),
|
|
114
138
|
onClose: (listener) => void closeListeners.push(listener),
|
|
115
139
|
close() {
|
|
116
|
-
|
|
140
|
+
sendClose(Buffer.alloc(0));
|
|
117
141
|
socket.end();
|
|
118
142
|
},
|
|
119
143
|
};
|
|
120
144
|
const onFrames = (frames) => {
|
|
121
145
|
for (const frame of frames) {
|
|
122
|
-
if (frame.opcode === OP_TEXT || frame.opcode === OP_CONTINUATION) {
|
|
123
|
-
|
|
146
|
+
if (frame.opcode === OP_TEXT || frame.opcode === OP_BINARY || frame.opcode === OP_CONTINUATION) {
|
|
147
|
+
if (frame.opcode !== OP_CONTINUATION) {
|
|
148
|
+
messageOpcode = frame.opcode;
|
|
149
|
+
parts = [];
|
|
150
|
+
}
|
|
151
|
+
if (messageOpcode === OP_TEXT)
|
|
152
|
+
parts.push(frame.payload);
|
|
124
153
|
if (frame.fin) {
|
|
125
|
-
const text = Buffer.concat(parts).toString("utf8");
|
|
154
|
+
const text = messageOpcode === OP_TEXT ? Buffer.concat(parts).toString("utf8") : null;
|
|
126
155
|
parts = [];
|
|
127
|
-
|
|
128
|
-
|
|
156
|
+
messageOpcode = null;
|
|
157
|
+
if (text !== null)
|
|
158
|
+
for (const listener of messageListeners)
|
|
159
|
+
listener(text);
|
|
129
160
|
}
|
|
130
161
|
}
|
|
131
162
|
else if (frame.opcode === OP_PING) {
|
|
132
163
|
write(OP_PONG, frame.payload);
|
|
133
164
|
}
|
|
134
165
|
else if (frame.opcode === OP_CLOSE) {
|
|
166
|
+
// Echo the status code, as RFC 6455 section 5.5.1 asks.
|
|
167
|
+
sendClose(frame.payload.subarray(0, 2));
|
|
135
168
|
socket.end();
|
|
136
169
|
}
|
|
137
170
|
}
|
package/dist/zmtp.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export declare function recvMultipart(reader: SocketReader): Promise<Uint8Array[
|
|
|
20
20
|
/** Async FIFO with blocking pop. */
|
|
21
21
|
export declare class Queue<T> {
|
|
22
22
|
#private;
|
|
23
|
+
/** Items waiting to be popped. */
|
|
24
|
+
get size(): number;
|
|
23
25
|
push(item: T): void;
|
|
24
26
|
pop(): Promise<T>;
|
|
25
27
|
}
|
|
@@ -29,7 +31,11 @@ export interface RoutedMessage {
|
|
|
29
31
|
peer: string;
|
|
30
32
|
frames: Uint8Array[];
|
|
31
33
|
}
|
|
32
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* ROUTER socket: each TCP connection is one peer with a generated identity.
|
|
36
|
+
* Writes ignore socket.write's backpressure signal: node:net buffers the
|
|
37
|
+
* rest, which is fine for loopback peers that read promptly.
|
|
38
|
+
*/
|
|
33
39
|
export declare class RouterSocket {
|
|
34
40
|
#private;
|
|
35
41
|
readonly incoming: Queue<RoutedMessage>;
|
package/dist/zmtp.js
CHANGED
|
@@ -36,6 +36,10 @@ export class SocketReader {
|
|
|
36
36
|
this.#waiter = null;
|
|
37
37
|
waiter?.();
|
|
38
38
|
}
|
|
39
|
+
// Each call joins every buffered chunk, so a reader that keeps a large
|
|
40
|
+
// backlog while small chunks trickle in recopies it per call (quadratic).
|
|
41
|
+
// Jupyter traffic over loopback arrives in few, large chunks; a chunk
|
|
42
|
+
// list with an offset would avoid the copies if that ever changes.
|
|
39
43
|
async readExact(n) {
|
|
40
44
|
while (this.#buffered < n) {
|
|
41
45
|
if (this.#error)
|
|
@@ -136,11 +140,28 @@ export async function recvMultipart(reader) {
|
|
|
136
140
|
return frames;
|
|
137
141
|
}
|
|
138
142
|
}
|
|
143
|
+
/** How long a port from the connection file may stay taken before the kernel gives up. */
|
|
144
|
+
const LISTEN_RETRY_MS = 5000;
|
|
145
|
+
const LISTEN_RETRY_INTERVAL_MS = 100;
|
|
139
146
|
function listen(port, host, onConn) {
|
|
140
147
|
const server = createServer((socket) => {
|
|
141
148
|
socket.setNoDelay(true);
|
|
142
149
|
onConn(socket);
|
|
143
150
|
});
|
|
151
|
+
// The frontend picks these ports by binding and releasing them, so another
|
|
152
|
+
// socket can take one before the kernel binds it. Wait for it to be freed
|
|
153
|
+
// rather than die before kernel_info.
|
|
154
|
+
const deadline = Date.now() + LISTEN_RETRY_MS;
|
|
155
|
+
const onError = (err) => {
|
|
156
|
+
if (err.code !== "EADDRINUSE" || Date.now() >= deadline) {
|
|
157
|
+
server.off("error", onError);
|
|
158
|
+
server.emit("error", err);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
setTimeout(() => server.listen(port, host), LISTEN_RETRY_INTERVAL_MS);
|
|
162
|
+
};
|
|
163
|
+
server.on("error", onError);
|
|
164
|
+
server.once("listening", () => server.off("error", onError));
|
|
144
165
|
server.listen(port, host);
|
|
145
166
|
return server;
|
|
146
167
|
}
|
|
@@ -148,6 +169,10 @@ function listen(port, host, onConn) {
|
|
|
148
169
|
export class Queue {
|
|
149
170
|
#items = [];
|
|
150
171
|
#waiters = [];
|
|
172
|
+
/** Items waiting to be popped. */
|
|
173
|
+
get size() {
|
|
174
|
+
return this.#items.length;
|
|
175
|
+
}
|
|
151
176
|
push(item) {
|
|
152
177
|
const waiter = this.#waiters.shift();
|
|
153
178
|
if (waiter)
|
|
@@ -173,7 +198,11 @@ export function startHeartbeat(port, host) {
|
|
|
173
198
|
})().catch(() => socket.destroy());
|
|
174
199
|
});
|
|
175
200
|
}
|
|
176
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* ROUTER socket: each TCP connection is one peer with a generated identity.
|
|
203
|
+
* Writes ignore socket.write's backpressure signal: node:net buffers the
|
|
204
|
+
* rest, which is fine for loopback peers that read promptly.
|
|
205
|
+
*/
|
|
177
206
|
export class RouterSocket {
|
|
178
207
|
incoming = new Queue();
|
|
179
208
|
peers = new Map();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tlockney/cdp-kernel",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Deno's Jupyter kernel, extracted to run notebooks on
|
|
3
|
+
"version": "0.5.0",
|
|
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>",
|
|
7
7
|
"repository": {
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"files": [
|
|
43
43
|
"dist/",
|
|
44
44
|
"runtime/*.mjs",
|
|
45
|
-
"runtime/*.d.mts"
|
|
45
|
+
"runtime/*.d.mts",
|
|
46
|
+
"THIRD-PARTY-NOTICES"
|
|
46
47
|
],
|
|
47
48
|
"publishConfig": {
|
|
48
49
|
"access": "public"
|
|
@@ -50,6 +51,10 @@
|
|
|
50
51
|
"scripts": {
|
|
51
52
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
52
53
|
"check": "tsc -p tsconfig.json",
|
|
54
|
+
"docs:dev": "vitepress dev docs",
|
|
55
|
+
"docs:build": "vitepress build docs",
|
|
56
|
+
"docs:notebooks": "node docs/.vitepress/run-notebooks.ts",
|
|
57
|
+
"docs:widgets": "uv run --with jupyterlab --with anywidget --with playwright --with nbformat python test/widget_screenshots.py",
|
|
53
58
|
"install:node": "node src/main.ts install --runtime node",
|
|
54
59
|
"install:deno": "node src/main.ts install --runtime deno",
|
|
55
60
|
"install:node-deno": "node src/main.ts install --runtime node --deno-compat",
|
|
@@ -57,7 +62,7 @@
|
|
|
57
62
|
"install:workerd": "node src/main.ts install --runtime workerd",
|
|
58
63
|
"install:celld": "node src/main.ts install --runtime celld",
|
|
59
64
|
"prepack": "npm run build",
|
|
60
|
-
"test": "node --test test/*.test.ts",
|
|
65
|
+
"test": "node --test --test-timeout=300000 test/*.test.ts",
|
|
61
66
|
"e2e": "uv run --with nbclient --with nbformat --with jupyter_client python test/e2e.py",
|
|
62
67
|
"test:lab": "uv run --with jupyterlab --with anywidget --with playwright --with nbformat python test/lab_widgets.py"
|
|
63
68
|
},
|
|
@@ -69,6 +74,7 @@
|
|
|
69
74
|
"devDependencies": {
|
|
70
75
|
"@types/node": "^24.19.2",
|
|
71
76
|
"typescript": "^5.9.3",
|
|
77
|
+
"vitepress": "^1.6.4",
|
|
72
78
|
"workerd": "1.20261015.1",
|
|
73
79
|
"wrangler": "4.151.0"
|
|
74
80
|
}
|
package/runtime/bootstrap.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
14
|
import { createJupyter } from "./display.mjs";
|
|
15
|
+
import { describeValue, inspectPath, resolvePath } from "./introspect.mjs";
|
|
15
16
|
|
|
16
17
|
const isDeno = typeof globalThis.Deno?.version?.deno === "string";
|
|
17
18
|
const isBun = typeof globalThis.Bun?.version === "string";
|
|
@@ -26,7 +27,7 @@ const cwdUrl = pathToFileURL(cwd.endsWith("/") ? cwd : cwd + "/");
|
|
|
26
27
|
// Stand-in referrer for cell code, like Deno's ./$deno$jupyter_repl.mts.
|
|
27
28
|
const replUrl = new URL("$cdpk$repl.mjs", cwdUrl);
|
|
28
29
|
|
|
29
|
-
const { inspect, readFile, writeOut, writeErr, readStdinLine } = await platform();
|
|
30
|
+
const { inspect, inspectPlain, readFile, writeOut, writeErr, readStdinLine } = await platform();
|
|
30
31
|
|
|
31
32
|
function base64(bytes) {
|
|
32
33
|
let binary = "";
|
|
@@ -54,7 +55,12 @@ function barrier(gated = false) {
|
|
|
54
55
|
function send(payload) {
|
|
55
56
|
const binding = globalThis.__cdpk_send;
|
|
56
57
|
if (typeof binding !== "function") return; // host not attached yet
|
|
57
|
-
|
|
58
|
+
// Serialize before writing the marker: if the payload can't be
|
|
59
|
+
// serialized (a BigInt, a cycle), the error reaches the cell without a
|
|
60
|
+
// gated marker the host would wait on forever (#129). `payload` always
|
|
61
|
+
// has a `kind`, so `body` is never "{}".
|
|
62
|
+
const body = JSON.stringify(payload);
|
|
63
|
+
binding(`{"barrier":${barrier(true)},${body.slice(1)}`);
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
const jupyter = createJupyter({
|
|
@@ -71,6 +77,13 @@ const jupyter = createJupyter({
|
|
|
71
77
|
const line = readStdinLine();
|
|
72
78
|
if (line === null) return null;
|
|
73
79
|
const reply = JSON.parse(line);
|
|
80
|
+
if (reply.interrupted === true) {
|
|
81
|
+
// Interrupted while waiting for the answer: unwind the cell, as
|
|
82
|
+
// KeyboardInterrupt does in Python's input() (#121).
|
|
83
|
+
const err = new Error("Execution interrupted");
|
|
84
|
+
err.name = "Interrupted";
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
74
87
|
return typeof reply.value === "string" ? reply.value : null;
|
|
75
88
|
},
|
|
76
89
|
});
|
|
@@ -122,7 +135,7 @@ function bunResolve(specifier) {
|
|
|
122
135
|
if (specifier.startsWith("npm:")) pkg = packages.parsePackageSpecifier(specifier.slice(4));
|
|
123
136
|
else if (specifier.startsWith("jsr:")) pkg = packages.jsrToNpm(specifier.slice(4));
|
|
124
137
|
if (pkg) {
|
|
125
|
-
const { dir, alias } = packages.ensureInstalled(pkg, "bun");
|
|
138
|
+
const { dir, alias } = packages.ensureInstalled(pkg, "bun", { lockFile: packages.findLockFile(cwd) });
|
|
126
139
|
return Bun.resolveSync(alias + pkg.subpath, dir);
|
|
127
140
|
}
|
|
128
141
|
// Relative, absolute, or a URL scheme such as node:, bun: or file:.
|
|
@@ -130,6 +143,24 @@ function bunResolve(specifier) {
|
|
|
130
143
|
return Bun.resolveSync(specifier, cwd);
|
|
131
144
|
}
|
|
132
145
|
|
|
146
|
+
// The inspector holds a promise it awaits (callFunctionOn with awaitPromise)
|
|
147
|
+
// only weakly until its then-callback runs, so a GC after the promise
|
|
148
|
+
// settles but before that callback fails the call with "Promise was
|
|
149
|
+
// collected" (#55). hold() keeps the promise until a macrotask after it
|
|
150
|
+
// settles, by which time the inspector's callback has run.
|
|
151
|
+
const held = new Set();
|
|
152
|
+
function hold(value) {
|
|
153
|
+
const promise = Promise.resolve(value);
|
|
154
|
+
held.add(promise);
|
|
155
|
+
const release = () => setTimeout(() => held.delete(promise), 0);
|
|
156
|
+
promise.then(release, release);
|
|
157
|
+
return promise;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Adds the cause to an error from a Deno member the --deno-compat shim
|
|
161
|
+
// lacks (#172); the identity everywhere else.
|
|
162
|
+
let explainError = (text) => text;
|
|
163
|
+
|
|
133
164
|
globalThis.__cdpk = {
|
|
134
165
|
// Cell code awaits this first; see PRELUDE in src/transform.ts.
|
|
135
166
|
tick: () => new Promise((resolve) => setTimeout(resolve, 0)),
|
|
@@ -148,7 +179,20 @@ globalThis.__cdpk = {
|
|
|
148
179
|
resolve: (specifier) => new URL(specifier, replUrl).href,
|
|
149
180
|
},
|
|
150
181
|
barrier,
|
|
182
|
+
hold,
|
|
183
|
+
// inspect_request (#10). CDP targets find the value with
|
|
184
|
+
// throwOnSideEffect and describe it here; Bun resolves the path here
|
|
185
|
+
// too, by descriptor, so nothing runs.
|
|
186
|
+
describeForInspect: (value, detail) => describeValue(value, { inspect: inspectPlain, detail }),
|
|
187
|
+
inspectPath: (path, detail) => inspectPath(path, { inspect: inspectPlain, detail }),
|
|
188
|
+
resolvePath,
|
|
151
189
|
broadcastResult: jupyter.broadcastResult,
|
|
190
|
+
// The debugger's rich variable viewer (#18), synchronously: it runs while
|
|
191
|
+
// the target is paused. A value whose $display is async shows as text.
|
|
192
|
+
richBundle(value) {
|
|
193
|
+
const bundle = jupyter.formatNow(value);
|
|
194
|
+
return JSON.stringify(typeof bundle?.then === "function" ? { "text/plain": inspectPlain(value) } : bundle);
|
|
195
|
+
},
|
|
152
196
|
// Frontend comm messages (#9), from the host's shell channel. Handlers
|
|
153
197
|
// run after one macrotask, like cells, so interrupt can reach them.
|
|
154
198
|
async commDispatch(msgType, content, metadata, buffers64) {
|
|
@@ -158,7 +202,9 @@ globalThis.__cdpk = {
|
|
|
158
202
|
},
|
|
159
203
|
describeError(value) {
|
|
160
204
|
if (value instanceof Error) {
|
|
161
|
-
|
|
205
|
+
const message = explainError(value.message);
|
|
206
|
+
const stack = stackOf(value).replace(value.message, () => message);
|
|
207
|
+
return JSON.stringify({ name: value.name, message, stack });
|
|
162
208
|
}
|
|
163
209
|
return JSON.stringify({ name: "Error", message: inspect(value), stack: "" });
|
|
164
210
|
},
|
|
@@ -171,26 +217,48 @@ globalThis.confirm = jupyter.confirm;
|
|
|
171
217
|
if (isDeno) {
|
|
172
218
|
// Existing Deno notebooks use Deno.jupyter, but outside `deno jupyter`
|
|
173
219
|
// that is a non-configurable getter that throws. The global binding
|
|
174
|
-
// itself is configurable, so swap in
|
|
175
|
-
//
|
|
220
|
+
// itself is configurable, so swap in an object with its own `jupyter`
|
|
221
|
+
// that inherits everything else from the real namespace. Not a Proxy:
|
|
222
|
+
// completion can't list a Proxy's properties, and Shift-Tab's
|
|
223
|
+
// side-effect-free evaluation can't read through one (#132). So
|
|
224
|
+
// Object.keys(Deno) and console.log(Deno) show only `jupyter`, and
|
|
225
|
+
// Deno's functions are called with `this` set to this object (they
|
|
226
|
+
// don't use it).
|
|
176
227
|
const real = globalThis.Deno;
|
|
177
228
|
const descriptor = Object.getOwnPropertyDescriptor(globalThis, "Deno");
|
|
178
|
-
const
|
|
179
|
-
|
|
229
|
+
const shim = Object.create(real, {
|
|
230
|
+
jupyter: { value: jupyter.namespace, enumerable: true, configurable: true, writable: true },
|
|
180
231
|
});
|
|
181
|
-
Object.defineProperty(globalThis, "Deno", { ...descriptor, value:
|
|
232
|
+
Object.defineProperty(globalThis, "Deno", { ...descriptor, value: shim });
|
|
182
233
|
} else if (process.env.CDPK_DENO_COMPAT === "1" && globalThis.Deno === undefined) {
|
|
183
234
|
// Opt-in (install --deno-compat): a Deno namespace for notebooks written
|
|
184
235
|
// against `deno jupyter`. See runtime/deno-compat.mjs for the trade-off.
|
|
185
236
|
// Absolute URL: the resolve hook re-parents this module's relative
|
|
186
237
|
// imports to the notebook directory.
|
|
187
|
-
const { createDenoShim } = await import(new URL("./deno-compat.mjs", import.meta.url).href);
|
|
188
|
-
|
|
238
|
+
const { createDenoShim, explainMissing } = await import(new URL("./deno-compat.mjs", import.meta.url).href);
|
|
239
|
+
const shim = createDenoShim({ jupyter: jupyter.namespace });
|
|
240
|
+
globalThis.Deno = shim;
|
|
241
|
+
explainError = (text) => explainMissing(text, shim);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* An error's stack, with its first line naming the error. Bun now and then
|
|
246
|
+
* (about one throw in twenty, #216) records a stack whose first line is a
|
|
247
|
+
* bare "Error", though name and message are set; the frames are right, so
|
|
248
|
+
* only the header is rebuilt.
|
|
249
|
+
*/
|
|
250
|
+
function stackOf(err) {
|
|
251
|
+
const stack = err.stack;
|
|
252
|
+
if (typeof stack !== "string") return "";
|
|
253
|
+
const nl = stack.indexOf("\n");
|
|
254
|
+
const first = nl === -1 ? stack : stack.slice(0, nl);
|
|
255
|
+
if (first !== "Error" || !err.message) return stack;
|
|
256
|
+
return `${err.name}: ${err.message}${nl === -1 ? "" : stack.slice(nl)}`;
|
|
189
257
|
}
|
|
190
258
|
|
|
191
259
|
// An error thrown later by a cell's timer or promise must not kill the kernel.
|
|
192
260
|
function reportUncaught(kind, err) {
|
|
193
|
-
writeErr(`${kind}: ${err instanceof Error ? (err
|
|
261
|
+
writeErr(`${kind}: ${explainError(err instanceof Error ? (stackOf(err) || err.message) : inspect(err))}\n`);
|
|
194
262
|
}
|
|
195
263
|
if (isDeno) {
|
|
196
264
|
globalThis.addEventListener("unhandledrejection", (e) => {
|
|
@@ -209,6 +277,29 @@ if (isDeno) {
|
|
|
209
277
|
// Keep the event loop alive between cells.
|
|
210
278
|
setInterval(() => {}, 1 << 30);
|
|
211
279
|
|
|
280
|
+
// A host killed by SIGKILL can't stop its runtime, and nothing visible to
|
|
281
|
+
// JS here changes when its inspector sessions drop, so watch for the host
|
|
282
|
+
// itself going: reparented, or no longer there (#219). Bun exits on its
|
|
283
|
+
// channel's EOF instead.
|
|
284
|
+
if (!isBun) {
|
|
285
|
+
// Held here, so a cell that reassigns them can't break the watch.
|
|
286
|
+
const proc = process;
|
|
287
|
+
const exit = process.exit.bind(process);
|
|
288
|
+
const host = proc.ppid;
|
|
289
|
+
const hostGone = () => {
|
|
290
|
+
if (proc.ppid !== host) return true;
|
|
291
|
+
try {
|
|
292
|
+
proc.kill(host, 0);
|
|
293
|
+
return false;
|
|
294
|
+
} catch (err) {
|
|
295
|
+
return err?.code === "ESRCH";
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
setInterval(() => {
|
|
299
|
+
if (hostGone()) exit(1);
|
|
300
|
+
}, 2000);
|
|
301
|
+
}
|
|
302
|
+
|
|
212
303
|
if (isBun) {
|
|
213
304
|
const { startChannel } = await import("./bun-channel.mjs");
|
|
214
305
|
const { createHandler } = await import("./evaluator.mjs");
|
|
@@ -222,6 +313,7 @@ async function platform() {
|
|
|
222
313
|
const stdinBuf = new Uint8Array(1);
|
|
223
314
|
return {
|
|
224
315
|
inspect: (v) => Deno.inspect(v, { colors: true, depth: 4 }),
|
|
316
|
+
inspectPlain: (v) => Deno.inspect(v, { colors: false, depth: 2 }),
|
|
225
317
|
readFile: (p) => Deno.readFileSync(p),
|
|
226
318
|
writeOut: (s) => Deno.stdout.writeSync(ENC.encode(s)),
|
|
227
319
|
writeErr: (s) => Deno.stderr.writeSync(ENC.encode(s)),
|
|
@@ -231,8 +323,17 @@ async function platform() {
|
|
|
231
323
|
const util = await import("node:util");
|
|
232
324
|
const fs = await import("node:fs");
|
|
233
325
|
const buf = Buffer.alloc(1);
|
|
326
|
+
// Node writes to a pipe without blocking and queues what doesn't fit for
|
|
327
|
+
// the event loop to flush. input() blocks the main thread in readSync, so
|
|
328
|
+
// queued output, and the input_request's barrier marker after it, would
|
|
329
|
+
// never reach the host (#131). Blocking writes finish before readSync
|
|
330
|
+
// starts, as Node already does on Windows. Bun has no _handle and needs
|
|
331
|
+
// its own fix (#180).
|
|
332
|
+
if (isBun) await syncBunOutput();
|
|
333
|
+
else for (const stream of [process.stdout, process.stderr]) stream._handle?.setBlocking?.(true);
|
|
234
334
|
return {
|
|
235
335
|
inspect: (v) => util.inspect(v, { colors: true, depth: 4 }),
|
|
336
|
+
inspectPlain: (v) => util.inspect(v, { colors: false, depth: 2 }),
|
|
236
337
|
readFile: (p) => fs.readFileSync(p),
|
|
237
338
|
writeOut: (s) => process.stdout.write(s),
|
|
238
339
|
writeErr: (s) => process.stderr.write(s),
|
|
@@ -250,6 +351,31 @@ async function platform() {
|
|
|
250
351
|
};
|
|
251
352
|
}
|
|
252
353
|
|
|
354
|
+
// Bun makes stdout and stderr non-blocking once process.stdout or
|
|
355
|
+
// process.stderr exists, and then queues what doesn't fit in the pipe for
|
|
356
|
+
// its event loop, console.log's output included. input() blocks the main
|
|
357
|
+
// thread in readSync, so queued output, and the input_request's barrier
|
|
358
|
+
// marker after it, would never reach the host (#180). Here every write
|
|
359
|
+
// finishes before it returns: process.stdout.write and process.stderr.write
|
|
360
|
+
// write through, and console is Node's Console over them, so console output
|
|
361
|
+
// is formatted as on Node.
|
|
362
|
+
async function syncBunOutput() {
|
|
363
|
+
const { writeAll } = await import("./bun-channel.mjs");
|
|
364
|
+
const { Console } = await import("node:console");
|
|
365
|
+
for (const [stream, fd] of [[process.stdout, 1], [process.stderr, 2]]) {
|
|
366
|
+
stream.write = (chunk, encoding, callback) => {
|
|
367
|
+
if (typeof encoding === "function") [callback, encoding] = [encoding, undefined];
|
|
368
|
+
writeAll(fd, typeof chunk === "string" ? Buffer.from(chunk, encoding) : chunk);
|
|
369
|
+
if (callback) process.nextTick(callback);
|
|
370
|
+
return true;
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const sync = new Console({ stdout: process.stdout, stderr: process.stderr, colorMode: true });
|
|
374
|
+
for (const key of Object.keys(console)) {
|
|
375
|
+
if (typeof sync[key] === "function") console[key] = sync[key].bind(sync);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
253
379
|
function readLine(nextByte) {
|
|
254
380
|
const bytes = [];
|
|
255
381
|
for (;;) {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import type { Handler } from "./evaluator.mjs";
|
|
2
2
|
|
|
3
3
|
export function startChannel(handle: Handler, ready: { [key: string]: unknown }, fd?: number): void;
|
|
4
|
+
|
|
5
|
+
/** Writes all of `text` to `fd` before returning, even if the fd is non-blocking. */
|
|
6
|
+
export function writeAll(fd: number, text: string | Uint8Array): void;
|
package/runtime/bun-channel.mjs
CHANGED
|
@@ -14,8 +14,8 @@ import fs from "node:fs";
|
|
|
14
14
|
const CHANNEL_FD = 3;
|
|
15
15
|
|
|
16
16
|
/** Synchronous, like a CDP binding call, so output order is kept. */
|
|
17
|
-
function writeAll(fd, text) {
|
|
18
|
-
const bytes = Buffer.from(text);
|
|
17
|
+
export function writeAll(fd, text) {
|
|
18
|
+
const bytes = typeof text === "string" ? Buffer.from(text) : Buffer.from(text.buffer, text.byteOffset, text.byteLength);
|
|
19
19
|
for (let offset = 0; offset < bytes.length;) {
|
|
20
20
|
try {
|
|
21
21
|
offset += fs.writeSync(fd, bytes, offset);
|
package/runtime/comm.mjs
CHANGED
|
@@ -89,7 +89,17 @@ export function createComms(send, newId = () => crypto.randomUUID()) {
|
|
|
89
89
|
if (msgType === "comm_open") {
|
|
90
90
|
const handler = targets.get(content.target_name);
|
|
91
91
|
if (!handler) return false;
|
|
92
|
-
|
|
92
|
+
const comm = track(content.comm_id, content.target_name);
|
|
93
|
+
try {
|
|
94
|
+
await handler(comm, msg);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// The host answers a failed comm_open with comm_close, so the comm
|
|
97
|
+
// is gone on the frontend: drop it here too, without running close
|
|
98
|
+
// handlers or sending anything (#137).
|
|
99
|
+
comm.closed = true;
|
|
100
|
+
forget(comm.id);
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
93
103
|
return true;
|
|
94
104
|
}
|
|
95
105
|
const comm = comms.get(content.comm_id);
|
|
@@ -114,3 +114,5 @@ export interface DenoShim {
|
|
|
114
114
|
|
|
115
115
|
export function denoError<E>(err: E): E | NotFound | AlreadyExists | PermissionDenied | IsADirectory | NotADirectory;
|
|
116
116
|
export function createDenoShim(options: { jupyter: unknown }): DenoShim;
|
|
117
|
+
/** Adds the cause to "Deno.x is not a function" when `shim` lacks `x`. */
|
|
118
|
+
export function explainMissing(message: string, shim: object): string;
|
package/runtime/deno-compat.mjs
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// shim installed they take their Deno code paths, which may call APIs not
|
|
8
8
|
// covered here (Deno.serve, Deno.Command, Deno.openKv, permissions...).
|
|
9
9
|
// Uncovered APIs are simply absent rather than throwing on access, so
|
|
10
|
-
// feature checks like `typeof Deno.serve === "function"` still work.
|
|
10
|
+
// feature checks like `typeof Deno.serve === "function"` still work. The
|
|
11
|
+
// error from calling one says so instead (explainMissing, #172).
|
|
11
12
|
|
|
12
13
|
import fs from "node:fs";
|
|
13
14
|
import fsp from "node:fs/promises";
|
|
@@ -100,6 +101,18 @@ const writeFlag = (options) => (options?.append ? "a" : options?.createNew ? "wx
|
|
|
100
101
|
const OS_NAMES = { darwin: "darwin", linux: "linux", win32: "windows", freebsd: "freebsd" };
|
|
101
102
|
const ARCH_NAMES = { arm64: "aarch64", x64: "x86_64" };
|
|
102
103
|
|
|
104
|
+
const MEMBERS_DOC = "https://cdp-kernel.org/docs/jupyter-api#notebooks-written-for-deno";
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Adds the cause to V8's "Deno.serve is not a function" when `shim` lacks
|
|
108
|
+
* that member (#172), or returns `message` unchanged.
|
|
109
|
+
*/
|
|
110
|
+
export function explainMissing(message, shim) {
|
|
111
|
+
return message.replace(/\b(Deno\.([\w$]+)) is not a (?:function|constructor)\b/, (all, expr, name) =>
|
|
112
|
+
name in shim ? all : `${all}: ${expr} is not implemented by cdp-kernel's --deno-compat namespace (see ${MEMBERS_DOC})`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
export function createDenoShim({ jupyter }) {
|
|
104
117
|
return {
|
|
105
118
|
jupyter,
|
|
@@ -161,7 +174,9 @@ export function createDenoShim({ jupyter }) {
|
|
|
161
174
|
rename: async((from, to) => fsp.rename(toPath(from), toPath(to))),
|
|
162
175
|
renameSync: sync((from, to) => fs.renameSync(toPath(from), toPath(to))),
|
|
163
176
|
realPath: async((p) => fsp.realpath(toPath(p))),
|
|
164
|
-
|
|
177
|
+
// The native call, like fsp.realpath above: the JavaScript one leaves
|
|
178
|
+
// Windows 8.3 names (RUNNER~1) unexpanded, so the two disagreed (#32).
|
|
179
|
+
realPathSync: sync((p) => fs.realpathSync.native(toPath(p))),
|
|
165
180
|
makeTempDir: async((options) =>
|
|
166
181
|
fsp.mkdtemp(path.join(options?.dir ?? os.tmpdir(), options?.prefix ?? "deno-"))
|
|
167
182
|
),
|
package/runtime/display.d.mts
CHANGED
|
@@ -47,6 +47,8 @@ export interface JupyterNamespace {
|
|
|
47
47
|
export interface Jupyter {
|
|
48
48
|
namespace: JupyterNamespace;
|
|
49
49
|
broadcastResult(executionCount: number, result: unknown): Promise<void>;
|
|
50
|
+
/** format without awaiting; a $display method's promise is returned as is. */
|
|
51
|
+
formatNow(obj: unknown): unknown;
|
|
50
52
|
confirm(message?: string): boolean;
|
|
51
53
|
prompt(message?: string, defaultValue?: string, options?: { password?: boolean }): string | null;
|
|
52
54
|
/** Routes a frontend comm message into the kernel's comms. */
|
package/runtime/display.mjs
CHANGED
|
@@ -146,7 +146,14 @@ function isMediaBundle(obj) {
|
|
|
146
146
|
*/
|
|
147
147
|
export function createJupyter(host) {
|
|
148
148
|
async function format(obj) {
|
|
149
|
-
|
|
149
|
+
return await formatNow(obj);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// format without awaiting: the debugger's variable viewer formats values
|
|
153
|
+
// while the target is paused, when no promise settles (#18). A $display
|
|
154
|
+
// method's promise is the caller's to handle.
|
|
155
|
+
function formatNow(obj) {
|
|
156
|
+
if (hasDisplaySymbol(obj)) return obj[$display]();
|
|
150
157
|
if (typeof obj !== "object" || obj === null) return { "text/plain": host.inspect(obj) };
|
|
151
158
|
if (isCanvasLike(obj)) {
|
|
152
159
|
const [prefix, data] = obj.toDataURL().split(",");
|
|
@@ -173,7 +180,14 @@ export function createJupyter(host) {
|
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
function image(obj) {
|
|
176
|
-
if (typeof obj === "string")
|
|
183
|
+
if (typeof obj === "string") {
|
|
184
|
+
// As Deno does: a path that can't be read gets the TypeError below.
|
|
185
|
+
try {
|
|
186
|
+
obj = host.readFile(obj);
|
|
187
|
+
} catch {
|
|
188
|
+
// fall through
|
|
189
|
+
}
|
|
190
|
+
}
|
|
177
191
|
if (isJpg(obj)) return { [$display]: () => ({ "image/jpeg": host.base64(obj) }) };
|
|
178
192
|
if (isPng(obj)) return { [$display]: () => ({ "image/png": host.base64(obj) }) };
|
|
179
193
|
throw new TypeError(
|
|
@@ -240,6 +254,7 @@ export function createJupyter(host) {
|
|
|
240
254
|
widget: createWidgetFactory(comms.api, $display),
|
|
241
255
|
},
|
|
242
256
|
broadcastResult,
|
|
257
|
+
formatNow,
|
|
243
258
|
confirm,
|
|
244
259
|
prompt,
|
|
245
260
|
commDispatch: comms.dispatch,
|
package/runtime/evaluator.d.mts
CHANGED
|
@@ -4,6 +4,10 @@ export interface EvaluatorShim {
|
|
|
4
4
|
describeError(value: unknown): string;
|
|
5
5
|
commDispatch(msgType: string, content: object, metadata: object, buffers64: string[]): Promise<unknown>;
|
|
6
6
|
barrier(): number;
|
|
7
|
+
/** inspect_request's text for a dotted path, or null (#10). */
|
|
8
|
+
inspectPath(path: string, detail: number): string | null;
|
|
9
|
+
/** Resolves a dotted path without running getters; see introspect.mjs. */
|
|
10
|
+
resolvePath?(path: string): import("./introspect.mjs").Resolved;
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
/** A request from the host: an `op` plus its parameters. */
|