Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async Actor #3233

Merged
merged 48 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
495f45d
Initial concept
HarelM Oct 18, 2023
3c368b0
More improvements
HarelM Oct 20, 2023
f850217
Fix lint
HarelM Oct 20, 2023
1cef230
Main commit to move most of the actor code to use promises and regist…
HarelM Oct 21, 2023
ba3831a
Fix actor tests
HarelM Oct 22, 2023
b485c9b
Remove more any when it comes to worker self typings.
HarelM Oct 22, 2023
0dca144
Improve more types, add more tests
HarelM Oct 22, 2023
931e11b
Improve more types, add more tests
HarelM Oct 22, 2023
7f983e1
Fix lint
HarelM Oct 22, 2023
614524c
Fix more tests
HarelM Oct 22, 2023
4292d0a
Fix tests
HarelM Oct 22, 2023
5734e5e
Fix some sytle tests
HarelM Oct 22, 2023
af2aabb
Add comment for todo.
HarelM Oct 22, 2023
210c425
Fix test related to message parameters change
HarelM Oct 23, 2023
746fe2d
Fix remaining tests
HarelM Oct 23, 2023
8d19b99
Added todo to accomodate for code review.
HarelM Oct 23, 2023
35a58bd
Fix build test
HarelM Oct 23, 2023
62a6a4a
Merge branch 'main' into async-actor
HarelM Oct 23, 2023
9451485
Fix geojson types
HarelM Oct 23, 2023
af73f12
Rename some events, remove clutter a bit
HarelM Oct 23, 2023
6b2c265
Fix issue with multiple maps found in integration tests
HarelM Oct 23, 2023
7a1c9a8
Fix tests and improve types and docs a bit more
HarelM Oct 23, 2023
a18507b
Final fixes to make the tests pass
HarelM Oct 23, 2023
b92e486
Fix build test
HarelM Oct 23, 2023
3a2fdfc
a couple of tweaks (#3267)
msbarry Oct 24, 2023
a365145
Merge remote-tracking branch 'origin/main' into async-actor
HarelM Oct 24, 2023
712cf53
Merge branch 'main' into async-actor
HarelM Oct 24, 2023
fe8e847
Add changelog item
HarelM Oct 24, 2023
23db865
More improvement to async `Actor` and replace more callbacks with pro…
HarelM Oct 25, 2023
b045e38
Replace getJson call with async code and remove more callbacks (#3273)
HarelM Oct 27, 2023
2893ab3
Move image queue to use promises, move getArrayBuffer to use promises…
HarelM Oct 30, 2023
8259409
Remove TODOs, tidy up last things (#3301)
HarelM Nov 2, 2023
837695c
Merge branch 'main' into async-actor
HarelM Nov 2, 2023
5e4cdde
Async actor docs additions (#3305)
HarelM Nov 7, 2023
3c374d6
Merge branch 'main' into async-actor
HarelM Nov 7, 2023
4513eb6
Improve docs for RTL plugin status and reduce code in geojson worker …
HarelM Nov 7, 2023
2a773bf
Last simplification to actor, simplify a style test.
HarelM Nov 7, 2023
001b7a8
Add missing docs comments
HarelM Nov 7, 2023
c63e731
Fix lint...
HarelM Nov 7, 2023
5b2b2bd
Fix Actor XSS, introduce subscribe (#3329)
HarelM Nov 10, 2023
4d99526
Fix developer diagram with updated flow
HarelM Nov 13, 2023
c944799
Merge branch 'main' into async-actor
HarelM Nov 14, 2023
da95a03
Async actor no log warnings and errors in unit tests (#3368)
HarelM Nov 15, 2023
9feae22
Async actor remove cancelables (#3371)
HarelM Nov 15, 2023
6c90f58
Merge branch 'main' into async-actor
HarelM Nov 22, 2023
78bb4a3
Async actor callback and promise (#3374)
HarelM Nov 22, 2023
40846ff
Merge branch 'main' into async-actor
HarelM Nov 23, 2023
3fbff23
Update changelog with most of the changes
HarelM Nov 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
## main

### ✨ Features and improvements

- Changed `ImageRequest` to be `Promise` based ([#3233](https://github.com/maplibre/maplibre-gl-js/pull/3233))
- ⚠️ Changed the undeling worker communication from callbacks to promises. This has a breaking effect on the implementation of custom `WorkerSource` and how it behaves ([#3233](https://github.com/maplibre/maplibre-gl-js/pull/3233))
- ⚠️ Changed the `Source` interface to return promises instead of callbacks ([#3233](https://github.com/maplibre/maplibre-gl-js/pull/3233))
- ⚠️ Changed all the sources to be promises based. ([#3233](https://github.com/maplibre/maplibre-gl-js/pull/3233))
- ⚠️ Changed the `map.loadImage` method to return a `Promise` ([#3233](https://github.com/maplibre/maplibre-gl-js/pull/3233))
- _...Add new stuff here..._

### 🐞 Bug fixes

- Fixes a security issue in `Actor` against XSS attacks in postMessage / onmessage ([#3239](https://github.com/maplibre/maplibre-gl-js/pull/3239))
- _...Add new stuff here..._

## 3.6.2
Expand Down
7 changes: 3 additions & 4 deletions build/generate-doc-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from 'path';
import fs from 'fs';
import puppeteer from 'puppeteer';
import packageJson from '../package.json' assert { type: 'json' };
import {sleep} from '../src/util/test/util';

const exampleName = process.argv[2];
const examplePath = path.resolve('test', 'examples');
Expand All @@ -28,10 +29,8 @@ async function createImage(exampleName) {
.then(async () => {
// Wait for 5 seconds on 3d model examples, since this takes longer to load.
const waitTime = exampleName.includes('3d-model') ? 5000 : 1500;
await new Promise((resolve) => {
console.log(`waiting for ${waitTime} ms`);
setTimeout(resolve, waitTime);
});
console.log(`waiting for ${waitTime} ms`);
await sleep(waitTime);
})
// map.loaded() does not evaluate to true within 3 seconds, it's probably an animated example.
// In this case we take the screenshot immediately.
Expand Down
24 changes: 12 additions & 12 deletions developer-guides/life-of-a-tile.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ sequenceDiagram
worker_tile->glyph manager: getImages
glyph manager->>ajax: Fetch icon<br>images
glyph manager-->>worker_tile: glyph/Image dependencies
worker_tile->>worker_tile: maybePrepare()
worker_tile->>worker_tile: wait for all requests to finish
worker_tile->>worker_tile: create GlyphAtlas
worker_tile->>worker_tile: create ImageAtlas
worker_tile->>bucket: addFeatures
Expand Down Expand Up @@ -169,19 +169,19 @@ sequenceDiagram
map->>painter: render(style)
painter->>source_cache: prepare(context)
loop for each tile
source_cache->>GPU: upload vertices
source_cache->>GPU: upload image textures
source_cache->>GPU: upload vertices
source_cache->>GPU: upload image textures
end
loop for each layer
painter->>layer: renderLayer(pass=offscreen)
painter->>layer: renderLayer(pass=opaque)
painter->>layer: renderLayer(pass=translucent)
painter->>layer: renderLayer(pass=debug)
loop renderLayer() call for each tile
layer->>GPU: load program
layer->>GPU: drawElements()
GPU->>user: display pixels
end
painter->>layer: renderLayer(pass=offscreen)
painter->>layer: renderLayer(pass=opaque)
painter->>layer: renderLayer(pass=translucent)
painter->>layer: renderLayer(pass=debug)
loop renderLayer() call for each tile
layer->>GPU: load program
layer->>GPU: drawElements()
GPU->>user: display pixels
end
end
map->>map: triggerRepaint()
```
Expand Down
39 changes: 26 additions & 13 deletions src/data/dem_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@ import {RGBAImage} from '../util/image';
import {warnOnce} from '../util/util';
import {register} from '../util/web_worker_transfer';

// DEMData is a data structure for decoding, backfilling, and storing elevation data for processing in the hillshade shaders
// data can be populated either from a pngraw image tile or from serliazed data sent back from a worker. When data is initially
// loaded from a image tile, we decode the pixel values using the appropriate decoding formula, but we store the
// elevation data as an Int32 value. we add 65536 (2^16) to eliminate negative values and enable the use of
// integer overflow when creating the texture used in the hillshadePrepare step.

// DEMData also handles the backfilling of data from a tile's neighboring tiles. This is necessary because we use a pixel's 8
// surrounding pixel values to compute the slope at that pixel, and we cannot accurately calculate the slope at pixels on a
// tile's edge without backfilling from neighboring tiles.

/**
* The possible DEM encoding types
*/
export type DEMEncoding = 'mapbox' | 'terrarium' | 'custom'

/**
* DEMData is a data structure for decoding, backfilling, and storing elevation data for processing in the hillshade shaders
* data can be populated either from a pngraw image tile or from serliazed data sent back from a worker. When data is initially
* loaded from a image tile, we decode the pixel values using the appropriate decoding formula, but we store the
* elevation data as an Int32 value. we add 65536 (2^16) to eliminate negative values and enable the use of
* integer overflow when creating the texture used in the hillshadePrepare step.
*
* DEMData also handles the backfilling of data from a tile's neighboring tiles. This is necessary because we use a pixel's 8
* surrounding pixel values to compute the slope at that pixel, and we cannot accurately calculate the slope at pixels on a
* tile's edge without backfilling from neighboring tiles.
*/
export class DEMData {
uid: string;
uid: string | number;
data: Uint32Array;
stride: number;
dim: number;
Expand All @@ -27,9 +31,18 @@ export class DEMData {
blueFactor: number;
baseShift: number;

// RGBAImage data has uniform 1px padding on all sides: square tile edge size defines stride
/**
* Constructs a `DEMData` object
* @param uid - the tile's unique id
* @param data - RGBAImage data has uniform 1px padding on all sides: square tile edge size defines stride
// and dim is calculated as stride - 2.
constructor(uid: string, data: RGBAImage, encoding: DEMEncoding, redFactor = 1.0, greenFactor = 1.0, blueFactor = 1.0, baseShift = 0.0) {
* @param encoding - the encoding type of the data
* @param redFactor - the red channel factor used to unpack the data, used for `custom` encoding only
* @param greenFactor - the green channel factor used to unpack the data, used for `custom` encoding only
* @param blueFactor - the blue channel factor used to unpack the data, used for `custom` encoding only
* @param baseShift - the base shift used to unpack the data, used for `custom` encoding only
*/
constructor(uid: string | number, data: RGBAImage | ImageData, encoding: DEMEncoding, redFactor = 1.0, greenFactor = 1.0, blueFactor = 1.0, baseShift = 0.0) {
this.uid = uid;
if (data.height !== data.width) throw new RangeError('DEM tiles must be square');
if (encoding && !['mapbox', 'terrarium', 'custom'].includes(encoding)) {
Expand Down
1 change: 0 additions & 1 deletion src/data/feature_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ type QueryParameters = {
};

/**
* @internal
* An in memory index class to allow fast interaction with features
*/
export class FeatureIndex {
Expand Down
68 changes: 33 additions & 35 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {config} from './util/config';
import maplibre from './index';
import {getJSON, getArrayBuffer} from './util/ajax';
import {ImageRequest} from './util/image_request';
import {isAbortError} from './util/abort_error';

describe('maplibre', () => {
beforeEach(() => {
Expand Down Expand Up @@ -34,65 +35,55 @@ describe('maplibre', () => {
expect(Object.keys(config.REGISTERED_PROTOCOLS)).toHaveLength(0);
});

test('#addProtocol - getJSON', done => {
test('#addProtocol - getJSON', async () => {
let protocolCallbackCalled = false;
maplibre.addProtocol('custom', (reqParam, callback) => {
protocolCallbackCalled = true;
callback(null, {'foo': 'bar'});
return {cancel: () => {}};
});
getJSON({url: 'custom://test/url/json'}, (error, data) => {
expect(error).toBeFalsy();
expect(data).toEqual({foo: 'bar'});
expect(protocolCallbackCalled).toBeTruthy();
done();
});
const response = await getJSON({url: 'custom://test/url/json'}, new AbortController());
expect(response.data).toEqual({foo: 'bar'});
expect(protocolCallbackCalled).toBeTruthy();
});

test('#addProtocol - getArrayBuffer', done => {
test('#addProtocol - getArrayBuffer', async () => {
let protocolCallbackCalled = false;
maplibre.addProtocol('custom', (reqParam, callback) => {
maplibre.addProtocol('custom', (_reqParam, callback) => {
protocolCallbackCalled = true;
callback(null, new ArrayBuffer(1));
callback(null, new ArrayBuffer(1), 'cache-control', 'expires');
return {cancel: () => {}};
});
getArrayBuffer({url: 'custom://test/url/getArrayBuffer'}, async (error, data) => {
expect(error).toBeFalsy();
expect(data).toBeInstanceOf(ArrayBuffer);
expect(protocolCallbackCalled).toBeTruthy();
done();
});
const response = await getArrayBuffer({url: 'custom://test/url/getArrayBuffer'}, new AbortController());
expect(response.data).toBeInstanceOf(ArrayBuffer);
expect(response.cacheControl).toBe('cache-control');
expect(response.expires).toBe('expires');
expect(protocolCallbackCalled).toBeTruthy();
});

test('#addProtocol - returning ImageBitmap for getImage', done => {
test('#addProtocol - returning ImageBitmap for getImage', async () => {
let protocolCallbackCalled = false;
maplibre.addProtocol('custom', (reqParam, callback) => {
maplibre.addProtocol('custom', (_reqParam, callback) => {
protocolCallbackCalled = true;
callback(null, new ImageBitmap());
return {cancel: () => {}};
});

ImageRequest.getImage({url: 'custom://test/url/getImage'}, async (error, img) => {
expect(error).toBeFalsy();
expect(img).toBeInstanceOf(ImageBitmap);
expect(protocolCallbackCalled).toBeTruthy();
done();
});
const img = await ImageRequest.getImage({url: 'custom://test/url/getImage'}, new AbortController());
expect(img.data).toBeInstanceOf(ImageBitmap);
expect(protocolCallbackCalled).toBeTruthy();
});

test('#addProtocol - returning HTMLImageElement for getImage', done => {
test('#addProtocol - returning HTMLImageElement for getImage', async () => {
let protocolCallbackCalled = false;
maplibre.addProtocol('custom', (reqParam, callback) => {
protocolCallbackCalled = true;
callback(null, new Image());
return {cancel: () => {}};
});
ImageRequest.getImage({url: 'custom://test/url/getImage'}, async (error, img) => {
expect(error).toBeFalsy();
expect(img).toBeInstanceOf(HTMLImageElement);
expect(protocolCallbackCalled).toBeTruthy();
done();
});
const img = await ImageRequest.getImage({url: 'custom://test/url/getImage'}, new AbortController());
expect(img.data).toBeInstanceOf(HTMLImageElement);
expect(protocolCallbackCalled).toBeTruthy();
});

test('#addProtocol - error', () => {
Expand All @@ -101,20 +92,27 @@ describe('maplibre', () => {
return {cancel: () => { }};
});

getJSON({url: 'custom://test/url/json'}, (error) => {
getJSON({url: 'custom://test/url/json'}, new AbortController()).catch((error) => {
expect(error).toBeTruthy();
});
});

test('#addProtocol - Cancel request', () => {
test('#addProtocol - Cancel request', async () => {
let cancelCalled = false;
maplibre.addProtocol('custom', () => {
return {cancel: () => {
cancelCalled = true;
}};
});
const request = getJSON({url: 'custom://test/url/json'}, () => { });
request.cancel();
const abortController = new AbortController();
const promise = getJSON({url: 'custom://test/url/json'}, abortController);
abortController.abort();
try {
await promise;
} catch (err) {
expect(isAbortError(err)).toBeTruthy();
}

expect(cancelCalled).toBeTruthy();
});

Expand Down
9 changes: 3 additions & 6 deletions src/render/glyph_atlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {AlphaImage} from '../util/image';
import {register} from '../util/web_worker_transfer';
import potpack from 'potpack';

import type {GlyphMetrics, StyleGlyph} from '../style/style_glyph';
import type {GlyphMetrics} from '../style/style_glyph';
import type {GetGlyphsResponse} from '../util/actor_messages';

const padding = 1;

Expand Down Expand Up @@ -37,11 +38,7 @@ export class GlyphAtlas {
image: AlphaImage;
positions: GlyphPositions;

constructor(stacks: {
[_: string]: {
[_: number]: StyleGlyph;
};
}) {
constructor(stacks: GetGlyphsResponse) {
const positions = {};
const bins = [];

Expand Down