Skip to content

Commit

Permalink
Add and test polyfill code to support Promise implementation.
Browse files Browse the repository at this point in the history
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=130341214
  • Loading branch information
brad4d authored and blickly committed Aug 16, 2016
1 parent 5dff1e1 commit 69e547f
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 1 deletion.
140 changes: 140 additions & 0 deletions src/com/google/javascript/jscomp/js/es6/promise.js
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');
1 change: 1 addition & 0 deletions src/com/google/javascript/jscomp/js/es6_runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'require es6/math';
'require es6/number';
'require es6/object';
'require es6/promise';
'require es6/reflect';
'require es6/set';
'require es6/string';
Expand Down
1 change: 1 addition & 0 deletions src/com/google/javascript/jscomp/js/polyfills.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Object.assign es6-impl es3 es6/object/assign
Object.getOwnPropertySymbols es6-impl es5 es6/object/getownpropertysymbols
Object.is es6-impl es3 es6/object/is
Object.setPrototypeOf es6 es5 es6/object/setprototypeof
Promise es6-impl es3 es6/promise
Proxy es6 es6
Reflect.apply es6 es3 es6/reflect/apply
Reflect.construct es6 es5 es6/reflect/construct
Expand Down
2 changes: 1 addition & 1 deletion src/com/google/javascript/jscomp/resources.json

Large diffs are not rendered by default.

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');
},
});

0 comments on commit 69e547f

Please sign in to comment.