nnq-cli 0.3.0 → 0.4.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: b3b69a427aa491a1d47cb1144cf96824469c9029658239250d0b98f9d69ae43a
4
- data.tar.gz: 43e68cf5d450fa777e79dd8913d9381eb0b436fe08d6e193c90cb47ecc42e036
3
+ metadata.gz: cfc34e5b7774c4c3288d440d5172391b098055fd2c376d3827e8a5944dd085d7
4
+ data.tar.gz: '0836095278d158e9b52234060cbfe111abcdc282a31659480ff2eb9db0bad5e8'
5
5
  SHA512:
6
- metadata.gz: afc90caf9d6ba1bdfe488af48991af91a73a95d88ce5d6fe156f0255c7faef85de7b6016bb9db36c41be94c504c9ac8562ebd28ba20544f280afa00a8fff2ee9
7
- data.tar.gz: 4fe278dcb3f5cdfc9171c6ebbb6de1cf2a30b49c01d1597b6c359cdb1d8903290be7ed068c7c7e2dab98001c9fac5207b544b5c6b1c611a18ebac1585baceedf
6
+ metadata.gz: d34031ee7de234d6f1f88305b8cbd7699932216bec86bb6f05191e6001608e899ff29160934cc9c9a34b9c8fc71c7bff2d74014904a85753bd63d947bf88f78b
7
+ data.tar.gz: 84a46b47822df0538eb9a875142b01893370c3222b22728ede2dfb1578354cd21775f22bc343daaded4bc60ba56f99af6a44dd0edb40f4181cab44040cc45fc7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 — 2026-04-16
4
+
5
+ - **`-P0` auto-sizes to `nproc`** (capped at 16). `nnq pipe -P 0`
6
+ now spawns one Ractor worker per online processor instead of
7
+ erroring. Explicit values still clamp to `1..16`.
8
+
9
+ ## 0.3.2 — 2026-04-16
10
+
11
+ - **REQ generator mode (`-E`/`-e` with no stdin).** `nnq req` now
12
+ produces each request from the send-eval alone when no `-d`/`-F`
13
+ is given and stdin is not piped, matching the existing PUSH/PUB
14
+ generator behaviour. Bounded by `-n` or paced by `-i` like the
15
+ other generator-capable runners.
16
+ - **System tests split into themed files under `test/system/`.**
17
+ Replaces the monolithic `test/system_test.sh` with 10 standalone
18
+ files sharing `test/system/support.sh` helpers.
19
+ `test/system/run_all.sh` chains them; each file also runs
20
+ standalone. New `rake test:system` task invokes `run_all.sh`.
21
+ - **System tests for REQ/PUSH/PUB `-E` generator mode.** REQ fires
22
+ `-E'"foo"' -n 3` against a REP running `-e 'it.upcase' -n 3` and
23
+ verifies three `FOO` replies round-trip; PUSH and PUB get the
24
+ same treatment.
25
+
26
+ ## 0.3.1 — 2026-04-15
27
+
28
+ - **Messages are single `String`s, not 1-element arrays.** Every
29
+ runner, the formatter, and the expression evaluator used to wrap
30
+ each message body in a one-element array as a historical
31
+ artifact of multipart thinking. That's gone now: `Formatter#pack`/
32
+ `#unpack` take and return a `String`, `Formatter.preview` takes a
33
+ `String`, and REQ/REP/SURVEYOR/RESPONDENT/pipe runners pass the
34
+ body straight through without `.first` or array literals. Fixes
35
+ an `undefined method 'first' for an instance of String` crash in
36
+ `ReqRunner#request_and_receive` that was masked by the tests.
37
+ - **`-e` / `-E` expressions use Ruby 3.4's `it` default block
38
+ variable** (and accept explicit `|msg|` block-parameter syntax
39
+ via `proc { |msg| ... }`). The old implicit `$_`/`$F` aliasing is
40
+ gone; expressions are compiled as plain `proc { EXPR }` and
41
+ evaluated via `instance_exec(msg, &block)`, so `it.upcase` and
42
+ `|msg| msg.upcase` both work. Cleaner, faster, and no global
43
+ state.
44
+ - **Requires nnq >= 0.6.1** for the `-vvv` trace fixes — cooked
45
+ REQ/REP/RESPONDENT now emit `>>` lines and recv previews strip
46
+ the SP backtrace header so you see the actual payload.
47
+ - **`test/system_test.sh`** — new shell-based system test suite
48
+ modeled on omq-cli's, covering REQ/REP (basic, `--echo`, `-e`),
49
+ the ported `-vvv` REQ/REP verbose-trace assertion from omq-cli
50
+ commit `950890911de078`, PUSH/PULL, PUB/SUB, and the `@name`
51
+ abstract-namespace shortcut for both `-b` and `-c`.
52
+
3
53
  ## 0.3.0 — 2026-04-15
4
54
 
5
55
  - **Compression switched from LZ4 to Zstd** via the new
@@ -5,14 +5,15 @@ module NNQ
5
5
  # Template runner base class for all socket-type CLI runners.
6
6
  # Subclasses override {#run_loop} to implement socket-specific behaviour.
7
7
  #
8
- # nnq carries one String body per message (no multipart). The runner
9
- # protocol wraps it in a 1-element Array internally so that eval
10
- # expressions using `$F`/`$_` work the same way as in omq-cli, and
11
- # unwraps to a bare String at the send boundary.
8
+ # nnq carries one String body per message (no multipart).
9
+ #
12
10
  class BaseRunner
