pubsubstub 0.0.9 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c8e3bd526a5467ef4743ae56b53494580dcdb2f1
4
- data.tar.gz: 56214dcdc3e9dcd228724e986c118e64f8ff4bd8
3
+ metadata.gz: 13fa1f1bf8d271516e2ed2389138bf55184d8b13
4
+ data.tar.gz: dd1bfc72703d76dc3f6a826bd8b69121efaf1a42
5
5
  SHA512:
6
- metadata.gz: fe3f0a3d0476c7efdb9e1d1e970e621a10dd4a26011f4cd3032952fa0b5b9f0c818fea9bf0b8f26f9c932b20bc759cc1aee726ee20739fc590cf329bb47b4632
7
- data.tar.gz: a0683e93134dff4296d28cb41b8f4e979c67fd7dc86a8db82aef765b66a91c5f7b537c5ecd4e3231dad35091c004393246f0855b6b3355b63dc78585e5fee44d
6
+ metadata.gz: d3948cd0bbbc474de69185969dd120135941a16ecc4eb8c24fe508f80f83130a3b3f6faf4d0a7c6845f1050580222b059bb0581630d4bb4f09916593c0c9dd82
7
+ data.tar.gz: 03fe2195a09f6c5bf6cb275399e0f2a447a609c4241e355f3ca7b732edb7d0307157bcb17aee95d89bba99126b4a3c6aa914811d80a6dbc7f436cb5a9a466154
data/.travis.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
4
- - 2.0.0
5
- - 2.1.1
3
+ - 2.2.2
6
4
  script: rspec spec
5
+ services:
6
+ - redis-server
data/example/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ gem 'pubsubstub', path: '../'
2
+ gem 'puma'
data/example/config.ru ADDED
@@ -0,0 +1,3 @@
1
+ require 'pubsubstub'
2
+
3
+ run Pubsubstub::Application
@@ -4,6 +4,12 @@ module Pubsubstub
4
4
  enable :logging
5
5
  end
6
6
 
7
+ configure :test do
8
+ set :dump_errors, false
9
+ set :raise_errors, true
10
+ set :show_exceptions, false
11
+ end
12
+
7
13
  def initialize(*)
8
14
  @channels = Hash.new { |h, k| h[k] = Channel.new(k) }
9
15
  @connections = []
@@ -27,7 +27,6 @@ module Pubsubstub
27
27
  pubsub.publish(event)
28
28
  end
29
29
 
30
- private
31
30
  def scrollback(connection, last_event_id)
32
31
  return unless last_event_id
33
32
  pubsub.scrollback(last_event_id) do |event|
@@ -35,6 +34,8 @@ module Pubsubstub
35
34
  end
36
35
  end
37
36
 
37
+ private
38
+
38
39
  def broadcast(json)
39
40
  string = Event.from_json(json).to_message
40
41
  @connections.each do |connection|
@@ -1,35 +1,38 @@
1
1
  module Pubsubstub
2
2
  class Event
3
- attr_reader :id, :name, :data
3
+ attr_reader :id, :name, :data, :retry_after
4
4
 
5
5
  def initialize(data, options = {})
6
6
  @id = options[:id] || time_now
7
7
  @name = options[:name]
8
+ @retry_after = options[:retry_after]
8
9
  @data = data
9
10
  end
10
11
 
11
12
  def to_json
12
- {id: @id, name: @name, data: @data}.to_json
13
+ {id: @id, name: @name, data: @data, retry_after: @retry_after}.to_json
13
14
  end
14
15
 
15
16
  def to_message
16
17
  data = @data.split("\n").map{ |segment| "data: #{segment}" }.join("\n")
17
18
  message = "id: #{id}" << "\n"
18
19
  message << "event: #{name}" << "\n" if name
20
+ message << "retry: #{retry_after}" << "\n" if retry_after
19
21
  message << data << "\n\n"
20
22
  message
