tether 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.
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "uri"
5
+
6
+ module Tether
7
+ # The socket a connection reads and writes.
8
+ #
9
+ # Deadlines are applied with `IO#timeout=`, which raises `IO::TimeoutError` at
10
+ # the point the operating system reports the socket is not ready, rather than
11
+ # at an arbitrary bytecode boundary the way `Timeout.timeout` would.
12
+ class Transport
13
+ DEFAULT_PORT = 4222
14
+ READ_SIZE = 64 * 1024
15
+
16
+ # @return [URI::Generic] the server this transport is connected to
17
+ attr_reader :uri
18
+
19
+ # Opens a TCP connection.
20
+ #
21
+ # @param uri [String, URI::Generic]
22
+ # @param deadline [Deadline] bounds the connect and every later read
23
+ # @return [Transport]
24
+ # @raise [ConnectionError] if the server cannot be reached
25
+ def self.connect(uri, deadline: Deadline.infinite)
26
+ new(uri).tap { _1.open(deadline) }
27
+ end
28
+
29
+ # Prepares a transport without opening it.
30
+ #
31
+ # @param uri [String, URI::Generic] a bare host:port is treated as nats://
32
+ def initialize(uri)
33
+ @uri = normalize(uri)
34
+ @lock = Mutex.new
35
+ end
36
+
37
+ # Opens the socket with TCP_NODELAY set, since the protocol is latency
38
+ # sensitive and frames are already written whole.
39
+ #
40
+ # @param deadline [Deadline]
41
+ # @return [self]
42
+ # @raise [ConnectionError]
43
+ def open(deadline = Deadline.infinite)
44
+ @socket = Socket.tcp(@uri.host, @uri.port, connect_timeout: deadline.remaining)
45
+ @socket.sync = true
46
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
47
+ self
48
+ rescue SystemCallError, IO::TimeoutError => e
49
+ raise ConnectionError, "could not connect to #{@uri}: #{e.message}"
50
+ end
51
+
52
+ # Reads whatever is available.
53
+ #
54
+ # @param deadline [Deadline]
55
+ # @return [String, nil] bytes, or nil at end of stream
56
+ # @raise [TimeoutError] if the deadline passes with nothing readable
57
+ def read(deadline = Deadline.infinite)
58
+ socket = @socket or raise ConnectionClosedError, "transport is closed"
59
+ socket.timeout = deadline.remaining
60
+ socket.readpartial(READ_SIZE)
61
+ rescue EOFError
62
+ nil
63
+ rescue IO::TimeoutError
64
+ raise TimeoutError, "timed out reading from #{@uri}"
65
+ rescue IOError, SystemCallError => e
66
+ raise ConnectionClosedError, "read failed on #{@uri}: #{e.message}"
67
+ end
68
+
69
+ # Writes one complete frame.
70
+ #
71
+ # The lock spans the whole frame rather than each write, so frames from
72
+ # different threads cannot interleave on the wire.
73
+ #
74
+ # @param frame [String] a complete, already encoded frame
75
+ # @return [Integer] bytes written
76
+ def write(frame)
77
+ @lock.synchronize do
78
+ socket = @socket or raise ConnectionClosedError, "transport is closed"
79
+ socket.write(frame)
80
+ end
81
+ rescue IOError, SystemCallError => e
82
+ raise ConnectionClosedError, "write failed on #{@uri}: #{e.message}"
83
+ end
84
+
85
+ # Closes the socket, which also unblocks a read waiting on it.
86
+ #
87
+ # @return [void]
88
+ def close
89
+ @lock.synchronize do
90
+ @socket&.close
91
+ @socket = nil
92
+ end
93
+ rescue IOError, SystemCallError
94
+ @socket = nil
95
+ end
96
+
97
+ # @return [Boolean] whether the socket is gone; every read and write raises after this
98
+ def closed?
99
+ @socket.nil? || @socket.closed?
100
+ end
101
+
102
+ private
103
+
104
+ def normalize(uri)
105
+ parsed = uri.is_a?(URI::Generic) ? uri : URI.parse(coerce_scheme(uri.to_s))
106
+ parsed.port ||= DEFAULT_PORT
107
+ parsed
108
+ end
109
+
110
+ def coerce_scheme(value)
111
+ value.include?("://") ? value : "nats://#{value}"
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tether
4
+ VERSION = "0.1.0"
5
+ end
data/lib/tether.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tether/version"
4
+ require_relative "tether/errors"
5
+ require_relative "tether/deadline"
6
+ require_relative "tether/nuid"
7
+ require_relative "tether/headers"
8
+ require_relative "tether/protocol"
9
+ require_relative "tether/protocol/encoder"
10
+ require_relative "tether/protocol/parser"
11
+ require_relative "tether/msg"
12
+ require_relative "tether/transport"
13
+ require_relative "tether/subscription"
14
+ require_relative "tether/dispatcher"
15
+ require_relative "tether/inbox_mux"
16
+ require_relative "tether/client"
17
+
18
+ # A NATS client for Ruby.
19
+ module Tether
20
+ # Connects to a NATS server.
21
+ #
22
+ # @param uri [String, nil] defaults to NATS_URL, then nats://127.0.0.1:4222
23
+ # @param options [Hash{Symbol => Object}] CONNECT overrides plus :connect_timeout
24
+ # @return [Client, Object] the client, or the block's value when a block is given
25
+ def self.connect(uri = nil, **options, &)
26
+ Client.connect(uri, **options, &)
27
+ end
28
+ end
@@ -0,0 +1,45 @@
1
+ module Tether
2
+ def self.connect: (?String? uri, **untyped options) ?{ (Client) -> untyped } -> untyped
3
+
4
+ class Client
5
+ DEFAULT_URI: String
6
+ DEFAULT_CONNECT_TIMEOUT: Integer
7
+ DEFAULT_FLUSH_TIMEOUT: Integer
8
+ DEFAULT_REQUEST_TIMEOUT: Integer
9
+
10
+ attr_reader server_info: Hash[String, untyped]?
11
+ attr_reader status: Symbol
12
+ attr_reader last_error: Exception?
13
+
14
+ def self.connect: (?String? uri, **untyped options) ?{ (Client) -> untyped } -> untyped
15
+
16
+ def initialize: (?String? uri, **untyped options) -> void
17
+ def connect: (?timeout: Numeric?) -> self
18
+ def publish: (String subject, ?String? payload, ?reply_to: String?, ?headers: Headers?) -> void
19
+ def subscribe: (String subject, ?queue: String?, ?max_pending: Integer) ?{ (Msg) -> void } -> Subscription
20
+ def unsubscribe: (Subscription subscription, ?Integer? max_msgs) -> void
21
+ def request: (String subject, ?String? payload, ?timeout: Numeric?, ?headers: Headers?) -> Msg
22
+ def new_inbox: () -> String
23
+ def flush: (?timeout: Numeric?) -> bool
24
+ def close: () -> void
25
+ def connected?: () -> bool
26
+ def closed?: () -> bool
27
+ def on_error: () { (Exception) -> void } -> void
28
+ def subscription_count: () -> Integer
29
+ def max_payload: () -> Integer?
30
+
31
+ private
32
+
33
+ def inbox_mux: () -> InboxMux
34
+ def build_dispatcher: () -> Dispatcher
35
+ def read_server_info: (Deadline deadline) -> void
36
+ def connect_options: () -> Hash[Symbol, untyped]
37
+ def start_reader: () -> Thread
38
+ def read_loop: () -> void
39
+ def next_sid: () -> String
40
+ def check_payload_size!: (String subject, String frame) -> void
41
+ def report_slow_consumer: (Subscription subscription) -> void
42
+ def record_error: (Exception error) -> void
43
+ def ensure_connected!: () -> void
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ module Tether
2
+ class Deadline
3
+ NONE: nil
4
+
5
+ attr_reader at: Float?
6
+
7
+ def self.after: (Numeric? seconds) -> Deadline
8
+ def self.infinite: () -> Deadline
9
+ def self.coerce: (Deadline | Numeric | nil value) -> Deadline
10
+ def self.now: () -> Float
11
+
12
+ def initialize: (Float? at) -> void
13
+ def infinite?: () -> bool
14
+ def remaining: () -> Float?
15
+ def expired?: () -> bool
16
+ def check!: (?String? subject) -> void
17
+ def with_at_most: (Numeric? seconds) -> Deadline
18
+ def earliest: (Deadline other) -> Deadline
19
+ def inspect: () -> String
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module Tether
2
+ class Dispatcher
3
+ def initialize: (**^(*untyped) -> void handlers) -> void
4
+ def add: (Subscription subscription) -> Subscription
5
+ def remove: (String sid) -> Subscription?
6
+ def []: (String sid) -> Subscription?
7
+ def subscriptions: () -> Array[Subscription]
8
+ def size: () -> Integer
9
+ def clear: () -> void
10
+
11
+ def on_msg: (String subject, String sid, String? reply_to, String? header_bytes, String payload) -> void
12
+ def on_ping: () -> void
13
+ def on_pong: () -> void
14
+ def on_ok: () -> void
15
+ def on_info: (String json) -> void
16
+ def on_error: (String description) -> void
17
+ def on_protocol_error: (String message) -> void
18
+ end
19
+ end
@@ -0,0 +1,110 @@
1
+ module Tether
2
+ class Error < StandardError
3
+ end
4
+
5
+ class ProtocolError < Error
6
+ end
7
+
8
+ class TimeoutError < Error
9
+ end
10
+
11
+ class NoRespondersError < Error
12
+ end
13
+
14
+ class MaxPayloadError < Error
15
+ end
16
+
17
+ class SlowConsumerError < Error
18
+ end
19
+
20
+ class InvalidSubjectError < Error
21
+ end
22
+
23
+ class ConnectionError < Error
24
+ end
25
+
26
+ class ConnectionClosedError < ConnectionError
27
+ end
28
+
29
+ class ConnectionDrainingError < ConnectionError
30
+ end
31
+
32
+ class ConnectionReconnectingError < ConnectionError
33
+ end
34
+
35
+ class NoServersError < ConnectionError
36
+ end
37
+
38
+ class ServerError < Error
39
+ MAPPING: Hash[String, Symbol]
40
+
41
+ def self.for: (String? description) -> ServerError
42
+ def self.fatal?: () -> bool
43
+ def fatal?: () -> bool
44
+ end
45
+
46
+ class AuthorizationError < ServerError
47
+ end
48
+
49
+ class AuthenticationExpiredError < ServerError
50
+ end
51
+
52
+ class AuthenticationRevokedError < ServerError
53
+ end
54
+
55
+ class AuthenticationTimeoutError < ServerError
56
+ end
57
+
58
+ class StaleConnectionError < ServerError
59
+ end
60
+
61
+ class MaxConnectionsError < ServerError
62
+ end
63
+
64
+ class MaxAccountConnectionsError < ServerError
65
+ end
66
+
67
+ class MaxPayloadViolationError < ServerError
68
+ end
69
+
70
+ class MaxControlLineError < ServerError
71
+ end
72
+
73
+ class NoRespondersRequiresHeadersError < ServerError
74
+ end
75
+
76
+ class ConnectionThrottledError < ServerError
77
+ end
78
+
79
+ class TLSRequiredError < ServerError
80
+ end
81
+
82
+ class InvalidClientProtocolError < ServerError
83
+ end
84
+
85
+ class UnknownProtocolError < ServerError
86
+ end
87
+
88
+ class WrongPortError < ServerError
89
+ end
90
+
91
+ class ServerInvalidSubjectError < ServerError
92
+ def self.fatal?: () -> bool
93
+ end
94
+
95
+ class InvalidPublishSubjectError < ServerError
96
+ def self.fatal?: () -> bool
97
+ end
98
+
99
+ class FailedAccountRegistrationError < ServerError
100
+ def self.fatal?: () -> bool
101
+ end
102
+
103
+ class MaxSubscriptionsError < ServerError
104
+ def self.fatal?: () -> bool
105
+ end
106
+
107
+ class PermissionsViolationError < ServerError
108
+ def self.fatal?: () -> bool
109
+ end
110
+ end
@@ -0,0 +1,50 @@
1
+ module Tether
2
+ class Headers
3
+ type entries = Hash[String, String | Array[String]]
4
+
5
+ VERSION: String
6
+ CONTROL: Integer
7
+ NO_CONTENT: Integer
8
+ BAD_REQUEST: Integer
9
+ NOT_FOUND: Integer
10
+ REQUEST_TIMEOUT: Integer
11
+ CONFLICT: Integer
12
+ REQUIRED_API_LEVEL: Integer
13
+ TOO_MANY_RESULTS: Integer
14
+ PIN_ID_MISMATCH: Integer
15
+ TOO_MANY_REQUESTS: Integer
16
+ INTERNAL_ERROR: Integer
17
+ NO_RESPONDERS: Integer
18
+
19
+ attr_reader status: Integer?
20
+ attr_reader description: String?
21
+
22
+ def self.parse: (String? bytes) -> Headers
23
+ def self.[]: (entries) -> Headers
24
+
25
+ def initialize: (?entries: entries, ?status: Integer?, ?description: String?) -> void
26
+ def []: (String key) -> String?
27
+ def []=: (String key, String value) -> String
28
+ def add: (String key, String value) -> self
29
+ def values: (String key) -> Array[String]
30
+ def delete: (String key) -> Array[String]?
31
+ def key?: (String key) -> bool
32
+ def each: () { (String, String) -> void } -> self
33
+ | () -> Enumerator[[String, String], self]
34
+ def keys: () -> Array[String]
35
+ def empty?: () -> bool
36
+ def no_responders?: () -> bool
37
+ def control?: () -> bool
38
+ def idle_heartbeat?: () -> bool
39
+ def flow_control?: () -> bool
40
+ def to_h: () -> Hash[String, Array[String]]
41
+ def to_wire: () -> String
42
+ def inspect: () -> String
43
+
44
+ private
45
+
46
+ def validate_key: (untyped key) -> String
47
+ def validate_value: (untyped value) -> String
48
+ def status_suffix: () -> String
49
+ end
50
+ end
@@ -0,0 +1,20 @@
1
+ module Tether
2
+ class InboxMux
3
+ INBOX_PREFIX: String
4
+
5
+ attr_reader prefix: String
6
+
7
+ def initialize: (Client client, ?prefix: String?, ?max_pending: Integer) -> void
8
+ def request: (String subject, ?String? payload, ?deadline: Deadline, ?headers: Headers?) -> Msg
9
+ def start: () -> Subscription
10
+ def started?: () -> bool
11
+ def pending: () -> Integer
12
+ def close: () -> void
13
+
14
+ private
15
+
16
+ def await: (Thread::Queue waiter, String subject, Deadline deadline) -> Msg
17
+ def resolve: (Msg msg) -> void
18
+ def next_token: () -> String
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ module Tether
2
+ class Msg
3
+ attr_reader subject: String
4
+ attr_reader data: String
5
+ attr_reader reply_to: String?
6
+ attr_reader subscription: Subscription?
7
+
8
+ def initialize: (subject: String, data: String, ?reply_to: String?, ?header_bytes: String?, ?subscription: Subscription?) -> void
9
+ def headers: () -> Headers?
10
+ def status: () -> Integer?
11
+ def no_responders?: () -> bool
12
+ def respond: (?String? payload, ?headers: Headers?) -> void
13
+ def empty?: () -> bool
14
+ def inspect: () -> String
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ module Tether
2
+ class NUID
3
+ DIGITS: String
4
+ BASE: Integer
5
+ PREFIX_LENGTH: Integer
6
+ SEQUENCE_LENGTH: Integer
7
+ TOTAL_LENGTH: Integer
8
+ MAX_SEQUENCE: Integer
9
+ MIN_INCREMENT: Integer
10
+ MAX_INCREMENT: Integer
11
+
12
+ def self.next: () -> String
13
+
14
+ def initialize: () -> void
15
+ def next: () -> String
16
+
17
+ private
18
+
19
+ def reseed!: () -> void
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ module Tether
2
+ module Protocol
3
+ module Encoder
4
+ def self.pub: (String subject, ?String? payload, ?String? reply_to) -> String
5
+ def self.hpub: (String subject, String? payload, Headers | String headers, ?String? reply_to) -> String
6
+ def self.sub: (String subject, Integer | String sid, ?queue: String?) -> String
7
+ def self.unsub: (Integer | String sid, ?Integer? max_msgs) -> String
8
+ DEFAULT_CONNECT: Hash[Symbol, untyped]
9
+
10
+ def self.connect: (?Hash[Symbol | String, untyped] options) -> String
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,43 @@
1
+ module Tether
2
+ module Protocol
3
+ class Parser
4
+ COMPACT_THRESHOLD: Integer
5
+ SNIPPET_LIMIT: Integer
6
+ AWAITING_CONTROL_LINE: Symbol
7
+ AWAITING_PAYLOAD: Symbol
8
+
9
+ attr_reader state: Symbol
10
+ attr_accessor max_payload: Integer?
11
+
12
+ def initialize: (_ParserSink sink, ?max_control_line: Integer, ?max_payload: Integer?) -> void
13
+ def <<: (String? data) -> self
14
+ alias parse <<
15
+ def reset!: () -> void
16
+ def broken?: () -> bool
17
+ def pending_bytes: () -> Integer
18
+
19
+ private
20
+
21
+ def run: () -> void
22
+ def consume_control_line: () -> bool
23
+ def match_message_line: () -> bool?
24
+ def start_payload: (MatchData match, bool headers) -> bool
25
+ def dispatch: (Integer start, Integer line_end) -> bool
26
+ def dispatch_ping_pong: (Integer start, Integer line_end) -> bool
27
+ def dispatch_ok: (Integer start, Integer line_end) -> bool
28
+ def dispatch_err: (Integer start, Integer line_end) -> bool
29
+ def dispatch_info: (Integer start, Integer line_end) -> bool
30
+ def consume_payload: () -> bool
31
+ def payload_terminated?: () -> bool
32
+ def emit_message: () -> void
33
+ def verb?: (Integer start, Integer limit, String verb) -> bool
34
+ def trimmed: (Integer from, Integer to) -> String
35
+ def snippet: (Integer start, Integer line_end) -> String
36
+ def clear_frame: () -> void
37
+ def compact: () -> void
38
+ def malformed: (Integer start, Integer line_end) -> bool
39
+ def unknown_operation: (Integer start, Integer line_end) -> bool
40
+ def fail_protocol!: (String message) -> bool
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,22 @@
1
+ module Tether
2
+ module Protocol
3
+ CRLF: String
4
+ CRLF_SIZE: Integer
5
+ EMPTY_PAYLOAD: String
6
+ PING: String
7
+ PONG: String
8
+ DEFAULT_MAX_CONTROL_LINE: Integer
9
+ BINARY: Encoding
10
+
11
+ # The contract {Parser} delivers frames to. Implemented by {Dispatcher}.
12
+ interface _ParserSink
13
+ def on_msg: (String subject, String sid, String? reply_to, String? headers, String payload) -> void
14
+ def on_ping: () -> void
15
+ def on_pong: () -> void
16
+ def on_ok: () -> void
17
+ def on_info: (String json) -> void
18
+ def on_error: (String description) -> void
19
+ def on_protocol_error: (String message) -> void
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module Tether
2
+ class Subscription
3
+ DEFAULT_MAX_PENDING: Integer
4
+
5
+ attr_reader sid: String
6
+ attr_reader subject: String
7
+ attr_reader queue_group: String?
8
+ attr_reader delivered: Integer
9
+ attr_reader dropped: Integer
10
+ attr_reader responder: Client?
11
+
12
+ def initialize: (sid: String, subject: String, ?queue_group: String?, ?max_pending: Integer, ?on_slow_consumer: (^(Subscription) -> void)?, ?responder: Client?) ?{ (Msg) -> void } -> void
13
+ def deliver: (Msg msg) -> bool
14
+ def next_message: (?Deadline deadline) -> Msg
15
+ def pending: () -> Integer
16
+ def closed?: () -> bool
17
+ def close: () -> void
18
+
19
+ private
20
+
21
+ def start_worker: () -> Thread
22
+ def invoke: (Msg msg) -> void
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ module Tether
2
+ class Transport
3
+ DEFAULT_PORT: Integer
4
+ READ_SIZE: Integer
5
+
6
+ attr_reader uri: URI::Generic
7
+
8
+ def self.connect: (String | URI::Generic uri, ?deadline: Deadline) -> Transport
9
+
10
+ def initialize: (String | URI::Generic uri) -> void
11
+ def open: (?Deadline deadline) -> self
12
+ def read: (?Deadline deadline) -> String?
13
+ def write: (String frame) -> Integer
14
+ def close: () -> void
15
+ def closed?: () -> bool
16
+
17
+ private
18
+
19
+ def normalize: (String | URI::Generic uri) -> URI::Generic
20
+ def coerce_scheme: (String value) -> String
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module Tether
2
+ VERSION: String
3
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tether
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dmitry Vorotilin
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: A NATS and JetStream client for Ruby, built around explicit deadlines
13
+ and per-subscription dispatch so concurrent use of one connection is safe by design.
14
+ email:
15
+ - d.vorotilin@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE.txt
21
+ - README.md
22
+ - Rakefile
23
+ - lib/tether.rb
24
+ - lib/tether/client.rb
25
+ - lib/tether/deadline.rb
26
+ - lib/tether/dispatcher.rb
27
+ - lib/tether/errors.rb
28
+ - lib/tether/headers.rb
29
+ - lib/tether/inbox_mux.rb
30
+ - lib/tether/msg.rb
31
+ - lib/tether/nuid.rb
32
+ - lib/tether/protocol.rb
33
+ - lib/tether/protocol/encoder.rb
34
+ - lib/tether/protocol/parser.rb
35
+ - lib/tether/subscription.rb
36
+ - lib/tether/transport.rb
37
+ - lib/tether/version.rb
38
+ - sig/tether/client.rbs
39
+ - sig/tether/deadline.rbs
40
+ - sig/tether/dispatcher.rbs
41
+ - sig/tether/errors.rbs
42
+ - sig/tether/headers.rbs
43
+ - sig/tether/inbox_mux.rbs
44
+ - sig/tether/msg.rbs
45
+ - sig/tether/nuid.rbs
46
+ - sig/tether/protocol.rbs
47
+ - sig/tether/protocol/encoder.rbs
48
+ - sig/tether/protocol/parser.rbs
49
+ - sig/tether/subscription.rbs
50
+ - sig/tether/transport.rbs
51
+ - sig/tether/version.rbs
52
+ homepage: https://github.com/bitsbeam/tether
53
+ licenses:
54
+ - MIT
55
+ metadata:
56
+ homepage_uri: https://github.com/bitsbeam/tether
57
+ source_code_uri: https://github.com/bitsbeam/tether
58
+ changelog_uri: https://github.com/bitsbeam/tether/blob/main/CHANGELOG.md
59
+ rubygems_mfa_required: 'true'
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 3.2.0
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 4.0.12
75
+ specification_version: 4
76
+ summary: A NATS client for Ruby
77
+ test_files: []