docs: update README and CONFIG for post-refactor API

This commit is contained in:
Jay
2026-06-06 20:42:31 -04:00
parent fe119b3f36
commit 60de9534dc
2 changed files with 79 additions and 140 deletions
+40 -29
View File
@@ -5,19 +5,16 @@ WebSocket connection and pool primitives in Go. Built for Nostr.
## Library Map
```txt
honeybee.go Pool, Worker, public types
pool.go Pool, public types
worker.go Worker, WorkerConfig
transport/ single-connection primitives and helpers
connection.go Connection, state machine, reader goroutine, pinger
config.go ConnectionConfig, RetryConfig, options
connection.go Connection, reader goroutine, pinger
retry.go exponential backoff with jitter
socket.go Dialer interface, AcquireSocket
socket.go Dialer interface, DialWithRetry
url.go parsing and normalization
watchdog.go IdleWatchdog helper
logging/ structured log construction
logging.go logger constructors, ForcedLevelHandler
types/ shared interfaces (Dialer, Socket, ReceivedMessage)
honeybeetest/ test helpers and mocks for consumers
```
@@ -28,14 +25,12 @@ Honeybee is a minimal, general-purpose WebSocket transport library.
- Client-Side: Manage a pool of outbound peer connections that reconnect automatically and surface lifecycle events.
- Server-Side: Wrap already-upgraded sockets in the connection primitive, which provides a ping-based heartbeat, automated read-loop, concurrent-safe writes, and classified disconnect errors.
- The same connection primitive may also be used directly on the client side when pool semantics are not needed, providing automated dialing retry with exponential backoff and jitter.
- Exposes a means to completely replace the internal pool worker to inject custom behavior.
- The same primitives may also be used directly on the client side when pool semantics are not needed: `transport.DialWithRetry` acquires a socket with configurable exponential backoff and jitter, and the resulting socket can be wrapped in `transport.NewConnection` for the same read-loop, heartbeat, and write primitives without pool semantics.
## What This Library Does Not Do
Honeybee is a pure transport layer, but it is also a deliberately simple one. Honeybee does not provide advanced features, relying on its extensibility features to allow you to customize it.
Honeybee is a pure transport layer, but it is also a deliberately simple one. Honeybee does not provide:
Honeybee does not provide:
- interpretation of message content. All messages are treated equally.
- message queuing, buffering, prioritization, batching, or coalescing.
@@ -87,6 +82,7 @@ go func() {
case honeybee.EventConnected:
case honeybee.EventDisconnected:
case honeybee.EventDialFailed:
case honeybee.EventRetired:
}
}
}()
@@ -106,19 +102,21 @@ After a disconnect, the worker waits for `ReconnectDelay` before attempting the
`Send` returns `ErrConnectionUnavailable` during the gap between a disconnect and the next successful reconnect. Callers should wait for `EventConnected` before retrying and maintain their own write buffers if needed.
Each failed dial attempt emits `EventDialFailed` on `pool.Events()` with the dialer error in `ev.Err`. These do not stop the pool; it continues retrying according to the connection's retry config. `EventDialFailed` is only sent when the peer fails to connect, not when dialing is stopped internally.
Each failed dial attempt emits `EventDialFailed` on `pool.Events()` with the dialer error in `ev.Err`. These do not stop the pool; it continues retrying according to the worker's retry config. `EventDialFailed` is only sent when the peer fails to connect, not when dialing is stopped internally.
When a peer's retry policy is exhausted, the worker deregisters itself from the pool and emits `EventRetired` on `pool.Events()`. The default `RetryConfig` sets `MaxRetries` to zero, which means infinite retries — so in default operation `EventRetired` is structurally unreachable. It will only appear if you explicitly configure a positive `MaxRetries` via `WithRetryConfig`.
## Server-Side Usage
### Connection
Use `transport.NewConnectionFromSocket` when your HTTP upgrade handler gives you an open socket. The connection starts in `StateConnected`; do not call `Connect`.
Use `transport.NewConnection` when your HTTP upgrade handler gives you an open socket.
```go
import "git.wisehodl.dev/jay/go-honeybee/transport"
// wsConn is a *websocket.Conn from your upgrade handler
conn, err := transport.NewConnectionFromSocket(wsConn, nil, logger)
conn, err := transport.NewConnection(ctx, wsConn, nil, handler)
if err != nil { /* handle error */ }
defer conn.Close()
@@ -146,7 +144,7 @@ When the reader exits, exactly one classified error reaches `Errors()` before th
- `ErrPeerClosedUnexpected` for abnormal close codes
- `ErrReadError` for anything else
Pass an `*slog.Logger` as the third argument to get structured logs. Pass nil to disable logging entirely.
Pass an `slog.Handler` as the fourth argument to enable structured logging. Pass `nil` to disable it.
### IdleWatchdog
@@ -199,19 +197,36 @@ When no activity signal arrives within the timeout, `onTimeout` is called once a
## Bare Connection
Use `transport.NewConnection` when you need a single outbound connection without pool semantics — for example, a one-shot query or a custom use-case. It adds several conveniences over a raw socket: a retry loop with exponential backoff, concurrent-safe writes, automatic write deadline enforcement, classified disconnect errors, and observable connection state.
Use `transport.DialWithRetry` and `transport.NewConnection` together when you need a single outbound connection without pool semantics — for example, a one-shot query or a standalone client.
First, acquire a socket. Then wrap it in a connection:
```go
import "git.wisehodl.dev/jay/go-honeybee/transport"
conn, err := transport.NewConnection("wss://example.com", nil, nil)
if err != nil { /* handle error */ }
// Step 1: dial with retry
retryCfg, _ := transport.NewRetryConfig() // or configure via WithMaxRetries, etc.
mgr := transport.NewRetryManager(retryCfg)
if err := conn.Connect(ctx); err != nil { /* handle error */ }
onError := func(err error) {
// called for each failed attempt; use to log or surface per-attempt errors
}
dialFn := func(ctx context.Context) (types.Socket, error) {
socket, _, err := myDialer.DialContext(ctx, "wss://example.com", nil)
return socket, err
}
socket, err := transport.DialWithRetry(ctx, mgr, dialFn, onError, nil)
if err != nil { /* context cancelled or retry policy exhausted */ }
// Step 2: wrap the socket
conn, err := transport.NewConnection(ctx, socket, nil, nil)
if err != nil { /* handle error */ }
defer conn.Close()
go func() {
for data := range conn.Incoming() { // data: []byte
for data := range conn.Incoming() {
// process incoming messages
}
}()
@@ -226,7 +241,9 @@ go func() {
conn.Send([]byte("hello"))
```
The connection goes through four states: `StateDisconnected`, `StateConnecting`, `StateConnected`, `StateClosed`. Transitions are atomic and observable via `conn.State()`. Once closed, the connection should not be reused; construct a new one with the same URL and reconnect.
Pass `nil` for `mgr` to disable retry — the first dial failure is then terminal. Pass `nil` for `onError` to ignore per-attempt errors. Context cancellation terminates the retry loop at any point.
For `RetryConfig` and `RetryManager` construction options, see CONFIG.md.
`Send` is safe for concurrent callers. `Close` is idempotent and safe to call from any goroutine.
@@ -242,7 +259,7 @@ Connections send periodic WebSocket ping frames and listen for the corresponding
Pong-derived heartbeats reset the keepalive timer alongside data messages and sends. A peer that sends no data but responds to pings will not be disconnected and reconnected by the keepalive mechanism.
The ping interval is configured via `transport.WithPingInterval` on the `transport.ConnectionConfig`. Import `git.wisehodl.dev/jay/go-honeybee/transport` to construct a `ConnectionConfig`, then pass it to the pool by value via `honeybee.WithConnectionConfig`, or supply it directly to `NewConnection` and `NewConnectionFromSocket`. The default is 20 seconds. Set to zero to disable pings entirely, in which case only data messages and outbound sends generate heartbeats.
The ping interval is configured via `transport.WithPingInterval` on the `transport.ConnectionConfig`. Import `git.wisehodl.dev/jay/go-honeybee/transport` to construct a `ConnectionConfig`, then wrap it in a `WorkerConfig` via `honeybee.WithConnectionConfig`, and pass the `WorkerConfig` to `NewPoolConfig` via `honeybee.WithWorkerConfig`. You can also supply the `ConnectionConfig` directly to `transport.NewConnection`. The default is 20 seconds. Set to zero to disable pings entirely, in which case only data messages and outbound sends generate heartbeats.
## Statistics
@@ -266,15 +283,9 @@ connStats := conn.Stats() // conn is a *transport.Connection
// connStats.TotalReceived, connStats.TotalSent, connStats.TotalHeartbeats
```
## Extending Pools
The pool owns peer registration, event plumbing, and lifecycle. The worker owns what happens on the wire. The default worker can be replaced entirely via `WorkerFactory`.
See EXTEND.md for the worker interface contract, the `PoolPlugin` fields, and extension patterns.
## Configuration
All configuration is done through option functions applied at construction time. There are three config scopes: `ConnectionConfig`, `WorkerConfig`, and `PoolConfig`. Logging can be enabled and its minimum level overridden independently at the pool, worker, and connection levels.
All configuration is done through option functions applied at construction time. `PoolConfig` is the top-level scope and embeds a `WorkerConfig`. `WorkerConfig` embeds both a `ConnectionConfig` and a `RetryConfig`. Consumers configure connection and retry behavior through this chain. Logging is not config-controlled; pass an `slog.Handler` to pool, worker, and connection constructors directly.
See CONFIG.md for the full option reference and defaults table.