@tlockney/cdp-kernel 0.3.0 → 0.5.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 (56) hide show
  1. package/README.md +60 -404
  2. package/THIRD-PARTY-NOTICES +62 -0
  3. package/dist/bun-target.d.ts +4 -2
  4. package/dist/bun-target.js +35 -8
  5. package/dist/cdp.d.ts +6 -1
  6. package/dist/cdp.js +25 -10
  7. package/dist/celld-target.d.ts +1 -1
  8. package/dist/celld-target.js +46 -8
  9. package/dist/children.d.ts +7 -0
  10. package/dist/children.js +32 -0
  11. package/dist/debugger.d.ts +42 -0
  12. package/dist/debugger.js +545 -0
  13. package/dist/globals.d.ts +3 -3
  14. package/dist/globals.js +3 -3
  15. package/dist/history.d.ts +18 -0
  16. package/dist/history.js +45 -0
  17. package/dist/kernel.js +211 -32
  18. package/dist/main.js +3 -1
  19. package/dist/mod.d.ts +5 -3
  20. package/dist/mod.js +5 -3
  21. package/dist/murmur2.d.ts +2 -0
  22. package/dist/murmur2.js +41 -0
  23. package/dist/package.d.ts +16 -2
  24. package/dist/package.js +26 -3
  25. package/dist/sourcemap.d.ts +22 -0
  26. package/dist/sourcemap.js +86 -0
  27. package/dist/target.d.ts +39 -3
  28. package/dist/target.js +281 -30
  29. package/dist/text.d.ts +11 -1
  30. package/dist/text.js +72 -2
  31. package/dist/transform.d.ts +19 -0
  32. package/dist/transform.js +74 -13
  33. package/dist/wire.d.ts +8 -1
  34. package/dist/wire.js +3 -1
  35. package/dist/workerd-target.d.ts +9 -1
  36. package/dist/workerd-target.js +108 -9
  37. package/dist/ws-client.d.ts +5 -1
  38. package/dist/ws-client.js +46 -13
  39. package/dist/zmtp.d.ts +7 -1
  40. package/dist/zmtp.js +30 -1
  41. package/package.json +10 -4
  42. package/runtime/bootstrap.mjs +138 -12
  43. package/runtime/bun-channel.d.mts +3 -0
  44. package/runtime/bun-channel.mjs +2 -2
  45. package/runtime/comm.mjs +11 -1
  46. package/runtime/deno-compat.d.mts +2 -0
  47. package/runtime/deno-compat.mjs +17 -2
  48. package/runtime/display.d.mts +2 -0
  49. package/runtime/display.mjs +17 -2
  50. package/runtime/evaluator.d.mts +4 -0
  51. package/runtime/evaluator.mjs +8 -0
  52. package/runtime/introspect.d.mts +13 -0
  53. package/runtime/introspect.mjs +120 -0
  54. package/runtime/node-hooks.d.mts +61 -1
  55. package/runtime/node-hooks.mjs +354 -21
  56. package/runtime/workerd-worker.mjs +82 -6
package/dist/text.js CHANGED
@@ -2,7 +2,53 @@
2
2
  //
3
3
  // Ported from Deno's cli/js/jupyter_kernel.js (v2.9.6), which is
4
4
  // Copyright 2018-2026 the Deno authors, MIT license.
