@ruby184/adonis-socket.io
v0.2.0
Published
AdonisJs 5 websocket provider using socket.io under the hood
Downloads
584
Maintainers
Readme
@ruby184/adonis-socket.io
AdonisJs 5 websocket provider using socket.io under the hood
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
- [x] allow
.where
regex definition for namespace dynamic parameters - [x] allow to define controller namespace for socket.io namespace
- [x] 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
- [x] 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
- Currently package supports authentication by api tokens.
Update middleware created by
@adonisjs/auth
inapp/Middleware/Auth.ts
and addwsHandle
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()
}
- 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')])
- Add events listeners in
start/socket.ts
Ws.namespace('/')
.connected('ActivityController.onConnected')
.disconnected('ActivityController.onDisconnected')
- 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)
}
}