@tlockney/cdp-kernel 0.0.0-stage → 0.1.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.
package/dist/wire.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ export declare const DELIMITER: Uint8Array<ArrayBuffer>;
2
+ export declare const PROTOCOL_VERSION = "5.3";
3
+ export type JsonObject = {
4
+ [key: string]: unknown;
5
+ };
6
+ export interface Header extends JsonObject {
7
+ msg_id: string;
8
+ session: string;
9
+ date: string;
10
+ username: string;
11
+ msg_type: string;
12
+ version: string;
13
+ }
14
+ export interface Message {
15
+ identities: Uint8Array[];
16
+ sig: string;
17
+ /** Raw bytes of header, parent_header, metadata, content as received. */
18
+ signedParts: Uint8Array[];
19
+ header: Header;
20
+ parentHeader: JsonObject;
21
+ metadata: JsonObject;
22
+ content: JsonObject;
23
+ buffers: Uint8Array[];
24
+ }
25
+ export declare function sign(key: string, parts: Uint8Array[]): string;
26
+ export declare function verify(key: string, parts: Uint8Array[], sig: string): boolean;
27
+ export declare function makeHeader(session: string, msgType: string): Header;
28
+ export interface EncodeOptions {
29
+ identities?: Uint8Array[];
30
+ parentHeader?: JsonObject;
31
+ metadata?: JsonObject;
32
+ buffers?: Uint8Array[];
33
+ }
34
+ export declare function encodeMessage(session: string, key: string, msgType: string, content: JsonObject, options?: EncodeOptions): Uint8Array[];
35
+ export declare function decodeMessage(frames: Uint8Array[]): Message;
package/dist/wire.js ADDED
@@ -0,0 +1,94 @@
1
+ // Jupyter wire protocol: message framing and HMAC signing.
2
+ //
3
+ // Ported from Deno's cli/js/jupyter_kernel.js (v2.9.6), which is
4
+ // Copyright 2018-2026 the Deno authors, MIT license. The structure is kept;
5
+ // WebCrypto's async HMAC is replaced with node:crypto's synchronous one.
6
+ import { Buffer } from "node:buffer";
7
+ import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
8
+ const ENC = new TextEncoder();
9
+ const DEC = new TextDecoder();
10
+ export const DELIMITER = ENC.encode("<IDS|MSG>");
11
+ export const PROTOCOL_VERSION = "5.3";
12
+ export function sign(key, parts) {
13
+ if (key.length === 0)
14
+ return "";
15
+ const hmac = createHmac("sha256", key);
16
+ for (const part of parts)
17
+ hmac.update(part);
18
+ return hmac.digest("hex");
19
+ }
20
+ // Messages that fail this check must be dropped: the kernel runs user code,
21
+ // so an unsigned execute_request from any local process would be code
22
+ // execution. Matches Deno's hmacVerify.
23
+ export function verify(key, parts, sig) {
24
+ if (key.length === 0)
25
+ return true;
26
+ if (!/^[0-9a-f]+$/i.test(sig) || sig.length % 2 !== 0)
27
+ return false;
28
+ const expected = Buffer.from(sign(key, parts), "hex");
29
+ const actual = Buffer.from(sig, "hex");
30
+ return expected.length === actual.length && timingSafeEqual(expected, actual);
31
+ }
32
+ export function makeHeader(session, msgType) {
33
+ return {
34
+ msg_id: randomUUID(),
35
+ session,
36
+ date: new Date().toISOString(),
37
+ username: "kernel",
38
+ msg_type: msgType,
39
+ version: PROTOCOL_VERSION,
40
+ };
41
+ }
42
+ export function encodeMessage(session, key, msgType, content, options = {}) {
43
+ const parts = [
44
+ makeHeader(session, msgType),
45
+ options.parentHeader ?? {},
46
+ options.metadata ?? {},
47
+ content,
48
+ ].map((part) => ENC.encode(JSON.stringify(part)));
49
+ return [
50
+ ...(options.identities ?? []),
51
+ DELIMITER,
52
+ ENC.encode(sign(key, parts)),
53
+ ...parts,
54
+ ...(options.buffers ?? []),
55
+ ];
56
+ }
57
+ function isDelimiter(frame) {
58
+ if (frame.length !== DELIMITER.length)
59
+ return false;
60
+ for (let i = 0; i < frame.length; i++) {
61
+ if (frame[i] !== DELIMITER[i])
62
+ return false;
63
+ }
64
+ return true;
65
+ }
66
+ function parseObject(frame) {
67
+ const value = JSON.parse(DEC.decode(frame));
68
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
69
+ throw new Error("expected a JSON object frame");
70
+ }
71
+ return value;
72
+ }
73
+ function isHeader(value) {
74
+ return typeof value.msg_id === "string" && typeof value.msg_type === "string";
75
+ }
76
+ export function decodeMessage(frames) {
77
+ const delimIdx = frames.findIndex(isDelimiter);
78
+ if (delimIdx === -1 || frames.length < delimIdx + 6) {
79
+ throw new Error("malformed message: no <IDS|MSG> delimiter");
80
+ }
81
+ const header = parseObject(frames[delimIdx + 2]);
82
+ if (!isHeader(header))
83
+ throw new Error("malformed message header");
84
+ return {
85
+ identities: frames.slice(0, delimIdx),
86
+ sig: DEC.decode(frames[delimIdx + 1]),
87
+ signedParts: frames.slice(delimIdx + 2, delimIdx + 6),
88
+ header,
89
+ parentHeader: parseObject(frames[delimIdx + 3]),
90
+ metadata: parseObject(frames[delimIdx + 4]),
91
+ content: parseObject(frames[delimIdx + 5]),
92
+ buffers: frames.slice(delimIdx + 6),
93
+ };
94
+ }
package/dist/zmtp.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { type Server, type Socket } from "node:net";
3
+ /** Pull-style reader over a socket's 'data' events. */
4
+ export declare class SocketReader {
5
+ #private;
6
+ constructor(socket: Socket);
7
+ readExact(n: number): Promise<Uint8Array>;
8
+ }
9
+ export interface Frame {
10
+ data: Uint8Array;
11
+ hasMore: boolean;
12
+ isCommand: boolean;
13
+ }
14
+ export declare function readFrame(reader: SocketReader): Promise<Frame>;
15
+ export declare function encodeFrame(data: Uint8Array, more: boolean, isCommand?: boolean): Uint8Array;
16
+ export declare function encodeMultipart(frames: Uint8Array[]): Buffer;
17
+ export declare function makeGreeting(): Uint8Array;
18
+ export declare function makeReadyCommand(socketType: string): Uint8Array;
19
+ export declare function recvMultipart(reader: SocketReader): Promise<Uint8Array[]>;
20
+ /** Async FIFO with blocking pop. */
21
+ export declare class Queue<T> {
22
+ #private;
23
+ push(item: T): void;
24
+ pop(): Promise<T>;
25
+ }
26
+ /** REP socket for the heartbeat channel: echo everything. */
27
+ export declare function startHeartbeat(port: number, host: string): Server;
28
+ export interface RoutedMessage {
29
+ peer: string;
30
+ frames: Uint8Array[];
31
+ }
32
+ /** ROUTER socket: each TCP connection is one peer with a generated identity. */
33
+ export declare class RouterSocket {
34
+ #private;
35
+ readonly incoming: Queue<RoutedMessage>;
36
+ readonly peers: Map<string, Socket>;
37
+ readonly server: Server;
38
+ constructor(port: number, host: string);
39
+ recv(): Promise<RoutedMessage>;
40
+ send(peer: string, frames: Uint8Array[]): void;
41
+ sendAll(frames: Uint8Array[]): void;
42
+ }
43
+ /** PUB socket for iopub: fan out to every subscriber, no topic filtering. */
44
+ export declare class PubSocket {
45
+ readonly subscribers: Set<Socket>;
46
+ readonly server: Server;
47
+ constructor(port: number, host: string);
48
+ send(frames: Uint8Array[]): void;
49
+ }
package/dist/zmtp.js ADDED
@@ -0,0 +1,241 @@
1
+ // Minimal ZMTP 3.0 server sockets (NULL mechanism) over node:net.
2
+ //
3
+ // Ported from Deno's cli/js/jupyter_kernel.js (v2.9.6), which is
4
+ // Copyright 2018-2026 the Deno authors, MIT license. Deno's kernel dropped
5
+ // libzmq in denoland/deno#34083 and speaks just enough ZMTP for Jupyter
6
+ // frontends; this keeps the same greeting, READY command and framing, and
7
+ // swaps Deno.listen for node:net plus a small buffered reader.
8
+ import { Buffer } from "node:buffer";
9
+ import { createServer } from "node:net";
10
+ import { randomBytes } from "node:crypto";
11
+ const ENC = new TextEncoder();
12
+ /** Pull-style reader over a socket's 'data' events. */
13
+ export class SocketReader {
14
+ #chunks = [];
15
+ #buffered = 0;
16
+ #waiter = null;
17
+ #closed = false;
18
+ #error = null;
19
+ constructor(socket) {
20
+ socket.on("data", (chunk) => {
21
+ this.#chunks.push(chunk);
22
+ this.#buffered += chunk.length;
23
+ this.#wake();
24
+ });
25
+ socket.on("close", () => {
26
+ this.#closed = true;
27
+ this.#wake();
28
+ });
29
+ socket.on("error", (err) => {
30
+ this.#error = err;
31
+ this.#wake();
32
+ });
33
+ }
34
+ #wake() {
35
+ const waiter = this.#waiter;
36
+ this.#waiter = null;
37
+ waiter?.();
38
+ }
39
+ async readExact(n) {
40
+ while (this.#buffered < n) {
41
+ if (this.#error)
42
+ throw this.#error;
43
+ if (this.#closed)
44
+ throw new Error("connection closed");
45
+ await new Promise((resolve) => (this.#waiter = resolve));
46
+ }
47
+ const all = this.#chunks.length === 1 ? this.#chunks[0] : Buffer.concat(this.#chunks);
48
+ const out = all.subarray(0, n);
49
+ const rest = all.subarray(n);
50
+ this.#chunks = rest.length > 0 ? [rest] : [];
51
+ this.#buffered = rest.length;
52
+ return new Uint8Array(out);
53
+ }
54
+ }
55
+ export async function readFrame(reader) {
56
+ const [flag] = await reader.readExact(1);
57
+ const isLong = (flag & 0x02) !== 0;
58
+ let size;
59
+ if (isLong) {
60
+ const b = await reader.readExact(8);
61
+ size = Number(new DataView(b.buffer, b.byteOffset, 8).getBigUint64(0));
62
+ }
63
+ else {
64
+ [size] = await reader.readExact(1);
65
+ }
66
+ return {
67
+ data: await reader.readExact(size),
68
+ hasMore: (flag & 0x01) !== 0,
69
+ isCommand: (flag & 0x04) !== 0,
70
+ };
71
+ }
72
+ export function encodeFrame(data, more, isCommand = false) {
73
+ const long = data.length > 255;
74
+ const flag = (more ? 0x01 : 0) | (long ? 0x02 : 0) | (isCommand ? 0x04 : 0);
75
+ const headerLen = long ? 9 : 2;
76
+ const buf = new Uint8Array(headerLen + data.length);
77
+ buf[0] = flag;
78
+ if (long) {
79
+ new DataView(buf.buffer).setBigUint64(1, BigInt(data.length));
80
+ }
81
+ else {
82
+ buf[1] = data.length;
83
+ }
84
+ buf.set(data, headerLen);
85
+ return buf;
86
+ }
87
+ // One write per multipart message. Deno found that many small writes
88
+ // interact badly with Nagle/delayed-ACK against libzmq peers.
89
+ export function encodeMultipart(frames) {
90
+ return Buffer.concat(frames.map((f, i) => encodeFrame(f, i < frames.length - 1)));
91
+ }
92
+ export function makeGreeting() {
93
+ // Signature 0xff..0x7f, version 3.0, mechanism "NULL", as-server = 0.
94
+ // Deno learned (regression from #34083) that libzmq peers stall if a
95
+ // binding NULL peer sets as-server=1, so mirror libzmq's own greeting.
96
+ const buf = new Uint8Array(64);
97
+ buf[0] = 0xff;
98
+ buf[9] = 0x7f;
99
+ buf[10] = 3;
100
+ buf[11] = 0;
101
+ buf.set(ENC.encode("NULL"), 12);
102
+ buf[32] = 0;
103
+ return buf;
104
+ }
105
+ export function makeReadyCommand(socketType) {
106
+ const name = ENC.encode("READY");
107
+ const propName = ENC.encode("Socket-Type");
108
+ const value = ENC.encode(socketType);
109
+ const body = new Uint8Array(1 + name.length + 1 + propName.length + 4 + value.length);
110
+ let o = 0;
111
+ body[o++] = name.length;
112
+ body.set(name, o);
113
+ o += name.length;
114
+ body[o++] = propName.length;
115
+ body.set(propName, o);
116
+ o += propName.length;
117
+ new DataView(body.buffer).setUint32(o, value.length);
118
+ o += 4;
119
+ body.set(value, o);
120
+ return encodeFrame(body, false, true);
121
+ }
122
+ async function handshake(socket, reader, socketType) {
123
+ socket.write(makeGreeting());
124
+ await reader.readExact(64);
125
+ socket.write(makeReadyCommand(socketType));
126
+ await readFrame(reader); // peer READY
127
+ }
128
+ export async function recvMultipart(reader) {
129
+ const frames = [];
130
+ for (;;) {
131
+ const frame = await readFrame(reader);
132
+ if (frame.isCommand)
133
+ continue; // e.g. SUBSCRIBE/PING commands
134
+ frames.push(frame.data);
135
+ if (!frame.hasMore)
136
+ return frames;
137
+ }
138
+ }
139
+ function listen(port, host, onConn) {
140
+ const server = createServer((socket) => {
141
+ socket.setNoDelay(true);
142
+ onConn(socket);
143
+ });
144
+ server.listen(port, host);
145
+ return server;
146
+ }
147
+ /** Async FIFO with blocking pop. */
148
+ export class Queue {
149
+ #items = [];
150
+ #waiters = [];
151
+ push(item) {
152
+ const waiter = this.#waiters.shift();
153
+ if (waiter)
154
+ waiter(item);
155
+ else
156
+ this.#items.push(item);
157
+ }
158
+ pop() {
159
+ const item = this.#items.shift();
160
+ if (item !== undefined)
161
+ return Promise.resolve(item);
162
+ return new Promise((resolve) => this.#waiters.push(resolve));
163
+ }
164
+ }
165
+ /** REP socket for the heartbeat channel: echo everything. */
166
+ export function startHeartbeat(port, host) {
167
+ return listen(port, host, (socket) => {
168
+ const reader = new SocketReader(socket);
169
+ (async () => {
170
+ await handshake(socket, reader, "REP");
171
+ for (;;)
172
+ socket.write(encodeMultipart(await recvMultipart(reader)));
173
+ })().catch(() => socket.destroy());
174
+ });
175
+ }
176
+ /** ROUTER socket: each TCP connection is one peer with a generated identity. */
177
+ export class RouterSocket {
178
+ incoming = new Queue();
179
+ peers = new Map();
180
+ server;
181
+ constructor(port, host) {
182
+ this.server = listen(port, host, (socket) => this.#handlePeer(socket));
183
+ }
184
+ #handlePeer(socket) {
185
+ const peer = randomBytes(5).toString("hex");
186
+ const reader = new SocketReader(socket);
187
+ (async () => {
188
+ // Register only after the handshake so a proactive send (stdin's
189
+ // input_request) can't land inside the greeting exchange.
190
+ await handshake(socket, reader, "ROUTER");
191
+ this.peers.set(peer, socket);
192
+ for (;;)
193
+ this.incoming.push({ peer, frames: await recvMultipart(reader) });
194
+ })()
195
+ .catch(() => { })
196
+ .finally(() => {
197
+ this.peers.delete(peer);
198
+ socket.destroy();
199
+ });
200
+ }
201
+ recv() {
202
+ return this.incoming.pop();
203
+ }
204
+ send(peer, frames) {
205
+ this.peers.get(peer)?.write(encodeMultipart(frames));
206
+ }
207
+ sendAll(frames) {
208
+ const buf = encodeMultipart(frames);
209
+ for (const socket of this.peers.values())
210
+ socket.write(buf);
211
+ }
212
+ }
213
+ /** PUB socket for iopub: fan out to every subscriber, no topic filtering. */
214
+ export class PubSocket {
215
+ subscribers = new Set();
216
+ server;
217
+ constructor(port, host) {
218
+ this.server = listen(port, host, (socket) => {
219
+ const reader = new SocketReader(socket);
220
+ (async () => {
221
+ await handshake(socket, reader, "PUB");
222
+ // SUB sends a SUBSCRIBE (a command frame in ZMTP 3.1, a 0x01-prefixed
223
+ // message in 3.0). Readiness doesn't depend on it, so register now and
224
+ // drain whatever arrives.
225
+ this.subscribers.add(socket);
226
+ for (;;)
227
+ await readFrame(reader);
228
+ })()
229
+ .catch(() => { })
230
+ .finally(() => {
231
+ this.subscribers.delete(socket);
232
+ socket.destroy();
233
+ });
234
+ });
235
+ }
236
+ send(frames) {
237
+ const buf = encodeMultipart(frames);
238
+ for (const socket of this.subscribers)
239
+ socket.write(buf);
240
+ }
241
+ }
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@tlockney/cdp-kernel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Deno's Jupyter kernel, extracted to run notebooks on any V8 runtime over the Chrome DevTools Protocol",
5
+ "license": "MIT",
6
+ "author": "Thomas Lockney <thomas@lockney.net>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/tlockney/cdp-kernel.git"
10
+ },
11
+ "keywords": ["jupyter", "kernel", "typescript", "notebook", "deno", "node", "cdp", "devtools-protocol"],
12
+ "type": "module",
13
+ "engines": {
14
+ "node": ">=24"
15
+ },
16
+ "bin": {
17
+ "cdp-kernel": "dist/main.js"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/mod.d.ts",
22
+ "default": "./dist/mod.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist/",
27
+ "runtime/*.mjs",
28
+ "runtime/*.d.mts"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
35
+ "check": "tsc -p tsconfig.json",
36
+ "install:node": "node src/main.ts install --runtime node",
37
+ "install:deno": "node src/main.ts install --runtime deno",
38
+ "install:node-deno": "node src/main.ts install --runtime node --deno-compat",
39
+ "prepack": "npm run build",
40
+ "test": "node --test test/*.test.ts",
41
+ "e2e": "uv run --with nbclient --with nbformat --with jupyter_client python test/e2e.py"
42
+ },
43
+ "dependencies": {
44
+ "acorn": "^8.15.0",
45
+ "acorn-walk": "^8.3.4",
46
+ "esbuild": "^0.25.10"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^24.19.2",
50
+ "typescript": "^5.9.3"
51
+ }
52
+ }
@@ -0,0 +1,190 @@
1
+ // Entry module for the target runtime. The kernel host launches
2
+ // node --inspect=127.0.0.1:0 runtime/bootstrap.mjs
3
+ // deno run -A --inspect=127.0.0.1:0 runtime/bootstrap.mjs
4
+ // then drives it over CDP. Everything the host needs from inside the
5
+ // runtime lives on globalThis.__cdpk; output to the host goes through the
6
+ // `__cdpk_send` CDP binding (Runtime.addBinding) plus plain stdout/stderr.
7
+ //
8
+ // Plain JavaScript on purpose: the same file must load unmodified in Node
9
+ // and in Deno.
10
+
11
+ import { createJupyter } from "./display.mjs";
12
+
13
+ const isDeno = typeof globalThis.Deno?.version?.deno === "string";
14
+ const ENC = new TextEncoder();
15
+
16
+ const cwd = isDeno ? Deno.cwd() : process.cwd();
17
+ const cwdUrl = new URL(`file://${cwd.endsWith("/") ? cwd : cwd + "/"}`);
18
+ // Stand-in referrer for cell code, like Deno's ./$deno$jupyter_repl.mts.
19
+ const replUrl = new URL("$cdpk$repl.mjs", cwdUrl);
20
+
21
+ const { inspect, readFile, writeOut, writeErr, readStdinLine } = await platform();
22
+
23
+ function base64(bytes) {
24
+ let binary = "";
25
+ for (let i = 0; i < bytes.length; i += 0x8000) {
26
+ binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
27
+ }
28
+ return btoa(binary);
29
+ }
30
+
31
+ // --- Output ordering ----------------------------------------------------
32
+ // stdout/stderr reach the host over pipes; display messages over the
33
+ // WebSocket. To keep them in order, every binding message is preceded by a
34
+ // numbered marker written to both pipes. The host holds the message until
35
+ // both pipes have delivered that marker, and a gated marker ("m") holds
36
+ // later pipe output until the message has gone out.
37
+ let barrierSeq = 0;
38
+ function barrier(gated = false) {
39
+ const n = ++barrierSeq;
40
+ const marker = `\x1b[cdpk:${n}${gated ? "m" : ""}]\x1b`;
41
+ writeOut(marker);
42
+ writeErr(marker);
43
+ return n;
44
+ }
45
+
46
+ function send(payload) {
47
+ const binding = globalThis.__cdpk_send;
48
+ if (typeof binding !== "function") return; // host not attached yet
49
+ binding(JSON.stringify({ ...payload, barrier: barrier(true) }));
50
+ }
51
+
52
+ const jupyter = createJupyter({
53
+ inspect: (value) => inspect(value),
54
+ base64,
55
+ readFile,
56
+ send(msgType, content, metadata, buffers) {
57
+ send({ kind: "iopub", msg_type: msgType, content, metadata, buffers: buffers.map(base64) });
58
+ },
59
+ // Synchronous like Deno's op_jupyter_input: ask the host, then block on
60
+ // stdin for its JSON-line reply.
61
+ input(prompt, password) {
62
+ send({ kind: "input_request", prompt, password });
63
+ const line = readStdinLine();
64
+ if (line === null) return null;
65
+ const reply = JSON.parse(line);
66
+ return typeof reply.value === "string" ? reply.value : null;
67
+ },
68
+ });
69
+
70
+ // --- Module loading -------------------------------------------------------
71
+ function resolveRelative(specifier) {
72
+ return /^\.{0,2}\//.test(specifier) ? new URL(specifier, replUrl).href : specifier;
73
+ }
74
+
75
+ if (!isDeno) {
76
+ // Deno resolves npm:/jsr: natively; Node gets a resolve hook that adds
77
+ // them and re-parents cell imports to the notebook's directory.
78
+ const { installNodeResolveHooks } = await import("./node-hooks.mjs");
79
+ installNodeResolveHooks({ shimUrl: import.meta.url, replUrl: replUrl.href });
80
+ }
81
+
82
+ globalThis.__cdpk = {
83
+ // Cell code awaits this first; see PRELUDE in src/transform.ts.
84
+ tick: () => new Promise((resolve) => setTimeout(resolve, 0)),
85
+ // Node: the resolve hook re-parents these imports. Deno would resolve
86
+ // relative paths against this file, so do it here.
87
+ import: (specifier, options) => import(isDeno ? resolveRelative(specifier) : specifier, options),
88
+ meta: {
89
+ url: replUrl.href,
90
+ dirname: cwd,
91
+ filename: replUrl.pathname,
92
+ resolve: (specifier) => new URL(specifier, replUrl).href,
93
+ },
94
+ barrier,
95
+ broadcastResult: jupyter.broadcastResult,
96
+ describeError(value) {
97
+ if (value instanceof Error) {
98
+ return JSON.stringify({ name: value.name, message: value.message, stack: value.stack ?? "" });
99
+ }
100
+ return JSON.stringify({ name: "Error", message: inspect(value), stack: "" });
101
+ },
102
+ };
103
+ Object.defineProperty(globalThis, "__cdpk", { enumerable: false });
104
+
105
+ globalThis.jupyter = jupyter.namespace;
106
+ globalThis.prompt = jupyter.prompt;
107
+ globalThis.confirm = jupyter.confirm;
108
+ if (isDeno) {
109
+ // Existing Deno notebooks use Deno.jupyter, but outside `deno jupyter`
110
+ // that is a non-configurable getter that throws. The global binding
111
+ // itself is configurable, so swap in a proxy that answers `jupyter` and
112
+ // forwards everything else to the real namespace.
113
+ const real = globalThis.Deno;
114
+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, "Deno");
115
+ const proxy = new Proxy(real, {
116
+ get: (target, key) => (key === "jupyter" ? jupyter.namespace : Reflect.get(target, key, target)),
117
+ });
118
+ Object.defineProperty(globalThis, "Deno", { ...descriptor, value: proxy });
119
+ } else if (process.env.CDPK_DENO_COMPAT === "1" && globalThis.Deno === undefined) {
120
+ // Opt-in (install --deno-compat): a Deno namespace for notebooks written
121
+ // against `deno jupyter`. See runtime/deno-compat.mjs for the trade-off.
122
+ // Absolute URL: the resolve hook re-parents this module's relative
123
+ // imports to the notebook directory.
124
+ const { createDenoShim } = await import(new URL("./deno-compat.mjs", import.meta.url).href);
125
+ globalThis.Deno = createDenoShim({ jupyter: jupyter.namespace });
126
+ }
127
+
128
+ // An error thrown later by a cell's timer or promise must not kill the kernel.
129
+ function reportUncaught(kind, err) {
130
+ writeErr(`${kind}: ${err instanceof Error ? (err.stack ?? err.message) : inspect(err)}\n`);
131
+ }
132
+ if (isDeno) {
133
+ globalThis.addEventListener("unhandledrejection", (e) => {
134
+ e.preventDefault();
135
+ reportUncaught("Uncaught (in promise)", e.reason);
136
+ });
137
+ globalThis.addEventListener("error", (e) => {
138
+ e.preventDefault();
139
+ reportUncaught("Uncaught", e.error);
140
+ });
141
+ } else {
142
+ process.on("unhandledRejection", (err) => reportUncaught("Uncaught (in promise)", err));
143
+ process.on("uncaughtException", (err) => reportUncaught("Uncaught", err));
144
+ }
145
+
146
+ // Keep the event loop alive between cells.
147
+ setInterval(() => {}, 1 << 30);
148
+
149
+ async function platform() {
150
+ if (isDeno) {
151
+ const stdinBuf = new Uint8Array(1);
152
+ return {
153
+ inspect: (v) => Deno.inspect(v, { colors: true, depth: 4 }),
154
+ readFile: (p) => Deno.readFileSync(p),
155
+ writeOut: (s) => Deno.stdout.writeSync(ENC.encode(s)),
156
+ writeErr: (s) => Deno.stderr.writeSync(ENC.encode(s)),
157
+ readStdinLine: () => readLine(() => (Deno.stdin.readSync(stdinBuf) ? stdinBuf[0] : null)),
158
+ };
159
+ }
160
+ const util = await import("node:util");
161
+ const fs = await import("node:fs");
162
+ const buf = Buffer.alloc(1);
163
+ return {
164
+ inspect: (v) => util.inspect(v, { colors: true, depth: 4 }),
165
+ readFile: (p) => fs.readFileSync(p),
166
+ writeOut: (s) => process.stdout.write(s),
167
+ writeErr: (s) => process.stderr.write(s),
168
+ readStdinLine: () =>
169
+ readLine(() => {
170
+ for (;;) {
171
+ try {
172
+ return fs.readSync(0, buf, 0, 1, null) === 1 ? buf[0] : null;
173
+ } catch (err) {
174
+ if (err.code !== "EAGAIN") throw err;
175
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
176
+ }
177
+ }
178
+ }),
179
+ };
180
+ }
181
+
182
+ function readLine(nextByte) {
183
+ const bytes = [];
184
+ for (;;) {
185
+ const byte = nextByte();
186
+ if (byte === null) return bytes.length ? new TextDecoder().decode(new Uint8Array(bytes)) : null;
187
+ if (byte === 0x0a) return new TextDecoder().decode(new Uint8Array(bytes));
188
+ bytes.push(byte);
189
+ }
190
+ }