/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * Enumeration for the progress status text. */ export const ProgressStatusText = Object.freeze({ // The value of the status text indicating that an operation is started. INITIATE: "initiate", // The value of the status text indicating an estimate for the size of the operation. SIZE_ESTIMATE: "size_estimate", // The value of the status text indicating that an operation is in progress. IN_PROGRESS: "in_progress", // The value of the status text indicating that an operation has completed. DONE: "done", }); /** * Enumeration for type of progress operations. */ export const ProgressType = Object.freeze({ // The value of the operation type for a remote downloading. DOWNLOAD: "downloading", // The value of the operation type when loading from cache LOAD_FROM_CACHE: "loading_from_cache", }); /** * This class encapsulates the parameters supported by a progress and status callback. */ export class ProgressAndStatusCallbackParams { // Params for progress callback /** * A float indicating the percentage of data loaded. Note that * 100% does not necessarily mean the operation is complete. * * @type {?float} */ progress = null; /** * A float indicating the total amount of data loaded so far. * In particular, this is the sum of currentLoaded across all call of the callback. * * @type {?float} */ totalLoaded = null; /** * The amount of data loaded in the current callback call. * * @type {?float} */ currentLoaded = null; /** * A float indicating an estimate of the total amount of data to be loaded. * Do not rely on this number as this is an estimate and the true total could be * either lower or higher. * * @type {?float} */ total = null; /** * The units in which the amounts are reported. * * @type {?string} */ units = null; // Params for status callback /** * The name of the operation being tracked. * * @type {?string} */ type = null; /** * A message indicating the status of the tracked operation. * * @type {?string} */ statusText = null; /** * An ID uniquely identifying the object/file being tracked. * * @type {?string} */ id = null; /** * A boolean indicating if the operation was successful. * true means we have a successful operation. * * @type {?boolean} */ ok = null; /** * Any additional metadata for the operation being tracked. * * @type {?object} */ metadata = null; constructor(params = {}) { this.update(params); } update(params = {}) { const allowedKeys = new Set(Object.keys(this)); const invalidKeys = Object.keys(params).filter(x => !allowedKeys.has(x)); if (invalidKeys.length) { throw new Error(`Received Invalid option: ${invalidKeys}`); } for (const key of allowedKeys) { if (key in params) { this[key] = params[key]; } } } } /** * Read and track progress when reading a Response object * * @param {any} response The Response object to read * @param {?function(ProgressAndStatusCallbackParams):void} progressCallback The function to call with progress updates * * @returns {Promise} A Promise that resolves with the Uint8Array buffer */ export async function readResponse(response, progressCallback) { const contentLength = response.headers.get("Content-Length"); if (!contentLength) { console.warn( "Unable to determine content-length from response headers. Will expand buffer when needed." ); } let total = parseInt(contentLength ?? "0"); progressCallback?.( new ProgressAndStatusCallbackParams({ progress: 0, totalLoaded: 0, currentLoaded: 0, total, units: "bytes", }) ); let buffer = new Uint8Array(total); let loaded = 0; for await (const value of response.body) { let newLoaded = loaded + value.length; if (newLoaded > total) { total = newLoaded; // Adding the new data will overflow buffer. // In this case, we extend the buffer // Happened when the content-length is lower than the actual lenght let newBuffer = new Uint8Array(total); // copy contents newBuffer.set(buffer); buffer = newBuffer; } buffer.set(value, loaded); loaded = newLoaded; const progress = (loaded / total) * 100; progressCallback?.( new ProgressAndStatusCallbackParams({ progress, totalLoaded: loaded, currentLoaded: value.length, total, units: "bytes", }) ); } // Ensure that buffer is not bigger than loaded // Sometimes content length is larger than the actual size buffer = buffer.slice(0, loaded); return buffer; } /** * Class for watching the progress bar of multiple events and combining * then into a single progress bar. */ export class MultiProgressAggregator { /** * A function to call with the aggregated statistics. * * @type {?function(ProgressAndStatusCallbackParams):void} */ progressCallback = null; /** * The name of the key that contains status information. * * @type {Set} */ watchedTypes; /** * The total amount of information loaded so far. * * @type {float} */ #combinedLoaded = 0; /** * The total amount of information to be loaded. * * @type {float} */ #combinedTotal = 0; /** * The number of operations that are yet to be completed. * * @type {float} */ #remainingEvents = 0; /** * The type of operation seen so far. * * @type {Set} */ #seenTypes; /** * The status of text seen so far. * * @type {Set} */ #seenStatus; /** * @param {object} config * @param {?function(ProgressAndStatusCallbackParams):void} config.progressCallback - A function to call with the aggregated statistics. * @param {Iterable} config.watchedTypes - The types to watch for aggregation */ constructor({ progressCallback, watchedTypes = [ProgressType.DOWNLOAD] }) { this.progressCallback = progressCallback; this.watchedTypes = new Set(watchedTypes); this.#seenTypes = new Set(); this.#seenStatus = new Set(); } /** * Callback function that will combined data from different objects/files. * * @param {ProgressAndStatusCallbackParams} data - object containing the data */ aggregateCallback(data) { if (this.watchedTypes.has(data.type)) { this.#seenTypes.add(data.type); this.#seenStatus.add(data.statusText); if (data.statusText == ProgressStatusText.INITIATE) { this.#remainingEvents += 1; } if (data.statusText == ProgressStatusText.SIZE_ESTIMATE) { this.#combinedTotal += data.total ?? 0; } if (data.statusText == ProgressStatusText.DONE) { this.#remainingEvents -= 1; } this.#combinedLoaded += data.currentLoaded ?? 0; if (this.progressCallback) { let statusText = data.statusText; if (this.#seenStatus.has(ProgressStatusText.IN_PROGRESS)) { statusText = ProgressStatusText.IN_PROGRESS; } if (this.#remainingEvents == 0) { statusText = ProgressStatusText.DONE; } this.progressCallback( new ProgressAndStatusCallbackParams({ type: data.type, statusText, id: data.id, total: this.#combinedTotal, currentLoaded: data.currentLoaded, totalLoaded: this.#combinedLoaded, progress: (this.#combinedLoaded / this.#combinedTotal) * 100, ok: data.ok, units: data.units, metadata: data, }) ); } } } } /** * Converts a model and its headers to a Response object. * * @param {ArrayBuffer} modelFile * @param {object|null} headers * @returns {Response} The generated Response instance */ export function modelToResponse(modelFile, headers) { let responseHeaders = {}; if (headers) { // Headers are converted to strings, as the cache may hold int keys like fileSize for (let key in headers) { if (headers[key] != null) { responseHeaders[key] = headers[key].toString(); } } } return new Response(modelFile, { status: 200, headers: responseHeaders, }); } // Create a "namespace" to make it easier to import multiple names. export var Progress = Progress || {}; Progress.ProgressAndStatusCallbackParams = ProgressAndStatusCallbackParams; Progress.ProgressStatusText = ProgressStatusText; Progress.ProgressType = ProgressType; Progress.readResponse = readResponse;