nats-pure 0.6.2 → 2.0.0.pre.rc1

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.
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