omq 0.8.0 → 0.10.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +87 -0
  3. data/README.md +9 -49
  4. data/lib/omq/channel.rb +3 -3
  5. data/lib/omq/client_server.rb +6 -6
  6. data/lib/omq/engine.rb +641 -0
  7. data/lib/omq/options.rb +46 -0
  8. data/lib/omq/pair.rb +2 -2
  9. data/lib/omq/peer.rb +3 -3
  10. data/lib/omq/pub_sub.rb +6 -6
  11. data/lib/omq/push_pull.rb +2 -2
  12. data/lib/omq/radio_dish.rb +2 -2
  13. data/lib/omq/reactor.rb +128 -0
  14. data/lib/omq/readable.rb +42 -0
  15. data/lib/omq/req_rep.rb +4 -4
  16. data/lib/omq/router_dealer.rb +4 -4
  17. data/lib/omq/routing/channel.rb +83 -0
  18. data/lib/omq/routing/client.rb +56 -0
  19. data/lib/omq/routing/dealer.rb +57 -0
  20. data/lib/omq/routing/dish.rb +78 -0
  21. data/lib/omq/routing/fan_out.rb +131 -0
  22. data/lib/omq/routing/gather.rb +46 -0
  23. data/lib/omq/routing/pair.rb +86 -0
  24. data/lib/omq/routing/peer.rb +101 -0
  25. data/lib/omq/routing/pub.rb +60 -0
  26. data/lib/omq/routing/pull.rb +46 -0
  27. data/lib/omq/routing/push.rb +81 -0
  28. data/lib/omq/routing/radio.rb +140 -0
  29. data/lib/omq/routing/rep.rb +101 -0
  30. data/lib/omq/routing/req.rb +65 -0
  31. data/lib/omq/routing/round_robin.rb +168 -0
  32. data/lib/omq/routing/router.rb +110 -0
  33. data/lib/omq/routing/scatter.rb +82 -0
  34. data/lib/omq/routing/server.rb +101 -0
  35. data/lib/omq/routing/sub.rb +78 -0
  36. data/lib/omq/routing/xpub.rb +72 -0
  37. data/lib/omq/routing/xsub.rb +83 -0
  38. data/lib/omq/routing.rb +66 -0
  39. data/lib/omq/scatter_gather.rb +4 -4
  40. data/lib/omq/single_frame.rb +18 -0
  41. data/lib/omq/socket.rb +24 -9
  42. data/lib/omq/transport/inproc.rb +355 -0
  43. data/lib/omq/transport/ipc.rb +117 -0
  44. data/lib/omq/transport/tcp.rb +111 -0
  45. data/lib/omq/version.rb +1 -1
  46. data/lib/omq/writable.rb +65 -0
  47. data/lib/omq.rb +60 -4
  48. metadata +38 -58
  49. data/exe/omq +0 -6
  50. data/lib/omq/cli/base_runner.rb +0 -459
  51. data/lib/omq/cli/channel.rb +0 -8
  52. data/lib/omq/cli/client_server.rb +0 -111
  53. data/lib/omq/cli/config.rb +0 -54
  54. data/lib/omq/cli/formatter.rb +0 -75
  55. data/lib/omq/cli/pair.rb +0 -31
  56. data/lib/omq/cli/peer.rb +0 -8
  57. data/lib/omq/cli/pipe.rb +0 -265
  58. data/lib/omq/cli/pub_sub.rb +0 -14
  59. data/lib/omq/cli/push_pull.rb +0 -14
  60. data/lib/omq/cli/radio_dish.rb +0 -27
  61. data/lib/omq/cli/req_rep.rb +0 -83
  62. data/lib/omq/cli/router_dealer.rb +0 -76
  63. data/lib/omq/cli/scatter_gather.rb +0 -14
  64. data/lib/omq/cli.rb +0 -540
  65. data/lib/omq/zmtp/engine.rb +0 -551
  66. data/lib/omq/zmtp/options.rb +0 -48
  67. data/lib/omq/zmtp/reactor.rb +0 -131
  68. data/lib/omq/zmtp/readable.rb +0 -29
  69. data/lib/omq/zmtp/routing/channel.rb +0 -81
  70. data/lib/omq/zmtp/routing/client.rb +0 -56
  71. data/lib/omq/zmtp/routing/dealer.rb +0 -57
  72. data/lib/omq/zmtp/routing/dish.rb +0 -80
  73. data/lib/omq/zmtp/routing/fan_out.rb +0 -131
  74. data/lib/omq/zmtp/routing/gather.rb +0 -48
  75. data/lib/omq/zmtp/routing/pair.rb +0 -84
  76. data/lib/omq/zmtp/routing/peer.rb +0 -100
  77. data/lib/omq/zmtp/routing/pub.rb +0 -62
  78. data/lib/omq/zmtp/routing/pull.rb +0 -48
  79. data/lib/omq/zmtp/routing/push.rb +0 -80
  80. data/lib/omq/zmtp/routing/radio.rb +0 -139
  81. data/lib/omq/zmtp/routing/rep.rb +0 -101
  82. data/lib/omq/zmtp/routing/req.rb +0 -65
  83. data/lib/omq/zmtp/routing/round_robin.rb +0 -143
  84. data/lib/omq/zmtp/routing/router.rb +0 -109
  85. data/lib/omq/zmtp/routing/scatter.rb +0 -81
  86. data/lib/omq/zmtp/routing/server.rb +0 -100
  87. data/lib/omq/zmtp/routing/sub.rb +0 -80
  88. data/lib/omq/zmtp/routing/xpub.rb +0 -74
  89. data/lib/omq/zmtp/routing/xsub.rb +0 -86
  90. data/lib/omq/zmtp/routing.rb +0 -65
  91. data/lib/omq/zmtp/single_frame.rb +0 -20
  92. data/lib/omq/zmtp/transport/inproc.rb +0 -359
  93. data/lib/omq/zmtp/transport/ipc.rb +0 -118
  94. data/lib/omq/zmtp/transport/tcp.rb +0 -117
  95. data/lib/omq/zmtp/writable.rb +0 -61
  96. data/lib/omq/zmtp.rb +0 -81
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # XSUB socket routing: like SUB but subscriptions sent as data messages.
6
+ #
7
+ # Subscriptions are sent as data frames: \x01 + prefix for subscribe,
8
+ # \x00 + prefix for unsubscribe.
9
+ #
10
+ class XSub
11
+
12
+ # @param engine [Engine]
13
+ #
14
+ def initialize(engine)
15
+ @engine = engine
16
+ @connections = []
17
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
18
+ @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
19
+ @tasks = []
20
+ @send_pump_started = false
21
+ @send_pump_idle = true
22
+ end
23
+
24
+ # @return [Async::LimitedQueue]
25
+ #
26
+ attr_reader :recv_queue, :send_queue
27
+
28
+ # @param connection [Connection]
29
+ #
30
+ def connection_added(connection)
31
+ @connections << connection
32
+ task = @engine.start_recv_pump(connection, @recv_queue)
33
+ @tasks << task if task
34
+ start_send_pump unless @send_pump_started
35
+ end
36
+
37
+ # @param connection [Connection]
38
+ #
39
+ def connection_removed(connection)
40
+ @connections.delete(connection)
41
+ end
42
+
43
+ # @param parts [Array<String>]
44
+ #
45
+ def enqueue(parts)
46
+ @send_queue.enqueue(parts)
47
+ end
48
+
49
+ #
50
+ def stop
51
+ @tasks.each(&:stop)
52
+ @tasks.clear
53
+ end
54
+
55
+ def send_pump_idle? = @send_pump_idle
56
+
57
+ private
58
+
59
+ def start_send_pump
60
+ @send_pump_started = true
61
+ @tasks << @engine.spawn_pump_task(annotation: "send pump") do
62
+ loop do
63
+ @send_pump_idle = true
64
+ parts = @send_queue.dequeue
65
+ @send_pump_idle = false
66
+ frame = parts.first&.b
67
+ next if frame.nil? || frame.empty?
68
+
69
+ flag = frame.getbyte(0)
70
+ prefix = frame.byteslice(1..) || "".b
71
+
72
+ case flag
73
+ when 0x01
74
+ @connections.each { |c| c.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix)) }
75
+ when 0x00
76
+ @connections.each { |c| c.send_command(Protocol::ZMTP::Codec::Command.cancel(prefix)) }
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "async/queue"
5
+ require "async/limited_queue"
6
+
7
+ module OMQ
8
+ # Routing strategies for each ZMQ socket type.
9
+ #
10
+ # Each strategy manages how messages flow between connections and
11
+ # the socket's send/recv queues.
12
+ #
13
+ module Routing
14
+ # Shared frozen empty binary string to avoid repeated allocations.
15
+ EMPTY_BINARY = "".b.freeze
16
+
17
+ # Drains all available messages from +queue+ into +batch+ without
18
+ # blocking. Call after the initial blocking dequeue.
19
+ #
20
+ # No cap is needed: IO::Stream auto-flushes at 64 KB, so the
21
+ # write buffer hits the wire naturally under sustained load.
22
+ # The explicit flush after the batch pushes out the remainder.
23
+ #
24
+ # @param queue [Async::LimitedQueue]
25
+ # @param batch [Array]
26
+ # @return [void]
27
+ #
28
+ def self.drain_send_queue(queue, batch)
29
+ loop do
30
+ msg = queue.dequeue(timeout: 0)
31
+ break unless msg
32
+ batch << msg
33
+ end
34
+ end
35
+
36
+ # Returns the routing strategy class for a socket type.
37
+ #
38
+ # @param socket_type [Symbol] e.g. :PAIR, :REQ
39
+ # @return [Class]
40
+ #
41
+ def self.for(socket_type)
42
+ case socket_type
43
+ when :PAIR then Pair
44
+ when :REQ then Req
45
+ when :REP then Rep
46
+ when :DEALER then Dealer
47
+ when :ROUTER then Router
48
+ when :PUB then Pub
49
+ when :SUB then Sub
50
+ when :XPUB then XPub
51
+ when :XSUB then XSub
52
+ when :PUSH then Push
53
+ when :PULL then Pull
54
+ when :CLIENT then Client
55
+ when :SERVER then Server
56
+ when :RADIO then Radio
57
+ when :DISH then Dish
58
+ when :SCATTER then Scatter
59
+ when :GATHER then Gather
60
+ when :PEER then Peer
61
+ when :CHANNEL then Channel
62
+ else raise ArgumentError, "unknown socket type: #{socket_type}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module OMQ
4
4
  class SCATTER < Socket
