diff --git a/Gruntfile.js b/Gruntfile.js index 50c273d60..c781b8ee1 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -16,13 +16,16 @@ module.exports = function (grunt) { 'UI/JsLibraries/jquery.cookie.js' : 'http://raw.github.com/carhartl/jquery-cookie/master/jquery.cookie.js', 'UI/JsLibraries/jquery.js' : 'http://code.jquery.com/jquery.js', 'UI/JsLibraries/jquery.backstretch.js' : 'http://raw.github.com/srobbin/jquery-backstretch/master/jquery.backstretch.js', + + 'UI/JsLibraries/jquery.signalR.js' : 'https://raw.github.com/SignalR/SignalR/master/samples/Microsoft.AspNet.SignalR.Hosting.AspNet.Samples/Scripts/jquery.signalR.js', + 'UI/JsLibraries/require.js' : 'http://raw.github.com/jrburke/requirejs/master/require.js', 'UI/JsLibraries/sugar.js' : 'http://raw.github.com/andrewplummer/Sugar/master/release/sugar-full.development.js', 'UI/JsLibraries/underscore.js' : 'http://underscorejs.org/underscore.js', 'UI/JsLibraries/backbone.pageable.js' : 'http://raw.github.com/wyuenho/backbone-pageable/master/lib/backbone-pageable.js', 'UI/JsLibraries/backbone.backgrid.js' : 'http://raw.github.com/wyuenho/backgrid/master/lib/backgrid.js', 'UI/JsLibraries/backbone.backgrid.paginator.js' : 'http://raw.github.com/wyuenho/backgrid/master/lib/extensions/paginator/backgrid-paginator.js', - 'UI/JsLibraries/backbone.backgrid.filter.js' : 'http://raw.github.com/wyuenho/backgrid/master/lib/extensions/filter/backgrid-filter.js', + 'UI/JsLibraries/backbone.backgrid.filter.js' : 'http://raw.github.com/wyuenho/backgrid/master/lib/extensions/filter/backgrid-filter.js', 'UI/JsLibraries/messenger.js' : 'http://raw.github.com/HubSpot/messenger/master/build/js/messenger.js', 'UI/JsLibraries/lunr.js' : 'http://raw.github.com/olivernn/lunr.js/master/lunr.js', 'UI/Content/messenger.css' : 'http://raw.github.com/HubSpot/messenger/master/build/css/messenger.css', diff --git a/NzbDrone.Api/Frontend/IndexModule.cs b/NzbDrone.Api/Frontend/IndexModule.cs index 8a6e15f80..042cae893 100644 --- a/NzbDrone.Api/Frontend/IndexModule.cs +++ b/NzbDrone.Api/Frontend/IndexModule.cs @@ -16,7 +16,8 @@ namespace NzbDrone.Api.Frontend if( Request.Path.Contains(".") || Request.Path.StartsWith("/static", StringComparison.CurrentCultureIgnoreCase) - || Request.Path.StartsWith("/api", StringComparison.CurrentCultureIgnoreCase)) + || Request.Path.StartsWith("/api", StringComparison.CurrentCultureIgnoreCase) + || Request.Path.StartsWith("/signalr", StringComparison.CurrentCultureIgnoreCase)) { return new NotFoundResponse(); } diff --git a/NzbDrone.Api/NzbDrone.Api.csproj b/NzbDrone.Api/NzbDrone.Api.csproj index f2bb6115f..0e3ca2be7 100644 --- a/NzbDrone.Api/NzbDrone.Api.csproj +++ b/NzbDrone.Api/NzbDrone.Api.csproj @@ -63,6 +63,9 @@ False ..\packages\FluentValidation.4.0.0.0\lib\Net40\FluentValidation.dll + + ..\packages\Microsoft.AspNet.SignalR.Core.1.0.1\lib\net40\Microsoft.AspNet.SignalR.Core.dll + False ..\packages\Nancy.0.16.1\lib\net40\Nancy.dll @@ -119,6 +122,7 @@ + @@ -139,6 +143,9 @@ + + + diff --git a/NzbDrone.Api/Series/SeriesConnection.cs b/NzbDrone.Api/Series/SeriesConnection.cs new file mode 100644 index 000000000..7a31077ba --- /dev/null +++ b/NzbDrone.Api/Series/SeriesConnection.cs @@ -0,0 +1,13 @@ +using NLog; +using NzbDrone.Api.SignalR; + +namespace NzbDrone.Api.Series +{ + public class SeriesConnection : BasicResourceConnection + { + public override string Resource + { + get { return "/Series"; } + } + } +} diff --git a/NzbDrone.Api/SignalR/BasicResourceConnection.cs b/NzbDrone.Api/SignalR/BasicResourceConnection.cs new file mode 100644 index 000000000..1a3a2af78 --- /dev/null +++ b/NzbDrone.Api/SignalR/BasicResourceConnection.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR; +using NLog; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Events; + +namespace NzbDrone.Api.SignalR +{ + public abstract class BasicResourceConnection : + NzbDronePersistentConnection, + IHandleAsync> + where T : ModelBase + { + private readonly Logger _logger; + + public BasicResourceConnection() + { + _logger = LogManager.GetCurrentClassLogger(); + } + + protected override Task OnConnected(IRequest request, string connectionId) + { + _logger.Debug("SignalR client connected. ID:{0}", connectionId); + return base.OnConnected(request, connectionId); + } + + public override Task ProcessRequest(Microsoft.AspNet.SignalR.Hosting.HostContext context) + { + _logger.Debug("Request: {0}", context); + return base.ProcessRequest(context); + } + + public void HandleAsync(ModelEvent message) + { + Connection.Broadcast(message); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/SignalR/NzbDronePersistentConnection.cs b/NzbDrone.Api/SignalR/NzbDronePersistentConnection.cs new file mode 100644 index 000000000..2e4c8444d --- /dev/null +++ b/NzbDrone.Api/SignalR/NzbDronePersistentConnection.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNet.SignalR; + +namespace NzbDrone.Api.SignalR +{ + public abstract class NzbDronePersistentConnection : PersistentConnection + { + public abstract string Resource { get; } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/SignalR/SignalrDependencyResolver.cs b/NzbDrone.Api/SignalR/SignalrDependencyResolver.cs new file mode 100644 index 000000000..e17d3623d --- /dev/null +++ b/NzbDrone.Api/SignalR/SignalrDependencyResolver.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.SignalR; +using TinyIoC; + +namespace NzbDrone.Api.SignalR +{ + public class SignalrDependencyResolver : DefaultDependencyResolver + { + private readonly TinyIoCContainer _container; + + public static void Register(TinyIoCContainer container) + { + GlobalHost.DependencyResolver = new SignalrDependencyResolver(container); + } + + private SignalrDependencyResolver(TinyIoCContainer container) + { + _container = container; + } + + public override object GetService(Type serviceType) + { + return _container.CanResolve(serviceType) ? _container.Resolve(serviceType) : base.GetService(serviceType); + } + + public override IEnumerable GetServices(Type serviceType) + { + var objects = _container.CanResolve(serviceType) ? _container.ResolveAll(serviceType) : new object[] { }; + return objects.Concat(base.GetServices(serviceType)); + } + } +} diff --git a/NzbDrone.Api/packages.config b/NzbDrone.Api/packages.config index b000c1262..d3fe03882 100644 --- a/NzbDrone.Api/packages.config +++ b/NzbDrone.Api/packages.config @@ -2,6 +2,7 @@ + diff --git a/NzbDrone.Common/ContainerBuilderBase.cs b/NzbDrone.Common/ContainerBuilderBase.cs index e038665ee..735f33c6e 100644 --- a/NzbDrone.Common/ContainerBuilderBase.cs +++ b/NzbDrone.Common/ContainerBuilderBase.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Common { public abstract class ContainerBuilderBase { - protected TinyIoCContainer Container; + protected readonly TinyIoCContainer Container; private readonly List _loadedTypes; protected ContainerBuilderBase(params string[] assemblies) diff --git a/NzbDrone.Common/NzbDrone.Common.csproj b/NzbDrone.Common/NzbDrone.Common.csproj index 8f570c0f1..7269dd716 100644 --- a/NzbDrone.Common/NzbDrone.Common.csproj +++ b/NzbDrone.Common/NzbDrone.Common.csproj @@ -140,7 +140,9 @@ - + + Designer + diff --git a/NzbDrone.Core.Test/Framework/DbTest.cs b/NzbDrone.Core.Test/Framework/DbTest.cs index e92011dd5..b1f0bc6fd 100644 --- a/NzbDrone.Core.Test/Framework/DbTest.cs +++ b/NzbDrone.Core.Test/Framework/DbTest.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Marr.Data; +using Moq; using NUnit.Framework; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Migration.Framework; @@ -140,25 +142,27 @@ namespace NzbDrone.Core.Test.Framework public class TestTestDatabase : ITestDatabase { private readonly IDatabase _dbConnection; + private IMessageAggregator _messageAggregator; public TestTestDatabase(IDatabase dbConnection) { + _messageAggregator = new Mock().Object; _dbConnection = dbConnection; } public void InsertMany(IEnumerable items) where T : ModelBase, new() { - new BasicRepository(_dbConnection).InsertMany(items.ToList()); + new BasicRepository(_dbConnection, _messageAggregator).InsertMany(items.ToList()); } public T Insert(T item) where T : ModelBase, new() { - return new BasicRepository(_dbConnection).Insert(item); + return new BasicRepository(_dbConnection, _messageAggregator).Insert(item); } public List All() where T : ModelBase, new() { - return new BasicRepository(_dbConnection).All().ToList(); + return new BasicRepository(_dbConnection, _messageAggregator).All().ToList(); } public T Single() where T : ModelBase, new() @@ -168,12 +172,12 @@ namespace NzbDrone.Core.Test.Framework public void Update(T childModel) where T : ModelBase, new() { - new BasicRepository(_dbConnection).Update(childModel); + new BasicRepository(_dbConnection, _messageAggregator).Update(childModel); } public void Delete(T childModel) where T : ModelBase, new() { - new BasicRepository(_dbConnection).Delete(childModel); + new BasicRepository(_dbConnection, _messageAggregator).Delete(childModel); } } } \ No newline at end of file diff --git a/NzbDrone.Core/Configuration/ConfigRepository.cs b/NzbDrone.Core/Configuration/ConfigRepository.cs index abcafd04e..4f256030d 100644 --- a/NzbDrone.Core/Configuration/ConfigRepository.cs +++ b/NzbDrone.Core/Configuration/ConfigRepository.cs @@ -1,5 +1,6 @@ using System.Data; using System.Linq; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Configuration @@ -12,8 +13,8 @@ namespace NzbDrone.Core.Configuration public class ConfigRepository : BasicRepository, IConfigRepository { - public ConfigRepository(IDatabase database) - : base(database) + public ConfigRepository(IDatabase database, IMessageAggregator messageAggregator) + : base(database, messageAggregator) { } diff --git a/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs b/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs index 1bc2fd21d..1e3aaec37 100644 --- a/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs +++ b/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs @@ -1,4 +1,5 @@ using System.Linq; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.DataAugmentation.Scene @@ -12,8 +13,8 @@ namespace NzbDrone.Core.DataAugmentation.Scene public class SceneMappingRepository : BasicRepository, ISceneMappingRepository { - public SceneMappingRepository(IDatabase database) - : base(database) + public SceneMappingRepository(IDatabase database, IMessageAggregator messageAggregator) + : base(database, messageAggregator) { } diff --git a/NzbDrone.Core/Datastore/BasicRepository.cs b/NzbDrone.Core/Datastore/BasicRepository.cs index 9b0f303f6..d0b5c2ff6 100644 --- a/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/NzbDrone.Core/Datastore/BasicRepository.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Linq.Expressions; using Marr.Data; using Marr.Data.QGen; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Tv; @@ -34,14 +36,16 @@ namespace NzbDrone.Core.Datastore public class BasicRepository : IBasicRepository where TModel : ModelBase, new() { + private readonly IMessageAggregator _messageAggregator; //TODO: add assertion to make sure model properly mapped private readonly IDataMapper _dataMapper; - public BasicRepository(IDatabase database) + public BasicRepository(IDatabase database, IMessageAggregator messageAggregator) { + _messageAggregator = messageAggregator; _dataMapper = database.DataMapper; } @@ -99,7 +103,9 @@ namespace NzbDrone.Core.Datastore throw new InvalidOperationException("Can't insert model with existing ID"); } - var id = _dataMapper.Insert(model); + _dataMapper.Insert(model); + _messageAggregator.PublishEvent(new ModelEvent(model, ModelEvent.RepositoryAction.Created)); + return model; } diff --git a/NzbDrone.Core/Datastore/Events/ModelEvent.cs b/NzbDrone.Core/Datastore/Events/ModelEvent.cs new file mode 100644 index 000000000..64016bbe8 --- /dev/null +++ b/NzbDrone.Core/Datastore/Events/ModelEvent.cs @@ -0,0 +1,27 @@ +using System; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Datastore.Events +{ + public class ModelEvent : IEvent where T : ModelBase + { + public T Model { get; set; } + public RepositoryAction Action { get; set; } + + public ModelEvent(T model, RepositoryAction action) + { + Model = model; + Action = action; + } + + + public enum RepositoryAction + { + Created, + Updated, + Deleted + } + } + + +} \ No newline at end of file diff --git a/NzbDrone.Core/ExternalNotification/ExternalNotificationRepository.cs b/NzbDrone.Core/ExternalNotification/ExternalNotificationRepository.cs index de173cfb0..03df97fee 100644 --- a/NzbDrone.Core/ExternalNotification/ExternalNotificationRepository.cs +++ b/NzbDrone.Core/ExternalNotification/ExternalNotificationRepository.cs @@ -1,5 +1,6 @@ using System.Data; using System.Linq; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.ExternalNotification @@ -11,11 +12,11 @@ namespace NzbDrone.Core.ExternalNotification public class ExternalNotificationRepository : BasicRepository, IExternalNotificationRepository { - public ExternalNotificationRepository(IDatabase database) - : base(database) + public ExternalNotificationRepository(IDatabase database, IMessageAggregator messageAggregator) + : base(database, messageAggregator) { } - + public ExternalNotificationDefinition Get(string name) { return Query.SingleOrDefault(c => c.Name.ToLower() == name.ToLower()); diff --git a/NzbDrone.Core/History/HistoryRepository.cs b/NzbDrone.Core/History/HistoryRepository.cs index 9a85633e8..acfe76c80 100644 --- a/NzbDrone.Core/History/HistoryRepository.cs +++ b/NzbDrone.Core/History/HistoryRepository.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Linq; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Datastore; using NzbDrone.Core.Tv; @@ -14,8 +15,8 @@ namespace NzbDrone.Core.History public class HistoryRepository : BasicRepository, IHistoryRepository { - public HistoryRepository(IDatabase database) - : base(database) + public HistoryRepository(IDatabase database, IMessageAggregator messageAggregator) + : base(database, messageAggregator) { } diff --git a/NzbDrone.Core/Indexers/IndexerRepository.cs b/NzbDrone.Core/Indexers/IndexerRepository.cs index 9fac1b49a..678c26fd6 100644 --- a/NzbDrone.Core/Indexers/IndexerRepository.cs +++ b/NzbDrone.Core/Indexers/IndexerRepository.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Indexers @@ -12,8 +13,8 @@ namespace NzbDrone.Core.Indexers public class IndexerRepository : BasicRepository, IIndexerRepository { - public IndexerRepository(IDatabase database) - : base(database) + public IndexerRepository(IDatabase database, IMessageAggregator messageAggregator) + : base(database, messageAggregator) { } diff --git a/NzbDrone.Core/Instrumentation/LogRepository.cs b/NzbDrone.Core/Instrumentation/LogRepository.cs index 1e7f00fe3..abba443e8 100644 --- a/NzbDrone.Core/Instrumentation/LogRepository.cs +++ b/NzbDrone.Core/Instrumentation/LogRepository.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Linq; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Instrumentation @@ -12,8 +13,8 @@ namespace NzbDrone.Core.Instrumentation public class LogRepository : BasicRepository, ILogRepository { - public LogRepository(IDatabase database) - : base(database) + public LogRepository(IDatabase database, IMessageAggregator messageAggregator) + : base(database, messageAggregator) { } diff --git a/NzbDrone.Core/Jobs/JobRepository.cs b/NzbDrone.Core/Jobs/JobRepository.cs index 746ab3275..7ed7ea466 100644 --- a/NzbDrone.Core/Jobs/JobRepository.cs +++ b/NzbDrone.Core/Jobs/JobRepository.cs @@ -19,8 +19,8 @@ namespace NzbDrone.Core.Jobs private readonly IEnumerable _jobs; private readonly Logger _logger; - public JobRepository(IDatabase database, IEnumerable jobs, Logger logger) - : base(database) + public JobRepository(IDatabase database, IEnumerable jobs, Logger logger, IMessageAggregator messageAggregator) + : base(database, messageAggregator) { _jobs = jobs; _logger = logger; diff --git a/NzbDrone.Core/MediaFiles/MediaFileRepository.cs b/NzbDrone.Core/MediaFiles/MediaFileRepository.cs index ef6859f49..f3d9fe7a7 100644 --- a/NzbDrone.Core/MediaFiles/MediaFileRepository.cs +++ b/NzbDrone.Core/MediaFiles/MediaFileRepository.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.MediaFiles @@ -15,8 +16,8 @@ namespace NzbDrone.Core.MediaFiles public class MediaFileRepository : BasicRepository, IMediaFileRepository { - public MediaFileRepository(IDatabase database) - : base(database) + public MediaFileRepository(IDatabase database, IMessageAggregator messageAggregator) + : base(database, messageAggregator) { } diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index b5b6ea7f8..4a44cb647 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -195,6 +195,7 @@ + diff --git a/NzbDrone.Core/Qualities/QualityProfileRepository.cs b/NzbDrone.Core/Qualities/QualityProfileRepository.cs index e63807019..fcb70f3ff 100644 --- a/NzbDrone.Core/Qualities/QualityProfileRepository.cs +++ b/NzbDrone.Core/Qualities/QualityProfileRepository.cs @@ -1,4 +1,5 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Qualities { @@ -9,8 +10,8 @@ namespace NzbDrone.Core.Qualities public class QualityProfileRepository : BasicRepository, IQualityProfileRepository { - public QualityProfileRepository(IDatabase database) - : base(database) + public QualityProfileRepository(IDatabase database, IMessageAggregator messageAggregator) + : base(database, messageAggregator) { } } diff --git a/NzbDrone.Core/Qualities/QualitySizeRepository.cs b/NzbDrone.Core/Qualities/QualitySizeRepository.cs index f0e2331e1..2f62a9db2 100644 --- a/NzbDrone.Core/Qualities/QualitySizeRepository.cs +++ b/NzbDrone.Core/Qualities/QualitySizeRepository.cs @@ -1,5 +1,6 @@ using System.Data; using System.Linq; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Qualities @@ -11,8 +12,8 @@ namespace NzbDrone.Core.Qualities public class QualitySizeRepository : BasicRepository, IQualitySizeRepository { - public QualitySizeRepository(IDatabase database) - : base(database) + public QualitySizeRepository(IDatabase database, IMessageAggregator messageAggregator) + : base(database, messageAggregator) { } diff --git a/NzbDrone.Core/Tv/EpisodeRepository.cs b/NzbDrone.Core/Tv/EpisodeRepository.cs index e50f199d9..cf3951828 100644 --- a/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -5,6 +5,7 @@ using System.Data; using System.Linq; using Marr.Data; using Marr.Data.QGen; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Datastore; using NzbDrone.Core.Model; @@ -31,8 +32,8 @@ namespace NzbDrone.Core.Tv { private readonly IDataMapper _dataMapper; - public EpisodeRepository(IDatabase database) - : base(database) + public EpisodeRepository(IDatabase database, IMessageAggregator messageAggregator) + : base(database, messageAggregator) { _dataMapper = database.DataMapper; } diff --git a/NzbDrone.Core/Tv/SeasonRepository.cs b/NzbDrone.Core/Tv/SeasonRepository.cs index 6c88e60c6..092131b98 100644 --- a/NzbDrone.Core/Tv/SeasonRepository.cs +++ b/NzbDrone.Core/Tv/SeasonRepository.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -using System.Data; using System.Linq; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Datastore; @@ -16,10 +16,9 @@ namespace NzbDrone.Core.Tv public class SeasonRepository : BasicRepository, ISeasonRepository { - private readonly IDbConnection _database; - public SeasonRepository(IDatabase database) - : base(database) + public SeasonRepository(IDatabase database, IMessageAggregator messageAggregator) + : base(database, messageAggregator) { } diff --git a/NzbDrone.Core/Tv/SeriesRepository.cs b/NzbDrone.Core/Tv/SeriesRepository.cs index 7d9c1d3ce..a07a62ae4 100644 --- a/NzbDrone.Core/Tv/SeriesRepository.cs +++ b/NzbDrone.Core/Tv/SeriesRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Tv @@ -18,8 +19,8 @@ namespace NzbDrone.Core.Tv public class SeriesRepository : BasicRepository, ISeriesRepository { - public SeriesRepository(IDatabase database) - : base(database) + public SeriesRepository(IDatabase database, IMessageAggregator messageAggregator) + : base(database, messageAggregator) { } diff --git a/NzbDrone/MainAppContainerBuilder.cs b/NzbDrone/MainAppContainerBuilder.cs index 991536a6f..d401734a0 100644 --- a/NzbDrone/MainAppContainerBuilder.cs +++ b/NzbDrone/MainAppContainerBuilder.cs @@ -3,6 +3,7 @@ using FluentMigrator.Runner; using NLog; using Nancy.Bootstrapper; using NzbDrone.Api; +using NzbDrone.Api.SignalR; using NzbDrone.Common; using NzbDrone.Common.Messaging; using NzbDrone.Core.Datastore; @@ -37,6 +38,8 @@ namespace NzbDrone Container.Register(typeof(IBasicRepository), typeof(BasicRepository)).AsMultiInstance(); Container.Register(typeof(IBasicRepository), typeof(BasicRepository)).AsMultiInstance(); + AutoRegisterImplementations(); + InitDatabase(); ReportingService.RestProvider = Container.Resolve(); diff --git a/NzbDrone/NzbDrone.csproj b/NzbDrone/NzbDrone.csproj index ab9ddbb9e..a6e64ec6c 100644 --- a/NzbDrone/NzbDrone.csproj +++ b/NzbDrone/NzbDrone.csproj @@ -1,4 +1,4 @@ - + Debug @@ -91,6 +91,12 @@ False ..\packages\FluentMigrator.1.0.6.0\tools\FluentMigrator.Runner.dll + + ..\packages\Microsoft.AspNet.SignalR.Core.1.0.1\lib\net40\Microsoft.AspNet.SignalR.Core.dll + + + ..\packages\Microsoft.AspNet.SignalR.Owin.1.0.1\lib\net40\Microsoft.AspNet.SignalR.Owin.dll + ..\packages\Microsoft.Owin.Host.HttpListener.0.21.0-pre\lib\net40\Microsoft.Owin.Host.HttpListener.dll @@ -106,6 +112,10 @@ False ..\packages\Nancy.Owin.0.16.1\lib\net40\Nancy.Owin.dll + + False + ..\packages\Newtonsoft.Json.4.5.11\lib\net35\Newtonsoft.Json.dll + False ..\packages\NLog.2.0.1.2\lib\net40\NLog.dll diff --git a/NzbDrone/Owin/MiddleWare/IOwinMiddleWare.cs b/NzbDrone/Owin/MiddleWare/IOwinMiddleWare.cs index 80a85102f..bcba8d456 100644 --- a/NzbDrone/Owin/MiddleWare/IOwinMiddleWare.cs +++ b/NzbDrone/Owin/MiddleWare/IOwinMiddleWare.cs @@ -4,6 +4,7 @@ namespace NzbDrone.Owin.MiddleWare { public interface IOwinMiddleWare { + int Order { get; } void Attach(IAppBuilder appBuilder); } } \ No newline at end of file diff --git a/NzbDrone/Owin/MiddleWare/NancyMiddleWare.cs b/NzbDrone/Owin/MiddleWare/NancyMiddleWare.cs index a30a5ea8b..84be7e9b7 100644 --- a/NzbDrone/Owin/MiddleWare/NancyMiddleWare.cs +++ b/NzbDrone/Owin/MiddleWare/NancyMiddleWare.cs @@ -16,6 +16,8 @@ namespace NzbDrone.Owin.MiddleWare _nancyBootstrapper = nancyBootstrapper; } + public int Order { get { return 1; } } + public void Attach(IAppBuilder appBuilder) { var nancyOwinHost = new NancyOwinHost(null, _nancyBootstrapper); diff --git a/NzbDrone/Owin/MiddleWare/SignalRMiddleWare.cs b/NzbDrone/Owin/MiddleWare/SignalRMiddleWare.cs index fddb7c0b4..2b21f9a7b 100644 --- a/NzbDrone/Owin/MiddleWare/SignalRMiddleWare.cs +++ b/NzbDrone/Owin/MiddleWare/SignalRMiddleWare.cs @@ -1,26 +1,31 @@ -using System; using System.Collections.Generic; -using System.Threading.Tasks; -using Nancy.Bootstrapper; -using Nancy.Owin; +using Microsoft.AspNet.SignalR; +using NzbDrone.Api.SignalR; using Owin; +using TinyIoC; namespace NzbDrone.Owin.MiddleWare { public class SignalRMiddleWare : IOwinMiddleWare { - private readonly INancyBootstrapper _nancyBootstrapper; + private readonly IEnumerable _persistentConnections; - public SignalRMiddleWare(INancyBootstrapper nancyBootstrapper) + public int Order { get { return 0; } } + + public SignalRMiddleWare(IEnumerable persistentConnections, TinyIoCContainer container) { - _nancyBootstrapper = nancyBootstrapper; + _persistentConnections = persistentConnections; + + SignalrDependencyResolver.Register(container); } public void Attach(IAppBuilder appBuilder) { - return; - var nancyOwinHost = new NancyOwinHost(null, _nancyBootstrapper); - appBuilder.Use((Func, Task>, Func, Task>>)(next => (Func, Task>)nancyOwinHost.Invoke), new object[0]); + foreach (var nzbDronePersistentConnection in _persistentConnections) + { + appBuilder.MapConnection("signalr/series", nzbDronePersistentConnection.GetType(), new ConnectionConfiguration { EnableCrossDomain = true }); + } + } } } \ No newline at end of file diff --git a/NzbDrone/Owin/OwinHostController.cs b/NzbDrone/Owin/OwinHostController.cs index cafe31a80..a2f30b020 100644 --- a/NzbDrone/Owin/OwinHostController.cs +++ b/NzbDrone/Owin/OwinHostController.cs @@ -8,6 +8,7 @@ using Nancy.Owin; using NzbDrone.Common; using NzbDrone.Owin.MiddleWare; using Owin; +using System.Linq; namespace NzbDrone.Owin { @@ -27,13 +28,20 @@ namespace NzbDrone.Owin public void StartServer() { - _host = WebApplication.Start(AppUrl, BuildApp); + var options = new StartOptions + { + App = GetType().AssemblyQualifiedName, + Port = _configFileProvider.Port + }; + + _host = WebApplication.Start(options, BuildApp); } private void BuildApp(IAppBuilder appBuilder) { - foreach (var middleWare in _owinMiddleWares) + foreach (var middleWare in _owinMiddleWares.OrderBy(c => c.Order)) { + _logger.Debug("Attaching {0} to host", middleWare.GetType().Name); middleWare.Attach(appBuilder); } } diff --git a/NzbDrone/packages.config b/NzbDrone/packages.config index 934bc606d..9b4b4b0a7 100644 --- a/NzbDrone/packages.config +++ b/NzbDrone/packages.config @@ -1,10 +1,13 @@  + + + diff --git a/UI/Index.html b/UI/Index.html index a95c090dd..96b565b08 100644 --- a/UI/Index.html +++ b/UI/Index.html @@ -52,17 +52,6 @@ - -
@@ -85,12 +74,15 @@ + + + + - @@ -110,9 +102,6 @@ - - - @@ -120,6 +109,7 @@ + diff --git a/UI/Instrumentation/ErrorHandler.js b/UI/Instrumentation/ErrorHandler.js index 635790804..d091011bd 100644 --- a/UI/Instrumentation/ErrorHandler.js +++ b/UI/Instrumentation/ErrorHandler.js @@ -1,5 +1,5 @@ "use strict"; -define(function () { +(function () { /* var model = new NzbDrone.Shared.NotificationModel(); model.set('title','test notification'); @@ -12,9 +12,6 @@ define(function () { window.Messenger().post(message); }; - - var self = this; - window.onerror = function (msg, url, line) { try { @@ -69,5 +66,5 @@ define(function () { return false; }); -}); +})(); diff --git a/UI/JsLibraries/backbone.backgrid.filter.js b/UI/JsLibraries/backbone.backgrid.filter.js index bc869bc57..c98c6658b 100644 --- a/UI/JsLibraries/backbone.backgrid.filter.js +++ b/UI/JsLibraries/backbone.backgrid.filter.js @@ -354,4 +354,4 @@ }); -}(jQuery, _, Backbone, Backgrid)); +}(jQuery, _, Backbone, Backgrid, lunr)); diff --git a/UI/JsLibraries/jquery.signalR.js b/UI/JsLibraries/jquery.signalR.js new file mode 100644 index 000000000..646c97482 --- /dev/null +++ b/UI/JsLibraries/jquery.signalR.js @@ -0,0 +1,2087 @@ +/* jquery.signalR.core.js */ +/*global window:false */ +/*! + * ASP.NET SignalR JavaScript Library v1.1.0-beta1 + * http://signalr.net/ + * + * Copyright Microsoft Open Technologies, Inc. All rights reserved. + * Licensed under the Apache 2.0 + * https://github.com/SignalR/SignalR/blob/master/LICENSE.md + * + */ + +/// +(function ($, window) { + "use strict"; + + if (typeof ($) !== "function") { + // no jQuery! + throw new Error("SignalR: jQuery not found. Please ensure jQuery is referenced before the SignalR.js file."); + } + + if (!window.JSON) { + // no 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 signalR, + _connection, + _pageLoaded = (window.document.readyState === "complete"), + _pageWindow = $(window), + + events = { + onStart: "onStart", + onStarting: "onStarting", + onReceived: "onReceived", + onError: "onError", + onConnectionSlow: "onConnectionSlow", + onReconnecting: "onReconnecting", + onReconnect: "onReconnect", + onStateChanged: "onStateChanged", + onDisconnect: "onDisconnect" + }, + + log = function (msg, logging) { + if (logging === false) { + return; + } + var m; + if (typeof (window.console) === "undefined") { + return; + } + m = "[" + new Date().toTimeString() + "] SignalR: " + msg; + if (window.console.debug) { + window.console.debug(m); + } else if (window.console.log) { + window.console.log(m); + } + }, + + changeState = function (connection, expectedState, newState) { + if (expectedState === connection.state) { + connection.state = newState; + + $(connection).triggerHandler(events.onStateChanged, [{ oldState: expectedState, newState: newState }]); + return true; + } + + return false; + }, + + isDisconnecting = function (connection) { + return connection.state === signalR.connectionState.disconnected; + }, + + configureStopReconnectingTimeout = function (connection) { + var stopReconnectingTimeout, + onReconnectTimeout; + + // Check if this connection has already been configured to stop reconnecting after a specified timeout. + // Without this check if a connection is stopped then started events will be bound multiple times. + if (!connection._.configuredStopReconnectingTimeout) { + onReconnectTimeout = function (connection) { + connection.log("Couldn't reconnect within the configured timeout (" + connection.disconnectTimeout + "ms), disconnecting."); + connection.stop(/* async */ false, /* notifyServer */ false); + }; + + connection.reconnecting(function () { + var connection = this; + + // Guard against state changing in a previous user defined even handler + if (connection.state === signalR.connectionState.reconnecting) { + stopReconnectingTimeout = window.setTimeout(function () { onReconnectTimeout(connection); }, connection.disconnectTimeout); + } + }); + + connection.stateChanged(function (data) { + if (data.oldState === signalR.connectionState.reconnecting) { + // Clear the pending reconnect timeout check + window.clearTimeout(stopReconnectingTimeout); + } + }); + + connection._.configuredStopReconnectingTimeout = true; + } + }; + + signalR = function (url, qs, logging) { + /// Creates a new SignalR connection for the given url + /// The URL of the long polling endpoint + /// + /// [Optional] Custom querystring parameters to add to the connection URL. + /// If an object, every non-function member will be added to the querystring. + /// If a string, it's added to the QS as specified. + /// + /// + /// [Optional] A flag indicating whether connection logging is enabled to the browser + /// console/log. Defaults to false. + /// + + 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; + + signalR.isDisconnecting = isDisconnecting; + + signalR.connectionState = { + connecting: 0, + connected: 1, + reconnecting: 2, + disconnected: 4 + }; + + signalR.hub = { + start: function () { + // This will get replaced with the real hub connection start method when hubs is referenced correctly + throw new Error("SignalR: Error loading hubs. Ensure your hubs reference is correct, e.g. ."); + } + }; + + _pageWindow.load(function () { _pageLoaded = true; }); + + function validateTransport(requestedTransport, connection) { + /// Validates the requested transport by cross checking it with the pre-defined signalR.transports + /// 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--) { + var transport = requestedTransport[i]; + if ($.type(requestedTransport) !== "object" && ($.type(transport) !== "string" || !signalR.transports[transport])) { + connection.log("Invalid transport: " + transport + ", removing it from the transports list."); + requestedTransport.splice(i, 1); + } + } + + // Verify we still have transports left, if we dont then we have invalid transports + if (requestedTransport.length === 0) { + connection.log("No transports remain within the specified transport array."); + requestedTransport = null; + } + } else if ($.type(requestedTransport) !== "object" && !signalR.transports[requestedTransport] && requestedTransport !== "auto") { + 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; + } + + function getDefaultPort(protocol) { + if(protocol === "http:") { + return 80; + } + else if (protocol === "https:") { + return 443; + } + } + + function addDefaultPort(protocol, url) { + // Remove ports from url. We have to check if there's a / or end of line + // following the port in order to avoid removing ports such as 8080. + if(url.match(/:\d+$/)) { + return url; + } else { + return url + ":" + getDefaultPort(protocol); + } + } + + signalR.fn = signalR.prototype = { + init: function (url, qs, logging) { + this.url = url; + this.qs = qs; + this._ = {}; + if (typeof (logging) === "boolean") { + this.logging = logging; + } + }, + + isCrossDomain: function (url, against) { + /// Checks if url is cross domain + /// The base URL + /// + /// An optional argument to compare the URL against, if not specified it will be set to window.location. + /// If specified it must contain a protocol and a host property. + /// + var link; + + url = $.trim(url); + if (url.indexOf("http") !== 0) { + return false; + } + + against = against || window.location; + + // Create an anchor tag. + link = window.document.createElement("a"); + link.href = url; + + // When checking for cross domain we have to special case port 80 because the window.location will remove the + return link.protocol + addDefaultPort(link.protocol, link.host) !== against.protocol + addDefaultPort(against.protocol, against.host); + }, + + ajaxDataType: "json", + + contentType: "application/json; charset=UTF-8", + + logging: false, + + state: signalR.connectionState.disconnected, + + keepAliveData: {}, + + reconnectDelay: 2000, + + disconnectTimeout: 30000, // This should be set by the server in response to the negotiate request (30s default) + + keepAliveWarnAt: 2 / 3, // Warn user of slow connection if we breach the X% mark of the keep alive timeout + + start: function (options, callback) { + /// Starts the connection + /// Options map + /// A callback function to execute when the connection has started + var connection = this, + config = { + waitForPageLoad: true, + transport: "auto", + jsonp: false + }, + initialize, + deferred = connection._deferral || $.Deferred(), // Check to see if there is a pre-existing deferral that's being built on, if so we want to keep using it + parser = window.document.createElement("a"); + + if ($.type(options) === "function") { + // Support calling with single callback parameter + callback = options; + } else if ($.type(options) === "object") { + $.extend(config, options); + if ($.type(config.callback) === "function") { + callback = config.callback; + } + } + + config.transport = validateTransport(config.transport, connection); + + // If the transport is invalid throw an error and abort start + if (!config.transport) { + throw new Error("SignalR: Invalid transport(s) specified, aborting start."); + } + + // Check to see if start is being called prior to page load + // If waitForPageLoad is true we then want to re-direct function call to the window load event + if (!_pageLoaded && config.waitForPageLoad === true) { + _pageWindow.load(function () { + connection._deferral = deferred; + connection.start(options, callback); + }); + return deferred.promise(); + } + + configureStopReconnectingTimeout(connection); + + if (changeState(connection, + signalR.connectionState.disconnected, + signalR.connectionState.connecting) === false) { + // Already started, just return + deferred.resolve(connection); + return deferred.promise(); + } + + // Resolve the full url + parser.href = connection.url; + if (!parser.protocol || parser.protocol === ":") { + connection.protocol = window.document.location.protocol; + connection.host = window.document.location.host; + connection.baseUrl = connection.protocol + "//" + connection.host; + } + else { + connection.protocol = parser.protocol; + connection.host = parser.host; + connection.baseUrl = parser.protocol + "//" + parser.host; + } + + // Set the websocket protocol + connection.wsProtocol = connection.protocol === "https:" ? "wss://" : "ws://"; + + // If jsonp with no/auto transport is specified, then set the transport to long polling + // since that is the only transport for which jsonp really makes sense. + // Some developers might actually choose to specify jsonp for same origin requests + // as demonstrated by Issue #623. + if (config.transport === "auto" && config.jsonp === true) { + config.transport = "longPolling"; + } + + if (this.isCrossDomain(connection.url)) { + connection.log("Auto detected cross domain url."); + + if (config.transport === "auto") { + // Try webSockets and longPolling since SSE doesn't support CORS + // TODO: Support XDM with foreverFrame + config.transport = ["webSockets", "longPolling"]; + } + + // Determine if jsonp is the only choice for negotiation, ajaxSend and ajaxAbort. + // i.e. if the browser doesn't supports CORS + // If it is, ignore any preference to the contrary, and switch to jsonp. + if (!config.jsonp) { + config.jsonp = !$.support.cors; + + if (config.jsonp) { + connection.log("Using jsonp because this browser doesn't support CORS"); + } + } + + connection.contentType = signalR._.defaultContentType; + } + + connection.ajaxDataType = config.jsonp ? "jsonp" : "json"; + + $(connection).bind(events.onStart, function (e, data) { + if ($.type(callback) === "function") { + callback.call(connection); + } + deferred.resolve(connection); + }); + + initialize = function (transports, index) { + index = index || 0; + 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."]); + 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(); + } + return; + } + + var transportName = transports[index], + transport = $.type(transportName) === "object" ? transportName : signalR.transports[transportName]; + + if (transportName.indexOf("_") === 0) { + // Private member + initialize(transports, index + 1); + return; + } + + transport.start(connection, function () { // success + if (transport.supportsKeepAlive && connection.keepAliveData.activated) { + signalR.transports._logic.monitorKeepAlive(connection); + } + + connection.transport = transport; + + changeState(connection, + signalR.connectionState.connecting, + signalR.connectionState.connected); + + $(connection).triggerHandler(events.onStart); + + _pageWindow.unload(function () { // failure + connection.stop(false /* async */); + }); + + }, function () { + initialize(transports, index + 1); + }); + }; + + var url = connection.url + "/negotiate"; + + url = signalR.transports._logic.addQs(url, connection); + + connection.log("Negotiating with '" + url + "'."); + $.ajax({ + url: url, + global: false, + cache: false, + type: "GET", + contentType: connection.contentType, + data: {}, + dataType: connection.ajaxDataType, + error: function (error) { + $(connection).triggerHandler(events.onError, [error.responseText]); + deferred.reject("SignalR: Error during negotiation request: " + error.responseText); + // Stop the connection if negotiate failed + connection.stop(); + }, + success: function (res) { + var keepAliveData = connection.keepAliveData; + + connection.appRelativeUrl = res.Url; + connection.id = res.ConnectionId; + connection.token = res.ConnectionToken; + connection.webSocketServerUrl = res.WebSocketServerUrl; + + // Once the server has labeled the PersistentConnection as Disconnected, we should stop attempting to reconnect + // after res.DisconnectTimeout seconds. + connection.disconnectTimeout = res.DisconnectTimeout * 1000; // in ms + + + // If we have a keep alive + if (res.KeepAliveTimeout) { + // Register the keep alive data as activated + keepAliveData.activated = true; + + // Timeout to designate when to force the connection into reconnecting converted to milliseconds + keepAliveData.timeout = res.KeepAliveTimeout * 1000; + + // Timeout to designate when to warn the developer that the connection may be dead or is hanging. + keepAliveData.timeoutWarning = keepAliveData.timeout * connection.keepAliveWarnAt; + + // Instantiate the frequency in which we check the keep alive. It must be short in order to not miss/pick up any changes + keepAliveData.checkInterval = (keepAliveData.timeout - keepAliveData.timeoutWarning) / 3; + } + else { + keepAliveData.activated = false; + } + + 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 + "."]); + 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; + } + + $(connection).triggerHandler(events.onStarting); + + var transports = [], + supportedTransports = []; + + $.each(signalR.transports, function (key) { + if (key === "webSockets" && !res.TryWebSockets) { + // Server said don't even try WebSockets, but keep processing the loop + return true; + } + supportedTransports.push(key); + }); + + if ($.isArray(config.transport)) { + // ordered list provided + $.each(config.transport, function () { + var transport = this; + if ($.type(transport) === "object" || ($.type(transport) === "string" && $.inArray("" + transport, supportedTransports) >= 0)) { + transports.push($.type(transport) === "string" ? "" + transport : transport); + } + }); + } else if ($.type(config.transport) === "object" || + $.inArray(config.transport, supportedTransports) >= 0) { + // specific transport provided, as object or a named transport, e.g. "longPolling" + transports.push(config.transport); + } else { // default "auto" + transports = supportedTransports; + } + initialize(transports); + } + }); + + return deferred.promise(); + }, + + starting: function (callback) { + /// Adds a callback that will be invoked before anything is sent over the connection + /// A callback function to execute before each time data is sent on the connection + /// + var connection = this; + $(connection).bind(events.onStarting, function (e, data) { + callback.call(connection); + }); + return connection; + }, + + send: function (data) { + /// Sends data over the connection + /// The data to send over the connection + /// + var connection = this; + + if (connection.state === signalR.connectionState.disconnected) { + // Connection hasn't been started yet + throw new Error("SignalR: Connection must be started before data can be sent. Call .start() before .send()"); + } + + if (connection.state === signalR.connectionState.connecting) { + // Connection hasn't been started yet + throw new Error("SignalR: Connection has not been fully initialized. Use .start().done() or .start().fail() to run logic after the connection has started."); + } + + connection.transport.send(connection, data); + // REVIEW: Should we return deferred here? + return connection; + }, + + received: function (callback) { + /// Adds a callback that will be invoked after anything is received over the connection + /// A callback function to execute when any data is received on the connection + /// + var connection = this; + $(connection).bind(events.onReceived, function (e, data) { + callback.call(connection, data); + }); + return connection; + }, + + stateChanged: function (callback) { + /// Adds a callback that will be invoked when the connection state changes + /// A callback function to execute when the connection state changes + /// + var connection = this; + $(connection).bind(events.onStateChanged, function (e, data) { + callback.call(connection, data); + }); + return connection; + }, + + error: function (callback) { + /// Adds a callback that will be invoked after an error occurs with the connection + /// A callback function to execute when an error occurs on the connection + /// + var connection = this; + $(connection).bind(events.onError, function (e, data) { + callback.call(connection, data); + }); + return connection; + }, + + disconnected: function (callback) { + /// Adds a callback that will be invoked when the client disconnects + /// A callback function to execute when the connection is broken + /// + var connection = this; + $(connection).bind(events.onDisconnect, function (e, data) { + callback.call(connection); + }); + return connection; + }, + + connectionSlow: function (callback) { + /// Adds a callback that will be invoked when the client detects a slow connection + /// A callback function to execute when the connection is slow + /// + var connection = this; + $(connection).bind(events.onConnectionSlow, function(e, data) { + callback.call(connection); + }); + + return connection; + }, + + reconnecting: function (callback) { + /// Adds a callback that will be invoked when the underlying transport begins reconnecting + /// A callback function to execute when the connection enters a reconnecting state + /// + var connection = this; + $(connection).bind(events.onReconnecting, function (e, data) { + callback.call(connection); + }); + return connection; + }, + + reconnected: function (callback) { + /// Adds a callback that will be invoked when the underlying transport reconnects + /// A callback function to execute when the connection is restored + /// + var connection = this; + $(connection).bind(events.onReconnect, function (e, data) { + callback.call(connection); + }); + return connection; + }, + + stop: function (async, notifyServer) { + /// Stops listening + /// Whether or not to asynchronously abort the connection + /// Whether we want to notify the server that we are aborting the connection + /// + var connection = this; + + if (connection.state === signalR.connectionState.disconnected) { + return; + } + + try { + if (connection.transport) { + if (notifyServer !== false) { + connection.transport.abort(connection, async); + } + + if (connection.transport.supportsKeepAlive && connection.keepAliveData.activated) { + signalR.transports._logic.stopMonitoringKeepAlive(connection); + } + + connection.transport.stop(connection); + connection.transport = null; + } + + // Trigger the disconnect event + $(connection).triggerHandler(events.onDisconnect); + + delete connection.messageId; + delete connection.groupsToken; + + // Remove the ID and the deferral on stop, this is to ensure that if a connection is restarted it takes on a new id/deferral. + delete connection.id; + delete connection._deferral; + } + finally { + changeState(connection, connection.state, signalR.connectionState.disconnected); + } + + return connection; + }, + + log: function (msg) { + log(msg, this.logging); + } + }; + + signalR.fn.init.prototype = signalR.fn; + + signalR.noConflict = function () { + /// Reinstates the original value of $.connection and returns the signalR object for manual assignment + /// + if ($.connection === signalR) { + $.connection = _connection; + } + return signalR; + }; + + if ($.connection) { + _connection = $.connection; + } + + $.connection = $.signalR = signalR; + +}(window.jQuery, window)); +/* jquery.signalR.transports.common.js */ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +/*global window:false */ +/// + +(function ($, window) { + "use strict"; + + var signalR = $.signalR, + events = $.signalR.events, + changeState = $.signalR.changeState; + + signalR.transports = {}; + + function checkIfAlive(connection) { + var keepAliveData = connection.keepAliveData, + diff, + timeElapsed; + + // Only check if we're connected + if (connection.state === signalR.connectionState.connected) { + diff = new Date(); + + diff.setTime(diff - keepAliveData.lastKeepAlive); + timeElapsed = diff.getTime(); + + // Check if the keep alive has completely timed out + if (timeElapsed >= keepAliveData.timeout) { + connection.log("Keep alive timed out. Notifying transport that connection has been lost."); + + // Notify transport that the connection has been lost + connection.transport.lostConnection(connection); + } + else if (timeElapsed >= keepAliveData.timeoutWarning) { + // This is to assure that the user only gets a single warning + if (!keepAliveData.userNotified) { + connection.log("Keep alive has been missed, connection may be dead/slow."); + $(connection).triggerHandler(events.onConnectionSlow); + keepAliveData.userNotified = true; + } + } + else { + keepAliveData.userNotified = false; + } + } + + // Verify we're monitoring the keep alive + // We don't want this as a part of the inner if statement above because we want keep alives to continue to be checked + // in the event that the server comes back online (if it goes offline). + if (keepAliveData.monitoring) { + window.setTimeout(function () { + checkIfAlive(connection); + }, keepAliveData.checkInterval); + } + } + + function isConnectedOrReconnecting(connection) { + return connection.state === signalR.connectionState.connected || + connection.state === signalR.connectionState.reconnecting; + } + + signalR.transports._logic = { + pingServer: function (connection, transport) { + /// Pings the server + /// Connection associated with the server ping + /// + var baseUrl = transport === "webSockets" ? "" : connection.baseUrl, + url = baseUrl + connection.appRelativeUrl + "/ping", + deferral = $.Deferred(); + + url = this.addQs(url, connection); + + $.ajax({ + url: url, + global: false, + cache: false, + type: "GET", + contentType: connection.contentType, + data: {}, + dataType: connection.ajaxDataType, + success: function (data) { + if (data.Response === "pong") { + deferral.resolve(); + } + else { + deferral.reject("SignalR: Invalid ping response when pinging server: " + (data.responseText || data.statusText)); + } + }, + error: function (data) { + deferral.reject("SignalR: Error pinging server: " + (data.responseText || data.statusText)); + } + }); + + return deferral.promise(); + }, + + addQs: function (url, connection) { + var appender = url.indexOf("?") !== -1 ? "&" : "?", + firstChar; + + if (!connection.qs) { + return url; + } + + if (typeof (connection.qs) === "object") { + return url + appender + $.param(connection.qs); + } + + if (typeof (connection.qs) === "string") { + firstChar = connection.qs.charAt(0); + + if (firstChar === "?" || firstChar === "&") { + appender = ""; + } + + return url + appender + connection.qs; + } + + throw new Error("Connections query string property must be either a string or object."); + }, + + getUrl: function (connection, transport, reconnecting, appendReconnectUrl) { + /// Gets the url for making a GET based connect request + var baseUrl = transport === "webSockets" ? "" : connection.baseUrl, + url = baseUrl + connection.appRelativeUrl, + qs = "transport=" + transport + "&connectionToken=" + window.encodeURIComponent(connection.token); + + if (connection.data) { + qs += "&connectionData=" + window.encodeURIComponent(connection.data); + } + + if (connection.groupsToken) { + qs += "&groupsToken=" + window.encodeURIComponent(connection.groupsToken); + } + + if (!reconnecting) { + url += "/connect"; + } else { + if (appendReconnectUrl) { + url += "/reconnect"; + } else { + // A silent reconnect should only ever occur with the longPolling transport + url += "/poll"; + } + + if (connection.messageId) { + qs += "&messageId=" + window.encodeURIComponent(connection.messageId); + } + } + url += "?" + qs; + url = this.addQs(url, connection); + url += "&tid=" + Math.floor(Math.random() * 11); + return url; + }, + + maximizePersistentResponse: function (minPersistentResponse) { + return { + MessageId: minPersistentResponse.C, + Messages: minPersistentResponse.M, + Disconnect: typeof (minPersistentResponse.D) !== "undefined" ? true : false, + TimedOut: typeof (minPersistentResponse.T) !== "undefined" ? true : false, + LongPollDelay: minPersistentResponse.L, + GroupsToken: minPersistentResponse.G + }; + }, + + updateGroups: function (connection, groupsToken) { + if (groupsToken) { + connection.groupsToken = groupsToken; + } + }, + + ajaxSend: function (connection, data) { + var url = connection.url + "/send" + "?transport=" + connection.transport.name + "&connectionToken=" + window.encodeURIComponent(connection.token); + url = this.addQs(url, connection); + return $.ajax({ + url: url, + global: false, + type: connection.ajaxDataType === "jsonp" ? "GET" : "POST", + contentType: signalR._.defaultContentType, + dataType: connection.ajaxDataType, + data: { + data: data + }, + success: function (result) { + if (result) { + $(connection).triggerHandler(events.onReceived, [result]); + } + }, + error: function (errData, textStatus) { + if (textStatus === "abort" || textStatus === "parsererror") { + // The parsererror happens for sends that don't return any data, and hence + // don't write the jsonp callback to the response. This is harder to fix on the server + // so just hack around it on the client for now. + return; + } + $(connection).triggerHandler(events.onError, [errData]); + } + }); + }, + + ajaxAbort: function (connection, async) { + if (typeof (connection.transport) === "undefined") { + return; + } + + // Async by default unless explicitly overidden + async = typeof async === "undefined" ? true : async; + + var url = connection.url + "/abort" + "?transport=" + connection.transport.name + "&connectionToken=" + window.encodeURIComponent(connection.token); + url = this.addQs(url, connection); + $.ajax({ + url: url, + async: async, + timeout: 1000, + global: false, + type: "POST", + contentType: connection.contentType, + dataType: connection.ajaxDataType, + data: {} + }); + + connection.log("Fired ajax abort async = " + async); + }, + + processMessages: function (connection, minData) { + var data; + // Transport can be null if we've just closed the connection + if (connection.transport) { + var $connection = $(connection); + + // If our transport supports keep alive then we need to update the last keep alive time stamp. + // Very rarely the transport can be null. + if (connection.transport.supportsKeepAlive && connection.keepAliveData.activated) { + this.updateKeepAlive(connection); + } + + if (!minData) { + return; + } + + data = this.maximizePersistentResponse(minData); + + if (data.Disconnect) { + connection.log("Disconnect command received from server"); + + // Disconnected by the server + connection.stop(false, false); + return; + } + + this.updateGroups(connection, data.GroupsToken); + + if (data.Messages) { + $.each(data.Messages, function (index, message) { + $connection.triggerHandler(events.onReceived, [message]); + }); + } + + if (data.MessageId) { + connection.messageId = data.MessageId; + } + } + }, + + monitorKeepAlive: function (connection) { + var keepAliveData = connection.keepAliveData, + that = this; + + // If we haven't initiated the keep alive timeouts then we need to + if (!keepAliveData.monitoring) { + keepAliveData.monitoring = true; + + // Initialize the keep alive time stamp ping + that.updateKeepAlive(connection); + + // Save the function so we can unbind it on stop + connection.keepAliveData.reconnectKeepAliveUpdate = function () { + that.updateKeepAlive(connection); + }; + + // Update Keep alive on reconnect + $(connection).bind(events.onReconnect, connection.keepAliveData.reconnectKeepAliveUpdate); + + connection.log("Now monitoring keep alive with a warning timeout of " + keepAliveData.timeoutWarning + " and a connection lost timeout of " + keepAliveData.timeout); + // Start the monitoring of the keep alive + checkIfAlive(connection); + } + else { + connection.log("Tried to monitor keep alive but it's already being monitored"); + } + }, + + stopMonitoringKeepAlive: function (connection) { + var keepAliveData = connection.keepAliveData; + + // Only attempt to stop the keep alive monitoring if its being monitored + if (keepAliveData.monitoring) { + // Stop monitoring + keepAliveData.monitoring = false; + + // Remove the updateKeepAlive function from the reconnect event + $(connection).unbind(events.onReconnect, connection.keepAliveData.reconnectKeepAliveUpdate); + + // Clear all the keep alive data + connection.keepAliveData = {}; + connection.log("Stopping the monitoring of the keep alive"); + } + }, + + updateKeepAlive: function (connection) { + connection.keepAliveData.lastKeepAlive = new Date(); + }, + + ensureReconnectingState: function (connection) { + if (changeState(connection, + signalR.connectionState.connected, + signalR.connectionState.reconnecting) === true) { + $(connection).triggerHandler(events.onReconnecting); + } + 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: {} + } + }; + +}(window.jQuery, window)); +/* jquery.signalR.transports.webSockets.js */ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +/*global window:false */ +/// + +(function ($, window) { + "use strict"; + + var signalR = $.signalR, + events = $.signalR.events, + changeState = $.signalR.changeState, + transportLogic = signalR.transports._logic; + + signalR.transports.webSockets = { + name: "webSockets", + + supportsKeepAlive: true, + + send: function (connection, data) { + connection.socket.send(data); + }, + + start: function (connection, onSuccess, onFailed) { + var url, + opened = false, + that = this, + reconnecting = !onSuccess, + $connection = $(connection); + + if (!window.WebSocket) { + onFailed(); + return; + } + + if (!connection.socket) { + if (connection.webSocketServerUrl) { + url = connection.webSocketServerUrl; + } + else { + url = connection.wsProtocol + connection.host; + } + + url += transportLogic.getUrl(connection, this.name, reconnecting); + + connection.log("Connecting to websocket endpoint '" + url + "'"); + connection.socket = new window.WebSocket(url); + connection.socket.onopen = function () { + opened = true; + connection.log("Websocket opened"); + + transportLogic.clearReconnectTimeout(connection); + + if (onSuccess) { + onSuccess(); + } else if (changeState(connection, + signalR.connectionState.reconnecting, + signalR.connectionState.connected) === true) { + $connection.triggerHandler(events.onReconnect); + } + }; + + connection.socket.onclose = function (event) { + // 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 === connection.socket) { + if (!opened) { + if (onFailed) { + onFailed(); + } + else if (reconnecting) { + that.reconnect(connection); + } + return; + } + else if (typeof event.wasClean !== "undefined" && event.wasClean === false) { + // Ideally this would use the websocket.onerror handler (rather than checking wasClean in onclose) but + // I found in some circumstances Chrome won't call onerror. This implementation seems to work on all browsers. + $(connection).triggerHandler(events.onError, [event.reason]); + connection.log("Unclean disconnect from websocket." + event.reason); + } + else { + connection.log("Websocket closed"); + } + + that.reconnect(connection); + } + }; + + connection.socket.onmessage = function (event) { + var data = window.JSON.parse(event.data), + $connection = $(connection); + + if (data) { + // data.M is PersistentResponse.Messages + if ($.isEmptyObject(data) || data.M) { + transportLogic.processMessages(connection, data); + } else { + // For websockets we need to trigger onReceived + // for callbacks to outgoing hub calls. + $connection.triggerHandler(events.onReceived, [data]); + } + } + }; + } + }, + + reconnect: function (connection) { + transportLogic.reconnect(connection, this.name); + }, + + lostConnection: function (connection) { + this.reconnect(connection); + + }, + + 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(); + connection.socket = null; + } + }, + + abort: function (connection) { + } + }; + +}(window.jQuery, window)); +/* jquery.signalR.transports.serverSentEvents.js */ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +/*global window:false */ +/// + +(function ($, window) { + "use strict"; + + var signalR = $.signalR, + events = $.signalR.events, + changeState = $.signalR.changeState, + transportLogic = signalR.transports._logic; + + signalR.transports.serverSentEvents = { + name: "serverSentEvents", + + supportsKeepAlive: true, + + timeOut: 3000, + + start: function (connection, onSuccess, onFailed) { + var that = this, + opened = false, + $connection = $(connection), + reconnecting = !onSuccess, + url, + connectTimeOut; + + if (connection.eventSource) { + connection.log("The connection already has an event source. Stopping it."); + connection.stop(); + } + + if (!window.EventSource) { + if (onFailed) { + connection.log("This browser doesn't support SSE."); + onFailed(); + } + return; + } + + url = transportLogic.getUrl(connection, this.name, reconnecting); + + try { + connection.log("Attempting to connect to SSE endpoint '" + url + "'"); + connection.eventSource = new window.EventSource(url); + } + catch (e) { + connection.log("EventSource failed trying to connect with error " + e.Message); + if (onFailed) { + // The connection failed, call the failed callback + onFailed(); + } + else { + $connection.triggerHandler(events.onError, [e]); + if (reconnecting) { + // If we were reconnecting, rather than doing initial connect, then try reconnect again + that.reconnect(connection); + } + } + return; + } + + // After connecting, if after the specified timeout there's no response stop the connection + // and raise on failed + connectTimeOut = window.setTimeout(function () { + if (opened === false) { + connection.log("EventSource timed out trying to connect"); + connection.log("EventSource readyState: " + connection.eventSource.readyState); + + if (!reconnecting) { + that.stop(connection); + } + + if (reconnecting) { + // If we're reconnecting and the event source is attempting to connect, + // don't keep retrying. This causes duplicate connections to spawn. + if (connection.eventSource.readyState !== window.EventSource.CONNECTING && + connection.eventSource.readyState !== window.EventSource.OPEN) { + // If we were reconnecting, rather than doing initial connect, then try reconnect again + that.reconnect(connection); + } + } else if (onFailed) { + onFailed(); + } + } + }, + that.timeOut); + + connection.eventSource.addEventListener("open", function (e) { + connection.log("EventSource connected"); + + if (connectTimeOut) { + window.clearTimeout(connectTimeOut); + } + + transportLogic.clearReconnectTimeout(connection); + + if (opened === false) { + opened = true; + + if (onSuccess) { + onSuccess(); + } else if (changeState(connection, + signalR.connectionState.reconnecting, + signalR.connectionState.connected) === true) { + // If there's no onSuccess handler we assume this is a reconnect + $connection.triggerHandler(events.onReconnect); + } + } + }, false); + + connection.eventSource.addEventListener("message", function (e) { + // process messages + if (e.data === "initialized") { + return; + } + + transportLogic.processMessages(connection, window.JSON.parse(e.data)); + }, false); + + connection.eventSource.addEventListener("error", function (e) { + // 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 === connection.eventSource) { + if (!opened) { + if (onFailed) { + onFailed(); + } + + return; + } + + connection.log("EventSource readyState: " + connection.eventSource.readyState); + + if (e.eventPhase === window.EventSource.CLOSED) { + // We don't use the EventSource's native reconnect function as it + // doesn't allow us to change the URL when reconnecting. We need + // to change the URL to not include the /connect suffix, and pass + // the last message id we received. + connection.log("EventSource reconnecting due to the server connection ending"); + that.reconnect(connection); + } else { + // connection error + connection.log("EventSource error"); + $connection.triggerHandler(events.onError); + } + } + }, false); + }, + + reconnect: function (connection) { + transportLogic.reconnect(connection, this.name); + }, + + lostConnection: function (connection) { + this.reconnect(connection); + }, + + send: function (connection, data) { + transportLogic.ajaxSend(connection, data); + }, + + stop: function (connection) { + // Don't trigger a reconnect after stopping + transportLogic.clearReconnectTimeout(connection); + + if (connection && connection.eventSource) { + connection.log("EventSource calling close()"); + connection.eventSource.close(); + connection.eventSource = null; + delete connection.eventSource; + } + }, + + abort: function (connection, async) { + transportLogic.ajaxAbort(connection, async); + } + }; + +}(window.jQuery, window)); +/* jquery.signalR.transports.foreverFrame.js */ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +/*global window:false */ +/// + +(function ($, window) { + "use strict"; + + var signalR = $.signalR, + events = $.signalR.events, + changeState = $.signalR.changeState, + 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", + + supportsKeepAlive: true, + + timeOut: 3000, + + start: function (connection, onSuccess, onFailed) { + var that = this, + frameId = (transportLogic.foreverFrame.count += 1), + url, + frame = $(""); + + if (window.EventSource) { + // If the browser supports SSE, don't use Forever Frame + if (onFailed) { + connection.log("This browser supports SSE, skipping Forever Frame."); + onFailed(); + } + 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); + url += "&frameId=" + frameId; + + // Set body prior to setting URL to avoid caching issues. + $("body").append(frame); + + frame.prop("src", url); + transportLogic.foreverFrame.connections[frameId] = connection; + + connection.log("Binding to iframe's readystatechange event."); + frame.bind("readystatechange", function () { + if ($.inArray(this.readyState, ["loaded", "complete"]) >= 0) { + connection.log("Forever frame iframe readyState changed to " + this.readyState + ", reconnecting"); + + that.reconnect(connection); + } + }); + + connection.frame = frame[0]; + connection.frameId = frameId; + + if (onSuccess) { + connection.onSuccess = onSuccess; + } + + // After connecting, if after the specified timeout there's no response stop the connection + // and raise on failed + window.setTimeout(function () { + if (connection.onSuccess) { + connection.log("Failed to connect using forever frame source, it timed out after " + that.timeOut + "ms."); + that.stop(connection); + + if (onFailed) { + onFailed(); + } + } + }, that.timeOut); + }, + + reconnect: function (connection) { + var that = this; + window.setTimeout(function () { + if (connection.frame && transportLogic.ensureReconnectingState(connection)) { + var frame = connection.frame, + src = transportLogic.getUrl(connection, that.name, true) + "&frameId=" + connection.frameId; + connection.log("Updating iframe src to '" + src + "'."); + frame.src = src; + } + }, connection.reconnectDelay); + }, + + lostConnection: function (connection) { + this.reconnect(connection); + }, + + send: function (connection, data) { + transportLogic.ajaxSend(connection, data); + }, + + receive: function (connection, data) { + var cw; + + transportLogic.processMessages(connection, data); + // Delete the script & div elements + connection.frameMessageCount = (connection.frameMessageCount || 0) + 1; + if (connection.frameMessageCount > 50) { + connection.frameMessageCount = 0; + cw = connection.frame.contentWindow || connection.frame.contentDocument; + if (cw && cw.document) { + $("body", cw.document).empty(); + } + } + }, + + stop: function (connection) { + var cw = null; + + // Stop attempting to prevent loading icon + loadPreventer.cancel(); + + if (connection.frame) { + if (connection.frame.stop) { + connection.frame.stop(); + } else { + try { + cw = connection.frame.contentWindow || connection.frame.contentDocument; + if (cw.document && cw.document.execCommand) { + cw.document.execCommand("Stop"); + } + } + catch (e) { + connection.log("SignalR: Error occured when stopping foreverFrame transport. Message = " + e.message); + } + } + $(connection.frame).remove(); + delete transportLogic.foreverFrame.connections[connection.frameId]; + connection.frame = null; + connection.frameId = null; + delete connection.frame; + delete connection.frameId; + connection.log("Stopping forever frame"); + } + }, + + abort: function (connection, async) { + transportLogic.ajaxAbort(connection, async); + }, + + getConnection: function (id) { + return transportLogic.foreverFrame.connections[id]; + }, + + started: function (connection) { + if (connection.onSuccess) { + connection.onSuccess(); + connection.onSuccess = null; + delete connection.onSuccess; + } else if (changeState(connection, + signalR.connectionState.reconnecting, + signalR.connectionState.connected) === true) { + // If there's no onSuccess handler we assume this is a reconnect + $(connection).triggerHandler(events.onReconnect); + } + } + }; + +}(window.jQuery, window)); +/* jquery.signalR.transports.longPolling.js */ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +/*global window:false */ +/// + +(function ($, window) { + "use strict"; + + var signalR = $.signalR, + events = $.signalR.events, + changeState = $.signalR.changeState, + isDisconnecting = $.signalR.isDisconnecting, + transportLogic = signalR.transports._logic; + + signalR.transports.longPolling = { + name: "longPolling", + + supportsKeepAlive: false, + + reconnectDelay: 3000, + + init: function (connection, onComplete) { + /// Pings the server to ensure availability + /// Connection associated with the server ping + /// Callback to call once initialization has completed + + var that = this, + pingLoop, + // pingFail is used to loop the re-ping behavior. When we fail we want to re-try. + pingFail = function (reason) { + if (isDisconnecting(connection) === false) { + connection.log("SignalR: Server ping failed because '" + reason + "', re-trying ping."); + window.setTimeout(pingLoop, that.reconnectDelay); + } + }; + + connection.log("SignalR: Initializing long polling connection with server."); + pingLoop = function () { + // Ping the server, on successful ping call the onComplete method, otherwise if we fail call the pingFail + transportLogic.pingServer(connection, that.name).done(onComplete).fail(pingFail); + }; + + pingLoop(); + }, + + start: function (connection, onSuccess, onFailed) { + /// Starts the long polling connection + /// The SignalR connection to start + var that = this, + initialConnectedFired = false, + fireConnect = function () { + if (initialConnectedFired) { + return; + } + initialConnectedFired = true; + onSuccess(); + connection.log("Longpolling connected"); + }; + + if (connection.pollXhr) { + connection.log("Polling xhr requests already exists, aborting."); + connection.stop(); + } + + // We start with an initialization procedure which pings the server to verify that it is there. + // On scucessful initialization we'll then proceed with starting the transport. + that.init(connection, function () { + connection.messageId = null; + + window.setTimeout(function () { + (function poll(instance, raiseReconnect) { + var messageId = instance.messageId, + connect = (messageId === null), + reconnecting = !connect, + url = transportLogic.getUrl(instance, that.name, reconnecting, raiseReconnect); + + // If we've disconnected during the time we've tried to re-instantiate the poll then stop. + if (isDisconnecting(instance) === true) { + return; + } + + connection.log("Attempting to connect to '" + url + "' using longPolling."); + instance.pollXhr = $.ajax({ + url: url, + global: false, + cache: false, + type: "GET", + dataType: connection.ajaxDataType, + contentType: connection.contentType, + success: function (minData) { + var delay = 0, + data; + + fireConnect(); + + if (minData) { + data = transportLogic.maximizePersistentResponse(minData); + } + + transportLogic.processMessages(instance, minData); + + if (data && + $.type(data.LongPollDelay) === "number") { + delay = data.LongPollDelay; + } + + if (data && data.Disconnect) { + return; + } + + if (isDisconnecting(instance) === true) { + return; + } + + // We never want to pass a raiseReconnect flag after a successful poll. This is handled via the error function + if (delay > 0) { + window.setTimeout(function () { + poll(instance, false); + }, delay); + } else { + poll(instance, false); + } + }, + + error: function (data, textStatus) { + if (textStatus === "abort") { + connection.log("Aborted xhr requst."); + return; + } + + if (connection.state !== signalR.connectionState.reconnecting) { + connection.log("An error occurred using longPolling. Status = " + textStatus + ". " + data.responseText); + $(instance).triggerHandler(events.onError, [data.responseText]); + } + + // Transition into the reconnecting state + transportLogic.ensureReconnectingState(instance); + + // If we've errored out we need to verify that the server is still there, so re-start initialization process + // This will ping the server until it successfully gets a response. + that.init(instance, function () { + // Call poll with the raiseReconnect flag as true + poll(instance, true); + }); + } + }); + + // 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); + } + } + }(connection)); + + // Set an arbitrary timeout to trigger onSuccess, this will alot for enough time on the server to wire up the connection. + // Will be fixed by #1189 and this code can be modified to not be a timeout + window.setTimeout(function () { + // Trigger the onSuccess() method because we've now instantiated a connection + fireConnect(); + }, 250); + }, 250); // Have to delay initial poll so Chrome doesn't show loader spinner in tab + }); + }, + + lostConnection: function (connection) { + throw new Error("Lost Connection not handled for LongPolling"); + }, + + send: function (connection, data) { + transportLogic.ajaxSend(connection, data); + }, + + stop: function (connection) { + /// Stops the long polling connection + /// The SignalR connection to stop + if (connection.pollXhr) { + connection.pollXhr.abort(); + connection.pollXhr = null; + delete connection.pollXhr; + } + }, + + abort: function (connection, async) { + transportLogic.ajaxAbort(connection, async); + } + }; + +}(window.jQuery, window)); +/* jquery.signalR.hubs.js */ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +/*global window:false */ +/// + +(function ($, window) { + "use strict"; + + // we use a global id for tracking callbacks so the server doesn't have to send extra info like hub name + var callbackId = 0, + callbacks = {}, + eventNamespace = ".hubProxy"; + + function makeEventName(event) { + return event + eventNamespace; + } + + // 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; + } + + function getArgValue(a) { + return $.isFunction(a) ? null : ($.type(a) === "undefined" ? null : a); + } + + function hasMembers(obj) { + for (var key in obj) { + // If we have any properties in our callback map then we have callbacks and can exit the loop via return + if (obj.hasOwnProperty(key)) { + return true; + } + } + + return false; + } + + // hubProxy + function hubProxy(hubConnection, hubName) { + /// + /// Creates a new proxy object for the given hub connection that can be used to invoke + /// methods on server hubs and handle client method invocation requests from the server. + /// + return new hubProxy.fn.init(hubConnection, hubName); + } + + hubProxy.fn = hubProxy.prototype = { + init: function (connection, hubName) { + this.state = {}; + this.connection = connection; + this.hubName = hubName; + this._ = { + callbackMap: {} + }; + }, + + hasSubscriptions: function () { + return hasMembers(this._.callbackMap); + }, + + on: function (eventName, callback) { + /// Wires up a callback to be invoked when a invocation request is received from the server hub. + /// The name of the hub event to register the callback for. + /// The callback to be invoked. + var self = this, + callbackMap = self._.callbackMap; + + // Normalize the event name to lowercase + eventName = eventName.toLowerCase(); + + // If there is not an event registered for this callback yet we want to create its event space in the callback map. + if (!callbackMap[eventName]) { + callbackMap[eventName] = {}; + } + + // Map the callback to our encompassed function + callbackMap[eventName][callback] = function (e, data) { + callback.apply(self, data); + }; + + $(self).bind(makeEventName(eventName), callbackMap[eventName][callback]); + + return self; + }, + + off: function (eventName, callback) { + /// Removes the callback invocation request from the server hub for the given event name. + /// The name of the hub event to unregister the callback for. + /// The callback to be invoked. + var self = this, + callbackMap = self._.callbackMap, + callbackSpace; + + // Normalize the event name to lowercase + eventName = eventName.toLowerCase(); + + callbackSpace = callbackMap[eventName]; + + // Verify that there is an event space to unbind + if (callbackSpace) { + // Only unbind if there's an event bound with eventName and a callback with the specified callback + if (callbackSpace[callback]) { + $(self).unbind(makeEventName(eventName), callbackSpace[callback]); + + // Remove the callback from the callback map + delete callbackSpace[callback]; + + // Check if there are any members left on the event, if not we need to destroy it. + if (!hasMembers(callbackSpace)) { + delete callbackMap[eventName]; + } + } + else if (!callback) { // Check if we're removing the whole event and we didn't error because of an invalid callback + $(self).unbind(makeEventName(eventName)); + + delete callbackMap[eventName]; + } + } + + return self; + }, + + invoke: function (methodName) { + /// Invokes a server hub method with the given arguments. + /// The name of the server hub method. + + var self = this, + args = $.makeArray(arguments).slice(1), + argValues = map(args, getArgValue), + data = { H: self.hubName, M: methodName, A: argValues, I: callbackId }, + d = $.Deferred(), + callback = function (minResult) { + var result = self._maximizeHubResponse(minResult); + + // Update the hub state + $.extend(self.state, result.State); + + if (result.Error) { + // Server hub method threw an exception, log it & reject the deferred + if (result.StackTrace) { + self.connection.log(result.Error + "\n" + result.StackTrace); + } + d.rejectWith(self, [result.Error]); + } else { + // Server invocation succeeded, resolve the deferred + d.resolveWith(self, [result.Result]); + } + }; + + callbacks[callbackId.toString()] = { scope: self, method: callback }; + callbackId += 1; + + if (!$.isEmptyObject(self.state)) { + data.S = self.state; + } + + self.connection.send(window.JSON.stringify(data)); + + return d.promise(); + }, + + _maximizeHubResponse: function (minHubResponse) { + return { + State: minHubResponse.S, + Result: minHubResponse.R, + Id: minHubResponse.I, + Error: minHubResponse.E, + StackTrace: minHubResponse.T + }; + } + }; + + hubProxy.fn.init.prototype = hubProxy.fn; + + // hubConnection + function hubConnection(url, options) { + /// Creates a new hub connection. + /// [Optional] The hub route url, defaults to "/signalr". + /// [Optional] Settings to use when creating the hubConnection. + var settings = { + qs: null, + logging: false, + useDefaultPath: true + }; + + $.extend(settings, options); + + if (!url || settings.useDefaultPath) { + url = (url || "") + "/signalr"; + } + return new hubConnection.fn.init(url, settings); + } + + hubConnection.fn = hubConnection.prototype = $.connection(); + + hubConnection.fn.init = function (url, options) { + var settings = { + qs: null, + logging: false, + useDefaultPath: true + }, + connection = this; + + $.extend(settings, options); + + // Call the base constructor + $.signalR.fn.init.call(connection, url, settings.qs, settings.logging); + + // Object to store hub proxies for this connection + connection.proxies = {}; + + // Wire up the received handler + connection.received(function (minData) { + var data, proxy, dataCallbackId, callback, hubName, eventName; + if (!minData) { + return; + } + + if (typeof (minData.I) !== "undefined") { + // We received the return value from a server method invocation, look up callback by id and call it + dataCallbackId = minData.I.toString(); + callback = callbacks[dataCallbackId]; + if (callback) { + // Delete the callback from the proxy + callbacks[dataCallbackId] = null; + delete callbacks[dataCallbackId]; + + // Invoke the callback + callback.method.call(callback.scope, minData); + } + } else { + data = this._maximizeClientHubInvocation(minData); + + // We received a client invocation request, i.e. broadcast from server hub + connection.log("Triggering client hub event '" + data.Method + "' on hub '" + data.Hub + "'."); + + // Normalize the names to lowercase + hubName = data.Hub.toLowerCase(); + eventName = data.Method.toLowerCase(); + + // Trigger the local invocation event + proxy = this.proxies[hubName]; + + // Update the hub state + $.extend(proxy.state, data.State); + $(proxy).triggerHandler(makeEventName(eventName), [data.Args]); + } + }); + }; + + hubConnection.fn._maximizeClientHubInvocation = function (minClientHubInvocation) { + return { + Hub: minClientHubInvocation.H, + Method: minClientHubInvocation.M, + Args: minClientHubInvocation.A, + State: minClientHubInvocation.S + }; + }; + + hubConnection.fn._registerSubscribedHubs = function () { + /// + /// Sets the starting event to loop through the known hubs and register any new hubs + /// that have been added to the proxy. + /// + + if (!this._subscribedToHubs) { + this._subscribedToHubs = true; + this.starting(function () { + // Set the connection's data object with all the hub proxies with active subscriptions. + // These proxies will receive notifications from the server. + var subscribedHubs = []; + + $.each(this.proxies, function (key) { + if (this.hasSubscriptions()) { + subscribedHubs.push({ name: key }); + } + }); + + this.data = window.JSON.stringify(subscribedHubs); + }); + } + }; + + hubConnection.fn.createHubProxy = function (hubName) { + /// + /// Creates a new proxy object for the given hub connection that can be used to invoke + /// methods on server hubs and handle client method invocation requests from the server. + /// + /// + /// The name of the hub on the server to create the proxy for. + /// + + // Normalize the name to lowercase + hubName = hubName.toLowerCase(); + + var proxy = this.proxies[hubName]; + if (!proxy) { + proxy = hubProxy(this, hubName); + this.proxies[hubName] = proxy; + } + + this._registerSubscribedHubs(); + + return proxy; + }; + + hubConnection.fn.init.prototype = hubConnection.fn; + + $.hubConnection = hubConnection; + +}(window.jQuery, window)); +/* jquery.signalR.version.js */ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +/*global window:false */ +/// +(function ($) { + $.signalR.version = "1.1.0-beta1"; +}(window.jQuery)); diff --git a/UI/Mixins/backbone.signalr.mixin.js b/UI/Mixins/backbone.signalr.mixin.js new file mode 100644 index 000000000..7e1bfa13b --- /dev/null +++ b/UI/Mixins/backbone.signalr.mixin.js @@ -0,0 +1,36 @@ +"use strict"; +(function ($) { + + var connection = $.connection('/signalr/series'); + + var _getStatus = function (status) { + switch (status) { + case 0: + return 'connecting'; + case 1: + return 'connected'; + case 2: + return 'reconnecting'; + case 4: + return 'disconnected'; + default: + throw 'invalid status ' + status; + } + + }; + + connection.stateChanged(function (change) { + + console.log('signalR [{0}]'.format(_getStatus(change.newState))); + }); + + connection.received(function (data) { + console.log(data); + }); + + connection.error(function (error) { + console.warn(error); + }); + + connection.start(); +})(jQuery); \ No newline at end of file diff --git a/UI/app.js b/UI/app.js index f8f806534..66ae7a77c 100644 --- a/UI/app.js +++ b/UI/app.js @@ -26,7 +26,7 @@ require.config({ } }); -define('app', ['Instrumentation/ErrorHandler'], function () { +define('app', function () { window.NzbDrone = new Backbone.Marionette.Application(); window.NzbDrone.Config = {};