Search code examples
javascriptnode.jseventemitter

Why do I need to wrap my EventEmitter's on function when passing/exposing it?


My application communicates to devices using several different mechanisms, such as serial (i.e. like USB CDC / Virtual COM port) and TCP (i.e. like telnet), and I've tried to encapsulate / hide / abstract this functionality with a higher-level interface so that other parts of the code that use these don't care which mechanism is being used.

In this case I have serial.js and tcp.js that each export a function called connect that returns an object with an on: function() { ... } property that I want to wire up to their internal EventEmitter instances. (I didn't think I should expose the entire object as that might allow other code to call emit & create hard-to-debug problems.)

What's confusing me is that if I just return { on: emitter.on } then the callbacks never seem to get called, but if I return { on: function(a, b) { return emitter.on(a,b); } it works fine. I feel like this has something to do with closures / scope or the time at which the symbol emitter is resolved, but this is unlike other troubles I've encountered before regarding these topics. Could someone please help me understand what's going on here that makes these two similar lines of code so different?

"use strict";

const net = require('net');
const EventEmitter = require('events');
const ConnectionEventNames = require('./events.js');


function connect(settings) {
    // ... (validates settings) ...
    const emitter = new EventEmitter();

    const socket = net.connect({
        host: settings.host,
        port: settings.port
    }, () => {
        emitter.emit(ConnectionEventNames.connected);
    });

    socket.on('data', (data) => {
        emitter.emit(ConnectionEventNames.received_data, data);
    });
    socket.on('error', (error) => {
        emitter.emit(ConnectionEventNames.error, error);
    });
    socket.on('end', () => {
        emitter.emit(ConnectionEventNames.disconnected);
    });
    socket.setNoDelay(settings.setNoDelay);

    return {
        // FIXME: why didn't this work the way I initially wrote it? (next line)
        on_original: emitter.on,
        on: function(eventName, listener) {
            return emitter.on(eventName, listener);
        },
        // ...
    };
}


module.exports = {
    connect
};

Solution

  • The problem when passing an instance's prototype functions around like that is that the context is lost. emitter.on is not implicitly bound to emitter, it's just a normal function. Without the emitter context, this inside the implementation for .on() won't point to what you'd expect.

    Typically you'd see exceptions thrown pretty quickly because most prototype functions generally assume a valid context and so try to access specific properties on this, however EventEmitter is a bit special in that it checks for missing internal properties and creates them when they're missing. So it'd be creating those properties on whatever context you're executing the function with (global or otherwise).