asterism 0.2.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.
@@ -1,5 +1,5 @@
1
1
  # Asterism::Zenoh with blocks, threads and Enumerators (CRuby only). The
2
- # polled API of asterism-zenoh (Session, each_pending, each_reply) is the
2
+ # portable API of asterism-zenoh (Session, each_pending, each_reply) is the
3
3
  # same on the boards and stays as it is; this is a layer on top of it.
4
4
  #
5
5
  # Asterism::Zenoh.open("tcp/192.0.2.2:7447") do |s|
@@ -8,6 +8,9 @@
8
8
  # s.put("home/pc/hello", "hi")
9
9
  # s.get("home/**/status").each { |reply| p reply }
10
10
  # s.subscribe("sensor/temp").each.lazy.map { _1.payload.to_f }.first(3)
11
+ # pub = s.publisher("home/pc/temp", encoding: "text/plain")
12
+ # pub.on_matching { |listening| puts "someone listens: #{listening}" }
13
+ # s.on_transport { |ev| puts "#{ev.zid} #{ev.kind}" }
11
14
  # s.run # until Ctrl-C (or s.start / s.stop)
12
15
  # end
13
16
  #
@@ -19,28 +22,8 @@ module Asterism
19
22
  # Pause between looks while an Enumerator waits for data (seconds).
20
23
  ENUM_STEP = 0.002
21
24
 
22
- # A received sample. payload and attachment are binary Strings (the
23
- # attachment nil when there was none); text is the payload as UTF-8.
24
- Sample = Data.define(:key, :payload, :attachment) do
25
- def text
26
- payload.dup.force_encoding(Encoding::UTF_8)
27
- end
28
-
29
- def to_s
30
- "#{key}: #{payload}"
31
- end
32
- end
33
-
34
- # A reply to a get: the key it was sent with, payload and attachment.
35
- Reply = Data.define(:key, :payload, :attachment) do
36
- def text
37
- payload.dup.force_encoding(Encoding::UTF_8)
38
- end
39
-
40
- def to_s
41
- "#{key}: #{payload}"
42
- end
43
- end
25
+ # Sample and Reply (the received values, with kind, encoding,
26
+ # timestamp and the rest) are asterism-zenoh's (asterism/zenoh/values).
44
27
 
45
28
  # A liveliness change: key of the token, and whether it appeared.
46
29
  Liveliness = Data.define(:key, :alive) do
@@ -57,6 +40,20 @@ module Asterism
57
40
  args.unshift(@asterism_key) if args.size == 1 && @asterism_key
58
41
  super(*args, **kw)
59
42
  end
43
+
44
+ # reply_del without a key: the queryable's own key, as reply.
45
+ def reply_del(key = nil, **kw)
46
+ super(key || @asterism_key, **kw)
47
+ end
48
+
49
+ def reply_err(payload, **kw)
50
+ super(payload.is_a?(String) ? payload : payload.to_s, **kw)
51
+ end
52
+ end
53
+
54
+ # A payload as a String (anything else is sent as to_s).
55
+ def self.bytes(payload)
56
+ payload.is_a?(String) ? payload : payload.to_s
60
57
  end
61
58
 
62
59
  # The queryable's key when it names one key (no wildcard).
@@ -83,19 +80,72 @@ module Asterism
83
80
  end
84
81
  end
85
82
 
86
- # Seconds (Float) or nil -> milliseconds for the polled API.
83
+ # Seconds (Float) or nil -> milliseconds for the portable API.
84
+ # @api private
87
85
  def self.ms(seconds, default_ms)
