Skip to content

Commit 1ab71b1

Browse files
authored
Cancel pending GeoJSON requests when GeoJSONSource.setData() is called (#1102)
* Cancel pending GeoJSON requests when GeoJSONSource.setData() is called * Add changelog entry * Improve unit tests * Add link to PR in changelog entry
1 parent b287640 commit 1ab71b1

6 files changed

Lines changed: 69 additions & 105 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### ✨ Features and improvements
44

5+
- Cancel pending GeoJSON requests when `GeoJSONSource.setData()` is called instead of waiting for any pending request to complete before issuing the request for the new URL (#1102)
56
- *...Add new stuff here...*
67

78
### 🐞 Bug fixes

src/source/geojson_source.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,18 @@ describe('GeoJSONSource#setData', () => {
9090
source.load();
9191
});
9292

93+
test('fires "dataabort" event', done => {
94+
const source = new GeoJSONSource('id', {} as any, wrapDispatcher({
95+
send(type, data, callback) {
96+
setTimeout(() => callback(null, {abandoned: true}));
97+
}
98+
}), undefined);
99+
source.on('dataabort', () => {
100+
done();
101+
});
102+
source.load();
103+
});
104+
93105
test('respects collectResourceTiming parameter on source', done => {
94106
const source = createSource({collectResourceTiming: true});
95107
source.map = {
@@ -137,6 +149,19 @@ describe('GeoJSONSource#setData', () => {
137149
});
138150
source.setData({} as GeoJSON.GeoJSON);
139151
});
152+
153+
test('marks source as loaded before firing "dataabort" event', done => {
154+
const source = new GeoJSONSource('id', {} as any, wrapDispatcher({
155+
send(type, data, callback) {
156+
setTimeout(() => callback(null, {abandoned: true}));
157+
}
158+
}), undefined);
159+
source.on('dataabort', () => {
160+
expect(source.loaded()).toBeTruthy();
161+
done();
162+
});
163+
source.setData({} as GeoJSON.GeoJSON);
164+
});
140165
});
141166

142167
describe('GeoJSONSource#onRemove', () => {

src/source/geojson_source.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -256,20 +256,13 @@ class GeoJSONSource extends Evented implements Source {
256256
this._pendingLoads--;
257257

258258
if (this._removed || (result && result.abandoned)) {
259+
this.fire(new Event('dataabort', {dataType: 'source', sourceDataType}));
259260
return;
260261
}
261262

262263
let resourceTiming = null;
263264
if (result && result.resourceTiming && result.resourceTiming[this.id])
264265
resourceTiming = result.resourceTiming[this.id].slice(0);
265-
// Any `loadData` calls that piled up while we were processing
266-
// this one will get coalesced into a single call when this
267-
// 'coalesce' message is processed.
268-
// We would self-send from the worker if we had access to its
269-
// message queue. Waiting instead for the 'coalesce' to round-trip
270-
// through the foreground just means we're throttling the worker
271-
// to run at a little less than full-throttle.
272-
this.actor.send(`${this.type}.coalesce`, {source: options.source}, null);
273266

274267
if (err) {
275268
this.fire(new ErrorEvent(err));

src/source/geojson_worker_source.test.ts

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ describe('reloadTile', () => {
4646

4747
function addData(callback) {
4848
source.loadData({source: 'sourceId', data: JSON.stringify(geoJson)} as LoadGeoJSONParameters, (err) => {
49-
source.coalesce();
5049
expect(err).toBeNull();
5150
callback();
5251
});
@@ -134,7 +133,10 @@ describe('resourceTiming', () => {
134133
window.performance.getEntriesByName = jest.fn().mockReturnValue([ exampleResourceTiming ]);
135134

136135
const layerIndex = new StyleLayerIndex(layers);
137-
const source = new GeoJSONWorkerSource(actor, layerIndex, [], (params, callback) => { return callback(null, geoJson); });
136+
const source = new GeoJSONWorkerSource(actor, layerIndex, [], (params, callback) => {
137+
callback(null, geoJson);
138+
return {cancel: () => {}};
139+
});
138140

139141
source.loadData({source: 'testSource', request: {url: 'http://localhost/nonexistent', collectResourceTiming: true}} as LoadGeoJSONParameters, (err, result) => {
140142
expect(err).toBeNull();
@@ -166,7 +168,10 @@ describe('resourceTiming', () => {
166168
jest.spyOn(perf, 'clearMeasures').mockImplementation(() => { return null; });
167169

168170
const layerIndex = new StyleLayerIndex(layers);
169-
const source = new GeoJSONWorkerSource(actor, layerIndex, [], (params, callback) => { return callback(null, geoJson); });
171+
const source = new GeoJSONWorkerSource(actor, layerIndex, [], (params, callback) => {
172+
callback(null, geoJson);
173+
return {cancel: () => {}};
174+
});
170175

171176
source.loadData({source: 'testSource', request: {url: 'http://localhost/nonexistent', collectResourceTiming: true}} as LoadGeoJSONParameters, (err, result) => {
172177
expect(err).toBeNull();
@@ -221,55 +226,46 @@ describe('loadData', () => {
221226
// (regardless of timing)
222227
const originalLoadGeoJSON = worker.loadGeoJSON;
223228
worker.loadGeoJSON = function(params, callback) {
224-
setTimeout(() => {
229+
const timeout = setTimeout(() => {
225230
originalLoadGeoJSON(params, callback);
226231
}, 0);
232+
233+
return {cancel: () => clearTimeout(timeout)};
227234
};
228235
return worker;
229236
}
230237

231-
test('abandons coalesced callbacks', done => {
232-
// Expect first call to run, second to be abandoned,
233-
// and third to run in response to coalesce
238+
test('abandons previous callbacks', done => {
234239
const worker = createWorker();
235-
worker.loadData({source: 'source1', data: JSON.stringify(geoJson)} as LoadGeoJSONParameters, (err, result) => {
236-
expect(err).toBeNull();
237-
expect(result && result.abandoned).toBeFalsy();
238-
worker.coalesce();
239-
});
240+
let firstCallbackHasRun = false;
240241

241242
worker.loadData({source: 'source1', data: JSON.stringify(geoJson)} as LoadGeoJSONParameters, (err, result) => {
242243
expect(err).toBeNull();
243244
expect(result && result.abandoned).toBeTruthy();
245+
firstCallbackHasRun = true;
244246
});
245247

246248
worker.loadData({source: 'source1', data: JSON.stringify(geoJson)} as LoadGeoJSONParameters, (err, result) => {
247249
expect(err).toBeNull();
248250
expect(result && result.abandoned).toBeFalsy();
251+
expect(firstCallbackHasRun).toBeTruthy();
249252
done();
250253
});
251254
});
252255

253256
test('removeSource aborts callbacks', done => {
254-
// Expect:
255-
// First loadData starts running before removeSource arrives
256-
// Second loadData is pending when removeSource arrives, gets cancelled
257-
// removeSource is executed immediately
258-
// First loadData finishes running, sends results back to foreground
259257
const worker = createWorker();
260-
worker.loadData({source: 'source1', data: JSON.stringify(geoJson)} as LoadGeoJSONParameters, (err, result) => {
261-
expect(err).toBeNull();
262-
expect(result && result.abandoned).toBeFalsy();
263-
done();
264-
});
265-
258+
let loadDataCallbackHasRun = false;
266259
worker.loadData({source: 'source1', data: JSON.stringify(geoJson)} as LoadGeoJSONParameters, (err, result) => {
267260
expect(err).toBeNull();
268261
expect(result && result.abandoned).toBeTruthy();
262+
loadDataCallbackHasRun = true;
269263
});
270264

271265
worker.removeSource({source: 'source1'}, (err) => {
272266
expect(err).toBeFalsy();
267+
expect(loadDataCallbackHasRun).toBeTruthy();
268+
done();
273269
});
274270

275271
});

src/source/geojson_worker_source.ts

Lines changed: 19 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type StyleLayerIndex from '../style/style_layer_index';
2121
import type {LoadVectorDataCallback} from './vector_tile_worker_source';
2222
import type {RequestParameters, ResponseCallback} from '../util/ajax';
2323
import type {Callback} from '../types/callback';
24+
import type {Cancelable} from '../types/cancelable';
2425

2526
export type LoadGeoJSONParameters = {
2627
request?: RequestParameters;
@@ -33,7 +34,7 @@ export type LoadGeoJSONParameters = {
3334
filter?: Array<unknown>;
3435
};
3536

36-
export type LoadGeoJSON = (params: LoadGeoJSONParameters, callback: ResponseCallback<any>) => void;
37+
export type LoadGeoJSON = (params: LoadGeoJSONParameters, callback: ResponseCallback<any>) => Cancelable;
3738

3839
export interface GeoJSONIndex {
3940
getTile(z: number, x: number, y: number): any;
@@ -72,10 +73,6 @@ function loadGeoJSONTile(params: WorkerTileParameters, callback: LoadVectorDataC
7273
});
7374
}
7475

75-
export type SourceState = // Source empty or data loaded
76-
'Idle' | // Data finished loading, but discard 'loadData' messages until receiving 'coalesced'
77-
'Coalescing' | 'NeedsLoadData'; // 'loadData' received while coalescing, trigger one more 'loadData' on receiving 'coalesced'
78-
7976
/**
8077
* The {@link WorkerSource} implementation that supports {@link GeoJSONSource}.
8178
* This class is designed to be easily reused to support custom source types
@@ -87,12 +84,11 @@ export type SourceState = // Source empty or data loaded
8784
* @private
8885
*/
8986
class GeoJSONWorkerSource extends VectorTileWorkerSource {
90-
_state: SourceState;
9187
_pendingCallback: Callback<{
9288
resourceTiming?: {[_: string]: Array<PerformanceResourceTiming>};
9389
abandoned?: boolean;
9490
}>;
95-
_pendingLoadDataParams: LoadGeoJSONParameters;
91+
_pendingRequest: Cancelable;
9692
_geoJSONIndex: GeoJSONIndex;
9793

9894
/**
@@ -117,9 +113,8 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource {
117113
* expecting `callback(error, data)` to be called with either an error or a
118114
* parsed GeoJSON object.
119115
*
120-
* When `loadData` requests come in faster than they can be processed,
121-
* they are coalesced into a single request using the latest data.
122-
* See {@link GeoJSONWorkerSource#coalesce}
116+
* When a `loadData` request comes in while a previous one is being processed,
117+
* the previous one is aborted.
123118
*
124119
* @param params
125120
* @param callback
@@ -129,40 +124,20 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource {
129124
resourceTiming?: {[_: string]: Array<PerformanceResourceTiming>};
130125
abandoned?: boolean;
131126
}>) {
127+
this._pendingRequest?.cancel();
132128
if (this._pendingCallback) {
133129
// Tell the foreground the previous call has been abandoned
134130
this._pendingCallback(null, {abandoned: true});
135131
}
136-
this._pendingCallback = callback;
137-
this._pendingLoadDataParams = params;
138-
139-
if (this._state &&
140-
this._state !== 'Idle') {
141-
this._state = 'NeedsLoadData';
142-
} else {
143-
this._state = 'Coalescing';
144-
this._loadData();
145-
}
146-
}
147-
148-
/**
149-
* Internal implementation: called directly by `loadData`
150-
* or by `coalesce` using stored parameters.
151-
*/
152-
_loadData() {
153-
if (!this._pendingCallback || !this._pendingLoadDataParams) {
154-
assert(false);
155-
return;
156-
}
157-
const callback = this._pendingCallback;
158-
const params = this._pendingLoadDataParams;
159-
delete this._pendingCallback;
160-
delete this._pendingLoadDataParams;
161132

162133
const perf = (params && params.request && params.request.collectResourceTiming) ?
163134
new RequestPerformance(params.request) : false;
164135

165-
this.loadGeoJSON(params, (err?: Error | null, data?: any | null) => {
136+
this._pendingCallback = callback;
137+
this._pendingRequest = this.loadGeoJSON(params, (err?: Error | null, data?: any | null) => {
138+
delete this._pendingCallback;
139+
delete this._pendingRequest;
140+
166141
if (err || !data) {
167142
return callback(err);
168143
} else if (typeof data !== 'object') {
@@ -204,35 +179,6 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource {
204179
});
205180
}
206181

207-
/**
208-
* While processing `loadData`, we coalesce all further
209-
* `loadData` messages into a single call to _loadData
210-
* that will happen once we've finished processing the
211-
* first message. {@link GeoJSONSource#_updateWorkerData}
212-
* is responsible for sending us the `coalesce` message
213-
* at the time it receives a response from `loadData`
214-
*
215-
* State: Idle
216-
* ↑ |
217-
* 'coalesce' 'loadData'
218-
* | (triggers load)
219-
* | ↓
220-
* State: Coalescing
221-
* ↑ |
222-
* (triggers load) |
223-
* 'coalesce' 'loadData'
224-
* | ↓
225-
* State: NeedsLoadData
226-
*/
227-
coalesce() {
228-
if (this._state === 'Coalescing') {
229-
this._state = 'Idle';
230-
} else if (this._state === 'NeedsLoadData') {
231-
this._state = 'Coalescing';
232-
this._loadData();
233-
}
234-
}
235-
236182
/**
237183
* Implements {@link WorkerSource#reloadTile}.
238184
*
@@ -264,24 +210,27 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource {
264210
* @param params
265211
* @param [params.url] A URL to the remote GeoJSON data.
266212
* @param [params.data] Literal GeoJSON data. Must be provided if `params.url` is not.
213+
* @returns {Cancelable} A Cancelable object.
267214
* @private
268215
*/
269-
loadGeoJSON(params: LoadGeoJSONParameters, callback: ResponseCallback<any>) {
216+
loadGeoJSON(params: LoadGeoJSONParameters, callback: ResponseCallback<any>): Cancelable {
270217
// Because of same origin issues, urls must either include an explicit
271218
// origin or absolute path.
272219
// ie: /foo/bar.json or http://example.com/bar.json
273220
// but not ../foo/bar.json
274221
if (params.request) {
275-
getJSON(params.request, callback);
222+
return getJSON(params.request, callback);
276223
} else if (typeof params.data === 'string') {
277224
try {
278-
return callback(null, JSON.parse(params.data));
225+
callback(null, JSON.parse(params.data));
279226
} catch (e) {
280-
return callback(new Error(`Input data given to '${params.source}' is not a valid GeoJSON object.`));
227+
callback(new Error(`Input data given to '${params.source}' is not a valid GeoJSON object.`));
281228
}
282229
} else {
283-
return callback(new Error(`Input data given to '${params.source}' is not a valid GeoJSON object.`));
230+
callback(new Error(`Input data given to '${params.source}' is not a valid GeoJSON object.`));
284231
}
232+
233+
return {cancel: () => {}};
285234
}
286235

287236
removeSource(params: {

src/ui/events.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,7 +1395,7 @@ export type MapEvent =
13951395
| 'style.load'
13961396

13971397
/**
1398-
* Fired when a request for one of the map's sources' tiles is aborted.
1398+
* Fired when a request for one of the map's sources' data is aborted.
13991399
* See {@link MapDataEvent} for more information.
14001400
*
14011401
* @event dataabort
@@ -1406,15 +1406,15 @@ export type MapEvent =
14061406
* // Initialize the map
14071407
* var map = new maplibregl.Map({ // map options });
14081408
* // Set an event listener that fires
1409-
* // when a request for one of the map's sources' tiles is aborted.
1409+
* // when a request for one of the map's sources' data is aborted.
14101410
* map.on('dataabort', function() {
14111411
* console.log('A dataabort event occurred.');
14121412
* });
14131413
*/
14141414
| 'dataabort'
14151415

14161416
/**
1417-
* Fired when a request for one of the map's sources' tiles is aborted.
1417+
* Fired when a request for one of the map's sources' data is aborted.
14181418
* See {@link MapDataEvent} for more information.
14191419
*
14201420
* @event sourcedataabort
@@ -1425,7 +1425,7 @@ export type MapEvent =
14251425
* // Initialize the map
14261426
* var map = new maplibregl.Map({ // map options });
14271427
* // Set an event listener that fires
1428-
* // when a request for one of the map's sources' tiles is aborted.
1428+
* // when a request for one of the map's sources' data is aborted.
14291429
* map.on('sourcedataabort', function() {
14301430
* console.log('A sourcedataabort event occurred.');
14311431
* });

0 commit comments

Comments
 (0)