scratch-storage
Version:
Load and store project and asset files for Scratch 3.0
218 lines (193 loc) • 6.92 kB
text/typescript
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.');
}
}