88
- return default_ms if seconds.nil?
89
- (seconds * 1000).round
86
+ ::Asterism.time_ms("timeout:", seconds, nil, nil, default_ms)
87
+ end
88
+
89
+ # Warns once for owner (a wrapper, or a Get) when raw (what it wraps)
90
+ # dropped something. Only for queues whose depth the application left
91
+ # at the default: one that chose depth: reads dropped itself.
92
+ # @api private
93
+ def self.note_dropped(owner, raw, what, unit)
94
+ d = raw.dropped
95
+ return false if d == 0
96
+ ::Asterism.warn_once(owner, "asterism: #{what}: #{d} #{unit} dropped, the queue was full " \
97
+ "(pass a larger depth:, or read dropped)")
98
+ end
99
+
100
+ # The keys liveliness_get returns: an Array, with the counts of the get
101
+ # behind it.
102
+ class KeyList < Array
103
+ # Replies that did not fit the get's queue (oldest first), and all
104
+ # that came.
105
+ attr_reader :dropped, :received
106
+
107
+ # @api private
108
+ def finish(get)
109
+ @dropped = get.dropped
110
+ @received = get.received
111
+ self
112
+ end
113
+ end
114
+
115
+ # What Connection#get and Querier#get return without a block: an
116
+ # Enumerator (each iteration sends the get again) that also has the
117
+ # counts of the last get it sent: dropped, received, errors (nil before
118
+ # the first).
119
+ class GetEnumerator < Enumerator
120
+ attr_reader :dropped, :received, :errors
121
+
122
+ # @api private
123
+ def finish(get)
124
+ @dropped = get.dropped
125
+ @received = get.received
126
+ @errors = get.errors
127
+ self
128
+ end
129
+ end
130
+
131
+ # Yields obj and closes it after the block (the block's value), or
132
+ # returns obj without a block. @api private
133
+ def self.scoped(obj)
134
+ return obj unless block_given?
135
+ begin
136
+ yield obj
137
+ ensure
138
+ obj.close
139
+ end
90
140
  end
91
141
 
92
142
  # Waits on the calling thread until a sample comes into sub (an
93
143
  # Asterism::Zenoh::Subscriber or LivelinessWatch), yielding each.
94
144
  # Ends when sub or the session closes, or after timeout seconds.
95
- def self.drain(sub, session, timeout)
145
+ def self.drain(sub, session, timeout, take: :each_pending)
96
146
  deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
97
147
  loop do
98
- got = sub.each_pending
148
+ got = sub.public_send(take)
99
149
  got.each { |e| yield e }
100
150
  break if sub.closed? || session.closed?
101
151
  break if deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
@@ -115,16 +165,75 @@ module Asterism
115
165
  @runner.add { @session.poll ? 0 : false }
116
166
  end
117
167
 
118
- def put(key, payload, attachment: nil)
119
- @session.put(key, payload.is_a?(String) ? payload : payload.to_s, attachment: attachment)
168
+ # opts: those of Session#put (attachment:, encoding:, priority:,
169
+ # congestion_control:, express:, reliability:, timestamp:,
170
+ # allowed_destination:). A payload that is not a String is sent as
171
+ # to_s.
172
+ def put(key, payload, **opts)
173
+ @session.put(key, Zenoh.bytes(payload), **opts)
174
+ nil
175
+ end
176
+
177
+ # Deletes key (subscribers get a Sample of kind :delete). opts: those
178
+ # of Session#delete.
179
+ def delete(key, **opts)
180
+ @session.delete(key, **opts)
120
181
  nil
121
182
  end
122
183
 
184
+ # A declared publisher (Connection::Publisher): put / delete,
185
+ # matching?, on_matching { |listening| }. opts: those of
186
+ # Session#publisher (encoding:, priority:, ...). With a block: yields
187
+ # it and closes it after the block (returns the block's value).
188
+ def publisher(key, **opts, &blk)
189
+ Zenoh.scoped(Publisher.new(self, @session.publisher(key, **opts), key), &blk)
190
+ end
191
+
192
+ # An advanced publisher (cache:, sample_miss_detection:,
193
+ # publisher_detection: and the publisher options): late subscribers
194
+ # with history get what it kept. Same object as publisher (and the
195
+ # same block form).
196
+ def advanced_publisher(key, **opts, &blk)
197
+ Zenoh.scoped(Publisher.new(self, @session.advanced_publisher(key, **opts), key), &blk)
198
+ end
199
+
200
+ # An advanced subscriber (history:, recovery:, subscriber_detection:,
201
+ # query_timeout: in seconds or query_timeout_ms:): like subscribe,
202
+ # plus on_publisher and on_miss.
203
+ def advanced_subscriber(key, depth: 16, **opts, &blk)
204
+ sub = AdvancedSubscription.new(self, @session.advanced_subscriber(key, depth: depth, **opts), key)
205
+ sub.handle(&blk) if blk
206
+ sub
207
+ end
208
+
209
+ # A declared get (Connection::Querier): get { |reply| }, matching?,
210
+ # on_matching. timeout: in seconds (2.0) or timeout_ms:; the rest as
211
+ # Session#querier.
212
+ def querier(key, timeout: nil, **opts)
213
+ Querier.new(self, @session.querier(key, timeout: timeout, **opts), key)
214
+ end
215
+
216
+ # on_transport(history: false) { |event| }: a TransportEvent (kind
217
+ # :added / :removed, zid, whatami) on the receiving thread each time a
218
+ # peer or router connects or goes. Returns an Events (close it).
219
+ def on_transport(history: false, depth: 16, &blk)
220
+ raise ArgumentError, "a block is needed" unless blk
221
+ Events.new(self, @session.transport_events(depth: depth, history: history), "on_transport", blk)
222
+ end
223
+
224
+ # on_link(history: false) { |event| }: a LinkEvent each time a link
225
+ # opens or closes.
226
+ def on_link(history: false, depth: 16, &blk)
227
+ raise ArgumentError, "a block is needed" unless blk
228
+ Events.new(self, @session.link_events(depth: depth, history: history), "on_link", blk)
229
+ end
230
+
123
231
  # With a block: every sample is given to it (as a Sample) on the