21
23
  end
22
24
 
23
25
  def self.from_json(json)
24
26
  hash = JSON.load(json)
25
- new(hash['data'], name: hash['name'], id: hash['id'])
27
+ new(hash['data'], name: hash['name'], id: hash['id'], retry_after: hash['retry_after'])
26
28
  end
27
29
 
28
30
  def ==(other)
29
- id == other.id && name == other.name && data == other.data
31
+ id == other.id && name == other.name && data == other.data && retry_after == other.retry_after
30
32
  end
31
33
 
32
34
  private
35
+
33
36
  def time_now
34
37
  (Time.now.to_f * 1000).to_i
35
38
  end
@@ -17,14 +17,21 @@ module Pubsubstub
17
17
  end
18
18
 
19
19
  def scrollback(since_event_id)
20
- self.class.nonblocking_redis.zrangebyscore(key('scrollback'), "(#{since_event_id.to_i}", '+inf') do |events|
20
+ redis = if EventMachine.reactor_running?
21
+ self.class.nonblocking_redis
22
+ else
23
+ self.class.blocking_redis
24
+ end
25
+
26
+ redis.zrangebyscore(key('scrollback'), "(#{since_event_id.to_i}", '+inf') do |events|
21
27
  events.each do |json|
22
28
  yield Pubsubstub::Event.from_json(json)
23
29
  end
24
30
  end
25
31
  end
26
32
 
27
- protected
33
+ private
34
+
28
35
  def key(purpose)
29
36
  [@channel_name, purpose].join(".")
30
37
  end
@@ -40,11 +47,15 @@ module Pubsubstub
40
47
  end
41
48
 
42
49
  def blocking_redis
43
- @blocking_redis ||= Redis.new(url: (ENV['REDIS_URL'] || "redis://localhost:6379"))
50
+ @blocking_redis ||= Redis.new(url: redis_url)
44
51
  end
45
52
 
46
53
  def nonblocking_redis
47
- @nonblocking_redis ||= EM::Hiredis.connect(ENV['REDIS_URL'] || "redis://localhost:6379")
54
+ @nonblocking_redis ||= EM::Hiredis.connect(redis_url)
55
+ end
56
+
57
+ def redis_url
58
+ ENV['REDIS_URL'] || "redis://localhost:6379"
48
59
  end
49
60
  end
50
61
  end
@@ -1,5 +1,7 @@
1
1
  module Pubsubstub
2
2
  class StreamAction < Pubsubstub::Action
3
+ RECONNECT_TIMEOUT = 10_000
4
+
3
5
  def initialize(*)
4
6
  super
5
7
  start_heartbeat
@@ -12,31 +14,72 @@ module Pubsubstub
12
14
  'X-Accel-Buffering' => 'no',
13
15
  'Connection' => 'keep-alive',
14
16
  })
17
+
18
+ if EventMachine.reactor_running?
19
+ subscribe_connection
20
+ else
21
+ return_scrollback
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def return_scrollback
28
+ buffer = ''
29
+ ensure_connection_has_event(buffer)
30
+
31
+ with_each_channel do |channel|
32
+ channel.scrollback(buffer, last_event_id)
33
+ end
34
+
35
+ buffer
36
+ end
37
+
38
+ def last_event_id
39
+ request.env['HTTP_LAST_EVENT_ID']
40
+ end
41
+
42
+ def subscribe_connection
15
43
  stream(:keep_open) do |connection|
16
44
  @connections << connection
17
- channels = params[:channels] || [:default]
18
- channels.each do |channel_name|
19
- channel(channel_name).subscribe(connection, last_event_id: request.env['HTTP_LAST_EVENT_ID'])
45
+ ensure_connection_has_event(connection)
46
+ with_each_channel do |channel|
47
+ channel.subscribe(connection, last_event_id: last_event_id)
20
48
  end
21
49
 
22
50
  connection.callback do
