@basthon/kernel-base 0.62.20 → 0.62.22

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,21 @@
1
+ import { Remote } from "comlink";
2
+ import { ProxyFetch } from "./worker/proxy";
3
+ declare global {
4
+ interface Window {
5
+ _remoteProxy: Remote<ProxyFetch>;
6
+ _assetsURL?: string;
7
+ }
8
+ }
9
+ /**
10
+ * An error thrown by not implemented API functions.
11
+ */
12
+ declare class NotImplementedError extends Error {
13
+ constructor(funcName: string);
14
+ }
15
+ /**
16
+ * we mock the fetch function to:
17
+ * - redirect queries to local FS
18
+ * - bypass the worker's opaque origin
19
+ */
20
+ declare const mockFetch: () => void;
21
+ export { NotImplementedError, mockFetch };
package/lib/commons.js ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * An error thrown by not implemented API functions.
3
+ */
4
+ class NotImplementedError extends Error {
5
+ constructor(funcName) {
6
+ super(`Function ${funcName} not implemented!`);
7
+ this.name = "NotImplementedError";
8
+ }
9
+ }
10
+ /* fetch from local FS */
11
+ 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
+ const prefix = localfsScheme;
23
+ const path = url.slice(prefix.length);
24
+ let content = null;
25
+ const options = {
26
+ status: 200,
27
+ statusText: "OK",
28
+ headers: new Headers(),
29
+ };
30
+ try {
31
+ //@ts-ignore
32
+ content = self.Basthon.getFile(path);
33
+ const dataURL = await toDataURL(content);
34
+ const mime = dataURL.substring(dataURL.indexOf(":") + 1, dataURL.indexOf(";"));
35
+ content = content.buffer;
36
+ options.headers.append("Content-Type", mime);
37
+ options.headers.append("Content-Length", content.byteLength);
38
+ }
39
+ catch (e) {
40
+ options.status = 404;
41
+ options.statusText = "Not Found";
42
+ }
43
+ return new Response(content, options);
44
+ };
45
+ /**
46
+ * Get the origin of an url.
47
+ */
48
+ const origin = (url) => new URL(url).origin;
49
+ /**
50
+ * Get the parent path of an url.
51
+ */
52
+ const parent = (url) => {
53
+ const abs = new URL(url).href;
54
+ return abs.substring(0, abs.lastIndexOf("/"));
55
+ };
56
+ /**
57
+ * we mock the fetch function to:
58
+ * - redirect queries to local FS
59
+ * - bypass the worker's opaque origin
60
+ */
61
+ const mockFetch = () => {
62
+ // already mocked?
63
+ if (self.fetch.name === "mockedFetch")
64
+ return;
65
+ const trueFetch = self.fetch;
66
+ async function mockedFetch(request, init) {
67
+ if (self.importScripts == null || self._assetsURL == null)
68
+ return Response.error();
69
+ // force all params to init object
70
+ if (request instanceof Request)
71
+ init = { ...request, ...init };
72
+ // ensure credentials are not sent
73
+ init = { ...init, ...{ credentials: "omit" } };
74
+ const url = request.url ?? request.toString();
75
+ const localfsScheme = "filesystem:/";
76
+ try {
77
+ // requests towards localfs
78
+ if (url.startsWith(localfsScheme)) {
79
+ return await fetchFromLocalFS(localfsScheme, url);
80
+ }
81
+ else if (origin(url) === origin(self._assetsURL)) {
82
+ // requests towards same origin
83
+ // only url under the assets folder are proxyied
84
+ if (parent(url).startsWith(self._assetsURL)) {
85
+ const proxy = self._remoteProxy;
86
+ const { body, options } = await proxy.fetch(url, init);
87
+ return new Response(body, options);
88
+ }
89
+ throw new Error("Ressource is outside the proxy' scope");
90
+ }
91
+ return await trueFetch(url, init);
92
+ }
93
+ catch (e) {
94
+ console.error(`security proxy: request to ${url} throw error: ${e.toString()}`);
95
+ return Response.error();
96
+ }
97
+ }
98
+ self.fetch = mockedFetch;
99
+ };
100
+ export { NotImplementedError, mockFetch };
package/lib/index.d.ts CHANGED
@@ -1 +1 @@
1
- export * from './kernel';
1
+ export * from "./kernel";
package/lib/index.js CHANGED
@@ -1 +1 @@
1
- export * from './kernel';
1
+ export * from "./kernel";
package/lib/kernel.d.ts CHANGED
@@ -1,15 +1,26 @@
1
+ import { PromiseDelegate } from "promise-delegate";
2
+ import { VERSION } from "./version";
3
+ export { VERSION };
1
4
  /**
2
5
  * API that any Basthon kernel should fill to be supported
3
6
  * in console/notebook.
4
7
  */
