omq-cli 0.11.4 → 0.12.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eec1276bfcd012427b39e93126e9b5315ab548be7a65f5a98e6a3629b2bb158f
4
- data.tar.gz: bc0019ca1bc43a0fb590fc751fb1618a2c4c24b48bdf01439d48b2d0bf59081d
3
+ metadata.gz: 24cc89ae4c000ff153c8277b140b6ebf58cdcb4b9c7ad73acfc86edb35341ade
4
+ data.tar.gz: 0ab5cd61e2cb7cba892eeb7fa92761d2dd057d0e3ffe5ae217d88e5dd222cf43
5
5
  SHA512:
6
- metadata.gz: e53d6a22109b9a653847a3691534bca375c7975788acd505d2efed425d67468e6b277d57927e3e94b54ea79f08903a97d22993026ffa3e575cc57a5a30a12a09
7
- data.tar.gz: 5d33c5a8ce29e04b9547c07099bd31c4ed7fd6f12c7cd179b659bf1dd39c6cca59e37762a7001f83bb44d8d45db4c5c4eebea05e3b7ec62397378e3dfd86705a
6
+ metadata.gz: 1b4857e0c3388e32a24e1b415132266af442308494c569a7c5441c8b0cff87a8c5e872cda9f48568a3416e9f5030a6dab66fdc309212000f6c00cb293b5a3925
7
+ data.tar.gz: 6de453e861b990520665178dbe0066e8b18372dc224985cc3f66b3356d0d172a0fd02a2286e70885c92ee2acd9e61eb809bc607e8079ae2565d41a79cb11ea6d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.1 — 2026-04-10
4
+
5
+ ### Changed
6
+
7
+ - **`it` replaces `$F` and `$_` in eval expressions.** The `-e`/`-E`
8
+ message parts variable is now Ruby's default block variable `it`
9
+ instead of the `$F` global. `$_` is removed — use `it.first` instead.
10
+ This also simplifies Ractor worker compilation by removing the
11
+ `$F` → `__F` rewrite.
12
+
13
+ - **Block parameter syntax in `-e`/`-E` expressions.** Expressions can
14
+ now declare parameters like Ruby blocks: `-e '|msg| msg.map(&:upcase)'`.
15
+ A single parameter receives the whole parts array. Use `|(a, b)|` for
16
+ destructuring.
17
+
18
+ ### Added
19
+
20
+ - **`@name` endpoint shorthand.** `-c@work` and `-b@sink` expand to
21
+ `ipc://@work` and `ipc://@sink` (Linux abstract namespace). Only
22
+ triggers when the value starts with `@` and has no `://` scheme.
23
+
24
+ ### Fixed
25
+
26
+ - **Flaky FFI backend tests.** Wait for both REP peers to be connected
27
+ before round-robining requests, instead of only waiting for the first.
28
+
29
+ - **Improved verbose preview format.** Empty frames render as `''`
30
+ instead of `[0B]`. Multipart messages show frame count: `(18B 4F)`.
31
+
32
+ - **Compression skips nil/empty frames.** `compress` passes `nil` parts
33
+ through (coerced to empty frames by the socket layer). `decompress`
34
+ skips empty frames instead of feeding them to LZ4.
35
+
36
+ - **Pipe `--out` without `--in` promotes bare endpoints.** Bare `-c`/`-b`
37
+ before `--out` are now treated as `--in` endpoints (and vice versa),
38
+ fixing `pipe -c SRC --out -c DST` which previously errored.
39
+
40
+ - **Pipe fan-out distributes across output peers.** Multi-output pipes
41
+ now yield after each send, giving send-pump fibers a turn to drain the
42
+ shared queue. Without this, one pump monopolized the queue via
43
+ `drain_send_queue_capped` when messages arrived in bursts.
44
+
45
+ - **Pipe waits for all output peers with `--timeout`.** `wait_for_peers_with_timeout`
46
+ now waits for `connection_count >= out_eps.size` instead of just the first peer.
47
+
3
48
  ## 0.11.4 — 2026-04-10
4
49
 
5
50
  ### Fixed
data/README.md CHANGED
@@ -25,7 +25,7 @@ omq rep -b tcp://:5555 --echo
25
25
  echo "hello" | omq req -c tcp://localhost:5555
26
26
 
27
27
  # Upcase server — -e evals Ruby on each incoming message
28
- omq rep -b tcp://:5555 -e '$F.map(&:upcase)'
28
+ omq rep -b tcp://:5555 -e 'it.map(&:upcase)'
29
29
  ```
30
30
 
31
31
  ```
