@basthon/kernel-base 0.62.21 → 0.63.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,203 @@
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 === 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
+ }
26
+ export class KernelMainBase extends KernelBase {
27
+ constructor(options) {
28
+ super(options);
29
+ this._legacy = false;
30
+ this._legacy = options?.legacy === true;
31
+ }
32
+ /**
33
+ * Is this a legacy worker? i.e. not runing in a worker.
34
+ */
35
+ isLegacy() {
36
+ return this._legacy;
37
+ }
38
+ /**
39
+ * Is this kernel safe? (loaded in an isolated web worker)
40
+ */
41
+ safeKernel() {
42
+ return !this.isLegacy();
43
+ }
44
+ /**
45
+ * Get a new worker (should be overloaded).
46
+ */
47
+ newWorker() {
48
+ throw new NotImplementedError("newWorker");
49
+ }
50
+ /**
51
+ * Import the code for a legacy worker (running in the main thread).
52
+ */
53
+ async importLegacyWorker() {
54
+ throw new NotImplementedError("importLegacyWorker");
55
+ }
56
+ /**
57
+ * Create the kernel webworker and setup communication.
58
+ */
59
+ async setupWorker() {
60
+ const comlinkChannel = new MessageChannel();
61
+ if (this.isLegacy()) {
62
+ // running in main thread via message channel (fake worker)
63
+ //@ts-ignore
64
+ self._commPort = comlinkChannel.port1;
65
+ await this.importLegacyWorker();
66
+ }
67
+ else {
68
+ // setup the kernel worker
69
+ const worker = this.newWorker();
70
+ // send communication port
71
+ worker.postMessage(comlinkChannel.port1, [comlinkChannel.port1]);
72
+ this._worker = worker;
73
+ }
74
+ const KernelWorker = wrap(comlinkChannel.port2);
75
+ // why typescript complains about this call? yet, this is advertised in
76
+ // https://github.com/GoogleChromeLabs/comlink/blob/dffe9050f63b1b39f30213adeb1dd4b9ed7d2594/docs/examples/03-classes-example/index.html#L15
77
+ //@ts-ignore
78
+ this._remote = await new KernelWorker(this._options);
79
+ // setup the communication channel
80
+ const commChannel = new MessageChannel();
81
+ commChannel.port2.onmessage = (e) => this.processWorkerMessage(e.data);
82
+ await this.remote?.setCommPort(transfer(commChannel.port1, [commChannel.port1]));
83
+ // WARNING: without the setCommPort call, KernelWorker (proxy)
84
+ // is garbage collected and comlink releases the proxy,
85
+ // breaking all the communications... (don't know really why)
86
+ // If you experience this, just keep a ref on the proxy with e.g.
87
+ // this.PROXY = KernelWorker
88
+ }
89
+ /**
90
+ * Start the worker.
91
+ */
92
+ async _start() {
93
+ await super._start();
94
+ // setup the proxy worker
95
+ const proxyWorker = new Worker(new URL("./comlink-proxy.js", import.meta.url), {
96
+ type: "module",
97
+ });
98
+ const proxy = wrap(proxyWorker);
99
+ // setup the kernel worker
100
+ await this.setupWorker();
101
+ // bind ports
102
+ const port = await proxy[createEndpoint]();
103
+ await this.remote?.setProxyPort(transfer(port, [port]));
104
+ // init remote
105
+ await this.remote?.init();
106
+ }
107
+ /**
108
+ * Comlink remote getter.
109
+ */
110
+ get remote() {
111
+ return this._remote;
112
+ }
113
+ /**
114
+ * Stop the worker.
115
+ */
116
+ async _stop() {
117
+ this._worker?.terminate();
118
+ this._worker = undefined;
119
+ this._remote = undefined;
120
+ await super._stop();
121
+ }
122
+ /**
123
+ * Process messages from worker (if any).
124
+ */
125
+ processWorkerMessage(msg) {
126
+ switch (msg.type) {
127
+ case "stream":
128
+ const { stream, text } = msg.content;
129
+ const dataEvent = this.clone(msg.data);
130
+ dataEvent.stream = stream;
131
+ dataEvent.content = text;
132
+ this.dispatchEvent("eval.output", dataEvent);
133
+ break;
134
+ case "download":
135
+ const { content, filename } = msg.content;
136
+ this.download(content, filename);
137
+ break;
138
+ case "display":
139
+ this.dispatchEvent("eval.display", msg.data);
140
+ break;
141
+ case "input":
142
+ const { prompt, password, data, sab } = msg.content;
143
+ (async () => {
144
+ const res = (await this.inputAsync(prompt, password, data));
145
+ const encodedRes = new TextEncoder().encode(res);
146
+ new Uint8Array(sab).set(encodedRes.subarray(0, sab.byteLength));
147
+ Atomics.notify(new Int32Array(sab), 0, 1);
148
+ })();
149
+ break;
150
+ default: // ignoring (probably a comlink message)
151
+ }
152
+ }
153
+ async evalAsync(code, outCallback, errCallback, data = null) {
154
+ // force interactivity in all modes
155
+ data.interactive = true;
156
+ this._execution_count++;
157
+ // evaluation
158
+ const execResult = (await this.remote?.eval(data, code));
159
+ // return result
160
+ let result;
161
+ switch (execResult?.status) {
162
+ case "ok":
163
+ result = execResult.result;
164
+ break;
165
+ case "error":
166
+ break;
167
+ }
168
+ return [result, this._execution_count];
169
+ }
170
+ /**
171
+ * Put a file on the local (emulated) filesystem.
172
+ */
173
+ async putFile(filename, content) {
174
+ await super.putFile(filename, content);
175
+ await this.remote?.putFile(filename, content);
176
+ }
177
+ /**
178
+ * Put an importable module on the local (emulated) filesystem
179
+ * and load dependencies.
180
+ */
181
+ async putModule(filename, content) {
182
+ await super.putModule(filename, content);
183
+ await this.remote?.putModule(filename, content);
184
+ }
185
+ /**
186
+ * Complete a code at the end (usefull for tab completion).
187
+ *
188
+ * Returns an array of two elements: the list of completions
189
+ * and the start index.
190
+ */
191
+ async complete(code) {
192
+ return (await this.remote?.complete(code)) ?? [];
193
+ }
194
+ /**
195
+ * Tell wether we should wait for more code or if it can
196
+ * be run as is.
197
+ *
198
+ * Useful to set ps1/ps2 in console prompt.
199
+ */
200
+ async more(code) {
201
+ return (await this.remote?.more(code)) ?? false;
202
+ }
203
+ }
@@ -0,0 +1,120 @@
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
+ protected _options: any;
18
+ protected _legacy: boolean;
19
+ private _ready;
20
+ protected __eval_data__: any;
21
+ protected postMessage?: InstanceType<typeof MessagePort>["postMessage"];
22
+ constructor(options?: any);
23
+ /**
24
+ * Is this a legacy worker? i.e. not runing in a worker.
25
+ */
26
+ isLegacy(): boolean;
27
+ protected _init(): Promise<void>;
28
+ /**
29
+ * Initialize the kernel. It is called by initWorker in KernelBase.
30
+ */
31
+ init(): Promise<void>;
32
+ /**
33
+ * Wait for the kernel to be ready.
34
+ */
35
+ ready(): Promise<void>;
36
+ /**
37
+ * Set communication port.
38
+ */
39
+ setCommPort(port: MessagePort): void;
40
+ /**
41
+ * Set the comlink communication port to proxy fetch.
42
+ */
43
+ setProxyPort(port: MessagePort): void;
44
+ /**
45
+ * Send stream to stdout/stderr.
46
+ */
47
+ private sendStream;
48
+ /**
49
+ * Send stream to stdout.
50
+ */
51
+ protected sendStdoutStream(data: any, text: string): void;
52
+ /**
53
+ * Send stream to stderr.
54
+ */
55
+ protected sendStderrStream(data: any, text: string): void;
56
+ protected blobToDataURL(blob: Blob): Promise<string>;
57
+ /**
58
+ * Display a blob (only png images are supported).
59
+ */
60
+ protected displayBlob(blob: Blob, data: any): Promise<void>;
61
+ /**
62
+ * Display an element like a canvas, ... in the frontend.
63
+ */
64
+ display(element: any): void;
65
+ /**
66
+ * Evaluate code and return an object with mime type keys and string value.
67
+ * To be overloaded.
68
+ */
69
+ protected _eval(data: any, code: string): Promise<undefined | {
70
+ [key: string]: string;
71
+ }>;
72
+ /**
73
+ * Evaluate a string of code. To be called by main thread (through comlink).
74
+ */
75
+ eval(data: any, code: string): Promise<ExecuteResult>;
76
+ /**
77
+ * Put a file on the local (emulated) filesystem.
78
+ */
79
+ putFile(filename: string, content: ArrayBuffer): void;
80
+ /**
81
+ * Put an importable module on the local (emulated) filesystem
82
+ * and load dependencies.
83
+ */
84
+ putModule(filename: string, content: ArrayBuffer): void;
85
+ /**
86
+ * Complete the submited code.
87
+ */
88
+ complete(code: string): Promise<[string[], number] | []>;
89
+ /**
90
+ * Tell wether we should wait for more code or if it can
91
+ * be run as is.
92
+ *
93
+ * Useful to set ps1/ps2 in console prompt.
94
+ */
95
+ more(code: string): Promise<boolean>;
96
+ /**
97
+ * Send a message to the main thread to download a file.
98
+ */
99
+ download(content: Uint8Array | string, filename: string): void;
100
+ /**
101
+ * Use JSON stringify/parse to copy an object.
102
+ */
103
+ protected clone(data: any): any;
104
+ /**
105
+ * Sync sleep for a certain duration in seconds.
106
+ */
107
+ sleep(duration: number): void;
108
+ /**
109
+ * Decode the string contained in a SharedArrayBuffer to
110
+ * bypas this issue:
111
+ * https://github.com/whatwg/encoding/issues/172
112
+ */
113
+ private decodeSharedArrayBuffer;
114
+ /**
115
+ * Sync input that does not use the ugly prompt but send
116
+ * an eval.input event tho the main thread and wait for the response.
117
+ */
118
+ input(prompt: string | null | undefined, password?: boolean): string;
119
+ }
120
+ export { ExecuteResult, KernelWorkerBase };
@@ -0,0 +1,258 @@
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
+ this._options = options;
8
+ this._legacy = options?.legacy === true;
9
+ /* we mock the fetch function to:
10
+ * - redirect queries to local FS
11
+ * - bypass the worker's opaque origin
12
+ */
13
+ mockFetch(this.isLegacy());
14
+ // useful for ffi
15
+ self.basthon = this;
16
+ // global prompt redirected to our input
17
+ if (self._prompt == null)
18
+ self._prompt = globalThis.prompt;
19
+ globalThis.prompt = (prompt) => this.input(prompt);
20
+ }
21
+ /**
22
+ * Is this a legacy worker? i.e. not runing in a worker.
23
+ */
24
+ isLegacy() {
25
+ return this._legacy;
26
+ }
27
+ /*
28
+ * Initialize the kernel. Should be overloaded.
29
+ */
30
+ async _init() { }
31
+ /**
32
+ * Initialize the kernel. It is called by initWorker in KernelBase.
33
+ */
34
+ async init() {
35
+ try {
36
+ await this._init();
37
+ this._ready.resolve();
38
+ }
39
+ catch (e) {
40
+ this._ready.reject(e);
41
+ }
42
+ }
43
+ /**
44
+ * Wait for the kernel to be ready.
45
+ */
46
+ ready() {
47
+ return this._ready.promise;
48
+ }
49
+ /**
50
+ * Set communication port.
51
+ */
52
+ setCommPort(port) {
53
+ this.postMessage = port.postMessage.bind(port);
54
+ }
55
+ /**
56
+ * Set the comlink communication port to proxy fetch.
57
+ */
58
+ setProxyPort(port) {
59
+ self._remoteProxy = wrap(port);
60
+ }
61
+ /**
62
+ * Send stream to stdout/stderr.
63
+ */
64
+ sendStream(data, stream, text) {
65
+ this.postMessage?.({
66
+ data,
67
+ type: "stream",
68
+ content: { stream, text },
69
+ });
70
+ }
71
+ /**
72
+ * Send stream to stdout.
73
+ */
74
+ sendStdoutStream(data, text) {
75
+ this.sendStream(data, "stdout", text);
76
+ }
77
+ /**
78
+ * Send stream to stderr.
79
+ */
80
+ sendStderrStream(data, text) {
81
+ this.sendStream(data, "stderr", text);
82
+ }
83
+ async blobToDataURL(blob) {
84
+ return await new Promise((resolve) => {
85
+ const reader = new FileReader();
86
+ reader.onloadend = () => resolve(reader.result);
87
+ reader.readAsDataURL(blob);
88
+ });
89
+ }
90
+ /**
91
+ * Display a blob (only png images are supported).
92
+ */
93
+ async displayBlob(blob, data) {
94
+ if (data == null)
95
+ data = this.clone(this.__eval_data__);
96
+ const dataURL = await this.blobToDataURL(blob);
97
+ const pngPrefix = "data:image/png;base64,";
98
+ if (dataURL.startsWith(pngPrefix)) {
99
+ const png = dataURL.slice(pngPrefix.length);
100
+ data["display_type"] = "multiple";
101
+ data["content"] = { "image/png": png };
102
+ }
103
+ else {
104
+ return;
105
+ }
106
+ this.postMessage?.({ data, type: "display" });
107
+ }
108
+ /**
109
+ * Display an element like a canvas, ... in the frontend.
110
+ */
111
+ display(element) {
112
+ const data = this.clone(this.__eval_data__);
113
+ if (globalThis.OffscreenCanvas != null &&
114
+ element instanceof OffscreenCanvas) {
115
+ element
116
+ .convertToBlob()
117
+ .then((blob) => this.displayBlob(blob, data));
118
+ }
119
+ else {
120
+ data["display_type"] = "multiple";
121
+ data["content"] = element;
122
+ this.postMessage?.({ data, type: "display" });
123
+ }
124
+ }
125
+ /**
126
+ * Evaluate code and return an object with mime type keys and string value.
127
+ * To be overloaded.
128
+ */
129
+ async _eval(data, code) {
130
+ throw new NotImplementedError("_eval");
131
+ }
132
+ /**
133
+ * Evaluate a string of code. To be called by main thread (through comlink).
134
+ */
135
+ async eval(data, code) {
136
+ this.__eval_data__ = data;
137
+ try {
138
+ const result = await this._eval(data, code);
139
+ return { data, status: "ok", result };
140
+ }
141
+ catch (e) {
142
+ const { name, stack, message } = e;
143
+ this.sendStderrStream(data, e.toString());
144
+ return {
145
+ data,
146
+ status: "error",
147
+ error: {
148
+ name: name,
149
+ value: message,
150
+ traceback: stack?.toString(),
151
+ },
152
+ };
153
+ }
154
+ }
155
+ /**
156
+ * Put a file on the local (emulated) filesystem.
157
+ */
158
+ putFile(filename, content) {
159
+ throw new NotImplementedError("putFile");
160
+ }
161
+ /**
162
+ * Put an importable module on the local (emulated) filesystem
163
+ * and load dependencies.
164
+ */
165
+ putModule(filename, content) {
166
+ throw new NotImplementedError("putModule");
167
+ }
168
+ /**
169
+ * Complete the submited code.
170
+ */
171
+ async complete(code) {
172
+ return [];
173
+ }
174
+ /**
175
+ * Tell wether we should wait for more code or if it can
176
+ * be run as is.
177
+ *
178
+ * Useful to set ps1/ps2 in console prompt.
179
+ */
180
+ async more(code) {
181
+ throw new NotImplementedError("more");
182
+ }
183
+ /**
184
+ * Send a message to the main thread to download a file.
185
+ */
186
+ download(content, filename) {
187
+ this.postMessage?.({
188
+ type: "download",
189
+ content: { content, filename },
190
+ });
191
+ }
192
+ /**
193
+ * Use JSON stringify/parse to copy an object.
194
+ */
195
+ clone(data) {
196
+ return JSON.parse(JSON.stringify(data));
197
+ }
198
+ /**
199
+ * Sync sleep for a certain duration in seconds.
200
+ */
201
+ sleep(duration) {
202
+ duration = duration * 1000; // to milliseconds
203
+ if (globalThis.crossOriginIsolated && !this.isLegacy()) {
204
+ const sab = new Int32Array(new SharedArrayBuffer(4));
205
+ Atomics.wait(sab, 0, 0, duration);
206
+ return;
207
+ }
208
+ else {
209
+ const t0 = Date.now();
210
+ // active sleep
211
+ while (Date.now() - t0 < duration)
212
+ ;
213
+ }
214
+ }
215
+ /**
216
+ * Decode the string contained in a SharedArrayBuffer to
217
+ * bypas this issue:
218
+ * https://github.com/whatwg/encoding/issues/172
219
+ */
220
+ decodeSharedArrayBuffer(sab) {
221
+ const array = new Uint8Array(sab);
222
+ let length = array.length;
223
+ for (; length > 0 && array[length - 1] === 0; length--)
224
+ ;
225
+ if (length === 0)
226
+ return "";
227
+ const cropped = new Uint8Array(length);
228
+ cropped.set(array.subarray(0, length));
229
+ try {
230
+ return new TextDecoder().decode(cropped);
231
+ }
232
+ catch (e) {
233
+ if (array.length === length)
234
+ throw new Error("String too long, can't decode");
235
+ throw e;
236
+ }
237
+ }
238
+ /**
239
+ * Sync input that does not use the ugly prompt but send
240
+ * an eval.input event tho the main thread and wait for the response.
241
+ */
242
+ input(prompt, password = false) {
243
+ if (globalThis.crossOriginIsolated && !this.isLegacy()) {
244
+ const data = this.clone(this.__eval_data__);
245
+ const sab = new SharedArrayBuffer(1024);
246
+ this.postMessage?.({
247
+ type: "input",
248
+ content: { data, prompt, password, sab },
249
+ });
250
+ Atomics.wait(new Int32Array(sab), 0, 0);
251
+ return this.decodeSharedArrayBuffer(sab);
252
+ }
253
+ else {
254
+ return self._prompt(prompt ?? "") ?? "";
255
+ }
256
+ }
257
+ }
258
+ 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
+ }
@@ -0,0 +1,6 @@
1
+ declare global {
2
+ interface DedicatedWorkerGlobalScope {
3
+ _commPort?: MessagePort;
4
+ }
5
+ }
6
+ export declare const expose: (kwc: any) => void;
@@ -0,0 +1,21 @@
1
+ import { expose as exposeComlink } from "comlink";
2
+ const insideWorker = () =>
3
+ //@ts-ignore
4
+ typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope;
5
+ export const expose = (kwc) => {
6
+ if (insideWorker()) {
7
+ // worker
8
+ const listener = (msg) => {
9
+ if (!(msg?.data instanceof MessagePort))
10
+ return;
11
+ removeEventListener("message", listener);
12
+ exposeComlink(kwc, msg.data);
13
+ };
14
+ addEventListener("message", listener);
15
+ }
16
+ else {
17
+ // main thread
18
+ exposeComlink(kwc, self._commPort);
19
+ self._commPort = undefined; // no more needed
20
+ }
21
+ };