13
11
  # @return [Config] frozen CLI configuration
12
+ attr_reader :config
13
+
14
+
14
15
  # @return [Object] the NNQ socket instance
15
- attr_reader :config, :sock
16
+ attr_reader :sock
16
17
 
17
18
 
18
19
  # @param config [Config] frozen CLI configuration
@@ -145,6 +146,7 @@ module NNQ
145
146
  # latecomers finish their handshake before we start sending.
146
147
  def apply_grace_period
147
148
  return unless config.binds.any? || config.connects.size > 1
149
+
148
150
  ri = @sock.options.reconnect_interval
149
151
  sleep(ri.is_a?(Range) ? ri.begin : ri)
150
152
  end
@@ -163,7 +165,9 @@ module NNQ
163
165
 
164
166
  def run_send_logic
165
167
  n = config.count
166
- sleep(config.delay) if config.delay
168
+
169
+ sleep config.delay if config.delay
170
+
167
171
  if config.interval
168
172
  run_interval_send(n)
169
173
  elsif config.data || config.file
@@ -189,21 +193,30 @@ module NNQ
189
193
 
190
194
  def run_interval_send(n)
191
195
  i = send_tick
192
- return if @send_tick_eof || (n && n > 0 && i >= n)
196
+
197
+ if @send_tick_eof || (n && n > 0 && i >= n)
198
+ return
199
+ end
200
+
193
201
  Async::Loop.quantized(interval: config.interval) do
194
202
  i += send_tick
195
- break if @send_tick_eof || (n && n > 0 && i >= n)
203
+
204
+ if @send_tick_eof || (n && n > 0 && i >= n)
205
+ break
206
+ end
196
207
  end
197
208
  end
198
209
 
199
210
 
200
211
  def run_stdin_send(n)
201
212
  i = 0
213
+
202
214
  loop do
203
- msg = read_next
204
- break unless msg
215
+ msg = read_next or break
205
216
  msg = eval_send_expr(msg)
217
+
206
218
  send_msg(msg) if msg
219
+
207
220
  i += 1
208
221
  break if n && n > 0 && i >= n
209
222
  end
@@ -212,6 +225,7 @@ module NNQ
212
225
 
213
226
  def send_tick
214
227
  raw = read_next_or_nil
228
+
215
229
  if raw.nil?
216
230
  if @send_eval_proc && !@stdin_ready
217
231
  # Pure generator mode: no stdin, eval produces output from nothing.
@@ -219,9 +233,11 @@ module NNQ
219
233
  send_msg(msg) if msg
220
234
  return 1
221
235
  end
236
+
222
237
  @send_tick_eof = true
223
238
  return 0
224
239
  end
240
+
225
241
  msg = eval_send_expr(raw)
226
242
  send_msg(msg) if msg
227
243
  1
@@ -231,14 +247,16 @@ module NNQ
231
247
  def run_recv_logic
232
248
  n = config.count
233
249
  i = 0
250
+
234
251
  if config.interval
235
252
  run_interval_recv(n)
236
253
  else
237
254
  loop do
238
- msg = recv_msg
239
- break if msg.nil?
255
+ msg = recv_msg or break
240
256
  msg = eval_recv_expr(msg)
257
+
241
258
  output(msg)
259
+
242
260
  i += 1
243
261
  break if n && n > 0 && i >= n
244
262
  end
@@ -248,21 +266,28 @@ module NNQ
248
266
 
249
267
  def run_interval_recv(n)
250
268
  i = recv_tick
269
+
251
270
  return if i == 0
252
271
  return if n && n > 0 && i >= n
272
+
253
273
  Async::Loop.quantized(interval: config.interval) do
254
274
  i += recv_tick
255
- break if @recv_tick_eof || (n && n > 0 && i >= n)
275
+
276
+ if @recv_tick_eof || (n && n > 0 && i >= n)
277
+ break
278
+ end
256
279
  end
257
280
  end
258
281
 
259
282
 
260
283
  def recv_tick
261
284
  msg = recv_msg
285
+
262
286
  if msg.nil?
263
287
  @recv_tick_eof = true
264
288
  return 0
265
289
  end
290
+
266
291
  msg = eval_recv_expr(msg)
267
292
  output(msg)
268
293
  1
@@ -286,22 +311,32 @@ module NNQ
286
311
  # -- Message I/O -------------------------------------------------
287
312
 
288
313
 
289
- # msg: 1-element Array. nnq sockets take a bare String body.
314
+ # @param msg [String]
315
+ #
290
316
  def send_msg(msg)
291
- return if msg.empty?
292
- body = config.format == :marshal ? Marshal.dump(msg.first) : msg.first
317
+ body = msg
318
+
319
+ case config.format
320
+ when :marshal
321
+ body = Marshal.dump(msg)
322
+ end
323
+
293
324
  @sock.send(body)
294
325
  transient_ready!
295
326
  end
296
327
 
297
328
 
298
- # @return [Array<String>, nil] 1-element Array (body), or nil on close.
329
+ # @return [String, nil] message body, or nil on close.
299
330
  def recv_msg
300
- raw = @sock.receive
301
- return nil if raw.nil?
302
- body = config.format == :marshal ? Marshal.load(raw) : raw
331
+ msg = @sock.receive or return
332
+
333
+ case config.format
334
+ when :marshal
335
+ msg = Marshal.load msg
336
+ end
337
+
303
338
  transient_ready!