124
232
  # receiving thread; returns the Subscription (close it to stop).
125
- # Without: a Subscription to take from (each, each_pending).
126
- def subscribe(key, depth: 16, &blk)
127
- sub = Subscription.new(self, @session.subscribe(key, depth), key)
233
+ # Without: a Subscription to take from (each, each_sample).
234
+ def subscribe(key, depth: nil, &blk)
235
+ d = depth.nil? ? DEFAULT_DEPTH : depth
236
+ sub = Subscription.new(self, @session.subscribe(key, depth: d), key, warn: depth.nil?)
128
237
  sub.handle(&blk) if blk
129
238
  sub
130
239
  end
@@ -133,47 +242,65 @@ module Asterism
133
242
  # finished when it returns (reply in the block with q.reply). Without:
134
243
  # a Queryable whose each yields the queries (finished after each).
135
244
  def queryable(key, depth: 16, complete: false, &blk)
136
- qa = Queryable.new(self, @session.queryable(key, depth, complete: complete), key)
245
+ qa = Queryable.new(self, @session.queryable(key, depth: depth, complete: complete), key)
137
246
  qa.handle(&blk) if blk
138
247
  qa
139
248
  end
140
249
 
141
250
  # The replies to a get: yields each Reply until every answer came or
142
- # timeout (seconds) ran out, and returns their number. Without a
143
- # block, an Enumerator (each iteration sends the get again).
144
- def get(key, timeout: 2.0, params: nil, payload: nil, attachment: nil, target: :all,
145
- consolidation: :none, &blk)
146
- args = [key, timeout, params, payload, attachment, target, consolidation]
147
- return enum_for(:get_each, *args) unless blk
148
- get_each(*args, &blk)
251
+ # timeout (seconds; or timeout_ms:) ran out, and returns their number
252
+ # (not named each, so the count stays). Without a block, a
253
+ # GetEnumerator (each iteration sends the get again; dropped /
254
+ # received / errors of the last one). errors: true also yields the
255
+ # error replies (Reply#error?). depth: the replies kept until taken
256
+ # (DEFAULT_GET_DEPTH, 1024); replies past it are dropped, which warns
257
+ # once unless depth: was given. opts: encoding:, priority:,
258
+ # congestion_control:, express:, accept_replies: (Session#get).
259
+ def get(key, timeout: nil, timeout_ms: nil, params: nil, payload: nil, attachment: nil, target: :all,
260
+ consolidation: :none, errors: false, depth: nil, **opts, &blk)
261
+ ms = ::Asterism.time_ms("Connection#get", timeout, timeout_ms, nil, 2000)
262
+ opts[:depth] = depth unless depth.nil?
263
+ start = lambda do
264
+ @session.get(key, timeout_ms: ms, params: params, payload: payload,
265
+ attachment: attachment, target: target, consolidation: consolidation, **opts)
266
+ end
267
+ get_run(start, errors, "get #{key}", depth.nil?, &blk)
149
268
  end
150
269
 
