omq 0.5.1 → 0.6.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +184 -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 +468 -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 +24 -3
  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 +10 -5
  28. data/lib/omq/zmtp/routing/channel.rb +8 -2
  29. data/lib/omq/zmtp/routing/fan_out.rb +38 -8
  30. data/lib/omq/zmtp/routing/pair.rb +8 -2
  31. data/lib/omq/zmtp/routing/peer.rb +7 -1
  32. data/lib/omq/zmtp/routing/push.rb +14 -7
  33. data/lib/omq/zmtp/routing/radio.rb +32 -11
  34. data/lib/omq/zmtp/routing/rep.rb +11 -7
  35. data/lib/omq/zmtp/routing/req.rb +1 -2
  36. data/lib/omq/zmtp/routing/round_robin.rb +35 -1
  37. data/lib/omq/zmtp/routing/router.rb +7 -1
  38. data/lib/omq/zmtp/routing/scatter.rb +16 -3
  39. data/lib/omq/zmtp/routing/server.rb +7 -1
  40. data/lib/omq/zmtp/routing/xsub.rb +7 -1
  41. data/lib/omq/zmtp/transport/inproc.rb +40 -5
  42. data/lib/omq/zmtp/transport/ipc.rb +9 -7
  43. data/lib/omq/zmtp/transport/tcp.rb +14 -7
  44. data/lib/omq/zmtp/writable.rb +21 -4
  45. data/lib/omq.rb +7 -0
  46. metadata +18 -3
  47. data/exe/omqcat +0 -532
