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;
 |