Source: operatorAvailability.js

/**
 * operatorAvailability.js
 *
 * Created by Matthias Seemann on 7.01.2013.
 * Copyright (c) 2013 Visisoft GbR. All rights reserved.
 *
 * @module operatorAvailability
 * @desc internal module
 */

define(['underscore', 'api/errorCodes', 'api/configuration', 'api/ManagedError', 'lib/DataFormats','lib/throttledNQueued', 'lib/ajax']
, function(_, ErrorCode, Configuration, ManagedError, DataFormats, throttledNQueued, ajax)
{
	/**
	 * Possible results of the availability check
	 * @enum {string}
	 * @readonly
	 * @memberof LiveSupport.VisitorAPI
	 */
	var Availability = {
		AVAILABLE : 'online',
		BUSY : 'busy',
		OFFLINE : 'offline'
	};

	var availabilityCheckScriptTag;
	var callbacksByDepartments = {};
	var timerByDepartments = {};

	/**
	 * Checks the availability status of the live support service.<br>
	 * Possible result values are {@link LiveSupport.VisitorAPI.Availability}.<br/>
	 * While a query is in progress identical queries with the same department id
	 * do not result in a new network request.<br><br>
	 *    <em>Please note:</em> On Internet Explorer 9 and older this method currently uses JSONP for network I/O.
	 *    Therefore on that browsers this method is internally <em>throttled</em> in that way
	 *    that is executes at most once every 20 seconds. In the meantime incoming calls are queued.
	 * 	This is in order to prevent a performance drain from the browser and server.
	 * 	<small>(This behaviour is due missing support for Ajax CORS in IE9 and older.)</small>
	 * <br><br><strong>Throws</strong> {@link LiveSupport.ManagedError} in case the callback is missing.
	 * @param {resultCallback} callback the user callback which receives a result
	 * of {@link LiveSupport.VisitorAPI.Availability}
	 * @param {string} [department=the previously associated department id] a particular yalst department id,
	 * if empty string any department applies
	 * @memberof LiveSupport.VisitorAPI
	 * @see module:operatorAvailability~_availabilityCallback
	 * @see module:operatorAvailability~abortUnfinishedRequest
	 * @example
LiveSupport.VisitorAPI.getOperatorAvailability(onAvailabilityDetected, "C");

function onAvailabilityDetected(status){
	if (status instanceof Error){
		alert("An error has occurred:" + status.toString());
	}
	else if (status == LiveSupport.VisitorAPI.Availability.AVAILABLE){
		var startButton = document.getElementById('start_button_dept_C');

		startButton.removeAttribute("disabled");
	}
}
	 */
	function getOperatorAvailability(callback, department)
	{
		if (typeof callback != "function")
		{
			throw new ManagedError(ErrorCode.ParameterTypeError
				, "Call to getOperatorAvailability is missing callback parameter!");
		}

		if (!Configuration.configuration.IS_ASSOCIATED)
		{
			_.defer(_.bind(callback, null, new ManagedError(ErrorCode.ServerNotYetAssociated
				, "Illegal API use. Attempt to call a method before associated to a live support server.")));

			return;
		}

		var queryParameters = {rnd: Math.random(), site: Configuration.configuration.PRODUCT_SITE};

		if ((typeof department == "string") && (department.length > 0))
		{
			queryParameters.dept = department;
		}
		else if ((typeof department != "string")
			&& (typeof Configuration.configuration.PRODUCT_DEPARTMENT == "string")
			&& (Configuration.configuration.PRODUCT_DEPARTMENT.length > 0))
		{
			queryParameters.dept = Configuration.configuration.PRODUCT_DEPARTMENT;
		}


		if (ajax.isCORSSupported() && ajax.isJsonSupported())
		{
			var didAbortAjaxRequest = false;
			var responseDurationTimeout;

			queryParameters.ajax = 1;

			var request = ajax.getJson(
				Configuration.configuration.PRODUCT_URL + 'online.js.php?'
				+ DataFormats.urlQueryStringFromObject(queryParameters)
			, function(response, error)
				{
					clearTimeout(responseDurationTimeout);

					if (error)
					{
						if (!didAbortAjaxRequest)
						{
							callback(new ManagedError(ErrorCode.NetworkError
								, "Network error: " + error.message)
							);
						}

						return;
					}

					if (_.contains(["online", "busy", "offline"], response["availability"]))
					{
						callback(response["availability"]);

						return;
					}

					var errorText = response["error"] || "Unknown error";

					callback(new ManagedError(ErrorCode.InternalError, errorText));
				}
			);

			responseDurationTimeout = setTimeout(function()
			{
				didAbortAjaxRequest = true;

				request.abort();

				callback(new ManagedError(ErrorCode.TimeoutError
					, "The operatorAvailability request has timed out!"));

			}, Configuration.configuration.SERVER_TIMEOUT_SECS * 1000);
		}
		else
		{
			/// JSONP for non CORS browsers IE9-

			var departmentKey = (typeof queryParameters.dept != "undefined") ? queryParameters.dept : "allDepartments";

			if (!(departmentKey in callbacksByDepartments))
			{
				callbacksByDepartments[departmentKey] = [];
			}

			callbacksByDepartments[departmentKey].push(callback);

			/// collect identical queries
			if (callbacksByDepartments[departmentKey].length > 1)
			{
				return;
			}

			/// introduce a globally visible callback function for the returned script
			/// which is just a curried version of the general _availabilityCallback

			/// for JSONP we must use global variables even if loaded as RequireJS module

			var namespace = window[Configuration.configuration.GLOBAL_NAMESPACE]
				= window[Configuration.configuration.GLOBAL_NAMESPACE] || {};

			namespace.VisitorAPI = namespace.VisitorAPI || {};

			namespace.VisitorAPI["_availabilityCallback" + departmentKey]
					= _.bind(_availabilityCallback, null, departmentKey);

			queryParameters.callback = Configuration.configuration.GLOBAL_NAMESPACE
				+ ".VisitorAPI._availabilityCallback" + departmentKey;

			if (availabilityCheckScriptTag)
			{
				availabilityCheckScriptTag.parentNode.removeChild(availabilityCheckScriptTag);
			}

			availabilityCheckScriptTag = document.createElement("script");
			var siblingElement = document.getElementsByTagName("script")[0];
			siblingElement.parentNode.insertBefore(availabilityCheckScriptTag, siblingElement);

			availabilityCheckScriptTag.src = Configuration.configuration.PRODUCT_URL + 'online.js.php?'
				+ DataFormats.urlQueryStringFromObject(queryParameters);


			timerByDepartments[departmentKey] = setTimeout(_.bind(abortUnfinishedRequest, null, departmentKey)
				, Configuration.configuration.SERVER_TIMEOUT_SECS * 1000);
		}
	}

	/**
	 * Internal callback function for <tt>getOperatorAvailability</tt>.<br>
	 * It gets called by the Javascript engine during execution of an externally loaded script.
	 * <br><br><strong>Throws</strong> {@link LiveSupport.ManagedError} in case the user callback can not be found
	 * @param {string} departmentKey
	 * @param {string} status
	 * @function module:operatorAvailability~_availabilityCallback
	 * @inner
	 *
	 */
	function _availabilityCallback(departmentKey, status)
	{
		if (!(departmentKey in callbacksByDepartments)
			|| (typeof callbacksByDepartments[departmentKey].length != "number"))
		{
			throw new ManagedError(ErrorCode.InternalError,
				"user callback for operatorAvailability in department '" + departmentKey + "' not found!");
		}

		var result;

		switch (status)
		{
			case "offline":
				result = Availability.OFFLINE;

				break;
			case "online":
				result = Availability.AVAILABLE;

				break;
			case "busy":
				result = Availability.BUSY;

				break;
			default:
				result = new ManagedError(ErrorCode.InternalError
					, "Unexpected result returned from server:" + status);
		}

		var userCallbacks = callbacksByDepartments[departmentKey];

		while (userCallbacks.length)
		{
			var callback = userCallbacks.shift();
			callback(result);
		}

		var timer = timerByDepartments[departmentKey];
		if (timer)
		{
			clearTimeout(timer);
			timerByDepartments[departmentKey] = 0;
		}
	}

	/**
	 * a <tt>setTimeout</tt> callback.
	 * <br><br><strong>Throws</strong> {@link LiveSupport.ManagedError} in case the user callback can not be found
	 * @function module:operatorAvailability~abortUnfinishedRequest
	 * @param {string} departmentKey
	 *
	 */
	function abortUnfinishedRequest(departmentKey)
	{
		if (!(departmentKey in callbacksByDepartments)
			|| (typeof callbacksByDepartments[departmentKey].length != "number"))
		{
			throw new ManagedError(ErrorCode.InternalError,
				"user callback for operatorAvailability in department '" + departmentKey + "' not found!");
		}

		var userCallbacks = callbacksByDepartments[departmentKey];

		while (userCallbacks.length)
		{
			var callback = userCallbacks.shift();
			callback(new ManagedError(ErrorCode.TimeoutError, "The operatorAvailability request has timed out!"));
		}

		timerByDepartments[departmentKey] = 0;
	}

	return {Availability : Availability
		, getOperatorAvailability : (ajax.isCORSSupported() && ajax.isJsonSupported())
			? getOperatorAvailability
			: throttledNQueued(getOperatorAvailability, 20000)
	};
});