data/lib/omq/cli.rb ADDED
@@ -0,0 +1,468 @@
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
+ ── Pipe (PULL → eval → PUSH) ────────────────────────────────
114
+
115
+ ┌──────┐ ┌──────┐ ┌──────┐
116
+ │ PUSH │────────→│ pipe │────────→│ PULL │
117
+ └──────┘ └──────┘ └──────┘
118
+
119
+ # terminal 1: producer
120
+ echo -e "hello\nworld" | omq push --bind ipc://@work
121
+
122
+ # terminal 2: worker — uppercase each message
123
+ omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
124
+
125
+ # terminal 3: collector
126
+ omq pull --bind ipc://@sink
127
+
128
+ # 4 Ractor workers in a single process (-P)
129
+ omq pipe -c ipc://@work -c ipc://@sink -P 4 \
130
+ -r ./fib.rb -e 'fib(Integer($_)).to_s'
131
+
132
+ # exit when producer disconnects (--transient)
133
+ omq pipe -c ipc://@work -c ipc://@sink --transient \
134
+ -e '$F.map(&:upcase)'
135
+
136
+ ── CLIENT / SERVER (draft) ──────────────────────────────────
137
+
138
+ ┌────────┐ "hello" ┌────────┐
139
+ │ CLIENT │───────────→│ SERVER │ --eval '$F.map(&:upcase)'
140
+ │ │←───────────│ │
141
+ └────────┘ "HELLO" └────────┘
142
+
143
+ # terminal 1: upcasing server
144
+ omq server --bind tcp://:5555 --eval '$F.map(&:upcase)'
145
+
146
+ # terminal 2: client
147
+ echo "hello" | omq client --connect tcp://localhost:5555
148
+
149
+ ── Formats ──────────────────────────────────────────────────
150
+
151
+ # ascii (default) — non-printable replaced with dots
152
+ omq pull --bind tcp://:5557 --ascii
153
+
154
+ # quoted — lossless, round-trippable (uses String#dump escaping)
155
+ omq pull --bind tcp://:5557 --quoted
156
+
157
+ # JSON Lines — structured, multipart as arrays
158
+ echo '["key","value"]' | omq push --connect tcp://localhost:5557 --jsonl
159
+ omq pull --bind tcp://:5557 --jsonl
160
+
161
+ # multipart via tabs
162
+ printf "routing-key\tpayload" | omq push --connect tcp://localhost:5557
163
+
164
+ ── Compression ──────────────────────────────────────────────
165
+
166
+ # both sides must use --compress
167
+ omq pull --bind tcp://:5557 --compress &
168
+ echo "compressible data" | omq push --connect tcp://localhost:5557 --compress
169
+
170
+ ── CURVE Encryption ─────────────────────────────────────────
171
+
172
+ # server (prints OMQ_SERVER_KEY=...)
173
+ omq rep --bind tcp://:5555 --echo --curve-server
174
+
175
+ # client (paste the server's key)
176
+ echo "secret" | omq req --connect tcp://localhost:5555 \
177
+ --curve-server-key '<key from server>'
178
+
179
+ ── ROUTER / DEALER ──────────────────────────────────────────
180
+
181
+ ┌────────┐ ┌────────┐
182
+ │ DEALER │─────────→│ ROUTER │
183
+ │ id=w1 │ │ │
184
+ └────────┘ └────────┘
185
+
186
+ # terminal 1: router shows identity + message
187
+ omq router --bind tcp://:5555
188
+
189
+ # terminal 2: dealer with identity
190
+ echo "hello" | omq dealer --connect tcp://localhost:5555 \
191
+ --identity worker-1
192
+
193
+ ── Ruby Eval ────────────────────────────────────────────────
194
+
195
+ # filter: only pass messages containing "error"
196
+ omq pull --bind tcp://:5557 \
197
+ --eval '$F.first.include?("error") ? $F : nil'
198
+
199
+ # transform with gems
200
+ omq sub --connect tcp://localhost:5556 --require json \
201
+ --eval 'JSON.parse($F.first)["temperature"]'
202
+
203
+ # require a local file, use its methods in --eval
204
+ omq rep --bind tcp://:5555 --require ./transform.rb \
205
+ --eval 'upcase_all($F)'
206
+
207
+ # next skips, break stops — regexps match against $_
208
+ omq pull --bind tcp://:5557 \
209
+ --eval 'next if /^#/; break if /quit/; $F'
210
+
211
+ # BEGIN/END blocks (like awk) — accumulate and summarize
212
+ omq pull --bind tcp://:5557 \
213
+ --eval 'BEGIN{ @sum = 0 } @sum += Integer($_); next END{ puts @sum }'
214
+ TEXT
215
+
216
+ module_function
217
+
218
+
219
+ # Displays text through the system pager, or prints directly
220
+ # when stdout is not a terminal.
221
+ #
222
+ def page(text)
223
+ if $stdout.tty?
224
+ if ENV["PAGER"]
225
+ pager = ENV["PAGER"]
226
+ else
227
+ ENV["LESS"] ||= "-FR"
228
+ pager = "less"
229
+ end
230
+ IO.popen(pager, "w") { |io| io.puts text }
231
+ else
232
+ puts text
233
+ end
234
+ rescue Errno::ENOENT
235
+ puts text
236
+ rescue Errno::EPIPE
237
+ # user quit pager early
238
+ end
239
+
240
+
241
+ # Parses CLI arguments, validates options, and runs the main
242
+ # event loop inside an Async reactor.
243
+ #
244
+ def run(argv = ARGV)
245
+ config = build_config(argv)
246
+
247
+ require_relative "../omq"
248
+ require "async"
249
+ require "json"
250
+ require "console"
251
+
252
+ validate_gems!(config)
253
+
254
+ trap("INT") { Process.exit!(0) }
255
+ trap("TERM") { Process.exit!(0) }
256
+
257
+ Console.logger = Console::Logger.new(Console::Output::Null.new) unless config.verbose
258
+
259
+ runner_class, socket_sym = RUNNER_MAP.fetch(config.type_name)
260
+
261
+ Async do |task|
262
+ runner = if socket_sym
263
+ runner_class.new(config, OMQ.const_get(socket_sym))
264
+ else
265
+ runner_class.new(config)
266
+ end
267
+ runner.call(task)
268
+ rescue IO::TimeoutError, Async::TimeoutError
269
+ $stderr.puts "omq: timeout" unless config.quiet
270
+ exit 2
271
+ end
272
+ end
273
+
274
+
275
+ # Builds a frozen Config from command-line arguments.
276
+ #
277
+ def build_config(argv)
278
+ opts = parse_options(argv)
279
+ validate!(opts)
280
+
281
+ opts[:has_msgpack] = begin; require "msgpack"; true; rescue LoadError; false; end
282
+ opts[:has_zstd] = begin; require "zstd-ruby"; true; rescue LoadError; false; end
283
+ opts[:stdin_is_tty] = $stdin.tty?
284
+
285
+ Ractor.make_shareable(Config.new(**opts))
286
+ end
287
+
288
+
289
+ # Parses command-line arguments into a mutable options hash.
290
+ #
291
+ def parse_options(argv)
292
+ opts = {
293
+ endpoints: [],
294
+ connects: [],
295
+ binds: [],
296
+ data: nil,
297
+ file: nil,
298
+ format: :ascii,
299
+ subscribes: [],
300
+ joins: [],
301
+ group: nil,
302
+ identity: nil,
303
+ target: nil,
304
+ interval: nil,
305
+ count: nil,
306
+ delay: nil,
307
+ timeout: nil,
308
+ linger: 5,
309
+ reconnect_ivl: nil,
310
+ heartbeat_ivl: nil,
311
+ conflate: false,
312
+ compress: false,
313
+ expr: nil,
314
+ parallel: nil,
315
+ transient: false,
316
+ verbose: false,
317
+ quiet: false,
318
+ echo: false,
319
+ curve_server: false,
320
+ curve_server_key: nil,
321
+ }
322
+
323
+ parser = OptionParser.new do |o|
324
+ o.banner = "Usage: omq TYPE [options]\n\n" \
325
+ "Types: req, rep, pub, sub, push, pull, pair, dealer, router\n" \
326
+ "Draft: client, server, radio, dish, scatter, gather, channel, peer\n" \
327
+ "Virtual: pipe (PULL → eval → PUSH)\n\n"
328
+
329
+ o.separator "Connection:"
330
+ o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v| opts[:endpoints] << Endpoint.new(v, false); opts[:connects] << v }
331
+ o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v| opts[:endpoints] << Endpoint.new(v, true); opts[:binds] << v }
332
+
333
+ o.separator "\nData source (REP: reply source):"
334
+ o.on( "--echo", "Echo received messages back (REP)") { opts[:echo] = true }
335
+ o.on("-D", "--data DATA", "Message data (literal string)") { |v| opts[:data] = v }
336
+ o.on("-F", "--file FILE", "Read message from file (- = stdin)") { |v| opts[:file] = v }
337
+
338
+ o.separator "\nFormat (input + output):"
339
+ o.on("-A", "--ascii", "Tab-separated frames, safe ASCII (default)") { opts[:format] = :ascii }
340
+ o.on("-Q", "--quoted", "C-style quoted with escapes") { opts[:format] = :quoted }
341
+ o.on( "--raw", "Raw binary, no framing") { opts[:format] = :raw }
342
+ o.on("-J", "--jsonl", "JSON Lines (array of strings per line)") { opts[:format] = :jsonl }
343
+ o.on( "--msgpack", "MessagePack arrays (binary stream)") { opts[:format] = :msgpack }
344
+ o.on("-M", "--marshal", "Ruby Marshal stream (binary, Array<String>)") { opts[:format] = :marshal }
345
+
346
+ o.separator "\nSubscription/groups:"
347
+ o.on("-s", "--subscribe PREFIX", "Subscribe prefix (SUB, default all)") { |v| opts[:subscribes] << v }
348
+ o.on("-j", "--join GROUP", "Join group (repeatable, DISH only)") { |v| opts[:joins] << v }
349
+ o.on("-g", "--group GROUP", "Publish group (RADIO only)") { |v| opts[:group] = v }
350
+
351
+ o.separator "\nIdentity/routing:"
352
+ o.on("--identity ID", "Set socket identity (DEALER/ROUTER)") { |v| opts[:identity] = v }
353
+ o.on("--target ID", "Target peer (ROUTER/SERVER/PEER, 0x prefix for binary)") { |v| opts[:target] = v }
354
+
355
+ o.separator "\nTiming:"
356
+ o.on("-i", "--interval SECS", Float, "Repeat interval") { |v| opts[:interval] = v }
357
+ o.on("-n", "--count COUNT", Integer, "Max iterations (0=inf)") { |v| opts[:count] = v }
358
+ o.on("-d", "--delay SECS", Float, "Delay before first send") { |v| opts[:delay] = v }
359
+ o.on("-t", "--timeout SECS", Float, "Send/receive timeout") { |v| opts[:timeout] = v }
360
+ o.on("-l", "--linger SECS", Float, "Drain time on close (default 5)") { |v| opts[:linger] = v }
361
+ o.on("--reconnect-ivl IVL", "Reconnect interval: SECS or MIN..MAX (default 0.1)") { |v|
362
+ opts[:reconnect_ivl] = if v.include?("..")
363
+ lo, hi = v.split("..", 2)
364
+ Float(lo)..Float(hi)
365
+ else
366
+ Float(v)
367
+ end
368
+ }
369
+ o.on("--heartbeat-ivl SECS", Float, "ZMTP heartbeat interval (detects dead peers)") { |v| opts[:heartbeat_ivl] = v }
370
+
371
+ o.separator "\nDelivery:"
372
+ o.on("--conflate", "Keep only last message per subscriber (PUB/RADIO)") { opts[:conflate] = true }
373
+
374
+ o.separator "\nCompression:"
375
+ o.on("-z", "--compress", "Zstandard compression per frame") { opts[:compress] = true }
376
+
377
+ o.separator "\nProcessing:"
378
+ o.on("-e", "--eval EXPR", "Eval Ruby for each message ($F = parts)") { |v| opts[:expr] = v }
379
+ o.on("-r", "--require LIB", "Require library or file (-r./lib.rb)") { |v|
380
+ v.start_with?("./", "../") ? require(File.expand_path(v)) : require(v)
381
+ }
382
+ o.on("-P", "--parallel [N]", Integer, "Parallel Ractor workers (pipe only, default: nproc)") { |v|
383
+ require "etc"
384
+ opts[:parallel] = v || Etc.nprocessors
385
+ }
386
+
387
+ o.separator "\nCURVE encryption (requires omq-curve gem):"
388
+ o.on("--curve-server", "Enable CURVE as server (generates keypair)") { opts[:curve_server] = true }
389
+ o.on("--curve-server-key KEY", "Enable CURVE as client (server's Z85 public key)") { |v| opts[:curve_server_key] = v }
390
+ o.separator " Env vars: OMQ_SERVER_KEY (client), OMQ_SERVER_PUBLIC + OMQ_SERVER_SECRET (server)"
391
+
392
+ o.separator "\nOther:"
393
+ o.on("-v", "--verbose", "Print connection events to stderr") { opts[:verbose] = true }
394
+ o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
395
+ o.on( "--transient", "Exit when all peers disconnect") { opts[:transient] = true }
396
+ o.on("-V", "--version") { require_relative "../omq"; puts "omq #{OMQ::VERSION}"; exit }
397
+ o.on("-h") { puts o; exit }
398
+ o.on( "--help") { page "#{o}\n#{EXAMPLES}"; exit }
399
+ o.on( "--examples") { page EXAMPLES; exit }
400
+
401
+ o.separator "\nExit codes: 0 = success, 1 = error, 2 = timeout"
402
+ end
403
+
404
+ begin
405
+ parser.parse!(argv)
406
+ rescue OptionParser::ParseError => e
407
+ abort e.message
408
+ end
409
+
410
+ type_name = argv.shift
411
+ abort parser.to_s unless type_name
412
+ unless SOCKET_TYPE_NAMES.include?(type_name.downcase)
413
+ abort "Unknown socket type: #{type_name}. Known: #{SOCKET_TYPE_NAMES.join(', ')}"
414
+ end
415
+
416
+ opts[:type_name] = type_name.downcase
417
+
418
+ normalize = ->(url) { url.sub(%r{\Atcp://:}, "tcp://*:") }
419
+ opts[:connects].map!(&normalize)
420
+ opts[:binds].map!(&normalize)
421
+ opts[:endpoints].map! { |ep| Endpoint.new(normalize.call(ep.url), ep.bind?) }
422
+
423
+ opts
424
+ end
425
+
426
+
427
+ # Validates option combinations.
428
+ #
429
+ def validate!(opts)
430
+ type_name = opts[:type_name]
431
+
432
+ if type_name == "pipe"
433
+ abort "pipe requires exactly 2 endpoints (pull-side and push-side)" if opts[:endpoints].size != 2
434
+ else
435
+ abort "At least one --connect or --bind is required" if opts[:connects].empty? && opts[:binds].empty?
436
+ end
437
+ abort "--data and --file are mutually exclusive" if opts[:data] && opts[:file]
438
+ abort "--subscribe is only valid for SUB" if !opts[:subscribes].empty? && type_name != "sub"
439
+ abort "--join is only valid for DISH" if !opts[:joins].empty? && type_name != "dish"
440
+ abort "--group is only valid for RADIO" if opts[:group] && type_name != "radio"
441
+ abort "--identity is only valid for DEALER/ROUTER" if opts[:identity] && !%w[dealer router].include?(type_name)
442
+ abort "--target is only valid for ROUTER/SERVER/PEER" if opts[:target] && !%w[router server peer].include?(type_name)
443
+ abort "--conflate is only valid for PUB/RADIO" if opts[:conflate] && !%w[pub radio].include?(type_name)
444
+
445
+ if opts[:parallel]
446
+ abort "-P/--parallel is only valid for pipe" unless type_name == "pipe"
447
+ abort "-P/--parallel must be >= 2" if opts[:parallel] < 2
448
+ abort "-P/--parallel requires both endpoints to use --connect (not --bind)" if opts[:endpoints].any?(&:bind?)
449
+ end
450
+
451
+ (opts[:connects] + opts[:binds]).each do |url|
452
+ abort "inproc not supported, use tcp:// or ipc://" if url.include?("inproc://")
453
+ end
454
+ end
455
+
456
+
457
+ # Validates that required gems are available.
458
+ #
459
+ def validate_gems!(config)
460
+ abort "--msgpack requires the msgpack gem" if config.format == :msgpack && !config.has_msgpack
461
+ abort "--compress requires the zstd-ruby gem" if config.compress && !config.has_zstd
462
+
463
+ if config.recv_only? && (config.data || config.file)
464
+ abort "--data/--file not valid for #{config.type_name} (receive-only)"
465
+ end
466
+ end
467
+ end
468
+ 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