@tlockney/cdp-kernel 0.2.0 → 0.3.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.
@@ -0,0 +1,189 @@
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
+
22
+ const BUILTIN = /^(?:node|cloudflare|workerd):/;
23
+
24
+ function base64(bytes) {
25
+ let binary = "";
26
+ for (let i = 0; i < bytes.length; i += 0x8000) {
27
+ binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
28
+ }
29
+ return btoa(binary);
30
+ }
31
+
32
+ // --- Output -----------------------------------------------------------------
33
+ // Lines go to the host's session. Anything emitted before it connects waits.
34
+ let sink = null;
35
+ const outbox = [];
36
+
37
+ function emit(message) {
38
+ const line = JSON.stringify(message);
39
+ if (sink) sink(line);
40
+ else outbox.push(line);
41
+ }
42
+
43
+ const stream = (name) => (...args) => emit({ kind: "stream", name, text: formatWithOptions({ colors: true }, ...args) + "\n" });
44
+ Object.assign(console, {
45
+ log: stream("stdout"),
46
+ info: stream("stdout"),
47
+ debug: stream("stdout"),
48
+ dir: (value) => emit({ kind: "stream", name: "stdout", text: inspect(value, { colors: true, depth: 4 }) + "\n" }),
49
+ warn: stream("stderr"),
50
+ error: stream("stderr"),
51
+ });
52
+
53
+ const jupyter = createJupyter({
54
+ inspect: (value) => inspect(value, { colors: true, depth: 4 }),
55
+ base64,
56
+ readFile() {
57
+ throw new Error("workerd has no file system to read from; pass the image bytes instead");
58
+ },
59
+ send(msgType, content, metadata, buffers) {
60
+ emit({ kind: "iopub", msg_type: msgType, content, metadata, buffers: buffers.map(base64), barrier: 0 });
61
+ },
62
+ // A request can't block for an answer, so prompt() and confirm() behave
63
+ // as when the frontend has no stdin.
64
+ input: () => null,
65
+ });
66
+
67
+ // --- Modules ----------------------------------------------------------------
68
+ const modules = new Map();
69
+
70
+ /** `require` for a host-built bundle's external imports (builtins). */
71
+ async function requireFor(specifiers) {
72
+ const loaded = new Map();
73
+ for (const specifier of specifiers) {
74
+ // Packages require Node builtins by bare name too ("fs").
75
+ const ns = await import(BUILTIN.test(specifier) ? specifier : `node:${specifier}`);
76
+ loaded.set(specifier, ns.default ?? ns);
77
+ }
78
+ return (specifier) => {
79
+ if (!loaded.has(specifier)) throw new Error(`cannot require ${specifier} in workerd`);
80
+ return loaded.get(specifier);
81
+ };
82
+ }
83
+
84
+ function cellImport(specifier, options) {
85
+ if (BUILTIN.test(specifier)) return import(specifier, options);
86
+ if (modules.has(specifier)) return modules.get(specifier);
87
+ return Promise.reject(new TypeError(`Cannot import "${specifier}": the host didn't bundle it`));
88
+ }
89
+
90
+ // --- The shim cells and the evaluator use -----------------------------------
91
+ let handle = null;
92
+
93
+ function install(env) {
94
+ const meta = JSON.parse(env.META);
95
+ globalThis.__cdpk = {
96
+ import: cellImport,
97
+ meta: { ...meta, resolve: (specifier) => new URL(specifier, meta.url).href },
98
+ requireFor,
99
+ barrier: () => 0,
100
+ broadcastResult: jupyter.broadcastResult,
101
+ async commDispatch(msgType, content, metadata, buffers64) {
102
+ const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
103
+ return await jupyter.commDispatch(msgType, content, metadata, buffers);
104
+ },
105
+ describeError(value) {
106
+ if (value instanceof Error) {
107
+ return JSON.stringify({ name: value.name, message: value.message, stack: value.stack ?? "" });
108
+ }
109
+ return JSON.stringify({ name: "Error", message: inspect(value), stack: "" });
110
+ },
111
+ };
112
+ Object.defineProperty(globalThis, "__cdpk", { enumerable: false });
113
+ globalThis.jupyter = jupyter.namespace;
114
+ globalThis.prompt = jupyter.prompt;
115
+ globalThis.confirm = jupyter.confirm;
116
+ // A cell has no fetch(request, env), so the Worker's bindings (#26) are a
117
+ // global, without the kernel's own.
118
+ const { UNSAFE, TOKEN, META, ...bindings } = env;
119
+ globalThis.env = bindings;
120
+ // workerd forbids eval without its UnsafeEval binding; celld (#25) allows
121
+ // a plain global eval and has no such binding.
122
+ const evaluate = env.UNSAFE ? (code) => env.UNSAFE.eval(code, "cell") : (code) => (0, eval)(code);
123
+ handle = createHandler(globalThis.__cdpk, evaluate);
124
+ return evaluate;
125
+ }
126
+
127
+ let evaluate = null;
128
+
129
+ /** Evaluates the bundles a cell brings, once each. */
130
+ async function loadModules(bundles = {}) {
131
+ for (const [specifier, script] of Object.entries(bundles)) {
132
+ if (modules.has(specifier)) continue;
133
+ 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));
136
+ modules.set(specifier, loading);
137
+ }
138
+ }
139
+
140
+ async function answer(req) {
141
+ if (req.op === "info") {
142
+ return {
143
+ v8: globalThis.process?.versions?.v8 ?? "",
144
+ bindings: Object.keys(globalThis.env),
145
+ // A Workers project's default export (src/workers-project.ts).
146
+ worker: globalThis.worker !== undefined,
147
+ };
148
+ }
149
+ if (req.op === "eval") await loadModules(req.modules);
150
+ return await handle(req);
151
+ }
152
+
153
+ export default {
154
+ async fetch(request, env) {
155
+ // A header, or the query string for a WebSocket client that can't set
156
+ // headers (celld's session uses the host's built-in WebSocket).
157
+ 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 });
159
+ if (request.headers.get("upgrade") !== "websocket") return new Response("expected a WebSocket", { status: 426 });
160
+ evaluate ??= install(env);
161
+
162
+ // The session: every request from the host arrives as a message, and
163
+ // all of them run in this one request's IoContext. So I/O objects (a
164
+ // Durable Object stub, a stream) made in one cell work in the next, and
165
+ // work a cell leaves running outlives it (#26).
166
+ const { 0: client, 1: server } = new WebSocketPair();
167
+ server.accept();
168
+ const send = (line) => {
169
+ try {
170
+ server.send(line);
171
+ } catch {
172
+ // The host is gone.
173
+ }
174
+ };
175
+ sink = send;
176
+ for (const line of outbox.splice(0)) send(line);
177
+ server.addEventListener("message", (event) => {
178
+ const req = JSON.parse(event.data);
179
+ answer(req).then(
180
+ (reply) => send(JSON.stringify({ kind: "reply", id: req.id, ...reply })),
181
+ (err) => send(JSON.stringify({ kind: "reply", id: req.id, failure: String(err?.stack ?? err) })),
182
+ );
183
+ });
184
+ server.addEventListener("close", () => {
185
+ if (sink === send) sink = null;
186
+ });
187
+ return new Response(null, { status: 101, webSocket: client });
188
+ },
189
+ };