151
- # A liveliness token (close it to withdraw).
152
- def liveliness(key)
153
- @session.liveliness(key)
270
+ # A liveliness token (close it to withdraw). With a block: the token
271
+ # is withdrawn after the block (returns the block's value).
272
+ def liveliness(key, &blk)
273
+ Zenoh.scoped(@session.liveliness(key), &blk)
154
274
  end
155
275
 
156
276
  # With a block: yields key and alive (true when the token appeared) on
157
277
  # the receiving thread for each change; the tokens alive now come
158
- # first. Without: a Watch whose each yields Liveliness values.
159
- def liveliness_watch(key, depth: 16, &blk)
160
- w = Watch.new(self, @session.liveliness_watch(key, depth), key)
278
+ # first, in one burst. Without: a Watch whose each yields Liveliness
279
+ # values. depth: DEFAULT_WATCH_DEPTH (1024); changes past it are
280
+ # dropped (Watch#dropped), which warns once unless depth: was given.
281
+ def liveliness_watch(key, depth: nil, &blk)
282
+ d = depth.nil? ? DEFAULT_WATCH_DEPTH : depth
283
+ w = Watch.new(self, @session.liveliness_watch(key, depth: d), key, warn: depth.nil?)
161
284
  w.handle(&blk) if blk
162
285
  w
163
286
  end
164
287
 
165
- # The keys of the liveliness tokens alive now (an Array; waits for
166
- # the answers, at most timeout seconds).
167
- def liveliness_get(key, timeout: 2.0)
168
- g = @session.liveliness_get(key, Zenoh.ms(timeout, 2000))
169
- keys = []
288
+ # The keys of the liveliness tokens alive now (a KeyList, an Array
289
+ # with dropped and received; waits for the answers, at most timeout
290
+ # seconds, or timeout_ms:). depth: DEFAULT_GET_DEPTH (1024); keys
291
+ # past it are dropped, which warns once unless depth: was given.
292
+ def liveliness_get(key, timeout: nil, timeout_ms: nil, depth: nil)
293
+ ms = ::Asterism.time_ms("Connection#liveliness_get", timeout, timeout_ms, nil, 2000)
294
+ g = @session.liveliness_get(key, timeout_ms: ms, depth: depth)
295
+ keys = KeyList.new
170
296
  loop do
297
+ ended = g.done? # before taking the replies (see get_each)
171
298
  g.each_reply.each { |r| keys << r[0] }
172
- break if g.done?
299
+ break if ended && g.pending == 0
173
300
  sleep(ENUM_STEP)
174
301
  end
175
- g.each_reply.each { |r| keys << r[0] }
176
- keys
302
+ Zenoh.note_dropped(g, g, "liveliness_get #{key}", "keys") if depth.nil?
303
+ keys.finish(g)
177
304
  end
178
305
 
179
306
  # Receives on a thread of its own (blocks run there). Returns self.
@@ -217,10 +344,31 @@ module Asterism
217
344
  @session.zid
218
345
  end
219
346
 
347
+ # The routers (client) or peers (peer mode) connected now.
348
+ def connection_count
349
+ @session.connection_count
350
+ end
351
+
352
+ # Deprecated: connection_count.
220
353
  def peers
221
- @session.peers
354
+ ::Asterism.deprecated("Connection#peers", "Connection#connection_count")
355
+ @session.connection_count
222
356
  end
223
357
 
358
+ # The Zenoh IDs of the peers / routers connected now.
359
+ def peer_zids = @session.peer_zids
360
+ def router_zids = @session.router_zids
361
+
362
+ # What is connected now: Arrays of Transport / Link.
363
+ def transports = @session.transports
364
+ def links = @session.links
365
+
366
+ # A new Timestamp from the session's clock (put(timestamp:)).
367
+ def new_timestamp = @session.new_timestamp
368
+
369
+ # A KeyExpr declared on the session (use it as the key afterwards).
370
+ def declare_keyexpr(key) = @session.declare_keyexpr(key)
371
+
224
372
  # Stops receiving and closes the session (with everything declared on
225
373
  # it). Idempotent.
226
374
  def close
@@ -235,36 +383,54 @@ module Asterism
235
383
 
236
384
  private
237
385
 