304
- [body]
339
+ msg
305
340
  end
306
341
 
307
342
 
@@ -328,7 +363,7 @@ module NNQ
328
363
  @fmt.decode_marshal($stdin)
329
364
  when :raw
330
365
  data = $stdin.read
331
- data.nil? || data.empty? ? nil : [data]
366
+ data.nil? || data.empty? ? nil : data
332
367
  else
333
368
  line = $stdin.gets
334
369
  line.nil? ? nil : @fmt.decode(line)
@@ -359,6 +394,7 @@ module NNQ
359
394
 
360
395
  def output(msg)
361
396
  return if config.quiet || msg.nil?
397
+
362
398
  $stdout.write(@fmt.encode(msg))
363
399
  $stdout.flush
364
400
  end
@@ -416,9 +452,11 @@ module NNQ
416
452
  title = ["nnq", config.type_name]
417
453
  title << (config.compress == :balanced ? "-Z" : "-z") if config.compress
418
454
  title << "-P#{config.parallel}" if config.parallel
455
+
419
456
  eps.each do |ep|
420
457
  title << (ep.respond_to?(:url) ? ep.url : ep.to_s)
421
458
  end
459
+
422
460
  Process.setproctitle(title.join(" "))
423
461
  end
424
462
 
@@ -428,6 +466,7 @@ module NNQ
428
466
 
429
467
  def log(msg)
430
468
  return unless config.verbose >= 1
469
+
431
470
  $stderr.write("#{Term.log_prefix(config.verbose)}nnq: #{msg}\n")
432
471
  end
433
472
 
@@ -437,11 +476,13 @@ module NNQ
437
476
  # -vvvv: prepend ISO8601 timestamps
438
477
  def start_event_monitor
439
478
  verbose = config.verbose >= 3
440
- v = config.verbose
479
+ v = config.verbose
480
+
441
481
  @sock.monitor(verbose: verbose) do |event|
442
482
  CLI::Term.write_event(event, v)
443
483
  end
444
484
  end
485
+
445
486
  end
446
487
  end
447
488
  end
@@ -10,13 +10,13 @@ module NNQ
10
10
  EXAMPLES = <<~'TEXT'
11
11
  -- Request / Reply ------------------------------------------
12
12
 
13
- +-----+ "hello" +-----+
13
+ +-----+ "hello" +-----+
14
14
  | REQ |------------->| REP |
15
15
  | |<-------------| |
16
- +-----+ "HELLO" +-----+
16
+ +-----+ "HELLO" +-----+
17
17
 
18
18
  # terminal 1: echo server
19
- nnq rep --bind tcp://:5555 --recv-eval '$_.upcase'
19
+ nnq rep --bind tcp://:5555 --recv-eval 'it.upcase'
20
20
 
21
21
  # terminal 2: send a request
22
22
  echo "hello" | nnq req --connect tcp://localhost:5555
@@ -27,9 +27,9 @@ module NNQ
27
27
 
28
28
  -- Publish / Subscribe --------------------------------------
29
29
 
30
- +-----+ "weather.nyc 72F" +-----+
30
+ +-----+ "weather.nyc 72F" +-----+
31
31
  | PUB |--------------------->| SUB | --subscribe "weather."
32
- +-----+ +-----+
32
+ +-----+ +-----+
33
33
 
34
34
  # terminal 1: subscriber (all topics by default)
35
35
  nnq sub --bind tcp://:5556
@@ -39,9 +39,9 @@ module NNQ
39
39
 
40
40
  -- Periodic Publish -------------------------------------------
41
41
 
42
- +-----+ "tick 1" +-----+
42
+ +-----+ "tick 1" +-----+
43
43
  | PUB |--(every 1s)-->| SUB |
44
- +-----+ +-----+
44
+ +-----+ +-----+
45
45
 
46
46
  # terminal 1: subscriber
47
47
  nnq sub --bind tcp://:5556
@@ -56,9 +56,9 @@ module NNQ
56
56
 
57
57
  -- Pipeline -------------------------------------------------
58
58
 
59
- +------+ +------+
59
+ +------+ +------+
60
60
  | PUSH |----------->| PULL |
61
- +------+ +------+
61
+ +------+ +------+
62
62
 
63
63
  # terminal 1: worker
64
64
  nnq pull --bind tcp://:5557
@@ -72,30 +72,30 @@ module NNQ
72
72
 
73
73
  -- Pipe (PULL -> eval -> PUSH) --------------------------------
74
74
 
75
- +------+ +------+ +------+
75
+ +------+ +------+ +------+
76
76
  | PUSH |--------->| pipe |--------->| PULL |
77
- +------+ +------+ +------+
77
+ +------+ +------+ +------+
78
78
 
79
79
  # terminal 1: producer
80
80
  echo -e "hello\nworld" | nnq push --bind ipc://@work
81
81
 
82
82
  # terminal 2: worker -- uppercase each message
83
- nnq pipe -c ipc://@work -c ipc://@sink -e '$_.upcase'
83
+ nnq pipe -c ipc://@work -c ipc://@sink -e 'it.upcase'
84
84
  # terminal 3: collector
85
85
  nnq pull --bind ipc://@sink
86
86
 
87
87
  # 4 Ractor workers in a single process (-P)
88
- nnq pipe -c ipc://@work -c ipc://@sink -P4 -r./fib -e 'fib(Integer($_)).to_s'
88
+ nnq pipe -c ipc://@work -c ipc://@sink -P4 -r./fib -e 'fib(Integer(it)).to_s'
89
89
 
