Broadcasting in AdonisJS
Tags: adonisjs, node, webdev, tutorial
Published: 2021-10-05 01:12:16

In this tutorial we'll build a broadcasting module for AdonisJS which resembles Laravel Broadcasting features (you can even use Laravel Echo). This module will cover up many of the websockets use cases in  a simple manner.

It is great for cases where the clients need to receive data in real-time, but don't need to send data in real-time.

Good  use cases are:
- Chats
- Live dashboards
- Sport scores

Bad use cases are:
- Games
- Work together  platforms

## Let's build it!

### Scaffolding a new app
Create a new AdonisJS project
```bash
$ npm init create-adonis-ts-app broadcasting
$ yarn create adonis-ts-app broadcasting
```

When prompted which project structure, select web and flag
> Configure webpack for compiling frontend assets? true

### Setting up our Broadcast server
Our broadcast module will be based in an open-source Pusher compatible server called [pWS](https://pws.soketi.app).


First, we will install it
```bash
$ npm i @soketi/pws
$ yarn add @soketi/pws
```

We can start the server by running
```bash
$ npm pws-server start
$ yarn pws-server start
```

But we need to configure it before running, so we will make a configuration file for it in <code>config/broadcasting.ts</code>

```typescript
// config/broadcasting.ts
import Env from '@ioc:Adonis/Core/Env'

const broadcastingConfig = {
  port: Env.get('BROADCASTING_PORT', 6001),
  appId: Env.get('BROADCASTING_APP_ID', 'app-id'),
  appKey: Env.get('BROADCASTING_APP_KEY', 'app-key'),
  appSecret: Env.get('BROADCASTING_APP_KEY', 'app-secret'),
}

export default broadcastingConfig
```

The configs  won't get magically loaded into pWS, so we will make a command to start it. To start it we will use [execa](https://github.com/sindresorhus/execa). So install it using:

```bash
$ npm i execa
$ yarn add execa
```

and create a command with

```bash
$ node ace make:command StartPws
```

The command will look like this:
```typescript
// commands/StartPws.ts
import { BaseCommand } from '@adonisjs/core/build/standalone'
import execa from 'execa'

export default class StartPws extends BaseCommand {
  public static commandName = 'start:pws'
  public static description = 'Start the pWS server with Adonis Configs'
  public static settings = {
    loadApp: true,
    stayAlive: true,
  }

  public async run() {
    const broadcastingConfig = this.application.config.get('broadcasting')
    const command = `
      PORT=${broadcastingConfig.port}
      DEFAULT_APP_ID=${broadcastingConfig.appId}
      DEFAULT_APP_KEY=${broadcastingConfig.appKey}
      DEFAULT_APP_SECRET=${broadcastingConfig.appSecret}
      yarn pws-server start`
    await execa(command, { shell: true }).stdout?.pipe(process.stdout)
  }
}
```

After creating the command, we need to regenerate the ace manifest, so it catches our new command, do it by running:

```bash
$ node ace generate:manifest
```

Then you can run it with
```bash
$ node ace start:pws
```

### Broadcasting events

As pWS is a drop-in Pusher replacement, we can interact with it using any Pusher client, as AdonisJS is a node framework, we will use the node Pusher client. Start by installing the node Pusher client:

```bash
$ npm i pusher
$ yarn add pusher
```

Then we will create a service to interact with the pWS server, it can be done as a simple service or as a AdonisJS provider, in this tutorial we will go the service way.

```typescript 
// app/Services/Broadcast.ts
import Pusher from 'pusher'
import broadcastingConfig from 'Config/broadcasting'
import Env from '@ioc:Adonis/Core/Env'

class Broadcast {
  private pusher = new Pusher({
    host: Env.get('HOST', 'localhost'),
    port: broadcastingConfig.port,
    appId: broadcastingConfig.appId,
    key: broadcastingConfig.appKey,
    secret: broadcastingConfig.appSecret,
  })

  public async broadcast(channel: string | string[], event: string, data: any) {
    const response = await this.pusher.trigger(channel, event, data)
    return response
  }
}

export default new Broadcast()
```

With this service we can broadcast events by simply using

```typescript
import Broadcast from 'App/Services/Broadcast'

await Broadcast.broadcast('test-channel', 'event', 'data')
```

### Listening to events

To listen to events in our frontend we can use PusherJS paired with Laravel Echo. Start by installing both:
```bash
$ npm i -D laravel-echo pusher-js
$ yarn add -D laravel-echo pusher-js
```

Set them up in our frontend:
```javascript
// resources/js/app.js
import '../css/app.css'
import Echo from 'laravel-echo'

window.Pusher = require('pusher-js')
window.Echo = new Echo({
  broadcaster: 'pusher',
  wsHost: 'localhost',
  wsPort: 6001,
  forceTLS: false,
  disableStats: true,
  key: 'app-key',
  namespace: '',
})
```

### Example setup
Append this to the end of <code>resources/js/app.js</code>
```
// resources/js/app.js
window.Echo.channel('messages').listen('message', (e) => {
  alert(JSON.stringify(e))
})
```

Paste this into the welcome view (<code>resources/views/welcome.edge</code>)
```html
<!-- resources/views/welcome.edge -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>AdonisJS - A fully featured web framework for Node.js</title>
    @entryPointStyles('app')
    @entryPointScripts('app')
  </head>
  <body>
    <main>
      <div>
        <!-- Just to show off how it works. You can safely ignore that -->
        <form method="POST" action="/message">
          <input name="message" type="text" />
          <button>Send Message</button>
        </form>
      </div>
    </main>
  </body>
</html>
```

After setting up that, we just need to setup our message route to broadcast a message event:
```typescript
// start/routes.ts
import Route from '@ioc:Adonis/Core/Route'
import Broadcast from 'App/Services/Broadcast'

Route.get('/', async ({ view }) => {
  return view.render('welcome')
})

Route.post('/message', async ({ request, response }) => {
  const message = request.input('message')
  await Broadcast.broadcast('messages', 'message', { message })
  return response.redirect().back()
})
```

### It's alive!

But it still doesn't works for private or presence channels, we will address that in next tutorial, stay tuned!