NNQ::Zstd — transparent Zstd compression for NNQ sockets
Wraps any NNQ::Socket with transparent per-message Zstd compression,
sender-side dictionary training, and in-band dictionary shipping. No
handshake, no negotiation — both peers must wrap for it to work.
See RFC.md for the normative wire protocol.
Quick start
require "nnq"
require "nnq/zstd"
push = NNQ::PUSH0.new
push.connect("tcp://127.0.0.1:5555")
push = NNQ::Zstd.wrap(push, level: -3) # fast (-3) or balanced (3)
push.send("payload") # compressed on the wire
pull = NNQ::PULL0.new
pull.bind("tcp://*:5555")
pull = NNQ::Zstd.wrap(pull) # receiver-only: no dict config
pull.receive # => "payload"
How it works
- Every message carries a 4-byte preamble:
00 00 00 00— uncompressed plaintext (preamble stripped on recv).- Zstd frame magic — a full Zstd frame (compressed).
- Zstd dict magic — a Zstd dictionary to install (not surfaced to the app).
- The sender trains a dict from its first ~1000 small messages (or 100 KiB cumulative sample bytes), then compresses subsequent small messages with it. The dict is shipped in-band before the next real payload.
- User-supplied dictionaries may be passed via
dict::ruby NNQ::Zstd.wrap(socket, level: -3, dict: [dict_bytes_1, dict_bytes_2])All supplied dicts are shipped to peers on the wire. Training is skipped whendict:is given. - Decompression is bounded by
min(16 MiB, recv_maxsz). Frames whose header omitsFrame_Content_Sizeare rejected. - Up to 32 dicts and 128 KiB cumulative per wrapper. Any
protocol violation raises
NNQ::Zstd::ProtocolError.
Out of scope
- Negotiation / auto-detection. Both peers must wrap.
- Dict persistence across process restarts. Training is per-session.
- Receiver-side training — receivers only install shipped dicts.
License
ISC. See LICENSE.