omq 0.8.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 852e5a45a7b9f61b32004aa2079e1289291c3ac2ecc5efeb78911d88d3b63b2e
4
- data.tar.gz: b475a15807b8e98d4a852ec78b07ecaa4a0b15e68d3aec2fd9b2702533cb5d5f
3
+ metadata.gz: a14af693117ccfb193f54e9bc2a4faae9c2bf6396e7769abee97f279fd62fdba
4
+ data.tar.gz: 1f5e99ae3b61402d2b9e506c84fdb07d2003695515d3f736159cf2c373cc39d0
5
5
  SHA512:
6
- metadata.gz: 0afb8f440937ae11bebe4ce3ecc1df599103f27300ed721b14437d708bd1cba1cd5b0aeb6873bd7281d054c42f6353abc29b1c8c542f4708152c2ad6b047b5f0
7
- data.tar.gz: b3477d54c520f7ed09dc4647b84841c9937d863036c4301afc3110bd959fd794bbd91db56d0204f317cd66ef62bf63ad8b1772521dc48f96912ae01e9bea8f81
6
+ metadata.gz: 76539e2a9fbdcbe59590e1fa499e300b8f7ad3d527255af6f94665139c3c0a60151f0b82202df372df1cdbc16e90f46369aa7b159342f951cf52babc897b837c
7
+ data.tar.gz: dba241d3283a0a0330ba0d12081c08f3ae074acaa8be7b1e9292dee9c19a483d822f896a4442ec585005dbeb36efa704b87f51c53e07c1de0ede54a85150f55c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 — 2026-03-31
4
+
5
+ ### Breaking
6
+
7
+ - **CLI extracted into omq-cli gem** — the `omq` executable, all CLI
8
+ code (`lib/omq/cli/`), tests, and `CLI.md` have moved to the
9
+ [omq-cli](https://github.com/paddor/omq-cli) gem. `gem install omq`
10
+ no longer provides the `omq` command — use `gem install omq-cli`.
11
+ - **`OMQ.outgoing` / `OMQ.incoming`** registration API moved to omq-cli.
12
+ Library-only users are unaffected (these were CLI-specific).
13
+
14
+ ### Changed
15
+
16
+ - **Gemspec is library-only** — no `exe/`, no `bindir`, no `executables`.
17
+ - **README** — restored title, replaced inline CLI section with a
18
+ pointer to omq-cli, fixed ZMTP attribution for protocol-zmtp.
19
+ - **DESIGN.md** — acknowledged protocol-zmtp, clarified transient
20
+ task / linger interaction, removed ZMTP wire protocol section (now in
21
+ protocol-zmtp), simplified inproc description, removed CLI section.
22
+
3
23
  ## 0.8.0 — 2026-03-31
4
24
 
5
25
  ### Breaking
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # OMQ — ZeroMQ in pure Ruby
1
+ # OMQ — Where did the C dependency go!?
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)
@@ -7,7 +7,7 @@
7
7
 
8
8
  `gem install omq` — that's it. No libzmq, no compiler, no system packages. Just Ruby.