5
- include ZMTP::Writable
6
- include ZMTP::SingleFrame
5
+ include Writable
6
+ include SingleFrame
7
7
 
8
8
  def initialize(endpoints = nil, linger: 0, send_hwm: nil, send_timeout: nil)
9
9
  _init_engine(:SCATTER, linger: linger, send_hwm: send_hwm, send_timeout: send_timeout)
@@ -12,8 +12,8 @@ module OMQ
12
12
  end
13
13
 
14
14
  class GATHER < Socket
15
- include ZMTP::Readable
16
- include ZMTP::SingleFrame
15
+ include Readable
16
+ include SingleFrame
17
17
 
18
18
  def initialize(endpoints = nil, linger: 0, recv_hwm: nil, recv_timeout: nil)
19
19
  _init_engine(:GATHER, linger: linger, recv_hwm: recv_hwm, recv_timeout: recv_timeout)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ # Mixin that rejects multipart messages.
5
+ #
6
+ # All draft socket types (CLIENT, SERVER, RADIO, DISH, SCATTER,
7
+ # GATHER, PEER, CHANNEL) require single-frame messages for
8
+ # thread-safe atomic operations.
9
+ #
10
+ module SingleFrame
11
+ def send(message)
12
+ if message.is_a?(Array) && message.size > 1
13
+ raise ArgumentError, "#{self.class} does not support multipart messages"
14
+ end
15
+ super
16
+ end
17
+ end
18
+ end
data/lib/omq/socket.rb CHANGED
@@ -6,7 +6,7 @@ module OMQ
6
6
  # Socket base class.
