From a3b3c42bd2b80157c8a29747887cac2fadda5800 Mon Sep 17 00:00:00 2001 From: Gary Sharp Date: Thu, 16 May 2013 19:14:29 +1000 Subject: [PATCH] Update: SignalR 1.1.0; T4MVC 3.6.4; Json.NET --- .../Scripts/Modules/jQuery-SignalR.js | 282 ++++++++++++------ .../Scripts/Modules/jQuery-SignalR.js.bundle | 2 +- .../Scripts/Modules/jQuery-SignalR.min.js | 2 +- .../Scripts/Modules/jQuery-SignalR.min.js.map | 6 +- ...gnalR-1.0.1.js => jquery.signalR-1.1.0.js} | 280 ++++++++++++----- Disco.Web/T4MVC.cs | 2 +- Disco.Web/T4MVC.tt | 90 ++++-- Disco.Web/packages.config | 16 +- 8 files changed, 469 insertions(+), 211 deletions(-) rename Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR/{jquery.signalR-1.0.1.js => jquery.signalR-1.1.0.js} (89%) diff --git a/Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR.js b/Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR.js index 04f59274..d81abe46 100644 --- a/Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR.js +++ b/Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR.js @@ -1,8 +1,8 @@ -///#source 1 1 /ClientSource/Scripts/Modules/jQuery-SignalR/jquery.signalR-1.0.1.js +///#source 1 1 /ClientSource/Scripts/Modules/jQuery-SignalR/jquery.signalR-1.1.0.js /* jquery.signalR.core.js */ /*global window:false */ /*! - * ASP.NET SignalR JavaScript Library v1.0.1 + * ASP.NET SignalR JavaScript Library v1.1.0 * http://signalr.net/ * * Copyright Microsoft Open Technologies, Inc. All rights reserved. @@ -121,6 +121,26 @@ return new signalR.fn.init(url, qs, logging); }; + signalR._ = { + defaultContentType: "application/x-www-form-urlencoded; charset=UTF-8", + ieVersion: (function () { + var version, + matches; + + if (window.navigator.appName === 'Microsoft Internet Explorer') { + // Check if the user agent has the pattern "MSIE (one or more numbers).(one or more numbers)"; + matches = /MSIE ([0-9]+\.[0-9]+)/.exec(window.navigator.userAgent); + + if (matches) { + version = window.parseFloat(matches[1]); + } + } + + // undefined value means not IE + return version; + })() + }; + signalR.events = events; signalR.changeState = changeState; @@ -148,6 +168,7 @@ /// The designated transports that the user has specified. /// The connection that will be using the requested transports. Used for logging purposes. /// + if ($.isArray(requestedTransport)) { // Go through transport array and remove an "invalid" tranports for (var i = requestedTransport.length - 1; i >= 0; i--) { @@ -167,6 +188,12 @@ connection.log("Invalid transport: " + requestedTransport.toString()); requestedTransport = null; } + else if (requestedTransport === "auto" && signalR._.ieVersion <= 8) + { + // If we're doing an auto transport and we're IE8 then force longPolling, #1764 + return ["longPolling"]; + + } return requestedTransport; } @@ -226,6 +253,8 @@ ajaxDataType: "json", + contentType: "application/json; charset=UTF-8", + logging: false, state: signalR.connectionState.disconnected, @@ -332,6 +361,8 @@ connection.log("Using jsonp because this browser doesn't support CORS"); } } + + connection.contentType = signalR._.defaultContentType; } connection.ajaxDataType = config.jsonp ? "jsonp" : "json"; @@ -348,7 +379,7 @@ if (index >= transports.length) { if (!connection.transport) { // No transport initialized successfully - $(connection).triggerHandler(events.onError, "SignalR: No transport could be initialized successfully. Try specifying a different transport or none at all for auto initialization."); + $(connection).triggerHandler(events.onError, ["SignalR: No transport could be initialized successfully. Try specifying a different transport or none at all for auto initialization."]); deferred.reject("SignalR: No transport could be initialized successfully. Try specifying a different transport or none at all for auto initialization."); // Stop the connection if it has connected and move it into the disconnected state connection.stop(); @@ -397,6 +428,7 @@ global: false, cache: false, type: "GET", + contentType: connection.contentType, data: {}, dataType: connection.ajaxDataType, error: function (error) { @@ -437,7 +469,7 @@ } if (!res.ProtocolVersion || res.ProtocolVersion !== "1.2") { - $(connection).triggerHandler(events.onError, "You are using a version of the client that isn't compatible with the server. Client version 1.2, server version " + res.ProtocolVersion + "."); + $(connection).triggerHandler(events.onError, ["You are using a version of the client that isn't compatible with the server. Client version 1.2, server version " + res.ProtocolVersion + "."]); deferred.reject("You are using a version of the client that isn't compatible with the server. Client version 1.2, server version " + res.ProtocolVersion + "."); return; } @@ -709,6 +741,11 @@ } } + function isConnectedOrReconnecting(connection) { + return connection.state === signalR.connectionState.connected || + connection.state === signalR.connectionState.reconnecting; + } + signalR.transports._logic = { pingServer: function (connection, transport) { /// Pings the server @@ -725,6 +762,7 @@ global: false, cache: false, type: "GET", + contentType: connection.contentType, data: {}, dataType: connection.ajaxDataType, success: function (data) { @@ -746,7 +784,7 @@ addQs: function (url, connection) { var appender = url.indexOf("?") !== -1 ? "&" : "?", firstChar; - + if (!connection.qs) { return url; } @@ -768,7 +806,7 @@ throw new Error("Connections query string property must be either a string or object."); }, - getUrl: function (connection, transport, reconnecting, appendReconnectUrl) { + getUrl: function (connection, transport, reconnecting, poll) { /// Gets the url for making a GET based connect request var baseUrl = transport === "webSockets" ? "" : connection.baseUrl, url = baseUrl + connection.appRelativeUrl, @@ -783,11 +821,15 @@ } if (!reconnecting) { - url = url + "/connect"; + url += "/connect"; } else { - if (appendReconnectUrl) { - url = url + "/reconnect"; + if (poll) { + // longPolling transport specific + url += "/poll"; + } else { + url += "/reconnect"; } + if (connection.messageId) { qs += "&messageId=" + window.encodeURIComponent(connection.messageId); } @@ -822,6 +864,7 @@ url: url, global: false, type: connection.ajaxDataType === "jsonp" ? "GET" : "POST", + contentType: signalR._.defaultContentType, dataType: connection.ajaxDataType, data: { data: data @@ -859,6 +902,7 @@ timeout: 1000, global: false, type: "POST", + contentType: connection.contentType, dataType: connection.ajaxDataType, data: {} }); @@ -895,8 +939,8 @@ this.updateGroups(connection, data.GroupsToken); if (data.Messages) { - $.each(data.Messages, function () { - $connection.triggerHandler(events.onReceived, [this]); + $.each(data.Messages, function (index, message) { + $connection.triggerHandler(events.onReceived, [message]); }); } @@ -964,6 +1008,32 @@ return connection.state === signalR.connectionState.reconnecting; }, + clearReconnectTimeout: function (connection) { + if (connection && connection._.reconnectTimeout) { + window.clearTimeout(connection._.reconnectTimeout); + delete connection._.reconnectTimeout; + } + }, + + reconnect: function (connection, transportName) { + var transport = signalR.transports[transportName], + that = this; + + // We should only set a reconnectTimeout if we are currently connected + // and a reconnectTimeout isn't already set. + if (isConnectedOrReconnecting(connection) && !connection._.reconnectTimeout) { + + connection._.reconnectTimeout = window.setTimeout(function () { + transport.stop(connection); + + if (that.ensureReconnectingState(connection)) { + connection.log(transportName + " reconnecting"); + transport.start(connection); + } + }, connection.reconnectDelay); + } + }, + foreverFrame: { count: 0, connections: {} @@ -990,10 +1060,6 @@ supportsKeepAlive: true, - attemptingReconnect: false, - - currentSocketID: 0, - send: function (connection, data) { connection.socket.send(data); }, @@ -1022,14 +1088,11 @@ connection.log("Connecting to websocket endpoint '" + url + "'"); connection.socket = new window.WebSocket(url); - connection.socket.ID = ++that.currentSocketID; connection.socket.onopen = function () { opened = true; connection.log("Websocket opened"); - if (that.attemptingReconnect) { - that.attemptingReconnect = false; - } + transportLogic.clearReconnectTimeout(connection); if (onSuccess) { onSuccess(); @@ -1044,7 +1107,7 @@ // Only handle a socket close if the close is from the current socket. // Sometimes on disconnect the server will push down an onclose event // to an expired socket. - if (this.ID === that.currentSocketID) { + if (this === connection.socket) { if (!opened) { if (onFailed) { onFailed(); @@ -1087,24 +1150,7 @@ }, reconnect: function (connection) { - var that = this; - - if (connection.state !== signalR.connectionState.disconnected) { - if (!that.attemptingReconnect) { - that.attemptingReconnect = true; - } - - window.setTimeout(function () { - if (that.attemptingReconnect) { - that.stop(connection); - } - - if (transportLogic.ensureReconnectingState(connection)) { - connection.log("Websocket reconnecting"); - that.start(connection); - } - }, connection.reconnectDelay); - } + transportLogic.reconnect(connection, this.name); }, lostConnection: function (connection) { @@ -1113,6 +1159,9 @@ }, stop: function (connection) { + // Don't trigger a reconnect after stopping + transportLogic.clearReconnectTimeout(connection); + if (connection.socket !== null) { connection.log("Closing the Websocket"); connection.socket.close(); @@ -1144,10 +1193,6 @@ supportsKeepAlive: true, - reconnectTimeout: false, - - currentEventSourceID: 0, - timeOut: 3000, start: function (connection, onSuccess, onFailed) { @@ -1176,7 +1221,6 @@ try { connection.log("Attempting to connect to SSE endpoint '" + url + "'"); connection.eventSource = new window.EventSource(url); - connection.eventSource.ID = ++that.currentEventSourceID; } catch (e) { connection.log("EventSource failed trying to connect with error " + e.Message); @@ -1227,9 +1271,7 @@ window.clearTimeout(connectTimeOut); } - if (that.reconnectTimeout) { - window.clearTimeout(that.reconnectTimeout); - } + transportLogic.clearReconnectTimeout(connection); if (opened === false) { opened = true; @@ -1258,7 +1300,7 @@ // Only handle an error if the error is from the current Event Source. // Sometimes on disconnect the server will push down an error event // to an expired Event Source. - if (this.ID === that.currentEventSourceID) { + if (this === connection.eventSource) { if (!opened) { if (onFailed) { onFailed(); @@ -1286,16 +1328,7 @@ }, reconnect: function (connection) { - var that = this; - - that.reconnectTimeout = window.setTimeout(function () { - that.stop(connection); - - if (transportLogic.ensureReconnectingState(connection)) { - connection.log("EventSource reconnecting"); - that.start(connection); - } - }, connection.reconnectDelay); + transportLogic.reconnect(connection, this.name); }, lostConnection: function (connection) { @@ -1307,14 +1340,17 @@ }, stop: function (connection) { + // Don't trigger a reconnect after stopping + transportLogic.clearReconnectTimeout(connection); + if (connection && connection.eventSource) { connection.log("EventSource calling close()"); - connection.eventSource.ID = null; connection.eventSource.close(); connection.eventSource = null; delete connection.eventSource; } }, + abort: function (connection, async) { transportLogic.ajaxAbort(connection, async); } @@ -1333,7 +1369,46 @@ var signalR = $.signalR, events = $.signalR.events, changeState = $.signalR.changeState, - transportLogic = signalR.transports._logic; + transportLogic = signalR.transports._logic, + // Used to prevent infinite loading icon spins in older versions of ie + // We build this object inside a closure so we don't pollute the rest of + // the foreverFrame transport with unnecessary functions/utilities. + loadPreventer = (function () { + var loadingFixIntervalId = null, + loadingFixInterval = 1000, + attachedTo = 0; + + return { + prevent: function () { + // Prevent additional iframe removal procedures from newer browsers + if (signalR._.ieVersion <= 8) { + // We only ever want to set the interval one time, so on the first attachedTo + if (attachedTo === 0) { + // Create and destroy iframe every 3 seconds to prevent loading icon, super hacky + loadingFixIntervalId = window.setInterval(function () { + var tempFrame = $(""); + + $("body").append(tempFrame); + tempFrame.remove(); + tempFrame = null; + }, loadingFixInterval); + } + + attachedTo++; + } + }, + cancel: function () { + // Only clear the interval if there's only one more object that the loadPreventer is attachedTo + if (attachedTo === 1) { + window.clearInterval(loadingFixIntervalId); + } + + if (attachedTo > 0) { + attachedTo--; + } + } + }; + })(); signalR.transports.foreverFrame = { name: "foreverFrame", @@ -1357,6 +1432,9 @@ return; } + // Start preventing loading icon + // This will only perform work if the loadPreventer is not attached to another connection. + loadPreventer.prevent(); // Build the url url = transportLogic.getUrl(connection, this.name); @@ -1435,6 +1513,10 @@ stop: function (connection) { var cw = null; + + // Stop attempting to prevent loading icon + loadPreventer.cancel(); + if (connection.frame) { if (connection.frame.stop) { connection.frame.stop(); @@ -1540,7 +1622,23 @@ initialConnectedFired = true; onSuccess(); connection.log("Longpolling connected"); - }; + }, + reconnectErrors = 0, + reconnectTimeoutId = null, + fireReconnected = function (instance) { + window.clearTimeout(reconnectTimeoutId); + reconnectTimeoutId = null; + + if (changeState(connection, + signalR.connectionState.reconnecting, + signalR.connectionState.connected) === true) { + // Successfully reconnected! + connection.log("Raising the reconnect event"); + $(instance).triggerHandler(events.onReconnect); + } + }, + // 1 hour + maxFireReconnectedTimeout = 3600000; if (connection.pollXhr) { connection.log("Polling xhr requests already exists, aborting."); @@ -1557,7 +1655,8 @@ var messageId = instance.messageId, connect = (messageId === null), reconnecting = !connect, - url = transportLogic.getUrl(instance, that.name, reconnecting, raiseReconnect); + polling = !raiseReconnect, + url = transportLogic.getUrl(instance, that.name, reconnecting, polling); // If we've disconnected during the time we've tried to re-instantiate the poll then stop. if (isDisconnecting(instance) === true) { @@ -1571,10 +1670,20 @@ cache: false, type: "GET", dataType: connection.ajaxDataType, + contentType: connection.contentType, success: function (minData) { var delay = 0, data; + // Reset our reconnect errors so if we transition into a reconnecting state again we trigger + // reconnected quickly + reconnectErrors = 0; + + // If there's currently a timeout to trigger reconnect, fire it now before processing messages + if (reconnectTimeoutId !== null) { + fireReconnected(); + } + fireConnect(); if (minData) { @@ -1607,11 +1716,21 @@ }, error: function (data, textStatus) { + // Stop trying to trigger reconnect, connection is in an error state + // If we're not in the reconnect state this will noop + window.clearTimeout(reconnectTimeoutId); + reconnectTimeoutId = null; + if (textStatus === "abort") { connection.log("Aborted xhr requst."); return; } + // Increment our reconnect errors, we assume all errors to be reconnect errors + // In the case that it's our first error this will cause Reconnect to be fired + // after 1 second due to reconnectErrors being = 1. + reconnectErrors++; + if (connection.state !== signalR.connectionState.reconnecting) { connection.log("An error occurred using longPolling. Status = " + textStatus + ". " + data.responseText); $(instance).triggerHandler(events.onError, [data.responseText]); @@ -1629,15 +1748,15 @@ } }); + // This will only ever pass after an error has occured via the poll ajax procedure. if (reconnecting && raiseReconnect === true) { - if (changeState(connection, - signalR.connectionState.reconnecting, - signalR.connectionState.connected) === true) { - // Successfully reconnected! - connection.log("Raising the reconnect event"); - $(instance).triggerHandler(events.onReconnect); - } + // We wait to reconnect depending on how many times we've failed to reconnect. + // This is essentially a heuristic that will exponentially increase in wait time before + // triggering reconnected. This depends on the "error" handler of Poll to cancel this + // timeout if it triggers before the Reconnected event fires. + // The Math.min at the end is to ensure that the reconnect timeout does not overflow. + reconnectTimeoutId = window.setTimeout(function () { fireReconnected(instance); }, Math.min(1000 * (Math.pow(2, reconnectErrors) - 1), maxFireReconnectedTimeout)); } }(connection)); @@ -1693,20 +1812,17 @@ return event + eventNamespace; } - // Array.prototype.map - if (!Array.prototype.hasOwnProperty("map")) { - Array.prototype.map = function (fun, thisp) { - var arr = this, - i, - length = arr.length, - result = []; - for (i = 0; i < length; i += 1) { - if (arr.hasOwnProperty(i)) { - result[i] = fun.call(thisp, arr[i], i, arr); - } + // Equivalent to Array.prototype.map + function map(arr, fun, thisp) { + var i, + length = arr.length, + result = []; + for (i = 0; i < length; i += 1) { + if (arr.hasOwnProperty(i)) { + result[i] = fun.call(thisp, arr[i], i, arr); } - return result; - }; + } + return result; } function getArgValue(a) { @@ -1815,7 +1931,7 @@ var self = this, args = $.makeArray(arguments).slice(1), - argValues = args.map(getArgValue), + argValues = map(args, getArgValue), data = { H: self.hubName, M: methodName, A: argValues, I: callbackId }, d = $.Deferred(), callback = function (minResult) { @@ -2004,6 +2120,6 @@ /*global window:false */ /// (function ($) { - $.signalR.version = "1.0.1"; + $.signalR.version = "1.1.0"; }(window.jQuery)); diff --git a/Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR.js.bundle b/Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR.js.bundle index d56aa373..15db738b 100644 --- a/Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR.js.bundle +++ b/Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR.js.bundle @@ -1,4 +1,4 @@ - /ClientSource/Scripts/Modules/jQuery-SignalR/jquery.signalR-1.0.1.js + /ClientSource/Scripts/Modules/jQuery-SignalR/jquery.signalR-1.1.0.js \ No newline at end of file diff --git a/Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR.min.js b/Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR.min.js index c1a776b2..92a0495d 100644 --- a/Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR.min.js +++ b/Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR.min.js @@ -1,2 +1,2 @@ -(function(n,t){"use strict";function a(t,r){var u,f;if(n.isArray(t)){for(u=t.length-1;u>=0;u--)f=t[u],n.type(t)==="object"||n.type(f)==="string"&&i.transports[f]||(r.log("Invalid transport: "+f+", removing it from the transports list."),t.splice(u,1));t.length===0&&(r.log("No transports remain within the specified transport array."),t=null)}else n.type(t)==="object"||i.transports[t]||t==="auto"||(r.log("Invalid transport: "+t.toString()),t=null);return t}function v(n){return n==="http:"?80:n==="https:"?443:void 0}function s(n,t){return t.match(/:\d+$/)?t:t+":"+v(n)}if(typeof n!="function")throw new Error("SignalR: jQuery not found. Please ensure jQuery is referenced before the SignalR.js file.");if(!t.JSON)throw new Error("SignalR: No JSON parser found. Please ensure json2.js is referenced before the SignalR.js file if you need to support clients without native JSON parsing support, e.g. IE<8.");var i,e,o=t.document.readyState==="complete",f=n(t),r={onStart:"onStart",onStarting:"onStarting",onReceived:"onReceived",onError:"onError",onConnectionSlow:"onConnectionSlow",onReconnecting:"onReconnecting",onReconnect:"onReconnect",onStateChanged:"onStateChanged",onDisconnect:"onDisconnect"},h=function(n,i){if(i!==!1){var r;typeof t.console!="undefined"&&(r="["+(new Date).toTimeString()+"] SignalR: "+n,t.console.debug?t.console.debug(r):t.console.log&&t.console.log(r))}},u=function(t,i,u){return i===t.state?(t.state=u,n(t).triggerHandler(r.onStateChanged,[{oldState:i,newState:u}]),!0):!1},c=function(n){return n.state===i.connectionState.disconnected},l=function(n){var r,u;n._.configuredStopReconnectingTimeout||(u=function(n){n.log("Couldn't reconnect within the configured timeout ("+n.disconnectTimeout+"ms), disconnecting."),n.stop(!1,!1)},n.reconnecting(function(){var n=this;n.state===i.connectionState.reconnecting&&(r=t.setTimeout(function(){u(n)},n.disconnectTimeout))}),n.stateChanged(function(n){n.oldState===i.connectionState.reconnecting&&t.clearTimeout(r)}),n._.configuredStopReconnectingTimeout=!0)};i=function(n,t,r){return new i.fn.init(n,t,r)},i.events=r,i.changeState=u,i.isDisconnecting=c,i.connectionState={connecting:0,connected:1,reconnecting:2,disconnected:4},i.hub={start:function(){throw new Error("SignalR: Error loading hubs. Ensure your hubs reference is correct, e.g.