@tlockney/cdp-kernel 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,11 +3,12 @@
3
3
  A Jupyter kernel extracted from Deno's (`deno jupyter`, v2.9.6) that drives
4
4
  any V8 runtime over the Chrome DevTools Protocol. The kernel host runs on
5
5
  Node or Deno; the code you type runs in a separate **target** runtime:
6
- Node or Deno today, and anything else whose inspector speaks CDP in
7
- principle.
6
+ Node or Deno over CDP. Bun, workerd and celld evaluate cells in-process
7
+ instead (see [Bun](#bun), [workerd](#workerd) and [celld](#celld)). In
8
+ principle, anything else whose inspector speaks CDP works too.
8
9
 
9
10
  This is early software. It passes a conformance suite derived from Deno's
10
- own Jupyter integration tests on both targets, and it's packaged for npm and
11
+ own Jupyter integration tests on every target, and it's packaged for npm and
11
12
  JSR as `@tlockney/cdp-kernel`, but it hasn't seen much real use yet. The
12
13
  feasibility study behind it covers the design,
13
14
  the other options considered (workerd, Bun, rusty_v8, FFI), and the
@@ -40,9 +41,9 @@ So the extraction is:
40
41
 
41
42
  ## Install
42
43
 
43
- Each install writes a Jupyter kernelspec: `cdp-node`, `cdp-deno`, or
44
- `cdp-node-deno` (Node with the Deno compatibility namespace). Then pick the
45
- kernel in JupyterLab or VS Code.
44
+ Each install writes a Jupyter kernelspec: `cdp-node`, `cdp-deno`,
45
+ `cdp-node-deno` (Node with the Deno compatibility namespace), `cdp-bun`,
46
+ `cdp-workerd`, or `cdp-celld`. Then pick the kernel in JupyterLab or VS Code.
46
47
 
47
48
  **From npm** (host runs on Node 24+; Node 26 is what's been tested):
48
49
 
@@ -51,6 +52,9 @@ npm install -g @tlockney/cdp-kernel
51
52
  cdp-kernel install --runtime node
52
53
  cdp-kernel install --runtime deno
53
54
  cdp-kernel install --runtime node --deno-compat
55
+ cdp-kernel install --runtime bun
56
+ cdp-kernel install --runtime workerd
57
+ cdp-kernel install --runtime celld
54
58
  ```
55
59
 
56
60
  The kernelspec records the absolute paths of `node` and the installed
@@ -65,7 +69,13 @@ deno run -A jsr:@tlockney/cdp-kernel/cli install --runtime deno
65
69
  The kernelspec pins the release it was installed from
66
70
  (`jsr:@tlockney/cdp-kernel@<version>/cli`). On first launch the target's
67
71
  runtime modules are copied into `~/.cache/cdp-kernel/runtime/`. A Node
68
- target needs `node` on `PATH`.
72
+ target needs `node` on `PATH`, and a Bun target needs `bun` (1.4 is what's
73
+ been tested). A workerd target uses the `workerd` in the notebook
74
+ directory's `node_modules` (a Workers project has one through wrangler),
75
+ or the one on `PATH` (`npm install -g workerd`; 2026-10-15 is what's been
76
+ tested). A celld target uses `~/.local/bin/celld`, where celld's installer
77
+ puts it, or the one on `PATH` (0.6.2 is what's been tested; celld has no
78
+ Windows build).
69
79
 
70
80
  Deno 2.9+ refuses package versions less than 24 hours old by default
71
81
  ([minimum dependency age](https://docs.deno.com/go/minimum-dependency-age)).
@@ -82,6 +92,9 @@ npm ci
82
92
  npm run install:node # kernelspec cdp-node
83
93
  npm run install:deno # kernelspec cdp-deno
84
94
  npm run install:node-deno # kernelspec cdp-node-deno
95
+ npm run install:bun # kernelspec cdp-bun
96
+ npm run install:workerd # kernelspec cdp-workerd
97
+ npm run install:celld # kernelspec cdp-celld
85
98
  ```
86
99
 
87
100
  These kernelspecs point at the checkout, so don't move it after installing.
@@ -106,6 +119,13 @@ In a cell:
106
119
  `~/.cache/cdp-kernel/npm`.
107
120
  - On Deno they use Deno's global cache, unless the notebook is in a Deno
108
121
  project (below).
122
+ - On Bun they install with `bun add` into `~/.cache/cdp-kernel/bun`. Other
123
+ bare imports resolve from the notebook's directory, so without a
124
+ `node_modules` there, Bun auto-installs them.
125
+ - On workerd, the host bundles every import other than a builtin
126
+ (`node:`, `cloudflare:`) with esbuild, using Workers' resolution
127
+ conditions, and sends it with the cell. `npm:` and `jsr:` install into
128
+ the npm cache first, as on Node.
109
129
  - **Deno projects.** If a `deno.json` or `deno.jsonc` sits in the notebook's
110
130
  directory or above it, a Deno target uses it, as `deno jupyter` does:
111
131
  - its import map (`"@/": "./lib/"` aliases, mapped packages);
@@ -120,8 +140,113 @@ In a cell:
120
140
  reported in the first cell's output, and the kernel runs as in a plain
121
141
  directory. Outside a project, the target runs with
122
142
  `--node-modules-dir=none --no-lock`.
123
- - `prompt()` and `confirm()` work over the stdin channel. Interrupt keeps
124
- state.
143
+ - `prompt()` and `confirm()` work over the stdin channel, except on
144
+ workerd. Interrupt keeps state, except for a busy cell on Bun or workerd
145
+ (below).
146
+
147
+ ## Bun
148
+
149
+ Bun's `--inspect` speaks the WebKit Inspector Protocol, which lacks what the
150
+ CDP backend relies on: `replMode`, `Runtime.addBinding`,
151
+ `Runtime.terminateExecution`, and top-level `await` in `Runtime.evaluate`.
152
+ So the Bun target has its own backend (`src/bun-target.ts`,
153
+ `runtime/bun-channel.mjs`):
154
+
155
+ - **No inspector.** The host and Bun exchange JSON lines over a pipe on
156
+ fd 3, which only the host holds, so there is no port to discover.
157
+ - **Cells run in-process.** Each cell becomes global code wrapped in an
158
+ async function (`transformCellForEval` in `src/transform.ts`). Its
159
+ top-level declarations become properties of `globalThis`, so later
160
+ cells, and closures from this one, see them. This is the
161
+ tslab/IJavascript technique.
162
+ - **Same output ordering.** stdout and stderr stay pipes, ordered against
163
+ display messages by the same barrier markers as the CDP targets.
164
+ - **Interrupt.** A cell that's waiting on a promise is interrupted in place,
165
+ keeping state. A busy one (`while (true) {}`) can't be reached, so Bun is
166
+ stopped and restarts on the next cell without the earlier state. The cell
167
+ output says so.
168
+
169
+ What differs from the CDP targets is listed under [Known gaps](#known-gaps).
170
+
171
+ ## workerd
172
+
173
+ workerd's inspector can evaluate code, but outside a request there is no
174
+ I/O context: timers, `fetch` and `crypto.randomUUID()` throw "Disallowed
175
+ operation called within global scope". So the workerd target runs its
176
+ cells inside a driver Worker (`src/workerd-target.ts`,
177
+ `runtime/workerd-worker.mjs`):
178
+
179
+ - **One session.** The host opens a single WebSocket to the driver, and
180
+ every cell runs inside that session's request. Cells get Workers'
181
+ `fetch`, `Request`, `caches`, timers and randomness, and anything one
182
+ cell makes carries over to the next: globals, a Durable Object stub, a
183
+ stream, a timer still pending.
184
+ - **A private socket.** Outside a Workers project, the host writes a
185
+ workerd config into a private temporary directory and serves the driver
186
+ on a Unix socket there, opened with a per-launch token. There is no TCP
187
+ port and no inspector.
188
+ - **Cells.** Cells are transformed as for Bun and run by workerd's
189
+ `UnsafeEval` binding (hence `--experimental`).
190
+ - **Output.** `console` output and display messages come back over the
191
+ session as they happen, in order.
192
+ - **Interrupt.** A cell that's waiting on something is abandoned in place,
193
+ keeping state, as on Bun. A busy one gets Bun's treatment too: workerd is
194
+ stopped and restarts on the next cell.
195
+
196
+ What workerd's own process prints, such as its warnings, goes to the Jupyter
197
+ server log, not the notebook.
198
+
199
+ ### Workers projects
200
+
201
+ If a `wrangler.jsonc`, `wrangler.json` or `wrangler.toml` sits in the
202
+ notebook's directory or above it, the driver runs on the project's own
203
+ Miniflare, the simulator `wrangler dev` uses, with the project's bindings
204
+ (`src/workers-project.ts`):
205
+
206
+ - `env` holds the bindings: whatever wrangler translates for Miniflare.
207
+ Vars, KV, D1 and Durable Objects are tested; R2, Queues and the others
208
+ go through the same path but aren't.
209
+ - The project's `main` is bundled into the driver, so its Durable Object
210
+ classes run, and `worker` is its default export:
211
+ `await worker.fetch(new Request("https://x/"), env)`.
212
+ - State lives in the project's `.wrangler/state/cdp-kernel`, beside
213
+ `wrangler dev`'s own, so the two never open the same Durable Object.
214
+ - The first cell's output names the bindings. If the project can't load,
215
+ for example because wrangler isn't installed in it, the first cell says
216
+ why and the kernel runs on plain workerd.
217
+
218
+ wrangler and Miniflare come from the project's `node_modules`, so they match
219
+ its versions. Miniflare 4 and 5 both work. Miniflare listens on a loopback
220
+ TCP port, so there the per-launch token is what keeps other local processes
221
+ out.
222
+
223
+ ## celld
224
+
225
+ [celld](https://github.com/denoland/celld) runs Workers and Durable Objects
226
+ on your own storage. Like workerd it has no I/O outside a request, so the
227
+ celld target runs the same driver Worker over the same WebSocket session
228
+ (`src/celld-target.ts`). Cells, output, interrupts and host-bundled imports
229
+ all work as on workerd. The differences:
230
+
231
+ - **No `UnsafeEval` needed.** celld allows a plain global `eval`, so it runs
232
+ without workerd's `--experimental`.
233
+ - **One module.** The host bundles the driver, and a project's `main`, into
234
+ a single module, and gives celld the result with `no_bundle`. celld
235
+ doesn't need its own esbuild.
236
+ - **Loopback TCP.** celld listens only on TCP, so the session is a loopback
237
+ WebSocket opened with a per-launch token. The kernel picks a free port
238
+ and runs `celld dev --no-watch` on a generated config.
239
+ - **Projects.** In a celld project (a `wrangler.jsonc` or `wrangler.json`
240
+ at or above the notebook), the generated config copies the project's
241
+ compatibility settings and bindings: vars, KV, D1, R2, Durable Objects,
242
+ Queues, Workflows, services, Worker Loaders. `env` and `worker` then work
243
+ as on workerd. The generated config is written to the project's
244
+ `.celld/cdp-kernel` and removed when the kernel stops. The state stays
245
+ there across kernels, beside `celld dev`'s own `.celld/dev`. celld rejects `wrangler.toml`; a project
246
+ that can't load is reported in the first cell, and the driver runs alone.
247
+ - **Stopping.** `celld dev` runs its node in a separate process group and
248
+ waits for it to stop gracefully, which a busy cell never allows. So an
249
+ interrupt of a busy cell kills the node, then `celld dev`.
125
250
 
126
251
  ## Editor types
127
252
 
@@ -216,8 +341,8 @@ long cell waits for the cell to finish.
216
341
  ```sh
217
342
  npm run check # tsc --strict over src/ and test/
218
343
  npm test # unit tests (node:test)
219
- npm run e2e # conformance suite via jupyter_client/libzmq, on node, deno, node-deno
220
- npm run test:lab # an anywidget counter in real JupyterLab, headless Chromium (node, deno)
344
+ npm run e2e # conformance suite via jupyter_client/libzmq, on every target variant
345
+ npm run test:lab # an anywidget counter in real JupyterLab, headless Chromium (node, deno, bun, workerd, celld)
221
346
  uv run --with nbclient --with nbformat python examples/run_notebook.py # Deno-style notebook on deno + node-deno
222
347
  ```
223
348
 
@@ -266,12 +391,36 @@ that failed.
266
391
  - Results are formatted with `util.inspect` on Node, so they look slightly
267
392
  different from Deno's `Deno.inspect`.
268
393
  - WebGPU texture and buffer display is dropped.
269
- - Targets:
270
- - **Bun** speaks the WebKit inspector protocol: no `replMode`, bindings,
271
- `terminateExecution`, or top-level await in `Runtime.evaluate`.
272
- - **workerd** forbids timers and I/O outside a request.
273
-
274
- Both need a different backend; see the writeup.
394
+ - On Bun:
395
+ - Interrupting a busy cell restarts Bun and loses state. An awaiting cell
396
+ keeps it.
397
+ - Top-level declarations are properties of `globalThis`. A `const` can be
398
+ reassigned, and declaring a global's name (`const fetch = ...`) replaces
399
+ that global rather than shadowing it.
400
+ - Only a trailing expression statement shows a result: `x` does, but
401
+ `if (c) { x }` doesn't.
402
+ - Bun 1.4 can't import a module whose path contains `?`.
403
+ - No `--deno-compat` namespace.
404
+ - On workerd:
405
+ - All cells share one request, so it never ends. Unlike production
406
+ Workers, there is no per-request CPU or duration limit, and
407
+ `ctx.waitUntil` isn't available in a cell.
408
+ - No `prompt()` or `confirm()` answers: they return `null`, as without a
409
+ stdin channel. `jupyter.image(path)` can't read files; pass the bytes.
410
+ - Each imported module is bundled with its dependencies. Two imports that
411
+ share a dependency each get their own copy.
412
+ - Interrupting a busy cell restarts workerd and loses state.
413
+ - Top-level declarations and results behave as on Bun: declarations are
414
+ `globalThis` properties, and only a trailing expression shows a result.
415
+ - In a Workers project, `main` is bundled with esbuild directly, not with
416
+ wrangler's build. Custom `build` commands, `alias` and module `rules`
417
+ aren't applied.
418
+ - On celld, as on workerd, except:
419
+ - Only the binding and compatibility keys of a project's config are
420
+ copied. `assets`, `triggers` and `containers` aren't, and neither is
421
+ `.dev.vars`. Relative paths inside the copied keys resolve from
422
+ `.celld/cdp-kernel`, not from the project.
423
+ - Interrupting a busy cell restarts celld and loses state.
275
424
 
276
425
  ## Provenance
277
426
 
@@ -0,0 +1,58 @@
1
+ import type { CdpParams, EvaluateResult, RemoteObject } from "./cdp.ts";
2
+ import { transformCellForEval } from "./transform.ts";
3
+ import { type KernelTarget, type TargetEvents } from "./target.ts";
4
+ /**
5
+ * How long an interrupted target has to answer a ping. A cell waiting on a
6
+ * promise leaves the event loop free, so the answer is near-instant; a busy
7
+ * one can't answer at all.
8
+ */
9
+ export declare const INTERRUPT_GRACE_MS = 500;
10
+ export declare const RESTART_NOTE: string;
11
+ export declare function bunLaunchCommand(bootstrap: string, executable?: string): {
12
+ cmd: string;
13
+ args: string[];
14
+ };
15
+ export declare class BunTarget implements KernelTarget {
16
+ #private;
17
+ readonly runtime = "bun";
18
+ readonly versions: {
19
+ runtime: string;
20
+ engine: string;
21
+ };
22
+ readonly transform: typeof transformCellForEval;
23
+ private constructor();
24
+ static launch(events: TargetEvents, options?: {
25
+ cwd?: string;
26
+ executable?: string;
27
+ }): Promise<BunTarget>;
28
+ evaluate(script: string): Promise<EvaluateResult>;
29
+ /**
30
+ * There's no terminateExecution, but a cell that's waiting on a promise
31
+ * leaves the event loop free: the target answers a ping, and the cell is
32
+ * abandoned with the session intact, like terminateExecution on an idle
33
+ * isolate (its pending work still runs). A busy cell can't be reached, so
34
+ * the process is stopped and restarts on the next cell, losing its state.
35
+ */
36
+ interrupt(): Promise<void>;
37
+ flush(): Promise<void>;
38
+ broadcastResult(executionCount: number, result: RemoteObject): Promise<void>;
39
+ dispatchComm(msgType: string, content: CdpParams, metadata: CdpParams, buffers: Uint8Array[]): Promise<{
40
+ handled: boolean;
41
+ } | {
42
+ error: {
43
+ name: string;
44
+ message: string;
45
+ stack: string;
46
+ };
47
+ }>;
48
+ describeError(exception: RemoteObject): Promise<{
49
+ name: string;
50
+ message: string;
51
+ stack: string;
52
+ }>;
53
+ propertyNames(expression: string): Promise<string[]>;
54
+ /** None: top-level declarations here are properties of globalThis. */
55
+ lexicalNames(): Promise<string[]>;
56
+ replyInput(value: string | null): void;
57
+ kill(): void;
58
+ }
@@ -0,0 +1,231 @@
1
+ // The Bun target (#7). Bun's --inspect speaks the WebKit Inspector Protocol,
2
+ // which has no replMode, Runtime.addBinding or Runtime.terminateExecution, so
3
+ // Bun isn't driven over an inspector at all. The host sends requests over a
4
+ // private pipe (fd 3) to runtime/bun-channel.mjs, which evaluates cells
5
+ // in-process (transformCellForEval). Output ordering is the CDP targets':
6
+ // stdout and stderr are pipes, ordered against channel messages by the
7
+ // bootstrap's barrier markers.
8
+ import { Buffer } from "node:buffer";
9
+ import process from "node:process";
10
+ import { spawn } from "node:child_process";
11
+ import { join } from "node:path";
12
+ import { transformCellForEval } from "./transform.js";
13
+ import { runtimeDir } from "./package.js";
14
+ import { Gates, Interrupted, isTargetMessage, PipeDemux } from "./target.js";
15
+ /**
16
+ * How long an interrupted target has to answer a ping. A cell waiting on a
17
+ * promise leaves the event loop free, so the answer is near-instant; a busy
18
+ * one can't answer at all.
19
+ */
20
+ export const INTERRUPT_GRACE_MS = 500;
21
+ export const RESTART_NOTE = "[cdp-kernel] bun was busy and can't be interrupted in place, so it was stopped; " +
22
+ "it restarts on the next cell, without the earlier cells' state";
23
+ /** Rejects a request whose reply the host no longer waits for. */
24
+ class Abandoned extends Error {
25
+ }
26
+ function isDuplex(stream) {
27
+ return typeof stream?.write === "function" && typeof stream?.on === "function";
28
+ }
29
+ function isRecord(value) {
30
+ return typeof value === "object" && value !== null;
31
+ }
32
+ export function bunLaunchCommand(bootstrap, executable) {
33
+ // No flags: Bun reads the notebook directory's bunfig.toml and package.json
34
+ // on its own, and auto-installs bare imports when there's no node_modules.
35
+ return { cmd: executable ?? "bun", args: [bootstrap] };
36
+ }
37
+ export class BunTarget {
38
+ runtime = "bun";
39
+ versions = { runtime: "", engine: "" };
40
+ transform = transformCellForEval;
41
+ #child;
42
+ #channel;
43
+ #stdout;
44
+ #stderr;
45
+ #gates = new Gates();
46
+ #pending = new Map();
47
+ #nextId = 0;
48
+ #messages = Promise.resolve();
49
+ #interrupted = false;
50
+ #stopNote;
51
+ constructor(child, channel, events) {
52
+ this.#child = child;
53
+ this.#channel = channel;
54
+ this.#stdout = new PipeDemux((text) => events.stream("stdout", text), (n) => this.#gates.wait(n));
55
+ this.#stderr = new PipeDemux((text) => events.stream("stderr", text), (n) => this.#gates.wait(n));
56
+ }
57
+ static async launch(events, options = {}) {
58
+ const bootstrap = join(await runtimeDir(new URL("../runtime/", import.meta.url).href), "bootstrap.mjs");
59
+ const { cmd, args } = bunLaunchCommand(bootstrap, options.executable);
60
+ const env = { ...process.env, FORCE_COLOR: "1" };
61
+ delete env.NO_COLOR;
62
+ const child = spawn(cmd, args, { cwd: options.cwd, stdio: ["pipe", "pipe", "pipe", "pipe"], env });
63
+ const channel = child.stdio[3];
64
+ if (!child.stdout || !child.stderr || !isDuplex(channel)) {
65
+ child.kill();
66
+ throw new Error("could not open the pipes to bun");
67
+ }
68
+ const target = new BunTarget(child, channel, events);
69
+ child.stdout.setEncoding("utf8");
70
+ child.stderr.setEncoding("utf8");
71
+ child.stdout.on("data", (chunk) => target.#stdout.push(chunk));
72
+ const ready = new Promise((resolve, reject) => {
73
+ let early = "";
74
+ const onEarly = (chunk) => (early += chunk);
75
+ child.stderr?.on("data", onEarly);
76
+ target.#onReady = (msg) => {
77
+ child.stderr?.off("data", onEarly);
78
+ if (early)
79
+ target.#stderr.push(early);
80
+ child.stderr?.on("data", (chunk) => target.#stderr.push(chunk));
81
+ resolve(msg);
82
+ };
83
+ child.once("error", reject);
84
+ child.once("exit", (code) => reject(new Error(`${cmd} exited (${code}) before it was ready:\n${early}`)));
85
+ });
86
+ target.#readChannel(events);
87
+ child.once("exit", (code) => target.#exited(code, events));
88
+ const { versions } = await ready;
89
+ if (isRecord(versions))
90
+ Object.assign(target.versions, versions);
91
+ return target;
92
+ }
93
+ #onReady = () => { };
94
+ #readChannel(events) {
95
+ let buffer = "";
96
+ this.#channel.setEncoding("utf8");
97
+ this.#channel.on("data", (chunk) => {
98
+ buffer += chunk;
99
+ for (let nl = buffer.indexOf("\n"); nl !== -1; nl = buffer.indexOf("\n")) {
100
+ const msg = JSON.parse(buffer.slice(0, nl));
101
+ buffer = buffer.slice(nl + 1);
102
+ if (isTargetMessage(msg))
103
+ this.#deliver(msg, events);
104
+ else if (isRecord(msg) && msg.kind === "reply")
105
+ this.#settle(msg);
106
+ else if (isRecord(msg) && msg.kind === "ready")
107
+ this.#onReady(msg);
108
+ }
109
+ });
110
+ // A closed pipe is reported through the child's exit.
111
+ this.#channel.on("error", () => { });
112
+ }
113
+ #settle(reply) {
114
+ const id = Number(reply.id);
115
+ const pending = this.#pending.get(id);
116
+ if (!pending)
117
+ return; // abandoned
118
+ this.#pending.delete(id);
119
+ if (typeof reply.failure === "string")
120
+ pending.reject(new Error(reply.failure));
121
+ else
122
+ pending.resolve(reply);
123
+ }
124
+ #exited(code, events) {
125
+ this.#gates.openAll();
126
+ this.#stdout.close();
127
+ this.#stderr.close();
128
+ events.exit(code, this.#stopNote);
129
+ this.#abandonPending(`bun exited (${code})`);
130
+ }
131
+ #abandonPending(reason) {
132
+ for (const pending of this.#pending.values())
133
+ pending.reject(new Abandoned(reason));
134
+ this.#pending.clear();
135
+ }
136
+ // Binding messages wait until both pipes have caught up to their marker,
137
+ // and are delivered strictly in order (as in Target).
138
+ #deliver(msg, events) {
139
+ this.#messages = this.#messages.then(async () => {
140
+ await Promise.all([this.#stdout.waitFor(msg.barrier), this.#stderr.waitFor(msg.barrier)]);
141
+ events.message(msg);
142
+ this.#gates.open(msg.barrier);
143
+ });
144
+ }
145
+ #request(op, params = {}) {
146
+ const id = ++this.#nextId;
147
+ return new Promise((resolve, reject) => {
148
+ this.#pending.set(id, { resolve, reject });
149
+ this.#channel.write(JSON.stringify({ id, op, ...params }) + "\n");
150
+ });
151
+ }
152
+ /** A request that interrupt() may cut short. */
153
+ async #interruptible(op, params, what) {
154
+ this.#interrupted = false;
155
+ try {
156
+ return await this.#request(op, params);
157
+ }
158
+ catch (err) {
159
+ if (err instanceof Abandoned || this.#interrupted)
160
+ throw new Interrupted(`${what} interrupted`);
161
+ throw err;
162
+ }
163
+ }
164
+ async evaluate(script) {
165
+ return (await this.#interruptible("eval", { code: script }, "Execution"));
166
+ }
167
+ /**
168
+ * There's no terminateExecution, but a cell that's waiting on a promise
169
+ * leaves the event loop free: the target answers a ping, and the cell is
170
+ * abandoned with the session intact, like terminateExecution on an idle
171
+ * isolate (its pending work still runs). A busy cell can't be reached, so
172
+ * the process is stopped and restarts on the next cell, losing its state.
173
+ */
174
+ async interrupt() {
175
+ this.#interrupted = true;
176
+ const answered = await Promise.race([
177
+ this.#request("ping").then(() => true, () => false),
178
+ new Promise((resolve) => setTimeout(() => resolve(false), INTERRUPT_GRACE_MS)),
179
+ ]);
180
+ if (answered) {
181
+ this.#abandonPending("interrupted");
182
+ return;
183
+ }
184
+ this.#stopNote = RESTART_NOTE;
185
+ this.#child.kill("SIGKILL");
186
+ }
187
+ async flush() {
188
+ const { value } = await this.#request("barrier");
189
+ const barrier = Number(value);
190
+ await Promise.all([this.#stdout.waitFor(barrier), this.#stderr.waitFor(barrier)]);
191
+ await this.#messages;
192
+ }
193
+ async broadcastResult(executionCount, result) {
194
+ if (result.objectId === undefined)
195
+ return;
196
+ await this.#request("broadcast", { count: executionCount, handle: result.objectId });
197
+ }
198
+ async dispatchComm(msgType, content, metadata, buffers) {
199
+ const reply = await this.#interruptible("comm", {
200
+ msgType,
201
+ content,
202
+ metadata,
203
+ buffers: buffers.map((b) => Buffer.from(b).toString("base64")),
204
+ }, "Comm handler");
205
+ if (isRecord(reply.error)) {
206
+ const { name, message, stack } = reply.error;
207
+ return { error: { name: String(name), message: String(message), stack: String(stack) } };
208
+ }
209
+ return { handled: reply.handled === true };
210
+ }
211
+ async describeError(exception) {
212
+ if (exception.objectId === undefined)
213
+ return { name: "Error", message: exception.description ?? "", stack: "" };
214
+ const { value } = await this.#request("describe", { handle: exception.objectId });
215
+ return JSON.parse(typeof value === "string" ? value : '{"name":"Error","message":"","stack":""}');
216
+ }
217
+ async propertyNames(expression) {
218
+ const reply = await this.#request("props", { expr: expression }).catch(() => null);
219
+ return Array.isArray(reply?.names) ? reply.names.filter((n) => typeof n === "string") : [];
220
+ }
221
+ /** None: top-level declarations here are properties of globalThis. */
222
+ lexicalNames() {
223
+ return Promise.resolve([]);
224
+ }
225
+ replyInput(value) {
226
+ this.#child.stdin?.write(JSON.stringify({ value }) + "\n");
227
+ }
228
+ kill() {
229
+ this.#child.kill();
230
+ }
231
+ }
@@ -0,0 +1,39 @@
1
+ import type { CdpParams } from "./cdp.ts";
2
+ import type { TargetEvents } from "./target.ts";
3
+ import { WorkerdTarget } from "./workerd-target.ts";
4
+ /** The project keys the generated config keeps: bindings and compatibility. */
5
+ export declare const PROJECT_KEYS: string[];
6
+ /**
7
+ * The celld config for the driver: the project's bindings (or none), the
8
+ * pre-built driver as its one module, and the kernel's token and import.meta
9
+ * as vars, which the driver hides from cells' `env`.
10
+ */
11
+ export declare function celldConfig(project: CdpParams | null, { token, meta }: {
12
+ token: string;
13
+ meta: {
14
+ url: string;
15
+ dirname: string;
16
+ filename: string;
17
+ };
18
+ }): CdpParams;
19
+ /**
20
+ * celld's installer puts it in ~/.local/bin, which a Jupyter server's PATH
21
+ * often lacks, so look there before falling back to PATH.
22
+ */
23
+ export declare function findCelld(home?: string): string;
24
+ /** The version in `celld --version`'s output ("celld X.Y.Z"). */
25
+ export declare function celldVersionOf(output: string): string;
26
+ /** The driver, and a project's main, as the single module celld runs. */
27
+ export declare function bundleDriver(runtime: string, projectMain?: string, define?: {
28
+ [name: string]: string;
29
+ }): Promise<string>;
30
+ /** A free loopback port: celld dev needs a number, and rejects 0. */
31
+ export declare function freePort(): Promise<number>;
32
+ /**
33
+ * A celld target. In a celld project its bindings come along; a project
34
+ * that can't load is reported in the first cell, and the driver runs alone.
35
+ */
36
+ export declare function launchCelld(events: TargetEvents, options?: {
37
+ cwd?: string;
38
+ executable?: string;
39
+ }): Promise<WorkerdTarget>;