/** * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal * @author Alexander Rose */ import UUID from './uuid'; import { iterableToArray } from '../mol-data/util'; import { ajaxGet, DataType, DataResponse, readFromFile } from './data-source'; import { Task } from '../mol-task'; export { AssetManager, Asset }; type _File = File; type Asset = Asset.Url | Asset.File namespace Asset { export type Url = { kind: 'url', id: UUID, url: string, title?: string, body?: string, headers?: [string, string][] } export type File = { kind: 'file', id: UUID, name: string, file?: _File } export function Url(url: string, options?: { body?: string, title?: string, headers?: [string, string][] }): Url { return { kind: 'url', id: UUID.create22(), url, ...options }; } export function File(file: _File): File { return { kind: 'file', id: UUID.create22(), name: file.name, file }; } export function isUrl(x?: Asset): x is Url { return x?.kind === 'url'; } export function isFile(x?: Asset): x is File { return x?.kind === 'file'; } export interface Wrapper { readonly data: DataResponse dispose: () => void } export function Wrapper(data: DataResponse, asset: Asset, manager: AssetManager) { return { data, dispose: () => { manager.release(asset); } }; } export function getUrl(url: string | Url) { return typeof url === 'string' ? url : url.url; } export function getUrlAsset(manager: AssetManager, url: string | Url, body?: string) { if (typeof url === 'string') { const asset = manager.tryFindUrl(url, body); return asset || Url(url, { body }); } return url; } } class AssetManager { // TODO: add URL based ref-counted cache? // TODO: when serializing, check for duplicates? private _assets = new Map(); get assets() { return iterableToArray(this._assets.values()); } tryFindUrl(url: string, body?: string): Asset.Url | undefined { const assets = this.assets.values(); while (true) { const v = assets.next(); if (v.done) return; const asset = v.value.asset; if (Asset.isUrl(asset) && asset.url === url && (asset.body || '') === (body || '')) return asset; } } set(asset: Asset, file: File) { this._assets.set(asset.id, { asset, file, refCount: 0 }); } resolve(asset: Asset, type: T, store = true): Task> { if (Asset.isUrl(asset)) { return Task.create(`Download ${asset.title || asset.url}`, async ctx => { if (this._assets.has(asset.id)) { const entry = this._assets.get(asset.id)!; entry.refCount++; return Asset.Wrapper(await readFromFile(entry.file, type).runInContext(ctx), asset, this); } if (!store) { return Asset.Wrapper(await ajaxGet({ ...asset, type }).runInContext(ctx), asset, this); } const data = await ajaxGet({ ...asset, type: 'binary' }).runInContext(ctx); const file = new File([data], 'raw-data'); this._assets.set(asset.id, { asset, file, refCount: 1 }); return Asset.Wrapper(await readFromFile(file, type).runInContext(ctx), asset, this); }); } else { return Task.create(`Read ${asset.name}`, async ctx => { if (this._assets.has(asset.id)) { const entry = this._assets.get(asset.id)!; entry.refCount++; return Asset.Wrapper(await readFromFile(entry.file, type).runInContext(ctx), asset, this); } if (!(asset.file instanceof File)) { throw new Error(`Cannot resolve file asset '${asset.name}' (${asset.id})`); } if (store) { this._assets.set(asset.id, { asset, file: asset.file, refCount: 1 }); } return Asset.Wrapper(await readFromFile(asset.file, type).runInContext(ctx), asset, this); }); } } release(asset: Asset) { const entry = this._assets.get(asset.id); if (!entry) return; entry.refCount--; if (entry.refCount <= 0) this._assets.delete(asset.id); } }