@@ -44,7 +44,7 @@ Every socket needs at least one `--bind` or `--connect`:
44
44
  omq pull --bind tcp://:5557 # listen on port 5557
45
45
  omq push --connect tcp://host:5557 # connect to host
46
46
  omq pull -b ipc:///tmp/feed.sock # IPC (unix socket)
47
- omq push -c ipc://@abstract # IPC (abstract namespace, Linux)
47
+ omq push -c@work # IPC abstract namespace (@name → ipc://@name)
48
48
  ```
49
49
 
50
50
  Multiple endpoints are allowed — `omq pull -b tcp://:5557 -b tcp://:5558` binds both.
@@ -82,7 +82,7 @@ omq pull -b tcp://:5557
82
82
  omq rep -b tcp://:5555 --echo
83
83
 
84
84
  # upcase server
85
- omq rep -b tcp://:5555 -e '$F.map(&:upcase)'
85
+ omq rep -b tcp://:5555 -e 'it.map(&:upcase)'
86
86
 
87
87
  # client
88
88
  echo "hello" | omq req -c tcp://localhost:5555
@@ -115,7 +115,7 @@ omq router -b tcp://:5555
115
115
  omq router -b tcp://:5555 --target worker-1 -D "reply"
116
116
 
117
117
  # dynamic routing via send-eval (first element = identity)
118
- omq router -b tcp://:5555 -E '["worker-1", $_.upcase]'
118
+ omq router -b tcp://:5555 -E '["worker-1", it.first.upcase]'
119
119
  ```
120
120
 
121
121
  `--target` and `--send-eval` are mutually exclusive on routing sockets.
@@ -125,10 +125,10 @@ omq router -b tcp://:5555 -E '["worker-1", $_.upcase]'
125
125
  Pipe creates an internal PULL → eval → PUSH pipeline:
126
126
 
127
127
  ```sh
128
- omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
128
+ omq pipe -c@work -c@sink -e 'it.map(&:upcase)'
129
129
 
130
130
  # with Ractor workers for CPU parallelism
131
- omq pipe -c ipc://@work -c ipc://@sink -P 4 -r./fib.rb -e 'fib(Integer($_)).to_s'
131
+ omq pipe -c@work -c@sink -P 4 -r./fib.rb -e 'fib(Integer(it.first)).to_s'
132
132
  ```
133
133
 
134
134
  The first endpoint is the pull-side (input), the second is the push-side (output).
@@ -139,12 +139,24 @@ Both must use `-c`.
139
139
  `-e` (alias `--recv-eval`) runs a Ruby expression for each **incoming** message.
140
140
  `-E` (alias `--send-eval`) runs a Ruby expression for each **outgoing** message.
141
141
 
142
- ### Globals
142
+ ### Variables
143
143
 
144
144
  | Variable | Value |
145
145
  |----------|-------|
146
- | `$F` | Message parts (`Array<String>`) |
147
- | `$_` | First part (`$F.first`) — works in inline expressions |
146
+ | `it` | Message parts (`Array<String>`) — Ruby's default block variable |
147
+
148
+ ### Block parameters
149
+
150
+ Expressions support Ruby block parameter syntax. A single parameter receives
151
+ the whole parts array; use `|(a, b)|` to destructure:
152
+
153
+ ```sh
154
+ # single param = parts array
155
+ omq pull -b tcp://:5557 -e '|msg| msg.map(&:upcase)'
156
+
157
+ # destructure multipart messages
158
+ omq pull -b tcp://:5557 -e '|(key, value)| "#{key}=#{value}"'
159
+ ```
148
160
 
149
161
  ### Return value
150
162
 
@@ -159,10 +171,10 @@ Both must use `-c`.
159
171
 
160
172
  ```sh
161
173
  # skip messages matching a pattern
162
- omq pull -b tcp://:5557 -e 'next if /^#/; $F'
174
+ omq pull -b tcp://:5557 -e 'next if /^#/; it'
163
175
 
164
176
  # stop on "quit"
165
- omq pull -b tcp://:5557 -e 'break if /quit/; $F'
177
+ omq pull -b tcp://:5557 -e 'break if /quit/; it'
166
178
  ```
167
179
 
168
180
  ### BEGIN/END blocks
@@ -170,7 +182,7 @@ omq pull -b tcp://:5557 -e 'break if /quit/; $F'
170
182
  Like awk — `BEGIN{}` runs once before the message loop, `END{}` runs after:
171
183
 
172
184
  ```sh
