diff --git a/src/source/tile.js b/src/source/tile.js index 2bb9ed0ec33..ee8032845fe 100644 --- a/src/source/tile.js +++ b/src/source/tile.js @@ -61,7 +61,6 @@ class Tile { expirationTime: any; expiredRequestCount: number; state: TileState; - placementThrottler: any; timeAdded: any; fadeEndTime: any; rawTileData: ArrayBuffer; diff --git a/src/ui/hash.js b/src/ui/hash.js index f79a0e4dad4..28e97dabfa6 100644 --- a/src/ui/hash.js +++ b/src/ui/hash.js @@ -2,6 +2,7 @@ const util = require('../util/util'); const window = require('../util/window'); +const throttle = require('../util/throttle'); import type Map from './map'; @@ -13,12 +14,16 @@ import type Map from './map'; */ class Hash { _map: Map; + _updateHash: () => number; constructor() { util.bindAll([ '_onHashChange', '_updateHash' ], this); + + // Mobile Safari doesn't allow updating the hash more than 100 times per 30 seconds. + this._updateHash = throttle(this._updateHashUnthrottled.bind(this), 30 * 1000 / 100); } /* @@ -82,7 +87,7 @@ class Hash { return false; } - _updateHash() { + _updateHashUnthrottled() { const hash = this.getHashString(); window.history.replaceState('', '', hash); } diff --git a/src/util/throttle.js b/src/util/throttle.js new file mode 100644 index 00000000000..ba5ec051738 --- /dev/null +++ b/src/util/throttle.js @@ -0,0 +1,26 @@ +// @flow + +/** + * Throttle the given function to run at most every `period` milliseconds. + */ +module.exports = function throttle(fn: () => void, time: number): () => number { + let pending = false; + let timerId = 0; + + const later = () => { + timerId = 0; + if (pending) { + fn(); + timerId = setTimeout(later, time); + pending = false; + } + }; + + return () => { + pending = true; + if (!timerId) { + later(); + } + return timerId; + }; +}; diff --git a/src/util/throttler.js b/src/util/throttler.js deleted file mode 100644 index f25638a6723..00000000000 --- a/src/util/throttler.js +++ /dev/null @@ -1,59 +0,0 @@ -// @flow - -const browser = require('./browser'); - -/** - * Throttles the provided function to run at most every - * 'frequency' milliseconds - * - * @interface Throttler - * @private - */ -class Throttler { - frequency: number; - throttledFunction: () => void; - lastInvocation: number; - pendingInvocation: ?number; - - constructor(frequency: number, throttledFunction: () => void) { - this.frequency = frequency; - this.throttledFunction = throttledFunction; - this.lastInvocation = 0; - } - - /** - * Request an invocation of the throttled function. - * - * @memberof Throttler - * @instance - */ - invoke() { - if (this.pendingInvocation) { - return; - } - - const timeToNextInvocation = this.lastInvocation === 0 ? - 0 : - (this.lastInvocation + this.frequency) - browser.now(); - - if (timeToNextInvocation <= 0) { - this.lastInvocation = browser.now(); - this.throttledFunction(); - } else { - this.pendingInvocation = setTimeout(() => { - this.pendingInvocation = undefined; - this.lastInvocation = browser.now(); - this.throttledFunction(); - }, timeToNextInvocation); - } - } - - stop() { - if (this.pendingInvocation) { - clearTimeout(this.pendingInvocation); - this.pendingInvocation = undefined; - } - } -} - -module.exports = Throttler; diff --git a/test/unit/ui/hash.test.js b/test/unit/ui/hash.test.js index b4a521c271f..67f7fa7b99d 100644 --- a/test/unit/ui/hash.test.js +++ b/test/unit/ui/hash.test.js @@ -7,7 +7,9 @@ const Map = require('../../../src/ui/map'); test('hash', (t) => { function createHash() { - return new Hash(); + const hash = new Hash(); + hash._updateHash = hash._updateHashUnthrottled.bind(hash); + return hash; } function createMap() { diff --git a/test/unit/util/throttle.test.js b/test/unit/util/throttle.test.js new file mode 100644 index 00000000000..962d89ba9da --- /dev/null +++ b/test/unit/util/throttle.test.js @@ -0,0 +1,53 @@ +'use strict'; +// @flow + +const test = require('mapbox-gl-js-test').test; +const throttle = require('../../../src/util/throttle'); + +test('throttle', (t) => { + + t.test('does not execute unthrottled function unless throttled function is invoked', (t) => { + let executionCount = 0; + throttle(() => { executionCount++; }, 0); + t.equal(executionCount, 0); + t.end(); + }); + + t.test('executes unthrottled function once per tick when period is 0', (t) => { + let executionCount = 0; + const throttledFunction = throttle(() => { executionCount++; }, 0); + throttledFunction(); + throttledFunction(); + t.equal(executionCount, 1); + setTimeout(() => { + throttledFunction(); + throttledFunction(); + t.equal(executionCount, 2); + t.end(); + }, 0); + }); + + t.test('executes unthrottled function immediately once when period is > 0', (t) => { + let executionCount = 0; + const throttledFunction = throttle(() => { executionCount++; }, 5); + throttledFunction(); + throttledFunction(); + throttledFunction(); + t.equal(executionCount, 1); + t.end(); + }); + + t.test('queues exactly one execution of unthrottled function when period is > 0', (t) => { + let executionCount = 0; + const throttledFunction = throttle(() => { executionCount++; }, 5); + throttledFunction(); + throttledFunction(); + throttledFunction(); + setTimeout(() => { + t.equal(executionCount, 2); + t.end(); + }, 10); + }); + + t.end(); +});