asymmetric encapsulation: zat hides websocket on consume, exposes it on serve #
symptom #
bumping zat in labelz from v0.3.0-alpha.7 to v0.3.2 hit a duplicate-module error: labelz declares its own websocket dep (to serve com.atproto.label.subscribeLabels over WebSocket), and zat brings websocket transitively. when zat bumped websocket v0.1.0 → v0.1.1, the two roots conflicted until labelz matched the pin.
diagnosis #
the version coupling is the surface. the deeper issue is asymmetric abstraction:
- consume side —
FirehoseClientandJetstreamClienttake(io, allocator, options)and emit typed events. consumers never touch websocket. clean. - serve side — nothing. labelz reaches around zat to use
websocket.Server,websocket.Conn,websocket.Handshake.KeyValuedirectly. the choice of WebSocket library leaks into the consumer'sbuild.zig.zonand source.
any future labeler, relay, or feed-stream server will hit the same wall. labelz also reinvents the XRPC subscription frame format (label.zig:100 — header CBOR ‖ payload CBOR), which zat already decodes internally but doesn't expose for encoding.
proposal #
add a server-side counterpart that mirrors the client abstraction:
var server = zat.xrpc.SubscriptionServer(Handler).init(io, allocator, .{
.port = 4100,
.nsid = "com.atproto.label.subscribeLabels",
});
defer server.deinit();
// Handler interface:
// onSubscribe(cursor: ?i64) -> backfill iterator
// broadcast(payload: cbor.Value) — server handles frame encoding
// onClose(conn_id)
try server.listen();
server owns:
- websocket transport (consumers drop their websocket dep entirely)
- XRPC event frame encoding (header
{op, t?}‖ payload) - error frames (FutureCursor, OutdatedCursor, etc.)
- HTTP fallback for non-WebSocket XRPC endpoints on the same port
non-goals #
- not a general HTTP server — only XRPC subscription pattern
- not a replacement for HttpTransport (that's the client transport)
- not auth/PDS server functionality
prior art / migration target #
labelz src/server.zig is the concrete shape this API needs to absorb. once landed, labelz drops websocket from build.zig.zon, deletes encodeEventFrame / httpRespond / queryParam, and the duplicate-module class of error becomes structurally impossible.
can prob close?