Support AdonisJs development by becoming a patron
Support AdonisJs

WebSocket

WebSocket Provider makes it super easy to build real-time applications in AdonisJs. It comes with out of the box support for authentication, channels and rooms management.

About Ws Provider

  1. You are supposed to define channels to receive incoming WebSocket connections and same is done inside app/Ws/socket.js file.

  2. All incoming connections can be authenticated using the auth middleware.

  3. All web sockets actions supports ES2015 generators. For example:

    Ws.channel('/chat', function (socket) {
      socket.on('message', function * (payload) {
      })
    })
  4. You can attach controllers to your channels, the same as with routes.

    Ws.channel('/chat', 'ChatController')

Setup

Feel free to skip the setup process if you are using AdonisJs 3.2 or later. Since Web Sockets are pre-configured.
Install via npm
npm i --save adonis-websocket

Next, we need to register the provider and setup alias inside bootstrap/app.js file.

bootstrap/app.js
const providers = [
  'adonis-websocket/providers/WsProvider'
]

const aliases = {
  Ws: 'Adonis/Addons/Ws'
}

Next, we need to create required directories and files to organize our code.

mkdir -p app/Ws/Controllers
touch app/Ws/socket.js
touch app/Ws/kernel.js
  1. The socket.js file is used to define websocket channels, you can think of it as the Http routes file.

    app/Ws/socket.js
    const Ws = use('Ws')
    
    Ws.channel('/chat', function (socket) {
      // here you go
    })
  2. The kernel.js file is used to define global and named middleware just like your Http middleware but for Websocket connections.

    app/Ws/kernel.js
    const Ws = use('Ws')
    
    const globalMiddleware = [
      'Adonis/Middleware/AuthInit'
    ]
    
    const namedMiddleware = {
      auth: 'Adonis/Middleware/Auth'
    }
    
    Ws.global(globalMiddleware)
    Ws.named(namedMiddleware)

Finally, we need to load socket.js and kernel.js file when booting the Http server and same can be done inside bootstrap/http.js file.

bootstrap/http.js
use(Helpers.makeNameSpace('Ws', 'kernel'))
use(Helpers.makeNameSpace('Ws', 'socket'))

Basic Example

Check out the following video showing how to exchange messages between client and the server.

Channels

Channels make it easier to distribute logic around exposing web socket endpoints. For each channel, you can follow different authentication process or bind different middleware to it.

Adonisjs makes use of multiplexing instead of creating a different connection for each channel.
app/Ws/socket.js
'use strict'

const Ws = use('Ws')

Ws.channel('chat', function (socket, request, presence) {
  socket.on('news', function (message) {

  })
})

The above closure will be executed every time a new socket will join the chat channel and receives following.

socket

User socket instance to emit and listen for events.

request

Instance of request instantiated at the time of handshake.

presence

A special presence instance to track the socket. Read more on presence.

Controllers

Apart from closures, you can also bind controllers to channels. All controllers are stored inside app/Ws/Controllers directory and can be referenced the same way as Route controllers.

Ws.channel('chat', 'ChatController')

Now controllers can listen for new events just by creating appropriate methods on it.

app/Ws/Controllers/ChatController.js
'use strict'

class ChatController {

  constructor (socket) {
    this.socket = socket
  }

  onMessage (message) {
    // listening for message event
  }

}

The onMessage method will be invoked every time message event will be fired from the client. Also, you can make your listeners a generator method for doing async operations.

onMessage (message) {

}

// CAN BE

* onMessage (message) {
  const savedMessage = yield Message.create({ body: message })
}

All events listeners must start with on and the camel case representation of the event name. For example new:user will invoke onNewUser method on the controller.

Event Name Controller Method

message

onMessage

new:user

onNewUser

user:left

onUserLeft

Rooms

Rooms make it easier to build multi-room chat systems. For example, Slack has public rooms that anyone can join and leave, whereas private rooms need further authorization.

In the same manner, AdonisJs gives you hooks to authorize a socket before it can listen for events inside a room.

Joining A Room

The joinRoom method on the channel controller has invoked automatically every time a socket tries to join a room. You can make use of this method to authorize the join action or deny it by throwing an exception.

Server

app/Ws/socket.js
const Ws = use('Ws')

Ws
.channel('chat', 'ChatController')
.middleware('auth')
app/Ws/Controllers/ChatController.js
'use strict'

class ChatController {
  constructor (socket) {
    this.socket = socket
  }

  * joinRoom (room) {
    const user = this.socket.currentUser
    // throw error to deny a socket from joining room
  }
}

Client

const io = ws('')
const client = io.channel('chat').connect()

client.joinRoom('lobby', {}, function (error, joined) {
  // status
})

Emitting Messages To A Room

Once a socket has joined a room, it can listen for messages.

Server

this.socket.inRoom('lobby').emit('message', 'Hello world')

Client

client.on('message', function (room, message) {
})

Leaving A Room

In order to leave a room, the client can call leaveRoom method.

Server

app/Ws/Controllers/ChatController.js
'use strict'

class ChatController {
  constructor (socket) {
    this.socket = socket
  }

  * leaveRoom (room) {
    // Do cleanup if required
  }

  * joinRoom (room) {
    const user = this.socket.currentUser
    // throw error to deny a socket from joining room
  }
}

Client

