pubsubstub 0.0.9 → 0.0.10

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 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