UNPKG

molstar

Version:

A comprehensive macromolecular library.

368 lines (367 loc) 14.6 kB
"use strict"; /** * Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Adam Midlik <midlik@gmail.com> * * Adapted from LiteMol */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DataCompressionMethod = void 0; exports.readStringFromFile = readStringFromFile; exports.readUint8ArrayFromFile = readUint8ArrayFromFile; exports.readFromFile = readFromFile; exports.ajaxGet = ajaxGet; exports.setFSModule = setFSModule; exports.ajaxGetMany = ajaxGetMany; const utf8_1 = require("../mol-io/common/utf8"); const mol_task_1 = require("../mol-task"); const assets_1 = require("./assets"); const nodejs_shims_1 = require("./nodejs-shims"); const zip_1 = require("./zip/zip"); var DataCompressionMethod; (function (DataCompressionMethod) { DataCompressionMethod[DataCompressionMethod["None"] = 0] = "None"; DataCompressionMethod[DataCompressionMethod["Gzip"] = 1] = "Gzip"; DataCompressionMethod[DataCompressionMethod["Zip"] = 2] = "Zip"; })(DataCompressionMethod || (exports.DataCompressionMethod = DataCompressionMethod = {})); function readStringFromFile(file) { return readFromFileInternal(file, 'string'); } function readUint8ArrayFromFile(file) { return readFromFileInternal(file, 'binary'); } function readFromFile(file, type) { return readFromFileInternal(file, type); } function ajaxGet(params) { if (typeof params === 'string') return ajaxGetInternal(params, params, 'string'); return ajaxGetInternal(params.title, params.url, params.type || 'string', params.body, params.headers); } function isDone(data) { if (nodejs_shims_1.RUNNING_IN_NODEJS) throw new Error('`isDone` should not be used when running in Node.js'); // XMLHttpRequest and FileReader are not available in Node.js if (data instanceof FileReader) { return data.readyState === FileReader.DONE; } else if (data instanceof XMLHttpRequest) { return data.readyState === XMLHttpRequest.DONE; } throw new Error('unknown data type'); } function genericError(isDownload) { if (isDownload) return 'Failed to download data. Possible reasons: Resource is not available, or CORS is not allowed on the server.'; return 'Failed to open file.'; } function readData(ctx, action, data) { if (nodejs_shims_1.RUNNING_IN_NODEJS) throw new Error('`readData` should not be used when running in Node.js'); // XMLHttpRequest is not available in Node.js return new Promise((resolve, reject) => { // first check if data reading is already done if (isDone(data)) { const { error } = data; if (error !== null && error !== undefined) { reject(error !== null && error !== void 0 ? error : genericError(data instanceof XMLHttpRequest)); } else { resolve(data); } return; } let hasError = false; data.onerror = (e) => { if (hasError) return; const { error } = e.target; reject(error !== null && error !== void 0 ? error : genericError(data instanceof XMLHttpRequest)); }; data.onprogress = (e) => { if (!ctx.shouldUpdate || hasError) return; try { if (e.lengthComputable) { ctx.update({ message: action, isIndeterminate: false, current: e.loaded, max: e.total }); } else { ctx.update({ message: `${action} ${(e.loaded / 1024 / 1024).toFixed(2)} MB`, isIndeterminate: true }); } } catch (e) { hasError = true; reject(e); } }; data.onload = (e) => { resolve(data); }; }); } function getCompression(name) { return /\.gz$/i.test(name) ? DataCompressionMethod.Gzip : /\.zip$/i.test(name) ? DataCompressionMethod.Zip : DataCompressionMethod.None; } const reFilterPath = /^(__MACOSX|.DS_Store)/; async function decompress(ctx, data, compression) { switch (compression) { case DataCompressionMethod.None: return data; case DataCompressionMethod.Gzip: return (0, zip_1.ungzip)(ctx, data); case DataCompressionMethod.Zip: const parsed = await (0, zip_1.unzip)(ctx, data.buffer); const names = Object.keys(parsed).filter(n => !reFilterPath.test(n)); if (names.length !== 1) throw new Error('can only decompress zip files with a single entry'); return parsed[names[0]]; } } async function processFile(ctx, fileContent, type, compression) { if (fileContent === null) throw new Error('no data given'); let data = new Uint8Array(fileContent); if (compression !== DataCompressionMethod.None && type !== 'zip') { // if type==='zip', data will be decompressed later data = await decompress(ctx, data, compression); } if (type === 'binary') { return data; } else if (type === 'zip') { return await (0, zip_1.unzip)(ctx, data.buffer); } else if (type === 'string') { return (0, utf8_1.utf8ReadLong)(data); } else if (type === 'xml') { const parser = new DOMParser(); return parser.parseFromString((0, utf8_1.utf8Read)(data), 'application/xml'); } else if (type === 'json') { return JSON.parse((0, utf8_1.utf8Read)(data)); } throw new Error(`could not get requested response data '${type}'`); } function readFromFileInternal(file, type) { if (nodejs_shims_1.RUNNING_IN_NODEJS) { return readFromFileInternal_NodeJS(file, type); } let reader = void 0; return mol_task_1.Task.create('Read File', async (ctx) => { try { await ctx.update({ message: 'Opening file...', canAbort: true }); reader = new FileReader(); reader.readAsArrayBuffer(file); const fileReader = await readData(ctx, 'Reading...', reader); const fileContent = fileReader.result; await ctx.update({ message: 'Processing file...', canAbort: false }); return await processFile(ctx, fileContent, type, getCompression(file.name)); } finally { reader = void 0; } }, () => { if (reader) reader.abort(); }); } function readFromFileInternal_NodeJS(file, type) { return mol_task_1.Task.create('Read File', async (ctx) => { await ctx.update({ message: 'Opening file...', canAbort: false }); const fileContent = await file.arrayBuffer(); await ctx.update({ message: 'Processing file...', canAbort: false }); return await processFile(ctx, fileContent, type, getCompression(file.name)); }); } class RequestPool { static get() { if (nodejs_shims_1.RUNNING_IN_NODEJS) throw new Error('`RequestPool.get` should not be used when running in Node.js'); // XMLHttpRequest is not available in Node.js if (this.pool.length) { return this.pool.pop(); } return new XMLHttpRequest(); } static emptyFunc() { } static deposit(req) { if (this.pool.length < this.poolSize) { req.onabort = RequestPool.emptyFunc; req.onerror = RequestPool.emptyFunc; req.onload = RequestPool.emptyFunc; req.onprogress = RequestPool.emptyFunc; this.pool.push(req); } } } RequestPool.pool = []; RequestPool.poolSize = 15; function processAjax(req, type) { if (req.status >= 200 && req.status < 400) { const { response } = req; RequestPool.deposit(req); if ((type === 'binary' || type === 'zip') && response instanceof ArrayBuffer) { return new Uint8Array(response); } else if (type === 'string' && typeof response === 'string') { return response; } else if (type === 'xml' && response instanceof XMLDocument) { return response; } else if (type === 'json' && typeof response === 'object') { return response; } throw new Error(`could not get requested response data '${type}'`); } else { RequestPool.deposit(req); throw new Error(`Download failed with status code ${req.status}`); } } function getRequestResponseType(type) { switch (type) { case 'json': return 'json'; case 'xml': return 'document'; case 'string': return 'text'; case 'binary': return 'arraybuffer'; case 'zip': return 'arraybuffer'; } } function ajaxGetInternal(title, url, type, body, headers) { if (nodejs_shims_1.RUNNING_IN_NODEJS) { if (url.startsWith('file://')) { return ajaxGetInternal_file_NodeJS(title, url, type, body, headers); } else { return ajaxGetInternal_http_NodeJS(title, url, type, body, headers); } } let xhttp = void 0; return mol_task_1.Task.create(title ? title : 'Download', async (ctx) => { xhttp = RequestPool.get(); xhttp.open(body ? 'post' : 'get', url, true); if (headers) { for (const [name, value] of headers) { xhttp.setRequestHeader(name, value); } } xhttp.responseType = getRequestResponseType(type); xhttp.send(body); await ctx.update({ message: 'Waiting for server...', canAbort: true }); const req = await readData(ctx, 'Downloading...', xhttp); xhttp = void 0; // guard against reuse, help garbage collector await ctx.update({ message: 'Parsing response...', canAbort: false }); const result = processAjax(req, type); return result; }, () => { if (xhttp) { xhttp.abort(); xhttp = void 0; // guard against reuse, help garbage collector } }); } // NOTE: a workaround for using this in Node.js let _fs = undefined; function getFS() { if (!_fs) { throw new Error('When running in Node.js and reading from files, call mol-util/data-source\'s setFSModule function first.'); } return _fs; } function setFSModule(fs) { _fs = fs; } function readFileAsync(filename) { return new Promise((resolve, reject) => { getFS().readFile(filename, (err, data) => { if (err) reject(err); else resolve(data); }); }); } /** Alternative implementation of ajaxGetInternal for NodeJS for file:// protocol */ function ajaxGetInternal_file_NodeJS(title, url, type, body, headers) { if (!nodejs_shims_1.RUNNING_IN_NODEJS) throw new Error('This function should only be used when running in Node.js'); if (!url.startsWith('file://')) throw new Error('This function is only for URLs with protocol file://'); return mol_task_1.Task.create(title !== null && title !== void 0 ? title : 'Download', async (ctx) => { const filename = url.substring('file://'.length); await ctx.update({ message: 'Loading file...', canAbort: false }); const data = await readFileAsync(filename); await ctx.update({ message: 'Parsing response...', canAbort: false }); const result = await processFile(ctx, data, type, DataCompressionMethod.None); return result; }); } /** Alternative implementation of ajaxGetInternal for NodeJS for http(s):// protocol */ function ajaxGetInternal_http_NodeJS(title, url, type, body, headers) { if (!nodejs_shims_1.RUNNING_IN_NODEJS) throw new Error('This function should only be used when running in Node.js'); const aborter = new AbortController(); return mol_task_1.Task.create(title !== null && title !== void 0 ? title : 'Download', async (ctx) => { await ctx.update({ message: 'Downloading...', canAbort: true }); const response = await fetch(url, { signal: aborter.signal }); if (!(response.status >= 200 && response.status < 400)) { throw new Error(`Download failed with status code ${response.status}`); } const fileContent = await response.bytes(); await ctx.update({ message: 'Parsing response...', canAbort: false }); const result = await processFile(ctx, fileContent, type, DataCompressionMethod.None); return result; }, () => { aborter.abort(); }); } async function ajaxGetMany(ctx, assetManager, sources, maxConcurrency) { const len = sources.length; const slots = new Array(sources.length); await ctx.update({ message: 'Downloading...', current: 0, max: len }); let promises = [], promiseKeys = []; let currentSrc = 0; for (let _i = Math.min(len, maxConcurrency); currentSrc < _i; currentSrc++) { const current = sources[currentSrc]; promises.push(wrapPromise(currentSrc, current.id, assetManager.resolve(assets_1.Asset.getUrlAsset(assetManager, current.url), current.isBinary ? 'binary' : 'string').runAsChild(ctx))); promiseKeys.push(currentSrc); } let done = 0; while (promises.length > 0) { const r = await Promise.race(promises); const src = sources[r.index]; const idx = promiseKeys.indexOf(r.index); done++; if (r.kind === 'error' && !src.canFail) { // TODO: cancel other downloads throw new Error(`${src.url}: ${r.error}`); } if (ctx.shouldUpdate) { await ctx.update({ message: 'Downloading...', current: done, max: len }); } slots[r.index] = r; promises = promises.filter(_filterRemoveIndex, idx); promiseKeys = promiseKeys.filter(_filterRemoveIndex, idx); if (currentSrc < len) { const current = sources[currentSrc]; const asset = assetManager.resolve(assets_1.Asset.getUrlAsset(assetManager, current.url), current.isBinary ? 'binary' : 'string').runAsChild(ctx); promises.push(wrapPromise(currentSrc, current.id, asset)); promiseKeys.push(currentSrc); currentSrc++; } } return slots; } function _filterRemoveIndex(_, i) { return this !== i; } async function wrapPromise(index, id, p) { try { const result = await p; return { kind: 'ok', result, index, id }; } catch (error) { return { kind: 'error', error, index, id }; } }