twitter-stream 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.
Potentially problematic release.
This version of twitter-stream might be problematic. Click here for more details.
- data/README.markdown +30 -0
- data/Rakefile +23 -0
- data/VERSION +1 -0
- data/examples/reader.rb +40 -0
- data/fixtures/twitter/basic_http.txt +14 -0
- data/lib/twitter/json_stream.rb +201 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/twitter/json_stream.rb +189 -0
- data/twitter-stream.gemspec +46 -0
- metadata +65 -0
data/README.markdown
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# twitter-stream
|
2
|
+
|
3
|
+
Simple Ruby client library for [twitter streaming API](http://apiwiki.twitter.com/Streaming-API-Documentation).
|
4
|
+
Uses [EventMachine](http://rubyeventmachine.com/) for connection handling. Adheres to twitter's [reconnection guidline](http://apiwiki.twitter.com/Streaming-API-Documentation#Connecting).
|
5
|
+
|
6
|
+
JSON format only.
|
7
|
+
|
8
|
+
## Usage
|
9
|
+
|
10
|
+
EventMachine::run {
|
11
|
+
stream = Twitter::JSONStream.connect(
|
12
|
+
:path => '/1/statuses/filter.json?track=football',
|
13
|
+
:auth => 'LOGIN:PASSWORD',
|
14
|
+
)
|
15
|
+
|
16
|
+
stream.each_item do |item|
|
17
|
+
# do someting with unparsed JSON item
|
18
|
+
end
|
19
|
+
|
20
|
+
stream.on_error do |message|
|
21
|
+
# log or something
|
22
|
+
end
|
23
|
+
}
|
24
|
+
|
25
|
+
|
26
|
+
## Examples
|
27
|
+
|
28
|
+
Open examples/reader.rb. Replace LOGIN:PASSWORD with your real twitter login and password. And
|
29
|
+
ruby examples/reader.rb
|
30
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'rspec'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
|
5
|
+
desc "Run all specs"
|
6
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
7
|
+
t.spec_files = FileList['spec/**/*.rb']
|
8
|
+
t.spec_opts = %w(-fs --color)
|
9
|
+
end
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gemspec|
|
14
|
+
gemspec.name = "twitter-stream"
|
15
|
+
gemspec.summary = "Twitter realtime API client"
|
16
|
+
gemspec.description = "Simple Ruby client library for twitter streaming API. Uses EventMachine for connection handling. Adheres to twitter's reconnection guidline. JSON format only."
|
17
|
+
gemspec.email = "voloko@gmail.com"
|
18
|
+
gemspec.homepage = "http://github.com/voloko/twitter-stream"
|
19
|
+
gemspec.authors = ["Vladimir Kolesnikov"]
|
20
|
+
end
|
21
|
+
rescue LoadError
|
22
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
23
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/examples/reader.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
lib_path = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
$LOAD_PATH.unshift lib_path unless $LOAD_PATH.include?(lib_path)
|
4
|
+
|
5
|
+
require 'twitter/json_stream'
|
6
|
+
|
7
|
+
EventMachine::run {
|
8
|
+
stream = Twitter::JSONStream.connect(
|
9
|
+
:path => '/1/statuses/filter.json',
|
10
|
+
:auth => 'LOGIN:PASSWORD',
|
11
|
+
:method => 'POST',
|
12
|
+
:content => 'track=basketball,football,baseball,footy,soccer'
|
13
|
+
)
|
14
|
+
|
15
|
+
stream.each_item do |item|
|
16
|
+
$stdout.print "item: #{item}\n"
|
17
|
+
$stdout.flush
|
18
|
+
end
|
19
|
+
|
20
|
+
stream.on_error do |message|
|
21
|
+
$stdout.print "error: #{message}\n"
|
22
|
+
$stdout.flush
|
23
|
+
end
|
24
|
+
|
25
|
+
stream.on_reconnect do |timeout, retries|
|
26
|
+
$stdout.print "reconnecting in: #{timeout} seconds\n"
|
27
|
+
$stdout.flush
|
28
|
+
end
|
29
|
+
|
30
|
+
stream.on_max_reconnects do |timeout, retries|
|
31
|
+
$stdout.print "Failed after #{retries} failed reconnects\n"
|
32
|
+
$stdout.flush
|
33
|
+
end
|
34
|
+
|
35
|
+
trap('TERM') {
|
36
|
+
stream.stop
|
37
|
+
EventMachine.stop if EventMachine.reactor_running?
|
38
|
+
}
|
39
|
+
}
|
40
|
+
puts "The event loop has ended"
|
@@ -0,0 +1,14 @@
|
|
1
|
+
HTTP/1.1 200 OK
|
2
|
+
Content-Type: application/json
|
3
|
+
Transfer-Encoding: chunked
|
4
|
+
Server: Jetty(6.1.17)
|
5
|
+
|
6
|
+
{"text":"Just wanted the world to know what our theologically deep pastor @bprentice has for a desktop background. http://yfrog.com/1rl4ij","favorited":false,"in_reply_to_user_id":null,"in_reply_to_screen_name":null,"source":"<a href=\"http://www.atebits.com/\" rel=\"nofollow\">Tweetie</a>","truncated":false,"created_at":"Thu Oct 08 19:34:09 +0000 2009","geo":null,"user":{"geo_enabled":false,"profile_text_color":"5b5252","location":"Stillwater, Oklahoma","statuses_count":122,"followers_count":70,"profile_link_color":"220099","description":"Taking an unchanging Savior to a changing world! Eagle Heights in Stillwater, Oklahoma.","following":null,"friends_count":136,"profile_sidebar_fill_color":"f37a20","url":"http://www.eagleheights.com","profile_image_url":"http://a3.twimg.com/profile_images/249941843/online_logo_normal.jpg","verified":false,"notifications":null,"favourites_count":0,"profile_sidebar_border_color":"c5f109","protected":false,"screen_name":"eagleheights","profile_background_tile":false,"profile_background_image_url":"http://a1.twimg.com/profile_background_images/5935314/EHBC_LOGO.jpg","created_at":"Tue Mar 17 14:52:04 +0000 2009","name":"Eagle Heights","time_zone":"Central Time (US & Canada)","id":24892440,"utc_offset":-21600,"profile_background_color":"3d3d3d"},"id":4714982187,"in_reply_to_status_id":null}
|
7
|
+
|
8
|
+
{"text":"I finally took a good pic of our resident BobCat @ the JakeCruise Ranch: http://yfrog.com/769vij","favorited":false,"in_reply_to_user_id":null,"in_reply_to_screen_name":null,"source":"<a href=\"http://www.atebits.com/\" rel=\"nofollow\">Tweetie</a>","truncated":false,"created_at":"Thu Oct 08 19:34:12 +0000 2009","geo":null,"user":{"geo_enabled":false,"profile_text_color":"141318","location":"West Hollywood, CA","statuses_count":265,"followers_count":80,"profile_link_color":"DA4425","description":"Gay Programmer in SoCal","following":null,"friends_count":37,"profile_sidebar_fill_color":"5DD2F4","url":"http://www.kelvo.com/","profile_image_url":"http://a1.twimg.com/profile_images/197950488/kelvis_green_current_normal.jpeg","verified":false,"notifications":null,"favourites_count":1,"profile_sidebar_border_color":"1F6926","protected":false,"screen_name":"KelvisWeHo","profile_background_tile":false,"profile_background_image_url":"http://a1.twimg.com/profile_background_images/9898116/thaneeya3.jpg","created_at":"Fri Apr 17 18:44:57 +0000 2009","name":"Kelvis Del Rio","time_zone":"Pacific Time (US & Canada)","id":32517583,"utc_offset":-28800,"profile_background_color":"cccccc"},"id":4714983168,"in_reply_to_status_id":null}
|
9
|
+
|
10
|
+
{"text":"Thursdays are long. But at least we're allowed a cheat sheet for stats. http://yfrog.com/9gjc7j","favorited":false,"in_reply_to_user_id":null,"in_reply_to_screen_name":null,"source":"<a href=\"http://twitterrific.com\" rel=\"nofollow\">Twitterrific</a>","truncated":false,"created_at":"Thu Oct 08 19:34:21 +0000 2009","geo":null,"user":{"geo_enabled":false,"profile_text_color":"663B12","location":"Calgary ","statuses_count":68,"followers_count":10,"profile_link_color":"1F98C7","description":"pure. love. art. music. ","following":null,"friends_count":22,"profile_sidebar_fill_color":"DAECF4","url":null,"profile_image_url":"http://a3.twimg.com/profile_images/391882135/Photo_550_normal.jpg","verified":false,"notifications":null,"favourites_count":5,"profile_sidebar_border_color":"C6E2EE","protected":false,"screen_name":"KarinRudmik","profile_background_tile":false,"profile_background_image_url":"http://a3.twimg.com/profile_background_images/33520483/shimmering__by_AnBystrowska.jpg","created_at":"Wed Jul 29 02:37:13 +0000 2009","name":"Karin Rudmik","time_zone":"Mountain Time (US & Canada)","id":61091098,"utc_offset":-25200,"profile_background_color":"C6E2EE"},"id":4714986148,"in_reply_to_status_id":null}
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
{"text":"acabando de almorzar, les comparto mi ultima creacion: http://yfrog.com/5mi94j pollo a la maracuya, con verduras sofritas ^^","favorited":false,"in_reply_to_user_id":null,"in_reply_to_screen_name":null,"source":"<a href=\"http://www.seesmic.com/\" rel=\"nofollow\">Seesmic</a>","truncated":false,"created_at":"Thu Oct 08 19:34:28 +0000 2009","geo":null,"user":{"geo_enabled":false,"profile_text_color":"3C3940","location":"Cartagena de Indias","statuses_count":1016,"followers_count":190,"profile_link_color":"0099B9","description":"Cartagenero extremadamente consentido, flojo y amante del anime, el manga, la cocina y los mmorpgs. pdt: y de los postres por supuesto!!!","following":null,"friends_count":253,"profile_sidebar_fill_color":"95E8EC","url":"http://www.flickr.com/photos/lobitokun/","profile_image_url":"http://a1.twimg.com/profile_images/451679242/shippo_normal.jpg","verified":false,"notifications":null,"favourites_count":9,"profile_sidebar_border_color":"5ED4DC","protected":false,"screen_name":"lobitokun","profile_background_tile":false,"profile_background_image_url":"http://a3.twimg.com/profile_background_images/24664583/hideki1.jpg","created_at":"Fri Jul 17 15:32:14 +0000 2009","name":"emiro gomez beltran","time_zone":"Bogota","id":57672295,"utc_offset":-18000,"profile_background_color":"0099B9"},"id":4714988998,"in_reply_to_status_id":null}
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'em/buftok'
|
3
|
+
|
4
|
+
module Twitter
|
5
|
+
class JSONStream < EventMachine::Connection
|
6
|
+
MAX_LINE_LENGTH = 16*1024
|
7
|
+
|
8
|
+
# network failure reconnections
|
9
|
+
NF_RECONNECT_START = 0.25
|
10
|
+
NF_RECONNECT_ADD = 0.25
|
11
|
+
NF_RECONNECT_MAX = 16
|
12
|
+
|
13
|
+
# app failure reconnections
|
14
|
+
AF_RECONNECT_START = 10
|
15
|
+
AF_RECONNECT_MUL = 2
|
16
|
+
|
17
|
+
RECONNECT_MAX = 320
|
18
|
+
RETRIES_MAX = 10
|
19
|
+
|
20
|
+
DEFAULT_OPTIONS = {
|
21
|
+
:method => 'GET',
|
22
|
+
:path => '/',
|
23
|
+
:content_type => "application/x-www-form-urlencoded",
|
24
|
+
:content => '',
|
25
|
+
:path => '/1/statuses/filter.json',
|
26
|
+
:host => 'stream.twitter.com',
|
27
|
+
:port => 80,
|
28
|
+
:auth => 'test:test',
|
29
|
+
:user_agent => 'TwitterStream',
|
30
|
+
}
|
31
|
+
|
32
|
+
attr_accessor :code
|
33
|
+
attr_accessor :headers
|
34
|
+
attr_accessor :nf_last_reconnect
|
35
|
+
attr_accessor :af_last_reconnect
|
36
|
+
attr_accessor :reconnect_retries
|
37
|
+
|
38
|
+
def self.connect options = {}
|
39
|
+
options = DEFAULT_OPTIONS.merge(options)
|
40
|
+
EventMachine.connect options[:host], options[:port], self, options
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize options = {}
|
44
|
+
@options = DEFAULT_OPTIONS.merge(options) # merge in case initialize called directly
|
45
|
+
@gracefully_closed = false
|
46
|
+
@nf_last_reconnect = nil
|
47
|
+
@af_last_reconnect = nil
|
48
|
+
@reconnect_retries = 0
|
49
|
+
end
|
50
|
+
|
51
|
+
def each_item &block
|
52
|
+
@each_item_callback = block
|
53
|
+
end
|
54
|
+
|
55
|
+
def on_error &block
|
56
|
+
@error_callback = block
|
57
|
+
end
|
58
|
+
|
59
|
+
def on_reconnect &block
|
60
|
+
@reconnect_callback = block
|
61
|
+
end
|
62
|
+
|
63
|
+
def on_max_reconnects &block
|
64
|
+
@max_reconnects_callback = block
|
65
|
+
end
|
66
|
+
|
67
|
+
def stop
|
68
|
+
@gracefully_closed = true
|
69
|
+
close_connection
|
70
|
+
end
|
71
|
+
|
72
|
+
def unbind
|
73
|
+
receive_line(@buffer.flush) unless @buffer.empty?
|
74
|
+
schedule_reconnect unless @gracefully_closed
|
75
|
+
end
|
76
|
+
|
77
|
+
def receive_data data
|
78
|
+
begin
|
79
|
+
@buffer.extract(data).each do |line|
|
80
|
+
receive_line(line)
|
81
|
+
end
|
82
|
+
rescue Exception => e
|
83
|
+
receive_error(e.message)
|
84
|
+
close_connection
|
85
|
+
return
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def post_init
|
90
|
+
reset_state
|
91
|
+
send_request
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
def schedule_reconnect
|
96
|
+
timeout = reconnect_timeout
|
97
|
+
@reconnect_retries += 1
|
98
|
+
if timeout <= RECONNECT_MAX && @reconnect_retries <= RETRIES_MAX
|
99
|
+
reconnect_after(timeout)
|
100
|
+
else
|
101
|
+
@max_reconnects_callback.call(timeout, @reconnect_retries) if @max_reconnects_callback
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def reconnect_after timeout
|
106
|
+
@reconnect_callback.call(timeout, @reconnect_retries) if @reconnect_callback
|
107
|
+
EventMachine.add_timer(timeout) do
|
108
|
+
reconnect @options[:host], @options[:port]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def reconnect_timeout
|
113
|
+
if (@code == 0) # network failure
|
114
|
+
if @nf_last_reconnect
|
115
|
+
@nf_last_reconnect += NF_RECONNECT_ADD
|
116
|
+
else
|
117
|
+
@nf_last_reconnect = NF_RECONNECT_START
|
118
|
+
end
|
119
|
+
[@nf_last_reconnect,NF_RECONNECT_MAX].min
|
120
|
+
else
|
121
|
+
if @af_last_reconnect
|
122
|
+
@af_last_reconnect *= AF_RECONNECT_MUL
|
123
|
+
else
|
124
|
+
@af_last_reconnect = AF_RECONNECT_START
|
125
|
+
end
|
126
|
+
@af_last_reconnect
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def reset_state
|
131
|
+
@code = 0
|
132
|
+
@headers = []
|
133
|
+
@state = :init
|
134
|
+
@buffer = BufferedTokenizer.new("\r", MAX_LINE_LENGTH)
|
135
|
+
end
|
136
|
+
|
137
|
+
def send_request
|
138
|
+
data = []
|
139
|
+
data << "#{@options[:method]} #{@options[:path]} HTTP/1.1"
|
140
|
+
data << "Host: #{@options[:host]}"
|
141
|
+
data << "User-agent: #{@options[:user_agent]}" if @options[:user_agent]
|
142
|
+
data << "Authorization: Basic " + [@options[:auth]].pack('m').delete("\r\n")
|
143
|
+
if @options[:method] == 'POST'
|
144
|
+
data << "Content-type: #{@options[:content_type]}"
|
145
|
+
data << "Content-length: #{@options[:content].length}"
|
146
|
+
end
|
147
|
+
data << "\r\n"
|
148
|
+
send_data data.join("\r\n") + @options[:content]
|
149
|
+
end
|
150
|
+
|
151
|
+
def receive_line ln
|
152
|
+
case @state
|
153
|
+
when :init
|
154
|
+
parse_response_line ln
|
155
|
+
when :headers
|
156
|
+
parse_header_line ln
|
157
|
+
when :stream
|
158
|
+
parse_stream_line ln
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def receive_error e
|
163
|
+
@error_callback.call(e) if @error_callback
|
164
|
+
end
|
165
|
+
|
166
|
+
def parse_stream_line ln
|
167
|
+
ln.strip!
|
168
|
+
unless ln.empty?
|
169
|
+
if ln[0,1] == '{'
|
170
|
+
@each_item_callback.call(ln) if @each_item_callback
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def parse_header_line ln
|
176
|
+
ln.strip!
|
177
|
+
if ln.empty?
|
178
|
+
reset_timeouts
|
179
|
+
@state = :stream
|
180
|
+
else
|
181
|
+
headers << ln
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def parse_response_line ln
|
186
|
+
if ln =~ /\AHTTP\/1\.[01] ([\d]{3})/
|
187
|
+
@code = $1.to_i
|
188
|
+
@state = :headers
|
189
|
+
else
|
190
|
+
receive_error('invalid response')
|
191
|
+
close_connection
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def reset_timeouts
|
196
|
+
@nf_last_reconnect = @af_last_reconnect = nil
|
197
|
+
@reconnect_retries = 0
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
lib_path = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
$LOAD_PATH.unshift lib_path unless $LOAD_PATH.include?(lib_path)
|
4
|
+
|
5
|
+
gem 'rspec'
|
6
|
+
require 'spec'
|
7
|
+
require 'spec/mocks'
|
8
|
+
|
9
|
+
def fixture_path(path)
|
10
|
+
File.join(File.dirname(__FILE__), '..', 'fixtures', path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def read_fixture(path)
|
14
|
+
File.read(fixture_path(path))
|
15
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
|
+
require 'twitter/json_stream'
|
3
|
+
|
4
|
+
include Twitter
|
5
|
+
|
6
|
+
describe JSONStream do
|
7
|
+
|
8
|
+
context "on create" do
|
9
|
+
|
10
|
+
it "should return stream" do
|
11
|
+
EM.should_receive(:connect).and_return('TEST INSTANCE')
|
12
|
+
stream = JSONStream.connect {}
|
13
|
+
stream.should == 'TEST INSTANCE'
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should define default properties" do
|
17
|
+
EM.should_receive(:connect).with do |host, port, handler, opts|
|
18
|
+
host.should == 'stream.twitter.com'
|
19
|
+
port.should == 80
|
20
|
+
opts[:path].should == '/1/statuses/filter.json'
|
21
|
+
opts[:method].should == 'GET'
|
22
|
+
end
|
23
|
+
stream = JSONStream.connect {}
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
Host = "127.0.0.1"
|
30
|
+
Port = 9550
|
31
|
+
|
32
|
+
class JSONServer < EM::Connection
|
33
|
+
attr_accessor :data
|
34
|
+
def receive_data data
|
35
|
+
$recieved_data = data
|
36
|
+
send_data $data_to_send
|
37
|
+
EventMachine.next_tick {
|
38
|
+
close_connection if $close_connection
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "on valid stream" do
|
44
|
+
before :each do
|
45
|
+
$data_to_send = read_fixture('twitter/basic_http.txt')
|
46
|
+
$recieved_data = ''
|
47
|
+
$close_connection = false
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should parse headers" do
|
51
|
+
EM.run {
|
52
|
+
EM.start_server Host, Port, JSONServer
|
53
|
+
@stream = JSONStream.connect :host => Host, :port => Port
|
54
|
+
EM.add_timer(0.5){ EM.stop }
|
55
|
+
}
|
56
|
+
@stream.code.should == 200
|
57
|
+
@stream.headers[0].downcase.should include 'content-type'
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should parse headers even after connection close" do
|
61
|
+
EM.run {
|
62
|
+
$close_connection = true
|
63
|
+
EM.start_server Host, Port, JSONServer
|
64
|
+
@stream = JSONStream.connect :host => Host, :port => Port
|
65
|
+
EM.add_timer(0.5){ EM.stop }
|
66
|
+
}
|
67
|
+
@stream.code.should == 200
|
68
|
+
@stream.headers[0].downcase.should include 'content-type'
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should extract records" do
|
72
|
+
EM.run {
|
73
|
+
EM.start_server Host, Port, JSONServer
|
74
|
+
@stream = JSONStream.connect :host => Host, :port => Port, :user_agent => 'TEST_USER_AGENT'
|
75
|
+
EM.add_timer(0.5){ EM.stop }
|
76
|
+
}
|
77
|
+
$recieved_data.upcase.should include('USER-AGENT: TEST_USER_AGENT')
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should send correct user agent" do
|
81
|
+
EM.run {
|
82
|
+
$close_connection = true
|
83
|
+
EM.start_server Host, Port, JSONServer
|
84
|
+
@stream = JSONStream.connect :host => Host, :port => Port
|
85
|
+
EM.add_timer(0.5){ EM.stop }
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "on network failure" do
|
91
|
+
before :each do
|
92
|
+
$data_to_send = ''
|
93
|
+
$close_connection = true
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should reconnect on network failure" do
|
97
|
+
EM.run {
|
98
|
+
EM.start_server Host, Port, JSONServer
|
99
|
+
@stream = JSONStream.connect :host => Host, :port => Port
|
100
|
+
@stream.should_receive(:reconnect)
|
101
|
+
EM.add_timer(0.5){ EM.stop }
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should reconnect with 0.25 at base" do
|
106
|
+
EM.run {
|
107
|
+
EM.start_server Host, Port, JSONServer
|
108
|
+
@stream = JSONStream.connect :host => Host, :port => Port
|
109
|
+
@stream.should_receive(:reconnect_after).with(0.25)
|
110
|
+
EM.add_timer(0.5){ EM.stop }
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should reconnect with linear timeout" do
|
115
|
+
EM.run {
|
116
|
+
EM.start_server Host, Port, JSONServer
|
117
|
+
@stream = JSONStream.connect :host => Host, :port => Port
|
118
|
+
@stream.nf_last_reconnect = 1
|
119
|
+
@stream.should_receive(:reconnect_after).with(1.25)
|
120
|
+
EM.add_timer(0.5){ EM.stop }
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should stop reconnecting after 100 times" do
|
125
|
+
EM.run {
|
126
|
+
EM.start_server Host, Port, JSONServer
|
127
|
+
@stream = JSONStream.connect :host => Host, :port => Port
|
128
|
+
@stream.reconnect_retries = 100
|
129
|
+
@stream.should_not_receive(:reconnect_after)
|
130
|
+
EM.add_timer(0.5){ EM.stop }
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should notify after reconnect limit is reached" do
|
135
|
+
timeout, retries = nil, nil
|
136
|
+
EM.run {
|
137
|
+
EM.start_server Host, Port, JSONServer
|
138
|
+
@stream = JSONStream.connect :host => Host, :port => Port
|
139
|
+
@stream.on_max_reconnects do |t, r|
|
140
|
+
timeout, retries = t, r
|
141
|
+
end
|
142
|
+
@stream.reconnect_retries = 100
|
143
|
+
EM.add_timer(0.5){ EM.stop }
|
144
|
+
}
|
145
|
+
timeout.should == 0.25
|
146
|
+
retries.should == 101
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
context "on application failure" do
|
151
|
+
before :each do
|
152
|
+
$data_to_send = 'HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm="Firehose"\r\n\r\n1'
|
153
|
+
$close_connection = true
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should reconnect on application failure 10 at base" do
|
157
|
+
EM.run {
|
158
|
+
EM.start_server Host, Port, JSONServer
|
159
|
+
@stream = JSONStream.connect :host => Host, :port => Port
|
160
|
+
@stream.should_receive(:reconnect_after).with(10)
|
161
|
+
EM.add_timer(0.5){ EM.stop }
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
it "should reconnect with exponential timeout" do
|
166
|
+
EM.run {
|
167
|
+
EM.start_server Host, Port, JSONServer
|
168
|
+
@stream = JSONStream.connect :host => Host, :port => Port
|
169
|
+
@stream.af_last_reconnect = 160
|
170
|
+
@stream.should_receive(:reconnect_after).with(320)
|
171
|
+
EM.add_timer(0.5){ EM.stop }
|
172
|
+
}
|
173
|
+
end
|
174
|
+
|
175
|
+
it "should not try to reconnect after limit is reached" do
|
176
|
+
EM.run {
|
177
|
+
EM.start_server Host, Port, JSONServer
|
178
|
+
@stream = JSONStream.connect :host => Host, :port => Port
|
179
|
+
@stream.af_last_reconnect = 320
|
180
|
+
@stream.should_not_receive(:reconnect_after)
|
181
|
+
EM.add_timer(0.5){ EM.stop }
|
182
|
+
}
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
|
188
|
+
end
|
189
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{twitter-stream}
|
5
|
+
s.version = "0.1.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Vladimir Kolesnikov"]
|
9
|
+
s.date = %q{2009-10-11}
|
10
|
+
s.description = %q{Simple Ruby client library for twitter streaming API. Uses EventMachine for connection handling. Adheres to twitter's reconnection guidline. JSON format only.}
|
11
|
+
s.email = %q{voloko@gmail.com}
|
12
|
+
s.extra_rdoc_files = [
|
13
|
+
"README.markdown"
|
14
|
+
]
|
15
|
+
s.files = [
|
16
|
+
"README.markdown",
|
17
|
+
"Rakefile",
|
18
|
+
"VERSION",
|
19
|
+
"examples/reader.rb",
|
20
|
+
"fixtures/twitter/basic_http.txt",
|
21
|
+
"lib/twitter/json_stream.rb",
|
22
|
+
"spec/spec_helper.rb",
|
23
|
+
"spec/twitter/json_stream.rb",
|
24
|
+
"twitter-stream.gemspec"
|
25
|
+
]
|
26
|
+
s.homepage = %q{http://github.com/voloko/twitter-stream}
|
27
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
28
|
+
s.require_paths = ["lib"]
|
29
|
+
s.rubygems_version = %q{1.3.4}
|
30
|
+
s.summary = %q{Twitter realtime API client}
|
31
|
+
s.test_files = [
|
32
|
+
"spec/spec_helper.rb",
|
33
|
+
"spec/twitter/json_stream.rb",
|
34
|
+
"examples/reader.rb"
|
35
|
+
]
|
36
|
+
|
37
|
+
if s.respond_to? :specification_version then
|
38
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
39
|
+
s.specification_version = 3
|
40
|
+
|
41
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
42
|
+
else
|
43
|
+
end
|
44
|
+
else
|
45
|
+
end
|
46
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: twitter-stream
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vladimir Kolesnikov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-11 00:00:00 +04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Simple Ruby client library for twitter streaming API. Uses EventMachine for connection handling. Adheres to twitter's reconnection guidline. JSON format only.
|
17
|
+
email: voloko@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.markdown
|
24
|
+
files:
|
25
|
+
- README.markdown
|
26
|
+
- Rakefile
|
27
|
+
- VERSION
|
28
|
+
- examples/reader.rb
|
29
|
+
- fixtures/twitter/basic_http.txt
|
30
|
+
- lib/twitter/json_stream.rb
|
31
|
+
- spec/spec_helper.rb
|
32
|
+
- spec/twitter/json_stream.rb
|
33
|
+
- twitter-stream.gemspec
|
34
|
+
has_rdoc: true
|
35
|
+
homepage: http://github.com/voloko/twitter-stream
|
36
|
+
licenses: []
|
37
|
+
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options:
|
40
|
+
- --charset=UTF-8
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.3.4
|
59
|
+
signing_key:
|
60
|
+
specification_version: 3
|
61
|
+
summary: Twitter realtime API client
|
62
|
+
test_files:
|
63
|
+
- spec/spec_helper.rb
|
64
|
+
- spec/twitter/json_stream.rb
|
65
|
+
- examples/reader.rb
|