tickle/modules/utils/makeFileLoader.mjs

178 lines
4.6 KiB
JavaScript

//@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<typeof makeFileLoader>} 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<Response>}
*/
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;