InteractiveFrameworks

Rooms and namespaces

Send events to some clients only: a namespace that keeps the staff's channel apart from the public desk, rooms that clients join and leave, and the difference between telling a room and telling the rest of it.

What you'll learn

  • Serve a gateway in a namespace of its own with @WebSocketGateway({ namespace }), and connect a client to it
  • Know that @WebSocketServer() gives a namespaced gateway its Namespace, not the Server
  • Put a client in a room with join(), take it out with leave(), and read the rooms it is in
  • Send to a room with server.to(room).emit(), which includes the sender if it is in the room, or with client.to(room).emit(), which never does

Telling everyone works for a desk with a handful of visitors. The staff's side is different. A volunteer looking after kennel A wants to hear when A is fed, not about every kennel in the building, and the public desk should never see the staff's chatter at all. socket.io has two ways to narrow who hears what: namespaces, separate channels a client chooses when it connects, and rooms, groups inside a namespace that sockets join and leave as they go.

Namespaces

Every socket.io server has the root namespace, /, which is where a gateway with no options serves. namespace gives a gateway a namespace of its own:

@WebSocketGateway({ namespace: 'volunteers' })
export class VolunteersGateway {
  @WebSocketServer()
  server: Namespace;
}

A client picks the namespace in the URL it connects to, io('http://localhost:3000/volunteers'), and only that namespace's handlers see its messages. An event emitted in one namespace never reaches a client of another. Connecting to a namespace that no gateway serves is refused with connect_error and the message Invalid namespace.

Two gateways in the same app share one socket.io server, each in its own namespace, so @WebSocketServer() in a namespaced gateway gives that gateway's Namespace, not the Server. A Namespace has the same emit() and to(), scoped to it.

Rooms

A room is a name. A socket joins one with client.join('room'), leaves it with client.leave('room'), and is in as many as it likes. Rooms need no declaring: a room exists while a socket is in it. Every socket is also in one room from the start, named after its own id, which is how socket.io sends to a single socket.

@SubscribeMessage('shift:start')
async start(@MessageBody('ward') ward: string, @ConnectedSocket() client: Socket) {
  await client.join(`ward:${ward}`);
  return { wards: [...client.rooms].filter((room) => room.startsWith('ward:')) };
}

join() returns a promise with some socket.io adapters, the ones that keep rooms outside the process, so it is awaited. client.rooms is a Set, which JSON cannot carry, so spread it before returning it.

Sending to a room

to(room) narrows an emit(), and where you call it decides whether the sender is included:

this.server.to('ward:3').emit('ward:alarm', alarm); // everyone in the room
client.to('ward:3').emit('ward:chat', message);     // everyone in the room but the sender

server.to(room) reaches every socket in the room, so a sender who is in it hears its own message. client.to(room) is client.broadcast narrowed to a room: everyone there but the sender. Several rooms can be chained, to('a').to('b'), and a socket in both still receives the event once.

Pick by what the event means. A feeding is a fact everyone watching the kennel should see, including whoever did it, if they watch it. A note written by a watcher goes to the other watchers; its author knows what they wrote.

Your task

CatsGateway serves the public desk in the root namespace. Add the staff's channel beside it.

  1. KennelsGateway serves the kennels namespace.
  2. kennel:watch with { kennel } puts the client in that kennel's room, and acknowledges { watching: ['A'] }, every kennel it now watches. kennel:unwatch takes it out and acknowledges what remains.
  3. kennel:feed with { kennel, food } tells everyone watching that kennel kennel:fed with the feeding, and acknowledges { fed: 'A' }.
  4. kennel:note with { kennel, text } passes the note on as kennel:note to the kennel's other watchers. The author gets nothing back.

In the Socket mode, connect you with namespace /kennels, watch kennel A, and feed it from a second client.

When it fails

  • ada could not connect: Invalid namespace: no gateway serves /kennels, because the namespace option is missing; the kennels gateway is in the root namespace.
  • ada received nothing, expected [{"event":"kennel:fed", ...}]: the feeding went nowhere. The room names in join() and to() differ, or the client never joined.
  • ada received [{"event":"kennel:note", ...}], expected nothing: the note came back to its author, sent with server.to(room) instead of client.to(room).
  • acknowledged with {"watching":["8I-RP52h_z88rHs9AAAA","kennel:A"]}: the list is every room the socket is in, the one named after its own id included. Keep only the kennels' rooms, without their prefix.

Remember

  • A namespace is chosen when connecting; @WebSocketGateway({ namespace }) serves one, and its @WebSocketServer() is a Namespace.
  • Rooms are names sockets join() and leave(); every socket is also in a room named after its id.
  • server.to(room) reaches the whole room; client.to(room) reaches the room without the sender.
Stuck? Show a hint

@WebSocketGateway({ namespace: 'kennels' }) and @WebSocketServer() server: Namespace. watch: await client.join(`kennel:${kennel}`), then the rooms starting with kennel: from client.rooms. unwatch: await client.leave(...). feed: this.server.to(`kennel:${kennel}`).emit('kennel:fed', feeding). note: client.to(`kennel:${kennel}`).emit('kennel:note', note), returning nothing.