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