23
51
  @connections.delete(connection)
24
- channels.each do |channel_name|
25
- channel(channel_name).unsubscribe(connection)
52
+ with_each_channel do |channel|
53
+ channel.unsubscribe(connection)
26
54
  end
27
55
  end
28
56
  end
29
57
  end
30
58
 
31
- private
59
+ def ensure_connection_has_event(connection)
60
+ return if last_event_id
61
+ connection << heartbeat_event.to_message
62
+ end
63
+
32
64
  def start_heartbeat
33
65
  @heartbeat = Thread.new do
34
66
  loop do
35
67
  sleep Pubsubstub.heartbeat_frequency
36
- event = Event.new('ping', name: 'heartbeat').to_message
68
+ event = heartbeat_frequency.to_message
37
69
  @connections.each { |connection| connection << event }
38
70
  end
39
71
  end
40
72
  end
73
+
74
+ def with_each_channel(&block)
75
+ channels = params[:channels] || [:default]
76
+ channels.each do |channel_name|
77
+ yield channel(channel_name)
78
+ end
79
+ end
80
+
81
+ def heartbeat_event
82
+ Event.new('ping', name: 'heartbeat', retry_after: RECONNECT_TIMEOUT)
83
+ end
41
84
  end
42
85
  end
@@ -1,3 +1,3 @@
1
1
  module Pubsubstub
2
- VERSION = "0.0.9"
2
+ VERSION = "0.0.10"
3
3
  end
data/pubsubstub.gemspec CHANGED
@@ -25,7 +25,10 @@ Gem::Specification.new do |spec|
25
25
 
26
26
  spec.add_development_dependency "bundler", "~> 1.5"
27
27
  spec.add_development_dependency "rake", "~> 10.2"
28
- spec.add_development_dependency "rspec", "~> 2.14"
29
- spec.add_development_dependency "pry", "~> 0.9"
28
+ spec.add_development_dependency "rspec", "3.1.0"
29
+ spec.add_development_dependency "pry-byebug"
30
30
  spec.add_development_dependency "thin", "~> 1.6"
31
+ spec.add_development_dependency "rack-test"
32
+ spec.add_development_dependency "timecop"
33
+ spec.add_development_dependency "em-spec"
31
34
  end
data/spec/channel_spec.rb CHANGED
@@ -80,4 +80,14 @@ describe Pubsubstub::Channel do
80
80
  subject.send(:broadcast, event.to_json)
81
81
  end
82
82
  end
83
+
84
+ context "#scrollback" do
85
+ it "sends events to the connection buffer" do
86
+ event = Pubsubstub::Event.new("message")
87
+ expect(pubsub).to receive(:scrollback).and_yield(event)
88
+ connection = ""
89
+ subject.scrollback(connection, 1)
90
+ expect(connection).to eq(event.to_message)
91
+ end
92
+ end
83
93
  end
data/spec/event_spec.rb CHANGED
@@ -1,15 +1,17 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Pubsubstub::Event do
4
- subject { Pubsubstub::Event.new("refresh #1500\nnew #1400", id: 12345678, name: "toto") }
4
+ subject {
5
+ Pubsubstub::Event.new("refresh #1500\nnew #1400", id: 12345678, name: "toto", retry_after: 1_000)
6
+ }
5
7
 
6
8
  it "#to_json serialization" do
7
- expect(subject.to_json).to be == {id: 12345678, name: "toto", data: "refresh #1500\nnew #1400"}.to_json
9
+ expect(subject.to_json).to be == {id: 12345678, name: "toto", data: "refresh #1500\nnew #1400", retry_after: 1_000}.to_json
8
10
  end
9
11
 
10
12
  context "#to_message" do
11
13
  it "serializes to sse" do
12
- expect(subject.to_message).to be == "id: 12345678\nevent: toto\ndata: refresh #1500\ndata: new #1400\n\n"
14
+ expect(subject.to_message).to be == "id: 12345678\nevent: toto\nretry: 1000\ndata: refresh #1500\ndata: new #1400\n\n"
13
15
  end
