Connection Lifecycle
Understanding the connection lifecycle on both server and client.
Server Side
Actor starts / wakes from hibernation
↓
onInit() called
↓
├─ Restore sessions from attachments
└─ Initialize persisted state (if defined)
↓
WebSocket connects
↓
extractMeta(request) → { userId, clientId, channels }
↓
storeAttachment(ws, meta)
↓
sessions.set(ws, { ws, meta })
↓
onConnect(ctx) → ctx.emit available, ctx.actor.state ready
↓
[connection active, messages flow]
↓
Event handlers (connection.on())
↓
State changes automatically persisted (if persistedKeys defined)
↓
WebSocket closes
↓
sessions.delete(ws)
↓
onDisconnect(ctx) → ctx.emit available
↓
Actor may hibernate (state persists)
↓
Actor destroyed (evicted or explicit)
↓
onDestroy(ctx) → cleanup, leave rooms, close sockets
Lifecycle Hooks with Socket.io-like API
onConnect - Called when a connection is established:
const connection = defineConnection({
onConnect(ctx) {
// ctx.emit is available here
ctx.emit("welcome", { message: "Connected!" });
}
});
Event Handlers - Handle incoming messages (recommended):
connection.on("chat.message", (ctx, data) => {
// ctx.emit is available here
ctx.emit.toRoom("chat").emit("chat.message", {
from: ctx.meta.userId,
text: data.text
});
});
onDisconnect - Called when a connection closes:
const connection = defineConnection({
onDisconnect(ctx) {
// ctx.emit is available here
ctx.emit.toRoom("chat").emit("user.left", {
userId: ctx.meta.userId
});
}
});
onDestroy - Called when the Actor is destroyed (evicted or explicitly):
const connection = defineConnection({
onDestroy(ctx) {
// Cleanup: notify other services, flush analytics, etc.
// Room leave and WebSocket close are handled automatically
}
});
ConnectionDOs automatically leave all rooms and close the WebSocket before calling onDestroy. Use this hook for external cleanup (analytics, third-party APIs, etc.).
RoomDOs also support onDestroy:
createRoomHandler({
name: "ChatRoom",
connectionBinding: "UserConnection",
onDestroy(ctx) {
// Room-level cleanup
}
});
Sending Messages: Verani provides emit APIs for sending messages:
ctx.emit("event", data)- Send to current socketctx.emit.toRoom(roomName).emit("event", data)- Broadcast to room via RPCctx.emit.toUser(userId).emit("event", data)- Send to specific user via RPC
Client Side
new VeraniClient(url)
↓
State: "connecting"
↓
WebSocket opens
↓
State: "connected"
↓
[connection active, messages flow]
↓
WebSocket closes (unexpected)
↓
State: "reconnecting"
↓
Exponential backoff delay
↓
Retry connection
State Initialization
If your room defines persisted state, it's initialized during onInit:
const connection = defineConnection({
state: {
count: 0,
lastActivity: null as Date | null
},
persistedKeys: ["count", "lastActivity"],
onConnect(ctx) {
// State is ready here - loaded from storage
console.log(`Current count: ${ctx.state.count}`);
// Changes are automatically persisted
ctx.state.count++;
}
});
Timeline:
onInit()- State loaded from Durable Object storageonConnect()- State is ready and accessible- State changes - Automatically persisted to storage
- Hibernation - State survives in storage
- Wake - State restored in
onInit() onDestroy()- Final cleanup before Actor is evicted
Related Documentation
- Architecture - System architecture
- Hibernation - Hibernation behavior
- Persistence - State persistence guide
- State Management - State types overview
- Server API - Lifecycle Hooks - Hook documentation
- Client API - Client lifecycle methods