@tlockney/cdp-kernel 0.4.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 -481
- 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 +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 +1 -1
- package/dist/debugger.js +21 -10
- package/dist/globals.d.ts +3 -3
- package/dist/globals.js +3 -3
- package/dist/kernel.js +163 -41
- package/dist/mod.d.ts +5 -3
- package/dist/mod.js +5 -3
- package/dist/package.d.ts +2 -2
- package/dist/package.js +3 -3
- package/dist/target.d.ts +31 -3
- package/dist/target.js +233 -32
- package/dist/text.d.ts +5 -1
- package/dist/text.js +55 -2
- package/dist/transform.d.ts +9 -0
- package/dist/transform.js +37 -9
- 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/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 +9 -3
- package/runtime/bootstrap.mjs +121 -10
- 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/node-hooks.d.mts +42 -0
- package/runtime/node-hooks.mjs +246 -41
- package/runtime/workerd-worker.mjs +77 -6
package/dist/wire.js
CHANGED
|
@@ -41,7 +41,7 @@ export function makeHeader(session, msgType) {
|
|
|
41
41
|
}
|
|
42
42
|
export function encodeMessage(session, key, msgType, content, options = {}) {
|
|
43
43
|
const parts = [
|
|
44
|
-
makeHeader(session, msgType),
|
|
44
|
+
options.header ?? makeHeader(session, msgType),
|
|
45
45
|
options.parentHeader ?? {},
|
|
46
46
|
options.metadata ?? {},
|
|
47
47
|
content,
|
|
@@ -70,6 +70,8 @@ function parseObject(frame) {
|
|
|
70
70
|
}
|
|
71
71
|
return value;
|
|
72
72
|
}
|
|
73
|
+
// Only msg_id and msg_type are required of a received header; nothing
|
|
74
|
+
// reads the rest, so a client that leaves them out still works.
|
|
73
75
|
function isHeader(value) {
|
|
74
76
|
return typeof value.msg_id === "string" && typeof value.msg_type === "string";
|
|
75
77
|
}
|
package/dist/workerd-target.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CdpParams, type EvaluateResult, type RemoteObject } from "./cdp.ts";
|
|
2
2
|
import { transformCellForEval } from "./transform.ts";
|
|
3
3
|
import { type KernelTarget, type TargetEvents } from "./target.ts";
|
|
4
4
|
import { type WsChannel } from "./ws-client.ts";
|
|
@@ -53,6 +53,12 @@ export interface WorkerHost {
|
|
|
53
53
|
stop(now: boolean): void;
|
|
54
54
|
onExit(listener: (code: number | null) => void): void;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Removes workerd directories under `root` whose kernel has died without
|
|
58
|
+
* cleaning up (SIGKILL runs no exit hook), killing their workerd if it's
|
|
59
|
+
* still running (#220).
|
|
60
|
+
*/
|
|
61
|
+
export declare function sweepStaleWorkerdDirs(root?: string): void;
|
|
56
62
|
/** The driver on a workerd of its own, behind a Unix socket. */
|
|
57
63
|
export declare class RawWorkerd implements WorkerHost {
|
|
58
64
|
#private;
|
package/dist/workerd-target.js
CHANGED
|
@@ -18,18 +18,20 @@ import { Buffer } from "node:buffer";
|
|
|
18
18
|
import process from "node:process";
|
|
19
19
|
import { execFileSync, spawn } from "node:child_process";
|
|
20
20
|
import { randomBytes } from "node:crypto";
|
|
21
|
-
import { copyFileSync, existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { copyFileSync, existsSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, rmSync, writeFileSync } from "node:fs";
|
|
22
22
|
import { builtinModules } from "node:module";
|
|
23
23
|
import { tmpdir } from "node:os";
|
|
24
24
|
import { dirname, join } from "node:path";
|
|
25
25
|
import { pathToFileURL } from "node:url";
|
|
26
26
|
import { build } from "esbuild";
|
|
27
|
+
import { asEvaluateResult } from "./cdp.js";
|
|
27
28
|
import { transformCellForEval } from "./transform.js";
|
|
28
29
|
import { runtimeDir } from "./package.js";
|
|
30
|
+
import { killOnExit } from "./children.js";
|
|
29
31
|
import { Interrupted, isTargetMessage } from "./target.js";
|
|
30
32
|
import { connectUnixWebSocket } from "./ws-client.js";
|
|
31
33
|
import { findWranglerConfig, startMiniflare } from "./workers-project.js";
|
|
32
|
-
import {
|
|
34
|
+
import { ensureInstalledAsync, findLockFile, jsrToNpm, parsePackageSpecifier } from "../runtime/node-hooks.mjs";
|
|
33
35
|
/** The driver Worker's modules; the first is its main module. */
|
|
34
36
|
export const WORKER_MODULES = ["workerd-worker.mjs", "display.mjs", "comm.mjs", "widget.mjs", "evaluator.mjs", "introspect.mjs"];
|
|
35
37
|
/** See BunTarget: how long an interrupted cell has to show it isn't busy. */
|
|
@@ -121,7 +123,8 @@ export async function bundleModule(specifier, cwd) {
|
|
|
121
123
|
? jsrToNpm(specifier.slice(4))
|
|
122
124
|
: null;
|
|
123
125
|
if (pkg) {
|
|
124
|
-
|
|
126
|
+
// Async: a sync install would stall the heartbeat and interrupts (#149).
|
|
127
|
+
const { dir, alias } = await ensureInstalledAsync(pkg, "npm", { lockFile: findLockFile(cwd) });
|
|
125
128
|
entry = alias + pkg.subpath;
|
|
126
129
|
resolveDir = dir;
|
|
127
130
|
}
|
|
@@ -168,6 +171,65 @@ export function cellMeta(cwd) {
|
|
|
168
171
|
const replUrl = new URL("$cdpk$repl.mjs", pathToFileURL(cwd.endsWith("/") ? cwd : cwd + "/"));
|
|
169
172
|
return { url: replUrl.href, dirname: cwd, filename: join(cwd, "$cdpk$repl.mjs") };
|
|
170
173
|
}
|
|
174
|
+
const WORKERD_DIR_PREFIX = "cdpk-workerd-";
|
|
175
|
+
/** In each workerd directory: `{ host, workerd }` pids. */
|
|
176
|
+
const OWNER_FILE = "owner.json";
|
|
177
|
+
function pidAlive(pid) {
|
|
178
|
+
try {
|
|
179
|
+
process.kill(pid, 0);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
return err.code === "EPERM";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Whether `pid` is still the workerd serving `dir`, not a reused pid. */
|
|
187
|
+
function isWorkerdIn(pid, dir) {
|
|
188
|
+
try {
|
|
189
|
+
if (process.platform === "linux")
|
|
190
|
+
return readlinkSync(`/proc/${pid}/cwd`) === dir;
|
|
191
|
+
const args = execFileSync("ps", ["-o", "args=", "-p", String(pid)], { encoding: "utf8" });
|
|
192
|
+
return /workerd/.test(args) && args.includes("config.capnp");
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Removes workerd directories under `root` whose kernel has died without
|
|
200
|
+
* cleaning up (SIGKILL runs no exit hook), killing their workerd if it's
|
|
201
|
+
* still running (#220).
|
|
202
|
+
*/
|
|
203
|
+
export function sweepStaleWorkerdDirs(root = tmpdir()) {
|
|
204
|
+
let names;
|
|
205
|
+
try {
|
|
206
|
+
names = readdirSync(root).filter((name) => name.startsWith(WORKERD_DIR_PREFIX));
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const name of names) {
|
|
212
|
+
const dir = join(root, name);
|
|
213
|
+
let owner;
|
|
214
|
+
try {
|
|
215
|
+
owner = JSON.parse(readFileSync(join(dir, OWNER_FILE), "utf8"));
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
continue; // Another user's, from before owner files, or being set up.
|
|
219
|
+
}
|
|
220
|
+
if (typeof owner.host !== "number" || pidAlive(owner.host))
|
|
221
|
+
continue;
|
|
222
|
+
if (typeof owner.workerd === "number" && isWorkerdIn(owner.workerd, dir)) {
|
|
223
|
+
try {
|
|
224
|
+
process.kill(owner.workerd, "SIGKILL");
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Gone already.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
rmSync(dir, { recursive: true, force: true });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
171
233
|
/** The driver on a workerd of its own, behind a Unix socket. */
|
|
172
234
|
export class RawWorkerd {
|
|
173
235
|
runtime;
|
|
@@ -184,7 +246,9 @@ export class RawWorkerd {
|
|
|
184
246
|
const source = await runtimeDir(new URL("../runtime/", import.meta.url).href);
|
|
185
247
|
// mkdtemp makes the directory 0700: only this user reaches the socket,
|
|
186
248
|
// and the token stops anything else running as them by accident.
|
|
187
|
-
|
|
249
|
+
sweepStaleWorkerdDirs();
|
|
250
|
+
const dir = mkdtempSync(join(tmpdir(), WORKERD_DIR_PREFIX));
|
|
251
|
+
writeFileSync(join(dir, OWNER_FILE), JSON.stringify({ host: process.pid }));
|
|
188
252
|
const socket = join(dir, "sock");
|
|
189
253
|
for (const name of WORKER_MODULES)
|
|
190
254
|
copyFileSync(join(source, name), join(dir, name));
|
|
@@ -194,11 +258,21 @@ export class RawWorkerd {
|
|
|
194
258
|
// --experimental: the UnsafeEval binding.
|
|
195
259
|
const child = spawn(workerd, ["serve", "config.capnp", "--experimental"], { cwd: dir, stdio: ["ignore", "pipe", "pipe"], env });
|
|
196
260
|
child.once("exit", () => rmSync(dir, { recursive: true, force: true }));
|
|
261
|
+
if (child.pid !== undefined)
|
|
262
|
+
writeFileSync(join(dir, OWNER_FILE), JSON.stringify({ host: process.pid, workerd: child.pid }));
|
|
263
|
+
killOnExit(child, () => {
|
|
264
|
+
child.kill("SIGKILL");
|
|
265
|
+
rmSync(dir, { recursive: true, force: true });
|
|
266
|
+
});
|
|
197
267
|
// Cell output is captured inside the Worker; what workerd itself prints
|
|
198
268
|
// (its own warnings, errors from background work) goes to the server log.
|
|
269
|
+
// `early` keeps that output for a startup error only, and stops growing
|
|
270
|
+
// once the session is open.
|
|
199
271
|
let early = "";
|
|
272
|
+
let starting = true;
|
|
200
273
|
const log = (chunk) => {
|
|
201
|
-
|
|
274
|
+
if (starting)
|
|
275
|
+
early += chunk.toString();
|
|
202
276
|
process.stderr.write(`[cdp-kernel workerd] ${chunk}`);
|
|
203
277
|
};
|
|
204
278
|
child.stdout?.on("data", log);
|
|
@@ -221,7 +295,10 @@ export class RawWorkerd {
|
|
|
221
295
|
}
|
|
222
296
|
})();
|
|
223
297
|
try {
|
|
224
|
-
|
|
298
|
+
const host = new RawWorkerd(child, await Promise.race([ready, exited]), compatibilityDate);
|
|
299
|
+
starting = false;
|
|
300
|
+
early = "";
|
|
301
|
+
return host;
|
|
225
302
|
}
|
|
226
303
|
catch (err) {
|
|
227
304
|
child.kill();
|
|
@@ -229,7 +306,16 @@ export class RawWorkerd {
|
|
|
229
306
|
}
|
|
230
307
|
}
|
|
231
308
|
stop(now) {
|
|
232
|
-
|
|
309
|
+
if (now) {
|
|
310
|
+
this.#child.kill("SIGKILL");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.#child.kill("SIGTERM");
|
|
314
|
+
// A busy isolate keeps workerd's event loop from ever seeing SIGTERM (#128).
|
|
315
|
+
setTimeout(() => {
|
|
316
|
+
if (this.#child.exitCode === null && this.#child.signalCode === null)
|
|
317
|
+
this.#child.kill("SIGKILL");
|
|
318
|
+
}, 3000).unref();
|
|
233
319
|
}
|
|
234
320
|
onExit(listener) {
|
|
235
321
|
this.#child.once("exit", listener);
|
|
@@ -342,6 +428,9 @@ export class WorkerdTarget {
|
|
|
342
428
|
this.#pending.clear();
|
|
343
429
|
}
|
|
344
430
|
#request(op, params = {}, interruptible = false) {
|
|
431
|
+
// The channel drops writes after it closes, so nothing would answer.
|
|
432
|
+
if (this.#exitedOnce)
|
|
433
|
+
return Promise.reject(new Abandoned("the workerd session is closed"));
|
|
345
434
|
const id = ++this.#nextId;
|
|
346
435
|
return new Promise((resolve, reject) => {
|
|
347
436
|
this.#pending.set(id, { resolve, reject });
|
|
@@ -377,7 +466,12 @@ export class WorkerdTarget {
|
|
|
377
466
|
if (!modules[specifier].startsWith("Promise.reject("))
|
|
378
467
|
this.#sent.add(specifier);
|
|
379
468
|
}
|
|
380
|
-
|
|
469
|
+
const { failedModules, ...reply } = await this.#interruptibly("eval", { code: script, modules }, "Execution");
|
|
470
|
+
// A module that threw while loading is rebuilt for its next import (#147).
|
|
471
|
+
if (Array.isArray(failedModules))
|
|
472
|
+
for (const specifier of failedModules)
|
|
473
|
+
this.#sent.delete(specifier);
|
|
474
|
+
return asEvaluateResult(reply);
|
|
381
475
|
}
|
|
382
476
|
/**
|
|
383
477
|
* A cell that's waiting on something leaves the driver free: it answers a
|
package/dist/ws-client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
export declare const OP_CONTINUATION = 0;
|
|
3
3
|
export declare const OP_TEXT = 1;
|
|
4
|
+
export declare const OP_BINARY = 2;
|
|
4
5
|
export declare const OP_CLOSE = 8;
|
|
5
6
|
export declare const OP_PING = 9;
|
|
6
7
|
export declare const OP_PONG = 10;
|
|
@@ -11,7 +12,10 @@ export interface Frame {
|
|
|
11
12
|
}
|
|
12
13
|
/** One frame; clients must mask what they send. */
|
|
13
14
|
export declare function encodeFrame(opcode: number, payload: Buffer, mask?: Buffer | null): Buffer;
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Splits a byte stream into frames, holding back a partial one. Chunks of a
|
|
17
|
+
* large frame are only joined once the whole frame has arrived.
|
|
18
|
+
*/
|
|
15
19
|
export declare class FrameDecoder {
|
|
16
20
|
#private;
|
|
17
21
|
push(chunk: Buffer): Frame[];
|
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",
|
|
@@ -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
|
}
|