@tlockney/cdp-kernel 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +259 -33
  2. package/dist/bun-target.d.ts +60 -0
  3. package/dist/bun-target.js +236 -0
  4. package/dist/celld-target.d.ts +39 -0
  5. package/dist/celld-target.js +274 -0
  6. package/dist/debugger.d.ts +42 -0
  7. package/dist/debugger.js +534 -0
  8. package/dist/history.d.ts +18 -0
  9. package/dist/history.js +45 -0
  10. package/dist/kernel.js +91 -15
  11. package/dist/main.js +8 -6
  12. package/dist/murmur2.d.ts +2 -0
  13. package/dist/murmur2.js +41 -0
  14. package/dist/package.d.ts +16 -2
  15. package/dist/package.js +28 -2
  16. package/dist/sourcemap.d.ts +22 -0
  17. package/dist/sourcemap.js +86 -0
  18. package/dist/target.d.ts +65 -7
  19. package/dist/target.js +57 -6
  20. package/dist/text.d.ts +6 -0
  21. package/dist/text.js +17 -0
  22. package/dist/transform.d.ts +16 -0
  23. package/dist/transform.js +173 -11
  24. package/dist/workerd-target.d.ts +125 -0
  25. package/dist/workerd-target.js +455 -0
  26. package/dist/workers-project.d.ts +27 -0
  27. package/dist/workers-project.js +195 -0
  28. package/dist/ws-client.d.ts +32 -0
  29. package/dist/ws-client.js +166 -0
  30. package/package.json +18 -4
  31. package/runtime/bootstrap.mjs +55 -5
  32. package/runtime/bun-channel.d.mts +3 -0
  33. package/runtime/bun-channel.mjs +58 -0
  34. package/runtime/deno-compat.mjs +3 -1
  35. package/runtime/display.d.mts +2 -0
  36. package/runtime/display.mjs +9 -1
  37. package/runtime/evaluator.d.mts +22 -0
  38. package/runtime/evaluator.mjs +91 -0
  39. package/runtime/introspect.d.mts +13 -0
  40. package/runtime/introspect.mjs +120 -0
  41. package/runtime/node-hooks.d.mts +29 -0
  42. package/runtime/node-hooks.mjs +160 -20
  43. package/runtime/workerd-worker.mjs +194 -0
