UNPKG

bakana

Version:

Backend for kana's single-cell analyses. This supports single or multiple samples, execution in Node.js or the browser, in-memory caching of results for iterative analyses, and serialization to/from file for redistribution.

247 lines (216 loc) 9.06 kB
import * as adb from "./ArtifactDB-abstract.js"; import JSZip from "jszip"; import * as afile from "./abstract/file.js"; class ZippedProjectNavigator { #zipfile; #ziphandle; constructor(zipfile, ziphandle) { this.#zipfile = zipfile; this.#ziphandle = null; } async file(path) { if (this.#ziphandle == null) { this.#ziphandle = await JSZip.loadAsync(this.#zipfile.buffer()); } return await this.#ziphandle.file(path).async("uint8array"); } async metadata(path) { if (this.#ziphandle == null) { this.#ziphandle = await JSZip.loadAsync(this.#zipfile.buffer()); } while (1) { if (!path.endsWith(".json")) { path += ".json"; } let contents = await this.#ziphandle.file(path).async("string"); let values = JSON.parse(contents); if (values["$schema"].startsWith("redirection/")){ path = values.redirection.targets[0].location; } else { return values; } } } clear() { this.#ziphandle = null; } }; /** * Search a ZIP file for SummarizedExperiments to use in {@linkplain ZippedArtifactdbDataset} or {@linkplain ZippedArtifactdbResult}. * * @param {JSZip} handle - A handle into the ZIP file, generated using the [**JSZip**](https://stuk.github.io/jszip/) package. * * @return {Map} Object where the keys are the paths/names of possible SummarizedExperiment objects, * and each value is a 2-element array of dimensions. */ export async function searchZippedArtifactdb(handle) { // Sorting by the number of slashes. let all_json = []; for (const name of Object.keys(handle.files)) { if (name.endsWith(".json")) { all_json.push({ name: name, path: name.split("/") }); } } all_json.sort((a, b) => a.path.length - b.path.length); let found_se = new Map; let nonchildren = new Map; let redirects = new Map; for (const x of all_json) { // Avoid loading JSONs for files in subdirectories of known SEs. let current = found_se; let already_found = false; for (const comp of x.path) { let val = current.get(comp); if (typeof val === "undefined") { val = new Map; current.set(comp, val); } else if (val === null) { already_found = true; break; } current = val; } // Otherwise, we load it in and peel out some information. if (!already_found) { let contents = await handle.file(x.name).async("string"); let values = JSON.parse(contents); if ("summarized_experiment" in values) { nonchildren.set(values.path, values.summarized_experiment.dimensions); } else if (values["$schema"].startsWith("redirection/")) { redirects.set(values.path, values.redirection.targets[0].location); } } } for (const [rr, loc] of redirects) { let found = nonchildren.get(loc); if (typeof found !== "undefined") { nonchildren.delete(loc); nonchildren.set(rr, found); } } return nonchildren; } /************************ ******* Dataset ******** ************************/ /** * Dataset as a ZIP file containing a SummarizedExperiment in the **ArtifactDB** representation, * e.g., as produced by {@linkcode saveSingleCellExperiment}. * Specifically, the ZIP file should contain the contents of an **ArtifactDB** project directory. * This project directory may contain multiple objects; the SummarizedExperiment of interest is identified in the constructor. * * @extends AbstractArtifactdbDataset */ export class ZippedArtifactdbDataset extends adb.AbstractArtifactdbDataset { #zipfile; #name; /** * @param {string} name - Name of the SummarizedExperiment object inside the project directory. * @param {SimpleFile|string|File} zipfile - Contents of the ZIP file containing the project directory. * On browsers, this may be a File object. * On Node.js, this may also be a string containing a file path. * @param {object} [options={}] - Optional parameters. * @param {?JSZip} [options.existingHandle=null] - An existing handle into the ZIP file, generated using the [**JSZip**](https://stuk.github.io/jszip/) package. * If an existing handle already exists, passing it in here will allow it to be re-used for greater efficiency. * If `null`, a new handle is created for this ZippedArtifactdbDataset instance. */ constructor(name, zipfile, options={}) { let ziphandle = null; if ("existingHandle" in options) { ziphandle = options.existingHandle; delete options.existingHandle; } else { if (!(zipfile instanceof afile.SimpleFile)) { zipfile = new afile.SimpleFile(zipfile); } } let nav = new ZippedProjectNavigator(zipfile, ziphandle); super(name, nav); this.#zipfile = zipfile; this.#name = name; } /** * @return {string} String specifying the format for this dataset. */ static format() { return "ArtifactDB-zipped"; } #dump_summary(fun) { let files = [ { type: "zip", file: fun(this.#zipfile) } ]; let opt = this.options(); opt.datasetName = this.#name; // storing the name as a special option... can't be bothered to store it as a separate file. return { files: files, options: opt }; } /** * @return {object} Object containing the abbreviated details of this dataset. */ abbreviate() { return this.#dump_summary(f => { return { size: f.size(), name: f.name() } }); } /** * @return {object} Object describing this dataset, containing: * * - `files`: Array of objects representing the files used in this dataset. * Each object corresponds to a single file and contains: * - `type`: a string denoting the type. * - `file`: a {@linkplain SimpleFile} object representing the file contents. * - `options`: An object containing additional options to saved. */ serialize() { return this.#dump_summary(f => f); } /** * @param {Array} files - Array of objects like that produced by {@linkcode ZippedArtifactdbDataset#serialize serialize}. * @param {object} options - Object containing additional options to be passed to the constructor. * @return {ZippedArtifactdbDataset} A new instance of this class. * @static */ static unserialize(files, options) { if (files.length != 1 || files[0].type != "zip") { throw new Error("expected exactly one file of type 'zip' for Zipped ArtifactDB unserialization"); } let name = options.datasetName; delete options.datasetName; let output = new ZippedArtifactdbDataset(name, files[0].file); output.setOptions(output); return output; } } /*********************** ******* Result ******** ***********************/ /** * Result as a ZIP file containing a SummarizedExperiment in the **ArtifactDB** representation, * e.g., as produced by {@linkcode saveSingleCellExperiment}. * Specifically, the ZIP file should contain the contents of an **ArtifactDB** project directory. * This project directory may contain multiple objects; the SummarizedExperiment of interest is identified in the constructor. * * @extends AbstractArtifactdbResult */ export class ZippedArtifactdbResult extends adb.AbstractArtifactdbResult { /** * @param {string} name - Name of the SummarizedExperiment object inside the project directory. * @param {SimpleFile|string|File} zipfile - Contents of the ZIP file containing the project directory. * On browsers, this may be a File object. * On Node.js, this may also be a string containing a file path. * @param {object} [options={}] - Optional parameters. * @param {?JSZip} [options.existingHandle=null] - An existing handle into the ZIP file, generated using the [**JSZip**](https://stuk.github.io/jszip/) package. * If an existing handle already exists, passing it in here will allow it to be re-used for greater efficiency. * If `null`, a new handle is created for this ZippedArtifactdbDataset instance. */ constructor(name, zipfile, options={}) { let ziphandle = null; if ("existingHandle" in options) { ziphandle = options.existingHandle; delete options.existingHandle; } else { if (!(zipfile instanceof afile.SimpleFile)) { zipfile = new afile.SimpleFile(zipfile); } } let nav = new ZippedProjectNavigator(zipfile, ziphandle); super(name, nav); } }