{"id":47739835,"url":"https://github.com/kartikrocks/wshub","last_synced_at":"2026-04-06T16:04:15.351Z","repository":{"id":343056622,"uuid":"1160152945","full_name":"KARTIKrocks/wshub","owner":"KARTIKrocks","description":"Production-ready WebSocket hub for Go — rooms, broadcasting, middleware, hooks, and pluggable metrics/logging. Zero business logic, pure infrastructure.","archived":false,"fork":false,"pushed_at":"2026-04-03T05:46:55.000Z","size":3117,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-03T08:29:22.987Z","etag":null,"topics":["broadcasting","go","golang","gorilla-websocket","hub","middleware","realtime","rooms","websocket","websocket-server"],"latest_commit_sha":null,"homepage":"https://kartikrocks.github.io/wshub/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/KARTIKrocks.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-17T15:53:26.000Z","updated_at":"2026-04-03T05:46:56.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/KARTIKrocks/wshub","commit_stats":null,"previous_names":["kartikrocks/wshub"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/KARTIKrocks/wshub","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KARTIKrocks%2Fwshub","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KARTIKrocks%2Fwshub/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KARTIKrocks%2Fwshub/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KARTIKrocks%2Fwshub/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/KARTIKrocks","download_url":"https://codeload.github.com/KARTIKrocks/wshub/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KARTIKrocks%2Fwshub/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31479009,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-06T14:34:32.243Z","status":"ssl_error","status_checked_at":"2026-04-06T14:34:31.723Z","response_time":112,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["broadcasting","go","golang","gorilla-websocket","hub","middleware","realtime","rooms","websocket","websocket-server"],"created_at":"2026-04-02T23:34:18.225Z","updated_at":"2026-04-06T16:04:15.343Z","avatar_url":"https://github.com/KARTIKrocks.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# wshub\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/KARTIKrocks/wshub.svg)](https://pkg.go.dev/github.com/KARTIKrocks/wshub)\n[![Go Report Card](https://goreportcard.com/badge/github.com/KARTIKrocks/wshub)](https://goreportcard.com/report/github.com/KARTIKrocks/wshub)\n[![Go Version](https://img.shields.io/github/go-mod/go-version/KARTIKrocks/wshub)](go.mod)\n[![CI](https://github.com/KARTIKrocks/wshub/actions/workflows/ci.yml/badge.svg)](https://github.com/KARTIKrocks/wshub/actions/workflows/ci.yml)\n[![GitHub tag](https://img.shields.io/github/v/tag/KARTIKrocks/wshub)](https://github.com/KARTIKrocks/wshub/releases)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![codecov](https://codecov.io/gh/KARTIKrocks/wshub/branch/main/graph/badge.svg)](https://codecov.io/gh/KARTIKrocks/wshub)\n\nA production-ready, scalable WebSocket package for Go with support for rooms, broadcasting, multi-node clustering, middleware, hooks, and extensibility.\n\n**[Documentation](https://kartikrocks.github.io/wshub/)** | **[API Reference](https://pkg.go.dev/github.com/KARTIKrocks/wshub)**\n\n## Features\n\n- **Production-Ready**: Proper concurrency, graceful shutdown \u0026 drain, error handling\n- **Horizontally Scalable**: Multi-node support via adapter pattern (Redis, NATS, or custom)\n- **Pluggable**: Bring your own logger, metrics\n- **Middleware System**: Chain handlers with custom logic\n- **Lifecycle Hooks**: Hook into connection, message, room, and backpressure events\n- **Room Support**: Group clients into rooms for targeted broadcasting\n- **Metrics \u0026 Logging**: Built-in interfaces for observability\n- **Configurable**: Extensive configuration with builder pattern\n- **Limits \u0026 Rate Limiting**: Control connections, rooms, and message rates\n- **Backpressure Control**: Configurable drop policies with notification hooks\n- **Write Coalescing**: Opt-in batching of text messages into single frames for reduced syscalls\n- **Health Probes**: Built-in `/healthz` and `/readyz` handlers with JSON responses for Kubernetes\n- **Global Counts**: Cluster-wide client and room counts via presence gossip\n- **Zero Business Logic**: Pure infrastructure, bring your own logic\n\n## Performance Highlights\n\nZero-allocation broadcasting, nanosecond lookups — built for scale. ([Full benchmarks](#benchmarks))\n\n| Operation                | Scale             | Time    | Allocs |\n| ------------------------ | ----------------- | ------- | ------ |\n| `Broadcast`              | 100,000 clients   | 22.0 ms | 0      |\n| `Broadcast`              | 1,000,000 clients | 263 ms  | 0      |\n| `BroadcastToRoom`        | 1,000,000 clients | 260 ms  | 0      |\n| `BroadcastParallel`      | 50,000 clients    | 5.5 ms  | 1      |\n| `SendToClient`           | 1,000,000 clients | 130 ns  | 0      |\n| `SendToUser`             | 1,000,000 users   | 192 ns  | 1      |\n| `GetClient`              | 1,000 clients     | 17.7 ns | 0      |\n| `GlobalClientCount`      | 500 nodes         | 4.2 μs  | 0      |\n| Middleware chain (built) | 3 middlewares     | 14.3 ns | 0      |\n\n\u003e Message size has no impact on dispatch — 64 B and 64 KB both take ~5.7 μs for 100 clients.\n\n## Installation\n\n```bash\ngo get github.com/KARTIKrocks/wshub\n```\n\n## Quick Start\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"log\"\n    \"net/http\"\n    \"time\"\n\n    \"github.com/KARTIKrocks/wshub\"\n)\n\nfunc main() {\n    // Create hub with configuration\n    config := wshub.DefaultConfig().\n        WithMaxMessageSize(1024 * 1024).\n        WithCompression(true)\n\n    hub := wshub.NewHub(\n        wshub.WithConfig(config),\n        wshub.WithMessageHandler(func(client *wshub.Client, msg *wshub.Message) error {\n            log.Printf(\"Message from %s: %s\", client.ID, msg.Text())\n            return client.Send(msg.Data)\n        }),\n    )\n\n    // Start the hub\n    go hub.Run()\n\n    // Set up HTTP handler\n    http.HandleFunc(\"/ws\", hub.HandleHTTP())\n\n    log.Println(\"Server starting on :8080\")\n    if err := http.ListenAndServe(\":8080\", nil); err != nil {\n        log.Fatal(err)\n    }\n\n    // Graceful drain + shutdown\n    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n    defer cancel()\n    hub.Drain(ctx)    // stop new connections, wait for existing ones\n    hub.Shutdown(ctx) // force-close anything remaining\n}\n```\n\n## Configuration\n\n### Basic Configuration\n\n```go\nconfig := wshub.DefaultConfig()\n\n// Or customize\nconfig := wshub.Config{\n    ReadBufferSize:    4096,\n    WriteBufferSize:   4096,\n    WriteWait:         10 * time.Second,\n    PongWait:          60 * time.Second,\n    PingPeriod:        54 * time.Second,\n    MaxMessageSize:    1024 * 1024,\n    SendChannelSize:   512,\n    EnableCompression: true,\n    CheckOrigin:       wshub.AllowAllOrigins,\n}\n```\n\n### Builder Pattern\n\n```go\nconfig := wshub.DefaultConfig().\n    WithBufferSizes(4096, 4096).\n    WithMaxMessageSize(1024 * 1024).\n    WithCompression(true).\n    WithCheckOrigin(wshub.AllowOrigins(\"https://example.com\"))\n```\n\n### Origin Checking\n\n```go\n// Allow all origins (default)\nconfig.CheckOrigin = wshub.AllowAllOrigins\n\n// Allow same origin only\nconfig.CheckOrigin = wshub.AllowSameOrigin\n\n// Allow specific origins\nconfig.CheckOrigin = wshub.AllowOrigins(\"https://example.com\", \"https://app.example.com\")\n\n// Custom checker\nconfig.CheckOrigin = func(r *http.Request) bool {\n    return strings.HasSuffix(r.Header.Get(\"Origin\"), \".example.com\")\n}\n```\n\n## Hub API\n\n### Client Management\n\n```go\n// Get all clients\nclients := hub.Clients()\ncount := hub.ClientCount()\n\n// Find client\nclient, ok := hub.GetClient(clientID)\nclient, ok := hub.GetClientByUserID(userID)\nclients := hub.GetClientsByUserID(userID)\n```\n\n### Broadcasting\n\n```go\n// Broadcast to all\nhub.Broadcast([]byte(\"Hello everyone\"))\nhub.BroadcastText(\"Hello everyone\")\nhub.BroadcastJSON(map[string]string{\"message\": \"Hello\"})\n\n// Broadcast pre-encoded JSON (zero-alloc, ideal for fan-out)\ndata, _ := json.Marshal(map[string]string{\"message\": \"Hello\"})\nhub.BroadcastRawJSON(data)\n\n// Broadcast with context\nctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\nhub.BroadcastWithContext(ctx, data)\n\n// Broadcast except one client\nhub.BroadcastExcept(data, excludeClient)\n\n// Send to specific client\nhub.SendToClient(clientID, data)\n\n// Send to all connections of a user\nhub.SendToUser(userID, data)\n```\n\n### Rooms\n\n```go\n// Join/leave rooms\nhub.JoinRoom(client, \"general\")\nhub.LeaveRoom(client, \"general\")\nhub.LeaveAllRooms(client)\n\n// Broadcast to room\nhub.BroadcastToRoom(\"general\", data)\nhub.BroadcastToRoomExcept(\"general\", data, exceptClient)\n\n// Room info\nclients := hub.RoomClients(\"general\")\ncount := hub.RoomCount(\"general\")\nrooms := hub.RoomNames()\nexists := hub.RoomExists(\"general\")\n```\n\n## Client API\n\n```go\n// Client properties\nclient.ID       // Unique client ID\n\n// Set user ID\nclient.SetUserID(\"user-123\")\nuserID := client.GetUserID()\n\n// Metadata\nclient.SetMetadata(\"role\", \"admin\")\nrole, ok := client.GetMetadata(\"role\")\nclient.DeleteMetadata(\"role\")\n\n// Send messages\nclient.Send([]byte(\"Hello\"))\nclient.SendText(\"Hello\")\nclient.SendJSON(map[string]string{\"message\": \"Hello\"})\nclient.SendRawJSON(preEncodedJSON) // skip marshaling\nclient.SendBinary(data)\nclient.SendWithContext(ctx, data)\n\n// Close connection\nclient.Close()\nclient.CloseWithCode(websocket.CloseNormalClosure, \"Goodbye\")\n\n// Room membership\nrooms := client.Rooms()\ninRoom := client.InRoom(\"general\")\ncount := client.RoomCount()\n\n// Status\nclosed := client.IsClosed()\nclosedAt := client.ClosedAt()\n\n// Client-specific handlers\nclient.OnMessage(func(c *wshub.Client, msg *wshub.Message) {\n    // Handle message\n})\n\nclient.OnClose(func(c *wshub.Client) {\n    // Handle close\n})\n\nclient.OnError(func(c *wshub.Client, err error) {\n    // Handle error\n})\n```\n\n## Hooks System\n\n```go\nhub := wshub.NewHub(\n    wshub.WithHooks(wshub.Hooks{\n        // Before connection upgrade\n        BeforeConnect: func(r *http.Request) error {\n            token := r.Header.Get(\"Authorization\")\n            if !validateToken(token) {\n                return wshub.ErrAuthenticationFailed\n            }\n            return nil\n        },\n\n        // After successful connection\n        AfterConnect: func(client *wshub.Client) {\n            log.Printf(\"Client connected: %s\", client.ID)\n        },\n\n        // Before message processing\n        BeforeMessage: func(client *wshub.Client, msg *wshub.Message) (*wshub.Message, error) {\n            if len(msg.Data) \u003e 1000 {\n                return nil, errors.New(\"message too large\")\n            }\n            return msg, nil\n        },\n\n        // After message processing\n        AfterMessage: func(client *wshub.Client, msg *wshub.Message, err error) {\n            if err != nil {\n                log.Printf(\"Message error: %v\", err)\n            }\n        },\n\n        // Before room join\n        BeforeRoomJoin: func(client *wshub.Client, room string) error {\n            if !canJoinRoom(client, room) {\n                return wshub.ErrUnauthorized\n            }\n            return nil\n        },\n\n        // After room join\n        AfterRoomJoin: func(client *wshub.Client, room string) {\n            hub.BroadcastToRoomExcept(room,\n                []byte(fmt.Sprintf(\"%s joined\", client.ID)),\n                client,\n            )\n        },\n\n        // On error\n        OnError: func(client *wshub.Client, err error) {\n            log.Printf(\"Client error: %v\", err)\n        },\n    }),\n)\n```\n\n## Middleware System\n\n```go\n// Create middleware chain\nchain := wshub.NewMiddlewareChain(handleMessage).\n    Use(wshub.RecoveryMiddleware(logger)).\n    Use(wshub.LoggingMiddleware(logger)).\n    Use(wshub.MetricsMiddleware(metrics)).\n    Build()\n\n// Use in message handler\nhub := wshub.NewHub(\n    wshub.WithMessageHandler(chain.Execute),\n)\n```\n\n### Built-in Middlewares\n\n```go\n// Logging\nwshub.LoggingMiddleware(logger)\n\n// Panic recovery\nwshub.RecoveryMiddleware(logger)\n\n// Metrics\nwshub.MetricsMiddleware(metrics)\n```\n\n### Custom Middleware\n\n```go\nfunc RateLimitMiddleware(limiter RateLimiter) wshub.Middleware {\n    return func(next wshub.HandlerFunc) wshub.HandlerFunc {\n        return func(client *wshub.Client, msg *wshub.Message) error {\n            if !limiter.Allow(client.ID) {\n                return wshub.ErrRateLimitExceeded\n            }\n            return next(client, msg)\n        }\n    }\n}\n\nfunc AuthMiddleware(auth AuthService) wshub.Middleware {\n    return func(next wshub.HandlerFunc) wshub.HandlerFunc {\n        return func(client *wshub.Client, msg *wshub.Message) error {\n            if client.GetUserID() == \"\" {\n                return wshub.ErrUnauthorized\n            }\n            return next(client, msg)\n        }\n    }\n}\n```\n\n## Logging\n\n```go\n// Implement the Logger interface\ntype ZapLogger struct {\n    logger *zap.Logger\n}\n\nfunc (l *ZapLogger) Debug(msg string, args ...any) {\n    l.logger.Sugar().Debugw(msg, args...)\n}\n\nfunc (l *ZapLogger) Info(msg string, args ...any) {\n    l.logger.Sugar().Infow(msg, args...)\n}\n\nfunc (l *ZapLogger) Warn(msg string, args ...any) {\n    l.logger.Sugar().Warnw(msg, args...)\n}\n\nfunc (l *ZapLogger) Error(msg string, args ...any) {\n    l.logger.Sugar().Errorw(msg, args...)\n}\n\n// Use it\nhub := wshub.NewHub(wshub.WithLogger(\u0026ZapLogger{logger}))\n```\n\n## Metrics\n\n```go\n// Implement the MetricsCollector interface\ntype PrometheusMetrics struct {\n    connections   prometheus.Gauge\n    messages      prometheus.Counter\n    messageSize   prometheus.Histogram\n    errors        *prometheus.CounterVec\n}\n\nfunc (m *PrometheusMetrics) IncrementConnections() {\n    m.connections.Inc()\n}\n\nfunc (m *PrometheusMetrics) IncrementMessages() {\n    m.messages.Inc()\n}\n\nfunc (m *PrometheusMetrics) RecordMessageSize(size int) {\n    m.messageSize.Observe(float64(size))\n}\n\nfunc (m *PrometheusMetrics) IncrementErrors(errorType string) {\n    m.errors.WithLabelValues(errorType).Inc()\n}\n\n// ... implement other methods\n\n// Use it\nhub := wshub.NewHub(wshub.WithMetrics(NewPrometheusMetrics()))\n```\n\n## Limits\n\n```go\nlimits := wshub.DefaultLimits().\n    WithMaxConnections(10000).\n    WithMaxConnectionsPerUser(5).\n    WithMaxRoomsPerClient(10).\n    WithMaxClientsPerRoom(100).\n    WithMaxMessageRate(100)\n\nhub := wshub.NewHub(wshub.WithLimits(limits))\n```\n\n## Multi-Node Scaling\n\nScale horizontally by connecting multiple hub instances through a shared message bus. All broadcasts and targeted sends are automatically relayed across nodes.\n\n```go\nimport wshubredis \"github.com/KARTIKrocks/wshub/adapter/redis\"\n\nrdb := goredis.NewClient(\u0026goredis.Options{Addr: \"localhost:6379\"})\nadapter := wshubredis.New(rdb)\n\nhub := wshub.NewHub(\n    wshub.WithAdapter(adapter),\n    wshub.WithNodeID(\"pod-web-1\"), // optional: stable ID for debugging\n)\ngo hub.Run()\n```\n\n### Available Adapters\n\n| Adapter | Install                                             | Best For                     |\n| ------- | --------------------------------------------------- | ---------------------------- |\n| Redis   | `go get github.com/KARTIKrocks/wshub/adapter/redis` | Most deployments, easy setup |\n| NATS    | `go get github.com/KARTIKrocks/wshub/adapter/nats`  | Low-latency, high-throughput |\n| Custom  | Implement `wshub.Adapter` interface                 | Any message bus              |\n\nAdapters are separate Go modules -- importing the core `wshub` package never pulls in Redis or NATS dependencies.\n\n### What Gets Relayed Across Nodes\n\n| Operation                                                                            | Cross-Node         |\n| ------------------------------------------------------------------------------------ | ------------------ |\n| `Broadcast`, `BroadcastBinary`, `BroadcastText`, `BroadcastJSON`, `BroadcastRawJSON` | Yes                |\n| `BroadcastExcept`                                                                    | Yes                |\n| `BroadcastToRoom`, `BroadcastToRoomExcept`                                           | Yes                |\n| `SendToUser`                                                                         | Yes                |\n| `SendToClient`                                                                       | Yes                |\n| `JoinRoom`, `LeaveRoom`                                                              | No (local per hub) |\n| `GetClient`, `ClientCount`                                                           | No (local per hub) |\n\n### Global Counts (Presence)\n\nEnable presence gossip to get cluster-wide totals:\n\n```go\nhub := wshub.NewHub(\n    wshub.WithAdapter(adapter),\n    wshub.WithPresence(5 * time.Second), // publish stats every 5s\n)\n\nhub.GlobalClientCount()          // total across all nodes\nhub.GlobalRoomCount(\"general\")   // room members across all nodes\n```\n\nNodes that miss 3 consecutive heartbeats are automatically evicted from the totals.\n\n## Graceful Draining\n\nFor zero-downtime rolling deploys (e.g. Kubernetes), call `Drain` before `Shutdown`. Drain stops accepting new connections (HTTP 503) while letting existing connections finish their in-flight messages. Idle connections are proactively closed after the drain timeout.\n\n```go\n// preStop / SIGTERM handler\nctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\ndefer cancel()\nhub.Drain(ctx)    // stop new connections, wait for existing ones\nhub.Shutdown(ctx) // force-close anything remaining\n```\n\n### Configuration\n\n```go\nhub := wshub.NewHub(\n    // Configure idle connection reaper timeout (default: 30s).\n    // Connections idle for this duration during drain are closed with CloseGoingAway.\n    // Set to 0 to disable the reaper entirely.\n    wshub.WithDrainTimeout(15 * time.Second),\n)\n```\n\n### Health \u0026 Readiness Probes\n\n```go\n// Drop-in HTTP handlers — respond with JSON and correct status codes\nhttp.Handle(\"/healthz\", hub.HealthHandler()) // 200 while Run() is alive, else 503\nhttp.Handle(\"/readyz\", hub.ReadyHandler())   // 200 while running, 503 when draining/stopped\n\n// Programmatic access\nhs := hub.Health()  // HealthStatus{Alive, Ready, State, Uptime, Clients}\nhub.Alive()         // true only while Run() goroutine is executing\nhub.Ready()         // true when alive and in StateRunning\nhub.Uptime()        // time.Duration since Run() started (0 if not started or exited)\n```\n\n## Backpressure Control\n\nWhen a client's send buffer is full, configure how messages are handled:\n\n```go\nhub := wshub.NewHub(\n    // DropNewest (default): discard the new message\n    // DropOldest: evict the oldest queued message to make room\n    wshub.WithDropPolicy(wshub.DropOldest),\n\n    wshub.WithHooks(wshub.Hooks{\n        OnSendDropped: func(client *wshub.Client, data []byte) {\n            log.Printf(\"dropped %d bytes for client %s\", len(data), client.ID)\n            // Options: disconnect slow client, log, queue externally\n            // client.Close()\n        },\n    }),\n)\n```\n\n| Policy       | Behavior                     | Best For                                         |\n| ------------ | ---------------------------- | ------------------------------------------------ |\n| `DropNewest` | Discards the new message     | Default, safe                                    |\n| `DropOldest` | Evicts oldest queued message | Real-time data (dashboards, tickers, game state) |\n\n## Write Coalescing\n\nWhen throughput is high and messages queue up, enable write coalescing to batch multiple text messages into a single WebSocket frame separated by newlines (`\\n`). This reduces syscalls at the cost of receivers needing to split frames:\n\n```go\ncfg := wshub.DefaultConfig().WithCoalesceWrites(true)\nhub := wshub.NewHub(wshub.WithConfig(cfg))\n```\n\n- Only **text messages** are coalesced; binary messages are always sent as individual frames\n- Receivers must split coalesced frames on `\\n` to recover individual messages\n- When disabled (default), every message is its own frame — no behavior change\n\n## Error Handling\n\n```go\nerr := hub.JoinRoom(client, room)\nswitch err {\ncase wshub.ErrClientNotFound:\n    // Client not registered\ncase wshub.ErrAlreadyInRoom:\n    // Client already in room\ncase wshub.ErrEmptyRoomName:\n    // Empty room name\ncase wshub.ErrRoomNotFound:\n    // Room doesn't exist\ncase wshub.ErrNotInRoom:\n    // Client not in room\ncase wshub.ErrConnectionClosed:\n    // Connection was closed\ncase wshub.ErrSendBufferFull:\n    // Send buffer full\ncase wshub.ErrHubNotStarted:\n    // Hub Run() has not been called yet\ncase wshub.ErrHubDraining:\n    // Hub is draining, not accepting new connections\ncase wshub.ErrHubStopped:\n    // Hub has been shut down\ncase wshub.ErrMaxConnectionsReached:\n    // Connection limit reached\ncase wshub.ErrMaxRoomsReached:\n    // Room limit per client reached\ncase wshub.ErrRoomFull:\n    // Room is full\ncase wshub.ErrRateLimitExceeded:\n    // Rate limit exceeded\ncase wshub.ErrAuthenticationFailed:\n    // Authentication failed\ncase wshub.ErrUnauthorized:\n    // Unauthorized action\n}\n```\n\n## Complete Example: Chat Application\n\nSee [examples/chat/](examples/chat/) for a complete chat application demonstrating:\n\n- Room management\n- Username tracking\n- Message broadcasting\n- Middleware (recovery + logging)\n- Rate limiting\n- Connection limits\n\n## Test Client\n\nSave as `index.html` and open in a browser while the server is running:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003ctitle\u003eWebSocket Test\u003c/title\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003ch1\u003eWebSocket Test\u003c/h1\u003e\n    \u003cdiv\u003e\n      \u003cinput type=\"text\" id=\"message\" placeholder=\"Type a message\" /\u003e\n      \u003cbutton onclick=\"send()\"\u003eSend\u003c/button\u003e\n    \u003c/div\u003e\n    \u003cdiv id=\"messages\"\u003e\u003c/div\u003e\n\n    \u003cscript\u003e\n      const ws = new WebSocket(\"ws://localhost:8080/ws\");\n\n      ws.onopen = () =\u003e {\n        console.log(\"Connected\");\n        addMessage(\"Connected to server\");\n      };\n\n      ws.onmessage = (event) =\u003e {\n        addMessage(\"Received: \" + event.data);\n      };\n\n      ws.onclose = () =\u003e {\n        addMessage(\"Disconnected\");\n      };\n\n      ws.onerror = (error) =\u003e {\n        console.error(\"WebSocket error:\", error);\n        addMessage(\"Error occurred\");\n      };\n\n      function send() {\n        const input = document.getElementById(\"message\");\n        ws.send(input.value);\n        addMessage(\"Sent: \" + input.value);\n        input.value = \"\";\n      }\n\n      function addMessage(msg) {\n        const div = document.getElementById(\"messages\");\n        div.innerHTML += \"\u003cp\u003e\" + msg + \"\u003c/p\u003e\";\n      }\n    \u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n## Best Practices\n\n1. **Always use middleware for cross-cutting concerns** (logging, metrics, auth)\n2. **Use hooks for lifecycle events** instead of wrapping the hub\n3. **Implement proper logging and metrics** for production observability\n4. **Set appropriate limits** to prevent resource exhaustion\n5. **Use `Drain` then `Shutdown`** for zero-downtime deploys\n6. **Handle errors appropriately** - don't ignore send failures\n7. **Use rooms for targeted messaging** instead of filtering in handlers\n8. **Set user ID after authentication** for multi-device support\n9. **Use metadata for request-scoped data** instead of global state\n10. **Test with concurrent clients** to ensure thread safety\n\n## Performance Tips\n\n- Increase `SendChannelSize` for high-throughput scenarios\n- Enable compression for large messages\n- Use `BroadcastWithContext` for timeout control\n- Batch messages when possible\n- Monitor send buffer sizes via metrics\n- Use `WithParallelBroadcast(batchSize)` for 1000+ concurrent clients — dispatches batches to a persistent worker pool instead of spawning goroutines per broadcast\n- Use `WithParallelBroadcastWorkers(n)` to tune the pool size (default: `runtime.NumCPU()`)\n\n## Benchmarks\n\nMeasured on an Intel i5-11400H @ 2.70GHz (12 cores), Go 1.26, Linux. See [performance highlights](#performance-highlights) for a quick summary.\n\nRun them yourself:\n\n```bash\ngo test -bench=. -benchmem ./...\n```\n\n### Broadcasting (zero allocations)\n\n| Operation               | Clients   | Time    | Allocs |\n| ----------------------- | --------- | ------- | ------ |\n| `Broadcast`             | 100,000   | 22.0 ms | 0      |\n| `Broadcast`             | 1,000,000 | 263 ms  | 0      |\n| `BroadcastToRoom`       | 100,000   | 23.2 ms | 0      |\n| `BroadcastToRoom`       | 1,000,000 | 260 ms  | 0      |\n| `BroadcastExcept`       | 100,000   | 25.9 ms | 1      |\n| `BroadcastExcept`       | 1,000,000 | 294 ms  | 1      |\n| `BroadcastToRoomExcept` | 100,000   | 26.0 ms | 1      |\n| `BroadcastToRoomExcept` | 1,000,000 | 277 ms  | 1      |\n\n### Parallel Broadcast (worker pool, 0–1 allocs)\n\nUses a persistent worker pool instead of spawning goroutines per broadcast. The hub snapshot slice is pre-built on register/unregister, so parallel broadcasts allocate nothing beyond the pool task. Enable with `WithParallelBroadcast(batchSize)`.\n\n| Operation           | Clients | Time   | Allocs |\n| ------------------- | ------- | ------ | ------ |\n| `BroadcastParallel` | 100     | 5.6 μs | 0      |\n| `BroadcastParallel` | 10,000  | 989 μs | 1      |\n| `BroadcastParallel` | 50,000  | 5.5 ms | 1      |\n\n### Targeted Send (O(1) at any scale, zero allocations)\n\n| Operation      | Scale             | Time   | Allocs |\n| -------------- | ----------------- | ------ | ------ |\n| `SendToClient` | 100,000 clients   | 129 ns | 0      |\n| `SendToClient` | 1,000,000 clients | 130 ns | 0      |\n| `SendToUser`   | 100,000 users     | 198 ns | 1      |\n| `SendToUser`   | 1,000,000 users   | 192 ns | 1      |\n\n### Global Counts — Presence (zero allocations)\n\n| Operation           | Nodes | Time   | Allocs |\n| ------------------- | ----- | ------ | ------ |\n| `GlobalClientCount` | 5     | 63 ns  | 0      |\n| `GlobalClientCount` | 50    | 397 ns | 0      |\n| `GlobalClientCount` | 100   | 715 ns | 0      |\n| `GlobalClientCount` | 500   | 4.2 μs | 0      |\n| `GlobalRoomCount`   | 5     | 118 ns | 0      |\n| `GlobalRoomCount`   | 50    | 823 ns | 0      |\n| `GlobalRoomCount`   | 100   | 1.7 μs | 0      |\n| `GlobalRoomCount`   | 500   | 9.7 μs | 0      |\n\n### Message size has no impact on dispatch\n\n| Payload | Time (100 clients) | Allocs |\n| ------- | ------------------ | ------ |\n| 64 B    | 5.7 μs             | 0      |\n| 512 B   | 5.5 μs             | 0      |\n| 4 KB    | 5.4 μs             | 0      |\n| 64 KB   | 5.7 μs             | 0      |\n\n### Client \u0026 Room Lookups (zero allocations)\n\n| Operation                   | Time    | Allocs |\n| --------------------------- | ------- | ------ |\n| `GetClient` (1,000 clients) | 17.7 ns | 0      |\n| `ClientCount`               | 0.28 ns | 0      |\n| `GetClientByUserID`         | 51.3 ns | 0      |\n| `RoomExists`                | 23.6 ns | 0      |\n| `RoomCount`                 | 22.1 ns | 0      |\n| `GetMetadata`               | 17.0 ns | 0      |\n| `SetMetadata`               | 30.6 ns | 0      |\n\n### Client Send\n\n| Operation     | Time    | Allocs |\n| ------------- | ------- | ------ |\n| `Send` (text) | 82.9 ns | 1      |\n| `SendJSON`    | 495 ns  | 5      |\n\n### Middleware Chain\n\n| Mode                 | Time    | Allocs |\n| -------------------- | ------- | ------ |\n| Built (cached)       | 14.3 ns | 0      |\n| Unbuilt (on-the-fly) | 17.0 ns | 0      |\n\n\u003e Always call `Build()` on your middleware chain for best performance.\n\n### Concurrent Access (parallel goroutines)\n\n| Operation                 | Time    | Allocs |\n| ------------------------- | ------- | ------ |\n| `GetClient`               | 31.0 ns | 0      |\n| `ClientCount`             | 0.23 ns | 0      |\n| `Metadata` (set+get)      | 76.5 ns | 0      |\n| `Broadcast` (100 clients) | 5.9 μs  | 120    |\n\n### Message Creation\n\n| Operation           | Time    | Allocs |\n| ------------------- | ------- | ------ |\n| `NewMessage`        | 30.5 ns | 0      |\n| `NewTextMessage`    | 32.0 ns | 0      |\n| `NewBinaryMessage`  | 30.2 ns | 0      |\n| `NewJSONMessage`    | 820 ns  | 9      |\n| `NewRawJSONMessage` | 30.9 ns | 0      |\n\n## Thread Safety\n\nAll Hub and Client methods are thread-safe. The package uses:\n\n- RWMutex for client/room maps\n- Separate mutexes for callbacks\n- Channels for cross-goroutine communication\n- WaitGroups for graceful shutdown\n\n## License\n\n[MIT](LICENSE)\n\n## Contributing\n\nContributions welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkartikrocks%2Fwshub","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkartikrocks%2Fwshub","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkartikrocks%2Fwshub/lists"}