Skip to content

catalinfl/adler

Repository files navigation

Adler

Adler is a lightweight WebSocket server toolkit for Go. It gives you a small, focused API for upgrading HTTP requests, handling sessions, broadcasting messages, and organizing clients into rooms.

Features

  • WebSocket upgrade from net/http
  • Session lifecycle hooks
  • Text, binary, and JSON messaging
  • Protocol-aware JSON or Protobuf serialization (Write, BroadcastAny)
  • Global broadcast, filtered broadcast, and targeted session sends
  • Rooms with join, leave, and room-level broadcast helpers
  • Per-session key-value storage
  • Optional access to the underlying request, protocol, and connection

Roadmap

  • Add a dedicated matchmaker module for queue creation, queue management, and match orchestration.
  • Add ELO-based matchmaking with configurable rating buckets and match quality rules.

Installation

go get github.com/catalinfl/adler

Quick Start

package main

import (
    "log"
    "net/http"

    "github.com/catalinfl/adler"
)

func main() {
    a := adler.New()

    a.HandleConnect(func(s *adler.Session) {
        log.Println("connected:", s.RemoteAddr())
    })

    a.HandleMessage(func(s *adler.Session, msg []byte) {
        _ = s.WriteText([]byte("echo: " + string(msg)))
    })

    a.HandleError(func(s *adler.Session, err error) {
        log.Println("ws error:", err)
    })

    http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
        if err := a.HandleRequest(w, r); err != nil {
            log.Println("handle request:", err)
        }
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

How The API Works

1. Create a server

Use adler.New() to build a server instance. You can pass configuration options at construction time:

a := adler.New(
    adler.WithDispatchAsync(true),
    adler.WithMessageBufferSize(256),
    adler.WithPingPeriod(30),
)

2. Register handlers before serving requests

Handlers are set on the Adler instance and are called during the session lifecycle.

  • HandleConnect runs after a session is registered.
  • HandleMessage receives text frames.
  • HandleMessageBinary receives binary frames.
  • HandlePong receives pong frames.
  • HandleClose receives close code and reason.
  • HandleSentMessage and HandleSentMessageBinary run after server writes succeed.
  • HandleError receives runtime errors from the session loop.
  • OnRoomJoin and OnRoomLeave receive room membership events.

Example:

a.HandleConnect(func(s *adler.Session) {
    s.Set("userID", "123")
})

a.HandleClose(func(s *adler.Session, code int, reason string) {
    log.Printf("client closed: code=%d reason=%q", code, reason)
})

3. Serve the websocket endpoint

Call HandleRequest from an HTTP handler. It upgrades the connection and blocks until the session ends.

http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
    _ = a.HandleRequest(w, r)
})

4. Work with a Session

Session is the object you receive in callbacks. It exposes message writers and a small key-value store.

Messaging helpers:

  • Write(any) (protocol-aware JSON text or Protobuf binary)
  • WriteText([]byte)
  • WriteTextWithDeadline([]byte, time.Duration)
  • WriteBinary([]byte)
  • WriteBinaryWithDeadline([]byte, time.Duration)
  • WriteJSON(adler.Map)
  • WriteJSONWithDeadline(any, time.Duration)
  • Close(...[]byte)

Protocol-aware helpers (Write, BroadcastAny, Room.BroadcastAny) use the server protocol (JSON by default). Set adler.WithProtocol(adler.Protobuf) to emit protobuf binary frames.

Storage helpers:

  • Set(key, value)
  • SetNX(key, value)
  • Get(key)
  • GetString(key)
  • GetInt(key)
  • GetInt64(key)
  • GetFloat(key)
  • GetBool(key)
  • Has(key)
  • Unset(key)
  • Keys()
  • Values()
  • Clear()
  • Incr(key) and Decr(key) for *int64 counters

Metadata:

  • Request() returns the original HTTP request
  • Protocol() returns the HTTP protocol string used during upgrade
  • LocalAddr() and RemoteAddr() expose the connection addresses
  • Room() returns the current room
  • UnsafeConn() exposes the raw network connection when you need low-level control

Example session storage:

a.HandleConnect(func(s *adler.Session) {
    s.Set("role", "admin")
    s.SetNX("seen", true)
})

a.HandleMessage(func(s *adler.Session, msg []byte) {
    role, _ := s.GetString("role")
    _ = s.WriteText([]byte("role=" + role + " msg=" + string(msg)))
})

5. Broadcast to all or some clients

