nats-pure 0.7.2 → 2.0.0.pre.alpha

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