omq-cli 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/README.md +2 -1
- data/lib/omq/cli/cli_parser.rb +23 -14
- data/lib/omq/cli/config.rb +1 -0
- data/lib/omq/cli/formatter.rb +18 -10
- data/lib/omq/cli/parallel_worker.rb +1 -1
- data/lib/omq/cli/pipe.rb +3 -2
- data/lib/omq/cli/pipe_worker.rb +4 -3
- data/lib/omq/cli/socket_setup.rb +26 -4
- data/lib/omq/cli/version.rb +1 -1
- metadata +8 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4acc7ab47167da27247f7b45b82729da6302868879f1a003727ff2b359d42d5
|
|
4
|
+
data.tar.gz: 4a667cab2f594ade3cdc7b4234dc97562bbee043707992935d566dd563530795
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41f4141def92292e2a35412af5ddd9d1a567af2b150b59e805452cde24f9a17279e162d47f44bcb84c9d1e7fb3ad8dad2971162ffad2f38814f753c17db95ab7
|
|
7
|
+
data.tar.gz: 340c7f417266cbeb7020cd19a3a0161de78310b78cf554443c71ba22cef05c69611e38f9d4ef9370ae37b2c27bed2b59241c15ff88e7e077663ae0d0503d6f9b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 — 2026-04-08
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **`--recv-maxsz` defaults to 1 MiB in the CLI** — the underlying `omq`
|
|
8
|
+
library no longer imposes a default (it's `nil`/unlimited as of this
|
|
9
|
+
release), but the CLI keeps a conservative 1 MiB cap for safety when
|
|
10
|
+
connecting to untrusted peers from a terminal. Pass `--recv-maxsz 0`
|
|
11
|
+
to disable the cap explicitly, or `--recv-maxsz N` to raise it.
|
|
12
|
+
- **Default HWM lowered to 100** (from libzmq's 1000) for both send and
|
|
13
|
+
recv. The CLI is typically used interactively or for short pipelines
|
|
14
|
+
where a smaller in-flight queue keeps memory bounded and surfaces
|
|
15
|
+
backpressure earlier. Users who want the old behavior can pass
|
|
16
|
+
`--send-hwm 1000 --recv-hwm 1000` (or `0` for unbounded). Pipe worker
|
|
17
|
+
sockets are unaffected — they still override to `PIPE_HWM` internally.
|
|
18
|
+
- **Compression codec: Zstandard → LZ4 frame format (BREAKING on the wire).**
|
|
19
|
+
`--compress` now uses the new [`rlz4`](../rlz4) gem (Rust extension over
|
|
20
|
+
`lz4_flex`) instead of `zstd-ruby`. Motivation: `zstd-ruby` is the only
|
|
21
|
+
existing Ractor-safe compressor gem, but LZ4 is a better fit for the
|
|
22
|
+
per-message-part workload (smaller frames, lower CPU). `rlz4` is
|
|
23
|
+
Ractor-safe by construction, so parallel `-P` workers now use the same
|
|
24
|
+
codec as the sequential path. **Wire format is incompatible** with prior
|
|
25
|
+
omq-cli versions when `--compress` is in use — both ends must upgrade.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **`--ffi` flag** — opt-in libzmq backend for any socket runner. Builds
|
|
30
|
+
sockets with `backend: :ffi`, so the CLI can drive native libzmq instead
|
|
31
|
+
of the pure-Ruby engine. Requires the optional `omq-ffi` gem and a
|
|
32
|
+
system libzmq 4.x; missing dependencies abort with a clear error.
|
|
33
|
+
Propagated through all socket construction sites: `BaseRunner`,
|
|
34
|
+
`PipeRunner`, `PipeWorker`, and `ParallelWorker`.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **`--send-eval` / `-E` on REP** — now rejected at validation time. REP
|
|
39
|
+
derives its reply from `--recv-eval` / `-e`, so `-E` was silently
|
|
40
|
+
ignored and the runner fell through to reading stdin, hanging the
|
|
41
|
+
request-reply cycle.
|
|
42
|
+
- **`-vvv` preview of REP/REQ envelopes** — empty delimiter frames now
|
|
43
|
+
render as `[0B]` instead of an empty string, so a REP reply with wire
|
|
44
|
+
parts `["", "1"]` previews as `(1B) [0B]|1` instead of the misleading
|
|
45
|
+
`(1B) |1` with a dangling leading pipe.
|
|
46
|
+
|
|
3
47
|
## 0.8.2 — 2026-04-08
|
|
4
48
|
|
|
5
49
|
### Fixed
|
data/README.md
CHANGED
|
@@ -336,7 +336,8 @@ omq pull -b tcp://:5557 -t 5
|
|
|
336
336
|
|
|
337
337
|
## Compression
|
|
338
338
|
|
|
339
|
-
Both sides must use `--compress` (`-z`).
|
|
339
|
+
Both sides must use `--compress` (`-z`). Uses LZ4 frame format, provided
|
|
340
|
+
by the `rlz4` gem (Ractor-safe, Rust extension via `lz4_flex`).
|
|
340
341
|
|
|
341
342
|
```sh
|
|
342
343
|
omq push -c tcp://remote:5557 -z < data.txt
|
data/lib/omq/cli/cli_parser.rb
CHANGED
|
@@ -233,6 +233,7 @@ module OMQ
|
|
|
233
233
|
curve_server: false,
|
|
234
234
|
curve_server_key: nil,
|
|
235
235
|
crypto: nil,
|
|
236
|
+
ffi: false,
|
|
236
237
|
}.freeze
|
|
237
238
|
|
|
238
239
|
|
|
@@ -338,9 +339,9 @@ module OMQ
|
|
|
338
339
|
end
|
|
339
340
|
}
|
|
340
341
|
o.on("--heartbeat-ivl SECS", Float, "ZMTP heartbeat interval (detects dead peers)") { |v| opts[:heartbeat_ivl] = v }
|
|
341
|
-
o.on("--recv-maxsz
|
|
342
|
-
o.on("--send-hwm N", Integer, "Send high water mark (default
|
|
343
|
-
o.on("--recv-hwm N", Integer, "Recv high water mark (default
|
|
342
|
+
o.on("--recv-maxsz SIZE", "Max inbound message size, e.g. 4096, 64K, 1M, 2G (default 1M, 0=unlimited; larger messages drop the connection)") { |v| opts[:recv_maxsz] = parse_byte_size(v) }
|
|
343
|
+
o.on("--send-hwm N", Integer, "Send high water mark (default 100, 0=unbounded)") { |v| opts[:send_hwm] = v }
|
|
344
|
+
o.on("--recv-hwm N", Integer, "Recv high water mark (default 100, 0=unbounded)") { |v| opts[:recv_hwm] = v }
|
|
344
345
|
o.on("--sndbuf N", "SO_SNDBUF kernel buffer size (e.g. 4K, 1M)") { |v| opts[:sndbuf] = parse_byte_size(v) }
|
|
345
346
|
o.on("--rcvbuf N", "SO_RCVBUF kernel buffer size (e.g. 4K, 1M)") { |v| opts[:rcvbuf] = parse_byte_size(v) }
|
|
346
347
|
|
|
@@ -348,8 +349,8 @@ module OMQ
|
|
|
348
349
|
o.on("--conflate", "Keep only last message per subscriber (PUB/RADIO)") { opts[:conflate] = true }
|
|
349
350
|
|
|
350
351
|
o.separator "\nCompression:"
|
|
351
|
-
o.on("-z", "--compress", "
|
|
352
|
-
require "
|
|
352
|
+
o.on("-z", "--compress", "LZ4 compression per frame (modal with --in/--out)") do
|
|
353
|
+
require "rlz4"
|
|
353
354
|
case pipe_side
|
|
354
355
|
when :in
|
|
355
356
|
opts[:compress_in] = true
|
|
@@ -383,6 +384,14 @@ module OMQ
|
|
|
383
384
|
o.on("-v", "--verbose", "Verbosity: -v endpoints, -vv events, -vvv messages") { opts[:verbose] += 1 }
|
|
384
385
|
o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
|
|
385
386
|
o.on( "--transient", "Exit when all peers disconnect") { opts[:transient] = true }
|
|
387
|
+
o.on( "--ffi", "Use libzmq FFI backend (requires omq-ffi gem + system libzmq 4.x)") do
|
|
388
|
+
begin
|
|
389
|
+
require "omq/ffi"
|
|
390
|
+
rescue LoadError => e
|
|
391
|
+
abort "omq: --ffi requires the omq-ffi gem and system libzmq 4.x (#{e.message})"
|
|
392
|
+
end
|
|
393
|
+
opts[:ffi] = true
|
|
394
|
+
end
|
|
386
395
|
o.on("-V", "--version") {
|
|
387
396
|
if ENV["OMQ_DEV"]
|
|
388
397
|
require_relative "../../../../omq/lib/omq/version"
|
|
@@ -430,21 +439,20 @@ module OMQ
|
|
|
430
439
|
end
|
|
431
440
|
|
|
432
441
|
|
|
433
|
-
# Parses a byte size string with optional K/M suffix
|
|
442
|
+
# Parses a byte size string with an optional K/M/G suffix (binary,
|
|
443
|
+
# i.e. 1K = 1024 bytes).
|
|
434
444
|
#
|
|
435
|
-
# @param str [String] e.g. "4096", "4K", "1M"
|
|
445
|
+
# @param str [String] e.g. "4096", "4K", "1M", "2G"
|
|
436
446
|
# @return [Integer] size in bytes
|
|
437
447
|
#
|
|
438
448
|
def parse_byte_size(str)
|
|
439
449
|
case str
|
|
440
|
-
when /\A(\d+)[kK]\z/
|
|
441
|
-
|
|
442
|
-
when /\A(\d+)[
|
|
443
|
-
|
|
444
|
-
when /\A\d+\z/
|
|
445
|
-
str.to_i
|
|
450
|
+
when /\A(\d+)[kK]\z/ then $1.to_i * 1024
|
|
451
|
+
when /\A(\d+)[mM]\z/ then $1.to_i * 1024 * 1024
|
|
452
|
+
when /\A(\d+)[gG]\z/ then $1.to_i * 1024 * 1024 * 1024
|
|
453
|
+
when /\A\d+\z/ then str.to_i
|
|
446
454
|
else
|
|
447
|
-
abort "invalid byte size: #{str} (use e.g. 4096, 4K, 1M)"
|
|
455
|
+
abort "invalid byte size: #{str} (use e.g. 4096, 4K, 1M, 2G)"
|
|
448
456
|
end
|
|
449
457
|
end
|
|
450
458
|
|
|
@@ -482,6 +490,7 @@ module OMQ
|
|
|
482
490
|
abort "--conflate is only valid for PUB/RADIO" if opts[:conflate] && !%w[pub radio].include?(type_name)
|
|
483
491
|
abort "--recv-eval is not valid for send-only sockets (use --send-eval / -E)" if opts[:recv_expr] && SEND_ONLY.include?(type_name)
|
|
484
492
|
abort "--send-eval is not valid for recv-only sockets (use --recv-eval / -e)" if opts[:send_expr] && RECV_ONLY.include?(type_name)
|
|
493
|
+
abort "--send-eval is not valid for REP (the reply is the result of --recv-eval / -e)" if opts[:send_expr] && type_name == "rep"
|
|
485
494
|
abort "--send-eval and --target are mutually exclusive" if opts[:send_expr] && opts[:target]
|
|
486
495
|
|
|
487
496
|
if opts[:parallel]
|
data/lib/omq/cli/config.rb
CHANGED
data/lib/omq/cli/formatter.rb
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module OMQ
|
|
4
4
|
module CLI
|
|
5
|
-
# Raised when
|
|
5
|
+
# Raised when LZ4 decompression fails.
|
|
6
6
|
class DecompressError < RuntimeError; end
|
|
7
7
|
|
|
8
8
|
# Handles encoding/decoding messages in the configured format,
|
|
9
|
-
# plus optional
|
|
9
|
+
# plus optional LZ4 compression.
|
|
10
10
|
class Formatter
|
|
11
11
|
# @param format [Symbol] wire format (:ascii, :quoted, :raw, :jsonl, :msgpack, :marshal)
|
|
12
|
-
# @param compress [Boolean] whether to apply
|
|
12
|
+
# @param compress [Boolean] whether to apply LZ4 compression per frame
|
|
13
13
|
def initialize(format, compress: false)
|
|
14
14
|
@format = format
|
|
15
15
|
@compress = compress
|
|
@@ -83,22 +83,22 @@ module OMQ
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
# Compresses each frame with
|
|
86
|
+
# Compresses each frame with LZ4 if compression is enabled.
|
|
87
87
|
#
|
|
88
88
|
# @param parts [Array<String>] message frames
|
|
89
89
|
# @return [Array<String>] optionally compressed frames
|
|
90
90
|
def compress(parts)
|
|
91
|
-
@compress ? parts.map { |p|
|
|
91
|
+
@compress ? parts.map { |p| RLZ4.compress(p) } : parts
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
# Decompresses each frame with
|
|
95
|
+
# Decompresses each frame with LZ4 if compression is enabled.
|
|
96
96
|
#
|
|
97
97
|
# @param parts [Array<String>] possibly compressed message frames
|
|
98
98
|
# @return [Array<String>] decompressed frames
|
|
99
99
|
def decompress(parts)
|
|
100
|
-
@compress ? parts.map { |p|
|
|
101
|
-
rescue
|
|
100
|
+
@compress ? parts.map { |p| RLZ4.decompress(p) } : parts
|
|
101
|
+
rescue RLZ4::DecompressError
|
|
102
102
|
raise DecompressError, "decompression failed (did the sender use --compress?)"
|
|
103
103
|
end
|
|
104
104
|
|
|
@@ -110,15 +110,23 @@ module OMQ
|
|
|
110
110
|
def self.preview(parts)
|
|
111
111
|
total = parts.sum(&:bytesize)
|
|
112
112
|
shown = parts.first(3).map { |p| preview_frame(p) }
|
|
113
|
-
tail
|
|
113
|
+
tail = parts.size > 3 ? "|...(#{parts.size} parts)" : ""
|
|
114
|
+
|
|
114
115
|
"(#{total}B) #{shown.join("|")}#{tail}"
|
|
115
116
|
end
|
|
116
117
|
|
|
117
118
|
|
|
118
119
|
def self.preview_frame(part)
|
|
119
120
|
bytes = part.b
|
|
120
|
-
|
|
121
|
+
# Empty frames must render as a visible marker, not as the empty
|
|
122
|
+
# string — otherwise joining with "|" would produce misleading
|
|
123
|
+
# output like "|body" for REP/REQ-style envelopes where the first
|
|
124
|
+
# wire frame is an empty delimiter.
|
|
125
|
+
return "[0B]" if bytes.empty?
|
|
126
|
+
|
|
127
|
+
sample = bytes[0, 12]
|
|
121
128
|
printable = sample.count("\x20-\x7e")
|
|
129
|
+
|
|
122
130
|
if printable < sample.bytesize / 2
|
|
123
131
|
"[#{bytes.bytesize}B]"
|
|
124
132
|
elsif bytes.bytesize > 12
|
|
@@ -41,7 +41,7 @@ module OMQ
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def setup_socket
|
|
44
|
-
@sock = OMQ.const_get(@socket_sym).new
|
|
44
|
+
@sock = @config.ffi ? OMQ.const_get(@socket_sym).new(backend: :ffi) : OMQ.const_get(@socket_sym).new
|
|
45
45
|
OMQ::CLI::SocketSetup.apply_options(@sock, @config)
|
|
46
46
|
@sock.identity = @config.identity if @config.identity
|
|
47
47
|
OMQ::CLI::SocketSetup.attach_endpoints(@sock, @endpoints, verbose: false)
|
data/lib/omq/cli/pipe.rb
CHANGED
|
@@ -80,8 +80,9 @@ module OMQ
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def build_pull_push(in_eps, out_eps)
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
kwargs = config.ffi ? { backend: :ffi } : {}
|
|
84
|
+
pull = OMQ::PULL.new(**kwargs)
|
|
85
|
+
push = OMQ::PUSH.new(**kwargs)
|
|
85
86
|
SocketSetup.apply_options(pull, config)
|
|
86
87
|
SocketSetup.apply_options(push, config)
|
|
87
88
|
pull.recv_hwm = PIPE_HWM unless config.recv_hwm
|
data/lib/omq/cli/pipe_worker.rb
CHANGED
|
@@ -37,8 +37,9 @@ module OMQ
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def setup_sockets
|
|
40
|
-
|
|
41
|
-
@
|
|
40
|
+
kwargs = @config.ffi ? { backend: :ffi } : {}
|
|
41
|
+
@pull = OMQ::PULL.new(**kwargs)
|
|
42
|
+
@push = OMQ::PUSH.new(**kwargs)
|
|
42
43
|
OMQ::CLI::SocketSetup.apply_options(@pull, @config)
|
|
43
44
|
OMQ::CLI::SocketSetup.apply_options(@push, @config)
|
|
44
45
|
@pull.recv_hwm = PipeRunner::PIPE_HWM unless @config.recv_hwm
|
|
@@ -71,7 +72,7 @@ module OMQ
|
|
|
71
72
|
when :message_received
|
|
72
73
|
"omq: << #{OMQ::CLI::Formatter.preview(event.detail[:parts])}"
|
|
73
74
|
else
|
|
74
|
-
ep
|
|
75
|
+
ep = event.endpoint ? " #{event.endpoint}" : ""
|
|
75
76
|
detail = event.detail ? " #{event.detail}" : ""
|
|
76
77
|
"omq: #{event.type}#{ep}#{detail}"
|
|
77
78
|
end
|
data/lib/omq/cli/socket_setup.rb
CHANGED
|
@@ -6,6 +6,21 @@ module OMQ
|
|
|
6
6
|
# All methods are module-level so callers compose rather than inherit.
|
|
7
7
|
#
|
|
8
8
|
module SocketSetup
|
|
9
|
+
# Default high water mark applied when the user does not pass
|
|
10
|
+
# --send-hwm / --recv-hwm. Lower than libzmq's default (1000) to keep
|
|
11
|
+
# memory footprint small for the typical CLI use cases (interactive
|
|
12
|
+
# debugging, short-lived pipelines). Pipe worker sockets override this
|
|
13
|
+
# with a still-smaller value for tighter backpressure.
|
|
14
|
+
DEFAULT_HWM = 100
|
|
15
|
+
|
|
16
|
+
# Default max inbound message size applied when the user does not
|
|
17
|
+
# pass --recv-maxsz. The omq library itself is unlimited by default;
|
|
18
|
+
# the CLI caps inbound messages at 1 MiB so that a misconfigured or
|
|
19
|
+
# malicious peer cannot force arbitrary memory allocation on a
|
|
20
|
+
# terminal user. Users can raise it with --recv-maxsz N, or disable
|
|
21
|
+
# it entirely with --recv-maxsz 0.
|
|
22
|
+
DEFAULT_RECV_MAXSZ = 1 << 20
|
|
23
|
+
|
|
9
24
|
# Apply common socket options from +config+ to +sock+.
|
|
10
25
|
#
|
|
11
26
|
def self.apply_options(sock, config)
|
|
@@ -14,8 +29,9 @@ module OMQ
|
|
|
14
29
|
sock.send_timeout = config.timeout if config.timeout
|
|
15
30
|
sock.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
|
|
16
31
|
sock.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
|
|
17
|
-
|
|
18
|
-
sock.
|
|
32
|
+
# nil → default; 0 stays 0 (unbounded), any other integer is taken as-is.
|
|
33
|
+
sock.send_hwm = config.send_hwm || DEFAULT_HWM
|
|
34
|
+
sock.recv_hwm = config.recv_hwm || DEFAULT_HWM
|
|
19
35
|
sock.sndbuf = config.sndbuf if config.sndbuf
|
|
20
36
|
sock.rcvbuf = config.rcvbuf if config.rcvbuf
|
|
21
37
|
end
|
|
@@ -24,10 +40,16 @@ module OMQ
|
|
|
24
40
|
# Create and fully configure a socket from +klass+ and +config+.
|
|
25
41
|
#
|
|
26
42
|
def self.build(klass, config)
|
|
27
|
-
sock = klass.new
|
|
43
|
+
sock = config.ffi ? klass.new(backend: :ffi) : klass.new
|
|
28
44
|
sock.conflate = true if config.conflate && %w[pub radio].include?(config.type_name)
|
|
29
45
|
apply_options(sock, config)
|
|
30
|
-
|
|
46
|
+
# --recv-maxsz: nil → 1 MiB default; 0 → explicitly unlimited; else → as-is.
|
|
47
|
+
sock.max_message_size =
|
|
48
|
+
case config.recv_maxsz
|
|
49
|
+
when nil then DEFAULT_RECV_MAXSZ
|
|
50
|
+
when 0 then nil
|
|
51
|
+
else config.recv_maxsz
|
|
52
|
+
end
|
|
31
53
|
sock.identity = config.identity if config.identity
|
|
32
54
|
sock.router_mandatory = true if config.type_name == "router"
|
|
33
55
|
sock
|
data/lib/omq/cli/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omq-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -128,24 +128,24 @@ dependencies:
|
|
|
128
128
|
- !ruby/object:Gem::Version
|
|
129
129
|
version: '7.0'
|
|
130
130
|
- !ruby/object:Gem::Dependency
|
|
131
|
-
name:
|
|
131
|
+
name: rlz4
|
|
132
132
|
requirement: !ruby/object:Gem::Requirement
|
|
133
133
|
requirements:
|
|
134
|
-
- - "
|
|
134
|
+
- - "~>"
|
|
135
135
|
- !ruby/object:Gem::Version
|
|
136
|
-
version: '0'
|
|
136
|
+
version: '0.1'
|
|
137
137
|
type: :runtime
|
|
138
138
|
prerelease: false
|
|
139
139
|
version_requirements: !ruby/object:Gem::Requirement
|
|
140
140
|
requirements:
|
|
141
|
-
- - "
|
|
141
|
+
- - "~>"
|
|
142
142
|
- !ruby/object:Gem::Version
|
|
143
|
-
version: '0'
|
|
143
|
+
version: '0.1'
|
|
144
144
|
description: Command-line tool for sending and receiving ZeroMQ messages on any socket
|
|
145
145
|
type (REQ/REP, PUB/SUB, PUSH/PULL, DEALER/ROUTER, and all draft types). Supports
|
|
146
146
|
Ruby eval (-e/-E), script handlers (-r), pipe virtual socket with Ractor parallelism,
|
|
147
|
-
multiple formats (ASCII, JSON Lines, msgpack, Marshal),
|
|
148
|
-
|
|
147
|
+
multiple formats (ASCII, JSON Lines, msgpack, Marshal), LZ4 compression, and CURVE
|
|
148
|
+
encryption. Like nngcat from libnng, but with Ruby superpowers.
|
|
149
149
|
email:
|
|
150
150
|
- paddor@gmail.com
|
|
151
151
|
executables:
|