90
90
  # exit when producer disconnects (--transient)
91
- nnq pipe -c ipc://@work -c ipc://@sink --transient -e '$_.upcase'
91
+ nnq pipe -c ipc://@work -c ipc://@sink --transient -e 'it.upcase'
92
92
 
93
93
  # fan-in: multiple sources -> one sink
94
94
  nnq pipe --in -c ipc://@work1 -c ipc://@work2 \
95
- --out -c ipc://@sink -e '$_.upcase'
95
+ --out -c ipc://@sink -e 'it.upcase'
96
96
 
97
97
  # fan-out: one source -> multiple sinks (round-robin)
98
- nnq pipe --in -b tcp://:5555 --out -c ipc://@sink1 -c ipc://@sink2 -e '$_'
98
+ nnq pipe --in -b tcp://:5555 --out -c ipc://@sink1 -c ipc://@sink2 -e 'it'
99
99
 
100
100
  -- Formats --------------------------------------------------
101
101
 
@@ -105,10 +105,6 @@ module NNQ
105
105
  # quoted -- lossless, round-trippable (uses String#dump escaping)
106
106
  nnq pull --bind tcp://:5557 --quoted
107
107
 
108
- # JSON Lines -- single-element arrays (nnq is single-body)
109
- echo '["payload"]' | nnq push --connect tcp://localhost:5557 --jsonl
110
- nnq pull --bind tcp://:5557 --jsonl
111
-
112
108
  # raw -- emit the body verbatim (no framing, no newline)
113
109
  nnq pull --bind tcp://:5557 --raw
114
110
 
@@ -121,25 +117,25 @@ module NNQ
121
117
  -- Ruby Eval ------------------------------------------------
122
118
 
123
119
  # filter incoming: only pass messages containing "error"
124
- nnq pull -b tcp://:5557 --recv-eval '$_.include?("error") ? $_ : nil'
120
+ nnq pull -b tcp://:5557 --recv-eval 'it.include?("error") ? it : nil'
125
121
 
126
122
  # transform incoming with gems
127
- nnq sub -c tcp://localhost:5556 -rjson -e 'JSON.parse($_)["temperature"]'
123
+ nnq sub -c tcp://localhost:5556 -rjson -e 'JSON.parse(it)["temperature"]'
128
124
 
129
125
  # require a local file, use its methods
130
- nnq rep --bind tcp://:5555 --require ./transform.rb -e 'upcase($_)'
126
+ nnq rep --bind tcp://:5555 --require ./transform.rb -e 'transform(it)'
131
127
 
132
- # next skips, break stops -- regexps match against $_
133
- nnq pull -b tcp://:5557 -e 'next if /^#/; break if /quit/; $_'
128
+ # next skips, break stops
129
+ nnq pull -b tcp://:5557 -e 'next if /^#/; break if it =~ /quit/; it'
134
130
 
135
131
  # BEGIN/END blocks (like awk) -- accumulate and summarize
136
- nnq pull -b tcp://:5557 -e 'BEGIN{@sum = 0} @sum += Integer($_); nil END{puts @sum}'
132
+ nnq pull -b tcp://:5557 -e 'BEGIN{@sum = 0} @sum += Integer(it); nil END{puts @sum}'
137
133
 
138
- # transform outgoing messages
139
- echo hello | nnq push -c tcp://localhost:5557 --send-eval '$_.upcase'
134
+ # transform outgoing messages (with explicit block variable msg)
135
+ echo hello | nnq push -c tcp://localhost:5557 --send-eval '|msg| msg.upcase'
140
136
 
141
137
  # REQ: transform request and reply independently
142
- echo hello | nnq req -c tcp://localhost:5555 -E '$_.upcase' -e '$_'
138
+ echo hello | nnq req -c tcp://localhost:5555 -E 'it.upcase' -e 'it'
143
139
 
144
140
  -- Script Handlers (-r) ------------------------------------
145
141
 
@@ -150,7 +146,7 @@ module NNQ
150
146
  nnq pull --bind tcp://:5557 -r./handler.rb
151
147
 
152
148
  # combine script handlers with inline eval
153
- nnq req -c tcp://localhost:5555 -r./handler.rb -E '$_.upcase'
149
+ nnq req -c tcp://localhost:5555 -r./handler.rb -E 'it.upcase'
154
150
 
155
151
  # NNQ.outgoing { |msg| ... } -- registered outgoing transform
156
152
  # NNQ.incoming { |msg| ... } -- registered incoming transform
@@ -268,7 +264,6 @@ module NNQ
268
264
  o.on("-A", "--ascii", "Safe ASCII, non-printable as dots (default)") { opts[:format] = :ascii }
269
265
  o.on("-Q", "--quoted", "C-style quoted with escapes") { opts[:format] = :quoted }
270
266
  o.on( "--raw", "Raw binary body, no framing, no newline") { opts[:format] = :raw }
271
- o.on("-J", "--jsonl", "JSON Lines (single-element array per line)") { opts[:format] = :jsonl }
272
267
  o.on( "--msgpack", "MessagePack (binary stream)") { require "msgpack"; opts[:format] = :msgpack }
273
268
  o.on("-M", "--marshal", "Ruby Marshal stream (binary)") { opts[:format] = :marshal }
274
269
 
@@ -316,14 +311,16 @@ module NNQ
316
311
  end
317
312
 
318
313
  o.separator "\nProcessing (-e = incoming, -E = outgoing):"
