Socket Protocol
Define type-safe protocols for Socket communication
Creating a Protocol
Define message types using defineProtocol:
// app/sockets/chat/protocol.ts
import { defineProtocol } from "@ademattos/bunbox/client";
export const ChatProtocol = defineProtocol({
"chat-message": { text: "", username: "" },
"user-joined": { username: "" },
"user-left": { username: "" },
"typing": { isTyping: false, username: "" },
});
export type ChatProtocolType = typeof ChatProtocol;Using the Protocol
Import and use the protocol in your socket route:
// app/sockets/chat/route.ts
import type { SocketUser, SocketContext, SocketMessage } from "@ademattos/bunbox";
export function onJoin(user: SocketUser, ctx: SocketContext) {
console.log("Client connected");
ctx.broadcast("user-joined", {
username: user.data.username
});
}
export function onMessage(
user: SocketUser,
message: SocketMessage,
ctx: SocketContext
) {
switch (message.type) {
case "chat-message":
ctx.broadcast("chat-message", {
username: user.data.username,
text: message.data.text,
});
break;
case "typing":
ctx.broadcast("typing", {
username: user.data.username,
isTyping: message.data.isTyping,
});
break;
}
}
export function onLeave(user: SocketUser, ctx: SocketContext) {
ctx.broadcast("user-left", {
username: user.data.username
});
}Client-Side Usage
Use the protocol with useSocket:
import { useSocket } from "@ademattos/bunbox/client";
import { ChatProtocol } from "@/app/sockets/chat/protocol";
export default function Chat() {
const { subscribe, publish } = useSocket(
"/sockets/chat",
ChatProtocol,
{ username: "John" }
);
// Subscribe to typed messages
subscribe("chat-message", (msg) => {
// msg.data is typed as { text: string, username: string }
console.log(`${msg.data.username}: ${msg.data.text}`);
});
subscribe("user-joined", (msg) => {
// msg.data is typed as { username: string }
console.log(`${msg.data.username} joined`);
});
// Publish typed messages
const sendMessage = (text: string) => {
publish("chat-message", { text, username: "John" });
};
return <button onClick={() => sendMessage("Hello!")}>Send</button>;
}Type Safety Benefits
Compile-Time Checking
// ✅ Valid - matches protocol
publish("chat-message", { text: "Hi", username: "John" });
// ❌ Error - missing required field
publish("chat-message", { text: "Hi" });
// ❌ Error - invalid message type
publish("invalid-type", { foo: "bar" });
// ❌ Error - wrong data shape
publish("chat-message", { message: "Hi" });Autocomplete
Your IDE provides autocomplete for:
- Message types
- Message data fields
- Field types
Refactoring
Rename or change message types safely - TypeScript catches all references.
Message Structure
All socket messages follow this structure:
interface SocketMessage<T = unknown> {
type: string; // From your protocol
data: T; // Typed based on protocol
timestamp: number; // Auto-added by server
userId: string; // Sender's user ID
}Protocol Patterns
Simple Messages
export const SimpleProtocol = defineProtocol({
"ping": {},
"hello": { name: "" },
});Complex Data
export const GameProtocol = defineProtocol({
"player-move": {
playerId: "",
position: { x: 0, y: 0 },
velocity: { x: 0, y: 0 },
},
"player-attack": {
playerId: "",
targetId: "",
damage: 0,
},
"game-state": {
players: [] as Array<{ id: string; hp: number }>,
time: 0,
},
});Union Types
export const NotificationProtocol = defineProtocol({
"notification": {
type: "" as "info" | "warning" | "error",
title: "",
message: "",
},
});Advanced Example
// protocol.ts
import { defineProtocol } from "@ademattos/bunbox/client";
export const CollaborationProtocol = defineProtocol({
// Document editing
"doc-update": {
documentId: "",
userId: "",
changes: [] as Array<{
position: number;
deleted: number;
inserted: string;
}>,
},
// Cursor positions
"cursor-move": {
userId: "",
position: 0,
selection: { start: 0, end: 0 },
},
// User presence
"user-online": {
userId: "",
username: "",
color: "",
},
"user-offline": {
userId: "",
},
// Comments
"comment-add": {
id: "",
userId: "",
text: "",
position: 0,
},
});
export type CollaborationProtocolType = typeof CollaborationProtocol;Sharing Types
Export and reuse protocol types:
// protocol.ts
export const MyProtocol = defineProtocol({
"message": { text: "", username: "" },
});
export type MyProtocolType = typeof MyProtocol;
// Can be used in both client and server code
export type MessageData = MyProtocolType["message"];Protocol Validation
Protocols provide runtime validation:
// Invalid messages are caught
try {
publish("chat-message", { invalid: "data" });
} catch (error) {
console.error("Invalid message:", error);
}Multiple Protocols
Use different protocols for different sockets:
// app/sockets/chat/protocol.ts
export const ChatProtocol = defineProtocol({
"message": { text: "" },
});
// app/sockets/game/protocol.ts
export const GameProtocol = defineProtocol({
"move": { x: 0, y: 0 },
});Best Practices
Keep Messages Small
// ✅ Good - small focused messages
export const Protocol = defineProtocol({
"player-move": { x: 0, y: 0 },
"player-attack": { targetId: "" },
});
// ❌ Avoid - large nested objects
export const Protocol = defineProtocol({
"update": {
player: { /* many fields */ },
world: { /* many fields */ },
// ...
},
});Use Descriptive Names
// ✅ Good
"player-joined", "chat-message", "document-updated"
// ❌ Avoid
"msg", "update", "event"Version Your Protocols
export const ProtocolV1 = defineProtocol({
"message": { text: "" },
});
export const ProtocolV2 = defineProtocol({
"message": { text: "", timestamp: 0 },
});Testing Protocols
Test your socket handlers with typed messages:
import { describe, test, expect } from "bun:test";
import { onMessage } from "./route";
describe("Chat socket", () => {
test("broadcasts messages", () => {
const user = { id: "123", data: { username: "John" } };
const message = {
type: "chat-message",
data: { text: "Hello" },
timestamp: Date.now(),
userId: "123",
};
// Test your handler
onMessage(user, message, mockContext);
});
});