-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add and test polyfill code to support Promise implementation.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=130341214
- Loading branch information
Showing
5 changed files
with
319 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
/* | ||
* Copyright 2016 The Closure Compiler Authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
'require base'; | ||
'require es6/util/makeiterator'; | ||
'require util/polyfill'; | ||
|
||
/** | ||
* Should we expose AsyncExecutor for testing? | ||
* TODO(bradfordcsmith): Set this false here & arrange for it to be set to true | ||
* only for tests. | ||
* @define {boolean} | ||
*/ | ||
$jscomp.EXPOSE_ASYNC_EXECUTOR = true; | ||
|
||
$jscomp.polyfill('Promise', function(NativePromise) { | ||
/** | ||
* Schedules code to be executed asynchronously. | ||
* @constructor | ||
* @struct | ||
*/ | ||
function AsyncExecutor() { | ||
/** | ||
* Batch of functions to execute. | ||
* | ||
* Will be {@code null} initially and immediately after a batch finishes | ||
* executing. | ||
* @private {?Array<function():?>} | ||
*/ | ||
this.batch_ = null; | ||
} | ||
|
||
/** | ||
* Schedule a function to execute asynchronously. | ||
* | ||
* - The function will execute: | ||
* - After the current call stack has completed executing. | ||
* - After any functions previously scheduled using this object. | ||
* - The return value will be ignored. | ||
* - An exception thrown by the method will be caught and asynchronously | ||
* rethrown when it cannot interrupt any other code. This class provides | ||
* no way to catch such exceptions. | ||
* @param {function():?} f | ||
* @return {!AsyncExecutor} this object | ||
*/ | ||
AsyncExecutor.prototype.asyncExecute = function(f) { | ||
if (this.batch_ == null) { | ||
// no batch created yet, or last batch was fully executed | ||
this.batch_ = []; | ||
this.asyncExecuteBatch_(); | ||
} | ||
this.batch_.push(f); | ||
return this; | ||
}; | ||
|
||
/** | ||
* Schedule execution of the jobs in {@code this.batch_}. | ||
* @private | ||
*/ | ||
AsyncExecutor.prototype.asyncExecuteBatch_ = function() { | ||
var self = this; | ||
this.asyncExecuteFunction(function() { self.executeBatch_(); }); | ||
}; | ||
|
||
// NOTE: We want to make sure AsyncExecutor will work as expected even if | ||
// testing code should override setTimeout() | ||
/** @const */ var nativeSetTimeout = setTimeout; | ||
|
||
/** | ||
* Schedule a function to execute asynchronously as soon as possible. | ||
* | ||
* NOTE: May be overridden for testing. | ||
* @package | ||
* @param {!Function} f | ||
*/ | ||
AsyncExecutor.prototype.asyncExecuteFunction = function(f) { | ||
nativeSetTimeout(f, 0); | ||
}; | ||
|
||
/** | ||
* Execute scheduled jobs in a batch until all are executed or the batch | ||
* execution time limit has been reached. | ||
* @private | ||
*/ | ||
AsyncExecutor.prototype.executeBatch_ = function() { | ||
while (this.batch_ && this.batch_.length) { | ||
var executingBatch = this.batch_; | ||
// Executions scheduled while executing this batch go into a new one to | ||
// avoid the batch array getting too big. | ||
this.batch_ = []; | ||
for (var i = 0; i < executingBatch.length; ++i) { | ||
var f = executingBatch[i]; | ||
delete executingBatch[i]; // free memory | ||
try { | ||
f(); | ||
} catch (error) { | ||
this.asyncThrow_(error); | ||
} | ||
} | ||
} | ||
// All jobs finished executing, so force scheduling a new batch next | ||
// time asyncExecute() is called. | ||
this.batch_ = null; | ||
}; | ||
|
||
/** | ||
* @private | ||
* @param {*} exception | ||
*/ | ||
AsyncExecutor.prototype.asyncThrow_ = function(exception) { | ||
this.asyncExecuteFunction(function() { throw exception; }); | ||
}; | ||
|
||
// TODO(bradfordcsmith): Actually implement PolyfillPromise | ||
var PolyfillPromise = NativePromise; | ||
|
||
if ($jscomp.EXPOSE_ASYNC_EXECUTOR) { | ||
// TODO(bradfordcsmith): Remove this once PolyfillPromise is implemented. | ||
PolyfillPromise = PolyfillPromise || {}; | ||
// expose AsyncExecutor so it can be tested independently. | ||
PolyfillPromise['$jscomp$new$AsyncExecutor'] = function() { | ||
return new AsyncExecutor(); | ||
}; | ||
} | ||
|
||
return PolyfillPromise; | ||
}, 'es6-impl', 'es3'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
176 changes: 176 additions & 0 deletions
176
test/com/google/javascript/jscomp/runtime_tests/polyfill_tests/asyncexecutor_test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
/* | ||
* Copyright 2016 The Closure Compiler Authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
goog.module('jscomp.runtime_tests.polyfill_tests.asyncexecutor_test'); | ||
goog.setTestOnly(); | ||
|
||
const testSuite = goog.require('goog.testing.testSuite'); | ||
|
||
// NOTE: Since they are implementation details of Promise, the AsyncExecutor | ||
// class type definition is not visible to the compiler here. | ||
|
||
/** @interface */ | ||
class AsyncExecutor { | ||
asyncExecute(/** function():? */ f) {} | ||
} | ||
|
||
/** | ||
* Captures AsyncExecutor calls to asyncExecuteFunction(). | ||
*/ | ||
class AsyncExecuteFunctionHandler { | ||
constructor() { | ||
/** @type {!Array<!Function>} */ | ||
this.scheduledFunctions = []; | ||
} | ||
|
||
/** | ||
* @param {!Function} f | ||
*/ | ||
asyncExecuteFunction(f) { this.scheduledFunctions.push(f); } | ||
|
||
executeNextFunction() { | ||
assertTrue('no functions scheduled', this.scheduledFunctions.length > 0); | ||
this.scheduledFunctions.shift().call(); | ||
} | ||
|
||
assertNextFunctionThrows(expectedException) { | ||
let actualException = assertThrows(() => this.executeNextFunction()); | ||
assertObjectEquals(expectedException, actualException); | ||
} | ||
|
||
static forExecutor(executor) { | ||
const handler = new AsyncExecuteFunctionHandler(); | ||
executor.asyncExecuteFunction = f => handler.asyncExecuteFunction(f); | ||
return handler; | ||
} | ||
} | ||
|
||
/** | ||
* Provides a method we can conveniently call asynchronously and a method to | ||
* test that the calls were made in the expected order. | ||
*/ | ||
class TestObject { | ||
constructor() { this.results = []; } | ||
|
||
pushResult(value) { this.results.push(value); } | ||
|
||
pushResultFunc(value) { return () => this.pushResult(value); } | ||
|
||
assertResultsInOrder(...expectedResults) { | ||
assertSameElements('unexpected results', expectedResults, this.results); | ||
assertObjectEquals('results out of order', expectedResults, this.results); | ||
} | ||
} | ||
|
||
/** | ||
* @return {!AsyncExecutor} | ||
* @throws if unable to create a new AsyncExecutor | ||
*/ | ||
function newAsyncExecutor() { | ||
// IMPORTANT NOTE: Promise must be used somewhere in this file to trigger | ||
// inclusion of the Promise polyfill code, which is where AsyncExecutor | ||
// is defined. | ||
return /** {!AsyncExecutor} */ (Promise['$jscomp$new$AsyncExecutor']()); | ||
} | ||
|
||
testSuite({ | ||
shouldRunTests() { | ||
try { | ||
// throws an exception if RealAsyncExecutor is undefined. | ||
newAsyncExecutor(); | ||
} catch (ignored) { | ||
return false; | ||
} | ||
return true; | ||
}, | ||
|
||
testAsyncExecutorSingleBatch() { | ||
const executor = newAsyncExecutor(); | ||
const asyncExecuteFunctionHandler = | ||
AsyncExecuteFunctionHandler.forExecutor(executor); | ||
const testObject = new TestObject(); | ||
|
||
executor.asyncExecute(testObject.pushResultFunc('one')); | ||
executor.asyncExecute(testObject.pushResultFunc('two')); | ||
executor.asyncExecute(testObject.pushResultFunc('three')); | ||
|
||
// No executions until we trigger the asyncExecuteFunctionHandler | ||
testObject.assertResultsInOrder(); | ||
// All executions done in order in a single batch. | ||
asyncExecuteFunctionHandler.executeNextFunction(); | ||
testObject.assertResultsInOrder('one', 'two', 'three'); | ||
}, | ||
|
||
testAsyncExecutorSeparateBatches() { | ||
const executor = newAsyncExecutor(); | ||
const asyncExecuteFunctionHandler = | ||
AsyncExecuteFunctionHandler.forExecutor(executor); | ||
const testObject = new TestObject(); | ||
|
||
executor.asyncExecute(testObject.pushResultFunc('one')); | ||
|
||
// only one result after first batch | ||
asyncExecuteFunctionHandler.executeNextFunction(); | ||
testObject.assertResultsInOrder('one'); | ||
|
||
executor.asyncExecute(testObject.pushResultFunc('two')); | ||
executor.asyncExecute(testObject.pushResultFunc('three')); | ||
|
||
// All executions done after the second batch | ||
asyncExecuteFunctionHandler.executeNextFunction(); | ||
testObject.assertResultsInOrder('one', 'two', 'three'); | ||
}, | ||
|
||
testAsyncExecutorExceptionsDoNotBlockOtherJobs() { | ||
const executor = newAsyncExecutor(); | ||
const asyncExecuteFunctionHandler = | ||
AsyncExecuteFunctionHandler.forExecutor(executor); | ||
const testObject = new TestObject(); | ||
const error1 = new Error('error 1'); | ||
const error2 = new Error('error 2'); | ||
|
||
executor.asyncExecute(testObject.pushResultFunc('one')); | ||
executor.asyncExecute(() => { throw error1; }); | ||
executor.asyncExecute(testObject.pushResultFunc('two')); | ||
executor.asyncExecute(() => { throw error2; }); | ||
executor.asyncExecute(testObject.pushResultFunc('three')); | ||
|
||
// All executions done in order in a single batch. | ||
asyncExecuteFunctionHandler.executeNextFunction(); | ||
testObject.assertResultsInOrder('one', 'two', 'three'); | ||
|
||
// errors thrown in separate jobs & same order they were thrown originally | ||
asyncExecuteFunctionHandler.assertNextFunctionThrows(error1); | ||
asyncExecuteFunctionHandler.assertNextFunctionThrows(error2); | ||
}, | ||
|
||
testNewJobsGetAddedToExecutingBatch() { | ||
const executor = newAsyncExecutor(); | ||
const asyncExecuteFunctionHandler = | ||
AsyncExecuteFunctionHandler.forExecutor(executor); | ||
const testObject = new TestObject(); | ||
|
||
executor.asyncExecute(testObject.pushResultFunc('one')); | ||
executor.asyncExecute( | ||
() => executor.asyncExecute(testObject.pushResultFunc('two'))); | ||
executor.asyncExecute( | ||
() => executor.asyncExecute( | ||
() => executor.asyncExecute(testObject.pushResultFunc('three')))); | ||
// All executions done in order in a single batch. | ||
asyncExecuteFunctionHandler.executeNextFunction(); | ||
testObject.assertResultsInOrder('one', 'two', 'three'); | ||
}, | ||
}); |