Skip to content

Commit 560a7a0

Browse files
stepankuzmingithub-actions[bot]
authored andcommitted
Add support for async transformRequest (internal-8378)
GitOrigin-RevId: 1c5576c0e0bffc0613a2eff6475e025aa9502d87
1 parent 263bd76 commit 560a7a0

32 files changed

Lines changed: 754 additions & 467 deletions

3d-style/render/model_manager.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,20 @@ class ModelManager extends Evented {
4545
this.numModelsLoading = Object.create(null) as ModelManager['numModelsLoading'];
4646
}
4747

48-
loadModel(id: string, url: string): Promise<Model | null | undefined> {
49-
return loadGLTF(this.requestManager.transformRequest(url, ResourceType.Model).url)
50-
.then(gltf => {
51-
const nodes = convertModel(gltf);
52-
const model = new Model(id, url, undefined, undefined, nodes);
53-
model.computeBoundsAndApplyParent();
54-
return model;
55-
})
56-
.catch((err) => {
57-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
58-
if (err && err.status === 404) {
59-
return null;
60-
}
61-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
62-
this.fire(new ErrorEvent(new Error(`Could not load model ${id} from ${url}: ${err.message}`)));
63-
});
48+
async loadModel(id: string, url: string): Promise<Model | null | undefined> {
49+
try {
50+
const request = await this.requestManager.transformRequest(url, ResourceType.Model);
51+
if (!this.modelByURL[url]) return null;
52+
53+
const gltf = await loadGLTF(request.url);
54+
const nodes = convertModel(gltf);
55+
const model = new Model(id, url, undefined, undefined, nodes);
56+
model.computeBoundsAndApplyParent();
57+
return model;
58+
} catch (e) {
59+
if ((e as {status?: number}).status === 404) return null;
60+
this.fire(new ErrorEvent(new Error(`Could not load model ${id} from ${url}`, {cause: e})));
61+
}
6462
}
6563

6664
load(modelUris: {

3d-style/source/model_source.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ class ModelSource extends Evented<SourceEvents> implements ISource {
9999
}
100100
}
101101

102-
private loadGLTFFromURI(uri: string, signal?: AbortSignal): Promise<GLTF> {
103-
return loadGLTF(this.map._requestManager.transformRequest(uri, ResourceType.Model).url, signal);
102+
private async loadGLTFFromURI(uri: string, signal?: AbortSignal): Promise<GLTF> {
103+
const request = await this.map._requestManager.transformRequest(uri, ResourceType.Model, signal);
104+
return loadGLTF(request.url, signal);
104105
}
105106

106107
private async loadModel(modelId: string, modelSpec: ModelSourceModelSpecification, signal: AbortSignal): Promise<void> {
@@ -120,9 +121,8 @@ class ModelSource extends Evented<SourceEvents> implements ISource {
120121
this.models.push(model);
121122
modelInfo.model = model;
122123
} catch (err: unknown) {
123-
if (err instanceof Error && err.name === 'AbortError') return;
124-
const message = err instanceof Error ? err.message : 'Unknown error';
125-
this.fire(new ErrorEvent(new Error(`Could not load model ${modelId} from ${modelSpec.uri}: ${message}`)));
124+
if (signal.aborted) return;
125+
this.fire(new ErrorEvent(new Error(`Could not load model ${modelId} from ${modelSpec.uri}`, {cause: err})));
126126
}
127127
}
128128

3d-style/source/tiled_3d_model_source.ts

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -137,43 +137,21 @@ class Tiled3DModelSource extends Evented<SourceEvents> implements ISource {
137137
return this._loaded;
138138
}
139139

140-
loadTile(tile: Tile, callback: Callback<undefined>) {
140+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
141+
async loadTile(tile: Tile, callback: Callback<undefined>): Promise<void> {
141142
const url = this.map._requestManager.normalizeTileURL(tile.tileID.canonical.url(this.tiles, this.scheme));
142-
const request = this.map._requestManager.transformRequest(url, ResourceType.Tile);
143-
const params: WorkerSourceTiled3dModelRequest = {
144-
request,
145-
data: undefined,
146-
uid: tile.uid,
147-
tileID: tile.tileID,
148-
tileZoom: tile.tileZoom,
149-
zoom: tile.tileID.overscaledZ,
150-
tileSize: this.tileSize * tile.tileID.overscaleFactor(),
151-
type: this.type,
152-
source: this.id,
153-
scope: this.scope,
154-
showCollisionBoxes: this.map.showCollisionBoxes,
155-
renderSourceType: tile.renderSourceType,
156-
brightness: this.map.style ? (this.map.style.getBrightness() || 0.0) : 0.0,
157-
pixelRatio: browser.devicePixelRatio,
158-
promoteId: this.promoteId,
159-
};
160-
161-
const done = (err?: AJAXError | null, data?: WorkerSourceVectorTileResult | null) => {
162-
if (tile.aborted) return callback(null);
163-
if (err && !isHttpNotFound(err)) return callback(err);
164-
if (this.map._refreshExpiredTiles && data) tile.setExpiryData(parseExpiryData(data.headers));
165-
tile.loadModelData(data, this.map.painter);
166-
tile.state = 'loaded';
167-
callback(null);
168-
};
169143

170-
if (!tile.actor || tile.state === 'expired') {
144+
// The actor/state branch stays synchronous so a re-entrant loadTile dedupes correctly.
145+
const isFresh = !tile.actor || tile.state === 'expired';
146+
if (isFresh) {
171147
tile.actor = this.dispatcher.getActor();
172-
tile.request = tile.actor.sendCancelable('loadTile', params, {}, done);
173-
} else if (tile.state === 'loading') {
174-
// schedule tile reloading after it has been loaded
175-
tile.reloadCallback = callback;
176148
} else {
149+
if (tile.state === 'loading') {
150+
// schedule tile reloading after it has been loaded
151+
tile.reloadCallback = callback;
152+
return;
153+
}
154+
177155
// If the tile has already been parsed we may just need to reevaluate
178156
if (tile.buckets) {
179157
const buckets = Object.values(tile.buckets) as Tiled3dModelBucket[];
@@ -183,8 +161,54 @@ class Tiled3DModelSource extends Evented<SourceEvents> implements ISource {
183161
tile.state = 'loaded';
184162
return;
185163
}
164+
}
165+
166+
const messageType = isFresh ? 'loadTile' : 'reloadTile';
167+
const controller = new AbortController();
168+
tile.request = controller;
169+
170+
const done = (err?: AJAXError | null, data?: WorkerSourceVectorTileResult | null) => {
171+
delete tile.request;
172+
if (tile.aborted) return callback(null);
173+
if (err && !isHttpNotFound(err)) return callback(err);
174+
if (this.map._refreshExpiredTiles && data) tile.setExpiryData(parseExpiryData(data.headers));
175+
tile.loadModelData(data, this.map.painter);
176+
tile.state = 'loaded';
177+
callback(null);
178+
179+
if (tile.reloadCallback) {
180+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
181+
this.loadTile(tile, tile.reloadCallback);
182+
tile.reloadCallback = null;
183+
}
184+
};
186185

187-
tile.request = tile.actor.sendCancelable('reloadTile', params, {}, done);
186+
try {
187+
const request = await this.map._requestManager.transformRequest(url, ResourceType.Tile, controller.signal);
188+
if (controller.signal.aborted) return callback(null);
189+
190+
const params: WorkerSourceTiled3dModelRequest = {
191+
request,
192+
data: undefined,
193+
uid: tile.uid,
194+
tileID: tile.tileID,
195+
tileZoom: tile.tileZoom,
196+
zoom: tile.tileID.overscaledZ,
197+
tileSize: this.tileSize * tile.tileID.overscaleFactor(),
198+
type: this.type,
199+
source: this.id,
200+
scope: this.scope,
201+
showCollisionBoxes: this.map.showCollisionBoxes,
202+
renderSourceType: tile.renderSourceType,
203+
brightness: this.map.style ? (this.map.style.getBrightness() || 0.0) : 0.0,
204+
pixelRatio: browser.devicePixelRatio,
205+
promoteId: this.promoteId,
206+
};
207+
208+
tile.request = tile.actor.sendCancelable(messageType, params, {}, done);
209+
} catch (err) {
210+
if (controller.signal.aborted) return callback(null);
211+
callback(err as Error);
188212
}
189213
}
190214

debug/session-tokens.html

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Mapbox GL JS debug page</title>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
7+
<link rel="stylesheet" href="../dist/mapbox-gl.css" />
8+
<style>
9+
body { margin: 0; padding: 0; }
10+
html, body, #map { height: 100%; }
11+
</style>
12+
</head>
13+
14+
<body>
15+
<div id="map"></div>
16+
17+
<script type="module">
18+
import * as mapboxgl from '../dist/esm-dev/mapbox-gl.js';
19+
import {getAccessToken} from './access_token_generated.js';
20+
21+
let session = null;
22+
function authenticate(signal) {
23+
if (signal) {
24+
signal.addEventListener('abort', () => console.log('request aborted'), {once: true});
25+
}
26+
27+
const now = Math.floor(Date.now() / 1000);
28+
if (!session || now >= session.exp) {
29+
session = {token: Date.now().toString(), exp: now + 30};
30+
}
31+
32+
return session;
33+
}
34+
35+
const map = window.map = new mapboxgl.Map({
36+
accessToken: getAccessToken(),
37+
container: 'map',
38+
hash: true,
39+
zoom: 12.5,
40+
center: [-122.4194, 37.7749],
41+
transformRequest: async (url, resourceType, {signal}) => {
42+
const {token} = await authenticate(signal);
43+
44+
const u = new URL(url);
45+
u.searchParams.set('ts', token);
46+
47+
return {url: u.toString()};
48+
}
49+
});
50+
51+
</script>
52+
</body>
53+
</html>

src/source/canvas_source.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ class CanvasSource extends ImageSource<'canvas'> {
111111
* @memberof CanvasSource
112112
*/
113113

114-
override load() {
114+
// eslint-disable-next-line @typescript-eslint/require-await
115+
override async load() {
115116
this._loaded = true;
116117
if (!this.canvas) {
117118
this.canvas = (this.options.canvas instanceof HTMLCanvasElement) ?
@@ -166,6 +167,7 @@ class CanvasSource extends ImageSource<'canvas'> {
166167

167168
override onAdd(map: Map) {
168169
this.map = map;
170+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
169171
this.load();
170172
if (this.canvas) {
171173
if (this.animate) this.play();

src/source/custom_source.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import type Dispatcher from '../util/dispatcher';
1010
import type {Callback} from '../types/callback';
1111
import type {OverscaledTileID} from './tile_id';
1212
import type {ISource, SourceEvents} from './source';
13-
import type {AJAXError} from '../util/ajax';
1413
import type {TextureImage} from '../render/texture';
1514

1615
type DataType = 'raster';
@@ -261,22 +260,14 @@ class CustomSource<T> extends Evented<SourceEvents> implements ISource {
261260
return !this.tileBounds || this.tileBounds.contains(tileID.canonical);
262261
}
263262

264-
loadTile(tile: Tile, callback: Callback<undefined>): void {
263+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
264+
async loadTile(tile: Tile, callback: Callback<undefined>): Promise<void> {
265265
const {x, y, z} = tile.tileID.canonical;
266266
const controller = new AbortController();
267267
tile.request = controller;
268268

269-
Promise
270-
.resolve(this._implementation.loadTile({x, y, z}, {signal: controller.signal}))
271-
.then(tileLoaded.bind(this))
272-
.catch((error?: Error | DOMException | AJAXError) => {
273-
// silence AbortError
274-
if (error.name === 'AbortError') return;
275-
tile.state = 'errored';
276-
callback(error);
277-
});
278-
279-
function tileLoaded(this: CustomSource<T>, data?: T | null) {
269+
try {
270+
const data = await this._implementation.loadTile({x, y, z}, {signal: controller.signal});
280271
delete tile.request;
281272

282273
if (tile.aborted) {
@@ -310,6 +301,10 @@ class CustomSource<T> extends Evented<SourceEvents> implements ISource {
310301
this.loadTileData(tile, data);
311302
tile.state = 'loaded';
312303
callback(null);
304+
} catch (error) {
305+
if (controller.signal.aborted) return;
306+
tile.state = 'errored';
307+
callback(error as Error);
313308
}
314309
}
315310

src/source/geojson_source.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -402,38 +402,42 @@ class GeoJSONSource extends Evented<SourceEvents> implements ISource {
402402

403403
options.scope = this.scope;
404404
const data = this._data;
405-
if (typeof data === 'string') {
406-
options.request = this.map._requestManager.transformRequest(browser.resolveURL(data), ResourceType.Source);
407-
options.request.collectResourceTiming = this._collectResourceTiming;
408-
} else {
409-
options.data = JSON.stringify(data);
410-
}
411405

412-
// target {this.type}.loadData rather than literally geojson.loadData,
413-
// so that other geojson-like source types can easily reuse this
414-
// implementation
406+
// Register _pendingLoad before any await, so a same-tick _updateWorkerData hits the coalesce guard.
415407
const controller = new AbortController();
416408
this._pendingLoad = controller;
417409

418410
try {
411+
if (typeof data === 'string') {
412+
options.request = await this.map._requestManager.transformRequest(browser.resolveURL(data), ResourceType.Source, controller.signal);
413+
if (controller.signal.aborted) {
414+
this._pendingLoad = null;
415+
this._coalesce = false;
416+
return;
417+
}
418+
options.request.collectResourceTiming = this._collectResourceTiming;
419+
} else {
420+
options.data = JSON.stringify(data);
421+
}
422+
419423
// The runtime type is `${this.type}.loadData` so geojson-like source types can reuse
420424
// this path, but every variant returns a LoadGeoJSONResult-shaped reply.
421425
const result = await this.actor.send(`${this.type}.loadData` as 'geojson.loadData', options, {signal: controller.signal});
422426
this._loaded = true;
423427
this._pendingLoad = null;
424428
// although GeoJSON sources contain no metadata, we fire this event at first
425429
// to let the SourceCache know its ok to start requesting tiles.
426-
const data: MapSourceDataEvent = {dataType: 'source', sourceDataType: this._metadataFired ? 'content' : 'metadata'};
430+
const dataEvent: MapSourceDataEvent = {dataType: 'source', sourceDataType: this._metadataFired ? 'content' : 'metadata'};
427431
const geojsonResult = result;
428432
if (this._collectResourceTiming && geojsonResult && geojsonResult.resourceTiming && geojsonResult.resourceTiming[this.id]) {
429-
data.resourceTiming = geojsonResult.resourceTiming[this.id];
433+
dataEvent.resourceTiming = geojsonResult.resourceTiming[this.id];
430434
}
431435
if (append) this._partialReload = true;
432-
this.fire(new Event('data', data));
436+
this.fire(new Event('data', dataEvent));
433437
this._partialReload = false;
434438
this._metadataFired = true;
435439
} catch (err) {
436-
if ((err as Error).name === 'AbortError') {
440+
if (controller.signal.aborted) {
437441
this._pendingLoad = null;
438442
// the load was cancelled (e.g. source removed); drop any queued coalesced update
439443
this._coalesce = false;

0 commit comments

Comments
 (0)