nats-pure 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f0d4251703ba9fd6a91f501686025e2f71e93dc7869f65984da7cd312361266
4
- data.tar.gz: 411fdfd23878cf89b26c76e04b68c760e10aaef14f2a68fae19c69a8d1392990
3
+ metadata.gz: f1d8302826b885f0c3a173530bd3d504b5e7a4eb7ec684ac3b43b1ee8f9cf4e3
4
+ data.tar.gz: 04aa8801b7ddf434155d1ebf40b03fdd6b89bfc6f95d968ff4a501684081ca02
5
5
  SHA512:
6
- metadata.gz: a3b88a713e2c1b6a515ac4f505cb6724c0059e72693d8d5c2ee61f3579d1b4b846408282e131864735dccc8b224e4a22933be91409d404fedadf0ec6c72051cf
7
- data.tar.gz: 9ef88d1a391c2cfec91731aca2742382634c923384b801fce4ee9155418de39bd538f7aaafceb6ba2a1b40b57c6b7db4363ad4cf673d8ff3b5e87b5f74104789
6
+ metadata.gz: a98b75d47e4b8256c05d284324399807e0bbaf210f2970856b106b46a1264cc12bd96cb00516cb007fd9c68cab001bf08516aee1db46718552bd7397cf8ccb2f
7
+ data.tar.gz: b752276fa498c423ecdd0002e530639514ef39a1f5c93e73606c6a6f0351cf275817e13c0339d880fadef9e956fdf7b1f63cc816ff0cd2a2e254db73e071344c
@@ -1936,6 +1936,8 @@ module NATS
1936
1936
  end
1937
1937
  end
1938
1938
 
1939
+ NANOSECONDS = 1_000_000_000
1940
+
1939
1941
  class MonotonicTime
1940
1942
  # Implementation of MonotonicTime adapted from
1941
1943
  # https://github.com/ruby-concurrency/concurrent-ruby/
data/lib/nats/io/js.rb CHANGED
@@ -19,7 +19,6 @@ require 'time'
19
19
  require 'base64'
20
20
 
21
21
  module NATS
22
-
23
22
  # JetStream returns a context with a similar API as the NATS::Client
24
23
  # but with enhanced functions to persist and consume messages from
25
24
  # the NATS JetStream engine.
@@ -118,6 +117,7 @@ module NATS
118
117
  manual_ack = params[:manual_ack]
119
118
  idle_heartbeat = params[:idle_heartbeat]
120
119
  flow_control = params[:flow_control]
120
+ config = params[:config]
121
121
 
122
122
  if queue
123
123
  if durable and durable != queue
@@ -184,7 +184,7 @@ module NATS
184
184
  config.flow_control = flow_control
185
185
  if idle_heartbeat or config.idle_heartbeat
186
186
  idle_heartbeat = config.idle_heartbeat if config.idle_heartbeat
187
- idle_heartbeat = idle_heartbeat * 1_000_000_000
187
+ idle_heartbeat = idle_heartbeat * ::NATS::NANOSECONDS
188
188
  config.idle_heartbeat = idle_heartbeat
189
189
  end
190
190
 
@@ -222,7 +222,9 @@ module NATS
222
222
  # @option params [Hash] :config Configuration for the consumer.
223
223
  # @return [NATS::JetStream::PullSubscription]
224
224
  def pull_subscribe(subject, durable, params={})
225
- raise JetStream::Error::InvalidDurableName.new("nats: invalid durable name") if durable.empty?
225
+ if durable.empty? && !params[:consumer]
226
+ raise JetStream::Error::InvalidDurableName.new("nats: invalid durable name")
227
+ end
226
228
  params[:consumer] ||= durable
227
229
  stream = params[:stream].nil? ? find_stream_name_by_subject(subject) : params[:stream]
228
230
 
@@ -329,7 +331,16 @@ module NATS
329
331
  else
330
332
  config
331
333
  end
