sinatra-pubsub 0.0.1
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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/examples/app.rb +26 -0
- data/examples/config.ru +3 -0
- data/examples/events.module.coffee +60 -0
- data/examples/stream.module.coffee +45 -0
- data/lib/sinatra/pubsub.rb +18 -0
- data/lib/sinatra/pubsub/app.rb +20 -0
- data/lib/sinatra/pubsub/helpers.rb +28 -0
- data/lib/sinatra/pubsub/redis.rb +28 -0
- data/lib/sinatra/pubsub/stream.rb +91 -0
- data/lib/sinatra/pubsub/version.rb +5 -0
- data/sinatra-pubsub.gemspec +27 -0
- metadata +148 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Alex MacCaw
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Sinatra::Pubsub
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'sinatra-pubsub'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install sinatra-pubsub
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/examples/app.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'sinatra/pubsub'
|
3
|
+
|
4
|
+
register Sinatra::PubSub
|
5
|
+
|
6
|
+
EventMachine.next_tick do
|
7
|
+
EventMachine::PeriodicTimer.new(1) do
|
8
|
+
Sinatra::PubSub.publish_all(type: 'tick')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
get '/' do
|
13
|
+
erb :stream
|
14
|
+
end
|
15
|
+
|
16
|
+
__END__
|
17
|
+
|
18
|
+
@@ stream
|
19
|
+
<pre id="log">
|
20
|
+
</pre>
|
21
|
+
|
22
|
+
<script>
|
23
|
+
// reading
|
24
|
+
var es = new EventSource('/subscribe');
|
25
|
+
es.onmessage = function(e) { log.innerHTML += "\n" + e.data };
|
26
|
+
</script>
|
data/examples/config.ru
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
Events =
|
2
|
+
on: (event, callback) ->
|
3
|
+
if typeof event isnt 'string'
|
4
|
+
throw new Error('event required')
|
5
|
+
|
6
|
+
if typeof callback isnt 'function'
|
7
|
+
throw new Error('callback required')
|
8
|
+
|
9
|
+
events = event.split(' ')
|
10
|
+
calls = @hasOwnProperty('events') and @events or= {}
|
11
|
+
|
12
|
+
for name in events
|
13
|
+
calls[name] or= []
|
14
|
+
calls[name].push(callback)
|
15
|
+
this
|
16
|
+
|
17
|
+
isOn: (event, callback) ->
|
18
|
+
list = @hasOwnProperty('events') and @events?[event]
|
19
|
+
list and callback in list
|
20
|
+
|
21
|
+
one: (event, callback) ->
|
22
|
+
if typeof callback isnt 'function'
|
23
|
+
throw new Error('callback required')
|
24
|
+
|
25
|
+
callee = ->
|
26
|
+
@off(event, callee)
|
27
|
+
callback.apply(this, arguments)
|
28
|
+
@on(event, callee)
|
29
|
+
|
30
|
+
trigger: (args...) ->
|
31
|
+
event = args.shift()
|
32
|
+
|
33
|
+
list = @hasOwnProperty('events') and @events?[event]
|
34
|
+
return unless list
|
35
|
+
|
36
|
+
for callback in list
|
37
|
+
if callback.apply(this, args) is false
|
38
|
+
break
|
39
|
+
true
|
40
|
+
|
41
|
+
off: (event, callback) ->
|
42
|
+
unless event
|
43
|
+
@events = {}
|
44
|
+
return this
|
45
|
+
|
46
|
+
list = @events?[event]
|
47
|
+
return this unless list
|
48
|
+
|
49
|
+
unless callback
|
50
|
+
delete @events[event]
|
51
|
+
return this
|
52
|
+
|
53
|
+
for cb, i in list when cb is callback
|
54
|
+
list = list.slice()
|
55
|
+
list.splice(i, 1)
|
56
|
+
@events[event] = list
|
57
|
+
break
|
58
|
+
this
|
59
|
+
|
60
|
+
module.exports = Events
|
@@ -0,0 +1,45 @@
|
|
1
|
+
Events = require('events')
|
2
|
+
|
3
|
+
class Stream
|
4
|
+
@::[k] = v for k,v of Events
|
5
|
+
|
6
|
+
@open: -> @get()
|
7
|
+
|
8
|
+
@get: ->
|
9
|
+
@stream or= new this
|
10
|
+
|
11
|
+
logPrefix: '[stream]'
|
12
|
+
|
13
|
+
constructor: (@url = @url) ->
|
14
|
+
@source = new EventSource(@url)
|
15
|
+
@source.addEventListener('open', @open, false)
|
16
|
+
@source.addEventListener('error', @error, false)
|
17
|
+
@source.addEventListener('message', @message, false)
|
18
|
+
@source.addEventListener('setup', @setup, false)
|
19
|
+
|
20
|
+
open: =>
|
21
|
+
@log 'open'
|
22
|
+
|
23
|
+
error: (e) =>
|
24
|
+
@log 'error', e
|
25
|
+
|
26
|
+
message: (e) =>
|
27
|
+
msg = JSON.parse(e.data)
|
28
|
+
|
29
|
+
if msg.options?.except is @id
|
30
|
+
return @log('ignored', msg.type)
|
31
|
+
else
|
32
|
+
@log('message', msg.type, msg.data)
|
33
|
+
|
34
|
+
@trigger('message', msg)
|
35
|
+
@trigger(msg.type, msg.data)
|
36
|
+
|
37
|
+
setup: (e) =>
|
38
|
+
@id = e.data
|
39
|
+
@log('setup', @id)
|
40
|
+
@trigger('setup', @id)
|
41
|
+
|
42
|
+
log: (args...) =>
|
43
|
+
console?.log?(@logPrefix, args...)
|
44
|
+
|
45
|
+
module.exports = Stream
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'sinatra/pubsub/version'
|
2
|
+
|
3
|
+
module Sinatra
|
4
|
+
module PubSub
|
5
|
+
autoload :Stream, 'sinatra/pubsub/stream'
|
6
|
+
autoload :Redis, 'sinatra/pubsub/redis'
|
7
|
+
autoload :App, 'sinatra/pubsub/app'
|
8
|
+
autoload :Helpers, 'sinatra/pubsub/helpers'
|
9
|
+
|
10
|
+
extend Helpers
|
11
|
+
|
12
|
+
def self.registered(app)
|
13
|
+
app.use App
|
14
|
+
app.helpers Helpers
|
15
|
+
subscribe
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module PubSub
|
3
|
+
class App < Sinatra::Base
|
4
|
+
get '/subscribe/?:channel?', :provides => 'text/event-stream' do
|
5
|
+
error 402 unless Stream.enabled?
|
6
|
+
|
7
|
+
stream :keep_open do |out|
|
8
|
+
stream = Stream.new(out)
|
9
|
+
|
10
|
+
if params[:channel]
|
11
|
+
stream.subscribe(params[:channel])
|
12
|
+
end
|
13
|
+
|
14
|
+
out.callback { stream.close! }
|
15
|
+
out.errback { stream.close! }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Sinatra
|
4
|
+
module PubSub
|
5
|
+
module Helpers extend self
|
6
|
+
def publish(channel, message)
|
7
|
+
Redis.publish(channel, message.to_json)
|
8
|
+
end
|
9
|
+
|
10
|
+
def publish_all(message)
|
11
|
+
publish(:all, message)
|
12
|
+
end
|
13
|
+
|
14
|
+
def subscribe
|
15
|
+
Thread.abort_on_exception = true
|
16
|
+
|
17
|
+
trap('TERM') do
|
18
|
+
Stream.disable!
|
19
|
+
Process.kill('INT', $$)
|
20
|
+
end
|
21
|
+
|
22
|
+
Thread.new do
|
23
|
+
Redis.subscribe
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module Sinatra
|
4
|
+
module PubSub
|
5
|
+
module Redis extend self
|
6
|
+
def subscribe
|
7
|
+
redis = ::Redis.connect
|
8
|
+
|
9
|
+
redis.psubscribe('pubsub', 'pubsub.*') do |on|
|
10
|
+
on.pmessage do |match, channel, message|
|
11
|
+
channel = channel.sub(/\Apubsub\.?/, '')
|
12
|
+
channel = channel.empty? ? :all : channel.to_sym
|
13
|
+
Stream.publish(channel, message)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def publish(channel, message)
|
19
|
+
redis = ::Redis.connect
|
20
|
+
redis.publish("pubsub.#{channel}", message)
|
21
|
+
end
|
22
|
+
|
23
|
+
def publish_all(message)
|
24
|
+
publish(:all, message)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module PubSub
|
3
|
+
class Stream
|
4
|
+
def self.streams
|
5
|
+
@streams ||= Hash.new { |hash, key| hash[key] = [] }
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.enabled?
|
9
|
+
unless defined? @enabled
|
10
|
+
@enabled = true
|
11
|
+
end
|
12
|
+
@enabled
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.disable!
|
16
|
+
@enabled = false
|
17
|
+
streams.values.flatten.each(&:close!)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.publish(channel, message, options = {})
|
21
|
+
streams = self.streams[channel.to_s]
|
22
|
+
|
23
|
+
if except_id = options[:except]
|
24
|
+
streams = streams.reject {|s| s.id == except_id }
|
25
|
+
end
|
26
|
+
|
27
|
+
streams.each {|c| c.publish(:message, message) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.publish_all(message, options = {})
|
31
|
+
publish(:all, message, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :out, :id
|
35
|
+
|
36
|
+
def initialize(out)
|
37
|
+
@id = SecureRandom.uuid
|
38
|
+
@out = out
|
39
|
+
|
40
|
+
subscribe :all
|
41
|
+
|
42
|
+
@timer = EventMachine::PeriodicTimer.new(
|
43
|
+
20, method(:keepalive)
|
44
|
+
)
|
45
|
+
|
46
|
+
setup
|
47
|
+
end
|
48
|
+
|
49
|
+
def subscribe(channel)
|
50
|
+
self.class.streams[channel.to_s] << self
|
51
|
+
end
|
52
|
+
|
53
|
+
def close!
|
54
|
+
@timer.cancel
|
55
|
+
|
56
|
+
self.class.streams.each do |channel, streams|
|
57
|
+
streams.delete(self)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def publish(event, message)
|
62
|
+
self << "event: #{event}\n"
|
63
|
+
self << "data: #{message}\n\n"
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def <<(data)
|
69
|
+
return close! if out.closed?
|
70
|
+
out << data
|
71
|
+
end
|
72
|
+
|
73
|
+
# Only keep streams alive for so long
|
74
|
+
def keepalive_timeout?
|
75
|
+
@keepalive_count ||= 0
|
76
|
+
@keepalive_count += 1
|
77
|
+
@keepalive_count > 10
|
78
|
+
end
|
79
|
+
|
80
|
+
def keepalive
|
81
|
+
return close! if keepalive_timeout?
|
82
|
+
publish(:keepalive, '')
|
83
|
+
end
|
84
|
+
|
85
|
+
def setup
|
86
|
+
publish(:setup, id)
|
87
|
+
self << "retry: 5000\n"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'sinatra/pubsub/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "sinatra-pubsub"
|
8
|
+
spec.version = Sinatra::PubSub::VERSION
|
9
|
+
spec.authors = ["Alex MacCaw"]
|
10
|
+
spec.email = ["alex@alexmaccaw.com"]
|
11
|
+
spec.description = %q{Server Sent Events for Sinatra}
|
12
|
+
spec.summary = %q{Sinatra PubSub}
|
13
|
+
spec.homepage = "http://github.com/maccman/sinatra-pubsub"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "thin"
|
24
|
+
|
25
|
+
spec.add_dependency "sinatra", "~> 1.4.3"
|
26
|
+
spec.add_dependency "redis", "~> 3.0.4"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sinatra-pubsub
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Alex MacCaw
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-06-16 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: thin
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: sinatra
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.4.3
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 1.4.3
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: redis
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 3.0.4
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 3.0.4
|
94
|
+
description: Server Sent Events for Sinatra
|
95
|
+
email:
|
96
|
+
- alex@alexmaccaw.com
|
97
|
+
executables: []
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files: []
|
100
|
+
files:
|
101
|
+
- .gitignore
|
102
|
+
- Gemfile
|
103
|
+
- LICENSE.txt
|
104
|
+
- README.md
|
105
|
+
- Rakefile
|
106
|
+
- examples/app.rb
|
107
|
+
- examples/config.ru
|
108
|
+
- examples/events.module.coffee
|
109
|
+
- examples/stream.module.coffee
|
110
|
+
- lib/sinatra/pubsub.rb
|
111
|
+
- lib/sinatra/pubsub/app.rb
|
112
|
+
- lib/sinatra/pubsub/helpers.rb
|
113
|
+
- lib/sinatra/pubsub/redis.rb
|
114
|
+
- lib/sinatra/pubsub/stream.rb
|
115
|
+
- lib/sinatra/pubsub/version.rb
|
116
|
+
- sinatra-pubsub.gemspec
|
117
|
+
homepage: http://github.com/maccman/sinatra-pubsub
|
118
|
+
licenses:
|
119
|
+
- MIT
|
120
|
+
post_install_message:
|
121
|
+
rdoc_options: []
|
122
|
+
require_paths:
|
123
|
+
- lib
|
124
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
125
|
+
none: false
|
126
|
+
requirements:
|
127
|
+
- - ! '>='
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
segments:
|
131
|
+
- 0
|
132
|
+
hash: 4486996886234436829
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - ! '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
segments:
|
140
|
+
- 0
|
141
|
+
hash: 4486996886234436829
|
142
|
+
requirements: []
|
143
|
+
rubyforge_project:
|
144
|
+
rubygems_version: 1.8.25
|
145
|
+
signing_key:
|
146
|
+
specification_version: 3
|
147
|
+
summary: Sinatra PubSub
|
148
|
+
test_files: []
|