omq 0.5.0 → 0.6.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +195 -0
  3. data/README.md +21 -19
  4. data/exe/omq +6 -0
  5. data/lib/omq/cli/base_runner.rb +423 -0
  6. data/lib/omq/cli/channel.rb +8 -0
  7. data/lib/omq/cli/client_server.rb +106 -0
  8. data/lib/omq/cli/config.rb +51 -0
  9. data/lib/omq/cli/formatter.rb +75 -0
  10. data/lib/omq/cli/pair.rb +31 -0
  11. data/lib/omq/cli/peer.rb +8 -0
  12. data/lib/omq/cli/pipe.rb +249 -0
  13. data/lib/omq/cli/pub_sub.rb +14 -0
  14. data/lib/omq/cli/push_pull.rb +14 -0
  15. data/lib/omq/cli/radio_dish.rb +27 -0
  16. data/lib/omq/cli/req_rep.rb +77 -0
  17. data/lib/omq/cli/router_dealer.rb +70 -0
  18. data/lib/omq/cli/scatter_gather.rb +14 -0
  19. data/lib/omq/cli.rb +444 -0
  20. data/lib/omq/pub_sub.rb +2 -2
  21. data/lib/omq/radio_dish.rb +2 -2
  22. data/lib/omq/socket.rb +74 -27
  23. data/lib/omq/version.rb +1 -1
  24. data/lib/omq/zmtp/connection.rb +59 -12
  25. data/lib/omq/zmtp/engine.rb +179 -17
  26. data/lib/omq/zmtp/options.rb +4 -3
  27. data/lib/omq/zmtp/reactor.rb +25 -36
  28. data/lib/omq/zmtp/routing/channel.rb +14 -3
  29. data/lib/omq/zmtp/routing/fan_out.rb +52 -10
  30. data/lib/omq/zmtp/routing/pair.rb +14 -3
  31. data/lib/omq/zmtp/routing/peer.rb +28 -6
  32. data/lib/omq/zmtp/routing/push.rb +14 -7
  33. data/lib/omq/zmtp/routing/radio.rb +45 -12
  34. data/lib/omq/zmtp/routing/rep.rb +32 -13
  35. data/lib/omq/zmtp/routing/req.rb +1 -2
  36. data/lib/omq/zmtp/routing/round_robin.rb +72 -3
  37. data/lib/omq/zmtp/routing/router.rb +30 -10
  38. data/lib/omq/zmtp/routing/scatter.rb +16 -3
  39. data/lib/omq/zmtp/routing/server.rb +28 -6
  40. data/lib/omq/zmtp/routing/xsub.rb +7 -1
  41. data/lib/omq/zmtp/routing.rb +19 -0
  42. data/lib/omq/zmtp/transport/inproc.rb +48 -5
  43. data/lib/omq/zmtp/transport/ipc.rb +9 -7
  44. data/lib/omq/zmtp/transport/tcp.rb +14 -7
  45. data/lib/omq/zmtp/writable.rb +21 -4
  46. data/lib/omq.rb +7 -0
  47. metadata +18 -3
  48. data/exe/omqcat +0 -532
