@here/harp-mapview
Version:
Functionality needed to render a map.
234 lines • 10.6 kB
JavaScript
;
/*
* Copyright (C) 2019-2021 HERE Europe B.V.
* Licensed under Apache 2.0, see full license in LICENSE
* SPDX-License-Identifier: Apache-2.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkerLoader = void 0;
require("@here/harp-fetch");
const harp_utils_1 = require("@here/harp-utils");
const WorkerBootstrapDefs_1 = require("./WorkerBootstrapDefs");
const logger = harp_utils_1.LoggerManager.instance.create("WorkerLoader");
/**
* Set of `Worker` loading and initialization helpers:
* - starting Worker from URL with fallback to XHR+blob {@link WorkerLoader.startWorker}
* - waiting for proper worker initialization, see {@link WorkerLoader.waitWorkerInitialized}
*/
class WorkerLoader {
/**
* Starts worker by first attempting load from `scriptUrl` using native `Worker` constructor.
* Then waits (using [[waitWorkerInitialized]]) for first message that indicates successful
* initialization.
* If `scriptUrl`'s origin is different than `baseUrl`, then in case of error falls back to
* [[startWorkerBlob]].
*
* We must resolve/reject promise at some time, so it is expected that any sane application will
* be able to load worker code in some amount of time.
* By default, this method timeouts after 10 seconds (configurable using `timeout` argument).
*
* This method is needed as browsers in general forbid to load worker if it's not on 'same
* origin' regardless of Content-Security-Policy.
*
* For blob-based fallback work, one need to ensure that Content Security Policy (CSP) allows
* loading web worker code from `Blob`s. By default browsers, allow 'blob:' for workers, but
* this may change.
*
* Following snippet setups CSP, so workers can be started from blob urls:
*
* <head>
* <meta http-equiv="Content-Security-Policy" content="child-src blob:">
* </head>
*
* Tested on:
* * Chrome 67 / Linux, Window, OSX, Android
* * Firefox 60 / Linux, Windows, OSX
* * Edge 41 / Windows
* * Safari 11 / OSX
* * Samsung Internet 7.2
*
* See
* * https://benohead.com/cross-domain-cross-browser-web-workers/
* * MapBox
* * https://stackoverflow.com/questions/21913673/execute-web-worker-from-different-origin
* * https://github.com/mapbox/mapbox-gl-js/issues/2658
* * https://github.com/mapbox/mapbox-gl-js/issues/559
* * https://github.com/mapbox/mapbox-gl-js/issues/6058
*
* Findings:
*
* * Chrome reports CSP by exception when constructing [[Worker]] instance.
* * Firefox reports CSP errors when loading in first event:
* https://bugzilla.mozilla.org/show_bug.cgi?id=1241888
* * Firefox 62, Chrome 67 obeys `<meta http-equiv="Content-Security-Policy">` with
* `worker-src blob:` but doesn't obey `worker-src URL` when used
* * Chrome 67 doesn't obey CSP `worker-src URL` despite it's documented as supported
* (https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/worker-src)
*
* @param scriptUrl - web worker script URL
* @param timeout - timeout in milliseconds, in which worker should set initial message
* (default 10 seconds)
*/
static startWorker(scriptUrl, timeout = 10000) {
if (scriptUrl.startsWith("blob:")) {
return this.startWorkerImmediately(scriptUrl, timeout);
}
if (this.directlyFallbackToBlobBasedLoading) {
return this.startWorkerBlob(scriptUrl, timeout);
}
return this.startWorkerImmediately(scriptUrl, timeout).catch(error => {
if (typeof window !== "undefined") {
const pageUrl = window.location.href;
const fullScriptUrl = new URL(scriptUrl, pageUrl).href;
if (harp_utils_1.getUrlOrigin(fullScriptUrl) === harp_utils_1.getUrlOrigin(pageUrl)) {
throw error;
}
logger.log("#startWorker: cross-origin worker construction failed, trying load with blob");
this.directlyFallbackToBlobBasedLoading = true;
return WorkerLoader.startWorkerBlob(scriptUrl, timeout);
}
else {
throw error;
}
});
}
/**
* Start worker, loading it immediately from `scriptUrl`. Waits (using
* [[waitWorkerInitialized]]) for successful worker start.
*
* @param scriptUrl - web worker script URL
*/
static startWorkerImmediately(scriptUrl, timeout) {
try {
const worker = new Worker(scriptUrl);
return this.waitWorkerInitialized(worker, timeout);
}
catch (error) {
return Promise.reject(error);
}
}
/**
* Start worker "via blob" by first loading worker script code with [[fetch]], creating `Blob`
* and attempting to start worker from blob url. Waits (using [[waitWorkerInitialized]]) for
* successful worker start.
*
* @param scriptUrl - web worker script URL
*/
static startWorkerBlob(scriptUrl, timeout) {
return this.fetchScriptSourceToBlobUrl(scriptUrl).then(blobUrl => {
return this.startWorkerImmediately(blobUrl, timeout);
});
}
/**
* Fetch script source as `Blob` url.
*
* Reuses results, if there are many simultaneous requests.
*
* @param scriptUrl - web worker script URL
* @return promise that resolves to url of a `Blob` with script source code
*/
static fetchScriptSourceToBlobUrl(scriptUrl) {
let loadingPromise = this.sourceLoaderCache.get(scriptUrl);
if (loadingPromise !== undefined) {
return loadingPromise;
}
loadingPromise = fetch(scriptUrl)
.then(response => response.text())
.catch(error => {
throw new Error(`WorkerLoader#fetchScriptSourceToBlob: failed to load worker script: ${error}`);
})
.then(scriptSource => {
this.sourceLoaderCache.delete(scriptUrl);
const blob = new Blob([scriptSource], { type: "application/javascript" });
return URL.createObjectURL(blob);
});
this.sourceLoaderCache.set(scriptUrl, loadingPromise);
return loadingPromise;
}
/**
* Waits for successful Web Worker start.
*
* Expects that worker script sends initial message.
*
* If first event is `message` then assumes that worker has been loaded sussesfully and promise
* resolves to `worker` object passed as argument.
*
* If first event is 'error', then it is assumed that worker failed to load and promise is
* rejected.
*
* (NOTE: The initial 'message' - if received - is immediately replayed using worker's
* `dispatchEvent`, so application code can also consume it as confirmation of successful
* worker initialization.
*
* We must resolve/reject promise at some time, so it is expected that any sane application will
* be able to load worker code in some amount of time.
*
* @param worker - [[Worker]] instance to be checked
* @param timeout - timeout in milliseconds, in which worker should set initial message
* @returns `Promise` that resolves to `worker` on success
*/
static waitWorkerInitialized(worker, timeout) {
return new Promise((resolve, reject) => {
const firstMessageCallback = (event) => {
const message = event.data;
if (WorkerBootstrapDefs_1.isWorkerBootstrapRequest(message)) {
const dependencies = message.dependencies;
const resolvedDependencies = [];
for (const dependency of dependencies) {
const resolved = this.dependencyUrlMapping[dependency];
if (!resolved) {
cleanup();
reject(new Error(`#waitWorkerInitialized: Unable to resolve '${dependency}'` +
` as needed by worker script.`));
return;
}
resolvedDependencies.push(resolved);
}
const response = {
type: "worker-bootstrap-response",
resolvedDependencies
};
worker.postMessage(response);
return;
}
cleanup();
resolve(worker);
// We've just consumed first message from worker before client has any chance to
// even call `addEventListener` on it, so here after resolve, we wait next tick and
// replay message so user has chance to intercept it in its own handler.
setTimeout(() => {
worker.dispatchEvent(event);
}, 0);
};
const errorCallback = (error) => {
cleanup();
// Error events do not carry any useful information on tested browsers, so we assume
// that any error before 'firstMessageCallback' as failed Worker initialization.
let message = "Error during worker initialization";
if (error.message) {
message = message + `: ${error.message}`;
}
if (typeof error.filename === "string" && typeof error.lineno === "number") {
message = message + ` in ${error.filename}:${error.lineno}`;
}
reject(new Error(message));
};
const cleanup = () => {
clearTimeout(timerId);
worker.removeEventListener("message", firstMessageCallback);
worker.removeEventListener("error", errorCallback);
};
worker.addEventListener("error", errorCallback);
worker.addEventListener("message", firstMessageCallback);
const timerId = setTimeout(() => {
cleanup();
reject(new Error("Timeout exceeded when waiting for first message from worker."));
}, timeout);
});
}
}
exports.WorkerLoader = WorkerLoader;
WorkerLoader.directlyFallbackToBlobBasedLoading = false;
WorkerLoader.sourceLoaderCache = new Map();
WorkerLoader.dependencyUrlMapping = {};
//# sourceMappingURL=WorkerLoader.js.map