332
- req_subject = if config[:durable_name]
334
+
335
+ req_subject = case
336
+ when config[:name]
337
+ # NOTE: Only supported after nats-server v2.9.0
338
+ if config[:filter_subject] && config[:filter_subject] != ">"
339
+ "#{@prefix}.CONSUMER.CREATE.#{stream}.#{config[:name]}.#{config[:filter_subject]}"
340
+ else
341
+ "#{@prefix}.CONSUMER.CREATE.#{stream}.#{config[:name]}"
342
+ end
343
+ when config[:durable_name]
333
344
  "#{@prefix}.CONSUMER.DURABLE.CREATE.#{stream}.#{config[:durable_name]}"
334
345
  else
335
346
  "#{@prefix}.CONSUMER.CREATE.#{stream}"
@@ -339,7 +350,11 @@ module NATS
339
350
  # Check if have to normalize ack wait so that it is in nanoseconds for Go compat.
340
351
  if config[:ack_wait]
341
352
  raise ArgumentError.new("nats: invalid ack wait") unless config[:ack_wait].is_a?(Integer)
342
- config[:ack_wait] = config[:ack_wait] * 1_000_000_000
353
+ config[:ack_wait] = config[:ack_wait] * ::NATS::NANOSECONDS
354
+ end
355
+ if config[:inactive_threshold]
356
+ raise ArgumentError.new("nats: invalid inactive threshold") unless config[:inactive_threshold].is_a?(Integer)
357
+ config[:inactive_threshold] = config[:inactive_threshold] * ::NATS::NANOSECONDS
343
358
  end
344
359
 