data/lib/omq/cli.rb ADDED
@@ -0,0 +1,444 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "cli/config"
5
+ require_relative "cli/formatter"
6
+ require_relative "cli/base_runner"
7
+ require_relative "cli/push_pull"
8
+ require_relative "cli/pub_sub"
9
+ require_relative "cli/scatter_gather"
10
+ require_relative "cli/radio_dish"
11
+ require_relative "cli/req_rep"
12
+ require_relative "cli/pair"
13
+ require_relative "cli/router_dealer"
14
+ require_relative "cli/channel"
15
+ require_relative "cli/client_server"
16
+ require_relative "cli/peer"
17
+ require_relative "cli/pipe"
18
+
19
+ module OMQ
20
+ module CLI
21
+ SOCKET_TYPE_NAMES = %w[
22
+ req rep pub sub push pull pair dealer router
23
+ client server radio dish scatter gather channel peer
24
+ pipe
25
+ ].freeze
26
+
27
+
28
+ RUNNER_MAP = {
29
+ "push" => [PushRunner, :PUSH],
30
+ "pull" => [PullRunner, :PULL],
31
+ "pub" => [PubRunner, :PUB],
32
+ "sub" => [SubRunner, :SUB],
33
+ "req" => [ReqRunner, :REQ],
34
+ "rep" => [RepRunner, :REP],
35
+ "dealer" => [DealerRunner, :DEALER],
36
+ "router" => [RouterRunner, :ROUTER],
37
+ "pair" => [PairRunner, :PAIR],
38
+ "client" => [ClientRunner, :CLIENT],
39
+ "server" => [ServerRunner, :SERVER],
40
+ "radio" => [RadioRunner, :RADIO],
41
+ "dish" => [DishRunner, :DISH],
42
+ "scatter" => [ScatterRunner, :SCATTER],
43
+ "gather" => [GatherRunner, :GATHER],
44
+ "channel" => [ChannelRunner, :CHANNEL],
45
+ "peer" => [PeerRunner, :PEER],
46
+ "pipe" => [PipeRunner, nil],
47
+ }.freeze
48
+
49
+
50
+ EXAMPLES = <<~'TEXT'
51
+ ── Request / Reply ──────────────────────────────────────────
52
+
53
+ ┌─────┐ "hello" ┌─────┐
54
+ │ REQ │────────────→│ REP │
55
+ │ │←────────────│ │
56
+ └─────┘ "HELLO" └─────┘
57
+
58
+ # terminal 1: echo server
59
+ omq rep --bind tcp://:5555 --eval '$F.map(&:upcase)'
60
+
61
+ # terminal 2: send a request
62
+ echo "hello" | omq req --connect tcp://localhost:5555
63
+
64
+ # or over IPC (unix socket, single machine)
65
+ omq rep --bind ipc:///tmp/echo.sock --echo &
66
+ echo "hello" | omq req --connect ipc:///tmp/echo.sock
67
+
68
+ ── Publish / Subscribe ──────────────────────────────────────
69
+
70
+ ┌─────┐ "weather.nyc 72F" ┌─────┐
71
+ │ PUB │────────────────────→│ SUB │ --subscribe "weather."
72
+ └─────┘ └─────┘
73
+
74
+ # terminal 1: subscriber (all topics by default)
75
+ omq sub --bind tcp://:5556
76
+
77
+ # terminal 2: publisher (needs --delay for subscription to propagate)
78
+ echo "weather.nyc 72F" | omq pub --connect tcp://localhost:5556 --delay 1
79
+
80
+ ── Periodic Publish ───────────────────────────────────────────
81
+
82
+ ┌─────┐ "tick 1" ┌─────┐
83
+ │ PUB │──(every 1s)─→│ SUB │
84
+ └─────┘ └─────┘
85
+
86
+ # terminal 1: subscriber
87
+ omq sub --bind tcp://:5556
88
+
89
+ # terminal 2: publish a tick every second (wall-clock aligned)
90
+ omq pub --connect tcp://localhost:5556 --delay 1 \
91
+ --data "tick" --interval 1
92
+
93
+ # 5 ticks, then exit
94
+ omq pub --connect tcp://localhost:5556 --delay 1 \
95
+ --data "tick" --interval 1 --count 5
96
+
97
+ ── Pipeline ─────────────────────────────────────────────────
98
+
99
+ ┌──────┐ ┌──────┐
100
+ │ PUSH │──────────→│ PULL │
101
+ └──────┘ └──────┘
102
+
103
+ # terminal 1: worker
104
+ omq pull --bind tcp://:5557
105
+
106
+ # terminal 2: send tasks
107
+ echo "task 1" | omq push --connect tcp://localhost:5557
108
+
109
+ # or over IPC (unix socket)
110
+ omq pull --bind ipc:///tmp/pipeline.sock &
111
+ echo "task 1" | omq push --connect ipc:///tmp/pipeline.sock
112
+
113
+ ── CLIENT / SERVER (draft) ──────────────────────────────────
114
+
115
+ ┌────────┐ "hello" ┌────────┐
116
+ │ CLIENT │───────────→│ SERVER │ --eval '$F.map(&:upcase)'
117
+ │ │←───────────│ │
118
+ └────────┘ "HELLO" └────────┘
119
+
120
+ # terminal 1: upcasing server
121
+ omq server --bind tcp://:5555 --eval '$F.map(&:upcase)'
122
+
123
+ # terminal 2: client
124
+ echo "hello" | omq client --connect tcp://localhost:5555
125
+
126
+ ── Formats ──────────────────────────────────────────────────
127
+
128
+ # ascii (default) — non-printable replaced with dots
129
+ omq pull --bind tcp://:5557 --ascii
130
+
131
+ # quoted — lossless, round-trippable (uses String#dump escaping)
132
+ omq pull --bind tcp://:5557 --quoted
133
+
134
+ # JSON Lines — structured, multipart as arrays
135
+ echo '["key","value"]' | omq push --connect tcp://localhost:5557 --jsonl
136
+ omq pull --bind tcp://:5557 --jsonl
137
+
138
+ # multipart via tabs
139
+ printf "routing-key\tpayload" | omq push --connect tcp://localhost:5557
140
+
141
+ ── Compression ──────────────────────────────────────────────
142
+
143
+ # both sides must use --compress
144
+ omq pull --bind tcp://:5557 --compress &
145
+ echo "compressible data" | omq push --connect tcp://localhost:5557 --compress
146
+
147
+ ── CURVE Encryption ─────────────────────────────────────────
148
+
149
+ # server (prints OMQ_SERVER_KEY=...)
150
+ omq rep --bind tcp://:5555 --echo --curve-server
151
+
152
+ # client (paste the server's key)
153
+ echo "secret" | omq req --connect tcp://localhost:5555 \
154
+ --curve-server-key '<key from server>'
155
+
156
+ ── ROUTER / DEALER ──────────────────────────────────────────
157
+
158
+ ┌────────┐ ┌────────┐
159
+ │ DEALER │─────────→│ ROUTER │
160
+ │ id=w1 │ │ │
161
+ └────────┘ └────────┘
162
+
163
+ # terminal 1: router shows identity + message
164
+ omq router --bind tcp://:5555
165
+
166
+ # terminal 2: dealer with identity
167
+ echo "hello" | omq dealer --connect tcp://localhost:5555 \
168
+ --identity worker-1
169
+
170
+ ── Ruby Eval ────────────────────────────────────────────────
171
+
172
+ # filter: only pass messages containing "error"
173
+ omq pull --bind tcp://:5557 \
174
+ --eval '$F.first.include?("error") ? $F : nil'
175
+
176
+ # transform with gems
177
+ omq sub --connect tcp://localhost:5556 --require json \
178
+ --eval 'JSON.parse($F.first)["temperature"]'
179
+
180
+ # require a local file, use its methods in --eval
181
+ omq rep --bind tcp://:5555 --require ./transform.rb \
182
+ --eval 'upcase_all($F)'
183
+
184
+ # next skips, break stops — regexps match against $_
185
+ omq pull --bind tcp://:5557 \
186
+ --eval 'next if /^#/; break if /quit/; $F'
187
+
188
+ # BEGIN/END blocks (like awk) — accumulate and summarize
189
+ omq pull --bind tcp://:5557 \
190
+ --eval 'BEGIN{ @sum = 0 } @sum += Integer($_); next END{ puts @sum }'
191
+ TEXT
192
+
193
+ module_function
194
+
195
+
196
+ # Displays text through the system pager, or prints directly
197
+ # when stdout is not a terminal.
198
+ #
199
+ def page(text)
200
+ if $stdout.tty?
201
+ if ENV["PAGER"]
202
+ pager = ENV["PAGER"]
203
+ else
204
+ ENV["LESS"] ||= "-FR"
205
+ pager = "less"
206
+ end
207
+ IO.popen(pager, "w") { |io| io.puts text }
208
+ else
209
+ puts text
210
+ end
211
+ rescue Errno::ENOENT
212
+ puts text
213
+ rescue Errno::EPIPE
214
+ # user quit pager early
215
+ end
216
+
217
+
218
+ # Parses CLI arguments, validates options, and runs the main
219
+ # event loop inside an Async reactor.
220
+ #
221
+ def run(argv = ARGV)
222
+ config = build_config(argv)
223
+
224
+ require_relative "../omq"
225
+ require "async"
226
+ require "json"
227
+ require "console"
228
+
229
+ validate_gems!(config)
230
+
231
+ trap("INT") { Process.exit!(0) }
232
+ trap("TERM") { Process.exit!(0) }
233
+
234
+ Console.logger = Console::Logger.new(Console::Output::Null.new) unless config.verbose
235
+
236
+ runner_class, socket_sym = RUNNER_MAP.fetch(config.type_name)
237
+
238
+ Async do |task|
239
+ runner = if socket_sym
240
+ runner_class.new(config, OMQ.const_get(socket_sym))
241
+ else
242
+ runner_class.new(config)
243
+ end
244
+ runner.call(task)
245
+ rescue IO::TimeoutError, Async::TimeoutError
246
+ $stderr.puts "omq: timeout" unless config.quiet
247
+ exit 2
248
+ end
249
+ end
250
+
251
+
252
+ # Builds a frozen Config from command-line arguments.
253
+ #
254
+ def build_config(argv)
255
+ opts = parse_options(argv)
256
+ validate!(opts)
257
+
258
+ opts[:has_msgpack] = begin; require "msgpack"; true; rescue LoadError; false; end
259
+ opts[:has_zstd] = begin; require "zstd-ruby"; true; rescue LoadError; false; end
260
+ opts[:stdin_is_tty] = $stdin.tty?
261
+
262
+ Ractor.make_shareable(Config.new(**opts))
263
+ end
264
+
265
+
266
+ # Parses command-line arguments into a mutable options hash.
267
+ #
268
+ def parse_options(argv)
269
+ opts = {
270
+ endpoints: [],
271
+ connects: [],
272
+ binds: [],
273
+ data: nil,
274
+ file: nil,
275
+ format: :ascii,
276
+ subscribes: [],
277
+ joins: [],
278
+ group: nil,
279
+ identity: nil,
280
+ target: nil,
281
+ interval: nil,
282
+ count: nil,
283
+ delay: nil,
284
+ timeout: nil,
285
+ linger: 5,
286
+ reconnect_ivl: nil,
287
+ heartbeat_ivl: nil,
288
+ conflate: false,
289
+ compress: false,
290
+ expr: nil,
291
+ parallel: nil,
292
+ transient: false,
293
+ verbose: false,
294
+ quiet: false,
295
+ echo: false,
296
+ curve_server: false,
297
+ curve_server_key: nil,
298
+ }
299
+
300
+ parser = OptionParser.new do |o|
301
+ o.banner = "Usage: omq TYPE [options]\n\n" \
302
+ "Types: req, rep, pub, sub, push, pull, pair, dealer, router\n" \
303
+ "Draft: client, server, radio, dish, scatter, gather, channel, peer\n\n"
304
+
305
+ o.separator "Connection:"
306
+ o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v| opts[:endpoints] << Endpoint.new(v, false); opts[:connects] << v }
307
+ o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v| opts[:endpoints] << Endpoint.new(v, true); opts[:binds] << v }
308
+
309
+ o.separator "\nData source (REP: reply source):"
310
+ o.on( "--echo", "Echo received messages back (REP)") { opts[:echo] = true }
311
+ o.on("-D", "--data DATA", "Message data (literal string)") { |v| opts[:data] = v }
312
+ o.on("-F", "--file FILE", "Read message from file (- = stdin)") { |v| opts[:file] = v }
313
+
314
+ o.separator "\nFormat (input + output):"
315
+ o.on("-A", "--ascii", "Tab-separated frames, safe ASCII (default)") { opts[:format] = :ascii }
316
+ o.on("-Q", "--quoted", "C-style quoted with escapes") { opts[:format] = :quoted }
317
+ o.on( "--raw", "Raw binary, no framing") { opts[:format] = :raw }
318
+ o.on("-J", "--jsonl", "JSON Lines (array of strings per line)") { opts[:format] = :jsonl }
319
+ o.on( "--msgpack", "MessagePack arrays (binary stream)") { opts[:format] = :msgpack }
320
+ o.on("-M", "--marshal", "Ruby Marshal stream (binary, Array<String>)") { opts[:format] = :marshal }
321
+
322
+ o.separator "\nSubscription/groups:"
323
+ o.on("-s", "--subscribe PREFIX", "Subscribe prefix (SUB, default all)") { |v| opts[:subscribes] << v }
324
+ o.on("-j", "--join GROUP", "Join group (repeatable, DISH only)") { |v| opts[:joins] << v }
325
+ o.on("-g", "--group GROUP", "Publish group (RADIO only)") { |v| opts[:group] = v }
326
+
327
+ o.separator "\nIdentity/routing:"
328
+ o.on("--identity ID", "Set socket identity (DEALER/ROUTER)") { |v| opts[:identity] = v }
329
+ o.on("--target ID", "Target peer (ROUTER/SERVER/PEER, 0x prefix for binary)") { |v| opts[:target] = v }
330
+
331
+ o.separator "\nTiming:"
332
+ o.on("-i", "--interval SECS", Float, "Repeat interval") { |v| opts[:interval] = v }
333
+ o.on("-n", "--count COUNT", Integer, "Max iterations (0=inf)") { |v| opts[:count] = v }
334
+ o.on("-d", "--delay SECS", Float, "Delay before first send") { |v| opts[:delay] = v }
335
+ o.on("-t", "--timeout SECS", Float, "Send/receive timeout") { |v| opts[:timeout] = v }
336
+ o.on("-l", "--linger SECS", Float, "Drain time on close (default 5)") { |v| opts[:linger] = v }
337
+ o.on("--reconnect-ivl IVL", "Reconnect interval: SECS or MIN..MAX (default 0.1)") { |v|
338
+ opts[:reconnect_ivl] = if v.include?("..")
339
+ lo, hi = v.split("..", 2)
340
+ Float(lo)..Float(hi)
341
+ else
342
+ Float(v)
343
+ end
344
+ }
345
+ o.on("--heartbeat-ivl SECS", Float, "ZMTP heartbeat interval (detects dead peers)") { |v| opts[:heartbeat_ivl] = v }
346
+
347
+ o.separator "\nDelivery:"
348
+ o.on("--conflate", "Keep only last message per subscriber (PUB/RADIO)") { opts[:conflate] = true }
349
+
350
+ o.separator "\nCompression:"
351
+ o.on("-z", "--compress", "Zstandard compression per frame") { opts[:compress] = true }
352
+
353
+ o.separator "\nProcessing:"
354
+ o.on("-e", "--eval EXPR", "Eval Ruby for each message ($F = parts)") { |v| opts[:expr] = v }
355
+ o.on("-r", "--require LIB", "Require library or file (-r./lib.rb)") { |v|
356
+ v.start_with?("./", "../") ? require(File.expand_path(v)) : require(v)
357
+ }
358
+ o.on("-P", "--parallel [N]", Integer, "Parallel Ractor workers (pipe only, default: nproc)") { |v|
359
+ require "etc"
360
+ opts[:parallel] = v || Etc.nprocessors
361
+ }
362
+
363
+ o.separator "\nCURVE encryption (requires omq-curve gem):"
364
+ o.on("--curve-server", "Enable CURVE as server (generates keypair)") { opts[:curve_server] = true }
365
+ o.on("--curve-server-key KEY", "Enable CURVE as client (server's Z85 public key)") { |v| opts[:curve_server_key] = v }
366
+ o.separator " Env vars: OMQ_SERVER_KEY (client), OMQ_SERVER_PUBLIC + OMQ_SERVER_SECRET (server)"
367
+
368
+ o.separator "\nOther:"
369
+ o.on("-v", "--verbose", "Print connection events to stderr") { opts[:verbose] = true }
370
+ o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
371
+ o.on( "--transient", "Exit when all peers disconnect") { opts[:transient] = true }
372
+ o.on("-V", "--version") { require_relative "../omq"; puts "omq #{OMQ::VERSION}"; exit }
373
+ o.on("-h") { puts o; exit }
374
+ o.on( "--help") { page "#{o}\n#{EXAMPLES}"; exit }
375
+ o.on( "--examples") { page EXAMPLES; exit }
376
+
377
+ o.separator "\nExit codes: 0 = success, 1 = error, 2 = timeout"
378
+ end
379
+
380
+ begin
381
+ parser.parse!(argv)
382
+ rescue OptionParser::ParseError => e
383
+ abort e.message
384
+ end
385
+
386
+ type_name = argv.shift
387
+ abort parser.to_s unless type_name
388
+ unless SOCKET_TYPE_NAMES.include?(type_name.downcase)
389
+ abort "Unknown socket type: #{type_name}. Known: #{SOCKET_TYPE_NAMES.join(', ')}"
390
+ end
391
+
392
+ opts[:type_name] = type_name.downcase
393
+
394
+ normalize = ->(url) { url.sub(%r{\Atcp://:}, "tcp://*:") }
395
+ opts[:connects].map!(&normalize)
396
+ opts[:binds].map!(&normalize)
397
+ opts[:endpoints].map! { |ep| Endpoint.new(normalize.call(ep.url), ep.bind?) }
398
+
399
+ opts
400
+ end
401
+
402
+
403
+ # Validates option combinations.
404
+ #
405
+ def validate!(opts)
406
+ type_name = opts[:type_name]
407
+
408
+ if type_name == "pipe"
409
+ abort "pipe requires exactly 2 endpoints (pull-side and push-side)" if opts[:endpoints].size != 2
410
+ else
411
+ abort "At least one --connect or --bind is required" if opts[:connects].empty? && opts[:binds].empty?
412
+ end
413
+ abort "--data and --file are mutually exclusive" if opts[:data] && opts[:file]
414
+ abort "--subscribe is only valid for SUB" if !opts[:subscribes].empty? && type_name != "sub"
415
+ abort "--join is only valid for DISH" if !opts[:joins].empty? && type_name != "dish"
416
+ abort "--group is only valid for RADIO" if opts[:group] && type_name != "radio"
417
+ abort "--identity is only valid for DEALER/ROUTER" if opts[:identity] && !%w[dealer router].include?(type_name)
418
+ abort "--target is only valid for ROUTER/SERVER/PEER" if opts[:target] && !%w[router server peer].include?(type_name)
419
+ abort "--conflate is only valid for PUB/RADIO" if opts[:conflate] && !%w[pub radio].include?(type_name)
420
+
421
+ if opts[:parallel]
422
+ abort "-P/--parallel is only valid for pipe" unless type_name == "pipe"
423
+ abort "-P/--parallel must be >= 2" if opts[:parallel] < 2
424
+ abort "-P/--parallel requires both endpoints to use --connect (not --bind)" if opts[:endpoints].any?(&:bind?)
425
+ end
426
+
427
+ (opts[:connects] + opts[:binds]).each do |url|
428
+ abort "inproc not supported, use tcp:// or ipc://" if url.include?("inproc://")
429
+ end
430
+ end
431
+
432
+
433
+ # Validates that required gems are available.
434
+ #
435
+ def validate_gems!(config)
436
+ abort "--msgpack requires the msgpack gem" if config.format == :msgpack && !config.has_msgpack
437
+ abort "--compress requires the zstd-ruby gem" if config.compress && !config.has_zstd
438
+
439
+ if config.recv_only? && (config.data || config.file)
440
+ abort "--data/--file not valid for #{config.type_name} (receive-only)"
441
+ end
442
+ end
443
+ end
444
+ end
data/lib/omq/pub_sub.rb CHANGED
@@ -4,8 +4,8 @@ module OMQ
4
4
  class PUB < Socket
5
5
  include ZMTP::Writable
6
6
 
7
- def initialize(endpoints = nil, linger: 0)
8
- _init_engine(:PUB, linger: linger)
7
+ def initialize(endpoints = nil, linger: 0, conflate: false)
8
+ _init_engine(:PUB, linger: linger, conflate: conflate)
9
9
  _attach(endpoints, default: :bind)
10
10
  end
11
11
  end
@@ -4,8 +4,8 @@ module OMQ
4
4
  class RADIO < Socket
5
5
  include ZMTP::Writable
6
6
 
7
- def initialize(endpoints = nil, linger: 0)
8
- _init_engine(:RADIO, linger: linger)
7
+ def initialize(endpoints = nil, linger: 0, conflate: false)
8
+ _init_engine(:RADIO, linger: linger, conflate: conflate)
9
9
  _attach(endpoints, default: :bind)
10
10
  end
11
11
 
data/lib/omq/socket.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  module OMQ
4
6
  # Socket base class.
5
7
  #
@@ -8,32 +10,34 @@ module OMQ
8
10
  #
9
11
  attr_reader :options
10
12
 
13
+
11
14
  # @return [Integer, nil] last auto-selected TCP port
12
15
  #
13
16
  attr_reader :last_tcp_port
14
17
 
18
+
15
19
  # Delegate socket option accessors to @options.
16
20
  #
17
- %i[
18
- send_hwm send_hwm=
19
- recv_hwm recv_hwm=
20
- linger linger=
21
- identity identity=
22
- recv_timeout recv_timeout=
23
- send_timeout send_timeout=
24
- read_timeout read_timeout=
25
- write_timeout write_timeout=
26
- router_mandatory router_mandatory=
27
- router_mandatory?
28
- reconnect_interval reconnect_interval=
29
- heartbeat_interval heartbeat_interval=
30
- heartbeat_ttl heartbeat_ttl=
31
- heartbeat_timeout heartbeat_timeout=
32
- max_message_size max_message_size=
33
- mechanism mechanism=
34
- ].each do |method|
35
- define_method(method) { |*args| @options.public_send(method, *args) }
36
- end
21
+ extend Forwardable
22
+
23
+ def_delegators :@options,
24
+ :send_hwm, :send_hwm=,
25
+ :recv_hwm, :recv_hwm=,
26
+ :linger, :linger=,
27
+ :identity, :identity=,
28
+ :recv_timeout, :recv_timeout=,
29
+ :send_timeout, :send_timeout=,
30
+ :read_timeout, :read_timeout=,
31
+ :write_timeout, :write_timeout=,
32
+ :router_mandatory, :router_mandatory=,
33
+ :router_mandatory?,
34
+ :reconnect_interval, :reconnect_interval=,
35
+ :heartbeat_interval, :heartbeat_interval=,
36
+ :heartbeat_ttl, :heartbeat_ttl=,
37
+ :heartbeat_timeout, :heartbeat_timeout=,
38
+ :max_message_size, :max_message_size=,
39
+ :mechanism, :mechanism=
40
+
37
41
 
38
42
  # Creates a new socket and binds it to the given endpoint.
39
43
  #
@@ -45,6 +49,7 @@ module OMQ
45
49
  new(nil, **opts).tap { |s| s.bind(endpoint) }
46
50
  end
47
51
 
52
+
48
53
  # Creates a new socket and connects it to the given endpoint.
49
54
  #
50
55
  # @param endpoint [String]
@@ -55,8 +60,10 @@ module OMQ
55
60
  new(nil, **opts).tap { |s| s.connect(endpoint) }
56
61
  end
57
62
 
63
+
58
64
  def initialize(endpoints = nil, linger: 0); end
59
65
 
66
+
60
67
  # Binds to an endpoint.
61
68
  #
62
69
  # @param endpoint [String]
@@ -67,6 +74,7 @@ module OMQ
67
74
  @last_tcp_port = @engine.last_tcp_port
68
75
  end
69
76
 
77
+
70
78
  # Connects to an endpoint.
71
79
  #
72
80
  # @param endpoint [String]
@@ -76,6 +84,7 @@ module OMQ
76
84
  @engine.connect(endpoint)
77
85
  end
78
86
 
87
+
79
88
  # Disconnects from an endpoint.
80
89
  #
81
90
  # @param endpoint [String]
@@ -85,6 +94,7 @@ module OMQ
85
94
  @engine.disconnect(endpoint)
86
95
  end
87
96
 
97
+
88
98
  # Unbinds from an endpoint.
89
99
  #
90
100
  # @param endpoint [String]
@@ -94,21 +104,52 @@ module OMQ
94
104
  @engine.unbind(endpoint)
95
105
  end
96
106
 
107
+
97
108
  # @return [String, nil] last bound endpoint
98
109
  #
99
110
  def last_endpoint
100
111
  @engine.last_endpoint
101
112
  end
102
113
 
103
- # Closes the socket.
114
+
115
+ # @return [Async::Promise] resolves when first peer completes handshake
116
+ def peer_connected = @engine.peer_connected
117
+
118
+
119
+ # @return [Async::Promise] resolves when first subscriber joins (PUB/XPUB only)
120
+ def subscriber_joined = @engine.routing.subscriber_joined
121
+
122
+
123
+ # @return [Async::Promise] resolves when all peers disconnect (after having had peers)
124
+ def all_peers_gone = @engine.all_peers_gone
125
+
126
+
127
+ # @return [Integer] current number of peer connections
128
+ def connection_count = @engine.connections.size
129
+
130
+
131
+ # Signals end-of-stream on the receive side. A subsequent
132
+ # +#receive+ call that would otherwise block returns +nil+.
104
133
  #
105
- # @return [void]
134
+ def close_read
135
+ @engine.dequeue_recv_sentinel
136
+ end
137
+
138
+
139
+ # Disable auto-reconnect for connected endpoints.
140
+ def reconnect_enabled=(val)
141
+ @engine.reconnect_enabled = val
142
+ end
143
+
144
+
145
+ # Closes the socket.
106
146
  #
107
147
  def close
108
148
  @engine.close
109
149
  nil
110
150
  end
111
151
 
152
+
112
153
  # Set socket to use unbounded pipes (HWM=0).
113
154
  #
114
155
  def set_unbounded
@@ -117,14 +158,17 @@ module OMQ
117
158
  nil
118
159
  end
119
160
 
161
+
120
162
  # @return [String]
121
163
  #
122
164
  def inspect
123
165
  format("#<%s last_endpoint=%p>", self.class, last_endpoint)
124
166
  end
125
167
 
168
+
126
169
  private
127
170
 
171
+
128
172
  # Runs a block with a timeout. Uses Async's with_timeout if inside
129
173
  # a reactor, otherwise falls back to Timeout.timeout.
130
174
  #
@@ -142,6 +186,7 @@ module OMQ
142
186
  raise IO::TimeoutError, "timed out"
143
187
  end
144
188
 
189
+
145
190
  # Connects or binds based on endpoint prefix convention.
146
191
  #
147
192
  # @param endpoints [String, nil]
@@ -159,18 +204,20 @@ module OMQ
159
204
  end
160
205
  end
161
206
 
207
+
162
208
  # Initializes engine and options for a socket type.
163
209
  #
164
210
  # @param socket_type [Symbol]
165
211
  # @param linger [Integer]
166
212
  #
167
213
  def _init_engine(socket_type, linger:, send_hwm: nil, recv_hwm: nil,
168
- send_timeout: nil, recv_timeout: nil)
214
+ send_timeout: nil, recv_timeout: nil, conflate: false)
169
215
  @options = ZMTP::Options.new(linger: linger)
170
- @options.send_hwm = send_hwm if send_hwm
171
- @options.recv_hwm = recv_hwm if recv_hwm
172
- @options.send_timeout = send_timeout if send_timeout
173
- @options.recv_timeout = recv_timeout if recv_timeout
216
+ @options.send_hwm = send_hwm if send_hwm
217
+ @options.recv_hwm = recv_hwm if recv_hwm
218
+ @options.send_timeout = send_timeout if send_timeout
219
+ @options.recv_timeout = recv_timeout if recv_timeout
220
+ @options.conflate = conflate
174
221
  @engine = ZMTP::Engine.new(socket_type, @options)
175
222
  end
176
223
  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.5.0"
4
+ VERSION = "0.6.0"
5
5
  end