How to maintain WebSocket client connections in Durable Object?

1) Would it be the correct understanding that in order to use the Durable Objects Hibernation API, I need to extends the DurableObject class? 2) If so, how can I delete a certain clientId on close? In the examples, it doesn't extend the DurableObject class so the handleWebsocketSession method is able to set listeners to delete clientConnectionIds. However, when extending the durableObject class, the webSocketClose and webSocketError methods handle the closing of the client, so I'm not able to refer to the same clientId anymore.. See code below:
2 Replies
Paul
PaulOP6mo ago
When extending the DurableObject class
import { Environment } from "@/src/types";
import { DurableObject } from "cloudflare:workers";

interface Client {
websocket: WebSocket;
id: string;
}

export class WebSocketHibernationServer extends DurableObject {
clients: Map<string, Client>;

constructor(ctx: DurableObjectState, env: Environment) {
super(ctx, env);
this.clients = new Map();
}

async fetch(request: Request): Promise<Response> {
let pair = new WebSocketPair();
const [client, server] = Object.values(pair);
this.ctx.acceptWebSocket(server);
await this.handleWebSocketSession(server);
return new Response(null, { status: 101, webSocket: client });
}

async handleWebSocketSession(webSocket: WebSocket) {
// Create our session and add it to the users map.
const clientId = crypto.randomUUID();
this.clients.set(clientId, {
id: clientId,
websocket: webSocket,
});

// I previously added event listeners here but they don't get triggered
// Previously I was able to delete the clientId from the map on close here
}

async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) {
this.broadcast(
`[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`
);
}

async webSocketClose(ws: WebSocket, code: number, reason: string) {
ws.close(code, reason);
}

broadcast(message: string) {
// Iterate over all the sessions sending them messages.
this.clients.forEach((client, key) => {
try {
client.websocket.send(message);
} catch (err) {
this.clients.delete(key);
}
});
}
import { Environment } from "@/src/types";
import { DurableObject } from "cloudflare:workers";

interface Client {
websocket: WebSocket;
id: string;
}

export class WebSocketHibernationServer extends DurableObject {
clients: Map<string, Client>;

constructor(ctx: DurableObjectState, env: Environment) {
super(ctx, env);
this.clients = new Map();
}

async fetch(request: Request): Promise<Response> {
let pair = new WebSocketPair();
const [client, server] = Object.values(pair);
this.ctx.acceptWebSocket(server);
await this.handleWebSocketSession(server);
return new Response(null, { status: 101, webSocket: client });
}

async handleWebSocketSession(webSocket: WebSocket) {
// Create our session and add it to the users map.
const clientId = crypto.randomUUID();
this.clients.set(clientId, {
id: clientId,
websocket: webSocket,
});

// I previously added event listeners here but they don't get triggered
// Previously I was able to delete the clientId from the map on close here
}

async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) {
this.broadcast(
`[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`
);
}

async webSocketClose(ws: WebSocket, code: number, reason: string) {
ws.close(code, reason);
}

broadcast(message: string) {
// Iterate over all the sessions sending them messages.
this.clients.forEach((client, key) => {
try {
client.websocket.send(message);
} catch (err) {
this.clients.delete(key);
}
});
}
Previously, I was able to do...
async handleWebSocketSession(webSocket: WebSocket) {
// ...
let closeOrErrorHandler = () => {
console.log("client disconnected", clientId);
this.clients.delete(clientId);
};
webSocket.addEventListener("close", closeOrErrorHandler);
webSocket.addEventListener("error", closeOrErrorHandler);
}
async handleWebSocketSession(webSocket: WebSocket) {
// ...
let closeOrErrorHandler = () => {
console.log("client disconnected", clientId);
this.clients.delete(clientId);
};
webSocket.addEventListener("close", closeOrErrorHandler);
webSocket.addEventListener("error", closeOrErrorHandler);
}
Or fundamentally, how can I restructure this whole thing so that I'm able to broadcast to all clients but delete the clients when they disconnect? I'm able to do it when NOT extending the "DurableObject" class, but then if I don't extend it, I don't think I'm able to use the Hibernation API via this.ctx.acceptWebsocket(server)
Paul
PaulOP6mo ago
As per Skye, the answer is below: You'd probably want to use the tags option when accepting the websocket found here, alongside the getTags and getWebsockets method, rather than maintaining your own state, as that won't persist across hibernations
Cloudflare Docs
WebSockets · Cloudflare Durable Objects docs
WebSockets are long-lived TCP connections that enable bi-directional, real-time communication between client and server.
Want results from more Discord servers?
Add your server