@@ -0,0 +1,120 @@
1
+ // What inspect_request (#10) shows for a value: the text of a JupyterLab
2
+ // tooltip (Shift-Tab) or inspector panel. Shared by every target; the host
3
+ // decides how the value is found (src/target.ts: CDP's throwOnSideEffect;
4
+ // in-process targets: resolvePath, below).
5
+ //
6
+ // No imports: this file must load in every target. `inspect` (util.inspect
7
+ // or Deno.inspect) is passed in.
8
+
9
+ const PREVIEW_CHARS = 1500;
10
+ const SOURCE_CHARS = 4000;
11
+ const MAX_PROPERTIES = 40;
12
+
13
+ const IDENTIFIER_PATH = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
14
+
15
+ /**
16
+ * Looks up a dotted path such as "a.b.c" from globalThis, by property
17
+ * descriptor, so nothing runs: a getter along the way is reported rather
18
+ * than called. In-process targets (Bun, workerd, celld) have no way to
19
+ * evaluate without side effects, so inspection and completion use this.
20
+ */
21
+ export function resolvePath(path, root = globalThis) {
22
+ if (!IDENTIFIER_PATH.test(path)) return { found: false };
23
+ let value = root;
24
+ for (const name of path.split(".")) {
25
+ if (value === null || value === undefined) return { found: false };
26
+ let descriptor;
27
+ for (let o = Object(value); o !== null && descriptor === undefined; o = Object.getPrototypeOf(o)) {
28
+ descriptor = Object.getOwnPropertyDescriptor(o, name);
29
+ }
30
+ if (descriptor === undefined) return { found: false };
31
+ if (!("value" in descriptor)) return { found: true, accessor: true, name };
32
+ value = descriptor.value;
33
+ }
34
+ return { found: true, value };
35
+ }
36
+
37
+ function truncate(text, limit) {
38
+ return text.length > limit ? `${text.slice(0, limit)}… (${text.length - limit} more characters)` : text;
39
+ }
40
+
41
+ /** "function add(a, b) {...}" -> "add(a, b)"; "class K {...}" -> "class K". */
42
+ export function signatureOf(fn) {
43
+ let source;
44
+ try {
45
+ source = Function.prototype.toString.call(fn);
46
+ } catch {
47
+ return `${fn.name || "anonymous"}(…)`;
48
+ }
49
+ if (/^class\b/.test(source)) return `class ${fn.name || "(anonymous)"}`;
50
+ const params = /^[^(]*\(([^)]*)\)/.exec(source)?.[1];
51
+ const native = /\{\s*\[native code\]\s*\}$/.test(source);
52
+ // One line, however the parameters were spread over the source.
53
+ const written = (params ?? "").replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim();
54
+ const args = native ? Array.from({ length: fn.length }, (_, i) => `arg${i}`).join(", ") : written;
55
+ const arrow = !/^(?:async\s+)?function\b/.test(source) && /=>/.test(source.split("{")[0] ?? "");
56
+ const name = fn.name || (arrow ? "(arrow function)" : "anonymous");
57
+ const prefix = /^async\b/.test(source) ? "async " : "";
58
+ return `${prefix}${name}(${args})`;
59
+ }
60
+
61
+ function typeOf(value) {
62
+ if (value === null) return "null";
63
+ if (typeof value !== "object" && typeof value !== "function") return typeof value;
64
+ const name = Object.getPrototypeOf(value)?.constructor?.name;
65
+ if (typeof value === "function") return /^class\b/.test(safeSource(value)) ? "class" : "function";
66
+ return name && name !== "Object" ? `object (${name})` : "object";
67
+ }
68
+
69
+ function safeSource(fn) {
70
+ try {
71
+ return Function.prototype.toString.call(fn);
72
+ } catch {
73
+ return "";
74
+ }
75
+ }
76
+
77
+ function propertyNames(value) {
78
+ if ((typeof value !== "object" && typeof value !== "function") || value === null) return [];
79
+ try {
80
+ // A function's own internals aren't worth listing; older V8 (Node 24)
81
+ // also gives sloppy-mode functions own `arguments` and `caller`.
82
+ const internal = ["length", "name", "prototype", "arguments", "caller"];
83
+ return Object.getOwnPropertyNames(value).filter((n) => !(typeof value === "function" && internal.includes(n)));
84
+ } catch {
85
+ return [];
86
+ }
87
+ }
88
+
89
+ /**
90
+ * The tooltip text for a value. `detail` 1 (Shift-Tab twice, or the
91
+ * inspector panel) adds a function's source.
92
+ */
93
+ export function describeValue(value, { inspect, detail = 0 }) {
94
+ const lines = [`Type: ${typeOf(value)}`];
95
+ if (typeof value === "function") lines.push(`Signature: ${signatureOf(value)}`);
96
+ let preview;
97
+ try {
98
+ preview = inspect(value);
99
+ } catch (err) {
100
+ preview = `(inspect failed: ${err instanceof Error ? err.message : String(err)})`;
101
+ }
102
+ lines.push(`Value: ${truncate(preview, PREVIEW_CHARS)}`);
103
+ const names = propertyNames(value);
104
+ if (names.length > 0) {
105
+ const shown = names.slice(0, MAX_PROPERTIES).join(", ");
106
+ lines.push(`Properties: ${shown}${names.length > MAX_PROPERTIES ? `, … (${names.length - MAX_PROPERTIES} more)` : ""}`);
107
+ }
108
+ if (detail > 0 && typeof value === "function") lines.push(`Source:\n${truncate(safeSource(value), SOURCE_CHARS)}`);
109
+ return lines.join("\n");
110
+ }
111
+
112
+ /** inspect_request for in-process targets: resolvePath, then describeValue. */
113
+ export function inspectPath(path, { inspect, detail = 0, root = globalThis }) {
114
+ const resolved = resolvePath(path, root);
115
+ if (!resolved.found) return null;
116
+ if (resolved.accessor) {
117
+ return `Type: accessor property\n${resolved.name} has a getter, which isn't called for inspection: it could have side effects.`;
118
+ }
119
+ return describeValue(resolved.value, { inspect, detail });
120
+ }
@@ -7,4 +7,33 @@ export interface PackageSpecifier {
7
7
  export function parsePackageSpecifier(spec: string): PackageSpecifier;
8
8
  export function jsrToNpm(spec: string): PackageSpecifier;
9
9
  export function aliasFor(name: string, range: string): string;
10
+ export type Installer = "npm" | "bun";
11
+ export function installCommand(
12
+ installer: Installer,
13
+ alias: string,
14
+ name: string,
15
+ range: string,
16
+ ): { cmd: string; args: string[]; env?: { [name: string]: string } };
17
+ /** Runs an install; `env` is added to the process environment. */
18
+ export type InstallRunner = (
19
+ cmd: string,
20
+ args: string[],
21
+ cwd: string,
22
+ env?: { [name: string]: string },
23
+ ) => { status: number | null; stderr?: string; error?: Error };
24
+ export function ensureInstalled(
25
+ pkg: { name: string; range: string },
26
+ installer?: Installer,
27
+ options?: { lockFile?: string | null; run?: InstallRunner },
28
+ ): { dir: string; alias: string };
29
+
30
+ export const LOCK_FILE: string;
31
+ export interface Lock {
32
+ version: 1;
33
+ packages: { [nameAtRange: string]: string };
34
+ }
35
+ export function findLockFile(dir: string): string | null;
36
+ export function readLock(path: string): Lock;
37
+ export function writeLock(path: string, lock: Lock): void;
38
+ export function withInstallLock<T>(dir: string, fn: () => T, options?: { timeoutMs?: number }): T;
10
39
  export function installNodeResolveHooks(urls: { shimUrl: string; replUrl: string }): void;
@@ -1,3 +1,4 @@
1
+ // @ts-self-types="./node-hooks.d.mts"
1
2
  // Node-only module resolution for cell code: the part of Deno's module
2
3
  // loader a notebook leans on most.
3
4
  // - Imports issued by cell code arrive with the bootstrap module as their
@@ -11,13 +12,15 @@
11
12
  // Uses module.registerHooks (synchronous, in-thread; Node >= 23.5), so the
12
13
  // install can run with spawnSync inside resolve.
13
14
 
14
- import { registerHooks } from "node:module";
15
+ // Not a named import: Bun, which loads this file for its npm:/jsr: helpers,
16
+ // has no registerHooks, and a missing named export fails at link time.
17
+ import module from "node:module";
15
18
  import { spawnSync } from "node:child_process";
16
19
  import { createHash } from "node:crypto";
17
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
20
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
18
21
  import { homedir } from "node:os";
19
- import { join } from "node:path";
20
- import { pathToFileURL } from "node:url";
22
+ import { dirname, join } from "node:path";
23
+ import { fileURLToPath, pathToFileURL } from "node:url";
21
24
 
22
25
  const SPEC_RE = /^((?:@[^/@]+\/)?[^/@]+)(?:@([^/]+))?(\/.*)?$/;
23
26
 
@@ -41,8 +44,12 @@ export function aliasFor(name, range) {
41
44
  return `cdpk-${digest}`;
42
45
  }
43
46
 
44
- function cacheDir() {
45
- const dir = process.env.CDPK_CACHE_DIR ?? join(homedir(), ".cache", "cdp-kernel", "npm");
47
+ // Each installer keeps its own cache, so npm's and Bun's lockfiles never
48
+ // meet; both read the @jsr registry from .npmrc.
49
+ function cacheDir(installer) {
50
+ const dir = installer === "npm"
51
+ ? process.env.CDPK_CACHE_DIR ?? join(homedir(), ".cache", "cdp-kernel", "npm")
52
+ : join(homedir(), ".cache", "cdp-kernel", installer);
46
53
  if (!existsSync(join(dir, "package.json"))) {
47
54
  mkdirSync(dir, { recursive: true });
48
55
  writeFileSync(join(dir, "package.json"), '{ "private": true }\n');
@@ -51,30 +58,163 @@ function cacheDir() {
51
58
  return dir;
52
59
  }
53
60
 
54
- function ensureInstalled({ name, range }) {
55
- const dir = cacheDir();
56
- const alias = aliasFor(name, range);
57
- if (!existsSync(join(dir, "node_modules", alias, "package.json"))) {
58
- const result = spawnSync(
59
- "npm",
60
- ["install", "--no-audit", "--no-fund", "--loglevel=error", `${alias}@npm:${name}@${range}`],
61
- { cwd: dir, encoding: "utf8" },
62
- );
63
- if (result.status !== 0) {
64
- throw new Error(`npm install ${name}@${range} failed:\n${result.stderr || result.error}`);
61
+ /** The command that installs name@range under `alias` with `installer`. */
62
+ // - Saved exactly (#40): npm records `npm:react@19.0.0` as `^19.0.0` by
63
+ // default, and every later install into the shared cache re-resolves the
64
+ // ranges, so pinned packages drifted (react 19.3.1 beside react-dom
65
+ // 19.0.0). With an exact save, a version stays put and a range is frozen
66
+ // at what it first resolved to. Bun already saves exactly; the flag says so.
67
+ // - JSR packages come as tarballs from npm.jsr.io, which npm 12 refuses as
68
+ // "remote" unless allowed (#39). `root` is the narrowest value that works
69
+ // (`true`, which looks right, is invalid and warns), and only the JSR
70
+ // install gets it.
71
+ export function installCommand(installer, alias, name, range) {
72
+ const spec = `${alias}@npm:${name}@${range}`;
73
+ // The Bun target runs this inside Bun, so it uses its own binary.
74
+ if (installer === "bun") return { cmd: process.execPath, args: ["add", "--silent", "--exact", spec] };
75
+ const args = ["install", "--no-audit", "--no-fund", "--loglevel=error", "--save-exact", spec];
76
+ return name.startsWith("@jsr/") ? { cmd: "npm", args, env: { npm_config_allow_remote: "root" } } : { cmd: "npm", args };
77
+ }
78
+
79
+ // --- Lockfile (#14) -----------------------------------------------------------
80
+ // A cdp-kernel.lock.json at or above the notebook pins each npm:/jsr:
81
+ // request to the exact version it first resolved to, so the same notebook
82
+ // gets the same packages on another machine. Opt-in: create the file (with
83
+ // `{}` in it) to start locking; the kernel then fills it in.
84
+
85
+ export const LOCK_FILE = "cdp-kernel.lock.json";
86
+
87
+ /** The nearest cdp-kernel.lock.json at or above `dir`, or null. */
88
+ export function findLockFile(dir) {
89
+ for (let at = dir;; at = dirname(at)) {
90
+ if (existsSync(join(at, LOCK_FILE))) return join(at, LOCK_FILE);
91
+ if (dirname(at) === at) return null;
92
+ }
93
+ }
94
+
95
+ /** `{ version: 1, packages: { "name@range": "exact version" } }`; `{}` counts as empty. */
96
+ export function readLock(path) {
97
+ const parsed = JSON.parse(readFileSync(path, "utf8") || "{}");
98
+ const packages = parsed && typeof parsed.packages === "object" && parsed.packages !== null ? parsed.packages : {};
99
+ return { version: 1, packages };
100
+ }
101
+
102
+ /** Sorted, so a lockfile's diff shows only what changed. */
103
+ export function writeLock(path, lock) {
104
+ const packages = Object.fromEntries(Object.entries(lock.packages).sort(([a], [b]) => a.localeCompare(b)));
105
+ writeFileSync(path, JSON.stringify({ version: 1, packages }, null, 2) + "\n");
106
+ }
107
+
108
+ // --- Serialized installs (#14) -------------------------------------------------
109
+ // Kernels share one cache per installer, and npm/bun both rewrite its
110
+ // package.json and node_modules, so installs take a lock: a directory, which
111
+ // mkdir creates atomically. A lock left by a process that died is taken over.
112
+
113
+ const LOCK_DIR = ".install-lock";
114
+ const STALE_MS = 10 * 60 * 1000;
115
+
116
+ function sleep(ms) {
117
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
118
+ }
119
+
120
+ function holderAlive(lockDir) {
121
+ try {
122
+ const pid = Number(readFileSync(join(lockDir, "pid"), "utf8"));
123
+ if (!Number.isInteger(pid) || pid <= 0) return false;
124
+ process.kill(pid, 0);
125
+ return true;
126
+ } catch (err) {
127
+ // EPERM: alive, but someone else's.
128
+ return err?.code === "EPERM";
129
+ }
130
+ }
131
+
132
+ /** Runs `fn` holding the cache's install lock. Synchronous, like the resolve hook. */
133
+ export function withInstallLock(dir, fn, { timeoutMs = STALE_MS } = {}) {
134
+ const lockDir = join(dir, LOCK_DIR);
135
+ const deadline = Date.now() + timeoutMs;
136
+ for (;;) {
137
+ try {
138
+ mkdirSync(lockDir);
139
+ writeFileSync(join(lockDir, "pid"), String(process.pid));
140
+ break;
141
+ } catch (err) {
142
+ if (err?.code !== "EEXIST") throw err;
143
+ let age = 0;
144
+ try {
145
+ age = Date.now() - statSync(lockDir).mtimeMs;
146
+ } catch {
147
+ continue; // released meanwhile
148
+ }
149
+ // A holder that died, or one stuck for longer than any install takes.
150
+ if ((age > 1000 && !holderAlive(lockDir)) || age > STALE_MS) {
151
+ rmSync(lockDir, { recursive: true, force: true });
152
+ continue;
153
+ }
154
+ if (Date.now() > deadline) throw new Error(`timed out waiting for the install lock ${lockDir}`);
155
+ sleep(100);
65
156
  }
66
157
  }
67
- return { dir, alias };
158
+ try {
159
+ return fn();
160
+ } finally {
161
+ rmSync(lockDir, { recursive: true, force: true });
162
+ }
163
+ }
164
+
165
+ function installedVersion(dir, alias) {
166
+ try {
167
+ return JSON.parse(readFileSync(join(dir, "node_modules", alias, "package.json"), "utf8")).version ?? null;
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ function spawnInstall(cmd, args, cwd, env = {}) {
174
+ return spawnSync(cmd, args, { cwd, encoding: "utf8", env: { ...process.env, ...env } });
175
+ }
176
+
177
+ /**
178
+ * Installs a package into the installer's cache unless it's there already.
179
+ * With a lockfile, a locked request installs its exact version, and an
180
+ * unlocked one is recorded once installed. `run` is the install command
181
+ * runner, replaceable in tests.
182
+ */
183
+ export function ensureInstalled({ name, range }, installer = "npm", { lockFile = null, run = spawnInstall } = {}) {
184
+ const dir = cacheDir(installer);
185
+ const key = `${name}@${range}`;
186
+ return withInstallLock(dir, () => {
187
+ const lock = lockFile ? readLock(lockFile) : null;
188
+ const locked = lock?.packages[key];
189
+ const version = typeof locked === "string" ? locked : range;
190
+ const alias = aliasFor(name, version);
191
+ if (installedVersion(dir, alias) === null) {
192
+ const { cmd, args, env } = installCommand(installer, alias, name, version);
193
+ const result = run(cmd, args, dir, env);
194
+ if (result.status !== 0) {
195
+ throw new Error(`${installer} install ${name}@${version} failed:\n${result.stderr || result.error}`);
196
+ }
197
+ }
198
+ if (lock && typeof locked !== "string") {
199
+ const resolved = installedVersion(dir, alias);
200
+ if (resolved !== null) {
201
+ lock.packages[key] = resolved;
202
+ writeLock(lockFile, lock);
203
+ }
204
+ }
205
+ return { dir, alias };
206
+ });
68
207
  }
69
208
 
70
209
  export function installNodeResolveHooks({ shimUrl, replUrl }) {
71
- registerHooks({
210
+ module.registerHooks({
72
211
  resolve(specifier, context, nextResolve) {
73
212
  let pkg = null;
74
213
  if (specifier.startsWith("npm:")) pkg = parsePackageSpecifier(specifier.slice(4));
75
214
  else if (specifier.startsWith("jsr:")) pkg = jsrToNpm(specifier.slice(4));
76
215
  if (pkg) {
77
- const { dir, alias } = ensureInstalled(pkg);
216
+ const lockFile = findLockFile(fileURLToPath(new URL(".", replUrl)));
217
+ const { dir, alias } = ensureInstalled(pkg, "npm", { lockFile });
78
218
  const parentURL = pathToFileURL(join(dir, "package.json")).href;
79
219
  return nextResolve(alias + pkg.subpath, { ...context, parentURL });
80
220
  }
@@ -0,0 +1,194 @@
1
+ // The driver Worker for the workerd target (#8).
2
+ //
3
+ // workerd's inspector can evaluate code, but outside a request there is no
4
+ // IoContext: timers, fetch and crypto.randomUUID throw "Disallowed operation
5
+ // called within global scope". So the host opens one WebSocket session to
6
+ // this Worker (over a Unix socket only it can reach, or through a project's
7
+ // Miniflare) and every request runs inside that session's request. The cell
8
+ // code itself comes from transformCellForEval and is run by the UnsafeEval
9
+ // binding as global code, so its declarations persist (see evaluator.mjs).
10
+ //
11
+ // Console output, iopub messages and replies all go back over the one
12
+ // socket, in order, so no barrier markers are needed.
13
+ //
14
+ // workerd can't load modules at runtime beyond its builtins, so the host
15
+ // bundles anything else a cell imports (src/workerd-target.ts) and sends it
16
+ // with the cell.
17
+
18
+ import { formatWithOptions, inspect } from "node:util";
19
+ import { createJupyter } from "./display.mjs";
20
+ import { createHandler } from "./evaluator.mjs";
21
+ import { describeValue, inspectPath, resolvePath } from "./introspect.mjs";
22
+
23
+ const BUILTIN = /^(?:node|cloudflare|workerd):/;
24
+
25
+ function base64(bytes) {
26
+ let binary = "";
27
+ for (let i = 0; i < bytes.length; i += 0x8000) {
28
+ binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
29
+ }
30
+ return btoa(binary);
31
+ }
32
+
33
+ // --- Output -----------------------------------------------------------------
34
+ // Lines go to the host's session. Anything emitted before it connects waits.
35
+ let sink = null;
36
+ const outbox = [];
37
+
38
+ function emit(message) {
39
+ const line = JSON.stringify(message);
40
+ if (sink) sink(line);
41
+ else outbox.push(line);
42
+ }
43
+
44
+ const stream = (name) => (...args) => emit({ kind: "stream", name, text: formatWithOptions({ colors: true }, ...args) + "\n" });
45
+ Object.assign(console, {
46
+ log: stream("stdout"),
47
+ info: stream("stdout"),
48
+ debug: stream("stdout"),
49
+ dir: (value) => emit({ kind: "stream", name: "stdout", text: inspect(value, { colors: true, depth: 4 }) + "\n" }),
50
+ warn: stream("stderr"),
51
+ error: stream("stderr"),
52
+ });
53
+
54
+ const jupyter = createJupyter({
55
+ inspect: (value) => inspect(value, { colors: true, depth: 4 }),
56
+ base64,
57
+ readFile() {
58
+ throw new Error("workerd has no file system to read from; pass the image bytes instead");
59
+ },
60
+ send(msgType, content, metadata, buffers) {
61
+ emit({ kind: "iopub", msg_type: msgType, content, metadata, buffers: buffers.map(base64), barrier: 0 });
62
+ },
63
+ // A request can't block for an answer, so prompt() and confirm() behave
64
+ // as when the frontend has no stdin.
65
+ input: () => null,
66
+ });
67
+
68
+ // --- Modules ----------------------------------------------------------------
69
+ const modules = new Map();
70
+
71
+ /** `require` for a host-built bundle's external imports (builtins). */
72
+ async function requireFor(specifiers) {
73
+ const loaded = new Map();
74
+ for (const specifier of specifiers) {
75
+ // Packages require Node builtins by bare name too ("fs").
76
+ const ns = await import(BUILTIN.test(specifier) ? specifier : `node:${specifier}`);
77
+ loaded.set(specifier, ns.default ?? ns);
78
+ }
79
+ return (specifier) => {
80
+ if (!loaded.has(specifier)) throw new Error(`cannot require ${specifier} in workerd`);
81
+ return loaded.get(specifier);
82
+ };
83
+ }
84
+
85
+ function cellImport(specifier, options) {
86
+ if (BUILTIN.test(specifier)) return import(specifier, options);
87
+ if (modules.has(specifier)) return modules.get(specifier);
88
+ return Promise.reject(new TypeError(`Cannot import "${specifier}": the host didn't bundle it`));
89
+ }
90
+
91
+ // --- The shim cells and the evaluator use -----------------------------------
92
+ let handle = null;
93
+
94
+ function install(env) {
95
+ const meta = JSON.parse(env.META);
96
+ globalThis.__cdpk = {
97
+ import: cellImport,
98
+ meta: { ...meta, resolve: (specifier) => new URL(specifier, meta.url).href },
99
+ requireFor,
100
+ // inspect_request (#10): by descriptor, so nothing runs.
101
+ describeForInspect: (value, detail) => describeValue(value, { inspect: (v) => inspect(v, { colors: false, depth: 2 }), detail }),
102
+ inspectPath: (path, detail) => inspectPath(path, { inspect: (v) => inspect(v, { colors: false, depth: 2 }), detail }),
103
+ resolvePath,
104
+ barrier: () => 0,
105
+ broadcastResult: jupyter.broadcastResult,
106
+ async commDispatch(msgType, content, metadata, buffers64) {
107
+ const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
108
+ return await jupyter.commDispatch(msgType, content, metadata, buffers);
109
+ },
110
+ describeError(value) {
111
+ if (value instanceof Error) {
112
+ return JSON.stringify({ name: value.name, message: value.message, stack: value.stack ?? "" });
113
+ }
114
+ return JSON.stringify({ name: "Error", message: inspect(value), stack: "" });
115
+ },
116
+ };
117
+ Object.defineProperty(globalThis, "__cdpk", { enumerable: false });
118
+ globalThis.jupyter = jupyter.namespace;
119
+ globalThis.prompt = jupyter.prompt;
120
+ globalThis.confirm = jupyter.confirm;
121
+ // A cell has no fetch(request, env), so the Worker's bindings (#26) are a
122
+ // global, without the kernel's own.
123
+ const { UNSAFE, TOKEN, META, ...bindings } = env;
124
+ globalThis.env = bindings;
125
+ // workerd forbids eval without its UnsafeEval binding; celld (#25) allows
126
+ // a plain global eval and has no such binding.
127
+ const evaluate = env.UNSAFE ? (code) => env.UNSAFE.eval(code, "cell") : (code) => (0, eval)(code);
128
+ handle = createHandler(globalThis.__cdpk, evaluate);
129
+ return evaluate;
130
+ }
131
+
132
+ let evaluate = null;
133
+
134
+ /** Evaluates the bundles a cell brings, once each. */
135
+ async function loadModules(bundles = {}) {
136
+ for (const [specifier, script] of Object.entries(bundles)) {
137
+ if (modules.has(specifier)) continue;
138
+ const loading = Promise.resolve().then(() => evaluate(script));
139
+ // A module that failed to load is retried by the next import of it.
140
+ loading.catch(() => modules.delete(specifier));
141
+ modules.set(specifier, loading);
142
+ }
143
+ }
144
+
145
+ async function answer(req) {
146
+ if (req.op === "info") {
147
+ return {
148
+ v8: globalThis.process?.versions?.v8 ?? "",
149
+ bindings: Object.keys(globalThis.env),
150
+ // A Workers project's default export (src/workers-project.ts).
151
+ worker: globalThis.worker !== undefined,
152
+ };
153
+ }
154
+ if (req.op === "eval") await loadModules(req.modules);
155
+ return await handle(req);
156
+ }
157
+
158
+ export default {
159
+ async fetch(request, env) {
160
+ // A header, or the query string for a WebSocket client that can't set
161
+ // headers (celld's session uses the host's built-in WebSocket).
162
+ const token = request.headers.get("x-cdpk-token") ?? new URL(request.url).searchParams.get("token");
163
+ if (token !== env.TOKEN) return new Response("forbidden", { status: 403 });
164
+ if (request.headers.get("upgrade") !== "websocket") return new Response("expected a WebSocket", { status: 426 });
165
+ evaluate ??= install(env);
166
+
167
+ // The session: every request from the host arrives as a message, and
168
+ // all of them run in this one request's IoContext. So I/O objects (a
169
+ // Durable Object stub, a stream) made in one cell work in the next, and
170
+ // work a cell leaves running outlives it (#26).
171
+ const { 0: client, 1: server } = new WebSocketPair();
172
+ server.accept();
173
+ const send = (line) => {
174
+ try {
175
+ server.send(line);
176
+ } catch {
177
+ // The host is gone.
178
+ }
179
+ };
180
+ sink = send;
181
+ for (const line of outbox.splice(0)) send(line);
182
+ server.addEventListener("message", (event) => {
183
+ const req = JSON.parse(event.data);
184
+ answer(req).then(
185
+ (reply) => send(JSON.stringify({ kind: "reply", id: req.id, ...reply })),
186
+ (err) => send(JSON.stringify({ kind: "reply", id: req.id, failure: String(err?.stack ?? err) })),
187
+ );
188
+ });
189
+ server.addEventListener("close", () => {
190
+ if (sink === send) sink = null;
191
+ });
192
+ return new Response(null, { status: 101, webSocket: client });
193
+ },
194
+ };