diff --git a/src/source/canvas_source.js b/src/source/canvas_source.js index f21785cb50a..992fa15632b 100644 --- a/src/source/canvas_source.js +++ b/src/source/canvas_source.js @@ -111,6 +111,7 @@ class CanvasSource extends ImageSource { */ load() { + this._loaded = true; if (!this.canvas) { this.canvas = (this.options.canvas instanceof window.HTMLCanvasElement) ? this.options.canvas : diff --git a/src/source/geojson_source.js b/src/source/geojson_source.js index 1d2c14495b8..0ab32d56352 100644 --- a/src/source/geojson_source.js +++ b/src/source/geojson_source.js @@ -11,6 +11,7 @@ import type {Source} from './source'; import type Map from '../ui/map'; import type Dispatcher from '../util/dispatcher'; import type Tile from './tile'; +import type Actor from '../util/actor'; import type {Callback} from '../types/callback'; import type {GeoJSON, GeoJSONFeature} from '@mapbox/geojson-types'; import type {GeoJSONSourceSpecification} from '../style-spec/types'; @@ -74,9 +75,8 @@ class GeoJSONSource extends Evented implements Source { _data: GeoJSON | string; _options: any; workerOptions: any; - dispatcher: Dispatcher; map: Map; - workerID: number; + actor: Actor; _loaded: boolean; _collectResourceTiming: boolean; _resourceTiming: Array; @@ -100,8 +100,9 @@ class GeoJSONSource extends Evented implements Source { this.isTileClipped = true; this.reparseOverscaled = true; this._removed = false; + this._loaded = false; - this.dispatcher = dispatcher; + this.actor = dispatcher.getActor(); this.setEventedParent(eventedParent); this._data = (options.data: any); @@ -202,7 +203,7 @@ class GeoJSONSource extends Evented implements Source { * @returns {GeoJSONSource} this */ getClusterExpansionZoom(clusterId: number, callback: Callback) { - this.dispatcher.send('geojson.getClusterExpansionZoom', { clusterId, source: this.id }, callback, this.workerID); + this.actor.send('geojson.getClusterExpansionZoom', { clusterId, source: this.id }, callback); return this; } @@ -214,7 +215,7 @@ class GeoJSONSource extends Evented implements Source { * @returns {GeoJSONSource} this */ getClusterChildren(clusterId: number, callback: Callback>) { - this.dispatcher.send('geojson.getClusterChildren', { clusterId, source: this.id }, callback, this.workerID); + this.actor.send('geojson.getClusterChildren', { clusterId, source: this.id }, callback); return this; } @@ -228,12 +229,12 @@ class GeoJSONSource extends Evented implements Source { * @returns {GeoJSONSource} this */ getClusterLeaves(clusterId: number, limit: number, offset: number, callback: Callback>) { - this.dispatcher.send('geojson.getClusterLeaves', { + this.actor.send('geojson.getClusterLeaves', { source: this.id, clusterId, limit, offset - }, callback, this.workerID); + }, callback); return this; } @@ -243,6 +244,7 @@ class GeoJSONSource extends Evented implements Source { * using geojson-vt or supercluster as appropriate. */ _updateWorkerData(callback: Callback) { + this._loaded = false; const options = extend({}, this.workerOptions); const data = this._data; if (typeof data === 'string') { @@ -255,7 +257,7 @@ class GeoJSONSource extends Evented implements Source { // target {this.type}.loadData rather than literally geojson.loadData, // so that other geojson-like source types can easily reuse this // implementation - this.workerID = this.dispatcher.send(`${this.type}.loadData`, options, (err, result) => { + this.actor.send(`${this.type}.loadData`, options, (err, result) => { if (this._removed || (result && result.abandoned)) { return; } @@ -271,14 +273,18 @@ class GeoJSONSource extends Evented implements Source { // message queue. Waiting instead for the 'coalesce' to round-trip // through the foreground just means we're throttling the worker // to run at a little less than full-throttle. - this.dispatcher.send(`${this.type}.coalesce`, { source: options.source }, null, this.workerID); + this.actor.send(`${this.type}.coalesce`, { source: options.source }, null); callback(err); + }); + } - }, this.workerID); + loaded(): boolean { + return this._loaded; } loadTile(tile: Tile, callback: Callback) { - const message = tile.workerID === undefined ? 'loadTile' : 'reloadTile'; + const message = !tile.actor ? 'loadTile' : 'reloadTile'; + tile.actor = this.actor; const params = { type: this.type, uid: tile.uid, @@ -291,7 +297,8 @@ class GeoJSONSource extends Evented implements Source { showCollisionBoxes: this.map.showCollisionBoxes }; - tile.workerID = this.dispatcher.send(message, params, (err, data) => { + tile.request = this.actor.send(message, params, (err, data) => { + delete tile.request; tile.unloadVectorData(); if (tile.aborted) { @@ -305,21 +312,25 @@ class GeoJSONSource extends Evented implements Source { tile.loadVectorData(data, this.map.painter, message === 'reloadTile'); return callback(null); - }, this.workerID); + }); } abortTile(tile: Tile) { + if (tile.request) { + tile.request.cancel(); + delete tile.request; + } tile.aborted = true; } unloadTile(tile: Tile) { tile.unloadVectorData(); - this.dispatcher.send('removeTile', { uid: tile.uid, type: this.type, source: this.id }, null, tile.workerID); + this.actor.send('removeTile', { uid: tile.uid, type: this.type, source: this.id }); } onRemove() { this._removed = true; - this.dispatcher.send('removeSource', { type: this.type, source: this.id }, null, this.workerID); + this.actor.send('removeSource', { type: this.type, source: this.id }); } serialize() { diff --git a/src/source/image_source.js b/src/source/image_source.js index 0c5e46bb8f7..71a60fc75b2 100644 --- a/src/source/image_source.js +++ b/src/source/image_source.js @@ -83,6 +83,7 @@ class ImageSource extends Evented implements Source { _boundsArray: RasterBoundsArray; boundsBuffer: VertexBuffer; boundsSegments: SegmentVector; + _loaded: boolean; /** * @private @@ -98,6 +99,7 @@ class ImageSource extends Evented implements Source { this.maxzoom = 22; this.tileSize = 512; this.tiles = {}; + this._loaded = false; this.setEventedParent(eventedParent); @@ -105,11 +107,13 @@ class ImageSource extends Evented implements Source { } load(newCoordinates?: Coordinates, successCallback?: () => void) { + this._loaded = false; this.fire(new Event('dataloading', {dataType: 'source'})); this.url = this.options.url; getImage(this.map._requestManager.transformRequest(this.url, ResourceType.Image), (err, image) => { + this._loaded = true; if (err) { this.fire(new ErrorEvent(err)); } else if (image) { @@ -125,6 +129,10 @@ class ImageSource extends Evented implements Source { }); } + loaded(): boolean { + return this._loaded; + } + /** * Updates the image URL and, optionally, the coordinates. To avoid having the image flash after changing, * set the `raster-fade-duration` paint property on the raster layer to 0. diff --git a/src/source/raster_dem_tile_source.js b/src/source/raster_dem_tile_source.js index 0a414bfb3c2..1937a103d6e 100644 --- a/src/source/raster_dem_tile_source.js +++ b/src/source/raster_dem_tile_source.js @@ -64,8 +64,9 @@ class RasterDEMTileSource extends RasterTileSource implements Source { encoding: this.encoding }; - if (!tile.workerID || tile.state === 'expired') { - tile.workerID = this.dispatcher.send('loadDEMTile', params, done.bind(this)); + if (!tile.actor || tile.state === 'expired') { + tile.actor = this.dispatcher.getActor(); + tile.actor.send('loadDEMTile', params, done.bind(this)); } } } @@ -125,7 +126,9 @@ class RasterDEMTileSource extends RasterTileSource implements Source { delete tile.neighboringTiles; tile.state = 'unloaded'; - this.dispatcher.send('removeDEMTile', { uid: tile.uid, source: this.id }, undefined, tile.workerID); + if (tile.actor) { + tile.actor.send('removeDEMTile', { uid: tile.uid, source: this.id }); + } } } diff --git a/src/source/raster_tile_source.js b/src/source/raster_tile_source.js index e4be8b19ff2..564b46bc7c7 100644 --- a/src/source/raster_tile_source.js +++ b/src/source/raster_tile_source.js @@ -62,9 +62,11 @@ class RasterTileSource extends Evented implements Source { } load() { + this._loaded = false; this.fire(new Event('dataloading', {dataType: 'source'})); this._tileJSONRequest = loadTileJSON(this._options, this.map._requestManager, (err, tileJSON) => { this._tileJSONRequest = null; + this._loaded = true; if (err) { this.fire(new ErrorEvent(err)); } else if (tileJSON) { @@ -83,6 +85,10 @@ class RasterTileSource extends Evented implements Source { }); } + loaded(): boolean { + return this._loaded; + } + onAdd(map: Map) { this.map = map; this.load(); diff --git a/src/source/source.js b/src/source/source.js index b0ab324a052..bea035c108e 100644 --- a/src/source/source.js +++ b/src/source/source.js @@ -50,6 +50,7 @@ export interface Source { vectorLayerIds?: Array, hasTransition(): boolean; + loaded(): boolean; fire(event: Event): mixed; diff --git a/src/source/source_cache.js b/src/source/source_cache.js index 80c25045330..a8b5af3b597 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -119,6 +119,7 @@ class SourceCache extends Evented { loaded(): boolean { if (this._sourceErrored) { return true; } if (!this._sourceLoaded) { return false; } + if (!this._source.loaded()) { return false; } for (const t in this._tiles) { const tile = this._tiles[t]; if (tile.state !== 'loaded' && tile.state !== 'errored') diff --git a/src/source/tile.js b/src/source/tile.js index 400590a7abe..2471331e70b 100644 --- a/src/source/tile.js +++ b/src/source/tile.js @@ -22,6 +22,7 @@ const CLOCK_SKEW_RETRY_TIMEOUT = 30000; import type {Bucket} from '../data/bucket'; import type StyleLayer from '../style/style_layer'; import type {WorkerTileResult} from './worker_source'; +import type Actor from '../util/actor'; import type DEMData from '../data/dem_data'; import type {AlphaImage} from '../util/image'; import type ImageAtlas from '../render/image_atlas'; @@ -73,7 +74,7 @@ class Tile { redoWhenDone: boolean; showCollisionBoxes: boolean; placementSource: any; - workerID: number | void; + actor: ?Actor; vtLayers: {[string]: VectorTileLayer}; mask: Mask; diff --git a/src/source/vector_tile_source.js b/src/source/vector_tile_source.js index 994c1208798..db0a3b49192 100644 --- a/src/source/vector_tile_source.js +++ b/src/source/vector_tile_source.js @@ -38,6 +38,7 @@ class VectorTileSource extends Evented implements Source { reparseOverscaled: boolean; isTileClipped: boolean; _tileJSONRequest: ?Cancelable; + _loaded: boolean; constructor(id: string, options: VectorSourceSpecification & {collectResourceTiming: boolean}, dispatcher: Dispatcher, eventedParent: Evented) { super(); @@ -51,6 +52,7 @@ class VectorTileSource extends Evented implements Source { this.tileSize = 512; this.reparseOverscaled = true; this.isTileClipped = true; + this._loaded = false; extend(this, pick(options, ['url', 'scheme', 'tileSize'])); this._options = extend({ type: 'vector' }, options); @@ -65,9 +67,11 @@ class VectorTileSource extends Evented implements Source { } load() { + this._loaded = false; this.fire(new Event('dataloading', {dataType: 'source'})); this._tileJSONRequest = loadTileJSON(this._options, this.map._requestManager, (err, tileJSON) => { this._tileJSONRequest = null; + this._loaded = true; if (err) { this.fire(new ErrorEvent(err)); } else if (tileJSON) { @@ -85,6 +89,10 @@ class VectorTileSource extends Evented implements Source { }); } + loaded(): boolean { + return this._loaded; + } + hasTile(tileID: OverscaledTileID) { return !this.tileBounds || this.tileBounds.contains(tileID.canonical); } @@ -120,16 +128,19 @@ class VectorTileSource extends Evented implements Source { }; params.request.collectResourceTiming = this._collectResourceTiming; - if (tile.workerID === undefined || tile.state === 'expired') { - tile.workerID = this.dispatcher.send('loadTile', params, done.bind(this)); + if (!tile.actor || tile.state === 'expired') { + tile.actor = this.dispatcher.getActor(); + tile.request = tile.actor.send('loadTile', params, done.bind(this)); } else if (tile.state === 'loading') { // schedule tile reloading after it has been loaded tile.reloadCallback = callback; } else { - this.dispatcher.send('reloadTile', params, done.bind(this), tile.workerID); + tile.request = tile.actor.send('reloadTile', params, done.bind(this)); } function done(err, data) { + delete tile.request; + if (tile.aborted) return callback(null); @@ -155,12 +166,20 @@ class VectorTileSource extends Evented implements Source { } abortTile(tile: Tile) { - this.dispatcher.send('abortTile', { uid: tile.uid, type: this.type, source: this.id }, undefined, tile.workerID); + if (tile.request) { + tile.request.cancel(); + delete tile.request; + } + if (tile.actor) { + tile.actor.send('abortTile', { uid: tile.uid, type: this.type, source: this.id }, undefined); + } } unloadTile(tile: Tile) { tile.unloadVectorData(); - this.dispatcher.send('removeTile', { uid: tile.uid, type: this.type, source: this.id }, undefined, tile.workerID); + if (tile.actor) { + tile.actor.send('removeTile', { uid: tile.uid, type: this.type, source: this.id }, undefined); + } } hasTransition() { diff --git a/src/source/video_source.js b/src/source/video_source.js index f08950c2f48..7bf0e04be88 100644 --- a/src/source/video_source.js +++ b/src/source/video_source.js @@ -63,6 +63,7 @@ class VideoSource extends ImageSource { } load() { + this._loaded = false; const options = this.options; this.urls = []; @@ -71,6 +72,7 @@ class VideoSource extends ImageSource { } getVideo(this.urls, (err, video) => { + this._loaded = true; if (err) { this.fire(new ErrorEvent(err)); } else if (video) { diff --git a/src/source/worker_source.js b/src/source/worker_source.js index 53f8a4edf5d..8d44390eb53 100644 --- a/src/source/worker_source.js +++ b/src/source/worker_source.js @@ -57,7 +57,7 @@ export type WorkerDEMTileCallback = (err: ?Error, result: ?DEMData) => void; * the WebWorkers. In addition to providing a custom * {@link WorkerSource#loadTile}, any other methods attached to a `WorkerSource` * implementation may also be targeted by the {@link Source} via - * `dispatcher.send('source-type.methodname', params, callback)`. + * `dispatcher.getActor().send('source-type.methodname', params, callback)`. * * @see {@link Map#addSourceType} * @private diff --git a/src/util/actor.js b/src/util/actor.js index 1e53d7c9a5e..227f15f3b4f 100644 --- a/src/util/actor.js +++ b/src/util/actor.js @@ -20,18 +20,26 @@ import type {Cancelable} from '../types/cancelable'; class Actor { target: any; parent: any; - mapId: string; - callbacks: any; - callbackID: number; + mapId: ?number; + callbacks: { number: any }; name: string; + tasks: { number: any }; + taskQueue: Array; + cancelCallbacks: { number: Cancelable }; + taskTimeout: ?TimeoutID; - constructor(target: any, parent: any, mapId: any) { + static taskId: number; + + constructor(target: any, parent: any, mapId: ?number) { this.target = target; this.parent = parent; this.mapId = mapId; this.callbacks = {}; - this.callbackID = 0; - bindAll(['receive'], this); + this.tasks = {}; + this.taskQueue = []; + this.taskTimeout = null; + this.cancelCallbacks = {}; + bindAll(['receive', 'process'], this); this.target.addEventListener('message', this.receive, false); } @@ -44,78 +52,140 @@ class Actor { * @private */ 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 id = ++Actor.taskId; + if (callback) { + this.callbacks[id] = callback; + } const buffers: Array = []; this.target.postMessage({ + id, + type, + hasCallback: !!callback, targetMapId, sourceMapId: this.mapId, - type, - id: String(id), data: serialize(data, buffers) }, buffers); - if (callback) { - return { - cancel: () => { + return { + cancel: () => { + if (callback) { // Set the callback to null so that it never fires after the request is aborted. - this.callbacks[id] = null; - this.target.postMessage({ - targetMapId, - sourceMapId: this.mapId, - type: '', - id: String(id) - }); + delete this.callbacks[id]; } - }; - } + this.target.postMessage({ + id, + type: '', + targetMapId, + sourceMapId: this.mapId + }); + } + }; } receive(message: Object) { const data = message.data, id = data.id; - let callback; - if (data.targetMapId && this.mapId !== data.targetMapId) + if (!id) { return; + } - const done = (err, data) => { - delete this.callbacks[id]; - const buffers: Array = []; - this.target.postMessage({ - sourceMapId: this.mapId, - type: '', - id: String(id), - error: err ? serialize(err) : null, - data: serialize(data, buffers) - }, buffers); - }; + if (data.targetMapId && this.mapId !== data.targetMapId) { + return; + } - if (data.type === '' || data.type === '') { - callback = this.callbacks[data.id]; - delete this.callbacks[data.id]; - if (callback && data.error) { - callback(deserialize(data.error)); - } else if (callback) { - callback(null, deserialize(data.data)); + if (data.type === '') { + // Remove the original request from the queue. This is only possible if it + // hasn't been kicked off yet. The id will remain in the queue, but because + // there is no associated task, it will be dropped once it's time to execute it. + delete this.tasks[id]; + const cancel = this.cancelCallbacks[id]; + delete this.cancelCallbacks[id]; + if (cancel) { + cancel(); + } + } else { + // Store the tasks that we need to process before actually processing them. This + // is necessary because we want to keep receiving messages, and in particular, + // messages. Some tasks may take a while in the worker thread, so before + // executing the next task in our queue, postMessage preempts this and + // messages can be processed. + this.tasks[id] = data; + this.taskQueue.push(id); + if (!this.taskTimeout) { + this.taskTimeout = setTimeout(this.process, 0); } - } else if (typeof data.id !== 'undefined' && this.parent[data.type]) { - // data.type == 'loadTile', 'removeTile', etc. - // 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.cancel; + } + } + + process() { + // Reset the timeout ID so that we know that no process call is scheduled in the future yet. + this.taskTimeout = null; + if (!this.taskQueue.length) { + return; + } + const id = this.taskQueue.shift(); + const task = this.tasks[id]; + delete this.tasks[id]; + // Schedule another process call if we know there's more to process _before_ invoking the + // current task. This is necessary so that processing continues even if the current task + // doesn't execute successfully. + if (this.taskQueue.length) { + this.taskTimeout = setTimeout(this.process, 0); + } + if (!task) { + // If the task ID doesn't have associated task data anymore, it was canceled. + return; + } + + if (task.type === '') { + // The done() function in the counterpart has been called, and we are now + // firing the callback in the originating actor, if there is one. + const callback = this.callbacks[id]; + delete this.callbacks[id]; + if (callback) { + // If we get a response, but don't have a callback, the request was canceled. + if (task.error) { + callback(deserialize(task.error)); + } else { + callback(null, deserialize(task.data)); + } } - } else if (typeof data.id !== 'undefined' && this.parent.getWorkerSource) { - // data.type == sourcetype.method - const keys = data.type.split('.'); - const params = (deserialize(data.data): any); - const workerSource = (this.parent: any).getWorkerSource(data.sourceMapId, keys[0], params.source); - workerSource[keys[1]](params, done); } else { - this.parent[data.type](deserialize(data.data)); + let completed = false; + const done = task.hasCallback ? (err, data) => { + completed = true; + delete this.cancelCallbacks[id]; + const buffers: Array = []; + this.target.postMessage({ + id, + type: '', + sourceMapId: this.mapId, + error: err ? serialize(err) : null, + data: serialize(data, buffers) + }, buffers); + } : (_) => { + completed = true; + }; + + let callback = null; + const params = (deserialize(task.data): any); + if (this.parent[task.type]) { + // task.type == 'loadTile', 'removeTile', etc. + callback = this.parent[task.type](task.sourceMapId, params, done); + } else if (this.parent.getWorkerSource) { + // task.type == sourcetype.method + const keys = task.type.split('.'); + const scope = (this.parent: any).getWorkerSource(task.sourceMapId, keys[0], params.source); + callback = scope[keys[1]](params, done); + } else { + // No function was found. + done(new Error(`Could not find function ${task.type}`)); + } + + if (!completed && callback && callback.cancel) { + // Allows canceling the task as long as it hasn't been completed yet. + this.cancelCallbacks[id] = callback.cancel; + } } } @@ -124,4 +194,6 @@ class Actor { } } +Actor.taskId = 0; + export default Actor; diff --git a/src/util/dispatcher.js b/src/util/dispatcher.js index b2f3b9b0adc..bb43c1d1864 100644 --- a/src/util/dispatcher.js +++ b/src/util/dispatcher.js @@ -45,18 +45,12 @@ class Dispatcher { } /** - * Send a message to a Worker. - * @param targetID The ID of the Worker to which to send this message. Omit to allow the dispatcher to choose. - * @returns The ID of the worker to which the message was sent. + * Acquires an actor to dispatch messages to. The actors are distributed in round-robin fashion. + * @returns An actor object backed by a web worker for processing messages. */ - send(type: string, data: mixed, callback?: ?Function, targetID?: number): number { - if (typeof targetID !== 'number' || isNaN(targetID)) { - // Use round robin to send requests to web workers. - targetID = this.currentActor = (this.currentActor + 1) % this.actors.length; - } - - this.actors[targetID].send(type, data, callback); - return targetID; + getActor(): Actor { + this.currentActor = (this.currentActor + 1) % this.actors.length; + return this.actors[this.currentActor]; } remove() { diff --git a/src/util/tile_request_cache.js b/src/util/tile_request_cache.js index 50b133ff8e8..853b415036d 100644 --- a/src/util/tile_request_cache.js +++ b/src/util/tile_request_cache.js @@ -115,7 +115,7 @@ let globalEntryCounter = Infinity; export function cacheEntryPossiblyAdded(dispatcher: Dispatcher) { globalEntryCounter++; if (globalEntryCounter > cacheCheckThreshold) { - dispatcher.send('enforceCacheSizeLimit', cacheLimit); + dispatcher.getActor().send('enforceCacheSizeLimit', cacheLimit); globalEntryCounter = 0; } } diff --git a/test/unit/source/geojson_source.test.js b/test/unit/source/geojson_source.test.js index f0446aecfcd..f1e867605d8 100644 --- a/test/unit/source/geojson_source.test.js +++ b/test/unit/source/geojson_source.test.js @@ -6,10 +6,18 @@ import Transform from '../../../src/geo/transform'; import LngLat from '../../../src/geo/lng_lat'; import { extend } from '../../../src/util/util'; -const mockDispatcher = { - send () {} +const wrapDispatcher = (dispatcher) => { + return { + getActor() { + return dispatcher; + } + }; }; +const mockDispatcher = wrapDispatcher({ + send () {} +}); + const hawkHill = { "type": "FeatureCollection", "features": [{ @@ -48,13 +56,13 @@ test('GeoJSONSource#setData', (t) => { function createSource(opts) { opts = opts || {}; opts = extend(opts, { data: {} }); - return new GeoJSONSource('id', opts, { + return new GeoJSONSource('id', opts, wrapDispatcher({ send (type, data, callback) { if (callback) { return setTimeout(callback, 0); } } - }); + })); } t.test('returns self', (t) => { @@ -85,13 +93,12 @@ test('GeoJSONSource#setData', (t) => { transformRequest: (url) => { return { url }; } } }; - source.dispatcher.send = function(type, params, cb) { + source.actor.send = function(type, params, cb) { if (type === 'geojson.loadData') { t.true(params.request.collectResourceTiming, 'collectResourceTiming is true on dispatcher message'); setTimeout(cb, 0); t.end(); } - return 1; }; source.setData('http://localhost/nonexistent'); }); @@ -101,8 +108,8 @@ test('GeoJSONSource#setData', (t) => { test('GeoJSONSource#onRemove', (t) => { t.test('broadcasts "removeSource" event', (t) => { - const source = new GeoJSONSource('id', {data: {}}, { - send (type, data, callback) { + const source = new GeoJSONSource('id', {data: {}}, wrapDispatcher({ + send(type, data, callback) { t.false(callback); t.equal(type, 'removeSource'); t.deepEqual(data, { type: 'geojson', source: 'id' }); @@ -111,7 +118,7 @@ test('GeoJSONSource#onRemove', (t) => { broadcast() { // Ignore } - }); + })); source.onRemove(); }); @@ -127,19 +134,19 @@ test('GeoJSONSource#update', (t) => { transform.setLocationAtPoint(lngLat, point); t.test('sends initial loadData request to dispatcher', (t) => { - const mockDispatcher = { + const mockDispatcher = wrapDispatcher({ send(message) { t.equal(message, 'geojson.loadData'); t.end(); } - }; + }); /* eslint-disable no-new */ new GeoJSONSource('id', {data: {}}, mockDispatcher).load(); }); t.test('forwards geojson-vt options with worker request', (t) => { - const mockDispatcher = { + const mockDispatcher = wrapDispatcher({ send(message, params) { t.equal(message, 'geojson.loadData'); t.deepEqual(params.geojsonVtOptions, { @@ -152,7 +159,7 @@ test('GeoJSONSource#update', (t) => { }); t.end(); } - }; + }); new GeoJSONSource('id', { data: {}, @@ -177,13 +184,13 @@ test('GeoJSONSource#update', (t) => { t.end(); }); t.test('fires event when metadata loads', (t) => { - const mockDispatcher = { + const mockDispatcher = wrapDispatcher({ send(message, args, callback) { if (callback) { setTimeout(callback, 0); } } - }; + }); const source = new GeoJSONSource('id', {data: {}}, mockDispatcher); @@ -195,13 +202,13 @@ test('GeoJSONSource#update', (t) => { }); t.test('fires "error"', (t) => { - const mockDispatcher = { + const mockDispatcher = wrapDispatcher({ send(message, args, callback) { if (callback) { setTimeout(callback.bind(null, 'error'), 0); } } - }; + }); const source = new GeoJSONSource('id', {data: {}}, mockDispatcher); @@ -215,7 +222,7 @@ test('GeoJSONSource#update', (t) => { t.test('sends loadData request to dispatcher after data update', (t) => { let expectedLoadDataCalls = 2; - const mockDispatcher = { + const mockDispatcher = wrapDispatcher({ send(message, args, callback) { if (message === 'geojson.loadData' && --expectedLoadDataCalls <= 0) { t.end(); @@ -224,7 +231,7 @@ test('GeoJSONSource#update', (t) => { setTimeout(callback, 0); } } - }; + }); const source = new GeoJSONSource('id', {data: {}}, mockDispatcher); source.map = { diff --git a/test/unit/source/query_features.test.js b/test/unit/source/query_features.test.js index 995828716d9..678aa3e7d0e 100644 --- a/test/unit/source/query_features.test.js +++ b/test/unit/source/query_features.test.js @@ -24,7 +24,11 @@ test('QueryFeatures#source', (t) => { type: 'geojson', data: { type: 'FeatureCollection', features: [] } }, { - send (type, params, callback) { return callback(); } + getActor() { + return { + send(type, params, callback) { return callback(); } + }; + } }); const result = querySourceFeatures(sourceCache, {}); t.deepEqual(result, []); diff --git a/test/unit/source/source_cache.test.js b/test/unit/source/source_cache.test.js index 4c707a51721..cc59deab049 100644 --- a/test/unit/source/source_cache.test.js +++ b/test/unit/source/source_cache.test.js @@ -34,6 +34,9 @@ function MockSourceType(id, sourceOptions, _dispatcher, eventedParent) { } setTimeout(callback, 0); } + loaded() { + return true; + } onAdd() { if (sourceOptions.noLoad) return; if (sourceOptions.error) { diff --git a/test/unit/source/vector_tile_source.test.js b/test/unit/source/vector_tile_source.test.js index ece71e24853..8db87f7f000 100644 --- a/test/unit/source/vector_tile_source.test.js +++ b/test/unit/source/vector_tile_source.test.js @@ -5,8 +5,20 @@ import window from '../../../src/util/window'; import { Evented } from '../../../src/util/evented'; import { RequestManager } from '../../../src/util/mapbox'; +const wrapDispatcher = (dispatcher) => { + return { + getActor() { + return dispatcher; + } + }; +}; + +const mockDispatcher = wrapDispatcher({ + send () {} +}); + function createSource(options, transformCallback) { - const source = new VectorTileSource('id', options, { send() {} }, options.eventedParent); + const source = new VectorTileSource('id', options, mockDispatcher, options.eventedParent); source.onAdd({ transform: { showCollisionBoxes: false }, _getMapId: () => 1, @@ -145,11 +157,13 @@ test('VectorTileSource', (t) => { scheme }); - source.dispatcher.send = function(type, params) { - t.equal(type, 'loadTile'); - t.equal(expectedURL, params.request.url); - t.end(); - }; + source.dispatcher = wrapDispatcher({ + send(type, params) { + t.equal(type, 'loadTile'); + t.equal(expectedURL, params.request.url); + t.end(); + } + }); source.on('data', (e) => { if (e.sourceDataType === 'metadata') source.loadTile({ @@ -191,11 +205,13 @@ test('VectorTileSource', (t) => { tiles: ["http://example.com/{z}/{x}/{y}.png"] }); const events = []; - source.dispatcher.send = function(type, params, cb) { - events.push(type); - if (cb) setTimeout(cb, 0); - return 1; - }; + source.dispatcher = wrapDispatcher({ + send(type, params, cb) { + events.push(type); + if (cb) setTimeout(cb, 0); + return 1; + } + }); source.on('data', (e) => { if (e.sourceDataType === 'metadata') { @@ -277,16 +293,18 @@ test('VectorTileSource', (t) => { tiles: ["http://example.com/{z}/{x}/{y}.png"], collectResourceTiming: true }); - source.dispatcher.send = function(type, params, cb) { - t.true(params.request.collectResourceTiming, 'collectResourceTiming is true on dispatcher message'); - setTimeout(cb, 0); - t.end(); + source.dispatcher = wrapDispatcher({ + send(type, params, cb) { + t.true(params.request.collectResourceTiming, 'collectResourceTiming is true on dispatcher message'); + setTimeout(cb, 0); + t.end(); - // do nothing for cache size check dispatch - source.dispatcher.send = function() {}; + // do nothing for cache size check dispatch + source.dispatcher = mockDispatcher; - return 1; - }; + return 1; + } + }); source.on('data', (e) => { if (e.sourceDataType === 'metadata') { diff --git a/test/unit/util/actor.test.js b/test/unit/util/actor.test.js index 38c31fcc1f8..97fdaf6547a 100644 --- a/test/unit/util/actor.test.js +++ b/test/unit/util/actor.test.js @@ -14,8 +14,8 @@ test('Actor', (t) => { const worker = new WebWorker(); - const m1 = new Actor(worker, {}, 'map-1'); - const m2 = new Actor(worker, {}, 'map-2'); + const m1 = new Actor(worker, {}, 1); + const m2 = new Actor(worker, {}, 2); t.plan(4); m1.send('test', { value: 1729 }, (err, response) => { @@ -40,15 +40,15 @@ test('Actor', (t) => { new Actor(worker, { test () { t.end(); } - }, 'map-1'); + }, 1); new Actor(worker, { test () { t.fail(); t.end(); } - }, 'map-2'); + }, 2); - workerActor.send('test', {}, () => {}, 'map-1'); + workerActor.send('test', {}, () => {}, 1); }); t.test('#remove unbinds event listener', (t) => {