173
- omq pull -b tcp://:5557 -e 'BEGIN{ @sum = 0 } @sum += Integer($_); next END{ puts @sum }'
185
+ omq pull -b tcp://:5557 -e 'BEGIN{ @sum = 0 } @sum += Integer(it.first); next END{ puts @sum }'
174
186
  ```
175
187
 
176
188
  Local variables won't work to share state between the blocks. Use `@ivars` instead.
@@ -191,23 +203,23 @@ Local variables won't work to share state between the blocks. Use `@ivars` inste
191
203
 
192
204
  ```sh
193
205
  # upcase echo server
194
- omq rep -b tcp://:5555 -e '$F.map(&:upcase)'
206
+ omq rep -b tcp://:5555 -e 'it.map(&:upcase)'
195
207
 
196
208
  # transform before sending
197
- echo hello | omq push -c tcp://localhost:5557 -E '$F.map(&:upcase)'
209
+ echo hello | omq push -c tcp://localhost:5557 -E 'it.map(&:upcase)'
198
210
 
199
211
  # filter incoming
200
- omq pull -b tcp://:5557 -e '$F.first.include?("error") ? $F : nil'
212
+ omq pull -b tcp://:5557 -e 'it.first.include?("error") ? it : nil'
201
213
 
202
214
  # REQ: different transforms per direction
203
215
  echo hello | omq req -c tcp://localhost:5555 \
204
- -E '$F.map(&:upcase)' -e '$F.map(&:reverse)'
216
+ -E 'it.map(&:upcase)' -e 'it.map(&:reverse)'
205
217
 
206
218
  # generate messages without stdin
207
219
  omq pub -c tcp://localhost:5556 -E 'Time.now.to_s' -i 1
208
220
 
209
221
  # use gems
210
- omq sub -c tcp://localhost:5556 -s "" -rjson -e 'JSON.parse($F.first)["temperature"]'
222
+ omq sub -c tcp://localhost:5556 -s "" -rjson -e 'JSON.parse(it.first)["temperature"]'
211
223
  ```
212
224
 
213
225
  ## Script handlers (-r)
@@ -235,7 +247,7 @@ omq req -c tcp://localhost:5555 -r./handler.rb
235
247
  | `OMQ.outgoing { |msg| ... }` | Register outgoing message transform |
236
248
  | `OMQ.incoming { |msg| ... }` | Register incoming message transform |
237
249
 
238
- - use explicit block variable (like `msg`) instead of `$F`/`$_`
250
+ - use explicit block variable (like `msg`) or `it`
239
251
  - Setup: use local variables and closures at the top of the script
240
252
  - Teardown: use Ruby's `at_exit { ... }`
241
253
  - CLI flags (`-e`/`-E`) override script-registered handlers for the same direction
@@ -243,7 +255,7 @@ omq req -c tcp://localhost:5555 -r./handler.rb
243
255
 
244
256
  ```sh
245
257
  # handler.rb registers recv_eval, CLI adds send_eval
246
- omq req -c tcp://localhost:5555 -r./handler.rb -E '$F.map(&:upcase)'
258
+ omq req -c tcp://localhost:5555 -r./handler.rb -E 'it.map(&:upcase)'
247
259
  ```
248
260
 
249
261
  ### Script handler examples
@@ -415,7 +427,7 @@ omq router -b tcp://:5555
415
427
  omq router -b tcp://:5555 --target worker-1 -D "reply"
416
428
 
417
429
  # ROUTER dynamic routing via -E (first element = routing identity)
418
- omq router -b tcp://:5555 -E '["worker-1", $_.upcase]'
430
+ omq router -b tcp://:5555 -E '["worker-1", it.first.upcase]'
419
431
 
420
432
  # binary routing IDs (0x prefix)
421
433
  omq router -b tcp://:5555 --target 0xdeadbeef -D "reply"
@@ -427,16 +439,16 @@ Pipe creates an in-process PULL → eval → PUSH pipeline:
427
439
 
428
440
  ```sh
429
441
  # basic pipe (positional: first = input, second = output)
430
- omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
442
+ omq pipe -c@work -c@sink -e 'it.map(&:upcase)'
431
443
 
432
444
  # parallel Ractor workers (default: all CPUs)
433
- omq pipe -c ipc://@work -c ipc://@sink -P -r./fib.rb -e 'fib(Integer($_)).to_s'
445
+ omq pipe -c@work -c@sink -P -r./fib.rb -e 'fib(Integer(it.first)).to_s'
434
446
 
435
447
  # fixed number of workers
436
- omq pipe -c ipc://@work -c ipc://@sink -P 4 -e '$F.map(&:upcase)'
448
+ omq pipe -c@work -c@sink -P 4 -e 'it.map(&:upcase)'
437
449
 
438
450
  # exit when producer disconnects
