diff --git a/src/style/style.js b/src/style/style.js index a9c07ce169f..5673f87a3e5 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -11,7 +11,7 @@ import GlyphManager from '../render/glyph_manager'; import Light from './light'; import LineAtlas from '../render/line_atlas'; import { pick, clone, extend, deepEqual, filterObject, mapObject } from '../util/util'; -import { getJSON, getReferrer, ResourceType } from '../util/ajax'; +import { getJSON, getReferrer, makeRequest, ResourceType } from '../util/ajax'; import { isMapboxURL, normalizeStyleURL } from '../util/mapbox'; import browser from '../util/browser'; import Dispatcher from '../util/dispatcher'; @@ -51,6 +51,7 @@ import type {Callback} from '../types/callback'; import type EvaluationParameters from './evaluation_parameters'; import type {Placement} from '../symbol/placement'; import type {Cancelable} from '../types/cancelable'; +import type {RequestParameters, ResponseCallback} from '../util/ajax'; import type {GeoJSON} from '@mapbox/geojson-types'; import type { LayerSpecification, @@ -1213,6 +1214,10 @@ class Style extends Evented { getGlyphs(mapId: string, params: {stacks: {[string]: Array}}, callback: Callback<{[string]: {[number]: ?StyleGlyph}}>) { this.glyphManager.getGlyphs(params.stacks, callback); } + + getResource(mapId: string, params: RequestParameters, callback: ResponseCallback): Cancelable { + return makeRequest(params, callback); + } } Style.getSourceType = getSourceType; diff --git a/src/util/actor.js b/src/util/actor.js index 357a4e8aa28..e663f5e261a 100644 --- a/src/util/actor.js +++ b/src/util/actor.js @@ -4,6 +4,7 @@ import { bindAll } from './util'; import { serialize, deserialize } from './web_worker_transfer'; import type {Transferable} from '../types/transferable'; +import type {Cancelable} from '../types/cancelable'; /** * An implementation of the [Actor design pattern](http://en.wikipedia.org/wiki/Actor_model) @@ -42,7 +43,7 @@ class Actor { * @param targetMapId A particular mapId to which to send this message. * @private */ - send(type: string, data: mixed, callback: ?Function, targetMapId: ?string) { + send(type: string, data: mixed, callback: ?Function, targetMapId: ?string): ?Cancelable { const id = callback ? `${this.mapId}:${this.callbackID++}` : null; if (callback) this.callbacks[id] = callback; const buffers: Array = []; @@ -53,6 +54,16 @@ class Actor { id: String(id), data: serialize(data, buffers) }, buffers); + if (callback) { + return { + cancel: () => this.target.postMessage({ + targetMapId, + sourceMapId: this.mapId, + type: '', + id: String(id) + }) + }; + } } receive(message: Object) { @@ -64,6 +75,7 @@ class Actor { return; const done = (err, data) => { + delete this.callbacks[id]; const buffers: Array = []; this.target.postMessage({ sourceMapId: this.mapId, @@ -74,7 +86,7 @@ class Actor { }, buffers); }; - if (data.type === '') { + if (data.type === '' || data.type === '') { callback = this.callbacks[data.id]; delete this.callbacks[data.id]; if (callback && data.error) { @@ -84,7 +96,14 @@ class Actor { } } else if (typeof data.id !== 'undefined' && this.parent[data.type]) { // data.type == 'loadTile', 'removeTile', etc. - this.parent[data.type](data.sourceMapId, deserialize(data.data), done); + // Add a placeholder so that we can discover when the done callback was called already. + this.callbacks[data.id] = null; + const cancelable = this.parent[data.type](data.sourceMapId, deserialize(data.data), done); + if (cancelable && this.callbacks[data.id] === null) { + // Only add the cancelable callback if the done callback wasn't already called. + // Otherwise we will never be able to delete it. + this.callbacks[data.id] = cancelable; + } } else if (typeof data.id !== 'undefined' && this.parent.getWorkerSource) { // data.type == sourcetype.method const keys = data.type.split('.'); diff --git a/src/util/ajax.js b/src/util/ajax.js index a9cf1b5b2cb..dff48a7294c 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -71,14 +71,17 @@ class AJAXError extends Error { } } +function isWorker() { + return typeof WorkerGlobalScope !== 'undefined' && typeof self !== 'undefined' && + self instanceof WorkerGlobalScope; +} + // Ensure that we're sending the correct referrer from blob URL worker bundles. // For files loaded from the local file system, `location.origin` will be set // to the string(!) "null" (Firefox), or "file://" (Chrome, Safari, Edge, IE), // and we will set an empty referrer. Otherwise, we're using the document's URL. /* global self, WorkerGlobalScope */ -export const getReferrer = typeof WorkerGlobalScope !== 'undefined' && - typeof self !== 'undefined' && - self instanceof WorkerGlobalScope ? +export const getReferrer = isWorker() ? () => self.worker && self.worker.referrer : () => { const origin = window.location.origin; @@ -158,7 +161,22 @@ function makeXMLHttpRequest(requestParameters: RequestParameters, callback: Resp return { cancel: () => xhr.abort() }; } -const makeRequest = window.fetch && window.Request && window.AbortController ? makeFetchRequest : makeXMLHttpRequest; +export const makeRequest = function(requestParameters: RequestParameters, callback: ResponseCallback): Cancelable { + // We're trying to use the Fetch API if possible. However, in some situations we can't use it: + // - IE11 doesn't support it at all. In this case, we dispatch the request to the main thread so + // that we can get an accruate referrer header. + // - Requests for resources with the file:// URI scheme don't work with the Fetch API either. In + // this case we unconditionally use XHR on the current thread since referrers don't matter. + if (!/^file:/.test(requestParameters.url)) { + if (window.fetch && window.Request && window.AbortController) { + return makeFetchRequest(requestParameters, callback); + } + if (isWorker() && self.worker && self.worker.actor) { + return self.worker.actor.send('getResource', requestParameters, callback); + } + } + return makeXMLHttpRequest(requestParameters, callback); +}; export const getJSON = function(requestParameters: RequestParameters, callback: ResponseCallback): Cancelable { return makeRequest(extend(requestParameters, { type: 'json' }), callback); diff --git a/test/ajax_stubs.js b/test/ajax_stubs.js index b45f684808b..70d61fa1169 100644 --- a/test/ajax_stubs.js +++ b/test/ajax_stubs.js @@ -67,6 +67,8 @@ export const getArrayBuffer = function({ url }, callback) { }); }; +export const makeRequest = getArrayBuffer; + export const postData = function({ url, body }, callback) { return request.post(url, body, (error, response, body) => { if (!error && response.statusCode >= 200 && response.statusCode < 300) {