nats-pure 2.4.0 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/README.md +10 -3
- data/lib/nats/client.rb +7 -3
- data/lib/nats/io/client.rb +303 -280
- data/lib/nats/io/errors.rb +2 -0
- data/lib/nats/io/jetstream/api.rb +53 -50
- data/lib/nats/io/jetstream/errors.rb +30 -14
- data/lib/nats/io/jetstream/js/config.rb +9 -3
- data/lib/nats/io/jetstream/js/header.rb +15 -9
- data/lib/nats/io/jetstream/js/status.rb +11 -5
- data/lib/nats/io/jetstream/js/sub.rb +4 -2
- data/lib/nats/io/jetstream/js.rb +10 -8
- data/lib/nats/io/jetstream/manager.rb +103 -101
- data/lib/nats/io/jetstream/msg/ack.rb +15 -9
- data/lib/nats/io/jetstream/msg/ack_methods.rb +24 -22
- data/lib/nats/io/jetstream/msg/metadata.rb +9 -7
- data/lib/nats/io/jetstream/msg.rb +11 -4
- data/lib/nats/io/jetstream/pull_subscription.rb +21 -10
- data/lib/nats/io/jetstream/push_subscription.rb +3 -1
- data/lib/nats/io/jetstream.rb +102 -106
- data/lib/nats/io/kv/api.rb +7 -3
- data/lib/nats/io/kv/bucket_status.rb +7 -5
- data/lib/nats/io/kv/errors.rb +25 -2
- data/lib/nats/io/kv/manager.rb +19 -10
- data/lib/nats/io/kv.rb +359 -22
- data/lib/nats/io/msg.rb +19 -19
- data/lib/nats/io/parser.rb +23 -23
- data/lib/nats/io/rails.rb +2 -0
- data/lib/nats/io/subscription.rb +25 -22
- data/lib/nats/io/version.rb +4 -2
- data/lib/nats/io/websocket.rb +10 -8
- data/lib/nats/nuid.rb +33 -22
- data/lib/nats/service/callbacks.rb +22 -0
- data/lib/nats/service/endpoint.rb +155 -0
- data/lib/nats/service/errors.rb +44 -0
- data/lib/nats/service/group.rb +37 -0
- data/lib/nats/service/monitoring.rb +108 -0
- data/lib/nats/service/stats.rb +52 -0
- data/lib/nats/service/status.rb +66 -0
- data/lib/nats/service/validator.rb +31 -0
- data/lib/nats/service.rb +121 -0
- data/lib/nats/utils/list.rb +26 -0
- data/lib/nats-pure.rb +5 -0
- data/lib/nats.rb +10 -6
- metadata +176 -5
data/lib/nats/io/jetstream.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Copyright 2021 The NATS Authors
|
2
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
3
5
|
# you may not use this file except in compliance with the License.
|
@@ -11,17 +13,14 @@
|
|
11
13
|
# See the License for the specific language governing permissions and
|
12
14
|
# limitations under the License.
|
13
15
|
#
|
14
|
-
require_relative
|
15
|
-
require_relative
|
16
|
-
require_relative
|
17
|
-
require_relative
|
18
|
-
require_relative
|
19
|
-
require_relative
|
20
|
-
require_relative
|
21
|
-
require_relative
|
22
|
-
require_relative 'jetstream/msg'
|
23
|
-
require_relative 'jetstream/pull_subscription'
|
24
|
-
require_relative 'jetstream/push_subscription'
|
16
|
+
require_relative "kv"
|
17
|
+
require_relative "jetstream/api"
|
18
|
+
require_relative "jetstream/errors"
|
19
|
+
require_relative "jetstream/js"
|
20
|
+
require_relative "jetstream/manager"
|
21
|
+
require_relative "jetstream/msg"
|
22
|
+
require_relative "jetstream/pull_subscription"
|
23
|
+
require_relative "jetstream/push_subscription"
|
25
24
|
|
26
25
|
module NATS
|
27
26
|
# JetStream returns a context with a similar API as the NATS::Client
|
@@ -33,6 +32,8 @@ module NATS
|
|
33
32
|
# js = nc.jetstream()
|
34
33
|
#
|
35
34
|
class JetStream
|
35
|
+
attr_reader :opts, :prefix, :nc
|
36
|
+
|
36
37
|
# Create a new JetStream context for a NATS connection.
|
37
38
|
#
|
38
39
|
# @param conn [NATS::Client]
|
@@ -40,15 +41,15 @@ module NATS
|
|
40
41
|
# @option params [String] :prefix JetStream API prefix to use for the requests.
|
41
42
|
# @option params [String] :domain JetStream Domain to use for the requests.
|
42
43
|
# @option params [Float] :timeout Default timeout to use for JS requests.
|
43
|
-
def initialize(conn, params={})
|
44
|
+
def initialize(conn, params = {})
|
44
45
|
@nc = conn
|
45
46
|
@prefix = if params[:prefix]
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
47
|
+
params[:prefix]
|
48
|
+
elsif params[:domain]
|
49
|
+
"$JS.#{params[:domain]}.API"
|
50
|
+
else
|
51
|
+
JS::DefaultAPIPrefix
|
52
|
+
end
|
52
53
|
@opts = params
|
53
54
|
@opts[:timeout] ||= 5 # seconds
|
54
55
|
params[:prefix] = @prefix
|
@@ -80,7 +81,7 @@ module NATS
|
|
80
81
|
# @option params [String] :stream Expected Stream to which the message is being published.
|
81
82
|
# @raise [NATS::Timeout] When it takes too long to receive an ack response.
|
82
83
|
# @return [PubAck] The pub ack response.
|
83
|
-
def publish(subject, payload="", **params)
|
84
|
+
def publish(subject, payload = "", **params)
|
84
85
|
params[:timeout] ||= @opts[:timeout]
|
85
86
|
if params[:stream]
|
86
87
|
params[:header] ||= {}
|
@@ -89,8 +90,8 @@ module NATS
|
|
89
90
|
|
90
91
|
# Send message with headers.
|
91
92
|
msg = NATS::Msg.new(subject: subject,
|
92
|
-
|
93
|
-
|
93
|
+
data: payload,
|
94
|
+
header: params[:header])
|
94
95
|
|
95
96
|
begin
|
96
97
|
resp = @nc.request_msg(msg, **params)
|
@@ -113,53 +114,49 @@ module NATS
|
|
113
114
|
# @option params [String] :durable Consumer durable name from where the messages will be fetched.
|
114
115
|
# @option params [Hash] :config Configuration for the consumer.
|
115
116
|
# @return [NATS::JetStream::PushSubscription]
|
116
|
-
def subscribe(subject, params={}, &cb)
|
117
|
+
def subscribe(subject, params = {}, &cb)
|
117
118
|
params[:consumer] ||= params[:durable]
|
118
119
|
params[:consumer] ||= params[:name]
|
119
|
-
multi_filter =
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
end
|
120
|
+
multi_filter = if subject.is_a?(Array) && (subject.size == 1)
|
121
|
+
subject = subject.first
|
122
|
+
false
|
123
|
+
elsif subject.is_a?(Array) && (subject.size > 1)
|
124
|
+
true
|
125
|
+
end
|
126
126
|
|
127
|
-
#
|
128
127
|
stream = if params[:stream].nil?
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
end
|
137
|
-
end
|
128
|
+
if multi_filter
|
129
|
+
# Use the first subject to try to find the stream.
|
130
|
+
streams = subject.map do |s|
|
131
|
+
find_stream_name_by_subject(s)
|
132
|
+
rescue NATS::JetStream::Error::NotFound
|
133
|
+
raise NATS::JetStream::Error.new("nats: could not find stream matching filter subject '#{s}'")
|
134
|
+
end
|
138
135
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
136
|
+
# Ensure that the filter subjects are not ambiguous.
|
137
|
+
streams.uniq!
|
138
|
+
if streams.count > 1
|
139
|
+
raise NATS::JetStream::Error.new("nats: multiple streams matched filter subjects: #{streams}")
|
140
|
+
end
|
144
141
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
142
|
+
streams.first
|
143
|
+
else
|
144
|
+
find_stream_name_by_subject(subject)
|
145
|
+
end
|
146
|
+
else
|
147
|
+
params[:stream]
|
148
|
+
end
|
152
149
|
|
153
150
|
queue = params[:queue]
|
154
151
|
durable = params[:durable]
|
155
|
-
|
152
|
+
params[:flow_control]
|
156
153
|
manual_ack = params[:manual_ack]
|
157
154
|
idle_heartbeat = params[:idle_heartbeat]
|
158
155
|
flow_control = params[:flow_control]
|
159
156
|
config = params[:config]
|
160
157
|
|
161
158
|
if queue
|
162
|
-
if durable
|
159
|
+
if durable && (durable != queue)
|
163
160
|
raise NATS::JetStream::Error.new("nats: cannot create queue subscription '#{queue}' to consumer '#{durable}'")
|
164
161
|
else
|
165
162
|
durable = queue
|
@@ -170,7 +167,7 @@ module NATS
|
|
170
167
|
consumer_found = false
|
171
168
|
should_create = false
|
172
169
|
|
173
|
-
if
|
170
|
+
if !durable
|
174
171
|
should_create = true
|
175
172
|
else
|
176
173
|
begin
|
@@ -185,18 +182,16 @@ module NATS
|
|
185
182
|
end
|
186
183
|
|
187
184
|
if consumer_found
|
188
|
-
if
|
185
|
+
if !config.deliver_group
|
189
186
|
if queue
|
190
187
|
raise NATS::JetStream::Error.new("nats: cannot create a queue subscription for a consumer without a deliver group")
|
191
188
|
elsif cinfo.push_bound
|
192
189
|
raise NATS::JetStream::Error.new("nats: consumer is already bound to a subscription")
|
193
190
|
end
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
raise NATS::JetStream::Error.new("nats: cannot create a queue subscription #{queue} for a consumer with a deliver group #{config.deliver_group}")
|
199
|
-
end
|
191
|
+
elsif !queue
|
192
|
+
raise NATS::JetStream::Error.new("nats: cannot create a subscription for a consumer with a deliver group #{config.deliver_group}")
|
193
|
+
elsif queue != config.deliver_group
|
194
|
+
raise NATS::JetStream::Error.new("nats: cannot create a queue subscription #{queue} for a consumer with a deliver group #{config.deliver_group}")
|
200
195
|
end
|
201
196
|
elsif should_create
|
202
197
|
# Auto-create consumer if none found.
|
@@ -209,8 +204,8 @@ module NATS
|
|
209
204
|
raise NATS::JetStream::Error.new("nats: invalid ConsumerConfig")
|
210
205
|
end
|
211
206
|
|
212
|
-
config.durable_name = durable if
|
213
|
-
config.deliver_group = queue if
|
207
|
+
config.durable_name = durable if !config.durable_name
|
208
|
+
config.deliver_group = queue if !config.deliver_group
|
214
209
|
|
215
210
|
# Create inbox for push consumer.
|
216
211
|
deliver = @nc.new_inbox
|
@@ -225,9 +220,8 @@ module NATS
|
|
225
220
|
|
226
221
|
# Heartbeats / FlowControl
|
227
222
|
config.flow_control = flow_control
|
228
|
-
if idle_heartbeat
|
223
|
+
if idle_heartbeat || config.idle_heartbeat
|
229
224
|
idle_heartbeat = config.idle_heartbeat if config.idle_heartbeat
|
230
|
-
idle_heartbeat = idle_heartbeat * ::NATS::NANOSECONDS
|
231
225
|
config.idle_heartbeat = idle_heartbeat
|
232
226
|
end
|
233
227
|
|
@@ -237,11 +231,16 @@ module NATS
|
|
237
231
|
end
|
238
232
|
|
239
233
|
# Enable auto acking for async callbacks unless disabled.
|
240
|
-
|
234
|
+
# In case ack policy is none then we also do not require to ack.
|
235
|
+
if cb && !manual_ack && (config.ack_policy != "none")
|
241
236
|
ocb = cb
|
242
237
|
new_cb = proc do |msg|
|
243
238
|
ocb.call(msg)
|
244
|
-
|
239
|
+
begin
|
240
|
+
msg.ack
|
241
|
+
rescue
|
242
|
+
JetStream::Error::MsgAlreadyAckd
|
243
|
+
end
|
245
244
|
end
|
246
245
|
cb = new_cb
|
247
246
|
end
|
@@ -250,7 +249,7 @@ module NATS
|
|
250
249
|
sub.jsi = JS::Sub.new(
|
251
250
|
js: self,
|
252
251
|
stream: stream,
|
253
|
-
consumer: consumer
|
252
|
+
consumer: consumer
|
254
253
|
)
|
255
254
|
sub
|
256
255
|
end
|
@@ -265,57 +264,54 @@ module NATS
|
|
265
264
|
# @option params [String] :name Name of the Consumer to which the PullSubscription will be bound.
|
266
265
|
# @option params [Hash] :config Configuration for the consumer.
|
267
266
|
# @return [NATS::JetStream::PullSubscription]
|
268
|
-
def pull_subscribe(subject, durable, params={})
|
269
|
-
if (!durable
|
267
|
+
def pull_subscribe(subject, durable, params = {})
|
268
|
+
if (!durable || durable.empty?) && !(params[:consumer] || params[:name])
|
270
269
|
raise JetStream::Error::InvalidDurableName.new("nats: invalid durable name")
|
271
270
|
end
|
272
|
-
multi_filter =
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
end
|
271
|
+
multi_filter = if subject.is_a?(Array) && (subject.size == 1)
|
272
|
+
subject = subject.first
|
273
|
+
false
|
274
|
+
elsif subject.is_a?(Array) && (subject.size > 1)
|
275
|
+
true
|
276
|
+
end
|
279
277
|
|
280
278
|
params[:consumer] ||= durable
|
281
279
|
params[:consumer] ||= params[:name]
|
282
280
|
stream = if params[:stream].nil?
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
end
|
291
|
-
end
|
281
|
+
if multi_filter
|
282
|
+
# Use the first subject to try to find the stream.
|
283
|
+
streams = subject.map do |s|
|
284
|
+
find_stream_name_by_subject(s)
|
285
|
+
rescue NATS::JetStream::Error::NotFound
|
286
|
+
raise NATS::JetStream::Error.new("nats: could not find stream matching filter subject '#{s}'")
|
287
|
+
end
|
292
288
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
289
|
+
# Ensure that the filter subjects are not ambiguous.
|
290
|
+
streams.uniq!
|
291
|
+
if streams.count > 1
|
292
|
+
raise NATS::JetStream::Error.new("nats: multiple streams matched filter subjects: #{streams}")
|
293
|
+
end
|
298
294
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
295
|
+
streams.first
|
296
|
+
else
|
297
|
+
find_stream_name_by_subject(subject)
|
298
|
+
end
|
299
|
+
else
|
300
|
+
params[:stream]
|
301
|
+
end
|
306
302
|
begin
|
307
303
|
consumer_info(stream, params[:consumer])
|
308
304
|
rescue NATS::JetStream::Error::NotFound => e
|
309
305
|
# If attempting to bind, then this is a hard error.
|
310
|
-
raise e if params[:stream]
|
306
|
+
raise e if params[:stream] && !multi_filter
|
311
307
|
|
312
|
-
config = if
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
308
|
+
config = if !(params[:config])
|
309
|
+
JetStream::API::ConsumerConfig.new
|
310
|
+
elsif params[:config].is_a?(JetStream::API::ConsumerConfig)
|
311
|
+
params[:config]
|
312
|
+
else
|
313
|
+
JetStream::API::ConsumerConfig.new(params[:config])
|
314
|
+
end
|
319
315
|
config[:durable_name] = durable
|
320
316
|
config[:ack_policy] ||= JS::Config::AckExplicit
|
321
317
|
if multi_filter
|
data/lib/nats/io/kv/api.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Copyright 2021 The NATS Authors
|
2
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
3
5
|
# you may not use this file except in compliance with the License.
|
@@ -27,11 +29,13 @@ module NATS
|
|
27
29
|
:placement,
|
28
30
|
:republish,
|
29
31
|
:direct,
|
30
|
-
|
31
|
-
|
32
|
+
:validate_keys,
|
33
|
+
keyword_init: true
|
34
|
+
) do
|
35
|
+
def initialize(opts = {})
|
32
36
|
rem = opts.keys - members
|
33
37
|
opts.delete_if { |k| rem.include?(k) }
|
34
|
-
super
|
38
|
+
super
|
35
39
|
end
|
36
40
|
end
|
37
41
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Copyright 2021 The NATS Authors
|
2
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
3
5
|
# you may not use this file except in compliance with the License.
|
@@ -15,23 +17,23 @@
|
|
15
17
|
module NATS
|
16
18
|
class KeyValue
|
17
19
|
class BucketStatus
|
18
|
-
attr_reader :bucket
|
20
|
+
attr_reader :bucket, :stream_info
|
19
21
|
|
20
22
|
def initialize(info, bucket)
|
21
|
-
@
|
23
|
+
@stream_info = info
|
22
24
|
@bucket = bucket
|
23
25
|
end
|
24
26
|
|
25
27
|
def values
|
26
|
-
@
|
28
|
+
@stream_info.state.messages
|
27
29
|
end
|
28
30
|
|
29
31
|
def history
|
30
|
-
@
|
32
|
+
@stream_info.config.max_msgs_per_subject
|
31
33
|
end
|
32
34
|
|
33
35
|
def ttl
|
34
|
-
@
|
36
|
+
@stream_info.config.max_age / ::NATS::NANOSECONDS
|
35
37
|
end
|
36
38
|
end
|
37
39
|
end
|
data/lib/nats/io/kv/errors.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Copyright 2021 The NATS Authors
|
2
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
3
5
|
# you may not use this file except in compliance with the License.
|
@@ -12,7 +14,7 @@
|
|
12
14
|
# limitations under the License.
|
13
15
|
#
|
14
16
|
|
15
|
-
require_relative
|
17
|
+
require_relative "../errors"
|
16
18
|
|
17
19
|
module NATS
|
18
20
|
class KeyValue
|
@@ -21,7 +23,7 @@ module NATS
|
|
21
23
|
# When a key is not found.
|
22
24
|
class KeyNotFoundError < Error
|
23
25
|
attr_reader :entry, :op
|
24
|
-
def initialize(params={})
|
26
|
+
def initialize(params = {})
|
25
27
|
@entry = params[:entry]
|
26
28
|
@op = params[:op]
|
27
29
|
@message = params[:message]
|
@@ -52,9 +54,30 @@ module NATS
|
|
52
54
|
def initialize(msg)
|
53
55
|
@msg = msg
|
54
56
|
end
|
57
|
+
|
55
58
|
def to_s
|
56
59
|
"nats: #{@msg}"
|
57
60
|
end
|
58
61
|
end
|
62
|
+
|
63
|
+
# When there are no keys.
|
64
|
+
class NoKeysFoundError < Error
|
65
|
+
def to_s
|
66
|
+
"nats: no keys found"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# When history is too large.
|
71
|
+
class KeyHistoryTooLargeError < Error
|
72
|
+
def to_s
|
73
|
+
"nats: history limited to a max of 64"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class InvalidKeyError < Error
|
78
|
+
def to_s
|
79
|
+
"nats: invalid key"
|
80
|
+
end
|
81
|
+
end
|
59
82
|
end
|
60
83
|
end
|
data/lib/nats/io/kv/manager.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Copyright 2021 The NATS Authors
|
2
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
3
5
|
# you may not use this file except in compliance with the License.
|
@@ -15,7 +17,7 @@
|
|
15
17
|
module NATS
|
16
18
|
class KeyValue
|
17
19
|
module Manager
|
18
|
-
def key_value(bucket)
|
20
|
+
def key_value(bucket, params = {})
|
19
21
|
stream = "KV_#{bucket}"
|
20
22
|
begin
|
21
23
|
si = stream_info(stream)
|
@@ -31,16 +33,18 @@ module NATS
|
|
31
33
|
stream: stream,
|
32
34
|
pre: "$KV.#{bucket}.",
|
33
35
|
js: self,
|
34
|
-
direct: si.config.allow_direct
|
36
|
+
direct: si.config.allow_direct,
|
37
|
+
validate_keys: params[:validate_keys]
|
35
38
|
)
|
36
39
|
end
|
37
40
|
|
38
41
|
def create_key_value(config)
|
39
|
-
config = if
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
42
|
+
config = if !config.is_a?(KeyValue::API::KeyValueConfig)
|
43
|
+
config = {bucket: config} if config.is_a?(String)
|
44
|
+
KeyValue::API::KeyValueConfig.new(config)
|
45
|
+
else
|
46
|
+
config
|
47
|
+
end
|
44
48
|
config.history ||= 1
|
45
49
|
config.replicas ||= 1
|
46
50
|
duplicate_window = 2 * 60 # 2 minutes
|
@@ -51,6 +55,10 @@ module NATS
|
|
51
55
|
config.ttl = config.ttl * ::NATS::NANOSECONDS
|
52
56
|
end
|
53
57
|
|
58
|
+
if config.history > 64
|
59
|
+
raise NATS::KeyValue::KeyHistoryTooLargeError
|
60
|
+
end
|
61
|
+
|
54
62
|
stream = JetStream::API::StreamConfig.new(
|
55
63
|
name: "KV_#{config.bucket}",
|
56
64
|
description: config.description,
|
@@ -68,8 +76,8 @@ module NATS
|
|
68
76
|
max_msgs_per_subject: config.history,
|
69
77
|
num_replicas: config.replicas,
|
70
78
|
storage: config.storage,
|
71
|
-
republish: config.republish
|
72
|
-
|
79
|
+
republish: config.republish
|
80
|
+
)
|
73
81
|
|
74
82
|
si = add_stream(stream)
|
75
83
|
KeyValue.new(
|
@@ -77,7 +85,8 @@ module NATS
|
|
77
85
|
stream: stream.name,
|
78
86
|
pre: "$KV.#{config.bucket}.",
|
79
87
|
js: self,
|
80
|
-
direct: si.config.allow_direct
|
88
|
+
direct: si.config.allow_direct,
|
89
|
+
validate_keys: config.validate_keys
|
81
90
|
)
|
82
91
|
end
|
83
92
|
|