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
Show file tree
Hide file tree
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
Expand Up @@ -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'
],

Expand Down
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();
});
});

});
Loading