9
9
 
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.
10
+ OMQ builds ZeroMQ socket patterns on top of [protocol-zmtp](https://github.com/paddor/protocol-zmtp) (a pure Ruby [ZMTP 3.1](https://rfc.zeromq.org/spec/23/) codec) 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
11
 
12
12
  > **234k msg/s** inproc | **49k msg/s** ipc | **36k msg/s** tcp
13
13
  >
@@ -31,12 +31,12 @@ See [GETTING_STARTED.md](GETTING_STARTED.md) for a ~30 min walkthrough of all ma
31
31
 
32
32
  - **Zero dependencies on C** — no extensions, no FFI, no libzmq. `gem install` just works everywhere
33
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)
34
+ - **[`omq` CLI](https://github.com/paddor/omq-cli)** — `gem install omq-cli` for a command-line tool with Ruby eval, Ractor parallelism, and script handlers
35
35
  - **Every socket pattern** — req/rep, pub/sub, push/pull, dealer/router, xpub/xsub, pair, and all draft types
36
36
  - **Every transport** — tcp, ipc (Unix domain sockets), inproc (in-process queues)
37
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
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
39
+ - **Bind/connect order doesn't matter** — connect before bind, bind before connect, peers come and go. ZeroMQ reconnects automatically and queued messages drain when peers arrive
40
40
 
41
41
  For architecture internals, see [DESIGN.md](DESIGN.md).
42
42
 
@@ -150,58 +150,18 @@ All sockets are thread-safe. Default HWM is 1000 messages per socket. Classes li
150
150
  | **PEER** | Routing-ID | Fair-queue | Block |
151
151
  | **CHANNEL** | Exclusive 1-to-1 | Exclusive 1-to-1 | Block |
152
152
 
153
- ## omq — CLI tool
153
+ ## CLI
154
154
 
155
- `omq` is a command-line tool for sending and receiving messages on any OMQ socket. Like `nngcat` from libnng, but with Ruby superpowers.
155
+ Install [omq-cli](https://github.com/paddor/omq-cli) for a command-line tool that sends, receives, pipes, and transforms ZeroMQ messages from the terminal:
156
156
 
157
157
  ```sh
158
- # Echo server
159
- omq rep -b tcp://:5555 --echo
160
-
161
- # Upcase server — -e evals Ruby on each incoming message
162
- omq rep -b tcp://:5555 -e '$F.map(&:upcase)'
158
+ gem install omq-cli
163
159
 
164
- # Client
160
+ omq rep -b tcp://:5555 --echo
165
161
  echo "hello" | omq req -c tcp://localhost:5555
166
- # => HELLO
167
-
168
- # PUB/SUB
169
- omq sub -b tcp://:5556 -s "weather." &
170
- echo "weather.nyc 72F" | omq pub -c tcp://localhost:5556 -d 0.3
171
-
172
- # Pipeline with filtering
173
- tail -f /var/log/syslog | omq push -c tcp://collector:5557
174
- omq pull -b tcp://:5557 -e 'next unless /error/; $F'
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
-
183
- # Pipe: PULL → eval → PUSH in one process
184
- omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
185
-
186
- # Pipe with Ractor workers for CPU parallelism (-P = all CPUs)
187
- omq pipe -c ipc://@work -c ipc://@sink -P -r./fib -e 'fib(Integer($_)).to_s'
188
- ```
189
-
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`:
191
-
192
- ```ruby
193
- # my_handler.rb
194
- db = DB.connect("postgres://localhost/app")
195
-
196
- OMQ.incoming { db.query($F.first) }
197
- at_exit { db.close }
198
- ```
199
-
200
- ```sh
201
- omq pull -b tcp://:5557 -r./my_handler.rb
202
162
  ```
203
163
 
204
- See [CLI.md](CLI.md) for full documentation, or `omq --help` / `omq --examples`.
164
+ See the [omq-cli README](https://github.com/paddor/omq-cli) for full documentation.
205
165
 
206
166
  ## Development
207
167
 
data/lib/omq/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OMQ
4
- VERSION = "0.8.0"
4
+ VERSION = "0.9.0"
5
5
  end
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -51,40 +51,21 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0.11'
54
- description: Pure Ruby implementation of the ZMTP 3.1 wire protocol (ZeroMQ) with
55
- all socket types (REQ/REP, PUB/SUB, PUSH/PULL, DEALER/ROUTER, and draft types) and
56
- TCP/IPC/inproc transports. Includes an `omq` CLI for composable message pipelines
57
- — pipe, filter, and transform across processes with Ruby eval, multiple formats
58
- (JSON, msgpack, marshal), Ractor parallelism, and compression. No native libraries
54
+ description: Pure Ruby implementation of ZeroMQ with all socket types (REQ/REP, PUB/SUB,
55
+ PUSH/PULL, DEALER/ROUTER, and draft types) and TCP/IPC/inproc transports. Built
56
+ on protocol-zmtp (ZMTP 3.1 wire protocol) and Async fibers. No native libraries
59
57
  required.
60
58
  email:
61
59
  - paddor@gmail.com
62
- executables:
63
- - omq
60
+ executables: []
64
61
  extensions: []
65
62
  extra_rdoc_files: []
66
63
  files:
67
64
  - CHANGELOG.md
68
65
  - LICENSE
69
66
  - README.md
70
- - exe/omq
71
67
  - lib/omq.rb
72
68
  - lib/omq/channel.rb
73
- - lib/omq/cli.rb
74
- - lib/omq/cli/base_runner.rb
75
- - lib/omq/cli/channel.rb
76
- - lib/omq/cli/client_server.rb
77
- - lib/omq/cli/config.rb
78
- - lib/omq/cli/formatter.rb
79
- - lib/omq/cli/pair.rb
80
- - lib/omq/cli/peer.rb
81
- - lib/omq/cli/pipe.rb
82
- - lib/omq/cli/pub_sub.rb
83
- - lib/omq/cli/push_pull.rb
84
- - lib/omq/cli/radio_dish.rb
85
- - lib/omq/cli/req_rep.rb
86
- - lib/omq/cli/router_dealer.rb
87
- - lib/omq/cli/scatter_gather.rb
88
69
  - lib/omq/client_server.rb
89
70
  - lib/omq/pair.rb
90
71
  - lib/omq/peer.rb
@@ -148,5 +129,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
129
  requirements: []
149
130
  rubygems_version: 4.0.6
150
131
  specification_version: 4
151
- summary: Pure Ruby ZMQ library + CLI
132
+ summary: Pure Ruby ZMQ library
152
133
  test_files: []
data/exe/omq DELETED
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- Warning[:experimental] = false
4
-
5
- require_relative "../lib/omq/cli"
6
- OMQ::CLI.run
@@ -1,459 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OMQ
4
- module CLI
5
- class BaseRunner
6
- attr_reader :config, :sock
7
-
8
-
9
- def initialize(config, socket_class)
10
- @config = config
11
- @klass = socket_class
12
- @fmt = Formatter.new(config.format, compress: config.compress)
13
- end
14
-
15
-
16
- def call(task)
17
- @sock = create_socket
18
- attach_endpoints
19
- setup_curve
20
- setup_subscriptions
21
- compile_expr
22
-
23
- if config.transient
24
- start_disconnect_monitor(task)
25
- Async::Task.current.yield # let monitor start waiting
26
- end
27
-
28
- sleep(config.delay) if config.delay && config.recv_only?
29
- wait_for_peer if needs_peer_wait?
30
-
31
- @sock.instance_exec(&@send_begin_proc) if @send_begin_proc
32
- @sock.instance_exec(&@recv_begin_proc) if @recv_begin_proc
33
- run_loop(task)
34
- @sock.instance_exec(&@send_end_proc) if @send_end_proc
35
- @sock.instance_exec(&@recv_end_proc) if @recv_end_proc
36
- ensure
37
- @sock&.close
38
- end
39
-
40
-
41
- private
42
-
43
-
44
- # Subclasses override this.
45
- def run_loop(task)
46
- raise NotImplementedError
47
- end
48
-
49
- # ── Socket creation ─────────────────────────────────────────────
50
-
51
-
52
- def create_socket
53
- sock_opts = { linger: config.linger }
54
- sock_opts[:conflate] = true if config.conflate && %w[pub radio].include?(config.type_name)
55
- sock = @klass.new(**sock_opts)
56
- sock.recv_timeout = config.timeout if config.timeout
57
- sock.send_timeout = config.timeout if config.timeout
58
- sock.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
59
- sock.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
60
- sock.identity = config.identity if config.identity
61
- sock.router_mandatory = true if config.type_name == "router"
62
- sock
63
- end
64
-
65
-
66
- def attach_endpoints
67
- config.binds.each do |url|
68
- @sock.bind(url)
69
- log "Bound to #{@sock.last_endpoint}"
70
- end
71
- config.connects.each do |url|
72
- @sock.connect(url)
73
- log "Connecting to #{url}"
74
- end
75
- end
76
-
77
- # ── Peer wait with grace period ─────────────────────────────────
78
-
79
-
80
- def needs_peer_wait?
81
- !config.recv_only? && (config.connects.any? || config.type_name == "router")
82
- end
83
-
84
-
85
- def wait_for_peer
86
- with_timeout(config.timeout) do
87
- @sock.peer_connected.wait
88
- log "Peer connected"
89
- if %w[pub xpub].include?(config.type_name)
90
- @sock.subscriber_joined.wait
91
- log "Subscriber joined"
92
- end
93
-
94
- # Grace period: when multiple peers may be connecting (bind or
95
- # multiple connect URLs), wait one reconnect interval so
96
- # latecomers finish their handshake before we start sending.
97
- if config.binds.any? || config.connects.size > 1
98
- ri = @sock.options.reconnect_interval
99
- sleep(ri.is_a?(Range) ? ri.begin : ri)
100
- end
101
- end
102
- end
103
-
104
- # ── Transient disconnect monitor ────────────────────────────────
105
-
106
-
107
- def start_disconnect_monitor(task)
108
- @transient_barrier = Async::Promise.new
109
- task.async do
110
- @transient_barrier.wait
111
- @sock.all_peers_gone.wait unless @sock.connection_count == 0
112
- log "All peers disconnected, exiting"
113
- @sock.reconnect_enabled = false
114
- if config.send_only?
115
- task.stop
116
- else
117
- @sock.close_read
118
- end
119
- end
120
- end
121
-
122
-
123
- def transient_ready!
124
- if config.transient && !@transient_barrier.resolved?
125
- @transient_barrier.resolve(true)
126
- end
127
- end
128
-
129
- # ── Timeout helper ──────────────────────────────────────────────
130
-
131
-
132
- def with_timeout(seconds)
133
- if seconds
134
- Async::Task.current.with_timeout(seconds) { yield }
135
- else
136
- yield
137
- end
138
- end
139
-
140
- # ── Socket setup ────────────────────────────────────────────────
141
-
142
-
143
- def setup_subscriptions
144
- case config.type_name
145
- when "sub"
146
- prefixes = config.subscribes.empty? ? [""] : config.subscribes
147
- prefixes.each { |p| @sock.subscribe(p) }
148
- when "dish"
149
- config.joins.each { |g| @sock.join(g) }
150
- end
151
- end
152
-
153
-
154
- def setup_curve
155
- server_key_z85 = config.curve_server_key || ENV["OMQ_SERVER_KEY"]
156
- server_mode = config.curve_server || (ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"])
157
-
158
- if server_key_z85
159
- if ENV["OMQ_DEV"]
160
- require_relative "../../../../omq-curve/lib/omq/curve"
161
- else
162
- require "omq/curve"
163
- end
164
- server_key = OMQ::Z85.decode(server_key_z85)
165
- client_key = RbNaCl::PrivateKey.generate
166
- @sock.mechanism = OMQ::Curve.client(
167
- client_key.public_key.to_s, client_key.to_s, server_key: server_key
168
- )
169
- elsif server_mode
170
- if ENV["OMQ_DEV"]
171
- require_relative "../../../../omq-curve/lib/omq/curve"
172
- else
173
- require "omq/curve"
174
- end
175
- if ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"]
176
- server_pub = OMQ::Z85.decode(ENV["OMQ_SERVER_PUBLIC"])
177
- server_sec = OMQ::Z85.decode(ENV["OMQ_SERVER_SECRET"])
178
- else
179
- key = RbNaCl::PrivateKey.generate
180
- server_pub = key.public_key.to_s
181
- server_sec = key.to_s
182
- end
183
- @sock.mechanism = OMQ::Curve.server(server_pub, server_sec)
184
- $stderr.puts "OMQ_SERVER_KEY='#{OMQ::Z85.encode(server_pub)}'"
185
- end
186
- rescue LoadError
187
- abort "omq-curve gem required for CURVE encryption: gem install omq-curve"
188
- end
189
-
190
- # ── Shared loop bodies ──────────────────────────────────────────
191
-
192
-
193
- def run_send_logic
194
- n = config.count
195
- i = 0
196
- sleep(config.delay) if config.delay
197
- if config.interval
198
- i += send_tick
199
- unless @send_tick_eof || (n && n > 0 && i >= n)
200
- Async::Loop.quantized(interval: config.interval) do
201
- i += send_tick
202
- break if @send_tick_eof || (n && n > 0 && i >= n)
203
- end
204
- end
205
- elsif config.data || config.file
206
- parts = eval_send_expr(read_next)
207
- send_msg(parts) if parts
208
- elsif stdin_ready?
209
- loop do
210
- parts = read_next
211
- break unless parts
212
- parts = eval_send_expr(parts)
213
- send_msg(parts) if parts
214
- i += 1
215
- break if n && n > 0 && i >= n
216
- end
217
- elsif @send_eval_proc
218
- parts = eval_send_expr(nil)
219
- send_msg(parts) if parts
220
- end
221
- end
222
-
223
-
224
- def send_tick
225
- raw = read_next_or_nil
226
- if raw.nil? && !@send_eval_proc
227
- @send_tick_eof = true
228
- return 0
229
- end
230
- parts = eval_send_expr(raw)
231
- send_msg(parts) if parts
232
- 1
233
- end
234
-
235
-
236
- def run_recv_logic
237
- n = config.count
238
- i = 0
239
- loop do
240
- parts = recv_msg
241
- break if parts.nil?
242
- parts = eval_recv_expr(parts)
243
- output(parts)
244
- i += 1
245
- break if n && n > 0 && i >= n
246
- end
247
- end
248
-
249
-
250
- def wait_for_loops(receiver, sender)
251
- if config.data || config.file || config.send_expr || config.recv_expr || config.target
252
- sender.wait
253
- receiver.stop
254
- elsif config.count && config.count > 0
255
- receiver.wait
256
- sender.stop
257
- else
258
- sender.wait
259
- receiver.stop
260
- end
261
- end
262
-
263
- # ── Message I/O ─────────────────────────────────────────────────
264
-
265
-
266
- def send_msg(parts)
267
- return if parts.empty?
268
- parts = [Marshal.dump(parts)] if config.format == :marshal
269
- parts = @fmt.compress(parts)
270
- @sock.send(parts)
271
- transient_ready!
272
- end
273
-
274
-
275
- def recv_msg
276
- raw = @sock.receive
277
- return nil if raw.nil?
278
- parts = @fmt.decompress(raw)
279
- parts = Marshal.load(parts.first) if config.format == :marshal
280
- transient_ready!
281
- parts
282
- end
283
-
284
-
285
- def recv_msg_raw
286
- msg = @sock.receive
287
- msg&.dup
288
- end
289
-
290
-
291
- def read_next
292
- if config.data
293
- @fmt.decode(config.data + "\n")
294
- elsif config.file
295
- @file_data ||= (config.file == "-" ? $stdin.read : File.read(config.file)).chomp
296
- @fmt.decode(@file_data + "\n")
297
- elsif config.format == :msgpack
298
- @fmt.decode_msgpack($stdin)
299
- elsif config.format == :marshal
300
- @fmt.decode_marshal($stdin)
301
- elsif config.format == :raw
302
- data = $stdin.read
303
- return nil if data.nil? || data.empty?
304
- [data]
305
- else
306
- line = $stdin.gets
307
- return nil if line.nil?
308
- @fmt.decode(line)
309
- end
310
- end
311
-
312
-
313
- def stdin_ready?
314
- return @stdin_ready unless @stdin_ready.nil?
315
-
316
- @stdin_ready = !$stdin.closed? &&
317
- !config.stdin_is_tty &&
318
- IO.select([$stdin], nil, nil, 0.01) &&
319
- !$stdin.eof?
320
- end
321
-
322
-
323
- def read_next_or_nil
324
- if config.data || config.file
325
- read_next
326
- elsif @send_eval_proc
327
- nil
328
- else
329
- read_next
330
- end
331
- end
332
-
333
-
334
- def output(parts)
335
- return if config.quiet || parts.nil?
336
- $stdout.write(@fmt.encode(parts))
337
- $stdout.flush
338
- end
339
-
340
- # ── Routing helpers ─────────────────────────────────────────────
341
-
342
-
343
- def display_routing_id(id)
344
- if id.bytes.all? { |b| b >= 0x20 && b <= 0x7E }
345
- id
346
- else
347
- "0x#{id.unpack1("H*")}"
348
- end
349
- end
350
-
351
-
352
- def resolve_target(target)
353
- if target.start_with?("0x")
354
- [target[2..].delete(" ")].pack("H*")
355
- else
356
- target
357
- end
358
- end
359
-
360
- # ── Eval ────────────────────────────────────────────────────────
361
-
362
-
363
- def compile_expr
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
388
- end
389
-
390
-
391
- def extract_blocks(expr)
392
- begin_body = end_body = nil
393
- expr, begin_body = extract_block(expr, "BEGIN")
394
- expr, end_body = extract_block(expr, "END")
395
- [expr, begin_body, end_body]
396
- end
397
-
398
-
399
- def extract_block(expr, keyword)
400
- start = expr.index(/#{keyword}\s*\{/)
401
- return [expr, nil] unless start
402
-
403
- # Find the opening brace
404
- i = expr.index("{", start)
405
- depth = 1
406
- j = i + 1
407
- while j < expr.length && depth > 0
408
- case expr[j]
409
- when "{" then depth += 1
410
- when "}" then depth -= 1
411
- end
412
- j += 1
413
- end
414
-
415
- body = expr[(i + 1)..(j - 2)]
416
- trimmed = expr[0...start] + expr[j..]
417
- [trimmed, body]
418
- end
419
-
420
-
421
- SENT = Object.new.freeze # sentinel: eval already sent the reply
422
-
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)
436
- $F = parts
437
- result = @sock.instance_exec(parts, &eval_proc)
438
- return nil if result.nil?
439
- return SENT if result.equal?(@sock)
440
- return [result] if config.format == :marshal
441
- case result
442
- when Array then result
443
- when String then [result]
444
- else [result.to_str]
445
- end
446
- rescue => e
447
- $stderr.puts "omq: eval error: #{e.message} (#{e.class})"
448
- exit 3
449
- end
450
-
451
- # ── Logging ─────────────────────────────────────────────────────
452
-
453
-
454
- def log(msg)
455
- $stderr.puts(msg) if config.verbose
456
- end
457
- end
458
- end
459
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OMQ
4
- module CLI
5
- class ChannelRunner < PairRunner
6
- end
7
- end
8
- end