Stopping memory leaks in AWS Amplify Hub
Tags: javascript
Published: 2020-06-02 00:30:06

[Uclusion](https://www.uclusion.com/?utm_source=devto&utm_medium=blog&utm_campaign=devmemory) makes extensive use of AWS Amplify Hub to allow components to subscribe to data change events. In Hub it’s really easy to create listeners :

    Hub.listen('MY_CHANNEL', (data) => {
      const { payload } = data;
      // do something more
    });

What’s not so easy is removing that listener when you’re done with it: Hub requires you to pass the exact same function object that you passed into the listen. Hence, you’d have to do something like.

    const myListener = (data) => {
      const { payload } = data;
      // do something more
    };
    
    Hub.listen('MY_CHANNEL', myListener);
    Hub.remove('MY_CHANNEL', myListener);

This really makes it difficult to have the listener and the cleanup in separate sections of the code. Worse, if you ignore the problem and don’t unregister, you’ll constantly be leaking memory.

How do we fix this? The way we fix it is to maintain the registry ourselves with a static object in an ES6 module. The code looks like this.

    import { Hub } from '@aws-amplify/core';
    const busListeners = {};
    
    / Adds a listener to under the UNIQUE name, to the channel
      If a listener with the name already exists, it will be removed
      before this one is added
      @param channel
      @param name
      @param callback
     /
    export function registerListener(channel, name, callback) {
      const previousListener = busListeners[name];
      if (!!previousListener) {
        Hub.remove(channel, previousListener);
      }
      busListeners[name] = callback;
      Hub.listen(channel, busListeners[name]);
    }
    
    /
      Removes a listener with the UNIQUE name, from the channel.
      @param channel
      @param name
     /
    export function removeListener(channel, name) {
      const listener = busListeners[name];
      if (!!listener) {
        Hub.remove(channel, listener);
      }
    }
    
    /
      Pushes a message out to the listeners of the channel
      @param channel
      @param message
     /
    export function pushMessage(channel, message) {
      Hub.dispatch(channel, message);
    }

See production code [here](https://github.com/Uclusion/uclusion_web_ui/blob/master/src/utils/MessageBusUtils.js).
This code also has the nice property that it abstracts my exact messaging system away. I can easily swap hub out for another library if I so chose. For completeness sake, here’s the code that registers a new listener and removes it in my abstraction

    import { registerListener, removeListener } from 'MessageBusUtils';
    const myListener = (data) => {
      const { payload } = data;
      // do something more
    };
    
    registerListener('MY_CHANNEL', 'my_demo_listener', callback);
    removeListener('MY_CHANNEL', 'my_demo_listener');

Sending a message looks like:

    import { pushMessage } from 'MessageBusUtils';
    pushMessage('MY_CHANNEL', { value: 1});

Final Thoughts/Alternate Implementation ideas:

If you didn’t want to maintain your own registry, or name your listeners with strings, you could maintain a file with all your listener functions declared as exported consts. The problem with that approach is it makes it hard to bind a listener with a closure in other parts of the code. By using names, it doesn’t matter where the actual callback function gets defined, or what it’s real scope is. However, if all you have is static functions anyways, then the exported function constants will work just as well.