@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.
- package/lib/commons.d.ts +21 -0
- package/lib/commons.js +100 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/kernel.d.ts +49 -14
- package/lib/kernel.js +113 -115
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/worker/comlink-proxy.d.ts +1 -0
- package/lib/worker/comlink-proxy.js +4 -0
- package/lib/worker/index.d.ts +3 -0
- package/lib/worker/index.js +3 -0
- package/lib/worker/kernel-main.d.ts +73 -0
- package/lib/worker/kernel-main.js +203 -0
- package/lib/worker/kernel-worker.d.ts +120 -0
- package/lib/worker/kernel-worker.js +258 -0
- package/lib/worker/proxy.d.ts +10 -0
- package/lib/worker/proxy.js +30 -0
- package/lib/worker/worker-utils.d.ts +6 -0
- package/lib/worker/worker-utils.js +21 -0
- package/package.json +18 -10
|
@@ -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,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,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
|
+
};
|