sinatra-sse 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/README.md +44 -0
- data/lib/sinatra/sse.rb +83 -0
- data/lib/sinatra/sse/marshal.rb +64 -0
- data/lib/sinatra/sse/version.rb +12 -0
- data/test/expect_test.rb +119 -0
- data/test/test_helper.rb +18 -0
- metadata +88 -0
data/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# sinatra-sse
|
|
2
|
+
|
|
3
|
+
Sinatra support for server-sent events. The sinatra-sse gem needs a web server, which
|
|
4
|
+
supports async operation. It is tested with thin.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
gem install sinatra-sse
|
|
9
|
+
|
|
10
|
+
## Getting started: the timer example
|
|
11
|
+
|
|
12
|
+
The timer example (in example/config.ru) sends time information over an SSE stream, like this:
|
|
13
|
+
|
|
14
|
+
require "sinatra/base"
|
|
15
|
+
require "sinatra/see"
|
|
16
|
+
|
|
17
|
+
class TimeServer < Sinatra::Base
|
|
18
|
+
include Sinatra::SSE
|
|
19
|
+
|
|
20
|
+
get '/' do
|
|
21
|
+
sse_stream do |out|
|
|
22
|
+
EM.add_periodic_timer(1) do
|
|
23
|
+
out.push :event => "timer", :data => Time.now.to_s
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
## Run the example
|
|
30
|
+
|
|
31
|
+
The SSE specs define the "text/event-stream" content type. Conforming clients
|
|
32
|
+
send these in the "Accept:" header, and conforming servers set the "Content-Type:"
|
|
33
|
+
header accordingly.
|
|
34
|
+
|
|
35
|
+
1.9.2 ~/sinatra-sse/lib[master] > curl -s -H "Accept: text/event-stream" http://0.0.0.0:9292
|
|
36
|
+
event: timer
|
|
37
|
+
data: 2012-10-21 11:21:48 +0200
|
|
38
|
+
|
|
39
|
+
event: timer
|
|
40
|
+
data: 2012-10-21 11:21:49 +0200
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
The sinatra-sse gem is (c) radiospiel, 2012; it is distributed under the terms of the Modified BSD License, see LICENSE.BSD for details.
|
data/lib/sinatra/sse.rb
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# This file is part of the sinatra-sse ruby gem.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2011, 2012 @radiospiel
|
|
4
|
+
# Distributed under the terms of the modified BSD license, see LICENSE.BSD
|
|
5
|
+
|
|
6
|
+
module Sinatra::SSE
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative "sse/version"
|
|
10
|
+
require_relative "sse/marshal"
|
|
11
|
+
|
|
12
|
+
# Support for SSE streams.
|
|
13
|
+
#
|
|
14
|
+
# The Sinatra::SSE module adds support for streaming server sent events.
|
|
15
|
+
#
|
|
16
|
+
# It wraps a sinatra stream (via <tt>stream :keep_open</tt>) in a structure
|
|
17
|
+
# which
|
|
18
|
+
#
|
|
19
|
+
# - formats events according to the SSE specs, and
|
|
20
|
+
# - keeps the connection alive by sending some data every 28 seconds.
|
|
21
|
+
#
|
|
22
|
+
module Sinatra::SSE
|
|
23
|
+
extend Marshal
|
|
24
|
+
|
|
25
|
+
# The keepalive period.
|
|
26
|
+
KEEP_ALIVE = 28
|
|
27
|
+
|
|
28
|
+
# The SSEStream class wraps a connection as created via the Sinatra
|
|
29
|
+
# stream helper.
|
|
30
|
+
class Stream #:nodoc:
|
|
31
|
+
def initialize(out)
|
|
32
|
+
@out = out
|
|
33
|
+
|
|
34
|
+
@out.callback do
|
|
35
|
+
cancel_keepalive_timer
|
|
36
|
+
@callback.call if @callback
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
reset_keepalive_timer
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def close
|
|
43
|
+
@out.close
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# set a callback block.
|
|
47
|
+
def callback(&block)
|
|
48
|
+
@callback = Proc.new
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Push an event to the client.
|
|
52
|
+
def push(hash)
|
|
53
|
+
raise ArgumentError unless hash.is_a?(Hash)
|
|
54
|
+
|
|
55
|
+
@out << Sinatra::SSE.marshal(hash)
|
|
56
|
+
reset_keepalive_timer
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def cancel_keepalive_timer #:nodoc:
|
|
62
|
+
@timer.cancel if @timer
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def reset_keepalive_timer #:nodoc:
|
|
66
|
+
cancel_keepalive_timer
|
|
67
|
+
@timer = EventMachine::PeriodicTimer.new(KEEP_ALIVE) { send_keepalive_traffic }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def send_keepalive_traffic #:nodoc:
|
|
71
|
+
@out << " \n"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Start a SSE stream.
|
|
76
|
+
def sse_stream(&block)
|
|
77
|
+
content_type "text/event-stream"
|
|
78
|
+
|
|
79
|
+
stream :keep_open do |out|
|
|
80
|
+
yield Sinatra::SSE::Stream.new(out)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# This file is part of the sinatra-sse ruby gem.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2011, 2012 @radiospiel
|
|
4
|
+
# Distributed under the terms of the modified BSD license, see LICENSE.BSD
|
|
5
|
+
|
|
6
|
+
require "expectation"
|
|
7
|
+
|
|
8
|
+
#
|
|
9
|
+
# packing/unpacking SSE events
|
|
10
|
+
module Sinatra::SSE::Marshal
|
|
11
|
+
FIXED_ORDER = {
|
|
12
|
+
:event => 1,
|
|
13
|
+
:id => 2,
|
|
14
|
+
:data => 3
|
|
15
|
+
} #:nodoc:
|
|
16
|
+
|
|
17
|
+
# converts an object into an event.
|
|
18
|
+
#
|
|
19
|
+
# The object must be a hash or a String.
|
|
20
|
+
def marshal(object)
|
|
21
|
+
expect! object => [ String, Hash ]
|
|
22
|
+
|
|
23
|
+
if object.is_a?(String)
|
|
24
|
+
object = { :data => object }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# sort all entries in a way, that make sure that event, id, data
|
|
28
|
+
# are at the end. This makes sure that confirming clients just
|
|
29
|
+
# ignore the extra entries. If we would just send them in random
|
|
30
|
+
# order we might produce "bogus" events, when "data", "event",
|
|
31
|
+
# and "id" are separated by invalid entries.
|
|
32
|
+
entries = object.sort_by { |key, value| FIXED_ORDER[key.to_sym] || 0 }
|
|
33
|
+
|
|
34
|
+
entries.map do |key, value|
|
|
35
|
+
escaped_value = value.gsub(/(\r\n|\r|\n)/, "\n#{key}: ")
|
|
36
|
+
"#{key}: #{escaped_value}\n"
|
|
37
|
+
end.join + "\n"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Unmarshals a single event in data. The string SHOULD NOT contain
|
|
41
|
+
# multipe events, or else the returned hash somehow mangles all these
|
|
42
|
+
# events into a single one.
|
|
43
|
+
def unmarshal(data)
|
|
44
|
+
event = Hash.new { |hash, key| hash[key] = [] }
|
|
45
|
+
|
|
46
|
+
data.split("\n").each do |line|
|
|
47
|
+
key, value = line.split(/: ?/, 2)
|
|
48
|
+
next if key =~ /^\s*$/
|
|
49
|
+
|
|
50
|
+
event[key.to_sym] << value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
event.inject({}) do |hash, (key, value)|
|
|
54
|
+
hash.update key => value.join("\n")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Extract all events from a data stream.
|
|
59
|
+
def unmarshal_all(data)
|
|
60
|
+
data.split(/\n\n/).
|
|
61
|
+
map { |event| unpack(event) }.
|
|
62
|
+
reject(&:empty?)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# This file is part of the sinatra-sse ruby gem.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2011, 2012 @radiospiel
|
|
4
|
+
# Distributed under the terms of the modified BSD license, see LICENSE.BSD
|
|
5
|
+
|
|
6
|
+
module Sinatra #:nodoc:
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module Sinatra::SSE
|
|
10
|
+
# The Sinatra::SSE version number
|
|
11
|
+
VERSION = "0.1"
|
|
12
|
+
end
|
data/test/expect_test.rb
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Author:: radiospiel (mailto:eno@radiospiel.org)
|
|
2
|
+
# Copyright:: Copyright (c) 2011, 2012 radiospiel
|
|
3
|
+
# License:: Distributes under the terms of the Modified BSD License, see LICENSE.BSD for details.
|
|
4
|
+
require_relative 'test_helper'
|
|
5
|
+
|
|
6
|
+
class ExpectationTest < Test::Unit::TestCase
|
|
7
|
+
def assert_expectation!(*expectation, &block)
|
|
8
|
+
assert_nothing_raised do
|
|
9
|
+
expect! *expectation, &block
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def assert_failed_expectation!(*expectation, &block)
|
|
14
|
+
assert_raise(ArgumentError) {
|
|
15
|
+
expect! *expectation, &block
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def assert_expectation(*expectation, &block)
|
|
20
|
+
assert_nothing_raised do
|
|
21
|
+
expect *expectation, &block
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def assert_failed_expectation(*expectation, &block)
|
|
26
|
+
assert_raise(ArgumentError) {
|
|
27
|
+
expect *expectation, &block
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Verify that the exception's backtrace is properly adjusted,
|
|
32
|
+
# i.e. points to this file.
|
|
33
|
+
def test_expectations_backtrace
|
|
34
|
+
backtrace = nil
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
expect! 1 => 0
|
|
38
|
+
rescue
|
|
39
|
+
backtrace = $!.backtrace
|
|
40
|
+
end
|
|
41
|
+
assert backtrace.first.include?("/expect_test.rb:")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_int_expectations
|
|
45
|
+
assert_expectation! 1 => 1
|
|
46
|
+
assert_expectation! 1 => Fixnum
|
|
47
|
+
assert_expectation! 1 => Integer
|
|
48
|
+
assert_expectation! 1 => 0..2
|
|
49
|
+
assert_expectation! 1 => 0..1
|
|
50
|
+
assert_expectation! 1 => 1..10
|
|
51
|
+
assert_expectation! 1 => [0,1,2]
|
|
52
|
+
assert_expectation! 1 => lambda { |i| i.odd? }
|
|
53
|
+
|
|
54
|
+
assert_failed_expectation! 1 => 2
|
|
55
|
+
assert_failed_expectation! 1 => Float
|
|
56
|
+
assert_failed_expectation! 1 => 0...1
|
|
57
|
+
assert_failed_expectation! 1 => 3..5
|
|
58
|
+
assert_failed_expectation! 1 => [3,4,5]
|
|
59
|
+
assert_failed_expectation! 1 => lambda { |i| i.even? }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_regexp_expectations
|
|
63
|
+
assert_expectation! " foo" => /foo/
|
|
64
|
+
assert_failed_expectation! " foo" => /^foo/
|
|
65
|
+
|
|
66
|
+
assert_expectation! "1" => /1/
|
|
67
|
+
assert_failed_expectation! "1" => /2/
|
|
68
|
+
|
|
69
|
+
assert_failed_expectation! 1 => /1/
|
|
70
|
+
assert_failed_expectation! 1 => /2/
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_multiple_expectations
|
|
74
|
+
assert_expectation! 1 => 1, :a => :a
|
|
75
|
+
assert_failed_expectation! 1 => 2, :a => :a
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_array_expectations
|
|
79
|
+
assert_expectation! 1, 1, 1, /1/
|
|
80
|
+
assert_expectation! 1, 1, "1" => /1/
|
|
81
|
+
|
|
82
|
+
assert_failed_expectation! 1, 1, "1" => /2/
|
|
83
|
+
assert_failed_expectation! 1, 1, 1 => /2/
|
|
84
|
+
assert_failed_expectation! 1, nil, "1" => /1/
|
|
85
|
+
assert_failed_expectation! 1, false, "1" => /1/
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_block_expectations
|
|
89
|
+
assert_expectation! do true end
|
|
90
|
+
assert_failed_expectation! do false end
|
|
91
|
+
assert_failed_expectation! do nil end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_hash_expectations
|
|
95
|
+
assert_failed_expectation!({} => { :key => "Foo" })
|
|
96
|
+
assert_expectation!({ :key => "Foo" } => { :key => "Foo" })
|
|
97
|
+
|
|
98
|
+
assert_failed_expectation!({ :other_key => "Foo" } => { :key => "Foo" })
|
|
99
|
+
assert_failed_expectation!({ :key => "Bar" } => { :key => "Foo" })
|
|
100
|
+
|
|
101
|
+
assert_expectation!({ :key => "Foo" } => { :key => String })
|
|
102
|
+
assert_expectation!({ :key => "Foo" } => { :key => [Integer,String] })
|
|
103
|
+
assert_failed_expectation!({ :key => "Foo" } => { :key => [Integer,"Bar"] })
|
|
104
|
+
assert_expectation!({ :other_key => "Foo" } => { :key => [nil, "Foo"] })
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_enable_and_disable
|
|
108
|
+
assert_failed_expectation! "foo" => "bar"
|
|
109
|
+
assert_failed_expectation "foo" => "bar"
|
|
110
|
+
|
|
111
|
+
Expectation.disable
|
|
112
|
+
assert_failed_expectation! "foo" => "bar"
|
|
113
|
+
assert_expectation "foo" => "bar"
|
|
114
|
+
|
|
115
|
+
Expectation.enable
|
|
116
|
+
assert_failed_expectation! "foo" => "bar"
|
|
117
|
+
assert_failed_expectation "foo" => "bar"
|
|
118
|
+
end
|
|
119
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'bundler/setup'
|
|
3
|
+
|
|
4
|
+
require 'simplecov'
|
|
5
|
+
require 'test/unit'
|
|
6
|
+
require 'test/unit/ui/console/testrunner'
|
|
7
|
+
|
|
8
|
+
class Test::Unit::UI::Console::TestRunner
|
|
9
|
+
def guess_color_availability; true; end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require 'mocha'
|
|
13
|
+
|
|
14
|
+
SimpleCov.start do
|
|
15
|
+
add_filter "test/*.rb"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require "expectation"
|
metadata
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sinatra-sse
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: '0.1'
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- radiospiel
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-10-21 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: expectation
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
none: false
|
|
18
|
+
requirements:
|
|
19
|
+
- - ! '>='
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
none: false
|
|
26
|
+
requirements:
|
|
27
|
+
- - ! '>='
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '0'
|
|
30
|
+
- !ruby/object:Gem::Dependency
|
|
31
|
+
name: sinatra
|
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
|
33
|
+
none: false
|
|
34
|
+
requirements:
|
|
35
|
+
- - ! '>='
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: '0'
|
|
38
|
+
type: :runtime
|
|
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
|
+
description: Sinatra support for server sent events
|
|
47
|
+
email: eno@radiospiel.org
|
|
48
|
+
executables: []
|
|
49
|
+
extensions: []
|
|
50
|
+
extra_rdoc_files: []
|
|
51
|
+
files:
|
|
52
|
+
- lib/sinatra/sse/marshal.rb
|
|
53
|
+
- lib/sinatra/sse/version.rb
|
|
54
|
+
- lib/sinatra/sse.rb
|
|
55
|
+
- README.md
|
|
56
|
+
- test/expect_test.rb
|
|
57
|
+
- test/test_helper.rb
|
|
58
|
+
homepage: http://github.com/radiospiel/sinatra-sse
|
|
59
|
+
licenses: []
|
|
60
|
+
post_install_message:
|
|
61
|
+
rdoc_options: []
|
|
62
|
+
require_paths:
|
|
63
|
+
- lib
|
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
65
|
+
none: false
|
|
66
|
+
requirements:
|
|
67
|
+
- - ! '>='
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
segments:
|
|
71
|
+
- 0
|
|
72
|
+
hash: 3390148179988891335
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
none: false
|
|
75
|
+
requirements:
|
|
76
|
+
- - ! '>='
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '0'
|
|
79
|
+
segments:
|
|
80
|
+
- 0
|
|
81
|
+
hash: 3390148179988891335
|
|
82
|
+
requirements: []
|
|
83
|
+
rubyforge_project:
|
|
84
|
+
rubygems_version: 1.8.24
|
|
85
|
+
signing_key:
|
|
86
|
+
specification_version: 3
|
|
87
|
+
summary: Sinatra support for server sent events
|
|
88
|
+
test_files: []
|