diff --git a/src/source/geojson_source.js b/src/source/geojson_source.js index d99d1e4377b..a11cfc16607 100644 --- a/src/source/geojson_source.js +++ b/src/source/geojson_source.js @@ -183,6 +183,14 @@ class GeoJSONSource extends Evented implements Source { // implementation this.workerID = this.dispatcher.send(`${this.type}.loadData`, options, (err) => { this._loaded = true; + // Any `loadData` calls that piled up while we were processing + // this one will get coalesced into a single call when this + // 'coalesce' message is processed. + // We would self-send from the worker if we had access to its + // message queue. Waiting instated 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`, null, null, this.workerID); callback(err); }, this.workerID); } diff --git a/src/source/geojson_worker_source.js b/src/source/geojson_worker_source.js index 36646e9e891..e5f59883075 100644 --- a/src/source/geojson_worker_source.js +++ b/src/source/geojson_worker_source.js @@ -66,6 +66,11 @@ function loadGeoJSONTile(params: WorkerTileParameters, callback: LoadVectorDataC }); } +export type SourceState = + | 'Idle' // Source empty or data loaded + | 'Coalescing' // Data finished loading, but discard 'loadData' messages until receiving 'coalesced' + | 'NeedsLoadData'; // 'loadData' received while coalescing, trigger one more 'loadData' on receiving 'coalesced' + /** * The {@link WorkerSource} implementation that supports {@link GeoJSONSource}. * This class is designed to be easily reused to support custom source types @@ -79,6 +84,9 @@ function loadGeoJSONTile(params: WorkerTileParameters, callback: LoadVectorDataC class GeoJSONWorkerSource extends VectorTileWorkerSource { _geoJSONIndexes: { [string]: GeoJSONIndex }; loadGeoJSON: LoadGeoJSON; + state: SourceState; + pendingCallback: Callback; + pendingLoadDataParams: LoadGeoJSONParameters; /** * @param [loadGeoJSON] Optional method for custom loading/parsing of @@ -92,6 +100,7 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource { } // object mapping source ids to geojson-vt-like tile indexes this._geoJSONIndexes = {}; + this.state = 'Idle'; } /** @@ -102,11 +111,35 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource { * Defers to {@link GeoJSONWorkerSource#loadGeoJSON} for the fetching/parsing, * expecting `callback(error, data)` to be called with either an error or a * parsed GeoJSON object. + * + * When `loadData` requests come in faster than they can be processed, + * they are coalesced into a single request using the latest data. + * See {@link GeoJSONWorkerSource#coalesce} + * * @param params * @param params.source The id of the source. * @param callback */ loadData(params: LoadGeoJSONParameters, callback: Callback) { + this.pendingCallback = callback; + this.pendingLoadDataParams = params; + if (this.state !== 'Idle') { + this.state = 'NeedsLoadData'; + } else { + this.state = 'Coalescing'; + this._loadData(); + } + } + + /** + * Internal implementation: called directly by `loadData` + * or by `coalesce` using stored parameters. + */ + _loadData() { + const callback = this.pendingCallback; + const params = this.pendingLoadDataParams; + delete this.pendingCallback; + delete this.pendingLoadDataParams; this.loadGeoJSON(params, (err, data) => { if (err || !data) { return callback(err); @@ -129,6 +162,35 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource { }); } + /** + * While processing `loadData`, we coalesce all further + * `loadData` messages into a single call to _loadData + * that will happen once we've finished processing the + * first message. {@link GeoJSONSource#_updateWorkerData} + * is responsible for sending us the `coalesce` message + * at the time it receives a response from `loadData` + * + * State: Idle + * ↑ | + * 'coalesce' 'loadData' + * | (triggers load) + * | ↓ + * State: Coalescing + * ↑ | + * (triggers load) | + * 'coalesce' 'loadData' + * | ↓ + * State: NeedsLoadData + */ + coalesce() { + if (this.state === 'Coalescing') { + this.state = 'Idle'; + } else if (this.state === 'NeedsLoadData') { + this.state = 'Coalescing'; + this._loadData(); + } + } + /** * Implements {@link WorkerSource#reloadTile}. * diff --git a/test/unit/source/geojson_source.test.js b/test/unit/source/geojson_source.test.js index a0c75863451..4bfe0039220 100644 --- a/test/unit/source/geojson_source.test.js +++ b/test/unit/source/geojson_source.test.js @@ -49,7 +49,9 @@ test('GeoJSONSource#setData', (t) => { function createSource() { return new GeoJSONSource('id', {data: {}}, { send: function (type, data, callback) { - return setTimeout(callback, 0); + if (callback) { + return setTimeout(callback, 0); + } } }); } @@ -153,7 +155,9 @@ test('GeoJSONSource#update', (t) => { t.test('fires event when metadata loads', (t) => { const mockDispatcher = { send: function(message, args, callback) { - setTimeout(callback, 0); + if (callback) { + setTimeout(callback, 0); + } } }; @@ -169,7 +173,9 @@ test('GeoJSONSource#update', (t) => { t.test('fires "error"', (t) => { const mockDispatcher = { send: function(message, args, callback) { - setTimeout(callback.bind(null, 'error'), 0); + if (callback) { + setTimeout(callback.bind(null, 'error'), 0); + } } }; @@ -190,7 +196,9 @@ test('GeoJSONSource#update', (t) => { if (message === 'geojson.loadData' && --expectedLoadDataCalls <= 0) { t.end(); } - setTimeout(callback, 0); + if (callback) { + setTimeout(callback, 0); + } } }; diff --git a/test/unit/source/geojson_worker_source.test.js b/test/unit/source/geojson_worker_source.test.js index dce1669b9f0..d74d6da9487 100644 --- a/test/unit/source/geojson_worker_source.test.js +++ b/test/unit/source/geojson_worker_source.test.js @@ -86,6 +86,7 @@ test('reloadTile', (t) => { function addData(callback) { source.loadData({ source: 'sourceId', data: JSON.stringify(geoJson) }, (err) => { + source.coalesce(); t.equal(err, null); callback(); });