diff --git a/package-lock.json b/package-lock.json index a0a46311..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", @@ -33,10 +34,12 @@ "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", "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", @@ -1983,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", @@ -5445,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", @@ -6392,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" @@ -7892,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", @@ -9594,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", @@ -9838,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", @@ -9867,6 +9905,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", @@ -9884,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" @@ -10211,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", @@ -13230,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", @@ -13257,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", @@ -13367,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", @@ -14228,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", @@ -14332,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", @@ -17125,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", @@ -18157,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", @@ -20905,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", @@ -23393,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", @@ -24090,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": { @@ -25248,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", @@ -26517,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", @@ -26677,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", @@ -26697,6 +26962,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", @@ -26708,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": { @@ -26939,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", @@ -29284,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", @@ -29303,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", @@ -29400,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", @@ -30025,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", @@ -30102,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", @@ -31979,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", @@ -32736,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 2a3c5349..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", @@ -28,10 +29,12 @@ "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", "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", @@ -63,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" @@ -86,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..5d5edbea --- /dev/null +++ b/patches/@bjorn3+browser_wasi_shim+0.2.17.patch @@ -0,0 +1,14 @@ +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", diff --git a/src/APIInterface.mjs b/src/APIInterface.mjs index 29c2e7f6..e0a44d43 100644 --- a/src/APIInterface.mjs +++ b/src/APIInterface.mjs @@ -12,11 +12,29 @@ 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"); } + // 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"); + } + + // 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/App.js b/src/App.js index 2ebe9404..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); @@ -183,7 +195,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 +202,6 @@ class App extends Component { diff --git a/src/GBZBaseAPI.mjs b/src/GBZBaseAPI.mjs new file mode 100644 index 00000000..36d4c2e6 --- /dev/null +++ b/src/GBZBaseAPI.mjs @@ -0,0 +1,210 @@ +import { APIInterface } from "./APIInterface.mjs"; +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"; +// 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. + +// Resolve with the bytes or Response of the WASM query blob, on Jest or Webpack. +async function getWasmBytes() { + let blobBytes = null; + + 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 { + 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. + } + } + + 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. + // + // 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); + return blobBytes; +} + +/** + * 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 = {}; + + // 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 = 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. + this.compiledWasm = await this.compiledWasm; + } + + // 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"); + } + + // Make sure this.compiledWasm is set. + // TODO: Change to an accessor method? + await this.setUp(); + + // 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 = ["RUST_BACKTRACE=full"]; + + // 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, + }); + + 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)); + } + } + + // 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 + ///////// + + async getChunkedData(viewTarget, cancelSignal) { + return { + graph: {}, + gam: {}, + region: null, + coloredNodes: [], + }; + } + + async getFilenames(cancelSignal) { + // 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: [], + }; + } + + async getPathNames(graphFile, cancelSignal) { + return { + pathNames: [], + }; + } + + async getChunkTracks(bedFile, chunk, cancelSignal) { + return { + tracks: [], + }; + } +} + +export default GBZBaseAPI; diff --git a/src/GBZBaseAPI.test.js b/src/GBZBaseAPI.test.js new file mode 100644 index 00000000..e8e3562c --- /dev/null +++ b/src/GBZBaseAPI.test.js @@ -0,0 +1,38 @@ +import { GBZBaseAPI } from "./GBZBaseAPI.mjs"; + +import fs from "fs-extra"; + +it("can be constructed", () => { + let api = new GBZBaseAPI(); +}); + +it("can self-test its WASM setup", async () => { + let api = new GBZBaseAPI(); + let working = await api.available(); + expect(working).toBeTruthy(); +}); + +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/ServerAPI.mjs b/src/ServerAPI.mjs index 199aeede..39ee0bf5 100644 --- a/src/ServerAPI.mjs +++ b/src/ServerAPI.mjs @@ -35,6 +35,107 @@ export class ServerAPI extends APIInterface { return json; } + subscribeToFilenameChanges(handler, cancelSignal) { + // We need something to hold the one currently active websocket. + let subscription = {}; + + // We make a function to connect the websocket, which we can call to reconnect. + let 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 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 4540504c..5a1b837f 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -645,81 +645,34 @@ 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); - } + if (file.size > config.MAXUPLOADSIZE) { + this.showFileSizeAlert(); + return; + } - resolve(xhr.response.path); - } - }; + this.setUploadInProgress(true); - 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) - ); - }; - - setUpWebsocket = () => { - this.ws = new WebSocket(this.props.apiUrl.replace(/^http/, "ws")); - this.ws.onmessage = (message) => { - if (!this.cancelSignal.aborted) { + try { + let fileName = await this.api.putFile(fileType, file, this.cancelSignal); + if (fileType === "graph") { + // Refresh the graphs right away this.getMountedFilenames(); - } else { - this.ws.close(); } - }; - this.ws.onclose = (event) => { + this.setUploadInProgress(false); + return fileName; + } catch (e) { if (!this.cancelSignal.aborted) { - setTimeout(this.setUpWebsocket, 1000); + // Only pass along errors if we haven't canceled our fetches. + throw e; } - }; - this.ws.onerror = (event) => { - this.ws.close(); - }; + } + }; + + setUpWebsocket = () => { + this.subscription = this.api.subscribeToFilenameChanges( + this.getMountedFilenames, + this.cancelSignal + ); }; /* Function for toggling simplify button, enabling vg simplify to be turned on or off */ @@ -907,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, 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;