omq 0.2.2 → 0.3.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.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README.md +2 -2
  4. data/exe/omqcat +505 -0
  5. data/lib/omq/version.rb +1 -1
  6. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02d85dfeec36f9d537cdfac7512a4a5edbfbe95145ae9583e8f5428a43a2548d
4
- data.tar.gz: 1598f1cdecdc0523cf4263a72318f0500b1bd9dccb46da703817fd0b30c74315
3
+ metadata.gz: 3d77750001a2c45808301e5a005f83c8a6f4df44ffa033aa121cc9229b597347
4
+ data.tar.gz: b009c94d5ea30d5e0983831fb443e1ceb31e8980725b1b9c75b9403814a7a469
5
5
  SHA512:
6
- metadata.gz: ee523b073066febf851dc2427b8ddd86c62bc44580a3a9c4283a4d72345ee52048e07a396da1db0de7265f7a641ac6aafe02827542a2786833c8e78b0a25bebf
7
- data.tar.gz: 88933632f2e57c092090af0a322772db4431af55f85f4e9f41c21df2aa88d68bc9641ef3691a6826335b89b8b030275576b978ebf9f558a22ab4a329da5c3bbe
6
+ metadata.gz: e6612b16ae23874f1539154f108359190ac70eeaf9691c326deb4b2545151590e4458071912a987258b9d59cf80c5015024fbfa546d1f5bbcd979564be5cdffe
7
+ data.tar.gz: 7706e73997b1a96446e6c5e6053756e57e4b33d6b084a82c49f78e40b9454aa26aed1d5e3ca14207c450be75864707618c3a87cf27c750020064393dbe85c71f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1 — 2026-03-26
4
+
5
+ ### Improved
6
+
7
+ - `omqcat --help` responds in ~90ms (was ~470ms) — defer heavy gem loading
8
+ until after option parsing
9
+
10
+ ## 0.3.0 — 2026-03-26
11
+
12
+ ### Added
13
+
14
+ - `omqcat` CLI tool — nngcat-like Swiss army knife for OMQ sockets
15
+ - Socket types: req, rep, pub, sub, push, pull, pair, dealer, router
16
+ - Formats: ascii (default, tab-separated), quoted, raw, jsonl, msgpack
17
+ - `-e` / `--eval` — Ruby code runs inside the socket instance
18
+ (`$F` = message parts, full socket API available: `self <<`, `send`,
19
+ `subscribe`, etc.). REP auto-replies with the return value;
20
+ PAIR/DEALER use `self <<` explicitly
21
+ - `-r` / `--require` to load gems for use in `-e`
22
+ - `-z` / `--compress` Zstandard compression per frame (requires `zstd-ruby`)
23
+ - `-D` / `-F` data sources, `-i` interval, `-n` count, `-d` delay
24
+ - CURVE encryption via `SERVER_KEY` / `SERVER_PUBLIC` + `SERVER_SECRET`
25
+ env vars (requires `omq-curve`)
26
+ - `--identity` / `--target` for DEALER/ROUTER patterns
27
+ - `tcp://:PORT` shorthand for `tcp://*:PORT` (no shell glob issues)
28
+ - 22 system tests via `rake test:cli`
29
+
3
30
  ## 0.2.2 — 2026-03-26
4
31
 
5
32
  ### Added
data/README.md CHANGED
@@ -7,11 +7,11 @@
7
7
 
8
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.
9
9
 
10
- > **167k msg/s** inproc | **42k msg/s** ipc | **32k msg/s** tcp
10
+ > **145k msg/s** inproc | **40k msg/s** ipc | **32k msg/s** tcp
11
11
  >
12
12
  > **15 µs** inproc latency | **62 µs** ipc | **88 µs** tcp
13
13
  >