345
360
  req = {
@@ -396,28 +411,96 @@ module NATS
396
411
  result[:streams].first
397
412
  end
398
413
 
399
- def get_last_msg(stream_name, subject)
400
- req_subject = "#{@prefix}.STREAM.MSG.GET.#{stream_name}"
401
- req = {'last_by_subj': subject}
414
+ # get_msg retrieves a message from the stream.
415
+ # @param next [Boolean] Fetch the next message for a subject.
416
+ # @param seq [Integer] Sequence number of a message.
417
+ # @param subject [String] Subject of the message.
418
+ # @param direct [Boolean] Use direct mode to for faster access (requires NATS v2.9.0)
419
+ def get_msg(stream_name, params={})
420
+ req = {}
421
+ case
422
+ when params[:next]
423
+ req[:seq] = params[:seq]
424
+ req[:next_by_subj] = params[:subject]
425
+ when params[:seq]
426
+ req[:seq] = params[:seq]
427
+ when params[:subject]
428
+ req[:last_by_subj] = params[:subject]
429
+ end
430
+
402
431
  data = req.to_json
403
- resp = api_request(req_subject, data)
404
- JetStream::API::RawStreamMsg.new(resp[:message])
432
+ if params[:direct]
433
+ if params[:subject] and not params[:seq]
434
+ # last_by_subject type request requires no payload.
435
+ data = ''
436
+ req_subject = "#{@prefix}.DIRECT.GET.#{stream_name}.#{params[:subject]}"
437
+ else
438
+ req_subject = "#{@prefix}.DIRECT.GET.#{stream_name}"
439
+ end
440
+ else
441
+ req_subject = "#{@prefix}.STREAM.MSG.GET.#{stream_name}"
442
+ end
443
+ resp = api_request(req_subject, data, direct: params[:direct])
444
+ msg = if params[:direct]
445
+ _lift_msg_to_raw_msg(resp)
446
+ else
447
+ JetStream::API::RawStreamMsg.new(resp[:message])
448
+ end
449
+
450
+ msg
451
+ end
452
+
453
+ def get_last_msg(stream_name, subject, params={})
454
+ params[:subject] = subject
455
+ get_msg(stream_name, params)
456
+ end
457
+
458
+ def account_info
459
+ api_request("#{@prefix}.INFO")
405
460
  end
406
461
 
407
462
  private
408
463
 
409
464
  def api_request(req_subject, req="", params={})
410
465
  params[:timeout] ||= @opts[:timeout]
411
- result = begin
412
- msg = @nc.request(req_subject, req, **params)
466
+ msg = begin
467
+ @nc.request(req_subject, req, **params)
468
+ rescue NATS::IO::NoRespondersError
469
+ raise JetStream::Error::ServiceUnavailable
470
+ end
471
+
472
+ result = if params[:direct]
473
+ msg
474
+ else
413
475
  JSON.parse(msg.data, symbolize_names: true)
414
- rescue NATS::IO::NoRespondersError
415
- raise JetStream::Error::ServiceUnavailable
416
476
  end
417
- raise JS.from_error(result[:error]) if result[:error]
477
+ if result.is_a?(Hash) and result[:error]
478
+ raise JS.from_error(result[:error])
479
+ end
418
480
 
419
481
  result
420
482
  end
483
+
484
+ def _lift_msg_to_raw_msg(msg)
485
+ if msg.header and msg.header['Status']
486
+ status = msg.header['Status']
487
+ if status == '404'
488
+ raise ::NATS::JetStream::Error::NotFound.new
489
+ else
490
+ raise JS.from_msg(msg)
491
+ end
492
+ end
493
+ subject = msg.header['Nats-Subject']
494
+ seq = msg.header['Nats-Sequence']
495
+ raw_msg = JetStream::API::RawStreamMsg.new(
496
+ subject: subject,
497
+ seq: seq,
498
+ headers: msg.header,
499
+ )
500
+ raw_msg.data = msg.data
501
+
502
+ raw_msg
503
+ end
421
504
  end
422
505
 
423
506
  # PushSubscription is included into NATS::Subscription so that it
@@ -1106,7 +1189,7 @@ module NATS
1106
1189
  opts[:created] = Time.parse(opts[:created])
1107
1190
  opts[:ack_floor] = SequenceInfo.new(opts[:ack_floor])
1108
1191
  opts[:delivered] = SequenceInfo.new(opts[:delivered])
1109
- opts[:config][:ack_wait] = opts[:config][:ack_wait] / 1_000_000_000
1192
+ opts[:config][:ack_wait] = opts[:config][:ack_wait] / ::NATS::NANOSECONDS
1110
1193
  opts[:config] = ConsumerConfig.new(opts[:config])
1111
1194
  opts.delete(:cluster)
1112
1195
  # Filter unrecognized fields just in case.
@@ -1135,12 +1218,24 @@ module NATS
1135
1218
  # @return [Integer]
1136
1219
  # @!attribute max_ack_pending
1137
1220
  # @return [Integer]
1138
- ConsumerConfig = Struct.new(:durable_name, :description, :deliver_subject,
1139
- :deliver_group, :deliver_policy, :opt_start_seq,
1140
- :opt_start_time, :ack_policy, :ack_wait, :max_deliver,
1221
+ ConsumerConfig = Struct.new(:name, :durable_name, :description,
1222
+ :deliver_policy, :opt_start_seq, :opt_start_time,
1223
+ :ack_policy, :ack_wait, :max_deliver, :backoff,
1141
1224
  :filter_subject, :replay_policy, :rate_limit_bps,
1142
1225
  :sample_freq, :max_waiting, :max_ack_pending,
1143
1226
  :flow_control, :idle_heartbeat, :headers_only,
1227
+
1228
+ # Pull based options
1229
+ :max_batch, :max_expires,
1230
+ # Push based consumers
1231
+ :deliver_subject, :deliver_group,
1232
+ # Ephemeral inactivity threshold
1233
+ :inactive_threshold,
1234
+ # Generally inherited by parent stream and other markers,
1235
+ # now can be configured directly.
1236
+ :num_replicas,
1237
+ # Force memory storage
1238
+ :mem_storage,
1144
1239
  keyword_init: true) do
1145
1240
  def initialize(opts={})
1146
1241
  # Filter unrecognized fields just in case.
@@ -1194,10 +1289,32 @@ module NATS
1194
1289
  # @return [Integer]
1195
1290
  # @!attribute duplicate_window
1196
1291
  # @return [Integer]
1197
- StreamConfig = Struct.new(:name, :subjects, :retention, :max_consumers,
1198
- :max_msgs, :max_bytes, :max_age,
1199
- :max_msgs_per_subject, :max_msg_size,
1200
- :discard, :storage, :num_replicas, :duplicate_window,
1292
+ StreamConfig = Struct.new(
1293
+ :name,
1294
+ :description,
1295
+ :subjects,
1296
+ :retention,
1297
+ :max_consumers,
1298
+ :max_msgs,
1299
+ :max_bytes,
1300
+ :discard,
1301
+ :max_age,
1302
+ :max_msgs_per_subject,
1303
+ :max_msg_size,
1304
+ :storage,
1305
+ :num_replicas,
1306
+ :no_ack,
1307
+ :duplicate_window,
1308
+ :placement,
1309
+ :mirror,
1310
+ :sources,
1311
+ :sealed,
1312
+ :deny_delete,
1313
+ :deny_purge,
1314
+ :allow_rollup_hdrs,
1315
+ :republish,
1316
+ :allow_direct,
1317
+ :mirror_direct,
1201
1318
  keyword_init: true) do
1202
1319
  def initialize(opts={})
1203
1320
  # Filter unrecognized fields just in case.
@@ -1230,7 +1347,7 @@ module NATS
1230
1347
  def initialize(opts={})
1231
1348
  opts[:config] = StreamConfig.new(opts[:config])
1232
1349
  opts[:state] = StreamState.new(opts[:state])
1233
- opts[:created] = Time.parse(opts[:created])
1350
+ opts[:created] = ::Time.parse(opts[:created])
1234
1351
 
1235
1352
  # Filter fields and freeze.
1236
1353
  rem = opts.keys - members
data/lib/nats/io/kv.rb CHANGED
@@ -20,46 +20,181 @@ module NATS
20
20
  KV_PURGE = "PURGE"
21
21
  MSG_ROLLUP_SUBJECT = "sub"
22
22
  MSG_ROLLUP_ALL = "all"
23
+ ROLLUP = "Nats-Rollup"
23
24
 
24
25
  def initialize(opts={})
25
26
  @name = opts[:name]
26
27
  @stream = opts[:stream]
27
28
  @pre = opts[:pre]
28
29
  @js = opts[:js]
30
+ @direct = opts[:direct]
31
+ end
32
+
33
+ class Error < NATS::Error; end
34
+
35
+ # When a key is not found.
36
+ class KeyNotFoundError < Error
37
+ attr_reader :entry, :op
38
+ def initialize(params={})
39
+ @entry = params[:entry]
40
+ @op = params[:op]
41
+ @message = params[:message]
42
+ end
43
+
44
+ def to_s
45
+ msg = "nats: key not found"
46
+ msg = "#{msg}: #{@message}" if @message
47
+ msg
48
+ end
29
49
  end
30
50
 
31
51
  # When a key is not found because it was deleted.
32
- class KeyDeletedError < NATS::Error; end
52
+ class KeyDeletedError < KeyNotFoundError
53
+ def to_s
54
+ "nats: key was deleted"
55
+ end
56
+ end
33
57
 
34
58
  # When there was no bucket present.
35
- class BucketNotFoundError < NATS::Error; end
59
+ class BucketNotFoundError < Error; end
36
60
 
37
61
  # When it is an invalid bucket.
38
- class BadBucketError < NATS::Error; end
62
+ class BadBucketError < Error; end
63
+
64
+ # When the result is an unexpected sequence.
65
+ class KeyWrongLastSequenceError < Error
66
+ def initialize(msg)
67
+ @msg = msg
68
+ end
69
+ def to_s
70
+ "nats: #{@msg}"
71
+ end
72
+ end
39
73
 
40
74
  # get returns the latest value for the key.
41
- def get(key)
42
- msg = @js.get_last_msg(@stream, "#{@pre}#{key}")
75
+ def get(key, params={})
76
+ entry = nil
77
+ begin
78
+ entry = _get(key, params)
79
+ rescue KeyDeletedError
80
+ raise KeyNotFoundError
81
+ end
82
+
83
+ entry
84
+ end
85
+
86
+ def _get(key, params={})
87
+ msg = nil
88
+ subject = "#{@pre}#{key}"
89
+
90
+ if params[:revision]
91
+ msg = @js.get_msg(@stream,
92
+ seq: params[:revision],
93
+ direct: @direct)
94
+ else
95
+ msg = @js.get_msg(@stream,
96
+ subject: subject,
97
+ seq: params[:revision],
98
+ direct: @direct)
99
+ end
100
+
43
101
  entry = Entry.new(bucket: @name, key: key, value: msg.data, revision: msg.seq)
44
102
 
103
+ if subject != msg.subject
104
+ raise KeyNotFoundError.new(
105
+ entry: entry,
106
+ message: "expected '#{subject}', but got '#{msg.subject}'"
107
+ )
108
+ end
109
+
45
110
  if not msg.headers.nil?
46
111
  op = msg.headers[KV_OP]
47
- raise KeyDeletedError.new("nats: key was deleted") if op == KV_DEL or op == KV_PURGE
112
+ if op == KV_DEL or op == KV_PURGE
113
+ raise KeyDeletedError.new(entry: entry, op: op)
114
+ end
48
115
  end
49
116
 
50
117
  entry
118
+ rescue NATS::JetStream::Error::NotFound
119
+ raise KeyNotFoundError
51
120
  end
121
+ private :_get
52
122
 
53
123
  # put will place the new value for the key into the store
54
124
  # and return the revision number.
55
125
  def put(key, value)
56
- @js.publish("#{@pre}#{key}", value)
126
+ ack = @js.publish("#{@pre}#{key}", value)
127
+ ack.seq
128
+ end
129
+
130
+ # create will add the key/value pair iff it does not exist.
131
+ def create(key, value)
132
+ pa = nil
133
+ begin
134
+ pa = update(key, value, last: 0)
135
+ rescue KeyWrongLastSequenceError => err
136
+ # In case of attempting to recreate an already deleted key,
137
+ # the client would get a KeyWrongLastSequenceError. When this happens,
138
+ # it is needed to fetch latest revision number and attempt to update.
139
+ begin
140
+ # NOTE: This reimplements the following behavior from Go client.
141
+ #
142
+ # Since we have tombstones for DEL ops for watchers, this could be from that
143
+ # so we need to double check.
144
+ #
145
+ _get(key)
146
+
147
+ # No exception so not a deleted key, so reraise the original KeyWrongLastSequenceError.
148
+ # If it was deleted then the error exception will contain metadata
149
+ # to recreate using the last revision.
150
+ raise err
151
+ rescue KeyDeletedError => err
152
+ pa = update(key, value, last: err.entry.revision)
153
+ end
154
+ end
155
+
156
+ pa
157
+ end
158
+
159
+ EXPECTED_LAST_SUBJECT_SEQUENCE = "Nats-Expected-Last-Subject-Sequence"
160
+
161
+ # update will update the value iff the latest revision matches.
162
+ def update(key, value, params={})
163
+ hdrs = {}
164
+ last = (params[:last] ||= 0)
165
+ hdrs[EXPECTED_LAST_SUBJECT_SEQUENCE] = last.to_s
166
+ ack = nil
167
+ begin
168
+ ack = @js.publish("#{@pre}#{key}", value, header: hdrs)
169
+ rescue NATS::JetStream::Error::APIError => err
170
+ if err.err_code == 10071
171
+ raise KeyWrongLastSequenceError.new(err.description)
172
+ else
173
+ raise err
174
+ end
175
+ end
176
+
177
+ ack.seq
57
178
  end
58
179
 
59
180
  # delete will place a delete marker and remove all previous revisions.
60
- def delete(key)
181
+ def delete(key, params={})
61
182
  hdrs = {}
62
183
  hdrs[KV_OP] = KV_DEL
184
+ last = (params[:last] ||= 0)
185
+ if last > 0
186
+ hdrs[EXPECTED_LAST_SUBJECT_SEQUENCE] = last.to_s
187
+ end
188
+ ack = @js.publish("#{@pre}#{key}", header: hdrs)
189
+
190
+ ack.seq
191
+ end
192
+
193
+ # purge will remove the key and all revisions.
194
+ def purge(key)
195
+ hdrs = {}
196
+ hdrs[KV_OP] = KV_PURGE
197
+ hdrs[ROLLUP] = MSG_ROLLUP_SUBJECT
63
198
  @js.publish("#{@pre}#{key}", header: hdrs)
64
199
  end
65
200
 
@@ -69,7 +204,7 @@ module NATS
69
204
  BucketStatus.new(info, @name)
70
205
  end
71
206
 
72
- Entry = Struct.new(:bucket, :key, :value, :revision, keyword_init: true) do
207
+ Entry = Struct.new(:bucket, :key, :value, :revision, :delta, :created, :operation, keyword_init: true) do
73
208
  def initialize(opts={})
74
209
  rem = opts.keys - members
75
210
  opts.delete_if { |k| rem.include?(k) }
@@ -94,14 +229,24 @@ module NATS
94
229
  end
95
230
 
96
231
  def ttl
97
- @nfo.config.max_age / 1_000_000_000
232
+ @nfo.config.max_age / ::NATS::NANOSECONDS
98
233
  end
99
234
  end
100
-
101
- module API
102
- KeyValueConfig = Struct.new(:bucket, :description, :max_value_size,
103
- :history, :ttl, :max_bytes, :storage, :replicas,
104
- keyword_init: true) do
235
+
236
+ module API
237
+ KeyValueConfig = Struct.new(
238
+ :bucket,
239
+ :description,
240
+ :max_value_size,
241
+ :history,
242
+ :ttl,
243
+ :max_bytes,
244
+ :storage,
245
+ :replicas,
246
+ :placement,
247
+ :republish,
248
+ :direct,
249
+ keyword_init: true) do
105
250
  def initialize(opts={})
106
251
  rem = opts.keys - members
107
252
  opts.delete_if { |k| rem.include?(k) }
@@ -127,40 +272,53 @@ module NATS
127
272
  stream: stream,
128
273
  pre: "$KV.#{bucket}.",
129
274
  js: self,
275
+ direct: si.config.allow_direct
130
276
  )
131
277
  end
132
278
 
133
279
  def create_key_value(config)
134
- config = if not config.is_a?(JetStream::API::StreamConfig)
280
+ config = if not config.is_a?(KeyValue::API::KeyValueConfig)
135
281
  KeyValue::API::KeyValueConfig.new(config)
136
282
  else
137
283
  config
138
284
  end
139
285
  config.history ||= 1
140
286
  config.replicas ||= 1
287
+ duplicate_window = 2 * 60 # 2 minutes
141
288
  if config.ttl
142
- config.ttl = config.ttl * 1_000_000_000
289
+ if config.ttl < duplicate_window
290
+ duplicate_window = config.ttl
291
+ end
292
+ config.ttl = config.ttl * ::NATS::NANOSECONDS
143
293
  end
144
294
 
145
295
  stream = JetStream::API::StreamConfig.new(
146
296
  name: "KV_#{config.bucket}",
297
+ description: config.description,
147
298
  subjects: ["$KV.#{config.bucket}.>"],
148
- max_msgs_per_subject: config.history,
149
- max_bytes: config.max_bytes,
299
+ allow_direct: config.direct,
300
+ allow_rollup_hdrs: true,
301
+ deny_delete: true,
302
+ discard: "new",
303
+ duplicate_window: duplicate_window * ::NATS::NANOSECONDS,
150
304
  max_age: config.ttl,
305
+ max_bytes: config.max_bytes,
306
+ max_consumers: -1,
151
307
  max_msg_size: config.max_value_size,
152
- storage: config.storage,
308
+ max_msgs: -1,
309
+ max_msgs_per_subject: config.history,
153
310
  num_replicas: config.replicas,
154
- allow_rollup_hdrs: true,
155
- deny_delete: true,
311
+ storage: config.storage,
312
+ republish: config.republish,
156
313
  )
157
- resp = add_stream(stream)
158
314
 
315
+ si = add_stream(stream)
159
316
  KeyValue.new(
160
317
  name: config.bucket,
161
318
  stream: stream.name,
162
319
  pre: "$KV.#{config.bucket}.",
163
320
  js: self,
321
+ direct: si.config.allow_direct
164
322
  )
165
323
  end
166
324
 
@@ -15,7 +15,7 @@
15
15
  module NATS
16
16
  module IO
17
17
  # VERSION is the version of the client announced on CONNECT to the server.
18
- VERSION = "2.1.0".freeze
18
+ VERSION = "2.2.0".freeze
19
19
 
20
20
  # LANG is the lang runtime of the client announced on CONNECT to the server.
21
21
  LANG = "#{RUBY_ENGINE}#{RUBY_VERSION}".freeze
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nats-pure
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Waldemar Quevedo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-06-09 00:00:00.000000000 Z
11
+ date: 2022-10-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: NATS is an open-source, high-performance, lightweight cloud messaging
14
14
  system.