Skip to content

AdonisJs 5 websocket provider using socket.io under the hood

License

Notifications You must be signed in to change notification settings

Ruby184/adonis-socket.io

Repository files navigation

@ruby184/adonis-socket.io

AdonisJs 5 websocket provider using socket.io under the hood

github-actions-image npm-image license-image typescript-image

This package is trying to implement main ideas from this Adonis RFC. Package is not production ready until v1.0. Use it at your own risk.

Currently implemented features:

  • define socket.io namespaces including dynamic ones with adonis-like routes syntax
  • add adonis-like class middlewares similar to route ones to namespaces
  • handle events on namespaces with controllers and allow to ack events with value returned from event handler
  • centralized exception handling similar to http
  • working with adonis auth middleware
  • more to come...

Installation

Install it from npm

npm i @ruby184/adonis-socket.io

and then configure it using adonis

node ace configure @ruby184/adonis-socket.io

TODO

  • allow .where regex definition for namespace dynamic parameters
  • allow to define controller namespace for socket.io namespace
  • define static namespaces directly as socket.io namespaces and use matching only for dynamic ones (perf)
  • test everything
  • we should not create and use response, but return Proxy to intercept and throw error when user tries to use response in websocket context
  • extract errors handling to dedicated exception handler to report and handle
  • look at how to make easy integration of socket.io multi server support with adonis
  • look how we can make use of socket middleware which is a function that gets executed for every incoming Packet
  • handle transformaton of adonis cors config to socket.io as they are not 100% compatible

Usage

Examples

Here is an example of tracking users online status using this package inspired by default examples from socket.io

  1. Currently package supports authentication by api tokens. Update middleware created by @adonisjs/auth in app/Middleware/Auth.ts and add wsHandle method to support websockets
import type { WsContextContract } from '@ioc:Ruby184/Socket.IO/WsContext'
  /**
   * Handle ws namespace connection
   */
  public async wsHandle(
    { auth }: WsContextContract,
    next: () => Promise<void>,
    customGuards: (keyof GuardsList)[]
  ) {
    /**
     * Uses the user defined guards or the default guard mentioned in
     * the config file
     */
    const guards = customGuards.length ? customGuards : [auth.name]
    await this.authenticate(auth, guards)
    await next()
  }
  1. Update start/wsKernel.ts to add authentication middleware updated in previous step. We will add global middleware but you can also use named one and just add it to required nameespace in next step.
Ws.middleware.register([() => import('App/Middleware/Auth')])
  1. Add events listeners in start/socket.ts
Ws.namespace('/')
  .connected('ActivityController.onConnected')
  .disconnected('ActivityController.onDisconnected')
  1. Create a websocket controller in app/Controllers/Ws/ActivityController.ts
import type { WsContextContract } from '@ioc:Ruby184/Socket.IO/WsContext'
import User from 'App/Models/User'

export default class ActivityController {
  private getUserRoom(user: User): string {
    return `user:${user.id}`
  }

  public async onConnected({ socket, auth, logger }: WsContextContract) {
    // all connections for the same authenticated user will be in the room
    const room = this.getUserRoom(auth.user!)
    const userSockets = await socket.in(room).allSockets()

    // this is first connection for given user
    if (userSockets.size === 0) {
      socket.broadcast.emit('user:online', auth.user)
    }

    // add this socket to user room
    socket.join(room)
    // add userId to data shared between Socket.IO servers
    // https://socket.io/docs/v4/server-api/#namespacefetchsockets
    socket.data.userId = auth.user!.id

    const allSockets = await socket.nsp.except(room).fetchSockets()
    const onlineIds = new Set<number>()

    for (const remoteSocket of allSockets) {
      onlineIds.add(remoteSocket.data.userId)
    }

    const onlineUsers = await User.findMany([...onlineIds])

    socket.emit('user:list', onlineUsers)

    logger.info('user connected: %d', auth.user!.id)
  }

  // see https://socket.io/get-started/private-messaging-part-2/#disconnection-handler
  public async onDisconnected({ socket, auth, logger }: WsContextContract, reason: string) {
    const room = this.getUserRoom(auth.user!)
    const userSockets = await socket.in(room).allSockets()

    // user is disconnected
    if (userSockets.size === 0) {
      // notify other users
      socket.broadcast.emit('user:offline', auth.user)
    }

    logger.info('user disconnected (%s): %d', reason, auth.user!.id)
  }
}

About

AdonisJs 5 websocket provider using socket.io under the hood

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published