@tlockney/cdp-kernel 0.6.0 → 0.7.1

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 (53) hide show
  1. package/README.md +5 -3
  2. package/dist/browser/channels.d.ts +27 -0
  3. package/dist/browser/channels.js +90 -0
  4. package/dist/browser/kernel.js +29800 -0
  5. package/dist/browser/kernel.js.map +7 -0
  6. package/dist/browser/mod.d.ts +25 -0
  7. package/dist/browser/mod.js +41 -0
  8. package/dist/browser/target.d.ts +94 -0
  9. package/dist/browser/target.js +264 -0
  10. package/dist/browser/worker.js +1071 -0
  11. package/dist/browser/worker.js.map +7 -0
  12. package/dist/bun-target.d.ts +2 -0
  13. package/dist/bun-target.js +15 -2
  14. package/dist/cdp.d.ts +11 -0
  15. package/dist/cdp.js +34 -7
  16. package/dist/celld-target.js +3 -2
  17. package/dist/debugger.d.ts +2 -0
  18. package/dist/debugger.js +63 -11
  19. package/dist/kernel-core.d.ts +78 -0
  20. package/dist/kernel-core.js +824 -0
  21. package/dist/kernel.d.ts +6 -1
  22. package/dist/kernel.js +104 -723
  23. package/dist/main.js +11 -4
  24. package/dist/message.d.ts +34 -0
  25. package/dist/message.js +14 -0
  26. package/dist/package.d.ts +2 -2
  27. package/dist/package.js +2 -2
  28. package/dist/target-api.d.ts +92 -0
  29. package/dist/target-api.js +48 -0
  30. package/dist/target.d.ts +5 -76
  31. package/dist/target.js +62 -33
  32. package/dist/traceback.d.ts +75 -0
  33. package/dist/traceback.js +200 -0
  34. package/dist/transform.d.ts +1 -0
  35. package/dist/transform.js +8 -1
  36. package/dist/wire.d.ts +4 -24
  37. package/dist/wire.js +3 -12
  38. package/dist/workerd-target.d.ts +6 -1
  39. package/dist/workerd-target.js +56 -12
  40. package/dist/workers-project.d.ts +1 -1
  41. package/dist/workers-project.js +4 -3
  42. package/dist/zmq-channels.d.ts +14 -0
  43. package/dist/zmq-channels.js +59 -0
  44. package/package.json +9 -2
  45. package/runtime/bootstrap.mjs +2 -0
  46. package/runtime/browser-worker.mjs +202 -0
  47. package/runtime/cdn.d.mts +2 -0
  48. package/runtime/cdn.mjs +17 -0
  49. package/runtime/display.mjs +202 -1
  50. package/runtime/evaluator.mjs +2 -0
  51. package/runtime/format.d.mts +2 -0
  52. package/runtime/format.mjs +125 -0
  53. package/runtime/workerd-worker.mjs +2 -0
