InteractiveFrameworks

The server and its clients

Make the desk feel live: lifecycle hooks for clients coming and going, the handshake's auth payload, the server behind the gateway, and the three ways to send an event: to one client, to everyone but the sender, to everyone.

What you'll learn

  • Run code when a client connects or disconnects with OnGatewayConnection and OnGatewayDisconnect, and when the server starts with OnGatewayInit
  • Read what a client sent in the handshake, socket.handshake.auth, before any message arrives
  • Get the socket.io Server with @WebSocketServer() and the sending client with @ConnectedSocket()
  • Choose between client.emit (that client), client.broadcast.emit (everyone else) and server.emit (everyone)

The first gateway only answered questions, and an HTTP API does that already. What a socket adds is the other direction: the server speaks first. When a visitor walks up to the live desk, the visitors already there should see them arrive. When someone adopts Tom, every screen should say so at once. That needs three things the first lesson did not have: knowing when clients come and go, knowing who they are, and a way to send to clients other than the one that asked.

Lifecycle hooks

A gateway can implement three interfaces from @nestjs/websockets, and Nest calls each method at its moment:

  • OnGatewayInit: afterInit(server) once, when the socket.io server exists, before any client connects.
  • OnGatewayConnection: handleConnection(client) for each client that connects.
  • OnGatewayDisconnect: handleDisconnect(client) for each client that leaves, by closing the tab, calling disconnect(), or losing its network.

The client is the socket.io Socket for that connection. Its id is unique per connection, which makes it a good key for anything the gateway remembers about a client, and it dies with the connection: a reconnecting client gets a new one.

The handshake

A socket.io client can send data while connecting, before any event: the handshake. Its auth option is meant for credentials and identity:

const socket = io('http://localhost:3000', { auth: { name: 'Ada' } });

On the server it is client.handshake.auth, available in handleConnection and in every handler for as long as the connection lasts. client.handshake also carries the request's headers and query.

Who receives an event

The Socket and the socket.io Server both have emit(event, data), and which one you call decides who receives it:

@WebSocketServer()
server: Server;

handleConnection(client: Socket) {
  client.emit('rota:today', this.rota.today());          // this client only
  client.broadcast.emit('rota:volunteer', this.nameOf(client)); // every client but this one
  this.server.emit('rota:volunteers', this.rota.count()); // every client
}

@WebSocketServer() puts the socket.io Server on a property of the gateway once it exists (it is undefined in the constructor). Inside a message handler, @ConnectedSocket() gives the socket of the client that sent the message, beside its @MessageBody():

@SubscribeMessage('rota:swap')
swap(@MessageBody() swap: Swap, @ConnectedSocket() client: Socket) {
  client.broadcast.emit('rota:swapped', swap);
  return { ok: true };
}

Choosing between them is the design decision. A client that just added a cat already has it in its acknowledgement, so the others are told with client.broadcast.emit and it is not told twice. An adoption is news for every screen, the adopter's included, so it goes through server.emit. Sending to everyone when you meant everyone else is the most common socket bug, and nothing crashes: one client simply sees everything twice.

Your task

Two cats, and a desk that tells everyone what happens.

  1. When a client connects, remember its name: the handshake's auth.name, or A visitor. Send that client desk:welcome with { visitors }, how many are connected, and send every other client desk:joined with the name.
  2. When a client disconnects, forget it, and send everyone still connected desk:left with its name.
  3. desk:visitors acknowledges the names of everyone connected.
  4. When a cat is added, every other client hears cats:added with it.
  5. When a cat is adopted, everyone hears cats:adopted with { id, name, by }, where by is the adopter's name. A refused adoption tells nobody.

Then use the Socket mode with two client names, you and friend, connected at once, and watch one hear what the other does.

When it fails

  • Cannot read properties of undefined (reading 'emit') in the console: the gateway calls this.server.emit and the property has no @WebSocketServer(), so it was never set.
  • ada received [... {"event":"cats:added", ...}], expected nothing: the sender heard its own addition, because server.emit was used where client.broadcast.emit was meant.
  • bob received [... "desk:joined" ...]: the newcomer was told about itself; the arrival went to everyone.
  • acknowledged with ["Ada","Bob"], expected ["Ada"]: handleDisconnect did not forget the client, or the class does not implement it and Nest never called it.

Remember

  • afterInit, handleConnection and handleDisconnect come from OnGatewayInit, OnGatewayConnection and OnGatewayDisconnect.
  • client.handshake.auth is what the client sent while connecting; client.id identifies the connection.
  • client.emit reaches that client, client.broadcast.emit everyone else, server.emit everyone.
  • @WebSocketServer() gives the gateway the server; @ConnectedSocket() gives a handler the sender.
Stuck? Show a hint

@WebSocketServer() server: Server; implement OnGatewayConnection and OnGatewayDisconnect. handleConnection(client: Socket): store client.handshake.auth.name ?? 'A visitor' by client.id, client.emit('desk:welcome', { visitors: size }), client.broadcast.emit('desk:joined', name). handleDisconnect: delete it and this.server.emit('desk:left', name). 'desk:visitors' returns the names. cats:add takes @ConnectedSocket() client and client.broadcast.emit('cats:added', created); cats:adopt uses this.server.emit('cats:adopted', { id, name, by }).