diff --git a/src/util/ajax.js b/src/util/ajax.js index 9cce7ff090f..98c4ea6b931 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -4,6 +4,7 @@ import window from './window'; import { extend } from './util'; import { isMapboxHTTPURL } from './mapbox'; import config from './config'; +import assert from 'assert'; import type { Callback } from '../types/callback'; import type { Cancelable } from '../types/cancelable'; @@ -110,6 +111,10 @@ function makeFetchRequest(requestParameters: RequestParameters, callback: Respon callback(new AJAXError(response.statusText, response.status, requestParameters.url)); } }).catch((error) => { + if (error.code === 20) { + // silence expected AbortError + return; + } callback(new Error(error.message)); }); @@ -175,8 +180,12 @@ function sameOrigin(url) { const transparentPngUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII='; -const imageQueue = []; -let numImageRequests = 0; +let imageQueue, numImageRequests; +export const resetImageRequestQueue = () => { + imageQueue = []; + numImageRequests = 0; +}; +resetImageRequestQueue(); export const getImage = function(requestParameters: RequestParameters, callback: Callback): Cancelable { // limit concurrent image loads to help with raster sources performance on big screens @@ -187,17 +196,25 @@ export const getImage = function(requestParameters: RequestParameters, callback: } numImageRequests++; - // request the image with XHR to work around caching issues - // see https://github.com/mapbox/mapbox-gl-js/issues/1470 - return getArrayBuffer(requestParameters, (err: ?Error, data: ?ArrayBuffer, cacheControl: ?string, expires: ?string) => { - + let advanced = false; + const advanceImageRequestQueue = () => { + if (advanced) return; + advanced = true; numImageRequests--; + assert(numImageRequests >= 0); while (imageQueue.length && numImageRequests < config.MAX_PARALLEL_IMAGE_REQUESTS) { // eslint-disable-line const {requestParameters, callback, cancelled} = imageQueue.shift(); if (!cancelled) { getImage(requestParameters, callback); } } + }; + + // request the image with XHR to work around caching issues + // see https://github.com/mapbox/mapbox-gl-js/issues/1470 + const request = getArrayBuffer(requestParameters, (err: ?Error, data: ?ArrayBuffer, cacheControl: ?string, expires: ?string) => { + + advanceImageRequestQueue(); if (err) { callback(err); @@ -215,6 +232,13 @@ export const getImage = function(requestParameters: RequestParameters, callback: img.src = data.byteLength ? URL.createObjectURL(blob) : transparentPngUrl; } }); + + return { + cancel: () => { + request.cancel(); + advanceImageRequestQueue(); + } + }; }; export const getVideo = function(urls: Array, callback: Callback): Cancelable { diff --git a/test/unit/util/ajax.test.js b/test/unit/util/ajax.test.js index 69f24906984..97c6f1ff2b6 100644 --- a/test/unit/util/ajax.test.js +++ b/test/unit/util/ajax.test.js @@ -3,7 +3,8 @@ import { getArrayBuffer, getJSON, postData, - getImage + getImage, + resetImageRequestQueue } from '../../../src/util/ajax'; import window from '../../../src/util/window'; import config from '../../../src/util/config'; @@ -128,5 +129,28 @@ test('ajax', (t) => { window.server.requests[0].respond(); }); + t.test('getImage cancelling frees up request for maxParallelImageRequests', (t) => { + resetImageRequestQueue(); + + window.server.respondWith(request => request.respond(200, {'Content-Type': 'image/png'}, '')); + + const maxRequests = config.MAX_PARALLEL_IMAGE_REQUESTS; + + // jsdom doesn't call image onload; fake it https://github.com/jsdom/jsdom/issues/1816 + const jsdomImage = window.Image; + window.Image = class { + set src(src) { + setTimeout(() => this.onload()); + } + }; + + for (let i = 0; i < maxRequests + 1; i++) { + getImage({url: ''}, () => t.fail).cancel(); + } + t.equals(window.server.requests.length, maxRequests + 1); + window.Image = jsdomImage; + t.end(); + }); + t.end(); });