7
7
  #
8
8
  class Socket
9
- # @return [ZMTP::Options]
9
+ # @return [Options]
10
10
  #
11
11
  attr_reader :options
12
12
 
@@ -70,8 +70,11 @@ module OMQ
70
70
  # @return [void]
71
71
  #
72
72
  def bind(endpoint)
73
- @engine.bind(endpoint)
74
- @last_tcp_port = @engine.last_tcp_port
73
+ ensure_parent_task
74
+ Reactor.run do
75
+ @engine.bind(endpoint)
76
+ @last_tcp_port = @engine.last_tcp_port
77
+ end
75
78
  end
76
79
 
77
80
 
@@ -81,7 +84,8 @@ module OMQ
81
84
  # @return [void]
82
85
  #
83
86
  def connect(endpoint)
84
- @engine.connect(endpoint)
87
+ ensure_parent_task
88
+ Reactor.run { @engine.connect(endpoint) }
85
89
  end
86
90
 
87
91
 
@@ -91,7 +95,7 @@ module OMQ
91
95
  # @return [void]
92
96
  #
93
97
  def disconnect(endpoint)
94
- @engine.disconnect(endpoint)
98
+ Reactor.run { @engine.disconnect(endpoint) }
95
99
  end
96
100
 
97
101
 
@@ -101,7 +105,7 @@ module OMQ
101
105
  # @return [void]
