nats-pure 0.7.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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