Electron Adventures: Episode 40: Event Bus API with ES6 Proxies
Tags: javascript, electron, svelte
Published: 2021-09-02 03:18:53

Our components communicate through event bus, and it does everything we want, and implementation is very simple.

On the other hand, event calls look messy. For example here's handler for double clicking a file:

```javascript
  function ondoubleclick() {
    eventBus.emit("app", "activatePanel", panelId)
    eventBus.emit(panelId, "focusOn", idx)
    eventBus.emit(panelId, "activateItem")
  }
```

Why doesn't it look more like this?

```javascript
  function ondoubleclick() {
    app.activatePanel(panelId)
    panel.focusOn(idx)
    panel.activateItem()
  }
```

Let's try to make it so!

### `Proxy`

In a language like Ruby this would be extremely simple to implement with `method_missing`. Javascript unfortunately doesn't have anything like that. Or at least it didn't use to.

ES6 created `Proxy`, which is a special kind of object, which basicaly has `method_missing` and other metaprogramming. The name is fairly stupid, as it has a lot of uses other than proxying stuff, such as creating nice APIs.

Most people never heard of it, as it's ES6-only, and unlike the rest of ES6, this one is impossible to transpile with Babel. So as long as you had to support IE (through Babel transpilation), there was no way to use them.

Nowadays, they're actually used by some frameworks behind the scene like Vue, but due to awkward way they're created, few people use them in apps directly.

Also their performance is not amazing, but we're just trying to make nice API here.

### Original `EventBus` implementation

Here's our starting point:

```javascript
export default class EventBus {
  constructor() {
    this.callbacks = {}
  }

  handle(target, map) {
    this.callbacks[target] = { ...(this.callbacks[target] || {}), ...map }
  }

  emit(target, event, ...details) {
    let handlers = this.callbacks[target]
    if (handlers) {
      if (handlers[event]) {
        handlers[event](...details)
      } else if (handlers["*"]) {
        handlers["*"](event, ...details)
      }
    }
  }
}
```

### `Proxy` implementation

We want `eventBus.target("app")` or `eventBus.target(panelId)` to return something we can then use with regular function calls. First part is very easy, we just create `EventTarget` object, passing `bus` and `target` as arguments:

```javascript
export default class EventBus {
  constructor() {
    this.callbacks = {}
  }

  handle(target, map) {
    this.callbacks[target] = { ...(this.callbacks[target] || {}), ...map }
  }

  emit(target, event, ...details) {
    let handlers = this.callbacks[target]
    if (handlers) {
      if (handlers[event]) {
        handlers[event](...details)
      } else if (handlers["*"]) {
        handlers["*"](event, ...details)
      }
    }
  }

  target(t) {
    return new EventTarget(this, t)
  }
}
```

Now we need to fake a fake object that's basically one big `method_missing`. Whichever method we call on it, it will return a function for calling that event:

```javascript
class EventTarget {
  constructor(bus, target) {
    this.bus = bus
    this.target = target
    return new Proxy(this, {
      get: (receiver, name) => {
        return (...args) => {
          bus.emit(target, name, ...args)
        }
      }
    })
  }
}
```

There's a lot to unpack here. First we set `this.bus` and `this.target` even though we strictly speaking don't need to, as they're in closure scope. It just makes it easier to read debug output in console, if we ever needed to debug code using such proxies.

Then we return a value from `constructor`. Returning a value from `constructor`? If you're used to just about any other language, you might be confused, as pretty much none of them support it - and even in Javascript it's very rare to actually use this feature. But constructor for a class can absolutely return something else than just a fresh instance of the class. Well, as long as that other thing is an object too, for some reason you cannot just return strings or numbers.

This is somehow valid Javascript:

```javascript
class Cat {
  constructor() {
    return {cat: "No Cat For You!"}
  }
  meow() {
    console.log("MEOW!")
  }
}
let cat = new Cat() // what we returned here is not a Cat
cat.meow() // TypeError: cat.meow is not a function
```

We have one good use case for this feature, returning `Proxy` when we create `EventTarget`. We even pass the original unwrapped object as `this`. But really we don't use it for anything, all we'll ever use on this object is `get`.

And this:

```javascript
eventBus.target("app").activatePanel(panelId)
```

Translates to this:

```javascript
(new EventTarget(eventBus, "app")).activatePanel(panelId)
```

Which then gets bamboozled to:

```javascript
(new Proxy(eventTarget, {get: ourGetFunction})).activatePanel(panelId)
```

Which translates to:

```javascript
proxy.get("activatePanel")(panelId)
```

Which translates to:

```javascript
((...args) => { eventBus.emit("app", name, ...args) })(panelId)
```

Which finally runs as:

```javascript
eventBus.emit("app", name, panelId)
```

### How to use this?

Implementation was complicated behind the scenes, but then we have nicer API:

```javascript
  let app = eventBus.target("app")
  let panel = eventBus.target(panelId)

  function onclick() {
    app.activatePanel(panelId)
    panel.focusOn(idx)
  }
  function onrightclick() {
    app.activatePanel(panelId)
    panel.focusOn(idx)
    panel.flipSelected(idx)
  }
  function ondoubleclick() {
    app.activatePanel(panelId)
    panel.focusOn(idx)
    panel.activateItem()
  }
```

That looks considirably more readable than:

```javascript
  function onclick() {
    eventBus.emit("app", "activatePanel", panelId)
    eventBus.emit(panelId, "focusOn", idx)
  }
  function onrightclick() {
    eventBus.emit("app", "activatePanel", panelId)
    eventBus.emit(panelId, "focusOn", idx)
    eventBus.emit(panelId, "flipSelected", idx)
  }
  function ondoubleclick() {
    eventBus.emit("app", "activatePanel", panelId)
    eventBus.emit(panelId, "focusOn", idx)
    eventBus.emit(panelId, "activateItem")
  }
```

### More Proxies?

We could use second layer of proxies so instead of:

```javascript
let app = eventBus.target("app")
let panel = eventBus.target(panelId)
```

We could then say:

```javascript
let app = eventBus.app
let panel = eventBus[panelId]
```

To do that we'd need to return a `Proxy` from `EventBus` constructor, which would redirect `get` calls to `this.target`. I'll leave this as an exercise for the reader.

### Why do we need this?

The obvious question is: why do we need this?

Why can't we just do this instead (in `App.svelte`):

```javascript
  eventBus.app = {switchPanel, activatePanel, quit, openPalette, closePalette}
  eventBus.activePanel = eventBus[$activePanel]
```

And then use it with code like this:

```javascript
  let app = eventBus.app
  let panel = eventBus[panelId]
  let activePanel = eventBus.activePanel

  app.switchPanel(panelId)
```

There are two issues with this. First components are created in some order. So if one component wants to do this when it's initialized, the other component might not have sen its events yet, so `eventBus.something` might be `undefined` at that point. This can be worked around with some delayed callback or reactivity, but that's adding boilerplate to save some other boilerplate.

The bigger problem is with `let activePanel = eventBus.activePanel`. If we do that, it will set `activePanel` to point at whichever panel was active when this code was run, and it will never update. So we'd need to make it reactive, but on what?

If we do this:

```javascript
$ activePanel = eventBus[$activePanelId]
```

Then every component needs to access some store with ID of the active panel. So, even more boilerplate all over.

`EventBus` based solutions don't have such problems, as they only lookup target when event is actually triggered.

### Result

Here's the results, identical to what we had before:

![Episode 40 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8j7mqcgd5vwmbr506nl8.png)

In the next episodes, we'll try out a framework you probably never heard about.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-40-event-bus-api-with-es6-proxies).