omq 0.6.5 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94402697e206444e39655869c1382cea2a2839732d373d6cbcc237dcb125cd44
4
- data.tar.gz: dbcaa10eb1213e935325d0290611716552d28394dd479d8fc79564a0af6f0e4d
3
+ metadata.gz: 852e5a45a7b9f61b32004aa2079e1289291c3ac2ecc5efeb78911d88d3b63b2e
4
+ data.tar.gz: b475a15807b8e98d4a852ec78b07ecaa4a0b15e68d3aec2fd9b2702533cb5d5f
5
5
  SHA512:
6
- metadata.gz: 9a4b4eab9767b52f874bf85816589558bf42e9730da827220490e80ca9dd499ae5ae604c06022d3db07402042a1b52ea1c88737e2c1556b4d78af90391dc1d2f
7
- data.tar.gz: e2db5e6aaea16af48024be90af8353a81f4d77a4b0b1973131f74dbd915b9aaf52fdd06d08cc57749ffb74b6bd70214ffb2ab04063925887c8a1b72afd4582cb
6
+ metadata.gz: 0afb8f440937ae11bebe4ce3ecc1df599103f27300ed721b14437d708bd1cba1cd5b0aeb6873bd7281d054c42f6353abc29b1c8c542f4708152c2ad6b047b5f0
7
+ data.tar.gz: b3477d54c520f7ed09dc4647b84841c9937d863036c4301afc3110bd959fd794bbd91db56d0204f317cd66ef62bf63ad8b1772521dc48f96912ae01e9bea8f81
data/CHANGELOG.md CHANGED
@@ -1,5 +1,82 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0 — 2026-03-31
4
+
5
+ ### Breaking
6
+
7
+ - **CURVE mechanism moved to protocol-zmtp** — `OMQ::ZMTP::Mechanism::Curve`
8
+ is now `Protocol::ZMTP::Mechanism::Curve` with a required `crypto:` parameter.
9
+ Pass `crypto: RbNaCl` (libsodium) or `crypto: Nuckle` (pure Ruby). The
10
+ omq-curve and omq-kurve gems are superseded.
11
+
12
+ ```ruby
13
+ # Before (omq-curve)
14
+ require "omq/curve"
15
+ rep.mechanism = OMQ::Curve.server(pub, sec)
16
+
17
+ # After (protocol-zmtp + any NaCl backend)
18
+ require "protocol/zmtp/mechanism/curve"
19
+ require "nuckle" # or: require "rbnacl"
20
+ rep.mechanism = Protocol::ZMTP::Mechanism::Curve.server(pub, sec, crypto: Nuckle)
21
+ ```
22
+
23
+ ### Changed
24
+
25
+ - **Protocol layer extracted into protocol-zmtp gem** — Codec (Frame,
26
+ Greeting, Command), Connection, Mechanism::Null, Mechanism::Curve,
27
+ ValidPeers, and Z85 now live in the
28
+ [protocol-zmtp](https://github.com/paddor/protocol-zmtp) gem. OMQ
29
+ re-exports them under `OMQ::ZMTP::` for backwards compatibility.
30
+ protocol-zmtp has zero runtime dependencies.
31
+ - **Unified CURVE mechanism** — one implementation with a pluggable
32
+ `crypto:` backend replaces the two near-identical copies in omq-curve
33
+ (RbNaCl) and omq-kurve (Nuckle). 1,088 → 467 lines (57% reduction).
34
+ - **Heartbeat ownership** — `Connection#start_heartbeat` removed.
35
+ Connection tracks timestamps only; the engine drives the PING/PONG loop.
36
+ - **CI no longer needs libsodium** — CURVE tests use
37
+ [nuckle](https://github.com/paddor/nuckle) (pure Ruby) by default.
38
+ Cross-backend interop tests run when rbnacl is available.
39
+
40
+ ## 0.7.0 — 2026-03-30
41
+
42
+ ### Breaking
43
+
44
+ - **`-e` is now `--recv-eval`** — evaluates incoming messages only.
45
+ Send-only sockets (PUSH, PUB, SCATTER, RADIO) must use `-E` /
46
+ `--send-eval` instead of `-e`.
47
+
48
+ ### Added
49
+
50
+ - **`-E` / `--send-eval`** — eval Ruby for each outgoing message.
51
+ REQ can now transform requests independently from replies.
52
+ ROUTER/SERVER/PEER: `-E` does dynamic routing (first element =
53
+ identity), mutually exclusive with `--target`.
54
+ - **`OMQ.outgoing` / `OMQ.incoming`** — registration API for script
55
+ handlers loaded via `-r`. Blocks receive message parts as a block
56
+ argument (`|msg|`). Setup via closures, teardown via `at_exit`.
57
+ CLI flags override registered handlers.
58
+ - **[CLI.md](CLI.md)** — comprehensive CLI documentation.
59
+ - **[GETTING_STARTED.md](GETTING_STARTED.md)** — renamed from
60
+ `ZGUIDE_SUMMARY.md` for discoverability.
61
+ - **Multi-peer pipe with `--in`/`--out`** — modal switches that assign
62
+ subsequent `-b`/`-c` to the PULL (input) or PUSH (output) side.
63
+ Enables fan-in, fan-out, and mixed bind/connect per side.
64
+ Backward compatible — without `--in`/`--out`, the positional
65
+ 2-endpoint syntax works as before.
66
+
67
+ ### Improved
68
+
69
+ - **YJIT recv pump** — replaced lambda/proc `transform:` parameter in
70
+ `Engine#start_recv_pump` with block captures. No-transform path
71
+ (PUSH/PULL, PUB/SUB) is now branch-free. ~2.5x YJIT speedup on
72
+ inproc, ~2x on ipc/tcp.
73
+
74
+ ### Fixed
75
+
76
+ - **Frozen array from `recv_msg_raw`** — ROUTER/SERVER receiver crashed
77
+ with `FrozenError` when shifting identity off frozen message arrays.
78
+ `#recv_msg_raw` now dups the array.
79
+
3
80
  ## 0.6.5 — 2026-03-30
4
81
 
5
82
  ### Fixed
@@ -462,4 +539,4 @@ Initial release. Pure Ruby implementation of ZMTP 3.1 (ZeroMQ) using Async.
462
539
  - Linger on close (drain send queue before closing)
463
540
  - `max_message_size` enforcement
464
541
  - Works inside Async reactors or standalone (shared IO thread)
465
- - Optional CURVE encryption via the [omq-curve](https://github.com/zeromq/omq-curve) gem
542
+ - Optional CURVE encryption via the [protocol-zmtp](https://github.com/paddor/protocol-zmtp) gem
data/README.md CHANGED
@@ -1,39 +1,44 @@
1
- # OMQ! Where did the C dependency go?!
1
+ # OMQ ZeroMQ in pure Ruby
2
2
 
3
3
  [![CI](https://github.com/zeromq/omq/actions/workflows/ci.yml/badge.svg)](https://github.com/zeromq/omq/actions/workflows/ci.yml)
4
4
  [![Gem Version](https://img.shields.io/gem/v/omq?color=e9573f)](https://rubygems.org/gems/omq)
5
5
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE)
6
6
  [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.3-CC342D?logo=ruby&logoColor=white)](https://www.ruby-lang.org)
7
7
 
8
- Pure Ruby implementation of the [ZMTP 3.1](https://rfc.zeromq.org/spec/23/) wire protocol ([ZeroMQ](https://zeromq.org/)) using the [Async](https://github.com/socketry/async) gem. No native libraries required.
8
+ `gem install omq` that's it. No libzmq, no compiler, no system packages. Just Ruby.
9
9
 
10
- > **244k msg/s** inproc | **47k msg/s** ipc | **36k msg/s** tcp
10
+ OMQ implements the [ZMTP 3.1](https://rfc.zeromq.org/spec/23/) wire protocol from scratch using [Async](https://github.com/socketry/async) fibers. It speaks native ZeroMQ on the wire and interoperates with libzmq, pyzmq, CZMQ, and everything else in the ZMQ ecosystem.
11
+
12
+ > **234k msg/s** inproc | **49k msg/s** ipc | **36k msg/s** tcp
11
13
  >
12
- > **9 µs** inproc latency | **47 µs** ipc | **61 µs** tcp
14
+ > **12 µs** inproc latency | **51 µs** ipc | **62 µs** tcp
13
15
  >
14
- > Ruby 4.0 + YJIT on a Linux VM on a 2019 MacBook Pro (Intel) — [~340k msg/s with io_uring](bench/README.md#io_uring)
16
+ > Ruby 4.0 + YJIT on a Linux VM see [`bench/`](bench/) for full results
15
17
 
16
18
  ---
17
19
 
18
- ## Highlights
20
+ ## What is ZeroMQ?
21
+
22
+ Brokerless message-oriented middleware. No central server, no extra hop — processes talk directly to each other, cutting latency in half compared to broker-based systems. You get the patterns you'd normally build on top of RabbitMQ or Redis — pub/sub, work distribution, request/reply, fan-out — but decentralized, with no single point of failure.
19
23
 
20
- - **Pure Ruby** no C extensions, no FFI, no libzmq/libczmq dependency
21
- - **All socket types** — req/rep, pub/sub, push/pull, dealer/router, xpub/xsub, pair + draft types (client/server, radio/dish, scatter/gather, peer, channel)
22
- - **Async-native** — built on [Async](https://github.com/socketry/async) fibers, also works with plain threads
23
- - **Ruby-idiomatic API** — messages as `Array<String>`, errors as exceptions, timeouts as `IO::TimeoutError`
24
- - **All transports** — tcp, ipc, inproc
24
+ Networking is hard. ZeroMQ abstracts away reconnection, queuing, load balancing, and framing so you can focus on what your system actually does. Start with threads talking over `inproc://`, split into processes with `ipc://`, scale across machines with `tcp://` — same code, same API, just change the URL.
25
25
 
26
- ## Why pure Ruby?
26
+ If you've ever wired up services with raw TCP, HTTP polling, or Redis pub/sub and wished it was simpler, this is what you've been looking for.
27
27
 
28
- Modern Ruby has closed the gap:
28
+ See [GETTING_STARTED.md](GETTING_STARTED.md) for a ~30 min walkthrough of all major patterns with working code.
29
29
 
30
- - **YJIT** — JIT-compiled hot paths close the throughput gap with C extensions
31
- - **Fiber Scheduler** — non-blocking I/O without callbacks or threads (`Async` builds on this)
32
- - **`io-stream`** — buffered I/O with read-ahead, from the Async ecosystem
30
+ ## Highlights
33
31
 
34
- When [CZTop](https://github.com/paddor/cztop) was written, none of this existed. Today, a pure Ruby ZMTP implementation is fast enough for production use — and you get `gem install` with no compiler toolchain, no system packages, and no segfaults.
32
+ - **Zero dependencies on C** no extensions, no FFI, no libzmq. `gem install` just works everywhere
33
+ - **Fast** — YJIT-optimized hot paths, batched sends, 234k msg/s inproc with 12 µs latency
34
+ - **`omq` CLI** — pipe, filter, and transform messages from the terminal with Ruby eval, Ractor parallelism, and [script handlers](CLI.md#script-handlers--r)
35
+ - **Every socket pattern** — req/rep, pub/sub, push/pull, dealer/router, xpub/xsub, pair, and all draft types
36
+ - **Every transport** — tcp, ipc (Unix domain sockets), inproc (in-process queues)
37
+ - **Async-native** — built on fibers, non-blocking from the ground up. A shared IO thread handles sockets outside of Async — no reactor needed for simple scripts
38
+ - **Wire-compatible** — interoperates with libzmq, pyzmq, CZMQ over tcp and ipc
39
+ - **Bind/connect order doesn't matter** — connect before bind, bind before connect, peers come and go. ZeroMQ reconnects and requeues automatically
35
40
 
36
- See [DESIGN.md](DESIGN.md) for architecture details: task trees, send pump batching, the ZMTP wire protocol, and how OMQ handles the fallacies of distributed computing.
41
+ For architecture internals, see [DESIGN.md](DESIGN.md).
37
42
 
38
43
  ## Install
39
44
 
@@ -45,10 +50,6 @@ gem install omq
45
50
  gem 'omq'
46
51
  ```
47
52
 
48
- ## Learning ZeroMQ
49
-
50
- New to ZeroMQ? See [ZGUIDE_SUMMARY.md](ZGUIDE_SUMMARY.md) — a ~30 min read covering all major patterns with working OMQ code examples.
51
-
52
53
  ## Quick Start
53
54
 
54
55
  ### Request / Reply
@@ -67,7 +68,7 @@ Async do |task|
67
68
  end
68
69
 
69
70
  req << 'hello'
70
- puts req.receive.inspect # => ["HELLO"]
71
+ p req.receive # => ["HELLO"]
71
72
  ensure
72
73
  req&.close
73
74
  rep&.close
@@ -83,7 +84,7 @@ Async do |task|
83
84
  sub.subscribe('') # subscribe to all
84
85
 
85
86
  task.async { pub << 'news flash' }
86
- puts sub.receive.inspect # => ["news flash"]
87
+ p sub.receive # => ["news flash"]
87
88
  ensure
88
89
  pub&.close
89
90
  sub&.close
@@ -98,51 +99,56 @@ Async do
98
99
  pull = OMQ::PULL.bind('inproc://pipeline')
99
100
 
100
101
  push << 'work item'
101
- puts pull.receive.inspect # => ["work item"]
102
+ p pull.receive # => ["work item"]
102
103
  ensure
103
104
  push&.close
104
105
  pull&.close
105
106
  end
106
107
  ```
107
108
 
108
- ## Socket Types
109
+ ### Without Async (IO thread)
109
110
 
110
- | Pattern | Classes | Direction | Multipart |
111
- |---------|---------|-----------|-----------|
112
- | Request/Reply | `REQ`, `REP` | bidirectional | yes |
113
- | Publish/Subscribe | `PUB`, `SUB`, `XPUB`, `XSUB` | unidirectional | yes |
114
- | Pipeline | `PUSH`, `PULL` | unidirectional | yes |
115
- | Routing | `DEALER`, `ROUTER` | bidirectional | yes |
116
- | Exclusive pair | `PAIR` | bidirectional | yes |
117
- | Client/Server | `CLIENT`, `SERVER` | bidirectional | no |
118
- | Group messaging | `RADIO`, `DISH` | unidirectional | no |
119
- | Pipeline (draft) | `SCATTER`, `GATHER` | unidirectional | no |
120
- | Peer-to-peer | `PEER` | bidirectional | no |
121
- | Channel (draft) | `CHANNEL` | bidirectional | no |
122
-
123
- All classes live under `OMQ::`. For the purists, `ØMQ` is an alias:
111
+ OMQ spawns a shared `omq-io` thread when used outside an Async reactor — no boilerplate needed:
124
112
 
125
113
  ```ruby
126
- req = ØMQ::REQ.new(">tcp://localhost:5555")
114
+ require 'omq'
115
+
116
+ push = OMQ::PUSH.bind('tcp://127.0.0.1:5557')
117
+ pull = OMQ::PULL.connect('tcp://127.0.0.1:5557')
118
+
119
+ push << 'hello'
120
+ p pull.receive # => ["hello"]
121
+
122
+ push.close
123
+ pull.close
127
124
  ```
128
125
 
129
- ## Performance
126
+ The IO thread runs all pumps, reconnection, and heartbeating in the background. When you're inside an `Async` block, OMQ uses the existing reactor instead.
130
127
 
131
- Benchmarked with benchmark-ips on Linux x86_64 (Ruby 4.0.2 +YJIT):
128
+ ## Socket Types
132
129
 
133
- #### Throughput (push/pull, 64 B messages)
130
+ All sockets are thread-safe. Default HWM is 1000 messages per socket. Classes live under `OMQ::` (alias: `ØMQ`).
134
131
 
135
- | inproc | ipc | tcp |
136
- |--------|-----|-----|
137
- | 244k/s | 47k/s | 36k/s |
132
+ #### Standard (multipart messages)
138
133
 
139
- #### Latency (req/rep roundtrip)
134
+ | Pattern | Send | Receive | When HWM full |
135
+ |---------|------|---------|---------------|
136
+ | **REQ** / **REP** | Round-robin / route-back | Fair-queue | Block |
137
+ | **PUB** / **SUB** | Fan-out to subscribers | Subscription filter | Drop |
138
+ | **PUSH** / **PULL** | Round-robin to workers | Fair-queue | Block |
139
+ | **DEALER** / **ROUTER** | Round-robin / identity-route | Fair-queue | Block |
140
+ | **XPUB** / **XSUB** | Fan-out (subscription events) | Fair-queue | Drop |
141
+ | **PAIR** | Exclusive 1-to-1 | Exclusive 1-to-1 | Block |
140
142
 
141
- | inproc | ipc | tcp |
142
- |--------|-----|-----|
143
- | 9 µs | 47 µs | 61 µs |
143
+ #### Draft (single-frame only)
144
144
 
145
- See [`bench/`](bench/) for full results and scripts.
145
+ | Pattern | Send | Receive | When HWM full |
146
+ |---------|------|---------|---------------|
147
+ | **CLIENT** / **SERVER** | Round-robin / routing-ID | Fair-queue | Block |
148
+ | **RADIO** / **DISH** | Group fan-out | Group filter | Drop |
149
+ | **SCATTER** / **GATHER** | Round-robin | Fair-queue | Block |
150
+ | **PEER** | Routing-ID | Fair-queue | Block |
151
+ | **CHANNEL** | Exclusive 1-to-1 | Exclusive 1-to-1 | Block |
146
152
 
147
153
  ## omq — CLI tool
148
154
 
@@ -152,7 +158,7 @@ See [`bench/`](bench/) for full results and scripts.
152
158
  # Echo server
153
159
  omq rep -b tcp://:5555 --echo
154
160
 
155
- # Upcase server in one line
161
+ # Upcase server -e evals Ruby on each incoming message
156
162
  omq rep -b tcp://:5555 -e '$F.map(&:upcase)'
157
163
 
158
164
  # Client
@@ -163,50 +169,39 @@ echo "hello" | omq req -c tcp://localhost:5555
163
169
  omq sub -b tcp://:5556 -s "weather." &
164
170
  echo "weather.nyc 72F" | omq pub -c tcp://localhost:5556 -d 0.3
165
171
 
166
- # Pipeline with filtering ($F = message parts, $_ = first part)
167
- # /regexp/ matches against $_, next skips, break stops
172
+ # Pipeline with filtering
168
173
  tail -f /var/log/syslog | omq push -c tcp://collector:5557
169
174
  omq pull -b tcp://:5557 -e 'next unless /error/; $F'
170
175
 
176
+ # Transform outgoing messages with -E
177
+ echo hello | omq push -c tcp://localhost:5557 -E '$F.map(&:upcase)'
178
+
179
+ # REQ: transform request and reply independently
180
+ echo hello | omq req -c tcp://localhost:5555 \
181
+ -E '$F.map(&:upcase)' -e '$F.map(&:reverse)'
182
+
171
183
  # Pipe: PULL → eval → PUSH in one process
172
184
  omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
173
185
 
174
186
  # Pipe with Ractor workers for CPU parallelism (-P = all CPUs)
175
187
  omq pipe -c ipc://@work -c ipc://@sink -P -r./fib -e 'fib(Integer($_)).to_s'
188
+ ```
176
189
 
177
- # Exit when all peers disconnect (pipeline workers, sinks)
178
- omq pipe -c ipc://@work -c ipc://@sink --transient -e '$F'
179
-
180
- # JSONL for structured data
181
- echo '["key","value"]' | omq push -c tcp://localhost:5557 -J
182
- omq pull -b tcp://:5557 -J
190
+ `-e` (recv-eval) transforms incoming messages, `-E` (send-eval) transforms outgoing messages. `$F` is the message parts array, `$_` is the first part. Use `-r` to require gems or load scripts that register handlers via `OMQ.incoming` / `OMQ.outgoing`:
183
191
 
184
- # Zstandard compression
185
- omq push -c tcp://remote:5557 -z < data.txt
186
- omq pull -b tcp://:5557 -z
192
+ ```ruby
193
+ # my_handler.rb
194
+ db = DB.connect("postgres://localhost/app")
187
195
 
188
- # CURVE encryption
189
- omq rep -b tcp://:5555 -D "secret" --curve-server
190
- # prints: OMQ_SERVER_KEY='...'
191
- omq req -c tcp://localhost:5555 --curve-server-key '...'
196
+ OMQ.incoming { db.query($F.first) }
197
+ at_exit { db.close }
192
198
  ```
193
199
 
194
- The `-e` flag runs Ruby inside the socket instance — the full socket API (`self <<`, `send`, `subscribe`, ...) is available. `$F` is the message parts array, `$_` is the first part. Use `-r` to require gems:
195
-
196
200
  ```sh
197
- omq sub -c tcp://localhost:5556 -s "" -r json \
198
- -e 'JSON.parse($F.first)["temperature"]'
199
-
200
- # BEGIN/END blocks (like awk) — accumulate and summarize
201
- omq pull -b tcp://:5557 \
202
- -e 'BEGIN{ @sum = 0 } @sum += Integer($_); next END{ puts @sum }'
201
+ omq pull -b tcp://:5557 -r./my_handler.rb
203
202
  ```
204
203
 
205
- Formats: `--ascii` (default, tab-separated), `--quoted`, `--raw`, `--jsonl`, `--msgpack`, `--marshal`. See `omq --help` for all options.
206
-
207
- ## Interop with native ZMQ
208
-
209
- OMQ speaks ZMTP 3.1 on the wire and interoperates with libzmq, CZMQ, pyzmq, etc. over **tcp** and **ipc**. The `inproc://` transport is OMQ-internal (in-process Ruby queues) and is not visible to native ZMQ running in the same process — use `ipc://` to talk across library boundaries.
204
+ See [CLI.md](CLI.md) for full documentation, or `omq --help` / `omq --examples`.
210
205
 
211
206
  ## Development
212
207
 
@@ -28,9 +28,11 @@ module OMQ
28
28
  sleep(config.delay) if config.delay && config.recv_only?
29
29
  wait_for_peer if needs_peer_wait?
30
30
 
31
- @sock.instance_exec(&@begin_proc) if @begin_proc
31
+ @sock.instance_exec(&@send_begin_proc) if @send_begin_proc
32
+ @sock.instance_exec(&@recv_begin_proc) if @recv_begin_proc
32
33
  run_loop(task)
33
- @sock.instance_exec(&@end_proc) if @end_proc
34
+ @sock.instance_exec(&@send_end_proc) if @send_end_proc
35
+ @sock.instance_exec(&@recv_end_proc) if @recv_end_proc
34
36
  ensure
35
37
  @sock&.close
36
38
  end
@@ -201,19 +203,19 @@ module OMQ
201
203
  end
202
204
  end
203
205
  elsif config.data || config.file
204
- parts = eval_expr(read_next)
206
+ parts = eval_send_expr(read_next)
205
207
  send_msg(parts) if parts
206
208
  elsif stdin_ready?
207
209
  loop do
208
210
  parts = read_next
209
211
  break unless parts
210
- parts = eval_expr(parts)
212
+ parts = eval_send_expr(parts)
211
213
  send_msg(parts) if parts
212
214
  i += 1
213
215
  break if n && n > 0 && i >= n
214
216
  end
215
- elsif @eval_proc
216
- parts = eval_expr(nil)
217
+ elsif @send_eval_proc
218
+ parts = eval_send_expr(nil)
217
219
  send_msg(parts) if parts
218
220
  end
219
221
  end
@@ -221,11 +223,11 @@ module OMQ
221
223
 
222
224
  def send_tick
223
225
  raw = read_next_or_nil
224
- if raw.nil? && !@eval_proc
226
+ if raw.nil? && !@send_eval_proc
225
227
  @send_tick_eof = true
226
228
  return 0
227
229
  end
228
- parts = eval_expr(raw)
230
+ parts = eval_send_expr(raw)
229
231
  send_msg(parts) if parts
230
232
  1
231
233
  end
@@ -237,7 +239,7 @@ module OMQ
237
239
  loop do
238
240
  parts = recv_msg
239
241
  break if parts.nil?
240
- parts = eval_expr(parts)
242
+ parts = eval_recv_expr(parts)
241
243
  output(parts)
242
244
  i += 1
243
245
  break if n && n > 0 && i >= n
@@ -246,7 +248,7 @@ module OMQ
246
248
 
247
249
 
248
250
  def wait_for_loops(receiver, sender)
249
- if config.data || config.file || config.expr || config.target
251
+ if config.data || config.file || config.send_expr || config.recv_expr || config.target
250
252
  sender.wait
251
253
  receiver.stop
252
254
  elsif config.count && config.count > 0
@@ -281,7 +283,8 @@ module OMQ
281
283
 
282
284
 
283
285
  def recv_msg_raw
284
- @sock.receive
286
+ msg = @sock.receive
287
+ msg&.dup
285
288
  end
286
289
 
287
290
 
@@ -320,7 +323,7 @@ module OMQ
320
323
  def read_next_or_nil
321
324
  if config.data || config.file
322
325
  read_next
323
- elsif @eval_proc
326
+ elsif @send_eval_proc
324
327
  nil
325
328
  else
326
329
  read_next
@@ -358,11 +361,30 @@ module OMQ
358
361
 
359
362
 
360
363
  def compile_expr
361
- return unless config.expr
362
- expr, begin_body, end_body = extract_blocks(config.expr)
363
- @begin_proc = eval("proc { #{begin_body} }") if begin_body
364
- @end_proc = eval("proc { #{end_body} }") if end_body
365
- @eval_proc = eval("proc { $_ = $F&.first; #{expr} }") if expr && !expr.strip.empty?
364
+ compile_one_expr(:send, config.send_expr)
365
+ compile_one_expr(:recv, config.recv_expr)
366
+ @send_eval_proc ||= wrap_registered_proc(OMQ.outgoing_proc)
367
+ @recv_eval_proc ||= wrap_registered_proc(OMQ.incoming_proc)
368
+ end
369
+
370
+
371
+ def wrap_registered_proc(block)
372
+ return unless block
373
+ proc do |msg|
374
+ $_ = msg&.first
375
+ block.call(msg)
376
+ end
377
+ end
378
+
379
+
380
+ def compile_one_expr(direction, src)
381
+ return unless src
382
+ expr, begin_body, end_body = extract_blocks(src)
383
+ instance_variable_set(:"@#{direction}_begin_proc", eval("proc { #{begin_body} }")) if begin_body
384
+ instance_variable_set(:"@#{direction}_end_proc", eval("proc { #{end_body} }")) if end_body
385
+ if expr && !expr.strip.empty?
386
+ instance_variable_set(:"@#{direction}_eval_proc", eval("proc { $_ = $F&.first; #{expr} }"))
387
+ end
366
388
  end
367
389
 
368
390
 
@@ -398,10 +420,21 @@ module OMQ
398
420
 
399
421
  SENT = Object.new.freeze # sentinel: eval already sent the reply
400
422
 
401
- def eval_expr(parts)
402
- return parts unless @eval_proc
423
+ def eval_send_expr(parts)
424
+ return parts unless @send_eval_proc
425
+ run_eval(@send_eval_proc, parts)
426
+ end
427
+
428
+
429
+ def eval_recv_expr(parts)
430
+ return parts unless @recv_eval_proc
431
+ run_eval(@recv_eval_proc, parts)
432
+ end
433
+
434
+
435
+ def run_eval(eval_proc, parts)
403
436
  $F = parts
404
- result = @sock.instance_exec(&@eval_proc)
437
+ result = @sock.instance_exec(parts, &eval_proc)
405
438
  return nil if result.nil?
406
439
  return SENT if result.equal?(@sock)
407
440
  return [result] if config.format == :marshal
@@ -411,7 +444,7 @@ module OMQ
411
444
  else [result.to_str]
412
445
  end
413
446
  rescue => e
414
- $stderr.puts "omq: -e error: #{e.message} (#{e.class})"
447
+ $stderr.puts "omq: eval error: #{e.message} (#{e.class})"
415
448
  exit 3
416
449
  end
417
450
 
@@ -11,7 +11,7 @@ module OMQ
11
11
 
12
12
 
13
13
  def run_loop(task)
14
- if config.echo || config.expr || config.data || config.file || !config.stdin_is_tty
14
+ if config.echo || config.recv_expr || @recv_eval_proc || config.data || config.file || !config.stdin_is_tty
15
15
  reply_loop
16
16
  else
17
17
  monitor_loop(task)
@@ -28,8 +28,8 @@ module OMQ
28
28
  routing_id = parts.shift
29
29
  body = @fmt.decompress(parts)
30
30
 
31
- if config.expr
32
- reply = eval_expr(body)
31
+ if config.recv_expr || @recv_eval_proc
32
+ reply = eval_recv_expr(body)
33
33
  output([display_routing_id(routing_id), *(reply || [""])])
34
34
  @sock.send_to(routing_id, @fmt.compress(reply || [""]).first)
35
35
  elsif config.echo
@@ -56,7 +56,7 @@ module OMQ
56
56
  break if parts.nil?
57
57
  routing_id = parts.shift
58
58
  parts = @fmt.decompress(parts)
59
- result = eval_expr([display_routing_id(routing_id), *parts])
59
+ result = eval_recv_expr([display_routing_id(routing_id), *parts])
60
60
  output(result)
61
61
  i += 1
62
62
  break if n && n > 0 && i >= n
@@ -71,18 +71,18 @@ module OMQ
71
71
  Async::Loop.quantized(interval: config.interval) do
72
72
  parts = read_next
73
73
  break unless parts
74
- send_targeted_or_plain(parts)
74
+ send_targeted_or_eval(parts)
75
75
  i += 1
76
76
  break if n && n > 0 && i >= n
77
77
  end
78
78
  elsif config.data || config.file
79
79
  parts = read_next
80
- send_targeted_or_plain(parts) if parts
80
+ send_targeted_or_eval(parts) if parts
81
81
  else
82
82
  loop do
83
83
  parts = read_next
84
84
  break unless parts
85
- send_targeted_or_plain(parts)
85
+ send_targeted_or_eval(parts)
86
86
  i += 1
87
87
  break if n && n > 0 && i >= n
88
88
  end
@@ -93,8 +93,13 @@ module OMQ
93
93
  end
94
94
 
95
95
 
96
- def send_targeted_or_plain(parts)
97
- if config.target
96
+ def send_targeted_or_eval(parts)
97
+ if @send_eval_proc
98
+ parts = eval_send_expr(parts)
99
+ return unless parts
100
+ routing_id = resolve_target(parts.shift)
101
+ @sock.send_to(routing_id, @fmt.compress(parts).first || "")
102
+ elsif config.target
98
103
  parts = @fmt.compress(parts)
99
104
  @sock.send_to(resolve_target(config.target), parts.first || "")
100
105
  else
@@ -2,6 +2,9 @@
2
2
 
3
3
  module OMQ
4
4
  module CLI
5
+ SEND_ONLY = %w[pub push scatter radio].freeze
6
+ RECV_ONLY = %w[sub pull gather dish].freeze
7
+
5
8
  Endpoint = Data.define(:url, :bind?) do
6
9
  def connect? = !bind?
7
10
  end
@@ -12,6 +15,8 @@ module OMQ
12
15
  :endpoints,
13
16
  :connects,
14
17
  :binds,
18
+ :in_endpoints,
19
+ :out_endpoints,
15
20
  :data,
16
21
  :file,
17
22
  :format,
@@ -29,7 +34,8 @@ module OMQ
29
34
  :heartbeat_ivl,
30
35
  :conflate,
31
36
  :compress,
32
- :expr,
37
+ :send_expr,
38
+ :recv_expr,
33
39
  :parallel,
34
40
  :transient,
35
41
  :verbose,
@@ -41,9 +47,6 @@ module OMQ
41
47
  :has_zstd,
42
48
  :stdin_is_tty,
43
49
  ) do
44
- SEND_ONLY = %w[pub push scatter radio].freeze
45
- RECV_ONLY = %w[sub pull gather dish].freeze
46
-
47
50
  def send_only? = SEND_ONLY.include?(type_name)
48
51
  def recv_only? = RECV_ONLY.include?(type_name)
49
52
  end
data/lib/omq/cli/pair.rb CHANGED
@@ -13,7 +13,7 @@ module OMQ
13
13
  loop do
14
14
  parts = recv_msg
15
15
  break if parts.nil?
16
- parts = eval_expr(parts)
16
+ parts = eval_recv_expr(parts)
17
17
  output(parts)
18
18
  i += 1
19
19
  break if n && n > 0 && i >= n