Going real time with donation alerts
Tags: webdev, javascript, node, bunjs
Published: 2023-10-20 19:34:44



![A streamer doing a live code and a donation alert at the bottom left corner of the screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/py39156sp2mm65ut34jc.png)At [AlertPix](alertpix.live) we allow streamers to receive donations from their audience via Pix Instant Payment and show an alert on the live stream.

This post shows how donation alerts feature was coded in just a day.

## Notification widget
Our users can use their Streamlabs account to use their own alert box in the live stream.
However, we also have users that don't use Streamlabs, that's where the Notification widget comes in.

## The PubSub
The ideia was simple: each widget listens into a topic and receives a new alert to show when its not showing any alerts.

This new service was bootstrapped with [Elysia](https://elysiajs.com/), the fastest Bun framework.

Now, to create a new http server is just as simple as:
```ts
const app = new Elysia()
```

Now we need to allow for widget to connect via WebSockets:
```ts
app
  .use(ws())
  .ws('/ws', {
    body: t.Object({ ... }),
    query: t.Object({ ... }),
    open(ws) {},
    close(ws) {},
    message(ws, message) {},
  })
```
Here is where the tech stack shines. We can allow WebSocket connections, add schema validation to their messages and request params.
We can also create a pub sub mechanism with a built-in API in bun.

This can be done in a line of code when the socket connects:
```ts
open(ws) {
  // ...
  ws.subscribe(id)
  free[id] = true
}
```

We also stored the widget in a `free` list. Meaning that the widget is not currently showing any alerts.

When the peer disconnects, we remove it from the list:
```ts
close(ws) {
  // ...
  delete free[id]
}
```

The widget can send a message telling that its free, so we handle that as well.
We check if the topic has items so we send the next alert, if any. Tagging it as free or not:
```ts
message(ws) {
  // ...
  const value = await redis.lRange(id, 0, 0);
  if (!value[0]) {
    free[id] = true
    return
  }

  await redis.lPop(id);

  free[id] = false
  app.server.publish(id, value[0])
}
```


## Posting a donation
We updated our API to store the notification in the queue and publish to the notifying service via an API call:
```js
export function donationReceived(context: Context, donation: Donation) {
  // ...

  context.redis.rPush(context.receiver, donation)
  notifyWidget({ topic: context.receiver, donation })

  // ...
}
```

API call that we defined with the same logic to notify the connected peers if they are free:
```ts
post("/publish", async ({ body }) => {
  if (free[body.receiver] === false) {
    return
  }

  const value = await client.lRange(body.receiver, 0, 0);
  await client.lPop(body.receiver);

  free[body.receiver] = false
  app.server!.publish(body.receiver, value[0])
})
```

## What we learned:
Choosing the right tech stack helps a lot. But knowing about the topic and understanding what is needed to build the feature, we can build things pretty fast. [In this post](https://dev.to/alertpix/1-hour-features-1la7) we show how at AlertPix we code each feature in one hour.

Since we launched we improve every day a little bit. Of course, today the implementation is pretty much different than this. But this blog aims to illustrate how you can write a pub sub without fighting against the code.

Take a look in the code exampls if it where a complete code so you can try it yourself:
```ts
import { Elysia } from "elysia"
import { cors } from '@elysiajs/cors'
import { createClient } from 'redis'

const free: Record<string, boolean> = {}

const client = await createClient({
  url: process.env.REDIS_URL,
  password: process.env.REDIS_PASSWORD,
})
  .on('error', err => console.log('Redis Client Error', err))
  .connect();

const app = new Elysia()
  .use(cors())
  .ws('/ws', {
    open(ws) {
      const id = ws.data.query.id

      ws.subscribe(id)
      free[id] = true
    },
    close(ws) {
      const id = ws.data.query.id

      delete free[id]
    },
    async message(ws, message) {
      const id = ws.data.query.id

      const value = await client.lRange(id, 0, 0);
      if (!value[0]) {
        free[id] = message.free
      }

      await client.lPop(id);

      free[id] = false
      app.server!.publish(id, value[0])
    },
  })
  .post("/publish", async ({ body }) => {
    if (free[body.receiver] === false) {
      return
    }

    const value = await client.lRange(body.receiver, 0, 0);
    await client.lPop(body.receiver);

    free[body.receiver] = false
    app.server!.publish(body.receiver, value[0])
  })
  .listen(process.env.PORT || "8080");

console.log(
  `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)
```

---
[AlertPix](alertpix.live)