@tlockney/cdp-kernel 0.9.0 → 0.10.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.
@@ -28,6 +28,13 @@ export interface BrowserTargetOptions {
28
28
  * called with `{ host, jupyter }` (#384).
29
29
  */
30
30
  preload?: string[];
31
+ /**
32
+ * The run's parameters, which cells read as `jupyter.params` (#456). The
33
+ * other targets read them from `GANYMEDE_PARAMS`.
34
+ */
35
+ params?: {
36
+ [name: string]: unknown;
37
+ };
31
38
  /** Answers the Worker's `host.request` calls; without it they reject (#384). */
32
39
  host?: HostHandler;
33
40
  }
@@ -48,7 +48,8 @@ export class BrowserTarget {
48
48
  const target = new BrowserTarget(options.createWorker(), events, options.host);
49
49
  const base = options.base ?? globalThis.location?.href ?? "file:///";
50
50
  try {
51
- const { userAgent } = await target.#request("info", { base, cdn: options.cdn, preload: options.preload ?? [] });
51
+ const params = options.params === undefined ? undefined : JSON.stringify(options.params);
52
+ const { userAgent } = await target.#request("info", { base, cdn: options.cdn, preload: options.preload ?? [], params });
52
53
  if (typeof userAgent === "string")
53
54
  target.versions.runtime = browserName(userAgent);
54
55
  target.versions.engine = engineName(typeof userAgent === "string" ? userAgent : "");
@@ -86,6 +86,174 @@ function createComms(send, newId = () => crypto.randomUUID()) {
86
86
  return { api, dispatch, list: list2 };
87
87
  }
88
88
 
89
+ // runtime/host.mjs
90
+ var HOST_TARGET = "ganymede.host.v1";
91
+ var CODES = /* @__PURE__ */ new Set(["unavailable", "denied", "cancelled", "timeout", "failed", "disconnected"]);
92
+ var HostError = class _HostError extends Error {
93
+ constructor(code, message) {
94
+ super(message);
95
+ this.name = "HostError";
96
+ this.code = code;
97
+ }
98
+ /** An abort becomes `cancelled`, a timeout signal's abort `timeout`, anything else `failed`. */
99
+ static from(err) {
100
+ if (err instanceof _HostError) return err;
101
+ const message = typeof err?.message === "string" ? err.message : String(err);
102
+ if (err?.name === "AbortError") return new _HostError("cancelled", message);
103
+ if (err?.name === "TimeoutError") return new _HostError("timeout", message);
104
+ if (typeof err?.code === "string" && CODES.has(err.code)) return new _HostError(err.code, message);
105
+ return new _HostError("failed", message);
106
+ }
107
+ };
108
+ function abortError(reason) {
109
+ const message = typeof reason?.message === "string" ? reason.message : String(reason ?? "aborted");
110
+ return new HostError(reason?.name === "TimeoutError" ? "timeout" : "cancelled", message);
111
+ }
112
+ function helloOf(data) {
113
+ if (data === null || typeof data !== "object" || typeof data.protocol !== "number") return null;
114
+ return {
115
+ protocol: data.protocol,
116
+ server: typeof data.server === "string" ? data.server : "",
117
+ notebook: typeof data.notebook === "string" ? data.notebook : null,
118
+ methods: Array.isArray(data.methods) ? data.methods.filter((m) => typeof m === "string") : []
119
+ };
120
+ }
121
+ function createHost(comms, defaultSignal = () => void 0) {
122
+ let link = null;
123
+ const waiters = /* @__PURE__ */ new Set();
124
+ let noServer = false;
125
+ let nextId = 0;
126
+ function settle() {
127
+ const host3 = link?.host ?? null;
128
+ if (host3 === null && link !== null) return;
129
+ for (const waiter of [...waiters]) waiter(host3);
130
+ }
131
+ function adopt(comm, data, ours = false) {
132
+ const old = link;
133
+ const l = { comm, host: null, pending: /* @__PURE__ */ new Map(), ours };
134
+ if (!ours) noServer = false;
135
+ link = l;
136
+ if (old !== null && !old.comm.closed) old.comm.close();
137
+ const hello = helloOf(data);
138
+ if (hello !== null) l.host = makeHost(l, hello);
139
+ comm.onMsg(({ data: data2 }) => {
140
+ if (data2.hello !== void 0) {
141
+ const hello2 = helloOf(data2.hello);
142
+ if (hello2 !== null) {
143
+ l.host = makeHost(l, hello2);
144
+ if (link === l) settle();
145
+ }
146
+ return;
147
+ }
148
+ const call = l.pending.get(data2.id);
149
+ if (call === void 0) return;
150
+ if (data2.error !== void 0) {
151
+ const { code, message } = data2.error ?? {};
152
+ call.reject(new HostError(CODES.has(code) ? code : "failed", typeof message === "string" ? message : String(code)));
153
+ } else {
154
+ call.resolve(data2.result);
155
+ }
156
+ });
157
+ comm.onClose(({ data: data2 }) => {
158
+ for (const call of l.pending.values()) {
159
+ call.reject(new HostError("disconnected", typeof data2?.error === "string" ? `host comm closed: ${data2.error}` : "host comm closed"));
160
+ }
161
+ l.pending.clear();
162
+ if (link === l) {
163
+ link = null;
164
+ if (l.ours && l.host === null) noServer = true;
165
+ settle();
166
+ }
167
+ });
168
+ if (l.host !== null) settle();
169
+ }
170
+ function makeHost(l, hello) {
171
+ const methods = Object.freeze([...hello.methods]);
172
+ function call(method, params = {}, { signal = defaultSignal() } = {}) {
173
+ if (signal?.aborted) return Promise.reject(abortError(signal.reason));
174
+ if (l.comm.closed) return Promise.reject(new HostError("disconnected", "host comm closed"));
175
+ const id = nextId++;
176
+ return new Promise((resolve, reject) => {
177
+ const onAbort = () => {
178
+ l.pending.delete(id);
179
+ if (!l.comm.closed) l.comm.send({ id, cancel: true });
180
+ reject(abortError(signal.reason));
181
+ };
182
+ const done = () => signal?.removeEventListener("abort", onAbort);
183
+ const call2 = {
184
+ resolve: (value) => (l.pending.delete(id), done(), resolve(value)),
185
+ reject: (error) => (l.pending.delete(id), done(), reject(error))
186
+ };
187
+ l.pending.set(id, call2);
188
+ signal?.addEventListener("abort", onAbort, { once: true });
189
+ try {
190
+ l.comm.send({ id, method, params });
191
+ } catch (err) {
192
+ call2.reject(new HostError("failed", `cannot send ${method}: ${err instanceof Error ? err.message : err}`));
193
+ }
194
+ });
195
+ }
196
+ const bind = (method) => (params, options) => call(method, params, options);
197
+ return Object.freeze({
198
+ protocol: hello.protocol,
199
+ server: hello.server,
200
+ notebook: hello.notebook,
201
+ methods,
202
+ has: (method) => methods.includes(method),
203
+ call,
204
+ notebooks: Object.freeze({
205
+ list: bind("notebooks.list"),
206
+ read: bind("notebooks.read"),
207
+ search: bind("notebooks.search"),
208
+ results: bind("notebooks.results"),
209
+ run: bind("notebooks.run")
210
+ }),
211
+ self: Object.freeze({
212
+ add: bind("self.add"),
213
+ edit: bind("self.edit"),
214
+ delete: bind("self.delete")
215
+ })
216
+ });
217
+ }
218
+ comms.registerTarget(HOST_TARGET, (comm, msg) => adopt(comm, msg.data));
219
+ async function host2({ timeout = 1e3 } = {}) {
220
+ if (link?.host) return link.host;
221
+ const signal = defaultSignal();
222
+ if (signal?.aborted) throw abortError(signal.reason);
223
+ if (noServer) return null;
224
+ if (link === null) adopt(comms.open(HOST_TARGET), null, true);
225
+ const mine = link;
226
+ return new Promise((resolve, reject) => {
227
+ let timer;
228
+ const onAbort = () => {
229
+ done();
230
+ reject(abortError(signal.reason));
231
+ };
232
+ const done = () => {
233
+ waiters.delete(waiter);
234
+ clearTimeout(timer);
235
+ signal?.removeEventListener("abort", onAbort);
236
+ };
237
+ const waiter = (host3) => {
238
+ done();
239
+ resolve(host3);
240
+ };
241
+ waiters.add(waiter);
242
+ signal?.addEventListener("abort", onAbort, { once: true });
243
+ timer = setTimeout(() => {
244
+ done();
245
+ if (waiters.size === 0 && link === mine && mine.host === null && mine.ours) {
246
+ link = null;
247
+ noServer = true;
248
+ mine.comm.close();
249
+ }
250
+ resolve(null);
251
+ }, timeout);
252
+ });
253
+ }
254
+ return { host: host2 };
255
+ }
256
+
89
257
  // runtime/widget.mjs
90
258
  var WIDGET_PROTOCOL_VERSION = "2.1.0";
91
259
  var WIDGET_TARGET = "jupyter.widget";
@@ -251,7 +419,7 @@ function createWidgetFactory(commApi, $display2) {
251
419
  }
252
420
 
253
421
  // runtime/display.mjs
254
- var $display = Symbol.for("Jupyter.display");
422
+ var $display = /* @__PURE__ */ Symbol.for("Jupyter.display");
255
423
  var rawToEntity = /* @__PURE__ */ new Map([
256
424
  ["&", "&"],
257
425
  ["<", "&lt;"],
@@ -524,6 +692,14 @@ function hasDisplaySymbol(obj) {
524
692
  function isMediaBundle(obj) {
525
693
  return obj !== null && typeof obj === "object" && !Array.isArray(obj);
526
694
  }
695
+ var RESULT_MIME = "application/vnd.ganymede.result+json";
696
+ function deepFreeze(value) {
697
+ if (typeof value === "object" && value !== null) {
698
+ for (const v of Object.values(value)) deepFreeze(v);
699
+ Object.freeze(value);
700
+ }
701
+ return value;
702
+ }
527
703
  function createJupyter(host2) {
528
704
  async function format2(obj) {
529
705
  return await formatNow(obj);
@@ -576,12 +752,12 @@ function createJupyter(host2) {
576
752
  transient: options.display_id ? { display_id: options.display_id } : {}
577
753
  });
578
754
  }
579
- async function broadcastResult(executionCount, result) {
580
- if (result === void 0) return;
755
+ async function broadcastResult(executionCount, result2) {
756
+ if (result2 === void 0) return;
581
757
  try {
582
758
  broadcast("execute_result", {
583
759
  execution_count: executionCount,
584
- data: await format2(result),
760
+ data: await format2(result2),
585
761
  metadata: {},
586
762
  // nbclient crashes on a null `transient`; Deno learned this too.
587
763
  transient: {}
@@ -603,6 +779,38 @@ function createJupyter(host2) {
603
779
  const answer2 = host2.input(message, password);
604
780
  return answer2 === "" ? defaultValue : answer2;
605
781
  }
782
+ let params;
783
+ function readParams() {
784
+ if (params !== void 0) return params;
785
+ const text = host2.params?.();
786
+ if (text === void 0 || text === "") return params = Object.freeze({});
787
+ let value;
788
+ try {
789
+ value = JSON.parse(text);
790
+ } catch (err) {
791
+ throw new TypeError(`GANYMEDE_PARAMS is not JSON: ${err instanceof Error ? err.message : err}`);
792
+ }
793
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
794
+ throw new TypeError('GANYMEDE_PARAMS must be a JSON object, such as {"counter": "Fremont"}');
795
+ }
796
+ return params = deepFreeze(value);
797
+ }
798
+ function result(name, value) {
799
+ if (typeof name !== "string" || name === "") throw new TypeError("jupyter.result: name must be a non-empty string");
800
+ let text;
801
+ try {
802
+ text = JSON.stringify(value);
803
+ } catch (err) {
804
+ throw new TypeError(`jupyter.result: ${name} is not JSON: ${err instanceof Error ? err.message : err}`);
805
+ }
806
+ if (text === void 0) throw new TypeError(`jupyter.result: ${name} is not JSON (${typeof value})`);
807
+ const json = JSON.parse(text);
808
+ broadcast("display_data", {
809
+ data: { [RESULT_MIME]: { name, value: json }, "text/plain": `${name} = ${text}` },
810
+ metadata: {},
811
+ transient: {}
812
+ });
813
+ }
606
814
  const comms = createComms((msgType, content, metadata, buffers) => host2.send(msgType, content, metadata, buffers));
607
815
  let cell = new AbortController();
608
816
  let current = cell;
@@ -616,6 +824,7 @@ function createJupyter(host2) {
616
824
  for (const controller of handlers.keys()) controller.abort(reason);
617
825
  handlers.clear();
618
826
  }
827
+ const hostApi = createHost(comms.api, () => current.signal);
619
828
  async function commDispatch(msgType, content, metadata, buffers, ownSignal = false) {
620
829
  const controller = ownSignal ? new AbortController() : cell;
621
830
  if (ownSignal) current = controller;
@@ -635,6 +844,10 @@ function createJupyter(host2) {
635
844
  },
636
845
  broadcast,
637
846
  display,
847
+ get params() {
848
+ return readParams();
849
+ },
850
+ result,
638
851
  format: format2,
639
852
  md: taggedTemplate("text/markdown"),
640
853
  html: taggedTemplate("text/html"),
@@ -642,6 +855,7 @@ function createJupyter(host2) {
642
855
  image,
643
856
  $display,
644
857
  comm: comms.api,
858
+ host: hostApi.host,
645
859
  widget: createWidgetFactory(comms.api, $display)
646
860
  },
647
861
  broadcastResult,
@@ -990,8 +1204,11 @@ var jupyter = createJupyter({
990
1204
  },
991
1205
  // A synchronous prompt would need SharedArrayBuffer and Atomics.wait, so
992
1206
  // a cross-origin isolated page; until then, as with no stdin.
993
- input: () => null
1207
+ input: () => null,
1208
+ // jupyter.params (#456): the target's `params` option, sent with info.
1209
+ params: () => paramsText
994
1210
  });
1211
+ var paramsText;
995
1212
  var cdn = DEFAULT_CDN;
996
1213
  var hostCalls = /* @__PURE__ */ new Map();
997
1214
  var hostListeners = /* @__PURE__ */ new Set();
@@ -1059,7 +1276,8 @@ async function preload(urls, base) {
1059
1276
  }
1060
1277
  }
1061
1278
  var handle = null;
1062
- function install({ base, cdn: cdnBase }) {
1279
+ function install({ base, cdn: cdnBase, params }) {
1280
+ if (typeof params === "string") paramsText = params;
1063
1281
  if (typeof cdnBase === "string") cdn = cdnBase.endsWith("/") ? cdnBase : cdnBase + "/";
1064
1282
  const url = new URL("$cdpk$repl.mjs", base).href;
1065
1283
  globalThis.__cdpk = {