@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
@@ -18,6 +18,7 @@
18
18
  import { formatWithOptions, inspect } from "node:util";
19
19
  import { createJupyter } from "./display.mjs";
20
20
  import { createHandler } from "./evaluator.mjs";
21
+ import { describeValue, inspectPath, resolvePath } from "./introspect.mjs";
21
22
 
22
23
  const BUILTIN = /^(?:node|cloudflare|workerd):/;
23
24
 
@@ -50,6 +51,44 @@ Object.assign(console, {
50
51
  error: stream("stderr"),
51
52
  });
52
53
 
54
+ // An error thrown later by a cell's timer or promise goes to stderr, as on
55
+ // the other targets (runtime/bootstrap.mjs). Workers has no global "error"
56
+ // event, so timer callbacks report their own (#148), and so do the
57
+ // promises they return: celld doesn't dispatch "unhandledrejection".
58
+ function reportUncaught(kind, err) {
59
+ emit({ kind: "stream", name: "stderr", text: `${kind}: ${err instanceof Error ? (err.stack ?? err.message) : inspect(err)}\n` });
60
+ }
61
+ // A rejection both runtimes' events report is shown once.
62
+ const reported = new WeakSet();
63
+ function reportRejection(reason) {
64
+ if (typeof reason === "object" && reason !== null) {
65
+ if (reported.has(reason)) return;
66
+ reported.add(reason);
67
+ }
68
+ reportUncaught("Uncaught (in promise)", reason);
69
+ }
70
+ globalThis.addEventListener("unhandledrejection", (event) => {
71
+ event.preventDefault();
72
+ reportRejection(event.reason);
73
+ });
74
+ globalThis.process?.on?.("unhandledRejection", reportRejection);
75
+ const reporting = (callback) =>
76
+ typeof callback !== "function" ? callback : function (...args) {
77
+ let result;
78
+ try {
79
+ result = callback.apply(this, args);
80
+ } catch (err) {
81
+ reportUncaught("Uncaught", err);
82
+ return;
83
+ }
84
+ if (typeof result?.then === "function") result.then(undefined, reportRejection);
85
+ return result;
86
+ };
87
+ for (const name of ["setTimeout", "setInterval", "queueMicrotask"]) {
88
+ const original = globalThis[name];
89
+ globalThis[name] = { [name]: (callback, ...rest) => original(reporting(callback), ...rest) }[name];
90
+ }
91
+
53
92
  const jupyter = createJupyter({
54
93
  inspect: (value) => inspect(value, { colors: true, depth: 4 }),
55
94
  base64,
@@ -96,6 +135,10 @@ function install(env) {
96
135
  import: cellImport,
97
136
  meta: { ...meta, resolve: (specifier) => new URL(specifier, meta.url).href },
98
137
  requireFor,
138
+ // inspect_request (#10): by descriptor, so nothing runs.
139
+ describeForInspect: (value, detail) => describeValue(value, { inspect: (v) => inspect(v, { colors: false, depth: 2 }), detail }),
140
+ inspectPath: (path, detail) => inspectPath(path, { inspect: (v) => inspect(v, { colors: false, depth: 2 }), detail }),
141
+ resolvePath,
99
142
  barrier: () => 0,
100
143
  broadcastResult: jupyter.broadcastResult,
101
144
  async commDispatch(msgType, content, metadata, buffers64) {
@@ -126,13 +169,23 @@ function install(env) {
126
169
 
127
170
  let evaluate = null;
128
171
 
172
+ // Modules whose load rejected. Importing one again gives its error again,
173
+ // and the next eval reply names it, so the host bundles it afresh for the
174
+ // next import (#147), which replaces it here.
175
+ const rejected = new Set();
176
+ const unreported = new Set();
177
+
129
178
  /** Evaluates the bundles a cell brings, once each. */
130
179
  async function loadModules(bundles = {}) {
131
180
  for (const [specifier, script] of Object.entries(bundles)) {
132
- if (modules.has(specifier)) continue;
181
+ if (modules.has(specifier) && !rejected.has(specifier)) continue;
182
+ rejected.delete(specifier);
133
183
  const loading = Promise.resolve().then(() => evaluate(script));
134
- // A module that failed to load is retried by the next import of it.
135
- loading.catch(() => modules.delete(specifier));
184
+ loading.catch(() => {
185
+ if (modules.get(specifier) !== loading) return;
186
+ rejected.add(specifier);
187
+ unreported.add(specifier);
188
+ });
136
189
  modules.set(specifier, loading);
137
190
  }
138
191
  }
@@ -146,8 +199,31 @@ async function answer(req) {
146
199
  worker: globalThis.worker !== undefined,
147
200
  };
148
201
  }
149
- if (req.op === "eval") await loadModules(req.modules);
150
- return await handle(req);
202
+ if (req.op !== "eval") return await handle(req);
203
+ await loadModules(req.modules);
204
+ const reply = await handle(req);
205
+ // After the cell, so a load that failed in it is counted.
206
+ const failedModules = [...unreported];
207
+ unreported.clear();
208
+ return { ...reply, failedModules };
209
+ }
210
+
211
+ const encoder = new TextEncoder();
212
+
213
+ /**
214
+ * Compares in constant time: under celld and Miniflare the driver listens on
215
+ * loopback TCP, where any local user can try tokens. The length (fixed by
216
+ * the host) is not secret.
217
+ */
218
+ function tokenMatches(token, expected) {
219
+ if (typeof token !== "string" || typeof expected !== "string") return false;
220
+ const a = encoder.encode(token);
221
+ const b = encoder.encode(expected);
222
+ if (a.byteLength !== b.byteLength) return false;
223
+ if (typeof crypto.subtle.timingSafeEqual === "function") return crypto.subtle.timingSafeEqual(a, b);
224
+ let diff = 0;
225
+ for (let i = 0; i < a.byteLength; i++) diff |= a[i] ^ b[i];
226
+ return diff === 0;
151
227
  }
152
228
 
153
229
  export default {
@@ -155,7 +231,7 @@ export default {
155
231
  // A header, or the query string for a WebSocket client that can't set
156
232
  // headers (celld's session uses the host's built-in WebSocket).
157
233
  const token = request.headers.get("x-cdpk-token") ?? new URL(request.url).searchParams.get("token");
158
- if (token !== env.TOKEN) return new Response("forbidden", { status: 403 });
234
+ if (!tokenMatches(token, env.TOKEN)) return new Response("forbidden", { status: 403 });
159
235
  if (request.headers.get("upgrade") !== "websocket") return new Response("expected a WebSocket", { status: 426 });
160
236
  evaluate ??= install(env);
161
237