blob: 91ffef404fcc64e5317a6d38c2a6d8eff83a63cf [file] [log] [blame]
// Copyright 2012 The Closure Library Authors. All Rights Reserved.
//
// 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.
/**
* @fileoverview This file provides primitives and tools (wait, transform,
* chain, combine) that make it easier to work with Results. This section
* gives an overview of their functionality along with some examples and the
* actual definitions have detailed descriptions next to them.
*
*
* NOTE: goog.result is soft deprecated - we expect to replace this and
* goog.async.Deferred with a wrapper around W3C Promises:
* http://dom.spec.whatwg.org/#promises.
*/
goog.provide('goog.result');
goog.require('goog.array');
goog.require('goog.result.DependentResult');
goog.require('goog.result.Result');
goog.require('goog.result.SimpleResult');
/**
* Returns a successful result containing the provided value.
*
* Example:
* <pre>
*
* var value = 'some-value';
* var result = goog.result.immediateResult(value);
* assertEquals(goog.result.Result.State.SUCCESS, result.getState());
* assertEquals(value, result.getValue());
*
* </pre>
*
* @param {*} value The value of the result.
* @return {!goog.result.Result} A Result object that has already been resolved
* to the supplied value.
*/
goog.result.successfulResult = function(value) {
var result = new goog.result.SimpleResult();
result.setValue(value);
return result;
};
/**
* Returns a failed result with the optional error slug set.
*
* Example:
* <pre>
*
* var error = new Error('something-failed');
* var result = goog.result.failedResult(error);
* assertEquals(goog.result.Result.State.ERROR, result.getState());
* assertEquals(error, result.getError());
*
* </pre>
*
* @param {*=} opt_error The error to which the result should resolve.
* @return {!goog.result.Result} A Result object that has already been resolved
* to the supplied Error.
*/
goog.result.failedResult = function(opt_error) {
var result = new goog.result.SimpleResult();
result.setError(opt_error);
return result;
};
/**
* Returns a canceled result.
* The result will be resolved to an error of type CancelError.
*
* Example:
* <pre>
*
* var result = goog.result.canceledResult();
* assertEquals(goog.result.Result.State.ERROR, result.getState());
* var error = result.getError();
* assertTrue(error instanceof goog.result.Result.CancelError);
*
* </pre>
*
* @return {!goog.result.Result} A canceled Result.
*/
goog.result.canceledResult = function() {
var result = new goog.result.SimpleResult();
result.cancel();
return result;
};
/**
* Calls the handler on resolution of the result (success or failure).
* The handler is passed the result object as the only parameter. The call will
* be immediate if the result is no longer pending.
*
* Example:
* <pre>
*
* var result = xhr.get('testdata/xhr_test_text.data');
*
* // Wait for the result to be resolved and alert it's state.
* goog.result.wait(result, function(result) {
* alert('State: ' + result.getState());
* });
* </pre>
*
* @param {!goog.result.Result} result The result to install the handlers.
* @param {function(this:T, !goog.result.Result)} handler The handler to be
* called. The handler is passed the result object as the only parameter.
* @param {T=} opt_scope Optional scope for the handler.
* @template T
*/
goog.result.wait = function(result, handler, opt_scope) {
result.wait(handler, opt_scope);
};
/**
* Calls the handler if the result succeeds. The result object is the only
* parameter passed to the handler. The call will be immediate if the result
* has already succeeded.
*
* Example:
* <pre>
*
* var result = xhr.get('testdata/xhr_test_text.data');
*
* // attach a success handler.
* goog.result.waitOnSuccess(result, function(resultValue, result) {
* var datavalue = result.getvalue();
* alert('value: ' + datavalue + ' == ' + resultValue);
* });
* </pre>
*
* @param {!goog.result.Result} result The result to install the handlers.
* @param {function(this:T, ?, !goog.result.Result)} handler The handler to be
* called. The handler is passed the result value and the result as
* parameters.
* @param {T=} opt_scope Optional scope for the handler.
* @template T
*/
goog.result.waitOnSuccess = function(result, handler, opt_scope) {
goog.result.wait(result, function(res) {
if (res.getState() == goog.result.Result.State.SUCCESS) {
// 'this' refers to opt_scope
handler.call(this, res.getValue(), res);
}
}, opt_scope);
};
/**
* Calls the handler if the result action errors. The result object is passed as
* the only parameter to the handler. The call will be immediate if the result
* object has already resolved to an error.
*
* Example:
*
* <pre>
*
* var result = xhr.get('testdata/xhr_test_text.data');
*
* // Attach a failure handler.
* goog.result.waitOnError(result, function(error) {
* // Failed asynchronous call!
* });
* </pre>
*
* @param {!goog.result.Result} result The result to install the handlers.
* @param {function(this:T, ?, !goog.result.Result)} handler The handler to be
* called. The handler is passed the error and the result object as
* parameters.
* @param {T=} opt_scope Optional scope for the handler.
* @template T
*/
goog.result.waitOnError = function(result, handler, opt_scope) {
goog.result.wait(result, function(res) {
if (res.getState() == goog.result.Result.State.ERROR) {
// 'this' refers to opt_scope
handler.call(this, res.getError(), res);
}
}, opt_scope);
};
/**
* Given a result and a transform function, returns a new result whose value,
* on success, will be the value of the given result after having been passed
* through the transform function.
*
* If the given result is an error, the returned result is also an error and the
* transform will not be called.
*
* Example:
* <pre>
*
* var result = xhr.getJson('testdata/xhr_test_json.data');
*
* // Transform contents of returned data using 'processJson' and create a
* // transformed result to use returned JSON.
* var transformedResult = goog.result.transform(result, processJson);
*
* // Attach success and failure handlers to the tranformed result.
* goog.result.waitOnSuccess(transformedResult, function(resultValue, result) {
* var jsonData = resultValue;
* assertEquals('ok', jsonData['stat']);
* });
*
* goog.result.waitOnError(transformedResult, function(error) {
* // Failed getJson call
* });
* </pre>
*
* @param {!goog.result.Result} result The result whose value will be
* transformed.
* @param {function(?):?} transformer The transformer
* function. The return value of this function will become the value of the
* returned result.
*
* @return {!goog.result.DependentResult} A new Result whose eventual value will
* be the returned value of the transformer function.
*/
goog.result.transform = function(result, transformer) {
var returnedResult = new goog.result.DependentResultImpl_([result]);
goog.result.wait(result, function(res) {
if (res.getState() == goog.result.Result.State.SUCCESS) {
returnedResult.setValue(transformer(res.getValue()));
} else {
returnedResult.setError(res.getError());
}
});
return returnedResult;
};
/**
* The chain function aids in chaining of asynchronous Results. This provides a
* convenience for use cases where asynchronous operations must happen serially
* i.e. subsequent asynchronous operations are dependent on data returned by
* prior asynchronous operations.
*
* It accepts a result and an action callback as arguments and returns a
* result. The action callback is called when the first result succeeds and is
* supposed to return a second result. The returned result is resolved when one
* of both of the results resolve (depending on their success or failure.) The
* state and value of the returned result in the various cases is documented
* below:
* <pre>
*
* First Result State: Second Result State: Returned Result State:
* SUCCESS SUCCESS SUCCESS
* SUCCESS ERROR ERROR
* ERROR Not created ERROR
* </pre>
*
* The value of the returned result, in the case both results succeed, is the
* value of the second result (the result returned by the action callback.)
*
* Example:
* <pre>
*
* var testDataResult = xhr.get('testdata/xhr_test_text.data');
*
* // Chain this result to perform another asynchronous operation when this
* // Result is resolved.
* var chainedResult = goog.result.chain(testDataResult,
* function(testDataResult) {
*
* // The result value of testDataResult is the URL for JSON data.
* var jsonDataUrl = testDataResult.getValue();
*
* // Create a new Result object when the original result is resolved.
* var jsonResult = xhr.getJson(jsonDataUrl);
*
* // Return the newly created Result.
* return jsonResult;
* });
*
* // The chained result resolves to success when both results resolve to
* // success.
* goog.result.waitOnSuccess(chainedResult, function(resultValue, result) {
*
* // At this point, both results have succeeded and we can use the JSON
* // data returned by the second asynchronous call.
* var jsonData = resultValue;
* assertEquals('ok', jsonData['stat']);
* });
*
* // Attach the error handler to be called when either Result fails.
* goog.result.waitOnError(chainedResult, function(result) {
* alert('chained result failed!');
* });
* </pre>
*
* @param {!goog.result.Result} result The result to chain.
* @param {function(this:T, !goog.result.Result):!goog.result.Result}
* actionCallback The callback called when the result is resolved. This
* callback must return a Result.
* @param {T=} opt_scope Optional scope for the action callback.
* @return {!goog.result.DependentResult} A result that is resolved when both
* the given Result and the Result returned by the actionCallback have
* resolved.
* @template T
*/
goog.result.chain = function(result, actionCallback, opt_scope) {
var dependentResult = new goog.result.DependentResultImpl_([result]);
// Wait for the first action.
goog.result.wait(result, function(result) {
if (result.getState() == goog.result.Result.State.SUCCESS) {
// The first action succeeded. Chain the contingent action.
var contingentResult = actionCallback.call(opt_scope, result);
dependentResult.addParentResult(contingentResult);
goog.result.wait(contingentResult, function(contingentResult) {
// The contingent action completed. Set the dependent result based on
// the contingent action's outcome.
if (contingentResult.getState() ==
goog.result.Result.State.SUCCESS) {
dependentResult.setValue(contingentResult.getValue());
} else {
dependentResult.setError(contingentResult.getError());
}
});
} else {
// First action failed, the dependent result should also fail.
dependentResult.setError(result.getError());
}
});
return dependentResult;
};
/**
* Returns a result that waits on all given results to resolve. Once all have
* resolved, the returned result will succeed (and never error).
*
* Example:
* <pre>
*
* var result1 = xhr.get('testdata/xhr_test_text.data');
*
* // Get a second independent Result.
* var result2 = xhr.getJson('testdata/xhr_test_json.data');
*
* // Create a Result that resolves when both prior results resolve.
* var combinedResult = goog.result.combine(result1, result2);
*
* // Process data after resolution of both results.
* goog.result.waitOnSuccess(combinedResult, function(results) {
* goog.array.forEach(results, function(result) {
* alert(result.getState());
* });
* });
* </pre>
*
* @param {...!goog.result.Result} var_args The results to wait on.
*
* @return {!goog.result.DependentResult} A new Result whose eventual value will
* be the resolved given Result objects.
*/
goog.result.combine = function(var_args) {
/** @type {!Array<!goog.result.Result>} */
var results = goog.array.clone(arguments);
var combinedResult = new goog.result.DependentResultImpl_(results);
var isResolved = function(res) {
return res.getState() != goog.result.Result.State.PENDING;
};
var checkResults = function() {
if (combinedResult.getState() == goog.result.Result.State.PENDING &&
goog.array.every(results, isResolved)) {
combinedResult.setValue(results);
}
};
goog.array.forEach(results, function(result) {
goog.result.wait(result, checkResults);
});
return combinedResult;
};
/**
* Returns a result that waits on all given results to resolve. Once all have
* resolved, the returned result will succeed if and only if all given results
* succeeded. Otherwise it will error.
*
* Example:
* <pre>
*
* var result1 = xhr.get('testdata/xhr_test_text.data');
*
* // Get a second independent Result.
* var result2 = xhr.getJson('testdata/xhr_test_json.data');
*
* // Create a Result that resolves when both prior results resolve.
* var combinedResult = goog.result.combineOnSuccess(result1, result2);
*
* // Process data after successful resolution of both results.
* goog.result.waitOnSuccess(combinedResult, function(results) {
* var textData = results[0].getValue();
* var jsonData = results[1].getValue();
* assertEquals('Just some data.', textData);
* assertEquals('ok', jsonData['stat']);
* });
*
* // Handle errors when either or both results failed.
* goog.result.waitOnError(combinedResult, function(combined) {
* var results = combined.getError();
*
* if (results[0].getState() == goog.result.Result.State.ERROR) {
* alert('result1 failed');
* }
*
* if (results[1].getState() == goog.result.Result.State.ERROR) {
* alert('result2 failed');
* }
* });
* </pre>
*
* @param {...!goog.result.Result} var_args The results to wait on.
*
* @return {!goog.result.DependentResult} A new Result whose eventual value will
* be an array of values of the given Result objects.
*/
goog.result.combineOnSuccess = function(var_args) {
var results = goog.array.clone(arguments);
var combinedResult = new goog.result.DependentResultImpl_(results);
var resolvedSuccessfully = function(res) {
return res.getState() == goog.result.Result.State.SUCCESS;
};
goog.result.wait(
goog.result.combine.apply(goog.result.combine, results),
// The combined result never ERRORs
function(res) {
var results = /** @type {Array<!goog.result.Result>} */ (
res.getValue());
if (goog.array.every(results, resolvedSuccessfully)) {
combinedResult.setValue(results);
} else {
combinedResult.setError(results);
}
});
return combinedResult;
};
/**
* Given a DependentResult, cancels the Results it depends on (that is, the
* results returned by getParentResults). This function does not recurse,
* so e.g. parents of parents are not canceled; only the immediate parents of
* the given Result are canceled.
*
* Example using @see goog.result.combine:
* <pre>
* var result1 = xhr.get('testdata/xhr_test_text.data');
*
* // Get a second independent Result.
* var result2 = xhr.getJson('testdata/xhr_test_json.data');
*
* // Create a Result that resolves when both prior results resolve.
* var combinedResult = goog.result.combineOnSuccess(result1, result2);
*
* combinedResult.wait(function() {
* if (combinedResult.isCanceled()) {
* goog.result.cancelParentResults(combinedResult);
* }
* });
*
* // Now, canceling combinedResult will cancel both result1 and result2.
* combinedResult.cancel();
* </pre>
* @param {!goog.result.DependentResult} dependentResult A Result that is
* dependent on the values of other Results (for example the Result of a
* goog.result.combine, goog.result.chain, or goog.result.transform call).
* @return {boolean} True if any results were successfully canceled; otherwise
* false.
* TODO(user): Implement a recursive version of this that cancels all
* ancestor results.
*/
goog.result.cancelParentResults = function(dependentResult) {
var anyCanceled = false;
var results = dependentResult.getParentResults();
for (var n = 0; n < results.length; n++) {
anyCanceled |= results[n].cancel();
}
return !!anyCanceled;
};
/**
* A DependentResult represents a Result whose eventual value depends on the
* value of one or more other Results. For example, the Result returned by
* @see goog.result.chain or @see goog.result.combine is dependent on the
* Results given as arguments.
*
* @param {!Array<!goog.result.Result>} parentResults A list of Results that
* will affect the eventual value of this Result.
* @constructor
* @implements {goog.result.DependentResult}
* @extends {goog.result.SimpleResult}
* @private
*/
goog.result.DependentResultImpl_ = function(parentResults) {
goog.result.DependentResultImpl_.base(this, 'constructor');
/**
* A list of Results that will affect the eventual value of this Result.
* @type {!Array<!goog.result.Result>}
* @private
*/
this.parentResults_ = parentResults;
};
goog.inherits(goog.result.DependentResultImpl_, goog.result.SimpleResult);
/**
* Adds a Result to the list of Results that affect this one.
* @param {!goog.result.Result} parentResult A result whose value affects the
* value of this Result.
*/
goog.result.DependentResultImpl_.prototype.addParentResult = function(
parentResult) {
this.parentResults_.push(parentResult);
};
/** @override */
goog.result.DependentResultImpl_.prototype.getParentResults = function() {
return this.parentResults_;
};