nats-async 0.1.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.
- checksums.yaml +7 -0
- data/.github/workflows/release.yml +15 -0
- data/.rspec +3 -0
- data/.rubocop.yml +226 -0
- data/README.erb +46 -0
- data/README.md +49 -0
- data/Rakefile +41 -0
- data/bin/nats-async +9 -0
- data/lib/nats-async.rb +5 -0
- data/lib/nats_async/command_line.rb +18 -0
- data/lib/nats_async/simple_connector.rb +553 -0
- data/lib/version.rb +5 -0
- data/spec/nats_async_spec.rb +19 -0
- data/spec/spec_helper.rb +3 -0
- metadata +230 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5711fb8f1a7bcbb63a2e0f25f2d8c397840517af36b050486c51f97a5766d959
|
|
4
|
+
data.tar.gz: cbe97a371adfb30e0f856941b791c61bbd79bb216e90d41b943e1d2fd962546f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b28f9fe2b99bcc2acf765df56e0681c0ea616708b1bdd873a612495604eb7a445111a07d7e31058aac6dbaa10c7b8d3d6e16e3071bf2d11a032c102b83a84bdd
|
|
7
|
+
data.tar.gz: dc559c667dfc89842ae802fd7d63263d6af0b63896f702a3f657636f1735093f9d4e77aa1ca9a56d9311695a3e8733063558f14f44ce3019b5245492619143ae
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
on: { push: { branches: [main] } }
|
|
2
|
+
|
|
3
|
+
jobs:
|
|
4
|
+
build_and_publish:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
steps:
|
|
7
|
+
- uses: actions/checkout@v3
|
|
8
|
+
- uses: ruby/setup-ruby@v1
|
|
9
|
+
with: { ruby-version: "3.4.4" }
|
|
10
|
+
- run: |
|
|
11
|
+
mkdir -p ~/.gem && touch ~/.gem/credentials && chmod 0600 ~/.gem/credentials
|
|
12
|
+
printf -- "---\n:rubygems_api_key: ${API_KEY}\n" > ~/.gem/credentials
|
|
13
|
+
bundle install && rake push
|
|
14
|
+
env:
|
|
15
|
+
API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
require:
|
|
2
|
+
- rubocop-rake
|
|
3
|
+
- rubocop-rspec
|
|
4
|
+
|
|
5
|
+
AllCops:
|
|
6
|
+
NewCops: enable
|
|
7
|
+
UseCache: false
|
|
8
|
+
Exclude:
|
|
9
|
+
- benchmarks/*.rb
|
|
10
|
+
- "*.gemspec"
|
|
11
|
+
|
|
12
|
+
Layout/SpaceAroundMethodCallOperator:
|
|
13
|
+
Enabled: false
|
|
14
|
+
|
|
15
|
+
Layout/SpaceInLambdaLiteral:
|
|
16
|
+
Enabled: false
|
|
17
|
+
|
|
18
|
+
Layout/MultilineMethodCallIndentation:
|
|
19
|
+
Enabled: true
|
|
20
|
+
EnforcedStyle: indented
|
|
21
|
+
|
|
22
|
+
Layout/FirstArrayElementIndentation:
|
|
23
|
+
EnforcedStyle: consistent
|
|
24
|
+
|
|
25
|
+
Layout/SpaceInsideHashLiteralBraces:
|
|
26
|
+
Enabled: true
|
|
27
|
+
EnforcedStyle: no_space
|
|
28
|
+
EnforcedStyleForEmptyBraces: no_space
|
|
29
|
+
|
|
30
|
+
Layout/LineLength:
|
|
31
|
+
Enabled: false
|
|
32
|
+
|
|
33
|
+
Layout/EmptyLineAfterGuardClause:
|
|
34
|
+
Enabled: false
|
|
35
|
+
|
|
36
|
+
Lint/AmbiguousBlockAssociation:
|
|
37
|
+
Enabled: true
|
|
38
|
+
Exclude:
|
|
39
|
+
- "spec/**/*.rb"
|
|
40
|
+
|
|
41
|
+
Lint/BooleanSymbol:
|
|
42
|
+
Enabled: false
|
|
43
|
+
|
|
44
|
+
Lint/ConstantDefinitionInBlock:
|
|
45
|
+
Exclude:
|
|
46
|
+
- "spec/**/*.rb"
|
|
47
|
+
|
|
48
|
+
Lint/RaiseException:
|
|
49
|
+
Enabled: false
|
|
50
|
+
|
|
51
|
+
Lint/StructNewOverride:
|
|
52
|
+
Enabled: false
|
|
53
|
+
|
|
54
|
+
Lint/SuppressedException:
|
|
55
|
+
Exclude:
|
|
56
|
+
- "spec/spec_helper.rb"
|
|
57
|
+
|
|
58
|
+
Lint/LiteralAsCondition:
|
|
59
|
+
Exclude:
|
|
60
|
+
- "spec/**/*.rb"
|
|
61
|
+
|
|
62
|
+
Naming/PredicatePrefix:
|
|
63
|
+
Enabled: false
|
|
64
|
+
|
|
65
|
+
Naming/PredicateMethod:
|
|
66
|
+
Enabled: false
|
|
67
|
+
|
|
68
|
+
Naming/FileName:
|
|
69
|
+
Exclude:
|
|
70
|
+
- "lib/*-*.rb"
|
|
71
|
+
|
|
72
|
+
Naming/MethodName:
|
|
73
|
+
Enabled: false
|
|
74
|
+
|
|
75
|
+
Naming/MethodParameterName:
|
|
76
|
+
Enabled: false
|
|
77
|
+
|
|
78
|
+
Naming/MemoizedInstanceVariableName:
|
|
79
|
+
Enabled: false
|
|
80
|
+
|
|
81
|
+
Metrics/MethodLength:
|
|
82
|
+
Enabled: false
|
|
83
|
+
|
|
84
|
+
Metrics/ClassLength:
|
|
85
|
+
Enabled: false
|
|
86
|
+
|
|
87
|
+
Metrics/BlockLength:
|
|
88
|
+
Enabled: false
|
|
89
|
+
|
|
90
|
+
Metrics/AbcSize:
|
|
91
|
+
Enabled: false
|
|
92
|
+
|
|
93
|
+
Metrics/ParameterLists:
|
|
94
|
+
Enabled: false
|
|
95
|
+
|
|
96
|
+
Metrics/CyclomaticComplexity:
|
|
97
|
+
Enabled: true
|
|
98
|
+
Max: 12
|
|
99
|
+
|
|
100
|
+
Style/ExponentialNotation:
|
|
101
|
+
Enabled: false
|
|
102
|
+
|
|
103
|
+
Style/HashEachMethods:
|
|
104
|
+
Enabled: false
|
|
105
|
+
|
|
106
|
+
Style/HashTransformKeys:
|
|
107
|
+
Enabled: false
|
|
108
|
+
|
|
109
|
+
Style/HashTransformValues:
|
|
110
|
+
Enabled: false
|
|
111
|
+
|
|
112
|
+
Style/AccessModifierDeclarations:
|
|
113
|
+
Enabled: false
|
|
114
|
+
|
|
115
|
+
Style/Alias:
|
|
116
|
+
Enabled: true
|
|
117
|
+
EnforcedStyle: prefer_alias_method
|
|
118
|
+
|
|
119
|
+
Style/AsciiComments:
|
|
120
|
+
Enabled: false
|
|
121
|
+
|
|
122
|
+
Style/BlockDelimiters:
|
|
123
|
+
Enabled: false
|
|
124
|
+
|
|
125
|
+
Style/ClassAndModuleChildren:
|
|
126
|
+
Exclude:
|
|
127
|
+
- "spec/**/*.rb"
|
|
128
|
+
|
|
129
|
+
Style/ConditionalAssignment:
|
|
130
|
+
Enabled: false
|
|
131
|
+
|
|
132
|
+
Style/DateTime:
|
|
133
|
+
Enabled: false
|
|
134
|
+
|
|
135
|
+
Style/Documentation:
|
|
136
|
+
Enabled: false
|
|
137
|
+
|
|
138
|
+
Style/FrozenStringLiteralComment:
|
|
139
|
+
Enabled: false
|
|
140
|
+
|
|
141
|
+
Style/EachWithObject:
|
|
142
|
+
Enabled: false
|
|
143
|
+
|
|
144
|
+
Style/FormatString:
|
|
145
|
+
Enabled: false
|
|
146
|
+
|
|
147
|
+
Style/FormatStringToken:
|
|
148
|
+
Enabled: false
|
|
149
|
+
|
|
150
|
+
Style/GuardClause:
|
|
151
|
+
Enabled: false
|
|
152
|
+
|
|
153
|
+
Style/IfUnlessModifier:
|
|
154
|
+
Enabled: false
|
|
155
|
+
|
|
156
|
+
Style/Lambda:
|
|
157
|
+
Enabled: false
|
|
158
|
+
|
|
159
|
+
Style/LambdaCall:
|
|
160
|
+
Enabled: false
|
|
161
|
+
|
|
162
|
+
Style/ParallelAssignment:
|
|
163
|
+
Enabled: false
|
|
164
|
+
|
|
165
|
+
Style/StabbyLambdaParentheses:
|
|
166
|
+
Enabled: false
|
|
167
|
+
|
|
168
|
+
Style/SymbolArray:
|
|
169
|
+
Exclude:
|
|
170
|
+
- "spec/**/*.rb"
|
|
171
|
+
|
|
172
|
+
Style/TrailingUnderscoreVariable:
|
|
173
|
+
Enabled: false
|
|
174
|
+
|
|
175
|
+
Style/MultipleComparison:
|
|
176
|
+
Enabled: false
|
|
177
|
+
|
|
178
|
+
Style/Next:
|
|
179
|
+
Enabled: false
|
|
180
|
+
|
|
181
|
+
Style/AccessorGrouping:
|
|
182
|
+
Enabled: false
|
|
183
|
+
|
|
184
|
+
Style/EmptyLiteral:
|
|
185
|
+
Enabled: false
|
|
186
|
+
|
|
187
|
+
Style/Semicolon:
|
|
188
|
+
Enabled: false
|
|
189
|
+
|
|
190
|
+
Style/HashAsLastArrayItem:
|
|
191
|
+
Exclude:
|
|
192
|
+
- "spec/**/*.rb"
|
|
193
|
+
|
|
194
|
+
Style/CaseEquality:
|
|
195
|
+
Exclude:
|
|
196
|
+
- "spec/**/*.rb"
|
|
197
|
+
|
|
198
|
+
Style/CombinableLoops:
|
|
199
|
+
Enabled: false
|
|
200
|
+
|
|
201
|
+
Style/EmptyElse:
|
|
202
|
+
Enabled: false
|
|
203
|
+
|
|
204
|
+
Style/DoubleNegation:
|
|
205
|
+
Enabled: false
|
|
206
|
+
|
|
207
|
+
Style/MultilineBlockChain:
|
|
208
|
+
Enabled: false
|
|
209
|
+
|
|
210
|
+
Style/NumberedParametersLimit:
|
|
211
|
+
Max: 2
|
|
212
|
+
|
|
213
|
+
Style/StringLiterals:
|
|
214
|
+
Enabled: false
|
|
215
|
+
|
|
216
|
+
Style/RedundantArgument:
|
|
217
|
+
Enabled: false
|
|
218
|
+
|
|
219
|
+
Style/SoleNestedConditional:
|
|
220
|
+
Enabled: false
|
|
221
|
+
|
|
222
|
+
Style/RescueModifier:
|
|
223
|
+
Enabled: false
|
|
224
|
+
|
|
225
|
+
Lint/UnusedMethodArgument:
|
|
226
|
+
Enabled: false
|
data/README.erb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Nats Async
|
|
2
|
+
|
|
3
|
+
`nats-async` packages the `nats-test` prototype as a Ruby gem with the same project layout and build flow as `dry-stack`.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
$ ./bin/nats-async --help
|
|
7
|
+
<%= %x{./bin/nats-async --help} %>
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Add the gem to your bundle:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem "nats-async"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or install it directly:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
gem install nats-async
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
require "nats-async"
|
|
28
|
+
|
|
29
|
+
connector = NatsAsync::SimpleConnector.new(url: "nats://127.0.0.1:4222", verbose: false)
|
|
30
|
+
connector.run(duration: 1) do |client, task|
|
|
31
|
+
client.subscribe("demo.subject") do |message|
|
|
32
|
+
puts "received: #{message.data}"
|
|
33
|
+
task.stop
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
client.publish("demo.subject", "hello")
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Development
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bundle install
|
|
44
|
+
bundle exec rspec
|
|
45
|
+
rake build
|
|
46
|
+
```
|
data/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Nats Async
|
|
2
|
+
|
|
3
|
+
`nats-async` packages the `nats-test` prototype as a Ruby gem with the same project layout and build flow as `dry-stack`.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
$ ./bin/nats-async --help
|
|
7
|
+
Usage: nats-async [--version] [--help]
|
|
8
|
+
|
|
9
|
+
Library gem for the NatsAsync connector prototype.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add the gem to your bundle:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "nats-async"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install it directly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gem install nats-async
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "nats-async"
|
|
31
|
+
|
|
32
|
+
connector = NatsAsync::SimpleConnector.new(url: "nats://127.0.0.1:4222", verbose: false)
|
|
33
|
+
connector.run(duration: 1) do |client, task|
|
|
34
|
+
client.subscribe("demo.subject") do |message|
|
|
35
|
+
puts "received: #{message.data}"
|
|
36
|
+
task.stop
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
client.publish("demo.subject", "hello")
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Development
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bundle install
|
|
47
|
+
bundle exec rspec
|
|
48
|
+
rake build
|
|
49
|
+
```
|
data/Rakefile
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "rspec/core/rake_task"
|
|
2
|
+
require_relative "lib/version"
|
|
3
|
+
|
|
4
|
+
rspec = RSpec::Core::RakeTask.new(:spec)
|
|
5
|
+
|
|
6
|
+
require "rubocop/rake_task"
|
|
7
|
+
|
|
8
|
+
RuboCop::RakeTask.new
|
|
9
|
+
|
|
10
|
+
task default: %i[rspec]
|
|
11
|
+
|
|
12
|
+
desc "CI Rspec run with reports"
|
|
13
|
+
task :rspec do
|
|
14
|
+
rspec.rspec_opts = "--profile --color -f documentation -f RspecJunitFormatter --out ./results/rspec.xml"
|
|
15
|
+
Rake::Task["spec"].invoke
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require "erb"
|
|
19
|
+
|
|
20
|
+
desc "Update readme"
|
|
21
|
+
task :readme do
|
|
22
|
+
puts "Update README.erb -> README.md"
|
|
23
|
+
template = File.read("./README.erb")
|
|
24
|
+
renderer = ERB.new(template, trim_mode: "-")
|
|
25
|
+
File.write("./README.md", renderer.result)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
desc "Build&push new version"
|
|
29
|
+
task push: %i[spec readme] do
|
|
30
|
+
puts "Build&push new version"
|
|
31
|
+
system "gem build nats-async.gemspec" or exit 1
|
|
32
|
+
system "gem install ./nats-async-#{NatsAsync::VERSION}.gem" or exit 1
|
|
33
|
+
system "gem push nats-async-#{NatsAsync::VERSION}.gem" or exit 1
|
|
34
|
+
system "gem list -r nats-async" or exit 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "Build new version"
|
|
38
|
+
task build: %i[spec readme] do
|
|
39
|
+
puts "Build new version"
|
|
40
|
+
system "gem build nats-async.gemspec" or exit 1
|
|
41
|
+
end
|
data/bin/nats-async
ADDED
data/lib/nats-async.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NatsAsync
|
|
4
|
+
module CommandLine
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def run(argv, stdout: $stdout)
|
|
8
|
+
case argv.first
|
|
9
|
+
when "-v", "--version"
|
|
10
|
+
stdout.puts(NatsAsync::VERSION)
|
|
11
|
+
else
|
|
12
|
+
stdout.puts("Usage: nats-async [--version] [--help]")
|
|
13
|
+
stdout.puts
|
|
14
|
+
stdout.puts("Library gem for the NatsAsync connector prototype.")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "async/condition"
|
|
5
|
+
require "async/semaphore"
|
|
6
|
+
ENV["CONSOLE_OUTPUT"] ||= "XTerm"
|
|
7
|
+
require "console"
|
|
8
|
+
require "io/endpoint"
|
|
9
|
+
require "io/endpoint/host_endpoint"
|
|
10
|
+
require "io/stream"
|
|
11
|
+
require "json"
|
|
12
|
+
require "timeout"
|
|
13
|
+
require "uri"
|
|
14
|
+
require "base64"
|
|
15
|
+
require "openssl"
|
|
16
|
+
|
|
17
|
+
module NatsAsync
|
|
18
|
+
class SimpleConnector
|
|
19
|
+
CR_LF = "\r\n"
|
|
20
|
+
|
|
21
|
+
class AckError < StandardError; end
|
|
22
|
+
class MsgAlreadyAcked < AckError; end
|
|
23
|
+
class NotAckable < AckError; end
|
|
24
|
+
class RequestError < StandardError; end
|
|
25
|
+
class ResponseParseError < RequestError; end
|
|
26
|
+
class ProtocolError < StandardError; end
|
|
27
|
+
|
|
28
|
+
class Message
|
|
29
|
+
ACK = "+ACK"
|
|
30
|
+
NAK = "-NAK"
|
|
31
|
+
TERM = "+TERM"
|
|
32
|
+
WPI = "+WPI"
|
|
33
|
+
|
|
34
|
+
attr_reader :subject, :sid, :reply, :data
|
|
35
|
+
|
|
36
|
+
def initialize(subject:, sid:, reply:, data:, connector:)
|
|
37
|
+
@subject = subject
|
|
38
|
+
@sid = sid
|
|
39
|
+
@reply = reply
|
|
40
|
+
@data = data
|
|
41
|
+
@connector = connector
|
|
42
|
+
@acked = false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ack(**_params) = finalize_ack!(ACK)
|
|
46
|
+
|
|
47
|
+
def ack_sync(timeout: 0.5, **_params) = finalize_ack!(ACK, timeout: timeout)
|
|
48
|
+
|
|
49
|
+
def nak(delay: nil, timeout: nil, **_params)
|
|
50
|
+
payload = delay ? "#{NAK} #{{delay: delay}.to_json}" : NAK
|
|
51
|
+
finalize_ack!(payload, timeout: timeout)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def term(timeout: nil, **_params) = finalize_ack!(TERM, timeout: timeout)
|
|
55
|
+
|
|
56
|
+
def in_progress(timeout: nil, **_params)
|
|
57
|
+
ensure_reply!
|
|
58
|
+
publish_ack(WPI, timeout: timeout)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def metadata
|
|
62
|
+
return unless reply&.start_with?("$JS.ACK.")
|
|
63
|
+
|
|
64
|
+
tokens = reply.split(".")
|
|
65
|
+
return if tokens.size < 9
|
|
66
|
+
|
|
67
|
+
if tokens.size >= 12
|
|
68
|
+
domain = tokens[2] == "_" ? "" : tokens[2]
|
|
69
|
+
stream = tokens[4]
|
|
70
|
+
consumer = tokens[5]
|
|
71
|
+
delivered = tokens[6].to_i
|
|
72
|
+
stream_seq = tokens[7].to_i
|
|
73
|
+
consumer_seq = tokens[8].to_i
|
|
74
|
+
timestamp_ns = tokens[9].to_i
|
|
75
|
+
pending = tokens[10].to_i
|
|
76
|
+
else
|
|
77
|
+
domain = ""
|
|
78
|
+
stream = tokens[2]
|
|
79
|
+
consumer = tokens[3]
|
|
80
|
+
delivered = tokens[4].to_i
|
|
81
|
+
stream_seq = tokens[5].to_i
|
|
82
|
+
consumer_seq = tokens[6].to_i
|
|
83
|
+
timestamp_ns = tokens[7].to_i
|
|
84
|
+
pending = tokens[8].to_i
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
stream: stream,
|
|
89
|
+
consumer: consumer,
|
|
90
|
+
delivered: delivered,
|
|
91
|
+
sequence: {stream: stream_seq, consumer: consumer_seq},
|
|
92
|
+
timestamp_ns: timestamp_ns,
|
|
93
|
+
pending: pending,
|
|
94
|
+
domain: domain
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def finalize_ack!(payload, timeout: nil)
|
|
101
|
+
raise MsgAlreadyAcked, "message already acknowledged" if @acked
|
|
102
|
+
|
|
103
|
+
publish_ack(payload, timeout: timeout)
|
|
104
|
+
@acked = true
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def publish_ack(payload, timeout: nil)
|
|
109
|
+
ensure_reply!
|
|
110
|
+
@connector.publish(@reply, payload)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def ensure_reply!
|
|
114
|
+
raise NotAckable, "message has no reply subject" if reply.nil? || reply.empty?
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def initialize(
|
|
119
|
+
url: "nats://127.0.0.1:4222",
|
|
120
|
+
verbose: true,
|
|
121
|
+
js_api_prefix: "$JS.API",
|
|
122
|
+
tls: nil,
|
|
123
|
+
tls_verify: true,
|
|
124
|
+
tls_ca_file: nil,
|
|
125
|
+
tls_ca_path: nil,
|
|
126
|
+
tls_hostname: nil,
|
|
127
|
+
tls_handshake_first: false,
|
|
128
|
+
user: nil,
|
|
129
|
+
password: nil,
|
|
130
|
+
nkey_seed: nil,
|
|
131
|
+
nkey_seed_file: nil,
|
|
132
|
+
nkey_public_key: nil
|
|
133
|
+
)
|
|
134
|
+
@url = URI(url)
|
|
135
|
+
@verbose = verbose
|
|
136
|
+
@js_api_prefix = normalize_subject_prefix(js_api_prefix)
|
|
137
|
+
@tls_enabled = tls.nil? ? %w[tls nats+tls].include?(@url.scheme) : tls
|
|
138
|
+
@tls_verify = tls_verify
|
|
139
|
+
@tls_ca_file = presence(tls_ca_file)
|
|
140
|
+
@tls_ca_path = presence(tls_ca_path)
|
|
141
|
+
@tls_hostname = presence(tls_hostname)
|
|
142
|
+
@tls_handshake_first = tls_handshake_first
|
|
143
|
+
@auth_user = presence(user || @url.user)
|
|
144
|
+
@auth_password = presence(password || @url.password)
|
|
145
|
+
@nkey_seed = presence(nkey_seed)
|
|
146
|
+
@nkey_seed_file = presence(nkey_seed_file)
|
|
147
|
+
@nkey_public_key = presence(nkey_public_key)
|
|
148
|
+
@stream = nil
|
|
149
|
+
@logger = Console.logger.with(level: (verbose ? :debug : :error), verbose: false)
|
|
150
|
+
|
|
151
|
+
@received_pings = 0
|
|
152
|
+
@received_pongs = 0
|
|
153
|
+
@sent_pings = 0
|
|
154
|
+
@server_info = nil
|
|
155
|
+
|
|
156
|
+
@read_task = nil
|
|
157
|
+
@read_error = nil
|
|
158
|
+
@write_lock = Async::Semaphore.new(1)
|
|
159
|
+
@pong_condition = Async::Condition.new
|
|
160
|
+
@sid_seq = 0
|
|
161
|
+
@subscriptions = {}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
attr_reader :received_pings, :received_pongs, :sent_pings, :server_info, :js_api_prefix
|
|
165
|
+
|
|
166
|
+
def run(duration: 10, ping_every: 2, ping_timeout: 2)
|
|
167
|
+
Async do |task|
|
|
168
|
+
connect!
|
|
169
|
+
read_initial_info!
|
|
170
|
+
send_connect!
|
|
171
|
+
|
|
172
|
+
@read_task = task.async { read_loop }
|
|
173
|
+
ping!(timeout: ping_timeout)
|
|
174
|
+
yield self, task if block_given?
|
|
175
|
+
|
|
176
|
+
deadline = monotonic_now + duration
|
|
177
|
+
while monotonic_now < deadline
|
|
178
|
+
task.sleep(ping_every)
|
|
179
|
+
ping!(timeout: ping_timeout)
|
|
180
|
+
end
|
|
181
|
+
ensure
|
|
182
|
+
stop
|
|
183
|
+
end.wait
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def stop
|
|
187
|
+
@read_task&.stop
|
|
188
|
+
safe_close_stream
|
|
189
|
+
@read_task = nil
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def ping!(timeout: 2)
|
|
193
|
+
expected_pongs = @received_pongs + 1
|
|
194
|
+
|
|
195
|
+
@sent_pings += 1
|
|
196
|
+
write_line("PING")
|
|
197
|
+
await(timeout: timeout, condition: @pong_condition, timeout_message: "timeout waiting for PONG after #{timeout}s", predicate: lambda {
|
|
198
|
+
raise @read_error if @read_error
|
|
199
|
+
@received_pongs >= expected_pongs
|
|
200
|
+
})
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def publish(subject, payload = "", reply: nil)
|
|
204
|
+
payload = payload.to_s
|
|
205
|
+
command = build_pub_command(subject, payload.bytesize, reply: reply)
|
|
206
|
+
@logger.debug("C->S #{command}")
|
|
207
|
+
|
|
208
|
+
@write_lock.acquire do
|
|
209
|
+
@stream.write("#{command}#{CR_LF}", flush: false)
|
|
210
|
+
@stream.write(payload, flush: false)
|
|
211
|
+
@stream.write(CR_LF, flush: true)
|
|
212
|
+
end
|
|
213
|
+
protocol_payload_out(payload)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def subscribe(subject, queue: nil, handler: nil, &block)
|
|
217
|
+
callback = handler || block
|
|
218
|
+
raise ArgumentError, "handler or block required for subscribe" unless callback
|
|
219
|
+
|
|
220
|
+
sid = next_sid
|
|
221
|
+
@subscriptions[sid] = callback
|
|
222
|
+
command = queue ? "SUB #{subject} #{queue} #{sid}" : "SUB #{subject} #{sid}"
|
|
223
|
+
write_line(command)
|
|
224
|
+
sid
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def unsubscribe(sid)
|
|
228
|
+
write_line("UNSUB #{sid}")
|
|
229
|
+
@subscriptions.delete(sid)
|
|
230
|
+
true
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def request(subject, payload = "", timeout: 0.5, parse_json: true)
|
|
234
|
+
response = request_message(subject, payload, timeout: timeout)
|
|
235
|
+
result = parse_json ? parse_json_response(subject, response.data) : response
|
|
236
|
+
ensure_request_ok!(subject, result) if parse_json
|
|
237
|
+
block_given? ? yield(result) : result
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def request_message(subject, payload = "", timeout: 0.5)
|
|
241
|
+
inbox = "_INBOX.#{rand(1 << 30)}.#{next_sid}"
|
|
242
|
+
response = nil
|
|
243
|
+
condition = Async::Condition.new
|
|
244
|
+
on_response = ->(msg) { response = msg; condition.signal }
|
|
245
|
+
with_temp_subscription(inbox, handler: on_response) do
|
|
246
|
+
publish(subject, request_payload(payload), reply: inbox)
|
|
247
|
+
await(timeout: timeout, condition: condition, timeout_message: "request timeout after #{timeout}s", predicate: lambda {
|
|
248
|
+
raise @read_error if @read_error
|
|
249
|
+
!response.nil?
|
|
250
|
+
})
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
block_given? ? yield(response) : response
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def js_api_subject(*tokens)
|
|
257
|
+
[js_api_prefix, *tokens.flatten].compact.map(&:to_s).reject(&:empty?).join(".")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
private
|
|
261
|
+
|
|
262
|
+
def connect!
|
|
263
|
+
host = @url.host || "127.0.0.1"
|
|
264
|
+
port = @url.port || 4222
|
|
265
|
+
socket = IO::Endpoint.tcp(host, port).connect
|
|
266
|
+
@stream = IO.Stream(socket)
|
|
267
|
+
|
|
268
|
+
return unless @tls_enabled
|
|
269
|
+
|
|
270
|
+
@server_info = parse_info_line(read_line) unless @tls_handshake_first
|
|
271
|
+
socket = wrap_tls_socket(socket, host)
|
|
272
|
+
@stream = IO.Stream(socket)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def read_initial_info!
|
|
276
|
+
return if @server_info
|
|
277
|
+
|
|
278
|
+
@server_info = parse_info_line(read_line)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def send_connect!
|
|
282
|
+
payload = {
|
|
283
|
+
verbose: false,
|
|
284
|
+
pedantic: false,
|
|
285
|
+
tls_required: @tls_enabled,
|
|
286
|
+
lang: "ruby",
|
|
287
|
+
version: "nats-async-playground",
|
|
288
|
+
protocol: 1,
|
|
289
|
+
echo: true
|
|
290
|
+
}
|
|
291
|
+
payload.merge!(auth_connect_fields)
|
|
292
|
+
|
|
293
|
+
write_line("CONNECT #{JSON.generate(payload)}")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def read_loop
|
|
297
|
+
loop do
|
|
298
|
+
line = read_line
|
|
299
|
+
case line
|
|
300
|
+
when "PING"
|
|
301
|
+
@received_pings += 1
|
|
302
|
+
write_line("PONG")
|
|
303
|
+
when "PONG"
|
|
304
|
+
@received_pongs += 1
|
|
305
|
+
@pong_condition.signal
|
|
306
|
+
when /\A-ERR\s+/
|
|
307
|
+
raise ProtocolError, "server error: #{line}"
|
|
308
|
+
when /\AINFO\s+/
|
|
309
|
+
@server_info = parse_info_line(line)
|
|
310
|
+
when /\AMSG\s+/
|
|
311
|
+
dispatch_msg(line)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
rescue StandardError => e
|
|
315
|
+
@read_error = e
|
|
316
|
+
@logger.error("read loop error: #{e.class}: #{e.message}")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def write_line(data)
|
|
320
|
+
@write_lock.acquire do
|
|
321
|
+
@stream.write("#{data}#{CR_LF}", flush: true)
|
|
322
|
+
end
|
|
323
|
+
@logger.debug("C->S #{data}")
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def read_line
|
|
327
|
+
data = @stream.read_until(CR_LF, chomp: true)
|
|
328
|
+
raise EOFError, "socket closed" unless data
|
|
329
|
+
|
|
330
|
+
@logger.debug("S->C #{data}")
|
|
331
|
+
data
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def await(timeout:, condition:, timeout_message:, predicate:)
|
|
335
|
+
return true if predicate.call
|
|
336
|
+
|
|
337
|
+
deadline = monotonic_now + timeout
|
|
338
|
+
until predicate.call
|
|
339
|
+
remaining = deadline - monotonic_now
|
|
340
|
+
raise Timeout::Error, timeout_message if remaining <= 0
|
|
341
|
+
|
|
342
|
+
Async::Task.current.with_timeout(remaining) { condition.wait }
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
true
|
|
346
|
+
rescue Timeout::Error
|
|
347
|
+
raise Timeout::Error, timeout_message
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def monotonic_now
|
|
351
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def request_payload(payload)
|
|
355
|
+
return "" if payload.nil?
|
|
356
|
+
return payload if payload.is_a?(String)
|
|
357
|
+
|
|
358
|
+
JSON.generate(payload)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def ensure_request_ok!(subject, result)
|
|
362
|
+
return result unless result.is_a?(Hash) && result[:error]
|
|
363
|
+
|
|
364
|
+
error = result[:error]
|
|
365
|
+
description = error.is_a?(Hash) ? error[:description] || error[:code] || error.inspect : error
|
|
366
|
+
raise RequestError, "request failed for #{subject}: #{description}"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def parse_json_response(subject, data)
|
|
370
|
+
JSON.parse(data, symbolize_names: true)
|
|
371
|
+
rescue JSON::ParserError => e
|
|
372
|
+
raise ResponseParseError, "request failed for #{subject}: invalid JSON response (#{e.message})"
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def parse_info_line(line)
|
|
376
|
+
raise ProtocolError, "expected INFO, got: #{line.inspect}" unless line.start_with?("INFO ")
|
|
377
|
+
|
|
378
|
+
JSON.parse(line.delete_prefix("INFO "), symbolize_names: true)
|
|
379
|
+
rescue JSON::ParserError => e
|
|
380
|
+
raise ProtocolError, "invalid INFO payload: #{e.message}"
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def next_sid
|
|
384
|
+
@sid_seq += 1
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def dispatch_msg(control_line)
|
|
388
|
+
subject, sid, reply, size = parse_msg_control_line(control_line)
|
|
389
|
+
payload = @stream.read_exactly(size)
|
|
390
|
+
suffix = @stream.read_exactly(CR_LF.bytesize)
|
|
391
|
+
raise ProtocolError, "malformed MSG payload ending: #{suffix.inspect}" unless suffix == CR_LF
|
|
392
|
+
|
|
393
|
+
msg = Message.new(subject: subject, sid: sid, reply: reply, data: payload, connector: self)
|
|
394
|
+
handler = @subscriptions[sid]
|
|
395
|
+
protocol_payload_in(payload)
|
|
396
|
+
|
|
397
|
+
if handler
|
|
398
|
+
handler.call(msg)
|
|
399
|
+
else
|
|
400
|
+
@logger.warn("no subscription handler for sid=#{sid}")
|
|
401
|
+
end
|
|
402
|
+
rescue StandardError => e
|
|
403
|
+
@logger.error("message dispatch error: #{e.class}: #{e.message}")
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def parse_msg_control_line(control_line)
|
|
407
|
+
tokens = control_line.split(" ")
|
|
408
|
+
raise ProtocolError, "malformed MSG line: #{control_line.inspect}" unless tokens.first == "MSG"
|
|
409
|
+
|
|
410
|
+
case tokens.length
|
|
411
|
+
when 4
|
|
412
|
+
[tokens[1], Integer(tokens[2]), nil, Integer(tokens[3])]
|
|
413
|
+
when 5
|
|
414
|
+
[tokens[1], Integer(tokens[2]), tokens[3], Integer(tokens[4])]
|
|
415
|
+
else
|
|
416
|
+
raise ProtocolError, "unexpected MSG control tokens: #{tokens.length}"
|
|
417
|
+
end
|
|
418
|
+
rescue ArgumentError => e
|
|
419
|
+
raise ProtocolError, "invalid MSG control values: #{e.message}"
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def protocol_payload_out(payload)
|
|
423
|
+
return if payload.empty?
|
|
424
|
+
|
|
425
|
+
@logger.debug("C->S PAYLOAD #{payload.inspect}")
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def protocol_payload_in(payload)
|
|
429
|
+
return if payload.empty?
|
|
430
|
+
|
|
431
|
+
@logger.debug("S->C PAYLOAD #{payload.inspect}")
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def with_temp_subscription(subject, queue: nil, handler: nil)
|
|
435
|
+
sid = subscribe(subject, queue: queue, handler: handler)
|
|
436
|
+
yield
|
|
437
|
+
ensure
|
|
438
|
+
unsubscribe(sid) if sid
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def build_pub_command(subject, size, reply: nil)
|
|
442
|
+
reply ? "PUB #{subject} #{reply} #{size}" : "PUB #{subject} #{size}"
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def nkey_connect_fields
|
|
446
|
+
return {} unless nkey_auth?
|
|
447
|
+
|
|
448
|
+
nonce = server_info&.dig(:nonce).to_s
|
|
449
|
+
raise ProtocolError, "server nonce is required for nkey auth" if nonce.empty?
|
|
450
|
+
|
|
451
|
+
{nkey: nkey_public_key_value, sig: nkey_signature(nonce)}
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def auth_connect_fields
|
|
455
|
+
return {user: @auth_user, pass: @auth_password} if @auth_user && @auth_password
|
|
456
|
+
return {auth_token: @auth_user} if @auth_user
|
|
457
|
+
|
|
458
|
+
nkey_connect_fields
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def nkey_auth? = !nkey_seed_value.to_s.empty?
|
|
462
|
+
|
|
463
|
+
def nkey_seed_value
|
|
464
|
+
return @nkey_seed if @nkey_seed
|
|
465
|
+
return unless @nkey_seed_file
|
|
466
|
+
|
|
467
|
+
@nkey_seed = File.read(@nkey_seed_file).strip
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def nkey_public_key_value
|
|
471
|
+
return @nkey_public_key if @nkey_public_key
|
|
472
|
+
|
|
473
|
+
with_nkey_pair { |kp| @nkey_public_key = kp.public_key.dup }
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def nkey_signature(nonce)
|
|
477
|
+
with_nkey_pair do |kp|
|
|
478
|
+
Base64.urlsafe_encode64(kp.sign(nonce)).delete("=")
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def with_nkey_pair
|
|
483
|
+
begin
|
|
484
|
+
require "nkeys"
|
|
485
|
+
rescue LoadError
|
|
486
|
+
raise LoadError, "nkeys gem is required for nkey auth"
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
seed = nkey_seed_value.to_s
|
|
490
|
+
raise ArgumentError, "nkey_seed or nkey_seed_file is required for nkey signing" if seed.empty?
|
|
491
|
+
|
|
492
|
+
kp = NKEYS.from_seed(seed)
|
|
493
|
+
yield kp
|
|
494
|
+
ensure
|
|
495
|
+
kp&.wipe!
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def normalize_subject_prefix(value)
|
|
499
|
+
prefix = value.to_s.strip.sub(/\.+\z/, "")
|
|
500
|
+
raise ArgumentError, "js_api_prefix cannot be empty" if prefix.empty?
|
|
501
|
+
|
|
502
|
+
prefix
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def tls_params
|
|
506
|
+
params = {}
|
|
507
|
+
if @tls_verify
|
|
508
|
+
params[:verify_mode] = OpenSSL::SSL::VERIFY_PEER
|
|
509
|
+
params[:ca_file] = @tls_ca_file if @tls_ca_file
|
|
510
|
+
params[:ca_path] = @tls_ca_path if @tls_ca_path
|
|
511
|
+
else
|
|
512
|
+
params[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
params
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def tls_hostname_for_ssl(default_host)
|
|
519
|
+
return @tls_hostname if @tls_hostname
|
|
520
|
+
return nil unless @tls_verify
|
|
521
|
+
|
|
522
|
+
default_host
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def wrap_tls_socket(socket, host)
|
|
526
|
+
context = OpenSSL::SSL::SSLContext.new
|
|
527
|
+
context.set_params(tls_params)
|
|
528
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, context)
|
|
529
|
+
ssl_socket.sync_close = true
|
|
530
|
+
if (hostname = tls_hostname_for_ssl(host))
|
|
531
|
+
ssl_socket.hostname = hostname if ssl_socket.respond_to?(:hostname=)
|
|
532
|
+
end
|
|
533
|
+
ssl_socket.connect
|
|
534
|
+
ssl_socket
|
|
535
|
+
rescue StandardError
|
|
536
|
+
ssl_socket&.close rescue nil
|
|
537
|
+
socket.close rescue nil
|
|
538
|
+
raise
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def presence(value)
|
|
542
|
+
stripped = value.to_s.strip
|
|
543
|
+
stripped.empty? ? nil : stripped
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def safe_close_stream
|
|
547
|
+
@stream&.close
|
|
548
|
+
@stream = nil
|
|
549
|
+
rescue StandardError
|
|
550
|
+
@stream = nil
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
end
|
data/lib/version.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe NatsAsync do
|
|
4
|
+
it "exposes a version" do
|
|
5
|
+
expect(NatsAsync::VERSION).to eq("0.1.0")
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it "loads the simple connector" do
|
|
9
|
+
expect(NatsAsync::SimpleConnector).to be_a(Class)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "formats cli help" do
|
|
13
|
+
output = StringIO.new
|
|
14
|
+
|
|
15
|
+
NatsAsync::CommandLine.run([], stdout: output)
|
|
16
|
+
|
|
17
|
+
expect(output.string).to include("Usage: nats-async")
|
|
18
|
+
end
|
|
19
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: nats-async
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- OpenAI Codex
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: async
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.36'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.36'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: base64
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.3'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.3'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: console
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.34'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.34'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: io-endpoint
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.17'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.17'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: io-stream
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.11'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.11'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: nkeys
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.1'
|
|
89
|
+
- - "<"
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '1.0'
|
|
92
|
+
type: :runtime
|
|
93
|
+
prerelease: false
|
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '0.1'
|
|
99
|
+
- - "<"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '1.0'
|
|
102
|
+
- !ruby/object:Gem::Dependency
|
|
103
|
+
name: rake
|
|
104
|
+
requirement: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - "~>"
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '13.0'
|
|
109
|
+
type: :development
|
|
110
|
+
prerelease: false
|
|
111
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - "~>"
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '13.0'
|
|
116
|
+
- !ruby/object:Gem::Dependency
|
|
117
|
+
name: rspec
|
|
118
|
+
requirement: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - "~>"
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '3.10'
|
|
123
|
+
type: :development
|
|
124
|
+
prerelease: false
|
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - "~>"
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '3.10'
|
|
130
|
+
- !ruby/object:Gem::Dependency
|
|
131
|
+
name: rubocop
|
|
132
|
+
requirement: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - "~>"
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '1.12'
|
|
137
|
+
type: :development
|
|
138
|
+
prerelease: false
|
|
139
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - "~>"
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '1.12'
|
|
144
|
+
- !ruby/object:Gem::Dependency
|
|
145
|
+
name: rubocop-rake
|
|
146
|
+
requirement: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - "~>"
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: 0.6.0
|
|
151
|
+
type: :development
|
|
152
|
+
prerelease: false
|
|
153
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
154
|
+
requirements:
|
|
155
|
+
- - "~>"
|
|
156
|
+
- !ruby/object:Gem::Version
|
|
157
|
+
version: 0.6.0
|
|
158
|
+
- !ruby/object:Gem::Dependency
|
|
159
|
+
name: rubocop-rspec
|
|
160
|
+
requirement: !ruby/object:Gem::Requirement
|
|
161
|
+
requirements:
|
|
162
|
+
- - "~>"
|
|
163
|
+
- !ruby/object:Gem::Version
|
|
164
|
+
version: 2.14.2
|
|
165
|
+
type: :development
|
|
166
|
+
prerelease: false
|
|
167
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
168
|
+
requirements:
|
|
169
|
+
- - "~>"
|
|
170
|
+
- !ruby/object:Gem::Version
|
|
171
|
+
version: 2.14.2
|
|
172
|
+
- !ruby/object:Gem::Dependency
|
|
173
|
+
name: rspec_junit_formatter
|
|
174
|
+
requirement: !ruby/object:Gem::Requirement
|
|
175
|
+
requirements:
|
|
176
|
+
- - "~>"
|
|
177
|
+
- !ruby/object:Gem::Version
|
|
178
|
+
version: 0.5.1
|
|
179
|
+
type: :development
|
|
180
|
+
prerelease: false
|
|
181
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
182
|
+
requirements:
|
|
183
|
+
- - "~>"
|
|
184
|
+
- !ruby/object:Gem::Version
|
|
185
|
+
version: 0.5.1
|
|
186
|
+
description: Lightweight async Ruby connector for NATS with request/reply and basic
|
|
187
|
+
JetStream ack helpers.
|
|
188
|
+
email:
|
|
189
|
+
- author@email.address
|
|
190
|
+
executables:
|
|
191
|
+
- nats-async
|
|
192
|
+
extensions: []
|
|
193
|
+
extra_rdoc_files: []
|
|
194
|
+
files:
|
|
195
|
+
- ".github/workflows/release.yml"
|
|
196
|
+
- ".rspec"
|
|
197
|
+
- ".rubocop.yml"
|
|
198
|
+
- README.erb
|
|
199
|
+
- README.md
|
|
200
|
+
- Rakefile
|
|
201
|
+
- bin/nats-async
|
|
202
|
+
- lib/nats-async.rb
|
|
203
|
+
- lib/nats_async/command_line.rb
|
|
204
|
+
- lib/nats_async/simple_connector.rb
|
|
205
|
+
- lib/version.rb
|
|
206
|
+
- spec/nats_async_spec.rb
|
|
207
|
+
- spec/spec_helper.rb
|
|
208
|
+
homepage: https://rubygems.org/gems/nats-async
|
|
209
|
+
licenses:
|
|
210
|
+
- Nonstandard
|
|
211
|
+
metadata:
|
|
212
|
+
source_code_uri: https://github.com/example/nats-async
|
|
213
|
+
rdoc_options: []
|
|
214
|
+
require_paths:
|
|
215
|
+
- lib
|
|
216
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
217
|
+
requirements:
|
|
218
|
+
- - ">="
|
|
219
|
+
- !ruby/object:Gem::Version
|
|
220
|
+
version: 3.4.4
|
|
221
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
222
|
+
requirements:
|
|
223
|
+
- - ">="
|
|
224
|
+
- !ruby/object:Gem::Version
|
|
225
|
+
version: '0'
|
|
226
|
+
requirements: []
|
|
227
|
+
rubygems_version: 3.6.7
|
|
228
|
+
specification_version: 4
|
|
229
|
+
summary: Async NATS connector prototype packaged as a gem
|
|
230
|
+
test_files: []
|