@basthon/kernel-base 0.64.5 → 0.70.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.
package/lib/commons.d.ts CHANGED
@@ -18,4 +18,12 @@ declare class NotImplementedError extends Error {
18
18
  * - bypass the worker's opaque origin
19
19
  */
20
20
  declare const mockFetch: (legacy: boolean) => void;
21
- export { NotImplementedError, mockFetch };
21
+ /**
22
+ * mock Worker to support sandboxing and keep webpack compatibility
23
+ */
24
+ declare const mockWorker: () => void;
25
+ /**
26
+ * we mock '(new Image()).src' = to support local fs access
27
+ */
28
+ declare const mockHTMLImageSrc: () => void;
29
+ export { NotImplementedError, mockFetch, mockWorker, mockHTMLImageSrc };
package/lib/commons.js CHANGED
@@ -7,18 +7,21 @@ class NotImplementedError extends Error {
7
7
  this.name = "NotImplementedError";
8
8
  }
9
9
  }
10
+ /**
11
+ * Convert an Uint8Array to a data URL.
12
+ */
13
+ const toDataURL = async (content) => {
14
+ return await new Promise((resolve, reject) => {
15
+ const reader = new FileReader();
16
+ reader.onloadend = () => {
17
+ resolve(reader.result);
18
+ };
19
+ reader.onerror = reject;
20
+ reader.readAsDataURL(new Blob([content]));
21
+ });
22
+ };
10
23
  /* fetch from local FS */
