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.
@@ -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: []