@plinthjs/reverb
v0.1.0
Published
Mason WebSocket server (the Laravel Reverb equivalent).
Maintainers
Readme
@plinthjs/reverb
A dependency-free WebSocket server for Mason, the Laravel Reverb equivalent. It performs the RFC
6455 handshake and framing itself on top of node:http, and speaks the Pusher channel protocol
(pusher:subscribe, pusher:ping, client-* whispers), so Pusher-compatible clients such as
Laravel Echo can connect to it. A ChannelManager tracks which connections are subscribed to which
channels, and broadcast() pushes server events to every subscriber.
Install
npm install @plinthjs/reverbUsage
import { createServer } from 'node:http'
import { ReverbServer } from '@plinthjs/reverb'
const reverb = new ReverbServer({
// Optional: gate private/presence channels. Return false to reject with pusher:error 4009.
authorizeSubscription: (connection, channel, auth) =>
!channel.startsWith('private-') || verifySignature(connection.id, channel, auth),
})
const http = createServer()
reverb.attach(http) // handles 'upgrade' requests; bad handshakes are destroyed
http.listen(8080)
// From anywhere in the app: deliver an event to every subscriber of a channel.
const delivered = reverb.broadcast('orders', 'OrderShipped', { id: 42 }) // number of recipientsWebSocketServer is an alias of ReverbServer.
Protocol
On connect each client receives pusher:connection_established with its socket_id. The server
then handles:
pusher:subscribe/pusher:unsubscribewith{ channel, auth? }; a successful subscription is acknowledged withpusher_internal:subscription_succeeded.pusher:ping, answered withpusher:pong. WebSocket ping frames are answered with pong frames.client-*events on a channel the sender has joined, relayed to every other member.
Unknown events and malformed JSON are ignored. Closing the socket unsubscribes the connection from every channel.
Channels and connections
reverb.channels.count('orders') // subscriber count
reverb.channels.members('orders') // Set<Connection> (a copy)
reverb.channels.channelNames() // every channel with at least one member
reverb.connections.get(socketId) // live connections keyed by socket id
for (const connection of reverb.channels.members('orders')) {
connection.sendJson({ event: 'Ping', channel: 'orders', data: {} })
connection.channels // the channels this connection has joined
}A Connection wraps a socket with send(text), sendJson(payload), sendBinary(data), ping(),
pong(payload?) and close(code?, reason?).
Low-level pieces
import { computeAcceptKey, decodeFrames, encodeFrame, OPCODES, upgrade } from '@plinthjs/reverb'
computeAcceptKey('dGhlIHNhbXBsZSBub25jZQ==') // 's3pPLMBiTxaQ9kYGzzhZRbK+xOo='
const frame = encodeFrame(OPCODES.text, 'hello') // unmasked server-to-client frame
const { frames, rest } = decodeFrames(buffer) // complete frames plus leftover bytesupgrade(req, socket) completes a handshake on a raw upgrade socket (throwing HandshakeError
on an invalid request), closeFrame, pingFrame and pongFrame build control frames, and
reverb.handleConnection(socket) adopts an already-upgraded socket, which is handy in tests.
