//@ts-check import { createTrackedResponse } from "./createTrackedResponse.mjs"; import { decodeContentLength } from "./decodeContentLength.mjs"; import { retryPromise } from "./retryPromise.mjs"; import { makeFileSizeFetcher } from "./makeFileSizeFetcher.mjs"; import { makeSignal } from "./makeSignal.mjs"; /** * @typedef {{ * total?: number, * onProgress?:(data:ProgressData)=>void, * signal?: AbortSignal * }} FileLoaderOptions */ /** * @typedef {ReturnType} FileLoader */ /** * Creates an object that downloads files and allows you to track progress. * You can optionally provide an initial file size; if not provided, the size * will be attempt to be acquired through reading the headers when download starts. * * Downloads can be aborted with an AbortSignal, and the response can be obtained on * `response`. * * `response` can be awaited even before the download starts. * * @typedef {{received: number, total:number}} ProgressData * @param {string} path * @param {FileLoaderOptions} options */ export const makeFileLoader = ( path, { total = 0, onProgress, signal } = {} ) => { const progress = makeSignal({ received: 0, total: 0 }); if (onProgress) { progress.connect(onProgress, { signal }); } const createResponse = () => createTrackedResponse( (received) => progress.emit({ received, total }), signal ); let responsePromise = createResponse(); /** * Retrieves the file size if `total` was not provided. */ const fetchSize = makeFileSizeFetcher(path, total); /** * Fetches a file, dispatching progress events while doing so. */ const start = () => fetch(path, { signal }).then((initialResponse) => { if (!initialResponse.body || !initialResponse.ok) { throw new Error(`Failed loading file '${path}'`); } total = total || decodeContentLength(initialResponse.headers); const reader = initialResponse.body.getReader(); // this triggers the deferred promise and starts the loading process responsePromise.start(reader); return responsePromise.response; }); /** * Loads a file, _if it isn't already loaded_. * If it is loaded, or if the loading has started, then this results in a * no-op. * It's therefore safe to use multiple times. * This function returns as soon as the download is triggered. To await the * download, await the `response` property and/or listen to the success event. * * @param {number} retries amount of time to try again in case of failure. * Defaults to 4. * @returns {Promise} */ const load = (retries = 4) => new Promise((ok, no) => { if (responsePromise.isStarted) { return ok(responsePromise.response); } if (responsePromise.isFailed) { return no(); } return retryPromise(start, retries).then(ok).catch(no); }); /** * clears loaded file from memory (if there are no more references to it) */ const unload = () => { responsePromise = createResponse(); }; return { fetchSize, load, unload, progress, get path() { return path; }, get total() { return total; }, get received() { return responsePromise.received; }, get isComplete() { return total > 0 && responsePromise.received === total; }, get response() { return responsePromise.response; }, }; }; /** * Allows to track a group of file loaders * @param {FileLoader[]} files */ export const makeFileLoadersTracker = (files) => { const done = makeSignal(); const progress = makeSignal({ received: 0, total: 0 }); let total = 0; let received = 0; let amount = files.length; const decrease = () => { if (--amount === 0) { done.emit(); } }; /** * Called every time any file's status changes. * Updates the total and received amounts. */ const update = () => { let _total = 0; let _received = 0; files.forEach((thing) => { _total += thing.total; _received += thing.received; console.log(thing.path, thing.received, thing.total); }); if (total != _total || received != _received) { total = _total; received = _received; progress.emit({ total, received }); } }; files.forEach((loader) => { loader.then(decrease); loader.progress.connect(update); }); const ensureSize = () => Promise.all(files.map(({ fetchSize }) => fetchSize())); const loadAll = () => Promise.all(files.map(({ load }) => load())); return { done, progress, ensureSize, loadAll }; }; export default makeFileLoader;