@tlockney/cdp-kernel 0.4.0 → 0.6.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 +78 -478
- package/THIRD-PARTY-NOTICES +62 -0
- package/dist/bun-target.d.ts +2 -2
- package/dist/bun-target.js +30 -8
- package/dist/cdp.d.ts +8 -1
- package/dist/cdp.js +31 -11
- package/dist/celld-target.d.ts +1 -1
- package/dist/celld-target.js +54 -19
- package/dist/children.d.ts +29 -0
- package/dist/children.js +114 -0
- package/dist/debugger.d.ts +1 -1
- package/dist/debugger.js +21 -10
- package/dist/deno-types.d.ts +30 -0
- package/dist/deno-types.js +93 -0
- package/dist/globals.d.ts +3 -3
- package/dist/globals.js +3 -3
- package/dist/kernel.d.ts +7 -1
- package/dist/kernel.js +348 -68
- package/dist/language-service-worker.d.ts +1 -0
- package/dist/language-service-worker.js +88 -0
- package/dist/language-service.d.ts +64 -0
- package/dist/language-service.js +128 -0
- package/dist/main.js +102 -8
- package/dist/mod.d.ts +6 -3
- package/dist/mod.js +6 -3
- package/dist/notebook-types.d.ts +62 -0
- package/dist/notebook-types.js +310 -0
- package/dist/package.d.ts +2 -2
- package/dist/package.js +3 -3
- package/dist/parent.d.ts +12 -0
- package/dist/parent.js +39 -0
- package/dist/port-holder.d.ts +13 -0
- package/dist/port-holder.js +105 -0
- package/dist/sourcemap.d.ts +7 -2
- package/dist/sourcemap.js +9 -3
- package/dist/strip-esbuild.d.ts +2 -0
- package/dist/strip-esbuild.js +44 -0
- package/dist/strip-sucrase.d.ts +2 -0
- package/dist/strip-sucrase.js +78 -0
- package/dist/target.d.ts +58 -6
- package/dist/target.js +412 -54
- package/dist/text.d.ts +5 -1
- package/dist/text.js +55 -2
- package/dist/transform.d.ts +30 -1
- package/dist/transform.js +68 -44
- package/dist/wire.d.ts +8 -1
- package/dist/wire.js +3 -1
- package/dist/workerd-target.d.ts +7 -1
- package/dist/workerd-target.js +102 -8
- package/dist/workers-project.js +13 -2
- package/dist/ws-client.d.ts +5 -1
- package/dist/ws-client.js +46 -13
- package/dist/zmtp.d.ts +9 -1
- package/dist/zmtp.js +42 -1
- package/package.json +17 -6
- package/runtime/bootstrap.mjs +233 -42
- 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 +14 -1
- package/runtime/display.mjs +8 -1
- package/runtime/introspect.mjs +1 -1
- package/runtime/node-hooks.d.mts +48 -0
- package/runtime/node-hooks.mjs +257 -41
- package/runtime/workerd-worker.mjs +77 -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,16 +20,24 @@ 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
|
}
|
|
28
|
+
/** Resolves once `server` is listening, after any retries. */
|
|
29
|
+
export declare function listening(server: Server): Promise<void>;
|
|
26
30
|
/** REP socket for the heartbeat channel: echo everything. */
|
|
27
31
|
export declare function startHeartbeat(port: number, host: string): Server;
|
|
28
32
|
export interface RoutedMessage {
|
|
29
33
|
peer: string;
|
|
30
34
|
frames: Uint8Array[];
|
|
31
35
|
}
|
|
32
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* ROUTER socket: each TCP connection is one peer with a generated identity.
|
|
38
|
+
* Writes ignore socket.write's backpressure signal: node:net buffers the
|
|
39
|
+
* rest, which is fine for loopback peers that read promptly.
|
|
40
|
+
*/
|
|
33
41
|
export declare class RouterSocket {
|
|
34
42
|
#private;
|
|
35
43
|
readonly incoming: Queue<RoutedMessage>;
|
package/dist/zmtp.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { Buffer } from "node:buffer";
|
|
9
9
|
import { createServer } from "node:net";
|
|
10
10
|
import { randomBytes } from "node:crypto";
|
|
11
|
+
import process from "node:process";
|
|
12
|
+
import { portHolders } from "./port-holder.js";
|
|
11
13
|
const ENC = new TextEncoder();
|
|
12
14
|
/** Pull-style reader over a socket's 'data' events. */
|
|
13
15
|
export class SocketReader {
|
|
@@ -36,6 +38,10 @@ export class SocketReader {
|
|
|
36
38
|
this.#waiter = null;
|
|
37
39
|
waiter?.();
|
|
38
40
|
}
|
|
41
|
+
// Each call joins every buffered chunk, so a reader that keeps a large
|
|
42
|
+
// backlog while small chunks trickle in recopies it per call (quadratic).
|
|
43
|
+
// Jupyter traffic over loopback arrives in few, large chunks; a chunk
|
|
44
|
+
// list with an offset would avoid the copies if that ever changes.
|
|
39
45
|
async readExact(n) {
|
|
40
46
|
while (this.#buffered < n) {
|
|
41
47
|
if (this.#error)
|
|
@@ -136,11 +142,34 @@ export async function recvMultipart(reader) {
|
|
|
136
142
|
return frames;
|
|
137
143
|
}
|
|
138
144
|
}
|
|
145
|
+
/** How long a port from the connection file may stay taken before the kernel gives up. */
|
|
146
|
+
const LISTEN_RETRY_MS = 5000;
|
|
147
|
+
const LISTEN_RETRY_INTERVAL_MS = 100;
|
|
139
148
|
function listen(port, host, onConn) {
|
|
140
149
|
const server = createServer((socket) => {
|
|
141
150
|
socket.setNoDelay(true);
|
|
142
151
|
onConn(socket);
|
|
143
152
|
});
|
|
153
|
+
// The frontend picks these ports by binding and releasing them, so another
|
|
154
|
+
// socket can take one before the kernel binds it. Wait for it to be freed
|
|
155
|
+
// rather than die before kernel_info.
|
|
156
|
+
const deadline = Date.now() + LISTEN_RETRY_MS;
|
|
157
|
+
const onError = (err) => {
|
|
158
|
+
if (err.code !== "EADDRINUSE" || Date.now() >= deadline) {
|
|
159
|
+
// Who kept it, for a port that never came free (#257).
|
|
160
|
+
if (err.code === "EADDRINUSE") {
|
|
161
|
+
const holders = portHolders(port).map((line) => ` ${line}\n`).join("") || " unknown\n";
|
|
162
|
+
process.stderr.write(`[cdp-kernel] port ${port} still in use after ${LISTEN_RETRY_MS} ms; this kernel is pid ${process.pid} ` +
|
|
163
|
+
`(ppid ${process.ppid}, ${process.execPath} ${process.version}); the port's sockets:\n${holders}`);
|
|
164
|
+
}
|
|
165
|
+
server.off("error", onError);
|
|
166
|
+
server.emit("error", err);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
setTimeout(() => server.listen(port, host), LISTEN_RETRY_INTERVAL_MS);
|
|
170
|
+
};
|
|
171
|
+
server.on("error", onError);
|
|
172
|
+
server.once("listening", () => server.off("error", onError));
|
|
144
173
|
server.listen(port, host);
|
|
145
174
|
return server;
|
|
146
175
|
}
|
|
@@ -148,6 +177,10 @@ function listen(port, host, onConn) {
|
|
|
148
177
|
export class Queue {
|
|
149
178
|
#items = [];
|
|
150
179
|
#waiters = [];
|
|
180
|
+
/** Items waiting to be popped. */
|
|
181
|
+
get size() {
|
|
182
|
+
return this.#items.length;
|
|
183
|
+
}
|
|
151
184
|
push(item) {
|
|
152
185
|
const waiter = this.#waiters.shift();
|
|
153
186
|
if (waiter)
|
|
@@ -162,6 +195,10 @@ export class Queue {
|
|
|
162
195
|
return new Promise((resolve) => this.#waiters.push(resolve));
|
|
163
196
|
}
|
|
164
197
|
}
|
|
198
|
+
/** Resolves once `server` is listening, after any retries. */
|
|
199
|
+
export function listening(server) {
|
|
200
|
+
return server.listening ? Promise.resolve() : new Promise((resolve) => server.once("listening", () => resolve()));
|
|
201
|
+
}
|
|
165
202
|
/** REP socket for the heartbeat channel: echo everything. */
|
|
166
203
|
export function startHeartbeat(port, host) {
|
|
167
204
|
return listen(port, host, (socket) => {
|
|
@@ -173,7 +210,11 @@ export function startHeartbeat(port, host) {
|
|
|
173
210
|
})().catch(() => socket.destroy());
|
|
174
211
|
});
|
|
175
212
|
}
|
|
176
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* ROUTER socket: each TCP connection is one peer with a generated identity.
|
|
215
|
+
* Writes ignore socket.write's backpressure signal: node:net buffers the
|
|
216
|
+
* rest, which is fine for loopback peers that read promptly.
|
|
217
|
+
*/
|
|
177
218
|
export class RouterSocket {
|
|
178
219
|
incoming = new Queue();
|
|
179
220
|
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.6.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": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"type": "module",
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
23
|
+
"node": ">=22.18"
|
|
24
24
|
},
|
|
25
25
|
"bin": {
|
|
26
26
|
"cdp-kernel": "dist/main.js"
|
|
@@ -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",
|
|
@@ -59,17 +64,23 @@
|
|
|
59
64
|
"prepack": "npm run build",
|
|
60
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",
|
|
67
|
+
"bench": "uv run --with jupyter_client python test/bench.py",
|
|
68
|
+
"soak": "uv run --with jupyter_client python test/soak.py",
|
|
62
69
|
"test:lab": "uv run --with jupyterlab --with anywidget --with playwright --with nbformat python test/lab_widgets.py"
|
|
63
70
|
},
|
|
64
71
|
"dependencies": {
|
|
65
72
|
"acorn": "^8.15.0",
|
|
66
73
|
"acorn-walk": "^8.3.4",
|
|
67
|
-
"esbuild": "^0.25.10"
|
|
74
|
+
"esbuild": "^0.25.10",
|
|
75
|
+
"typescript": "^6.0.3"
|
|
68
76
|
},
|
|
69
77
|
"devDependencies": {
|
|
70
78
|
"@types/node": "^24.19.2",
|
|
71
|
-
"
|
|
79
|
+
"vitepress": "^1.6.4",
|
|
72
80
|
"workerd": "1.20261015.1",
|
|
73
81
|
"wrangler": "4.151.0"
|
|
82
|
+
},
|
|
83
|
+
"optionalDependencies": {
|
|
84
|
+
"sucrase": "^3.35.1"
|
|
74
85
|
}
|
|
75
86
|
}
|