439
- omq pipe -c ipc://@work -c ipc://@sink --transient -e '$F.map(&:upcase)'
451
+ omq pipe -c@work -c@sink --transient -e 'it.map(&:upcase)'
440
452
  ```
441
453
 
442
454
  ### Multi-peer pipe with `--in`/`--out`
@@ -446,16 +458,16 @@ Use `--in` and `--out` to attach multiple endpoints per side. These are modal sw
446
458
 
447
459
  ```sh
448
460
  # fan-in: 2 producers → 1 consumer
449
- omq pipe --in -c ipc://@work1 -c ipc://@work2 --out -c ipc://@sink -e '$F'
461
+ omq pipe --in -c@work1 -c@work2 --out -c@sink -e 'it'
450
462
 
451
463
  # fan-out: 1 producer → 2 consumers (round-robin)
452
- omq pipe --in -b tcp://:5555 --out -c ipc://@sink1 -c ipc://@sink2 -e '$F'
464
+ omq pipe --in -b tcp://:5555 --out -c@sink1 -c@sink2 -e 'it'
453
465
 
454
466
  # bind on input, connect on output
455
- omq pipe --in -b tcp://:5555 -b tcp://:5556 --out -c tcp://sink:5557 -e '$F'
467
+ omq pipe --in -b tcp://:5555 -b tcp://:5556 --out -c tcp://sink:5557 -e 'it'
456
468
 
457
469
  # parallel workers with fan-in (all must be -c)
458
- omq pipe --in -c ipc://@a -c ipc://@b --out -c ipc://@sink -P 4 -e '$F'
470
+ omq pipe --in -c@a -c@b --out -c@sink -P 4 -e 'it'
459
471
  ```
460
472
 
461
473
  `-P`/`--parallel` requires all endpoints to be `--connect`. In parallel mode, each Ractor worker
@@ -467,7 +479,7 @@ gets its own PULL/PUSH pair connecting to all endpoints.
467
479
 
468
480
  ```sh
469
481
  # worker exits when producer is done
470
- omq pipe -c ipc://@work -c ipc://@sink --transient -e '$F.map(&:upcase)'
482
+ omq pipe -c@work -c@sink --transient -e 'it.map(&:upcase)'
471
483
 
472
484
  # sink exits when all workers disconnect
473
485
  omq pull -b tcp://:5557 --transient
@@ -10,13 +10,13 @@ module OMQ
10
10
  EXAMPLES = <<~'TEXT'
11
11
  -- Request / Reply ------------------------------------------
12
12
 
13
- +-----+ "hello" +-----+
13
+ +-----+ "hello" +-----+
14
14
  | REQ |------------->| REP |
15
15
  | |<-------------| |
16
- +-----+ "HELLO" +-----+
16
+ +-----+ "HELLO" +-----+
17
17
 
18
18
  # terminal 1: echo server
19
- omq rep --bind tcp://:5555 --recv-eval '$F.map(&:upcase)'
19
+ omq rep --bind tcp://:5555 --recv-eval 'it.map(&:upcase)'
20
20
 
21
21
  # terminal 2: send a request
22
22
  echo "hello" | omq req --connect tcp://localhost:5555
@@ -28,7 +28,7 @@ module OMQ
28
28
  -- Publish / Subscribe --------------------------------------
29
29
 
30
30
  +-----+ "weather.nyc 72F" +-----+
31
- | PUB |--------------------->| SUB | --subscribe "weather."
31
+ | PUB |-------------------->| SUB | --subscribe "weather."
32
32
  +-----+ +-----+
33
33
 
34
34
  # terminal 1: subscriber (all topics by default)
@@ -39,25 +39,23 @@ module OMQ
39
39
 
40
40
  -- Periodic Publish -------------------------------------------
41
41
 
42
- +-----+ "tick 1" +-----+
42
+ +-----+ "tick 1" +-----+
43
43
  | PUB |--(every 1s)-->| SUB |
44
- +-----+ +-----+
44
+ +-----+ +-----+
45
45
 
46
46
  # terminal 1: subscriber
47
47
  omq sub --bind tcp://:5556
48
48
 
49
49
  # terminal 2: publish a tick every second (wall-clock aligned)
50
- omq pub --connect tcp://localhost:5556 --delay 1 \
51
- --data "tick" --interval 1
50
+ omq pub --connect tcp://localhost:5556 --delay 1 --data "tick" --interval 1
52
51
 
53
52
  # 5 ticks, then exit
54
- omq pub --connect tcp://localhost:5556 --delay 1 \
55
- --data "tick" --interval 1 --count 5
53
+ omq pub --connect tcp://localhost:5556 -d1 -D "tick" -i0.5 --count 5
56
54
 