11
24
  const fetchFromLocalFS = async (localfsScheme, url) => {
12
- const toDataURL = async (content) => {
13
- return await new Promise((resolve, reject) => {
14
- const reader = new FileReader();
15
- reader.onloadend = () => {
16
- resolve(reader.result);
17
- };
18
- reader.onerror = reject;
19
- reader.readAsDataURL(new Blob([content]));
20
- });
21
- };
22
25
  const prefix = localfsScheme;
23
26
  const path = url.slice(prefix.length);
24
27
  let content = null;
@@ -63,25 +66,34 @@ const mockFetch = (legacy) => {
63
66
  if (self.fetch.name === "mockedFetch")
64
67
  return;
65
68
  const trueFetch = self.fetch;
69
+ const assetsURL = self._assetsURL;
66
70
  async function mockedFetch(request, init) {
67
- if (!legacy && (self.importScripts == null || self._assetsURL == null))
68
- return Response.error();
69
71
  // force all params to init object
70
72
  if (request instanceof Request)
71
73
  init = { ...request, ...init };
72
- // ensure credentials are not sent
73
- init = { ...init, ...{ credentials: "omit" } };
74
74
  const url = request.url ?? request.toString();
75
75
  const localfsScheme = "filesystem:/";
76
+ // in legacy mode, only local FS is mocked
77
+ if (legacy) {
78
+ if (url.startsWith(localfsScheme))
79
+ return await fetchFromLocalFS(localfsScheme, url);
80
+ return await trueFetch(url, init);
81
+ }
82
+ // not a true worker? -> error
83
+ if (self.importScripts == null || assetsURL == null)
84
+ return Response.error();
85
+ // non-legacy mode (true isolated secure worker)
86
+ // ensure credentials are not sent
87
+ init = { ...init, ...{ credentials: "omit" } };
76
88
  try {
77
89
  // requests towards localfs
78
90
  if (url.startsWith(localfsScheme)) {
79
91
  return await fetchFromLocalFS(localfsScheme, url);
80
92
  }
81
- else if (!legacy && origin(url) === origin(self._assetsURL)) {
93
+ else if (origin(url) === origin(assetsURL)) {
82
94
  // requests towards same origin
83
95
  // only url under the assets folder are proxyied
84
- if (parent(url).startsWith(self._assetsURL)) {
96
+ if (parent(url).startsWith(assetsURL)) {
85
97
  const proxy = self._remoteProxy;
86
98
  const { body, options } = await proxy.fetch(url, init);
87
99
  return new Response(body, options);
@@ -97,4 +109,66 @@ const mockFetch = (legacy) => {
97
109
  }
98
110
  self.fetch = mockedFetch;
99
111
  };
100
- export { NotImplementedError, mockFetch };
112
+ /**
113
+ * mock Worker to support sandboxing and keep webpack compatibility
114
+ */
115
+ const mockWorker = () => {
116
+ // already mocked?
117
+ if (globalThis.Worker.name === "SandboxedWorker")
118
+ return;
119
+ class SandboxedWorker extends Worker {
120
+ constructor(url, options) {
121
+ if (options?.sandboxed === true) {
122
+ const absURL = new URL(url).href;
123
+ const assetsURL = absURL.substring(0, absURL.lastIndexOf("/"));
124
+ // worker created with a 'data' url scheme has opaque origin
125
+ // https://html.spec.whatwg.org/multipage/workers.html#script-settings-for-workers:concept-origin-opaque
126
+ const workerScript = `
127
+ self._assetsURL = "${assetsURL}";
128
+ globalThis = {importScripts: self.importScripts, location: "${url}"};
129
+ importScripts("${url}");
130
+ globalThis = self;
131
+ `.replace(/\s/g, "");
132
+ super(`data:text/javascript;base64,${btoa(workerScript)}`);
133
+ }
134
+ else {
135
+ super(url, options);
136
+ }
137
+ }
138
+ }
139
+ globalThis.Worker = SandboxedWorker;
140
+ };
141
+ /**
142
+ * we mock '(new Image()).src' = to support local fs access
143
+ */
144
+ const mockHTMLImageSrc = () => {
145
+ // inside main thread?
146
+ if (globalThis?.HTMLImageElement == null)
147
+ return;
148
+ const desc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, "src");
149
+ // already mocked?
150
+ if (desc?.set?.name === "mockedSrcSet")
151
+ return;
152
+ function mockedSrcSet(e) {
153
+ const prefix = "filesystem:/";
154
+ if (e.startsWith(prefix)) {
155
+ const path = e.slice(prefix.length);
156
+ (async () => {
157
+ //@ts-ignore
158
+ const content = (await globalThis.Basthon?.getFile?.(path))?.buffer;
159
+ desc?.set?.call(this, await toDataURL(content));
160
+ })();
161
+ }
162
+ else {
163
+ desc?.set?.call(this, e);
164
+ }
165
+ }
166
+ Object.defineProperty(HTMLImageElement.prototype, "src", {
167
+ ...desc,
168
+ get: function () {
169
+ return desc?.get?.call(this);
170
+ },
171
+ set: mockedSrcSet,
172
+ });
173
+ };
174
+ export { NotImplementedError, mockFetch, mockWorker, mockHTMLImageSrc };
package/lib/kernel.d.ts CHANGED
@@ -49,15 +49,15 @@ export declare class KernelBase {
49
49
  evalAsync(code: string, outCallback: (_: string) => void, errCallback: (_: string) => void, data?: any): Promise<any>;
50
50
  putFile(filename: string, content: ArrayBuffer): Promise<void>;
51
51
  putModule(filename: string, content: ArrayBuffer): Promise<void>;
52
- userModules(): string[];
52
+ userModules(): Promise<string[]>;
53
53
  /**
54
54
  * Get a file content from the VFS.
55
55
  */
56
- getFile(path: string): Uint8Array;
56
+ getFile(path: string): Promise<Uint8Array>;
57
57
  /**
58
58
  * Get a user module file content.
59
59
  */
60
- getUserModuleFile(filename: string): Uint8Array;
60
+ getUserModuleFile(filename: string): Promise<Uint8Array>;
61
61
  more(source: string): Promise<boolean>;
62
62
  complete(code: string): Promise<[string[], number] | []>;
63
63
  banner(): string;
@@ -100,9 +100,13 @@ export declare class KernelBase {
100
100
  * Promise that resolves when the kernel is ready (started).
101
101
  */
102
102
  ready(): Promise<void>;
103
+ /**
104
+ * Root path for assets files.
105
+ */
106
+ assetsURL(absolute?: boolean): string;
103
107
  /**
104
108
  * Root for kernel files. This is always the language directory
105
- * inside the version number directory inside the kernel directory.
109
+ * inside the version number directory inside the assets directory.
106
110
  */
107
111
  basthonRoot(absolute?: boolean): string;
108
112
  /**
@@ -110,10 +114,6 @@ export declare class KernelBase {
110
114
  * (opening browser dialog).
111
115
  */
112
116
  download(data: Uint8Array | string, filename: string): void;
113
- /**
114
- * Dynamically load a script asynchronously.
115
- */
116
- static loadScript(url: string): Promise<any>;
117
117
  /**
118
118
  * Wrapper around document.dispatchEvent.
119
119
  * It adds the 'basthon.' prefix to each event name and
package/lib/kernel.js CHANGED
@@ -1,27 +1,6 @@
1
1
  import { PromiseDelegate } from "promise-delegate";
2
2
  import { VERSION } from "./version";
3
3
  import { NotImplementedError } from "./commons";
4
- // const desc = Object.getOwnPropertyDescriptor(
5
- // HTMLImageElement.prototype,
6
- // "src"
7
- // );
8
- // Object.defineProperty(HTMLImageElement.prototype, "src", {
9
- // ...desc,
10
- // get: function () {
11
- // return desc?.get?.call(this);
12
- // },
13
- // set: function (e) {
14
- // const prefix = _localfsScheme;
15
- // if (e.startsWith(prefix)) {
16
- // const path = e.slice(prefix.length);
17
- // //@ts-ignore
18
- // const content = window.Basthon.getFile(path).buffer;
19
- // (async () => desc?.set?.call(this, await toDataURL(content)))();
20
- // } else {
21
- // desc?.set?.call(this, e);
22
- // }
23
- // },
24
- // });
25
4
  /**
26
5
  * Event for Basthon's dispatch/listen.
27
6
  */
@@ -101,19 +80,19 @@ export class KernelBase {
101
80
  async putModule(filename, content) {
102
81
  this._modules.set(filename, content);
103
82
  }
104
- userModules() {
83
+ async userModules() {
105
84
  return [];
106
85
  }
107
86
  /**
108
87
  * Get a file content from the VFS.
109
88
  */
110
- getFile(path) {
89
+ async getFile(path) {
111
90
  throw new NotImplementedError("getFile");
112
91
  }
113
92
  /**
114
93
  * Get a user module file content.
115
94
  */
116
- getUserModuleFile(filename) {
95
+ async getUserModuleFile(filename) {
117
96
  throw new NotImplementedError("getUserModuleFile");
118
97
  }
119
98
  async more(source) {
@@ -147,7 +126,7 @@ export class KernelBase {
147
126
  }
148
127
  catch (error) {
149
128
  this._ready.reject(error);
150
- return;
129
+ throw error;
151
130
  }
152
131
  // connecting eval to basthon.eval.request event.
153
132
  this.addEventListener("eval.request", this.evalFromEvent.bind(this));
@@ -168,7 +147,7 @@ export class KernelBase {
168
147
  }
169
148
  catch (error) {
170
149
  this._ready.reject(error);
171
- return;
150
+ throw error;
172
151
  }
173
152
  this._files.forEach((v, k) => this.putFile(k, v));
174
153
  this._modules.forEach((v, k) => this.putModule(k, v));
@@ -208,17 +187,23 @@ export class KernelBase {
208
187
  return this._ready.promise;
209
188
  }
210
189
  /**
211
- * Root for kernel files. This is always the language directory
212
- * inside the version number directory inside the kernel directory.
190
+ * Root path for assets files.
213
191
  */
214
- basthonRoot(absolute = false) {
215
- let url = this._rootPath + "/" + this.version() + "/" + this.language();
192
+ assetsURL(absolute = false) {
193
+ let url = this._rootPath;
216
194
  if (absolute && !url.startsWith("http")) {
217
195
  const base = window.location.origin + window.location.pathname;
218
196
  url = base.substring(0, base.lastIndexOf("/")) + "/" + url;
219
197
  }
220
198
  return url;
221
199
  }
200
+ /**
201
+ * Root for kernel files. This is always the language directory
202
+ * inside the version number directory inside the assets directory.
203
+ */
204
+ basthonRoot(absolute = false) {
205
+ return (this.assetsURL(absolute) + "/" + this.version() + "/" + this.language());
206
+ }
222
207
  /**
223
208
  * Downloading data (bytes array or data URL) as filename
224
209
  * (opening browser dialog).
@@ -237,18 +222,6 @@ export class KernelBase {
237
222
  anchor.click();
238
223
  document.body.removeChild(anchor);
239
224
  }
240
- /**
241
- * Dynamically load a script asynchronously.
242
- */
243
- static loadScript(url) {
244
- return new Promise(function (resolve, reject) {
245
- let script = document.createElement("script");
246
- script.onload = resolve;
247
- script.onerror = reject;
248
- script.src = url;
249
- document.head.appendChild(script);
250
- });
251
- }
252
225
  /**
253
226
  * Wrapper around document.dispatchEvent.
254
227
  * It adds the 'basthon.' prefix to each event name and
package/lib/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.64.5";
1
+ export declare const VERSION = "0.70.0";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.64.5";
1
+ export const VERSION = "0.70.0";
@@ -4,13 +4,20 @@ import { KernelBase } from "../kernel";
4
4
  export declare class KernelMainBase<Type extends KernelWorkerBase> extends KernelBase {
5
5
  private _worker?;
6
6
  private _remote?;
7
- private _legacy;
7
+ private readonly _legacy;
8
+ private postMessageToWorker?;
8
9
  private _sab?;
10
+ private _channelSyncer;
9
11
  constructor(options: any);
10
12
  /**
11
13
  * Is this a legacy worker? i.e. not runing in a worker.
12
14
  */
13
15
  isLegacy(): boolean;
16
+ /**
17
+ * Is synchrnous communication with worker available?
18
+ * (through SharedArrayBuffer atomic waits)
19
+ */
20
+ syncCommSupport(): boolean;
14
21
  /**
15
22
  * Is this kernel safe? (loaded in an isolated web worker)
16
23
  */
@@ -1,33 +1,19 @@
1
1
  import { wrap, createEndpoint, transfer } from "comlink";
2
- import { NotImplementedError } from "../commons";
2
+ import { NotImplementedError, mockWorker, mockHTMLImageSrc } from "../commons";
3
3
  import { KernelBase } from "../kernel";
4
+ import { PromiseDelegate } from "promise-delegate";
4
5
  // mock Worker to support sandboxing and keep webpack compatibility
5
- if (globalThis.Worker.name !== "SandboxedWorker") {
6
- class SandboxedWorker extends Worker {
7
- constructor(url, options) {
8
- if (options?.sandboxed === true) {
9
- const absURL = new URL(url).href;
10
- const assetsURL = absURL.substring(0, absURL.lastIndexOf("/"));
11
- // worker created with a 'data' url scheme has opaque origin
12
- // https://html.spec.whatwg.org/multipage/workers.html#script-settings-for-workers:concept-origin-opaque
13
- const workerScript = `self._assetsURL = "${assetsURL}";
14
- globalThis = {importScripts: self.importScripts, location: "${url}"};
15
- importScripts("${url}");
16
- globalThis = self;`.replace(/\s/g, "");
17
- super(`data:text/javascript;base64,${btoa(workerScript)}`);
18
- }
19
- else {
20
- super(url, options);
21
- }
22
- }
23
- }
24
- globalThis.Worker = SandboxedWorker;
25
- }
6
+ mockWorker();
7
+ // mock HTMLImageElement.src to support localfs access
8
+ mockHTMLImageSrc();
26
9
  export class KernelMainBase extends KernelBase {
27
10
  constructor(options) {
28
11
  super(options);
29
- this._legacy = false;
12
+ // synchronise comlink and communication channels
13
+ this._channelSyncer = new PromiseDelegate();
30
14
  this._legacy = options?.legacy === true;
15
+ // synchronous wait on SAB cannot be used in a legacy context
16
+ // (atomic wait will block the main thread)
31
17
  if (!this.isLegacy() && globalThis.crossOriginIsolated)
32
18
  this._sab = new SharedArrayBuffer(1024);
33
19
  }
@@ -37,6 +23,13 @@ export class KernelMainBase extends KernelBase {
37
23
  isLegacy() {
38
24
  return this._legacy;
39
25
  }
26
+ /**
27
+ * Is synchrnous communication with worker available?
28
+ * (through SharedArrayBuffer atomic waits)
29
+ */
30
+ syncCommSupport() {
31
+ return this._sab != null;
32
+ }
40
33
  /**
41
34
  * Is this kernel safe? (loaded in an isolated web worker)
42
35
  */
@@ -64,6 +57,8 @@ export class KernelMainBase extends KernelBase {
64
57
  // running in main thread via message channel (fake worker)
65
58
  //@ts-ignore
66
59
  self._commPort = comlinkChannel.port1;
60
+ // this global variable will be removed by worker
61
+ self._assetsURL = this.assetsURL(true);
67
62
  await this.importLegacyWorker();
68
63
  }
69
64
  else {
@@ -79,9 +74,10 @@ export class KernelMainBase extends KernelBase {
79
74
  //@ts-ignore
80
75
  this._remote = await new KernelWorker(this._options);
81
76
  // setup the communication channel
82
- const commChannel = new MessageChannel();
83
- commChannel.port2.onmessage = (e) => this.processWorkerMessage(e.data);
84
- await this.remote?.setCommPort(transfer(commChannel.port1, [commChannel.port1]));
77
+ const { port1, port2 } = new MessageChannel();
78
+ port2.onmessage = (e) => this.processWorkerMessage(e.data);
79
+ this.postMessageToWorker = port2.postMessage.bind(port2);
80
+ await this.remote?.setCommPort(transfer(port1, [port1]));
85
81
  // WARNING: without the setCommPort call, KernelWorker (proxy)
86
82
  // is garbage collected and comlink releases the proxy,
87
83
  // breaking all the communications... (don't know really why)
@@ -93,6 +89,11 @@ export class KernelMainBase extends KernelBase {
93
89
  */
94
90
  async _start() {
95
91
  await super._start();
92
+ // kernel restart in legacy mode is lazy, not a full restart
93
+ if (this.isLegacy() && this.remote != null) {
94
+ await this.remote?.legacyStart();
95
+ return;
96
+ }
96
97
  // setup the proxy worker
97
98
  const proxyWorker = new Worker(new URL("./comlink-proxy.js", import.meta.url), {
98
99
  type: "module",
@@ -100,7 +101,7 @@ export class KernelMainBase extends KernelBase {
100
101
  const proxy = wrap(proxyWorker);
101
102
  // setup the kernel worker
102
103
  await this.setupWorker();
103
- if (this._sab != null) {
104
+ if (this.syncCommSupport()) {
104
105
  console.log("Using SharedArrayBuffer for synchronous worker communication.");
105
106
  await this._remote?.setSAB(this._sab);
106
107
  }
@@ -120,9 +121,14 @@ export class KernelMainBase extends KernelBase {
120
121
  * Stop the worker.
121
122
  */
122
123
  async _stop() {
123
- this._worker?.terminate();
124
- this._worker = undefined;
125
- this._remote = undefined;
124
+ if (this.isLegacy()) {
125
+ await this.remote?.legacyStop();
126
+ }
127
+ else {
128
+ this._worker?.terminate();
129
+ this._worker = undefined;
130
+ this._remote = undefined;
131
+ }
126
132
  await super._stop();
127
133
  }
128
134
  /**
@@ -130,6 +136,10 @@ export class KernelMainBase extends KernelBase {
130
136
  */
131
137
  async processWorkerMessage(msg) {
132
138
  switch (msg.type) {
139
+ case "eval-end":
140
+ this._channelSyncer.resolve();
141
+ this._channelSyncer = new PromiseDelegate();
142
+ break;
133
143
  case "stream":
134
144
  const { stream, text } = msg.content;
135
145
  const dataEvent = this.clone(msg.data);
@@ -158,11 +168,25 @@ export class KernelMainBase extends KernelBase {
158
168
  case "input":
159
169
  const { prompt, password, data } = msg.content;
160
170
  const res = (await this.inputAsync(prompt, password, data));
161
- if (this._sab == null)
162
- return;
163
- const encodedRes = new TextEncoder().encode(res);
164
- new Uint8Array(this._sab).set(encodedRes.subarray(0, this._sab.byteLength));
165
- Atomics.notify(new Int32Array(this._sab), 0, 1);
171
+ if (msg.content.toplevel === true) {
172
+ this.postMessageToWorker({
173
+ type: "toplevel-input",
174
+ content: res,
175
+ data,
176
+ });
177
+ }
178
+ else if (this.syncCommSupport()) {
179
+ const encodedRes = new TextEncoder().encode(res);
180
+ new Uint8Array(this._sab).set(encodedRes.subarray(0, this._sab.byteLength));
181
+ Atomics.notify(new Int32Array(this._sab), 0, 1);
182
+ }
183
+ break;
184
+ case "breakpoint-move-on":
185
+ //@ts-ignore
186
+ const notebook = Jupyter?.notebook;
187
+ const msg_id = msg.content.data?.parent_msg?.header?.msg_id;
188
+ const index = notebook?.last_msg_id_to_cell_index?.(msg_id);
189
+ notebook?.next_breakpoint_cell?.(index);
166
190
  break;
167
191
  default: // ignoring (probably a comlink message)
168
192
  }
@@ -171,8 +195,12 @@ export class KernelMainBase extends KernelBase {
171
195
  // force interactivity in all modes
172
196
  data.interactive = true;
173
197
  this._execution_count++;
198
+ // to be sure we do not miss communications
199
+ const syncChannels = this._channelSyncer.promise;
174
200
  // evaluation
175
201
  const execResult = (await this.remote?.eval(data, code));
202
+ // wait for all communications to end
203
+ await syncChannels;
176
204
  // return result
177
205
  let result;
178
206
  switch (execResult?.status) {
@@ -2,6 +2,7 @@ import { Remote } from "comlink";
2
2
  import { ProxyFetch } from "./proxy";
3
3
  declare global {
4
4
  interface DedicatedWorkerGlobalScope {
5
+ _assetsURL?: string;
5
6
  _remoteProxy: Remote<ProxyFetch>;
6
7
  basthon: KernelWorkerBase;
7
8
  _prompt: (p?: string, _d?: string) => string | null;
@@ -17,21 +18,50 @@ type ExecuteResult = {
17
18
  };
18
19
  declare class KernelWorkerBase {
19
20
  protected _options: any;
20
- protected _legacy: boolean;
21
+ protected readonly _legacy: boolean;
22
+ private readonly _basthonRoot;
21
23
  private _ready;
22
24
  protected __eval_data__: any;
23
25
  protected postMessage?: InstanceType<typeof MessagePort>["postMessage"];
26
+ private _evalPromises;
24
27
  private _sab?;
25
28
  private _domNodeBus?;
29
+ private _toplevelInputPromise?;
26
30
  constructor(options?: any);
31
+ /**
32
+ * Language implemented in the kernel (string).
33
+ * Generally lower case.
34
+ */
35
+ language(): string;
27
36
  /**
28
37
  * Is this a legacy worker? i.e. not runing in a worker.
29
38
  */
30
39
  isLegacy(): boolean;
40
+ /**
41
+ * The basthon's root directory.
42
+ */
43
+ basthonRoot(): string;
44
+ /**
45
+ * Is synchronous communication with main thread available?
46
+ * (through SharedArrayBuffer atomic waits)
47
+ */
48
+ syncCommSupport(): boolean;
49
+ /**
50
+ * Special case of starting a legacy kernel.
51
+ */
52
+ legacyStart(): Promise<void>;
53
+ /**
54
+ * Special case of stoping a legacy kernel.
55
+ */
56
+ legacyStop(): Promise<void>;
31
57
  /**
32
58
  * Set SharedArrayBuffer from main thread.
33
59
  */
34
60
  setSAB(sab: SharedArrayBuffer): void;
61
+ /**
62
+ * Add a promise we should wait for during eval process.
63
+ */
64
+ protected addEvalPromise(promise: Promise<void>): void;
35
65
  protected _init(): Promise<void>;
36
66
  /**
37
67
  * Initialize the kernel. It is called by initWorker in KernelBase.
@@ -133,5 +163,22 @@ declare class KernelWorkerBase {
133
163
  * an eval.input event to the main thread and wait for the response.
134
164
  */
135
165
  input(prompt: string | null | undefined, password?: boolean): string;
166
+ /**
167
+ * Process messages from the main thread.
168
+ */
169
+ protected processMainThreadMessage(msg: {
170
+ data: any;
171
+ type: string;
172
+ content: any;
173
+ }): Promise<void>;
174
+ /**
175
+ * Ask for an input in the main thread and wait for the answer
176
+ * then return it.
177
+ */
178
+ toplevelInput(prompt: string | null | undefined, password?: boolean, data?: any): Promise<string>;
179
+ /**
180
+ * Ask main thread to go to the next breakpoint.
181
+ */
182
+ breakpointMoveOn(): void;
136
183
  }
137
184
  export { ExecuteResult, KernelWorkerBase };
@@ -1,12 +1,15 @@
1
1
  import { PromiseDelegate } from "promise-delegate";
2
2
  import { NotImplementedError, mockFetch } from "../commons";
3
3
  import { wrap } from "comlink";
4
+ import { VERSION } from "../version";
4
5
  import { DOMNodeBus } from "./worker-utils";
5
6
  class KernelWorkerBase {
6
7
  constructor(options) {
7
8
  this._ready = new PromiseDelegate();
9
+ this._evalPromises = []; // used to wait all promises emitted during computation
8
10
  this._options = options;
9
11
  this._legacy = options?.legacy === true;
12
+ this._basthonRoot = self._assetsURL + "/" + VERSION + "/" + this.language();
10
13
  /* we mock the fetch function to:
11
14
  * - redirect queries to local FS
12
15
  * - bypass the worker's opaque origin
@@ -36,6 +39,15 @@ class KernelWorkerBase {
36
39
  // (to transfer elements from worker to main thread, they are the same!)
37
40
  if (this.isLegacy())
38
41
  this._domNodeBus = new DOMNodeBus();
42
+ // not needed anymore
43
+ self._assetsURL = undefined;
44
+ }
45
+ /**
46
+ * Language implemented in the kernel (string).
47
+ * Generally lower case.
48
+ */
49
+ language() {
50
+ throw new NotImplementedError("language");
39
51
  }
40
52
  /**
41
53
  * Is this a legacy worker? i.e. not runing in a worker.
@@ -43,12 +55,43 @@ class KernelWorkerBase {
43
55
  isLegacy() {
44
56
  return this._legacy;
45
57
  }
58
+ /**
59
+ * The basthon's root directory.
60
+ */
61
+ basthonRoot() {
62
+ return this._basthonRoot;
63
+ }
64
+ /**
65
+ * Is synchronous communication with main thread available?
66
+ * (through SharedArrayBuffer atomic waits)
67
+ */
68
+ syncCommSupport() {
69
+ return this._sab != null;
70
+ }
71
+ /**
72
+ * Special case of starting a legacy kernel.
73
+ */
74
+ async legacyStart() {
75
+ throw new NotImplementedError("legacyStart");
76
+ }
77
+ /**
78
+ * Special case of stoping a legacy kernel.
79
+ */
80
+ async legacyStop() {
81
+ throw new NotImplementedError("legacyStop");
82
+ }
46
83
  /**
47
84
  * Set SharedArrayBuffer from main thread.
48
85
  */
49
86
  setSAB(sab) {
50
87
  this._sab = sab;
51
88
  }
89
+ /**
90
+ * Add a promise we should wait for during eval process.
91
+ */
92
+ addEvalPromise(promise) {
93
+ this._evalPromises.push(promise);
94
+ }
52
95
  /*
53
96
  * Initialize the kernel. Should be overloaded.
54
97
  */
@@ -61,8 +104,9 @@ class KernelWorkerBase {
61
104
  await this._init();
62
105
  this._ready.resolve();
63
106
  }
64
- catch (e) {
65
- this._ready.reject(e);
107
+ catch (error) {
108
+ this._ready.reject(error);
109
+ throw error;
66
110
  }
67
111
  }
68
112
  /**
@@ -76,6 +120,7 @@ class KernelWorkerBase {
76
120
  */
77
121
  setCommPort(port) {
78
122
  this.postMessage = port.postMessage.bind(port);
123
+ port.onmessage = (e) => this.processMainThreadMessage(e.data);
79
124
  }
80
125
  /**
81
126
  * Set the comlink communication port to proxy fetch.
@@ -137,9 +182,10 @@ class KernelWorkerBase {
137
182
  const data = this.clone(this.__eval_data__);
138
183
  if (globalThis.OffscreenCanvas != null &&
139
184
  element instanceof OffscreenCanvas) {
140
- element
141
- .convertToBlob()
142
- .then((blob) => this.displayBlob(blob, data));
185
+ this.addEvalPromise((async () => {
186
+ const blob = await element.convertToBlob();
187
+ await this.displayBlob(blob, data);
188
+ })());
143
189
  }
144
190
  else {
145
191
  // in legacy mode, send HTML elements via DOM node bus
@@ -180,15 +226,17 @@ class KernelWorkerBase {
180
226
  * Evaluate a string of code. To be called by main thread (through comlink).
181
227
  */
182
228
  async eval(data, code) {
229
+ this._evalPromises = [];
183
230
  this.__eval_data__ = data;
231
+ let res;
184
232
  try {
185
233
  const result = await this._eval(data, code);
186
- return { data, status: "ok", result };
234
+ res = { data, status: "ok", result };
187
235
  }
188
236
  catch (e) {
189
237
  const { name, stack, message } = e;
190
238
  this.sendStderrStream(data, e.toString());
191
- return {
239
+ res = {
192
240
  data,
193
241
  status: "error",
194
242
  error: {
@@ -198,6 +246,9 @@ class KernelWorkerBase {
198
246
  },
199
247
  };
200
248
  }
249
+ this.postMessage?.({ type: "eval-end" }); // for channel syncing
250
+ await Promise.all(this._evalPromises);
251
+ return res;
201
252
  }
202
253
  /**
203
254
  * Put a file on the local (emulated) filesystem.
@@ -247,15 +298,15 @@ class KernelWorkerBase {
247
298
  */
248
299
  sleep(duration) {
249
300
  duration = duration * 1000; // to milliseconds
250
- if (this._sab != null) {
301
+ if (this.syncCommSupport()) {
251
302
  // ensure value before waiting
252
303
  Atomics.store(new Int32Array(this._sab), 0, 0);
253
304
  Atomics.wait(new Int32Array(this._sab), 0, 0, duration);
254
305
  return;
255
306
  }
256
307
  else {
308
+ // default to active sleep
257
309
  const t0 = Date.now();
258
- // active sleep
259
310
  while (Date.now() - t0 < duration)
260
311
  ;
261
312
  }
@@ -288,7 +339,7 @@ class KernelWorkerBase {
288
339
  * an eval.input event to the main thread and wait for the response.
289
340
  */
290
341
  input(prompt, password = false) {
291
- if (this._sab != null) {
342
+ if (this.syncCommSupport()) {
292
343
  const data = this.clone(this.__eval_data__);
293
344
  this.postMessage?.({
294
345
  type: "input",
@@ -299,9 +350,47 @@ class KernelWorkerBase {
299
350
  Atomics.wait(new Int32Array(this._sab), 0, 0);
300
351
  return this.decodeSharedArrayBuffer(this._sab);
301
352
  }
302
- else {
353
+ else if (self._prompt != null) {
354
+ // in legacy context, we default to js' prompt
303
355
  return self._prompt(prompt ?? "") ?? "";
304
356
  }
357
+ else {
358
+ // basthon's API not yet impelemented
359
+ throw new Error("Your browser does not support SharedArrayBuffer. Please try to update it.");
360
+ }
361
+ }
362
+ /**
363
+ * Process messages from the main thread.
364
+ */
365
+ async processMainThreadMessage(msg) {
366
+ switch (msg.type) {
367
+ case "toplevel-input":
368
+ this._toplevelInputPromise?.resolve(msg.content);
369
+ break;
370
+ default: // this should not append...
371
+ break;
372
+ }
373
+ }
374
+ /**
375
+ * Ask for an input in the main thread and wait for the answer
376
+ * then return it.
377
+ */
378
+ async toplevelInput(prompt, password = false, data = undefined) {
379
+ this._toplevelInputPromise = new PromiseDelegate();
380
+ this.postMessage?.({
381
+ type: "input",
382
+ content: { data, prompt, password, toplevel: true },
383
+ });
384
+ const res = await this._toplevelInputPromise.promise;
385
+ this._toplevelInputPromise = undefined;
386
+ return res;
387
+ }
388
+ /**
389
+ * Ask main thread to go to the next breakpoint.
390
+ */
391
+ breakpointMoveOn() {
392
+ const data = this.clone(this.__eval_data__);
393
+ this.postMessage?.({ type: "breakpoint-move-on", content: { data } });
305
394
  }
306
395
  }
307
396
  export { KernelWorkerBase };
@@ -3,12 +3,12 @@ declare global {
3
3
  _commPort?: MessagePort;
4
4
  }
5
5
  }
6
- export declare const expose: (kwc: any) => void;
6
+ declare const expose: (kwc: any) => void;
7
7
  /**
8
8
  * DOM node exchanger (Bus) to bypass stringifying
9
9
  * messages between frontend and kernel that prevents DOMNode sharing.
10
10
  */
11
- export declare class DOMNodeBus extends Map<number, any> {
11
+ declare class DOMNodeBus extends Map<number, any> {
12
12
  /**
13
13
  * Push an object to the bus and get back an id to later pop it.
14
14
  */
@@ -18,3 +18,8 @@ export declare class DOMNodeBus extends Map<number, any> {
18
18
  */
19
19
  pop(id: number): any;
20
20
  }
21
+ /**
22
+ * Dynamically load a script (support main thread and worker usage).
23
+ */
24
+ declare const loadScript: (url: string) => Promise<void>;
25
+ export { expose, DOMNodeBus, loadScript };
@@ -2,7 +2,7 @@ import { expose as exposeComlink } from "comlink";
2
2
  const insideWorker = () =>
3
3
  //@ts-ignore
4
4
  typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope;
5
- export const expose = (kwc) => {
5
+ const expose = (kwc) => {
6
6
  if (insideWorker()) {
7
7
  // worker
8
8
  const listener = (msg) => {
@@ -23,7 +23,7 @@ export const expose = (kwc) => {
23
23
  * DOM node exchanger (Bus) to bypass stringifying
24
24
  * messages between frontend and kernel that prevents DOMNode sharing.
25
25
  */
26
- export class DOMNodeBus extends Map {
26
+ class DOMNodeBus extends Map {
27
27
  /**
28
28
  * Push an object to the bus and get back an id to later pop it.
29
29
  */
@@ -44,3 +44,19 @@ export class DOMNodeBus extends Map {
44
44
  return res;
45
45
  }
46
46
  }
47
+ /**
48
+ * Dynamically load a script (support main thread and worker usage).
49
+ */
50
+ const loadScript = async (url) => {
51
+ if (globalThis.importScripts != null)
52
+ return importScripts(url);
53
+ // we are probably in main thread
54
+ return new Promise((resolve, reject) => {
55
+ const script = document.createElement("script");
56
+ script.onload = resolve;
57
+ script.onerror = reject;
58
+ script.src = url;
59
+ document.head.appendChild(script);
60
+ });
61
+ };
62
+ export { expose, DOMNodeBus, loadScript };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basthon/kernel-base",
3
- "version": "0.64.5",
3
+ "version": "0.70.0",
4
4
  "description": "Basthon - Base Kernel",
5
5
  "homepage": "https://basthon.fr",
6
6
  "bugs": {
@@ -39,10 +39,10 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "json": "^11.0.0",
42
- "typescript": "^5.4.5"
42
+ "typescript": "~5.4.5"
43
43
  },
44
44
  "publishConfig": {
45
45
  "access": "public"
46
46
  },
47
- "gitHead": "4380b6e71e6c575efda6271a6c9e495aaf16f88c"
47
+ "gitHead": "ff1c44d9fb90b0d8f3554de858a2ad01a7360a52"
48
48
  }