@tlockney/cdp-kernel 0.2.0 → 0.4.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.
Files changed (43) hide show
  1. package/README.md +259 -33
  2. package/dist/bun-target.d.ts +60 -0
  3. package/dist/bun-target.js +236 -0
  4. package/dist/celld-target.d.ts +39 -0
  5. package/dist/celld-target.js +274 -0
  6. package/dist/debugger.d.ts +42 -0
  7. package/dist/debugger.js +534 -0
  8. package/dist/history.d.ts +18 -0
  9. package/dist/history.js +45 -0
  10. package/dist/kernel.js +91 -15
  11. package/dist/main.js +8 -6
  12. package/dist/murmur2.d.ts +2 -0
  13. package/dist/murmur2.js +41 -0
  14. package/dist/package.d.ts +16 -2
  15. package/dist/package.js +28 -2
  16. package/dist/sourcemap.d.ts +22 -0
  17. package/dist/sourcemap.js +86 -0
  18. package/dist/target.d.ts +65 -7
  19. package/dist/target.js +57 -6
  20. package/dist/text.d.ts +6 -0
  21. package/dist/text.js +17 -0
  22. package/dist/transform.d.ts +16 -0
  23. package/dist/transform.js +173 -11
  24. package/dist/workerd-target.d.ts +125 -0
  25. package/dist/workerd-target.js +455 -0
  26. package/dist/workers-project.d.ts +27 -0
  27. package/dist/workers-project.js +195 -0
  28. package/dist/ws-client.d.ts +32 -0
  29. package/dist/ws-client.js +166 -0
  30. package/package.json +18 -4
  31. package/runtime/bootstrap.mjs +55 -5
  32. package/runtime/bun-channel.d.mts +3 -0
  33. package/runtime/bun-channel.mjs +58 -0
  34. package/runtime/deno-compat.mjs +3 -1
  35. package/runtime/display.d.mts +2 -0
  36. package/runtime/display.mjs +9 -1
  37. package/runtime/evaluator.d.mts +22 -0
  38. package/runtime/evaluator.mjs +91 -0
  39. package/runtime/introspect.d.mts +13 -0
  40. package/runtime/introspect.mjs +120 -0
  41. package/runtime/node-hooks.d.mts +29 -0
  42. package/runtime/node-hooks.mjs +160 -20
  43. package/runtime/workerd-worker.mjs +194 -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.2.0",
3
+ "version": "0.4.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": ["jupyter", "kernel", "typescript", "notebook", "deno", "node", "cdp", "devtools-protocol"],
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"
@@ -44,8 +53,11 @@
44
53
  "install:node": "node src/main.ts install --runtime node",
45
54
  "install:deno": "node src/main.ts install --runtime deno",
46
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",
47
59
  "prepack": "npm run build",
48
- "test": "node --test test/*.test.ts",
60
+ "test": "node --test --test-timeout=300000 test/*.test.ts",
49
61
  "e2e": "uv run --with nbclient --with nbformat --with jupyter_client python test/e2e.py",
50
62
  "test:lab": "uv run --with jupyterlab --with anywidget --with playwright --with nbformat python test/lab_widgets.py"
51
63
  },
@@ -56,6 +68,8 @@
56
68
  },
57
69
  "devDependencies": {
58
70
  "@types/node": "^24.19.2",
59
- "typescript": "^5.9.3"
71
+ "typescript": "^5.9.3",
72
+ "workerd": "1.20261015.1",
73
+ "wrangler": "4.151.0"
60
74
  }
61
75
  }
@@ -4,14 +4,18 @@
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 in Deno.
10
+ // Plain JavaScript on purpose: the same file must load unmodified in Node,
11
+ // Deno and Bun.
10
12
 
11
13
  import { fileURLToPath, pathToFileURL } from "node:url";
12
14
  import { createJupyter } from "./display.mjs";
15
+ import { describeValue, inspectPath, resolvePath } from "./introspect.mjs";
13
16
 
14
17
  const isDeno = typeof globalThis.Deno?.version?.deno === "string";
18
+ const isBun = typeof globalThis.Bun?.version === "string";
15
19
  const ENC = new TextEncoder();
16
20
 
17
21
  const cwd = isDeno ? Deno.cwd() : process.cwd();
@@ -23,7 +27,7 @@ const cwdUrl = pathToFileURL(cwd.endsWith("/") ? cwd : cwd + "/");
23
27
  // Stand-in referrer for cell code, like Deno's ./$deno$jupyter_repl.mts.
