DEV Community

Christopher Ribeiro for AlertPix

Posted on

Going real time with donation alerts

A streamer doing a live code and a donation alert at the bottom left corner of the screenAt AlertPix 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, the fastest Bun framework.

Now, to create a new http server is just as simple as:

const app = new Elysia()
Enter fullscreen mode Exit fullscreen mode

Now we need to allow for widget to connect via WebSockets:

app
  .use(ws())
  .ws('/ws', {
    body: t.Object({ ... }),
    query: t.Object({ ... }),
    open(ws) {},
    close(ws) {},
    message(ws, message) {},
  })
Enter fullscreen mode Exit fullscreen mode

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:

open(ws) {
  // ...
  ws.subscribe(id)
  free[id] = true
}
Enter fullscreen mode Exit fullscreen mode

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:

close(ws) {
  // ...
  delete free[id]
}
Enter fullscreen mode Exit fullscreen mode

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:

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

Posting a donation

We updated our API to store the notification in the queue and publish to the notifying service via an API call:

export function donationReceived(context: Context, donation: Donation) {
  // ...

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

  // ...
}
Enter fullscreen mode Exit fullscreen mode

API call that we defined with the same logic to notify the connected peers if they are free:

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

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 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:

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

AlertPix

Top comments (0)