57
55
  -- Pipeline -------------------------------------------------
58
56
 
59
57
  +------+ +------+
60
- | PUSH |----------->| PULL |
58
+ | PUSH |---------->| PULL |
61
59
  +------+ +------+
62
60
 
63
61
  # terminal 1: worker
@@ -73,39 +71,38 @@ module OMQ
73
71
  -- Pipe (PULL -> eval -> PUSH) --------------------------------
74
72
 
75
73
  +------+ +------+ +------+
76
- | PUSH |--------->| pipe |--------->| PULL |
74
+ | PUSH |-------->| pipe |-------->| PULL |
77
75
  +------+ +------+ +------+
78
76
 
79
77
  # terminal 1: producer
80
- echo -e "hello\nworld" | omq push --bind ipc://@work
78
+ echo -e "hello\nworld" | omq push -b@work
81
79
 
82
80
  # terminal 2: worker -- uppercase each message
83
- omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
81
+ omq pipe -c@work -c@sink -e 'it.map(&:upcase)'
84
82
  # terminal 3: collector
85
- omq pull --bind ipc://@sink
83
+ omq pull -b@sink
86
84
 
87
85
  # 4 Ractor workers in a single process (-P)
88
- omq pipe -c ipc://@work -c ipc://@sink -P4 -r./fib -e 'fib(Integer($_)).to_s'
86
+ omq pipe -c@work -c@sink -P4 -r./fib -e 'fib(it.first.to_i).to_s'
89
87
 
90
88
  # exit when producer disconnects (--transient)
91
- omq pipe -c ipc://@work -c ipc://@sink --transient -e '$F.map(&:upcase)'
89
+ omq pipe -c@work -c@sink --transient -e 'it.map(&:upcase)'
92
90
 
93
91
  # fan-in: multiple sources -> one sink
94
- omq pipe --in -c ipc://@work1 -c ipc://@work2 \
95
- --out -c ipc://@sink -e '$F.map(&:upcase)'
92
+ omq pipe --in -c@work1 -c@work2 --out -c@sink -e 'it.map(&:upcase)'
96
93
 
97
94
  # fan-out: one source -> multiple sinks (round-robin)
98
- omq pipe --in -b tcp://:5555 --out -c ipc://@sink1 -c ipc://@sink2 -e '$F'
95
+ omq pipe --in -b tcp://:5555 --out -c@sink1 -c@sink2 -e 'it'
99
96
 
100
97
  -- CLIENT / SERVER (draft) ----------------------------------
101
98
 
102
- +--------+ "hello" +--------+
103
- | CLIENT |------------>| SERVER | --recv-eval '$F.map(&:upcase)'
99
+ +--------+ "hello" +--------+
100
+ | CLIENT |------------>| SERVER | --recv-eval 'it.map(&:upcase)'
104
101
  | |<------------| |
105
- +--------+ "HELLO" +--------+
102
+ +--------+ "HELLO" +--------+
106
103
 
107
104
  # terminal 1: upcasing server
108
- omq server --bind tcp://:5555 --recv-eval '$F.map(&:upcase)'
105
+ omq server --bind tcp://:5555 --recv-eval 'it.map(&:upcase)'
109
106
 
110
107
  # terminal 2: client
111
108
  echo "hello" | omq client --connect tcp://localhost:5555
@@ -127,9 +124,9 @@ module OMQ
127
124
 
128
125
  -- Compression ----------------------------------------------
129
126
 
130
- # both sides must use --compress
127
+ # both sides must use --compress/-z
131
128
  omq pull --bind tcp://:5557 --compress &
132
- echo "compressible data" | omq push --connect tcp://localhost:5557 --compress
129
+ echo "compressible data" | omq push --connect tcp://localhost:5557 -z
133
130
 
134
131
  -- CURVE Encryption -----------------------------------------
135
132
 
@@ -143,7 +140,7 @@ module OMQ
143
140
  -- ROUTER / DEALER ------------------------------------------
144
141
 
145
142
  +--------+ +--------+
146
- | DEALER |---------->| ROUTER |
143
+ | DEALER |--------->| ROUTER |
147
144
  | id=w1 | | |
148
145
  +--------+ +--------+
149
146
 
@@ -156,25 +153,31 @@ module OMQ
156
153
  -- Ruby Eval ------------------------------------------------
157
154
 
158
155
  # filter incoming: only pass messages containing "error"
159
- omq pull -b tcp://:5557 --recv-eval '$F.first.include?("error") ? $F : nil'
156
+ omq pull -b tcp://:5557 --recv-eval 'it.first.include?("error") ? it : nil'
160
157
 
161
158
  # transform incoming with gems
