diff --git a/README-deferred.md b/README-deferred.md new file mode 100644 index 0000000..9a11c02 --- /dev/null +++ b/README-deferred.md @@ -0,0 +1,53 @@ +## Deferred sockets + +Useful for applications that doesn't connect on start, but wait for some user or system interaction. + +Code in the application remain as usual (no promises are needed), a real socket can be later passed and even sockets can be swapped. + +### Usage + +Use exactly as you would use the original `socketFactory`, just pass the deferred socket instead. An extra methods is added to the original factory to replace / swap the socket. + +Some application logic changes should be considered, i.e. + - Application features that require connection, shouldn't be available before a real connection is made + - Disconnect event is not available + +#### Examples: + +```javascript +deferred_socket = deferredSocketFactory(); +socket = socketFactory( { + scope: scope, + ioSocket: deferred_socket +}); +``` +In you app use as usual + +```javascript +socket.on('connect, function); +``` + +Swap your real socket when you're ready + +```javascript +function connect(params){ + // do whatever you need to do + var realSocket = io.connect(); + socket.swapSocket(realSocket) ; +} + +function changeServer(newserver) { + var newSocket = io.connect(newserver); + socket.swapSocket(newSocket); +} +``` + +#### Notes + +These changes are based on the work of @davisford but refactored to: + +- Preserve original module (by @btford) functionality and operation nearly intact +- Allow swap sockets (between real io-sockets) +- Follow the same order and structure as original @btford module for easier maintenance +- Acts as an endpoint insted of modifying or rewrapping angular-socket-io +- Pass all tests, needs tests for socket swap (real socket for another real socket) diff --git a/karma.conf.js b/karma.conf.js index 39df1c3..6278402 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -7,6 +7,7 @@ module.exports = function (config) { 'node_modules/angular/angular.js', 'node_modules/angular-mocks/angular-mocks.js', 'socket.js', + 'socket-deferred.js', '*.spec.js' ], diff --git a/socket-deferred.js b/socket-deferred.js new file mode 100644 index 0000000..c9ae11c --- /dev/null +++ b/socket-deferred.js @@ -0,0 +1,89 @@ +/* + * @license + * angular-socket-io v0.7.0 + * (c) 2014 Brian Ford http://briantford.com + * License: MIT + */ + +angular.module('btford.socket-io'). + + factory('deferredSocketFactory', function () { + + 'use strict'; + + return function deferredSocketFactory () { + + var queue = { + addListener: [], + once: [], + forward: [], + emit: [] + }; + + /*jshint unused: false */ + var addListener = function (eventName, callback) { + var array = Array.prototype.slice.call(arguments); + queue.addListener.push(array); + }; + + var removeListener = function (eventName, fn) { + if (fn) { + for (var i = 0, len = queue.addListener.length; i < len; i++) { + if (queue.addListener[i][0] === eventName && queue.addListener[i][1] === fn) { + break; + } + } + queue.addListener.splice(i, 1); + } else { + // Remove every instance or just return? + } + }; + + var removeAllListeners = function () { + queue.addListener.length = 0; + queue.once.length = 0; + }; + + var processDeferred = function (socket) { + for (var key in queue) { + var deferredCalls = queue[key]; + if (deferredCalls.length > 0) { + /*jshint -W083 */ + deferredCalls.map(function (array) { + + var has = socket.hasOwnProperty(key); + var fn = socket[key]; + + socket[key].apply(null, array); + }); + } + } + // Clear once and emit (as they are passed to the real socket) + queue.once.length = 0; + queue.emit.length = 0; + }; + + // Create our deferred wrapper + return { + deferred: true, + bootstrap: processDeferred, + on: addListener, + addListener: addListener, + once: function (eventName, callback) { + var array = Array.prototype.slice.call(arguments); + queue.once.push(array); + }, + emit: function(eventName, data, callback) { + var array = Array.prototype.slice.apply(arguments); + queue.emit.push(array); + }, + removeListener: removeListener, + removeAllListeners: removeAllListeners, + disconnect: function () { + throw new Error('Disconnect is not deferrable'); + }, + connect: processDeferred, + //~ forward: is a wrapper event not a socket event + }; + }; + }); diff --git a/socket-deferred.spec.js b/socket-deferred.spec.js new file mode 100644 index 0000000..f3a0f44 --- /dev/null +++ b/socket-deferred.spec.js @@ -0,0 +1,239 @@ +/* + * angular-socket-io v0.4.1 + * (c) 2014 Brian Ford http://briantford.com + * License: MIT + */ + +'use strict'; + + +describe('deferredSocketFactory', function () { + + beforeEach(module('btford.socket-io')); + + var socket, + scope, + $timeout, + $browser, + mockIoSocket, + spy, + deferred_socket; + + beforeEach( + inject(function (socketFactory, _$browser_, $rootScope, _$timeout_, deferredSocketFactory) { + $browser = _$browser_; + $timeout = _$timeout_; + scope = $rootScope.$new(); + spy = jasmine.createSpy('emitSpy'); + + // Use a deferred socket instead + deferred_socket = deferredSocketFactory(); + + // Now pass our socket using the standard options + socket = socketFactory({ + ioSocket: deferred_socket,// mockIoSocket, + scope: scope + }); + }) + ); + + beforeEach(function() { + mockIoSocket = io.connect(); + socket.swapSocket(mockIoSocket); + }); + + describe('#on', function () { + + it('should apply asynchronously', function () { + socket.on('event', spy); + + mockIoSocket.emit('event'); + + expect(spy).not.toHaveBeenCalled(); + $timeout.flush(); + + expect(spy).toHaveBeenCalled(); + }); + + }); + + + describe('#disconnect', function () { + + it('should call the underlying socket.disconnect', function () { + mockIoSocket.disconnect = spy; + socket.disconnect(); + expect(spy).toHaveBeenCalled(); + }); + + }); + + + describe('#once', function () { + + it('should apply asynchronously', function () { + socket.once('event', spy); + + mockIoSocket.emit('event'); + + expect(spy).not.toHaveBeenCalled(); + $timeout.flush(); + + expect(spy).toHaveBeenCalled(); + }); + + it('should only run once', function () { + var counter = 0; + socket.once('event', function () { + counter += 1; + }); + + mockIoSocket.emit('event'); + mockIoSocket.emit('event'); + $timeout.flush(); + + expect(counter).toBe(1); + }); + + }); + + + describe('#emit', function () { + + it('should call the delegate socket\'s emit', function () { + spyOn(mockIoSocket, 'emit'); + + socket.emit('event', {foo: 'bar'}); + + expect(mockIoSocket.emit).toHaveBeenCalled(); + }); + + it('should allow multiple data arguments', function () { + spyOn(mockIoSocket, 'emit'); + socket.emit('event', 'x', 'y'); + expect(mockIoSocket.emit).toHaveBeenCalledWith('event', 'x', 'y'); + }); + + it('should wrap the callback with multiple data arguments', function () { + spyOn(mockIoSocket, 'emit'); + socket.emit('event', 'x', 'y', spy); + expect(mockIoSocket.emit.mostRecentCall.args[3]).toNotBe(spy); + + mockIoSocket.emit.mostRecentCall.args[3](); + expect(spy).not.toHaveBeenCalled(); + $timeout.flush(); + + expect(spy).toHaveBeenCalled(); + }); + + }); + + + describe('#removeListener', function () { + + it('should not call after removing an event', function () { + socket.on('event', spy); + socket.removeListener('event', spy); + + mockIoSocket.emit('event'); + + expect($browser.deferredFns.length).toBe(0); + }); + + }); + + + describe('#removeAllListeners', function () { + + it('should not call after removing listeners for an event', function () { + socket.on('event', spy); + socket.removeAllListeners('event'); + + mockIoSocket.emit('event'); + + expect($browser.deferredFns.length).toBe(0); + }); + + it('should not call after removing all listeners', function () { + socket.on('event', spy); + socket.on('event2', spy); + socket.removeAllListeners(); + + mockIoSocket.emit('event'); + mockIoSocket.emit('event2'); + + expect($browser.deferredFns.length).toBe(0); + }); + + }); + + + describe('#forward', function () { + + it('should forward events', function () { + socket.forward('event'); + + scope.$on('socket:event', spy); + mockIoSocket.emit('event'); + $timeout.flush(); + + expect(spy).toHaveBeenCalled(); + }); + + it('should forward an array of events', function () { + socket.forward(['e1', 'e2']); + + scope.$on('socket:e1', spy); + scope.$on('socket:e2', spy); + + mockIoSocket.emit('e1'); + mockIoSocket.emit('e2'); + $timeout.flush(); + expect(spy.callCount).toBe(2); + }); + + it('should remove watchers when the scope is removed', function () { + + socket.forward('event'); + scope.$on('socket:event', spy); + mockIoSocket.emit('event'); + $timeout.flush(); + + expect(spy).toHaveBeenCalled(); + + scope.$destroy(); + spy.reset(); + mockIoSocket.emit('event'); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should use the specified prefix', inject(function (socketFactory) { + var socket = socketFactory({ + ioSocket: mockIoSocket, + scope: scope, + prefix: 'custom:' + }); + + socket.forward('event'); + + scope.$on('custom:event', spy); + mockIoSocket.emit('event'); + $timeout.flush(); + + expect(spy).toHaveBeenCalled(); + })); + + it('should forward to the specified scope when one is provided', function () { + var child = scope.$new(); + spyOn(child, '$broadcast'); + socket.forward('event', child); + + scope.$on('socket:event', spy); + mockIoSocket.emit('event'); + $timeout.flush(); + + expect(child.$broadcast).toHaveBeenCalled(); + }); + }); + +}); diff --git a/socket-deferred2.spec.js b/socket-deferred2.spec.js new file mode 100644 index 0000000..93ff01e --- /dev/null +++ b/socket-deferred2.spec.js @@ -0,0 +1,273 @@ +/* + * angular-socket-io v0.4.1 + * (c) 2014 Brian Ford http://briantford.com + * License: MIT + * + * This test all methods again, allowing the deferred socket to testing events, and replacing it before checking results + * * We test that the methods are correctly passed into the real socket, + * Socket is swapped before direct calls to the mockIOSocket (or testing events would not be registered) + */ + +'use strict'; + + +describe('deferredSocketFactory-B', function () { + + beforeEach(module('btford.socket-io')); + + var socket, + scope, + $timeout, + $browser, + mockIoSocket, + spy, + deferred_socket; + + beforeEach( + inject(function (socketFactory, _$browser_, $rootScope, _$timeout_, deferredSocketFactory) { + $browser = _$browser_; + $timeout = _$timeout_; + scope = $rootScope.$new(); + spy = jasmine.createSpy('emitSpy'); + + // Create the socket for testing, but don't replace until after use + mockIoSocket = io.connect(); + + // Use a deferred socket instead + deferred_socket = deferredSocketFactory(); + + // Now pass our socket using the standard options + socket = socketFactory({ + ioSocket: deferred_socket,// mockIoSocket, + scope: scope + }); + }) + ); + + + + + describe('#on', function () { + + it('should apply asynchronously', function () { + socket.on('event', spy); + + socket.swapSocket(mockIoSocket); + + mockIoSocket.emit('event'); + expect(spy).not.toHaveBeenCalled(); + $timeout.flush(); + expect(spy).toHaveBeenCalled(); + }); + + }); + + + describe('#disconnect', function () { + + it('should call the underlying socket.disconnect', function () { + mockIoSocket.disconnect = spy; + socket.swapSocket(mockIoSocket); // Disconnect event is only possible on a real socket + socket.disconnect(); + expect(spy).toHaveBeenCalled(); + }); + + }); + + + describe('#once', function () { + + it('should apply asynchronously', function () { + socket.once('event', spy); + socket.swapSocket(mockIoSocket); + + mockIoSocket.emit('event'); + + expect(spy).not.toHaveBeenCalled(); + $timeout.flush(); + + expect(spy).toHaveBeenCalled(); + }); + + it('should only run once', function () { + var counter = 0; + socket.once('event', function () { + counter += 1; + }); + + socket.swapSocket(mockIoSocket); + mockIoSocket.emit('event'); + mockIoSocket.emit('event'); + $timeout.flush(); + + expect(counter).toBe(1); + }); + + }); + + + describe('#emit', function () { + + it('should call the delegate socket\'s emit', function () { + spyOn(mockIoSocket, 'emit'); + + socket.emit('event', {foo: 'bar'}); + + socket.swapSocket(mockIoSocket); + expect(mockIoSocket.emit).toHaveBeenCalled(); + }); + + it('should allow multiple data arguments', function () { + spyOn(mockIoSocket, 'emit'); + socket.emit('event', 'x', 'y'); + + socket.swapSocket(mockIoSocket); + expect(mockIoSocket.emit).toHaveBeenCalledWith('event', 'x', 'y'); + }); + + it('should wrap the callback with multiple data arguments', function () { + spyOn(mockIoSocket, 'emit'); + socket.emit('event', 'x', 'y', spy); + + socket.swapSocket(mockIoSocket); + expect(mockIoSocket.emit.mostRecentCall.args[3]).toNotBe(spy); + + mockIoSocket.emit.mostRecentCall.args[3](); + expect(spy).not.toHaveBeenCalled(); + $timeout.flush(); + + expect(spy).toHaveBeenCalled(); + }); + + }); + + + describe('#removeListener', function () { + + it('should not call after removing an event', function () { + socket.on('event', spy); + socket.removeListener('event', spy); + socket.swapSocket(mockIoSocket); // Only real socket support removal + + mockIoSocket.emit('event'); + + expect($browser.deferredFns.length).toBe(0); + }); + + }); + + + describe('#removeAllListeners', function () { + + it('should not call after removing listeners for an event', function () { + socket.on('event', spy); + socket.removeAllListeners('event'); + +socket.swapSocket(mockIoSocket); // Inject the actual socket + + mockIoSocket.emit('event'); + expect($browser.deferredFns.length).toBe(0); + }); + + it('should not call after removing all listeners', function () { + socket.on('event', spy); + socket.on('event2', spy); + socket.removeAllListeners(); + +socket.swapSocket(mockIoSocket); // Inject the actual socket + + mockIoSocket.emit('event'); + mockIoSocket.emit('event2'); + + expect($browser.deferredFns.length).toBe(0); + }); + + }); + + + describe('#forward', function () { + + it('should forward events', function () { + socket.forward('event'); + + scope.$on('socket:event', spy); + + socket.swapSocket(mockIoSocket); // Inject the actual socket + mockIoSocket.emit('event'); + + + $timeout.flush(); + + expect(spy).toHaveBeenCalled(); + }); + + it('should forward an array of events', function () { + socket.forward(['e1', 'e2']); + + scope.$on('socket:e1', spy); + scope.$on('socket:e2', spy); + + socket.swapSocket(mockIoSocket); // Inject the actual socket + mockIoSocket.emit('e1'); + mockIoSocket.emit('e2'); + + $timeout.flush(); + expect(spy.callCount).toBe(2); + }); + + it('should remove watchers when the scope is removed', function () { + + socket.forward('event'); + scope.$on('socket:event', spy); + + socket.swapSocket(mockIoSocket); // Inject the actual socket + + mockIoSocket.emit('event'); + $timeout.flush(); + + expect(spy).toHaveBeenCalled(); + + scope.$destroy(); + spy.reset(); + mockIoSocket.emit('event'); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should use the specified prefix', inject(function (socketFactory) { + var socket = socketFactory({ + //~ ioSocket: mockIoSocket, + ioSocket: deferred_socket, + scope: scope, + prefix: 'custom:' + }); + + socket.forward('event'); + + scope.$on('custom:event', spy); + + socket.swapSocket(mockIoSocket); + + mockIoSocket.emit('event'); + + $timeout.flush(); + + expect(spy).toHaveBeenCalled(); + })); + + it('should forward to the specified scope when one is provided', function () { + var child = scope.$new(); + spyOn(child, '$broadcast'); + socket.forward('event', child); + + scope.$on('socket:event', spy); + socket.swapSocket(mockIoSocket); // Inject the actual socket + + mockIoSocket.emit('event'); + + $timeout.flush(); + + expect(child.$broadcast).toHaveBeenCalled(); + }); + }); + +}); diff --git a/socket.js b/socket.js index 649c416..d852c62 100644 --- a/socket.js +++ b/socket.js @@ -97,6 +97,21 @@ angular.module('btford.socket-io', []). } }; + // Add conditional support for deferred sockets + var keep_deferred; + + if (socket.hasOwnProperty('deferred')) { + wrappedSocket.swapSocket = function(newSocket) { + // Keep a reference for later on + if (socket.hasOwnProperty('deferred')) keep_deferred = socket; + // Allow for more than one replacement, i.e connect to a different server + if (keep_deferred) { + socket = newSocket; + keep_deferred.bootstrap(wrappedSocket); + } + } + } + return wrappedSocket; }; }];