📜 ⬆️ ⬇️

AngularJS service concept for socket.io named connections

Good day, friends!

In the process of working on a single service, the frontend of which is built on AngularJS , it became necessary to communicate with the socket.io server via different logical channels. At the same time, there was a desire to do with just one real physical connection to the server and to have in the application code separate connection objects for each channel. What came out of this, you can see under the cut.



')
socket.io namespaces

As it turned out, the socket.io library provides the ability to create so-called namespaces that help multiplex messages from different subsystems within the same physical connection. The client code looks like this:

var channelMessages = io.connect('http://localhost:3000/messages'), channelMessages.on('message received', function() { /* notify about new message */ }); // ... channelCommands = io.connect('http://localhost:3000/commands'); channelCommands.emit('init'); channelCommands.on('command received', function() { /* process new command */ }); 

Those. each io.connect call returns a new connection object that has on and emit . That's it for such connection objects and I wanted to have an AngularJS service for each for convenient communication with the server.

AngularJS service socket v.1

At the time of the desire to get a few named sockets, the service for working with socket.io looked like it was described in this article .

 app.factory('socket', function ($rootScope) { var socket = io.connect(); return { on: function (eventName, callback) { socket.on(eventName, function () { var args = arguments; $rootScope.$apply(function () { apply(socket, args); }); }); }, emit: function (eventName, data, callback) { socket.emit(eventName, data, function () { var args = arguments; $rootScope.$apply(function () { if (callback) { callback.apply(socket, args); } }); }) } }; }); 

In fact, this is a primitive wrapper around on and emit , leading to the update of all scopes when a message is received / confirmed to be sent. The connection to the server occurs once when the service is initialized (since AngularJS calls the factory method once to ensure that the services are singletone).

Disclaimer
Updating all scopes for every sneeze is not cool and you should try to minimize the area being updated with $apply() . But for this article, this is offtopic.

With the need for namespaces, the approach to creating a connection to the server at the time of service initialization socket stopped working. Plus there was a need for several instances of the service, each of which is connected to its own channel. And one more obvious requirement is to avoid duplication of code when creating named connections in the future.

AngularJS-service Socket v.2

At about this point in development, some cognitive dissonance began. There must be a lot of named connections, and the AngularJS service is a singleton. The first attempt to solve this problem was the idea that AngularJS already knows how to do something similar out of the box. As it turned out, there are at least 3 ways to create services. The simplest is module.service , which is accepted by the constructor with which the service object will be created on demand. A slightly more flexible way is the module.factory , which makes it more convenient than directly in the constructor to perform some additional actions before returning the service instance. And the most flexible way is module.provider . Judging by the name, it can be assumed that it is possible to specify the dependence of client modules on the provider, and write something like socketsProvider.get('foo') in the client code to get the named connection /foo . However, module.provider allows only once to configure the service instance, and the client code should not depend on the provider, but directly on the service itself.

After discussing the problem with colleagues, an idea arose to extend the on and emit of the socket service, adding the first namespace to them, and keeping a pool of lazy connections inside the service. For each on or emit , it would be necessary to check whether a connection already exists with the specified namespace, and if not, create a new one. And for the implementation of objects of named connections, one would have to create lightweight services socketFoo , socketBar , etc., which have their own on and emit , currying socket.on and socket.emit , fixing the namespace parameter with constant values ​​of 'foo' and 'bar'. A working solution, but with a significant drawback - when expanding the set of socket methods, clients of the socketFoo and socketBar services socketBar not be able to call new socket methods without changing the existing code of the socketFoo and socketBar .

Breaking the head a little more we managed to remember that any object, including a function, can act as a service in AngularJS! The classic pattern of using services as instances can be changed to the following approach:

 var module = angular.module('myApp.services', []); app.factory('MyService', function() { function MyService(options) { /*    */ } MyService.prototype.baz = function() { /* ... */ }; MyService.prototype.qux = function() { /* ... */ }; return MyService; }); // ... module.factory('clientService', function(MyService) { var myService = new MyService({foo: 1, bar: 42}); myService.qux(); // return ... }); 

Changed not only the way the service is created, but also the naming method. Instead of the traditional camelCase (meaning that we are dealing with an instance), CamelCase used to show that the service is actually a designer. Using this approach, the Socket service was implemented:

Socket.js
 var services = angular.module('myApp.services', []); services.factory('Socket', ['$rootScope', function($rootScope) { var connections = {}; //  ,       function getConnection(channel) { if (!connections[channel]) { connections[channel] = io.connect('http://localhost:3000/' + channel); } return connections[channel]; } //    ,   namespace-  . function Socket(namespace) { this.namespace = namespace; } Socket.prototype.on = function(eventName, callback) { var con = getConnection(this.namespace), self = this; //      con.on(eventName, function() { var args = arguments; $rootScope.$apply(function() { callback.apply(con, args); }); }); }; Socket.prototype.emit = function(eventName, data, callback) { var con = getConnection(this.namespace); //     . con.emit(eventName, data, function() { var args = arguments; $rootScope.$apply(function() { if (callback) { callback.apply(con, args); } }); }) }; return Socket; }]); 


Specific implementations of named connections on which client modules will depend may look something like this:

Examples.js
 var services = angular.module('myApp.services.channels', []); //  . services.factory('channelFoo', function(Socket) { return new Socket('foo'); }); //    . services.factory('channelBar', function(Socket) { function ChannelBar() { this.namespace = 'bar'; } ChannelBar.prototype = angular.extend(Socket.prototype, {}); ChannelBar.prototype.start = function() { this.emit('start'); }; ChannelBar.prototype.exit = function() { this.emit('exit'); }; return new ChannelBar(); }); 


Channels created in this way possess both the advantage of automatic inheritance of all the functionality from the base Socket object and require significantly less generic code than in the case of socket.on and socket.emit .

Conclusion

The given example of the implementation of the Socket service is only a concept. To fully use it, it is necessary to supplement it with the possibility of injecting an io object, setting the connection string and authorization, as well as the possibility of specifying the scope that should be updated when receiving messages from the server. Code with an example can be found on github .

Source: https://habr.com/ru/post/215427/


All Articles