238
- def get_each(key, timeout, params, payload, attachment, target, consolidation)
239
- g = @session.get(key, Zenoh.ms(timeout, 2000), params, payload,
240
- attachment: attachment, target: target, consolidation: consolidation)
386
+ # The block form (get_each, the count) or a GetEnumerator over it.
387
+ def get_run(start, errors, what, warn, &blk)
388
+ return get_each(start, errors, what, warn, nil, &blk) if blk
389
+ e = nil
390
+ e = GetEnumerator.new { |y| get_each(start, errors, what, warn, e) { |r| y << r } }
391
+ end
392
+
393
+ # Sends the get (start) and yields each Reply; error replies only
394
+ # with errors. The done? before taking the replies: once the get is
395
+ # done, every reply it had is already in the queue. Warns once when
396
+ # replies were dropped (warn: depth left at the default); stats (a
397
+ # GetEnumerator or nil) gets the counts.
398
+ def get_each(start, errors, what = "get", warn = true, stats = nil)
399
+ g = start.call
241
400
  n = 0
242
401
  loop do
243
- got = g.each_reply
402
+ done = g.done?
403
+ got = g.each_result
244
404
  got.each do |r|
405
+ next if r.error? && !errors
245
406
  n += 1
246
- yield Reply.new(r[0], r[1], r[2])
407
+ yield r
247
408
  end
248
- break if g.done? && g.pending == 0
409
+ break if done && g.pending == 0
249
410
  sleep(ENUM_STEP) if got.empty?
250
411
  end
412
+ Zenoh.note_dropped(g, g, what, "replies") if warn
413
+ stats&.finish(g)
251
414
  n
252
415
  end
253
416
 
254
417
  # A subscription of a Connection. Either its block gets the samples
255
418
  # (on the receiving thread), or the application takes them: each (an
256
419
  # Enumerator that waits for samples) or each_pending (what is there
257
- # now, as the polled API).
420
+ # now, as the portable API).
258
421
  class Subscription
259
422
  include Enumerable
260
423
 
261
424
  attr_reader :key, :subscriber
262
425
 
263
- def initialize(conn, subscriber, key)
426
+ # warn: say once when samples were dropped (the depth was left at
427
+ # the default).
428
+ def initialize(conn, subscriber, key, warn: false)
264
429
  @conn = conn
265
430
  @subscriber = subscriber
266
431
  @key = key
267
432
  @task = nil
433
+ @warn = warn
268
434
  end
269
435
 
270
436
  def handle(&blk)
@@ -272,31 +438,50 @@ module Asterism
272
438
  where = "subscribe #{@key}"
273
439
  r = @conn.runner
274
440
  @task = r.add do
275
- got = @subscriber.each_pending
276
- got.each { |e| r.guard(where) { blk.call(Sample.new(e[0], e[1], e[2])) } }
441
+ got = @subscriber.each_sample
442
+ got.each { |s| r.guard(where) { blk.call(s) } }
443
+ note_dropped unless got.empty?
277
444
  got.size
278
445
  end
279
446
  self
280
447
  end
281
448
 
449
+ # @api private
450
+ def note_dropped
451
+ @warn && Zenoh.note_dropped(self, @subscriber, "subscribe #{@key}", "samples")
452
+ end
453
+
282
454
  # Yields each Sample as it comes, waiting for the next; ends when the
283
455
  # subscription or the session closes, or after timeout seconds.
284
456
  # Without a block, an Enumerator (each.lazy.map { ... }.first(3)).
285
457
  def each(timeout: nil, &blk)
286
458
  return enum_for(:each, timeout: timeout) unless blk
287
459
  raise ::Asterism::Error, "this subscription has a block; its samples go there" if @task
288
- Zenoh.drain(@subscriber, @conn.session, timeout) { |e| blk.call(Sample.new(e[0], e[1], e[2])) }
460
+ Zenoh.drain(@subscriber, @conn.session, timeout, take: :each_sample) do |s|
461
+ note_dropped
462
+ blk.call(s)
463
+ end
289
464
  self
290
465
  end
291
466
 
292
- # The samples there now as Samples (an Array), or yields them.
293
- def each_pending
294
- got = @subscriber.each_pending.map { |e| Sample.new(e[0], e[1], e[2]) }
467
+ # The samples there now as Samples (an Array), or yields them and
468
+ # returns their number. (The portable API's each_sample.)
469
+ def each_sample
470
+ got = @subscriber.each_sample
471
+ note_dropped unless got.empty?
295
472
  return got unless block_given?