24
28
  const replUrl = new URL("$cdpk$repl.mjs", cwdUrl);
25
29
 
26
- const { inspect, readFile, writeOut, writeErr, readStdinLine } = await platform();
30
+ const { inspect, inspectPlain, readFile, writeOut, writeErr, readStdinLine } = await platform();
27
31
 
28
32
  function base64(bytes) {
29
33
  let binary = "";
@@ -77,7 +81,7 @@ function resolveRelative(specifier) {
77
81
  return /^\.{0,2}\//.test(specifier) ? new URL(specifier, replUrl).href : specifier;
78
82
  }
79
83
 
80
- if (!isDeno) {
84
+ if (!isDeno && !isBun) {
81
85
  // Deno resolves npm:/jsr: natively; Node gets a resolve hook that adds
82
86
  // them and re-parents cell imports to the notebook's directory.
83
87
  const { installNodeResolveHooks } = await import("./node-hooks.mjs");
@@ -108,12 +112,36 @@ async function denoImport(specifier, options) {
108
112
  }
109
113
  }
110
114
 
115
+ // Bun has neither npm: nor jsr:, and would resolve a bare name from this
116
+ // file's directory. npm:/jsr: install into a per-user cache as on Node
117
+ // (with `bun add`); anything else bare resolves from the notebook's
118
+ // directory, so its node_modules, or Bun's auto-install, supplies it.
119
+ const packages = isBun ? await import("./node-hooks.mjs") : null;
120
+
121
+ function bunResolve(specifier) {
122
+ let pkg = null;
123
+ if (specifier.startsWith("npm:")) pkg = packages.parsePackageSpecifier(specifier.slice(4));
124
+ else if (specifier.startsWith("jsr:")) pkg = packages.jsrToNpm(specifier.slice(4));
125
+ if (pkg) {
126
+ const { dir, alias } = packages.ensureInstalled(pkg, "bun", { lockFile: packages.findLockFile(cwd) });
127
+ return Bun.resolveSync(alias + pkg.subpath, dir);
128
+ }
129
+ // Relative, absolute, or a URL scheme such as node:, bun: or file:.
130
+ if (/^(?:\.{0,2}\/|[a-z][a-z0-9+.-]*:)/i.test(specifier)) return resolveRelative(specifier);
131
+ return Bun.resolveSync(specifier, cwd);
132
+ }
133
+
111
134
  globalThis.__cdpk = {
112
135
  // Cell code awaits this first; see PRELUDE in src/transform.ts.
113
136
  tick: () => new Promise((resolve) => setTimeout(resolve, 0)),
114
137
  // Node: the resolve hook re-parents these imports. Deno would resolve
115
138
  // relative paths against this file, so denoImport does it here.
116
- import: (specifier, options) => (isDeno ? denoImport(specifier, options) : import(specifier, options)),
139
+ import: (specifier, options) =>
140
+ isDeno
141
+ ? denoImport(specifier, options)
142
+ : isBun
143
+ ? import(bunResolve(specifier), options)
144
+ : import(specifier, options),
117
145
  meta: {
118
146
  url: replUrl.href,
119
147
  dirname: cwd,
@@ -121,7 +149,19 @@ globalThis.__cdpk = {
121
149
  resolve: (specifier) => new URL(specifier, replUrl).href,
122
150
  },
123
151
  barrier,
152
+ // inspect_request (#10). CDP targets find the value with
153
+ // throwOnSideEffect and describe it here; Bun resolves the path here
154
+ // too, by descriptor, so nothing runs.
155
+ describeForInspect: (value, detail) => describeValue(value, { inspect: inspectPlain, detail }),
156
+ inspectPath: (path, detail) => inspectPath(path, { inspect: inspectPlain, detail }),
157
+ resolvePath,
124
158
  broadcastResult: jupyter.broadcastResult,
159
+ // The debugger's rich variable viewer (#18), synchronously: it runs while
160
+ // the target is paused. A value whose $display is async shows as text.
161
+ richBundle(value) {
162
+ const bundle = jupyter.formatNow(value);
163
+ return JSON.stringify(typeof bundle?.then === "function" ? { "text/plain": inspectPlain(value) } : bundle);
164
+ },
125
165
  // Frontend comm messages (#9), from the host's shell channel. Handlers
126
166
  // run after one macrotask, like cells, so interrupt can reach them.
127
167
  async commDispatch(msgType, content, metadata, buffers64) {
@@ -182,11 +222,20 @@ if (isDeno) {
182
222
  // Keep the event loop alive between cells.
183
223
  setInterval(() => {}, 1 << 30);
184
224
 
225
+ if (isBun) {
226
+ const { startChannel } = await import("./bun-channel.mjs");
227
+ const { createHandler } = await import("./evaluator.mjs");
228
+ startChannel(createHandler(globalThis.__cdpk), {
229
+ versions: { runtime: Bun.version, engine: `JavaScriptCore (WebKit ${process.versions.webkit.slice(0, 12)})` },
230
+ });
231
+ }
232
+
185
233
  async function platform() {
186
234
  if (isDeno) {
187
235
  const stdinBuf = new Uint8Array(1);
188
236
  return {
189
237
  inspect: (v) => Deno.inspect(v, { colors: true, depth: 4 }),
238
+ inspectPlain: (v) => Deno.inspect(v, { colors: false, depth: 2 }),
190
239
  readFile: (p) => Deno.readFileSync(p),
191
240
  writeOut: (s) => Deno.stdout.writeSync(ENC.encode(s)),
192
241
  writeErr: (s) => Deno.stderr.writeSync(ENC.encode(s)),
@@ -198,6 +247,7 @@ async function platform() {
198
247
  const buf = Buffer.alloc(1);
199
248
  return {
200
249
  inspect: (v) => util.inspect(v, { colors: true, depth: 4 }),
250
+ inspectPlain: (v) => util.inspect(v, { colors: false, depth: 2 }),
201
251
  readFile: (p) => fs.readFileSync(p),
202
252
  writeOut: (s) => process.stdout.write(s),
203
253
  writeErr: (s) => process.stderr.write(s),
@@ -0,0 +1,3 @@
1
+ import type { Handler } from "./evaluator.mjs";
2
+
3
+ export function startChannel(handle: Handler, ready: { [key: string]: unknown }, fd?: number): void;
@@ -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
+ }
@@ -161,7 +161,9 @@ export function createDenoShim({ jupyter }) {
161
161
  rename: async((from, to) => fsp.rename(toPath(from), toPath(to))),
162
162
  renameSync: sync((from, to) => fs.renameSync(toPath(from), toPath(to))),
163
163
  realPath: async((p) => fsp.realpath(toPath(p))),
164
- realPathSync: sync((p) => fs.realpathSync(toPath(p))),
164
+ // The native call, like fsp.realpath above: the JavaScript one leaves
165
+ // Windows 8.3 names (RUNNER~1) unexpanded, so the two disagreed (#32).
166
+ realPathSync: sync((p) => fs.realpathSync.native(toPath(p))),
165
167
  makeTempDir: async((options) =>
166
168
  fsp.mkdtemp(path.join(options?.dir ?? os.tmpdir(), options?.prefix ?? "deno-"))
167
169
  ),
@@ -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. */
@@ -146,7 +146,14 @@ function isMediaBundle(obj) {
146
146
  */
147
147
  export function createJupyter(host) {
148
148
  async function format(obj) {
149
- if (hasDisplaySymbol(obj)) return await obj[$display]();
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(",");
@@ -240,6 +247,7 @@ export function createJupyter(host) {
240
247
  widget: createWidgetFactory(comms.api, $display),
241
248
  },
242
249
  broadcastResult,
250
+ formatNow,
243
251
  confirm,
244
252
  prompt,
245
253
  commDispatch: comms.dispatch,
@@ -0,0 +1,22 @@
1
+ /** The parts of globalThis.__cdpk the evaluator calls. */
2
+ export interface EvaluatorShim {
3
+ broadcastResult(executionCount: number, value: unknown): void | Promise<void>;
4
+ describeError(value: unknown): string;
5
+ commDispatch(msgType: string, content: object, metadata: object, buffers64: string[]): Promise<unknown>;
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;
11
+ }
12
+
13
+ /** A request from the host: an `op` plus its parameters. */
14
+ export interface EvaluatorRequest {
15
+ op: string;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ export type Handler = (request: EvaluatorRequest) => Promise<{ [key: string]: unknown }>;
20
+
21
+ export function propertyNames(value: unknown): string[];
22
+ export function createHandler(shim: EvaluatorShim, evaluate?: (code: string) => unknown): Handler;
@@ -0,0 +1,91 @@
1
+ // The in-process evaluator behind the targets that don't use CDP: Bun (#7),
2
+ // over a pipe (bun-channel.mjs), and workerd (#8), inside a request
3
+ // (workerd-worker.mjs). Cells arrive as global code from
4
+ // src/transform.ts's transformCellForEval, whose value is a promise of the
5
+ // boxed result. Requests and replies mirror the CDP calls the kernel makes
6
+ // of the other targets.
7
+ //
8
+ // No imports: this file must load in every target.
9
+
10
+ /** Own and inherited property names, as Runtime.getProperties lists them. */
11
+ export function propertyNames(value) {
12
+ if (value === null || value === undefined) return [];
13
+ const names = new Set();
14
+ for (let o = Object(value); o !== null; o = Object.getPrototypeOf(o)) {
15
+ for (const name of Object.getOwnPropertyNames(o)) names.add(name);
16
+ }
17
+ return [...names];
18
+ }
19
+
20
+ /**
21
+ * Answers the host's requests. `shim` is globalThis.__cdpk; `evaluate` runs
22
+ * global code. Values the host refers to later (a result to display, an
23
+ * error to describe) stay here under a handle, the stand-in for a CDP
24
+ * objectId.
25
+ */
26
+ export function createHandler(shim, evaluate = (code) => (0, eval)(code)) {
27
+ const handles = new Map();
28
+ let nextHandle = 0;
29
+ const remote = (value) => {
30
+ const objectId = `h${++nextHandle}`;
31
+ handles.set(objectId, value);
32
+ return { type: value === null ? "object" : typeof value, objectId };
33
+ };
34
+ const take = (objectId) => {
35
+ const value = handles.get(objectId);
36
+ handles.delete(objectId);
37
+ return value;
38
+ };
39
+
40
+ return async function handle(request) {
41
+ switch (request.op) {
42
+ case "ping":
43
+ return {};
44
+ case "eval": {
45
+ // The host uses a result's handle before its next cell.
46
+ handles.clear();
47
+ try {
48
+ const box = await evaluate(request.code);
49
+ return { result: box === undefined ? { type: "undefined" } : remote(box[0]) };
50
+ } catch (err) {
51
+ return {
52
+ exceptionDetails: { text: "Uncaught", lineNumber: 0, columnNumber: 0, exception: remote(err) },
53
+ };
54
+ }
55
+ }
56
+ case "broadcast":
57
+ await shim.broadcastResult(request.count, take(request.handle));
58
+ return {};
59
+ case "describe":
60
+ return { value: shim.describeError(take(request.handle)) };
61
+ case "comm":
62
+ try {
63
+ const handled = await shim.commDispatch(request.msgType, request.content, request.metadata, request.buffers);
64
+ return { handled: handled === true };
65
+ } catch (err) {
66
+ return { error: JSON.parse(shim.describeError(err)) };
67
+ }
68
+ case "props": {
69
+ // Completion's expression is a dotted path; resolved by descriptor
70
+ // where the shim can, so completing `obj.` never runs a getter (#10).
71
+ if (typeof shim.resolvePath === "function") {
72
+ const resolved = shim.resolvePath(request.expr);
73
+ return { names: resolved.found && !resolved.accessor ? propertyNames(resolved.value) : [] };
74
+ }
75
+ let value;
76
+ try {
77
+ value = evaluate(`(${request.expr})`);
78
+ } catch {
79
+ return { names: [] };
80
+ }
81
+ return { names: propertyNames(value) };
82
+ }
83
+ case "inspect":
84
+ return { text: shim.inspectPath(request.path, request.detail ?? 0) };
85
+ case "barrier":
86
+ return { value: shim.barrier() };
87
+ default:
88
+ throw new Error(`unknown request ${JSON.stringify(request.op)}`);
89
+ }
90
+ };
91
+ }
@@ -0,0 +1,13 @@
1
+ export type Resolved =
2
+ | { found: false }
3
+ | { found: true; accessor: true; name: string }
4
+ | { found: true; value: unknown; accessor?: undefined };
5
+
6
+ export function resolvePath(path: string, root?: object): Resolved;
7
+ // deno-lint-ignore ban-types -- classes and functions alike
8
+ export function signatureOf(fn: Function): string;
9
+ export function describeValue(value: unknown, options: { inspect: (value: unknown) => string; detail?: number }): string;
10
+ export function inspectPath(
11
+ path: string,
12
+ options: { inspect: (value: unknown) => string; detail?: number; root?: object },
13
+ ): string | null;