2

my question is if its possible to limit the bind listener possibility for specific event? For example I have a listener:

//In server
socket.emit('something', { message: 'Hello World!' });

//At client
socket.on('something', (data) => {
    console.log(data.message);  
});

//Output in console
Hello World!

But if I duplicate that snippet somewhere else in the code, I bind 2 same listeners to one event, so I get 2 console logs where I wanted only one.

//In server
socket.emit('something', { message: 'Hello World!' });

//At client
socket.on('something', (data) => {
    console.log(data.message);  
});

socket.on('something', (data) => {
    console.log(data.message);  
});

//Output in console
Hello World!
Hello World!

I know that there is possible to have multiple listeners for different actions on single event but when I use socket.io in some frameworks and switch between components (where I have binding in constructor of the component) the framework just binds the same listener to same event every time that i switch between components. So the result is like above.

So my question is:

  • Is it possible to check if some event already has listener and ignore second bind?
  • Or if it's there is some socket.io configuration setting that would allow to bind only one listener per event and when you bind a new one that it would override the old one?
  • Or just some good practices on how to handle such situation?
Amiga500
  • 5,874
  • 10
  • 64
  • 117
BoonZ
  • 443
  • 1
  • 6
  • 16
  • 1
    stop listening when you change component by using: socket.removeListener('something', something); more info: https://stackoverflow.com/questions/23092624/socket-io-removing-specific-listener – Paolo Jun 15 '17 at 15:01
  • thank you, i have composed an interface like service in angular2 and posted it here as answer where i used your preposition with socket.off function (which is the same as socket.removeListener as far as i know) – BoonZ Jun 19 '17 at 08:33

3 Answers3

3

jfriend00's answer can be further simplified b/c Emitter class has a method called hasListeners, https://socket.io/docs/client-api/

The socket actually inherits every method of the Emitter class, like hasListeners, once or off (to remove an event listener).

let exist = socket.hasListeners(eventName)
if (exist) {
   // maybe just return
} else { 
   // the usual stuff
   socket.on(eventName, ...)
}
Qiulang
  • 10,295
  • 11
  • 80
  • 129
2

A socket.io socket is derived from an EventEmitter object. As such, its listener functionality comes entirely from that implementation. The EventEmitter object does not have a feature to prevent more than one listener for a given event.

So, you have a few choices:

  1. You can remove the first event listener whenever it is that you no longer want it attached.

  2. You can override .on() to either deny setting a new handler for an event that already has a handler or you can remove the prior handler before setting the new one.

  3. You can change your logic so that your own code removes its own event listener when you no longer want it to be active.

Here's a function you could call on each new socket to avoid ever having more than one listener for a given eventName:

function hookSocketSingle(socket) {
    let origOn = socket.on;
    socket.on = function(eventName, listener) {
        // allow the event handler to be registered only if one doesn't already exist
        if (this.listenerCount(eventName) === 0) {
            return origOn.call(this, eventName, listener);
        } else {
            return this;
        }
    }
}
jfriend00
  • 683,504
  • 96
  • 985
  • 979
0

From the help of "paolo mania" comment and "jfriend00" answer I have solved the issue for Angular2 framework and Socket.IO library.

So first i have created a service to keep the Socket.IO connection open throughout the component creation and destruction cycles.

import { Injectable } from '@angular/core';

declare var io: any;

@Injectable()
export class SocketService {

    // We create socket object for socket.io lib and events object 
    // to store all events that are bound from specific component 
    socket;
    events;

    // When the service is injected it checks if socket.io and our 
    // events objects are initialised
    constructor() {
        if(!this.socket) {
            this.socket = io(environment.apiURL);
            this.events = {};
        }
    }

    // Then we create a middleware function to intercept the socket.on()
    // function and we request one more parameter which is the component name.
    on(component, event, callback) {
        // We check if component key already exists in our events object. 
        if(this.events[component]) {
            // We check if the wanted event is already bound in selected component
            // (basically we check if its added to events[component] array) 
            if(this.events[component].indexOf(event) < 1) {
                // If it's not added we bind the event to socket and add it to events[component] array
                this.events[component].push(event);
                this.socket.on(event, callback);
            }
        // If the component key does not exist we initialise event[component] as an array 
        // and we add our event to the array as well as we bind the socket event
        } else {
            this.events[component] = [];
            this.events[component].push(event);
            this.socket.on(event, callback);
        }
    }

    // We also create a middleware function to intercept the socket.emit()
    // function and just forward data to socket.emit
    emit(event, data = {}) {
        this.socket.emit(event, data);
    }

    // And last we add unsubscribe function so we can unbind all listeners from
    // single component (mostly used in onNgDestroy() when we don't want that bound events
    // are persistent between more than one component)
    unsubscribe(component) {
        // We check if component key exists in events object
        if(this.events[component]) {
            // We iterate through array and we remove listeners for every 
            // event that was bound to selected component
            this.events[component].forEach((event) => {
                this.socket.off(event);
            });

            // And in the end we remove component key from events
            delete this.events[component];
        }
    }
}

So we have service on which we can bind as many events we want, they are sorted by components so we know which component has which bindings. If we want to bind same event in same component we just ignore it and when we want we can unbind everything in component so some funny actions are not triggered when our component is destroyed or not active.

On component side we use the service like all other:

import { ApiService } from '../core/socket.service'; 

@Component({
    templateUrl: './example.component.html',
    styleUrls: ['./example.component.scss']
})
export class ExampleComponent {

    // We inject our socket service in constructor
    constructor(private socket: SocketService) {
        // We bind wanted events with component name and handle them as we would normally in socket.io
        this.socket.on(this.constructor.name, 'event1', (data) => {
            console.log(data);
        });

        this.socket.on(this.constructor.name, 'event2', (data) => {
            console.log(data);
        });
    }

    ... other functions ...

     // If we don't want to keep our events persistent for another component we just unsubscribe.
     // When we will return to this component it will re-bind the events stated in component constructor.
     // But if we choose to use persistent mode it will not duplicate the events.
     ngOnDestroy() {
        this.socket.unsubscribe(this.constructor.name);
    }
}
BoonZ
  • 443
  • 1
  • 6
  • 16
  • Is one supposed to get all of this from the question you asked? It seems like my answer answered the question you asked and what you've shown here is a lot of stuff unrelated to the actual question posed, but built on top of the concepts I explained in my answer. I get that this is the totality of the problem you were trying to solve, but most of it doesn't appear to have anything to do with the question you asked. A little surprised I guess that this is the accepted answer. – jfriend00 Jun 19 '17 at 21:31
  • Doesn't really encourage people to supply answers when you take concepts from their answer, combine it with a lot of stuff unrelated to what you asked about in your question and then accept your own answer. – jfriend00 Jun 19 '17 at 21:32
  • Sry, i have changed the accepted answer to yours, but as i wanted to share my final conclusion it did solve my issue spot on :D (indeed using your concept) but when i'm asking a question i try to be as abstract as i can be because more people can answer it. So its hard not to get conceptual answers on abstract question. Sincerely – BoonZ Jun 20 '17 at 07:45
  • Actually abstract questions here are usually NOT as useful as specific questions with your real code. I've probably posted 500 times asking people to get rid of the abstract make-up code and post their real code so we can help with the real problem. Anyway, glad you got a solution. Asking an abstract question about a particular solution assumes you know the best type of solution. Posting about the real problem (not just issues with your solution) allows people to suggest solutions you have not even thought of. – jfriend00 Jun 20 '17 at 13:41