296
473
  got.each { |s| yield s }
297
474
  got.size
298
475
  end
299
476
 
477
+ # Deprecated: each_sample. In the portable API each_pending gives
478
+ # [key, payload, attachment] Arrays; here it gave Samples, so one
479
+ # name had two shapes.
480
+ def each_pending(&blk)
481
+ ::Asterism.deprecated("Connection::Subscription#each_pending", "each_sample")
482
+ each_sample(&blk)
483
+ end
484
+
300
485
  def pending = @subscriber.pending
301
486
  def received = @subscriber.received
302
487
  def dropped = @subscriber.dropped
@@ -380,11 +565,14 @@ module Asterism
380
565
 
381
566
  attr_reader :key, :watch
382
567
 
383
- def initialize(conn, watch, key)
568
+ # warn: say once when changes were dropped (the depth was left at
569
+ # the default).
570
+ def initialize(conn, watch, key, warn: false)
384
571
  @conn = conn
385
572
  @watch = watch
386
573
  @key = key
387
574
  @task = nil
575
+ @warn = warn
388
576
  end
389
577
 
390
578
  def handle(&blk)
@@ -394,6 +582,7 @@ module Asterism
394
582
  @task = r.add do
395
583
  got = @watch.each_pending
396
584
  got.each { |e| r.guard(where) { blk.call(e[0], e[1]) } }
585
+ note_dropped unless got.empty?
397
586
  got.size
398
587
  end
399
588
  self
@@ -404,10 +593,24 @@ module Asterism
404
593
  def each(timeout: nil, &blk)
405
594
  return enum_for(:each, timeout: timeout) unless blk
406
595
  raise ::Asterism::Error, "this watch has a block; its changes go there" if @task
407
- Zenoh.drain(@watch, @conn.session, timeout) { |e| blk.call(Liveliness.new(e[0], e[1])) }
596
+ Zenoh.drain(@watch, @conn.session, timeout) do |e|
597
+ note_dropped
598
+ blk.call(Liveliness.new(e[0], e[1]))
599
+ end
408
600
  self
409
601
  end
410
602
 
603
+ # @api private
604
+ def note_dropped
605
+ @warn && Zenoh.note_dropped(self, @watch, "liveliness_watch #{@key}", "changes")
606
+ end
607
+
608
+ # Changes waiting / received so far / dropped because the queue was
609
+ # full (the oldest go; the watch no longer knows which tokens are
610
+ # alive, so declare it again).
611
+ def pending = @watch.pending
612
+ def received = @watch.received
613
+ def dropped = @watch.dropped
411
614
  def closed? = @watch.closed?
412
615
 
413
616
  def close
@@ -417,6 +620,159 @@ module Asterism
417
620
  nil
418
621
  end
419
622
  end