319
- o.on("-e", "--recv-eval EXPR", "Eval Ruby for each incoming message ($_ = body)") { |v| opts[:recv_expr] = v }
320
- o.on("-E", "--send-eval EXPR", "Eval Ruby for each outgoing message ($_ = body)") { |v| opts[:send_expr] = v }
314
+ o.on("-e", "--recv-eval EXPR", "Eval Ruby for each incoming message (it = msg)") { |v| opts[:recv_expr] = v }
315
+ o.on("-E", "--send-eval EXPR", "Eval Ruby for each outgoing message (it = msg)") { |v| opts[:send_expr] = v }
321
316
  o.on("-r", "--require LIB", "Require lib/file in Async context; use '-' for stdin. Scripts can register NNQ.outgoing/incoming") { |v|
322
317
  require "nnq" unless defined?(NNQ::VERSION)
323
318
  opts[:scripts] << (v == "-" ? :stdin : (v.start_with?("./", "../") ? File.expand_path(v) : v))
324
319
  }
325
- o.on("-P", "--parallel N", Integer, "Parallel Ractor workers (max 16)") { |v|
326
- opts[:parallel] = [v, 16].min
320
+ o.on("-P", "--parallel N", Integer, "Parallel Ractor workers, 1..16 (0 = nproc, capped at 16)") { |v|
321
+ require "etc"
322
+ resolved = v.zero? ? Etc.nprocessors : v
323
+ opts[:parallel] = [resolved, 16].min
327
324
  }
328
325
 
329
326
  o.separator "\nOther:"
@@ -8,8 +8,10 @@ module NNQ
8
8
  #
9
9
  # One instance per direction (send or recv).
10
10
  #
11
- # nnq has no multipart, so `$F` is always a 1-element array and `$_`
12
- # is the body string.
11
+ # The expression sees the message body via the default block
12
+ # variable `it`, or callers can declare an explicit parameter with
13
+ # block-literal syntax: `-e '|msg| msg.upcase'` compiles to
14
+ # `proc { |msg| msg.upcase }`.
13
15
  #
14
16
  class ExpressionEvaluator
15
17
  attr_reader :begin_proc, :end_proc, :eval_proc
@@ -28,34 +30,30 @@ module NNQ
28
30
 
29
31
  if src
30
32
  expr, begin_body, end_body = extract_blocks(src)
31
- @begin_proc = eval("proc { #{begin_body} }") if begin_body # rubocop:disable Security/Eval
32
- @end_proc = eval("proc { #{end_body} }") if end_body # rubocop:disable Security/Eval
33
+ @begin_proc = eval("proc { #{begin_body} }") if begin_body
34
+ @end_proc = eval("proc { #{end_body} }") if end_body
35
+
33
36
  if expr && !expr.strip.empty?
34
- @eval_proc = eval("proc { $_ = $F&.first; #{expr} }") # rubocop:disable Security/Eval
37
+ @eval_proc = eval("proc { #{expr} }")
35
38
  end
36
39
  elsif fallback_proc
37
- @eval_proc = proc { |msg|
38
- body = msg&.first
39
- $_ = body
40
- fallback_proc.call(body)
41
- }
40
+ @eval_proc = proc { |msg| fallback_proc.call(msg) }
42
41
  end
43
42
  end
44
43
 
45
44
 
46
45
  # Runs the eval proc against +msg+ using +context+ as self.
47
- # Returns the normalised result Array, nil (filter/skip), or SENT.
46
+ # Returns the normalised result (as String), nil (filter/skip), or SENT.
47
+ #
48
48
  def call(msg, context)
49
49
  return msg unless @eval_proc
50
50
 
51
- $F = msg
52
51
  result = context.instance_exec(msg, &@eval_proc)
53
52
  return nil if result.nil?
54
53
  return SENT if result.equal?(context)
55
- return [result] if @format == :marshal
54
+ return result if @format == :marshal
56
55
 
57
- result = result.is_a?(Array) ? result.first(1) : [result]
58
- result.map!(&:to_s)
56
+ result.to_s
59
57
  rescue => e
60
58
  $stderr.puts "nnq: eval error: #{e.message} (#{e.class})"
61
59
  exit 3
@@ -66,8 +64,7 @@ module NNQ
66
64
  # Used inside Ractor worker blocks.
67
65
  def self.normalize_result(result)
68
66
  return nil if result.nil?
69
- result = result.is_a?(Array) ? result.first(1) : [result]
70
- result.map!(&:to_s)
67
+ result.to_s
71
68
  end
72
69
 
73
70
 
@@ -77,52 +74,34 @@ module NNQ
77
74
  def self.compile_inside_ractor(src)
78
75
  return [nil, nil, nil] unless src
79
76
 
80
- extract = ->(expr, kw) {
81
- s = expr.index(/#{kw}\s*\{/)
82
- return [expr, nil] unless s
83
- ci = expr.index("{", s)
84
- depth = 1
85
- j = ci + 1
86
- while j < expr.length && depth > 0
87
- depth += 1 if expr[j] == "{"
88
- depth -= 1 if expr[j] == "}"
89
- j += 1
90
- end
91
- [expr[0...s] + expr[j..], expr[(ci + 1)..(j - 2)]]
92
- }
93
-
94
- expr, begin_body = extract.(src, "BEGIN")
95
- expr, end_body = extract.(expr, "END")
77
+ expr, begin_body = extract_block(src, "BEGIN")
78
+ expr, end_body = extract_block(expr, "END")
96
79
 
97
- begin_proc = eval("proc { #{begin_body} }") if begin_body # rubocop:disable Security/Eval
98
- end_proc = eval("proc { #{end_body} }") if end_body # rubocop:disable Security/Eval
80
+ begin_proc = eval("proc { #{begin_body} }") if begin_body
81
+ end_proc = eval("proc { #{end_body} }") if end_body
99
82
  eval_proc = nil
83
+
100
84
  if expr && !expr.strip.empty?
101
- ractor_expr = expr.gsub(/\$F\b/, "__F")
102
- eval_proc = eval("proc { |__F| $_ = __F&.first; #{ractor_expr} }") # rubocop:disable Security/Eval
85
+ eval_proc = eval("proc { #{expr} }")
103
86
  end
104
87
 
105
88
  [begin_proc, end_proc, eval_proc]
106
89
  end
107
90
 
108
91
 
109
- private
110
-
111
-
112
- def extract_blocks(expr)
113
- expr, begin_body = extract_block(expr, "BEGIN")
114
- expr, end_body = extract_block(expr, "END")
115
- [expr, begin_body, end_body]
116
- end
117
-
118
-
119
- def extract_block(expr, keyword)
120
- start = expr.index(/#{keyword}\s*\{/)
121
- return [expr, nil] unless start
92
+ # Strips a +BEGIN {...}+ or +END {...}+ block from +expr+ and
93
+ # returns +[trimmed_expr, block_body_or_nil]+. Brace-matched scan,
94
+ # so nested `{}` inside the block body are handled. Shared by
95
+ # instance and Ractor compile paths, so must be a class method
96
+ # (Ractors cannot call back into instance state).
97
+ #
98
+ def self.extract_block(expr, keyword)
99
+ start = expr.index(/#{keyword}\s*\{/) or return [expr, nil]
122
100
 
123
101
  i = expr.index("{", start)
124
102
  depth = 1
125
103
  j = i + 1
104
+
126
105
  while j < expr.length && depth > 0
127
106
  case expr[j]
128
107
  when "{"
@@ -130,6 +109,7 @@ module NNQ
130
109
  when "}"
131
110
  depth -= 1
132
111
  end
112
+
133
113
  j += 1
134
114
  end
135
115
 
@@ -137,6 +117,17 @@ module NNQ
137
117
  trimmed = expr[0...start] + expr[j..]
138
118
  [trimmed, body]
139
119
  end
120
+
121
+
122
+ private
123
+
124
+
125
+ def extract_blocks(expr)
126
+ expr, begin_body = self.class.extract_block(expr, "BEGIN")
127
+ expr, end_body = self.class.extract_block(expr, "END")
128
+ [expr, begin_body, end_body]
129
+ end
130
+
140
131
  end
141
132
  end
142
133
  end
@@ -7,11 +7,10 @@ module NNQ
7
7
  # the socket, not by the formatter.
8
8
  #
9
9
  # Unlike omq-cli's Formatter, nnq messages are not multipart — one
10
- # `String` body per message. The API still accepts/returns a
11
- # 1-element array so that `$F`-based eval expressions work the same
12
- # way.
10
+ # `String` body per message. The API takes and returns a plain
11
+ # `String`.
13
12
  class Formatter
14
- # @param format [Symbol] wire format (:ascii, :quoted, :raw, :jsonl, :msgpack, :marshal)
13
+ # @param format [Symbol] wire format (:ascii, :quoted, :raw, :msgpack, :marshal)
15
14
  def initialize(format)
16
15
  @format = format
17
16
  end
@@ -19,45 +18,38 @@ module NNQ
19
18
 
20
19
  # Encodes a message body into a printable string for output.
21
20
  #
22
- # @param msg [Array<String>] single-element array (the body)
21
+ # @param msg [String] message body
23
22
  # @return [String] formatted output line
23
+ #
24
24
  def encode(msg)
25
- body = msg.first.to_s
26
25
  case @format
27
26
  when :ascii
28
- body.b.gsub(/[^[:print:]\t]/, ".") + "\n"
27
+ msg.b.gsub(/[^[:print:]\t]/, ".") << "\n"
29
28
  when :quoted
30
- body.b.dump[1..-2] + "\n"
29
+ msg.b.dump[1..-2] << "\n"
31
30
  when :raw
32
- body
33
- when :jsonl
34
- JSON.generate([body]) + "\n"
31
+ msg # FIXME: are these really the wire bytes?
35
32
  when :msgpack
36
- MessagePack.pack([body])
33
+ MessagePack.pack(msg)
37
34
  when :marshal
38
- body.inspect + "\n"
35
+ msg.inspect << "\n"
39
36
  end
40
37
  end
41
38
 
42
39
 
43
- # Decodes a formatted input line into a 1-element message array.
40
+ # Decodes a formatted input line into a message body.
44
41
  #
45
42
  # @param line [String] input line (newline-terminated)
46
- # @return [Array<String>] 1-element array
43
+ # @return [String] message
44
+ #
47
45
  def decode(line)
48
46
  case @format
49
47
  when :ascii, :marshal
50
- [line.chomp]
48
+ line.chomp
51
49
  when :quoted
52
- ["\"#{line.chomp}\"".undump]
50
+ "\"#{line.chomp}\"".undump
53
51
  when :raw
54
- [line]
55
- when :jsonl
56
- arr = JSON.parse(line.chomp)
57
- unless arr.is_a?(Array) && arr.all? { |e| e.is_a?(String) }
58
- abort "JSON Lines input must be an array of strings"
59
- end
60
- arr.first(1)
52
+ line
61
53
  end
62
54
  end
63
55
 
@@ -87,10 +79,10 @@ module NNQ
87
79
 
88
80
  # Formats a message body for human-readable preview (logging).
89
81
  #
90
- # @param msg [Array<String>] single-element array
82
+ # @param body [String] message body
91
83
  # @return [String] truncated preview
92
- def self.preview(msg)
93
- body = msg.first.to_s
84
+ def self.preview(body)
85
+ body = body.to_s
94
86
  "(#{body.bytesize}B) #{preview_body(body)}"
95
87
  end
96
88
 
data/lib/nnq/cli/pipe.rb CHANGED
@@ -105,11 +105,9 @@ module NNQ
105
105
  loop do
106
106
  body = @pull.receive
107
107
  break if body.nil?
108
- msg = eval_recv_expr([body])
108
+ msg = eval_recv_expr(body)
109
109
 
110
- if msg && !msg.empty?
111
- @push.send(msg.first)
112
- end
110
+ @push.send(msg) if msg
113
111
 
114
112
  # Yield after send so send-pump fibers can drain the queue
115
113
  # before the next message is enqueued. Without this, one pump
@@ -65,9 +65,9 @@ module NNQ
65
65
  def format_event(event)
66
66
  case event.type
67
67
  when :message_sent
68
- "nnq: >> #{NNQ::CLI::Formatter.preview([event.detail[:body]])}"
68
+ "nnq: >> #{NNQ::CLI::Formatter.preview(event.detail[:body])}"
69
69
  when :message_received
70
- "nnq: << #{NNQ::CLI::Formatter.preview([event.detail[:body]])}"
70
+ "nnq: << #{NNQ::CLI::Formatter.preview(event.detail[:body])}"
71
71
  else
72
72
  ep = event.endpoint ? " #{event.endpoint}" : ""
73
73
  detail = event.detail ? " #{event.detail}" : ""
@@ -100,11 +100,9 @@ module NNQ
100
100
  body = @pull.receive
101
101
  break if body.nil?
102
102
  msg = NNQ::CLI::ExpressionEvaluator.normalize_result(
103
- @ctx.instance_exec([body], &@eval_proc)
103
+ @ctx.instance_exec(body, &@eval_proc)
104
104
  )
105
- unless msg.nil? || msg.empty?
106
- @push.send(msg.first)
107
- end
105
+ @push.send(msg) if msg
108
106
  n -= 1 if n && n > 0
109
107
  break if n == 0
110
108
  end
@@ -127,9 +125,7 @@ module NNQ
127
125
  out = NNQ::CLI::ExpressionEvaluator.normalize_result(
128
126
  @ctx.instance_exec(&@end_proc)
129
127
  )
130
- if out && !out.empty?
131
- @push.send(out.first)
132
- end
128
+ @push.send(out) if out
133
129
  end
134
130
  end
135
131
  end
@@ -13,31 +13,39 @@ module NNQ
13
13
  def run_loop(task)
14
14
  n = config.count
15
15
  i = 0
16
- sleep(config.delay) if config.delay
16
+
17
+ sleep config.delay if config.delay
18
+ generator = @send_eval_proc && !config.data && !config.file && !stdin_ready?
19
+
17
20
  loop do
18
- msg = read_next
19
- break unless msg
20
- msg = eval_send_expr(msg)
21
+ if generator
22
+ msg = eval_send_expr(nil)
23
+ else
24
+ msg = read_next
25
+ break unless msg
26
+ msg = eval_send_expr(msg)
27
+ end
28
+
21
29
  next unless msg
30
+
22
31
  reply = request_and_receive(msg)
23
32
  break if reply.nil?
24
33
  output(eval_recv_expr(reply))
25
34
  i += 1
26
35
  break if n && n > 0 && i >= n
27
- break if !config.interval && (config.data || config.file)
36
+ break if !config.interval && (generator || config.data || config.file) && !(n && n > 0)
28
37
  wait_for_interval if config.interval
29
38
  end
30
39
  end
31
40
 
32
41
 
33
42
  def request_and_receive(msg)
34
- return nil if msg.empty?
35
- body = config.format == :marshal ? Marshal.dump(msg.first) : msg.first
43
+ return nil if msg.nil? || msg.empty?
44
+ body = config.format == :marshal ? Marshal.dump(msg) : msg
36
45
  reply_body = @sock.send_request(body)
37
46
  transient_ready!
38
47
  return nil if reply_body.nil?
39
- reply = config.format == :marshal ? Marshal.load(reply_body) : reply_body
40
- [reply]
48
+ config.format == :marshal ? Marshal.load(reply_body) : reply_body
41
49
  end
42
50
 
43
51
 
@@ -74,7 +82,7 @@ module NNQ
74
82
  reply = eval_recv_expr(msg)
75
83
  unless reply.equal?(SENT)
76
84
  output(reply)
77
- send_reply(reply || [""])
85
+ send_reply(reply || "")
78
86
  end
79
87
  elsif config.echo
80
88
  output(msg)
@@ -92,8 +100,8 @@ module NNQ
92
100
 
93
101
 
94
102
  def send_reply(msg)
95
- return if msg.empty?
96
- body = config.format == :marshal ? Marshal.dump(msg.first) : msg.first
103
+ return if msg.nil?
104
+ body = config.format == :marshal ? Marshal.dump(msg) : msg
97
105
  @sock.send_reply(body)
98
106
  transient_ready!
99
107
  end
@@ -10,12 +10,14 @@ module NNQ
10
10
  # (one batch exactly fills a full queue).
11
11
  DEFAULT_HWM = 64
12
12
 
13
+
13
14
  # Default max inbound message size (1 MiB) so a misconfigured or
14
15
  # malicious peer can't force arbitrary memory allocation on a
15
16
  # terminal user. Users can raise it with --recv-maxsz N, or
16
17
  # disable it entirely with --recv-maxsz 0.
17
18
  DEFAULT_RECV_MAXSZ = 1 << 20
18
19
 
20
+
19
21
  # Apply post-construction socket options from +config+ to +sock+.
20
22
  # send_hwm and linger are construction-time kwargs (see {.build});
21
23
  # the rest of the options are set here and read later by the
@@ -24,12 +26,15 @@ module NNQ
24
26
  sock.options.read_timeout = config.timeout if config.timeout
25
27
  sock.options.write_timeout = config.timeout if config.timeout
26
28
  sock.options.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
27
- sock.options.max_message_size =
28
- case config.recv_maxsz
29
- when nil then DEFAULT_RECV_MAXSZ
30
- when 0 then nil
31
- else config.recv_maxsz
32
- end
29
+
30
+ case config.recv_maxsz
31
+ when nil
32
+ sock.options.max_message_size = DEFAULT_RECV_MAXSZ
33
+ when 0
34
+ sock.options.max_message_size = nil
35
+ else
36
+ config.recv_maxsz
37
+ end
33
38
  end
34
39
 
35
40
 
@@ -38,10 +43,10 @@ module NNQ
38
43
  # (send_hwm is captured during routing init and can't be changed
39
44
  # after the fact), so we pass them there.
40
45
  def self.build(klass, config)
41
- sock = klass.new(
42
- linger: config.linger,
43
- send_hwm: config.send_hwm || DEFAULT_HWM,
44
- )
46
+ linger = config.linger
47
+ send_hwm = config.send_hwm || DEFAULT_HWM
48
+ sock = klass.new(linger:, send_hwm:)
49
+
45
50
  apply_options(sock, config)
46
51
  sock
47
52
  end
@@ -54,6 +59,7 @@ module NNQ
54
59
  sock.bind(url)
55
60
  CLI::Term.write_attach(:bind, sock.last_endpoint, verbose) if verbose >= 1
56
61
  end
62
+
57
63
  config.connects.each do |url|
58
64
  sock.connect(url)
59
65
  CLI::Term.write_attach(:connect, url, verbose) if verbose >= 1
@@ -98,6 +104,7 @@ module NNQ
98
104
  prefixes = config.subscribes.empty? ? [""] : config.subscribes
99
105
  prefixes.each { |p| sock.subscribe(p) }
100
106
  end
107
+
101
108
  end
102
109
  end
103
110
  end
@@ -29,8 +29,8 @@ module NNQ
29
29
 
30
30
 
31
31
  def survey_and_collect(msg)
32
- return if msg.empty?
33
- body = config.format == :marshal ? Marshal.dump(msg.first) : msg.first
32
+ return if msg.nil? || msg.empty?
33
+ body = config.format == :marshal ? Marshal.dump(msg) : msg
34
34
  @sock.send_survey(body)
35
35
  transient_ready!
36
36
  collect_replies
@@ -42,7 +42,7 @@ module NNQ
42
42
  body = @sock.receive
43
43
  break if body.nil?
44
44
  reply = config.format == :marshal ? Marshal.load(body) : body
45
- output(eval_recv_expr([reply]))
45
+ output(eval_recv_expr(reply))
46
46
  rescue NNQ::TimedOut
47
47
  break
48
48
  end
@@ -81,7 +81,7 @@ module NNQ
81
81
  reply = eval_recv_expr(msg)
82
82
  unless reply.equal?(SENT)
83
83
  output(reply)
84
- send_reply(reply || [""])
84
+ send_reply(reply || "")
85
85
  end
86
86
  elsif config.echo
87
87
  output(msg)
@@ -99,8 +99,8 @@ module NNQ
99
99
 
100
100
 
101
101
  def send_reply(msg)
102
- return if msg.empty?
103
- body = config.format == :marshal ? Marshal.dump(msg.first) : msg.first
102
+ return if msg.nil?
103
+ body = config.format == :marshal ? Marshal.dump(msg) : msg
104
104
  @sock.send_reply(body)
105
105
  transient_ready!
106
106
  end
data/lib/nnq/cli/term.rb CHANGED
@@ -36,9 +36,9 @@ module NNQ
36
36
  prefix = log_prefix(verbose)
37
37
  case event.type
38
38
  when :message_sent
39
- "#{prefix}nnq: >> #{Formatter.preview([event.detail[:body]])}"
39
+ "#{prefix}nnq: >> #{Formatter.preview(event.detail[:body])}"
40
40
  when :message_received
41
- "#{prefix}nnq: << #{Formatter.preview([event.detail[:body]])}"
41
+ "#{prefix}nnq: << #{Formatter.preview(event.detail[:body])}"
42
42
  else
43
43
  ep = event.endpoint ? " #{event.endpoint}" : ""
44
44
  detail = event.detail ? " #{event.detail}" : ""
@@ -2,6 +2,6 @@
2
2
 
3
3
  module NNQ
4
4
  module CLI
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nnq-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger