nnq 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68a6dd62dc097b93740827c44f95bbfa983d5c7d6072b4625257bd2350ba23fe
4
- data.tar.gz: 376c1ef08eda16a8950ae703d1798256c8c5ba720cd1e5d0e988f1a87067c093
3
+ metadata.gz: 0a336ac1e24bc6210ddeac6731163baab6f8980d9eebbe3235fac13157416fcf
4
+ data.tar.gz: f049bf9038235487966cae1c8920b452abf9984e3776b2b93cbbd95e067fb485
5
5
  SHA512:
6
- metadata.gz: f88c29c241ea5922930342a11c00181007cf698045b219c6e7cc111d2563e37e7e56c64efbb61b48efa608eaa6ab3e7104bc33983c6202410f1745a50a082012
7
- data.tar.gz: e1e42983059b5a280495216a08b6e6ac6a9e6eff9385a2093ad62b9ac7698a80d24a2f36f14c4f4f9579962b7fde9c83a042f04e1819e5963ba2ae94b5cbed4f
6
+ metadata.gz: dcef45943a41f1bc53bbcf8ecc0c34c2779e4a16dea04e8f78854cf6a9debe11168596b47b764728e49393f61c319d41e2d79e489b37dec809adc59cbc0741f0
7
+ data.tar.gz: fd7aaa30c57d5b8fbfd6279aba8b96423e201837ca4ef0144a315ec020da5e0183aaede0c7327fc49bf1bd318894099e4fe02657a12aebf98c1d895582583c62
data/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 — 2026-04-15
4
+
5
+ - **NNG-style raw mode for REQ/REP and SURVEYOR/RESPONDENT.** Constructing
6
+ any of the four with `raw: true` bypasses the cooked state machine
7
+ (request-id tracking, pending-reply slot, survey window) and exposes
8
+ the full SP backtrace header as an opaque, caller-supplied handle.
9
+ - `#receive` returns `[pipe, header, body]` where `pipe` is the live
10
+ `NNQ::Connection` that delivered the message (idiomatic Ruby handle
11
+ — no opaque pipe_id token, no lookup registry), `header` is the
12
+ parsed backtrace bytes, and `body` is the payload.
13
+ - Raw REQ/SURVEYOR send: `send(body, header:)` — fans round-robin /
14
+ fans out.
15
+ - Raw REP/RESPONDENT send: `send(body, to:, header:)` — routes
16
+ directly to a prior `pipe` with the stored `header` written
17
+ verbatim, so the cooked peer matches the reply. Closed peer or
18
+ over-TTL header → silent drop (matches NNG behavior).
19
+ - Cooked-mode methods (`send_request`, `send_reply`, `send_survey`)
20
+ raise `NNQ::Error` in raw mode and vice versa.
21
+ - Unblocks proxy/device-style use cases (forwarders, request routers)
22
+ without touching the cooked code paths. `lib/nnq/routing/{req,rep,
23
+ surveyor,respondent}_raw.rb` live alongside their cooked siblings;
24
+ `build_routing` branches on `@raw` inside REQ0/REP0/SURVEYOR0/
25
+ RESPONDENT0. PUB/SUB and PUSH/PULL raw are still out of scope.
26
+ - **Zero-alloc cooked send paths via protocol-sp `header:` kwarg.**
27
+ `Connection#send_message` / `#write_message` grow an optional
28
+ `header:` kwarg that protocol-sp writes between the SP length prefix
29
+ and the body as a third buffered write (coalesced into a single
30
+ `writev`). Cooked `Req#send_request`, `Rep#send_reply`, and
31
+ `Respondent#send_reply` no longer allocate the `header + body`
32
+ intermediate String on every send — the savings apply to every
33
+ REQ/REP round trip regardless of whether raw mode is used.
34
+ Requires `protocol-sp >= 0.3`.
35
+ - **`Options#recv_hwm`** — new option, defaults to `Options::DEFAULT_HWM`
36
+ (same as `send_hwm`). Bounds the raw routing strategies' receive
37
+ queues; the cooked paths still use their existing (unbounded) state
38
+ and are unaffected.
39
+
3
40
  ## 0.5.0 — 2026-04-15
