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.
KennelsGatewayserves thekennelsnamespace.kennel:watchwith{ kennel }puts the client in that kennel's room, and acknowledges{ watching: ['A'] }, every kennel it now watches.kennel:unwatchtakes it out and acknowledges what remains.kennel:feedwith{ kennel, food }tells everyone watching that kennelkennel:fedwith the feeding, and acknowledges{ fed: 'A' }.kennel:notewith{ kennel, text }passes the note on askennel:noteto 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 thenamespaceoption is missing; the kennels gateway is in the root namespace.ada received nothing, expected [{"event":"kennel:fed", ...}]: the feeding went nowhere. The room names injoin()andto()differ, or the client never joined.ada received [{"event":"kennel:note", ...}], expected nothing: the note came back to its author, sent withserver.to(room)instead ofclient.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 aNamespace. - Rooms are names sockets
join()andleave(); 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.