14
16
 
15
17
  it "does not have event if no name is specified" do
@@ -65,7 +65,7 @@ describe Pubsubstub::RedisPubSub do
65
65
  it "yields the events in the scrollback" do
66
66
  redis = double('redis')
67
67
  expect(redis).to receive(:zrangebyscore).with('test.scrollback', '(1234', '+inf').and_yield([event1.to_json, event2.to_json])
68
- expect(Pubsubstub::RedisPubSub).to receive(:nonblocking_redis).and_return(redis)
68
+ expect(Pubsubstub::RedisPubSub).to receive(:blocking_redis).and_return(redis)
69
69
  expect { |block| subject.scrollback(1234, &block) }.to yield_successive_args(event1, event2)
70
70
  end
71
71
  end
data/spec/spec_helper.rb CHANGED
@@ -1,18 +1,21 @@
1
+ require 'rack/test'
2
+ require 'pry'
3
+ require 'pry-byebug'
4
+ require 'timecop'
5
+ require 'em-spec/rspec'
6
+
7
+ ENV['RACK_ENV'] = 'test'
1
8
  require_relative '../lib/pubsubstub'
2
- # This file was generated by the `rspec --init` command. Conventionally, all
3
- # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
4
- # Require this file using `require "spec_helper"` to ensure that it is only
5
- # loaded once.
6
- #
7
- # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
9
+
8
10
  RSpec.configure do |config|
9
- config.treat_symbols_as_metadata_keys_with_true_values = true
11
+ config.include Rack::Test::Methods
12
+
10
13
  config.run_all_when_everything_filtered = true
11
14
  config.filter_run :focus
15
+ config.color = true
12
16
 
13
- # Run specs in random order to surface order dependencies. If you find an
14
- # order dependency and want to debug it, you can fix the order by providing
15
- # the seed, which is printed after each run.
16
- # --seed 1234
17
17
  config.order = 'random'
18
+
19
+ config.before(:each) { Pubsubstub::RedisPubSub.blocking_redis.flushdb }
18
20
  end
21
+
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Pubsubstub::StreamAction without EventMachine" do
4
+ before {
5
+ allow(EventMachine).to receive(:reactor_running?).and_return(false)
6
+ }
7
+
8
+ let(:app) { Pubsubstub::StreamAction.new }
9
+ it "returns a heartbeat if there is no LAST_EVENT_ID" do
10
+ Timecop.freeze(DateTime.parse("2015-01-01T00:00:00+00:00")) do
11
+ event = Pubsubstub::Event.new(
12
+ 'ping',
13
+ name: 'heartbeat',
14
+ retry_after: Pubsubstub::StreamAction::RECONNECT_TIMEOUT,
15
+ )
16
+ get "/"
17
+ expect(last_response.body).to eq(event.to_message)
18
+ end
19
+ end
20
+
21
+ it "returns an empty body if a LAST_EVENT_ID is provided and there is no scrollback" do
22
+ get "/", {}, 'HTTP_LAST_EVENT_ID' => 1
23
+ expect(last_response.body).to eq('')
24
+ end
25
+
26
+ it "returns the content of the scrollback" do
27
+ event = Pubsubstub::Event.new("test")
28
+ expect_any_instance_of(Pubsubstub::Channel).to receive(:scrollback).and_return([event])
29
+
30
+ get "/", {}, 'HTTP_LAST_EVENT_ID' => 1
31
+ end
32
+ end
33
+
34
+ describe "Pubsubstub::StreamAction with EventMachine" do
35
+ include EventMachine::SpecHelper
36
+
37
+ let(:app) {
38
+ Pubsubstub::StreamAction.new
39
+ }
40
+
41
+ it "returns a heartbeat if there is no LAST_EVENT_ID" do
42
+ Timecop.freeze(DateTime.parse("2015-01-01T00:00:00+00:00")) do
43
+ event = Pubsubstub::Event.new(
44
+ 'ping',
45
+ name: 'heartbeat',
46
+ retry_after: Pubsubstub::StreamAction::RECONNECT_TIMEOUT,
47
+ )
48
+ get "/"
49
+ expect(last_response.body).to eq(event.to_message)
50
+ end
51
+ end
52
+
53
+ it "subscribes the connection to the channel" do
54
+ event = Pubsubstub::Event.new('ping')
55
+ channel = Pubsubstub::Channel.new(:default)
56
+ allow_any_instance_of(Pubsubstub::StreamAction).to receive(:with_each_channel).and_yield(channel)
57
+
58
+ em do
59
+ env = current_session.send(:env_for, "/", 'HTTP_LAST_EVENT_ID' => 1)
60
+ request = Rack::Request.new(env)
61
+ status, headers, body = app.call(request.env)
62
+
63
+ response = Rack::MockResponse.new(status, headers, body, env["rack.errors"].flush)
64
+ channel.send(:broadcast, event.to_json)
65
+
66
+ EM.next_tick {
67
+ body.close
68
+ response.finish
69
+
70
+ expect(response.body).to eq(event.to_message)
71
+ EM.stop
72
+ }
73
+ end
74
+ end
75
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pubsubstub
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guillaume Malette
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-13 00:00:00.000000000 Z
11
+ date: 2015-05-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sinatra
@@ -98,30 +98,30 @@ dependencies:
98
98
  name: rspec
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - "~>"
101
+ - - '='
102
102
  - !ruby/object:Gem::Version