14
- > Ruby 4.0 + YJIT on a Linux VM on a 2019 MacBook Pro (Intel)
14
+ > Ruby 4.0 + YJIT on a Linux VM on a 2019 MacBook Pro (Intel) — [223k msg/s with io_uring](bench/README.md#io_uring)
15
15
 
16
16
  ---
17
17
 
data/exe/omqcat ADDED
@@ -0,0 +1,505 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # omqcat — command-line access to OMQ (ZeroMQ) sockets.
6
+ #
7
+ # Usage: omqcat TYPE [options]
8
+ #
9
+ # Examples:
10
+ # omqcat rep -b tcp://:5555 -D "pong"
11
+ # echo "ping" | omqcat req -c tcp://localhost:5555
12
+ # omqcat sub -c tcp://localhost:5556 -s "weather."
13
+ #
14
+
15
+ require "optparse"
16
+
17
+ SOCKET_TYPE_NAMES = %w[req rep pub sub push pull pair dealer router].freeze
18
+
19
+
20
+ # ── Option parsing ──────────────────────────────────────────────────
21
+
22
+ opts = {
23
+ connects: [],
24
+ binds: [],
25
+ data: nil,
26
+ file: nil,
27
+ format: :ascii,
28
+ subscribes: [],
29
+ identity: nil,
30
+ target: nil,
31
+ interval: nil,
32
+ count: 0,
33
+ delay: nil,
34
+ recv_timeout: nil,
35
+ send_timeout: nil,
36
+ compress: false,
37
+ expr: nil,
38
+ verbose: false,
39
+ quiet: false,
40
+ }
41
+
42
+ parser = OptionParser.new do |o|
43
+ o.banner = "Usage: omqcat TYPE [options]\n\n" \
44
+ "Types: #{SOCKET_TYPE_NAMES.join(', ')}\n\n"
45
+
46
+ o.separator "Connection:"
47
+ o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v| opts[:connects] << v }
48
+ o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v| opts[:binds] << v }
49
+
50
+ o.separator "\nData source:"
51
+ o.on("-D", "--data DATA", "Message data (literal string)") { |v| opts[:data] = v }
52
+ o.on("-F", "--file FILE", "Read message from file (- = stdin)") { |v| opts[:file] = v }
53
+
54
+ o.separator "\nFormat (input + output):"
55
+ o.on("-A", "--ascii", "Tab-separated frames, safe ASCII (default)") { opts[:format] = :ascii }
56
+ o.on("-Q", "--quoted", "C-style quoted with escapes") { opts[:format] = :quoted }
57
+ o.on( "--raw", "Raw binary, no framing") { opts[:format] = :raw }
58
+ o.on("-J", "--jsonl", "JSON Lines (array of strings per line)") { opts[:format] = :jsonl }
59
+ o.on( "--msgpack", "MessagePack arrays (binary stream)") { opts[:format] = :msgpack }
60
+
61
+ o.separator "\nSubscription:"
62
+ o.on("-s", "--subscribe PREFIX", "Subscribe prefix (repeatable, SUB only)") { |v| opts[:subscribes] << v }
63
+
64
+ o.separator "\nIdentity/routing:"
65
+ o.on("--identity ID", "Set socket identity (DEALER/ROUTER)") { |v| opts[:identity] = v }
66
+ o.on("--target ID", "Target peer identity (ROUTER sending)") { |v| opts[:target] = v }
67
+
68
+ o.separator "\nTiming:"
69
+ o.on("-i", "--interval SECS", Float, "Repeat interval") { |v| opts[:interval] = v }
70
+ o.on("-n", "--count COUNT", Integer, "Max iterations (0=inf)") { |v| opts[:count] = v }
71
+ o.on("-d", "--delay SECS", Float, "Delay before first send") { |v| opts[:delay] = v }
72
+ o.on("-t", "--recv-timeout SECS", Float, "Receive timeout") { |v| opts[:recv_timeout] = v }
73
+ o.on( "--send-timeout SECS", Float, "Send timeout") { |v| opts[:send_timeout] = v }
74
+
75
+ o.separator "\nCompression:"
76
+ o.on("-z", "--compress", "Zstandard compression per frame") { opts[:compress] = true }
77
+
78
+ o.separator "\nProcessing:"
79
+ o.on("-e", "--eval EXPR", "Eval Ruby for each message ($F = parts)") { |v| opts[:expr] = v }
80
+ o.on("-r", "--require LIB", "Require a Ruby library (repeatable)") { |v| require v }
81
+
82
+ o.separator "\nOther:"
83
+ o.on("-v", "--verbose", "Print connection events to stderr") { opts[:verbose] = true }
84
+ o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
85
+ o.on("-V", "--version") { require "omq"; puts "omqcat #{OMQ::VERSION}"; exit }
86
+ o.on("-h", "--help") { puts o; exit }
87
+ end
88
+
89
+ begin
90
+ parser.parse!
91
+ rescue OptionParser::ParseError => e
92
+ abort e.message
93
+ end
94
+
95
+ type_name = ARGV.shift
96
+ abort parser.to_s unless type_name
97
+ abort "Unknown socket type: #{type_name}. Known: #{SOCKET_TYPE_NAMES.join(', ')}" unless SOCKET_TYPE_NAMES.include?(type_name.downcase)
98
+
99
+ # ── Validation (fast, before loading gems) ──────────────────────────
100
+
101
+ abort "At least one --connect or --bind is required" if opts[:connects].empty? && opts[:binds].empty?
102
+ abort "--data and --file are mutually exclusive" if opts[:data] && opts[:file]
103
+ abort "--subscribe is only valid for SUB" if !opts[:subscribes].empty? && type_name.downcase != "sub"
104
+ abort "--identity is only valid for DEALER/ROUTER" if opts[:identity] && !%w[dealer router].include?(type_name.downcase)
105
+ abort "--target is only valid for ROUTER" if opts[:target] && type_name.downcase != "router"
106
+
107
+ # ── Load gems ───────────────────────────────────────────────────────
108
+
109
+ require "omq"
110
+ require "async"
111
+ require "json"
112
+ require "console"
113
+
114
+ HAS_MSGPACK = begin; require "msgpack"; true; rescue LoadError; false; end
115
+ HAS_ZSTD = begin; require "zstd-ruby"; true; rescue LoadError; false; end
116
+
117
+ SOCKET_TYPES = {
118
+ "req" => OMQ::REQ, "rep" => OMQ::REP,
119
+ "pub" => OMQ::PUB, "sub" => OMQ::SUB,
120
+ "push" => OMQ::PUSH, "pull" => OMQ::PULL,
121
+ "pair" => OMQ::PAIR,
122
+ "dealer" => OMQ::DEALER, "router" => OMQ::ROUTER,
123
+ }.freeze
124
+
125
+ SEND_ONLY = [OMQ::PUB, OMQ::PUSH].freeze
126
+ RECV_ONLY = [OMQ::SUB, OMQ::PULL].freeze
127
+
128
+ klass = SOCKET_TYPES[type_name.downcase]
129
+
130
+ abort "--msgpack requires the msgpack gem" if opts[:format] == :msgpack && !HAS_MSGPACK
131
+ abort "--compress requires the zstd-ruby gem" if opts[:compress] && !HAS_ZSTD
132
+
133
+ (opts[:connects] + opts[:binds]).each do |url|
134
+ abort "inproc not supported, use tcp:// or ipc://" if url.include?("inproc://")
135
+ end
136
+
137
+ if RECV_ONLY.include?(klass) && (opts[:data] || opts[:file])
138
+ abort "--data/--file not valid for #{type_name} (receive-only)"
139
+ end
140
+
141
+ # ── URL normalization ───────────────────────────────────────────────
142
+
143
+ normalize = ->(url) { url.sub(%r{\Atcp://:}, "tcp://*:") }
144
+ opts[:connects].map!(&normalize)
145
+ opts[:binds].map!(&normalize)
146
+
147
+ # ── Format helpers ──────────────────────────────────────────────────
148
+
149
+ def format_output(parts, fmt)
150
+ case fmt
151
+ when :ascii
152
+ parts.map { |p| p.b.gsub(/[^[:print:]\t]/, ".") }.join("\t") + "\n"
153
+ when :quoted
154
+ parts.map { |p|
155
+ p.b.gsub(/[^[:print:]]/) { |c|
156
+ case c
157
+ when "\n" then "\\n"
158
+ when "\r" then "\\r"
159
+ when "\t" then "\\t"
160
+ when "\\" then "\\\\"
161
+ else format("\\x%02x", c.ord)
162
+ end
163
+ }
164
+ }.join("\t") + "\n"
165
+ when :raw
166
+ parts.join
167
+ when :jsonl
168
+ JSON.generate(parts) + "\n"
169
+ when :msgpack
170
+ MessagePack.pack(parts)
171
+ end
172
+ end
173
+
174
+ def parse_input(line, fmt)
175
+ case fmt
176
+ when :ascii
177
+ line.chomp.split("\t")
178
+ when :quoted
179
+ line.chomp.split("\t").map { |p|
180
+ p.gsub(/\\(?:n|r|t|\\|x[0-9a-fA-F]{2})/) { |m|
181
+ case m
182
+ when "\\n" then "\n"
183
+ when "\\r" then "\r"
184
+ when "\\t" then "\t"
185
+ when "\\\\" then "\\"
186
+ else [m[2..].to_i(16)].pack("C")
187
+ end
188
+ }
189
+ }
190
+ when :raw
191
+ [line]
192
+ when :jsonl
193
+ arr = JSON.parse(line.chomp)
194
+ abort "JSON Lines input must be an array of strings" unless arr.is_a?(Array) && arr.all? { |e| e.is_a?(String) }
195
+ arr
196
+ end
197
+ end
198
+
199
+ def read_message_msgpack(io)
200
+ @msgpack_unpacker ||= MessagePack::Unpacker.new(io)
201
+ @msgpack_unpacker.read
202
+ rescue EOFError
203
+ nil
204
+ end
205
+
206
+ # ── Compression helpers ─────────────────────────────────────────────
207
+
208
+ def compress_frames(parts)
209
+ parts.map { |p| Zstd.compress(p) }
210
+ end
211
+
212
+ def decompress_frames(parts)
213
+ parts.map { |p| Zstd.decompress(p) }
214
+ end
215
+
216
+ # ── CURVE setup ─────────────────────────────────────────────────────
217
+
218
+ def setup_curve(sock)
219
+ if ENV["SERVER_KEY"]
220
+ require "omq/curve"
221
+ server_key = OMQ::Z85.decode(ENV["SERVER_KEY"])
222
+ client_key = RbNaCl::PrivateKey.generate
223
+ sock.mechanism = OMQ::Curve.client(
224
+ client_key.public_key.to_s, client_key.to_s, server_key: server_key
225
+ )
226
+ $stderr.puts "CURVE client mode" if $VERBOSE
227
+ elsif ENV["SERVER_PUBLIC"] && ENV["SERVER_SECRET"]
228
+ require "omq/curve"
229
+ server_pub = OMQ::Z85.decode(ENV["SERVER_PUBLIC"])
230
+ server_sec = OMQ::Z85.decode(ENV["SERVER_SECRET"])
231
+ sock.mechanism = OMQ::Curve.server(server_pub, server_sec)
232
+ $stderr.puts "SERVER_KEY=#{ENV["SERVER_PUBLIC"]}"
233
+ end
234
+ rescue LoadError
235
+ abort "omq-curve gem required for CURVE encryption: gem install omq-curve"
236
+ end
237
+
238
+ # ── I/O helpers ─────────────────────────────────────────────────────
239
+
240
+ def read_next(opts)
241
+ if opts[:data]
242
+ parse_input(opts[:data] + "\n", opts[:format])
243
+ elsif opts[:file]
244
+ @file_data ||= (opts[:file] == "-" ? $stdin.read : File.read(opts[:file])).chomp
245
+ parse_input(@file_data + "\n", opts[:format])
246
+ elsif opts[:format] == :msgpack
247
+ read_message_msgpack($stdin)
248
+ elsif opts[:format] == :raw
249
+ data = $stdin.read
250
+ return nil if data.nil? || data.empty?
251
+ [data]
252
+ else
253
+ line = $stdin.gets
254
+ return nil if line.nil?
255
+ parse_input(line, opts[:format])
256
+ end
257
+ end
258
+
259
+ def send_msg(sock, parts, opts)
260
+ return if parts.empty?
261
+ parts = compress_frames(parts) if opts[:compress]
262
+ sock.send(parts)
263
+ end
264
+
265
+ def recv_msg(sock, opts)
266
+ parts = sock.receive
267
+ parts = decompress_frames(parts) if opts[:compress]
268
+ parts
269
+ end
270
+
271
+ def eval_expr(parts, sock, opts)
272
+ return parts unless opts[:expr]
273
+ $F = parts
274
+ result = sock.instance_eval(opts[:expr], "-e") # rubocop:disable Security/Eval
275
+ case result
276
+ when nil then nil
277
+ when Array then result
278
+ when String then [result]
279
+ else [result.to_s]
280
+ end
281
+ end
282
+
283
+ def output(parts, opts)
284
+ return if opts[:quiet]
285
+ parts = [""] if parts.nil?
286
+ $stdout.write(format_output(parts, opts[:format]))
287
+ $stdout.flush
288
+ end
289
+
290
+ def log(msg)
291
+ $stderr.puts(msg)
292
+ end
293
+
294
+ # ── Loop methods ────────────────────────────────────────────────────
295
+
296
+ def send_loop(sock, opts)
297
+ i = 0
298
+ if opts[:data] || opts[:file]
299
+ loop do
300
+ parts = read_next(opts)
301
+ break unless parts
302
+ parts = eval_expr(parts, sock, opts)
303
+ sleep(opts[:delay]) if opts[:delay] && i == 0
304
+ send_msg(sock, parts, opts) if parts
305
+ i += 1
306
+ break if opts[:count] > 0 && i >= opts[:count]
307
+ if opts[:interval]
308
+ sleep(opts[:interval])
309
+ else
310
+ break # single send without -i
311
+ end
312
+ end
313
+ else
314
+ loop do
315
+ parts = read_next(opts)
316
+ break unless parts
317
+ parts = eval_expr(parts, sock, opts)
318
+ sleep(opts[:delay]) if opts[:delay] && i == 0
319
+ send_msg(sock, parts, opts) if parts
320
+ i += 1
321
+ break if opts[:count] > 0 && i >= opts[:count]
322
+ sleep(opts[:interval]) if opts[:interval]
323
+ end
324
+ end
325
+ end
326
+
327
+ def recv_loop(sock, opts)
328
+ i = 0
329
+ loop do
330
+ parts = recv_msg(sock, opts)
331
+ parts = eval_expr(parts, sock, opts)
332
+ output(parts, opts)
333
+ i += 1
334
+ break if opts[:count] > 0 && i >= opts[:count]
335
+ end
336
+ end
337
+
338
+ def req_loop(sock, opts)
339
+ i = 0
340
+ loop do
341
+ parts = read_next(opts)
342
+ break unless parts
343
+ sleep(opts[:delay]) if opts[:delay] && i == 0
344
+ send_msg(sock, parts, opts)
345
+ reply = recv_msg(sock, opts)
346
+ reply = eval_expr(reply, sock, opts)
347
+ output(reply, opts)
348
+ i += 1
349
+ break if opts[:count] > 0 && i >= opts[:count]
350
+ if opts[:interval]
351
+ sleep(opts[:interval])
352
+ elsif !opts[:data] && !opts[:file]
353
+ next # stdin mode: keep reading lines
354
+ else
355
+ break # single exchange with -D/-F and no -i
356
+ end
357
+ end
358
+ end
359
+
360
+ def rep_loop(sock, opts)
361
+ i = 0
362
+ loop do
363
+ msg = recv_msg(sock, opts)
364
+ if opts[:expr]
365
+ reply = eval_expr(msg, sock, opts)
366
+ output(reply, opts)
367
+ send_msg(sock, reply || [""], opts)
368
+ else
369
+ output(msg, opts)
370
+ reply = opts[:data] ? parse_input(opts[:data] + "\n", opts[:format]) : msg
371
+ send_msg(sock, reply, opts)
372
+ end
373
+ i += 1
374
+ break if opts[:count] > 0 && i >= opts[:count]
375
+ end
376
+ end
377
+
378
+ def pair_loop(sock, opts, task)
379
+ receiver = task.async do
380
+ i = 0
381
+ loop do
382
+ parts = recv_msg(sock, opts)
383
+ parts = eval_expr(parts, sock, opts)
384
+ output(parts, opts)
385
+ i += 1
386
+ break if opts[:count] > 0 && i >= opts[:count]
387
+ end
388
+ end
389
+
390
+ sender = task.async do
391
+ i = 0
392
+ loop do
393
+ parts = read_next(opts)
394
+ break unless parts
395
+ parts = eval_expr(parts, sock, opts)
396
+ sleep(opts[:delay]) if opts[:delay] && i == 0
397
+ send_msg(sock, parts, opts) if parts
398
+ i += 1
399
+ break if opts[:count] > 0 && i >= opts[:count]
400
+ break if (opts[:data] || opts[:file]) && !opts[:interval]
401
+ sleep(opts[:interval]) if opts[:interval]
402
+ end
403
+ end
404
+
405
+ if opts[:count] > 0
406
+ receiver.wait
407
+ sender.stop
408
+ else
409
+ sender.wait
410
+ receiver.stop
411
+ end
412
+ end
413
+
414
+ def router_loop(sock, opts, task)
415
+ receiver = task.async do
416
+ i = 0
417
+ loop do
418
+ parts = recv_msg(sock, opts)
419
+ # parts = [identity, "", ...payload]
420
+ identity = parts.shift
421
+ parts.shift if parts.first == "" # remove empty delimiter
422
+ # Z85-encode binary identities for display
423
+ id_display = identity.bytes.all? { |b| b >= 0x20 && b <= 0x7E } ? identity : OMQ::Z85.encode(identity.ljust(((identity.bytesize + 3) / 4) * 4, "\x00"))
424
+ result = eval_expr([id_display, *parts], sock, opts)
425
+ output(result, opts)
426
+ i += 1
427
+ break if opts[:count] > 0 && i >= opts[:count]
428
+ end
429
+ end
430
+
431
+ sender = task.async do
432
+ i = 0
433
+ loop do
434
+ parts = read_next(opts)
435
+ break unless parts
436
+ sleep(opts[:delay]) if opts[:delay] && i == 0
437
+ if opts[:target]
438
+ parts = [opts[:target], "", *parts]
439
+ end
440
+ send_msg(sock, parts, opts)
441
+ i += 1
442
+ break if opts[:count] > 0 && i >= opts[:count]
443
+ break if (opts[:data] || opts[:file]) && !opts[:interval]
444
+ sleep(opts[:interval]) if opts[:interval]
445
+ end
446
+ end
447
+
448
+ # If count is set, the receiver will exit when count is reached.
449
+ # Otherwise, wait for Ctrl-C.
450
+ if opts[:count] > 0
451
+ receiver.wait
452
+ sender.stop
453
+ else
454
+ sender.wait
455
+ receiver.stop
456
+ end
457
+ end
458
+
459
+ # ── Signal handling ─────────────────────────────────────────────────
460
+
461
+ trap("INT") { Process.exit!(0) }
462
+ trap("TERM") { Process.exit!(0) }
463
+
464
+ # Silence Async's noisy task warnings unless verbose.
465
+ Console.logger = Console::Logger.new(Console::Output::Null.new) unless opts[:verbose]
466
+
467
+ # ── Main ────────────────────────────────────────────────────────────
468
+
469
+ Async do |task|
470
+ sock = klass.new(nil, linger: 1)
471
+ sock.recv_timeout = opts[:recv_timeout] if opts[:recv_timeout]
472
+ sock.send_timeout = opts[:send_timeout] if opts[:send_timeout]
473
+ sock.identity = opts[:identity] if opts[:identity]
474
+
475
+ setup_curve(sock)
476
+
477
+ opts[:binds].each do |url|
478
+ sock.bind(url)
479
+ log "Bound to #{sock.last_endpoint}" if opts[:verbose]
480
+ end
481
+
482
+ opts[:connects].each do |url|
483
+ sock.connect(url)
484
+ log "Connecting to #{url}" if opts[:verbose]
485
+ end
486
+
487
+ if klass == OMQ::SUB
488
+ prefixes = opts[:subscribes].empty? ? [""] : opts[:subscribes]
489
+ prefixes.each { |p| sock.subscribe(p) }
490
+ end
491
+
492
+ sleep(opts[:delay]) if opts[:delay] && RECV_ONLY.include?(klass)
493
+
494
+ if SEND_ONLY.include?(klass) then send_loop(sock, opts)
495
+ elsif RECV_ONLY.include?(klass) then recv_loop(sock, opts)
496
+ elsif klass == OMQ::REQ then req_loop(sock, opts)
497
+ elsif klass == OMQ::REP then rep_loop(sock, opts)
498
+ elsif klass == OMQ::ROUTER then router_loop(sock, opts, task)
499
+ elsif opts[:data] || opts[:file] then send_loop(sock, opts)
500
+ else pair_loop(sock, opts, task)
501
+ end
502
+
503
+ ensure
504
+ sock&.close
505
+ end
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.2.2"
4
+ VERSION = "0.3.1"
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.2.2
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -41,13 +41,15 @@ description: Pure Ruby implementation of the ZMTP 3.1 wire protocol (ZeroMQ) usi
41
41
  the Async gem. No native libraries required.
42
42
  email:
43
43
  - paddor@gmail.com
44
- executables: []
44
+ executables:
45
+ - omqcat
45
46
  extensions: []
46
47
  extra_rdoc_files: []
47
48
  files:
48
49
  - CHANGELOG.md
49
50
  - LICENSE
50
51
  - README.md
52
+ - exe/omqcat
51
53
  - lib/omq.rb
52
54
  - lib/omq/pair.rb
53
55
  - lib/omq/pub_sub.rb