@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.
- 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 +2 -0
- package/lib/worker/index.js +2 -0
- package/lib/worker/kernel-main.d.ts +60 -0
- package/lib/worker/kernel-main.js +163 -0
- package/lib/worker/kernel-worker.d.ts +109 -0
- package/lib/worker/kernel-worker.js +244 -0
- package/lib/worker/proxy.d.ts +10 -0
- package/lib/worker/proxy.js +30 -0
- package/package.json +18 -10
package/lib/commons.d.ts
ADDED
|
@@ -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
|
|
1
|
+
export * from "./kernel";
|
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from
|
|
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
|
|
12
|
+
private _stopped;
|
|
9
13
|
protected _execution_count: number;
|
|
10
|
-
protected _pendingInput
|
|
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
|
-
*
|
|
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
|
|
98
|
+
get isReady(): boolean;
|
|
68
99
|
/**
|
|
69
|
-
* Promise that
|
|
100
|
+
* Promise that resolves when the kernel is ready (started).
|
|
70
101
|
*/
|
|
71
|
-
|
|
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<
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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.
|
|
97
|
-
this.
|
|
41
|
+
this._isReady = false;
|
|
42
|
+
this._ready = new PromiseDelegate();
|
|
43
|
+
this._stopped = Promise.resolve();
|
|
98
44
|
this._execution_count = 0;
|
|
99
|
-
this._pendingInput =
|
|
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
|
-
|
|
102
|
+
this._files.set(filename, content);
|
|
154
103
|
}
|
|
155
104
|
async putModule(filename, content) {
|
|
156
|
-
|
|
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
|
-
*
|
|
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.
|
|
146
|
+
await this._init();
|
|
194
147
|
}
|
|
195
148
|
catch (error) {
|
|
196
|
-
this.
|
|
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.
|
|
202
|
-
|
|
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
|
|
208
|
-
return this.
|
|
201
|
+
get isReady() {
|
|
202
|
+
return this._isReady;
|
|
209
203
|
}
|
|
210
204
|
/**
|
|
211
|
-
* Promise that
|
|
205
|
+
* Promise that resolves when the kernel is ready (started).
|
|
212
206
|
*/
|
|
213
|
-
async
|
|
214
|
-
|
|
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
|
|
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
|
|
299
|
-
|
|
300
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
1
|
+
export declare const VERSION = "0.62.22";
|
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.62.
|
|
1
|
+
export const VERSION = "0.62.22";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,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.
|
|
3
|
+
"version": "0.62.22",
|
|
4
4
|
"description": "Basthon - Base Kernel",
|
|
5
5
|
"homepage": "https://basthon.fr",
|
|
6
6
|
"bugs": {
|
|
7
|
-
"url": "https://
|
|
7
|
+
"url": "https://forge.apps.education.fr/basthon/basthon-kernel/issues"
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://
|
|
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": "
|
|
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
|
-
"
|
|
32
|
-
"
|
|
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": "
|
|
47
|
+
"gitHead": "1dd6225a7caf14cbee203687cac611495f5b18d4"
|
|
40
48
|
}
|