nats-pure 0.7.2 → 2.0.0

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,1302 @@
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
+ when JS::Status::RequestTimeout
511
+ # Skip
512
+ else
513
+ raise JS.from_msg(msg)
514
+ end
515
+ else
516
+ msgs << msg
517
+ end
518
+ end
519
+ end
520
+
521
+ # Make lingering request with expiration.
522
+ next_req[:expires] = expires
523
+ if msgs.empty?
524
+ # Make publish request and wait for response.
525
+ @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
526
+
527
+ # Wait for result of fetch or timeout.
528
+ synchronize { wait_for_msgs_cond.wait(timeout) }
529
+
530
+ msgs << @pending_queue.pop unless @pending_queue.empty?
531
+
532
+ duration = MonotonicTime.since(t)
533
+ if duration > timeout
534
+ raise ::NATS::Timeout.new("nats: fetch timeout")
535
+ end
536
+
537
+ # Should have received at least a message at this point,
538
+ # if that is not the case then error already.
539
+ if JS.is_status_msg(msgs.first)
540
+ msg = msgs.first
541
+ case msg.header[JS::Header::Status]
542
+ when JS::Status::RequestTimeout
543
+ raise NATS::Timeout.new("nats: fetch request timeout")
544
+ else
545
+ raise JS.from_msg(msgs.first)
546
+ end
547
+ end
548
+ end
549
+ when batch > 1
550
+ ####################################################
551
+ # Fetch (n) #
552
+ ####################################################
553
+
554
+ # Check if there already enough in the pending buffer.
555
+ synchronize do
556
+ if batch <= @pending_queue.size
557
+ batch.times do
558
+ msg = @pending_queue.pop
559
+
560
+ # Check for a no msgs response status.
561
+ if JS.is_status_msg(msg)
562
+ case msg.header[JS::Header::Status]
563
+ when JS::Status::NoMsgs, JS::Status::RequestTimeout
564
+ # Skip these
565
+ next
566
+ else
567
+ raise JS.from_msg(msg)
568
+ end
569
+ else
570
+ msgs << msg
571
+ end
572
+ end
573
+
574
+ return msgs
575
+ end
576
+ end
577
+
578
+ # Make publish request and wait any response.
579
+ next_req[:no_wait] = true
580
+ @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
581
+
582
+ # Not receiving even one is a timeout.
583
+ start_time = MonotonicTime.now
584
+ msg = nil
585
+ synchronize {
586
+ wait_for_msgs_cond.wait(timeout)
587
+ msg = @pending_queue.pop unless @pending_queue.empty?
588
+ }
589
+
590
+ # Check if the first message was a response saying that
591
+ # there are no messages.
592
+ if !msg.nil? && JS.is_status_msg(msg)
593
+ case msg.header[JS::Header::Status]
594
+ when JS::Status::NoMsgs
595
+ # Make another request that does wait.
596
+ next_req[:expires] = expires
597
+ next_req.delete(:no_wait)
598
+
599
+ @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
600
+ when JS::Status::RequestTimeout
601
+ raise NATS::Timeout.new("nats: fetch request timeout")
602
+ else
603
+ raise JS.from_msg(msg)
604
+ end
605
+ else
606
+ msgs << msg unless msg.nil?
607
+ end
608
+
609
+ # Check if have not received yet a single message.
610
+ duration = MonotonicTime.since(start_time)
611
+ if msgs.empty? and duration > timeout
612
+ raise NATS::Timeout.new("nats: fetch timeout")
613
+ end
614
+
615
+ needed = batch - msgs.count
616
+ while needed > 0 and MonotonicTime.since(start_time) < timeout
617
+ duration = MonotonicTime.since(start_time)
618
+
619
+ # Wait for the rest of the messages.
620
+ synchronize do
621
+
622
+ # Wait until there is a message delivered.
623
+ if @pending_queue.empty?
624
+ deadline = timeout - duration
625
+ wait_for_msgs_cond.wait(deadline) if deadline > 0
626
+
627
+ duration = MonotonicTime.since(start_time)
628
+ if msgs.empty? && @pending_queue.empty? and duration > timeout
629
+ raise NATS::Timeout.new("nats: fetch timeout")
630
+ end
631
+ else
632
+ msg = @pending_queue.pop
633
+
634
+ if JS.is_status_msg(msg)
635
+ case msg.header[JS::Header::Status]
636
+ when JS::Status::NoMsgs, JS::Status::RequestTimeout
637
+ duration = MonotonicTime.since(start_time)
638
+
639
+ if duration > timeout
640
+ # Only received a subset of the messages.
641
+ if !msgs.empty?
642
+ return msgs
643
+ else
644
+ raise NATS::Timeout.new("nats: fetch timeout")
645
+ end
646
+ end
647
+ else
648
+ raise JS.from_msg(msg)
649
+ end
650
+
651
+ else
652
+ # Add to the set of messages that will be returned.
653
+ msgs << msg
654
+ needed -= 1
655
+ end
656
+ end
657
+ end # :end: synchronize
658
+ end
659
+ end
660
+
661
+ msgs
662
+ end
663
+
664
+ # consumer_info retrieves the current status of the pull subscription consumer.
665
+ # @param params [Hash] Options to customize API request.
666
+ # @option params [Float] :timeout Time to wait for response.
667
+ # @return [JetStream::API::ConsumerInfo] The latest ConsumerInfo of the consumer.
668
+ def consumer_info(params={})
669
+ @jsi.js.consumer_info(@jsi.stream, @jsi.consumer, params)
670
+ end
671
+ end
672
+ private_constant :PullSubscription
673
+
674
+ #######################################
675
+ # #
676
+ # JetStream Message and Ack Methods #
677
+ # #
678
+ #######################################
679
+
680
+ # JetStream::Msg module includes the methods so that a regular NATS::Msg
681
+ # can be enhanced with JetStream features like acking and metadata.
682
+ module Msg
683
+ module Ack
684
+ # Ack types
685
+ Ack = ("+ACK".freeze)
686
+ Nak = ("-NAK".freeze)
687
+ Progress = ("+WPI".freeze)
688
+ Term = ("+TERM".freeze)
689
+
690
+ Empty = (''.freeze)
691
+ DotSep = ('.'.freeze)
692
+ NoDomainName = ('_'.freeze)
693
+
694
+ # Position
695
+ Prefix0 = ('$JS'.freeze)
696
+ Prefix1 = ('ACK'.freeze)
697
+ Domain = 2
698
+ AccHash = 3
699
+ Stream = 4
700
+ Consumer = 5
701
+ NumDelivered = 6
702
+ StreamSeq = 7
703
+ ConsumerSeq = 8
704
+ Timestamp = 9
705
+ NumPending = 10
706
+
707
+ # Subject without domain:
708
+ # $JS.ACK.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>
709
+ #
710
+ V1TokenCounts = 9
711
+
712
+ # Subject with domain:
713
+ # $JS.ACK.<domain>.<account hash>.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>.<a token with a random value>
714
+ #
715
+ V2TokenCounts = 12
716
+
717
+ SequencePair = Struct.new(:stream, :consumer)
718
+ end
719
+ private_constant :Ack
720
+
721
+ class Metadata
722
+ attr_reader :sequence, :num_delivered, :num_pending, :timestamp, :stream, :consumer, :domain
723
+
724
+ def initialize(opts)
725
+ @sequence = Ack::SequencePair.new(opts[Ack::StreamSeq].to_i, opts[Ack::ConsumerSeq].to_i)
726
+ @domain = opts[Ack::Domain]
727
+ @num_delivered = opts[Ack::NumDelivered].to_i
728
+ @num_pending = opts[Ack::NumPending].to_i
729
+ @timestamp = Time.at((opts[Ack::Timestamp].to_i / 1_000_000_000.0))
730
+ @stream = opts[Ack::Stream]
731
+ @consumer = opts[Ack::Consumer]
732
+ # TODO: Not exposed in Go client either right now.
733
+ # account = opts[Ack::AccHash]
734
+ end
735
+ end
736
+
737
+ module AckMethods
738
+ def ack(**params)
739
+ ensure_is_acked_once!
740
+
741
+ resp = if params[:timeout]
742
+ @nc.request(@reply, Ack::Ack, **params)
743
+ else
744
+ @nc.publish(@reply, Ack::Ack)
745
+ end
746
+ @sub.synchronize { @ackd = true }
747
+
748
+ resp
749
+ end
750
+
751
+ def ack_sync(**params)
752
+ ensure_is_acked_once!
753
+
754
+ params[:timeout] ||= 0.5
755
+ resp = @nc.request(@reply, Ack::Ack, **params)
756
+ @sub.synchronize { @ackd = true }
757
+
758
+ resp
759
+ end
760
+
761
+ def nak(**params)
762
+ ensure_is_acked_once!
763
+
764
+ resp = if params[:timeout]
765
+ @nc.request(@reply, Ack::Nak, **params)
766
+ else
767
+ @nc.publish(@reply, Ack::Nak)
768
+ end
769
+ @sub.synchronize { @ackd = true }
770
+
771
+ resp
772
+ end
773
+
774
+ def term(**params)
775
+ ensure_is_acked_once!
776
+
777
+ resp = if params[:timeout]
778
+ @nc.request(@reply, Ack::Term, **params)
779
+ else
780
+ @nc.publish(@reply, Ack::Term)
781
+ end
782
+ @sub.synchronize { @ackd = true }
783
+
784
+ resp
785
+ end
786
+
787
+ def in_progress(**params)
788
+ params[:timeout] ? @nc.request(@reply, Ack::Progress, **params) : @nc.publish(@reply, Ack::Progress)
789
+ end
790
+
791
+ def metadata
792
+ @meta ||= parse_metadata(reply)
793
+ end
794
+
795
+ private
796
+
797
+ def ensure_is_acked_once!
798
+ @sub.synchronize do
799
+ if @ackd
800
+ raise JetStream::Error::MsgAlreadyAckd.new("nats: message was already acknowledged: #{self}")
801
+ end
802
+ end
803
+ end
804
+
805
+ def parse_metadata(reply)
806
+ tokens = reply.split(Ack::DotSep)
807
+ n = tokens.count
808
+
809
+ case
810
+ when n < Ack::V1TokenCounts || (n > Ack::V1TokenCounts and n < Ack::V2TokenCounts)
811
+ raise NotJSMessage.new("nats: not a jetstream message")
812
+ when tokens[0] != Ack::Prefix0 || tokens[1] != Ack::Prefix1
813
+ raise NotJSMessage.new("nats: not a jetstream message")
814
+ when n == Ack::V1TokenCounts
815
+ tokens.insert(Ack::Domain, Ack::Empty)
816
+ tokens.insert(Ack::AccHash, Ack::Empty)
817
+ when tokens[Ack::Domain] == Ack::NoDomainName
818
+ tokens[Ack::Domain] = Ack::Empty
819
+ end
820
+
821
+ Metadata.new(tokens)
822
+ end
823
+ end
824
+ end
825
+
826
+ ####################################
827
+ # #
828
+ # JetStream Configuration Options #
829
+ # #
830
+ ####################################
831
+
832
+ # Misc internal functions to support JS API.
833
+ # @private
834
+ module JS
835
+ DefaultAPIPrefix = ("$JS.API".freeze)
836
+
837
+ module Status
838
+ CtrlMsg = ("100".freeze)
839
+ NoMsgs = ("404".freeze)
840
+ NotFound = ("404".freeze)
841
+ RequestTimeout = ("408".freeze)
842
+ ServiceUnavailable = ("503".freeze)
843
+ end
844
+
845
+ module Header
846
+ Status = ("Status".freeze)
847
+ Desc = ("Description".freeze)
848
+ MsgID = ("Nats-Msg-Id".freeze)
849
+ ExpectedStream = ("Nats-Expected-Stream".freeze)
850
+ ExpectedLastSeq = ("Nats-Expected-Last-Sequence".freeze)
851
+ ExpectedLastSubjSeq = ("Nats-Expected-Last-Subject-Sequence".freeze)
852
+ ExpectedLastMsgID = ("Nats-Expected-Last-Msg-Id".freeze)
853
+ LastConsumerSeq = ("Nats-Last-Consumer".freeze)
854
+ LastStreamSeq = ("Nats-Last-Stream".freeze)
855
+ end
856
+
857
+ module Config
858
+ # AckPolicy
859
+ AckExplicit = ("explicit".freeze)
860
+ AckAll = ("all".freeze)
861
+ AckNone = ("none".freeze)
862
+ end
863
+
864
+ class Sub
865
+ attr_reader :js, :stream, :consumer, :nms
866
+
867
+ def initialize(opts={})
868
+ @js = opts[:js]
869
+ @stream = opts[:stream]
870
+ @consumer = opts[:consumer]
871
+ @nms = opts[:nms]
872
+ end
873
+ end
874
+
875
+ class << self
876
+ def next_req_to_json(next_req)
877
+ req = {}
878
+ req[:batch] = next_req[:batch]
879
+ req[:expires] = next_req[:expires].to_i if next_req[:expires]
880
+ req[:no_wait] = next_req[:no_wait] if next_req[:no_wait]
881
+ req.to_json
882
+ end
883
+
884
+ def is_status_msg(msg)
885
+ return (!msg.nil? and (!msg.header.nil? and msg.header[Header::Status]))
886
+ end
887
+
888
+ # check_503_error raises exception when a NATS::Msg has a 503 status header.
889
+ # @param msg [NATS::Msg] The message with status headers.
890
+ # @raise [NATS::JetStream::Error::ServiceUnavailable]
891
+ def check_503_error(msg)
892
+ return if msg.nil? or msg.header.nil?
893
+ if msg.header[Header::Status] == Status::ServiceUnavailable
894
+ raise ::NATS::JetStream::Error::ServiceUnavailable
895
+ end
896
+ end
897
+
898
+ # from_msg takes a plain NATS::Msg and checks its headers to confirm
899
+ # if it was an error:
900
+ #
901
+ # msg.header={"Status"=>"503"})
902
+ # msg.header={"Status"=>"408", "Description"=>"Request Timeout"})
903
+ #
904
+ # @param msg [NATS::Msg] The message with status headers.
905
+ # @return [NATS::JetStream::API::Error]
906
+ def from_msg(msg)
907
+ check_503_error(msg)
908
+ code = msg.header[JS::Header::Status]
909
+ desc = msg.header[JS::Header::Desc]
910
+ return ::NATS::JetStream::API::Error.new({code: code, description: desc})
911
+ end
912
+
913
+ # from_error takes an API response that errored and maps the error
914
+ # into a JetStream error type based on the status and error code.
915
+ def from_error(err)
916
+ return unless err
917
+ case err[:code]
918
+ when 503
919
+ ::NATS::JetStream::Error::ServiceUnavailable.new(err)
920
+ when 500
921
+ ::NATS::JetStream::Error::ServerError.new(err)
922
+ when 404
923
+ case err[:err_code]
924
+ when 10059
925
+ ::NATS::JetStream::Error::StreamNotFound.new(err)
926
+ when 10014
927
+ ::NATS::JetStream::Error::ConsumerNotFound.new(err)
928
+ else
929
+ ::NATS::JetStream::Error::NotFound.new(err)
930
+ end
931
+ when 400
932
+ ::NATS::JetStream::Error::BadRequest.new(err)
933
+ else
934
+ ::NATS::JetStream::API::Error.new(err)
935
+ end
936
+ end
937
+ end
938
+ end
939
+ private_constant :JS
940
+
941
+ #####################
942
+ # #
943
+ # JetStream Errors #
944
+ # #
945
+ #####################
946
+
947
+ # Error is any error that may arise when interacting with JetStream.
948
+ class Error < Error
949
+
950
+ # When there is a NATS::IO::NoResponders error after making a publish request.
951
+ class NoStreamResponse < Error; end
952
+
953
+ # When an invalid durable or consumer name was attempted to be used.
954
+ class InvalidDurableName < Error; end
955
+
956
+ # When an ack not longer valid.
957
+ class InvalidJSAck < Error; end
958
+
959
+ # When an ack has already been acked.
960
+ class MsgAlreadyAckd < Error; end
961
+
962
+ # When the delivered message does not behave as a message delivered by JetStream,
963
+ # for example when the ack reply has unrecognizable fields.
964
+ class NotJSMessage < Error; end
965
+
966
+ # When the stream name is invalid.
967
+ class InvalidStreamName < Error; end
968
+
969
+ # When the consumer name is invalid.
970
+ class InvalidConsumerName < Error; end
971
+
972
+ # When the server responds with an error from the JetStream API.
973
+ class APIError < Error
974
+ attr_reader :code, :err_code, :description, :stream, :seq
975
+
976
+ def initialize(params={})
977
+ @code = params[:code]
978
+ @err_code = params[:err_code]
979
+ @description = params[:description]
980
+ @stream = params[:stream]
981
+ @seq = params[:seq]
982
+ end
983
+
984
+ def to_s
985
+ "#{@description} (status_code=#{@code}, err_code=#{@err_code})"
986
+ end
987
+ end
988
+
989
+ # When JetStream is not currently available, this could be due to JetStream
990
+ # not being enabled or temporarily unavailable due to a leader election when
991
+ # running in cluster mode.
992
+ # This condition is represented with a message that has 503 status code header.
993
+ class ServiceUnavailable < APIError
994
+ def initialize(params={})
995
+ super(params)
996
+ @code ||= 503
997
+ end
998
+ end
999
+
1000
+ # When there is a hard failure in the JetStream.
1001
+ # This condition is represented with a message that has 500 status code header.
1002
+ class ServerError < APIError
1003
+ def initialize(params={})
1004
+ super(params)
1005
+ @code ||= 500
1006
+ end
1007
+ end
1008
+
1009
+ # When a JetStream object was not found.
1010
+ # This condition is represented with a message that has 404 status code header.
1011
+ class NotFound < APIError
1012
+ def initialize(params={})
1013
+ super(params)
1014
+ @code ||= 404
1015
+ end
1016
+ end
1017
+
1018
+ # When the stream is not found.
1019
+ class StreamNotFound < NotFound; end
1020
+
1021
+ # When the consumer or durable is not found by name.
1022
+ class ConsumerNotFound < NotFound; end
1023
+
1024
+ # When the JetStream client makes an invalid request.
1025
+ # This condition is represented with a message that has 400 status code header.
1026
+ class BadRequest < APIError
1027
+ def initialize(params={})
1028
+ super(params)
1029
+ @code ||= 400
1030
+ end
1031
+ end
1032
+ end
1033
+
1034
+ #######################
1035
+ # #
1036
+ # JetStream API Types #
1037
+ # #
1038
+ #######################
1039
+
1040
+ # JetStream::API are the types used to interact with the JetStream API.
1041
+ module API
1042
+ # When the server responds with an error from the JetStream API.
1043
+ Error = ::NATS::JetStream::Error::APIError
1044
+
1045
+ # SequenceInfo is a pair of consumer and stream sequence and last activity.
1046
+ # @!attribute consumer_seq
1047
+ # @return [Integer] The consumer sequence.
1048
+ # @!attribute stream_seq
1049
+ # @return [Integer] The stream sequence.
1050
+ SequenceInfo = Struct.new(:consumer_seq, :stream_seq, :last_active,
1051
+ keyword_init: true) do
1052
+ def initialize(opts={})
1053
+ # Filter unrecognized fields and freeze.
1054
+ rem = opts.keys - members
1055
+ opts.delete_if { |k| rem.include?(k) }
1056
+ super(opts)
1057
+ freeze
1058
+ end
1059
+ end
1060
+
1061
+ # ConsumerInfo is the current status of a JetStream consumer.
1062
+ #
1063
+ # @!attribute stream_name
1064
+ # @return [String] name of the stream to which the consumer belongs.
1065
+ # @!attribute name
1066
+ # @return [String] name of the consumer.
1067
+ # @!attribute created
1068
+ # @return [String] time when the consumer was created.
1069
+ # @!attribute config
1070
+ # @return [ConsumerConfig] consumer configuration.
1071
+ # @!attribute delivered
1072
+ # @return [SequenceInfo]
1073
+ # @!attribute ack_floor
1074
+ # @return [SequenceInfo]
1075
+ # @!attribute num_ack_pending
1076
+ # @return [Integer]
1077
+ # @!attribute num_redelivered
1078
+ # @return [Integer]
1079
+ # @!attribute num_waiting
1080
+ # @return [Integer]
1081
+ # @!attribute num_pending
1082
+ # @return [Integer]
1083
+ # @!attribute cluster
1084
+ # @return [Hash]
1085
+ ConsumerInfo = Struct.new(:type, :stream_name, :name, :created,
1086
+ :config, :delivered, :ack_floor,
1087
+ :num_ack_pending, :num_redelivered, :num_waiting,
1088
+ :num_pending, :cluster, :push_bound,
1089
+ keyword_init: true) do
1090
+ def initialize(opts={})
1091
+ opts[:created] = Time.parse(opts[:created])
1092
+ opts[:ack_floor] = SequenceInfo.new(opts[:ack_floor])
1093
+ opts[:delivered] = SequenceInfo.new(opts[:delivered])
1094
+ opts[:config][:ack_wait] = opts[:config][:ack_wait] / 1_000_000_000
1095
+ opts[:config] = ConsumerConfig.new(opts[:config])
1096
+ opts.delete(:cluster)
1097
+ # Filter unrecognized fields just in case.
1098
+ rem = opts.keys - members
1099
+ opts.delete_if { |k| rem.include?(k) }
1100
+ super(opts)
1101
+ freeze
1102
+ end
1103
+ end
1104
+
1105
+ # ConsumerConfig is the consumer configuration.
1106
+ #
1107
+ # @!attribute durable_name
1108
+ # @return [String]
1109
+ # @!attribute deliver_policy
1110
+ # @return [String]
1111
+ # @!attribute ack_policy
1112
+ # @return [String]
1113
+ # @!attribute ack_wait
1114
+ # @return [Integer]
1115
+ # @!attribute max_deliver
1116
+ # @return [Integer]
1117
+ # @!attribute replay_policy
1118
+ # @return [String]
1119
+ # @!attribute max_waiting
1120
+ # @return [Integer]
1121
+ # @!attribute max_ack_pending
1122
+ # @return [Integer]
1123
+ ConsumerConfig = Struct.new(:durable_name, :description, :deliver_subject,
1124
+ :deliver_group, :deliver_policy, :opt_start_seq,
1125
+ :opt_start_time, :ack_policy, :ack_wait, :max_deliver,
1126
+ :filter_subject, :replay_policy, :rate_limit_bps,
1127
+ :sample_freq, :max_waiting, :max_ack_pending,
1128
+ :flow_control, :idle_heartbeat, :headers_only,
1129
+ keyword_init: true) do
1130
+ def initialize(opts={})
1131
+ # Filter unrecognized fields just in case.
1132
+ rem = opts.keys - members
1133
+ opts.delete_if { |k| rem.include?(k) }
1134
+ super(opts)
1135
+ end
1136
+
1137
+ def to_json(*args)
1138
+ config = self.to_h
1139
+ config.delete_if { |_k, v| v.nil? }
1140
+ config.to_json(*args)
1141
+ end
1142
+ end
1143
+
1144
+ # StreamConfig represents the configuration of a stream from JetStream.
1145
+ #
1146
+ # @!attribute type
1147
+ # @return [String]
1148
+ # @!attribute config
1149
+ # @return [Hash]
1150
+ # @!attribute created
1151
+ # @return [String]
1152
+ # @!attribute state
1153
+ # @return [StreamState]
1154
+ # @!attribute did_create
1155
+ # @return [Boolean]
1156
+ # @!attribute name
1157
+ # @return [String]
1158
+ # @!attribute subjects
1159
+ # @return [Array]
1160
+ # @!attribute retention
1161
+ # @return [String]
1162
+ # @!attribute max_consumers
1163
+ # @return [Integer]
1164
+ # @!attribute max_msgs
1165
+ # @return [Integer]
1166
+ # @!attribute max_bytes
1167
+ # @return [Integer]
1168
+ # @!attribute max_age
1169
+ # @return [Integer]
1170
+ # @!attribute max_msgs_per_subject
1171
+ # @return [Integer]
1172
+ # @!attribute max_msg_size
1173
+ # @return [Integer]
1174
+ # @!attribute discard
1175
+ # @return [String]
1176
+ # @!attribute storage
1177
+ # @return [String]
1178
+ # @!attribute num_replicas
1179
+ # @return [Integer]
1180
+ # @!attribute duplicate_window
1181
+ # @return [Integer]
1182
+ StreamConfig = Struct.new(:name, :subjects, :retention, :max_consumers,
1183
+ :max_msgs, :max_bytes, :max_age,
1184
+ :max_msgs_per_subject, :max_msg_size,
1185
+ :discard, :storage, :num_replicas, :duplicate_window,
1186
+ keyword_init: true) do
1187
+ def initialize(opts={})
1188
+ # Filter unrecognized fields just in case.
1189
+ rem = opts.keys - members
1190
+ opts.delete_if { |k| rem.include?(k) }
1191
+ super(opts)
1192
+ end
1193
+
1194
+ def to_json(*args)
1195
+ config = self.to_h
1196
+ config.delete_if { |_k, v| v.nil? }
1197
+ config.to_json(*args)
1198
+ end
1199
+ end
1200
+
1201
+ # StreamInfo is the info about a stream from JetStream.
1202
+ #
1203
+ # @!attribute type
1204
+ # @return [String]
1205
+ # @!attribute config
1206
+ # @return [Hash]
1207
+ # @!attribute created
1208
+ # @return [String]
1209
+ # @!attribute state
1210
+ # @return [Hash]
1211
+ # @!attribute domain
1212
+ # @return [String]
1213
+ StreamInfo = Struct.new(:type, :config, :created, :state, :domain,
1214
+ keyword_init: true) do
1215
+ def initialize(opts={})
1216
+ opts[:config] = StreamConfig.new(opts[:config])
1217
+ opts[:state] = StreamState.new(opts[:state])
1218
+ opts[:created] = Time.parse(opts[:created])
1219
+
1220
+ # Filter fields and freeze.
1221
+ rem = opts.keys - members
1222
+ opts.delete_if { |k| rem.include?(k) }
1223
+ super(opts)
1224
+ freeze
1225
+ end
1226
+ end
1227
+
1228
+ # StreamState is the state of a stream.
1229
+ #
1230
+ # @!attribute messages
1231
+ # @return [Integer]
1232
+ # @!attribute bytes
1233
+ # @return [Integer]
1234
+ # @!attribute first_seq
1235
+ # @return [Integer]
1236
+ # @!attribute last_seq
1237
+ # @return [Integer]
1238
+ # @!attribute consumer_count
1239
+ # @return [Integer]
1240
+ StreamState = Struct.new(:messages, :bytes, :first_seq, :first_ts,
1241
+ :last_seq, :last_ts, :consumer_count,
1242
+ keyword_init: true) do
1243
+ def initialize(opts={})
1244
+ rem = opts.keys - members
1245
+ opts.delete_if { |k| rem.include?(k) }
1246
+ super(opts)
1247
+ end
1248
+ end
1249
+
1250
+ # StreamCreateResponse is the response from the JetStream $JS.API.STREAM.CREATE API.
1251
+ #
1252
+ # @!attribute type
1253
+ # @return [String]
1254
+ # @!attribute config
1255
+ # @return [StreamConfig]
1256
+ # @!attribute created
1257
+ # @return [String]
1258
+ # @!attribute state
1259
+ # @return [StreamState]
1260
+ # @!attribute did_create
1261
+ # @return [Boolean]
1262
+ StreamCreateResponse = Struct.new(:type, :config, :created, :state, :did_create,
1263
+ keyword_init: true) do
1264
+ def initialize(opts={})
1265
+ rem = opts.keys - members
1266
+ opts.delete_if { |k| rem.include?(k) }
1267
+ opts[:config] = StreamConfig.new(opts[:config])
1268
+ opts[:state] = StreamState.new(opts[:state])
1269
+ super(opts)
1270
+ freeze
1271
+ end
1272
+ end
1273
+
1274
+ RawStreamMsg = Struct.new(:subject, :seq, :data, :headers, keyword_init: true) do
1275
+ def initialize(opts)
1276
+ opts[:data] = Base64.decode64(opts[:data]) if opts[:data]
1277
+ if opts[:hdrs]
1278
+ header = Base64.decode64(opts[:hdrs])
1279
+ hdr = {}
1280
+ lines = header.lines
1281
+ lines.slice(1, header.size).each do |line|
1282
+ line.rstrip!
1283
+ next if line.empty?
1284
+ key, value = line.strip.split(/\s*:\s*/, 2)
1285
+ hdr[key] = value
1286
+ end
1287
+ opts[:headers] = hdr
1288
+ end
1289
+
1290
+ # Filter out members not present.
1291
+ rem = opts.keys - members
1292
+ opts.delete_if { |k| rem.include?(k) }
1293
+ super(opts)
1294
+ end
1295
+
1296
+ def sequence
1297
+ self.seq
1298
+ end
1299
+ end
1300
+ end
1301
+ end
1302
+ end