103
- version: '2.14'
103
+ version: 3.1.0
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - "~>"
108
+ - - '='
109
109
  - !ruby/object:Gem::Version
110
- version: '2.14'
110
+ version: 3.1.0
111
111
  - !ruby/object:Gem::Dependency
112
- name: pry
112
+ name: pry-byebug
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - "~>"
115
+ - - ">="
116
116
  - !ruby/object:Gem::Version
117
- version: '0.9'
117
+ version: '0'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - "~>"
122
+ - - ">="
123
123
  - !ruby/object:Gem::Version
124
- version: '0.9'
124
+ version: '0'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: thin
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -136,6 +136,48 @@ dependencies:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
138
  version: '1.6'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rack-test
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: timecop
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: em-spec
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
139
181
  description: Pubsubstub can be added to a rack Application or deployed standalone.
140
182
  It uses Redis to do the Pub/Sub
141
183
  email:
@@ -151,6 +193,8 @@ files:
151
193
  - LICENSE.txt
152
194
  - README.md
153
195
  - Rakefile
196
+ - example/Gemfile
197
+ - example/config.ru
154
198
  - lib/pubsubstub.rb
155
199
  - lib/pubsubstub/action.rb
156
200
  - lib/pubsubstub/application.rb
@@ -165,6 +209,7 @@ files:
165
209
  - spec/event_spec.rb
166
210
  - spec/redis_pub_sub_spec.rb
167
211
  - spec/spec_helper.rb
212
+ - spec/stream_action_spec.rb
168
213
  homepage: https://github.com/gmalette/pubsubstub
169
214
  licenses:
170
215
  - MIT
@@ -185,7 +230,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
230
  version: '0'
186
231
  requirements: []
187
232
  rubyforge_project:
188
- rubygems_version: 2.2.2
233
+ rubygems_version: 2.2.3
189
234
  signing_key:
190
235
  specification_version: 4
191
236
  summary: Pubsubstub is a rack middleware to add Pub/Sub
@@ -194,3 +239,4 @@ test_files:
194
239
  - spec/event_spec.rb
195
240
  - spec/redis_pub_sub_spec.rb
196
241
  - spec/spec_helper.rb
242
+ - spec/stream_action_spec.rb