102
106
  #
103
107
  def unbind(endpoint)
104
- @engine.unbind(endpoint)
108
+ Reactor.run { @engine.unbind(endpoint) }
105
109
  end
106
110
 
107
111
 
@@ -145,7 +149,7 @@ module OMQ
145
149
  # Closes the socket.
146
150
  #
147
151
  def close
148
- @engine.close
152
+ Reactor.run { @engine.close }
149
153
  nil
150
154
  end
151
155
 
@@ -187,6 +191,15 @@ module OMQ
187
191
  end
188
192
 
189
193
 
194
+ # Sets the engine's parent task before the first bind or connect.
195
+ # Must be called OUTSIDE Reactor.run so that non-Async callers
196
+ # get the IO thread's root task, not an ephemeral work task.
197
+ #
198
+ def ensure_parent_task
199
+ @engine.capture_parent_task
200
+ end
201
+
202
+
190
203
  # Connects or binds based on endpoint prefix convention.
191
204
  #
192
205
  # @param endpoints [String, nil]
@@ -212,13 +225,15 @@ module OMQ
212
225
  #
213
226
  def _init_engine(socket_type, linger:, send_hwm: nil, recv_hwm: nil,
214
227
  send_timeout: nil, recv_timeout: nil, conflate: false)
215
- @options = ZMTP::Options.new(linger: linger)
228
+ @options = Options.new(linger: linger)
216
229
  @options.send_hwm = send_hwm if send_hwm
217
230
  @options.recv_hwm = recv_hwm if recv_hwm
218
231
  @options.send_timeout = send_timeout if send_timeout
219
232
  @options.recv_timeout = recv_timeout if recv_timeout
220
233
  @options.conflate = conflate
221
- @engine = ZMTP::Engine.new(socket_type, @options)
234
+ @recv_buffer = []
235
+ @recv_mutex = Mutex.new
236
+ @engine = Engine.new(socket_type, @options)
222
237
  end
223
238
  end
224
239
  end
