@tlockney/cdp-kernel 0.5.0 → 0.7.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 +27 -6
- package/dist/browser/channels.d.ts +27 -0
- package/dist/browser/channels.js +90 -0
- package/dist/browser/kernel.js +29674 -0
- package/dist/browser/kernel.js.map +7 -0
- package/dist/browser/mod.d.ts +20 -0
- package/dist/browser/mod.js +38 -0
- package/dist/browser/target.d.ts +71 -0
- package/dist/browser/target.js +206 -0
- package/dist/browser/worker.js +1000 -0
- package/dist/browser/worker.js.map +7 -0
- package/dist/cdp.d.ts +11 -0
- package/dist/cdp.js +6 -1
- package/dist/celld-target.js +9 -12
- package/dist/children.d.ts +25 -3
- package/dist/children.js +84 -2
- package/dist/debugger.d.ts +2 -0
- package/dist/debugger.js +12 -6
- package/dist/deno-types.d.ts +30 -0
- package/dist/deno-types.js +93 -0
- package/dist/kernel-core.d.ts +69 -0
- package/dist/kernel-core.js +754 -0
- package/dist/kernel.d.ts +12 -1
- package/dist/kernel.js +132 -605
- package/dist/language-service-worker.d.ts +1 -0
- package/dist/language-service-worker.js +88 -0
- package/dist/language-service.d.ts +64 -0
- package/dist/language-service.js +128 -0
- package/dist/main.js +102 -8
- package/dist/message.d.ts +34 -0
- package/dist/message.js +14 -0
- package/dist/mod.d.ts +1 -0
- package/dist/mod.js +1 -0
- package/dist/notebook-types.d.ts +62 -0
- package/dist/notebook-types.js +310 -0
- package/dist/package.d.ts +2 -2
- package/dist/package.js +2 -2
- package/dist/parent.d.ts +12 -0
- package/dist/parent.js +39 -0
- package/dist/port-holder.d.ts +13 -0
- package/dist/port-holder.js +105 -0
- package/dist/sourcemap.d.ts +7 -2
- package/dist/sourcemap.js +9 -3
- package/dist/strip-esbuild.d.ts +2 -0
- package/dist/strip-esbuild.js +44 -0
- package/dist/strip-sucrase.d.ts +2 -0
- package/dist/strip-sucrase.js +78 -0
- package/dist/target-api.d.ts +80 -0
- package/dist/target-api.js +22 -0
- package/dist/target.d.ts +26 -74
- package/dist/target.js +232 -58
- package/dist/traceback.d.ts +75 -0
- package/dist/traceback.js +200 -0
- package/dist/transform.d.ts +22 -1
- package/dist/transform.js +46 -43
- package/dist/wire.d.ts +4 -24
- package/dist/wire.js +3 -12
- package/dist/workerd-target.js +1 -1
- package/dist/workers-project.js +13 -2
- package/dist/zmq-channels.d.ts +14 -0
- package/dist/zmq-channels.js +59 -0
- package/dist/zmtp.d.ts +2 -0
- package/dist/zmtp.js +12 -0
- package/package.json +15 -5
- package/runtime/bootstrap.mjs +113 -33
- package/runtime/browser-worker.mjs +119 -0
- package/runtime/cdn.d.mts +2 -0
- package/runtime/cdn.mjs +17 -0
- package/runtime/display.mjs +202 -1
- package/runtime/format.d.mts +2 -0
- package/runtime/format.mjs +125 -0
- package/runtime/introspect.mjs +1 -1
- package/runtime/node-hooks.d.mts +6 -0
- package/runtime/node-hooks.mjs +11 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TypeStripper, TypeStripperName } from "../transform.ts";
|
|
2
|
+
import { type Channel, type JupyterMessage, type MessageListener } from "./channels.ts";
|
|
3
|
+
import { type BrowserTargetOptions } from "./target.ts";
|
|
4
|
+
export type { Channel, JupyterMessage, MessageListener } from "./channels.ts";
|
|
5
|
+
export { BrowserTarget, type BrowserTargetOptions, type WorkerLike } from "./target.ts";
|
|
6
|
+
export interface BrowserKernelOptions extends BrowserTargetOptions {
|
|
7
|
+
/** What strips cells' types; sucrase by default, since esbuild's native binary can't run in a page. */
|
|
8
|
+
typeStrip?: TypeStripperName | TypeStripper;
|
|
9
|
+
/** Where the kernel's own log lines go; console.debug by default. */
|
|
10
|
+
log?(text: string): void;
|
|
11
|
+
}
|
|
12
|
+
export interface BrowserKernel {
|
|
13
|
+
/** A message from the frontend, on shell, control or stdin. */
|
|
14
|
+
send(channel: Exclude<Channel, "iopub">, msg: JupyterMessage): void;
|
|
15
|
+
/** Every message the kernel sends, with its channel. Returns a function that stops listening. */
|
|
16
|
+
onMessage(listener: MessageListener): () => void;
|
|
17
|
+
/** Settles once a shutdown_request has been answered. */
|
|
18
|
+
readonly closed: Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare function startBrowserKernel(options: BrowserKernelOptions): BrowserKernel;
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
const core = new KernelCore({
|
|
24
|
+
runtime: "browser",
|
|
25
|
+
version: VERSION,
|
|
26
|
+
channels,
|
|
27
|
+
launchTarget: (events) => BrowserTarget.launch(events, options),
|
|
28
|
+
typeStrip: options.typeStrip ?? "sucrase",
|
|
29
|
+
log,
|
|
30
|
+
exit: () => closed(),
|
|
31
|
+
});
|
|
32
|
+
core.run().catch((err) => log(`kernel stopped: ${err instanceof Error ? err.stack : err}`));
|
|
33
|
+
return {
|
|
34
|
+
send: (channel, msg) => channels.deliver(channel, msg),
|
|
35
|
+
onMessage: (listener) => channels.onMessage(listener),
|
|
36
|
+
closed: closedPromise,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
export declare class BrowserTarget implements KernelTarget {
|
|
27
|
+
#private;
|
|
28
|
+
readonly runtime: "browser";
|
|
29
|
+
readonly versions: {
|
|
30
|
+
runtime: string;
|
|
31
|
+
engine: string;
|
|
32
|
+
};
|
|
33
|
+
readonly transform: typeof transformCellForEval;
|
|
34
|
+
private constructor();
|
|
35
|
+
static launch(events: TargetEvents, options: BrowserTargetOptions): Promise<BrowserTarget>;
|
|
36
|
+
evaluate(script: string): Promise<EvaluateResult>;
|
|
37
|
+
/**
|
|
38
|
+
* A cell that's waiting on something leaves the Worker free: it answers a
|
|
39
|
+
* ping, and the cell is abandoned with its state intact. A busy cell
|
|
40
|
+
* can't be reached, so the Worker is terminated, and a new one starts on
|
|
41
|
+
* the next cell.
|
|
42
|
+
*/
|
|
43
|
+
interrupt(): Promise<void>;
|
|
44
|
+
/** Output arrives before the reply it precedes. */
|
|
45
|
+
flush(): Promise<void>;
|
|
46
|
+
broadcastResult(executionCount: number, result: RemoteObject): Promise<void>;
|
|
47
|
+
dispatchComm(msgType: string, content: CdpParams, metadata: CdpParams, buffers: Uint8Array[]): Promise<{
|
|
48
|
+
handled: boolean;
|
|
49
|
+
} | {
|
|
50
|
+
error: {
|
|
51
|
+
name: string;
|
|
52
|
+
message: string;
|
|
53
|
+
stack: string;
|
|
54
|
+
};
|
|
55
|
+
}>;
|
|
56
|
+
describeError(exception: RemoteObject): Promise<{
|
|
57
|
+
name: string;
|
|
58
|
+
message: string;
|
|
59
|
+
stack: string;
|
|
60
|
+
}>;
|
|
61
|
+
propertyNames(expression: string): Promise<string[]>;
|
|
62
|
+
/** Resolved by property descriptor in the Worker, so nothing runs (#10). */
|
|
63
|
+
inspect(path: string, detail: number): Promise<string | null>;
|
|
64
|
+
/** None: top-level declarations here are properties of globalThis. */
|
|
65
|
+
lexicalNames(): Promise<string[]>;
|
|
66
|
+
/** Never asked for: prompt() in the Worker answers null without asking. */
|
|
67
|
+
replyInput(): void;
|
|
68
|
+
kill(): void;
|
|
69
|
+
}
|
|
70
|
+
/** "Chrome 141", "Firefox 144", "Safari 26": the browser and its major version. */
|
|
71
|
+
export declare function browserName(userAgent: string): string;
|
|
@@ -0,0 +1,206 @@
|
|
|
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 } 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
|
+
#nextId = 0;
|
|
27
|
+
#interrupted = false;
|
|
28
|
+
#stopNote;
|
|
29
|
+
#exited = false;
|
|
30
|
+
constructor(worker, events) {
|
|
31
|
+
this.#worker = worker;
|
|
32
|
+
this.#events = events;
|
|
33
|
+
worker.addEventListener("message", (event) => this.#receive(event.data));
|
|
34
|
+
// A driver that failed to load: what's pending can't be answered.
|
|
35
|
+
worker.addEventListener("error", (event) => {
|
|
36
|
+
this.#events.stream("stderr", `[cdp-kernel] browser Worker error: ${event.message ?? "unknown"}\n`);
|
|
37
|
+
this.#exit(null);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
static async launch(events, options) {
|
|
41
|
+
const target = new BrowserTarget(options.createWorker(), events);
|
|
42
|
+
const base = options.base ?? globalThis.location?.href ?? "file:///";
|
|
43
|
+
try {
|
|
44
|
+
const { userAgent } = await target.#request("info", { base, cdn: options.cdn });
|
|
45
|
+
if (typeof userAgent === "string")
|
|
46
|
+
target.versions.runtime = browserName(userAgent);
|
|
47
|
+
target.versions.engine = engineName(typeof userAgent === "string" ? userAgent : "");
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
target.kill();
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
return target;
|
|
54
|
+
}
|
|
55
|
+
#receive(msg) {
|
|
56
|
+
if (isRecord(msg) && msg.kind === "stream") {
|
|
57
|
+
this.#events.stream(msg.name === "stderr" ? "stderr" : "stdout", String(msg.text));
|
|
58
|
+
}
|
|
59
|
+
else if (isTargetMessage(msg)) {
|
|
60
|
+
this.#events.message(msg);
|
|
61
|
+
}
|
|
62
|
+
else if (isRecord(msg) && msg.kind === "reply") {
|
|
63
|
+
const id = Number(msg.id);
|
|
64
|
+
const pending = this.#pending.get(id);
|
|
65
|
+
if (!pending)
|
|
66
|
+
return; // abandoned
|
|
67
|
+
this.#pending.delete(id);
|
|
68
|
+
this.#interruptible.delete(id);
|
|
69
|
+
if (typeof msg.failure === "string")
|
|
70
|
+
pending.reject(new Error(msg.failure));
|
|
71
|
+
else
|
|
72
|
+
pending.resolve(msg);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
#exit(code) {
|
|
76
|
+
if (this.#exited)
|
|
77
|
+
return;
|
|
78
|
+
this.#exited = true;
|
|
79
|
+
this.#worker.terminate();
|
|
80
|
+
// Reported before the requests it cut off fail, so the note lands in
|
|
81
|
+
// the interrupted cell's output.
|
|
82
|
+
this.#events.exit(code, this.#stopNote);
|
|
83
|
+
for (const pending of this.#pending.values())
|
|
84
|
+
pending.reject(new Abandoned("the browser Worker stopped"));
|
|
85
|
+
this.#pending.clear();
|
|
86
|
+
}
|
|
87
|
+
#request(op, params = {}, interruptible = false) {
|
|
88
|
+
if (this.#exited)
|
|
89
|
+
return Promise.reject(new Abandoned("the browser Worker stopped"));
|
|
90
|
+
const id = ++this.#nextId;
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
this.#pending.set(id, { resolve, reject });
|
|
93
|
+
if (interruptible)
|
|
94
|
+
this.#interruptible.add(id);
|
|
95
|
+
this.#worker.postMessage({ id, op, ...params });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async #interruptibly(op, params, what) {
|
|
99
|
+
this.#interrupted = false;
|
|
100
|
+
try {
|
|
101
|
+
return await this.#request(op, params, true);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
if (err instanceof Abandoned || this.#interrupted)
|
|
105
|
+
throw new Interrupted(`${what} interrupted`);
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async evaluate(script) {
|
|
110
|
+
return asEvaluateResult(await this.#interruptibly("eval", { code: script }, "Execution"));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A cell that's waiting on something leaves the Worker free: it answers a
|
|
114
|
+
* ping, and the cell is abandoned with its state intact. A busy cell
|
|
115
|
+
* can't be reached, so the Worker is terminated, and a new one starts on
|
|
116
|
+
* the next cell.
|
|
117
|
+
*/
|
|
118
|
+
async interrupt() {
|
|
119
|
+
this.#interrupted = true;
|
|
120
|
+
const running = [...this.#interruptible];
|
|
121
|
+
const answered = await Promise.race([
|
|
122
|
+
this.#request("ping").then(() => true, () => false),
|
|
123
|
+
new Promise((resolve) => setTimeout(() => resolve(false), INTERRUPT_GRACE_MS)),
|
|
124
|
+
]);
|
|
125
|
+
if (answered) {
|
|
126
|
+
for (const id of running) {
|
|
127
|
+
this.#pending.get(id)?.reject(new Abandoned("interrupted"));
|
|
128
|
+
this.#pending.delete(id);
|
|
129
|
+
this.#interruptible.delete(id);
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.#stopNote = RESTART_NOTE;
|
|
134
|
+
this.#exit(null);
|
|
135
|
+
}
|
|
136
|
+
/** Output arrives before the reply it precedes. */
|
|
137
|
+
flush() {
|
|
138
|
+
return Promise.resolve();
|
|
139
|
+
}
|
|
140
|
+
async broadcastResult(executionCount, result) {
|
|
141
|
+
if (result.objectId === undefined)
|
|
142
|
+
return;
|
|
143
|
+
await this.#request("broadcast", { count: executionCount, handle: result.objectId });
|
|
144
|
+
}
|
|
145
|
+
async dispatchComm(msgType, content, metadata, buffers) {
|
|
146
|
+
const reply = await this.#interruptibly("comm", { msgType, content, metadata, buffers: buffers.map(toBase64) }, "Comm handler");
|
|
147
|
+
if (isRecord(reply.error)) {
|
|
148
|
+
const { name, message, stack } = reply.error;
|
|
149
|
+
return { error: { name: String(name), message: String(message), stack: String(stack) } };
|
|
150
|
+
}
|
|
151
|
+
return { handled: reply.handled === true };
|
|
152
|
+
}
|
|
153
|
+
async describeError(exception) {
|
|
154
|
+
if (exception.objectId === undefined)
|
|
155
|
+
return { name: "Error", message: exception.description ?? "", stack: "" };
|
|
156
|
+
const { value } = await this.#request("describe", { handle: exception.objectId });
|
|
157
|
+
return JSON.parse(typeof value === "string" ? value : '{"name":"Error","message":"","stack":""}');
|
|
158
|
+
}
|
|
159
|
+
async propertyNames(expression) {
|
|
160
|
+
const reply = await this.#request("props", { expr: expression }).catch(() => null);
|
|
161
|
+
return Array.isArray(reply?.names) ? reply.names.filter((n) => typeof n === "string") : [];
|
|
162
|
+
}
|
|
163
|
+
/** Resolved by property descriptor in the Worker, so nothing runs (#10). */
|
|
164
|
+
async inspect(path, detail) {
|
|
165
|
+
const reply = await this.#request("inspect", { path, detail }).catch(() => null);
|
|
166
|
+
return typeof reply?.text === "string" ? reply.text : null;
|
|
167
|
+
}
|
|
168
|
+
/** None: top-level declarations here are properties of globalThis. */
|
|
169
|
+
lexicalNames() {
|
|
170
|
+
return Promise.resolve([]);
|
|
171
|
+
}
|
|
172
|
+
/** Never asked for: prompt() in the Worker answers null without asking. */
|
|
173
|
+
replyInput() { }
|
|
174
|
+
kill() {
|
|
175
|
+
this.#exit(null);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function toBase64(bytes) {
|
|
179
|
+
let binary = "";
|
|
180
|
+
for (let i = 0; i < bytes.length; i += 0x8000)
|
|
181
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
182
|
+
return btoa(binary);
|
|
183
|
+
}
|
|
184
|
+
/** "Chrome 141", "Firefox 144", "Safari 26": the browser and its major version. */
|
|
185
|
+
export function browserName(userAgent) {
|
|
186
|
+
for (const [name, re] of [
|
|
187
|
+
["Edge", /\bEdg\/(\d+)/],
|
|
188
|
+
["Firefox", /\bFirefox\/(\d+)/],
|
|
189
|
+
["Chrome", /\b(?:Headless)?Chrome\/(\d+)/],
|
|
190
|
+
["Safari", /\bVersion\/(\d+)[\d.]* .*Safari\//],
|
|
191
|
+
]) {
|
|
192
|
+
const m = re.exec(userAgent);
|
|
193
|
+
if (m)
|
|
194
|
+
return `${name} ${m[1]}`;
|
|
195
|
+
}
|
|
196
|
+
return "a browser";
|
|
197
|
+
}
|
|
198
|
+
function engineName(userAgent) {
|
|
199
|
+
if (/\bFirefox\//.test(userAgent))
|
|
200
|
+
return "SpiderMonkey";
|
|
201
|
+
if (/\b(?:Chrome|Chromium|Edg)\//.test(userAgent))
|
|
202
|
+
return "V8";
|
|
203
|
+
if (/\bSafari\//.test(userAgent))
|
|
204
|
+
return "JavaScriptCore";
|
|
205
|
+
return "";
|
|
206
|
+
}
|