Use server-level broadcast helpers when you want to send to multiple sessions.

  • Broadcast([]byte) sends text to all connected sessions
  • BroadcastFilter([]byte, func(*Session) bool) sends text to matching sessions
  • BroadcastBinary([]byte) sends binary to all connected sessions
  • BroadcastBinaryFilter([]byte, func(*Session) bool) sends binary to matching sessions
  • BroadcastJSON(adler.Map) broadcasts JSON
  • BroadcastJSONFilter(adler.Map, func(*Session) bool) broadcasts JSON to matching sessions
  • BroadcastAny(any) broadcasts using the configured protocol
  • BroadcastOthers([]byte, *Session) sends to everyone except the target session
  • SendTo([]byte, *Session) sends only to one session

Example:

a.Broadcast([]byte("server says hello"))

a.SendTo([]byte("private message"), session)

a.BroadcastFilter([]byte("admins only"), func(s *adler.Session) bool {
    role, _ := s.GetString("role")
    return role == "admin"
})

6. Group clients in rooms

Rooms help you manage subsets of sessions.

room := a.NewRoom("lobby")

a.HandleConnect(func(s *adler.Session) {
    _ = room.Join(s)
})

room.Broadcast([]byte("welcome to the lobby"))

Room helpers:

  • NewRoom(name) returns an existing room or creates one
  • DeleteRoom(name) removes a room manually when it is empty
  • Name() returns the room name
  • Len() returns the number of members
  • Sessions() returns a snapshot of current members
  • Join(*Session) adds a session to the room
  • Leave(*Session) removes a session
  • OpenRoom() allows joins again
  • CloseRoom() blocks new joins
  • Broadcast, BroadcastBinary, BroadcastFilter, BroadcastJSON, BroadcastJSONFilter, BroadcastAny

7. Matchmaking with the Matchmaker Module

The matchmaker module provides queue-based matchmaking that groups sessions into rooms automatically. It uses a background goroutine to manage queues and ensure fair player distribution.

Features:

  • Non-blocking queue operations: AddToQueue() and RemoveFromQueue() send commands to a worker goroutine
  • Main and waiting queues: Keeps a main queue and an optional waiting queue when the main queue reaches capacity
  • Automatic room creation: Creates Adler rooms when enough players are queued
  • JSON event notifications: Sends events to sessions as they move through the queue

Matchmaker queue events are defined in matchmaker/matchmaking.proto and emitted as protobuf QueueStatus messages. When the Adler protocol is JSON (default), these events are marshaled to the same JSON shapes as before:

  • "queue_joined" - Session added to main queue
  • "wait_queue_joined" - Session added to waiting queue (main is full)
  • "promoted_to_queue" - Session promoted from waiting to main queue
  • "match_found" - Match room created with room_id and player count

Example:

import "github.com/catalinfl/adler/matchmaker"

mm := matchmaking.NewMatchmaker(a, 
    matchmaking.WithRoomSize(4),
    matchmaking.WithQueueLength(20),
)

a.HandleConnect(func(s *adler.Session) {
    _ = mm.AddToQueue(s)
})

a.HandleMessage(func(s *adler.Session, msg []byte) {
    if string(msg) == "leave_queue" {
        mm.RemoveFromQueue(s)
    }
})

// Use adler.WithProtocol(adler.Protobuf) to send queue events as protobuf frames.

8. Close handling

If the client sends a close frame, HandleClose receives the close status code and reason.

a.HandleClose(func(s *adler.Session, code int, reason string) {
    log.Printf("close: code=%d reason=%q", code, reason)
})

Configuration

Use these options with adler.New(...):

  • WithWriteWait(time.Duration) interprets the argument as seconds; WithWriteWait(10) means 10 seconds
  • WithPongWait(time.Duration) interprets the argument as seconds; WithPongWait(60) means 60 seconds, and WithPongWait(0) disables idle disconnects
  • WithPingPeriod(time.Duration) interprets the argument as seconds; WithPingPeriod(54) means 54 seconds
  • WithMessageBufferSize(int) sets the outbound queue size; start with 64-256 and increase only if you hit ErrBufferFull under normal bursts
  • WithDispatchAsync(bool) switches inbound dispatch to goroutine-per-message when enabled
  • WithDeleteRoomOnEmpty(bool) controls automatic room deletion when the last session leaves (default: true)
  • WithProtocol(adler.Protocol) selects adler.JSON (default) or adler.Protobuf for protocol-aware messaging

Notes On Concurrency

  • Session storage is protected by an internal mutex.
  • Server broadcast methods are safe for normal concurrent use.
  • UnsafeConn() bypasses Adler's internal coordination; only use it if you manage access carefully.

Minimal Room Example

room := a.NewRoom("test")

a.HandleConnect(func(s *adler.Session) {
    _ = room.Join(s)
})

room.HandleJoin(func(s *adler.Session) {
    _ = s.WriteText([]byte("joined room"))
})

License

This project is licensed under the MIT License. See LICENSE for the full text.

Packages

 
 
 

Contributors

Languages