powpow/ws

High-level WebSocket API: a self-managed client facade plus the standalone server, both reachable through a single import.

import powpow/ws

let client = newWsClient()
client.onOpen do (ws: WsConnection):
  ws.sendMessage("hello")
client.onMessage do (ws: WsConnection, kind: WsFrameKind,
                     data: openArray[byte]):
  echo cast[string](@data)
discard client.connect("wss://example.com/chat")
client.run()        # blocks; auto-reconnects per policy until close()

The client creates and owns its event loop internally (like newWsServer() without arguments): connect() arms everything, run() drives the loop on the calling thread. Failed handshakes and abnormal drops are retried automatically with exponential backoff and jitter; deliberate closes never trigger a reconnect.

Types

WsClient = ref object
High-level WebSocket client. Owns its private event loop; user callbacks survive reconnects. One session at a time: while connected or mid-attempt, further connect calls return false.
WsGiveUpCb = proc (attempts: int) {.closure.}
Fired once when the policy's maxRetries is exhausted; run() returns shortly afterwards.
WsReconnectPolicy = object
  maxRetries*: int           ## Consecutive failed attempts allowed before giving
                             ## up (-1 = unlimited). Successful connects reset
                             ## the counter.
  backoffStartMs*: int       ## Delay before the first retry (default 500 ms)
  backoffMaxMs*: int         ## Ceiling for the exponential backoff (10 s)
  jitter*: float             ## Fraction of randomization applied to each delay
                             ## (0..0.95); avoids synchronized reconnect storms
Reconnect behaviour for WsClient.
WsRetryCb = proc (attempt: int; delayMs: int) {.closure.}
Fired before scheduling retry number attempt, which will run after roughly delayMs milliseconds.

Consts

DefaultWsReconnectPolicy = (maxRetries: -1, backoffStartMs: 500,
                            backoffMaxMs: 10000, jitter: 0.25)

Procs

proc attemptCount(c: WsClient): int {.inline, ...raises: [], tags: [], forbids: [].}
Total dial attempts since construction, including successful ones.
proc close(c: WsClient; code: int = 1000; reason: string = "") {.
    ...raises: [KeyError, OSError, Exception], tags: [RootEffect], forbids: [].}
Deliberate shutdown: tears down the session (if any), cancels pending retries and stops the internal loop so a blocked run() returns. Deliberate closes never trigger a reconnect. This client cannot be reused afterwards; create a new one instead.
proc connect(c: WsClient; host: string; port: int; path: string = "/";
             headers: openArray[(string, string)] = [];
             protocols: openArray[string] = []; pingIntervalMs: int = 0;
             idleTimeoutMs: int = 0): bool {....raises: [], tags: [TimeEffect],
    forbids: [].}
Connect to host:port over plain TCP (use connect(url) for wss://).
proc connect(c: WsClient; url: string;
             headers: openArray[(string, string)] = [];
             protocols: openArray[string] = []; pingIntervalMs: int = 0;
             idleTimeoutMs: int = 0): bool {....raises: [WsError, SslError],
    tags: [TimeEffect], forbids: [].}
Connect to a ws:// or wss:// URL. Raises WsError when the URL is malformed; wss:// targets get a verifying TLS context unless one was assigned earlier via setTlsContext. Returns false when this client is already connecting or connected; otherwise schedules the first attempt (it runs once run() is entered).
proc isConnected(c: WsClient): bool {.inline, ...raises: [], tags: [], forbids: [].}
True when the current session completed the handshake and its transport is still open.
proc newWsClient(policy: WsReconnectPolicy = DefaultWsReconnectPolicy;
                 maxFrameSize: int = DefaultMaxFrameSize;
                 handshakeTimeoutMs: int = 10000): WsClient {....raises: [OSError],
    tags: [TimeEffect], forbids: [].}
Create a WebSocket client that owns its event loop. Call the on* setters, then connect(), then run().
proc onClose(c: WsClient; cb: WsCloseCb) {.inline, ...raises: [], tags: [],
    forbids: [].}
Fires when a session ends, whether or not a reconnect follows.
proc onError(c: WsClient; cb: WsErrorCb) {.inline, ...raises: [], tags: [],
    forbids: [].}
proc onGiveUp(c: WsClient; cb: WsGiveUpCb) {.inline, ...raises: [], tags: [],
    forbids: [].}
proc onMessage(c: WsClient; cb: WsMessageCb) {.inline, ...raises: [], tags: [],
    forbids: [].}
proc onOpen(c: WsClient; cb: WsOpenCb) {.inline, ...raises: [], tags: [],
    forbids: [].}
Fires for every established session.
proc onRetry(c: WsClient; cb: WsRetryCb) {.inline, ...raises: [], tags: [],
    forbids: [].}
proc run(c: WsClient) {....raises: [Exception], tags: [TimeEffect, RootEffect],
                        forbids: [].}
Drive the client's internal loop on the calling thread. Returns after close() or when the reconnect policy gives up. The loop is closed on exit; the client is terminal at that point.
proc sendMessage(c: WsClient; data: seq[byte]) {.inline,
    ...raises: [KeyError, OSError, Exception], tags: [RootEffect], forbids: [].}
Send a binary frame on the current session (no-op while disconnected).
proc sendMessage(c: WsClient; data: string) {.inline,
    ...raises: [KeyError, OSError, Exception], tags: [RootEffect], forbids: [].}
Send a text frame on the current session (no-op while disconnected).
proc session(c: WsClient): WsConnection {.inline, ...raises: [], tags: [],
    forbids: [].}
The current (or most recent) session. Prefer the ws argument handed to callbacks; this accessor exists for code outside them.
proc setTlsContext(c: WsClient; ctx: SslContext) {.inline, ...raises: [], tags: [],
    forbids: [].}
Provide the TLS context used for subsequent wss:// connects (e.g. a non-verifying one against self-signed servers).