@@ -0,0 +1,355 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "async/queue"
5
+
6
+ module OMQ
7
+ module Transport
8
+ # In-process transport.
9
+ #
10
+ # Both peers are Ruby backend sockets in the same process (native
11
+ # ZMQ's inproc registry is separate and unreachable). Messages are
12
+ # transferred as Ruby arrays — no ZMTP framing, no byte
13
+ # serialization. String parts are frozen by Writable#send to
14
+ # prevent shared mutable state without copying.
15
+ #
16
+ module Inproc
17
+ # Socket types that exchange commands (SUBSCRIBE/CANCEL) over inproc.
18
+ #
19
+ COMMAND_TYPES = %i[PUB SUB XPUB XSUB RADIO DISH].freeze
20
+
21
+ # Global registry of bound inproc endpoints.
22
+ #
23
+ @registry = {}
24
+ @mutex = Mutex.new
25
+ @waiters = Hash.new { |h, k| h[k] = [] }
26
+
27
+
28
+ class << self
29
+ # Binds an engine to an inproc endpoint.
30
+ #
31
+ # @param endpoint [String] e.g. "inproc://my-endpoint"
32
+ # @param engine [Engine] the owning engine
33
+ # @return [Listener]
34
+ # @raise [ArgumentError] if endpoint is already bound
35
+ #
36
+ def bind(endpoint, engine)
37
+ @mutex.synchronize do
38
+ raise ArgumentError, "endpoint already bound: #{endpoint}" if @registry.key?(endpoint)
39
+ @registry[endpoint] = engine
40
+
41
+ # Wake any pending connects
42
+ @waiters[endpoint].each { |p| p.resolve(true) }
43
+ @waiters.delete(endpoint)
44
+ end
45
+ Listener.new(endpoint)
46
+ end
47
+
48
+
49
+ # Connects to a bound inproc endpoint.
50
+ #
51
+ # @param endpoint [String] e.g. "inproc://my-endpoint"
52
+ # @param engine [Engine] the connecting engine
53
+ # @return [void]
54
+ #
55
+ def connect(endpoint, engine)
56
+ bound_engine = @mutex.synchronize { @registry[endpoint] }
57
+
58
+ unless bound_engine
59
+ # Endpoint not bound yet. Wait with timeout derived from
60
+ # reconnect_interval. If it doesn't appear, silently return —
61
+ # matching ZMQ 4.x behavior where inproc connect to an
62
+ # unbound endpoint succeeds but messages go nowhere.
63
+ # A background task retries periodically.
64
+ ri = engine.options.reconnect_interval
65
+ timeout = ri.is_a?(Range) ? ri.begin : ri
66
+ promise = Async::Promise.new
67
+ @mutex.synchronize { @waiters[endpoint] << promise }
68
+ unless promise.wait?(timeout: timeout)
69
+ @mutex.synchronize { @waiters[endpoint].delete(promise) }
70
+ start_connect_retry(endpoint, engine)
71
+ return
72
+ end
73
+ bound_engine = @mutex.synchronize { @registry[endpoint] }
74
+ end
75
+
76
+ establish_link(engine, bound_engine, endpoint)
77
+ end
78
+
79
+
80
+ # Removes a bound endpoint from the registry.
81
+ #
82
+ # @param endpoint [String]
83
+ # @return [void]
84
+ #
85
+ def unbind(endpoint)
86
+ @mutex.synchronize { @registry.delete(endpoint) }
87
+ end
88
+
89
+
90
+ # Resets the registry. Used in tests.
91
+ #
92
+ # @return [void]
93
+ #
94
+ def reset!
95
+ @mutex.synchronize do
96
+ @registry.clear
97
+ @waiters.clear
98
+ end
99
+ end
100
+
101
+
102
+ private
103
+
104
+
105
+ # Wires up a client-server inproc pipe pair after validating
106
+ # that the two socket types are compatible.
107
+ #
108
+ # @param client_engine [Engine] the connecting engine
109
+ # @param server_engine [Engine] the bound engine
110
+ # @param endpoint [String] the inproc endpoint name
111
+ #
112
+ def establish_link(client_engine, server_engine, endpoint)
113
+ client_type = client_engine.socket_type
114
+ server_type = server_engine.socket_type
115
+
116
+ unless Protocol::ZMTP::VALID_PEERS[client_type]&.include?(server_type)
117
+ raise Protocol::ZMTP::Error,
118
+ "incompatible socket types: #{client_type} cannot connect to #{server_type}"
119
+ end
120
+
121
+ # Only PUB/SUB-family types exchange commands (SUBSCRIBE/CANCEL)
122
+ # over inproc. All other types use only the direct recv queue
123
+ # bypass for data, so no internal queues are needed.
124
+ needs_commands = COMMAND_TYPES.include?(client_type) ||
125
+ COMMAND_TYPES.include?(server_type)
126
+
127
+ if needs_commands
128
+ a_to_b = Async::Queue.new
129
+ b_to_a = Async::Queue.new
130
+ end
131
+
132
+ client_pipe = DirectPipe.new(
133
+ send_queue: needs_commands ? a_to_b : nil,
134
+ receive_queue: needs_commands ? b_to_a : nil,
135
+ peer_identity: server_engine.options.identity,
136
+ peer_type: server_type.to_s,
137
+ )
138
+ server_pipe = DirectPipe.new(
139
+ send_queue: needs_commands ? b_to_a : nil,
140
+ receive_queue: needs_commands ? a_to_b : nil,
141
+ peer_identity: client_engine.options.identity,
142
+ peer_type: client_type.to_s,
143
+ )
144
+
145
+ client_pipe.peer = server_pipe
146
+ server_pipe.peer = client_pipe
147
+
148
+ client_engine.connection_ready(client_pipe, endpoint: endpoint)
149
+ server_engine.connection_ready(server_pipe, endpoint: endpoint)
150
+ end
151
+
152
+
153
+ # Spawns a background task that periodically retries
154
+ # #establish_link until the endpoint appears in the registry.
155
+ #
156
+ # @param endpoint [String] the inproc endpoint name
157
+ # @param engine [Engine] the connecting engine
158
+ #
159
+ def start_connect_retry(endpoint, engine)
160
+ engine.spawn_inproc_retry(endpoint) do |ivl|
161
+ loop do
162
+ sleep ivl
163
+ bound_engine = @mutex.synchronize { @registry[endpoint] }
164
+ if bound_engine
165
+ establish_link(engine, bound_engine, endpoint)
166
+ break
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ # A bound inproc endpoint handle.
174
+ #
175
+ class Listener
176
+ # @return [String] the bound endpoint
177
+ #
178
+ attr_reader :endpoint
179
+
180
+
181
+ # @param endpoint [String]
182
+ #
183
+ def initialize(endpoint)
184
+ @endpoint = endpoint
185
+ end
186
+
187
+
188
+ # Stops the listener by removing it from the registry.
189
+ #
190
+ # @return [void]
191
+ #
192
+ def stop
193
+ Inproc.unbind(@endpoint)
194
+ end
195
+ end
196
+
197
+ # A direct in-process pipe that transfers Ruby arrays through queues.
198
+ #
199
+ # Implements the same interface as Connection so routing strategies
200
+ # can use it transparently.
201
+ #
202
+ # When a routing strategy sets {#direct_recv_queue} on a pipe,
203
+ # {#send_message} enqueues directly into the peer's recv queue,
204
+ # bypassing the intermediate pipe queues and the recv pump task.
205
+ # This reduces inproc from 3 queue hops to 2 (send_queue →
206
+ # recv_queue), eliminating the internal pipe queue in between.
207
+ #
208
+ class DirectPipe
209
+ # @return [String] peer's socket type
210
+ #
211
+ attr_reader :peer_socket_type
212
+
213
+
214
+ # @return [String] peer's identity
215
+ #
216
+ attr_reader :peer_identity
217
+
218
+
219
+ # @return [DirectPipe, nil] the other end of this pipe pair
220
+ #
221
+ attr_accessor :peer
222
+
223
+
224
+ # @return [Async::LimitedQueue, nil] when set, {#send_message}
225
+ # enqueues directly here instead of using the internal queue
226
+ #
227
+ attr_reader :direct_recv_queue
228
+
229
+
230
+ # @return [Proc, nil] optional transform applied before
231
+ # enqueuing into {#direct_recv_queue}
232
+ #
233
+ attr_accessor :direct_recv_transform
234
+
235
+
236
+ # @param send_queue [Async::Queue, nil] outgoing command queue
237
+ # (nil for non-PUB/SUB types that don't exchange commands)
238
+ # @param receive_queue [Async::Queue, nil] incoming command queue
239
+ # @param peer_identity [String]
240
+ # @param peer_type [String]
241
+ #
242
+ def initialize(send_queue: nil, receive_queue: nil, peer_identity:, peer_type:)
243
+ @send_queue = send_queue
244
+ @receive_queue = receive_queue
245
+ @peer_identity = peer_identity || "".b
246
+ @peer_socket_type = peer_type
247
+ @closed = false
248
+ @peer = nil
249
+ @direct_recv_queue = nil
250
+ @direct_recv_transform = nil
251
+ @pending_direct = nil
252
+ end
253
+
254
+
255
+ # Sets the direct recv queue. Drains any messages that were
256
+ # buffered before the queue was available.
257
+ #
258
+ def direct_recv_queue=(queue)
259
+ @direct_recv_queue = queue
260
+ if queue && @pending_direct
261
+ @pending_direct.each do |msg|
262
+ queue.enqueue(msg)
263
+ end
264
+ @pending_direct = nil
265
+ end
266
+ end
267
+
268
+
269
+ # Sends a multi-frame message.
270
+ #
271
+ # When {#direct_recv_queue} is set (inproc fast path), the
272
+ # message is delivered directly to the peer's recv queue,
273
+ # skipping the internal pipe queues and the recv pump.
274
+ #
275
+ # @param parts [Array<String>]
276
+ # @return [void]
277
+ #
278
+ def send_message(parts)
279
+ raise IOError, "closed" if @closed
280
+ if @direct_recv_queue
281
+ msg = @direct_recv_transform ? @direct_recv_transform.call(parts).freeze : parts
282
+ @direct_recv_queue.enqueue(msg)
283
+ elsif @send_queue
284
+ @send_queue.enqueue(parts)
285
+ else
286
+ msg = @direct_recv_transform ? @direct_recv_transform.call(parts).freeze : parts
287
+ (@pending_direct ||= []) << msg
288
+ end
289
+ end
290
+
291
+
292
+ alias write_message send_message
293
+
294
+
295
+ # No-op — inproc has no IO buffer to flush.
296
+ #
297
+ # @return [void]
298
+ #
299
+ def flush = nil
300
+
301
+
302
+ # Receives a multi-frame message.
303
+ #
304
+ # @return [Array<String>]
305
+ # @raise [EOFError] if closed
306
+ #
307
+ def receive_message
308
+ msg = @receive_queue.dequeue
309
+ raise EOFError, "connection closed" if msg.nil?
310
+ msg
311
+ end
312
+
313
+
314
+ # Sends a command via the internal command queue.
315
+ # Only available for PUB/SUB-family pipes.
316
+ #
317
+ # @param command [Protocol::ZMTP::Codec::Command]
318
+ # @return [void]
319
+ #
320
+ def send_command(command)
321
+ raise IOError, "closed" if @closed
322
+ @send_queue.enqueue([:command, command])
323
+ end
324
+
325
+
326
+ # Reads one command frame from the internal command queue.
327
+ # Used by PUB/XPUB subscription listeners.
328
+ #
329
+ # @return [Protocol::ZMTP::Codec::Frame]
330
+ #
331
+ def read_frame
332
+ loop do
333
+ item = @receive_queue.dequeue
334
+ raise EOFError, "connection closed" if item.nil?
335
+ if item.is_a?(Array) && item.first == :command
336
+ cmd = item[1]
337
+ return Protocol::ZMTP::Codec::Frame.new(cmd.to_body, command: true)
338
+ end
339
+ end
340
+ end
341
+
342
+
343
+ # Closes this pipe end.
344
+ #
345
+ # @return [void]
346
+ #
347
+ def close
348
+ return if @closed
349
+ @closed = true
350
+ @send_queue&.enqueue(nil) # close sentinel
351
+ end
352
+ end
353
+ end
354
+ end
355
+ end