--- url: /dialogue/guide/start/getting-started.md --- # Getting Started with Dialogue This guide covers installation, basic setup, and your first real-time application using Dialogue. ## 1. Overview Dialogue is an event-based realtime communication library built on Socket.IO, Hono, and Bun. It provides a simple, type-safe API for building real-time applications like chat, notifications, live dashboards, multiplayer games, and IoT systems. ### 1.1 Key Features - **Config-first with dynamic rooms**: Define common rooms upfront, create others at runtime - **Event-centric**: Events are first-class citizens with optional Zod schema validation - **Type-safe**: Full TypeScript support with inferred types from schemas - **Bounded rooms**: Optional `maxSize` for predictable scaling - **Unified mental model**: Backend and frontend share similar APIs ## 2. Installation Install Dialogue and zod: ```bash bun add dialogue-ts zod ``` Dialogue requires Bun runtime for the backend server. ## 3. Quick Start ### 3.1 Define Events Create a configuration file to define your events and rooms: ```typescript // dialogue.config.ts import { createDialogue, defineEvent } from "dialogue-ts"; import { z } from "zod"; // Define events with optional schema validation export const Message = defineEvent("message", { schema: z.object({ text: z.string().min(1).max(1000), senderId: z.string(), }), description: "Chat message sent by a user", }); export const Typing = defineEvent("typing", { schema: z.object({ isTyping: z.boolean(), }), }); // Create the dialogue instance export const dialogue = createDialogue({ port: 3000, rooms: { chat: { name: "Chat Room", events: [Message, Typing], defaultSubscriptions: ["message"], }, }, hooks: { clients: { onConnected: (client) => { console.log(`Client connected: ${client.userId}`); client.join("chat"); }, }, }, }); ``` ### Dynamic Room Creation While config-first is recommended, you can also create rooms dynamically at runtime: ```typescript // Create room on-demand dialogue.createRoom({ id: `game-${gameId}`, name: 'Game Session', events: [ { name: 'move', schema: z.object({ x: z.number(), y: z.number() }) }, { name: 'chat', schema: z.object({ message: z.string() }) } ] }); ``` **Recommendation:** Use predefined rooms for ~80% of your use cases (known room types), and dynamic creation for ~20% (user-generated content, temporary sessions). ### 3.2 Start the Server ```typescript // server.ts import { dialogue } from "./dialogue.config"; await dialogue.start(); // Server running on http://localhost:3000 ``` Run with Bun: ```bash bun run server.ts ``` ### 3.3 Trigger Events from Backend You can trigger events from anywhere in your backend code: ```typescript import { dialogue, Message } from "./dialogue.config"; // From an API route, webhook, or background job dialogue.trigger("chat", Message, { text: "Welcome to the chat!", senderId: "system", }); ``` ### 3.4 Connect from Frontend ```typescript import { createDialogueClient } from "dialogue-ts/client"; const client = createDialogueClient({ url: "ws://localhost:3000", auth: { userId: "user-123" }, }); await client.connect(); // Join a room const chat = await client.join("chat"); // Listen for events chat.on("message", (msg) => { console.log(`${msg.from}: ${msg.data.text}`); }); // Send events chat.trigger("message", { text: "Hello, everyone!", senderId: "user-123", }); ``` ## 4. Project Structure Here's the recommended structure for a Dialogue application: ``` my-app/ ├── server/ │ ├── index.ts # Dialogue server setup │ └── rooms.ts # Room configurations ├── client/ │ ├── App.tsx # React app (or your framework) │ └── useDialogue.ts # Client hooks └── package.json ``` This is a minimal, focused view showing only application code. The Dialogue library itself is installed as dependencies and doesn't appear in your project structure. ## 5. Next Steps - Read the [Configuration Guide](/guide/api/configuration) for detailed configuration options - Explore the [Backend API](/guide/api/backend-api) for server-side features - Learn about the [Client API](/guide/api/client-api) for frontend integration - See [Examples](/guide/examples/chat-application) for complete use-case implementations *This documentation reflects the current implementation and is subject to evolution. Contributions and feedback are welcome.* --- url: /dialogue/guide/examples/chat-application.md --- # Chat Application A basic chat application with messages, typing indicators, and user presence. ## Backend Configuration ```typescript // dialogue.config.ts import { createDialogue, defineEvent } from "./dialogue"; import { z } from "zod"; // Define events export const Message = defineEvent("message", { schema: z.object({ text: z.string().min(1).max(2000), senderId: z.string(), replyTo: z.string().optional(), }), }); export const Typing = defineEvent("typing", { schema: z.object({ isTyping: z.boolean(), }), }); export const UserJoined = defineEvent("user:joined", { schema: z.object({ userId: z.string(), username: z.string(), }), }); export const UserLeft = defineEvent("user:left", { schema: z.object({ userId: z.string(), }), }); export const dialogue = createDialogue({ port: 3000, rooms: { general: { name: "General Chat", description: "Public chat room for everyone", events: [Message, Typing, UserJoined, UserLeft], defaultSubscriptions: ["message", "user:joined", "user:left"], syncHistoryOnJoin: 50, // Send last 50 messages on join }, }, hooks: { clients: { onConnected: async (client) => { // Get user info from database const user = await getUserById(client.userId); // Auto-join general chat client.join("general"); // Notify others dialogue.trigger( "general", UserJoined, { userId: client.userId, username: user.name, }, client.userId ); }, }, }, }); // Persist messages to database dialogue.on("general", Message, async (msg) => { await db.messages.create({ roomId: msg.roomId, text: msg.data.text, senderId: msg.from, replyTo: msg.data.replyTo, createdAt: new Date(msg.timestamp), }); }); ``` ## Frontend Client ```typescript // chat-client.ts import { createDialogueClient } from "./client"; interface ChatMessage { text: string; senderId: string; replyTo?: string; } function createChatClient(url: string, userId: string, token: string) { const client = createDialogueClient({ url, auth: { userId, token }, }); let room: RoomContext | null = null; return { async connect(): Promise { await client.connect(); room = await client.join("general"); }, onMessage(handler: (msg: ChatMessage, from: string) => void): () => void { if (!room) throw new Error("Not connected"); return room.on("message", (msg) => { handler(msg.data, msg.from); }); }, onUserJoined(handler: (userId: string, username: string) => void): () => void { if (!room) throw new Error("Not connected"); return room.on<{ userId: string; username: string }>( "user:joined", (msg) => { handler(msg.data.userId, msg.data.username); } ); }, sendMessage(text: string, replyTo?: string): void { if (!room) throw new Error("Not connected"); room.trigger("message", { text, senderId: client.userId, replyTo, }); }, setTyping(isTyping: boolean): void { if (!room) return; room.trigger("typing", { isTyping }); }, disconnect(): void { room?.leave(); client.disconnect(); }, }; } ``` *This documentation reflects the current implementation and is subject to evolution. Contributions and feedback are welcome.* --- url: /dialogue/guide/examples/live-notifications.md --- # Live Notifications A notification system that delivers real-time alerts to users. ## Backend Configuration ```typescript // notifications.config.ts import { createDialogue, defineEvent } from "./dialogue"; import { z } from "zod"; export const Alert = defineEvent("alert", { schema: z.object({ title: z.string(), message: z.string(), type: z.enum(["info", "warning", "error", "success"]), action: z .object({ label: z.string(), url: z.string().url(), }) .optional(), }), }); export const Badge = defineEvent("badge", { schema: z.object({ count: z.number().min(0), type: z.enum(["messages", "notifications", "tasks"]), }), }); export const dialogue = createDialogue({ port: 3000, rooms: {}, hooks: { clients: { onConnected: (client) => { // Create a personal notification room for each user const roomId = `user:${client.userId}`; // Dynamically register room if not exists if (!dialogue.room(roomId)) { // Note: In production, register rooms upfront or use a factory } client.join(roomId); }, }, }, }); // API to send notifications export function sendNotification( userId: string, notification: z.infer ): void { dialogue.trigger(`user:${userId}`, Alert, notification, "system"); } // API to update badge count export function updateBadge( userId: string, type: "messages" | "notifications" | "tasks", count: number ): void { dialogue.trigger(`user:${userId}`, Badge, { count, type }, "system"); } ``` ## Usage in API Routes ```typescript // routes/orders.ts import { sendNotification } from "../notifications.config"; app.post("/orders/:id/ship", async (c) => { const order = await shipOrder(c.req.param("id")); // Send real-time notification to customer sendNotification(order.customerId, { title: "Order Shipped", message: `Your order #${order.id} has been shipped!`, type: "success", action: { label: "Track Order", url: `/orders/${order.id}/track`, }, }); return c.json({ status: true }); }); ``` *This documentation reflects the current implementation and is subject to evolution. Contributions and feedback are welcome.* --- url: /dialogue/guide/examples/live-dashboard.md --- # Live Dashboard A real-time dashboard showing live metrics and updates. ## Backend Configuration ```typescript // dashboard.config.ts import { createDialogue, defineEvent } from "./dialogue"; import { z } from "zod"; export const MetricsUpdate = defineEvent("metrics:update", { schema: z.object({ cpu: z.number().min(0).max(100), memory: z.number().min(0).max(100), requests: z.number(), errors: z.number(), latency: z.number(), }), }); export const AlertTriggered = defineEvent("alert:triggered", { schema: z.object({ alertId: z.string(), severity: z.enum(["low", "medium", "high", "critical"]), message: z.string(), timestamp: z.number(), }), }); export const dialogue = createDialogue({ port: 3000, rooms: { dashboard: { name: "Dashboard", description: "Real-time metrics dashboard", events: [MetricsUpdate, AlertTriggered], defaultSubscriptions: ["metrics:update", "alert:triggered"], }, }, hooks: { clients: { onConnected: (client) => { // Only admins can view dashboard if (client.meta.role === "admin") { client.join("dashboard"); } }, }, }, }); // Broadcast metrics every second setInterval(async () => { const metrics = await collectMetrics(); dialogue.trigger("dashboard", MetricsUpdate, { cpu: metrics.cpuUsage, memory: metrics.memoryUsage, requests: metrics.requestsPerSecond, errors: metrics.errorsPerSecond, latency: metrics.avgLatency, }); }, 1000); // Send alerts when thresholds exceeded async function checkAlerts(): Promise { const metrics = await collectMetrics(); if (metrics.cpuUsage > 90) { dialogue.trigger("dashboard", AlertTriggered, { alertId: `cpu-${Date.now()}`, severity: "high", message: `CPU usage critical: ${metrics.cpuUsage}%`, timestamp: Date.now(), }); } } ``` *This documentation reflects the current implementation and is subject to evolution. Contributions and feedback are welcome.* --- url: /dialogue/guide/examples/multiplayer-game.md --- # Multiplayer Game A simple multiplayer game with player positions and game state. ## Backend Configuration ```typescript // game.config.ts import { createDialogue, defineEvent } from "./dialogue"; import { z } from "zod"; export const PlayerMove = defineEvent("player:move", { schema: z.object({ playerId: z.string(), x: z.number(), y: z.number(), direction: z.enum(["up", "down", "left", "right"]), }), }); export const GameState = defineEvent("game:state", { schema: z.object({ players: z.array( z.object({ id: z.string(), x: z.number(), y: z.number(), health: z.number(), score: z.number(), }) ), gameTime: z.number(), status: z.enum(["waiting", "playing", "ended"]), }), }); export const PlayerAction = defineEvent("player:action", { schema: z.object({ playerId: z.string(), action: z.enum(["attack", "defend", "heal", "special"]), targetId: z.string().optional(), }), }); export const dialogue = createDialogue({ port: 3000, rooms: { "game:lobby": { name: "Game Lobby", events: [GameState], maxSize: 8, defaultSubscriptions: ["game:state"], }, "game:match-1": { name: "Match 1", events: [PlayerMove, GameState, PlayerAction], maxSize: 4, defaultSubscriptions: ["player:move", "game:state", "player:action"], }, }, hooks: { clients: { onConnected: (client) => { client.join("game:lobby"); }, }, }, }); // Game loop - broadcast state 30 times per second const TICK_RATE = 1000 / 30; setInterval(() => { const gameState = computeGameState(); dialogue.trigger("game:match-1", GameState, { players: gameState.players, gameTime: gameState.time, status: gameState.status, }); }, TICK_RATE); // Handle player moves dialogue.on("game:match-1", PlayerMove, (msg) => { updatePlayerPosition(msg.data.playerId, msg.data.x, msg.data.y); }); // Handle player actions dialogue.on("game:match-1", PlayerAction, (msg) => { processPlayerAction(msg.data.playerId, msg.data.action, msg.data.targetId); }); ``` *This documentation reflects the current implementation and is subject to evolution. Contributions and feedback are welcome.* --- url: /dialogue/guide/examples/iot-monitoring.md --- # IoT Device Monitoring Real-time monitoring of IoT devices with sensor data. ## Backend Configuration ```typescript // iot.config.ts import { createDialogue, defineEvent } from "./dialogue"; import { z } from "zod"; export const SensorReading = defineEvent("sensor:reading", { schema: z.object({ deviceId: z.string(), temperature: z.number(), humidity: z.number(), pressure: z.number(), battery: z.number().min(0).max(100), timestamp: z.number(), }), }); export const DeviceStatus = defineEvent("device:status", { schema: z.object({ deviceId: z.string(), status: z.enum(["online", "offline", "error", "maintenance"]), lastSeen: z.number(), }), }); export const DeviceAlert = defineEvent("device:alert", { schema: z.object({ deviceId: z.string(), alertType: z.enum(["temperature", "battery", "connectivity", "error"]), message: z.string(), value: z.number().optional(), threshold: z.number().optional(), }), }); export const dialogue = createDialogue({ port: 3000, rooms: { sensors: { name: "Sensor Data", description: "Real-time sensor readings", events: [SensorReading, DeviceStatus, DeviceAlert], defaultSubscriptions: ["sensor:reading", "device:status", "device:alert"], }, }, }); // Process incoming sensor data (e.g., from MQTT bridge) export function processSensorData(data: { deviceId: string; temperature: number; humidity: number; pressure: number; battery: number; }): void { const reading = { ...data, timestamp: Date.now(), }; // Broadcast to dashboard dialogue.trigger("sensors", SensorReading, reading, data.deviceId); // Check for alerts if (data.temperature > 40) { dialogue.trigger( "sensors", DeviceAlert, { deviceId: data.deviceId, alertType: "temperature", message: `High temperature detected: ${data.temperature}°C`, value: data.temperature, threshold: 40, }, "system" ); } if (data.battery < 20) { dialogue.trigger( "sensors", DeviceAlert, { deviceId: data.deviceId, alertType: "battery", message: `Low battery: ${data.battery}%`, value: data.battery, threshold: 20, }, "system" ); } } ``` *This documentation reflects the current implementation and is subject to evolution. Contributions and feedback are welcome.* --- url: /dialogue/guide/examples/collaborative-editing.md --- # Collaborative Document Editing Real-time collaborative editing with cursor positions and document changes. ## Backend Configuration ```typescript // collab.config.ts import { createDialogue, defineEvent } from "./dialogue"; import { z } from "zod"; export const DocumentChange = defineEvent("doc:change", { schema: z.object({ userId: z.string(), operations: z.array( z.object({ type: z.enum(["insert", "delete", "retain"]), position: z.number(), text: z.string().optional(), length: z.number().optional(), }) ), version: z.number(), }), }); export const CursorUpdate = defineEvent("cursor:update", { schema: z.object({ userId: z.string(), username: z.string(), position: z.number(), selection: z .object({ start: z.number(), end: z.number(), }) .optional(), color: z.string(), }), }); export const UserPresence = defineEvent("user:presence", { schema: z.object({ userId: z.string(), username: z.string(), color: z.string(), status: z.enum(["active", "idle", "away"]), }), }); export const dialogue = createDialogue({ port: 3000, rooms: {}, hooks: { clients: { onConnected: (client) => { // Rooms are created per document // Join handled by explicit API call }, }, }, }); // Create room for a document export function createDocumentRoom(documentId: string): void { // In a real app, you'd dynamically register rooms // or use a pattern-based room system } // Broadcast document change export function broadcastChange( documentId: string, change: z.infer ): void { dialogue.trigger(`doc:${documentId}`, DocumentChange, change, change.userId); } ``` *This documentation reflects the current implementation and is subject to evolution. Contributions and feedback are welcome.* --- url: /dialogue/guide/examples/event-persistence.md --- # Event History with Database Persistence A chat application with persistent history using SQLite/PostgreSQL and the `onCleanup`/`onLoad` hooks. ## Database Schema ```sql -- messages table for persisted history CREATE TABLE messages ( id SERIAL PRIMARY KEY, room_id VARCHAR(255) NOT NULL, event_name VARCHAR(100) NOT NULL, event_data JSONB NOT NULL, from_user VARCHAR(255) NOT NULL, timestamp BIGINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_messages_room_event ON messages(room_id, event_name); CREATE INDEX idx_messages_timestamp ON messages(timestamp DESC); ``` ## Database Operations ```typescript // db/messages.ts import { EventMessage } from "dialogue-ts"; interface MessageRow { id: number; room_id: string; event_name: string; event_data: Record; from_user: string; timestamp: number; } /** * Insert multiple messages into the database (for onCleanup) */ export async function insertMessages( roomId: string, eventName: string, events: EventMessage[] ): Promise { const values = events.map((event) => ({ room_id: roomId, event_name: eventName, event_data: event.data, from_user: event.from, timestamp: event.timestamp, })); await db.insert(messages).values(values); } /** * Load messages from the database (for onLoad pagination) */ export async function loadMessages( roomId: string, eventName: string, start: number, end: number ): Promise { const rows = await db .select() .from(messages) .where( and( eq(messages.room_id, roomId), eq(messages.event_name, eventName) ) ) .orderBy(desc(messages.timestamp)) .offset(start) .limit(end - start); return rows.map((row) => ({ event: row.event_name, roomId: row.room_id, data: row.event_data, from: row.from_user, timestamp: row.timestamp, })); } ``` ## Dialogue Configuration with Persistence ```typescript // dialogue.config.ts import { createDialogue, defineEvent } from "dialogue-ts"; import { z } from "zod"; import { insertMessages, loadMessages } from "./db/messages"; export const Message = defineEvent("message", { schema: z.object({ text: z.string().min(1).max(2000), username: z.string(), }), history: { enabled: true, limit: 100 }, // Keep 100 in memory }); export const dialogue = createDialogue({ port: 3000, rooms: { general: { name: "General Chat", events: [Message], syncHistoryOnJoin: 50, // Send last 50 on join }, }, hooks: { clients: { onConnected: (client) => { client.join("general"); }, }, events: { // Persist events when evicted from memory onCleanup: async (roomId, eventName, events) => { console.log(`Persisting ${events.length} ${eventName} events from ${roomId}`); await insertMessages(roomId, eventName, events); }, // Load older events from database for pagination onLoad: async (roomId, eventName, start, end) => { console.log(`Loading ${eventName} events ${start}-${end} from ${roomId}`); return loadMessages(roomId, eventName, start, end); }, }, }, }); ``` ## Client-Side Infinite Scroll ```typescript // client/chat.ts import { createDialogueClient } from "dialogue-ts/client"; const client = createDialogueClient({ url: "ws://localhost:3000", auth: { userId: "user-123" }, }); const messages: EventMessage[] = []; let isLoadingMore = false; // Handle history sent on join client.onHistory((roomId, events) => { console.log(`Received ${events.length} historical messages`); messages.push(...events); renderMessages(); }); await client.connect(); const room = await client.join("general"); // Listen for new messages room.on("message", (msg) => { messages.unshift(msg); // Add to beginning (newest) renderMessages(); }); // Load more when scrolling to top async function loadMoreMessages(): Promise { if (isLoadingMore) return; isLoadingMore = true; try { const currentCount = messages.length; const olderMessages = await room.getHistory("message", currentCount, currentCount + 20); if (olderMessages.length > 0) { messages.push(...olderMessages); renderMessages(); } } finally { isLoadingMore = false; } } // Attach to scroll event chatContainer.addEventListener("scroll", () => { if (chatContainer.scrollTop === 0) { loadMoreMessages(); } }); ``` ## How It Works 1. **In-Memory Buffer**: Dialogue keeps the last 100 messages per event type in memory for fast access. 2. **Automatic Eviction**: When a new message arrives and the buffer exceeds 100, the oldest messages are evicted. 3. **onCleanup Hook**: Evicted messages are passed to `onCleanup`, where you persist them to your database. 4. **onLoad Hook**: When a client requests messages beyond the in-memory buffer, `onLoad` is called to fetch from the database. 5. **Seamless Pagination**: Clients can paginate through unlimited history - recent messages come from memory, older ones from the database. ``` ┌─────────────────────────────────────────────────────────────┐ │ Message Flow │ ├─────────────────────────────────────────────────────────────┤ │ │ │ New Message ──► In-Memory Buffer (100 events) │ │ │ │ │ ▼ │ │ Buffer Full? ──Yes──► onCleanup() ──► Database │ │ │ │ │ No │ │ │ │ │ ▼ │ │ Client Request (0-100)? ──► In-Memory Buffer │ │ │ │ │ Client Request (100+)? ──► onLoad() ──► DB │ │ │ └─────────────────────────────────────────────────────────────┘ ``` *This documentation reflects the current implementation and is subject to evolution. Contributions and feedback are welcome.* --- url: /dialogue/guide/api/configuration/index.md --- # Configuration Guide This guide covers all configuration options for Dialogue, including event definitions, room configurations, and server settings. ## Overview Dialogue uses a config-first approach where all rooms and events are defined upfront. This enables type safety, validation, and predictable behavior across your application. ## Topics - **[Event Definitions](./events)** - Define events with schema validation using Zod - **[Room Configuration](./rooms)** - Configure rooms, capacity limits, and default subscriptions - **[Dialogue Configuration](./dialogue-config)** - Main server configuration, CORS, and options - **[Lifecycle Hooks](./hooks)** - Client, room, socket, and event lifecycle hooks - **[Authentication](./authentication)** - Client authentication and JWT setup - **[TypeScript Types](./types)** - Type definitions and interfaces --- url: /dialogue/guide/api/configuration/events.md --- # Event Definitions Events are defined using the `defineEvent` function. Each event has a unique name and optional schema validation. ## Basic Event Definition ```typescript import { defineEvent } from "./dialogue"; // Simple event without validation const Typing = defineEvent("typing"); // Event with description const Ping = defineEvent("ping", { description: "Health check event", }); ``` ## Events with Schema Validation Use Zod schemas to validate event payloads at runtime: ```typescript import { defineEvent } from "./dialogue"; import { z } from "zod"; const Message = defineEvent("message", { schema: z.object({ text: z.string().min(1).max(1000), senderId: z.string(), replyTo: z.string().optional(), }), description: "Chat message sent by a user", }); ``` ## Event Definition Options | Property | Type | Required | Description | |----------|------|----------|-------------| | **`schema`** | `z.ZodType` | No | Zod schema for validating event data | | **`description`** | `string` | No | Human-readable description of the event | | **`history`** | `{ enabled: boolean; limit: number }` | No | Enable history storage for this event type | ## Type Inference Event types are automatically inferred from the schema: ```typescript const OrderUpdated = defineEvent("order:updated", { schema: z.object({ orderId: z.string().uuid(), status: z.enum(["pending", "processing", "shipped", "delivered"]), updatedAt: z.coerce.date(), }), }); // TypeScript infers the data type from the schema type OrderData = z.infer; // { orderId: string; status: "pending" | "processing" | "shipped" | "delivered"; updatedAt: Date } ``` ## See Also - [Room Configuration](./rooms) - Using events in rooms - [Lifecycle Hooks](./hooks#event-middleware-hooks) - Event middleware and hooks - [TypeScript Types](./types#eventdefinition) - EventDefinition interface --- url: /dialogue/guide/api/configuration/rooms.md --- # Room Configuration Rooms are defined in the `DialogueConfig.rooms` object, keyed by room ID. ## Basic Room Configuration ```typescript import { createDialogue, defineEvent } from "./dialogue"; const Message = defineEvent("message"); const Typing = defineEvent("typing"); const dialogue = createDialogue({ rooms: { chat: { name: "Chat Room", events: [Message, Typing], }, }, }); ``` ## Room Configuration Options | Property | Type | Required | Description | |----------|------|----------|-------------| | **`name`** | `string` | Yes | Human-readable room name | | **`description`** | `string` | No | Room description | | **`maxSize`** | `number` | No | Maximum concurrent connections. Undefined means unlimited | | **`events`** | `EventDefinition[]` | Yes | Events allowed in this room. Empty array allows any event | | **`defaultSubscriptions`** | `string[]` | No | Event names to auto-subscribe clients to on join | | **`createdById`** | `string` | No | User ID of room creator for ownership tracking | | **`syncHistoryOnJoin`** | `boolean \| number` | No | Auto-send history on join. `true` = all, number = limit per event type | ## Bounded Rooms Limit concurrent connections for predictable scaling: ```typescript const dialogue = createDialogue({ rooms: { game: { name: "Game Lobby", events: [GameState, PlayerMove], maxSize: 4, // Max 4 players per game }, }, }); ``` When a room is full, new clients receive a `dialogue:error` event with code `ROOM_FULL`. ## Default Subscriptions Auto-subscribe clients to specific events when they join: ```typescript const dialogue = createDialogue({ rooms: { notifications: { name: "Notifications", events: [Alert, Message, SystemUpdate], defaultSubscriptions: ["alert", "message"], // Skip system-update by default }, }, }); ``` ## Event Control Patterns **Accept All Events (Wildcard):** To create a room that accepts any event type, use the wildcard `"*"`: ```typescript const dialogue = createDialogue({ rooms: { sandbox: { name: "Sandbox", events: [{ name: '*' }], // Accepts any event name defaultSubscriptions: ['*'] // Subscribe to all events }, }, }); ``` **Reject All Events (Empty Array):** To create a room that rejects all trigger attempts (listen-only or system-controlled): ```typescript const dialogue = createDialogue({ rooms: { readonly: { name: "Read-Only Notifications", events: [], // No events can be triggered by clients // Server can still push via dialogue.trigger() if needed }, }, }); ``` **How it works:** - `events: []` - No events allowed (all triggers rejected) - `events: [{ name: '*' }]` - All events allowed (no validation) - `events: [specificEvent1, specificEvent2]` - Only listed events allowed - `defaultSubscriptions: ['*']` - Clients auto-subscribe to all events when joining **Use cases:** - **Wildcard (`*`)**: Chat rooms, debug channels, flexible communication - **Empty array (`[]`)**: Read-only rooms, server-only broadcasting - **Specific events**: Most production use cases with validated schemas **Warning:** Wildcard rooms bypass event validation. Use with caution in production. ## See Also - [Event Definitions](./events) - Defining events for rooms - [Lifecycle Hooks](./hooks#room-join-hook) - Room access control with beforeJoin hook - [Dialogue Configuration](./dialogue-config) - Full configuration example --- url: /dialogue/guide/api/configuration/dialogue-config.md --- # Dialogue Configuration The main configuration object passed to `createDialogue`. ## Full Configuration Example ```typescript import { Hono } from "hono"; import { createDialogue, defineEvent } from "./dialogue"; import { z } from "zod"; const Message = defineEvent("message", { schema: z.object({ text: z.string(), senderId: z.string() }), history: { enabled: true, limit: 100 }, // Enable history storage }); const app = new Hono(); // Add HTTP routes app.get("/health", (c) => c.json({ status: "ok" })); const dialogue = createDialogue({ port: 3000, app, // Use existing Hono app rooms: { chat: { name: "Chat", events: [Message], defaultSubscriptions: ["message"], syncHistoryOnJoin: true, // Auto-send history on join }, }, hooks: { clients: { onConnected: async (client) => { // Called when a client connects console.log(`Client ${client.userId} connected`); // Auto-join rooms based on user permissions const userRooms = await getUserRooms(client.userId); for (const roomId of userRooms) { client.join(roomId); } // Send initial data client.send("sync", { timestamp: Date.now() }); }, onDisconnected: (client) => { console.log(`Client ${client.userId} disconnected`); }, }, }, }); ``` ## Configuration Options | Property | Type | Required | Description | |----------|------|----------|-------------| | **`port`** | `number` | No | Port to run server on. Defaults to 3000 | | **`app`** | `Hono` | No | Existing Hono app to attach to. Creates new one if not provided | | **`rooms`** | `Record` | Yes | Room configurations keyed by room ID | | **`hooks`** | `HooksConfig` | No | Lifecycle hooks for clients, rooms, and events | | **`logger`** | `Logger` | No | Custom logger implementation. Uses console logger if not provided | | **`cors`** | `CorsConfig \| boolean` | No | CORS configuration. Defaults to allowing all origins | ## CORS Configuration By default, Dialogue enables CORS for all origins, making it easy to develop with frontend and backend on different ports. CORS is applied to both HTTP requests (Socket.IO polling) and WebSocket connections. ```typescript // Default behavior - allows all origins const dialogue = createDialogue({ rooms: { /* ... */ }, }); // Restrict to specific origins const dialogue = createDialogue({ rooms: { /* ... */ }, cors: { origin: "https://myapp.com", credentials: true, }, }); // Allow multiple origins const dialogue = createDialogue({ rooms: { /* ... */ }, cors: { origin: ["https://myapp.com", "https://admin.myapp.com"], }, }); // Disable CORS (same-origin only) const dialogue = createDialogue({ rooms: { /* ... */ }, cors: false, }); ``` **CorsConfig Options:** | Property | Type | Default | Description | |----------|------|---------|-------------| | **`origin`** | `string \| string[] \| boolean` | `true` | Allowed origins. Use `true` for all origins | | **`methods`** | `string[]` | `["GET", "POST"]` | Allowed HTTP methods | | **`credentials`** | `boolean` | `true` | Whether to allow credentials (cookies, auth headers) | ## DialogueContext All authentication and event hooks receive a `DialogueContext` object providing global runtime awareness: ```typescript interface DialogueContext { io: Server; // Socket.IO server instance clients: Record; // All connected clients by ID rooms: Record; // All active rooms by ID } ``` **Example usage in hooks:** ```typescript beforeEach: ({ context, roomId, message }) => { // Access all clients const totalClients = Object.keys(context.clients).length; // Access specific room const room = context.rooms[roomId]; // Access Socket.IO server context.io.emit("global-event", { data: "..." }); return Ok(message); } ``` ## See Also - [Lifecycle Hooks](./hooks) - Configure hooks option - [Room Configuration](./rooms) - Configure rooms option - [Authentication](./authentication) - Auth setup and JWT --- url: /dialogue/guide/api/configuration/hooks.md --- # Lifecycle Hooks Handle client and room lifecycle events using the `hooks` configuration: ```typescript const dialogue = createDialogue({ rooms: { chat: { name: "Chat", events: [Message, UserLeft], }, }, hooks: { clients: { onConnected: (client) => { client.join("chat"); dialogue.trigger("chat", UserJoined, { username: client.userId }); }, onDisconnected: (client) => { // Notify others when a user leaves dialogue.trigger("chat", UserLeft, { username: client.userId }); console.log(`${client.userId} disconnected`); }, onJoined: (client, roomId) => { console.log(`${client.userId} joined ${roomId}`); }, onLeft: (client, roomId) => { console.log(`${client.userId} left ${roomId}`); }, }, rooms: { onCreated: (room) => { console.log(`Room ${room.name} created`); }, onDeleted: (roomId) => { console.log(`Room ${roomId} deleted`); }, }, }, }); ``` ## HooksConfig Options | Hook | Type | Description | |------|------|-------------| | **`socket.authenticate`** | `(params: { context: DialogueContext, clientSocket: Socket, authData: unknown }) => Result` | Validates authentication and returns JWT claims | | **`socket.onConnect`** | `(params: { context: DialogueContext, clientSocket: Socket }) => void \| Promise` | Called when a socket connects (before ConnectedClient creation) | | **`socket.onDisconnect`** | `(params: { context: DialogueContext, clientSocket: Socket }) => void \| Promise` | Called when a socket disconnects | | **`clients.beforeJoin`** | `(params: { context: DialogueContext, client: ConnectedClient, roomId: string, room: Room }) => Result` | Called before client joins a room (can block) | | **`clients.onConnected`** | `(client: ConnectedClient) => void \| Promise` | Called when a client connects | | **`clients.onDisconnected`** | `(client: ConnectedClient) => void \| Promise` | Called when a client disconnects | | **`clients.onJoined`** | `(client: ConnectedClient, roomId: string) => void \| Promise` | Called when a client joins a room | | **`clients.onLeft`** | `(client: ConnectedClient, roomId: string) => void \| Promise` | Called when a client leaves a room | | **`rooms.onCreated`** | `(room: Room) => void \| Promise` | Called when a room is created | | **`rooms.onDeleted`** | `(roomId: string) => void \| Promise` | Called when a room is deleted | | **`events.beforeEach`** | `(params: { context: DialogueContext, roomId: string, message: EventMessage, from: string }) => Result` | Called before event broadcast (can block or transform) | | **`events.afterEach`** | `(params: { context: DialogueContext, roomId: string, message: EventMessage, recipientCount: number }) => void` | Called after event broadcast (for side effects) | | **`events.onTriggered`** | `(roomId: string, event: EventMessage) => void \| Promise` | Called when any event is triggered | | **`events.onCleanup`** | `(roomId: string, eventName: string, events: EventMessage[]) => void \| Promise` | Called when events are evicted from memory | | **`events.onLoad`** | `(roomId: string, eventName: string, start: number, end: number) => Promise` | Called to load historical events from external storage | ## Authentication Hook The `authenticate` hook allows you to validate client authentication data and return JWT claims: ```typescript import { Ok, Err } from "slang-ts"; import jwt from "jsonwebtoken"; const dialogue = createDialogue({ rooms: { /* ... */ }, hooks: { authenticate: ({ context, clientSocket, authData }) => { // Validate token from auth data const token = authData?.token as string; if (!token) { return Err("Authentication token required"); } try { // Verify and decode JWT const claims = jwt.verify(token, process.env.JWT_SECRET) as { sub: string; // User ID (required) exp: number; // Expiration timestamp iat: number; // Issued at timestamp role?: string; email?: string; }; // Return auth data with JWT claims return Ok({ jwt: claims, // Add any additional auth fields here }); } catch (error) { return Err(`Invalid token: ${error.message}`); } }, }, }); ``` **Hook Signature:** ```typescript authenticate: (params: { context: DialogueContext; clientSocket: Socket; authData: unknown; }) => Result ``` **Parameters:** - **`context`**: Global context with `io`, `clients`, and `rooms` - **`clientSocket`**: The raw Socket.IO socket - **`authData`**: Data sent from client during connection **Returns:** `Ok(AuthData)` on success or `Err(string)` with error message **AuthData Structure:** ```typescript interface AuthData { jwt: { sub: string; // User ID (extracted to client.userId) exp: number; // Expiration timestamp iat: number; // Issued at timestamp [key: string]: unknown; // Additional JWT claims }; // Additional auth fields can be added here } ``` The authenticated user's data is available via `client.auth` and the user ID is extracted from `jwt.sub`. ## Socket Lifecycle Hooks The `socket.onConnect` and `socket.onDisconnect` hooks provide low-level access to Socket.IO socket lifecycle events. These hooks receive the raw socket before the `ConnectedClient` wrapper is created, making them useful for socket-level operations. ```typescript const dialogue = createDialogue({ rooms: { /* ... */ }, hooks: { socket: { onConnect: ({ context, clientSocket }) => { // Called when socket connects (before ConnectedClient is created) console.log(`Socket ${clientSocket.id} connected`); // Access socket-level data console.log('Handshake:', clientSocket.handshake); console.log('IP:', clientSocket.handshake.address); // You can also emit directly to the socket clientSocket.emit('server-info', { version: '1.0.0' }); }, onDisconnect: ({ context, clientSocket }) => { // Called when socket disconnects console.log(`Socket ${clientSocket.id} disconnected`); console.log('Disconnect reason:', clientSocket.disconnected); }, }, }, }); ``` **onConnect Hook Signature:** ```typescript onConnect: (params: { context: DialogueContext; clientSocket: Socket; }) => void | Promise ``` **onDisconnect Hook Signature:** ```typescript onDisconnect: (params: { context: DialogueContext; clientSocket: Socket; }) => void | Promise ``` **Parameters:** - **`context`**: Global context with `io`, `clients`, and `rooms` - **`clientSocket`**: The raw Socket.IO socket instance **When to use socket hooks vs client hooks:** - Use **`socket.onConnect`/`onDisconnect`** when you need: - Access to raw socket data (handshake, IP address, socket rooms) - Socket-level operations before ConnectedClient creation - Logging or monitoring at the socket layer - Use **`clients.onConnected`/`onDisconnected`** when you need: - Access to the high-level ConnectedClient API - User-level operations (joining rooms, sending messages) - Business logic based on user ID or auth data ## Room Join Hook The `beforeJoin` hook allows you to control room access and validate join requests: ```typescript const dialogue = createDialogue({ rooms: { /* ... */ }, hooks: { clients: { beforeJoin: ({ context, client, roomId, room }) => { // Check permissions if (roomId === "vip-room" && client.auth?.jwt.role !== "vip") { return Err("VIP access required"); } // Check room capacity if (room.isFull()) { return Err("Room is full"); } // Allow join return Ok(undefined); }, }, }, }); ``` **Hook Signature:** ```typescript beforeJoin: (params: { context: DialogueContext; client: ConnectedClient; roomId: string; room: Room; }) => Result ``` ## Event Middleware Hooks The `beforeEach` and `afterEach` hooks allow you to intercept and transform events: ```typescript const dialogue = createDialogue({ rooms: { /* ... */ }, hooks: { events: { // Run before event is broadcast - can block or transform beforeEach: ({ context, roomId, message, from }) => { // Filter profanity if (containsProfanity(message.data.text)) { return Err("Message contains inappropriate content"); } // Transform message const transformed = { ...message, data: { ...message.data, text: sanitize(message.data.text), }, }; return Ok(transformed); }, // Run after event is broadcast - for side effects afterEach: ({ context, roomId, message, recipientCount }) => { console.log(`Event ${message.event} sent to ${recipientCount} clients`); // Log to analytics analytics.track("event_broadcast", { roomId, eventName: message.event, recipientCount, }); }, }, }, }); ``` **beforeEach Hook Signature:** ```typescript beforeEach: (params: { context: DialogueContext; roomId: string; message: EventMessage; from: string; }) => Result ``` **afterEach Hook Signature:** ```typescript afterEach: (params: { context: DialogueContext; roomId: string; message: EventMessage; recipientCount: number; }) => void ``` ## See Also - [Authentication](./authentication) - Auth data structure and client auth - [TypeScript Types](./types) - Hook type signatures and interfaces - [Dialogue Configuration](./dialogue-config#dialoguecontext) - DialogueContext object --- url: /dialogue/guide/api/configuration/authentication.md --- # Authentication ## Client Authentication Clients pass authentication data in the `auth` option when connecting: ```typescript // Frontend const client = createDialogueClient({ url: "ws://localhost:3000", auth: { userId: "user-123", token: "jwt-token-here", role: "admin", }, }); ``` ## Accessing Auth Data The `onConnected` hook receives auth data via the `client` object: ```typescript const dialogue = createDialogue({ rooms: { /* ... */ }, hooks: { clients: { onConnected: (client) => { console.log(client.userId); // "user-123" or socket.id if not provided console.log(client.meta); // { token: "jwt-token-here", role: "admin" } // Validate token and permissions if (!isValidToken(client.meta.token)) { client.disconnect(); return; } // Join rooms based on role if (client.meta.role === "admin") { client.join("admin-dashboard"); } }, }, }, }); ``` ## See Also - [Lifecycle Hooks](./hooks#authentication-hook) - authenticate hook for JWT validation - [TypeScript Types](./types#authdata) - AuthData and JwtClaims types --- url: /dialogue/guide/api/configuration/types.md --- # TypeScript Types ## Core Types ```typescript import type { Dialogue, DialogueConfig, DialogueContext, AuthData, JwtClaims, RoomConfig, EventDefinition, EventHistoryConfig, EventMessage, ConnectedClient, Room, HooksConfig, } from "./dialogue"; ``` ## EventDefinition ```typescript interface EventDefinition { readonly name: string; readonly description?: string; readonly schema?: z.ZodType; readonly history?: EventHistoryConfig; } ``` ## EventHistoryConfig ```typescript interface EventHistoryConfig { /** Whether to store this event type in history */ enabled: boolean; /** Maximum number of events to keep in memory per room */ limit: number; } ``` ## EventMessage ```typescript interface EventMessage { event: string; roomId: string; data: T; from: string; timestamp: number; } ``` ## DialogueContext ```typescript interface DialogueContext { io: Server; // Socket.IO server instance clients: Record; // All connected clients rooms: Record; // All active rooms } ``` ## AuthData ```typescript interface AuthData { jwt: JwtClaims; // Additional authentication fields can be added } ``` ## JwtClaims ```typescript interface JwtClaims { sub: string; // Subject (user ID) exp: number; // Expiration timestamp iat: number; // Issued at timestamp [key: string]: unknown; // Additional custom claims } ``` ## See Also - [Event Definitions](./events) - Using EventDefinition - [Authentication](./authentication) - Using AuthData and JwtClaims - [Dialogue Configuration](./dialogue-config) - Using DialogueContext --- *This documentation reflects the current implementation and is subject to evolution. Contributions and feedback are welcome.* --- url: /dialogue/guide/api/backend-api.md --- # Backend API Reference This document covers the complete backend API for Dialogue, including the Dialogue instance, rooms, and connected clients. ## 1. Overview The backend API provides methods for triggering events, subscribing to events, managing rooms, and controlling the server lifecycle. ## 2. Dialogue Instance The main Dialogue instance is created using `createDialogue()` and provides the core API. ### 2.1 Creating a Dialogue Instance ```typescript import { createDialogue, defineEvent } from "./dialogue"; import { z } from "zod"; const Message = defineEvent("message", { schema: z.object({ text: z.string() }), }); const dialogue = createDialogue({ port: 3000, rooms: { chat: { name: "Chat", events: [Message], }, }, }); ``` ### 2.2 Dialogue Properties | Property | Type | Description | |----------|------|-------------| | **`app`** | `Hono` | The Hono app instance | | **`io`** | `Server` | The Socket.IO server instance | ### 2.3 dialogue.trigger() Triggers an event to all subscribers in a room. Call this from anywhere in your backend. **Signature:** ```typescript dialogue.trigger( roomId: string, event: EventDefinition, data: T, from?: string ): void ``` **Parameters:** - **`roomId`**: The room to broadcast to - **`event`**: The event definition - **`data`**: The event payload (validated against schema if present) - **`from`**: Optional sender identifier (defaults to "system") **Example:** ```typescript import { dialogue, Message } from "./dialogue.config"; // From an API route app.post("/messages", async (c) => { const { text, userId } = await c.req.json(); // Trigger to all clients in the chat room dialogue.trigger("chat", Message, { text, senderId: userId }, userId); return c.json({ status: true }); }); // From a webhook handler app.post("/webhooks/payment", async (c) => { const event = await c.req.json(); dialogue.trigger("orders", OrderUpdated, { orderId: event.orderId, status: "paid", }); return c.json({ received: true }); }); ``` ## EventMessage Structure All events are wrapped in a standardized `EventMessage` envelope. This structure is enforced by Dialogue and **not customizable** by developers (except for the `data` payload). ```typescript interface EventMessage { event: string; // Event name roomId: string; // Room ID where event occurred data: T; // Your custom payload (validated by schema) from: string; // User ID of sender (or "system") timestamp: number; // Unix timestamp (milliseconds) meta?: Record; // Optional flexible metadata } ``` ### The `meta` Field The `meta` field provides a flexible way to add contextual information without changing your event schemas: ```typescript // Example: Add request context dialogue.trigger('chat', chatMessage, { event: 'message', data: { text: 'Hello world' }, from: 'user-123', timestamp: Date.now(), meta: { ip: '192.168.1.1', userAgent: 'Mozilla/5.0...', correlationId: 'abc-123' } }); // Example: Add permission context room.trigger(updateEvent, data, userId, { permissions: ['admin', 'write'], sessionId: 'xyz-789' }); ``` **Use cases:** - Request metadata (IP, user agent, trace IDs) - Permission/authorization context - A/B test variants - Feature flags - Debug information **Important:** `meta` is optional and has no schema validation - use responsibly. ### 2.4 dialogue.on() Subscribes to events for backend side-effects like logging, persistence, or triggering other actions. **Signature:** ```typescript dialogue.on( roomId: string, event: EventDefinition, handler: (msg: EventMessage) => void | Promise ): () => void ``` **Parameters:** - **`roomId`**: The room to listen to - **`event`**: The event definition - **`handler`**: Callback function receiving the event message **Returns:** Unsubscribe function **Example:** ```typescript import { dialogue, Message } from "./dialogue.config"; // Log all messages const unsubscribe = dialogue.on("chat", Message, (msg) => { console.log(`[${msg.roomId}] ${msg.from}: ${msg.data.text}`); }); // Persist messages to database dialogue.on("chat", Message, async (msg) => { await db.messages.create({ roomId: msg.roomId, text: msg.data.text, senderId: msg.from, createdAt: new Date(msg.timestamp), }); }); // Send push notifications dialogue.on("notifications", Alert, async (msg) => { const users = await getOfflineUsers(msg.roomId); await sendPushNotifications(users, msg.data); }); // Unsubscribe when needed unsubscribe(); ``` ### 2.5 dialogue.room() Gets a room instance by ID. **Signature:** ```typescript dialogue.room(id: string): Room | null ``` **Example:** ```typescript const chatRoom = dialogue.room("chat"); if (chatRoom) { console.log(`${chatRoom.name} has ${chatRoom.size()} participants`); } ``` ### 2.6 dialogue.rooms() Gets all registered rooms. **Signature:** ```typescript dialogue.rooms(): Room[] ``` **Example:** ```typescript const allRooms = dialogue.rooms(); for (const room of allRooms) { console.log(`${room.name}: ${room.size()} participants`); } ``` ### 2.7 dialogue.createRoom() Creates a new room at runtime. Useful for dynamic room creation based on user actions. **Signature:** ```typescript dialogue.createRoom( id: string, config: { name: string; description?: string; events: EventDefinition[]; defaultSubscriptions?: string[]; maxSize?: number; }, createdById?: string ): Room ``` **Parameters:** - **`id`**: Unique room identifier - **`config.name`**: Human-readable room name - **`config.description`**: Optional room description - **`config.events`**: Array of allowed event definitions - **`config.defaultSubscriptions`**: Event names to auto-subscribe on join - **`config.maxSize`**: Optional maximum participants - **`createdById`**: Optional user ID of the room creator **Returns:** The created `Room` instance **Example:** ```typescript import { dialogue, Message, UserJoined } from "./dialogue.config"; // Create a room dynamically const room = dialogue.createRoom( "project-123", { name: "Project Discussion", description: "Chat for project #123", events: [Message, UserJoined], defaultSubscriptions: ["message", "user-joined"], maxSize: 50, }, "user-456" // Creator ID ); console.log(`Created room: ${room.name}`); ``` **Best Practices:** - **Always set `defaultSubscriptions`** for rooms with known events to ensure clients receive messages immediately upon joining - Use explicit event names for most subscriptions: `defaultSubscriptions: ["message", "userJoined"]` - Use wildcard `"*"` sparingly (debugging, logging, analytics): `events: [{ name: "*" }]` - Use empty array `[]` for read-only/server-only rooms where clients can't trigger events - Server-side `defaultSubscriptions` provides convenience but doesn't replace explicit client-side subscription when needed **Note:** Creating a room broadcasts a `dialogue:roomCreated` event to all connected clients. ### 2.8 dialogue.deleteRoom() Deletes a room at runtime. All clients in the room will be notified. **Signature:** ```typescript dialogue.deleteRoom(id: string): boolean ``` **Parameters:** - **`id`**: The room ID to delete **Returns:** `true` if the room was deleted, `false` if it didn't exist **Example:** ```typescript // Delete a room when no longer needed const deleted = dialogue.deleteRoom("project-123"); if (deleted) { console.log("Room deleted successfully"); } else { console.log("Room not found"); } ``` **Note:** Deleting a room broadcasts a `dialogue:roomDeleted` event to all connected clients. ### 2.9 dialogue.start() Starts the server. **Signature:** ```typescript dialogue.start(): Promise ``` **Example:** ```typescript await dialogue.start(); console.log("Server running on http://localhost:3000"); ``` ### 2.10 dialogue.stop() Stops the server and disconnects all clients. **Signature:** ```typescript dialogue.stop(): Promise ``` **Example:** ```typescript process.on("SIGTERM", async () => { await dialogue.stop(); process.exit(0); }); ``` ### 2.11 dialogue.getClients() Gets all connected clients for a specific user ID. Returns an array since a user may have multiple connections (e.g., multiple tabs or devices). **Signature:** ```typescript dialogue.getClients(userId: string): ConnectedClient[] ``` **Example:** ```typescript const clients = dialogue.getClients("user-123"); console.log(`User has ${clients.length} active connections`); ``` ### 2.12 dialogue.getAllClients() Gets all currently connected clients. **Signature:** ```typescript dialogue.getAllClients(): ConnectedClient[] ``` **Example:** ```typescript const allClients = dialogue.getAllClients(); console.log(`Total connected clients: ${allClients.length}`); ``` ### 2.13 dialogue.getClientRooms() Gets all room IDs that a user is currently in, with helper methods for managing room membership. Aggregates rooms across all connections for this user. **Signature:** ```typescript dialogue.getClientRooms(userId: string): ClientRooms ``` **Returns:** `ClientRooms` object with: | Property/Method | Type | Description | |-----------------|------|-------------| | **`ids`** | `string[]` | Array of room IDs the user is in | | **`forAll(callback)`** | `(cb: (roomId: string) => void) => void` | Execute callback for each room (no side effects) | | **`leaveAll(callback?)`** | `(cb?: (roomId: string) => void) => void` | Remove user from all rooms, optionally executing callback for each | **Example:** ```typescript const rooms = dialogue.getClientRooms("user-123"); // Access room IDs console.log(`User is in rooms: ${rooms.ids.join(", ")}`); // Do something for all rooms (without leaving) rooms.forAll((roomId) => { dialogue.trigger(roomId, UserTyping, { userId: "user-123", isTyping: false }); }); // Leave all rooms with notification rooms.leaveAll((roomId) => { dialogue.trigger(roomId, UserLeft, { username: "user-123" }); }); // Or just leave without notification rooms.leaveAll(); ``` **Common pattern in onDisconnect:** ```typescript onDisconnect: (client) => { dialogue.getClientRooms(client.userId).leaveAll((roomId) => { dialogue.trigger(roomId, UserLeft, { username: client.userId }); }); } ``` ### 2.14 dialogue.isInRoom() Checks if a user is in a specific room (any of their connections). **Signature:** ```typescript dialogue.isInRoom(userId: string, roomId: string): boolean ``` **Example:** ```typescript if (dialogue.isInRoom("user-123", "vip-room")) { // User has access to VIP features } ``` ## 3. Room API Room instances provide methods for broadcasting and managing participants. ### 3.1 Room Properties | Property | Type | Description | |----------|------|-------------| | **`id`** | `string` | Unique room identifier | | **`name`** | `string` | Human-readable room name | | **`description`** | `string \| undefined` | Room description | | **`maxSize`** | `number \| undefined` | Maximum connections | | **`events`** | `EventDefinition[]` | Allowed events | | **`defaultSubscriptions`** | `string[]` | Auto-subscribe event names | | **`createdById`** | `string \| undefined` | Room creator ID | ### 3.2 room.trigger() Triggers an event to all subscribers in this room. **Signature:** ```typescript room.trigger(event: EventDefinition, data: T, from?: string): void ``` **Example:** ```typescript const room = dialogue.room("chat"); room?.trigger(Message, { text: "Hello!" }, "system"); ``` ### 3.3 room.on() Subscribes to events in this room. **Signature:** ```typescript room.on( event: EventDefinition, handler: (msg: EventMessage) => void | Promise ): () => void ``` **Example:** ```typescript const room = dialogue.room("chat"); room?.on(Message, (msg) => { console.log(`Message in ${room.name}: ${msg.data.text}`); }); ``` ### 3.4 room.size() Returns the current number of connected participants. **Signature:** ```typescript room.size(): number ``` ### 3.5 room.isFull() Returns true if the room is at maximum capacity. **Signature:** ```typescript room.isFull(): boolean ``` ### 3.6 room.participants() Returns all connected clients in the room. **Signature:** ```typescript room.participants(): ConnectedClient[] ``` **Example:** ```typescript const room = dialogue.room("chat"); const participants = room?.participants() ?? []; for (const client of participants) { console.log(`- ${client.userId}`); } ``` ## 4. ConnectedClient API Represents a connected socket with user context. ### 4.1 Client Properties | Property | Type | Description | |----------|------|-------------| | **`id`** | `string` | Unique client/session ID | | **`userId`** | `string` | Application user ID from auth (extracted from JWT sub claim) | | **`socket`** | `Socket` | Underlying Socket.IO socket | | **`auth`** | `AuthData \| undefined` | Authentication data with JWT claims (if authenticated) | | **`meta`** | `Record` | Additional user metadata (deprecated - use auth instead) | ### 4.2 client.join() Joins a room by ID. **Signature:** ```typescript client.join(roomId: string): void ``` **Example:** ```typescript onConnect: (client) => { client.join("general"); client.join("notifications"); } ``` ### 4.3 client.leave() Leaves a room by ID. **Signature:** ```typescript client.leave(roomId: string): void ``` ### 4.4 client.subscribe() Subscribes to a specific event in a room. **Signature:** ```typescript client.subscribe(roomId: string, eventName: string): void ``` ### 4.5 client.subscribeAll() Subscribes to all events in a room (wildcard). **Signature:** ```typescript client.subscribeAll(roomId: string): void ``` ### 4.6 client.unsubscribe() Unsubscribes from an event in a room. **Signature:** ```typescript client.unsubscribe(roomId: string, eventName: string): void ``` ### 4.7 client.rooms() Returns list of room IDs the client has joined. **Signature:** ```typescript client.rooms(): string[] ``` ### 4.8 client.subscriptions() Returns subscribed event names for a room. **Signature:** ```typescript client.subscriptions(roomId: string): string[] ``` ### 4.9 client.send() Sends data directly to this client only. **Signature:** ```typescript client.send(event: string, data: T): void ``` **Example:** ```typescript onConnect: (client) => { // Send welcome message to this client only client.send("welcome", { message: "Welcome to the server!", serverTime: Date.now(), }); } ``` ### 4.10 client.disconnect() Disconnects this client. **Signature:** ```typescript client.disconnect(): void ``` ## 5. Utility Functions ### 5.1 defineEvent() Creates a typed event definition. **Signature:** ```typescript function defineEvent( name: string, options?: { schema?: z.ZodType; description?: string; history?: EventHistoryConfig; } ): EventDefinition ``` **Parameters:** - **`name`**: Unique event name (e.g., 'message', 'order:updated') - **`options.schema`**: Optional Zod schema for validation - **`options.description`**: Human-readable description - **`options.history`**: History configuration - when enabled, events are stored in memory **Example:** ```typescript import { defineEvent } from "./dialogue"; import { z } from "zod"; // Simple event const Typing = defineEvent("typing"); // Event with validation const Message = defineEvent("message", { schema: z.object({ text: z.string().min(1).max(1000), senderId: z.string(), }), description: "Chat message sent by a user", history: { enabled: true, limit: 50 }, }); ``` ### 5.2 validateEventData() Validates event data against its schema. **Signature:** ```typescript function validateEventData( event: EventDefinition, data: unknown ): Result ``` **Parameters:** - **`event`**: The event definition with optional schema - **`data`**: Data to validate **Returns:** `Result` - `Ok(data)` on success or `Err(message)` on validation failure **Example:** ```typescript import { validateEventData, Message } from "./dialogue.config"; const result = validateEventData(Message, { text: "Hello" }); if (result.isOk) { console.log("Valid:", result.value); } else { console.error("Invalid:", result.error); } ``` ### 5.3 isEventAllowed() Checks if an event is allowed in a room based on the room's event list. If the room has no events defined (empty array), all events are allowed. **Signature:** ```typescript function isEventAllowed( eventName: string, allowedEvents: EventDefinition[] ): boolean ``` **Parameters:** - **`eventName`**: Name of the event to check - **`allowedEvents`**: List of allowed events for the room **Returns:** `true` if event is allowed, `false` otherwise **Example:** ```typescript import { isEventAllowed, Message, Typing } from "./dialogue.config"; const roomEvents = [Message, Typing]; console.log(isEventAllowed("message", roomEvents)); // true console.log(isEventAllowed("unknown", roomEvents)); // false // Empty array allows all events console.log(isEventAllowed("anything", [])); // true ``` ### 5.4 getEventByName() Gets an event definition by name from a list of events. **Signature:** ```typescript function getEventByName( eventName: string, events: EventDefinition[] ): EventDefinition | undefined ``` **Parameters:** - **`eventName`**: Name of the event to find - **`events`**: List of event definitions to search **Returns:** The event definition or `undefined` if not found **Example:** ```typescript import { getEventByName, Message, Typing } from "./dialogue.config"; const roomEvents = [Message, Typing]; const messageEvent = getEventByName("message", roomEvents); if (messageEvent) { console.log(`Found event: ${messageEvent.name}`); console.log(`Has schema: ${messageEvent.schema !== undefined}`); } const unknownEvent = getEventByName("unknown", roomEvents); console.log(unknownEvent); // undefined ``` ## 6. Factory Functions These factory functions create specialized components for Dialogue. ### 6.1 createHistoryManager() Creates a history manager for storing events in memory. **Signature:** ```typescript function createHistoryManager(config?: { maxEventsPerType?: number; maxRooms?: number; }): HistoryManager ``` **Parameters:** - **`config.maxEventsPerType`**: Maximum events to store per event type (default: 100) - **`config.maxRooms`**: Maximum rooms to track (default: 1000) **Returns:** `HistoryManager` instance **Example:** ```typescript import { createDialogue, createHistoryManager } from "./dialogue"; const historyManager = createHistoryManager({ maxEventsPerType: 200, maxRooms: 500, }); // History managers are automatically used when passed to createDialogue // or when events have history.enabled = true ``` ### 6.2 createRateLimiter() Creates a rate limiter for throttling event triggers. **Signature:** ```typescript function createRateLimiter(config: { maxEvents: number; windowMs: number; }): RateLimiter ``` **Parameters:** - **`config.maxEvents`**: Maximum events allowed in the time window - **`config.windowMs`**: Time window in milliseconds **Returns:** `RateLimiter` instance **Example:** ```typescript import { createRateLimiter } from "./dialogue"; const limiter = createRateLimiter({ maxEvents: 10, // 10 events windowMs: 1000, // per second }); // Check if action is allowed if (limiter.isAllowed(userId)) { // Trigger event dialogue.trigger("chat", Message, data, userId); } else { // Rate limit exceeded console.warn(`User ${userId} is rate limited`); } ``` ### 6.3 createDefaultLogger() Creates a default console-based logger. **Signature:** ```typescript function createDefaultLogger(): Logger ``` **Example:** ```typescript import { createDialogue, createDefaultLogger } from "./dialogue"; const logger = createDefaultLogger(); const dialogue = createDialogue({ rooms: { /* ... */ }, logger, // Use default logger }); ``` ### 6.4 createSilentLogger() Creates a silent logger that suppresses all output. Useful for tests. **Signature:** ```typescript function createSilentLogger(): Logger ``` **Example:** ```typescript import { createDialogue, createSilentLogger } from "./dialogue"; const logger = createSilentLogger(); const dialogue = createDialogue({ rooms: { /* ... */ }, logger, // Suppress all logging }); ``` *This documentation reflects the current implementation and is subject to evolution. Contributions and feedback are welcome.* --- url: /dialogue/guide/api/client-api.md --- # Client API Reference This document covers the client SDK for connecting to a Dialogue server from frontend applications. ## 1. Overview The Dialogue client SDK provides a simple, type-safe API for connecting to a Dialogue server, joining rooms, and handling real-time events. ## 2. Installation Install the package and its peer dependency: ```bash bun add dialogue-ts zod ``` The client SDK is included in the Dialogue package: ```typescript import { createDialogueClient } from "dialogue-ts/client"; ``` ## 3. DialogueClient The main client factory for connecting to a Dialogue server. ### 3.1 Creating a Client ```typescript import { createDialogueClient } from "./client"; const client = createDialogueClient({ url: "ws://localhost:3000", auth: { userId: "user-123", token: "jwt-token-here", }, }); ``` ### 3.2 Configuration Options | Property | Type | Required | Default | Description | |----------|------|----------|---------|-------------| | **`url`** | `string` | Yes | - | WebSocket server URL | | **`auth`** | `object` | No | - | Authentication data sent with connection | | **`auth.userId`** | `string` | No | - | User ID for identification | | **`auth.token`** | `string` | No | - | JWT or other auth token | | **`autoConnect`** | `boolean` | No | `true` | Auto-connect on instantiation | | **`reconnection`** | `boolean` | No | `true` | Enable automatic reconnection | | **`reconnectionAttempts`** | `number` | No | `5` | Maximum reconnection attempts | ### 3.3 Properties | Property | Type | Description | |----------|------|-------------| | **`userId`** | `string` | User ID assigned by the server | | **`connected`** | `boolean` | Whether the client is connected | | **`state`** | `ConnectionState` | Current connection state: `"disconnected"`, `"connecting"`, or `"connected"` | ### 3.4 client.connect() Connects to the server. Returns a promise that resolves when connected. **Signature:** ```typescript client.connect(): Promise ``` **Example:** ```typescript try { await client.connect(); console.log("Connected as:", client.userId); } catch (error) { console.error("Connection failed:", error); } ``` ### 3.5 client.disconnect() Disconnects from the server and leaves all joined rooms. **Signature:** ```typescript client.disconnect(): void ``` **Example:** ```typescript // Cleanup on component unmount useEffect(() => { return () => { client.disconnect(); }; }, []); ``` ### 3.6 client.join() Joins a room and returns a RoomContext for interacting with it. **Signature:** ```typescript client.join(roomId: string): Promise ``` **Example:** ```typescript const chat = await client.join("chat"); console.log(`Joined ${chat.roomName}`); // Listen for messages chat.on("message", (msg) => { console.log(`${msg.from}: ${msg.data.text}`); }); ``` ### 3.7 client.getRoom() Gets a previously joined room context. **Signature:** ```typescript client.getRoom(roomId: string): RoomContext | undefined ``` **Example:** ```typescript const chat = client.getRoom("chat"); if (chat) { chat.trigger("message", { text: "Hello!" }); } ``` ### 3.8 client.listRooms() Lists all available rooms on the server. **Signature:** ```typescript client.listRooms(): Promise ``` **Returns:** Array of `RoomInfo` objects: ```typescript interface RoomInfo { id: string; name: string; description?: string; size: number; maxSize?: number; createdById?: string; // User ID of room creator (for dynamic rooms) } ``` **Example:** ```typescript const rooms = await client.listRooms(); for (const room of rooms) { console.log(`${room.name}: ${room.size}/${room.maxSize ?? "∞"} participants`); } ``` ### 3.9 client.onConnect() Registers a handler called when connected. **Signature:** ```typescript client.onConnect(handler: () => void): () => void ``` **Returns:** Unsubscribe function **Example:** ```typescript const unsubscribe = client.onConnect(() => { console.log("Connected!"); }); ``` ### 3.10 client.onDisconnect() Registers a handler called when disconnected. **Signature:** ```typescript client.onDisconnect(handler: (reason: string) => void): () => void ``` **Example:** ```typescript client.onDisconnect((reason) => { console.log("Disconnected:", reason); }); ``` ### 3.11 client.onError() Registers an error handler. **Signature:** ```typescript client.onError(handler: (error: Error) => void): () => void ``` **Example:** ```typescript client.onError((error) => { console.error("Error:", error.message); }); ``` ### 3.12 client.createRoom() Creates a new room on the server. Requires server to support dynamic room creation. **Signature:** ```typescript client.createRoom(options: CreateRoomOptions): Promise ``` **CreateRoomOptions:** ```typescript interface CreateRoomOptions { id: string; name: string; description?: string; maxSize?: number; } ``` **Returns:** `RoomInfo` object with the created room details **Example:** ```typescript const roomInfo = await client.createRoom({ id: "tech-talk", name: "Tech Talk", description: "Discuss the latest in technology", maxSize: 100, }); console.log(`Created room: ${roomInfo.name}`); // Now join the room const room = await client.join(roomInfo.id); ``` ### 3.13 client.deleteRoom() Deletes a room from the server. Only the room creator can delete a room. **Signature:** ```typescript client.deleteRoom(roomId: string): Promise ``` **Example:** ```typescript try { await client.deleteRoom("tech-talk"); console.log("Room deleted successfully"); } catch (error) { console.error("Failed to delete room:", error.message); } ``` ### 3.14 client.onRoomCreated() Registers a handler called when a new room is created on the server. **Signature:** ```typescript client.onRoomCreated(handler: (room: RoomInfo) => void): () => void ``` **Returns:** Unsubscribe function **Example:** ```typescript client.onRoomCreated((room) => { console.log(`New room available: ${room.name}`); // Update UI to show new room in list updateRoomList(); }); ``` ### 3.15 client.onRoomDeleted() Registers a handler called when a room is deleted from the server. **Signature:** ```typescript client.onRoomDeleted(handler: (roomId: string) => void): () => void ``` **Returns:** Unsubscribe function **Example:** ```typescript client.onRoomDeleted((roomId) => { console.log(`Room deleted: ${roomId}`); // If user was in this room, redirect them if (currentRoomId === roomId) { showMessage("This room has been deleted"); navigateToRoomList(); } }); ``` ### 3.16 client.onHistory() Registers a handler called when historical events are received on room join (when `syncHistoryOnJoin` is enabled). **Signature:** ```typescript client.onHistory(handler: (roomId: string, events: EventMessage[]) => void): () => void ``` **Returns:** Unsubscribe function **Example:** ```typescript client.onHistory((roomId, events) => { console.log(`Received ${events.length} historical events for ${roomId}`); // Render historical messages events.forEach((event) => { addMessage(event, { isHistory: true }); }); }); ``` ## 4. RoomContext Returned by `client.join()`, provides methods for interacting with a joined room. ### 4.1 Properties | Property | Type | Description | |----------|------|-------------| | **`roomId`** | `string` | Room identifier | | **`roomName`** | `string` | Human-readable room name | ### 4.2 room.trigger() Triggers an event to all subscribers in the room. **Signature:** ```typescript room.trigger(eventName: string, data: T): void ``` **Example:** ```typescript // Send a message room.trigger("message", { text: "Hello, everyone!", senderId: client.userId, }); // Send a typing indicator room.trigger("typing", { isTyping: true }); ``` ### 4.3 room.on() Listens for a specific event. Returns an unsubscribe function. **Signature:** ```typescript room.on(eventName: string, handler: (msg: EventMessage) => void): () => void ``` **EventMessage structure:** ```typescript interface EventMessage { event: string; roomId: string; data: T; from: string; timestamp: number; meta?: Record; } ``` ### Meta Field The optional `meta` field can contain arbitrary contextual data: ```typescript room.trigger('action', { type: 'click' }, { sessionId: 'abc-123', variant: 'A', timestamp: Date.now() }); // Receive meta in handlers room.on('action', (msg) => { console.log(msg.data); // { type: 'click' } console.log(msg.meta); // { sessionId: '...', variant: 'A', ... } }); ``` **Common use cases:** - Analytics tracking (session IDs, A/B variants) - Request tracing (correlation IDs) - Debug information - Client-side state hints **Example:** ```typescript // Listen for messages const unsubscribe = room.on<{ text: string; senderId: string }>( "message", (msg) => { console.log(`[${msg.from}] ${msg.data.text}`); console.log(`Received at: ${new Date(msg.timestamp)}`); } ); // Stop listening unsubscribe(); ``` ### 4.4 room.onAny() Listens for all events in the room. Useful for logging or debugging. **Signature:** ```typescript room.onAny( handler: (eventName: string, msg: EventMessage) => void ): () => void ``` **Example:** ```typescript room.onAny((eventName, msg) => { console.log(`Event: ${eventName}`, msg.data); }); ``` ### 4.5 room.subscribe() Subscribes to an additional event type. **Signature:** ```typescript room.subscribe(eventName: string): void ``` **Example:** ```typescript // Subscribe to typing events (if not in defaultSubscriptions) room.subscribe("typing"); ``` ### 4.6 room.subscribeAll() Subscribe to **all events** in the room using the wildcard pattern: **Signature:** ```typescript room.subscribeAll(): void ``` **Example:** ```typescript const room = await client.join("project-123"); // Subscribe to all events in this room room.subscribeAll(); // Internally calls: room.subscribe('*') // Now set up handlers room.on("message", (msg) => { console.log(msg.data); }); ``` This subscribes to every event type in the room, including: - Events defined in room config - Dynamic events (if room uses wildcard) - Future events added to the room **Use cases:** - Logging/debugging during development - Analytics collection - Admin dashboards showing all activity **Performance note:** Only use when you genuinely need all events. Specific subscriptions are more efficient. **Note:** If the room has `defaultSubscriptions` configured on the server, clients are automatically subscribed to those events when joining. Use `subscribeAll()` for rooms without default subscriptions or when you need to ensure you receive all events. ### 4.7 room.unsubscribe() Unsubscribes from an event type. **Signature:** ```typescript room.unsubscribe(eventName: string): void ``` **Example:** ```typescript // Stop receiving typing events room.unsubscribe("typing"); ``` ### 4.8 room.leave() Leaves the room and cleans up all event handlers. **Signature:** ```typescript room.leave(): void ``` **Example:** ```typescript // Leave when closing a chat room.leave(); ``` ### 4.8 room.getHistory() Fetches historical events for a specific event type with pagination. **Signature:** ```typescript room.getHistory(eventName: string, start?: number, end?: number): Promise ``` **Parameters:** - **`eventName`**: The event type to fetch history for - **`start`**: Starting index (0 = most recent). Defaults to 0 - **`end`**: Ending index (exclusive). Defaults to 50 **Returns:** Promise resolving to array of historical events (newest first) **Example:** ```typescript // Get the last 20 messages const recentMessages = await room.getHistory("message", 0, 20); // Paginate: skip first 20, get next 20 const olderMessages = await room.getHistory("message", 20, 40); // Load more on scroll async function loadMore() { const currentCount = messages.length; const older = await room.getHistory("message", currentCount, currentCount + 20); setMessages((prev) => [...prev, ...older]); } ``` ## 5. Complete Example ```typescript import { createDialogueClient } from "./client"; async function main() { // Create client with authentication const client = createDialogueClient({ url: "ws://localhost:3000", auth: { userId: "user-123", token: "my-jwt-token", }, }); // Set up connection handlers client.onConnect(() => { console.log("Connected as:", client.userId); }); client.onDisconnect((reason) => { console.log("Disconnected:", reason); }); client.onError((error) => { console.error("Error:", error.message); }); // Connect to server await client.connect(); // List available rooms const rooms = await client.listRooms(); console.log("Available rooms:", rooms); // Join chat room const chat = await client.join("chat"); console.log(`Joined: ${chat.roomName}`); // Listen for messages chat.on<{ text: string; senderId: string }>("message", (msg) => { console.log(`[${msg.from}] ${msg.data.text}`); }); // Listen for typing indicators chat.on<{ isTyping: boolean }>("typing", (msg) => { if (msg.data.isTyping) { console.log(`${msg.from} is typing...`); } }); // Send a message chat.trigger("message", { text: "Hello, everyone!", senderId: client.userId, }); // Send typing indicator chat.trigger("typing", { isTyping: true }); // Later: leave room and disconnect // chat.leave(); // client.disconnect(); } main().catch(console.error); ``` ## 6. React Integration ### 6.1 Basic Hook ```typescript import { useEffect, useState } from "react"; import { createDialogueClient } from "./client"; import type { RoomContext } from "./client/types"; export function useDialogue(url: string, userId: string) { const [client] = useState( () => createDialogueClient({ url, auth: { userId }, autoConnect: false, }) ); const [connected, setConnected] = useState(false); useEffect(() => { client.onConnect(() => setConnected(true)); client.onDisconnect(() => setConnected(false)); client.connect(); return () => { client.disconnect(); }; }, [client]); return { client, connected }; } export function useRoom(client: ReturnType, roomId: string) { const [room, setRoom] = useState(null); useEffect(() => { if (!client.connected) return; client.join(roomId).then(setRoom); return () => { room?.leave(); }; }, [client, roomId, client.connected]); return room; } ``` ### 6.2 Usage in Component ```typescript function ChatRoom({ roomId }: { roomId: string }) { const { client, connected } = useDialogue("ws://localhost:3000", "user-123"); const room = useRoom(client, roomId); const [messages, setMessages] = useState([]); useEffect(() => { if (!room) return; const unsubscribe = room.on("message", (msg) => { setMessages((prev) => [...prev, msg]); }); return unsubscribe; }, [room]); if (!connected || !room) { return
Connecting...
; } return (

{room.roomName}

{messages.map((msg) => (
{msg.from}: {msg.data.text}
))}
); } ``` *This documentation reflects the current implementation and is subject to evolution. Contributions and feedback are welcome.* --- url: /dialogue/guide/others/architecture.md --- # Dialogue Architecture This document describes the internal architecture, design decisions, and component interactions of the Dialogue real-time communication library. ## 1. Overview Dialogue is an event-based realtime communication library built on Socket.IO and Hono for Bun/Node environments. The architecture prioritizes simplicity, type safety, and predictable behavior over flexibility. ### 1.1 Core Philosophy - **Config-first**: All rooms and events defined upfront in one file - **Event-centric**: Events are first-class citizens, not just message payloads - **Bounded rooms**: Optional `maxSize` for predictable scaling - **Same mental model**: Frontend and backend share similar patterns - **Extensible**: Designed for future SSE, Web Push, and FCM channels ### 1.2 Technology Stack - **Bun**: JavaScript runtime and bundler - **Socket.IO**: WebSocket abstraction with fallbacks - **Hono**: Lightweight HTTP framework - **Zod**: Runtime schema validation - **slang-ts**: Result pattern utilities (`Ok`, `Err`, `Result`) ## 2. System Architecture ### 2.1 High-Level Component Diagram ``` +------------------+ WebSocket +------------------+ | | <----------------> | | | DialogueClient | Socket.IO | Dialogue | | (Frontend) | | (Backend) | | | | | +------------------+ +------------------+ | | v v +------------------+ +------------------+ | RoomContext | | RoomManager | | (per room) | | (coordinator) | +------------------+ +------------------+ | +-----------+-----------+ | | | v v v +-------+ +-------+ +-------+ | Room | | Room | | Room | | chat | | orders| | ... | +-------+ +-------+ +-------+ ``` ### 2.2 Component Responsibilities | Component | Responsibility | |-----------|----------------| | `Dialogue` | Main API surface, coordinates rooms, triggers events | | `RoomManager` | Tracks all rooms and their participants | | `Room` | Manages participants, subscriptions, event broadcasting | | `ConnectedClient` | Wraps socket with user context and subscriptions | | `DialogueClient` | Frontend client factory for connecting and joining rooms | | `RoomContext` | Frontend room handle for triggering and listening | ## 3. Backend Architecture ### 3.1 Module Structure ``` dialogue/ types.ts # Type definitions (interfaces, no implementation) define-event.ts # Event definition factory with Zod validation room.ts # Room creation and room manager client-handler.ts # Connected client wrapper server.ts # Socket.IO + Hono + Bun server setup create-dialogue.ts # Main factory function index.ts # Barrel exports ``` ### 3.2 Initialization Flow When `createDialogue(config)` is called: ``` 1. createDialogue(config) | +--> Create or use existing Hono app | +--> setupServer(app, config) | +--> Create Socket.IO server | +--> Create BunEngine adapter | +--> io.bind(engine) | +--> createRoomManager(io) | | | +--> For each room in config: | roomManager.register(id, config) | +--> Set up connection handler | | | +--> io.on("connection", ...) | +--> Return { io, roomManager, start, stop } | +--> Return Dialogue instance ``` ### 3.3 Room Manager The `RoomManager` is the central coordinator for all rooms. It maintains two parallel maps: ```typescript const rooms = new Map(); const roomParticipants = new Map>(); ``` **Why two maps?** The `Room` instance is immutable after creation. Participant tracking is handled separately in `roomParticipants` to allow the room manager to enforce capacity limits across all operations. ### 3.4 Event Flow (Server-Side Trigger) When `dialogue.trigger(roomId, event, data)` is called: ``` 1. dialogue.trigger(roomId, event, data) | +--> roomManager.get(roomId) | +--> room.trigger(event, data, from) | +--> isEventAllowed(event.name, config.events) | +--> validateEventData(eventDef, data) [Zod validation] | +--> Create EventMessage envelope | { | event: "message", | roomId: "chat", | data: { text: "Hello" }, | from: "user-123", | timestamp: 1707750000000 | } | +--> io.to(roomId).emit("dialogue:event", message) | +--> Call all registered event handlers ``` ### 3.5 Event Flow (Client-Triggered) When a client triggers an event via WebSocket: ``` 1. Client emits "dialogue:trigger" { roomId, event, data } | +--> Server validates roomId and event name | +--> roomManager.get(roomId) | +--> Check if event is allowed in room | +--> room.trigger(eventDef, data, client.userId) | +--> [Same flow as server-side trigger] ``` ## 4. Client Architecture ### 4.1 Module Structure ``` client/ types.ts # Client-side type definitions dialogue-client.ts # Main DialogueClient class room-context.ts # RoomContext factory index.ts # Barrel exports ``` ### 4.2 Connection Flow ``` 1. createDialogueClient({ url, auth }) | +--> Create socket.io-client instance | +--> Connect with auth in handshake | +--> Wait for "dialogue:connected" event | +--> Extract userId from response | +--> Set connected = true ``` ### 4.3 Room Join Flow ``` 1. client.join("chat") | +--> socket.emit("dialogue:join", { roomId: "chat" }) | +--> Wait for "dialogue:joined" event | +--> createRoomContext(socket, roomId, roomName) | +--> Return RoomContext ``` ### 4.4 RoomContext Event Handling The `RoomContext` listens for `dialogue:event` messages and filters by room: ```typescript socket.on("dialogue:event", (msg) => { if (msg.roomId !== roomId) return; // Call specific event handlers const handlers = eventHandlers.get(msg.event); if (handlers) { handlers.forEach(h => h(msg)); } // Call wildcard handlers anyHandlers.forEach(h => h(msg.event, msg)); }); ``` ## 5. Wire Protocol ### 5.1 Socket.IO Events All events are prefixed with `dialogue:` to avoid conflicts. **Client to Server:** | Event | Payload | Description | |-------|---------|-------------| | `dialogue:join` | `{ roomId }` | Request to join room | | `dialogue:leave` | `{ roomId }` | Request to leave room | | `dialogue:subscribe` | `{ roomId, eventName }` | Subscribe to event | | `dialogue:subscribeAll` | `{ roomId }` | Subscribe to all events | | `dialogue:unsubscribe` | `{ roomId, eventName }` | Unsubscribe from event | | `dialogue:trigger` | `{ roomId, event, data }` | Trigger event | | `dialogue:listRooms` | (none) | Request room list | **Server to Client:** | Event | Payload | Description | |-------|---------|-------------| | `dialogue:connected` | `{ clientId, userId }` | Connection established | | `dialogue:joined` | `{ roomId, roomName }` | Successfully joined room | | `dialogue:left` | `{ roomId }` | Successfully left room | | `dialogue:event` | `EventMessage` | Event broadcast | | `dialogue:rooms` | `RoomInfo[]` | Room list response | | `dialogue:error` | `{ code, message }` | Error notification | ### 5.2 EventMessage Envelope All events are wrapped in a consistent envelope: ```typescript interface EventMessage { event: string; // Event name (e.g., "message") roomId: string; // Room ID (e.g., "chat") data: T; // Event payload from: string; // Sender's userId timestamp: number; // Unix timestamp in milliseconds } ``` ## 6. Design Decisions ### 6.1 Config-First with Dynamic Creation Dialogue is designed with a **config-first philosophy** while supporting dynamic room creation for flexibility. #### Recommended Approach (80/20 Rule) **80% Predefined Rooms** (config-first): ```typescript const dialogue = createDialogue({ rooms: [ { id: 'lobby', name: 'Main Lobby', events: [...] }, { id: 'notifications', name: 'Notifications', events: [...] }, { id: 'support', name: 'Support Chat', events: [...] } ] }); ``` **Benefits:** - Type safety and validation at startup - Clear system architecture - Predictable resource usage - Better documentation **20% Dynamic Rooms** (runtime creation): ```typescript // User creates a game room dialogue.createRoom({ id: `game-${gameId}`, name: `Game ${gameId}`, events: gameEvents }); // Clean up when done dialogue.deleteRoom(`game-${gameId}`); ``` **Use for:** - User-generated content (custom game rooms, DMs) - Temporary sessions (video calls, screen shares) - Per-entity rooms (document editing, ticket threads) #### Hybrid Example ```typescript // Predefined: System-wide rooms const systemRooms = [ { id: 'global-chat', name: 'Chat', events: [chatEvent] }, { id: 'notifications', name: 'Notifications', events: [notifEvent] } ]; const dialogue = createDialogue({ rooms: systemRooms }); // Dynamic: User-specific rooms app.post('/games', async (c) => { const gameId = nanoid(); dialogue.createRoom({ id: `game-${gameId}`, name: 'Game Session', events: [moveEvent, scoreEvent], maxSize: 4 }); return c.json({ gameId }); }); ``` #### When to Use Each | Use Case | Approach | Example | |----------|----------|---------| | System-wide features | Predefined | Notifications, global chat | | Known room types | Predefined | Support channels, lobbies | | User-generated | Dynamic | Private DMs, custom games | | Temporary sessions | Dynamic | Video calls, collaborations | | Per-entity rooms | Dynamic | Document editing, tickets | **Key principle:** If you know the room type at build time, define it in config. If it's created by user actions, create it dynamically. ### 6.2 Why Event-Centric? **Problem**: Generic "message" events require runtime type checking and are error-prone. **Solution**: First-class event definitions with optional Zod schemas: ```typescript const Message = defineEvent("message", { schema: z.object({ text: z.string(), senderId: z.string() }) }); ``` Benefits: - Compile-time type inference - Runtime validation - Self-documenting code - IDE autocomplete ### 6.3 Why Bounded Rooms? **Problem**: Unbounded rooms can grow indefinitely, causing memory issues and performance degradation. **Solution**: Optional `maxSize` configuration: ```typescript rooms: { chat: { name: "Support Chat", maxSize: 50, // Enforced at join time events: [Message] } } ``` ### 6.4 Why Separate RoomManager? **Problem**: Rooms need to track participants, but participant state must be consistent across the system. **Solution**: The `RoomManager` owns participant state in a separate map, ensuring: - Consistent capacity enforcement - Single source of truth for participants - Clean separation between room definition and runtime state ### 6.5 Why Socket.IO Over Raw WebSockets? **Advantages**: - Automatic reconnection - Fallback transports (polling) - Built-in room abstraction - Mature, well-tested library - Easy integration with existing infrastructure **Trade-offs**: - Larger bundle size - Additional protocol overhead - Less control over low-level behavior ## 7. Data Flow Diagrams ### 7.1 Client Sends Message ``` DialogueClient Socket.IO Dialogue Room | | | | | trigger("message", | | | | { text: "Hi" }) | | | |-------------------->| | | | | dialogue:trigger | | | |----------------->| | | | | room.trigger() | | | |--------------->| | | | | | | | validate() | | | |<---------------| | | | | | | io.to(roomId) | | | | .emit() | | | |<-----------------| | | dialogue:event | | | |<--------------------| | | | | | | ``` ### 7.2 Server Broadcasts Event ``` API Route Dialogue Room Clients | | | | | trigger("orders", | | | | OrderUpdated, | | | | { status: ... }) | | | |------------------->| | | | | room.trigger() | | | |--------------->| | | | | | | | | validate() | | | | | | | io.to(roomId) | | | | .emit() | | | |--------------->|--------------->| | | | | ``` ## 8. Security Considerations ### 8.1 Authentication Authentication is handled via Socket.IO handshake: ```typescript const client = createDialogueClient({ url: "ws://localhost:3000", auth: { token: "user-jwt-token" } }); ``` The server extracts user identity in `extractUserFromSocket()`: ```typescript export function extractUserFromSocket(socket: Socket) { const auth = socket.handshake.auth; // Extract userId from token or auth payload // Return { userId, meta } } ``` ### 8.2 Event Validation All events with Zod schemas are validated before broadcasting. Validation returns a `Result` using the slang-ts pattern: ```typescript const validation = validateEventData(eventDef, data); if (validation.isErr) { // Reject invalid data - validation.error contains the error message return; } // validation.value contains the validated data ``` ### 8.3 Room Access Control Room access can be controlled in the `onConnect` handler: ```typescript onConnect: (client) => { if (client.meta.role === "admin") { client.join("admin-room"); } } ``` ## 9. Scalability Considerations ### 9.1 Current Limitations - Single server instance only - In-memory participant tracking - No persistence layer ### 9.2 Future Scaling Options **Horizontal Scaling**: Add Redis adapter for multi-instance: ```typescript // Future API (not implemented) import { createRedisAdapter } from "dialogue/adapters/redis"; const dialogue = createDialogue({ adapter: createRedisAdapter({ host: "localhost", port: 6379 }), // ... }); ``` **Persistence Layer**: Add event persistence interface: ```typescript // Future API (not implemented) const dialogue = createDialogue({ persistence: { saveEvent: (msg) => db.events.insert(msg), loadEvents: (roomId, limit) => db.events.find({ roomId }).limit(limit) } }); ``` ## 10. Performance Characteristics ### 10.1 Memory Usage - Each connected client: ~1-2 KB (socket + metadata) - Each room: ~200 bytes + participants - Event handlers: ~100 bytes per handler ### 10.2 Message Latency - Local (same machine): < 1ms - Network: RTT + ~1-2ms processing ### 10.3 Throughput - Depends on Bun/Node event loop - Socket.IO overhead: ~5-10% vs raw WebSockets - Zod validation: ~0.1ms per event (for typical payloads) ## 11. Extension Points ### 11.1 Custom Authentication Override `extractUserFromSocket()` for custom auth strategies: ```typescript // Verify JWT, check database, etc. export function extractUserFromSocket(socket: Socket) { const token = socket.handshake.auth.token; const user = verifyJWT(token); return { userId: user.id, meta: { role: user.role } }; } ``` ### 11.2 Event Middleware (Future) Planned middleware pipeline for events: ```typescript // Future API (not implemented) dialogue.use("chat", (msg, next) => { // Rate limiting, content filtering, etc. if (isSpam(msg)) return; next(); }); ``` ### 11.3 Additional Channels (Future) Planned support for alternative delivery channels: - **SSE**: Server-sent events for one-way server to client - **Web Push**: Push notifications via FCM/APNS - **HTTP Polling**: For environments without WebSocket support *This specification reflects the current implementation and is subject to evolution. Contributions and feedback are welcome.* --- url: /dialogue/guide/others/llms-txt.md --- # LLMs.txt [View LLMs.txt](/llms.txt) --- url: /dialogue/guide/others/llms-full-txt.md --- # LLMs Full [View LLMs Full](/llms-full.txt) --- url: /dialogue/index.md ---