From 5eaef1c631bcf70aed98ac575adb898fee5ba241 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Tue, 6 Sep 2016 14:36:30 -0700 Subject: [PATCH] Introduce Batchinator Reviewed By: devknoll Differential Revision: D3796349 fbshipit-source-id: 2e23a2361a612107596cf6381e67252238e970bf --- Libraries/Interaction/Batchinator.js | 77 +++++++++++++++++ .../Interaction/__tests__/Batchinator-test.js | 83 +++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 Libraries/Interaction/Batchinator.js create mode 100644 Libraries/Interaction/__tests__/Batchinator-test.js diff --git a/Libraries/Interaction/Batchinator.js b/Libraries/Interaction/Batchinator.js new file mode 100644 index 000000000..e807e5515 --- /dev/null +++ b/Libraries/Interaction/Batchinator.js @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule Batchinator + * @flow + */ +'use strict'; + +const InteractionManager = require('InteractionManager'); + +/** + * A simple class for batching up invocations of a low-pri callback. A timeout is set to run the + * callback once after a delay, no matter how many times it's scheduled. Once the delay is reached, + * InteractionManager.runAfterInteractions is used to invoke the callback after any hi-pri + * interactions are done running. + * + * Make sure to cleanup with dispose(). Example: + * + * class Widget extends React.Component { + * _batchedSave: new Batchinator(() => this._saveState, 1000); + * _saveSate() { + * // save this.state to disk + * } + * componentDidUpdate() { + * this._batchedSave.schedule(); + * } + * componentWillUnmount() { + * this._batchedSave.dispose(); + * } + * ... + * } + */ +class Batchinator { + _callback: () => void; + _delay: number; + _taskHandle: ?{cancel: () => void}; + constructor(callback: () => void, delayMS: number) { + this._delay = delayMS; + this._callback = callback; + } + /* + * Cleanup any pending tasks. + * + * By default, if there is a pending task the callback is run immediately. Set the option abort to + * true to not call the callback if it was pending. + */ + dispose(options: {abort: boolean} = {abort: false}) { + if (this._taskHandle) { + this._taskHandle.cancel(); + if (!options.abort) { + this._callback(); + } + this._taskHandle = null; + } + } + schedule() { + if (this._taskHandle) { + return; + } + const timeoutHandle = setTimeout(() => { + this._taskHandle = InteractionManager.runAfterInteractions(() => { + // Note that we clear the handle before invoking the callback so that if the callback calls + // schedule again, it will actually schedule another task. + this._taskHandle = null; + this._callback(); + }); + }, this._delay); + this._taskHandle = {cancel: () => clearTimeout(timeoutHandle)}; + } +} + +module.exports = Batchinator; diff --git a/Libraries/Interaction/__tests__/Batchinator-test.js b/Libraries/Interaction/__tests__/Batchinator-test.js new file mode 100644 index 000000000..9ba143073 --- /dev/null +++ b/Libraries/Interaction/__tests__/Batchinator-test.js @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +'use strict'; + +jest + .disableAutomock() + .mock('ErrorUtils') + .mock('BatchedBridge'); + +function expectToBeCalledOnce(fn) { + expect(fn.mock.calls.length).toBe(1); +} + +describe('Batchinator', () => { + const Batchinator = require('Batchinator'); + + it('executes vanilla tasks', () => { + const callback = jest.fn(); + const batcher = new Batchinator(callback, 10000); + batcher.schedule(); + jest.runAllTimers(); + expectToBeCalledOnce(callback); + }); + + it('batches up tasks', () => { + const callback = jest.fn(); + const batcher = new Batchinator(callback, 10000); + batcher.schedule(); + batcher.schedule(); + batcher.schedule(); + batcher.schedule(); + expect(callback).not.toBeCalled(); + jest.runAllTimers(); + expectToBeCalledOnce(callback); + }); + + it('flushes on dispose', () => { + const callback = jest.fn(); + const batcher = new Batchinator(callback, 10000); + batcher.schedule(); + batcher.schedule(); + batcher.dispose(); + expectToBeCalledOnce(callback); + jest.runAllTimers(); + expectToBeCalledOnce(callback); + }); + + it('should call tasks scheduled by the callback', () => { + let batcher = null; + let hasRescheduled = false; + const callback = jest.fn(() => { + if (!hasRescheduled) { + batcher.schedule(); + hasRescheduled = true; + } + }); + batcher = new Batchinator(callback, 10000); + batcher.schedule(); + jest.runAllTimers(); + expect(callback.mock.calls.length).toBe(2); + }); + + it('does not run callbacks more than once', () => { + const callback = jest.fn(); + const batcher = new Batchinator(callback, 10000); + batcher.schedule(); + batcher.schedule(); + jest.runAllTimers(); + expectToBeCalledOnce(callback); + jest.runAllTimers(); + expectToBeCalledOnce(callback); + batcher.dispose(); + expectToBeCalledOnce(callback); + }); +});