162
- omq sub -c tcp://localhost:5556 -rjson -e 'JSON.parse($F.first)["temperature"]'
159
+ omq sub -c tcp://localhost:5556 -rjson -e 'JSON.parse(it.first)["temperature"]'
163
160
 
164
161
  # require a local file, use its methods
165
- omq rep --bind tcp://:5555 --require ./transform.rb -e 'upcase_all($F)'
162
+ omq rep --bind tcp://:5555 --require ./transform.rb -e 'upcase_all(it)'
166
163
 
167
- # next skips, break stops -- regexps match against $_
168
- omq pull -b tcp://:5557 -e 'next if /^#/; break if /quit/; $F'
164
+ # next skips, break stops
165
+ omq pull -b tcp://:5557 -e 'next if it.first =~ /^#/; break if it.first =~ /quit/; it'
169
166
 
170
167
  # BEGIN/END blocks (like awk) -- accumulate and summarize
171
- omq pull -b tcp://:5557 -e 'BEGIN{@sum = 0} @sum += Integer($_); nil END{puts @sum}'
168
+ omq pull -b tcp://:5557 -e 'BEGIN{@sum = 0} @sum += it.first.to_i; nil END{puts @sum}'
172
169
 
173
170
  # transform outgoing messages
174
- echo hello | omq push -c tcp://localhost:5557 --send-eval '$F.map(&:upcase)'
171
+ echo hello | omq push -c tcp://localhost:5557 --send-eval 'it.map(&:upcase)'
175
172
 
176
173
  # REQ: transform request and reply independently
177
- echo hello | omq req -c tcp://localhost:5555 -E '$F.map(&:upcase)' -e '$_'
174
+ echo hello | omq req -c tcp://localhost:5555 -E 'it.map(&:upcase)' -e 'it.first'
175
+
176
+ # block parameter: single param receives parts array
177
+ omq pull -b tcp://:5557 -e '|msg| msg.map(&:upcase)'
178
+
179
+ # destructure multipart messages with parens
180
+ omq pull -b tcp://:5557 -e '|(key, value)| "#{key}=#{value}"'
178
181
 
179
182
  -- Script Handlers (-r) ------------------------------------
180
183
 
@@ -185,7 +188,7 @@ module OMQ
185
188
  omq pull --bind tcp://:5557 -r./handler.rb
186
189
 
187
190
  # combine script handlers with inline eval
188
- omq req -c tcp://localhost:5555 -r./handler.rb -E '$F.map(&:upcase)'
191
+ omq req -c tcp://localhost:5555 -r./handler.rb -E 'it.map(&:upcase)'
189
192
 
190
193
  # OMQ.outgoing { |msg| ... } -- registered outgoing transform
191
194
  # OMQ.incoming { |msg| ... } -- registered incoming transform
@@ -278,6 +281,7 @@ module OMQ
278
281
 
279
282
  o.separator "Connection:"
280
283
  o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v|
284
+ v = expand_endpoint(v)
281
285
  ep = Endpoint.new(v, false)
282
286
  case pipe_side
283
287
  when :in
@@ -290,6 +294,7 @@ module OMQ
290
294
  end
291
295
  }
292
296
  o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v|
297
+ v = expand_endpoint(v)
293
298
  ep = Endpoint.new(v, true)
294
299
  case pipe_side
295
300
  when :in
@@ -373,8 +378,8 @@ module OMQ
373
378
  end
374
379
 
375
380
  o.separator "\nProcessing (-e = incoming, -E = outgoing):"
