UNPKG

scratch-storage

Version:

Load and store project and asset files for Scratch 3.0

218 lines (193 loc) 6.92 kB
import {Headers, applyMetadata} from './scratchFetch'; import {ScratchGetRequest, Tool} from './Tool'; interface DeferredJob { id: string, resolve: (buffer: ArrayBuffer) => void; reject: (error: unknown) => void; } /** * Get and send assets with a worker that uses fetch. */ class PrivateFetchWorkerTool implements Tool { private _workerSupport: {fetch: boolean}; private _supportError: unknown; private worker: Worker | null; private jobs: Record<string, DeferredJob | undefined>; constructor () { /** * What does the worker support of the APIs we need? * @type {{fetch:boolean}} */ this._workerSupport = { fetch: typeof fetch !== 'undefined' }; /** * A possible error occurred standing up the worker. * @type {Error?} */ this._supportError = null; /** * The worker that runs fetch and returns data for us. * @type {Worker?} */ this.worker = null; /** * A map of ids to fetch job objects. * @type {object} */ this.jobs = {}; try { if (this.isGetSupported) { // Yes, this is a browser API and we've specified `browser: false` in the eslint env, // but `isGetSupported` checks for the presence of Worker and uses it only if present. // Also see https://webpack.js.org/guides/web-workers/ const worker = new Worker( /* webpackChunkName: "fetch-worker" */ new URL('./FetchWorkerTool.worker', import.meta.url) ); worker.addEventListener('message', ({data}) => { if (data.support) { this._workerSupport = data.support; return; } for (const message of data) { const job = this.jobs[message.id]; if (job) { if (message.error) { job.reject(message.error); } else { job.resolve(message.buffer); } delete this.jobs[message.id]; } } }); this.worker = worker; } } catch (error) { this._supportError = error; } } /** * Is get supported? * * false if the environment does not workers, fetch, or fetch from inside a * worker. Finding out the worker supports fetch is asynchronous and will * guess that it does if the window does until the worker can inform us. * @returns {boolean} Is get supported? */ get isGetSupported (): boolean { return ( typeof Worker !== 'undefined' && this._workerSupport.fetch && !this._supportError ); } /** * Request data from a server with a worker using fetch. * @param {{url:string}} reqConfig - Request configuration for data to get. * @param {{method:string}} options - Additional options to configure fetch. * @returns {Promise.<Buffer|Uint8Array|null>} Resolve to Buffer of data from server. */ get ({url, ...options}: ScratchGetRequest): Promise<Uint8Array | null> { const worker = this.worker; if (!worker) { return Promise.reject(new Error('The worker could not be initialized')); } return new Promise<ArrayBuffer>((resolve, reject) => { // TODO: Use a Scratch standard ID generator ... const id = Math.random().toString(16) .substring(2); const augmentedOptions = applyMetadata( Object.assign({method: 'GET'}, options) ); // the Fetch spec says options.headers could be: // "A Headers object, an object literal, or an array of two-item arrays to set request's headers." // structured clone (postMessage) doesn't support Headers objects // so turn it into an array of two-item arrays to make it to the worker intact if (augmentedOptions && augmentedOptions.headers instanceof Headers) { augmentedOptions.headers = Array.from(augmentedOptions.headers.entries()); } worker.postMessage({ id, url, options: augmentedOptions }); this.jobs[id] = { id, resolve, reject }; }) /* eslint no-confusing-arrow: ["error", {"allowParens": true}] */ .then(body => (body ? new Uint8Array(body) : null)); } /** * Is sending supported? always false for FetchWorkerTool. * @returns {boolean} Is sending supported? */ get isSendSupported (): boolean { return false; } /** * Send data to a server. * @throws {Error} A not implemented error. */ send (): never { throw new Error('Not implemented.'); } private static _instance?: PrivateFetchWorkerTool; /** * Return a static PrivateFetchWorkerTool instance on demand. * @returns {PrivateFetchWorkerTool} A static PrivateFetchWorkerTool * instance */ static get instance () { if (!this._instance) { this._instance = new PrivateFetchWorkerTool(); } return this._instance; } } /** * Get and send assets with a worker that uses fetch. */ export default class PublicFetchWorkerTool { private inner: PrivateFetchWorkerTool; constructor () { /** * Shared instance of an internal worker. PublicFetchWorkerTool proxies * it. * @type {PrivateFetchWorkerTool} */ this.inner = PrivateFetchWorkerTool.instance; } /** * Is get supported? * @returns {boolean} Is get supported? */ get isGetSupported (): boolean { return this.inner.isGetSupported; } /** * Request data from a server with a worker that uses fetch. * @param {{url:string}} reqConfig - Request configuration for data to get. * @returns {Promise.<Buffer|Uint8Array|null>} Resolve to Buffer of data from server. */ get (reqConfig: ScratchGetRequest): Promise<Uint8Array | null> { return this.inner.get(reqConfig); } /** * Is sending supported? * @returns {boolean} Is sending supported? */ get isSendSupported (): boolean { return false; } /** * Send data to a server with a worker that uses fetch. * @throws {Error} A not implemented error. */ send (): never { throw new Error('Not implemented.'); } }