nnq-cli 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2aac6e193d793225be560b51a32d4f3e713afbab8b25652be6cd76750f789e3e
4
+ data.tar.gz: a31568088dbc3ba5ad3df07f3e1f0f98543c1ad0c631718ae71f76d02b602c0a
5
+ SHA512:
6
+ metadata.gz: fe5db04b1ebb441274c4b182d3eabed59229c040593fe7f0388c123972a6059572c6a2dff3633e1f72abd0a6771b3b6e2812e6b00d07b9c4d75ad9bd831b8059
7
+ data.tar.gz: bba50ee147da4897cefce7297834584127183a04a6bb3a8c15525dabff70c04f865eaa8204dc69eebc991f72fc1cc1c01928ad5490be1ee164f656f39c70cc45
data/CHANGELOG.md ADDED
@@ -0,0 +1,78 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-04-15
4
+
5
+ - **Peer wait for bind-mode bounded senders** — `nnq push -b ... -d
6
+ hello` now waits for the first peer before sending, so one-shot
7
+ `-d`/`-f`/`-E` payloads don't just get queued into HWM and dropped on
8
+ exit. Interactive stdin still sends without waiting.
9
+ - **`--count N` honored on one-shot sends** — `-d`/`-f` and pure-
10
+ generator `-E` loop N times instead of firing once.
11
+ - **Interactive TTY fallback** — bare `nnq push -c tcp://...` on a
12
+ terminal reads lines from the TTY until ^D (matching omq-cli).
13
+ Pure-generator `-E` is checked before the TTY fallback.
14
+ - **`nnq:` log prefix** — `BaseRunner#log` routes through
15
+ `Term.log_prefix` with an `nnq: ` prefix, so every stderr line from a
16
+ CLI run looks consistent with attach/event lines.
17
+ - **No more "frames"/"parts"** — NNG has no multipart concept; all
18
+ `parts` variables renamed to `msg`, and comments/docs updated from
19
+ "frame" to "message"/"body".
20
+ - **Eval `#to_s` coercion** — non-string eval results (e.g.
21
+ `-E 'Time.now'`) are coerced via `#to_s` instead of raising
22
+ `NoMethodError` on `#to_str`. Array elements are coerced
23
+ individually.
24
+ - **`@name` IPC shorthand** — `@foo` expands to `ipc://@foo`
25
+ (Linux abstract namespace) in `-b`/`-c` arguments.
26
+ - **Pipe: bare endpoint promotion** — `pipe -c SRC --out -c DST`
27
+ automatically promotes the bare `-c SRC` to `--in`.
28
+ - **Pipe: fan-out fairness yield** — multi-output pipes yield after
29
+ each send so send-pump fibers distribute messages fairly across
30
+ output peers.
31
+ - **Formatter: empty-frame preview** — empty bodies render as `''`
32
+ instead of `[0B]` in verbose output.
33
+ - **Formatter: nil-safe compression** — `compress`/`decompress` skip
34
+ nil and empty frames instead of crashing.
35
+ - **`NNQ::CLI::Term` module** — consolidates verbose log formatting
36
+ (timestamps at `-vvvv`, monitor events, endpoint attach lines) into a
37
+ stateless module. Replaces four duplicated inline formatting blocks
38
+ across BaseRunner, PipeRunner, PipeWorker, and SocketSetup.
39
+ - **Default HWM → 64** — down from 100. 64 matches the send pump's
40
+ per-fairness-batch limit (one batch exactly fills a full queue).
41
+ Pipe sockets no longer use a separate `PIPE_HWM = 16`; they go
42
+ through `SocketSetup.build` like every other socket type.
43
+ - **Pipe: drop peer wait unless `--timeout`** — without `--timeout`,
44
+ `PULL#receive` blocks naturally and `PUSH` buffers up to `send_hwm`.
45
+ Only wait for peers in fail-fast mode.
46
+ - **Endpoint normalization** — binds: `tcp://:PORT` normalizes to
47
+ loopback (`[::1]` on IPv6-capable hosts, `127.0.0.1` otherwise);
48
+ `tcp://*:PORT` normalizes to `0.0.0.0`. Connects: both `tcp://:PORT`
49
+ and `tcp://*:PORT` normalize to `tcp://localhost:PORT` (preserving
50
+ Happy Eyeballs).
51
+ - **YJIT by default** — `exe/nnq` calls `RubyVM::YJIT.enable` before
52
+ loading the CLI (skipped if `RUBYOPT` is set, interpreter lacks YJIT,
53
+ or YJIT is already on).
54
+ - **Consistent `nnq:` prefix** on all attach and event log lines.
55
+ - **`-vvvv` timestamps** — ISO8601 UTC with µs precision.
56
+ - **3 new socket runners** — `nnq bus`, `nnq surveyor`, `nnq respondent`.
57
+ - **Versioned socket symbols** — RUNNER_MAP uses `:PUSH0`, `:PULL0`, etc.
58
+
59
+ ## 0.1.0 — 2026-04-09
60
+
61
+ Initial release — NNQ command-line tool, sister of omq-cli for the SP
62
+ (nanomsg) wire protocol.
63
+
64
+ - 7 socket-type runners: push, pull, pub, sub, req, rep, pair.
65
+ - `pipe` virtual socket (PULL → eval → PUSH) with Ractor-based
66
+ parallelism (`-P N`). Each worker owns its own Async reactor and
67
+ PULL/PUSH pair; messages fan out via round-robin PUSH to workers and
68
+ merge back through a shared PULL sink.
69
+ - Ruby eval (`-e` / `-E` with BEGIN/END blocks), `-r` script loading
70
+ with `NNQ.outgoing` / `NNQ.incoming` handlers.
71
+ - 6 formats: ASCII, quoted, raw, JSON Lines, msgpack, Marshal.
72
+ - LZ4 compression (`--compress` / `--compress-in` / `--compress-out`).
73
+ - `--transient` mode — cleanly exits once all peers have disconnected.
74
+ - `-v` / `-vv` / `-vvv` monitor events piped to stderr.
75
+ - No CURVE, no heartbeat, no multipart — nnq is single-frame SP.
76
+
77
+ Requires `nnq ~> 0.4` (for `freeze_for_ractors!`, `Socket#monitor`,
78
+ `#all_peers_gone`, and `PULL#receive` `read_timeout` support).
data/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Patrik Wenger
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,391 @@
1
+ # nnq — nanomsg SP CLI
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/nnq-cli?color=e9573f)](https://rubygems.org/gems/nnq-cli)
4
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE)
5
+ [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%204.0-CC342D?logo=ruby&logoColor=white)](https://www.ruby-lang.org)
6
+
7
+ Command-line tool for sending and receiving nanomsg SP protocol messages on
8
+ any nnq socket type. Like `nngcat` from libnng, but with Ruby eval, Ractor
9
+ parallelism, and message handlers.
10
+
11
+ Built on [nnq](https://github.com/paddor/nnq) — pure Ruby SP wire protocol, no
12
+ C dependencies. Wire-compatible with libnng peers.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ gem install nnq-cli
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```sh
23
+ # Echo server
24
+ nnq rep -b tcp://:5555 --echo
25
+
26
+ # Client
27
+ echo "hello" | nnq req -c tcp://localhost:5555
28
+
29
+ # Upcase server — -e evals Ruby on each incoming message
30
+ nnq rep -b tcp://:5555 -e '$_.upcase'
31
+ ```
32
+
33
+ ```
34
+ Usage: nnq TYPE [options]
35
+
36
+ Types: req, rep, pub, sub, push, pull, pair
37
+ Virtual: pipe (PULL → eval → PUSH)
38
+ ```
39
+
40
+ ## SP messages are single-frame
41
+
42
+ Unlike ZeroMQ, nanomsg SP messages have **one frame**, not many. The CLI
43
+ exposes `$_` (the message body, a `String`) and `$F` (a 1-element array
44
+ `[$_]`) for compatibility with omq-cli expressions, but multipart shenanigans
45
+ don't apply.
46
+
47
+ ## Connection
48
+
49
+ Every socket needs at least one `--bind` or `--connect`:
50
+
51
+ ```sh
52
+ nnq pull --bind tcp://:5557 # listen on port 5557
53
+ nnq push --connect tcp://host:5557 # connect to host
54
+ nnq pull -b ipc:///tmp/feed.sock # IPC (unix socket)
55
+ nnq push -c ipc://@abstract # IPC (abstract namespace, Linux)
56
+ nnq push -c inproc://name # in-process queue
57
+ ```
58
+
59
+ Bind/connect order doesn't matter — `connect` is non-blocking and the engine
60
+ retries with exponential back-off until the peer is reachable. Multiple
61
+ endpoints are allowed: `nnq pull -b tcp://:5557 -b tcp://:5558` binds both.
62
+
63
+ Pipe takes two positional endpoints (input, output) or uses `--in`/`--out` for
64
+ multiple per side.
65
+
66
+ ## Socket types
67
+
68
+ ### Unidirectional (send-only / recv-only)
69
+
70
+ | Send | Recv | Pattern |
71
+ |------|------|---------|
72
+ | `push` | `pull` | Pipeline — round-robin to workers |
73
+ | `pub` | `sub` | Publish/subscribe — fan-out with topic prefix filtering |
74
+
75
+ Send-only sockets read from stdin (or `--data`/`--file`) and send. Recv-only
76
+ sockets receive and write to stdout.
77
+
78
+ ```sh
79
+ echo "task" | nnq push -c tcp://worker:5557
80
+ nnq pull -b tcp://:5557
81
+ ```
82
+
83
+ ### Bidirectional (request-reply)
84
+
85
+ | Type | Behavior |
86
+ |------|----------|
87
+ | `req` | Sends a request, waits for reply, prints reply |
88
+ | `rep` | Receives request, sends reply (from `--echo`, `-e`, `--data`, `--file`, or stdin) |
89
+
90
+ ```sh
91
+ # echo server
92
+ nnq rep -b tcp://:5555 --echo
93
+
94
+ # upcase server
95
+ nnq rep -b tcp://:5555 -e '$_.upcase'
96
+
97
+ # client
98
+ echo "hello" | nnq req -c tcp://localhost:5555
99
+ ```
100
+
101
+ ### Bidirectional (concurrent send + recv)
102
+
103
+ | Type | Behavior |
104
+ |------|----------|
105
+ | `pair` | Exclusive 1-to-1 — concurrent send and recv tasks |
106
+
107
+ These spawn two concurrent tasks: a receiver (prints incoming) and a sender
108
+ (reads stdin). `-e` transforms incoming, `-E` transforms outgoing.
109
+
110
+ ### Pipe (virtual)
111
+
112
+ Pipe creates an internal PULL → eval → PUSH pipeline:
113
+
114
+ ```sh
115
+ nnq pipe -c ipc://@work -c ipc://@sink -e '$_.upcase'
116
+
117
+ # with Ractor workers for CPU parallelism
118
+ nnq pipe -c ipc://@work -c ipc://@sink -P 4 -r./fib.rb -e 'fib(Integer($_)).to_s'
119
+ ```
120
+
121
+ The first endpoint is the pull-side (input), the second is the push-side
122
+ (output). For parallel mode (`-P`) all endpoints must be `--connect`.
123
+
124
+ ## Eval: -e and -E
125
+
126
+ `-e` (alias `--recv-eval`) runs a Ruby expression for each **incoming** message.
127
+ `-E` (alias `--send-eval`) runs a Ruby expression for each **outgoing** message.
128
+
129
+ ### Globals
130
+
131
+ | Variable | Value |
132
+ |----------|-------|
133
+ | `$_` | Message body (`String`) |
134
+ | `$F` | `[$_]` — 1-element array, kept for omq-cli compatibility |
135
+
136
+ ### Return value
137
+
138
+ | Return | Effect |
139
+ |--------|--------|
140
+ | `String` | Used as the message body |
141
+ | `Array` | First element used as the body |
142
+ | `nil` | Message is skipped (filtered) |
143
+ | `self` (the socket) | Signals "I already sent" (REP only) |
144
+
145
+ ### Control flow
146
+
147
+ ```sh
148
+ # skip messages matching a pattern
149
+ nnq pull -b tcp://:5557 -e 'next if /^#/.match?($_); $_'
150
+
151
+ # stop on "quit"
152
+ nnq pull -b tcp://:5557 -e 'break if /quit/.match?($_); $_'
153
+ ```
154
+
155
+ ### BEGIN/END blocks
156
+
157
+ Like awk — `BEGIN{}` runs once before the message loop, `END{}` runs after:
158
+
159
+ ```sh
160
+ nnq pull -b tcp://:5557 -e 'BEGIN{ @sum = 0 } @sum += Integer($_); next END{ puts @sum }'
161
+ ```
162
+
163
+ Local variables won't share state between blocks. Use `@ivars` instead.
164
+
165
+ ### Which sockets accept which flag
166
+
167
+ | Socket | `-E` (send) | `-e` (recv) |
168
+ |--------|-------------|-------------|
169
+ | push, pub | transforms outgoing | error |
170
+ | pull, sub | error | transforms incoming |
171
+ | req | transforms request | transforms reply |
172
+ | rep | error | transforms request → return = reply |
173
+ | pair | transforms outgoing | transforms incoming |
174
+ | pipe | error | transforms in pipeline |
175
+
176
+ ### Examples
177
+
178
+ ```sh
179
+ # upcase echo server
180
+ nnq rep -b tcp://:5555 -e '$_.upcase'
181
+
182
+ # transform before sending
183
+ echo hello | nnq push -c tcp://localhost:5557 -E '$_.upcase'
184
+
185
+ # filter incoming
186
+ nnq pull -b tcp://:5557 -e '$_.include?("error") ? $_ : nil'
187
+
188
+ # REQ: different transforms per direction
189
+ echo hello | nnq req -c tcp://localhost:5555 \
190
+ -E '$_.upcase' -e '$_.reverse'
191
+
192
+ # generate messages without stdin
193
+ nnq pub -c tcp://localhost:5556 -E 'Time.now.to_s' -i 1
194
+
195
+ # use gems
196
+ nnq sub -c tcp://localhost:5556 -s "" -rjson -e 'JSON.parse($_)["temperature"]'
197
+ ```
198
+
199
+ ## Script handlers (-r)
200
+
201
+ For non-trivial transforms, put the logic in a Ruby file and load it with `-r`:
202
+
203
+ ```ruby
204
+ # handler.rb
205
+ db = PG.connect("dbname=app")
206
+
207
+ NNQ.outgoing { |msg| msg.upcase }
208
+ NNQ.incoming { |msg| db.exec(msg).values.flatten.first }
209
+
210
+ at_exit { db.close }
211
+ ```
212
+
213
+ ```sh
214
+ nnq req -c tcp://localhost:5555 -r./handler.rb
215
+ ```
216
+
217
+ ### Registration API
218
+
219
+ | Method | Effect |
220
+ |--------|--------|
221
+ | `NNQ.outgoing { |msg| ... }` | Register outgoing message transform |
222
+ | `NNQ.incoming { |msg| ... }` | Register incoming message transform |
223
+
224
+ - `msg` is a `String` (the message body)
225
+ - Setup: use local variables and closures at the top of the script
226
+ - Teardown: use Ruby's `at_exit { ... }`
227
+ - CLI flags (`-e`/`-E`) override script-registered handlers for the same direction
228
+ - A script can register one direction while the CLI handles the other
229
+
230
+ ## Data sources
231
+
232
+ | Flag | Behavior |
233
+ |------|----------|
234
+ | (stdin) | Read lines from stdin, one message per line |
235
+ | `-D "text"` | Send literal string (one-shot or repeated with `-i`) |
236
+ | `-F file` | Read message from file (`-F -` reads stdin as blob) |
237
+ | `--echo` | Echo received messages back (REP only) |
238
+
239
+ `-D` and `-F` are mutually exclusive.
240
+
241
+ ## Formats
242
+
243
+ | Flag | Format |
244
+ |------|--------|
245
+ | `-A` / `--ascii` | Tab-separated frames, non-printable → dots (default) |
246
+ | `-Q` / `--quoted` | C-style escapes, lossless round-trip |
247
+ | `--raw` | Raw body, newline-delimited |
248
+ | `-J` / `--jsonl` | JSON Lines — `["body"]` per line |
249
+ | `--msgpack` | MessagePack arrays (binary stream) |
250
+ | `-M` / `--marshal` | Ruby Marshal (binary stream of `Array<String>` objects) |
251
+
252
+ Since SP messages are single-frame, ASCII/quoted modes don't insert tabs.
253
+
254
+ ```sh
255
+ nnq push -c tcp://localhost:5557 < data.txt
256
+ nnq pull -b tcp://:5557 -J
257
+ ```
258
+
259
+ ## Timing
260
+
261
+ | Flag | Effect |
262
+ |------|--------|
263
+ | `-i SECS` | Repeat send every N seconds (wall-clock aligned) |
264
+ | `-n COUNT` | Max messages to send/receive (0 = unlimited) |
265
+ | `-d SECS` | Delay before first send |
266
+ | `-t SECS` | Send/receive timeout |
267
+ | `-l SECS` | Linger time on close (default 5s) |
268
+ | `--reconnect-ivl` | Reconnect interval: `SECS` or `MIN..MAX` (default 0.1) |
269
+
270
+ ```sh
271
+ # publish a tick every second, 10 times
272
+ nnq pub -c tcp://localhost:5556 -D "tick" -i 1 -n 10 -d 1
273
+
274
+ # receive with 5s timeout
275
+ nnq pull -b tcp://:5557 -t 5
276
+ ```
277
+
278
+ ## Compression
279
+
280
+ Both sides must use `--compress` (`-z`). Uses LZ4 frame format, provided by
281
+ the `rlz4` gem (Ractor-safe, Rust extension via `lz4_flex`).
282
+
283
+ ```sh
284
+ nnq push -c tcp://remote:5557 -z < data.txt
285
+ nnq pull -b tcp://:5557 -z
286
+ ```
287
+
288
+ ## Subscriptions
289
+
290
+ ```sh
291
+ # subscribe to topic prefix
292
+ nnq sub -b tcp://:5556 -s "weather."
293
+
294
+ # subscribe to all (default)
295
+ nnq sub -b tcp://:5556
296
+
297
+ # multiple subscriptions
298
+ nnq sub -b tcp://:5556 -s "weather." -s "sports."
299
+ ```
300
+
301
+ ## Pipe
302
+
303
+ Pipe creates an in-process PULL → eval → PUSH pipeline:
304
+
305
+ ```sh
306
+ # basic pipe (positional: first = input, second = output)
307
+ nnq pipe -c ipc://@work -c ipc://@sink -e '$_.upcase'
308
+
309
+ # parallel Ractor workers (default: all CPUs)
310
+ nnq pipe -c ipc://@work -c ipc://@sink -P -r./fib.rb -e 'fib(Integer($_)).to_s'
311
+
312
+ # fixed number of workers
313
+ nnq pipe -c ipc://@work -c ipc://@sink -P 4 -e '$_.upcase'
314
+
315
+ # exit when producer disconnects
316
+ nnq pipe -c ipc://@work -c ipc://@sink --transient -e '$_.upcase'
317
+ ```
318
+
319
+ ### Multi-peer pipe with `--in`/`--out`
320
+
321
+ Use `--in` and `--out` to attach multiple endpoints per side. These are modal
322
+ switches — subsequent `-b`/`-c` flags attach to the current side:
323
+
324
+ ```sh
325
+ # fan-in: 2 producers → 1 consumer
326
+ nnq pipe --in -c ipc://@work1 -c ipc://@work2 --out -c ipc://@sink -e '$_'
327
+
328
+ # fan-out: 1 producer → 2 consumers (round-robin)
329
+ nnq pipe --in -b tcp://:5555 --out -c ipc://@sink1 -c ipc://@sink2 -e '$_'
330
+
331
+ # parallel workers with fan-in (all must be -c)
332
+ nnq pipe --in -c ipc://@a -c ipc://@b --out -c ipc://@sink -P 4 -e '$_'
333
+ ```
334
+
335
+ `-P`/`--parallel` requires all endpoints to be `--connect`. In parallel mode,
336
+ each Ractor worker gets its own PULL/PUSH pair connecting to all endpoints.
337
+
338
+ ## Transient mode
339
+
340
+ `--transient` makes the socket exit when all peers disconnect. Useful for
341
+ pipeline workers and sinks:
342
+
343
+ ```sh
344
+ # worker exits when producer is done
345
+ nnq pipe -c ipc://@work -c ipc://@sink --transient -e '$_.upcase'
346
+
347
+ # sink exits when all workers disconnect
348
+ nnq pull -b tcp://:5557 --transient
349
+ ```
350
+
351
+ ## Verbose / monitor mode
352
+
353
+ Pass `-v` (repeatable) for increasingly chatty output:
354
+
355
+ | Level | Output |
356
+ |-------|--------|
357
+ | `-v` | Bind/connect endpoints |
358
+ | `-vv` | Lifecycle events (`:listening`, `:connected`, `:disconnected`, ...) |
359
+ | `-vvv` | Per-message trace (`:message_sent`, `:message_received`) |
360
+
361
+ ```sh
362
+ nnq pub -b tcp://:5556 -vv
363
+ ```
364
+
365
+ ## Exit codes
366
+
367
+ | Code | Meaning |
368
+ |------|---------|
369
+ | 0 | Success |
370
+ | 1 | Error (connection, argument, runtime) |
371
+ | 2 | Timeout |
372
+ | 3 | Eval error (`-e`/`-E` expression raised) |
373
+
374
+ ## Interop with libnng / nngcat
375
+
376
+ nnq-cli speaks the SP wire protocol, so it interoperates with `nngcat` and any
377
+ libnng-based peer over `tcp://` and `ipc://`:
378
+
379
+ ```sh
380
+ # nnq → nngcat
381
+ nngcat --pull0 --listen tcp://127.0.0.1:5555 --quoted &
382
+ echo hello | nnq push -c tcp://127.0.0.1:5555
383
+
384
+ # nngcat → nnq
385
+ nnq pull -b tcp://127.0.0.1:5555 &
386
+ nngcat --push0 --dial tcp://127.0.0.1:5555 --data "hello-from-nngcat"
387
+ ```
388
+
389
+ ## License
390
+
391
+ [ISC](LICENSE)
data/exe/nnq ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ Warning[:experimental] = false
4
+
5
+ if !ENV["RUBYOPT"] && defined?(RubyVM::YJIT) && !RubyVM::YJIT.enabled?
6
+ RubyVM::YJIT.enable
7
+ end
8
+
9
+ require "nnq/cli"
10
+ NNQ::CLI.run