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