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']
, function(_, ErrorCode, Configuration, ManagedError, DataFormats, throttledNQueued)
{
	/**
	 * 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> This method currently uses a more <em>expensive I/O</em> method
	 *    (Callback method Javascript loading) than a simple
	 *    Ajax request. Therefore 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 to
	 *    support for the IE 7 browser which lacks a Ajax CORS implementation.)</small>
	 * @param {(userCallback|function)} 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
	 * @throws {LiveSupport.ManagedError} in case the callback is missing
	 * @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;
		}

		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
		window[Configuration.configuration.GLOBAL_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.
	 * @param {string} departmentKey
	 * @param {string} status
	 * @function module:operatorAvailability~_availabilityCallback
	 * @inner
	 * @throws {LiveSupport.ManagedError} in case the user callback can not be found
	 */
	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.
	 * @function module:operatorAvailability~abortUnfinishedRequest
	 * @param {string} departmentKey
	 * @throws {LiveSupport.ManagedError} in case the user callback can not be found
	 */
	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 : throttledNQueued(getOperatorAvailability, 20000)
	};
});