Skip to content

Add support for deferred sockets. #99

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions README-deferred.md
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
@@ -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'
],

89 changes: 89 additions & 0 deletions socket-deferred.js
Original file line number Diff line number Diff line change
@@ -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
};
};
});
239 changes: 239 additions & 0 deletions socket-deferred.spec.js
Original file line number Diff line number Diff line change
@@ -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();
});
});

});
273 changes: 273 additions & 0 deletions socket-deferred2.spec.js
Original file line number Diff line number Diff line change
@@ -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();
});
});

});
15 changes: 15 additions & 0 deletions socket.js
Original file line number Diff line number Diff line change
@@ -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;
};
}];