const io = ws('')
const client = io.channel('chat').connect()
client.leaveRoom('lobby', {}, function (error, left) {
  // status
})

Presence

The presence feature allows you to track sockets for a given user. It is helpful in showing the list of online users and number of devices they are online from. Also when a user logouts, you can disconnect all of their related sockets to make sure they do not receive any realtime messages.

Checkout this video to understand the presence in-depth.

Presence Methods

Below is the list of presence methods.

track(socket, userId, [meta])

The track method allows you to track a socket for a given user using their userId. Optionally you can pass meta data too.

class ChatController {

  constructor (socket, request, presence) {
    presence.track(socket, socket.currentUser.id, {
      device: 'chrome'
    })
  }

}

pull(userId, callback)

Pull a list of sockets from the presence list for a given user. Pulled sockets will not be tracked anymore.

const Ws = use('Ws')
const chatChannel = Ws.channel('chat')
const chromeOnlySockets = chatChannel.presence.pull(userId, function (payload) {
  return payload.meta.device === 'chrome'
})

// disconnect user sockets from chrome
chromeOnlySockets.forEach((payload) => {
  payload.socket.disconnect()
})

Socket Methods

Below is the list of methods you can call from the socket instance.

on(event, callback)

Listen for an event.

socket.on('greet', function (greeting) {

})

once(event, callback)

Listen for an event only once.

socket.once('greet', function (greeting) {

})

emit(event, …​properties)

Emit an event.

socket.emit('greet', 'Hello world')

toEveryone()

Emit a message to everyone including the originating socket itself.

socket.toEveryone().emit('greet', 'Hello world')

toMe()

Emit a message to the originating socket only.

socket.toMe().emit('greet', 'Hello world')

exceptMe()

Emit a message to everyone except the originating socket.

socket.exceptMe().emit('user:join', 'User joined!')

to(ids)

Emit a message to specific socket ids only.

socket.to([]).emit('greet', 'Hello world')

inRoom(room)

Emit a message a given room.

socket.inRoom('lobby').emit('greet', 'Hello world')

inRooms(rooms)

Emit a message to multiple rooms.

socket.inRoom(['lobby', 'watercooler']).emit('greet', 'Hello world')

disconnect

Disconnect a socket from receiving/sending messages.

socket.disconnect()

Channel Methods

Below is the list of methods can be used on the channel instance.

middleware(…​middleware)

Apply an array of middleware on a given channel. Make sure to define middleware inside app/Ws/kernel.js file.

Ws
  .channel('chat')
  .middleware('auth')

// OR

Ws
  .channel('chat')
  .middleware('auth:jwt')

emit(event, …​properties)

Emit a message to all the sockets connected to a given channel.

const chatChannel = Ws.channel('chat')
chatChannel.emit('message', 'Hello world')

inRoom(room)

Emit a message a given room.

const chatChannel = Ws.channel('chat')
chatChannel.inRoom('lobby').emit('message', 'Hello world')

inRooms(rooms)

Emit a message to all given rooms.

const chatChannel = Ws.channel('chat')
chatChannel.inRooms(['lobby', 'watercooler']).emit('message', 'Hello world')

to(ids)

Emit a message to specific socket ids only.

const chatChannel = Ws.channel('chat')
chatChannel.to([]).emit('greet', 'Hello world')

get(socketId)

Get socket instance using the socket id.

const chatChannel = Ws.channel('chat')
const socket = chatChannel.get(socketId)

WebSocket Client

The client library to be used with browser-based web apps can be installed as Common Js module from npm, AMD module from bower or you can reference it from a CDN.

CommonJs Usage

After installation, you can require the module just like any other npm module.

npm i --save adonis-websocket-client
const ws = require('adonis-websocket-client')
const io = ws('http://localhost:3333', {})

AMD Usage

First, install the package from bower.

bower i --save adonis-websocket-client
requirejs(['adonis-websocket-client'], function (ws) {
  const io = ws('http://localhost:3333', {})
})

CDN Usage

The CDN script file will create a ws global.

<script src="https://unpkg.com/adonis-websocket-client/dist/ws.min.js"></script>
<script>
  const io = ws('http://localhost:3333', {})
</script>

Client Channel Methods

Below is the list of methods you can call using the client SDK.

connect(callback)

Connect to a given channel.

const client = io.channel('chat')
client.connect(function (error, connected) {
  if (error) {
    // do something
    return
  }
  // all good
})

emit(event, …​properties)

Emit an event.

client.emit('message', 'Hello world')

on(event, callback)

Listen for an event.

client.on('message', function (message) {
})

once(event, callback)

Listen for an event only once.

client.once('message', function (message) {
})

joinRoom(room, payload, callback)

Notify server to join a room and send optional data object as payload.

client.joinRoom('lobby', {}, function (error, joined) {
})

leaveRoom(room, payload, callback)

Leave a room.

client.leaveRoom('lobby', {}, function (error, left) {
})

withBasicAuth(username, password)

Connect to the channel by passing the username and password to be used for basic auth.

client
  .withBasicAuth('foo', 'secret')
  .connect(function () {
  })

withJwt(token)

Connect to the channel by passing JWT token to be used for authentication.

client
  .withJwt('token')
  .connect(function () {
  })

withApiKey(token)

Connect to the channel by passing personal API token to be used for authentication.

client
  .withApiKey('personal_token')
  .connect(function () {
  })