178 lines
4.6 KiB
JavaScript
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;
|