623
+
624
+ # A publisher (or advanced publisher) of a Connection.
625
+ class Publisher
626
+ attr_reader :key, :publisher
627
+
628
+ def initialize(conn, publisher, key)
629
+ @conn = conn
630
+ @publisher = publisher
631
+ @key = key.to_s
632
+ @tasks = []
633
+ end
634
+
635
+ # opts: attachment:, encoding:, timestamp:. A payload that is not a
636
+ # String is sent as to_s.
637
+ def put(payload, **opts)
638
+ @publisher.put(Zenoh.bytes(payload), **opts)
639
+ nil
640
+ end
641
+
642
+ def delete(**opts)
643
+ @publisher.delete(**opts)
644
+ nil
645
+ end
646
+
647
+ # True when a subscriber matches the key now.
648
+ def matching? = @publisher.matching?
649
+
650
+ # on_matching { |listening| }: true when the first subscriber
651
+ # appears, false when the last one goes, on the receiving thread.
652
+ def on_matching(depth: 16, &blk)
653
+ raise ArgumentError, "a block is needed" unless blk
654
+ @tasks << Events.new(@conn, @publisher.matching_listener(depth), "on_matching #{@key}", blk)
655
+ self
656
+ end
657
+
658
+ def closed? = @publisher.closed?
659
+
660
+ def close
661
+ @tasks.each(&:close)
662
+ @tasks.clear
663
+ @publisher.close
664
+ nil
665
+ end
666
+
667
+ def inspect
668
+ "#<Asterism::Zenoh::Connection::Publisher #{@key}>"
669
+ end
670
+ end
671
+
672
+ # A querier of a Connection: the same get, sent again and again.
673
+ class Querier
674
+ attr_reader :key, :querier
675
+
676
+ def initialize(conn, querier, key)
677
+ @conn = conn
678
+ @querier = querier
679
+ @key = key.to_s
680
+ @tasks = []
681
+ end
682
+
683
+ # Yields each Reply until every answer came or the querier's time
684
+ # ran out; returns their number. errors: true also yields the error
685
+ # replies. depth: as Connection#get. Without a block, a
686
+ # GetEnumerator.
687
+ def get(params: nil, payload: nil, attachment: nil, encoding: nil, errors: false, depth: nil, &blk)
688
+ opts = { attachment: attachment, encoding: encoding }
689
+ opts[:depth] = depth unless depth.nil?
690
+ start = -> { @querier.get(params, payload, **opts) }
691
+ @conn.send(:get_run, start, errors, "querier get #{@key}", depth.nil?, &blk)
692
+ end
693
+
694
+ def matching? = @querier.matching?
695
+
696
+ # on_matching { |answered| }: true when a queryable matches, false
697
+ # when none does any more.
698
+ def on_matching(depth: 16, &blk)
699
+ raise ArgumentError, "a block is needed" unless blk
700
+ @tasks << Events.new(@conn, @querier.matching_listener(depth), "on_matching #{@key}", blk)
701
+ self
702
+ end
703
+
704
+ def closed? = @querier.closed?
705
+
706
+ def close
707
+ @tasks.each(&:close)
708
+ @tasks.clear
709
+ @querier.close
710
+ nil
711
+ end
712
+ end
713
+
714
+ # A subscription with history and recovery (advanced_subscriber).
715
+ class AdvancedSubscription < Subscription
716
+ def initialize(conn, subscriber, key)
717
+ super
718
+ @extra = []
719
+ end
720
+
721
+ # on_publisher { |key, alive| }: the advanced publishers on the key
722
+ # that declared publisher_detection, as they come and go (the ones
723
+ # there now first).
724
+ def on_publisher(depth: 16, history: true, &blk)
725
+ raise ArgumentError, "a block is needed" unless blk
726
+ w = Watch.new(@conn, @subscriber.detect_publishers(depth, history: history), @key)
727
+ w.handle(&blk)
728
+ @extra << w
729
+ self
730
+ end
731
+
732
+ # on_miss { |miss| }: samples that never came (Miss: source_zid,
733
+ # source_eid, count); needs sample_miss_detection on the publisher.
734
+ def on_miss(depth: 16, &blk)
735
+ raise ArgumentError, "a block is needed" unless blk
736
+ @extra << Events.new(@conn, @subscriber.miss_listener(depth), "on_miss #{@key}", blk)
737
+ self
738
+ end
739
+
740
+ def close
741
+ @extra.each(&:close)
742
+ @extra.clear
743
+ super
744
+ end
745
+
746
+ def inspect
747
+ "#<Asterism::Zenoh::Connection::AdvancedSubscription #{@key}#{@task ? ' (block)' : ''}>"
748
+ end
749
+ end
750
+
751
+ # The values of a listener (MatchingListener or EventListener) given
752
+ # to a block on the receiving thread. close stops it.
753
+ class Events
754
+ attr_reader :listener
755
+
756
+ def initialize(conn, listener, where, blk)
757
+ @conn = conn
758
+ @listener = listener
759
+ r = conn.runner
760
+ @task = r.add do
761
+ got = @listener.each_pending
762
+ got.each { |v| r.guard(where) { blk.call(v) } }
763
+ got.size
764
+ end
765
+ end
766
+
767
+ def closed? = @listener.closed?
768
+
769
+ def close
770
+ @conn.runner.remove(@task) if @task
771
+ @task = nil
772
+ @listener.close
773
+ nil
774
+ end
775
+ end
420
776
  end
421
777
  end
422
778
  end
@@ -1,5 +1,5 @@
1
1
  module Asterism
2
2
  # The gem's version (the CRuby gem asterism; it depends on asterism-zenoh
3
3
  # of the same version).
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end