BoonZ
BoonZ

Reputation: 453

Is it possible to limit socket.io to have only one listener per event?

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:

Upvotes: 2

Views: 4127

Answers (3)

Qiulang
Qiulang

Reputation: 12505

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, ...)
}

Upvotes: 4

BoonZ
BoonZ

Reputation: 453

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);
    }
}

Upvotes: 0

jfriend00
jfriend00

Reputation: 708146

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;
        }
    }
}

Upvotes: 2

Related Questions