nats-pure 0.6.2 → 2.0.0.pre.rc1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/nats/io/js.rb ADDED
@@ -0,0 +1,1274 @@
1
+ # Copyright 2021 The NATS Authors
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ #
14
+ require_relative 'msg'
15
+ require_relative 'client'
16
+ require_relative 'errors'
17
+ require_relative 'kv'
18
+ require 'time'
19
+ require 'base64'
20
+
21
+ module NATS
22
+
23
+ # JetStream returns a context with a similar API as the NATS::Client
24
+ # but with enhanced functions to persist and consume messages from
25
+ # the NATS JetStream engine.
26
+ #
27
+ # @example
28
+ # nc = NATS.connect("demo.nats.io")
29
+ # js = nc.jetstream()
30
+ #
31
+ class JetStream
32
+ # Create a new JetStream context for a NATS connection.
33
+ #
34
+ # @param conn [NATS::Client]
35
+ # @param params [Hash] Options to customize JetStream context.
36
+ # @option params [String] :prefix JetStream API prefix to use for the requests.
37
+ # @option params [String] :domain JetStream Domain to use for the requests.
38
+ # @option params [Float] :timeout Default timeout to use for JS requests.
39
+ def initialize(conn, params={})
40
+ @nc = conn
41
+ @prefix = if params[:prefix]
42
+ params[:prefix]
43
+ elsif params[:domain]
44
+ "$JS.#{params[:domain]}.API"
45
+ else
46
+ JS::DefaultAPIPrefix
47
+ end
48
+ @opts = params
49
+ @opts[:timeout] ||= 5 # seconds
50
+ params[:prefix] = @prefix
51
+
52
+ # Include JetStream::Manager
53
+ extend Manager
54
+ extend KeyValue::Manager
55
+ end
56
+
57
+ # PubAck is the API response from a successfully published message.
58
+ #
59
+ # @!attribute [stream] stream
60
+ # @return [String] Name of the stream that processed the published message.
61
+ # @!attribute [seq] seq
62
+ # @return [Fixnum] Sequence of the message in the stream.
63
+ # @!attribute [duplicate] duplicate
64
+ # @return [Boolean] Indicates whether the published message is a duplicate.
65
+ # @!attribute [domain] domain
66
+ # @return [String] JetStream Domain that processed the ack response.
67
+ PubAck = Struct.new(:stream, :seq, :duplicate, :domain, keyword_init: true)
68
+
69
+ # publish produces a message for JetStream.
70
+ #
71
+ # @param subject [String] The subject from a stream where the message will be sent.
72
+ # @param payload [String] The payload of the message.
73
+ # @param params [Hash] Options to customize the publish message request.
74
+ # @option params [Float] :timeout Time to wait for an PubAck response or an error.
75
+ # @option params [Hash] :header NATS Headers to use for the message.
76
+ # @option params [String] :stream Expected Stream to which the message is being published.
77
+ # @raise [NATS::Timeout] When it takes too long to receive an ack response.
78
+ # @return [PubAck] The pub ack response.
79
+ def publish(subject, payload="", **params)
80
+ params[:timeout] ||= @opts[:timeout]
81
+ if params[:stream]
82
+ params[:header] ||= {}
83
+ params[:header][JS::Header::ExpectedStream] = params[:stream]
84
+ end
85
+
86
+ # Send message with headers.
87
+ msg = NATS::Msg.new(subject: subject,
88
+ data: payload,
89
+ header: params[:header])
90
+
91
+ begin
92
+ resp = @nc.request_msg(msg, **params)
93
+ result = JSON.parse(resp.data, symbolize_names: true)
94
+ rescue ::NATS::IO::NoRespondersError
95
+ raise JetStream::Error::NoStreamResponse.new("nats: no response from stream")
96
+ end
97
+ raise JS.from_error(result[:error]) if result[:error]
98
+
99
+ PubAck.new(result)
100
+ end
101
+
102
+ # subscribe binds or creates a push subscription to a JetStream pull consumer.
103
+ #
104
+ # @param subject [String] Subject from which the messages will be fetched.
105
+ # @param params [Hash] Options to customize the PushSubscription.
106
+ # @option params [String] :stream Name of the Stream to which the consumer belongs.
107
+ # @option params [String] :consumer Name of the Consumer to which the PushSubscription will be bound.
108
+ # @option params [String] :durable Consumer durable name from where the messages will be fetched.
109
+ # @option params [Hash] :config Configuration for the consumer.
110
+ # @return [NATS::JetStream::PushSubscription]
111
+ def subscribe(subject, params={}, &cb)
112
+ params[:consumer] ||= params[:durable]
113
+ stream = params[:stream].nil? ? find_stream_name_by_subject(subject) : params[:stream]
114
+
115
+ queue = params[:queue]
116
+ durable = params[:durable]
117
+ flow_control = params[:flow_control]
118
+ manual_ack = params[:manual_ack]
119
+ idle_heartbeat = params[:idle_heartbeat]
120
+ flow_control = params[:flow_control]
121
+
122
+ if queue
123
+ if durable and durable != queue
124
+ raise NATS::JetStream::Error.new("nats: cannot create queue subscription '#{queue}' to consumer '#{durable}'")
125
+ else
126
+ durable = queue
127
+ end
128
+ end
129
+
130
+ cinfo = nil
131
+ consumer_found = false
132
+ should_create = false
133
+
134
+ if not durable
135
+ should_create = true
136
+ else
137
+ begin
138
+ cinfo = consumer_info(stream, durable)
139
+ config = cinfo.config
140
+ consumer_found = true
141
+ consumer = durable
142
+ rescue NATS::JetStream::Error::NotFound
143
+ should_create = true
144
+ consumer_found = false
145
+ end
146
+ end
147
+
148
+ if consumer_found
149
+ if not config.deliver_group
150
+ if queue
151
+ raise NATS::JetStream::Error.new("nats: cannot create a queue subscription for a consumer without a deliver group")
152
+ elsif cinfo.push_bound
153
+ raise NATS::JetStream::Error.new("nats: consumer is already bound to a subscription")
154
+ end
155
+ else
156
+ if not queue
157
+ raise NATS::JetStream::Error.new("nats: cannot create a subscription for a consumer with a deliver group #{config.deliver_group}")
158
+ elsif queue != config.deliver_group
159
+ raise NATS::JetStream::Error.new("nats: cannot create a queue subscription #{queue} for a consumer with a deliver group #{config.deliver_group}")
160
+ end
161
+ end
162
+ elsif should_create
163
+ # Auto-create consumer if none found.
164
+ if config.nil?
165
+ # Defaults
166
+ config = JetStream::API::ConsumerConfig.new({ack_policy: "explicit"})
167
+ elsif config.is_a?(Hash)
168
+ config = JetStream::API::ConsumerConfig.new(config)
169
+ elsif !config.is_a?(JetStream::API::ConsumerConfig)
170
+ raise NATS::JetStream::Error.new("nats: invalid ConsumerConfig")
171
+ end
172
+
173
+ config.durable_name = durable if not config.durable_name
174
+ config.deliver_group = queue if not config.deliver_group
175
+
176
+ # Create inbox for push consumer.
177
+ deliver = @nc.new_inbox
178
+ config.deliver_subject = deliver
179
+
180
+ # Auto created consumers use the filter subject.
181
+ config.filter_subject = subject
182
+
183
+ # Heartbeats / FlowControl
184
+ config.flow_control = flow_control
185
+ if idle_heartbeat or config.idle_heartbeat
186
+ idle_heartbeat = config.idle_heartbeat if config.idle_heartbeat
187
+ idle_heartbeat = idle_heartbeat * 1_000_000_000
188
+ config.idle_heartbeat = idle_heartbeat
189
+ end
190
+
191
+ # Auto create the consumer.
192
+ cinfo = add_consumer(stream, config)
193
+ consumer = cinfo.name
194
+ end
195
+
196
+ # Enable auto acking for async callbacks unless disabled.
197
+ if cb and not manual_ack
198
+ ocb = cb
199
+ new_cb = proc do |msg|
200
+ ocb.call(msg)
201
+ msg.ack rescue JetStream::Error::MsgAlreadyAckd
202
+ end
203
+ cb = new_cb
204
+ end
205
+ sub = @nc.subscribe(config.deliver_subject, queue: config.deliver_group, &cb)
206
+ sub.extend(PushSubscription)
207
+ sub.jsi = JS::Sub.new(
208
+ js: self,
209
+ stream: stream,
210
+ consumer: consumer,
211
+ )
212
+ sub
213
+ end
214
+
215
+ # pull_subscribe binds or creates a subscription to a JetStream pull consumer.
216
+ #
217
+ # @param subject [String] Subject from which the messages will be fetched.
218
+ # @param durable [String] Consumer durable name from where the messages will be fetched.
219
+ # @param params [Hash] Options to customize the PullSubscription.
220
+ # @option params [String] :stream Name of the Stream to which the consumer belongs.
221
+ # @option params [String] :consumer Name of the Consumer to which the PullSubscription will be bound.
222
+ # @option params [Hash] :config Configuration for the consumer.
223
+ # @return [NATS::JetStream::PullSubscription]
224
+ def pull_subscribe(subject, durable, params={})
225
+ raise JetStream::Error::InvalidDurableName.new("nats: invalid durable name") if durable.empty?
226
+ params[:consumer] ||= durable
227
+ stream = params[:stream].nil? ? find_stream_name_by_subject(subject) : params[:stream]
228
+
229
+ begin
230
+ consumer_info(stream, params[:consumer])
231
+ rescue NATS::JetStream::Error::NotFound => e
232
+ # If attempting to bind, then this is a hard error.
233
+ raise e if params[:stream]
234
+
235
+ config = if not params[:config]
236
+ JetStream::API::ConsumerConfig.new
237
+ elsif params[:config].is_a?(JetStream::API::ConsumerConfig)
238
+ params[:config]
239
+ else
240
+ JetStream::API::ConsumerConfig.new(params[:config])
241
+ end
242
+ config[:durable_name] = durable
243
+ config[:ack_policy] ||= JS::Config::AckExplicit
244
+ add_consumer(stream, config)
245
+ end
246
+
247
+ deliver = @nc.new_inbox
248
+ sub = @nc.subscribe(deliver)
249
+ sub.extend(PullSubscription)
250
+
251
+ consumer = params[:consumer]
252
+ subject = "#{@prefix}.CONSUMER.MSG.NEXT.#{stream}.#{consumer}"
253
+ sub.jsi = JS::Sub.new(
254
+ js: self,
255
+ stream: stream,
256
+ consumer: params[:consumer],
257
+ nms: subject
258
+ )
259
+ sub
260
+ end
261
+
262
+ # A JetStream::Manager can be used to make requests to the JetStream API.
263
+ #
264
+ # @example
265
+ # require 'nats/client'
266
+ #
267
+ # nc = NATS.connect("demo.nats.io")
268
+ #
269
+ # config = JetStream::API::StreamConfig.new()
270
+ # nc.jsm.add_stream(config)
271
+ #
272
+ #
273
+ module Manager
274
+ # add_stream creates a stream with a given config.
275
+ # @param config [JetStream::API::StreamConfig] Configuration of the stream to create.
276
+ # @param params [Hash] Options to customize API request.
277
+ # @option params [Float] :timeout Time to wait for response.
278
+ # @return [JetStream::API::StreamCreateResponse] The result of creating a Stream.
279
+ def add_stream(config, params={})
280
+ config = if not config.is_a?(JetStream::API::StreamConfig)
281
+ JetStream::API::StreamConfig.new(config)
282
+ else
283
+ config
284
+ end
285
+ stream = config[:name]
286
+ raise ArgumentError.new(":name is required to create streams") unless stream
287
+ req_subject = "#{@prefix}.STREAM.CREATE.#{stream}"
288
+ result = api_request(req_subject, config.to_json, params)
289
+ JetStream::API::StreamCreateResponse.new(result)
290
+ end
291
+
292
+ # stream_info retrieves the current status of a stream.
293
+ # @param stream [String] Name of the stream.
294
+ # @param params [Hash] Options to customize API request.
295
+ # @option params [Float] :timeout Time to wait for response.
296
+ # @return [JetStream::API::StreamInfo] The latest StreamInfo of the stream.
297
+ def stream_info(stream, params={})
298
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
299
+
300
+ req_subject = "#{@prefix}.STREAM.INFO.#{stream}"
301
+ result = api_request(req_subject, '', params)
302
+ JetStream::API::StreamInfo.new(result)
303
+ end
304
+
305
+ # delete_stream deletes a stream.
306
+ # @param stream [String] Name of the stream.
307
+ # @param params [Hash] Options to customize API request.
308
+ # @option params [Float] :timeout Time to wait for response.
309
+ # @return [Boolean]
310
+ def delete_stream(stream, params={})
311
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
312
+
313
+ req_subject = "#{@prefix}.STREAM.DELETE.#{stream}"
314
+ result = api_request(req_subject, '', params)
315
+ result[:success]
316
+ end
317
+
318
+ # add_consumer creates a consumer with a given config.
319
+ # @param stream [String] Name of the stream.
320
+ # @param config [JetStream::API::ConsumerConfig] Configuration of the consumer to create.
321
+ # @param params [Hash] Options to customize API request.
322
+ # @option params [Float] :timeout Time to wait for response.
323
+ # @return [JetStream::API::ConsumerInfo] The result of creating a Consumer.
324
+ def add_consumer(stream, config, params={})
325
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
326
+ config = if not config.is_a?(JetStream::API::ConsumerConfig)
327
+ JetStream::API::ConsumerConfig.new(config)
328
+ else
329
+ config
330
+ end
331
+ req_subject = if config[:durable_name]
332
+ "#{@prefix}.CONSUMER.DURABLE.CREATE.#{stream}.#{config[:durable_name]}"
333
+ else
334
+ "#{@prefix}.CONSUMER.CREATE.#{stream}"
335
+ end
336
+
337
+ config[:ack_policy] ||= JS::Config::AckExplicit
338
+ # Check if have to normalize ack wait so that it is in nanoseconds for Go compat.
339
+ if config[:ack_wait]
340
+ raise ArgumentError.new("nats: invalid ack wait") unless config[:ack_wait].is_a?(Integer)
341
+ config[:ack_wait] = config[:ack_wait] * 1_000_000_000
342
+ end
343
+
344
+ req = {
345
+ stream_name: stream,
346
+ config: config
347
+ }
348
+ result = api_request(req_subject, req.to_json, params)
349
+ JetStream::API::ConsumerInfo.new(result).freeze
350
+ end
351
+
352
+ # consumer_info retrieves the current status of a consumer.
353
+ # @param stream [String] Name of the stream.
354
+ # @param consumer [String] Name of the consumer.
355
+ # @param params [Hash] Options to customize API request.
356
+ # @option params [Float] :timeout Time to wait for response.
357
+ # @return [JetStream::API::ConsumerInfo] The latest ConsumerInfo of the consumer.
358
+ def consumer_info(stream, consumer, params={})
359
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
360
+ raise JetStream::Error::InvalidConsumerName.new("nats: invalid consumer name") if consumer.nil? or consumer.empty?
361
+
362
+ req_subject = "#{@prefix}.CONSUMER.INFO.#{stream}.#{consumer}"
363
+ result = api_request(req_subject, '', params)
364
+ JetStream::API::ConsumerInfo.new(result)
365
+ end
366
+
367
+ # delete_consumer deletes a consumer.
368
+ # @param stream [String] Name of the stream.
369
+ # @param consumer [String] Name of the consumer.
370
+ # @param params [Hash] Options to customize API request.
371
+ # @option params [Float] :timeout Time to wait for response.
372
+ # @return [Boolean]
373
+ def delete_consumer(stream, consumer, params={})
374
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
375
+ raise JetStream::Error::InvalidConsumerName.new("nats: invalid consumer name") if consumer.nil? or consumer.empty?
376
+
377
+ req_subject = "#{@prefix}.CONSUMER.DELETE.#{stream}.#{consumer}"
378
+ result = api_request(req_subject, '', params)
379
+ result[:success]
380
+ end
381
+
382
+ # find_stream_name_by_subject does a lookup for the stream to which
383
+ # the subject belongs.
384
+ # @param subject [String] The subject that belongs to a stream.
385
+ # @param params [Hash] Options to customize API request.
386
+ # @option params [Float] :timeout Time to wait for response.
387
+ # @return [String] The name of the JetStream stream for the subject.
388
+ def find_stream_name_by_subject(subject, params={})
389
+ req_subject = "#{@prefix}.STREAM.NAMES"
390
+ req = { subject: subject }
391
+ result = api_request(req_subject, req.to_json, params)
392
+ raise JetStream::Error::NotFound unless result[:streams]
393
+
394
+ result[:streams].first
395
+ end
396
+
397
+ def get_last_msg(stream_name, subject)
398
+ req_subject = "#{@prefix}.STREAM.MSG.GET.#{stream_name}"
399
+ req = {'last_by_subj': subject}
400
+ data = req.to_json
401
+ resp = api_request(req_subject, data)
402
+ JetStream::API::RawStreamMsg.new(resp[:message])
403
+ end
404
+
405
+ private
406
+
407
+ def api_request(req_subject, req="", params={})
408
+ params[:timeout] ||= @opts[:timeout]
409
+ result = begin
410
+ msg = @nc.request(req_subject, req, **params)
411
+ JSON.parse(msg.data, symbolize_names: true)
412
+ rescue NATS::IO::NoRespondersError
413
+ raise JetStream::Error::ServiceUnavailable
414
+ end
415
+ raise JS.from_error(result[:error]) if result[:error]
416
+
417
+ result
418
+ end
419
+ end
420
+
421
+ # PushSubscription is included into NATS::Subscription so that it
422
+ #
423
+ # @example Create a push subscription using JetStream context.
424
+ #
425
+ # require 'nats/client'
426
+ #
427
+ # nc = NATS.connect
428
+ # js = nc.jetstream
429
+ # sub = js.subscribe("foo", "bar")
430
+ # msg = sub.next_msg
431
+ # msg.ack
432
+ # sub.unsubscribe
433
+ #
434
+ # @!visibility public
435
+ module PushSubscription
436
+ # consumer_info retrieves the current status of the pull subscription consumer.
437
+ # @param params [Hash] Options to customize API request.
438
+ # @option params [Float] :timeout Time to wait for response.
439
+ # @return [JetStream::API::ConsumerInfo] The latest ConsumerInfo of the consumer.
440
+ def consumer_info(params={})
441
+ @jsi.js.consumer_info(@jsi.stream, @jsi.consumer, params)
442
+ end
443
+ end
444
+ private_constant :PushSubscription
445
+
446
+ # PullSubscription is included into NATS::Subscription so that it
447
+ # can be used to fetch messages from a pull based consumer from
448
+ # JetStream.
449
+ #
450
+ # @example Create a pull subscription using JetStream context.
451
+ #
452
+ # require 'nats/client'
453
+ #
454
+ # nc = NATS.connect
455
+ # js = nc.jetstream
456
+ # psub = js.pull_subscribe("foo", "bar")
457
+ #
458
+ # loop do
459
+ # msgs = psub.fetch(5)
460
+ # msgs.each do |msg|
461
+ # msg.ack
462
+ # end
463
+ # end
464
+ #
465
+ # @!visibility public
466
+ module PullSubscription
467
+ # next_msg is not available for pull based subscriptions.
468
+ # @raise [NATS::JetStream::Error]
469
+ def next_msg(params={})
470
+ raise ::NATS::JetStream::Error.new("nats: pull subscription cannot use next_msg")
471
+ end
472
+
473
+ # fetch makes a request to be delivered more messages from a pull consumer.
474
+ #
475
+ # @param batch [Fixnum] Number of messages to pull from the stream.
476
+ # @param params [Hash] Options to customize the fetch request.
477
+ # @option params [Float] :timeout Duration of the fetch request before it expires.
478
+ # @return [Array<NATS::Msg>]
479
+ def fetch(batch=1, params={})
480
+ if batch < 1
481
+ raise ::NATS::JetStream::Error.new("nats: invalid batch size")
482
+ end
483
+
484
+ t = MonotonicTime.now
485
+ timeout = params[:timeout] ||= 5
486
+ expires = (timeout * 1_000_000_000) - 100_000
487
+ next_req = {
488
+ batch: batch
489
+ }
490
+
491
+ msgs = []
492
+ case
493
+ when batch < 1
494
+ raise ::NATS::JetStream::Error.new("nats: invalid batch size")
495
+ when batch == 1
496
+ ####################################################
497
+ # Fetch (1) #
498
+ ####################################################
499
+
500
+ # Check if there is any pending message in the queue that is
501
+ # ready to be consumed.
502
+ synchronize do
503
+ unless @pending_queue.empty?
504
+ msg = @pending_queue.pop
505
+ # Check for a no msgs response status.
506
+ if JS.is_status_msg(msg)
507
+ case msg.header["Status"]
508
+ when JS::Status::NoMsgs
509
+ msg = nil
510
+ else
511
+ raise JS.from_msg(msg)
512
+ end
513
+ else
514
+ msgs << msg
515
+ end
516
+ end
517
+ end
518
+
519
+ # Make lingering request with expiration.
520
+ next_req[:expires] = expires
521
+ if msgs.empty?
522
+ # Make publish request and wait for response.
523
+ @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
524
+
525
+ # Wait for result of fetch or timeout.
526
+ synchronize { wait_for_msgs_cond.wait(timeout) }
527
+
528
+ msgs << @pending_queue.pop unless @pending_queue.empty?
529
+
530
+ duration = MonotonicTime.since(t)
531
+ if duration > timeout
532
+ raise ::NATS::Timeout.new("nats: fetch timeout")
533
+ end
534
+
535
+ # Should have received at least a message at this point,
536
+ # if that is not the case then error already.
537
+ if JS.is_status_msg(msgs.first)
538
+ raise JS.from_msg(msgs.first)
539
+ end
540
+ end
541
+ when batch > 1
542
+ ####################################################
543
+ # Fetch (n) #
544
+ ####################################################
545
+
546
+ # Check if there already enough in the pending buffer.
547
+ synchronize do
548
+ if batch <= @pending_queue.size
549
+ batch.times { msgs << @pending_queue.pop }
550
+
551
+ return msgs
552
+ end
553
+ end
554
+
555
+ # Make publish request and wait any response.
556
+ next_req[:no_wait] = true
557
+ @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
558
+
559
+ # Not receiving even one is a timeout.
560
+ start_time = MonotonicTime.now
561
+ msg = nil
562
+ synchronize {
563
+ wait_for_msgs_cond.wait(timeout)
564
+ msg = @pending_queue.pop unless @pending_queue.empty?
565
+ }
566
+
567
+ # Check if the first message was a response saying that
568
+ # there are no messages.
569
+ if !msg.nil? && JS.is_status_msg(msg)
570
+ case msg.header[JS::Header::Status]
571
+ when JS::Status::NoMsgs
572
+ # Make another request that does wait.
573
+ next_req[:expires] = expires
574
+ next_req.delete(:no_wait)
575
+
576
+ @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
577
+ else
578
+ raise JS.from_msg(msg)
579
+ end
580
+ else
581
+ msgs << msg
582
+ end
583
+
584
+ # Check if have not received yet a single message.
585
+ duration = MonotonicTime.since(start_time)
586
+ if msgs.empty? and duration > timeout
587
+ raise NATS::Timeout.new("nats: fetch timeout")
588
+ end
589
+
590
+ needed = batch - msgs.count
591
+ while needed > 0 and MonotonicTime.since(start_time) < timeout
592
+ duration = MonotonicTime.since(start_time)
593
+
594
+ # Wait for the rest of the messages.
595
+ synchronize do
596
+
597
+ # Wait until there is a message delivered.
598
+ if @pending_queue.empty?
599
+ deadline = timeout - duration
600
+ wait_for_msgs_cond.wait(deadline) if deadline > 0
601
+
602
+ duration = MonotonicTime.since(start_time)
603
+ if msgs.empty? && @pending_queue.empty? and duration > timeout
604
+ raise NATS::Timeout.new("nats: fetch timeout")
605
+ end
606
+ else
607
+ msg = @pending_queue.pop
608
+
609
+ if JS.is_status_msg(msg)
610
+ case msg.header["Status"]
611
+ when JS::Status::NoMsgs, JS::Status::RequestTimeout
612
+ duration = MonotonicTime.since(start_time)
613
+
614
+ # Do not time out if we received at least some messages.
615
+ if msgs.empty? && @pending_queue.empty? and duration > timeout
616
+ raise NATS::Timeout.new("nats: fetch timeout")
617
+ end
618
+
619
+ # Likely only received a subset of the messages.
620
+ return msgs
621
+ else
622
+ raise JS.from_msg(msg)
623
+ end
624
+ else
625
+ msgs << msg
626
+ needed -= 1
627
+ end
628
+ end
629
+ end # :end: synchronize
630
+ end
631
+ end
632
+
633
+ msgs
634
+ end
635
+
636
+ # consumer_info retrieves the current status of the pull subscription consumer.
637
+ # @param params [Hash] Options to customize API request.
638
+ # @option params [Float] :timeout Time to wait for response.
639
+ # @return [JetStream::API::ConsumerInfo] The latest ConsumerInfo of the consumer.
640
+ def consumer_info(params={})
641
+ @jsi.js.consumer_info(@jsi.stream, @jsi.consumer, params)
642
+ end
643
+ end
644
+ private_constant :PullSubscription
645
+
646
+ #######################################
647
+ # #
648
+ # JetStream Message and Ack Methods #
649
+ # #
650
+ #######################################
651
+
652
+ # JetStream::Msg module includes the methods so that a regular NATS::Msg
653
+ # can be enhanced with JetStream features like acking and metadata.
654
+ module Msg
655
+ module Ack
656
+ # Ack types
657
+ Ack = ("+ACK".freeze)
658
+ Nak = ("-NAK".freeze)
659
+ Progress = ("+WPI".freeze)
660
+ Term = ("+TERM".freeze)
661
+
662
+ Empty = (''.freeze)
663
+ DotSep = ('.'.freeze)
664
+ NoDomainName = ('_'.freeze)
665
+
666
+ # Position
667
+ Prefix0 = ('$JS'.freeze)
668
+ Prefix1 = ('ACK'.freeze)
669
+ Domain = 2
670
+ AccHash = 3
671
+ Stream = 4
672
+ Consumer = 5
673
+ NumDelivered = 6
674
+ StreamSeq = 7
675
+ ConsumerSeq = 8
676
+ Timestamp = 9
677
+ NumPending = 10
678
+
679
+ # Subject without domain:
680
+ # $JS.ACK.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>
681
+ #
682
+ V1TokenCounts = 9
683
+
684
+ # Subject with domain:
685
+ # $JS.ACK.<domain>.<account hash>.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>.<a token with a random value>
686
+ #
687
+ V2TokenCounts = 12
688
+
689
+ SequencePair = Struct.new(:stream, :consumer)
690
+ end
691
+ private_constant :Ack
692
+
693
+ class Metadata
694
+ attr_reader :sequence, :num_delivered, :num_pending, :timestamp, :stream, :consumer, :domain
695
+
696
+ def initialize(opts)
697
+ @sequence = Ack::SequencePair.new(opts[Ack::StreamSeq].to_i, opts[Ack::ConsumerSeq].to_i)
698
+ @domain = opts[Ack::Domain]
699
+ @num_delivered = opts[Ack::NumDelivered].to_i
700
+ @num_pending = opts[Ack::NumPending].to_i
701
+ @timestamp = Time.at((opts[Ack::Timestamp].to_i / 1_000_000_000.0))
702
+ @stream = opts[Ack::Stream]
703
+ @consumer = opts[Ack::Consumer]
704
+ # TODO: Not exposed in Go client either right now.
705
+ # account = opts[Ack::AccHash]
706
+ end
707
+ end
708
+
709
+ module AckMethods
710
+ def ack(**params)
711
+ ensure_is_acked_once!
712
+
713
+ resp = if params[:timeout]
714
+ @nc.request(@reply, Ack::Ack, **params)
715
+ else
716
+ @nc.publish(@reply, Ack::Ack)
717
+ end
718
+ @sub.synchronize { @ackd = true }
719
+
720
+ resp
721
+ end
722
+
723
+ def ack_sync(**params)
724
+ ensure_is_acked_once!
725
+
726
+ params[:timeout] ||= 0.5
727
+ resp = @nc.request(@reply, Ack::Ack, **params)
728
+ @sub.synchronize { @ackd = true }
729
+
730
+ resp
731
+ end
732
+
733
+ def nak(**params)
734
+ ensure_is_acked_once!
735
+
736
+ resp = if params[:timeout]
737
+ @nc.request(@reply, Ack::Nak, **params)
738
+ else
739
+ @nc.publish(@reply, Ack::Nak)
740
+ end
741
+ @sub.synchronize { @ackd = true }
742
+
743
+ resp
744
+ end
745
+
746
+ def term(**params)
747
+ ensure_is_acked_once!
748
+
749
+ resp = if params[:timeout]
750
+ @nc.request(@reply, Ack::Term, **params)
751
+ else
752
+ @nc.publish(@reply, Ack::Term)
753
+ end
754
+ @sub.synchronize { @ackd = true }
755
+
756
+ resp
757
+ end
758
+
759
+ def in_progress(**params)
760
+ params[:timeout] ? @nc.request(@reply, Ack::Progress, **params) : @nc.publish(@reply, Ack::Progress)
761
+ end
762
+
763
+ def metadata
764
+ @meta ||= parse_metadata(reply)
765
+ end
766
+
767
+ private
768
+
769
+ def ensure_is_acked_once!
770
+ @sub.synchronize do
771
+ if @ackd
772
+ raise JetStream::Error::MsgAlreadyAckd.new("nats: message was already acknowledged: #{self}")
773
+ end
774
+ end
775
+ end
776
+
777
+ def parse_metadata(reply)
778
+ tokens = reply.split(Ack::DotSep)
779
+ n = tokens.count
780
+
781
+ case
782
+ when n < Ack::V1TokenCounts || (n > Ack::V1TokenCounts and n < Ack::V2TokenCounts)
783
+ raise NotJSMessage.new("nats: not a jetstream message")
784
+ when tokens[0] != Ack::Prefix0 || tokens[1] != Ack::Prefix1
785
+ raise NotJSMessage.new("nats: not a jetstream message")
786
+ when n == Ack::V1TokenCounts
787
+ tokens.insert(Ack::Domain, Ack::Empty)
788
+ tokens.insert(Ack::AccHash, Ack::Empty)
789
+ when tokens[Ack::Domain] == Ack::NoDomainName
790
+ tokens[Ack::Domain] = Ack::Empty
791
+ end
792
+
793
+ Metadata.new(tokens)
794
+ end
795
+ end
796
+ end
797
+
798
+ ####################################
799
+ # #
800
+ # JetStream Configuration Options #
801
+ # #
802
+ ####################################
803
+
804
+ # Misc internal functions to support JS API.
805
+ # @private
806
+ module JS
807
+ DefaultAPIPrefix = ("$JS.API".freeze)
808
+
809
+ module Status
810
+ CtrlMsg = ("100".freeze)
811
+ NoMsgs = ("404".freeze)
812
+ NotFound = ("404".freeze)
813
+ RequestTimeout = ("408".freeze)
814
+ ServiceUnavailable = ("503".freeze)
815
+ end
816
+
817
+ module Header
818
+ Status = ("Status".freeze)
819
+ Desc = ("Description".freeze)
820
+ MsgID = ("Nats-Msg-Id".freeze)
821
+ ExpectedStream = ("Nats-Expected-Stream".freeze)
822
+ ExpectedLastSeq = ("Nats-Expected-Last-Sequence".freeze)
823
+ ExpectedLastSubjSeq = ("Nats-Expected-Last-Subject-Sequence".freeze)
824
+ ExpectedLastMsgID = ("Nats-Expected-Last-Msg-Id".freeze)
825
+ LastConsumerSeq = ("Nats-Last-Consumer".freeze)
826
+ LastStreamSeq = ("Nats-Last-Stream".freeze)
827
+ end
828
+
829
+ module Config
830
+ # AckPolicy
831
+ AckExplicit = ("explicit".freeze)
832
+ AckAll = ("all".freeze)
833
+ AckNone = ("none".freeze)
834
+ end
835
+
836
+ class Sub
837
+ attr_reader :js, :stream, :consumer, :nms
838
+
839
+ def initialize(opts={})
840
+ @js = opts[:js]
841
+ @stream = opts[:stream]
842
+ @consumer = opts[:consumer]
843
+ @nms = opts[:nms]
844
+ end
845
+ end
846
+
847
+ class << self
848
+ def next_req_to_json(next_req)
849
+ req = {}
850
+ req[:batch] = next_req[:batch]
851
+ req[:expires] = next_req[:expires].to_i if next_req[:expires]
852
+ req[:no_wait] = next_req[:no_wait] if next_req[:no_wait]
853
+ req.to_json
854
+ end
855
+
856
+ def is_status_msg(msg)
857
+ return (!msg.nil? and (!msg.header.nil? and msg.header[Header::Status]))
858
+ end
859
+
860
+ # check_503_error raises exception when a NATS::Msg has a 503 status header.
861
+ # @param msg [NATS::Msg] The message with status headers.
862
+ # @raise [NATS::JetStream::Error::ServiceUnavailable]
863
+ def check_503_error(msg)
864
+ return if msg.nil? or msg.header.nil?
865
+ if msg.header[Header::Status] == Status::ServiceUnavailable
866
+ raise ::NATS::JetStream::Error::ServiceUnavailable
867
+ end
868
+ end
869
+
870
+ # from_msg takes a plain NATS::Msg and checks its headers to confirm
871
+ # if it was an error:
872
+ #
873
+ # msg.header={"Status"=>"503"})
874
+ # msg.header={"Status"=>"408", "Description"=>"Request Timeout"})
875
+ #
876
+ # @param msg [NATS::Msg] The message with status headers.
877
+ # @return [NATS::JetStream::API::Error]
878
+ def from_msg(msg)
879
+ check_503_error(msg)
880
+ code = msg.header[JS::Header::Status]
881
+ desc = msg.header[JS::Header::Desc]
882
+ return ::NATS::JetStream::API::Error.new({code: code, description: desc})
883
+ end
884
+
885
+ # from_error takes an API response that errored and maps the error
886
+ # into a JetStream error type based on the status and error code.
887
+ def from_error(err)
888
+ return unless err
889
+ case err[:code]
890
+ when 503
891
+ ::NATS::JetStream::Error::ServiceUnavailable.new(err)
892
+ when 500
893
+ ::NATS::JetStream::Error::ServerError.new(err)
894
+ when 404
895
+ case err[:err_code]
896
+ when 10059
897
+ ::NATS::JetStream::Error::StreamNotFound.new(err)
898
+ when 10014
899
+ ::NATS::JetStream::Error::ConsumerNotFound.new(err)
900
+ else
901
+ ::NATS::JetStream::Error::NotFound.new(err)
902
+ end
903
+ when 400
904
+ ::NATS::JetStream::Error::BadRequest.new(err)
905
+ else
906
+ ::NATS::JetStream::API::Error.new(err)
907
+ end
908
+ end
909
+ end
910
+ end
911
+ private_constant :JS
912
+
913
+ #####################
914
+ # #
915
+ # JetStream Errors #
916
+ # #
917
+ #####################
918
+
919
+ # Error is any error that may arise when interacting with JetStream.
920
+ class Error < Error
921
+
922
+ # When there is a NATS::IO::NoResponders error after making a publish request.
923
+ class NoStreamResponse < Error; end
924
+
925
+ # When an invalid durable or consumer name was attempted to be used.
926
+ class InvalidDurableName < Error; end
927
+
928
+ # When an ack not longer valid.
929
+ class InvalidJSAck < Error; end
930
+
931
+ # When an ack has already been acked.
932
+ class MsgAlreadyAckd < Error; end
933
+
934
+ # When the delivered message does not behave as a message delivered by JetStream,
935
+ # for example when the ack reply has unrecognizable fields.
936
+ class NotJSMessage < Error; end
937
+
938
+ # When the stream name is invalid.
939
+ class InvalidStreamName < Error; end
940
+
941
+ # When the consumer name is invalid.
942
+ class InvalidConsumerName < Error; end
943
+
944
+ # When the server responds with an error from the JetStream API.
945
+ class APIError < Error
946
+ attr_reader :code, :err_code, :description, :stream, :seq
947
+
948
+ def initialize(params={})
949
+ @code = params[:code]
950
+ @err_code = params[:err_code]
951
+ @description = params[:description]
952
+ @stream = params[:stream]
953
+ @seq = params[:seq]
954
+ end
955
+
956
+ def to_s
957
+ "#{@description} (status_code=#{@code}, err_code=#{@err_code})"
958
+ end
959
+ end
960
+
961
+ # When JetStream is not currently available, this could be due to JetStream
962
+ # not being enabled or temporarily unavailable due to a leader election when
963
+ # running in cluster mode.
964
+ # This condition is represented with a message that has 503 status code header.
965
+ class ServiceUnavailable < APIError
966
+ def initialize(params={})
967
+ super(params)
968
+ @code ||= 503
969
+ end
970
+ end
971
+
972
+ # When there is a hard failure in the JetStream.
973
+ # This condition is represented with a message that has 500 status code header.
974
+ class ServerError < APIError
975
+ def initialize(params={})
976
+ super(params)
977
+ @code ||= 500
978
+ end
979
+ end
980
+
981
+ # When a JetStream object was not found.
982
+ # This condition is represented with a message that has 404 status code header.
983
+ class NotFound < APIError
984
+ def initialize(params={})
985
+ super(params)
986
+ @code ||= 404
987
+ end
988
+ end
989
+
990
+ # When the stream is not found.
991
+ class StreamNotFound < NotFound; end
992
+
993
+ # When the consumer or durable is not found by name.
994
+ class ConsumerNotFound < NotFound; end
995
+
996
+ # When the JetStream client makes an invalid request.
997
+ # This condition is represented with a message that has 400 status code header.
998
+ class BadRequest < APIError
999
+ def initialize(params={})
1000
+ super(params)
1001
+ @code ||= 400
1002
+ end
1003
+ end
1004
+ end
1005
+
1006
+ #######################
1007
+ # #
1008
+ # JetStream API Types #
1009
+ # #
1010
+ #######################
1011
+
1012
+ # JetStream::API are the types used to interact with the JetStream API.
1013
+ module API
1014
+ # When the server responds with an error from the JetStream API.
1015
+ Error = ::NATS::JetStream::Error::APIError
1016
+
1017
+ # SequenceInfo is a pair of consumer and stream sequence and last activity.
1018
+ # @!attribute consumer_seq
1019
+ # @return [Integer] The consumer sequence.
1020
+ # @!attribute stream_seq
1021
+ # @return [Integer] The stream sequence.
1022
+ SequenceInfo = Struct.new(:consumer_seq, :stream_seq, :last_active,
1023
+ keyword_init: true) do
1024
+ def initialize(opts={})
1025
+ # Filter unrecognized fields and freeze.
1026
+ rem = opts.keys - members
1027
+ opts.delete_if { |k| rem.include?(k) }
1028
+ super(opts)
1029
+ freeze
1030
+ end
1031
+ end
1032
+
1033
+ # ConsumerInfo is the current status of a JetStream consumer.
1034
+ #
1035
+ # @!attribute stream_name
1036
+ # @return [String] name of the stream to which the consumer belongs.
1037
+ # @!attribute name
1038
+ # @return [String] name of the consumer.
1039
+ # @!attribute created
1040
+ # @return [String] time when the consumer was created.
1041
+ # @!attribute config
1042
+ # @return [ConsumerConfig] consumer configuration.
1043
+ # @!attribute delivered
1044
+ # @return [SequenceInfo]
1045
+ # @!attribute ack_floor
1046
+ # @return [SequenceInfo]
1047
+ # @!attribute num_ack_pending
1048
+ # @return [Integer]
1049
+ # @!attribute num_redelivered
1050
+ # @return [Integer]
1051
+ # @!attribute num_waiting
1052
+ # @return [Integer]
1053
+ # @!attribute num_pending
1054
+ # @return [Integer]
1055
+ # @!attribute cluster
1056
+ # @return [Hash]
1057
+ ConsumerInfo = Struct.new(:type, :stream_name, :name, :created,
1058
+ :config, :delivered, :ack_floor,
1059
+ :num_ack_pending, :num_redelivered, :num_waiting,
1060
+ :num_pending, :cluster, :push_bound,
1061
+ keyword_init: true) do
1062
+ def initialize(opts={})
1063
+ opts[:created] = Time.parse(opts[:created])
1064
+ opts[:ack_floor] = SequenceInfo.new(opts[:ack_floor])
1065
+ opts[:delivered] = SequenceInfo.new(opts[:delivered])
1066
+ opts[:config][:ack_wait] = opts[:config][:ack_wait] / 1_000_000_000
1067
+ opts[:config] = ConsumerConfig.new(opts[:config])
1068
+ opts.delete(:cluster)
1069
+ # Filter unrecognized fields just in case.
1070
+ rem = opts.keys - members
1071
+ opts.delete_if { |k| rem.include?(k) }
1072
+ super(opts)
1073
+ freeze
1074
+ end
1075
+ end
1076
+
1077
+ # ConsumerConfig is the consumer configuration.
1078
+ #
1079
+ # @!attribute durable_name
1080
+ # @return [String]
1081
+ # @!attribute deliver_policy
1082
+ # @return [String]
1083
+ # @!attribute ack_policy
1084
+ # @return [String]
1085
+ # @!attribute ack_wait
1086
+ # @return [Integer]
1087
+ # @!attribute max_deliver
1088
+ # @return [Integer]
1089
+ # @!attribute replay_policy
1090
+ # @return [String]
1091
+ # @!attribute max_waiting
1092
+ # @return [Integer]
1093
+ # @!attribute max_ack_pending
1094
+ # @return [Integer]
1095
+ ConsumerConfig = Struct.new(:durable_name, :description, :deliver_subject,
1096
+ :deliver_group, :deliver_policy, :opt_start_seq,
1097
+ :opt_start_time, :ack_policy, :ack_wait, :max_deliver,
1098
+ :filter_subject, :replay_policy, :rate_limit_bps,
1099
+ :sample_freq, :max_waiting, :max_ack_pending,
1100
+ :flow_control, :idle_heartbeat, :headers_only,
1101
+ keyword_init: true) do
1102
+ def initialize(opts={})
1103
+ # Filter unrecognized fields just in case.
1104
+ rem = opts.keys - members
1105
+ opts.delete_if { |k| rem.include?(k) }
1106
+ super(opts)
1107
+ end
1108
+
1109
+ def to_json(*args)
1110
+ config = self.to_h
1111
+ config.delete_if { |_k, v| v.nil? }
1112
+ config.to_json(*args)
1113
+ end
1114
+ end
1115
+
1116
+ # StreamConfig represents the configuration of a stream from JetStream.
1117
+ #
1118
+ # @!attribute type
1119
+ # @return [String]
1120
+ # @!attribute config
1121
+ # @return [Hash]
1122
+ # @!attribute created
1123
+ # @return [String]
1124
+ # @!attribute state
1125
+ # @return [StreamState]
1126
+ # @!attribute did_create
1127
+ # @return [Boolean]
1128
+ # @!attribute name
1129
+ # @return [String]
1130
+ # @!attribute subjects
1131
+ # @return [Array]
1132
+ # @!attribute retention
1133
+ # @return [String]
1134
+ # @!attribute max_consumers
1135
+ # @return [Integer]
1136
+ # @!attribute max_msgs
1137
+ # @return [Integer]
1138
+ # @!attribute max_bytes
1139
+ # @return [Integer]
1140
+ # @!attribute max_age
1141
+ # @return [Integer]
1142
+ # @!attribute max_msgs_per_subject
1143
+ # @return [Integer]
1144
+ # @!attribute max_msg_size
1145
+ # @return [Integer]
1146
+ # @!attribute discard
1147
+ # @return [String]
1148
+ # @!attribute storage
1149
+ # @return [String]
1150
+ # @!attribute num_replicas
1151
+ # @return [Integer]
1152
+ # @!attribute duplicate_window
1153
+ # @return [Integer]
1154
+ StreamConfig = Struct.new(:name, :subjects, :retention, :max_consumers,
1155
+ :max_msgs, :max_bytes, :max_age,
1156
+ :max_msgs_per_subject, :max_msg_size,
1157
+ :discard, :storage, :num_replicas, :duplicate_window,
1158
+ keyword_init: true) do
1159
+ def initialize(opts={})
1160
+ # Filter unrecognized fields just in case.
1161
+ rem = opts.keys - members
1162
+ opts.delete_if { |k| rem.include?(k) }
1163
+ super(opts)
1164
+ end
1165
+
1166
+ def to_json(*args)
1167
+ config = self.to_h
1168
+ config.delete_if { |_k, v| v.nil? }
1169
+ config.to_json(*args)
1170
+ end
1171
+ end
1172
+
1173
+ # StreamInfo is the info about a stream from JetStream.
1174
+ #
1175
+ # @!attribute type
1176
+ # @return [String]
1177
+ # @!attribute config
1178
+ # @return [Hash]
1179
+ # @!attribute created
1180
+ # @return [String]
1181
+ # @!attribute state
1182
+ # @return [Hash]
1183
+ # @!attribute domain
1184
+ # @return [String]
1185
+ StreamInfo = Struct.new(:type, :config, :created, :state, :domain,
1186
+ keyword_init: true) do
1187
+ def initialize(opts={})
1188
+ opts[:config] = StreamConfig.new(opts[:config])
1189
+ opts[:state] = StreamState.new(opts[:state])
1190
+ opts[:created] = Time.parse(opts[:created])
1191
+
1192
+ # Filter fields and freeze.
1193
+ rem = opts.keys - members
1194
+ opts.delete_if { |k| rem.include?(k) }
1195
+ super(opts)
1196
+ freeze
1197
+ end
1198
+ end
1199
+
1200
+ # StreamState is the state of a stream.
1201
+ #
1202
+ # @!attribute messages
1203
+ # @return [Integer]
1204
+ # @!attribute bytes
1205
+ # @return [Integer]
1206
+ # @!attribute first_seq
1207
+ # @return [Integer]
1208
+ # @!attribute last_seq
1209
+ # @return [Integer]
1210
+ # @!attribute consumer_count
1211
+ # @return [Integer]
1212
+ StreamState = Struct.new(:messages, :bytes, :first_seq, :first_ts,
1213
+ :last_seq, :last_ts, :consumer_count,
1214
+ keyword_init: true) do
1215
+ def initialize(opts={})
1216
+ rem = opts.keys - members
1217
+ opts.delete_if { |k| rem.include?(k) }
1218
+ super(opts)
1219
+ end
1220
+ end
1221
+
1222
+ # StreamCreateResponse is the response from the JetStream $JS.API.STREAM.CREATE API.
1223
+ #
1224
+ # @!attribute type
1225
+ # @return [String]
1226
+ # @!attribute config
1227
+ # @return [StreamConfig]
1228
+ # @!attribute created
1229
+ # @return [String]
1230
+ # @!attribute state
1231
+ # @return [StreamState]
1232
+ # @!attribute did_create
1233
+ # @return [Boolean]
1234
+ StreamCreateResponse = Struct.new(:type, :config, :created, :state, :did_create,
1235
+ keyword_init: true) do
1236
+ def initialize(opts={})
1237
+ rem = opts.keys - members
1238
+ opts.delete_if { |k| rem.include?(k) }
1239
+ opts[:config] = StreamConfig.new(opts[:config])
1240
+ opts[:state] = StreamState.new(opts[:state])
1241
+ super(opts)
1242
+ freeze
1243
+ end
1244
+ end
1245
+
1246
+ RawStreamMsg = Struct.new(:subject, :seq, :data, :headers, keyword_init: true) do
1247
+ def initialize(opts)
1248
+ opts[:data] = Base64.decode64(opts[:data]) if opts[:data]
1249
+ if opts[:hdrs]
1250
+ header = Base64.decode64(opts[:hdrs])
1251
+ hdr = {}
1252
+ lines = header.lines
1253
+ lines.slice(1, header.size).each do |line|
1254
+ line.rstrip!
1255
+ next if line.empty?
1256
+ key, value = line.strip.split(/\s*:\s*/, 2)
1257
+ hdr[key] = value
1258
+ end
1259
+ opts[:headers] = hdr
1260
+ end
1261
+
1262
+ # Filter out members not present.
1263
+ rem = opts.keys - members
1264
+ opts.delete_if { |k| rem.include?(k) }
1265
+ super(opts)
1266
+ end
1267
+
1268
+ def sequence
1269
+ self.seq
1270
+ end
1271
+ end
1272
+ end
1273
+ end
1274
+ end