Adding mapbox-gl branch

This commit is contained in:
Andreas Hocevar
2015-03-16 18:50:27 +01:00
parent 7985f030fa
commit 57ee7f52fd
3109 changed files with 943365 additions and 0 deletions
@@ -0,0 +1,94 @@
// 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 Simple image loader, used for preloading.
* @author nnaze@google.com (Nathan Naze)
*/
goog.provide('goog.labs.net.image');
goog.require('goog.Promise');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.net.EventType');
goog.require('goog.userAgent');
/**
* Loads a single image. Useful for preloading images.
*
* @param {string} uri URI of the image.
* @param {(!Image|function(): !Image)=} opt_image If present, instead of
* creating a new Image instance the function will use the passed Image
* instance or the result of calling the Image factory respectively. This
* can be used to control exactly how Image instances are created, for
* example if they should be created in a particular document element, or
* have fields that will trigger CORS image fetches.
* @return {!goog.Promise<!Image>} A Promise that will be resolved with the
* given image if the image successfully loads.
*/
goog.labs.net.image.load = function(uri, opt_image) {
return new goog.Promise(function(resolve, reject) {
var image;
if (!goog.isDef(opt_image)) {
image = new Image();
} else if (goog.isFunction(opt_image)) {
image = opt_image();
} else {
image = opt_image;
}
// IE's load event on images can be buggy. For older browsers, wait for
// readystatechange events and check if readyState is 'complete'.
// See:
// http://msdn.microsoft.com/en-us/library/ie/ms536957(v=vs.85).aspx
// http://msdn.microsoft.com/en-us/library/ie/ms534359(v=vs.85).aspx
//
// Starting with IE11, start using standard 'load' events.
// See:
// http://msdn.microsoft.com/en-us/library/ie/dn467845(v=vs.85).aspx
var loadEvent = (goog.userAgent.IE && goog.userAgent.VERSION < 11) ?
goog.net.EventType.READY_STATE_CHANGE : goog.events.EventType.LOAD;
var handler = new goog.events.EventHandler();
handler.listen(
image,
[loadEvent, goog.net.EventType.ABORT, goog.net.EventType.ERROR],
function(e) {
// We only registered listeners for READY_STATE_CHANGE for IE.
// If readyState is now COMPLETE, the image has loaded.
// See related comment above.
if (e.type == goog.net.EventType.READY_STATE_CHANGE &&
image.readyState != goog.net.EventType.COMPLETE) {
return;
}
// At this point, we know whether the image load was successful
// and no longer care about image events.
goog.dispose(handler);
// Whether the image successfully loaded.
if (e.type == loadEvent) {
resolve(image);
} else {
reject(null);
}
});
// Initiate the image request.
image.src = uri;
});
};
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
Author: nnaze@google.com (Nathan Naze)
-->
<head>
<title>Closure Unit Tests - goog.labs.net.image</title>
<script src="../../base.js"></script>
</head>
<body>
<script>
goog.require('goog.labs.net.imageTest');
</script>
</body>
</html>
@@ -0,0 +1,87 @@
// 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 Unit tests for goog.labs.net.Image.
*
* @author nnaze@google.com (Nathan Naze)
*/
/** @suppress {extraProvide} */
goog.provide('goog.labs.net.imageTest');
goog.require('goog.labs.net.image');
goog.require('goog.string');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
goog.setTestOnly('goog.labs.net.ImageTest');
var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
function testValidImage() {
var url = 'testdata/cleardot.gif';
asyncTestCase.waitForAsync('image load');
goog.labs.net.image.load(url).then(function(value) {
assertEquals('IMG', value.tagName);
assertTrue(goog.string.endsWith(value.src, url));
asyncTestCase.continueTesting();
});
}
function testInvalidImage() {
var url = 'testdata/invalid.gif'; // This file does not exist.
asyncTestCase.waitForAsync('image load');
goog.labs.net.image.load(url).then(
fail /* opt_onResolved */,
function() {
asyncTestCase.continueTesting();
});
}
function testImageFactory() {
var returnedImage = new Image();
var factory = function() {
return returnedImage;
};
var countedFactory = goog.testing.recordFunction(factory);
var url = 'testdata/cleardot.gif';
asyncTestCase.waitForAsync('image load');
goog.labs.net.image.load(url, countedFactory).then(function(value) {
assertEquals(returnedImage, value);
assertEquals(1, countedFactory.getCallCount());
asyncTestCase.continueTesting();
});
}
function testExistingImage() {
var image = new Image();
var url = 'testdata/cleardot.gif';
asyncTestCase.waitForAsync('image load');
goog.labs.net.image.load(url, image).then(function(value) {
assertEquals(image, value);
asyncTestCase.continueTesting();
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

@@ -0,0 +1,2 @@
while(1);
{"stat":"ok","count":12345}
@@ -0,0 +1 @@
Just some data.
@@ -0,0 +1,302 @@
// Copyright 2013 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 The API spec for the WebChannel messaging library.
*
* Similar to HTML5 WebSocket and Closure BrowserChannel, WebChannel
* offers an abstraction for point-to-point socket-like communication between
* a browser client and a remote origin.
*
* WebChannels are created via <code>WebChannel</code>. Multiple WebChannels
* may be multiplexed over the same WebChannelTransport, which represents
* the underlying physical connectivity over standard wire protocols
* such as HTTP and SPDY.
*
* A WebChannels in turn represents a logical communication channel between
* the client and server end point. A WebChannel remains open for
* as long as the client or server end-point allows.
*
* Messages may be delivered in-order or out-of-order, reliably or unreliably
* over the same WebChannel. Message delivery guarantees of a WebChannel is
* to be specified by the application code; and the choice of the
* underlying wire protocols is completely transparent to the API users.
*
* Client-to-client messaging via WebRTC based transport may also be support
* via the same WebChannel API in future.
*
* Note that we have no immediate plan to move this API out of labs. While
* the implementation is production ready, the API is subject to change
* (addition):
* 1. Completely new W3C APIs for Web messaging may emerge in near future.
* 2. New programming models for cloud (on the server-side) may require
* new APIs to be defined.
* 3. WebRTC DataChannel alignment
* Lastly, we also want to white-list all internal use cases. As a general rule,
* we expect most applications to rely on stateless/RPC services.
*
*/
goog.provide('goog.net.WebChannel');
goog.require('goog.events');
goog.require('goog.events.Event');
/**
* A WebChannel represents a logical bi-directional channel over which the
* client communicates with a remote server that holds the other endpoint
* of the channel. A WebChannel is always created in the context of a shared
* {@link WebChannelTransport} instance. It is up to the underlying client-side
* and server-side implementations to decide how or when multiplexing is
* to be enabled.
*
* @interface
* @extends {EventTarget}
*/
goog.net.WebChannel = function() {};
/**
* Configuration spec for newly created WebChannel instances.
*
* WebChannels are configured in the context of the containing
* {@link WebChannelTransport}. The configuration parameters are specified
* when a new instance of WebChannel is created via {@link WebChannelTransport}.
*
* messageHeaders: custom headers to be added to every message sent to the
* server.
*
* messageUrlParams: custom url query parameters to be added to every message
* sent to the server.
*
* clientProtocolHeaderRequired: whether a special header should be added to
* each message so that the server can dispatch webchannel messages without
* knowing the URL path prefix. Defaults to false.
*
* concurrentRequestLimit: the maximum number of in-flight HTTP requests allowed
* when SPDY is enabled. Currently we only detect SPDY in Chrome.
* This parameter defaults to 10. When SPDY is not enabled, this parameter
* will have no effect.
*
* supportsCrossDomainXhr: setting this to true to allow the use of sub-domains
* (as configured by the server) to send XHRs with the CORS withCredentials
* bit set to true.
*
* testUrl: the test URL for detecting connectivity during the initial
* handshake. This parameter defaults to "/<channel_url>/test".
*
*
* @typedef {{
* messageHeaders: (!Object<string, string>|undefined),
* messageUrlParams: (!Object<string, string>|undefined),
* clientProtocolHeaderRequired: (boolean|undefined),
* concurrentRequestLimit: (number|undefined),
* supportsCrossDomainXhr: (boolean|undefined),
* testUrl: (string|undefined)
* }}
*/
goog.net.WebChannel.Options;
/**
* Types that are allowed as message data.
*
* @typedef {(ArrayBuffer|Blob|Object<string, string>|Array)}
*/
goog.net.WebChannel.MessageData;
/**
* Open the WebChannel against the URI specified in the constructor.
*/
goog.net.WebChannel.prototype.open = goog.abstractMethod;
/**
* Close the WebChannel.
*/
goog.net.WebChannel.prototype.close = goog.abstractMethod;
/**
* Sends a message to the server that maintains the other end point of
* the WebChannel.
*
* @param {!goog.net.WebChannel.MessageData} message The message to send.
*/
goog.net.WebChannel.prototype.send = goog.abstractMethod;
/**
* Common events fired by WebChannels.
* @enum {string}
*/
goog.net.WebChannel.EventType = {
/** Dispatched when the channel is opened. */
OPEN: goog.events.getUniqueId('open'),
/** Dispatched when the channel is closed. */
CLOSE: goog.events.getUniqueId('close'),
/** Dispatched when the channel is aborted due to errors. */
ERROR: goog.events.getUniqueId('error'),
/** Dispatched when the channel has received a new message. */
MESSAGE: goog.events.getUniqueId('message')
};
/**
* The event interface for the MESSAGE event.
*
* @constructor
* @extends {goog.events.Event}
*/
goog.net.WebChannel.MessageEvent = function() {
goog.net.WebChannel.MessageEvent.base(
this, 'constructor', goog.net.WebChannel.EventType.MESSAGE);
};
goog.inherits(goog.net.WebChannel.MessageEvent, goog.events.Event);
/**
* The content of the message received from the server.
*
* @type {!goog.net.WebChannel.MessageData}
*/
goog.net.WebChannel.MessageEvent.prototype.data;
/**
* WebChannel level error conditions.
* @enum {number}
*/
goog.net.WebChannel.ErrorStatus = {
/** No error has occurred. */
OK: 0,
/** Communication to the server has failed. */
NETWORK_ERROR: 1,
/** The server fails to accept the WebChannel. */
SERVER_ERROR: 2
};
/**
* The event interface for the ERROR event.
*
* @constructor
* @extends {goog.events.Event}
*/
goog.net.WebChannel.ErrorEvent = function() {
goog.net.WebChannel.ErrorEvent.base(
this, 'constructor', goog.net.WebChannel.EventType.ERROR);
};
goog.inherits(goog.net.WebChannel.ErrorEvent, goog.events.Event);
/**
* The error status.
*
* @type {!goog.net.WebChannel.ErrorStatus}
*/
goog.net.WebChannel.ErrorEvent.prototype.status;
/**
* @return {!goog.net.WebChannel.RuntimeProperties} The runtime properties
* of the WebChannel instance.
*/
goog.net.WebChannel.prototype.getRuntimeProperties = goog.abstractMethod;
/**
* The readonly runtime properties of the WebChannel instance.
*
* This class is defined for debugging and monitoring purposes, and for
* optimization functions that the application may choose to manage by itself.
*
* @interface
*/
goog.net.WebChannel.RuntimeProperties = function() {};
/**
* @return {number} The effective limit for the number of concurrent HTTP
* requests that are allowed to be made for sending messages from the client
* to the server. When SPDY is not enabled, this limit will be one.
*/
goog.net.WebChannel.RuntimeProperties.prototype.getConcurrentRequestLimit =
goog.abstractMethod;
/**
* For applications that need support multiple channels (e.g. from
* different tabs) to the same origin, use this method to decide if SPDY is
* enabled and therefore it is safe to open multiple channels.
*
* If SPDY is disabled, the application may choose to limit the number of active
* channels to one or use other means such as sub-domains to work around
* the browser connection limit.
*
* @return {boolean} Whether SPDY is enabled for the origin against which
* the channel is created.
*/
goog.net.WebChannel.RuntimeProperties.prototype.isSpdyEnabled =
goog.abstractMethod;
/**
* This method may be used by the application to stop ack of received messages
* as a means of enabling or disabling flow-control on the server-side.
*
* @param {boolean} enabled If true, enable flow-control behavior on the
* server side. Setting it to false will cancel ay previous enabling action.
*/
goog.net.WebChannel.RuntimeProperties.prototype.setServerFlowControl =
goog.abstractMethod;
/**
* This method may be used by the application to throttle the rate of outgoing
* messages, as a means of sender initiated flow-control.
*
* @return {number} The total number of messages that have not received
* ack from the server and therefore remain in the buffer.
*/
goog.net.WebChannel.RuntimeProperties.prototype.getNonAckedMessageCount =
goog.abstractMethod;
/**
* A special header to indicate to the server what messaging protocol
* each HTTP message is speaking.
*
* @type {string}
*/
goog.net.WebChannel.X_CLIENT_PROTOCOL = 'X-Client-Protocol';
/**
* The value for x-client-protocol when the messaging protocol is WebChannel.
*
* @type {string}
*/
goog.net.WebChannel.X_CLIENT_PROTOCOL_WEB_CHANNEL = 'webchannel';
@@ -0,0 +1,519 @@
// Copyright 2006 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 Base TestChannel implementation.
*
*/
goog.provide('goog.labs.net.webChannel.BaseTestChannel');
goog.require('goog.labs.net.webChannel.Channel');
goog.require('goog.labs.net.webChannel.ChannelRequest');
goog.require('goog.labs.net.webChannel.requestStats');
goog.require('goog.labs.net.webChannel.requestStats.Stat');
/**
* A TestChannel is used during the first part of channel negotiation
* with the server to create the channel. It helps us determine whether we're
* behind a buffering proxy.
*
* @constructor
* @struct
* @param {!goog.labs.net.webChannel.Channel} channel The channel
* that owns this test channel.
* @param {!goog.labs.net.webChannel.WebChannelDebug} channelDebug A
* WebChannelDebug instance to use for logging.
* @implements {goog.labs.net.webChannel.Channel}
*/
goog.labs.net.webChannel.BaseTestChannel = function(channel, channelDebug) {
/**
* The channel that owns this test channel
* @private {!goog.labs.net.webChannel.Channel}
*/
this.channel_ = channel;
/**
* The channel debug to use for logging
* @private {!goog.labs.net.webChannel.WebChannelDebug}
*/
this.channelDebug_ = channelDebug;
/**
* Extra HTTP headers to add to all the requests sent to the server.
* @private {Object}
*/
this.extraHeaders_ = null;
/**
* The test request.
* @private {goog.labs.net.webChannel.ChannelRequest}
*/
this.request_ = null;
/**
* Whether we have received the first result as an intermediate result. This
* helps us determine whether we're behind a buffering proxy.
* @private {boolean}
*/
this.receivedIntermediateResult_ = false;
/**
* The time when the test request was started. We use timing in IE as
* a heuristic for whether we're behind a buffering proxy.
* @private {?number}
*/
this.startTime_ = null;
/**
* The time for of the first result part. We use timing in IE as a
* heuristic for whether we're behind a buffering proxy.
* @private {?number}
*/
this.firstTime_ = null;
/**
* The time for of the last result part. We use timing in IE as a
* heuristic for whether we're behind a buffering proxy.
* @private {?number}
*/
this.lastTime_ = null;
/**
* The relative path for test requests.
* @private {?string}
*/
this.path_ = null;
/**
* The last status code received.
* @private {number}
*/
this.lastStatusCode_ = -1;
/**
* A subdomain prefix for using a subdomain in IE for the backchannel
* requests.
* @private {?string}
*/
this.hostPrefix_ = null;
/**
* The effective client protocol as indicated by the initial handshake
* response via the x-client-wire-protocol header.
*
* @private {?string}
*/
this.clientProtocol_ = null;
};
goog.scope(function() {
var BaseTestChannel = goog.labs.net.webChannel.BaseTestChannel;
var WebChannelDebug = goog.labs.net.webChannel.WebChannelDebug;
var ChannelRequest = goog.labs.net.webChannel.ChannelRequest;
var requestStats = goog.labs.net.webChannel.requestStats;
var Channel = goog.labs.net.webChannel.Channel;
/**
* Enum type for the test channel state machine
* @enum {number}
* @private
*/
BaseTestChannel.State_ = {
/**
* The state for the TestChannel state machine where we making the
* initial call to get the server configured parameters.
*/
INIT: 0,
/**
* The state for the TestChannel state machine where we're checking to
* se if we're behind a buffering proxy.
*/
CONNECTION_TESTING: 1
};
/**
* The state of the state machine for this object.
*
* @private {?BaseTestChannel.State_}
*/
BaseTestChannel.prototype.state_ = null;
/**
* Time between chunks in the test connection that indicates that we
* are not behind a buffering proxy. This value should be less than or
* equals to the time between chunks sent from the server.
* @type {number}
* @private
*/
BaseTestChannel.MIN_TIME_EXPECTED_BETWEEN_DATA_ = 500;
/**
* Sets extra HTTP headers to add to all the requests sent to the server.
*
* @param {Object} extraHeaders The HTTP headers.
*/
BaseTestChannel.prototype.setExtraHeaders = function(extraHeaders) {
this.extraHeaders_ = extraHeaders;
};
/**
* Starts the test channel. This initiates connections to the server.
*
* @param {string} path The relative uri for the test connection.
*/
BaseTestChannel.prototype.connect = function(path) {
this.path_ = path;
var sendDataUri = this.channel_.getForwardChannelUri(this.path_);
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_ONE_START);
this.startTime_ = goog.now();
// If the channel already has the result of the handshake, then skip it.
var handshakeResult = this.channel_.getConnectionState().handshakeResult;
if (goog.isDefAndNotNull(handshakeResult)) {
this.hostPrefix_ = this.channel_.correctHostPrefix(handshakeResult[0]);
this.state_ = BaseTestChannel.State_.CONNECTION_TESTING;
this.checkBufferingProxy_();
return;
}
// the first request returns server specific parameters
sendDataUri.setParameterValues('MODE', 'init');
this.request_ = ChannelRequest.createChannelRequest(this, this.channelDebug_);
this.request_.setExtraHeaders(this.extraHeaders_);
this.request_.xmlHttpGet(sendDataUri, false /* decodeChunks */,
null /* hostPrefix */, true /* opt_noClose */);
this.state_ = BaseTestChannel.State_.INIT;
};
/**
* Begins the second stage of the test channel where we test to see if we're
* behind a buffering proxy. The server sends back a multi-chunked response
* with the first chunk containing the content '1' and then two seconds later
* sending the second chunk containing the content '2'. Depending on how we
* receive the content, we can tell if we're behind a buffering proxy.
* @private
*/
BaseTestChannel.prototype.checkBufferingProxy_ = function() {
this.channelDebug_.debug('TestConnection: starting stage 2');
// If the test result is already available, skip its execution.
var bufferingProxyResult =
this.channel_.getConnectionState().bufferingProxyResult;
if (goog.isDefAndNotNull(bufferingProxyResult)) {
this.channelDebug_.debug(
'TestConnection: skipping stage 2, precomputed result is ' +
bufferingProxyResult ? 'Buffered' : 'Unbuffered');
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_START);
if (bufferingProxyResult) { // Buffered/Proxy connection
requestStats.notifyStatEvent(requestStats.Stat.PROXY);
this.channel_.testConnectionFinished(this, false);
} else { // Unbuffered/NoProxy connection
requestStats.notifyStatEvent(requestStats.Stat.NOPROXY);
this.channel_.testConnectionFinished(this, true);
}
return; // Skip the test
}
this.request_ = ChannelRequest.createChannelRequest(this, this.channelDebug_);
this.request_.setExtraHeaders(this.extraHeaders_);
var recvDataUri = this.channel_.getBackChannelUri(this.hostPrefix_,
/** @type {string} */ (this.path_));
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_START);
if (!ChannelRequest.supportsXhrStreaming()) {
recvDataUri.setParameterValues('TYPE', 'html');
this.request_.tridentGet(recvDataUri, Boolean(this.hostPrefix_));
} else {
recvDataUri.setParameterValues('TYPE', 'xmlhttp');
this.request_.xmlHttpGet(recvDataUri, false /** decodeChunks */,
this.hostPrefix_, false /** opt_noClose */);
}
};
/**
* @override
*/
BaseTestChannel.prototype.createXhrIo = function(hostPrefix) {
return this.channel_.createXhrIo(hostPrefix);
};
/**
* Aborts the test channel.
*/
BaseTestChannel.prototype.abort = function() {
if (this.request_) {
this.request_.cancel();
this.request_ = null;
}
this.lastStatusCode_ = -1;
};
/**
* Returns whether the test channel is closed. The ChannelRequest object expects
* this method to be implemented on its handler.
*
* @return {boolean} Whether the channel is closed.
* @override
*/
BaseTestChannel.prototype.isClosed = function() {
return false;
};
/**
* Callback from ChannelRequest for when new data is received
*
* @param {ChannelRequest} req The request object.
* @param {string} responseText The text of the response.
* @override
*/
BaseTestChannel.prototype.onRequestData = function(req, responseText) {
this.lastStatusCode_ = req.getLastStatusCode();
if (this.state_ == BaseTestChannel.State_.INIT) {
this.channelDebug_.debug('TestConnection: Got data for stage 1');
if (!responseText) {
this.channelDebug_.debug('TestConnection: Null responseText');
// The server should always send text; something is wrong here
this.channel_.testConnectionFailure(this, ChannelRequest.Error.BAD_DATA);
return;
}
/** @preserveTry */
try {
var respArray = this.channel_.getWireCodec().decodeMessage(responseText);
} catch (e) {
this.channelDebug_.dumpException(e);
this.channel_.testConnectionFailure(this, ChannelRequest.Error.BAD_DATA);
return;
}
this.hostPrefix_ = this.channel_.correctHostPrefix(respArray[0]);
} else if (this.state_ == BaseTestChannel.State_.CONNECTION_TESTING) {
if (this.receivedIntermediateResult_) {
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_DATA_TWO);
this.lastTime_ = goog.now();
} else {
// '11111' is used instead of '1' to prevent a small amount of buffering
// by Safari.
if (responseText == '11111') {
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_DATA_ONE);
this.receivedIntermediateResult_ = true;
this.firstTime_ = goog.now();
if (this.checkForEarlyNonBuffered_()) {
// If early chunk detection is on, and we passed the tests,
// assume HTTP_OK, cancel the test and turn on noproxy mode.
this.lastStatusCode_ = 200;
this.request_.cancel();
this.channelDebug_.debug(
'Test connection succeeded; using streaming connection');
requestStats.notifyStatEvent(requestStats.Stat.NOPROXY);
this.channel_.testConnectionFinished(this, true);
}
} else {
requestStats.notifyStatEvent(
requestStats.Stat.TEST_STAGE_TWO_DATA_BOTH);
this.firstTime_ = this.lastTime_ = goog.now();
this.receivedIntermediateResult_ = false;
}
}
}
};
/**
* Callback from ChannelRequest that indicates a request has completed.
*
* @param {!ChannelRequest} req The request object.
* @override
*/
BaseTestChannel.prototype.onRequestComplete = function(req) {
this.lastStatusCode_ = this.request_.getLastStatusCode();
if (!this.request_.getSuccess()) {
this.channelDebug_.debug(
'TestConnection: request failed, in state ' + this.state_);
if (this.state_ == BaseTestChannel.State_.INIT) {
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_ONE_FAILED);
} else if (this.state_ == BaseTestChannel.State_.CONNECTION_TESTING) {
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_FAILED);
}
this.channel_.testConnectionFailure(this,
/** @type {ChannelRequest.Error} */
(this.request_.getLastError()));
return;
}
if (this.state_ == BaseTestChannel.State_.INIT) {
this.recordClientProtocol_(req);
this.state_ = BaseTestChannel.State_.CONNECTION_TESTING;
this.channelDebug_.debug(
'TestConnection: request complete for initial check');
this.checkBufferingProxy_();
} else if (this.state_ == BaseTestChannel.State_.CONNECTION_TESTING) {
this.channelDebug_.debug('TestConnection: request complete for stage 2');
var goodConn = false;
if (!ChannelRequest.supportsXhrStreaming()) {
// we always get Trident responses in separate calls to
// onRequestData, so we have to check the time they came
var ms = this.lastTime_ - this.firstTime_;
if (ms < 200) {
// TODO: need to empirically verify that this number is OK
// for slow computers
goodConn = false;
} else {
goodConn = true;
}
} else {
goodConn = this.receivedIntermediateResult_;
}
if (goodConn) {
this.channelDebug_.debug(
'Test connection succeeded; using streaming connection');
requestStats.notifyStatEvent(requestStats.Stat.NOPROXY);
this.channel_.testConnectionFinished(this, true);
} else {
this.channelDebug_.debug(
'Test connection failed; not using streaming');
requestStats.notifyStatEvent(requestStats.Stat.PROXY);
this.channel_.testConnectionFinished(this, false);
}
}
};
/**
* Record the client protocol header from the initial handshake response.
*
* @param {!ChannelRequest} req The request object.
* @private
*/
BaseTestChannel.prototype.recordClientProtocol_ = function(req) {
var xmlHttp = req.getXhr();
if (xmlHttp) {
var protocolHeader = xmlHttp.getResponseHeader('x-client-wire-protocol');
this.clientProtocol_ = protocolHeader ? protocolHeader : null;
}
};
/**
* @return {?string} The client protocol as recorded with the init handshake
* request.
*/
BaseTestChannel.prototype.getClientProtocol = function() {
return this.clientProtocol_;
};
/**
* Returns the last status code received for a request.
* @return {number} The last status code received for a request.
*/
BaseTestChannel.prototype.getLastStatusCode = function() {
return this.lastStatusCode_;
};
/**
* @return {boolean} Whether we should be using secondary domains when the
* server instructs us to do so.
* @override
*/
BaseTestChannel.prototype.shouldUseSecondaryDomains = function() {
return this.channel_.shouldUseSecondaryDomains();
};
/**
* @override
*/
BaseTestChannel.prototype.isActive = function() {
return this.channel_.isActive();
};
/**
* @return {boolean} True if test stage 2 detected a non-buffered
* channel early and early no buffering detection is enabled.
* @private
*/
BaseTestChannel.prototype.checkForEarlyNonBuffered_ = function() {
var ms = this.firstTime_ - this.startTime_;
// we always get Trident responses in separate calls to
// onRequestData, so we have to check the time that the first came in
// and verify that the data arrived before the second portion could
// have been sent. For all other browser's we skip the timing test.
return ChannelRequest.supportsXhrStreaming() ||
ms < BaseTestChannel.MIN_TIME_EXPECTED_BETWEEN_DATA_;
};
/**
* @override
*/
BaseTestChannel.prototype.getForwardChannelUri = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.getBackChannelUri = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.correctHostPrefix = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.createDataUri = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.testConnectionFinished = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.testConnectionFailure = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.getConnectionState = goog.abstractMethod;
}); // goog.scope
@@ -0,0 +1,186 @@
// Copyright 2013 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 A shared interface for WebChannelBase and BaseTestChannel.
*
* @visibility {:internal}
*/
goog.provide('goog.labs.net.webChannel.Channel');
/**
* Shared interface between Channel and TestChannel to support callbacks
* between WebChannelBase and BaseTestChannel and between Channel and
* ChannelRequest.
*
* @interface
*/
goog.labs.net.webChannel.Channel = function() {};
goog.scope(function() {
var Channel = goog.labs.net.webChannel.Channel;
/**
* Determines whether to use a secondary domain when the server gives us
* a host prefix. This allows us to work around browser per-domain
* connection limits.
*
* Currently, we use secondary domains when using Trident's ActiveXObject,
* because it supports cross-domain requests out of the box. Note that in IE10
* we no longer use ActiveX since it's not supported in Metro mode and IE10
* supports XHR streaming.
*
* If you need to use secondary domains on other browsers and IE10,
* you have two choices:
* 1) If you only care about browsers that support CORS
* (https://developer.mozilla.org/en-US/docs/HTTP_access_control), you
* can use {@link #setSupportsCrossDomainXhrs} and set the appropriate
* CORS response headers on the server.
* 2) Or, override this method in a subclass, and make sure that those
* browsers use some messaging mechanism that works cross-domain (e.g
* iframes and window.postMessage).
*
* @return {boolean} Whether to use secondary domains.
* @see http://code.google.com/p/closure-library/issues/detail?id=339
*/
Channel.prototype.shouldUseSecondaryDomains = goog.abstractMethod;
/**
* Called when creating an XhrIo object. Override in a subclass if
* you need to customize the behavior, for example to enable the creation of
* XHR's capable of calling a secondary domain. Will also allow calling
* a secondary domain if withCredentials (CORS) is enabled.
* @param {?string} hostPrefix The host prefix, if we need an XhrIo object
* capable of calling a secondary domain.
* @return {!goog.net.XhrIo} A new XhrIo object.
*/
Channel.prototype.createXhrIo = goog.abstractMethod;
/**
* Callback from ChannelRequest that indicates a request has completed.
* @param {!goog.labs.net.webChannel.ChannelRequest} request
* The request object.
*/
Channel.prototype.onRequestComplete = goog.abstractMethod;
/**
* Returns whether the channel is closed
* @return {boolean} true if the channel is closed.
*/
Channel.prototype.isClosed = goog.abstractMethod;
/**
* Callback from ChannelRequest for when new data is received
* @param {goog.labs.net.webChannel.ChannelRequest} request
* The request object.
* @param {string} responseText The text of the response.
*/
Channel.prototype.onRequestData = goog.abstractMethod;
/**
* Gets whether this channel is currently active. This is used to determine the
* length of time to wait before retrying. This call delegates to the handler.
* @return {boolean} Whether the channel is currently active.
*/
Channel.prototype.isActive = goog.abstractMethod;
/**
* Not needed for testchannel.
*
* Gets the Uri used for the connection that sends data to the server.
* @param {string} path The path on the host.
* @return {goog.Uri} The forward channel URI.
*/
Channel.prototype.getForwardChannelUri = goog.abstractMethod;
/**
* Not needed for testchannel.
*
* Gets the Uri used for the connection that receives data from the server.
* @param {?string} hostPrefix The host prefix.
* @param {string} path The path on the host.
* @return {goog.Uri} The back channel URI.
*/
Channel.prototype.getBackChannelUri = goog.abstractMethod;
/**
* Not needed for testchannel.
*
* Allows the handler to override a host prefix provided by the server. Will
* be called whenever the channel has received such a prefix and is considering
* its use.
* @param {?string} serverHostPrefix The host prefix provided by the server.
* @return {?string} The host prefix the client should use.
*/
Channel.prototype.correctHostPrefix = goog.abstractMethod;
/**
* Not needed for testchannel.
*
* Creates a data Uri applying logic for secondary hostprefix, port
* overrides, and versioning.
* @param {?string} hostPrefix The host prefix.
* @param {string} path The path on the host (may be absolute or relative).
* @param {number=} opt_overridePort Optional override port.
* @return {goog.Uri} The data URI.
*/
Channel.prototype.createDataUri = goog.abstractMethod;
/**
* Not needed for testchannel.
*
* Callback from TestChannel for when the channel is finished.
* @param {goog.labs.net.webChannel.BaseTestChannel} testChannel
* The TestChannel.
* @param {boolean} useChunked Whether we can chunk responses.
*/
Channel.prototype.testConnectionFinished = goog.abstractMethod;
/**
* Not needed for testchannel.
*
* Callback from TestChannel for when the channel has an error.
* @param {goog.labs.net.webChannel.BaseTestChannel} testChannel
* The TestChannel.
* @param {goog.labs.net.webChannel.ChannelRequest.Error} errorCode
* The error code of the failure.
*/
Channel.prototype.testConnectionFailure = goog.abstractMethod;
/**
* Not needed for testchannel.
* Gets the result of previous connectivity tests.
*
* @return {!goog.labs.net.webChannel.ConnectionState} The connectivity state.
*/
Channel.prototype.getConnectionState = goog.abstractMethod;
}); // goog.scope
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.labs.net.webChannel.ChannelRequest</title>
<script src="../../../base.js"></script>
</head>
<body>
<script>
goog.require('goog.labs.net.webChannel.channelRequestTest');
</script>
<div id="debug" style="font-size: small"></div>
</body>
</html>
@@ -0,0 +1,310 @@
// Copyright 2013 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 Unit tests for goog.labs.net.webChannel.ChannelRequest.
*
*/
goog.provide('goog.labs.net.webChannel.channelRequestTest');
goog.require('goog.Uri');
goog.require('goog.functions');
goog.require('goog.labs.net.webChannel.ChannelRequest');
goog.require('goog.labs.net.webChannel.WebChannelDebug');
goog.require('goog.labs.net.webChannel.requestStats');
goog.require('goog.labs.net.webChannel.requestStats.ServerReachability');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.net.XhrIo');
goog.require('goog.testing.recordFunction');
goog.setTestOnly('goog.labs.net.webChannel.channelRequestTest');
var channelRequest;
var mockChannel;
var mockClock;
var stubs;
var xhrIo;
var reachabilityEvents;
/**
* Time to wait for a network request to time out, before aborting.
*/
var WATCHDOG_TIME = 2000;
/**
* Time to throttle readystatechange events.
*/
var THROTTLE_TIME = 500;
/**
* A really long time - used to make sure no more timeouts will fire.
*/
var ALL_DAY_MS = 1000 * 60 * 60 * 24;
function setUp() {
mockClock = new goog.testing.MockClock();
mockClock.install();
reachabilityEvents = {};
stubs = new goog.testing.PropertyReplacer();
// Mock out the stat notification code.
var notifyServerReachabilityEvent = function(reachabilityType) {
if (!reachabilityEvents[reachabilityType]) {
reachabilityEvents[reachabilityType] = 0;
}
reachabilityEvents[reachabilityType]++;
};
stubs.set(goog.labs.net.webChannel.requestStats,
'notifyServerReachabilityEvent', notifyServerReachabilityEvent);
}
function tearDown() {
stubs.reset();
mockClock.uninstall();
}
/**
* Constructs a duck-type WebChannelBase that tracks the completed requests.
* @constructor
* @struct
* @final
*/
function MockWebChannelBase() {
this.isClosed = function() {
return false;
};
this.isActive = function() {
return true;
};
this.shouldUseSecondaryDomains = function() {
return false;
};
this.completedRequests = [];
this.onRequestComplete = function(request) {
this.completedRequests.push(request);
};
this.onRequestData = function(request, data) {};
}
/**
* Creates a real ChannelRequest object, with some modifications for
* testability:
* <ul>
* <li>The channel is a mock channel.
* <li>The new watchdogTimeoutCallCount property tracks onWatchDogTimeout_()
* calls.
* <li>The timeout is set to WATCHDOG_TIME.
* </ul>
*/
function createChannelRequest() {
xhrIo = new goog.testing.net.XhrIo();
xhrIo.abort = xhrIo.abort || function() {
this.active_ = false;
};
// Install mock channel and no-op debug logger.
mockChannel = new MockWebChannelBase();
channelRequest = new goog.labs.net.webChannel.ChannelRequest(
mockChannel,
new goog.labs.net.webChannel.WebChannelDebug());
// Install test XhrIo.
mockChannel.createXhrIo = function() {
return xhrIo;
};
// Install watchdogTimeoutCallCount.
channelRequest.watchdogTimeoutCallCount = 0;
channelRequest.originalOnWatchDogTimeout = channelRequest.onWatchDogTimeout_;
channelRequest.onWatchDogTimeout_ = function() {
channelRequest.watchdogTimeoutCallCount++;
return channelRequest.originalOnWatchDogTimeout();
};
channelRequest.setTimeout(WATCHDOG_TIME);
}
/**
* Run through the lifecycle of a long lived request, checking that the right
* network events are reported.
*/
function testNetworkEvents() {
createChannelRequest();
channelRequest.xmlHttpPost(new goog.Uri('some_uri'), 'some_postdata', true);
checkReachabilityEvents(1, 0, 0, 0);
if (goog.labs.net.webChannel.ChannelRequest.supportsXhrStreaming()) {
xhrIo.simulatePartialResponse('17\nI am a BC Message');
checkReachabilityEvents(1, 0, 0, 1);
xhrIo.simulatePartialResponse('23\nI am another BC Message');
checkReachabilityEvents(1, 0, 0, 2);
xhrIo.simulateResponse(200, '16\Final BC Message');
checkReachabilityEvents(1, 1, 0, 2);
} else {
xhrIo.simulateResponse(200, '16\Final BC Message');
checkReachabilityEvents(1, 1, 0, 0);
}
}
/**
* Test throttling of readystatechange events.
*/
function testNetworkEvents_throttleReadyStateChange() {
createChannelRequest();
channelRequest.setReadyStateChangeThrottle(THROTTLE_TIME);
var recordedHandler =
goog.testing.recordFunction(channelRequest.xmlHttpHandler_);
stubs.set(channelRequest, 'xmlHttpHandler_', recordedHandler);
channelRequest.xmlHttpPost(new goog.Uri('some_uri'), 'some_postdata', true);
assertEquals(1, recordedHandler.getCallCount());
checkReachabilityEvents(1, 0, 0, 0);
if (goog.labs.net.webChannel.ChannelRequest.supportsXhrStreaming()) {
xhrIo.simulatePartialResponse('17\nI am a BC Message');
checkReachabilityEvents(1, 0, 0, 1);
assertEquals(3, recordedHandler.getCallCount());
// Second event should be throttled
xhrIo.simulatePartialResponse('23\nI am another BC Message');
assertEquals(3, recordedHandler.getCallCount());
xhrIo.simulatePartialResponse('27\nI am yet another BC Message');
assertEquals(3, recordedHandler.getCallCount());
mockClock.tick(THROTTLE_TIME);
checkReachabilityEvents(1, 0, 0, 3);
// Only one more call because of throttling.
assertEquals(4, recordedHandler.getCallCount());
xhrIo.simulateResponse(200, '16\Final BC Message');
checkReachabilityEvents(1, 1, 0, 3);
assertEquals(5, recordedHandler.getCallCount());
} else {
xhrIo.simulateResponse(200, '16\Final BC Message');
checkReachabilityEvents(1, 1, 0, 0);
}
}
/**
* Make sure that the request "completes" with an error when the timeout
* expires.
*/
function testRequestTimeout() {
createChannelRequest();
channelRequest.xmlHttpPost(new goog.Uri('some_uri'), 'some_postdata', true);
assertEquals(0, channelRequest.watchdogTimeoutCallCount);
assertEquals(0, channelRequest.channel_.completedRequests.length);
// Watchdog timeout.
mockClock.tick(WATCHDOG_TIME);
assertEquals(1, channelRequest.watchdogTimeoutCallCount);
assertEquals(1, channelRequest.channel_.completedRequests.length);
assertFalse(channelRequest.getSuccess());
// Make sure no more timers are firing.
mockClock.tick(ALL_DAY_MS);
assertEquals(1, channelRequest.watchdogTimeoutCallCount);
assertEquals(1, channelRequest.channel_.completedRequests.length);
checkReachabilityEvents(1, 0, 1, 0);
}
function testRequestTimeoutWithUnexpectedException() {
createChannelRequest();
channelRequest.channel_.createXhrIo = goog.functions.error('Weird error');
try {
channelRequest.xmlHttpGet(new goog.Uri('some_uri'), true, null);
fail('Expected error');
} catch (e) {
assertEquals('Weird error', e.message);
}
assertEquals(0, channelRequest.watchdogTimeoutCallCount);
assertEquals(0, channelRequest.channel_.completedRequests.length);
// Watchdog timeout.
mockClock.tick(WATCHDOG_TIME);
assertEquals(1, channelRequest.watchdogTimeoutCallCount);
assertEquals(1, channelRequest.channel_.completedRequests.length);
assertFalse(channelRequest.getSuccess());
// Make sure no more timers are firing.
mockClock.tick(ALL_DAY_MS);
assertEquals(1, channelRequest.watchdogTimeoutCallCount);
assertEquals(1, channelRequest.channel_.completedRequests.length);
checkReachabilityEvents(0, 0, 1, 0);
}
function testActiveXBlocked() {
createChannelRequest();
stubs.set(goog.global, 'ActiveXObject',
goog.functions.error('Active X blocked'));
channelRequest.tridentGet(new goog.Uri('some_uri'), false);
assertFalse(channelRequest.getSuccess());
assertEquals(
goog.labs.net.webChannel.ChannelRequest.Error.ACTIVE_X_BLOCKED,
channelRequest.getLastError());
checkReachabilityEvents(0, 0, 0, 0);
}
function checkReachabilityEvents(reqMade, reqSucceeded, reqFail, backChannel) {
var Reachability =
goog.labs.net.webChannel.requestStats.ServerReachability;
assertEquals(reqMade,
reachabilityEvents[Reachability.REQUEST_MADE] || 0);
assertEquals(reqSucceeded,
reachabilityEvents[Reachability.REQUEST_SUCCEEDED] || 0);
assertEquals(reqFail,
reachabilityEvents[Reachability.REQUEST_FAILED] || 0);
assertEquals(backChannel,
reachabilityEvents[Reachability.BACK_CHANNEL_ACTIVITY] || 0);
}
function testDuplicatedRandomParams() {
createChannelRequest();
channelRequest.xmlHttpGet(new goog.Uri('some_uri'), true, null, true,
true /* opt_duplicateRandom */);
var z = xhrIo.getLastUri().getParameterValue('zx');
var z1 = xhrIo.getLastUri().getParameterValue('zx1');
assertTrue(goog.isDefAndNotNull(z));
assertTrue(goog.isDefAndNotNull(z1));
assertEquals(z1, z);
}
@@ -0,0 +1,50 @@
// Copyright 2013 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 class manages the network connectivity state.
*
* Some of the connectivity state may be exposed to the client code in future,
* e.g. the initial handshake state, in order to save one RTT when a channel
* has to be reestablished. TODO(user).
*
* @visibility {:internal}
*/
goog.provide('goog.labs.net.webChannel.ConnectionState');
/**
* The connectivity state of the channel.
*
* @constructor
* @struct
*/
goog.labs.net.webChannel.ConnectionState = function() {
/**
* Handshake result.
* @type {Array<string>}
*/
this.handshakeResult = null;
/**
* The result of checking if there is a buffering proxy in the network.
* True means the connection is buffered, False means unbuffered,
* null means that the result is not available.
* @type {?boolean}
*/
this.bufferingProxyResult = null;
};
@@ -0,0 +1,278 @@
// Copyright 2013 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 A pool of forward channel requests to enable real-time
* messaging from the client to server.
*
* @visibility {:internal}
*/
goog.provide('goog.labs.net.webChannel.ForwardChannelRequestPool');
goog.require('goog.array');
goog.require('goog.string');
goog.require('goog.structs.Set');
goog.scope(function() {
// type checking only (no require)
var ChannelRequest = goog.labs.net.webChannel.ChannelRequest;
/**
* This class represents the state of all forward channel requests.
*
* @param {number=} opt_maxPoolSize The maximum pool size.
*
* @constructor
* @final
*/
goog.labs.net.webChannel.ForwardChannelRequestPool = function(opt_maxPoolSize) {
/**
* THe max pool size as configured.
*
* @private {number}
*/
this.maxPoolSizeConfigured_ = opt_maxPoolSize ||
goog.labs.net.webChannel.ForwardChannelRequestPool.MAX_POOL_SIZE_;
/**
* The current size limit of the request pool. This limit is meant to be
* read-only after the channel is fully opened.
*
* If SPDY is enabled, set it to the max pool size, which is also
* configurable.
*
* @private {number}
*/
this.maxSize_ = ForwardChannelRequestPool.isSpdyEnabled_() ?
this.maxPoolSizeConfigured_ : 1;
/**
* The container for all the pending request objects.
*
* @private {goog.structs.Set<ChannelRequest>}
*/
this.requestPool_ = null;
if (this.maxSize_ > 1) {
this.requestPool_ = new goog.structs.Set();
}
/**
* The single request object when the pool size is limited to one.
*
* @private {ChannelRequest}
*/
this.request_ = null;
};
var ForwardChannelRequestPool =
goog.labs.net.webChannel.ForwardChannelRequestPool;
/**
* The default size limit of the request pool.
*
* @private {number}
*/
ForwardChannelRequestPool.MAX_POOL_SIZE_ = 10;
/**
* @return {boolean} True if SPDY is enabled for the current page using
* chrome specific APIs.
* @private
*/
ForwardChannelRequestPool.isSpdyEnabled_ = function() {
return !!(window.chrome && window.chrome.loadTimes &&
window.chrome.loadTimes() && window.chrome.loadTimes().wasFetchedViaSpdy);
};
/**
* Once we know the client protocol (from the handshake), check if we need
* enable the request pool accordingly. This is more robust than using
* browser-internal APIs (specific to Chrome).
*
* @param {string} clientProtocol The client protocol
*/
ForwardChannelRequestPool.prototype.applyClientProtocol = function(
clientProtocol) {
if (this.requestPool_) {
return;
}
if (goog.string.contains(clientProtocol, 'spdy') ||
goog.string.contains(clientProtocol, 'quic')) {
this.maxSize_ = this.maxPoolSizeConfigured_;
this.requestPool_ = new goog.structs.Set();
if (this.request_) {
this.addRequest(this.request_);
this.request_ = null;
}
}
};
/**
* @return {boolean} True if the pool is full.
*/
ForwardChannelRequestPool.prototype.isFull = function() {
if (this.request_) {
return true;
}
if (this.requestPool_) {
return this.requestPool_.getCount() >= this.maxSize_;
}
return false;
};
/**
* @return {number} The current size limit.
*/
ForwardChannelRequestPool.prototype.getMaxSize = function() {
return this.maxSize_;
};
/**
* @return {number} The number of pending requests in the pool.
*/
ForwardChannelRequestPool.prototype.getRequestCount = function() {
if (this.request_) {
return 1;
}
if (this.requestPool_) {
return this.requestPool_.getCount();
}
return 0;
};
/**
* @param {ChannelRequest} req The channel request.
* @return {boolean} True if the request is a included inside the pool.
*/
ForwardChannelRequestPool.prototype.hasRequest = function(req) {
if (this.request_) {
return this.request_ == req;
}
if (this.requestPool_) {
return this.requestPool_.contains(req);
}
return false;
};
/**
* Adds a new request to the pool.
*
* @param {!ChannelRequest} req The new channel request.
*/
ForwardChannelRequestPool.prototype.addRequest = function(req) {
if (this.requestPool_) {
this.requestPool_.add(req);
} else {
this.request_ = req;
}
};
/**
* Removes the given request from the pool.
*
* @param {ChannelRequest} req The channel request.
* @return {boolean} Whether the request has been removed from the pool.
*/
ForwardChannelRequestPool.prototype.removeRequest = function(req) {
if (this.request_ && this.request_ == req) {
this.request_ = null;
return true;
}
if (this.requestPool_ && this.requestPool_.contains(req)) {
this.requestPool_.remove(req);
return true;
}
return false;
};
/**
* Clears the pool and cancel all the pending requests.
*/
ForwardChannelRequestPool.prototype.cancel = function() {
if (this.request_) {
this.request_.cancel();
this.request_ = null;
return;
}
if (this.requestPool_ && !this.requestPool_.isEmpty()) {
goog.array.forEach(this.requestPool_.getValues(), function(val) {
val.cancel();
});
this.requestPool_.clear();
}
};
/**
* @return {boolean} Whether there are any pending requests.
*/
ForwardChannelRequestPool.prototype.hasPendingRequest = function() {
return (this.request_ != null) ||
(this.requestPool_ != null && !this.requestPool_.isEmpty());
};
/**
* Cancels all pending requests and force the completion of channel requests.
*
* Need go through the standard onRequestComplete logic to expose the max-retry
* failure in the standard way.
*
* @param {!function(!ChannelRequest)} onComplete The completion callback.
* @return {boolean} true if any request has been forced to complete.
*/
ForwardChannelRequestPool.prototype.forceComplete = function(onComplete) {
if (this.request_ != null) {
this.request_.cancel();
onComplete(this.request_);
return true;
}
if (this.requestPool_ && !this.requestPool_.isEmpty()) {
goog.array.forEach(this.requestPool_.getValues(),
function(val) {
val.cancel();
onComplete(val);
});
return true;
}
return false;
};
}); // goog.scope
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.labs.net.webChannel.ForwardChannelRequestPool</title>
<script src="../../../base.js"></script>
</head>
<body>
<script>
goog.require('goog.labs.net.webChannel.forwardChannelRequestPoolTest');
</script>
<div id="debug" style="font-size: small"></div>
</body>
</html>
@@ -0,0 +1,119 @@
// Copyright 2013 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 Unit tests for
* goog.labs.net.webChannel.ForwardChannelRequestPool.
* @suppress {accessControls} Private methods are accessed for test purposes.
*
*/
goog.provide('goog.labs.net.webChannel.forwardChannelRequestPoolTest');
goog.require('goog.labs.net.webChannel.ChannelRequest');
goog.require('goog.labs.net.webChannel.ForwardChannelRequestPool');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.asserts');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.labs.net.webChannel.forwardChannelRequestPoolTest');
var propertyReplacer = new goog.testing.PropertyReplacer();
var req = new goog.labs.net.webChannel.ChannelRequest(null, null);
function setUp() {
}
function tearDown() {
propertyReplacer.reset();
}
function stubSpdyCheck(spdyEnabled) {
propertyReplacer.set(goog.labs.net.webChannel.ForwardChannelRequestPool,
'isSpdyEnabled_',
function() {
return spdyEnabled;
});
}
function testSpdyEnabled() {
stubSpdyCheck(true);
var pool = new goog.labs.net.webChannel.ForwardChannelRequestPool();
assertFalse(pool.isFull());
assertEquals(0, pool.getRequestCount());
pool.addRequest(req);
assertTrue(pool.hasPendingRequest());
assertTrue(pool.hasRequest(req));
pool.removeRequest(req);
assertFalse(pool.hasPendingRequest());
for (var i = 0; i < pool.getMaxSize(); i++) {
pool.addRequest(new goog.labs.net.webChannel.ChannelRequest(null, null));
}
assertTrue(pool.isFull());
// do not fail
pool.addRequest(req);
assertTrue(pool.isFull());
}
function testSpdyNotEnabled() {
stubSpdyCheck(false);
var pool = new goog.labs.net.webChannel.ForwardChannelRequestPool();
assertFalse(pool.isFull());
assertEquals(0, pool.getRequestCount());
pool.addRequest(req);
assertTrue(pool.hasPendingRequest());
assertTrue(pool.hasRequest(req));
assertTrue(pool.isFull());
pool.removeRequest(req);
assertFalse(pool.hasPendingRequest());
// do not fail
pool.addRequest(req);
assertTrue(pool.isFull());
}
function testApplyClientProtocol() {
stubSpdyCheck(false);
var pool = new goog.labs.net.webChannel.ForwardChannelRequestPool();
assertEquals(1, pool.getMaxSize());
pool.applyClientProtocol('spdy/3');
assertTrue(pool.getMaxSize() > 1);
pool.applyClientProtocol('foo-bar'); // no effect
assertTrue(pool.getMaxSize() > 1);
pool = new goog.labs.net.webChannel.ForwardChannelRequestPool();
assertEquals(1, pool.getMaxSize());
pool.applyClientProtocol('quic/x');
assertTrue(pool.getMaxSize() > 1);
stubSpdyCheck(true);
pool = new goog.labs.net.webChannel.ForwardChannelRequestPool();
assertTrue(pool.getMaxSize() > 1);
pool.applyClientProtocol('foo/3'); // no effect
assertTrue(pool.getMaxSize() > 1);
}
@@ -0,0 +1,162 @@
// Copyright 2013 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 Utility functions for managing networking, such as
* testing network connectivity.
*
* @visibility {:internal}
*/
goog.provide('goog.labs.net.webChannel.netUtils');
goog.require('goog.Uri');
goog.require('goog.labs.net.webChannel.WebChannelDebug');
goog.scope(function() {
var netUtils = goog.labs.net.webChannel.netUtils;
var WebChannelDebug = goog.labs.net.webChannel.WebChannelDebug;
/**
* Default timeout to allow for URI pings.
* @type {number}
*/
netUtils.NETWORK_TIMEOUT = 10000;
/**
* Pings the network with an image URI to check if an error is a server error
* or user's network error.
*
* The caller needs to add a 'rand' parameter to make sure the response is
* not fulfilled by browser cache.
*
* @param {function(boolean)} callback The function to call back with results.
* @param {goog.Uri=} opt_imageUri The URI (of an image) to use for the network
* test.
*/
netUtils.testNetwork = function(callback, opt_imageUri) {
var uri = opt_imageUri;
if (!uri) {
// default google.com image
uri = new goog.Uri('//www.google.com/images/cleardot.gif');
if (!(goog.global.location && goog.global.location.protocol == 'http')) {
uri.setScheme('https'); // e.g. chrome-extension
}
uri.makeUnique();
}
netUtils.testLoadImage(uri.toString(), netUtils.NETWORK_TIMEOUT, callback);
};
/**
* Test loading the given image, retrying if necessary.
* @param {string} url URL to the image.
* @param {number} timeout Milliseconds before giving up.
* @param {function(boolean)} callback Function to call with results.
* @param {number} retries The number of times to retry.
* @param {number=} opt_pauseBetweenRetriesMS Optional number of milliseconds
* between retries - defaults to 0.
*/
netUtils.testLoadImageWithRetries = function(url, timeout, callback,
retries, opt_pauseBetweenRetriesMS) {
var channelDebug = new WebChannelDebug();
channelDebug.debug('TestLoadImageWithRetries: ' + opt_pauseBetweenRetriesMS);
if (retries == 0) {
// no more retries, give up
callback(false);
return;
}
var pauseBetweenRetries = opt_pauseBetweenRetriesMS || 0;
retries--;
netUtils.testLoadImage(url, timeout, function(succeeded) {
if (succeeded) {
callback(true);
} else {
// try again
goog.global.setTimeout(function() {
netUtils.testLoadImageWithRetries(url, timeout, callback,
retries, pauseBetweenRetries);
}, pauseBetweenRetries);
}
});
};
/**
* Test loading the given image.
* @param {string} url URL to the image.
* @param {number} timeout Milliseconds before giving up.
* @param {function(boolean)} callback Function to call with results.
*/
netUtils.testLoadImage = function(url, timeout, callback) {
var channelDebug = new WebChannelDebug();
channelDebug.debug('TestLoadImage: loading ' + url);
var img = new Image();
img.onload = goog.partial(netUtils.imageCallback_, channelDebug, img,
'TestLoadImage: loaded', true, callback);
img.onerror = goog.partial(netUtils.imageCallback_, channelDebug, img,
'TestLoadImage: error', false, callback);
img.onabort = goog.partial(netUtils.imageCallback_, channelDebug, img,
'TestLoadImage: abort', false, callback);
img.ontimeout = goog.partial(netUtils.imageCallback_, channelDebug, img,
'TestLoadImage: timeout', false, callback);
goog.global.setTimeout(function() {
if (img.ontimeout) {
img.ontimeout();
}
}, timeout);
img.src = url;
};
/**
* Wrap the image callback with debug and cleanup logic.
* @param {!WebChannelDebug} channelDebug The WebChannelDebug object.
* @param {!Image} img The image element.
* @param {string} debugText The debug text.
* @param {boolean} result The result of image loading.
* @param {function(boolean)} callback The image callback.
* @private
*/
netUtils.imageCallback_ = function(channelDebug, img, debugText, result,
callback) {
try {
channelDebug.debug(debugText);
netUtils.clearImageCallbacks_(img);
callback(result);
} catch (e) {
channelDebug.dumpException(e);
}
};
/**
* Clears handlers to avoid memory leaks.
* @param {Image} img The image to clear handlers from.
* @private
*/
netUtils.clearImageCallbacks_ = function(img) {
img.onload = null;
img.onerror = null;
img.onabort = null;
img.ontimeout = null;
};
}); // goog.scope
@@ -0,0 +1,386 @@
// Copyright 2013 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 Static utilities for collecting stats associated with
* ChannelRequest.
*
* @visibility {:internal}
*/
goog.provide('goog.labs.net.webChannel.requestStats');
goog.provide('goog.labs.net.webChannel.requestStats.Event');
goog.provide('goog.labs.net.webChannel.requestStats.ServerReachability');
goog.provide('goog.labs.net.webChannel.requestStats.ServerReachabilityEvent');
goog.provide('goog.labs.net.webChannel.requestStats.Stat');
goog.provide('goog.labs.net.webChannel.requestStats.StatEvent');
goog.provide('goog.labs.net.webChannel.requestStats.TimingEvent');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.scope(function() {
var requestStats = goog.labs.net.webChannel.requestStats;
/**
* Events fired.
* @const
*/
requestStats.Event = {};
/**
* Singleton event target for firing stat events
* @type {goog.events.EventTarget}
* @private
*/
requestStats.statEventTarget_ = new goog.events.EventTarget();
/**
* The type of event that occurs every time some information about how reachable
* the server is is discovered.
*/
requestStats.Event.SERVER_REACHABILITY_EVENT = 'serverreachability';
/**
* Types of events which reveal information about the reachability of the
* server.
* @enum {number}
*/
requestStats.ServerReachability = {
REQUEST_MADE: 1,
REQUEST_SUCCEEDED: 2,
REQUEST_FAILED: 3,
BACK_CHANNEL_ACTIVITY: 4
};
/**
* Event class for SERVER_REACHABILITY_EVENT.
*
* @param {goog.events.EventTarget} target The stat event target for
the channel.
* @param {requestStats.ServerReachability} reachabilityType
* The reachability event type.
* @constructor
* @extends {goog.events.Event}
*/
requestStats.ServerReachabilityEvent = function(target, reachabilityType) {
goog.events.Event.call(this,
requestStats.Event.SERVER_REACHABILITY_EVENT, target);
/**
* @type {requestStats.ServerReachability}
*/
this.reachabilityType = reachabilityType;
};
goog.inherits(requestStats.ServerReachabilityEvent, goog.events.Event);
/**
* Notify the channel that a particular fine grained network event has occurred.
* Should be considered package-private.
* @param {requestStats.ServerReachability} reachabilityType
* The reachability event type.
*/
requestStats.notifyServerReachabilityEvent = function(reachabilityType) {
var target = requestStats.statEventTarget_;
target.dispatchEvent(
new requestStats.ServerReachabilityEvent(target, reachabilityType));
};
/**
* Stat Event that fires when things of interest happen that may be useful for
* applications to know about for stats or debugging purposes.
*/
requestStats.Event.STAT_EVENT = 'statevent';
/**
* Enum that identifies events for statistics that are interesting to track.
* @enum {number}
*/
requestStats.Stat = {
/** Event indicating a new connection attempt. */
CONNECT_ATTEMPT: 0,
/** Event indicating a connection error due to a general network problem. */
ERROR_NETWORK: 1,
/**
* Event indicating a connection error that isn't due to a general network
* problem.
*/
ERROR_OTHER: 2,
/** Event indicating the start of test stage one. */
TEST_STAGE_ONE_START: 3,
/** Event indicating the start of test stage two. */
TEST_STAGE_TWO_START: 4,
/** Event indicating the first piece of test data was received. */
TEST_STAGE_TWO_DATA_ONE: 5,
/**
* Event indicating that the second piece of test data was received and it was
* recieved separately from the first.
*/
TEST_STAGE_TWO_DATA_TWO: 6,
/** Event indicating both pieces of test data were received simultaneously. */
TEST_STAGE_TWO_DATA_BOTH: 7,
/** Event indicating stage one of the test request failed. */
TEST_STAGE_ONE_FAILED: 8,
/** Event indicating stage two of the test request failed. */
TEST_STAGE_TWO_FAILED: 9,
/**
* Event indicating that a buffering proxy is likely between the client and
* the server.
*/
PROXY: 10,
/**
* Event indicating that no buffering proxy is likely between the client and
* the server.
*/
NOPROXY: 11,
/** Event indicating an unknown SID error. */
REQUEST_UNKNOWN_SESSION_ID: 12,
/** Event indicating a bad status code was received. */
REQUEST_BAD_STATUS: 13,
/** Event indicating incomplete data was received */
REQUEST_INCOMPLETE_DATA: 14,
/** Event indicating bad data was received */
REQUEST_BAD_DATA: 15,
/** Event indicating no data was received when data was expected. */
REQUEST_NO_DATA: 16,
/** Event indicating a request timeout. */
REQUEST_TIMEOUT: 17,
/**
* Event indicating that the server never received our hanging GET and so it
* is being retried.
*/
BACKCHANNEL_MISSING: 18,
/**
* Event indicating that we have determined that our hanging GET is not
* receiving data when it should be. Thus it is dead dead and will be retried.
*/
BACKCHANNEL_DEAD: 19,
/**
* The browser declared itself offline during the lifetime of a request, or
* was offline when a request was initially made.
*/
BROWSER_OFFLINE: 20,
/** ActiveX is blocked by the machine's admin settings. */
ACTIVE_X_BLOCKED: 21
};
/**
* Event class for STAT_EVENT.
*
* @param {goog.events.EventTarget} eventTarget The stat event target for
the channel.
* @param {requestStats.Stat} stat The stat.
* @constructor
* @extends {goog.events.Event}
*/
requestStats.StatEvent = function(eventTarget, stat) {
goog.events.Event.call(this, requestStats.Event.STAT_EVENT, eventTarget);
/**
* The stat
* @type {requestStats.Stat}
*/
this.stat = stat;
};
goog.inherits(requestStats.StatEvent, goog.events.Event);
/**
* Returns the singleton event target for stat events.
* @return {goog.events.EventTarget} The event target for stat events.
*/
requestStats.getStatEventTarget = function() {
return requestStats.statEventTarget_;
};
/**
* Helper function to call the stat event callback.
* @param {requestStats.Stat} stat The stat.
*/
requestStats.notifyStatEvent = function(stat) {
var target = requestStats.statEventTarget_;
target.dispatchEvent(new requestStats.StatEvent(target, stat));
};
/**
* An event that fires when POST requests complete successfully, indicating
* the size of the POST and the round trip time.
*/
requestStats.Event.TIMING_EVENT = 'timingevent';
/**
* Event class for requestStats.Event.TIMING_EVENT
*
* @param {goog.events.EventTarget} target The stat event target for
the channel.
* @param {number} size The number of characters in the POST data.
* @param {number} rtt The total round trip time from POST to response in MS.
* @param {number} retries The number of times the POST had to be retried.
* @constructor
* @extends {goog.events.Event}
*/
requestStats.TimingEvent = function(target, size, rtt, retries) {
goog.events.Event.call(this,
requestStats.Event.TIMING_EVENT, target);
/**
* @type {number}
*/
this.size = size;
/**
* @type {number}
*/
this.rtt = rtt;
/**
* @type {number}
*/
this.retries = retries;
};
goog.inherits(requestStats.TimingEvent, goog.events.Event);
/**
* Helper function to notify listeners about POST request performance.
*
* @param {number} size Number of characters in the POST data.
* @param {number} rtt The amount of time from POST start to response.
* @param {number} retries The number of times the POST had to be retried.
*/
requestStats.notifyTimingEvent = function(size, rtt, retries) {
var target = requestStats.statEventTarget_;
target.dispatchEvent(
new requestStats.TimingEvent(
target, size, rtt, retries));
};
/**
* Allows the application to set an execution hooks for when a channel
* starts processing requests. This is useful to track timing or logging
* special information. The function takes no parameters and return void.
* @param {Function} startHook The function for the start hook.
*/
requestStats.setStartThreadExecutionHook = function(startHook) {
requestStats.startExecutionHook_ = startHook;
};
/**
* Allows the application to set an execution hooks for when a channel
* stops processing requests. This is useful to track timing or logging
* special information. The function takes no parameters and return void.
* @param {Function} endHook The function for the end hook.
*/
requestStats.setEndThreadExecutionHook = function(endHook) {
requestStats.endExecutionHook_ = endHook;
};
/**
* Application provided execution hook for the start hook.
*
* @type {Function}
* @private
*/
requestStats.startExecutionHook_ = function() { };
/**
* Application provided execution hook for the end hook.
*
* @type {Function}
* @private
*/
requestStats.endExecutionHook_ = function() { };
/**
* Helper function to call the start hook
*/
requestStats.onStartExecution = function() {
requestStats.startExecutionHook_();
};
/**
* Helper function to call the end hook
*/
requestStats.onEndExecution = function() {
requestStats.endExecutionHook_();
};
/**
* Wrapper around SafeTimeout which calls the start and end execution hooks
* with a try...finally block.
* @param {Function} fn The callback function.
* @param {number} ms The time in MS for the timer.
* @return {number} The ID of the timer.
*/
requestStats.setTimeout = function(fn, ms) {
if (!goog.isFunction(fn)) {
throw Error('Fn must not be null and must be a function');
}
return goog.global.setTimeout(function() {
requestStats.onStartExecution();
try {
fn();
} finally {
requestStats.onEndExecution();
}
}, ms);
};
}); // goog.scope
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2009 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.labs.net.webChannel.WebChannelBase</title>
<script src="../../../base.js"></script>
</head>
<body>
<script>
goog.require('goog.labs.net.webChannel.webChannelBaseTest');
</script>
<div id="debug" style="font-size: small"></div>
</body>
</html>
@@ -0,0 +1,376 @@
// Copyright 2013 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 Implementation of a WebChannel transport using WebChannelBase.
*
* When WebChannelBase is used as the underlying transport, the capabilities
* of the WebChannel are limited to what's supported by the implementation.
* Particularly, multiplexing is not possible, and only strings are
* supported as message types.
*
*/
goog.provide('goog.labs.net.webChannel.WebChannelBaseTransport');
goog.require('goog.asserts');
goog.require('goog.events.EventTarget');
goog.require('goog.labs.net.webChannel.WebChannelBase');
goog.require('goog.log');
goog.require('goog.net.WebChannel');
goog.require('goog.net.WebChannelTransport');
goog.require('goog.object');
goog.require('goog.string.path');
/**
* Implementation of {@link goog.net.WebChannelTransport} with
* {@link goog.labs.net.webChannel.WebChannelBase} as the underlying channel
* implementation.
*
* @constructor
* @struct
* @implements {goog.net.WebChannelTransport}
* @final
*/
goog.labs.net.webChannel.WebChannelBaseTransport = function() {};
goog.scope(function() {
var WebChannelBaseTransport = goog.labs.net.webChannel.WebChannelBaseTransport;
var WebChannelBase = goog.labs.net.webChannel.WebChannelBase;
/**
* @override
*/
WebChannelBaseTransport.prototype.createWebChannel = function(
url, opt_options) {
return new WebChannelBaseTransport.Channel(url, opt_options);
};
/**
* Implementation of the {@link goog.net.WebChannel} interface.
*
* @param {string} url The URL path for the new WebChannel instance.
* @param {!goog.net.WebChannel.Options=} opt_options Configuration for the
* new WebChannel instance.
*
* @constructor
* @implements {goog.net.WebChannel}
* @extends {goog.events.EventTarget}
* @final
*/
WebChannelBaseTransport.Channel = function(url, opt_options) {
WebChannelBaseTransport.Channel.base(this, 'constructor');
/**
* The underlying channel object.
*
* @private {!WebChannelBase}
*/
this.channel_ = new WebChannelBase(opt_options);
/**
* The URL of the target server end-point.
*
* @private {string}
*/
this.url_ = url;
/**
* The test URL of the target server end-point. This value defaults to
* this.url_ + '/test'.
*
* @private {string}
*/
this.testUrl_ = (opt_options && opt_options.testUrl) ? opt_options.testUrl :
goog.string.path.join(this.url_, 'test');
/**
* The logger for this class.
* @private {goog.log.Logger}
*/
this.logger_ = goog.log.getLogger(
'goog.labs.net.webChannel.WebChannelBaseTransport');
/**
* @private {Object<string, string>} messageUrlParams_ Extra URL parameters
* to be added to each HTTP request.
*/
this.messageUrlParams_ =
(opt_options && opt_options.messageUrlParams) || null;
var messageHeaders = (opt_options && opt_options.messageHeaders) || null;
// default is false
if (opt_options && opt_options.clientProtocolHeaderRequired) {
if (messageHeaders) {
goog.object.set(messageHeaders,
goog.net.WebChannel.X_CLIENT_PROTOCOL,
goog.net.WebChannel.X_CLIENT_PROTOCOL_WEB_CHANNEL);
} else {
messageHeaders = goog.object.create(
goog.net.WebChannel.X_CLIENT_PROTOCOL,
goog.net.WebChannel.X_CLIENT_PROTOCOL_WEB_CHANNEL);
}
}
this.channel_.setExtraHeaders(messageHeaders);
/**
* @private {boolean} supportsCrossDomainXhr_ Whether to enable CORS.
*/
this.supportsCrossDomainXhr_ =
(opt_options && opt_options.supportsCrossDomainXhr) || false;
};
goog.inherits(WebChannelBaseTransport.Channel, goog.events.EventTarget);
/**
* The channel handler.
*
* @type {WebChannelBase.Handler}
* @private
*/
WebChannelBaseTransport.Channel.prototype.channelHandler_ = null;
/**
* Test path is always set to "/url/test".
*
* @override
*/
WebChannelBaseTransport.Channel.prototype.open = function() {
this.channel_.connect(this.testUrl_, this.url_,
(this.messageUrlParams_ || undefined));
this.channelHandler_ = new WebChannelBaseTransport.Channel.Handler_(this);
this.channel_.setHandler(this.channelHandler_);
if (this.supportsCrossDomainXhr_) {
this.channel_.setSupportsCrossDomainXhrs(true);
}
};
/**
* @override
*/
WebChannelBaseTransport.Channel.prototype.close = function() {
this.channel_.disconnect();
};
/**
* The WebChannelBase only supports object types.
*
* @param {!goog.net.WebChannel.MessageData} message The message to send.
* @override
*/
WebChannelBaseTransport.Channel.prototype.send = function(message) {
goog.asserts.assert(goog.isObject(message), 'only object type expected');
this.channel_.sendMap(message);
};
/**
* @override
*/
WebChannelBaseTransport.Channel.prototype.disposeInternal = function() {
this.channel_.setHandler(null);
delete this.channelHandler_;
this.channel_.disconnect();
delete this.channel_;
WebChannelBaseTransport.Channel.base(this, 'disposeInternal');
};
/**
* The message event.
*
* @param {!Array<?>} array The data array from the underlying channel.
* @constructor
* @extends {goog.net.WebChannel.MessageEvent}
* @final
*/
WebChannelBaseTransport.Channel.MessageEvent = function(array) {
WebChannelBaseTransport.Channel.MessageEvent.base(this, 'constructor');
this.data = array;
};
goog.inherits(WebChannelBaseTransport.Channel.MessageEvent,
goog.net.WebChannel.MessageEvent);
/**
* The error event.
*
* @param {WebChannelBase.Error} error The error code.
* @constructor
* @extends {goog.net.WebChannel.ErrorEvent}
* @final
*/
WebChannelBaseTransport.Channel.ErrorEvent = function(error) {
WebChannelBaseTransport.Channel.ErrorEvent.base(this, 'constructor');
/**
* Transport specific error code is not to be propagated with the event.
*/
this.status = goog.net.WebChannel.ErrorStatus.NETWORK_ERROR;
};
goog.inherits(WebChannelBaseTransport.Channel.ErrorEvent,
goog.net.WebChannel.ErrorEvent);
/**
* Implementation of {@link WebChannelBase.Handler} interface.
*
* @param {!WebChannelBaseTransport.Channel} channel The enclosing WebChannel.
*
* @constructor
* @extends {WebChannelBase.Handler}
* @private
*/
WebChannelBaseTransport.Channel.Handler_ = function(channel) {
WebChannelBaseTransport.Channel.Handler_.base(this, 'constructor');
/**
* @type {!WebChannelBaseTransport.Channel}
* @private
*/
this.channel_ = channel;
};
goog.inherits(WebChannelBaseTransport.Channel.Handler_, WebChannelBase.Handler);
/**
* @override
*/
WebChannelBaseTransport.Channel.Handler_.prototype.channelOpened = function(
channel) {
goog.log.info(this.channel_.logger_,
'WebChannel opened on ' + this.channel_.url_);
this.channel_.dispatchEvent(goog.net.WebChannel.EventType.OPEN);
};
/**
* @override
*/
WebChannelBaseTransport.Channel.Handler_.prototype.channelHandleArray =
function(channel, array) {
goog.asserts.assert(array, 'array expected to be defined');
this.channel_.dispatchEvent(
new WebChannelBaseTransport.Channel.MessageEvent(array));
};
/**
* @override
*/
WebChannelBaseTransport.Channel.Handler_.prototype.channelError = function(
channel, error) {
goog.log.info(this.channel_.logger_,
'WebChannel aborted on ' + this.channel_.url_ +
' due to channel error: ' + error);
this.channel_.dispatchEvent(
new WebChannelBaseTransport.Channel.ErrorEvent(error));
};
/**
* @override
*/
WebChannelBaseTransport.Channel.Handler_.prototype.channelClosed = function(
channel, opt_pendingMaps, opt_undeliveredMaps) {
goog.log.info(this.channel_.logger_,
'WebChannel closed on ' + this.channel_.url_);
this.channel_.dispatchEvent(goog.net.WebChannel.EventType.CLOSE);
};
/**
* @override
*/
WebChannelBaseTransport.Channel.prototype.getRuntimeProperties = function() {
return new WebChannelBaseTransport.ChannelProperties(this.channel_);
};
/**
* Implementation of the {@link goog.net.WebChannel.RuntimeProperties}.
*
* @param {!WebChannelBase} channel The underlying channel object.
*
* @constructor
* @implements {goog.net.WebChannel.RuntimeProperties}
* @final
*/
WebChannelBaseTransport.ChannelProperties = function(channel) {
/**
* The underlying channel object.
*
* @private {!WebChannelBase}
*/
this.channel_ = channel;
/**
* The flag to turn on/off server-side flow control.
*
* @private {boolean}
*/
this.serverFlowControlEnabled_ = false;
};
/**
* @override
*/
WebChannelBaseTransport.ChannelProperties.prototype.getConcurrentRequestLimit =
function() {
return this.channel_.getForwardChannelRequestPool().getMaxSize();
};
/**
* @override
*/
WebChannelBaseTransport.ChannelProperties.prototype.isSpdyEnabled =
function() {
return this.getConcurrentRequestLimit() > 1;
};
/**
* @override
*/
WebChannelBaseTransport.ChannelProperties.prototype.setServerFlowControl =
goog.abstractMethod;
/**
* @override
*/
WebChannelBaseTransport.ChannelProperties.prototype.getNonAckedMessageCount =
goog.abstractMethod;
}); // goog.scope
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.labs.net.webChannel.WebChannelBaseTransport</title>
<script src="../../../base.js"></script>
</head>
<body>
<script>
goog.require('goog.labs.net.webChannel.webChannelBaseTransportTest');
</script>
<div id="debug" style="font-size: small"></div>
</body>
</html>
@@ -0,0 +1,254 @@
// Copyright 2013 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 Unit tests for goog.labs.net.webChannel.WebChannelBase.
* @suppress {accessControls} Private methods are accessed for test purposes.
*
*/
goog.provide('goog.labs.net.webChannel.webChannelBaseTransportTest');
goog.require('goog.events');
goog.require('goog.labs.net.webChannel.WebChannelBaseTransport');
goog.require('goog.net.WebChannel');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.labs.net.webChannel.webChannelBaseTransportTest');
var webChannel;
var channelUrl = 'http://127.0.0.1:8080/channel';
function setUp() {
}
function tearDown() {
goog.dispose(webChannel);
}
function testOpenWithUrl() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
webChannel = webChannelTransport.createWebChannel(channelUrl);
var eventFired = false;
goog.events.listen(webChannel, goog.net.WebChannel.EventType.OPEN,
function(e) {
eventFired = true;
});
webChannel.open();
assertFalse(eventFired);
var channel = webChannel.channel_;
assertNotNull(channel);
simulateOpenEvent(channel);
assertTrue(eventFired);
}
function testOpenWithTestUrl() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
var options = {'testUrl': channelUrl + '/footest'};
webChannel = webChannelTransport.createWebChannel(channelUrl, options);
webChannel.open();
var testPath = webChannel.channel_.connectionTest_.path_;
assertNotNullNorUndefined(testPath);
}
function testOpenWithCustomHeaders() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
var options = {'messageHeaders': {'foo-key': 'foo-value'}};
webChannel = webChannelTransport.createWebChannel(channelUrl, options);
webChannel.open();
var extraHeaders_ = webChannel.channel_.extraHeaders_;
assertNotNullNorUndefined(extraHeaders_);
assertEquals('foo-value', extraHeaders_['foo-key']);
assertEquals(undefined, extraHeaders_['X-Client-Protocol']);
}
function testClientProtocolHeaderRequired() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
var options = {'clientProtocolHeaderRequired': true};
webChannel = webChannelTransport.createWebChannel(channelUrl, options);
webChannel.open();
var extraHeaders_ = webChannel.channel_.extraHeaders_;
assertNotNullNorUndefined(extraHeaders_);
assertEquals('webchannel', extraHeaders_['X-Client-Protocol']);
}
function testClientProtocolHeaderNotRequiredByDefault() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
webChannel = webChannelTransport.createWebChannel(channelUrl);
webChannel.open();
var extraHeaders_ = webChannel.channel_.extraHeaders_;
assertNull(extraHeaders_);
}
function testClientProtocolHeaderRequiredWithCustomHeader() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
var options = {
'clientProtocolHeaderRequired': true,
'messageHeaders': {'foo-key': 'foo-value'}
};
webChannel = webChannelTransport.createWebChannel(channelUrl, options);
webChannel.open();
var extraHeaders_ = webChannel.channel_.extraHeaders_;
assertNotNullNorUndefined(extraHeaders_);
assertEquals('foo-value', extraHeaders_['foo-key']);
assertEquals('webchannel', extraHeaders_['X-Client-Protocol']);
}
function testOpenWithCustomParams() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
var options = {'messageUrlParams': {'foo-key': 'foo-value'}};
webChannel = webChannelTransport.createWebChannel(channelUrl, options);
webChannel.open();
var extraParams = webChannel.channel_.extraParams_;
assertNotNullNorUndefined(extraParams);
}
function testOpenWithCorsEnabled() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
var options = {'supportsCrossDomainXhr': true};
webChannel = webChannelTransport.createWebChannel(channelUrl, options);
webChannel.open();
assertTrue(webChannel.channel_.supportsCrossDomainXhrs_);
}
function testOpenThenCloseChannel() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
webChannel = webChannelTransport.createWebChannel(channelUrl);
var eventFired = false;
goog.events.listen(webChannel, goog.net.WebChannel.EventType.CLOSE,
function(e) {
eventFired = true;
});
webChannel.open();
assertFalse(eventFired);
var channel = webChannel.channel_;
assertNotNull(channel);
simulateCloseEvent(channel);
assertTrue(eventFired);
}
function testChannelError() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
webChannel = webChannelTransport.createWebChannel(channelUrl);
var eventFired = false;
goog.events.listen(webChannel, goog.net.WebChannel.EventType.ERROR,
function(e) {
eventFired = true;
assertEquals(goog.net.WebChannel.ErrorStatus.NETWORK_ERROR, e.status);
});
webChannel.open();
assertFalse(eventFired);
var channel = webChannel.channel_;
assertNotNull(channel);
simulateErrorEvent(channel);
assertTrue(eventFired);
}
function testChannelMessage() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
webChannel = webChannelTransport.createWebChannel(channelUrl);
var eventFired = false;
var data = 'foo';
goog.events.listen(webChannel, goog.net.WebChannel.EventType.MESSAGE,
function(e) {
eventFired = true;
assertEquals(e.data, data);
});
webChannel.open();
assertFalse(eventFired);
var channel = webChannel.channel_;
assertNotNull(channel);
simulateMessageEvent(channel, data);
assertTrue(eventFired);
}
/**
* Simulates the WebChannelBase firing the open event for the given channel.
* @param {!goog.labs.net.webChannel.WebChannelBase} channel The WebChannelBase.
*/
function simulateOpenEvent(channel) {
assertNotNull(channel.getHandler());
channel.getHandler().channelOpened();
}
/**
* Simulates the WebChannelBase firing the close event for the given channel.
* @param {!goog.labs.net.webChannel.WebChannelBase} channel The WebChannelBase.
*/
function simulateCloseEvent(channel) {
assertNotNull(channel.getHandler());
channel.getHandler().channelClosed();
}
/**
* Simulates the WebChannelBase firing the error event for the given channel.
* @param {!goog.labs.net.webChannel.WebChannelBase} channel The WebChannelBase.
*/
function simulateErrorEvent(channel) {
assertNotNull(channel.getHandler());
channel.getHandler().channelError();
}
/**
* Simulates the WebChannelBase firing the message event for the given channel.
* @param {!goog.labs.net.webChannel.WebChannelBase} channel The WebChannelBase.
* @param {String} data The message data.
*/
function simulateMessageEvent(channel, data) {
assertNotNull(channel.getHandler());
channel.getHandler().channelHandleArray(channel, data);
}
@@ -0,0 +1,300 @@
// Copyright 2006 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 Provides a utility for tracing and debugging WebChannel
* requests.
*
* @visibility {:internal}
*/
goog.provide('goog.labs.net.webChannel.WebChannelDebug');
goog.require('goog.json');
goog.require('goog.log');
/**
* Logs and keeps a buffer of debugging info for the Channel.
*
* @constructor
* @struct
* @final
*/
goog.labs.net.webChannel.WebChannelDebug = function() {
/**
* The logger instance.
* @const
* @private
*/
this.logger_ = goog.log.getLogger('goog.labs.net.webChannel.WebChannelDebug');
};
goog.scope(function() {
var WebChannelDebug = goog.labs.net.webChannel.WebChannelDebug;
/**
* Gets the logger used by this ChannelDebug.
* @return {goog.debug.Logger} The logger used by this WebChannelDebug.
*/
WebChannelDebug.prototype.getLogger = function() {
return this.logger_;
};
/**
* Logs that the browser went offline during the lifetime of a request.
* @param {goog.Uri} url The URL being requested.
*/
WebChannelDebug.prototype.browserOfflineResponse = function(url) {
this.info('BROWSER_OFFLINE: ' + url);
};
/**
* Logs an XmlHttp request..
* @param {string} verb The request type (GET/POST).
* @param {goog.Uri} uri The request destination.
* @param {string|number|undefined} id The request id.
* @param {number} attempt Which attempt # the request was.
* @param {?string} postData The data posted in the request.
*/
WebChannelDebug.prototype.xmlHttpChannelRequest =
function(verb, uri, id, attempt, postData) {
this.info(
'XMLHTTP REQ (' + id + ') [attempt ' + attempt + ']: ' +
verb + '\n' + uri + '\n' +
this.maybeRedactPostData_(postData));
};
/**
* Logs the meta data received from an XmlHttp request.
* @param {string} verb The request type (GET/POST).
* @param {goog.Uri} uri The request destination.
* @param {string|number|undefined} id The request id.
* @param {number} attempt Which attempt # the request was.
* @param {goog.net.XmlHttp.ReadyState} readyState The ready state.
* @param {number} statusCode The HTTP status code.
*/
WebChannelDebug.prototype.xmlHttpChannelResponseMetaData =
function(verb, uri, id, attempt, readyState, statusCode) {
this.info(
'XMLHTTP RESP (' + id + ') [ attempt ' + attempt + ']: ' +
verb + '\n' + uri + '\n' + readyState + ' ' + statusCode);
};
/**
* Logs the response data received from an XmlHttp request.
* @param {string|number|undefined} id The request id.
* @param {?string} responseText The response text.
* @param {?string=} opt_desc Optional request description.
*/
WebChannelDebug.prototype.xmlHttpChannelResponseText =
function(id, responseText, opt_desc) {
this.info(
'XMLHTTP TEXT (' + id + '): ' +
this.redactResponse_(responseText) +
(opt_desc ? ' ' + opt_desc : ''));
};
/**
* Logs a Trident ActiveX request.
* @param {string} verb The request type (GET/POST).
* @param {goog.Uri} uri The request destination.
* @param {string|number|undefined} id The request id.
* @param {number} attempt Which attempt # the request was.
*/
WebChannelDebug.prototype.tridentChannelRequest =
function(verb, uri, id, attempt) {
this.info(
'TRIDENT REQ (' + id + ') [ attempt ' + attempt + ']: ' +
verb + '\n' + uri);
};
/**
* Logs the response text received from a Trident ActiveX request.
* @param {string|number|undefined} id The request id.
* @param {string} responseText The response text.
*/
WebChannelDebug.prototype.tridentChannelResponseText =
function(id, responseText) {
this.info(
'TRIDENT TEXT (' + id + '): ' +
this.redactResponse_(responseText));
};
/**
* Logs the done response received from a Trident ActiveX request.
* @param {string|number|undefined} id The request id.
* @param {boolean} successful Whether the request was successful.
*/
WebChannelDebug.prototype.tridentChannelResponseDone =
function(id, successful) {
this.info(
'TRIDENT TEXT (' + id + '): ' + successful ? 'success' : 'failure');
};
/**
* Logs a request timeout.
* @param {goog.Uri} uri The uri that timed out.
*/
WebChannelDebug.prototype.timeoutResponse = function(uri) {
this.info('TIMEOUT: ' + uri);
};
/**
* Logs a debug message.
* @param {string} text The message.
*/
WebChannelDebug.prototype.debug = function(text) {
this.info(text);
};
/**
* Logs an exception
* @param {Error} e The error or error event.
* @param {string=} opt_msg The optional message, defaults to 'Exception'.
*/
WebChannelDebug.prototype.dumpException = function(e, opt_msg) {
this.severe((opt_msg || 'Exception') + e);
};
/**
* Logs an info message.
* @param {string} text The message.
*/
WebChannelDebug.prototype.info = function(text) {
goog.log.info(this.logger_, text);
};
/**
* Logs a warning message.
* @param {string} text The message.
*/
WebChannelDebug.prototype.warning = function(text) {
goog.log.warning(this.logger_, text);
};
/**
* Logs a severe message.
* @param {string} text The message.
*/
WebChannelDebug.prototype.severe = function(text) {
goog.log.error(this.logger_, text);
};
/**
* Removes potentially private data from a response so that we don't
* accidentally save private and personal data to the server logs.
* @param {?string} responseText A JSON response to clean.
* @return {?string} The cleaned response.
* @private
*/
WebChannelDebug.prototype.redactResponse_ = function(responseText) {
if (!responseText) {
return null;
}
/** @preserveTry */
try {
var responseArray = goog.json.unsafeParse(responseText);
if (responseArray) {
for (var i = 0; i < responseArray.length; i++) {
if (goog.isArray(responseArray[i])) {
this.maybeRedactArray_(responseArray[i]);
}
}
}
return goog.json.serialize(responseArray);
} catch (e) {
this.debug('Exception parsing expected JS array - probably was not JS');
return responseText;
}
};
/**
* Removes data from a response array that may be sensitive.
* @param {!Array<?>} array The array to clean.
* @private
*/
WebChannelDebug.prototype.maybeRedactArray_ = function(array) {
if (array.length < 2) {
return;
}
var dataPart = array[1];
if (!goog.isArray(dataPart)) {
return;
}
if (dataPart.length < 1) {
return;
}
var type = dataPart[0];
if (type != 'noop' && type != 'stop') {
// redact all fields in the array
for (var i = 1; i < dataPart.length; i++) {
dataPart[i] = '';
}
}
};
/**
* Removes potentially private data from a request POST body so that we don't
* accidentally save private and personal data to the server logs.
* @param {?string} data The data string to clean.
* @return {?string} The data string with sensitive data replaced by 'redacted'.
* @private
*/
WebChannelDebug.prototype.maybeRedactPostData_ = function(data) {
if (!data) {
return null;
}
var out = '';
var params = data.split('&');
for (var i = 0; i < params.length; i++) {
var param = params[i];
var keyValue = param.split('=');
if (keyValue.length > 1) {
var key = keyValue[0];
var value = keyValue[1];
var keyParts = key.split('_');
if (keyParts.length >= 2 && keyParts[1] == 'type') {
out += key + '=' + value + '&';
} else {
out += key + '=' + 'redacted' + '&';
}
}
}
return out;
};
}); // goog.scope
@@ -0,0 +1,75 @@
// Copyright 2013 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 Interface and shared data structures for implementing
* different wire protocol versions.
* @visibility {//closure/goog/bin/sizetests:__pkg__}
*/
goog.provide('goog.labs.net.webChannel.Wire');
/**
* The interface class.
*
* @interface
*/
goog.labs.net.webChannel.Wire = function() {};
goog.scope(function() {
var Wire = goog.labs.net.webChannel.Wire;
/**
* The latest protocol version that this class supports. We request this version
* from the server when opening the connection. Should match
* LATEST_CHANNEL_VERSION on the server code.
* @type {number}
*/
Wire.LATEST_CHANNEL_VERSION = 8;
/**
* Simple container class for a (mapId, map) pair.
* @param {number} mapId The id for this map.
* @param {!Object|!goog.structs.Map} map The map itself.
* @param {!Object=} opt_context The context associated with the map.
* @constructor
* @struct
*/
Wire.QueuedMap = function(mapId, map, opt_context) {
/**
* The id for this map.
* @type {number}
*/
this.mapId = mapId;
/**
* The map itself.
* @type {!Object|!goog.structs.Map}
*/
this.map = map;
/**
* The context for the map.
* @type {Object}
*/
this.context = opt_context || null;
};
}); // goog.scope
@@ -0,0 +1,136 @@
// Copyright 2013 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 Codec functions of the v8 wire protocol. Eventually we'd want
* to support pluggable wire-format to improve wire efficiency and to enable
* binary encoding. Such support will require an interface class, which
* will be added later.
*
* @visibility {:internal}
*/
goog.provide('goog.labs.net.webChannel.WireV8');
goog.require('goog.asserts');
goog.require('goog.json');
goog.require('goog.json.NativeJsonProcessor');
goog.require('goog.structs');
/**
* The v8 codec class.
*
* @constructor
* @struct
*/
goog.labs.net.webChannel.WireV8 = function() {
/**
* Parser for a response payload. The parser should return an array.
* @private {!goog.string.Parser}
*/
this.parser_ = new goog.json.NativeJsonProcessor();
};
goog.scope(function() {
var WireV8 = goog.labs.net.webChannel.WireV8;
var Wire = goog.labs.net.webChannel.Wire;
/**
* Encodes a standalone message into the wire format.
*
* May throw exception if the message object contains any invalid elements.
*
* @param {!Object|!goog.structs.Map} message The message data.
* V8 only support JS objects (or Map).
* @param {!Array<string>} buffer The text buffer to write the message to.
* @param {string=} opt_prefix The prefix for each field of the object.
*/
WireV8.prototype.encodeMessage = function(message, buffer, opt_prefix) {
var prefix = opt_prefix || '';
try {
goog.structs.forEach(message, function(value, key) {
var encodedValue = value;
if (goog.isObject(value)) {
encodedValue = goog.json.serialize(value);
} // keep the fast-path for primitive types
buffer.push(prefix + key + '=' + encodeURIComponent(encodedValue));
});
} catch (ex) {
// We send a map here because lots of the retry logic relies on map IDs,
// so we have to send something (possibly redundant).
buffer.push(prefix + 'type' + '=' + encodeURIComponent('_badmap'));
throw ex;
}
};
/**
* Encodes all the buffered messages of the forward channel.
*
* @param {!Array<Wire.QueuedMap>} messageQueue The message data.
* V8 only support JS objects.
* @param {number} count The number of messages to be encoded.
* @param {?function(!Object)} badMapHandler Callback for bad messages.
*/
WireV8.prototype.encodeMessageQueue = function(messageQueue, count,
badMapHandler) {
var sb = ['count=' + count];
var offset;
if (count > 0) {
// To save a bit of bandwidth, specify the base mapId and the rest as
// offsets from it.
offset = messageQueue[0].mapId;
sb.push('ofs=' + offset);
} else {
offset = 0;
}
for (var i = 0; i < count; i++) {
var mapId = messageQueue[i].mapId;
var map = messageQueue[i].map;
mapId -= offset;
try {
this.encodeMessage(map, sb, 'req' + mapId + '_');
} catch (ex) {
if (badMapHandler) {
badMapHandler(map);
}
}
}
return sb.join('&');
};
/**
* Decodes a standalone message received from the wire. May throw exception
* if text is ill-formatted.
*
* Must be valid JSON as it is insecure to use eval() to decode JS literals;
* and eval() is disallowed in Chrome apps too.
*
* Invalid JS literals include null array elements, quotas etc.
*
* @param {string} messageText The string content as received from the wire.
* @return {*} The decoded message object.
*/
WireV8.prototype.decodeMessage = function(messageText) {
var response = this.parser_.parse(messageText);
goog.asserts.assert(goog.isArray(response)); // throw exception
return response;
};
}); // goog.scope
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.labs.net.webChannel.WireV8</title>
<script src="../../../base.js"></script>
</head>
<body>
<script>
goog.require('goog.labs.net.webChannel.WireV8Test');
</script>
<div id="debug" style="font-size: small"></div>
</body>
</html>
@@ -0,0 +1,99 @@
// Copyright 2013 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 Unit tests for goog.labs.net.webChannel.WireV8.
*
*/
goog.provide('goog.labs.net.webChannel.WireV8Test');
goog.require('goog.labs.net.webChannel.WireV8');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.labs.net.webChannel.WireV8Test');
var wireCodec;
function setUp() {
wireCodec = new goog.labs.net.webChannel.WireV8();
}
function tearDown() {
}
function testEncodeSimpleMessage() {
// scalar types only
var message = {
a: 'a',
b: 'b'
};
var buff = [];
wireCodec.encodeMessage(message, buff, 'prefix_');
assertEquals(2, buff.length);
assertEquals('prefix_a=a', buff[0]);
assertEquals('prefix_b=b', buff[1]);
}
function testEncodeComplexMessage() {
var message = {
a: 'a',
b: {
x: 1,
y: 2
}
};
var buff = [];
wireCodec.encodeMessage(message, buff, 'prefix_');
assertEquals(2, buff.length);
assertEquals('prefix_a=a', buff[0]);
// a round-trip URI codec
assertEquals('prefix_b={\"x\":1,\"y\":2}', decodeURIComponent(buff[1]));
}
function testEncodeMessageQueue() {
var message1 = {
a: 'a'
};
var queuedMessage1 = {
map: message1,
mapId: 3
};
var message2 = {
b: 'b'
};
var queuedMessage2 = {
map: message2,
mapId: 4
};
var queue = [queuedMessage1, queuedMessage2];
var result = wireCodec.encodeMessageQueue(queue, 2, null);
assertEquals('count=2&ofs=3&req0_a=a&req1_b=b', result);
}
function testDecodeMessage() {
var message = wireCodec.decodeMessage('[{"a":"a", "x":1}, {"b":"b"}]');
assertTrue(goog.isArray(message));
assertEquals(2, message.length);
assertEquals('a', message[0].a);
assertEquals(1, message[0].x);
assertEquals('b', message[1].b);
}
@@ -0,0 +1,75 @@
// Copyright 2013 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 Transport support for WebChannel.
*
* The <code>WebChannelTransport</code> implementation serves as the factory
* for <code>WebChannel</code>, which offers an abstraction for
* point-to-point socket-like communication similar to what BrowserChannel
* or HTML5 WebSocket offers.
*
*/
goog.provide('goog.net.WebChannelTransport');
/**
* A WebChannelTransport instance represents a shared context of logical
* connectivity between a browser client and a remote origin.
*
* Over a single WebChannelTransport instance, multiple WebChannels may be
* created against different URLs, which may all share the same
* underlying connectivity (i.e. TCP connection) whenever possible.
*
* When multi-domains are supported, such as CORS, multiple origins may be
* supported over a single WebChannelTransport instance at the same time.
*
* Sharing between different window contexts such as tabs is not addressed
* by WebChannelTransport. Applications may choose HTML5 shared workers
* or other techniques to access the same transport instance
* across different window contexts.
*
* @interface
*/
goog.net.WebChannelTransport = function() {};
/**
* The latest protocol version. The protocol version is requested
* from the server which is responsible for terminating the underlying
* wire protocols.
*
* @const
* @type {number}
* @private
*/
goog.net.WebChannelTransport.LATEST_VERSION_ = 0;
/**
* Create a new WebChannel instance.
*
* The new WebChannel is to be opened against the server-side resource
* as specified by the given URL. See {@link goog.net.WebChannel} for detailed
* semantics.
*
* @param {string} url The URL path for the new WebChannel instance.
* @param {!goog.net.WebChannel.Options=} opt_options Configuration for the
* new WebChannel instance. The configuration object is reusable after
* the new channel instance is created.
* @return {!goog.net.WebChannel} the newly created WebChannel instance.
*/
goog.net.WebChannelTransport.prototype.createWebChannel = goog.abstractMethod;
@@ -0,0 +1,35 @@
// Copyright 2013 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 Default factory for <code>WebChannelTransport</code> to
* avoid exposing concrete classes to clients.
*
*/
goog.provide('goog.net.createWebChannelTransport');
goog.require('goog.functions');
goog.require('goog.labs.net.webChannel.WebChannelBaseTransport');
/**
* Create a new WebChannelTransport instance using the default implementation.
*
* @return {!goog.net.WebChannelTransport} the newly created transport instance.
*/
goog.net.createWebChannelTransport =
/** @type {function(): !goog.net.WebChannelTransport} */ (
goog.partial(goog.functions.create,
goog.labs.net.webChannel.WebChannelBaseTransport));
@@ -0,0 +1,468 @@
// Copyright 2011 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 Offered as an alternative to XhrIo as a way for making requests
* via XMLHttpRequest. Instead of mirroring the XHR interface and exposing
* events, results are used as a way to pass a "promise" of the response to
* interested parties.
*
*/
goog.provide('goog.labs.net.xhr');
goog.provide('goog.labs.net.xhr.Error');
goog.provide('goog.labs.net.xhr.HttpError');
goog.provide('goog.labs.net.xhr.Options');
goog.provide('goog.labs.net.xhr.PostData');
goog.provide('goog.labs.net.xhr.ResponseType');
goog.provide('goog.labs.net.xhr.TimeoutError');
goog.require('goog.Promise');
goog.require('goog.debug.Error');
goog.require('goog.json');
goog.require('goog.net.HttpStatus');
goog.require('goog.net.XmlHttp');
goog.require('goog.string');
goog.require('goog.uri.utils');
goog.require('goog.userAgent');
goog.scope(function() {
var xhr = goog.labs.net.xhr;
var HttpStatus = goog.net.HttpStatus;
/**
* Configuration options for an XMLHttpRequest.
* - headers: map of header key/value pairs.
* - timeoutMs: number of milliseconds after which the request will be timed
* out by the client. Default is to allow the browser to handle timeouts.
* - withCredentials: whether user credentials are to be included in a
* cross-origin request. See:
* http://www.w3.org/TR/XMLHttpRequest/#the-withcredentials-attribute
* - mimeType: allows the caller to override the content-type and charset for
* the request. See:
* http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-overridemimetype
* - responseType: may be set to change the response type to an arraybuffer or
* blob for downloading binary data. See:
* http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-responsetype]
* - xmlHttpFactory: allows the caller to override the factory used to create
* XMLHttpRequest objects.
* - xssiPrefix: Prefix used for protecting against XSSI attacks, which should
* be removed before parsing the response as JSON.
*
* @typedef {{
* headers: (Object<string>|undefined),
* mimeType: (string|undefined),
* responseType: (xhr.ResponseType|undefined),
* timeoutMs: (number|undefined),
* withCredentials: (boolean|undefined),
* xmlHttpFactory: (goog.net.XmlHttpFactory|undefined),
* xssiPrefix: (string|undefined)
* }}
*/
xhr.Options;
/**
* Defines the types that are allowed as post data.
* @typedef {(ArrayBuffer|Blob|Document|FormData|null|string|undefined)}
*/
xhr.PostData;
/**
* The Content-Type HTTP header name.
* @type {string}
*/
xhr.CONTENT_TYPE_HEADER = 'Content-Type';
/**
* The Content-Type HTTP header value for a url-encoded form.
* @type {string}
*/
xhr.FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded;charset=utf-8';
/**
* Supported data types for the responseType field.
* See: http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-response
* @enum {string}
*/
xhr.ResponseType = {
ARRAYBUFFER: 'arraybuffer',
BLOB: 'blob',
DOCUMENT: 'document',
JSON: 'json',
TEXT: 'text'
};
/**
* Sends a get request, returning a promise that will be resolved
* with the response text once the request completes.
*
* @param {string} url The URL to request.
* @param {xhr.Options=} opt_options Configuration options for the request.
* @return {!goog.Promise<string>} A promise that will be resolved with the
* response text once the request completes.
*/
xhr.get = function(url, opt_options) {
return xhr.send('GET', url, null, opt_options).then(function(request) {
return request.responseText;
});
};
/**
* Sends a post request, returning a promise that will be resolved
* with the response text once the request completes.
*
* @param {string} url The URL to request.
* @param {xhr.PostData} data The body of the post request.
* @param {xhr.Options=} opt_options Configuration options for the request.
* @return {!goog.Promise<string>} A promise that will be resolved with the
* response text once the request completes.
*/
xhr.post = function(url, data, opt_options) {
return xhr.send('POST', url, data, opt_options).then(function(request) {
return request.responseText;
});
};
/**
* Sends a get request, returning a promise that will be resolved with
* the parsed response text once the request completes.
*
* @param {string} url The URL to request.
* @param {xhr.Options=} opt_options Configuration options for the request.
* @return {!goog.Promise<Object>} A promise that will be resolved with the
* response JSON once the request completes.
*/
xhr.getJson = function(url, opt_options) {
return xhr.send('GET', url, null, opt_options).then(function(request) {
return xhr.parseJson_(request.responseText, opt_options);
});
};
/**
* Sends a get request, returning a promise that will be resolved with the
* response as an array of bytes.
*
* Supported in all XMLHttpRequest level 2 browsers, as well as IE9. IE8 and
* earlier are not supported.
*
* @param {string} url The URL to request.
* @param {xhr.Options=} opt_options Configuration options for the request. The
* responseType will be overwritten to 'arraybuffer' if it was set.
* @return {!goog.Promise<!Uint8Array|!Array<number>>} A promise that will be
* resolved with an array of bytes once the request completes.
*/
xhr.getBytes = function(url, opt_options) {
if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
throw new Error('getBytes is not supported in this browser.');
}
var options = opt_options || {};
options.responseType = xhr.ResponseType.ARRAYBUFFER;
return xhr.send('GET', url, null, options).then(function(request) {
// Use the ArrayBuffer response in browsers that support XMLHttpRequest2.
// This covers nearly all modern browsers: http://caniuse.com/xhr2
if (request.response) {
return new Uint8Array(/** @type {!ArrayBuffer} */ (request.response));
}
// Fallback for IE9: the response may be accessed as an array of bytes with
// the non-standard responseBody property, which can only be accessed as a
// VBArray. IE7 and IE8 require significant amounts of VBScript to extract
// the bytes.
// See: http://stackoverflow.com/questions/1919972/
if (goog.global['VBArray']) {
return new goog.global['VBArray'](request['responseBody']).toArray();
}
// Nearly all common browsers are covered by the cases above. If downloading
// binary files in older browsers is necessary, the MDN article "Sending and
// Receiving Binary Data" provides techniques that may work with
// XMLHttpRequest level 1 browsers: http://goo.gl/7lEuGN
throw new xhr.Error(
'getBytes is not supported in this browser.', url, request);
});
};
/**
* Sends a post request, returning a promise that will be resolved with
* the parsed response text once the request completes.
*
* @param {string} url The URL to request.
* @param {xhr.PostData} data The body of the post request.
* @param {xhr.Options=} opt_options Configuration options for the request.
* @return {!goog.Promise<Object>} A promise that will be resolved with the
* response JSON once the request completes.
*/
xhr.postJson = function(url, data, opt_options) {
return xhr.send('POST', url, data, opt_options).then(function(request) {
return xhr.parseJson_(request.responseText, opt_options);
});
};
/**
* Sends a request, returning a promise that will be resolved
* with the XHR object once the request completes.
*
* If content type hasn't been set in opt_options headers, and hasn't been
* explicitly set to null, default to form-urlencoded/UTF8 for POSTs.
*
* @param {string} method The HTTP method for the request.
* @param {string} url The URL to request.
* @param {xhr.PostData} data The body of the post request.
* @param {xhr.Options=} opt_options Configuration options for the request.
* @return {!goog.Promise<!goog.net.XhrLike.OrNative>} A promise that will be
* resolved with the XHR object once the request completes.
*/
xhr.send = function(method, url, data, opt_options) {
return new goog.Promise(function(resolve, reject) {
var options = opt_options || {};
var timer;
var request = options.xmlHttpFactory ?
options.xmlHttpFactory.createInstance() : goog.net.XmlHttp();
try {
request.open(method, url, true);
} catch (e) {
// XMLHttpRequest.open may throw when 'open' is called, for example, IE7
// throws "Access Denied" for cross-origin requests.
reject(new xhr.Error('Error opening XHR: ' + e.message, url, request));
}
// So sad that IE doesn't support onload and onerror.
request.onreadystatechange = function() {
if (request.readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
goog.global.clearTimeout(timer);
// Note: When developing locally, XHRs to file:// schemes return
// a status code of 0. We mark that case as a success too.
if (HttpStatus.isSuccess(request.status) ||
request.status === 0 && !xhr.isEffectiveSchemeHttp_(url)) {
resolve(request);
} else {
reject(new xhr.HttpError(request.status, url, request));
}
}
};
request.onerror = function() {
reject(new xhr.Error('Network error', url, request));
};
// Set the headers.
var contentType;
if (options.headers) {
for (var key in options.headers) {
var value = options.headers[key];
if (goog.isDefAndNotNull(value)) {
request.setRequestHeader(key, value);
}
}
contentType = options.headers[xhr.CONTENT_TYPE_HEADER];
}
// Browsers will automatically set the content type to multipart/form-data
// when passed a FormData object.
var dataIsFormData = (goog.global['FormData'] &&
(data instanceof goog.global['FormData']));
// If a content type hasn't been set, it hasn't been explicitly set to null,
// and the data isn't a FormData, default to form-urlencoded/UTF8 for POSTs.
// This is because some proxies have been known to reject posts without a
// content-type.
if (method == 'POST' && contentType === undefined && !dataIsFormData) {
request.setRequestHeader(xhr.CONTENT_TYPE_HEADER, xhr.FORM_CONTENT_TYPE);
}
// Set whether to include cookies with cross-domain requests. See:
// http://www.w3.org/TR/XMLHttpRequest/#the-withcredentials-attribute
if (options.withCredentials) {
request.withCredentials = options.withCredentials;
}
// Allows setting an alternative response type, such as an ArrayBuffer. See:
// http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-responsetype
if (options.responseType) {
request.responseType = options.responseType;
}
// Allow the request to override the MIME type of the response. See:
// http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-overridemimetype
if (options.mimeType) {
request.overrideMimeType(options.mimeType);
}
// Handle timeouts, if requested.
if (options.timeoutMs > 0) {
timer = goog.global.setTimeout(function() {
// Clear event listener before aborting so the errback will not be
// called twice.
request.onreadystatechange = goog.nullFunction;
request.abort();
reject(new xhr.TimeoutError(url, request));
}, options.timeoutMs);
}
// Trigger the send.
try {
request.send(data);
} catch (e) {
// XMLHttpRequest.send is known to throw on some versions of FF,
// for example if a cross-origin request is disallowed.
request.onreadystatechange = goog.nullFunction;
goog.global.clearTimeout(timer);
reject(new xhr.Error('Error sending XHR: ' + e.message, url, request));
}
});
};
/**
* @param {string} url The URL to test.
* @return {boolean} Whether the effective scheme is HTTP or HTTPs.
* @private
*/
xhr.isEffectiveSchemeHttp_ = function(url) {
var scheme = goog.uri.utils.getEffectiveScheme(url);
// NOTE(user): Empty-string is for the case under FF3.5 when the location
// is not defined inside a web worker.
return scheme == 'http' || scheme == 'https' || scheme == '';
};
/**
* JSON-parses the given response text, returning an Object.
*
* @param {string} responseText Response text.
* @param {xhr.Options|undefined} options The options object.
* @return {Object} The JSON-parsed value of the original responseText.
* @private
*/
xhr.parseJson_ = function(responseText, options) {
var prefixStrippedResult = responseText;
if (options && options.xssiPrefix) {
prefixStrippedResult = xhr.stripXssiPrefix_(
options.xssiPrefix, prefixStrippedResult);
}
return goog.json.parse(prefixStrippedResult);
};
/**
* Strips the XSSI prefix from the input string.
*
* @param {string} prefix The XSSI prefix.
* @param {string} string The string to strip the prefix from.
* @return {string} The input string without the prefix.
* @private
*/
xhr.stripXssiPrefix_ = function(prefix, string) {
if (goog.string.startsWith(string, prefix)) {
string = string.substring(prefix.length);
}
return string;
};
/**
* Generic error that may occur during a request.
*
* @param {string} message The error message.
* @param {string} url The URL that was being requested.
* @param {!goog.net.XhrLike.OrNative} request The XHR that failed.
* @extends {goog.debug.Error}
* @constructor
*/
xhr.Error = function(message, url, request) {
xhr.Error.base(this, 'constructor', message + ', url=' + url);
/**
* The URL that was requested.
* @type {string}
*/
this.url = url;
/**
* The XMLHttpRequest corresponding with the failed request.
* @type {!goog.net.XhrLike.OrNative}
*/
this.xhr = request;
};
goog.inherits(xhr.Error, goog.debug.Error);
/** @override */
xhr.Error.prototype.name = 'XhrError';
/**
* Class for HTTP errors.
*
* @param {number} status The HTTP status code of the response.
* @param {string} url The URL that was being requested.
* @param {!goog.net.XhrLike.OrNative} request The XHR that failed.
* @extends {xhr.Error}
* @constructor
* @final
*/
xhr.HttpError = function(status, url, request) {
xhr.HttpError.base(
this, 'constructor', 'Request Failed, status=' + status, url, request);
/**
* The HTTP status code for the error.
* @type {number}
*/
this.status = status;
};
goog.inherits(xhr.HttpError, xhr.Error);
/** @override */
xhr.HttpError.prototype.name = 'XhrHttpError';
/**
* Class for Timeout errors.
*
* @param {string} url The URL that timed out.
* @param {!goog.net.XhrLike.OrNative} request The XHR that failed.
* @extends {xhr.Error}
* @constructor
* @final
*/
xhr.TimeoutError = function(url, request) {
xhr.TimeoutError.base(this, 'constructor', 'Request timed out', url, request);
};
goog.inherits(xhr.TimeoutError, xhr.Error);
/** @override */
xhr.TimeoutError.prototype.name = 'XhrTimeoutError';
}); // goog.scope
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.labs.net.xhr
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.net.xhrTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,462 @@
// Copyright 2011 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.
goog.provide('goog.labs.net.xhrTest');
goog.setTestOnly('goog.labs.net.xhrTest');
goog.require('goog.Promise');
goog.require('goog.labs.net.xhr');
goog.require('goog.net.WrapperXmlHttpFactory');
goog.require('goog.net.XmlHttp');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
function stubXhrToReturn(status, opt_responseText, opt_latency) {
if (goog.isDefAndNotNull(opt_latency)) {
mockClock = new goog.testing.MockClock(true);
}
var stubXhr = {
sent: false,
aborted: false,
status: 0,
headers: {},
open: function(method, url, async) {
this.method = method;
this.url = url;
this.async = async;
},
setRequestHeader: function(key, value) {
this.headers[key] = value;
},
overrideMimeType: function(mimeType) {
this.mimeType = mimeType;
},
abort: function() {
this.aborted = true;
this.load(0);
},
send: function(data) {
if (mockClock) {
mockClock.tick(opt_latency);
}
this.data = data;
this.sent = true;
this.load(status);
},
load: function(status) {
this.status = status;
if (goog.isDefAndNotNull(opt_responseText)) {
this.responseText = opt_responseText;
}
this.readyState = 4;
if (this.onreadystatechange) this.onreadystatechange();
}
};
stubXmlHttpWith(stubXhr);
}
function stubXhrToThrow(err) {
stubXmlHttpWith(buildThrowingStubXhr(err));
}
function buildThrowingStubXhr(err) {
return {
sent: false,
aborted: false,
status: 0,
headers: {},
open: function(method, url, async) {
this.method = method;
this.url = url;
this.async = async;
},
setRequestHeader: function(key, value) {
this.headers[key] = value;
},
overrideMimeType: function(mimeType) {
this.mimeType = mimeType;
},
send: function(data) {
throw err;
}
};
}
function stubXmlHttpWith(stubXhr) {
goog.net.XmlHttp = function() {
return stubXhr;
};
for (var x in originalXmlHttp) {
goog.net.XmlHttp[x] = originalXmlHttp[x];
}
}
var xhr = goog.labs.net.xhr;
var originalXmlHttp = goog.net.XmlHttp;
var mockClock;
function tearDown() {
if (mockClock) {
mockClock.dispose();
mockClock = null;
}
goog.net.XmlHttp = originalXmlHttp;
}
/**
* Tests whether the test was loaded from a file: protocol. Tests that use a
* real network request cannot be run from the local file system due to
* cross-origin restrictions, but will run if the tests are hosted on a server.
* A log message is added to the test case to warn users that the a test was
* skipped.
*
* @return {boolean} Whether the test is running on a local file system.
*/
function isRunningLocally() {
if (window.location.protocol == 'file:') {
var testCase = goog.global['G_testRunner'].testCase;
testCase.saveMessage('Test skipped while running on local file system.');
return true;
}
return false;
}
function testSimpleRequest() {
if (isRunningLocally()) return;
return xhr.send('GET', 'testdata/xhr_test_text.data').then(function(xhr) {
assertEquals('Just some data.', xhr.responseText);
assertEquals(200, xhr.status);
});
}
function testGetText() {
if (isRunningLocally()) return;
return xhr.get('testdata/xhr_test_text.data').then(function(responseText) {
assertEquals('Just some data.', responseText);
});
}
function testGetTextWithJson() {
if (isRunningLocally()) return;
return xhr.get('testdata/xhr_test_json.data').then(function(responseText) {
assertEquals('while(1);\n{"stat":"ok","count":12345}\n', responseText);
});
}
function testPostText() {
if (isRunningLocally()) return;
return xhr.post('testdata/xhr_test_text.data', 'post-data').then(
function(responseText) {
// No good way to test post-data gets transported.
assertEquals('Just some data.', responseText);
});
}
function testGetJson() {
if (isRunningLocally()) return;
return xhr.getJson(
'testdata/xhr_test_json.data', {xssiPrefix: 'while(1);\n'}).then(
function(responseObj) {
assertEquals('ok', responseObj['stat']);
assertEquals(12345, responseObj['count']);
});
}
function testGetBytes() {
if (isRunningLocally()) return;
// IE8 requires a VBScript fallback to read the bytes from the response.
if (goog.userAgent.IE && !goog.userAgent.isDocumentMode(9)) {
return;
}
return xhr.getBytes('testdata/cleardot.gif').then(function(bytes) {
assertElementsEquals([
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0xFF,
0x00, 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, 0x01, 0x00,
0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3B
], bytes);
});
}
function testSerialRequests() {
if (isRunningLocally()) return;
return xhr.get('testdata/xhr_test_text.data').
then(function(response) {
return xhr.getJson(
'testdata/xhr_test_json.data', {xssiPrefix: 'while(1);\n'});
}).then(function(responseObj) {
// Data that comes through to callbacks should be from the 2nd request.
assertEquals('ok', responseObj['stat']);
assertEquals(12345, responseObj['count']);
});
}
function testBadUrlDetectedAsError() {
if (isRunningLocally()) return;
return xhr.getJson('unknown-file.dat').then(
fail /* opt_onFulfilled */,
function(err) {
assertTrue(
'Error should be an HTTP error', err instanceof xhr.HttpError);
assertEquals(404, err.status);
assertNotNull(err.xhr);
});
}
function testBadOriginTriggersOnErrorHandler() {
return xhr.get('http://www.google.com').then(
fail /* opt_onFulfilled */,
function(err) {
// In IE this will be a goog.labs.net.xhr.Error since it is thrown
// when calling xhr.open(), other browsers will raise an HttpError.
assertTrue('Error should be an xhr error', err instanceof xhr.Error);
assertNotNull(err.xhr);
});
}
//============================================================================
// The following tests use a stubbed out XMLHttpRequest.
//============================================================================
function testAbortRequest() {
stubXhrToReturn(200);
var promise = xhr.send('GET', 'test-url', null).thenCatch(
function(error) {
assertTrue(error instanceof goog.Promise.CancellationError);
});
promise.cancel();
return promise;
}
function testSendNoOptions() {
var called = false;
stubXhrToReturn(200);
assertFalse('Callback should not yet have been called', called);
return xhr.send('GET', 'test-url', null).then(function(stubXhr) {
called = true;
assertEquals('GET', stubXhr.method);
assertEquals('test-url', stubXhr.url);
});
}
function testSendPostSetsDefaultHeader() {
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', null).then(function(stubXhr) {
assertEquals('POST', stubXhr.method);
assertEquals('test-url', stubXhr.url);
assertEquals('application/x-www-form-urlencoded;charset=utf-8',
stubXhr.headers['Content-Type']);
});
}
function testSendPostDoesntSetHeaderWithFormData() {
if (!goog.global['FormData']) { return; }
var formData = new goog.global['FormData']();
formData.append('name', 'value');
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', formData).then(function(stubXhr) {
assertEquals('POST', stubXhr.method);
assertEquals('test-url', stubXhr.url);
assertEquals(undefined, stubXhr.headers['Content-Type']);
});
}
function testSendPostHeaders() {
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', null,
{ headers: {'Content-Type': 'text/plain', 'X-Made-Up': 'FooBar'} }).
then(function(stubXhr) {
assertEquals('POST', stubXhr.method);
assertEquals('test-url', stubXhr.url);
assertEquals('text/plain', stubXhr.headers['Content-Type']);
assertEquals('FooBar', stubXhr.headers['X-Made-Up']);
});
}
function testSendPostHeadersWithFormData() {
if (!goog.global['FormData']) { return; }
var formData = new goog.global['FormData']();
formData.append('name', 'value');
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', formData,
{ headers: {'Content-Type': 'text/plain', 'X-Made-Up': 'FooBar'} }).
then(function(stubXhr) {
assertEquals('POST', stubXhr.method);
assertEquals('test-url', stubXhr.url);
assertEquals('text/plain', stubXhr.headers['Content-Type']);
assertEquals('FooBar', stubXhr.headers['X-Made-Up']);
});
}
function testSendNullPostHeaders() {
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', null, {
headers: {
'Content-Type': null,
'X-Made-Up': 'FooBar',
'Y-Made-Up': null
}
}).then(function(stubXhr) {
assertEquals('POST', stubXhr.method);
assertEquals('test-url', stubXhr.url);
assertEquals(undefined, stubXhr.headers['Content-Type']);
assertEquals('FooBar', stubXhr.headers['X-Made-Up']);
assertEquals(undefined, stubXhr.headers['Y-Made-Up']);
});
}
function testSendNullPostHeadersWithFormData() {
if (!goog.global['FormData']) { return; }
var formData = new goog.global['FormData']();
formData.append('name', 'value');
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', formData, {
headers: {
'Content-Type': null,
'X-Made-Up': 'FooBar',
'Y-Made-Up': null
}
}).then(function(stubXhr) {
assertEquals('POST', stubXhr.method);
assertEquals('test-url', stubXhr.url);
assertEquals(undefined, stubXhr.headers['Content-Type']);
assertEquals('FooBar', stubXhr.headers['X-Made-Up']);
assertEquals(undefined, stubXhr.headers['Y-Made-Up']);
});
}
function testSendWithCredentials() {
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', null, {withCredentials: true}).
then(function(stubXhr) {
assertTrue('XHR should have been sent', stubXhr.sent);
assertTrue(stubXhr.withCredentials);
});
}
function testSendWithMimeType() {
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', null, {mimeType: 'text/plain'}).
then(function(stubXhr) {
assertTrue('XHR should have been sent', stubXhr.sent);
assertEquals('text/plain', stubXhr.mimeType);
});
}
function testSendWithHttpError() {
stubXhrToReturn(500);
return xhr.send('POST', 'test-url', null).then(
fail /* opt_onResolved */,
function(err) {
assertTrue(err instanceof xhr.HttpError);
assertTrue(err.xhr.sent);
assertEquals(500, err.status);
});
}
function testSendWithTimeoutNotHit() {
stubXhrToReturn(200, null /* opt_responseText */, 1400 /* opt_latency */);
return xhr.send('POST', 'test-url', null, {timeoutMs: 1500}).
then(function(stubXhr) {
assertTrue(mockClock.getTimeoutsMade() > 0);
assertTrue('XHR should have been sent', stubXhr.sent);
assertFalse('XHR should not have been aborted', stubXhr.aborted);
});
}
function testSendWithTimeoutHit() {
stubXhrToReturn(200, null /* opt_responseText */, 50 /* opt_latency */);
return xhr.send('POST', 'test-url', null, {timeoutMs: 50}).then(
fail /* opt_onResolved */,
function(err) {
assertTrue('XHR should have been sent', err.xhr.sent);
assertTrue('XHR should have been aborted', err.xhr.aborted);
assertTrue(err instanceof xhr.TimeoutError);
});
}
function testCancelRequest() {
stubXhrToReturn(200, null /* opt_responseText */, 25);
var promise = xhr.send('GET', 'test-url', null, {timeoutMs: 50});
promise.then(
fail /* opt_onResolved */,
function(error) {
assertTrue('XHR should have been sent', error.xhr.sent);
if (error instanceof goog.Promise.CancellationError) {
error.xhr.abort();
}
assertTrue('XHR should have been aborted', error.xhr.aborted);
assertTrue(error instanceof goog.Promise.CancellationError);
});
promise.cancel();
return promise;
}
function testGetJson() {
var stubXhr = stubXhrToReturn(200, '{"a": 1, "b": 2}');
xhr.getJson('test-url').then(function(responseObj) {
assertObjectEquals({a: 1, b: 2}, responseObj);
});
}
function testGetJsonWithXssiPrefix() {
stubXhrToReturn(200, 'while(1);\n{"a": 1, "b": 2}');
return xhr.getJson('test-url', {xssiPrefix: 'while(1);\n'}).then(
function(responseObj) {
assertObjectEquals({a: 1, b: 2}, responseObj);
});
}
function testSendWithClientException() {
stubXhrToThrow(new Error('CORS XHR with file:// schemas not allowed.'));
return xhr.send('POST', 'file://test-url', null).then(
fail /* opt_onResolved */,
function(err) {
assertFalse('XHR should not have been sent', err.xhr.sent);
assertTrue(err instanceof Error);
assertTrue(
/CORS XHR with file:\/\/ schemas not allowed./.test(err.message));
});
}
function testSendWithFactory() {
stubXhrToReturn(200);
var options = {
xmlHttpFactory: new goog.net.WrapperXmlHttpFactory(
goog.partial(buildThrowingStubXhr, new Error('Bad factory')),
goog.net.XmlHttp.getOptions)
};
return xhr.send('POST', 'file://test-url', null, options).then(
fail /* opt_onResolved */,
function(err) {
assertTrue(err instanceof Error);
});
}