5
- /** Bracket/quote/comment scanner deciding whether a cell is finished. */
5
+ // Keywords after which a `/` starts a regex literal rather than dividing.
6
+ const REGEX_AFTER_WORD = new Set([
7
+ "return", "typeof", "instanceof", "in", "of", "new", "delete", "void", "throw",
8
+ "case", "do", "else", "yield", "await",
9
+ ]);
10
+ /**
11
+ * Whether a `/` at `i` (not starting a comment) begins a regex literal: the
12
+ * usual heuristic, true unless the previous token can end an operand (a
13
+ * name, number, closing bracket or string). It can't tell `a++ / b` or a
14
+ * block's `} /re/` apart; those are rare enough in a console.
15
+ */
16
+ function startsRegex(code, i) {
17
+ let j = i - 1;
18
+ while (j >= 0 && /\s/.test(code[j]))
19
+ j--;
20
+ if (j < 0)
21
+ return true;
22
+ const prev = code[j];
23
+ if (/[\w$]/.test(prev)) {
24
+ const word = /[\w$]+$/.exec(code.slice(0, j + 1))?.[0] ?? "";
25
+ return REGEX_AFTER_WORD.has(word);
26
+ }
27
+ return !/[)\]}'"`]/.test(prev);
28
+ }
29
+ /** The index just past a regex literal starting at `i`, or -1 if it doesn't end on its line. */
30
+ function regexEnd(code, i) {
31
+ let inClass = false;
32
+ for (let j = i + 1; j < code.length; j++) {
33
+ const c = code[j];
34
+ if (c === "\n")
35
+ return -1;
36
+ if (c === "\\")
37
+ j++;
38
+ else if (c === "[")
39
+ inClass = true;
40
+ else if (c === "]")
41
+ inClass = false;
42
+ else if (c === "/" && !inClass)
43
+ return j + 1;
44
+ }
45
+ return -1;
46
+ }
47
+ /**
48
+ * Bracket/quote/comment scanner deciding whether a cell is finished. Regex
49
+ * literals are recognised heuristically (see startsRegex), and a template's
50
+ * `${...}` is skipped like the rest of the template.
51
+ */
6
52
  export function checkIsComplete(code) {
7
53
  const stack = [];
8
54
  const closers = { ")": "(", "]": "[", "}": "{" };
@@ -21,11 +67,18 @@ export function checkIsComplete(code) {
21
67
  i = end + 2;
22
68
  continue;
23
69
  }
70
+ if (ch === "/" && startsRegex(code, i)) {
71
+ const end = regexEnd(code, i);
72
+ if (end !== -1) {
73
+ i = end;
74
+ continue;
75
+ }
76
+ }
24
77
  if (ch === "'" || ch === '"' || ch === "`") {
25
78
  i++;
26
79
  let closed = false;
27
80
  while (i < code.length) {
28
- if (code[i] === "\\" && ch !== "`") {
81
+ if (code[i] === "\\") {
29
82
  i += 2;
30
83
  continue;
31
84
  }
@@ -51,6 +104,23 @@ export function exprAtCursor(code, cursor) {
51
104
  const m = /[\w$.]+$/.exec(code.slice(0, cursor));
52
105
  return m ? m[0] : "";
53
106
  }
107
+ /**
108
+ * The dotted name an inspect_request means (#10): the one the cursor is in
109
+ * or just after, or, right after "name(" while typing a call, that callee.
110
+ * Null when it isn't a plain path; nothing else is evaluated.
111
+ */
112
+ export function inspectTarget(code, cursor) {
113
+ let end = cursor;
114
+ // Inside a call's parentheses with nothing typed yet: "add(|".
115
+ const before = code.slice(0, cursor);
116
+ const call = /([\w$.]+)\(\s*$/.exec(before);
117
+ if (call)
118
+ end = cursor - (before.length - before.lastIndexOf("(", cursor));
119
+ const head = /[\w$.]*$/.exec(code.slice(0, end))?.[0] ?? "";
120
+ const tail = call ? "" : (/^[\w$]*/.exec(code.slice(end))?.[0] ?? "");
121
+ const path = (head + tail).replace(/\.$/, "");
122
+ return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(path) ? path : null;
123
+ }
54
124
  // Jupyter measures cursor_pos in Unicode codepoints; JS strings index UTF-16
55
125
  // code units (denoland/deno#22771).
56
126
  export function codePointToUtf16Index(str, cpOffset) {
@@ -1,4 +1,14 @@
1
+ import { LineMap } from "./sourcemap.ts";
1
2
  export declare const SHIM = "__cdpk";
3
+ /**
4
+ * Each replMode cell also ends by yielding one macrotask, so the script
5
+ * settles with no other microtasks queued ahead of the inspector's
6
+ * then-callback. Until that callback runs, the inspector holds the result
7
+ * promise only weakly, and a GC in between fails the evaluate with "Promise
8
+ * was collected" (#55). A declaration has no completion value, so the
9
+ * cell's value is unchanged; replMode lets `let` be declared again.
10
+ */
11
+ export declare const SETTLE = "\n;let __cdpk_settled = await __cdpk.tick();";
2
12
  export declare class CellSyntaxError extends Error {
3
13
  name: string;
4
14
  }
@@ -23,6 +33,15 @@ export interface CellTransformOptions {
23
33
  /** Rewrites module-only syntax in plain JavaScript into script syntax. */
24
34
  export declare function rewriteModuleSyntax(js: string): string;
25
35
  export declare function transformCell(code: string, options?: CellTransformOptions): string;
36
+ /**
37
+ * transformCell, plus the map between the cell's lines and the script's,
38
+ * for the debugger (#18). The rewrite keeps lines (see applyEdits), so the
39
+ * script's lines are esbuild's, after the prelude's one.
40
+ */
41
+ export declare function transformCellWithLines(code: string, options?: CellTransformOptions): {
42
+ script: string;
43
+ lines: LineMap;
44
+ };
26
45
  /**
27
46
  * Like transformCell, for a target that evaluates cells itself, as global
28
47
  * code (an indirect eval), instead of through the inspector's replMode: see
package/dist/transform.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // TypeScript/JSX part and acorn finds the module syntax that a script can't
7
7
  // contain:
8
8
  // import x, { y as z } from "m" -> var __cdpk_m0 = await __cdpk.import("m");
9
- // var x = __cdpk_m0.default, z = __cdpk_m0.y;
9
+ // var x = __cdpk_m0["default"], z = __cdpk_m0["y"];
10
10
  // import("m") -> __cdpk.import("m")
11
11
  // import.meta -> __cdpk.meta
12
12
  // export const a = 1 -> const a = 1
@@ -17,6 +17,7 @@
17
17
  import { transformSync } from "esbuild";
18
18
  import { parse } from "acorn";
19
19
  import { full, recursive } from "acorn-walk";
20
+ import { LineMap } from "./sourcemap.js";
20
21
  export const SHIM = "__cdpk";
21
22
  // Each cell first yields one macrotask. User code then runs as an ordinary
22
23
  // task instead of nested inside the inspector's dispatch of Runtime.evaluate,
@@ -24,6 +25,15 @@ export const SHIM = "__cdpk";
24
25
  // interrupt a busy loop. (Within that dispatch the inspector processes no
25
26
  // other messages, so an interrupt would never be seen.)
26
27
  const PRELUDE = `'use strict'; await ${SHIM}.tick();\n`;
28
+ /**
29
+ * Each replMode cell also ends by yielding one macrotask, so the script
30
+ * settles with no other microtasks queued ahead of the inspector's
31
+ * then-callback. Until that callback runs, the inspector holds the result
32
+ * promise only weakly, and a GC in between fails the evaluate with "Promise
33
+ * was collected" (#55). A declaration has no completion value, so the
34
+ * cell's value is unchanged; replMode lets `let` be declared again.
35
+ */
36
+ export const SETTLE = `\n;let ${SHIM}_settled = await ${SHIM}.tick();`;
27
37
  export class CellSyntaxError extends Error {
28
38
  name = "SyntaxError";
29
39
  }
@@ -33,7 +43,14 @@ function jsxSettings(jsx) {
33
43
  }
34
44
  return { jsx: "transform", jsxFactory: jsx?.factory, jsxFragment: jsx?.fragment };
35
45
  }
36
- function stripTypes(code, options) {
46
+ // esbuild compiles `import x = require("m")` to a `require` call, which a
47
+ // cell doesn't have (#143); `import type x = require("m")` is erased.
48
+ const IMPORT_REQUIRE = /^\s*(?:export\s+)?import\s+(?!type\s)[\w$]+\s*=\s*require\s*\(/m;
49
+ /** esbuild's output, with its source map's `mappings` when asked for. */
50
+ function stripTypesMapped(code, options, sourcemap) {
51
+ if (IMPORT_REQUIRE.test(code)) {
52
+ throw new CellSyntaxError('import x = require("m") is not supported in a cell; use import x from "m"');
53
+ }
37
54
  const compilerOptions = {
38
55
  verbatimModuleSyntax: true,
39
56
  ...(options.experimentalDecorators ? { experimentalDecorators: true } : {}),
@@ -42,13 +59,22 @@ function stripTypes(code, options) {
42
59
  let firstError;
43
60
  for (const loader of ["tsx", "ts"]) {
44
61
  try {
45
- return transformSync(code, {
62
+ const result = transformSync(code, {
46
63
  loader,
47
64
  target: "esnext",
65
+ // Neither acorn nor V8 parses standard (TC39) decorators or
66
+ // `accessor` yet, so esbuild lowers them (#142).
67
+ supported: { decorators: false },
48
68
  ...jsxSettings(options.jsx),
49
69
  tsconfigRaw: { compilerOptions },
50
70
  logLevel: "silent",
51
- }).code;
71
+ ...(sourcemap ? { sourcemap: "external" } : {}),
72
+ });
73
+ const map = sourcemap ? JSON.parse(result.map) : null;
74
+ const mappings = typeof map?.mappings === "string"
75
+ ? map.mappings
76
+ : "";
77
+ return { code: result.code, mappings };
52
78
  }
53
79
  catch (err) {
54
80
  firstError ??= err;
@@ -57,6 +83,9 @@ function stripTypes(code, options) {
57
83
  const message = firstError instanceof Error ? firstError.message : String(firstError);
58
84
  throw new CellSyntaxError(message.replace(/^Transform failed with \d+ errors?:\n/, ""));
59
85
  }
86
+ function stripTypes(code, options) {
87
+ return stripTypesMapped(code, options, false).code;
88
+ }
60
89
  function importedName(spec) {
61
90
  const imported = spec.imported;
62
91
  if (!imported)
@@ -69,8 +98,9 @@ function rewriteImport(code, node, index) {
69
98
  ? `, { with: { ${node.attributes.map((a) => code.slice(a.start, a.end)).join(", ")} } }`
70
99
  : "";
71
100
  const load = `await ${SHIM}.import(${source}${attrs})`;
101
+ // `void`: as a cell's last statement, the import isn't its result (#139).
72
102
  if (node.specifiers.length === 0)
73
- return `${load};`;
103
+ return `void ${load};`;
74
104
  const ns = `${SHIM}_m${index}`;
75
105
  const bindings = node.specifiers.map((spec) => {
76
106
  const local = String(spec.local.name);
@@ -129,7 +159,13 @@ export function rewriteModuleSyntax(js) {
129
159
  }
130
160
  case "ExportDefaultDeclaration": {
131
161
  const decl = node;
132
- edits.push({ start: decl.start, end: decl.declaration.start, text: "" });
162
+ const anonymous = (decl.declaration.type === "FunctionDeclaration" || decl.declaration.type === "ClassDeclaration") &&
163
+ decl.declaration.id === null;
164
+ // An anonymous `function () {}` or `class {}` isn't a statement on
165
+ // its own; parenthesized, it's an expression and the cell's result (#143).
166
+ edits.push({ start: decl.start, end: decl.declaration.start, text: anonymous ? "(" : "" });
167
+ if (anonymous)
168
+ edits.push({ start: decl.declaration.end, end: decl.declaration.end, text: ");" });
133
169
  break;
134
170
  }
135
171
  case "ExportAllDeclaration":
@@ -140,12 +176,25 @@ export function rewriteModuleSyntax(js) {
140
176
  // keyword edit doesn't overlap its arguments), so apply back to front.
141
177
  return applyEdits(js, edits);
142
178
  }
179
+ function newlines(text) {
180
+ let count = 0;
181
+ for (const char of text)
182
+ if (char === "\n")
183
+ count++;
184
+ return count;
185
+ }
143
186
  function applyEdits(js, edits) {
144
187
  // Back to front; at a shared start, a replacement goes before an insertion.
145
188
  edits.sort((a, b) => b.start - a.start || b.end - a.end);
146
189
  let out = js;
147
- for (const edit of edits)
148
- out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
190
+ for (const edit of edits) {
191
+ // Keep every later line where it was, so a cell's lines still map to
192
+ // esbuild's (the debugger relies on it, #18): a multi-line import
193
+ // rewritten to one line is padded with the newlines it replaced.
194
+ const lost = newlines(js.slice(edit.start, edit.end)) - newlines(edit.text);
195
+ const text = lost > 0 ? edit.text + "\n".repeat(lost) : edit.text;
196
+ out = out.slice(0, edit.start) + text + out.slice(edit.end);
197
+ }
149
198
  return out;
150
199
  }
151
200
  /** Adds the names a binding pattern declares. */
@@ -178,8 +227,8 @@ function patternNames(pattern, names) {
178
227
  * surrounding script, which a global-code eval turns into a property of
179
228
  * globalThis. Inside the function the declarations become assignments to
180
229
  * those globals:
181
- * const { a } = o; class K {} -> var a, K;
182
- * (async () => { ({ a } = o); K = class K {}; })()
230
+ * const { a } = o; class K {} -> var a, K; (async () => {
231
+ * ({ a } = o); K = class K {}; })()
183
232
  * So later cells, and closures from this one, all see one binding. A `var`
184
233
  * in a top-level block or loop head is global too, as in a script; block
185
234
  * scoped and function-local declarations stay where they are. Functions
@@ -253,18 +302,30 @@ function hoistForEval(js) {
253
302
  edits.push({ start: stmt.end, end: stmt.end, text: ";" });
254
303
  }
255
304
  else if (stmt.type === "ExpressionStatement" && i === program.body.length - 1) {
256
- replace(stmt, `return [${src(stmt.expression)}];`);
305
+ // Parenthesized, so `1, 2` returns [2] rather than [1, 2] (#140).
306
+ replace(stmt, `return [(${src(stmt.expression)})];`);
257
307
  }
258
308
  else {
259
309
  nestedVars(stmt);
260
310
  }
261
311
  });
262
- const declare = names.size > 0 ? `var ${[...names].join(", ")};\n` : "";
312
+ // All on the first line, so the cell always starts on line 2, as after
313
+ // transformCell's one-line prelude (#150).
314
+ const declare = names.size > 0 ? `var ${[...names].join(", ")}; ` : "";
263
315
  const publish = functions.map((name) => ` globalThis.${name} = ${name};`).join("");
264
316
  return `${declare}(async () => { 'use strict';${publish}\n${applyEdits(js, edits)}\n})()`;
265
317
  }
266
318
  export function transformCell(code, options = {}) {
267
- return PRELUDE + rewriteModuleSyntax(stripTypes(code, options));
319
+ return PRELUDE + rewriteModuleSyntax(stripTypes(code, options)) + SETTLE;
320
+ }
321
+ /**
322
+ * transformCell, plus the map between the cell's lines and the script's,
323
+ * for the debugger (#18). The rewrite keeps lines (see applyEdits), so the
324
+ * script's lines are esbuild's, after the prelude's one.
325
+ */
326
+ export function transformCellWithLines(code, options = {}) {
327
+ const stripped = stripTypesMapped(code, options, true);
328
+ return { script: PRELUDE + rewriteModuleSyntax(stripped.code) + SETTLE, lines: new LineMap(stripped.mappings, newlines(PRELUDE)) };
268
329
  }
269
330
  /**
270
331
  * Like transformCell, for a target that evaluates cells itself, as global
package/dist/wire.d.ts CHANGED
@@ -11,12 +11,17 @@ export interface Header extends JsonObject {
11
11
  msg_type: string;
12
12
  version: string;
13
13
  }
14
+ /** A header as received: the fields the kernel relies on, plus whatever else came. */
15
+ export interface ReceivedHeader extends JsonObject {
16
+ msg_id: string;
17
+ msg_type: string;
18
+ }
14
19
  export interface Message {
15
20
  identities: Uint8Array[];
16
21
  sig: string;
17
22
  /** Raw bytes of header, parent_header, metadata, content as received. */
18
23
  signedParts: Uint8Array[];
19
- header: Header;
24
+ header: ReceivedHeader;
20
25
  parentHeader: JsonObject;
21
26
  metadata: JsonObject;
22
27
  content: JsonObject;
@@ -26,6 +31,8 @@ export declare function sign(key: string, parts: Uint8Array[]): string;
26
31
  export declare function verify(key: string, parts: Uint8Array[], sig: string): boolean;
27
32
  export declare function makeHeader(session: string, msgType: string): Header;
28
33
  export interface EncodeOptions {
34
+ /** Defaults to a fresh header; pass one to know its msg_id. */
35
+ header?: Header;
29
36
  identities?: Uint8Array[];
30
37
  parentHeader?: JsonObject;
31
38
  metadata?: JsonObject;
package/dist/wire.js CHANGED
@@ -41,7 +41,7 @@ export function makeHeader(session, msgType) {
41
41
  }
42
42
  export function encodeMessage(session, key, msgType, content, options = {}) {
43
43
  const parts = [
44
- makeHeader(session, msgType),
44
+ options.header ?? makeHeader(session, msgType),
45
45
  options.parentHeader ?? {},
46
46
  options.metadata ?? {},
47
47
  content,
@@ -70,6 +70,8 @@ function parseObject(frame) {
70
70
  }
71
71
  return value;
72
72
  }
73
+ // Only msg_id and msg_type are required of a received header; nothing
74
+ // reads the rest, so a client that leaves them out still works.
73
75
  function isHeader(value) {
74
76
  return typeof value.msg_id === "string" && typeof value.msg_type === "string";
75
77
  }
@@ -1,4 +1,4 @@
1
- import type { CdpParams, EvaluateResult, RemoteObject } from "./cdp.ts";
1
+ import { type CdpParams, type EvaluateResult, type RemoteObject } from "./cdp.ts";
2
2
  import { transformCellForEval } from "./transform.ts";
3
3
  import { type KernelTarget, type TargetEvents } from "./target.ts";
4
4
  import { type WsChannel } from "./ws-client.ts";
@@ -53,6 +53,12 @@ export interface WorkerHost {
53
53
  stop(now: boolean): void;
54
54
  onExit(listener: (code: number | null) => void): void;
55
55
  }
56
+ /**
57
+ * Removes workerd directories under `root` whose kernel has died without
58
+ * cleaning up (SIGKILL runs no exit hook), killing their workerd if it's
59
+ * still running (#220).
60
+ */
61
+ export declare function sweepStaleWorkerdDirs(root?: string): void;
56
62
  /** The driver on a workerd of its own, behind a Unix socket. */
57
63
  export declare class RawWorkerd implements WorkerHost {
58
64
  #private;
@@ -115,6 +121,8 @@ export declare class WorkerdTarget implements KernelTarget {
115
121
  stack: string;
116
122
  }>;
117
123
  propertyNames(expression: string): Promise<string[]>;
124
+ /** Resolved by property descriptor in the driver, so nothing runs (#10). */
125
+ inspect(path: string, detail: number): Promise<string | null>;
118
126
  /** None: top-level declarations here are properties of globalThis. */
119
127
  lexicalNames(): Promise<string[]>;
120
128
  /** Never asked for: prompt() in workerd answers null without asking. */
@@ -18,20 +18,22 @@ import { Buffer } from "node:buffer";
18
18
  import process from "node:process";
19
19
  import { execFileSync, spawn } from "node:child_process";
20
20
  import { randomBytes } from "node:crypto";
21
- import { copyFileSync, existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
21
+ import { copyFileSync, existsSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, rmSync, writeFileSync } from "node:fs";
22
22
  import { builtinModules } from "node:module";
23
23
  import { tmpdir } from "node:os";
24
24
  import { dirname, join } from "node:path";
25
25
  import { pathToFileURL } from "node:url";
26
26
  import { build } from "esbuild";
27
+ import { asEvaluateResult } from "./cdp.js";
27
28
  import { transformCellForEval } from "./transform.js";
28
29
  import { runtimeDir } from "./package.js";
30
+ import { killOnExit } from "./children.js";
29
31
  import { Interrupted, isTargetMessage } from "./target.js";
30
32
  import { connectUnixWebSocket } from "./ws-client.js";
31
33
  import { findWranglerConfig, startMiniflare } from "./workers-project.js";
32
- import { ensureInstalled, jsrToNpm, parsePackageSpecifier } from "../runtime/node-hooks.mjs";
34
+ import { ensureInstalledAsync, findLockFile, jsrToNpm, parsePackageSpecifier } from "../runtime/node-hooks.mjs";
33
35
  /** The driver Worker's modules; the first is its main module. */
34
- export const WORKER_MODULES = ["workerd-worker.mjs", "display.mjs", "comm.mjs", "widget.mjs", "evaluator.mjs"];
36
+ export const WORKER_MODULES = ["workerd-worker.mjs", "display.mjs", "comm.mjs", "widget.mjs", "evaluator.mjs", "introspect.mjs"];
35
37
  /** See BunTarget: how long an interrupted cell has to show it isn't busy. */
36
38
  export const INTERRUPT_GRACE_MS = 500;
37
39
  export function restartNote(runtime) {
@@ -121,7 +123,8 @@ export async function bundleModule(specifier, cwd) {
121
123
  ? jsrToNpm(specifier.slice(4))
122
124
  : null;
123
125
  if (pkg) {
124
- const { dir, alias } = ensureInstalled(pkg);
126
+ // Async: a sync install would stall the heartbeat and interrupts (#149).
127
+ const { dir, alias } = await ensureInstalledAsync(pkg, "npm", { lockFile: findLockFile(cwd) });
125
128
  entry = alias + pkg.subpath;
126
129
  resolveDir = dir;
127
130
  }
@@ -168,6 +171,65 @@ export function cellMeta(cwd) {
168
171
  const replUrl = new URL("$cdpk$repl.mjs", pathToFileURL(cwd.endsWith("/") ? cwd : cwd + "/"));
169
172
  return { url: replUrl.href, dirname: cwd, filename: join(cwd, "$cdpk$repl.mjs") };
170
173
  }
174
+ const WORKERD_DIR_PREFIX = "cdpk-workerd-";
175
+ /** In each workerd directory: `{ host, workerd }` pids. */
176
+ const OWNER_FILE = "owner.json";
177
+ function pidAlive(pid) {
178
+ try {
179
+ process.kill(pid, 0);
180
+ return true;
181
+ }
182
+ catch (err) {
183
+ return err.code === "EPERM";
184
+ }
185
+ }
186
+ /** Whether `pid` is still the workerd serving `dir`, not a reused pid. */
187
+ function isWorkerdIn(pid, dir) {
188
+ try {
189
+ if (process.platform === "linux")
190
+ return readlinkSync(`/proc/${pid}/cwd`) === dir;
191
+ const args = execFileSync("ps", ["-o", "args=", "-p", String(pid)], { encoding: "utf8" });
192
+ return /workerd/.test(args) && args.includes("config.capnp");
193
+ }
194
+ catch {
195
+ return false;
196
+ }
197
+ }
198
+ /**
199
+ * Removes workerd directories under `root` whose kernel has died without
200
+ * cleaning up (SIGKILL runs no exit hook), killing their workerd if it's
201
+ * still running (#220).
202
+ */
203
+ export function sweepStaleWorkerdDirs(root = tmpdir()) {
204
+ let names;
205
+ try {
206
+ names = readdirSync(root).filter((name) => name.startsWith(WORKERD_DIR_PREFIX));
207
+ }
208
+ catch {
209
+ return;
210
+ }
211
+ for (const name of names) {
212
+ const dir = join(root, name);
213
+ let owner;
214
+ try {
215
+ owner = JSON.parse(readFileSync(join(dir, OWNER_FILE), "utf8"));
216
+ }
217
+ catch {
218
+ continue; // Another user's, from before owner files, or being set up.
219
+ }
220
+ if (typeof owner.host !== "number" || pidAlive(owner.host))
221
+ continue;
222
+ if (typeof owner.workerd === "number" && isWorkerdIn(owner.workerd, dir)) {
223
+ try {
224
+ process.kill(owner.workerd, "SIGKILL");
225
+ }
226
+ catch {
227
+ // Gone already.
228
+ }
229
+ }
230
+ rmSync(dir, { recursive: true, force: true });
231
+ }
232
+ }
171
233
  /** The driver on a workerd of its own, behind a Unix socket. */
172
234
  export class RawWorkerd {
173
235
  runtime;
@@ -184,7 +246,9 @@ export class RawWorkerd {
184
246
  const source = await runtimeDir(new URL("../runtime/", import.meta.url).href);
185
247
  // mkdtemp makes the directory 0700: only this user reaches the socket,
186
248
  // and the token stops anything else running as them by accident.
187
- const dir = mkdtempSync(join(tmpdir(), "cdpk-workerd-"));
249
+ sweepStaleWorkerdDirs();
250
+ const dir = mkdtempSync(join(tmpdir(), WORKERD_DIR_PREFIX));
251
+ writeFileSync(join(dir, OWNER_FILE), JSON.stringify({ host: process.pid }));
188
252
  const socket = join(dir, "sock");
189
253
  for (const name of WORKER_MODULES)
190
254
  copyFileSync(join(source, name), join(dir, name));
@@ -194,11 +258,21 @@ export class RawWorkerd {
194
258
  // --experimental: the UnsafeEval binding.
195
259
  const child = spawn(workerd, ["serve", "config.capnp", "--experimental"], { cwd: dir, stdio: ["ignore", "pipe", "pipe"], env });
196
260
  child.once("exit", () => rmSync(dir, { recursive: true, force: true }));
261
+ if (child.pid !== undefined)
262
+ writeFileSync(join(dir, OWNER_FILE), JSON.stringify({ host: process.pid, workerd: child.pid }));
263
+ killOnExit(child, () => {
264
+ child.kill("SIGKILL");
265
+ rmSync(dir, { recursive: true, force: true });
266
+ });
197
267
  // Cell output is captured inside the Worker; what workerd itself prints
198
268
  // (its own warnings, errors from background work) goes to the server log.
269
+ // `early` keeps that output for a startup error only, and stops growing
270
+ // once the session is open.
199
271
  let early = "";
272
+ let starting = true;
200
273
  const log = (chunk) => {
201
- early += chunk.toString();
274
+ if (starting)
275
+ early += chunk.toString();
202
276
  process.stderr.write(`[cdp-kernel workerd] ${chunk}`);
203
277
  };
204
278
  child.stdout?.on("data", log);
@@ -221,7 +295,10 @@ export class RawWorkerd {
221
295
  }
222
296
  })();
223
297
  try {
224
- return new RawWorkerd(child, await Promise.race([ready, exited]), compatibilityDate);
298
+ const host = new RawWorkerd(child, await Promise.race([ready, exited]), compatibilityDate);
299
+ starting = false;
300
+ early = "";
301
+ return host;
225
302
  }
226
303
  catch (err) {
227
304
  child.kill();
@@ -229,7 +306,16 @@ export class RawWorkerd {
229
306
  }
230
307
  }
231
308
  stop(now) {
232
- this.#child.kill(now ? "SIGKILL" : "SIGTERM");
309
+ if (now) {
310
+ this.#child.kill("SIGKILL");
311
+ return;
312
+ }
313
+ this.#child.kill("SIGTERM");
314
+ // A busy isolate keeps workerd's event loop from ever seeing SIGTERM (#128).
315
+ setTimeout(() => {
316
+ if (this.#child.exitCode === null && this.#child.signalCode === null)
317
+ this.#child.kill("SIGKILL");
318
+ }, 3000).unref();
233
319
  }
234
320
  onExit(listener) {
235
321
  this.#child.once("exit", listener);
@@ -342,6 +428,9 @@ export class WorkerdTarget {
342
428
  this.#pending.clear();
343
429
  }
344
430
  #request(op, params = {}, interruptible = false) {
431
+ // The channel drops writes after it closes, so nothing would answer.
432
+ if (this.#exitedOnce)
433
+ return Promise.reject(new Abandoned("the workerd session is closed"));
345
434
  const id = ++this.#nextId;
346
435
  return new Promise((resolve, reject) => {
347
436
  this.#pending.set(id, { resolve, reject });
@@ -377,7 +466,12 @@ export class WorkerdTarget {
377
466
  if (!modules[specifier].startsWith("Promise.reject("))
378
467
  this.#sent.add(specifier);
379
468
  }
380
- return (await this.#interruptibly("eval", { code: script, modules }, "Execution"));
469
+ const { failedModules, ...reply } = await this.#interruptibly("eval", { code: script, modules }, "Execution");
470
+ // A module that threw while loading is rebuilt for its next import (#147).
471
+ if (Array.isArray(failedModules))
472
+ for (const specifier of failedModules)
473
+ this.#sent.delete(specifier);
474
+ return asEvaluateResult(reply);
381
475
  }
382
476
  /**
383
477
  * A cell that's waiting on something leaves the driver free: it answers a
@@ -435,6 +529,11 @@ export class WorkerdTarget {
435
529
  const reply = await this.#request("props", { expr: expression }).catch(() => null);
436
530
  return Array.isArray(reply?.names) ? reply.names.filter((n) => typeof n === "string") : [];
437
531
  }
532
+ /** Resolved by property descriptor in the driver, so nothing runs (#10). */
533
+ async inspect(path, detail) {
534
+ const reply = await this.#request("inspect", { path, detail }).catch(() => null);
535
+ return typeof reply?.text === "string" ? reply.text : null;
536
+ }
438
537
  /** None: top-level declarations here are properties of globalThis. */
439
538
  lexicalNames() {
440
539
  return Promise.resolve([]);
@@ -1,6 +1,7 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  export declare const OP_CONTINUATION = 0;
3
3
  export declare const OP_TEXT = 1;
4
+ export declare const OP_BINARY = 2;
4
5
  export declare const OP_CLOSE = 8;
5
6
  export declare const OP_PING = 9;
6
7
  export declare const OP_PONG = 10;
@@ -11,7 +12,10 @@ export interface Frame {
11
12
  }
12
13
  /** One frame; clients must mask what they send. */
13
14
  export declare function encodeFrame(opcode: number, payload: Buffer, mask?: Buffer | null): Buffer;
14
- /** Splits a byte stream into frames, holding back a partial one. */
15
+ /**
16
+ * Splits a byte stream into frames, holding back a partial one. Chunks of a
17
+ * large frame are only joined once the whole frame has arrived.
18
+ */
15
19
  export declare class FrameDecoder {
16
20
  #private;
17
21
  push(chunk: Buffer): Frame[];