5
8
  export declare class KernelBase {
6
9
  private _rootPath;
10
+ private _isReady;
7
11
  private _ready;
8
- private readonly _loaded;
12
+ private _stopped;
9
13
  protected _execution_count: number;
10
- protected _pendingInput: boolean;
14
+ protected _pendingInput?: PromiseDelegate<string>;
11
15
  private _listeners;
16
+ protected _options: any;
17
+ protected _files: Map<string, ArrayBuffer>;
18
+ protected _modules: Map<string, ArrayBuffer>;
12
19
  constructor(options: any);
20
+ /**
21
+ * Is this kernel safe? (loaded from an isolated web worker)
22
+ */
23
+ safeKernel(): boolean;
13
24
  /**
14
25
  * Kernel version number (string).
15
26
  */
@@ -28,10 +39,6 @@ export declare class KernelBase {
28
39
  * Script (module) file extensions
29
40
  */
30
41
  moduleExts(): string[];
31
- /**
32
- * Launch the kernel.
33
- */
34
- launch(): Promise<void>;
35
42
  /**
36
43
  * Execution count getter.
37
44
  */
@@ -40,7 +47,6 @@ export declare class KernelBase {
40
47
  * Async code evaluation that resolves with the result.
41
48
  */
42
49
  evalAsync(code: string, outCallback: (_: string) => void, errCallback: (_: string) => void, data?: any): Promise<any>;
43
- restart(): void;
44
50
  putFile(filename: string, content: ArrayBuffer): Promise<void>;
45
51
  putModule(filename: string, content: ArrayBuffer): Promise<void>;
46
52
  userModules(): string[];
@@ -52,23 +58,48 @@ export declare class KernelBase {
52
58
  * Get a user module file content.
53
59
  */
54
60
  getUserModuleFile(filename: string): Uint8Array;
55
- more(source: string): boolean;
56
- complete(code: string): [string[], number] | [];
61
+ more(source: string): Promise<boolean>;
62
+ complete(code: string): Promise<[string[], number] | []>;
57
63
  banner(): string;
58
64
  ps1(): string;
59
65
  ps2(): string;
60
66
  /**
61
- * Initialize the kernel.
67
+ * Launch the kernel (used to wrap a promise around init).
68
+ */
69
+ protected _init(): Promise<void>;
70
+ /**
71
+ * Initialize the kernel and start it.
62
72
  */
63
73
  init(): Promise<void>;
74
+ /**
75
+ * Start the kernel (used to wrap a promise around start).
76
+ */
77
+ protected _start(): Promise<void>;
78
+ /**
79
+ * Start the kernel.
80
+ */
81
+ start(): Promise<void>;
82
+ /**
83
+ * Perform async part of the stoping process.
84
+ */
85
+ protected _stop(): Promise<void>;
86
+ /**
87
+ * Stop the kernel synchronously (to ensure kernel is stopped after the call).
88
+ */
89
+ stop(): void;
90
+ /**
91
+ * Restart the kernel (stop and start).
92
+ * Use await this.ready() to ensure kernel is started
93
+ */
94
+ restart(): void;
64
95
  /**
65
96
  * Is the kernel ready?
66
97
  */
67
- get ready(): boolean;
98
+ get isReady(): boolean;
68
99
  /**
69
- * Promise that resolve when the kernel is loaded.
100
+ * Promise that resolves when the kernel is ready (started).
70
101
  */
71
- loaded(): Promise<void>;
102
+ ready(): Promise<void>;
72
103
  /**
73
104
  * Root for kernel files. This is always the language directory
74
105
  * inside the version number directory inside the kernel directory.
@@ -103,7 +134,11 @@ export declare class KernelBase {
103
134
  /**
104
135
  * Send eval.input event then wait for the user response and return it.
105
136
  */
106
- inputAsync(prompt: string | null | undefined, password?: boolean, data?: any): Promise<unknown>;
137
+ inputAsync(prompt: string | null | undefined, password?: boolean, data?: any): Promise<string>;
138
+ /**
139
+ * Close a pending input.
140
+ */
141
+ resolvePendingInput(): void;
107
142
  /**
108
143
  * Simple clone via JSON copy.
109
144
  */
package/lib/kernel.js CHANGED
@@ -1,83 +1,27 @@
1
1
  import { PromiseDelegate } from "promise-delegate";
2
2
  import { VERSION } from "./version";
3
- const _localfsScheme = "filesystem:/";
4
- /* we mock the fetch function to redirect queries to local FS */
5
- (function () {
6
- var _a;
7
- const toDataURL = async (content) => {
8
- return await new Promise((resolve, reject) => {
9
- const reader = new FileReader();
10
- reader.onloadend = () => {
11
- resolve(reader.result);
12
- };
13
- reader.onerror = reject;
14
- reader.readAsDataURL(new Blob([content]));
15
- });
16
- };
17
- //@ts-ignore
18
- window._fetch = (_a = window._fetch) !== null && _a !== void 0 ? _a : window.fetch;
19
- window.fetch = async function (request, opts) {
20
- var _a;
21
- //@ts-ignore
22
- const url = (_a = request.url) !== null && _a !== void 0 ? _a : request.toString();
23
- const prefix = _localfsScheme;
24
- if (url.startsWith(prefix)) {
25
- const path = url.slice(prefix.length);
26
- let content = null;
27
- const options = {
28
- status: 200,
29
- statusText: "OK",
30
- headers: new Headers(),
31
- };
32
- try {
33
- //@ts-ignore
34
- content = window.Basthon.getFile(path);
35
- const dataURL = await toDataURL(content);
36
- const mime = dataURL.substring(dataURL.indexOf(":") + 1, dataURL.indexOf(";"));
37
- content = content.buffer;
38
- options.headers.append("Content-Type", mime);
39
- options.headers.append("Content-Length", content.byteLength);
40
- }
41
- catch (e) {
42
- options.status = 404;
43
- options.statusText = "Not Found";
44
- }
45
- return new Response(content, options);
46
- }
47
- //@ts-ignore
48
- return await window._fetch(request, opts);
49
- };
50
- const desc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, "src");
51
- Object.defineProperty(HTMLImageElement.prototype, "src", {
52
- ...desc,
53
- get: function () {
54
- var _a;
55
- return (_a = desc === null || desc === void 0 ? void 0 : desc.get) === null || _a === void 0 ? void 0 : _a.call(this);
56
- },
57
- set: function (e) {
58
- var _a;
59
- const prefix = _localfsScheme;
60
- if (e.startsWith(prefix)) {
61
- const path = e.slice(prefix.length);
62
- //@ts-ignore
63
- const content = window.Basthon.getFile(path).buffer;
64
- (async () => { var _a; return (_a = desc === null || desc === void 0 ? void 0 : desc.set) === null || _a === void 0 ? void 0 : _a.call(this, await toDataURL(content)); })();
65
- }
66
- else {
67
- (_a = desc === null || desc === void 0 ? void 0 : desc.set) === null || _a === void 0 ? void 0 : _a.call(this, e);
68
- }
69
- },
70
- });
71
- })();
72
- /**
73
- * An error thrown by not implemented API functions.
74
- */
75
- class NotImplementedError extends Error {
76
- constructor(funcName) {
77
- super(`Function ${funcName} not implemented!`);
78
- this.name = "NotImplementedError";
79
- }
80
- }
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
+ // });
81
25
  /**
82
26
  * Event for Basthon's dispatch/listen.
83
27
  */
@@ -87,20 +31,34 @@ class BasthonEvent extends Event {
87
31
  this.detail = data;
88
32
  }
89
33
  }
34
+ export { VERSION };
90
35
  /**
91
36
  * API that any Basthon kernel should fill to be supported
92
37
  * in console/notebook.
93
38
  */
94
39
  export class KernelBase {
95
40
  constructor(options) {
96
- this._ready = false;
97
- this._loaded = new PromiseDelegate();
41
+ this._isReady = false;
42
+ this._ready = new PromiseDelegate();
43
+ this._stopped = Promise.resolve();
98
44
  this._execution_count = 0;
99
- this._pendingInput = false;
45
+ this._pendingInput = undefined;
100
46
  // a map to register wrapped listeners to allow later remove
101
47
  this._listeners = new Map();
48
+ this._files = new Map();
49
+ this._modules = new Map();
50
+ this._options = this.clone(options);
102
51
  // root path where kernel is installed
103
52
  this._rootPath = options.rootPath;
53
+ if (!this.safeKernel())
54
+ console.warn(`%c⚠ DANGER ⚠: kernel ${this.language()} not safe!` +
55
+ " Please consider using a safe kernel.", "color: red; font-size: 20px;");
56
+ }
57
+ /**
58
+ * Is this kernel safe? (loaded from an isolated web worker)
59
+ */
60
+ safeKernel() {
61
+ return false;
104
62
  }
105
63
  /**
106
64
  * Kernel version number (string).
@@ -128,12 +86,6 @@ export class KernelBase {
128
86
  moduleExts() {
129
87
  throw new NotImplementedError("moduleExts");
130
88
  }
131
- /**
132
- * Launch the kernel.
133
- */
134
- async launch() {
135
- throw new NotImplementedError("launch");
136
- }
137
89
  /**
138
90
  * Execution count getter.
139
91
  */
@@ -146,14 +98,11 @@ export class KernelBase {
146
98
  evalAsync(code, outCallback, errCallback, data = null) {
147
99
  throw new NotImplementedError("evalAsync");
148
100
  }
149
- restart() {
150
- throw new NotImplementedError("restart");
151
- }
152
101
  async putFile(filename, content) {
153
- throw new NotImplementedError("putFile");
102
+ this._files.set(filename, content);
154
103
  }
155
104
  async putModule(filename, content) {
156
- throw new NotImplementedError("putModule");
105
+ this._modules.set(filename, content);
157
106
  }
158
107
  userModules() {
159
108
  return [];
@@ -170,10 +119,10 @@ export class KernelBase {
170
119
  getUserModuleFile(filename) {
171
120
  throw new NotImplementedError("getUserModuleFile");
172
121
  }
173
- more(source) {
122
+ async more(source) {
174
123
  throw new NotImplementedError("more");
175
124
  }
176
- complete(code) {
125
+ async complete(code) {
177
126
  return [];
178
127
  }
179
128
  banner() {
@@ -186,32 +135,77 @@ export class KernelBase {
186
135
  return "... ";
187
136
  }
188
137
  /**
189
- * Initialize the kernel.
138
+ * Launch the kernel (used to wrap a promise around init).
139
+ */
140
+ async _init() { }
141
+ /**
142
+ * Initialize the kernel and start it.
190
143
  */
191
144
  async init() {
192
145
  try {
193
- await this.launch();
146
+ await this._init();
194
147
  }
195
148
  catch (error) {
196
- this._loaded.reject(error);
149
+ this._ready.reject(error);
197
150
  return;
198
151
  }
199
152
  // connecting eval to basthon.eval.request event.
200
153
  this.addEventListener("eval.request", this.evalFromEvent.bind(this));
201
- this._ready = true;
202
- this._loaded.resolve();
154
+ await this.start();
155
+ }
156
+ /**
157
+ * Start the kernel (used to wrap a promise around start).
158
+ */
159
+ async _start() { }
160
+ /**
161
+ * Start the kernel.
162
+ */
163
+ async start() {
164
+ await this._stopped;
165
+ this._execution_count = 0;
166
+ try {
167
+ await this._start();
168
+ }
169
+ catch (error) {
170
+ this._ready.reject(error);
171
+ return;
172
+ }
173
+ this._files.forEach((v, k) => this.putFile(k, v));
174
+ this._modules.forEach((v, k) => this.putModule(k, v));
175
+ this._isReady = true;
176
+ this._ready.resolve();
177
+ }
178
+ /**
179
+ * Perform async part of the stoping process.
180
+ */
181
+ async _stop() { }
182
+ /**
183
+ * Stop the kernel synchronously (to ensure kernel is stopped after the call).
184
+ */
185
+ stop() {
186
+ this._isReady = false;
187
+ this._ready = new PromiseDelegate();
188
+ this._stopped = this._stop();
189
+ }
190
+ /**
191
+ * Restart the kernel (stop and start).
192
+ * Use await this.ready() to ensure kernel is started
193
+ */
194
+ restart() {
195
+ this.stop(); // sync
196
+ this.start(); // async
203
197
  }
204
198
  /**
205
199
  * Is the kernel ready?
206
200
  */
207
- get ready() {
208
- return this._ready;
201
+ get isReady() {
202
+ return this._isReady;
209
203
  }
210
204
  /**
211
- * Promise that resolve when the kernel is loaded.
205
+ * Promise that resolves when the kernel is ready (started).
212
206
  */
213
- async loaded() {
214
- await this._loaded.promise;
207
+ async ready() {
208
+ return this._ready.promise;
215
209
  }
216
210
  /**
217
211
  * Root for kernel files. This is always the language directory
@@ -287,7 +281,7 @@ export class KernelBase {
287
281
  removeEventListener(eventName, callback) {
288
282
  // get the wrapped callback
289
283
  const listeners = this._listeners.get(eventName);
290
- document.removeEventListener(`basthon.${eventName}`, listeners === null || listeners === void 0 ? void 0 : listeners.get(callback));
284
+ document.removeEventListener(`basthon.${eventName}`, listeners?.get(callback));
291
285
  }
292
286
  /**
293
287
  * Send eval.input event then wait for the user response and return it.
@@ -295,16 +289,21 @@ export class KernelBase {
295
289
  async inputAsync(prompt, password = false, data = undefined) {
296
290
  data = this.clone(data);
297
291
  data.content = { prompt, password };
298
- const promise = new Promise(function (resolve, reject) {
299
- data.resolve = resolve;
300
- data.reject = reject;
301
- });
302
- this._pendingInput = true;
292
+ const pd = new PromiseDelegate();
293
+ data.resolve = pd.resolve.bind(pd);
294
+ data.reject = pd.reject.bind(pd);
295
+ this._pendingInput = pd;
303
296
  this.dispatchEvent("eval.input", data);
304
- const res = await promise;
305
- this._pendingInput = false;
297
+ const res = await pd.promise;
298
+ this._pendingInput = undefined;
306
299
  return res;
307
300
  }
301
+ /**
302
+ * Close a pending input.
303
+ */
304
+ resolvePendingInput() {
305
+ this._pendingInput?.resolve("");
306
+ }
308
307
  /**
309
308
  * Simple clone via JSON copy.
310
309
  */
@@ -317,8 +316,7 @@ export class KernelBase {
317
316
  * Detection is based on extension.
318
317
  */
319
318
  async putRessource(filename, content) {
320
- var _a;
321
- const ext = (_a = filename.split(".").pop()) !== null && _a !== void 0 ? _a : "";
319
+ const ext = filename.split(".").pop() ?? "";
322
320
  if (this.moduleExts().includes(ext)) {
323
321
  return await this.putModule(filename, content);
324
322
  }
@@ -330,7 +328,7 @@ export class KernelBase {
330
328
  * Is an input pending?
331
329
  */
332
330
  pendingInput() {
333
- return this._pendingInput;
331
+ return this._pendingInput != null;
334
332
  }
335
333
  /**
336
334
  * Internal. Code evaluation after an eval.request event.
package/lib/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.62.20";
1
+ export declare const VERSION = "0.62.22";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.62.20";
1
+ export const VERSION = "0.62.22";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import { expose } from "comlink";
2
+ import { ProxyFetch } from "./proxy";
3
+ const proxy = new ProxyFetch();
4
+ expose(proxy);
@@ -0,0 +1,2 @@
1
+ export * from "./kernel-main";
2
+ export * from "./kernel-worker";
@@ -0,0 +1,2 @@
1
+ export * from "./kernel-main";
2
+ export * from "./kernel-worker";
@@ -0,0 +1,60 @@
1
+ import { Remote } from "comlink";
2
+ import { KernelWorkerBase } from "./kernel-worker";
3
+ import { KernelBase } from "../kernel";
4
+ export declare class KernelMainBase<Type extends KernelWorkerBase> extends KernelBase {
5
+ private _worker?;
6
+ private _remote?;
7
+ constructor(options: any);
8
+ /**
9
+ * Is this kernel safe? (loaded in an isolated web worker)
10
+ */
11
+ safeKernel(): boolean;
12
+ /**
13
+ * Get a new worker (should be overloaded).
14
+ */
15
+ protected newWorker(): Worker;
16
+ /**
17
+ * Start the worker.
18
+ */
19
+ _start(): Promise<void>;
20
+ /**
21
+ * Comlink remote getter.
22
+ */
23
+ protected get remote(): Remote<Type> | undefined;
24
+ /**
25
+ * Stop the worker.
26
+ */
27
+ _stop(): Promise<void>;
28
+ /**
29
+ * Process messages from worker (if any).
30
+ */
31
+ protected processWorkerMessage(msg: {
32
+ data: any;
33
+ type: string;
34
+ content: any;
35
+ }): void;
36
+ evalAsync(code: string, outCallback: (_: string) => void, errCallback: (_: string) => void, data?: any): Promise<any>;
37
+ /**
38
+ * Put a file on the local (emulated) filesystem.
39
+ */
40
+ putFile(filename: string, content: ArrayBuffer): Promise<void>;
41
+ /**
42
+ * Put an importable module on the local (emulated) filesystem
43
+ * and load dependencies.
44
+ */
45
+ putModule(filename: string, content: ArrayBuffer): Promise<void>;
46
+ /**
47
+ * Complete a code at the end (usefull for tab completion).
48
+ *
49
+ * Returns an array of two elements: the list of completions
50
+ * and the start index.
51
+ */
52
+ complete(code: string): Promise<[string[], number] | []>;
53
+ /**
54
+ * Tell wether we should wait for more code or if it can
55
+ * be run as is.
56
+ *
57
+ * Useful to set ps1/ps2 in console prompt.
58
+ */
59
+ more(code: string): Promise<boolean>;
60
+ }
@@ -0,0 +1,163 @@
1
+ import { wrap, createEndpoint, transfer } from "comlink";
2
+ import { NotImplementedError } from "../commons";
3
+ import { KernelBase } from "../kernel";
4
+ // 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) {
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
+ }
26
+ export class KernelMainBase extends KernelBase {
27
+ constructor(options) {
28
+ super(options);
29
+ }
30
+ /**
31
+ * Is this kernel safe? (loaded in an isolated web worker)
32
+ */
33
+ safeKernel() {
34
+ return true;
35
+ }
36
+ /**
37
+ * Get a new worker (should be overloaded).
38
+ */
39
+ newWorker() {
40
+ throw new NotImplementedError("newWorker");
41
+ }
42
+ /**
43
+ * Start the worker.
44
+ */
45
+ async _start() {
46
+ await super._start();
47
+ const worker = this.newWorker();
48
+ // setup the proxy worker
49
+ const proxyWorker = new Worker(new URL("./comlink-proxy.js", import.meta.url), {
50
+ type: "module",
51
+ });
52
+ const proxy = wrap(proxyWorker);
53
+ // setup the kernel worker
54
+ this._worker = worker;
55
+ worker.onmessage = (e) => this.processWorkerMessage(e.data);
56
+ const KernelWorker = wrap(worker);
57
+ // why typescript complains about this call? yet, this is advertised in
58
+ // https://github.com/GoogleChromeLabs/comlink/blob/dffe9050f63b1b39f30213adeb1dd4b9ed7d2594/docs/examples/03-classes-example/index.html#L15
59
+ //@ts-ignore
60
+ this._remote = await new KernelWorker(this._options);
61
+ // bind ports
62
+ const port = await proxy[createEndpoint]();
63
+ await this.remote?.setProxyPort(transfer(port, [port]));
64
+ // init remote
65
+ await this.remote?.init();
66
+ }
67
+ /**
68
+ * Comlink remote getter.
69
+ */
70
+ get remote() {
71
+ return this._remote;
72
+ }
73
+ /**
74
+ * Stop the worker.
75
+ */
76
+ async _stop() {
77
+ this._worker?.terminate();
78
+ this._worker = undefined;
79
+ this._remote = undefined;
80
+ await super._stop();
81
+ }
82
+ /**
83
+ * Process messages from worker (if any).
84
+ */
85
+ processWorkerMessage(msg) {
86
+ switch (msg.type) {
87
+ case "stream":
88
+ const { stream, text } = msg.content;
89
+ const dataEvent = this.clone(msg.data);
90
+ dataEvent.stream = stream;
91
+ dataEvent.content = text;
92
+ this.dispatchEvent("eval.output", dataEvent);
93
+ break;
94
+ case "download":
95
+ const { content, filename } = msg.content;
96
+ this.download(content, filename);
97
+ break;
98
+ case "display":
99
+ this.dispatchEvent("eval.display", msg.data);
100
+ break;
101
+ case "input":
102
+ const { prompt, password, data, sab } = msg.content;
103
+ (async () => {
104
+ const res = (await this.inputAsync(prompt, password, data));
105
+ const encodedRes = new TextEncoder().encode(res);
106
+ new Uint8Array(sab).set(encodedRes.subarray(0, sab.byteLength));
107
+ Atomics.notify(new Int32Array(sab), 0, 1);
108
+ })();
109
+ break;
110
+ default: // ignoring (probably a comlink message)
111
+ }
112
+ }
113
+ async evalAsync(code, outCallback, errCallback, data = null) {
114
+ // force interactivity in all modes
115
+ data.interactive = true;
116
+ this._execution_count++;
117
+ // evaluation
118
+ const execResult = (await this.remote?.eval(data, code));
119
+ // return result
120
+ let result;
121
+ switch (execResult?.status) {
122
+ case "ok":
123
+ result = execResult.result;
124
+ break;
125
+ case "error":
126
+ break;
127
+ }
128
+ return [result, this._execution_count];
129
+ }
130
+ /**
131
+ * Put a file on the local (emulated) filesystem.
132
+ */
133
+ async putFile(filename, content) {
134
+ await super.putFile(filename, content);
135
+ await this.remote?.putFile(filename, content);
136
+ }
137
+ /**
138
+ * Put an importable module on the local (emulated) filesystem
139
+ * and load dependencies.
140
+ */
141
+ async putModule(filename, content) {
142
+ await super.putModule(filename, content);
143
+ await this.remote?.putModule(filename, content);
144
+ }
145
+ /**
146
+ * Complete a code at the end (usefull for tab completion).
147
+ *
148
+ * Returns an array of two elements: the list of completions
149
+ * and the start index.
150
+ */
151
+ async complete(code) {
152
+ return (await this.remote?.complete(code)) ?? [];
153
+ }
154
+ /**
155
+ * Tell wether we should wait for more code or if it can
156
+ * be run as is.
157
+ *
158
+ * Useful to set ps1/ps2 in console prompt.
159
+ */
160
+ async more(code) {
161
+ return (await this.remote?.more(code)) ?? false;
162
+ }
163
+ }
@@ -0,0 +1,109 @@
1
+ import { Remote } from "comlink";
2
+ import { ProxyFetch } from "./proxy";
3
+ declare global {
4
+ interface DedicatedWorkerGlobalScope {
5
+ _remoteProxy: Remote<ProxyFetch>;
6
+ basthon: KernelWorkerBase;
7
+ _prompt: (p?: string, _d?: string) => string | null;
8
+ }
9
+ }
10
+ type ExecuteResult = {
11
+ data: any;
12
+ status: "ok" | "error";
13
+ result?: any;
14
+ error?: any;
15
+ };
16
+ declare class KernelWorkerBase {
17
+ private _ready;
18
+ protected __eval_data__: any;
19
+ constructor(options?: any);
20
+ protected _init(): Promise<void>;
21
+ /**
22
+ * Initialize the kernel. It is called by initWorker in KernelBase.
23
+ */
24
+ init(): Promise<void>;
25
+ /**
26
+ * Wait for the kernel to be ready.
27
+ */
28
+ ready(): Promise<void>;
29
+ /**
30
+ * Set the comlink communication port to proxy fetch.
31
+ */
32
+ setProxyPort(port: MessagePort): void;
33
+ /**
34
+ * Send stream to stdout/stderr.
35
+ */
36
+ private sendStream;
37
+ /**
38
+ * Send stream to stdout.
39
+ */
40
+ protected sendStdoutStream(data: any, text: string): void;
41
+ /**
42
+ * Send stream to stderr.
43
+ */
44
+ protected sendStderrStream(data: any, text: string): void;
45
+ protected blobToDataURL(blob: Blob): Promise<string>;
46
+ /**
47
+ * Display a blob (only png images are supported).
48
+ */
49
+ protected displayBlob(blob: Blob, data: any): Promise<void>;
50
+ /**
51
+ * Display an element like a canvas, ... in the frontend.
52
+ */
53
+ display(element: any): void;
54
+ /**
55
+ * Evaluate code and return an object with mime type keys and string value.
56
+ * To be overloaded.
57
+ */
58
+ protected _eval(data: any, code: string): Promise<undefined | {
59
+ [key: string]: string;
60
+ }>;
61
+ /**
62
+ * Evaluate a string of code. To be called by main thread (through comlink).
63
+ */
64
+ eval(data: any, code: string): Promise<ExecuteResult>;
65
+ /**
66
+ * Put a file on the local (emulated) filesystem.
67
+ */
68
+ putFile(filename: string, content: ArrayBuffer): void;
69
+ /**
70
+ * Put an importable module on the local (emulated) filesystem
71
+ * and load dependencies.
72
+ */
73
+ putModule(filename: string, content: ArrayBuffer): void;
74
+ /**
75
+ * Complete the submited code.
76
+ */
77
+ complete(code: string): Promise<[string[], number] | []>;
78
+ /**
79
+ * Tell wether we should wait for more code or if it can
80
+ * be run as is.
81
+ *
82
+ * Useful to set ps1/ps2 in console prompt.
83
+ */
84
+ more(code: string): Promise<boolean>;
85
+ /**
86
+ * Send a message to the main thread to download a file.
87
+ */
88
+ download(content: Uint8Array | string, filename: string): void;
89
+ /**
90
+ * Use JSON stringify/parse to copy an object.
91
+ */
92
+ protected clone(data: any): any;
93
+ /**
94
+ * Sync sleep for a certain duration in seconds.
95
+ */
96
+ sleep(duration: number): void;
97
+ /**
98
+ * Decode the string contained in a SharedArrayBuffer to
99
+ * bypas this issue:
100
+ * https://github.com/whatwg/encoding/issues/172
101
+ */
102
+ private decodeSharedArrayBuffer;
103
+ /**
104
+ * Sync input that does not use the ugly prompt but send
105
+ * an eval.input event tho the main thread and wait for the response.
106
+ */
107
+ input(prompt: string | null | undefined, password?: boolean): string;
108
+ }
109
+ export { ExecuteResult, KernelWorkerBase };
@@ -0,0 +1,244 @@
1
+ import { PromiseDelegate } from "promise-delegate";
2
+ import { NotImplementedError, mockFetch } from "../commons";
3
+ import { wrap } from "comlink";
4
+ class KernelWorkerBase {
5
+ constructor(options) {
6
+ this._ready = new PromiseDelegate();
7
+ /* we mock the fetch function to:
8
+ * - redirect queries to local FS
9
+ * - bypass the worker's opaque origin
10
+ */
11
+ mockFetch();
12
+ // useful for ffi
13
+ self.basthon = this;
14
+ // global prompt redirected to our input
15
+ if (self._prompt == null)
16
+ self._prompt = globalThis.prompt;
17
+ globalThis.prompt = (prompt) => this.input(prompt);
18
+ }
19
+ /*
20
+ * Initialize the kernel. Should be overloaded.
21
+ */
22
+ async _init() { }
23
+ /**
24
+ * Initialize the kernel. It is called by initWorker in KernelBase.
25
+ */
26
+ async init() {
27
+ try {
28
+ await this._init();
29
+ this._ready.resolve();
30
+ }
31
+ catch (e) {
32
+ this._ready.reject(e);
33
+ }
34
+ }
35
+ /**
36
+ * Wait for the kernel to be ready.
37
+ */
38
+ ready() {
39
+ return this._ready.promise;
40
+ }
41
+ /**
42
+ * Set the comlink communication port to proxy fetch.
43
+ */
44
+ setProxyPort(port) {
45
+ self._remoteProxy = wrap(port);
46
+ }
47
+ /**
48
+ * Send stream to stdout/stderr.
49
+ */
50
+ sendStream(data, stream, text) {
51
+ globalThis.postMessage({
52
+ data,
53
+ type: "stream",
54
+ content: { stream, text },
55
+ });
56
+ }
57
+ /**
58
+ * Send stream to stdout.
59
+ */
60
+ sendStdoutStream(data, text) {
61
+ this.sendStream(data, "stdout", text);
62
+ }
63
+ /**
64
+ * Send stream to stderr.
65
+ */
66
+ sendStderrStream(data, text) {
67
+ this.sendStream(data, "stderr", text);
68
+ }
69
+ async blobToDataURL(blob) {
70
+ return await new Promise((resolve) => {
71
+ const reader = new FileReader();
72
+ reader.onloadend = () => resolve(reader.result);
73
+ reader.readAsDataURL(blob);
74
+ });
75
+ }
76
+ /**
77
+ * Display a blob (only png images are supported).
78
+ */
79
+ async displayBlob(blob, data) {
80
+ if (data == null)
81
+ data = this.clone(this.__eval_data__);
82
+ const dataURL = await this.blobToDataURL(blob);
83
+ const pngPrefix = "data:image/png;base64,";
84
+ if (dataURL.startsWith(pngPrefix)) {
85
+ const png = dataURL.slice(pngPrefix.length);
86
+ data["display_type"] = "multiple";
87
+ data["content"] = { "image/png": png };
88
+ }
89
+ else {
90
+ return;
91
+ }
92
+ globalThis.postMessage({ data, type: "display" });
93
+ }
94
+ /**
95
+ * Display an element like a canvas, ... in the frontend.
96
+ */
97
+ display(element) {
98
+ const data = this.clone(this.__eval_data__);
99
+ if (globalThis.OffscreenCanvas != null &&
100
+ element instanceof OffscreenCanvas) {
101
+ element
102
+ .convertToBlob()
103
+ .then((blob) => this.displayBlob(blob, data));
104
+ }
105
+ else {
106
+ data["display_type"] = "multiple";
107
+ data["content"] = element;
108
+ globalThis.postMessage({ data, type: "display" });
109
+ }
110
+ }
111
+ /**
112
+ * Evaluate code and return an object with mime type keys and string value.
113
+ * To be overloaded.
114
+ */
115
+ async _eval(data, code) {
116
+ throw new NotImplementedError("_eval");
117
+ }
118
+ /**
119
+ * Evaluate a string of code. To be called by main thread (through comlink).
120
+ */
121
+ async eval(data, code) {
122
+ this.__eval_data__ = data;
123
+ try {
124
+ const result = await this._eval(data, code);
125
+ return { data, status: "ok", result };
126
+ }
127
+ catch (e) {
128
+ const { name, stack, message } = e;
129
+ this.sendStderrStream(data, e.toString());
130
+ return {
131
+ data,
132
+ status: "error",
133
+ error: {
134
+ name: name,
135
+ value: message,
136
+ traceback: stack?.toString(),
137
+ },
138
+ };
139
+ }
140
+ }
141
+ /**
142
+ * Put a file on the local (emulated) filesystem.
143
+ */
144
+ putFile(filename, content) {
145
+ throw new NotImplementedError("putFile");
146
+ }
147
+ /**
148
+ * Put an importable module on the local (emulated) filesystem
149
+ * and load dependencies.
150
+ */
151
+ putModule(filename, content) {
152
+ throw new NotImplementedError("putModule");
153
+ }
154
+ /**
155
+ * Complete the submited code.
156
+ */
157
+ async complete(code) {
158
+ return [];
159
+ }
160
+ /**
161
+ * Tell wether we should wait for more code or if it can
162
+ * be run as is.
163
+ *
164
+ * Useful to set ps1/ps2 in console prompt.
165
+ */
166
+ async more(code) {
167
+ throw new NotImplementedError("more");
168
+ }
169
+ /**
170
+ * Send a message to the main thread to download a file.
171
+ */
172
+ download(content, filename) {
173
+ globalThis.postMessage({
174
+ type: "download",
175
+ content: { content, filename },
176
+ });
177
+ }
178
+ /**
179
+ * Use JSON stringify/parse to copy an object.
180
+ */
181
+ clone(data) {
182
+ return JSON.parse(JSON.stringify(data));
183
+ }
184
+ /**
185
+ * Sync sleep for a certain duration in seconds.
186
+ */
187
+ sleep(duration) {
188
+ duration = duration * 1000; // to milliseconds
189
+ if (globalThis.crossOriginIsolated) {
190
+ const sab = new Int32Array(new SharedArrayBuffer(4));
191
+ Atomics.wait(sab, 0, 0, duration);
192
+ return;
193
+ }
194
+ else {
195
+ const t0 = Date.now();
196
+ // active sleep
197
+ while (Date.now() - t0 < duration)
198
+ ;
199
+ }
200
+ }
201
+ /**
202
+ * Decode the string contained in a SharedArrayBuffer to
203
+ * bypas this issue:
204
+ * https://github.com/whatwg/encoding/issues/172
205
+ */
206
+ decodeSharedArrayBuffer(sab) {
207
+ const array = new Uint8Array(sab);
208
+ let length = array.length;
209
+ for (; length > 0 && array[length - 1] === 0; length--)
210
+ ;
211
+ if (length === 0)
212
+ return "";
213
+ const cropped = new Uint8Array(length);
214
+ cropped.set(array.subarray(0, length));
215
+ try {
216
+ return new TextDecoder().decode(cropped);
217
+ }
218
+ catch (e) {
219
+ if (array.length === length)
220
+ throw new Error("String too long, can't decode");
221
+ throw e;
222
+ }
223
+ }
224
+ /**
225
+ * Sync input that does not use the ugly prompt but send
226
+ * an eval.input event tho the main thread and wait for the response.
227
+ */
228
+ input(prompt, password = false) {
229
+ if (globalThis.crossOriginIsolated) {
230
+ const data = this.clone(this.__eval_data__);
231
+ const sab = new SharedArrayBuffer(1024);
232
+ globalThis.postMessage({
233
+ type: "input",
234
+ content: { data, prompt, password, sab },
235
+ });
236
+ Atomics.wait(new Int32Array(sab), 0, 0);
237
+ return this.decodeSharedArrayBuffer(sab);
238
+ }
239
+ else {
240
+ return self._prompt(prompt ?? "") ?? "";
241
+ }
242
+ }
243
+ }
244
+ export { KernelWorkerBase };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A class for a proxy that allows a worker with opaque origin
3
+ * to fetch assets.
4
+ */
5
+ export declare class ProxyFetch {
6
+ fetch(url: string, init: RequestInit | undefined): Promise<{
7
+ body: any;
8
+ options: any | undefined;
9
+ }>;
10
+ }
@@ -0,0 +1,30 @@
1
+ import { transfer } from "comlink";
2
+ /**
3
+ * A class for a proxy that allows a worker with opaque origin
4
+ * to fetch assets.
5
+ */
6
+ export class ProxyFetch {
7
+ async fetch(url, init) {
8
+ const parentURL = (s) => s.substring(0, s.lastIndexOf("/"));
9
+ const assetsURL = parentURL(new URL(globalThis.location.toString()).href);
10
+ const baseURL = parentURL(new URL(url).href);
11
+ if (!baseURL.startsWith(assetsURL))
12
+ throw new Error(`URL ${url} is outside of proxy' scope`);
13
+ // force credentials omition
14
+ if (init?.credentials !== "omit")
15
+ throw new Error("Request tries to use credentials");
16
+ const response = await globalThis.fetch(url, init);
17
+ const buffer = await response.arrayBuffer();
18
+ return {
19
+ body: transfer(buffer, [buffer]),
20
+ options: {
21
+ status: response.status,
22
+ statusText: response.statusText,
23
+ // wrong types for headers in worker...
24
+ // see https://github.com/microsoft/TypeScript/pull/15050/files#diff-1fe269c5fa7bcd3a57e9e6a91ce3566d3e3a5c38b21e6ff0d78ad0dd090f8d3aR24
25
+ // @ts-ignore
26
+ headers: new Map(response.headers), // Headers are not clonable
27
+ },
28
+ };
29
+ }
30
+ }
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@basthon/kernel-base",
3
- "version": "0.62.20",
3
+ "version": "0.62.22",
4
4
  "description": "Basthon - Base Kernel",
5
5
  "homepage": "https://basthon.fr",
6
6
  "bugs": {
7
- "url": "https://framagit.org/basthon/basthon-kernel/issues"
7
+ "url": "https://forge.apps.education.fr/basthon/basthon-kernel/issues"
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "https://framagit.org/basthon/basthon-kernel.git"
11
+ "url": "https://forge.apps.education.fr/basthon/basthon-kernel.git"
12
12
  },
13
13
  "license": "GPL-3.0-or-later",
14
14
  "author": "Romain Casati",
@@ -20,21 +20,29 @@
20
20
  "files": [
21
21
  "lib"
22
22
  ],
23
+ "exports": {
24
+ ".": {
25
+ "types": "./lib/index.d.ts",
26
+ "default": "./lib/index.js"
27
+ },
28
+ "./worker": {
29
+ "types": "./lib/worker/index.d.ts",
30
+ "default": "./lib/worker/index.js"
31
+ }
32
+ },
23
33
  "scripts": {
24
- "build": "gulp all",
25
- "clean": "rm -rf lib/"
34
+ "build": "echo \"export const VERSION = \\\"$(json -f ../../lerna.json version)\\\";\" > src/version.ts && tsc",
35
+ "clean": "rm -rf lib/ src/version.ts"
26
36
  },
27
37
  "dependencies": {
28
38
  "promise-delegate": "^1.0.1"
29
39
  },
30
40
  "devDependencies": {
31
- "gulp": "^4.0.2",
32
- "gulp-typescript": "^6.0.0-alpha.1",
33
- "typescript": "~4.4.4",
34
- "vinyl-buffer": "^1.0.1"
41
+ "json": "^11.0.0",
42
+ "typescript": "^5.4.5"
35
43
  },
36
44
  "publishConfig": {
37
45
  "access": "public"
38
46
  },
39
- "gitHead": "fdaa2ed08c687a4b4e44f07ff28de1ad4cffeb38"
47
+ "gitHead": "1dd6225a7caf14cbee203687cac611495f5b18d4"
40
48
  }