376
- o.on("-e", "--recv-eval EXPR", "Eval Ruby for each incoming message ($F = parts)") { |v| opts[:recv_expr] = v }
377
- o.on("-E", "--send-eval EXPR", "Eval Ruby for each outgoing message ($F = parts)") { |v| opts[:send_expr] = v }
381
+ o.on("-e", "--recv-eval EXPR", "Eval Ruby for each incoming message (it = parts, or |a, b|)") { |v| opts[:recv_expr] = v }
382
+ o.on("-E", "--send-eval EXPR", "Eval Ruby for each outgoing message (it = parts, or |a, b|)") { |v| opts[:send_expr] = v }
378
383
  o.on("-r", "--require LIB", "Require lib/file in Async context; use '-' for stdin. Scripts can register OMQ.outgoing/incoming") { |v|
379
384
  require "omq" unless defined?(OMQ::VERSION)
380
385
  opts[:scripts] << (v == "-" ? :stdin : (v.start_with?("./", "../") ? File.expand_path(v) : v))
@@ -495,6 +500,15 @@ module OMQ
495
500
  if type_name == "pipe"
496
501
  has_in_out = opts[:in_endpoints].any? || opts[:out_endpoints].any?
497
502
  if has_in_out
503
+ # Promote bare endpoints into the missing side:
504
+ # `pipe -c SRC --out -c DST` → bare SRC becomes --in
505
+ if opts[:in_endpoints].empty? && opts[:endpoints].any?
506
+ opts[:in_endpoints] = opts[:endpoints]
507
+ opts[:endpoints] = []
508
+ elsif opts[:out_endpoints].empty? && opts[:endpoints].any?
509
+ opts[:out_endpoints] = opts[:endpoints]
510
+ opts[:endpoints] = []
511
+ end
498
512
  abort "pipe --in requires at least one endpoint" if opts[:in_endpoints].empty?
499
513
  abort "pipe --out requires at least one endpoint" if opts[:out_endpoints].empty?
500
514
  abort "pipe: don't mix --in/--out with bare -b/-c endpoints" unless opts[:endpoints].empty?
@@ -543,6 +557,14 @@ module OMQ
543
557
  end
544
558
 
545
559
 
560
+ # Expands shorthand `@name` to `ipc://@name` (Linux abstract namespace).
561
+ # Only triggers when the value starts with `@` and has no `://` scheme.
562
+ #
563
+ def expand_endpoint(url)
564
+ url.start_with?("@") && !url.include?("://") ? "ipc://#{url}" : url
565
+ end
566
+
567
+
546
568
  # Returns the loopback address for bind normalization.
547
569
  # Prefers IPv6 loopback ([::1]) when the host has at least one
548
570
  # non-loopback, non-link-local IPv6 address, otherwise 127.0.0.1.
@@ -26,16 +26,13 @@ module OMQ
26
26
 
27
27
  if src
28
28
  expr, begin_body, end_body = extract_blocks(src)
29
- @begin_proc = eval("proc { #{begin_body} }") if begin_body # rubocop:disable Security/Eval
30
- @end_proc = eval("proc { #{end_body} }") if end_body # rubocop:disable Security/Eval
29
+ @begin_proc = eval("proc { #{begin_body} }") if begin_body
30
+ @end_proc = eval("proc { #{end_body} }") if end_body
31
31
  if expr && !expr.strip.empty?
32
- @eval_proc = eval("proc { $_ = $F&.first; #{expr} }") # rubocop:disable Security/Eval
32
+ @eval_proc = eval("proc { #{expr} }")
33
33
  end
34
34
  elsif fallback_proc
35
- @eval_proc = proc { |msg|
36
- $_ = msg&.first
37
- fallback_proc.call(msg)
38
- }
35
+ @eval_proc = proc { fallback_proc.call(it) }
39
36
  end
40
37
  end
41
38
 
@@ -46,7 +43,6 @@ module OMQ
46
43
  def call(parts, context)
47
44
  return parts unless @eval_proc
48
45
 
49
- $F = parts
50
46
  result = context.instance_exec(parts, &@eval_proc)
51
47
  return nil if result.nil?
52
48
  return SENT if result.equal?(context)
@@ -108,12 +104,11 @@ module OMQ
108
104
  expr, begin_body = extract.(src, "BEGIN")
109
105
  expr, end_body = extract.(expr, "END")
110
106
 
111
- begin_proc = eval("proc { #{begin_body} }") if begin_body # rubocop:disable Security/Eval
112
- end_proc = eval("proc { #{end_body} }") if end_body # rubocop:disable Security/Eval
107
+ begin_proc = eval("proc { #{begin_body} }") if begin_body
108
+ end_proc = eval("proc { #{end_body} }") if end_body
113
109
  eval_proc = nil
114
110
  if expr && !expr.strip.empty?
115
- ractor_expr = expr.gsub(/\$F\b/, "__F")
116
- eval_proc = eval("proc { |__F| $_ = __F&.first; #{ractor_expr} }") # rubocop:disable Security/Eval
111
+ eval_proc = eval("proc { #{expr} }")
117
112
  end
118
113
 
119
114
  [begin_proc, end_proc, eval_proc]
@@ -88,16 +88,17 @@ module OMQ
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| RLZ4.compress(p) } : parts
91
+ @compress ? parts.map { |p| RLZ4.compress(p) if p } : parts
92
92
  end
93
93
 
94
94
 
95
95
  # Decompresses each frame with LZ4 if compression is enabled.
96
+ # nil/empty frames pass through — they were nil before send coercion.
96
97
  #
97
98
  # @param parts [Array<String>] possibly compressed message frames
98
99
  # @return [Array<String>] decompressed frames
99
100
  def decompress(parts)
