From 91622d8d846766b97cee5d5e63cad3cc12f1d188 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Mon, 8 Jan 2024 17:02:43 -0500 Subject: [PATCH 01/11] Start on a WASM-base API implementation --- package-lock.json | 11 +++++++++++ package.json | 1 + src/GBZBaseAPI.mjs | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 src/GBZBaseAPI.mjs diff --git a/package-lock.json b/package-lock.json index a0a46311..f11640fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "es-dirname": "^0.1.0", "express": "^4.18.2", "fs-extra": "^10.1.0", + "gbz-base": "^0.1.0-alpha.0", "gh-pages": "^4.0.0", "markdown-to-jsx": "^7.2.0", "multer": "^1.4.5-lts.1", @@ -9867,6 +9868,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gbz-base": { + "version": "0.1.0-alpha.0", + "resolved": "https://registry.npmjs.org/gbz-base/-/gbz-base-0.1.0-alpha.0.tgz", + "integrity": "sha512-OkIQeQpRH5K5uiNp/4+0eMP5eug/bdsABwVGSHHQLY+YW+I+uPxkMvF2gNx/FBcHKtDZxATZDz6zFDXM2LGT0A==" + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -26697,6 +26703,11 @@ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" }, + "gbz-base": { + "version": "0.1.0-alpha.0", + "resolved": "https://registry.npmjs.org/gbz-base/-/gbz-base-0.1.0-alpha.0.tgz", + "integrity": "sha512-OkIQeQpRH5K5uiNp/4+0eMP5eug/bdsABwVGSHHQLY+YW+I+uPxkMvF2gNx/FBcHKtDZxATZDz6zFDXM2LGT0A==" + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/package.json b/package.json index 2a3c5349..cd98d5c7 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "es-dirname": "^0.1.0", "express": "^4.18.2", "fs-extra": "^10.1.0", + "gbz-base": "^0.1.0-alpha.0", "gh-pages": "^4.0.0", "markdown-to-jsx": "^7.2.0", "multer": "^1.4.5-lts.1", diff --git a/src/GBZBaseAPI.mjs b/src/GBZBaseAPI.mjs new file mode 100644 index 00000000..429cc66d --- /dev/null +++ b/src/GBZBaseAPI.mjs @@ -0,0 +1,32 @@ +import { APIInterface } from "./APIInterface.mjs"; + +/** + * API implementation that uses tools compiled to WebAssembly, client-side. + */ +export class GBZBaseAPI extends APIInterface { + constructor() { + super(); + } + + async getChunkedData(viewTarget, cancelSignal) { + return {}; + } + + async getFilenames(cancelSignal) { + return []; + } + + async getBedRegions(bedFile, cancelSignal) { + return []; + } + + async getPathNames(graphFile, cancelSignal) { + return []; + } + + async getChunkTracks(bedFile, chunk, cancelSignal) { + return []; + } +} + +export default GBZBaseAPI; From 39674e04ce5509af69e48be57f357c5a9180c867 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Mon, 8 Jan 2024 17:20:38 -0500 Subject: [PATCH 02/11] Put filename update subscriptions behind the API object --- src/APIInterface.mjs | 7 +++++++ src/GBZBaseAPI.mjs | 28 +++++++++++++++++++++++----- src/ServerAPI.mjs | 35 +++++++++++++++++++++++++++++++++++ src/components/HeaderForm.js | 17 +---------------- 4 files changed, 66 insertions(+), 21 deletions(-) diff --git a/src/APIInterface.mjs b/src/APIInterface.mjs index 29c2e7f6..35e25698 100644 --- a/src/APIInterface.mjs +++ b/src/APIInterface.mjs @@ -17,6 +17,13 @@ export class APIInterface { throw new Error("getFilenames function not implemented"); } + // Get notifications (via calls to handler()) when the set of filenames available from getFilenames() has changed. + // Returns a subscription object that should be kept around as long as you still want updates. + // cancelSignal is an AbortSignal that can be used to cancel the stream of notifications. + subscribeToFilenameChanges(handler, cancelSignal) { + throw new Error("subscribeToFilenameChanges function not implemented"); + } + // Takes in a bedfile path or a url pointing to a raw bed file. // Returns object with key: bedRegions. // bedRegions contains information extrapolated from each line of the bedfile. diff --git a/src/GBZBaseAPI.mjs b/src/GBZBaseAPI.mjs index 429cc66d..177e3cc6 100644 --- a/src/GBZBaseAPI.mjs +++ b/src/GBZBaseAPI.mjs @@ -9,23 +9,41 @@ export class GBZBaseAPI extends APIInterface { } async getChunkedData(viewTarget, cancelSignal) { - return {}; + return { + graph: {}, + gam: {}, + region: null, + coloredNodes: [] + }; } async getFilenames(cancelSignal) { - return []; + return { + files: [], + bedFiles: [] + }; + } + + subscribeToFilenameChanges(handler, cancelSignal) { + return {}; } async getBedRegions(bedFile, cancelSignal) { - return []; + return { + bedRegions: [] + }; } async getPathNames(graphFile, cancelSignal) { - return []; + return { + pathNames: [] + }; } async getChunkTracks(bedFile, chunk, cancelSignal) { - return []; + return { + tracks: [] + }; } } diff --git a/src/ServerAPI.mjs b/src/ServerAPI.mjs index 199aeede..9396c8b0 100644 --- a/src/ServerAPI.mjs +++ b/src/ServerAPI.mjs @@ -35,6 +35,41 @@ export class ServerAPI extends APIInterface { return json; } + subscribeToFilenameChanges(handler, cancelSignal) { + // We need something to hold the one currently active websocket. + subscription = {}; + + // We make a function to connect the websocket, which we can call to reconnect. + function connect() { + subscription.ws = new WebSocket(this.apiUrl.replace(/^http/, "ws")); + subscription.ws.onmessage = (message) => { + if (!cancelSignal.aborted) { + // Tell the user that something changed + handler(); + } else { + subscription.ws.close(); + } + }; + subscription.ws.onclose = (event) => { + if (!cancelSignal.aborted) { + // Reconnect if the socket closed + setTimeout(connect, 1000); + } + }; + subscription.ws.onerror = (event) => { + // Close the socket if something went wrong + subscription.ws.close(); + }; + } + + connect(); + + // Give the subscription back to the caller to hold. + // TODO: Do we really need to hold the web socket in scope? + // TODO: How does the user close the socket without a message arriving after cancelation? + return subscription; + } + async getBedRegions(bedFile, cancelSignal) { const json = await fetchAndParse(`${this.apiUrl}/getBedRegions`, { signal: cancelSignal, diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index 4540504c..f63b52e1 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -704,22 +704,7 @@ class HeaderForm extends Component { }; setUpWebsocket = () => { - this.ws = new WebSocket(this.props.apiUrl.replace(/^http/, "ws")); - this.ws.onmessage = (message) => { - if (!this.cancelSignal.aborted) { - this.getMountedFilenames(); - } else { - this.ws.close(); - } - }; - this.ws.onclose = (event) => { - if (!this.cancelSignal.aborted) { - setTimeout(this.setUpWebsocket, 1000); - } - }; - this.ws.onerror = (event) => { - this.ws.close(); - }; + this.subscription = this.api.subscribeToFilenameChanges(this.getMountedFilenames, this.cancelSignal); }; /* Function for toggling simplify button, enabling vg simplify to be turned on or off */ From 2e00347d1aa5e92f9fb00eea7b5ac60db990b49a Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Mon, 8 Jan 2024 17:47:24 -0500 Subject: [PATCH 03/11] Bring file uploads and new file notifications through the API client --- src/APIInterface.mjs | 9 +++++ src/ServerAPI.mjs | 64 +++++++++++++++++++++++++++++++- src/components/HeaderForm.js | 71 ++++++++---------------------------- 3 files changed, 87 insertions(+), 57 deletions(-) diff --git a/src/APIInterface.mjs b/src/APIInterface.mjs index 35e25698..6da90164 100644 --- a/src/APIInterface.mjs +++ b/src/APIInterface.mjs @@ -24,6 +24,15 @@ export class APIInterface { throw new Error("subscribeToFilenameChanges function not implemented"); } + // Upload a file. + // fileType is a track type like "graph" or "read". + // file is the file data (Blob or File). + // cancelSignal is an AbortSignal that can be used to cancel the upload. + // Resolves with the file name that can be used to refer to the uploaded file. + async putFile(fileType, file, cancelSignal) { + throw new Error("putFile function not implemented"); + } + // Takes in a bedfile path or a url pointing to a raw bed file. // Returns object with key: bedRegions. // bedRegions contains information extrapolated from each line of the bedfile. diff --git a/src/ServerAPI.mjs b/src/ServerAPI.mjs index 9396c8b0..037e6efe 100644 --- a/src/ServerAPI.mjs +++ b/src/ServerAPI.mjs @@ -37,10 +37,10 @@ export class ServerAPI extends APIInterface { subscribeToFilenameChanges(handler, cancelSignal) { // We need something to hold the one currently active websocket. - subscription = {}; + let subscription = {}; // We make a function to connect the websocket, which we can call to reconnect. - function connect() { + let connect = () => { subscription.ws = new WebSocket(this.apiUrl.replace(/^http/, "ws")); subscription.ws.onmessage = (message) => { if (!cancelSignal.aborted) { @@ -70,6 +70,66 @@ export class ServerAPI extends APIInterface { return subscription; } + async putFile(fileType, file, cancelSignal) { + + // Prepare the form data for upload + const formData = new FormData(); + // If the file is anything other than a Blob, it will be turned into a + // string and added as a normal form value. If it is a Blob it will + // become a file upload. Note that a File is a kind of Blob. See + // + // + // But in jsdom in the test environment there are two Blob types: Node's + // and jdsom's, and only jsdom's will work. Node's will turn into a + // string. And it seems hard to get at both types in a way that makes + // sense in a browser. So we will add the file and make sure it added OK + // and didn't stringify. + + // According to , we *must* set a filename for uploads. + // In jsdom it turns on jsdom's own type checking support. + let fileName = file.name || "upload.dat"; + formData.append("trackFile", file, fileName); + if (typeof formData.get("trackFile") == "string") { + // Catch stringification in case jsdom didn't. + console.error( + "Cannot upload file because it is not the appropriate type:", + file + ); + throw new Error("File is not an appropriate type to upload"); + } + // Make sure server can identify a Read file + formData.append("fileType", fileType); + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.responseType = "json"; + xhr.onreadystatechange = () => { + if (cancelSignal.aborted && xhr.readyState != 0) { + // First time we have noticed we are aborted. Stop the request. + xhr.abort(); + reject(new Error("Upload aborted")); + return + } + + if (xhr.readyState === 4) { + if (xhr.status === 200 && xhr.response.path) { + // Every thing ok, file uploaded, and we got a path. + resolve(xhr.response.path); + } else { + // Something weird happened. + reject(new Error("Failed to upload file: status " + xhr.status + " and response: " + xhr.response)); + } + } + }; + + console.log("Uploading file", file); + console.log("Sending form data", formData); + console.log("Form file is a " + typeof formData.get("trackFile")); + xhr.open("POST", `${this.apiUrl}/trackFileSubmission`, true); + xhr.send(formData); + }); + } + async getBedRegions(bedFile, cancelSignal) { const json = await fetchAndParse(`${this.apiUrl}/getBedRegions`, { signal: cancelSignal, diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index f63b52e1..01c9d3a6 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -645,62 +645,23 @@ class HeaderForm extends Component { // Sends uploaded file to server and returns a path to the file handleFileUpload = async (fileType, file) => { - return new Promise( - function (resolve, reject) { - if (file.size > config.MAXUPLOADSIZE) { - this.showFileSizeAlert(); - return; - } - - this.setUploadInProgress(true); - - const formData = new FormData(); - // If the file is anything other than a Blob, it will be turned into a - // string and added as a normal form value. If it is a Blob it will - // become a file upload. Note that a File is a kind of Blob. See - // - // - // But in jsdom in the test environment there are two Blob types: Node's - // and jdsom's, and only jsdom's will work. Node's will turn into a - // string. And it seems hard to get at both types in a way that makes - // sense in a browser. So we will add the file and make sure it added OK - // and didn't stringify. - - // According to , we *must* set a filename for uploads. - // In jsdom it turns on jsdom's own type checking support. - let fileName = file.name || "upload.dat"; - formData.append("trackFile", file, fileName); - if (typeof formData.get("trackFile") == "string") { - // Catch stringification in case jsdom didn't. - console.error( - "Cannot upload file because it is not the appropriate type:", - file - ); - throw new Error("File is not an appropriate type to upload"); - } - // Make sure server can identify a Read file - formData.append("fileType", fileType); - const xhr = new XMLHttpRequest(); - xhr.responseType = "json"; - xhr.onreadystatechange = () => { - if (xhr.readyState === 4 && xhr.status === 200) { - // Every thing ok, file uploaded - this.setUploadInProgress(false); - if (fileType === "graph") { - this.getPathNames(xhr.response.path); - } - - resolve(xhr.response.path); - } - }; + if (file.size > config.MAXUPLOADSIZE) { + this.showFileSizeAlert(); + return; + } - console.log("Uploading file", file); - console.log("Sending form data", formData); - console.log("Form file is a " + typeof formData.get("trackFile")); - xhr.open("POST", `${this.props.apiUrl}/trackFileSubmission`, true); - xhr.send(formData); - }.bind(this) - ); + this.setUploadInProgress(true); + + try { + let fileName = await this.api.putFile(fileType, file, this.cancelSignal); + this.setUploadInProgress(false); + return fileName; + } catch (e) { + if (!this.cancelSignal.aborted) { + // Only pass along errors if we haven't canceled our fetches. + throw e; + } + } }; setUpWebsocket = () => { From 557b0bb03d60fb53b33091cba2cdcad5c18a65bb Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Mon, 8 Jan 2024 18:33:10 -0500 Subject: [PATCH 04/11] Start on WASM integration and immediately find we have no good way to load it --- src/APIInterface.mjs | 2 ++ src/GBZBaseAPI.mjs | 53 +++++++++++++++++++++++++++++++++++- src/GBZBaseAPI.test.js | 33 ++++++++++++++++++++++ src/components/HeaderForm.js | 4 +++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/GBZBaseAPI.test.js diff --git a/src/APIInterface.mjs b/src/APIInterface.mjs index 6da90164..e0a44d43 100644 --- a/src/APIInterface.mjs +++ b/src/APIInterface.mjs @@ -12,6 +12,8 @@ export class APIInterface { // Returns files used to determine what options are available in the track picker. // Returns object with keys: files, bedFiles. + // files holds an array of objects like { name: string; type: filetype;}, where filetype is a file type like "graph". + // bedFiles just holds an array of strings. // cancelSignal is an AbortSignal that can be used to cancel the request. async getFilenames(cancelSignal) { throw new Error("getFilenames function not implemented"); diff --git a/src/GBZBaseAPI.mjs b/src/GBZBaseAPI.mjs index 177e3cc6..31645e17 100644 --- a/src/GBZBaseAPI.mjs +++ b/src/GBZBaseAPI.mjs @@ -1,11 +1,32 @@ import { APIInterface } from "./APIInterface.mjs"; +// TODO: The Webpack way to get the WASM would be something like: +//import QueryWasm from "gbz-base/target/wasm32-wasi/release/query.wasm"; +// if the export mapping is broken, or +//import QueryWasm from "gbz-base/query.wasm"; +// if it is working. In Jest, not only is the export mapping not working, but +// also it can't get us a fetch-able string from the import like Webpack does. +// So we will need some fancy Jest config to mock the WASM file into a js +// module that does *something*, and also to mock fetch() into something that +// can fetch it. Or else we need to hide that all behind something that can +// fetch the WASM on either Webpack or Jest with its own strategies/by being +// swapped out. + /** * API implementation that uses tools compiled to WebAssembly, client-side. */ export class GBZBaseAPI extends APIInterface { constructor() { super(); + + // We can take user uploads, in which case we need to hold on to them somewhere. + // This holds all the file objects. + this.files = []; + + // We need to index all their names by type. + this.filesByType = {}; + + // We need to set up our WASM } async getChunkedData(viewTarget, cancelSignal) { @@ -18,16 +39,46 @@ export class GBZBaseAPI extends APIInterface { } async getFilenames(cancelSignal) { - return { + // Set up an empty response. + let response = { files: [], bedFiles: [] }; + + for (let type of this.filesByType) { + if (type == "bed") { + // Just send all these files in bedFiles. + response.bedFiles = this.filesByType[type]; + } else { + for (let fileName of this.filesByType[type]) { + // We sens a name/type record for each non-BED file + response.files.push({name: fileName, type: type}); + } + } + } + + return response; } subscribeToFilenameChanges(handler, cancelSignal) { return {}; } + async putFile(fileType, file, cancelSignal) { + // We track files just by array index. + let fileName = this.files.length.toString(); + // Just hang on to the File object. + this.files.push(file); + + if (this.filesByType[fileType] === undefined) { + this.filesByType[fileType] = []; + } + // Index the name we produced by type. + this.filesByType[fileType].push(fileName); + + return fileName; + } + async getBedRegions(bedFile, cancelSignal) { return { bedRegions: [] diff --git a/src/GBZBaseAPI.test.js b/src/GBZBaseAPI.test.js new file mode 100644 index 00000000..829abe04 --- /dev/null +++ b/src/GBZBaseAPI.test.js @@ -0,0 +1,33 @@ +import { GBZBaseAPI } from './GBZBaseAPI.mjs'; + +import fs from "fs-extra"; + +it("can be constructed", () => { + let api = new GBZBaseAPI(); +}); + +it("can have a file uploaded", async () => { + let api = new GBZBaseAPI(); + + // We need to make sure we make a jsdom File (which is a jsdom Blob), and not + // a Node Blob, for our test file. Otherwise it doesn't work with jsdom's + // upload machinery. + // See for example for + // background on the many flavors of Blob. + const fileData = await fs.readFileSync("exampleData/cactus.vg"); + // Since a Node Buffer is an ArrayBuffer, we can use it to make a jsdom File. + // We need to put the data block in an enclosing array, or else the block + // will be iterated and each byte will be stringified and *those* bytes will + // be uploaded. + const file = new window.File([fileData], "cactus.vg", { + type: "application/octet-stream", + }); + + // Set up for canceling the upload + let controller = new AbortController(); + + let uploadName = await api.putFile("graph", file, controller.signal); + + expect(uploadName).toBeTruthy(); + +}); diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index 01c9d3a6..d1345b3a 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -654,6 +654,10 @@ class HeaderForm extends Component { try { let fileName = await this.api.putFile(fileType, file, this.cancelSignal); + if (fileType == "graph") { + // Refresh the graphs right away + this.getMountedFilenames(); + } this.setUploadInProgress(false); return fileName; } catch (e) { From 9b8de9b5dbfcd1a17f7c6d7af98604d1d0d4b661 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Wed, 10 Jan 2024 10:39:45 -0500 Subject: [PATCH 05/11] Satisfy build linter --- src/ServerAPI.mjs | 2 +- src/components/HeaderForm.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ServerAPI.mjs b/src/ServerAPI.mjs index 037e6efe..dff4418b 100644 --- a/src/ServerAPI.mjs +++ b/src/ServerAPI.mjs @@ -104,7 +104,7 @@ export class ServerAPI extends APIInterface { const xhr = new XMLHttpRequest(); xhr.responseType = "json"; xhr.onreadystatechange = () => { - if (cancelSignal.aborted && xhr.readyState != 0) { + if (cancelSignal.aborted && xhr.readyState !== 0) { // First time we have noticed we are aborted. Stop the request. xhr.abort(); reject(new Error("Upload aborted")); diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index d1345b3a..ff8f86b8 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -654,7 +654,7 @@ class HeaderForm extends Component { try { let fileName = await this.api.putFile(fileType, file, this.cancelSignal); - if (fileType == "graph") { + if (fileType === "graph") { // Refresh the graphs right away this.getMountedFilenames(); } From 5734933711d60bf0a280b29b7ad6f0f064763bc5 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Wed, 10 Jan 2024 10:40:30 -0500 Subject: [PATCH 06/11] Run formatter --- src/GBZBaseAPI.mjs | 12 ++++++------ src/GBZBaseAPI.test.js | 7 +++---- src/ServerAPI.mjs | 14 ++++++++++---- src/components/HeaderForm.js | 7 +++++-- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/GBZBaseAPI.mjs b/src/GBZBaseAPI.mjs index 31645e17..4b2a56ae 100644 --- a/src/GBZBaseAPI.mjs +++ b/src/GBZBaseAPI.mjs @@ -34,7 +34,7 @@ export class GBZBaseAPI extends APIInterface { graph: {}, gam: {}, region: null, - coloredNodes: [] + coloredNodes: [], }; } @@ -42,7 +42,7 @@ export class GBZBaseAPI extends APIInterface { // Set up an empty response. let response = { files: [], - bedFiles: [] + bedFiles: [], }; for (let type of this.filesByType) { @@ -52,7 +52,7 @@ export class GBZBaseAPI extends APIInterface { } else { for (let fileName of this.filesByType[type]) { // We sens a name/type record for each non-BED file - response.files.push({name: fileName, type: type}); + response.files.push({ name: fileName, type: type }); } } } @@ -81,19 +81,19 @@ export class GBZBaseAPI extends APIInterface { async getBedRegions(bedFile, cancelSignal) { return { - bedRegions: [] + bedRegions: [], }; } async getPathNames(graphFile, cancelSignal) { return { - pathNames: [] + pathNames: [], }; } async getChunkTracks(bedFile, chunk, cancelSignal) { return { - tracks: [] + tracks: [], }; } } diff --git a/src/GBZBaseAPI.test.js b/src/GBZBaseAPI.test.js index 829abe04..6d2ca415 100644 --- a/src/GBZBaseAPI.test.js +++ b/src/GBZBaseAPI.test.js @@ -1,4 +1,4 @@ -import { GBZBaseAPI } from './GBZBaseAPI.mjs'; +import { GBZBaseAPI } from "./GBZBaseAPI.mjs"; import fs from "fs-extra"; @@ -22,12 +22,11 @@ it("can have a file uploaded", async () => { const file = new window.File([fileData], "cactus.vg", { type: "application/octet-stream", }); - + // Set up for canceling the upload let controller = new AbortController(); let uploadName = await api.putFile("graph", file, controller.signal); - expect(uploadName).toBeTruthy(); - + expect(uploadName).toBeTruthy(); }); diff --git a/src/ServerAPI.mjs b/src/ServerAPI.mjs index dff4418b..39ee0bf5 100644 --- a/src/ServerAPI.mjs +++ b/src/ServerAPI.mjs @@ -60,7 +60,7 @@ export class ServerAPI extends APIInterface { // Close the socket if something went wrong subscription.ws.close(); }; - } + }; connect(); @@ -71,7 +71,6 @@ export class ServerAPI extends APIInterface { } async putFile(fileType, file, cancelSignal) { - // Prepare the form data for upload const formData = new FormData(); // If the file is anything other than a Blob, it will be turned into a @@ -108,7 +107,7 @@ export class ServerAPI extends APIInterface { // First time we have noticed we are aborted. Stop the request. xhr.abort(); reject(new Error("Upload aborted")); - return + return; } if (xhr.readyState === 4) { @@ -117,7 +116,14 @@ export class ServerAPI extends APIInterface { resolve(xhr.response.path); } else { // Something weird happened. - reject(new Error("Failed to upload file: status " + xhr.status + " and response: " + xhr.response)); + reject( + new Error( + "Failed to upload file: status " + + xhr.status + + " and response: " + + xhr.response + ) + ); } } }; diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index ff8f86b8..73f7b5a5 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -651,7 +651,7 @@ class HeaderForm extends Component { } this.setUploadInProgress(true); - + try { let fileName = await this.api.putFile(fileType, file, this.cancelSignal); if (fileType === "graph") { @@ -669,7 +669,10 @@ class HeaderForm extends Component { }; setUpWebsocket = () => { - this.subscription = this.api.subscribeToFilenameChanges(this.getMountedFilenames, this.cancelSignal); + this.subscription = this.api.subscribeToFilenameChanges( + this.getMountedFilenames, + this.cancelSignal + ); }; /* Function for toggling simplify button, enabling vg simplify to be turned on or off */ From 272498531de425b392c94044410fdeee77fef8ec Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Wed, 10 Jan 2024 10:44:28 -0500 Subject: [PATCH 07/11] Hide the apiUrl behind the client --- src/App.js | 2 -- src/components/HeaderForm.js | 1 - src/components/TubeMapContainer.js | 3 +-- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/App.js b/src/App.js index 2ebe9404..371e5f8e 100644 --- a/src/App.js +++ b/src/App.js @@ -183,7 +183,6 @@ class App extends Component { setDataOrigin={this.setDataOrigin} setColorSetting={this.setColorSetting} dataOrigin={this.state.dataOrigin} - apiUrl={this.props.apiUrl} defaultViewTarget={this.defaultViewTarget} getCurrentViewTarget={this.getCurrentViewTarget} APIInterface={this.APIInterface} @@ -191,7 +190,6 @@ class App extends Component { diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index 73f7b5a5..5a1b837f 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -860,7 +860,6 @@ class HeaderForm extends Component { } HeaderForm.propTypes = { - apiUrl: PropTypes.string.isRequired, dataOrigin: PropTypes.string.isRequired, setColorSetting: PropTypes.func.isRequired, setDataOrigin: PropTypes.func.isRequired, diff --git a/src/components/TubeMapContainer.js b/src/components/TubeMapContainer.js index dd8ac702..47fa48ac 100644 --- a/src/components/TubeMapContainer.js +++ b/src/components/TubeMapContainer.js @@ -218,7 +218,7 @@ class TubeMapContainer extends Component { } catch (error) { this.handleFetchError( error, - `Fetching and parsing POST to ${this.props.apiUrl}/getChunkedData failed:` + "Fetching and parsing getChunkedData failed:" ); } }; @@ -296,7 +296,6 @@ class TubeMapContainer extends Component { } TubeMapContainer.propTypes = { - apiUrl: PropTypes.string.isRequired, dataOrigin: PropTypes.oneOf(Object.values(dataOriginTypes)).isRequired, viewTarget: PropTypes.object.isRequired, visOptions: PropTypes.object.isRequired, From 6992eca038473b34ab69f0b7dda35e0190539fc3 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Wed, 10 Jan 2024 13:04:35 -0500 Subject: [PATCH 08/11] Get WASM blob to load and Rust-panic in Jest --- package-lock.json | 455 ++++++++++++++++-- package.json | 7 +- .../@bjorn3+browser_wasi_shim+0.2.17.patch | 4 + src/GBZBaseAPI.mjs | 76 ++- src/GBZBaseAPI.test.js | 5 + 5 files changed, 516 insertions(+), 31 deletions(-) create mode 100644 patches/@bjorn3+browser_wasi_shim+0.2.17.patch diff --git a/package-lock.json b/package-lock.json index f11640fa..2ffa6061 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "@bjorn3/browser_wasi_shim": "^0.2.17", "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", "@fortawesome/fontawesome-svg-core": "^6.4.0", @@ -38,6 +39,7 @@ "markdown-to-jsx": "^7.2.0", "multer": "^1.4.5-lts.1", "node-cron": "^3.0.2", + "patch-package": "^8.0.0", "path-is-inside": "^1.0.2", "polyfill-object.fromentries": "^1.0.1", "prop-types": "^15.8.1", @@ -1984,6 +1986,11 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, + "node_modules/@bjorn3/browser_wasi_shim": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.2.17.tgz", + "integrity": "sha512-B2qcaGROo4e2s4nXb3VPATrczVrntM4BUXtAU1gEzUOfqKTcVuePq4NfhH5hmLBSvZ45YcT4gflDRUFYqLhkxA==" + }, "node_modules/@csstools/normalize.css": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz", @@ -5446,6 +5453,11 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==" + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -6393,12 +6405,13 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7893,6 +7906,19 @@ "node": ">= 10" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -9595,6 +9621,14 @@ "node": ">=8" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -9839,9 +9873,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -9890,13 +9927,14 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10217,6 +10255,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -13236,6 +13285,23 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-stable-stringify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.0.tgz", + "integrity": "sha512-zfA+5SuwYN2VWqN1/5HZaDzQKLJHaBVMZIIM+wuYjdptkaQsqzDdqjqf+lZZJUuJq1aanHiY8LhH8LmH+qBYJA==", + "dependencies": { + "call-bind": "^1.0.5", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -13263,6 +13329,14 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsonpointer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", @@ -13373,6 +13447,14 @@ "node": ">=0.10.0" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -14234,6 +14316,14 @@ "node": ">= 0.8.0" } }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -14338,6 +14428,121 @@ "tslib": "^2.0.3" } }, + "node_modules/patch-package": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", + "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^9.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "rimraf": "^2.6.3", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.0.33", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/patch-package/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/patch-package/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/patch-package/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/patch-package/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/patch-package/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/patch-package/node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "engines": { + "node": ">= 14" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -17131,6 +17336,20 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -18163,6 +18382,17 @@ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -20911,6 +21141,11 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, + "@bjorn3/browser_wasi_shim": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.2.17.tgz", + "integrity": "sha512-B2qcaGROo4e2s4nXb3VPATrczVrntM4BUXtAU1gEzUOfqKTcVuePq4NfhH5hmLBSvZ45YcT4gflDRUFYqLhkxA==" + }, "@csstools/normalize.css": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz", @@ -23399,6 +23634,11 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==" + }, "abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -24096,12 +24336,13 @@ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" } }, "callsites": { @@ -25254,6 +25495,16 @@ "execa": "^5.0.0" } }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -26523,6 +26774,14 @@ "path-exists": "^4.0.0" } }, + "find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "requires": { + "micromatch": "^4.0.2" + } + }, "flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -26683,9 +26942,9 @@ "optional": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "function.prototype.name": { "version": "1.1.5", @@ -26719,13 +26978,14 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, "get-own-enumerable-property-symbols": { @@ -26950,6 +27210,14 @@ "has-symbols": "^1.0.2" } }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "requires": { + "function-bind": "^1.1.2" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -29295,6 +29563,17 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-stable-stringify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.0.tgz", + "integrity": "sha512-zfA+5SuwYN2VWqN1/5HZaDzQKLJHaBVMZIIM+wuYjdptkaQsqzDdqjqf+lZZJUuJq1aanHiY8LhH8LmH+qBYJA==", + "requires": { + "call-bind": "^1.0.5", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + } + }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -29314,6 +29593,11 @@ "universalify": "^2.0.0" } }, + "jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==" + }, "jsonpointer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", @@ -29411,6 +29695,14 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, + "klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "requires": { + "graceful-fs": "^4.1.11" + } + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -30036,6 +30328,11 @@ "word-wrap": "^1.2.3" } }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==" + }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -30113,6 +30410,89 @@ "tslib": "^2.0.3" } }, + "patch-package": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", + "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", + "requires": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^9.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "rimraf": "^2.6.3", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.0.33", + "yaml": "^2.2.2" + }, + "dependencies": { + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==" + } + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -31990,6 +32370,17 @@ "send": "0.18.0" } }, + "set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -32747,6 +33138,14 @@ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index cd98d5c7..9754631b 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "author": "Wolfgang Beyer", "license": "MIT", "dependencies": { + "@bjorn3/browser_wasi_shim": "^0.2.17", "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", "@fortawesome/fontawesome-svg-core": "^6.4.0", @@ -33,6 +34,7 @@ "markdown-to-jsx": "^7.2.0", "multer": "^1.4.5-lts.1", "node-cron": "^3.0.2", + "patch-package": "^8.0.0", "path-is-inside": "^1.0.2", "polyfill-object.fromentries": "^1.0.1", "prop-types": "^15.8.1", @@ -64,7 +66,8 @@ "predeploy": "npm run build", "deploy": "gh-pages -d build", "serve": "node ./src/server.mjs", - "format": "prettier --write \"**/*.+(mjs|js|css)\"" + "format": "prettier --write \"**/*.+(mjs|js|css)\"", + "postinstall": "patch-package" }, "eslintConfig": { "extends": "react-app" @@ -87,7 +90,7 @@ "jest": { "resetMocks": false, "transformIgnorePatterns": [ - "node_modules/(?!(@streamparser/json)/)" + "node_modules/(?!(@streamparser/json|@bjorn3/browser_wasi_shim)/)" ] } } diff --git a/patches/@bjorn3+browser_wasi_shim+0.2.17.patch b/patches/@bjorn3+browser_wasi_shim+0.2.17.patch new file mode 100644 index 00000000..30c65581 --- /dev/null +++ b/patches/@bjorn3+browser_wasi_shim+0.2.17.patch @@ -0,0 +1,4 @@ +diff --git a/node_modules/@bjorn3/browser_wasi_shim/.package.json.swp b/node_modules/@bjorn3/browser_wasi_shim/.package.json.swp +new file mode 100644 +index 0000000..334b4b6 +Binary files /dev/null and b/node_modules/@bjorn3/browser_wasi_shim/.package.json.swp differ diff --git a/src/GBZBaseAPI.mjs b/src/GBZBaseAPI.mjs index 4b2a56ae..0988c3df 100644 --- a/src/GBZBaseAPI.mjs +++ b/src/GBZBaseAPI.mjs @@ -1,4 +1,6 @@ import { APIInterface } from "./APIInterface.mjs"; +import { readFile } from "fs-extra"; +import { WASI, File, OpenFile, PreopenDirectory } from "@bjorn3/browser_wasi_shim"; // TODO: The Webpack way to get the WASM would be something like: //import QueryWasm from "gbz-base/target/wasm32-wasi/release/query.wasm"; @@ -12,6 +14,30 @@ import { APIInterface } from "./APIInterface.mjs"; // fetch the WASM on either Webpack or Jest with its own strategies/by being // swapped out. +// Resolve with the bytes of the WASM query blob, on Jest or Webpack. +async function getWasmBytes() { + let blobBytes = null; + + if (!jest) { + // Not running on Jest, we should be able to dynamic import a binary asset and get the bytes, and Webpack will handle it. + try { + blobBytes = await import("gbz-base/target/wasm32-wasi/release/query.wasm"); + } catch (e) { + console.error("Could not dynamically import WASM blob.", e); + // Leave blobBytes unset to try a fallback method. + } + } + + if (!blobBytes) { + // Either we're on Jest, or the dynamic import didn't work (maybe we're on plain Node?). + // Try to open the file from the filesystem. + blobBytes = await readFile("node_modules/gbz-base/target/wasm32-wasi/release/query.wasm"); + } + + console.log("Got blob bytes: ", blobBytes); + return blobBytes; +} + /** * API implementation that uses tools compiled to WebAssembly, client-side. */ @@ -26,9 +52,57 @@ export class GBZBaseAPI extends APIInterface { // We need to index all their names by type. this.filesByType = {}; - // We need to set up our WASM + // This is a promise for the compiled WebAssembly blob. + this.compiledWasm = undefined; } + // Make sure our WASM backend is ready. + async setUp() { + if (this.compiledWasm === undefined) { + // Kick off and save exactly one request to get and load the WASM bytes. + this.compiledWasm = WebAssembly.compile(await getWasmBytes()); + } + + // Wait for the bytes to be available. + this.compiledWasm = await this.compiledWasm; + } + + // Make a call into the WebAssembly code and return the result. + async callWasm(argv) { + await this.setUp(); + let wasm = this.compiledWasm; + + // Define the places to store program input and output + let stdin = new File([]); + let stdout = new File([]); + let stderr = new File([]); + + // Environment variables as NAME=value strings + const environment = []; + + // File descriptors for the process in number order + let file_descriptors = [new OpenFile(stdin), new OpenFile(stdout), new OpenFile(stderr)]; + + // Set up the WASI interface + let wasi = new WASI(argv, environment, file_descriptors); + + // Set up the WebAssembly run + let instantiation = await WebAssembly.instantiate(this.compiledWasm, { + "wasi_snapshot_preview1": wasi.wasiImport, + }); + + // Make the WASI system call main + let returnCode = wasi.start(instantiation); + + console.log("Return code:", returnCode); + console.log("Standard Output:", stdout); + console.log("Standard Error:", stderr); + } + + ///////// + // Tube Map API implementation + ///////// + async getChunkedData(viewTarget, cancelSignal) { return { graph: {}, diff --git a/src/GBZBaseAPI.test.js b/src/GBZBaseAPI.test.js index 6d2ca415..9b8da810 100644 --- a/src/GBZBaseAPI.test.js +++ b/src/GBZBaseAPI.test.js @@ -6,6 +6,11 @@ it("can be constructed", () => { let api = new GBZBaseAPI(); }); +it("can run the WASM blob", async () => { + let api = new GBZBaseAPI(); + await api.callWasm([]); +}); + it("can have a file uploaded", async () => { let api = new GBZBaseAPI(); From 08c8e7783d694bec591910f2e16b5a72ec7d20bb Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Wed, 10 Jan 2024 13:27:32 -0500 Subject: [PATCH 09/11] Get WASM code to run under Jest at all --- src/GBZBaseAPI.mjs | 22 +++++++++++++++------- src/GBZBaseAPI.test.js | 2 +- src/setupTests.js | 11 +++++++++++ 3 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 src/setupTests.js diff --git a/src/GBZBaseAPI.mjs b/src/GBZBaseAPI.mjs index 0988c3df..a4029a31 100644 --- a/src/GBZBaseAPI.mjs +++ b/src/GBZBaseAPI.mjs @@ -69,6 +69,11 @@ export class GBZBaseAPI extends APIInterface { // Make a call into the WebAssembly code and return the result. async callWasm(argv) { + if (argv.length < 1) { + // We need at least one command line argument to be the program name. + throw new Error("Not safe to invoke main() without program name"); + } + await this.setUp(); let wasm = this.compiledWasm; @@ -78,7 +83,7 @@ export class GBZBaseAPI extends APIInterface { let stderr = new File([]); // Environment variables as NAME=value strings - const environment = []; + const environment = ["RUST_BACKTRACE=full"]; // File descriptors for the process in number order let file_descriptors = [new OpenFile(stdin), new OpenFile(stdout), new OpenFile(stderr)]; @@ -91,12 +96,15 @@ export class GBZBaseAPI extends APIInterface { "wasi_snapshot_preview1": wasi.wasiImport, }); - // Make the WASI system call main - let returnCode = wasi.start(instantiation); - - console.log("Return code:", returnCode); - console.log("Standard Output:", stdout); - console.log("Standard Error:", stderr); + try { + // Make the WASI system call main + let returnCode = wasi.start(instantiation); + console.log("Return code:", returnCode); + } finally { + // The WASM code can throw right out of the WASI shim if Rust panics. + console.log("Standard Output:", new TextDecoder().decode(stdout.data)); + console.log("Standard Error:", new TextDecoder().decode(stderr.data)); + } } ///////// diff --git a/src/GBZBaseAPI.test.js b/src/GBZBaseAPI.test.js index 9b8da810..da1a03a0 100644 --- a/src/GBZBaseAPI.test.js +++ b/src/GBZBaseAPI.test.js @@ -8,7 +8,7 @@ it("can be constructed", () => { it("can run the WASM blob", async () => { let api = new GBZBaseAPI(); - await api.callWasm([]); + await api.callWasm(["query", "--help"]); }); it("can have a file uploaded", async () => { diff --git a/src/setupTests.js b/src/setupTests.js new file mode 100644 index 00000000..24149bdd --- /dev/null +++ b/src/setupTests.js @@ -0,0 +1,11 @@ +// Configure Jest environment for all tests. +// +// create-react-app auto-magically executes this file by name for all the test +// suites. See . + +// Make TextEncoder and TextDecoder available. Browsers and Node both have them +// but jsdom refuses to let us use them. See +// +import { TextEncoder, TextDecoder } from "util"; +globalThis.TextEncoder = TextEncoder; +globalThis.TextDecoder = TextDecoder; From 4a64c49bc567f92a010d33939ea17a44807569aa Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Wed, 10 Jan 2024 13:57:39 -0500 Subject: [PATCH 10/11] Make WASM setup test itself and log when the app is loaded --- src/App.js | 12 ++++++++++ src/GBZBaseAPI.mjs | 53 +++++++++++++++++++++++++++++++----------- src/GBZBaseAPI.test.js | 5 ++-- 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/src/App.js b/src/App.js index 371e5f8e..b92cc3ce 100644 --- a/src/App.js +++ b/src/App.js @@ -15,6 +15,7 @@ import { dataOriginTypes } from "./enums"; import "./config-client.js"; import { config } from "./config-global.mjs"; import ServerAPI from "./ServerAPI.mjs"; +import { GBZBaseAPI } from "./GBZBaseAPI.mjs"; const EXAMPLE_TRACKS = [ // Fake tracks for the generated examples. @@ -46,6 +47,17 @@ class App extends Component { constructor(props) { super(props); + // See if the WASM API is available. + // Right now this just tests and logs, but eventually we will be able to use it. + let gbzApi = new GBZBaseAPI(); + gbzApi.available().then((working) => { + if (working) { + console.log("WASM API implementation available!"); + } else { + console.error("WASM API implementation not available!"); + } + }); + this.APIInterface = new ServerAPI(props.apiUrl); console.log("App component starting up with API URL: " + props.apiUrl); diff --git a/src/GBZBaseAPI.mjs b/src/GBZBaseAPI.mjs index a4029a31..36d4c2e6 100644 --- a/src/GBZBaseAPI.mjs +++ b/src/GBZBaseAPI.mjs @@ -1,6 +1,5 @@ import { APIInterface } from "./APIInterface.mjs"; -import { readFile } from "fs-extra"; -import { WASI, File, OpenFile, PreopenDirectory } from "@bjorn3/browser_wasi_shim"; +import { WASI, File, OpenFile } from "@bjorn3/browser_wasi_shim"; // TODO: The Webpack way to get the WASM would be something like: //import QueryWasm from "gbz-base/target/wasm32-wasi/release/query.wasm"; @@ -14,14 +13,16 @@ import { WASI, File, OpenFile, PreopenDirectory } from "@bjorn3/browser_wasi_shi // fetch the WASM on either Webpack or Jest with its own strategies/by being // swapped out. -// Resolve with the bytes of the WASM query blob, on Jest or Webpack. +// Resolve with the bytes or Response of the WASM query blob, on Jest or Webpack. async function getWasmBytes() { let blobBytes = null; - if (!jest) { - // Not running on Jest, we should be able to dynamic import a binary asset and get the bytes, and Webpack will handle it. + if (!window["jest"]) { + // Not running on Jest, we should be able to dynamic import a binary asset + // by export name and get the bytes, and Webpack will handle it. try { - blobBytes = await import("gbz-base/target/wasm32-wasi/release/query.wasm"); + let blobImport = await import("gbz-base/query.wasm"); + return fetch(blobImport.default); } catch (e) { console.error("Could not dynamically import WASM blob.", e); // Leave blobBytes unset to try a fallback method. @@ -29,9 +30,15 @@ async function getWasmBytes() { } if (!blobBytes) { - // Either we're on Jest, or the dynamic import didn't work (maybe we're on plain Node?). + // Either we're on Jest, or the dynamic import didn't work (maybe we're on + // plain Node?). + // // Try to open the file from the filesystem. - blobBytes = await readFile("node_modules/gbz-base/target/wasm32-wasi/release/query.wasm"); + // + // Don't actually try and ship the filesystem module in the browser though: + // see + let fs = await import(/* webpackIgnore: true */ "fs-extra"); + blobBytes = await fs.readFile("node_modules/gbz-base/target/wasm32-wasi/release/query.wasm"); } console.log("Got blob bytes: ", blobBytes); @@ -60,7 +67,16 @@ export class GBZBaseAPI extends APIInterface { async setUp() { if (this.compiledWasm === undefined) { // Kick off and save exactly one request to get and load the WASM bytes. - this.compiledWasm = WebAssembly.compile(await getWasmBytes()); + this.compiledWasm = getWasmBytes().then((result) => { + if (result instanceof Response) { + // If a fetch request was made, compile as it streams in + return WebAssembly.compileStreaming(result); + } else { + // We have all the bytes, so compile right away. + // TODO: Put this logic in the function? + return WebAssembly.compile(result); + } + }); } // Wait for the bytes to be available. @@ -73,10 +89,11 @@ export class GBZBaseAPI extends APIInterface { // We need at least one command line argument to be the program name. throw new Error("Not safe to invoke main() without program name"); } - + + // Make sure this.compiledWasm is set. + // TODO: Change to an accessor method? await this.setUp(); - let wasm = this.compiledWasm; - + // Define the places to store program input and output let stdin = new File([]); let stdout = new File([]); @@ -106,6 +123,16 @@ export class GBZBaseAPI extends APIInterface { console.log("Standard Error:", new TextDecoder().decode(stderr.data)); } } + + // Return true if the WASM setup is working, and false otherwise. + async available() { + try { + await this.callWasm(["query", "--help"]); + return true; + } catch { + return false; + } + } ///////// // Tube Map API implementation @@ -128,7 +155,7 @@ export class GBZBaseAPI extends APIInterface { }; for (let type of this.filesByType) { - if (type == "bed") { + if (type === "bed") { // Just send all these files in bedFiles. response.bedFiles = this.filesByType[type]; } else { diff --git a/src/GBZBaseAPI.test.js b/src/GBZBaseAPI.test.js index da1a03a0..e8e3562c 100644 --- a/src/GBZBaseAPI.test.js +++ b/src/GBZBaseAPI.test.js @@ -6,9 +6,10 @@ it("can be constructed", () => { let api = new GBZBaseAPI(); }); -it("can run the WASM blob", async () => { +it("can self-test its WASM setup", async () => { let api = new GBZBaseAPI(); - await api.callWasm(["query", "--help"]); + let working = await api.available(); + expect(working).toBeTruthy(); }); it("can have a file uploaded", async () => { From 2d3e257f7c7fa02ca778d36dc0950a0e2b377a1b Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Thu, 11 Jan 2024 10:43:34 -0500 Subject: [PATCH 11/11] Make an actual patch file to actually make the edit --- patches/@bjorn3+browser_wasi_shim+0.2.17.patch | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/patches/@bjorn3+browser_wasi_shim+0.2.17.patch b/patches/@bjorn3+browser_wasi_shim+0.2.17.patch index 30c65581..5d5edbea 100644 --- a/patches/@bjorn3+browser_wasi_shim+0.2.17.patch +++ b/patches/@bjorn3+browser_wasi_shim+0.2.17.patch @@ -1,4 +1,14 @@ -diff --git a/node_modules/@bjorn3/browser_wasi_shim/.package.json.swp b/node_modules/@bjorn3/browser_wasi_shim/.package.json.swp -new file mode 100644 -index 0000000..334b4b6 -Binary files /dev/null and b/node_modules/@bjorn3/browser_wasi_shim/.package.json.swp differ +diff --git a/node_modules/@bjorn3/browser_wasi_shim/package.json b/node_modules/@bjorn3/browser_wasi_shim/package.json +index af9de55..2e7a121 100644 +--- a/node_modules/@bjorn3/browser_wasi_shim/package.json ++++ b/node_modules/@bjorn3/browser_wasi_shim/package.json +@@ -21,7 +21,8 @@ + "exports": { + ".": { + "types": "./typings/index.d.ts", +- "import": "./dist/index.js" ++ "import": "./dist/index.js", ++ "default": "./dist/index.js" + } + }, + "typings": "./typings/index.d.ts",