···11-/**
22- * The main bootstrap script for loading pyodide.
33- */
44-const port = process.env.PORT || 1212;
55-66-export const languagePluginLoader = new Promise((resolve, reject) => {
77- let baseURL = `http://localhost:${port}/src/`;
88- // var baseURL = self.languagePluginUrl || 'https://iodide.io/pyodide-demo/';
99- baseURL = `${baseURL.substr(0, baseURL.lastIndexOf('/'))}/`;
1010-1111- /// /////////////////////////////////////////////////////////
1212- // Package loading
1313- const loadedPackages = [];
1414- let loadPackagePromise = new Promise((resolve) => resolve());
1515- // Regexp for validating package name and URI
1616- var package_name_regexp = '[a-z0-9_][a-z0-9_-]*';
1717- const package_uri_regexp = new RegExp(
1818- `^https?://.*?(${package_name_regexp}).js$`,
1919- 'i'
2020- );
2121- var package_name_regexp = new RegExp(`^${package_name_regexp}$`, 'i');
2222-2323- const _uri_to_package_name = (package_uri) => {
2424- // Generate a unique package name from URI
2525-2626- if (package_name_regexp.test(package_uri)) {
2727- return package_uri;
2828- }
2929- if (package_uri_regexp.test(package_uri)) {
3030- const match = package_uri_regexp.exec(package_uri);
3131- // Get the regexp group corresponding to the package name
3232- return match[1];
3333- }
3434- return null;
3535- };
3636-3737- // clang-format off
3838- const preloadWasm = () => {
3939- // On Chrome, we have to instantiate wasm asynchronously. Since that
4040- // can't be done synchronously within the call to dlopen, we instantiate
4141- // every .so that comes our way up front, caching it in the
4242- // `preloadedWasm` dictionary.
4343-4444- let promise = new Promise((resolve) => resolve());
4545- const { FS } = pyodide._module;
4646-4747- function recurseDir(rootpath) {
4848- let dirs;
4949- try {
5050- dirs = FS.readdir(rootpath);
5151- } catch (err) {
5252- return;
5353- }
5454- for (const entry of dirs) {
5555- if (entry.startsWith('.')) {
5656- continue;
5757- }
5858- const path = rootpath + entry;
5959- if (entry.endsWith('.so')) {
6060- if (Module.preloadedWasm[path] === undefined) {
6161- promise = promise
6262- .then(() =>
6363- Module.loadWebAssemblyModule(FS.readFile(path), {
6464- loadAsync: true,
6565- })
6666- )
6767- .then((module) => {
6868- Module.preloadedWasm[path] = module;
6969- });
7070- }
7171- } else if (FS.isDir(FS.lookupPath(path).node.mode)) {
7272- recurseDir(`${path}/`);
7373- }
7474- }
7575- }
7676-7777- recurseDir('/');
7878-7979- return promise;
8080- };
8181- // clang-format on
8282-8383- function loadScript(url, onload, onerror) {
8484- if (self.document) {
8585- // browser
8686- const script = self.document.createElement('script');
8787- script.src = url;
8888- script.onload = (e) => {
8989- onload();
9090- };
9191- script.onerror = (e) => {
9292- onerror();
9393- };
9494- self.document.head.appendChild(script);
9595- } else if (self.importScripts) {
9696- // webworker
9797- try {
9898- self.importScripts(url);
9999- onload();
100100- } catch (err) {
101101- onerror();
102102- }
103103- }
104104- }
105105-106106- const _loadPackage = (names, messageCallback) => {
107107- // DFS to find all dependencies of the requested packages
108108- const packages = self.pyodide._module.packages.dependencies;
109109- const { loadedPackages } = self.pyodide;
110110- const queue = [].concat(names || []);
111111- const toLoad = [];
112112- while (queue.length) {
113113- let package_uri = queue.pop();
114114-115115- const packageName = _uri_to_package_name(package_uri);
116116-117117- if (packageName == null) {
118118- console.error(`Invalid package name or URI '${package_uri}'`);
119119- return;
120120- }
121121- if (packageName == package_uri) {
122122- package_uri = 'default channel';
123123- }
124124-125125- if (packageName in loadedPackages) {
126126- if (package_uri != loadedPackages[packageName]) {
127127- console.error(
128128- `URI mismatch, attempting to load package ` +
129129- `${packageName} from ${package_uri} while it is already ` +
130130- `loaded from ${loadedPackages[packageName]}!`
131131- );
132132- return;
133133- }
134134- } else if (packageName in toLoad) {
135135- if (package_uri != toLoad[packageName]) {
136136- console.error(
137137- `URI mismatch, attempting to load package ` +
138138- `${packageName} from ${package_uri} while it is already ` +
139139- `being loaded from ${toLoad[packageName]}!`
140140- );
141141- return;
142142- }
143143- } else {
144144- console.log(`Loading ${packageName} from ${package_uri}`);
145145-146146- toLoad[packageName] = package_uri;
147147- if (packages.hasOwnProperty(packageName)) {
148148- packages[packageName].forEach((subpackage) => {
149149- if (!(subpackage in loadedPackages) && !(subpackage in toLoad)) {
150150- queue.push(subpackage);
151151- }
152152- });
153153- } else {
154154- console.error(`Unknown package '${packageName}'`);
155155- }
156156- }
157157- }
158158-159159- self.pyodide._module.locateFile = (path) => {
160160- // handle packages loaded from custom URLs
161161- const packageName = path.replace(/\.data$/, '');
162162- if (packageName in toLoad) {
163163- const package_uri = toLoad[packageName];
164164- if (package_uri != 'default channel') {
165165- return package_uri.replace(/\.js$/, '.data');
166166- }
167167- }
168168- return baseURL + path;
169169- };
170170-171171- const promise = new Promise((resolve, reject) => {
172172- if (Object.keys(toLoad).length === 0) {
173173- resolve('No new packages to load');
174174- return;
175175- }
176176-177177- const packageList = Array.from(Object.keys(toLoad)).join(', ');
178178- if (messageCallback !== undefined) {
179179- messageCallback(`Loading ${packageList}`);
180180- }
181181-182182- // monitorRunDependencies is called at the beginning and the end of each
183183- // package being loaded. We know we are done when it has been called
184184- // exactly "toLoad * 2" times.
185185- let packageCounter = Object.keys(toLoad).length * 2;
186186-187187- self.pyodide._module.monitorRunDependencies = () => {
188188- packageCounter--;
189189- if (packageCounter === 0) {
190190- for (const packageName in toLoad) {
191191- self.pyodide.loadedPackages[packageName] = toLoad[packageName];
192192- }
193193- delete self.pyodide._module.monitorRunDependencies;
194194- self.removeEventListener('error', windowErrorHandler);
195195- if (!isFirefox) {
196196- preloadWasm().then(() => {
197197- resolve(`Loaded ${packageList}`);
198198- });
199199- } else {
200200- resolve(`Loaded ${packageList}`);
201201- }
202202- }
203203- };
204204-205205- // Add a handler for any exceptions that are thrown in the process of
206206- // loading a package
207207- var windowErrorHandler = (err) => {
208208- delete self.pyodide._module.monitorRunDependencies;
209209- self.removeEventListener('error', windowErrorHandler);
210210- // Set up a new Promise chain, since this one failed
211211- loadPackagePromise = new Promise((resolve) => resolve());
212212- reject(err.message);
213213- };
214214- self.addEventListener('error', windowErrorHandler);
215215-216216- for (const packageName in toLoad) {
217217- let scriptSrc;
218218- const package_uri = toLoad[packageName];
219219- if (package_uri == 'default channel') {
220220- scriptSrc = `${baseURL}${packageName}.js`;
221221- } else {
222222- scriptSrc = `${package_uri}`;
223223- }
224224- loadScript(
225225- scriptSrc,
226226- () => {},
227227- () => {
228228- // If the package_uri fails to load, call monitorRunDependencies twice
229229- // (so packageCounter will still hit 0 and finish loading), and remove
230230- // the package from toLoad so we don't mark it as loaded.
231231- console.error(`Couldn't load package from URL ${scriptSrc}`);
232232- const index = toLoad.indexOf(packageName);
233233- if (index !== -1) {
234234- toLoad.splice(index, 1);
235235- }
236236- for (let i = 0; i < 2; i++) {
237237- self.pyodide._module.monitorRunDependencies();
238238- }
239239- }
240240- );
241241- }
242242-243243- // We have to invalidate Python's import caches, or it won't
244244- // see the new files. This is done here so it happens in parallel
245245- // with the fetching over the network.
246246- self.pyodide.runPython(
247247- 'import importlib as _importlib\n' + '_importlib.invalidate_caches()\n'
248248- );
249249- });
250250-251251- return promise;
252252- };
253253-254254- const loadPackage = (names, messageCallback) => {
255255- /* We want to make sure that only one loadPackage invocation runs at any
256256- * given time, so this creates a "chain" of promises. */
257257- loadPackagePromise = loadPackagePromise.then(() =>
258258- _loadPackage(names, messageCallback)
259259- );
260260- return loadPackagePromise;
261261- };
262262-263263- /// /////////////////////////////////////////////////////////
264264- // Fix Python recursion limit
265265- function fixRecursionLimit(pyodide) {
266266- // The Javascript/Wasm call stack may be too small to handle the default
267267- // Python call stack limit of 1000 frames. This is generally the case on
268268- // Chrom(ium), but not on Firefox. Here, we determine the Javascript call
269269- // stack depth available, and then divide by 50 (determined heuristically)
270270- // to set the maximum Python call stack depth.
271271-272272- let depth = 0;
273273- function recurse() {
274274- depth += 1;
275275- recurse();
276276- }
277277- try {
278278- recurse();
279279- } catch (err) {}
280280-281281- let recursionLimit = depth / 50;
282282- if (recursionLimit > 1000) {
283283- recursionLimit = 1000;
284284- }
285285- pyodide.runPython(
286286- `import sys; sys.setrecursionlimit(int(${recursionLimit}))`
287287- );
288288- }
289289-290290- /// /////////////////////////////////////////////////////////
291291- // Rearrange namespace for public API
292292- const PUBLIC_API = [
293293- 'globals',
294294- 'loadPackage',
295295- 'loadedPackages',
296296- 'pyimport',
297297- 'repr',
298298- 'runPython',
299299- 'runPythonAsync',
300300- 'checkABI',
301301- 'version',
302302- ];
303303-304304- function makePublicAPI(module, public_api) {
305305- const namespace = { _module: module };
306306- for (const name of public_api) {
307307- namespace[name] = module[name];
308308- }
309309- return namespace;
310310- }
311311-312312- /// /////////////////////////////////////////////////////////
313313- // Loading Pyodide
314314- const wasmURL = `${baseURL}pyodide.asm.wasm`;
315315- let Module = {};
316316- self.Module = Module;
317317-318318- Module.noImageDecoding = true;
319319- Module.noAudioDecoding = true;
320320- Module.noWasmDecoding = true;
321321- Module.preloadedWasm = {};
322322- let isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
323323-324324- const wasm_promise = WebAssembly.compileStreaming(fetch(wasmURL));
325325- Module.instantiateWasm = (info, receiveInstance) => {
326326- wasm_promise
327327- .then((module) => WebAssembly.instantiate(module, info))
328328- .then((instance) => receiveInstance(instance));
329329- return {};
330330- };
331331-332332- Module.checkABI = function (ABI_number) {
333333- if (ABI_number !== parseInt('1')) {
334334- const ABI_mismatch_exception = `ABI numbers differ. Expected 1, got ${ABI_number}`;
335335- console.error(ABI_mismatch_exception);
336336- throw ABI_mismatch_exception;
337337- }
338338- return true;
339339- };
340340-341341- Module.locateFile = (path) => baseURL + path;
342342- const postRunPromise = new Promise((resolve, reject) => {
343343- Module.postRun = () => {
344344- delete self.Module;
345345- fetch(`${baseURL}packages.json`)
346346- .then((response) => response.json())
347347- .then((json) => {
348348- fixRecursionLimit(self.pyodide);
349349- self.pyodide.globals = self.pyodide.runPython(
350350- 'import sys\nsys.modules["__main__"]'
351351- );
352352- self.pyodide = makePublicAPI(self.pyodide, PUBLIC_API);
353353- self.pyodide._module.packages = json;
354354- resolve();
355355- });
356356- };
357357- });
358358-359359- const dataLoadPromise = new Promise((resolve, reject) => {
360360- Module.monitorRunDependencies = (n) => {
361361- if (n === 0) {
362362- delete Module.monitorRunDependencies;
363363- resolve();
364364- }
365365- };
366366- });
367367-368368- Promise.all([postRunPromise, dataLoadPromise]).then(() => resolve());
369369-370370- const data_script_src = `${baseURL}pyodide.asm.data.js`;
371371- loadScript(
372372- data_script_src,
373373- () => {
374374- const scriptSrc = `${baseURL}pyodide.asm.js`;
375375- loadScript(
376376- scriptSrc,
377377- () => {
378378- // The emscripten module needs to be at this location for the core
379379- // filesystem to install itself. Once that's complete, it will be replaced
380380- // by the call to `makePublicAPI` with a more limited public API.
381381- self.pyodide = pyodide(Module);
382382- self.pyodide.loadedPackages = [];
383383- self.pyodide.loadPackage = loadPackage;
384384- },
385385- () => {}
386386- );
387387- },
388388- () => {}
389389- );
390390-});
+23-28
app/utils/pyodide/webworker.js
···33 * pyodide to be used in a web worker within this
44 */
5566-self.languagePluginUrl = './src';
77-importScripts('./pyodide.js');
66+// self.languagePluginUrl = './src';
77+importScripts('./src/pyodide/pyodide.js');
8899-const onmessage = function (e) {
1010- // eslint-disable-line no-unused-vars
1111- languagePluginLoader.then(() => {
1212- // Preloaded packages
1313- self.pyodide.loadPackage(['matplotlib', 'mne', 'pandas']).then(() => {
1414- const { data } = e;
1515- const keys = Object.keys(data);
1616- for (const key of keys) {
1717- if (key !== 'python') {
1818- // Keys other than python must be arguments for the python script.
1919- // Set them on self, so that `from js import key` works.
2020- self[key] = data[key];
2121- }
2222- }
99+async function loadPyodideAndPackages() {
1010+ await loadPyodide({ indexURL: './src/pyodide/' });
1111+ await self.pyodide.loadPackage(['matplotlib', 'mne', 'pandas']);
1212+}
1313+let pyodideReadyPromise = loadPyodideAndPackages();
23142424- self.pyodide
2525- .runPythonAsync(data.python, () => {})
2626- .then((results) => {
2727- self.postMessage({ results });
2828- })
2929- .catch((err) => {
3030- // if you prefer messages with the error
3131- self.postMessage({ error: err.message });
3232- // if you prefer onerror events
3333- // setTimeout(() => { throw err; });
3434- });
1515+self.onmessage = async (event) => {
1616+ // make sure loading is done
1717+ await pyodideReadyPromise;
1818+ // Don't bother yet with this line, suppose our API is built in such a way:
1919+ const { python, ...context } = event.data;
2020+ // The worker copies the context in its own "memory" (an object mapping name to values)
2121+ for (const key of Object.keys(context)) {
2222+ self[key] = context[key];
2323+ }
2424+ // Now is the easy part, the one that is similar to working in the main thread:
2525+ try {
2626+ self.postMessage({
2727+ results: await self.pyodide.runPythonAsync(python),
3528 });
3636- });
2929+ } catch (error) {
3030+ self.postMessage({ error: error.message });
3131+ }
3732};