zws is still pre-1.0, but it now has an explicit compatibility policy.
The public exports in src/root.zig are the supported package API:
HandshakeConn- frame/message read and write helpers
Extensions.PerMessageDeflateandConn.PerMessageDeflateConfig- timeout runtime hook types (
Observe.TimeoutConfig,Observe.DefaultRuntimeHooks)
Within a patch release, those symbols and their documented semantics should not break.
These areas are still allowed to evolve more aggressively before 1.0:
- the exact shape of
Conn.ConfigandConn.StaticConfigif a feature needs another field - compression tuning knobs beyond the negotiated
permessage-deflateparameters - validation/build helper steps under
zig build
If a breaking change lands in a provisional area, it should come with a migration note in the changelog or commit message.
Before 1.0:
- patch releases should be source-compatible for the stable surface
- minor releases may contain targeted breaking changes, but only with clear motivation
- protocol correctness fixes are allowed even if they cause previously-accepted malformed traffic to start failing
After 1.0, the intent is standard semver for the stable surface.
zws is a synchronous, stream-oriented websocket core.
The library expects:
- a reader positioned at the first websocket frame byte
- a writer targeting the same upgraded TCP stream
- the HTTP upgrade already accepted, or a caller using the handshake helpers to do it
That makes it fit two common models:
- raw
std.Io.net.Stream - custom runtimes that can hand over borrowed reader/writer pairs
For message-oriented application code, you can also run a typed handler loop with Handler.run(...) and keep state in user-owned structs via ctx.state.
Handler.run(...) is the built-in typed message loop adapter. It does not introduce runtime interfaces/vtables; handler dispatch is comptime-specialized.
- Handler signatures:
- sequential:
fn(ctx: *Handler.SliceContext(opts, ConnType, State)) ResponseType - async-compatible:
fn(io: std.Io, ctx: *Handler.SliceContext(opts, ConnType, State)) !ResponseType
- sequential:
ctx.stategives typed access to user-owned per-loop/per-connection state.- Receive mode is configured through
Handler.Options.receive_mode:.solid_slice: handlers receive a fully assembled message slice from caller-provided message buffer..stream: handlers pull chunks withctx.readChunk(...)and can avoid full-message assembly in the library. Compressed (RSV1) messages are rejected in this mode.
- Execution mode is configured through
Handler.Options.execution:.sequential: one message at a time on the connection..async: bounded concurrent handler execution for solid-slice mode; writes are serialized through the shared connection writer.
- Supported sync return shapes:
[]const u8[][]const u8/[]const []const u8Handler.Response- structs with
body(and optionalopcode)
Response writing stays on the core websocket writer path (Conn.writeText / Conn.writeBinary / fragmented frames for chunk arrays), and flushing is controlled by Handler.Options.auto_flush.
The connection API assumes buffered I/O.
- borrowed frame reads only work when the entire frame fits in the reader buffer
- frame and message helpers operate correctly without borrowing, but may copy into caller buffers
- write batching is left to the caller’s writer buffering and flush policy
Timeouts are configured per connection through Conn.Config.timeouts.
read_ns,write_ns, andflush_nsbound individual blocking operationsConn.TypeWithHooks(..., Hooks)lets the embedding framework provide the elapsed-time source and deadline mappingConn.Default,Conn.Server,Conn.Client, and plain handshake helpers useObserve.DefaultRuntimeHooks
Without custom hooks, timeout enforcement is cooperative: the library can detect an operation ran longer than budget once that operation returns. With hook-provided deadline methods, the framework can map the same budget into socket or runtime deadlines so blocking I/O can fail promptly.
Runtime hooks are now timeout-only.
- connection flows can use
Conn.TypeWithHooks(.{ ... }, Hooks)plusinitWithHooks(...) - hook types provide
nowNs,setReadDeadlineNs,setWriteDeadlineNs, andsetFlushDeadlineNs - there is no event callback surface in the core hook contract
permessage-deflate is optional.
- if the client offers
permessage-deflate,Handshake.upgrade(...)returns the negotiated settings - propagate the negotiated
Handshake.upgrade(...)result intoConn.Config.permessage_deflate - compressed message I/O uses
std.compress.flatefor RFC7692 framing and optional context-takeover support - compression remains disabled by default (
Conn.Config.permessage_deflate = null) - outgoing compressed writes are opt-in (
Conn.PerMessageDeflateConfig.compress_outgoing = falseby default) - context takeover runtime support is disabled by default (
Conn.StaticConfig.permessage_deflate_context_takeover = false) - enable takeover support by instantiating
Conn.Type(.{ .permessage_deflate_context_takeover = true, ... }) - when enabled,
Conn.StaticConfighas conservative compile-time defaults:permessage_deflate_min_payload_len = 64permessage_deflate_require_compression_gain = true
The library negotiates the extension conservatively and defaults to:
server_no_context_takeoverclient_no_context_takeover
With context takeover support disabled, this keeps the runtime model simple and avoids cross-message compressor state.
benchmark/zws_server.zig is a benchmark harness, not a hardened application server.
For a real integration reference, use:
examples/echo_server.zigfor a message-oriented raw-stream serverexamples/frame_echo_server.zigfor a frame-oriented raw-stream serverexamples/ws_client.zigfor a client-side handshake plusConn.Clientflow
Production callers should still decide their own:
- accept loop / task model
- connection limits
- timeout budgets and idle handling policy
- how to collect timeout counters/latency metrics in your runtime
- TLS termination
zws now ships with four validation layers.
zig build testThis covers protocol parsing, close handling, masking, fragmentation, handshake validation, and compression helpers.
The in-tree test suite includes:
- malformed frame fuzzing
- randomized client/server roundtrips
- randomized fragmented masked message reconstruction
Those live in src/validation_tests.zig.
zig build interopThis runs zws against real external peers in both directions:
- Node
wsclient/server - Python
aiohttpclient/server - compressed and uncompressed paths
The matrix is orchestrated by validation/run_interop.zig.
zig build soakThis starts the example echo server and drives it with many concurrent long-lived websocket connections using the in-tree Zig soak runner.
The default soak step runs both:
- uncompressed
permessage-deflate
zig build validateThat runs:
- unit and property tests
- interop
- soak
Benchmarks remain separate:
zig build bench-compare -Doptimize=ReleaseFast