1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @constructor
 26  * @class
 27  * This static class provides an interface for send requests to a server. It
 28  * essentially wraps {@link AjxRpcRequest}. This {@link AjxRpc} link maintains a cache of
 29  * {@link AjxRpcRequest} objects which it attempts to reuse before allocating additional
 30  * objects. It also has a mechanism whereby if an {@link AjxRpcRequest} object is 
 31  * in a "busy" state for a extended period of time, it will reap it appropriately.
 32  *
 33  * @author Ross Dargahi
 34  * @author Conrad Damon
 35  * 
 36  * @see AjxRpcRequest
 37  */
 38 AjxRpc = function() {
 39 };
 40 
 41 AjxRpc.__rpcCache		= [];		// The pool of RPC contexts available
 42 AjxRpc.__rpcOutstanding	= {};		// The pool of RPC contexts in use
 43 
 44 AjxRpc.__RPC_CACHE_MAX		= 50;		// maximum number of busy contexts we can have
 45 AjxRpc.__RPC_ID				= 0;		// used for context IDs
 46 AjxRpc.__RPC_IN_USE			= 0;		// number of contexts that are busy
 47 AjxRpc.__RPC_HIGH_WATER		= 0;		// high water mark for busy contexts
 48 AjxRpc.__RPC_REAP_AGE		= 300000;	// 5 minutes; mark any context older than this (in ms) as free
 49 AjxRpc.__RPC_REAP_INTERVAL	= 1800000;	// 30 minutes; run the reaper this often
 50 
 51 /**
 52  * Submits a request to a URL. The request is handled through a pool of request
 53  * contexts (each a wrapped XmlHttpRequest). The context does the real work.
 54  *
 55  * @param {string} [requestStr] 	the HTTP request string/document
 56  * @param {string} serverUrl 	the request target 
 57  * @param {array} [requestHeaders] an array of HTTP request headers
 58  * @param {AjxCallback} callback 	the callback for asynchronous requests. This callback 
 59  * 		will be invoked when the requests completes. It will be passed the same
 60  * 		values as when this method is invoked synchronously (see the return values
 61  * 		below) with the exception that if the call times out (see timeout param 
 62  * 		below), then the object passed to the callback will be the same as in the 
 63  * 		error case with the exception that the status will be set to 
 64  * 		{@link AjxRpcRequest.TIMEDOUT}.
 65  * @param {Constant} [method] 		the HTTP method -- GET, POST, PUT, DELETE. if <code>true</code>, use get method for backward compatibility
 66  * @param {number} [timeout] 		the timeout (in milliseconds) after which the request is canceled
 67  * 
 68  * @return {object|hash}	if invoking in asynchronous mode, then it will return the id of the 
 69  * 		underlying {@link AjxRpcRequest} object. Else if invoked synchronously, if
 70  * 		there is no error (i.e. we get a HTTP result code of 200 from the server),
 71  * 		an object with the following attributes is returned
 72  * 		<ul>
 73  * 		<li>text - the string response text</li>
 74  * 		<li>xml - the string response xml</li>
 75  * 		<li>success - boolean set to true</li>
 76  * 		</ul>
 77  * 		If there is an error, then the following will be returned
 78  * 		<ul>
 79  * 		<li>text - the string response text<li>
 80  * 		<li>xml - the string response xml </li>
 81  * 		<li>success - boolean set to <code>false</code></li>
 82  * 		<li>status - HTTP status</li>
 83  * 		</ul>
 84  * 
 85  * @throws	{AjxException.NETWORK_ERROR}	a network error occurs
 86  * @throws	{AjxException.UNKNOWN_ERROR}	an unknown error occurs
 87  * 
 88  * @see	AjxRpcRequest#invoke
 89  * 
 90  */
 91 AjxRpc.invoke =
 92 function(requestStr, serverUrl, requestHeaders, callback, method, timeout) {
 93 
 94 	var asyncMode = (callback != null);
 95 	var rpcCtxt = AjxRpc.__getFreeRpcCtxt();
 96 
 97 	try {
 98 		var response = rpcCtxt.invoke(requestStr, serverUrl, requestHeaders, callback, method, timeout);
 99 	} catch (ex) {
100 		var newEx = new AjxException();
101 		newEx.method = "AjxRpc.prototype._invoke";
102 		if (ex instanceof Error) {
103 			newEx.detail = ex.message;
104 			newEx.code = AjxException.NETWORK_ERROR;
105 			newEx.msg = "Network error";
106 		} else if (ex.code == 101){
107 			// Chrome 
108 			newEx.detail = ex.message;
109 			newEx.code = AjxException.NETWORK_ERROR;
110 			newEx.msg = "Network error";
111 		} else {
112 			newEx.detail = ex.toString();
113 			newEx.code = AjxException.UNKNOWN_ERROR;
114 			newEx.msg = "Unknown Error";
115 		}
116 		// exception hit: we're done whether sync or async, free the context
117 		AjxRpc.freeRpcCtxt(rpcCtxt);
118 		throw newEx;
119 	}
120 	if (!asyncMode) {
121 		// we've returned from a sync request, free the context
122 		AjxRpc.freeRpcCtxt(rpcCtxt);
123 	}
124 	return response;
125 };
126 
127 /**
128  * @private
129  */
130 AjxRpc.freeRpcCtxt =
131 function(rpcCtxt) {
132 	// we're done using this rpcCtxt. Add it back to the pool
133 	if (AjxRpc.__rpcOutstanding[rpcCtxt.id]) {
134 		AjxDebug.println(AjxDebug.RPC, AjxDebug._getTimeStamp() + " --- freeing rpcCtxt " + rpcCtxt.id);
135 		AjxRpc.__rpcCache.push(rpcCtxt);
136 		delete AjxRpc.__rpcOutstanding[rpcCtxt.id];
137 		AjxRpc.__RPC_IN_USE--;
138 	}
139 };
140 
141 AjxRpc.removeRpcCtxt =
142 function(rpcCtxt) {
143 	AjxDebug.println(AjxDebug.RPC, AjxDebug._getTimeStamp() + " REMOVE rpcCtxt " + rpcCtxt.id);
144 	if (AjxRpc.__rpcOutstanding[rpcCtxt.id]) {
145 		delete AjxRpc.__rpcOutstanding[rpcCtxt.id];
146 		AjxRpc.__RPC_IN_USE--;
147 	}
148 	AjxUtil.arrayRemove(AjxRpc.__rpcCache, rpcCtxt);
149 };
150 
151 /**
152  * Returns the request from the RPC context with the given ID.
153  *
154  * @param {String} id RPC context ID
155  * 
156  * @return The <i>AjxRpcRequest</i> object associated with <code>id</code> or null
157  * 		if no object exists for the supplied id
158  * @type AjxRpcRequest
159  * 
160  * @private
161  */
162 AjxRpc.getRpcRequestById = 
163 function(id) {
164 	return (AjxRpc.__rpcOutstanding[id]);
165 };
166 
167 /**
168  * Factory method for getting context objects.
169  * 
170  * @private
171  */
172 AjxRpc.__getFreeRpcCtxt = 
173 function() {
174 	var rpcCtxt;
175 
176 	if (AjxRpc.__rpcCache.length > 0) {
177 		rpcCtxt = AjxRpc.__rpcCache.pop();
178 		AjxDebug.println(AjxDebug.RPC, AjxDebug._getTimeStamp() + " reusing RPC ID " + rpcCtxt.id);
179 	} else {
180 		if (AjxRpc.__RPC_IN_USE < AjxRpc.__RPC_CACHE_MAX) {
181 			// we haven't reached our limit, so create a new AjxRpcRequest
182 			var id = "__RpcCtxt_" + AjxRpc.__RPC_ID;
183 			rpcCtxt = new AjxRpcRequest(id);
184 			AjxRpc.__RPC_ID++;
185 			AjxDebug.println(AjxDebug.RPC, AjxDebug._getTimeStamp() + " Created RPC " + id);
186 		} else {
187 			// yikes, we're out of rpc's! Look for an old one to kill.
188 			rpcCtxt = AjxRpc.__reap();
189 
190 			// if reap didn't find one either, bail.
191 			if (!rpcCtxt) {
192 				var text = [];
193 				for (var i in AjxRpc.__rpcOutstanding) {
194 					var rpcCtxt = AjxRpc.__rpcOutstanding[i];
195 					text.push(rpcCtxt.methodName);
196 				}
197 				var detail = text.join("<br>") + "<br>";
198 				AjxDebug.println(AjxDebug.RPC, AjxDebug._getTimeStamp() + " Out of RPC cache!!! Outstanding requests: " + detail);
199 				throw new AjxException("Out of RPC cache", AjxException.OUT_OF_RPC_CACHE, "AjxRpc.__getFreeRpcCtxt", detail);
200 			}
201 		}
202 	}
203 
204 	AjxRpc.__rpcOutstanding[rpcCtxt.id] = rpcCtxt;
205 	AjxRpc.__RPC_IN_USE++;
206 	if (AjxRpc.__RPC_IN_USE > AjxRpc.__RPC_HIGH_WATER) {
207 		AjxRpc.__RPC_HIGH_WATER = AjxRpc.__RPC_IN_USE;
208 		AjxDebug.println(AjxDebug.RPC, AjxDebug._getTimeStamp() + " High water mark: " + AjxRpc.__RPC_HIGH_WATER);
209 	}
210 
211 	// always reset timestamp before returning rpcCtxt
212 	rpcCtxt.timestamp = (new Date()).getTime();
213 	return rpcCtxt;
214 };
215 
216 /**
217  * Frees expired contexts.
218  * 
219  * @param {boolean}	all		if true, frees all expired contexts; otherwise, returns the first one it finds
220  * @private
221  */
222 AjxRpc.__reap =
223 function(all) {
224 	var rpcCtxt;
225 	var time = (new Date()).getTime();
226 	AjxDebug.println(AjxDebug.RPC, AjxDebug._getTimeStamp() + " Running RPC context reaper");
227 	for (var i in AjxRpc.__rpcOutstanding) {
228 		rpcCtxt = AjxRpc.__rpcOutstanding[i];
229 		if ((rpcCtxt.timestamp + AjxRpc.__RPC_REAP_AGE) < time) {
230 			DBG.println(AjxDebug.DBG1, "AjxRpc.__reap: cleared RPC context " + rpcCtxt.id);
231 			AjxDebug.println(AjxDebug.RPC, AjxDebug._getTimeStamp() + " AjxRpc.__reap: cleared RPC context " + rpcCtxt.id);
232 			rpcCtxt.cancel();
233 			delete AjxRpc.__rpcOutstanding[i];
234 			AjxRpc.__RPC_IN_USE--;
235 			if (!all) {
236 				return rpcCtxt;
237 			}
238 		}
239 	}
240 	return null;
241 };
242 
243 window.setInterval(AjxRpc.__reap.bind(null, true), AjxRpc.__RPC_REAP_INTERVAL);
244