nats 0.3.12

Sign up to get free protection for your applications and to get access to all the features.
data/COPYING ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 Derek Collison <derek.collison@gmail.com>. All rights reserved.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19
+ IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # NATS
2
+
3
+ EventMachine based Publish-Subscribe Messaging that just works.
4
+
5
+ ## Supported Platforms
6
+
7
+ This gem currently works on the following Ruby platforms:
8
+
9
+ - MRI 1.8 and 1.9 (Performance is best on 1.9.2)
10
+ - Rubinius
11
+ - JRuby (not quite yet)
12
+
13
+ ## Getting Started
14
+
15
+ [sudo] gem install nats
16
+
17
+ or
18
+
19
+ git clone
20
+ [sudo] rake geminstall
21
+
22
+ nats-sub foo &
23
+ nats-pub foo "Hello World!'
24
+
25
+ ## Usage
26
+
27
+ require "nats/client"
28
+
29
+ NATS.start do
30
+
31
+ # Simple Subscriber
32
+ NATS.subscribe('foo') { |msg| puts "Msg received : '#{msg}'" }
33
+
34
+ # Simple Publisher
35
+ NATS.publish('foo.bar.baz', 'Hello World!')
36
+
37
+ # Publish with closure, callback fires when server has processed the message
38
+ NATS.publish('foo', 'You done?') { puts 'msg processed!' }
39
+
40
+ # Unsubscribing
41
+ s = NATS.subscribe('bar') { |msg| puts "Msg received : '#{msg}'" }
42
+ NATS.unsubscribe(s)
43
+
44
+ # Request/Response
45
+
46
+ # The helper
47
+ NATS.subscribe('help') do |msg, reply|
48
+ NATS.publish(reply, "I'll help!")
49
+ end
50
+
51
+ # Help request
52
+ NATS.request('help') { |response|
53
+ puts "Got a response: '#{response}'"
54
+ }
55
+
56
+ # Wildcard Subscriptions
57
+
58
+ # '*" matches any token
59
+ NATS.subscribe('foo.*.baz') { |msg, _, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
60
+
61
+ # '>" can only be last token, and matches to any depth
62
+ NATS.subscribe('foo.>') { |msg, _, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
63
+
64
+
65
+ # Stop using NATS.stop, exits EM loop if NATS.start started it
66
+ NATS.stop
67
+
68
+ end
69
+
70
+ See examples and benchmark for more..
71
+
72
+ ## License
73
+
74
+ (The MIT License)
75
+
76
+ Copyright (c) 2010 Derek Collison
77
+
78
+ Permission is hereby granted, free of charge, to any person obtaining a copy
79
+ of this software and associated documentation files (the "Software"), to
80
+ deal in the Software without restriction, including without limitation the
81
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
82
+ sell copies of the Software, and to permit persons to whom the Software is
83
+ furnished to do so, subject to the following conditions:
84
+
85
+ The above copyright notice and this permission notice shall be included in
86
+ all copies or substantial portions of the Software.
87
+
88
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
89
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
90
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
91
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
92
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
93
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
94
+ IN THE SOFTWARE.
95
+
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+
2
+ desc "Run rspec"
3
+ task :spec do
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new do |t|
7
+ t.rspec_opts = %w(-fs -c)
8
+ end
9
+ end
10
+
11
+ desc "Build the gem"
12
+ task :gem do
13
+ sh 'gem build *.gemspec'
14
+ end
15
+
16
+ desc "Install the gem"
17
+ task :geminstall do
18
+ sh 'gem build *.gemspec'
19
+ sh 'gem install *.gem'
20
+ sh 'rm *.gem'
21
+ end
22
+
23
+ desc "Synonym for spec"
24
+ task :test => :spec
25
+ desc "Synonym for spec"
26
+ task :tests => :spec
27
+
28
+ desc "Synonym for gem"
29
+ task :pkg => :gem
30
+ desc "Synonym for gem"
31
+ task :package => :gem
data/bin/nats-pub ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'nats/client'
5
+
6
+ def usage
7
+ puts "Usage: nats-pub <subject> <msg>"; exit
8
+ end
9
+
10
+ subject, msg = ARGV
11
+ usage unless subject
12
+ msg ||= 'Hello World'
13
+
14
+ NATS.on_error { |err| puts "Server Error: #{err}"; exit! }
15
+
16
+ NATS.start do
17
+ NATS.publish(subject, msg) { NATS.stop }
18
+ end
19
+
20
+ puts "Published [#{subject}] : '#{msg}'"
data/bin/nats-server ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # NATS command line interface script.
3
+ # Run <tt>nats-server -h</tt> to get more usage.
4
+
5
+ require 'nats/server'
data/bin/nats-sub ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'nats/client'
5
+
6
+ trap("TERM") { NATS.stop }
7
+ trap("INT") { NATS.stop }
8
+
9
+ def usage
10
+ puts "Usage: nats-sub <subject>"; exit
11
+ end
12
+
13
+ subject = ARGV.shift
14
+ usage unless subject
15
+ i = 0
16
+
17
+ NATS.on_error { |err| puts "Server Error: #{err}"; exit! }
18
+
19
+ NATS.start do
20
+ puts "Listening on [#{subject}]"
21
+ NATS.subscribe(subject) { |msg, _, sub| puts "\##{i+=1}: Received on [#{sub}] : '#{msg}'" }
22
+ end
@@ -0,0 +1,424 @@
1
+
2
+ require 'uri'
3
+
4
+ require File.dirname(__FILE__) + '/ext/em'
5
+ require File.dirname(__FILE__) + '/ext/bytesize'
6
+ require File.dirname(__FILE__) + '/ext/json'
7
+
8
+ # NATS is a simple publish-subscribe messaging system.
9
+ #
10
+ # == Usage
11
+ # <tt>
12
+ # require "nats/client"
13
+ #
14
+ # NATS.start do
15
+ #
16
+ # # Simple Subscriber
17
+ # NATS.subscribe('foo') { |msg| puts "Msg received : '#{msg}'" }
18
+ #
19
+ # # Simple Publisher
20
+ # NATS.publish('foo.bar.baz', 'Hello World!')
21
+ #
22
+ # # Publish with closure, callback fires when server has processed the message
23
+ # NATS.publish('foo', 'You done?') { puts 'msg processed!' }
24
+ #
25
+ # # Unsubscribing
26
+ # s = NATS.subscribe('bar') { |msg| puts "Msg received : '#{msg}'" }
27
+ # NATS.unsubscribe(s)
28
+ #
29
+ # # Request/Response
30
+ #
31
+ # # The helper
32
+ # NATS.subscribe('help') do |msg, reply|
33
+ # NATS.publish(reply, "I'll help!")
34
+ # end
35
+ #
36
+ # # Help request
37
+ # NATS.request('help') { |response|
38
+ # puts "Got a response: '#{response}'"
39
+ # }
40
+ #
41
+ # # Wildcard Subscriptions
42
+ #
43
+ # # '*" matches any token
44
+ # NATS.subscribe('foo.*.baz') { |msg, _, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
45
+ #
46
+ # # '>" can only be last token, and matches to any depth
47
+ # NATS.subscribe('foo.>') { |msg, _, sub| puts "Msg received on [#{sub}] : '#{msg}'" }
48
+ #
49
+ #
50
+ # # Stop using NATS.stop, exits EM loop if NATS.start started it
51
+ # NATS.stop
52
+ #
53
+ # end
54
+ #
55
+ # </tt>
56
+
57
+
58
+ module NATS
59
+
60
+ # Version <b>0.3.12</b>
61
+ VERSION = "0.3.12".freeze
62
+
63
+ # Default port: <b>4222</b>
64
+ DEFAULT_PORT = 4222
65
+
66
+ # Default URI to connect to the server, <b>nats://localhost:4222</b>
67
+ DEFAULT_URI = "nats://localhost:#{DEFAULT_PORT}".freeze
68
+
69
+ # Max attempts at a reconnect: <b>10</b>
70
+ MAX_RECONNECT_ATTEMPTS = 10
71
+
72
+ # Maximum time to wait for a reconnect: <b>2 seconds</b>
73
+ RECONNECT_TIME_WAIT = 2
74
+
75
+ # Protocol
76
+ MSG = /^MSG\s+(\S+)\s+(\S+)\s+((\S+)\s+)?(\d+)$/i #:nodoc:
77
+ OK = /^\+OK/i #:nodoc:
78
+ ERR = /^-ERR\s+('.+')?/i #:nodoc:
79
+ PING = /^PING/i #:nodoc:
80
+ PONG = /^PONG/i #:nodoc:
81
+ INFO = /^INFO\s+(.+)/i #:nodoc:
82
+
83
+ # Responses
84
+ CR_LF = ("\r\n".freeze) #:nodoc:
85
+ CR_LF_SIZE = (CR_LF.bytesize) #:nodoc:
86
+
87
+ PING_REQUEST = ("PING#{CR_LF}".freeze) #:nodoc:
88
+ PONG_RESPONSE = ("PONG#{CR_LF}".freeze) #:nodoc:
89
+
90
+ EMPTY_MSG = (''.freeze) #:nodoc:
91
+
92
+ # Used for future pedantic Mode
93
+ SUB = /^([^\.\*>\s]+|>$|\*)(\.([^\.\*>\s]+|>$|\*))*$/ #:nodoc:
94
+ SUB_NO_WC = /^([^\.\*>\s]+)(\.([^\.\*>\s]+))*$/ #:nodoc:
95
+
96
+ # Duplicate autostart protection
97
+ @@tried_autostart = {}
98
+
99
+ class Error < StandardError #:nodoc:
100
+ end
101
+
102
+ class << self
103
+ attr_reader :client, :reactor_was_running, :err_cb, :err_cb_overridden #:nodoc:
104
+ alias :reactor_was_running? :reactor_was_running
105
+
106
+ # Create and return a connection to the server with the given options. The server will be autostarted if needed if
107
+ # the <b>uri</b> is determined to be local. The optional block will be called when the connection has been completed.
108
+ #
109
+ def connect(options = {}, &blk)
110
+ options[:uri] ||= ENV['NATS_URI'] || DEFAULT_URI
111
+ options[:debug] ||= ENV['NATS_DEBUG']
112
+ options[:autostart] = (ENV['NATS_AUTO'] || true) unless options[:autostart] != nil
113
+ uri = options[:uri] = URI.parse(options[:uri])
114
+ @err_cb = proc { raise Error, "Could not connect to server on #{uri}."} unless err_cb
115
+ check_autostart(uri) if options[:autostart]
116
+ client = EM.connect(uri.host, uri.port, self, options)
117
+ client.on_connect(&blk) if blk
118
+ return client
119
+ end
120
+
121
+ # Create a default client connection to the server. See connect for more information.
122
+ def start(*args, &blk)
123
+ @reactor_was_running = EM.reactor_running?
124
+ unless (@reactor_was_running || blk)
125
+ raise(Error, "EM needs to be running when NATS.start called without a run block")
126
+ end
127
+ EM.run { @client = connect(*args, &blk) }
128
+ end
129
+
130
+ # Close the default client connection and optionally call the associated block.
131
+ def stop(&blk)
132
+ client.close if (client and client.connected?)
133
+ blk.call if blk
134
+ end
135
+
136
+ # Set the default on_error callback.
137
+ def on_error(&callback)
138
+ @err_cb, @err_cb_overridden = callback, true
139
+ end
140
+
141
+ # Publish a message using the default client connection. See NATS#publish for more information.
142
+ def publish(*args, &blk)
143
+ (@client ||= connect).publish(*args, &blk)
144
+ end
145
+
146
+ # Subscribe using the default client connection. See NATS#subscribe for more information.
147
+ def subscribe(*args, &blk)
148
+ (@client ||= connect).subscribe(*args, &blk)
149
+ end
150
+
151
+ # Cancel a subscription on the default client connection.
152
+ def unsubscribe(*args)
153
+ (@client ||= connect).unsubscribe(*args)
154
+ end
155
+
156
+ # Publish a message and wait for a response on the default client connection. See NATS#request for more information.
157
+ def request(*args, &blk)
158
+ (@client ||= connect).request(*args, &blk)
159
+ end
160
+
161
+ # Returns a subject that can be used for "directed" communications, utilized in #request.
162
+ def create_inbox
163
+ v = [rand(0x0010000),rand(0x0010000),rand(0x0010000),
164
+ rand(0x0010000),rand(0x0010000),rand(0x1000000)]
165
+ "_INBOX.%04x%04x%04x%04x%04x%06x" % v
166
+ end
167
+
168
+ def check_autostart(uri) #:nodoc:
169
+ return if uri_is_remote?(uri) || @@tried_autostart[uri]
170
+ @@tried_autostart[uri] = true
171
+ return if server_running?(uri)
172
+ return unless try_autostart_succeeded?(uri)
173
+ wait_for_server(uri)
174
+ end
175
+
176
+ def uri_is_remote?(uri) #:nodoc:
177
+ uri.host != 'localhost' && uri.host != '127.0.0.1'
178
+ end
179
+
180
+ def try_autostart_succeeded?(uri) #:nodoc:
181
+ port_arg = "-p #{uri.port}"
182
+ user_arg = "--user #{uri.user}" if uri.user
183
+ pass_arg = "--pass #{uri.password}" if uri.password
184
+ log_arg = '-l /tmp/nats-server.log'
185
+ pid_arg = '-P /tmp/nats-server.pid'
186
+ # daemon mode to release client
187
+ system("nats-server #{port_arg} #{user_arg} #{pass_arg} #{log_arg} #{pid_arg} -d 2> /dev/null")
188
+ $? == 0
189
+ end
190
+
191
+ def wait_for_server(uri) #:nodoc:
192
+ start = Time.now
193
+ while (Time.now - start < 5) # Wait 5 seconds max
194
+ break if server_running?(uri)
195
+ sleep(0.1)
196
+ end
197
+ end
198
+
199
+ def server_running?(uri) #:nodoc:
200
+ require 'socket'
201
+ s = TCPSocket.new(uri.host, uri.port)
202
+ s.close
203
+ return true
204
+ rescue
205
+ return false
206
+ end
207
+
208
+ end
209
+
210
+ attr_reader :connect_cb, :err_cb, :err_cb_overridden, :connected, :closing, :reconnecting #:nodoc:
211
+
212
+ alias :connected? :connected
213
+ alias :closing? :closing
214
+ alias :reconnecting? :reconnecting
215
+
216
+ def initialize(options)
217
+ @uri = options[:uri]
218
+ @debug = options[:debug]
219
+ @ssid, @subs = 1, {}
220
+ @err_cb = NATS.err_cb
221
+ @reconnect_timer, @needed = nil, nil
222
+ @connected, @closing, @reconnecting = false, false, false
223
+ send_connect_command
224
+ end
225
+
226
+ # Publish a message to a given subject, with optional reply subject and completion block
227
+ def publish(subject, data=EMPTY_MSG, opt_reply=nil, &blk)
228
+ return unless subject
229
+ data = data.to_s
230
+ send_command("PUB #{subject} #{opt_reply} #{data.bytesize}#{CR_LF}#{data}#{CR_LF}")
231
+ queue_server_rt(&blk) if blk
232
+ end
233
+
234
+ # Subscribe to a subject with optional wildcards. Messages will be delivered to the supplied callback.
235
+ # Callback can take any number of the supplied arguments as defined by the list: msg, reply, sub.
236
+ # Returns subscription id which can be passed to NATS#unsubscribe.
237
+ def subscribe(subject, &callback)
238
+ return unless subject
239
+ @ssid += 1
240
+ @subs[@ssid] = { :subject => subject, :callback => callback }
241
+ send_command("SUB #{subject} #{@ssid}#{CR_LF}")
242
+ @ssid
243
+ end
244
+
245
+ # Cancel a subscription.
246
+ def unsubscribe(sid)
247
+ @subs.delete(sid)
248
+ send_command("UNSUB #{sid}#{CR_LF}")
249
+ end
250
+
251
+ # Send a request and have the response delivered to the supplied callback.
252
+ # Returns subscription id which can be passed to NATS#unsubscribe.
253
+ def request(subject, data=nil, &cb)
254
+ return unless subject
255
+ inbox = NATS.create_inbox
256
+ s = subscribe(inbox) { |msg, reply|
257
+ case cb.arity
258
+ when 0 then cb.call
259
+ when 1 then cb.call(msg)
260
+ else cb.call(msg, reply)
261
+ end
262
+ }
263
+ publish(subject, data, inbox)
264
+ return s
265
+ end
266
+
267
+ # Define a callback to be called when the client connection has been established.
268
+ def on_connect(&callback)
269
+ @connect_cb = callback
270
+ end
271
+
272
+ # Define a callback to be called when errors occur on the client connection.
273
+ def on_error(&callback)
274
+ @err_cb, @err_cb_overridden = callback, true
275
+ end
276
+
277
+ # Define a callback to be called when a reconnect attempt is being made.
278
+ def on_reconnect(&callback)
279
+ @reconnect_cb = callback
280
+ end
281
+
282
+ # Close the connection to the server.
283
+ def close
284
+ @closing = true
285
+ close_connection_after_writing
286
+ end
287
+
288
+ def user_err_cb? #:nodoc:
289
+ err_cb_overridden || NATS.err_cb_overridden
290
+ end
291
+
292
+ def send_connect_command #:nodoc:
293
+ cs = { :verbose => false, :pedantic => false }
294
+ if @uri.user
295
+ cs[:user] = @uri.user
296
+ cs[:pass] = @uri.password
297
+ end
298
+ send_command("CONNECT #{cs.to_json}#{CR_LF}")
299
+ end
300
+
301
+ def queue_server_rt(&cb) #:nodoc:
302
+ return unless cb
303
+ (@pongs ||= []) << cb
304
+ send_command(PING_REQUEST)
305
+ end
306
+
307
+ def on_msg(subject, sid, reply, msg) #:nodoc:
308
+ return unless subscriber = @subs[sid]
309
+ if cb = subscriber[:callback]
310
+ case cb.arity
311
+ when 0 then cb.call
312
+ when 1 then cb.call(msg)
313
+ when 2 then cb.call(msg, reply)
314
+ else cb.call(msg, reply, subject)
315
+ end
316
+ end
317
+ end
318
+
319
+ def flush_pending #:nodoc:
320
+ return unless @pending
321
+ @pending.each { |p| send_data(p) }
322
+ @pending = nil
323
+ end
324
+
325
+ def receive_data(data) #:nodoc:
326
+ (@buf ||= '') << data
327
+ while (@buf && !@buf.empty?)
328
+ if (@needed && @buf.bytesize >= @needed + CR_LF_SIZE)
329
+ payload = @buf.slice(0, @needed)
330
+ on_msg(@sub, @sid, @reply, payload)
331
+ @buf = @buf.slice((@needed + CR_LF_SIZE), @buf.bytesize)
332
+ @sub = @sid = @reply = @needed = nil
333
+ elsif @buf =~ /^(.*)\r\n/ # Process a control line
334
+ @buf = $'
335
+ op = $1
336
+ case op
337
+ when MSG
338
+ @sub, @sid, @reply, @needed = $1, $2.to_i, $4, $5.to_i
339
+ when OK # No-op right now
340
+ when ERR
341
+ @err_cb = proc { raise Error, "Error received from server :#{$1}."} unless user_err_cb?
342
+ err_cb.call($1)
343
+ when PING
344
+ send_command(PONG_RESPONSE)
345
+ when PONG
346
+ cb = @pongs.shift
347
+ cb.call if cb
348
+ when INFO
349
+ process_info($1)
350
+ end
351
+ else # Waiting for additional data
352
+ return
353
+ end
354
+ end
355
+ end
356
+
357
+ def process_info(info) #:nodoc:
358
+ @server_info = JSON.parse(info, :symbolize_keys => true)
359
+ end
360
+
361
+ def connection_completed #:nodoc:
362
+ @connected = true
363
+ if reconnecting?
364
+ EM.cancel_timer(@reconnect_timer)
365
+ send_connect_command
366
+ @subs.each_pair { |k, v| send_command("SUB #{v[:subject]} #{k}#{CR_LF}") }
367
+ end
368
+ flush_pending if @pending
369
+ @err_cb = proc { raise Error, "Client disconnected from server on #{@uri}."} unless user_err_cb? or reconnecting?
370
+ if (connect_cb and not reconnecting?)
371
+ # We will round trip the server here to make sure all state from any pending commands
372
+ # has been processed before calling the connect callback.
373
+ queue_server_rt { connect_cb.call(self) }
374
+ end
375
+ @reconnecting = false
376
+ end
377
+
378
+ def schedule_reconnect(wait=RECONNECT_TIME_WAIT) #:nodoc:
379
+ @reconnecting = true
380
+ @reconnect_attempts = 0
381
+ @reconnect_timer = EM.add_periodic_timer(wait) { attempt_reconnect }
382
+ end
383
+
384
+ def unbind #:nodoc:
385
+ if connected? and not closing? and not reconnecting?
386
+ schedule_reconnect
387
+ else
388
+ process_disconnect unless reconnecting?
389
+ end
390
+ end
391
+
392
+ def process_disconnect #:nodoc:
393
+ if not closing? and @err_cb
394
+ err_string = @connected ? "Client disconnected from server on #{@uri}." : "Could not connect to server on #{@uri}"
395
+ err_cb.call(err_string)
396
+ end
397
+ ensure
398
+ EM.cancel_timer(@reconnect_timer) if @reconnect_timer
399
+ EM.stop if (NATS.client == self and connected? and closing? and not NATS.reactor_was_running?)
400
+ @connected = @reconnecting = false
401
+ true # Chaining
402
+ end
403
+
404
+ def attempt_reconnect #:nodoc:
405
+ process_disconnect and return if (@reconnect_attempts += 1) > MAX_RECONNECT_ATTEMPTS
406
+ EM.reconnect(@uri.host, @uri.port, self)
407
+ end
408
+
409
+ def send_command(command) #:nodoc:
410
+ queue_command(command) and return unless connected?
411
+ send_data(command)
412
+ end
413
+
414
+ def queue_command(command) #:nodoc:
415
+ (@pending ||= []) << command
416
+ true
417
+ end
418
+
419
+ def inspect #:nodoc:
420
+ "<nats client v#{NATS::VERSION}>"
421
+ end
422
+
423
+ end
424
+