DEV Community

David Israel for Uclusion

Posted on • Updated on

Stopping memory leaks in AWS Amplify Hub

Uclusion 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
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

See production code here.
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');
Enter fullscreen mode Exit fullscreen mode

Sending a message looks like:

import { pushMessage } from 'MessageBusUtils';
pushMessage('MY_CHANNEL', { value: 1});
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)