4
41
 
5
42
  - **Send-path freezes the body** — every public send method (PUSH,
@@ -35,10 +35,12 @@ module NNQ
35
35
  # Writes one message into the SP connection's send buffer (no flush).
36
36
  #
37
37
  # @param body [String]
38
+ # @param header [String, nil] optional binary prefix written between
39
+ # the SP length prefix and body (see Protocol::SP::Connection)
38
40
  # @return [void]
39
- def write_message(body)
41
+ def write_message(body, header: nil)
40
42
  raise ClosedError, "connection closed" if @closed
41
- @sp.write_message(body)
43
+ @sp.write_message(body, header: header)
42
44
  end
43
45
 
44
46
 
@@ -57,10 +59,11 @@ module NNQ
57
59
  # each call is request-paced and there's nothing to batch.
58
60
  #
59
61
  # @param body [String]
62
+ # @param header [String, nil] optional binary prefix
60
63
  # @return [void]
61
- def send_message(body)
64
+ def send_message(body, header: nil)
62
65
  raise ClosedError, "connection closed" if @closed
63
- @sp.send_message(body)
66
+ @sp.send_message(body, header: header)
64
67
  end
65
68
 
66
69
 
data/lib/nnq/options.rb CHANGED
@@ -18,19 +18,21 @@ module NNQ
18
18
  attr_accessor :reconnect_interval
19
19
  attr_accessor :max_message_size
20
20
  attr_accessor :send_hwm
21
+ attr_accessor :recv_hwm
21
22
  attr_accessor :survey_time
22
23
 
23
24
 
24
25
  # @param linger [Numeric] linger period in seconds on close
25
26
  # (default Float::INFINITY = wait forever, matching libzmq).
26
27
  # Pass 0 for immediate drop-on-close.
27
- def initialize(linger: Float::INFINITY, send_hwm: DEFAULT_HWM)
28
+ def initialize(linger: Float::INFINITY, send_hwm: DEFAULT_HWM, recv_hwm: DEFAULT_HWM)
28
29
  @linger = linger
29
30
  @read_timeout = nil
30
31
  @write_timeout = nil
31
32
  @reconnect_interval = 0.1
32
33
  @max_message_size = nil
33
34
  @send_hwm = send_hwm
35
+ @recv_hwm = recv_hwm
34
36
  @survey_time = 1.0
35
37
  end
36
38
 
data/lib/nnq/req_rep.rb CHANGED
@@ -3,21 +3,45 @@
3
3
  require_relative "socket"
4
4
  require_relative "routing/req"
5
5
  require_relative "routing/rep"
6
+ require_relative "routing/req_raw"
7
+ require_relative "routing/rep_raw"
6
8
 
7
9
  module NNQ
8
- # REQ (nng req0): client side of request/reply. Single in-flight
9
- # request per socket. #send_request blocks until the matching reply
10
- # comes back.
10
+ # REQ (nng req0): client side of request/reply. Cooked mode keeps a
11
+ # single in-flight request and matches replies by id; raw mode bypasses
12
+ # the state machine entirely and delivers replies as
13
+ # `[pipe, header, body]` tuples so the app can correlate verbatim.
11
14
  #
12
15
  class REQ0 < Socket
13
- # Sends +body+ as a request, blocks until the matching reply
14
- # arrives. Returns the reply body (without the id header).
16
+ # Cooked: sends +body+ as a request, blocks until the matching reply
17
+ # arrives. Returns the reply body (without the id header). Raises in
18
+ # raw mode — use {#send} / {#receive} there.
15
19
  def send_request(body)
20
+ raise Error, "REQ#send_request not available in raw mode" if raw?
16
21
  body = frozen_binary(body)
17
22
  Reactor.run { @engine.routing.send_request(body) }
18
23
  end
19
24
 
20
25
 
26
+ # Raw: round-robins +body+ to the next connected peer with
27
+ # +header+ (typically `[id | 0x80000000].pack("N")`) written
28
+ # verbatim between the SP length prefix and the body. Raises in
29
+ # cooked mode.
30
+ def send(body, header:)
31
+ raise Error, "REQ#send not available in cooked mode" unless raw?
32
+ body = frozen_binary(body)
33
+ Reactor.run { @engine.routing.send(body, header: header) }
34
+ end
35
+
36
+
37
+ # Raw: blocks until the next reply arrives, returns
38
+ # `[pipe, header, body]`. Raises in cooked mode.
39
+ def receive
40
+ raise Error, "REQ#receive not available in cooked mode" unless raw?
41
+ Reactor.run { @engine.routing.receive }
42
+ end
43
+
44
+
21
45
  private
22
46
 
23
47
 
@@ -27,28 +51,44 @@ module NNQ
27
51
 
28
52
 
29
53
  def build_routing(engine)
30
- Routing::Req.new(engine)
54
+ raw? ? Routing::ReqRaw.new(engine) : Routing::Req.new(engine)
31
55
  end
32
56
  end
33
57
 
34
58
 
35
- # REP (nng rep0): server side of request/reply. Strict alternation
36
- # of #receive then #send_reply, per request.
59
+ # REP (nng rep0): server side of request/reply. Cooked mode strictly
60
+ # alternates #receive / #send_reply and stashes the backtrace
61
+ # internally; raw mode exposes the backtrace as an opaque +header+ and
62
+ # the originating pipe as a live Connection, so the app can drive the
63
+ # reply protocol itself (e.g. proxy/device use cases).
37
64
  #
38
65
  class REP0 < Socket
39
- # Blocks until the next request arrives. Returns the request body.
66
+ # Cooked: returns the next request body. Raw: returns
67
+ # `[pipe, header, body]`.
40
68
  def receive
41
69
  Reactor.run { @engine.routing.receive }
42
70
  end
43
71
 
44
72
 
45
- # Routes +body+ back to the pipe the most recent #receive came from.
73
+ # Cooked: routes +body+ back to the pipe the most recent #receive
74
+ # came from. Raises in raw mode.
46
75
  def send_reply(body)
76
+ raise Error, "REP#send_reply not available in raw mode" if raw?
47
77
  body = frozen_binary(body)
48
78
  Reactor.run { @engine.routing.send_reply(body) }
49
79
  end
50
80
 
51
81
 
82
+ # Raw: writes +body+ with +header+ (the opaque backtrace handed out
83
+ # by a prior #receive) back to +to+ (the Connection from the same
84
+ # tuple). Silent drop if +to+ is closed. Raises in cooked mode.
85
+ def send(body, to:, header:)
86
+ raise Error, "REP#send not available in cooked mode" unless raw?
87
+ body = frozen_binary(body)
88
+ Reactor.run { @engine.routing.send(body, to: to, header: header) }
89
+ end
90
+
91
+
52
92
  private
53
93
 
54
94
 
@@ -58,7 +98,7 @@ module NNQ
58
98
 
59
99
 
60
100
  def build_routing(engine)
61
- Routing::Rep.new(engine)
101
+ raw? ? Routing::RepRaw.new(engine) : Routing::Rep.new(engine)
62
102
  end
63
103
  end
64
104
 
@@ -34,6 +34,14 @@ module NNQ
34
34
  nil # exceeded TTL without finding terminator
35
35
  end
36
36
 
37
+
38
+ # Raw-mode TTL check: returns true if +header+ contains at least
39
+ # MAX_HOPS 4-byte words (i.e. forwarding it would push total hops
40
+ # over the cap). Cheap: just bytesize arithmetic.
41
+ def self.too_many_hops?(header)
42
+ header.bytesize >= MAX_HOPS * 4
43
+ end
44
+
37
45
  end
38
46
  end
39
47
  end
@@ -69,7 +69,7 @@ module NNQ
69
69
  end
70
70
 
71
71
  return if conn.closed?
72
- conn.send_message(btrace + body)
72
+ conn.send_message(body, header: btrace)
73
73
  end
74
74
 
75
75
 
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async/limited_queue"
4
+ require_relative "backtrace"
5
+
6
+ module NNQ
7
+ module Routing
8
+ # Raw REP: bypasses the cooked state machine. The incoming
9
+ # backtrace header is split off once at parse time and handed to
10
+ # the caller alongside the live Connection as `[pipe, header, body]`.
11
+ # Replies go back via `send(body, to:, header:)` which writes the
12
+ # caller-supplied header verbatim — no cooked pending/echo logic,
13
+ # no single-in-flight constraint.
14
+ #
15
+ class RepRaw
16
+ include Backtrace
17
+
18
+
19
+ def initialize(engine)
20
+ @engine = engine
21
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
22
+ end
23
+
24
+
25
+ # @return [Array, nil] [pipe, header, body] or nil on close
26
+ def receive
27
+ @recv_queue.dequeue
28
+ end
29
+
30
+
31
+ # Sends +body+ with the caller-supplied +header+ back to +to+
32
+ # (a Connection handed out by a prior #receive). Silent drop
33
+ # if the target is closed or the header would push total hops
34
+ # over MAX_HOPS.
35
+ def send(body, to:, header:)
36
+ return if to.closed?
37
+ return if Backtrace.too_many_hops?(header)
38
+ to.send_message(body, header: header)
39
+ rescue ClosedError
40
+ # peer went away between receive and send — drop
41
+ end
42
+
43
+
44
+ # Called by the engine recv loop.
45
+ def enqueue(wire_bytes, conn)
46
+ header, payload = parse_backtrace(wire_bytes)
47
+ return unless header # malformed / over-TTL — drop
48
+ @recv_queue.enqueue([conn, header, payload])
49
+ end
50
+
51
+
52
+ def close
53
+ @recv_queue.enqueue(nil)
54
+ end
55
+
56
+
57
+ def close_read
58
+ @recv_queue.enqueue(nil)
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -55,7 +55,7 @@ module NNQ
55
55
 
56
56
  conn = pick_peer
57
57
  header = [id].pack("N")
58
- conn.send_message(header + body)
58
+ conn.send_message(body, header: header)
59
59
  promise.wait
60
60
  ensure
61
61
  @mutex.synchronize do
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async/limited_queue"
4
+ require_relative "backtrace"
5
+
6
+ module NNQ
7
+ module Routing
8
+ # Raw REQ: bypasses the cooked single-in-flight request-id
9
+ # state machine. Sends are fire-and-forget round-robin with a
10
+ # caller-supplied header (typically `[id | 0x80000000].pack("N")`);
11
+ # replies land in a bounded queue and are delivered as
12
+ # `[pipe, header, body]` tuples so the app can correlate by
13
+ # header verbatim without ever parsing or slicing bytes.
14
+ #
15
+ class ReqRaw
16
+ include Backtrace
17
+
18
+
19
+ def initialize(engine)
20
+ @engine = engine
21
+ @next_idx = 0
22
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
23
+ end
24
+
25
+
26
+ def send(body, header:)
27
+ conn = pick_peer
28
+ conn.send_message(body, header: header)
29
+ end
30
+
31
+
32
+ def receive
33
+ @recv_queue.dequeue
34
+ end
35
+
36
+
37
+ def enqueue(wire_bytes, conn)
38
+ header, payload = parse_backtrace(wire_bytes)
39
+ return unless header
40
+ @recv_queue.enqueue([conn, header, payload])
41
+ end
42
+
43
+
44
+ def close
45
+ @recv_queue.enqueue(nil)
46
+ end
47
+
48
+
49
+ def close_read
50
+ @recv_queue.enqueue(nil)
51
+ end
52
+
53
+
54
+ private
55
+
56
+
57
+ def pick_peer
58
+ loop do
59
+ conns = @engine.connections.keys
60
+
61
+ if conns.empty?
62
+ @engine.new_pipe.wait
63
+ next
64
+ end
65
+
66
+ @next_idx = (@next_idx + 1) % conns.size
67
+ return conns[@next_idx]
68
+ end
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -51,7 +51,7 @@ module NNQ
51
51
  end
52
52
 
53
53
  return if conn.closed?
54
- conn.send_message(btrace + body)
54
+ conn.send_message(body, header: btrace)
55
55
  end
56
56
 
57
57
 
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async/limited_queue"
4
+ require_relative "backtrace"
5
+
6
+ module NNQ
7
+ module Routing
8
+ # Raw RESPONDENT: mirror of {RepRaw} for the survey pattern.
9
+ # No survey-window state, no pending slot — the app receives
10
+ # `[pipe, header, body]` tuples and chooses whether (and when)
11
+ # to reply via `send(body, to:, header:)`.
12
+ #
13
+ class RespondentRaw
14
+ include Backtrace
15
+
16
+
17
+ def initialize(engine)
18
+ @engine = engine
19
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
20
+ end
21
+
22
+
23
+ def receive
24
+ @recv_queue.dequeue
25
+ end
26
+
27
+
28
+ def send(body, to:, header:)
29
+ return if to.closed?
30
+ return if Backtrace.too_many_hops?(header)
31
+ to.send_message(body, header: header)
32
+ rescue ClosedError
33
+ end
34
+
35
+
36
+ def enqueue(wire_bytes, conn)
37
+ header, payload = parse_backtrace(wire_bytes)
38
+ return unless header
39
+ @recv_queue.enqueue([conn, header, payload])
40
+ end
41
+
42
+
43
+ def close
44
+ @recv_queue.enqueue(nil)
45
+ end
46
+
47
+
48
+ def close_read
49
+ @recv_queue.enqueue(nil)
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "async/limited_queue"
5
+ require_relative "backtrace"
6
+
7
+ module NNQ
8
+ module Routing
9
+ # Raw SURVEYOR: fans out surveys to all peers like cooked
10
+ # {Surveyor}, but without a survey window, survey-id matching,
11
+ # or timeout. Replies are delivered as `[pipe, header, body]`
12
+ # tuples so the app can correlate by header verbatim.
13
+ #
14
+ # Each per-conn send queue holds `[header, body]` pairs and the
15
+ # pump calls `conn.write_message(body, header: header)` so the
16
+ # protocol-sp header kwarg is threaded through the fan-out —
17
+ # zero concat even on the broadcast path.
18
+ #
19
+ class SurveyorRaw
20
+ include Backtrace
21
+
22
+
23
+ def initialize(engine)
24
+ @engine = engine
25
+ @queues = {} # conn => Async::LimitedQueue
26
+ @pump_tasks = {} # conn => Async::Task
27
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
28
+ end
29
+
30
+
31
+ def send(body, header:)
32
+ @queues.each_value do |q|
33
+ q.enqueue([header, body]) unless q.limited?
34
+ end
35
+ end
36
+
37
+
38
+ def receive
39
+ @recv_queue.dequeue
40
+ end
41
+
42
+
43
+ def enqueue(wire_bytes, conn)
44
+ header, payload = parse_backtrace(wire_bytes)
45
+ return unless header
46
+ @recv_queue.enqueue([conn, header, payload])
47
+ end
48
+
49
+
50
+ def connection_added(conn)
51
+ queue = Async::LimitedQueue.new(@engine.options.send_hwm)
52
+ @queues[conn] = queue
53
+ @pump_tasks[conn] = spawn_pump(conn, queue)
54
+ end
55
+
56
+
57
+ def connection_removed(conn)
58
+ @queues.delete(conn)
59
+ task = @pump_tasks.delete(conn)
60
+
61
+ return unless task
62
+ return if task == Async::Task.current
63
+
64
+ task.stop
65
+ rescue IOError, Errno::EPIPE
66
+ end
67
+
68
+
69
+ def send_queue_drained?
70
+ @queues.each_value.all? { |q| q.empty? }
71
+ end
72
+
73
+
74
+ def close
75
+ @pump_tasks.each_value(&:stop)
76
+ @pump_tasks.clear
77
+ @queues.clear
78
+ @recv_queue.enqueue(nil)
79
+ end
80
+
81
+
82
+ def close_read
83
+ @recv_queue.enqueue(nil)
84
+ end
85
+
86
+
87
+ private
88
+
89
+
90
+ def spawn_pump(conn, queue)
91
+ annotation = "nnq surveyor_raw pump #{conn.endpoint}"
92
+ parent = @engine.connections[conn]&.barrier || @engine.barrier
93
+
94
+ @engine.spawn_task(annotation:, parent:) do
95
+ loop do
96
+ header, body = queue.dequeue
97
+ conn.send_message(body, header: header)
98
+ @engine.emit_verbose_msg_sent(body)
99
+ rescue EOFError, IOError, Errno::EPIPE, Errno::ECONNRESET
100
+ break
101
+ end
102
+ end
103
+ end
104
+
105
+ end
106
+ end
107
+ end
data/lib/nnq/socket.rb CHANGED
@@ -32,9 +32,9 @@ module NNQ
32
32
 
33
33
  # @yieldparam [self] the socket; when a block is passed the socket
34
34
  # is {#close}d when the block returns (or raises), File.open-style.
35
- def initialize(raw: false, linger: Float::INFINITY, send_hwm: Options::DEFAULT_HWM)
35
+ def initialize(raw: false, linger: Float::INFINITY, send_hwm: Options::DEFAULT_HWM, recv_hwm: Options::DEFAULT_HWM)
36
36
  @raw = raw
37
- @options = Options.new(linger: linger, send_hwm: send_hwm)
37
+ @options = Options.new(linger: linger, send_hwm: send_hwm, recv_hwm: recv_hwm)
38
38
  @engine = Engine.new(protocol: protocol, options: @options) { |engine| build_routing(engine) }
39
39
 
40
40
  begin
@@ -3,27 +3,37 @@
3
3
  require_relative "socket"
4
4
  require_relative "routing/surveyor"
5
5
  require_relative "routing/respondent"
6
+ require_relative "routing/surveyor_raw"
7
+ require_relative "routing/respondent_raw"
6
8
 
7
9
  module NNQ
8
10
  # SURVEYOR (nng surveyor0): broadcast side of the survey pattern.
9
- # Sends a survey to all connected respondents, then collects replies
10
- # within a timed window (`options.survey_time`, default 1s).
11
- #
12
- # Only one outstanding survey at a time — sending a new survey
13
- # abandons the previous one. Respondents are not obliged to reply.
11
+ # Cooked mode enforces a timed survey window and matches replies by
12
+ # survey id; raw mode fans out with a caller-supplied +header+ and
13
+ # delivers replies as `[pipe, header, body]` with no timer.
14
14
  #
15
15
  class SURVEYOR0 < Socket
16
- # Broadcasts +body+ as a survey to all connected respondents.
16
+ # Cooked: broadcasts +body+ as a survey to all connected respondents.
17
17
  def send_survey(body)
18
+ raise Error, "SURVEYOR#send_survey not available in raw mode" if raw?
18
19
  body = frozen_binary(body)
19
20
  Reactor.run { @engine.routing.send_survey(body) }
20
21
  end
21
22
 
22
23
 
23
- # Receives the next reply. Raises {NNQ::TimedOut} when the survey
24
- # window expires.
25
- #
26
- # @return [String] reply body
24
+ # Raw: broadcasts +body+ with +header+ to all connected respondents
25
+ # via per-conn send pumps (header is threaded through the
26
+ # protocol-sp header kwarg — no concat). Raises in cooked mode.
27
+ def send(body, header:)
28
+ raise Error, "SURVEYOR#send not available in cooked mode" unless raw?
29
+ body = frozen_binary(body)
30
+ Reactor.run { @engine.routing.send(body, header: header) }
31
+ end
32
+
33
+
34
+ # Cooked: receives the next reply within the survey window, raises
35
+ # {NNQ::TimedOut} on window expiry. Raw: returns `[pipe, header, body]`
36
+ # and blocks indefinitely (no survey window).
27
37
  def receive
28
38
  Reactor.run { @engine.routing.receive }
29
39
  end
@@ -38,31 +48,41 @@ module NNQ
38
48
 
39
49
 
40
50
  def build_routing(engine)
41
- Routing::Surveyor.new(engine)
51
+ raw? ? Routing::SurveyorRaw.new(engine) : Routing::Surveyor.new(engine)
42
52
  end
43
53
  end
44
54
 
45
55
 
46
56
  # RESPONDENT (nng respondent0): reply side of the survey pattern.
47
- # Receives surveys, processes them, and optionally sends replies.
48
- # Strict alternation: #receive then #send_reply.
57
+ # Cooked mode strictly alternates #receive / #send_reply; raw mode
58
+ # exposes the backtrace as an opaque +header+ and the originating
59
+ # surveyor pipe as a live Connection.
49
60
  #
50
61
  class RESPONDENT0 < Socket
51
- # Blocks until the next survey arrives.
52
- #
53
- # @return [String, nil] survey body, or nil if the socket was closed
62
+ # Cooked: blocks until the next survey arrives. Raw: returns
63
+ # `[pipe, header, body]`.
54
64
  def receive
55
65
  Reactor.run { @engine.routing.receive }
56
66
  end
57
67
 
58
68
 
59
- # Routes +body+ back to the surveyor that sent the most recent survey.
69
+ # Cooked: routes +body+ back to the surveyor that sent the most
70
+ # recent survey. Raises in raw mode.
60
71
  def send_reply(body)
72
+ raise Error, "RESPONDENT#send_reply not available in raw mode" if raw?
61
73
  body = frozen_binary(body)
62
74
  Reactor.run { @engine.routing.send_reply(body) }
63
75
  end
64
76
 
65
77
 
78
+ # Raw: writes +body+ with +header+ back to +to+. Raises in cooked mode.
79
+ def send(body, to:, header:)
80
+ raise Error, "RESPONDENT#send not available in cooked mode" unless raw?
81
+ body = frozen_binary(body)
82
+ Reactor.run { @engine.routing.send(body, to: to, header: header) }
83
+ end
84
+
85
+
66
86
  private
67
87
 
68
88
 
@@ -72,7 +92,7 @@ module NNQ
72
92
 
73
93
 
74
94
  def build_routing(engine)
75
- Routing::Respondent.new(engine)
95
+ raw? ? Routing::RespondentRaw.new(engine) : Routing::Respondent.new(engine)
76
96
  end
77
97
  end
78
98
  end
data/lib/nnq/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NNQ
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nnq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '0.1'
46
+ version: '0.3'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: '0.1'
53
+ version: '0.3'
54
54
  description: Pure Ruby implementation of nanomsg's Scalability Protocols (SP) on top
55
55
  of async + io-stream. Per-socket HWM, opportunistic batching, wire-compatible with
56
56
  libnng over inproc/ipc/tcp.
@@ -85,11 +85,15 @@ files:
85
85
  - lib/nnq/routing/pull.rb
86
86
  - lib/nnq/routing/push.rb
87
87
  - lib/nnq/routing/rep.rb
88
+ - lib/nnq/routing/rep_raw.rb
88
89
  - lib/nnq/routing/req.rb
90
+ - lib/nnq/routing/req_raw.rb
89
91
  - lib/nnq/routing/respondent.rb
92
+ - lib/nnq/routing/respondent_raw.rb
90
93
  - lib/nnq/routing/send_pump.rb
91
94
  - lib/nnq/routing/sub.rb
92
95
  - lib/nnq/routing/surveyor.rb
96
+ - lib/nnq/routing/surveyor_raw.rb
93
97
  - lib/nnq/socket.rb
94
98
  - lib/nnq/surveyor_respondent.rb
95
99
  - lib/nnq/transport/inproc.rb