sinatra-sse 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -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
@@ -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
@@ -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: []