@@ -0,0 +1,25 @@
1
+ import type { JsonObject } from "../message.ts";
2
+ import type { TypeStripper, TypeStripperName } from "../transform.ts";
3
+ import { type Channel, type JupyterMessage, type MessageListener } from "./channels.ts";
4
+ import { type BrowserTargetOptions } from "./target.ts";
5
+ export type { Channel, JupyterMessage, MessageListener } from "./channels.ts";
6
+ export { BrowserTarget, type BrowserTargetOptions, type HostHandler, type HostRequest, type WorkerLike } from "./target.ts";
7
+ export interface BrowserKernelOptions extends BrowserTargetOptions {
8
+ /** What strips cells' types; sucrase by default, since esbuild's native binary can't run in a page. */
9
+ typeStrip?: TypeStripperName | TypeStripper;
10
+ /** Where the kernel's own log lines go; console.debug by default. */
11
+ log?(text: string): void;
12
+ /** Returned as `capabilities` in kernel_info_reply: what this page offers cells (#384). */
13
+ capabilities?: JsonObject;
14
+ }
15
+ export interface BrowserKernel {
16
+ /** A message from the frontend, on shell, control or stdin. */
17
+ send(channel: Exclude<Channel, "iopub">, msg: JupyterMessage): void;
18
+ /** Every message the kernel sends, with its channel. Returns a function that stops listening. */
19
+ onMessage(listener: MessageListener): () => void;
20
+ /** Settles once a shutdown_request has been answered. */
21
+ readonly closed: Promise<void>;
22
+ /** An event for the Worker's `host.onEvent` listeners; dropped while no Worker runs (#384). */
23
+ notifyHost(event: string, value?: unknown): void;
24
+ }
25
+ export declare function startBrowserKernel(options: BrowserKernelOptions): BrowserKernel;
@@ -0,0 +1,41 @@
1
+ // cdp-kernel in a browser page: the kernel's message handling
2
+ // (src/kernel-core.ts) on the page, and cells in a Web Worker
3
+ // (runtime/browser-worker.mjs, via src/browser/target.ts).
4
+ //
5
+ // import { startBrowserKernel } from "@tlockney/cdp-kernel/browser";
6
+ // const kernel = startBrowserKernel({
7
+ // createWorker: () => new Worker(new URL("./worker.js", import.meta.url), { type: "module" }),
8
+ // });
9
+ // kernel.onMessage((channel, msg) => console.log(channel, msg.header.msg_type));
10
+ // kernel.send("shell", executeRequest);
11
+ import { KernelCore } from "../kernel-core.js";
12
+ // The browser build (scripts/build-browser.mjs) swaps package.ts, which
13
+ // imports Node, for its VERSION alone.
14
+ import { VERSION } from "../package.js";
15
+ import { PageChannels } from "./channels.js";
16
+ import { BrowserTarget } from "./target.js";
17
+ export { BrowserTarget } from "./target.js";
18
+ export function startBrowserKernel(options) {
19
+ const channels = new PageChannels();
20
+ const log = options.log ?? ((text) => console.debug(`[cdp-kernel] ${text}`));
21
+ let closed;
22
+ const closedPromise = new Promise((resolve) => (closed = resolve));
23
+ let target = null;
24
+ const core = new KernelCore({
25
+ runtime: "browser",
26
+ version: VERSION,
27
+ channels,
28
+ launchTarget: async (events) => (target = await BrowserTarget.launch(events, options)),
29
+ typeStrip: options.typeStrip ?? "sucrase",
30
+ capabilities: options.capabilities,
31
+ log,
32
+ exit: () => closed(),
33
+ });
34
+ core.run().catch((err) => log(`kernel stopped: ${err instanceof Error ? err.stack : err}`));
35
+ return {
36
+ send: (channel, msg) => channels.deliver(channel, msg),
37
+ onMessage: (listener) => channels.onMessage(listener),
38
+ closed: closedPromise,
39
+ notifyHost: (event, value) => target?.notifyHost(event, value),
40
+ };
41
+ }
@@ -0,0 +1,94 @@
1
+ import { type CdpParams, type EvaluateResult, type RemoteObject } from "../cdp.ts";
2
+ import { type KernelTarget, type TargetEvents } from "../target-api.ts";
3
+ import { transformCellForEval } from "../transform.ts";
4
+ /** See BunTarget: how long an interrupted cell has to show it isn't busy. */
5
+ export declare const INTERRUPT_GRACE_MS = 500;
6
+ export declare const RESTART_NOTE: string;
7
+ /** The part of a Worker the target uses, so tests can stand in for one. */
8
+ export interface WorkerLike {
9
+ postMessage(message: unknown): void;
10
+ addEventListener(type: "message", listener: (event: {
11
+ data: unknown;
12
+ }) => void): void;
13
+ addEventListener(type: "error", listener: (event: {
14
+ message?: string;
15
+ }) => void): void;
16
+ terminate(): void;
17
+ }
18
+ export interface BrowserTargetOptions {
19
+ /** Makes the driver Worker: e.g. `new Worker(url, { type: "module" })`. */
20
+ createWorker(): WorkerLike;
21
+ /** What relative imports and import.meta.url resolve against; the page's URL by default. */
22
+ base?: string;
23
+ /** Where npm:, jsr: and bare imports load from; esm.sh by default. */
24
+ cdn?: string;
25
+ /**
26
+ * Modules the Worker imports before the first cell, each time it starts,
27
+ * resolved against `base`. A module's default export, if a function, is
28
+ * called with `{ host, jupyter }` (#384).
29
+ */
30
+ preload?: string[];
31
+ /** Answers the Worker's `host.request` calls; without it they reject (#384). */
32
+ host?: HostHandler;
33
+ }
34
+ /** A request a preload module made with `host.request(method, params)`. */
35
+ export interface HostRequest {
36
+ method: string;
37
+ params: unknown;
38
+ }
39
+ /**
40
+ * Resolves with the value `host.request` resolves with in the Worker, or
41
+ * rejects, and the Worker's promise rejects with an Error carrying the
42
+ * thrown error's `name`, `message` and `code`. `signal` aborts when the
43
+ * Worker cancels the call or stops.
44
+ */
45
+ export type HostHandler = (request: HostRequest, signal: AbortSignal) => unknown;
46
+ export declare class BrowserTarget implements KernelTarget {
47
+ #private;
48
+ readonly runtime: "browser";
49
+ readonly versions: {
50
+ runtime: string;
51
+ engine: string;
52
+ };
53
+ readonly transform: typeof transformCellForEval;
54
+ private constructor();
55
+ static launch(events: TargetEvents, options: BrowserTargetOptions): Promise<BrowserTarget>;
56
+ /** Sends `host.onEvent` listeners in the Worker an event (#384). */
57
+ notifyHost(event: string, value?: unknown): void;
58
+ evaluate(script: string): Promise<EvaluateResult>;
59
+ /**
60
+ * A cell that's waiting on something leaves the Worker free: it answers a
61
+ * ping, and the cell is abandoned with its state intact. A busy cell
62
+ * can't be reached, so the Worker is terminated, and a new one starts on
63
+ * the next cell.
64
+ */
65
+ interrupt(): Promise<void>;
66
+ /** Output arrives before the reply it precedes. */
67
+ flush(): Promise<void>;
68
+ broadcastResult(executionCount: number, result: RemoteObject): Promise<void>;
69
+ dispatchComm(msgType: string, content: CdpParams, metadata: CdpParams, buffers: Uint8Array[]): Promise<{
70
+ handled: boolean;
71
+ } | {
72
+ error: {
73
+ name: string;
74
+ message: string;
75
+ stack: string;
76
+ };
77
+ }>;
78
+ format(value: RemoteObject): Promise<CdpParams>;
79
+ describeError(exception: RemoteObject): Promise<{
80
+ name: string;
81
+ message: string;
82
+ stack: string;
83
+ }>;
84
+ propertyNames(expression: string): Promise<string[]>;
85
+ /** Resolved by property descriptor in the Worker, so nothing runs (#10). */
86
+ inspect(path: string, detail: number): Promise<string | null>;
87
+ /** None: top-level declarations here are properties of globalThis. */
88
+ lexicalNames(): Promise<string[]>;
89
+ /** Never asked for: prompt() in the Worker answers null without asking. */
90
+ replyInput(): void;
91
+ kill(): void;
92
+ }
93
+ /** "Chrome 141", "Firefox 144", "Safari 26": the browser and its major version. */
94
+ export declare function browserName(userAgent: string): string;
@@ -0,0 +1,264 @@
1
+ // The browser target: cells run in a module Web Worker
2
+ // (runtime/browser-worker.mjs) that the page creates. Requests and replies
3
+ // are messages, in the shape of the workerd target's session
4
+ // (src/workerd-target.ts), whose request and interrupt handling this
5
+ // follows. Cells are transformed as for Bun and workerd
6
+ // (transformCellForEval).
7
+ import { asEvaluateResult, isRecord } from "../cdp.js";
8
+ import { Interrupted, isTargetMessage, parseBundle } from "../target-api.js";
9
+ import { transformCellForEval } from "../transform.js";
10
+ /** See BunTarget: how long an interrupted cell has to show it isn't busy. */
11
+ export const INTERRUPT_GRACE_MS = 500;
12
+ export const RESTART_NOTE = "[cdp-kernel] the browser target was busy and can't be interrupted in place, so its " +
13
+ "Worker was stopped; a new one starts on the next cell, without the earlier cells' state";
14
+ /** Rejects a request whose reply the target no longer waits for. */
15
+ class Abandoned extends Error {
16
+ }
17
+ export class BrowserTarget {
18
+ runtime = "browser";
19
+ versions = { runtime: "", engine: "" };
20
+ transform = transformCellForEval;
21
+ #worker;
22
+ #events;
23
+ #pending = new Map();
24
+ /** Requests that interrupt() abandons when the driver can still answer. */
25
+ #interruptible = new Set();
26
+ /** Host requests in flight, by the Worker's id (#384). */
27
+ #hostCalls = new Map();
28
+ #host;
29
+ #nextId = 0;
30
+ #interrupted = false;
31
+ #stopNote;
32
+ #exited = false;
33
+ constructor(worker, events, host) {
34
+ this.#worker = worker;
35
+ this.#events = events;
36
+ this.#host = host;
37
+ worker.addEventListener("message", (event) => this.#receive(event.data));
38
+ // A driver that failed to load: what's pending can't be answered.
39
+ worker.addEventListener("error", (event) => {
40
+ this.#events.stream("stderr", `[cdp-kernel] browser Worker error: ${event.message ?? "unknown"}\n`);
41
+ this.#exit(null);
42
+ });
43
+ }
44
+ static async launch(events, options) {
45
+ const target = new BrowserTarget(options.createWorker(), events, options.host);
46
+ const base = options.base ?? globalThis.location?.href ?? "file:///";
47
+ try {
48
+ const { userAgent } = await target.#request("info", { base, cdn: options.cdn, preload: options.preload ?? [] });
49
+ if (typeof userAgent === "string")
50
+ target.versions.runtime = browserName(userAgent);
51
+ target.versions.engine = engineName(typeof userAgent === "string" ? userAgent : "");
52
+ }
53
+ catch (err) {
54
+ target.kill();
55
+ throw err;
56
+ }
57
+ return target;
58
+ }
59
+ #receive(msg) {
60
+ if (isRecord(msg) && msg.kind === "stream") {
61
+ this.#events.stream(msg.name === "stderr" ? "stderr" : "stdout", String(msg.text));
62
+ }
63
+ else if (isRecord(msg) && msg.kind === "host") {
64
+ this.#hostRequest(msg);
65
+ }
66
+ else if (isTargetMessage(msg)) {
67
+ this.#events.message(msg);
68
+ }
69
+ else if (isRecord(msg) && msg.kind === "reply") {
70
+ const id = Number(msg.id);
71
+ const pending = this.#pending.get(id);
72
+ if (!pending)
73
+ return; // abandoned
74
+ this.#pending.delete(id);
75
+ this.#interruptible.delete(id);
76
+ if (typeof msg.failure === "string")
77
+ pending.reject(new Error(msg.failure));
78
+ else
79
+ pending.resolve(msg);
80
+ }
81
+ }
82
+ #hostRequest(msg) {
83
+ const id = Number(msg.id);
84
+ if (msg.cancel === true) {
85
+ this.#hostCalls.get(id)?.abort();
86
+ this.#hostCalls.delete(id);
87
+ return;
88
+ }
89
+ const controller = new AbortController();
90
+ this.#hostCalls.set(id, controller);
91
+ const answer = (reply) => {
92
+ if (this.#hostCalls.get(id) !== controller)
93
+ return; // cancelled, or the Worker stopped
94
+ this.#hostCalls.delete(id);
95
+ try {
96
+ this.#worker.postMessage({ op: "host", id, ...reply });
97
+ }
98
+ catch (err) {
99
+ // A result structured clone can't copy, such as a function.
100
+ this.#worker.postMessage({ op: "host", id, error: describeHostError(err) });
101
+ }
102
+ };
103
+ const host = this.#host;
104
+ new Promise((resolve) => {
105
+ if (!host)
106
+ throw new Error("this page offers no host requests");
107
+ resolve(host({ method: String(msg.method), params: msg.params }, controller.signal));
108
+ }).then((result) => answer({ result }), (err) => answer({ error: describeHostError(err) }));
109
+ }
110
+ /** Sends `host.onEvent` listeners in the Worker an event (#384). */
111
+ notifyHost(event, value) {
112
+ if (!this.#exited)
113
+ this.#worker.postMessage({ op: "host", event, value });
114
+ }
115
+ #exit(code) {
116
+ if (this.#exited)
117
+ return;
118
+ this.#exited = true;
119
+ this.#worker.terminate();
120
+ for (const controller of this.#hostCalls.values())
121
+ controller.abort();
122
+ this.#hostCalls.clear();
123
+ // Reported before the requests it cut off fail, so the note lands in
124
+ // the interrupted cell's output.
125
+ this.#events.exit(code, this.#stopNote);
126
+ for (const pending of this.#pending.values())
127
+ pending.reject(new Abandoned("the browser Worker stopped"));
128
+ this.#pending.clear();
129
+ }
130
+ #request(op, params = {}, interruptible = false) {
131
+ if (this.#exited)
132
+ return Promise.reject(new Abandoned("the browser Worker stopped"));
133
+ const id = ++this.#nextId;
134
+ return new Promise((resolve, reject) => {
135
+ this.#pending.set(id, { resolve, reject });
136
+ if (interruptible)
137
+ this.#interruptible.add(id);
138
+ this.#worker.postMessage({ id, op, ...params });
139
+ });
140
+ }
141
+ async #interruptibly(op, params, what) {
142
+ this.#interrupted = false;
143
+ try {
144
+ return await this.#request(op, params, true);
145
+ }
146
+ catch (err) {
147
+ if (err instanceof Abandoned || this.#interrupted)
148
+ throw new Interrupted(`${what} interrupted`);
149
+ throw err;
150
+ }
151
+ }
152
+ async evaluate(script) {
153
+ return asEvaluateResult(await this.#interruptibly("eval", { code: script }, "Execution"));
154
+ }
155
+ /**
156
+ * A cell that's waiting on something leaves the Worker free: it answers a
157
+ * ping, and the cell is abandoned with its state intact. A busy cell
158
+ * can't be reached, so the Worker is terminated, and a new one starts on
159
+ * the next cell.
160
+ */
161
+ async interrupt() {
162
+ this.#interrupted = true;
163
+ const running = [...this.#interruptible];
164
+ const answered = await Promise.race([
165
+ this.#request("ping").then(() => true, () => false),
166
+ new Promise((resolve) => setTimeout(() => resolve(false), INTERRUPT_GRACE_MS)),
167
+ ]);
168
+ if (answered) {
169
+ for (const id of running) {
170
+ this.#pending.get(id)?.reject(new Abandoned("interrupted"));
171
+ this.#pending.delete(id);
172
+ this.#interruptible.delete(id);
173
+ }
174
+ return;
175
+ }
176
+ this.#stopNote = RESTART_NOTE;
177
+ this.#exit(null);
178
+ }
179
+ /** Output arrives before the reply it precedes. */
180
+ flush() {
181
+ return Promise.resolve();
182
+ }
183
+ async broadcastResult(executionCount, result) {
184
+ if (result.objectId === undefined)
185
+ return;
186
+ await this.#request("broadcast", { count: executionCount, handle: result.objectId });
187
+ }
188
+ async dispatchComm(msgType, content, metadata, buffers) {
189
+ const reply = await this.#interruptibly("comm", { msgType, content, metadata, buffers: buffers.map(toBase64) }, "Comm handler");
190
+ if (isRecord(reply.error)) {
191
+ const { name, message, stack } = reply.error;
192
+ return { error: { name: String(name), message: String(message), stack: String(stack) } };
193
+ }
194
+ return { handled: reply.handled === true };
195
+ }
196
+ async format(value) {
197
+ const reply = await this.#request("format", { handle: value.objectId });
198
+ return parseBundle(reply.value);
199
+ }
200
+ async describeError(exception) {
201
+ if (exception.objectId === undefined)
202
+ return { name: "Error", message: exception.description ?? "", stack: "" };
203
+ const { value } = await this.#request("describe", { handle: exception.objectId });
204
+ return JSON.parse(typeof value === "string" ? value : '{"name":"Error","message":"","stack":""}');
205
+ }
206
+ async propertyNames(expression) {
207
+ const reply = await this.#request("props", { expr: expression }).catch(() => null);
208
+ return Array.isArray(reply?.names) ? reply.names.filter((n) => typeof n === "string") : [];
209
+ }
210
+ /** Resolved by property descriptor in the Worker, so nothing runs (#10). */
211
+ async inspect(path, detail) {
212
+ const reply = await this.#request("inspect", { path, detail }).catch(() => null);
213
+ return typeof reply?.text === "string" ? reply.text : null;
214
+ }
215
+ /** None: top-level declarations here are properties of globalThis. */
216
+ lexicalNames() {
217
+ return Promise.resolve([]);
218
+ }
219
+ /** Never asked for: prompt() in the Worker answers null without asking. */
220
+ replyInput() { }
221
+ kill() {
222
+ this.#exit(null);
223
+ }
224
+ }
225
+ function describeHostError(err) {
226
+ if (isRecord(err)) {
227
+ const { name, message, code } = err;
228
+ return {
229
+ name: typeof name === "string" ? name : "Error",
230
+ message: typeof message === "string" ? message : String(err),
231
+ ...(code === undefined ? {} : { code: code }),
232
+ };
233
+ }
234
+ return { name: "Error", message: String(err) };
235
+ }
236
+ function toBase64(bytes) {
237
+ let binary = "";
238
+ for (let i = 0; i < bytes.length; i += 0x8000)
239
+ binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
240
+ return btoa(binary);
241
+ }
242
+ /** "Chrome 141", "Firefox 144", "Safari 26": the browser and its major version. */
243
+ export function browserName(userAgent) {
244
+ for (const [name, re] of [
245
+ ["Edge", /\bEdg\/(\d+)/],
246
+ ["Firefox", /\bFirefox\/(\d+)/],
247
+ ["Chrome", /\b(?:Headless)?Chrome\/(\d+)/],
248
+ ["Safari", /\bVersion\/(\d+)[\d.]* .*Safari\//],
249
+ ]) {
250
+ const m = re.exec(userAgent);
251
+ if (m)
252
+ return `${name} ${m[1]}`;
253
+ }
254
+ return "a browser";
255
+ }
256
+ function engineName(userAgent) {
257
+ if (/\bFirefox\//.test(userAgent))
258
+ return "SpiderMonkey";
259
+ if (/\b(?:Chrome|Chromium|Edg)\//.test(userAgent))
260
+ return "V8";
261
+ if (/\bSafari\//.test(userAgent))
262
+ return "JavaScriptCore";
263
+ return "";
264
+ }