100
- @compress ? parts.map { |p| RLZ4.decompress(p) } : parts
101
+ @compress ? parts.map { |p| p && !p.empty? ? RLZ4.decompress(p) : p } : parts
101
102
  rescue RLZ4::DecompressError
102
103
  raise DecompressError, "decompression failed (did the sender use --compress?)"
103
104
  end
@@ -108,11 +109,13 @@ module OMQ
108
109
  # @param parts [Array<String>] message frames
109
110
  # @return [String] truncated preview of each frame joined by |
110
111
  def self.preview(parts)
111
- total = parts.sum(&:bytesize)
112
- shown = parts.first(3).map { |p| preview_frame(p) }
113
- tail = parts.size > 3 ? "|...(#{parts.size} parts)" : ""
112
+ total = parts.sum(&:bytesize)
113
+ nparts = parts.size
114
+ shown = parts.first(3).map { |p| preview_frame(p) }
115
+ tail = nparts > 3 ? "|..." : ""
116
+ header = nparts > 1 ? "(#{total}B #{nparts}F)" : "(#{total}B)"
114
117
 
115
- "(#{total}B) #{shown.join("|")}#{tail}"
118
+ "#{header} #{shown.join("|")}#{tail}"
116
119
  end
117
120
 
118
121
 
@@ -122,7 +125,7 @@ module OMQ
122
125
  # string — otherwise joining with "|" would produce misleading
123
126
  # output like "|body" for REP/REQ-style envelopes where the first
124
127
  # wire frame is an empty delimiter.
125
- return "[0B]" if bytes.empty?
128
+ return "''" if bytes.empty?
126
129
 
127
130
  sample = bytes[0, 12]
128
131
  printable = sample.count("\x20-\x7e")
data/lib/omq/cli/pipe.rb CHANGED
@@ -55,7 +55,7 @@ module OMQ
55
55
  wait_for_peers_with_timeout if config.timeout
56
56
  setup_sequential_transient(task)
57
57
  @sock.instance_exec(&@recv_begin_proc) if @recv_begin_proc
58
- sequential_message_loop
58
+ sequential_message_loop(fan_out: out_eps.size > 1)
59
59
  @sock.instance_exec(&@recv_end_proc) if @recv_end_proc
60
60
  ensure
61
61
  @pull&.close
@@ -67,10 +67,13 @@ module OMQ
67
67
  # it, there's no point waiting: PULL#receive blocks naturally
68
68
  # and PUSH buffers up to send_hwm when no peer is present.
69
69
  def wait_for_peers_with_timeout
70
+ _, out_eps = resolve_endpoints
70
71
  Fiber.scheduler.with_timeout(config.timeout) do
71
72
  Barrier do |barrier|
72
- barrier.async(annotation: "wait push peer") { @push.peer_connected.wait }
73
73
  barrier.async(annotation: "wait pull peer") { @pull.peer_connected.wait }
74
+ barrier.async(annotation: "wait push peers") do
75
+ sleep 0.01 until @push.connection_count >= out_eps.size
76
+ end
74
77
  end
75
78
  end
76
79
  end
@@ -98,7 +101,7 @@ module OMQ
98
101
  end
99
102
 
100
103
 
101
- def sequential_message_loop
104
+ def sequential_message_loop(fan_out: false)
102
105
  n = config.count
103
106
  i = 0
104
107
  loop do
@@ -109,6 +112,12 @@ module OMQ
109
112
  if parts && !parts.empty?
110
113
  @push.send(@fmt_out.compress(parts))
111
114
  end
115
+ # Yield after send so send-pump fibers can drain the queue
116
+ # before the next message is enqueued. Without this, one pump
117
+ # monopolizes the shared queue via drain_send_queue_capped when
118
+ # messages arrive in bursts (recv prefetch). Only needed for
119
+ # multi-output pipes; single-output has no fairness concern.
120
+ Async::Task.current.yield if fan_out
112
121
  i += 1
113
122
  break if n && n > 0 && i >= n
114
123
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OMQ
4
4
  module CLI
5
- VERSION = "0.11.4"
5
+ VERSION = "0.12.1"
6
6
  end
7
7
  end
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.11.4
4
+ version: 0.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -18,7 +18,7 @@ dependencies:
18
18
  version: '0.17'
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: 0.17.6
21
+ version: 0.17.8
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -28,7 +28,7 @@ dependencies:
28
28
  version: '0.17'
29
29
  - - ">="
30
30
  - !ruby/object:Gem::Version
31
- version: 0.17.6
31
+ version: 0.17.8
32
32
  - !ruby/object:Gem::Dependency
33
33
  name: omq-ffi
34
34
  requirement: !ruby/object:Gem::Requirement