Skip to content

Commit

Permalink
Refactor RTL plugin loading code (#3418)
Browse files Browse the repository at this point in the history
* Initial commit to split rtl to worker and main thread implementations.

* Finish all fixes for rtl plugin split

* Fix the tests that fail, add proper idle event handling

* Changelog, lint, build and render

* Improve render test

* Fix changelog, add idle console logging

* Return a promise from the public API

* Add more logs to tests

* More logs, more timeout

* Fix lint

* Clean error for failed tests

* Fix typo
  • Loading branch information
HarelM authored Nov 28, 2023
1 parent c7d6bb0 commit d68b919
Show file tree
Hide file tree
Showing 33 changed files with 333 additions and 334 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### ✨ Features and improvements

- ⚠️ Changed the `setRTLTextPlugin` function to return a promise instead of using callback ([#3418](https://github.com/maplibre/maplibre-gl-js/pull/3418)) this also changed how the RTL pluing code is handled internally by splitting the main thread and worker thread code.
- _...Add new stuff here..._

### 🐞 Bug fixes
Expand Down
6 changes: 3 additions & 3 deletions src/data/bucket/symbol_bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {getSizeData, MAX_PACKED_SIZE} from '../../symbol/symbol_size';
import {register} from '../../util/web_worker_transfer';
import {EvaluationParameters} from '../../style/evaluation_parameters';
import {Formatted, ResolvedImage} from '@maplibre/maplibre-gl-style-spec';
import {plugin as globalRTLTextPlugin, getRTLTextPluginStatus} from '../../source/rtl_text_plugin';
import {rtlWorkerPlugin} from '../../source/rtl_text_plugin_worker';
import {mat4} from 'gl-matrix';
import {getOverlapMode} from '../../style/style_layer/overlap_mode';
import type {CanonicalTileID} from '../../source/tile_id';
Expand Down Expand Up @@ -481,8 +481,8 @@ export class SymbolBucket implements Bucket {
}
if (
!this.hasRTLText || // non-rtl text so can proceed safely
getRTLTextPluginStatus() === 'unavailable' || // We don't intend to lazy-load the rtl text plugin, so proceed with incorrect shaping
this.hasRTLText && globalRTLTextPlugin.isParsed() // Use the rtlText plugin to shape text
rtlWorkerPlugin.getRTLTextPluginStatus() === 'unavailable' || // We don't intend to lazy-load the rtl text plugin, so proceed with incorrect shaping
this.hasRTLText && rtlWorkerPlugin.isParsed() // Use the rtlText plugin to shape text
) {
text = transformText(formattedText, layer, evaluationFeature);
}
Expand Down
7 changes: 3 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {Evented} from './util/evented';
import {config} from './util/config';
import {Debug} from './util/debug';
import {isSafari} from './util/util';
import {setRTLTextPlugin, getRTLTextPluginStatus} from './source/rtl_text_plugin';
import {rtlMainThreadPluginFactory} from './source/rtl_text_plugin_main_thread';
import {WorkerPool} from './util/worker_pool';
import {prewarm, clearPrewarmedResources} from './util/global_worker_pool';
import {PerformanceUtils} from './util/performance';
Expand Down Expand Up @@ -73,7 +73,6 @@ class MapLibreGL {
* Necessary for supporting the Arabic and Hebrew languages, which are written right-to-left.
*
* @param pluginURL - URL pointing to the Mapbox RTL text plugin source.
* @param callback - Called with an error argument if there is an error.
* @param lazy - If set to `true`, mapboxgl will defer loading the plugin until rtl text is encountered,
* rtl text will then be rendered only after the plugin finishes loading.
* @example
Expand All @@ -82,7 +81,7 @@ class MapLibreGL {
* ```
* @see [Add support for right-to-left scripts](https://maplibre.org/maplibre-gl-js/docs/examples/mapbox-gl-rtl-text/)
*/
static setRTLTextPlugin = setRTLTextPlugin;
static setRTLTextPlugin = (pluginURL: string, lazy: boolean) => rtlMainThreadPluginFactory().setRTLTextPlugin(pluginURL, lazy);
/**
* Gets the map's [RTL text plugin](https://www.mapbox.com/mapbox-gl-js/plugins/#mapbox-gl-rtl-text) status.
* The status can be `unavailable` (i.e. not requested or removed), `loading`, `loaded` or `error`.
Expand All @@ -93,7 +92,7 @@ class MapLibreGL {
* const pluginStatus = maplibregl.getRTLTextPluginStatus();
* ```
*/
static getRTLTextPluginStatus = getRTLTextPluginStatus;
static getRTLTextPluginStatus = () => rtlMainThreadPluginFactory().getRTLTextPluginStatus();
/**
* Initializes resources like WebWorkers that can be shared across maps to lower load
* times in some situations. `maplibregl.workerUrl` and `maplibregl.workerCount`, if being
Expand Down
152 changes: 0 additions & 152 deletions src/source/rtl_text_plugin.ts

This file was deleted.

82 changes: 82 additions & 0 deletions src/source/rtl_text_plugin_main_thread.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {FakeServer, fakeServer} from 'nise';
import {rtlMainThreadPluginFactory} from './rtl_text_plugin_main_thread';
import {sleep} from '../util/test/util';
import {browser} from '../util/browser';

const rtlMainThreadPlugin = rtlMainThreadPluginFactory();

describe('RTLMainThreadPlugin', () => {
let server: FakeServer;
let broadcastSpy: jest.SpyInstance;
beforeEach(() => {
server = fakeServer.create();
global.fetch = null;
// Reset the singleton instance before each test
rtlMainThreadPlugin.clearRTLTextPlugin();
broadcastSpy = jest.spyOn(rtlMainThreadPlugin.dispatcher, 'broadcast').mockImplementation(() => { return Promise.resolve({} as any); });
});

afterEach(() => {
server.restore();
broadcastSpy.mockRestore();
});

it('should get the RTL text plugin status', () => {
const status = rtlMainThreadPlugin.getRTLTextPluginStatus();
expect(status).toBe('unavailable');
});

it('should set the RTL text plugin and download it', async () => {
const url = 'http://example.com/plugin';
server.respondWith(new ArrayBuffer(0));

const promise = rtlMainThreadPlugin.setRTLTextPlugin(url);
await sleep(0);
server.respond();
await promise;
expect(rtlMainThreadPlugin.pluginURL).toEqual(url);
});

it('should set the RTL text plugin but deffer downloading', async () => {
const url = 'http://example.com/plugin';
await rtlMainThreadPlugin.setRTLTextPlugin(url, true);
expect(server.requests).toHaveLength(0);
expect(rtlMainThreadPlugin.pluginStatus).toBe('deferred');
});

it('should throw if the plugin is already set', async () => {
const url = 'http://example.com/plugin';
await rtlMainThreadPlugin.setRTLTextPlugin(url, true);
await expect(rtlMainThreadPlugin.setRTLTextPlugin(url)).rejects.toThrow('setRTLTextPlugin cannot be called multiple times.');
});

it('should throw if the plugin url is not set', async () => {
const spy = jest.spyOn(browser, 'resolveURL').mockImplementation(() => { return ''; });
await expect(rtlMainThreadPlugin.setRTLTextPlugin(null)).rejects.toThrow('rtl-text-plugin cannot be downloaded unless a pluginURL is specified');
spy.mockRestore();
});

it('should be in error state if download fails', async () => {
const url = 'http://example.com/plugin';
server.respondWith(request => request.respond(404));
const promise = rtlMainThreadPlugin.setRTLTextPlugin(url);
await sleep(0);
server.respond();
await promise;
expect(rtlMainThreadPlugin.pluginURL).toEqual(url);
expect(rtlMainThreadPlugin.pluginStatus).toBe('error');
});

it('should lazy load the plugin if deffered', async () => {
const url = 'http://example.com/plugin';
server.respondWith(new ArrayBuffer(0));
await rtlMainThreadPlugin.setRTLTextPlugin(url, true);
expect(server.requests).toHaveLength(0);
expect(rtlMainThreadPlugin.pluginStatus).toBe('deferred');
const promise = rtlMainThreadPlugin.lazyLoadRTLTextPlugin();
await sleep(0);
server.respond();
await promise;
expect(rtlMainThreadPlugin.pluginStatus).toBe('loaded');
});
});
70 changes: 70 additions & 0 deletions src/source/rtl_text_plugin_main_thread.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {getArrayBuffer} from '../util/ajax';
import {browser} from '../util/browser';
import {Event, Evented} from '../util/evented';
import {RTLPluginStatus, PluginState} from './rtl_text_plugin_status';
import {Dispatcher} from '../util/dispatcher';
import {getGlobalWorkerPool} from '../util/global_worker_pool';

class RTLMainThreadPlugin extends Evented {
pluginStatus: RTLPluginStatus = 'unavailable';
pluginURL: string = null;
dispatcher: Dispatcher = new Dispatcher(getGlobalWorkerPool(), 'rtl-text-plugin-dispacher');
queue: PluginState[] = [];

async _sendPluginStateToWorker() {
await this.dispatcher.broadcast('syncRTLPluginState', {pluginStatus: this.pluginStatus, pluginURL: this.pluginURL});
this.fire(new Event('pluginStateChange', {pluginStatus: this.pluginStatus, pluginURL: this.pluginURL}));
}

getRTLTextPluginStatus() {
return this.pluginStatus;
}

clearRTLTextPlugin() {
this.pluginStatus = 'unavailable';
this.pluginURL = null;
}

async setRTLTextPlugin(url: string, deferred: boolean = false): Promise<void> {
if (this.pluginStatus === 'deferred' || this.pluginStatus === 'loading' || this.pluginStatus === 'loaded') {
throw new Error('setRTLTextPlugin cannot be called multiple times.');
}
this.pluginURL = browser.resolveURL(url);
this.pluginStatus = 'deferred';
await this._sendPluginStateToWorker();
if (!deferred) {
//Start downloading the plugin immediately if not intending to lazy-load
await this._downloadRTLTextPlugin();
}
}

async _downloadRTLTextPlugin() {
if (this.pluginStatus !== 'deferred' || !this.pluginURL) {
throw new Error('rtl-text-plugin cannot be downloaded unless a pluginURL is specified');
}
try {
this.pluginStatus = 'loading';
await this._sendPluginStateToWorker();
await getArrayBuffer({url: this.pluginURL}, new AbortController());
this.pluginStatus = 'loaded';
} catch {
this.pluginStatus = 'error';
}
await this._sendPluginStateToWorker();
}

async lazyLoadRTLTextPlugin() {
if (this.pluginStatus === 'deferred') {
await this._downloadRTLTextPlugin();
}
}
}

let rtlMainThreadPlugin: RTLMainThreadPlugin = null;

export function rtlMainThreadPluginFactory(): RTLMainThreadPlugin {
if (!rtlMainThreadPlugin) {
rtlMainThreadPlugin = new RTLMainThreadPlugin();
}
return rtlMainThreadPlugin;
}
22 changes: 22 additions & 0 deletions src/source/rtl_text_plugin_status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* The possible option of the plugin's status
*
* `unavailable`: Not loaded.
*
* `deferred`: The plugin URL has been specified, but loading has been deferred.
*
* `loading`: request in-flight.
*
* `loaded`: The plugin is now loaded
*
* `error`: The plugin failed to load
*/
export type RTLPluginStatus = 'unavailable' | 'deferred' | 'loading' | 'loaded' | 'error';

/**
* The RTL plugin state
*/
export type PluginState = {
pluginStatus: RTLPluginStatus;
pluginURL: string;
};
Loading

0 comments on commit d68b919

Please sign in to comment.