angelo 0.1.14 → 0.1.15

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ab8dc2d9fc7243d0fafec32530e5509351ecfcec
4
- data.tar.gz: 37488c61321c6ffd9d4524371eb295fd2b425a8b
3
+ metadata.gz: e3fe7e1fbbed26101e7b8204067db4e0cf8d8b24
4
+ data.tar.gz: 0d80c3741af219d78fdd2273717588ad286afa4a
5
5
  SHA512:
6
- metadata.gz: 07c8b7aef24a27aece1e2d012bfd910598445241943b24714f21739cfbef8ac686c7594c873331fb4f71702af1dd11d7161db74861fdc79464bcea2abb822f81
7
- data.tar.gz: 32ffbf2ce2540b29be61110aeaf7b14aef8d21fb50634ee07692e32eac0cb3a25ec7b0a356cdd5804ffb748df7e8e2b7cae5a8d2efaaee867154ba4be54ee616
6
+ metadata.gz: 34fea99d0f4f8c0b4e10eaf2d1903df121d1ba541fb354e87f9b61caacfbcc4741894f6ec7bd8b4ccf1b7aab27770bf19d830b8a588afed113363c44d74fe26d
7
+ data.tar.gz: 52ee6e7b8111bdcf75c77ee828b061d831c921d27f50181d8f6d6f2b69abf1164498f23a0d01a846f920375214400330ce8ab880242cf3c50786e3ea780557b2
data/CHANGELOG.md CHANGED
@@ -1,6 +1,15 @@
1
1
  changelog
2
2
  =========
3
3
 
4
+ ### 0.1.15 24 jul 2014
5
+
6
+ * WebsocketResponder -> Responder::Websocket
7
+ * add Responder::Eventsource
8
+ * split Stash into module with Websocket and SSE classes
9
+ * add SSE eventsource route builder and helper
10
+ * add #event and #message to Stash::SSE
11
+ * add .sse_event and .sse_message to Base
12
+
4
13
  ### 0.1.14 3 jul 2014
5
14
 
6
15
  * add send_file with disposition support
data/Gemfile.lock CHANGED
@@ -15,7 +15,7 @@ GEM
15
15
  httpclient (2.4.0)
16
16
  method_source (0.8.2)
17
17
  mime-types (2.3)
18
- minitest (5.3.5)
18
+ minitest (5.4.0)
19
19
  mustermann (0.2.0)
20
20
  nio4r (1.0.0)
21
21
  nio4r (1.0.0-java)
@@ -37,13 +37,13 @@ GEM
37
37
  http_parser.rb (>= 0.6.0)
38
38
  websocket_parser (>= 0.1.6)
39
39
  ruby-prof (0.15.1)
40
- slop (3.5.0)
40
+ slop (3.6.0)
41
41
  spoon (0.0.4)
42
42
  ffi
43
43
  tilt (2.0.1)
44
44
  timers (1.1.0)
45
- websocket-driver (0.3.3)
46
- websocket-driver (0.3.3-java)
45
+ websocket-driver (0.3.4)
46
+ websocket-driver (0.3.4-java)
47
47
  websocket_parser (0.1.6)
48
48
 
49
49
  PLATFORMS
data/README.md CHANGED
@@ -8,7 +8,8 @@ A [Sinatra](https://github.com/sinatra/sinatra)-esque DSL for [Reel](https://git
8
8
  ### tl;dr
9
9
 
10
10
  * websocket support via `websocket('/path'){|s| ... }` route builder
11
- * contextual websocket stashing via `websockets` helper
11
+ * SSE support via `eventsource('/path'){|s| ... }` route builder
12
+ * contextual websocket/sse stashing via `websockets` and `sses` helpers
12
13
  * `task` handling via `async` and `future` helpers
13
14
  * no rack
14
15
  * optional tilt/erb support
@@ -99,6 +100,105 @@ When a `POST /` with a 'foo' param is received, any value is messaged out to any
99
100
  websockets. When a `POST /bar` with a 'bar' param is received, any value is messaged out to all
100
101
  websockets that connected to '/bar'.
101
102
 
103
+ ### SSE - Server-Sent Events
104
+
105
+ The `eventsource` route builder also accepts a path and a block, and passes the socket to the block,
106
+ just like the `websocket` builder. This socket is actually the raw `Celluloid::IO::TCPSocket` and is
107
+ "detached" from the regular handling. There are no "on-*" methods; the `write` method should suffice.
108
+ To make it easier to deal with creation of the properly formatted Strings to send, Angelo provides
109
+ a couple helpers.
110
+
111
+ ##### `sse_event` helper
112
+
113
+ To create an "event" that a javascript EventListener on the client can respond to:
114
+
115
+ ```ruby
116
+ eventsource '/sse' do |s|
117
+ event = sse_event :foo, some_key: 'blah', other_key: 'boo'
118
+ s.write event
119
+ s.close
120
+ end
121
+ ```
122
+
123
+ In this case, the EventListener would have to be configured to listen for the `foo` event:
124
+
125
+ ```javascript
126
+ var sse = new EventSource('/sse');
127
+ sse.addEventListener('foo', function(e){ console.log("got foo event!\n" + JSON.parse(e.data)); });
128
+ ```
129
+
130
+ The `sse_event` helper accepts a normal `String` for the data, but will automatically convert a `Hash`
131
+ argument to a JSON object.
132
+
133
+ ##### `sse_message` helper
134
+
135
+ The `sse_message` helper behaves exactly the same as `sse_event`, but does not take an event name:
136
+
137
+ ```ruby
138
+ eventsource '/sse' do |s|
139
+ msg = sse_msg some_key: 'blah', other_key: 'boo'
140
+ s.write msg
141
+ s.close
142
+ end
143
+ ```
144
+
145
+ The client javascript would need to be altered to use the `EventSource.onmessage` property as well:
146
+
147
+ ```javascript
148
+ var sse = new EventSource('/sse');
149
+ sse.onmessage = function(e){ console.log("got message!\n" + JSON.parse(e.data)); };
150
+ ```
151
+
152
+ ##### `sses` helper
153
+
154
+ Angelo also includes a "stash" helper for SSE connections. One can `<<` a socket into `sses` from
155
+ inside an `eventsource` handler block. These can also be "later" be iterated over so one can do things
156
+ like emit a message on every SSE connection when the service receives a POST request.
157
+
158
+ The `sses` helper includes the same a context ability as the `websockets` helper. In addition, the
159
+ `sses` stash includes methods for easily sending events or messages to all stashed connections. **Note
160
+ that the `Stash::SSE#event` method only works on non-default contexts and uses the context name as
161
+ the event name.**
162
+
163
+ ```ruby
164
+ eventsource '/sse' do |s|
165
+ sses[:foo] << s
166
+ end
167
+
168
+ post '/sse_message' do
169
+ sses[:foo].message params[:data]
170
+ end
171
+
172
+ post '/sse_event' do
173
+ sses[:foo].event params[:data]
174
+ end
175
+ ```
176
+
177
+ ##### `eventsource` instance helper
178
+
179
+ Additionally, you can also start SSE handling *conditionally* from inside a GET block:
180
+
181
+ ```ruby
182
+ get '/sse_maybe' do
183
+ if params[:sse]
184
+ eventsource do |c|
185
+ sses << c
186
+ c.write sse_message 'wooo fancy SSE for you!'
187
+ end
188
+ else
189
+ 'boring regular old get response'
190
+ end
191
+ end
192
+
193
+ post '/sse_event' do
194
+ sses.each {|sse| sse.write sse_event(:foo, 'fancy sse event!')}
195
+ end
196
+ ```
197
+
198
+ Handling this on the client may require conditionals for [browsers](http://caniuse.com/eventsource) that
199
+ do not support EventSource yet, since this will respond with a non-"text/event-stream" Content-Type if
200
+ 'sse' is not present in the params.
201
+
102
202
  ### Tasks + Async / Future
103
203
 
104
204
  Angelo is built on Reel and Celluloid::IO, giving your web application class the ability to define
data/lib/angelo/base.rb CHANGED
@@ -6,6 +6,7 @@ module Angelo
6
6
 
7
7
  extend Forwardable
8
8
  def_delegators :@responder, :content_type, :headers, :redirect, :request
9
+ def_delegators :@klass, :websockets, :sses, :sse_event, :sse_message
9
10
 
10
11
  @@addr = DEFAULT_ADDR
11
12
  @@port = DEFAULT_PORT
@@ -13,6 +14,8 @@ module Angelo
13
14
  @@ping_time = DEFAULT_PING_TIME
14
15
  @@log_level = DEFAULT_LOG_LEVEL
15
16
 
17
+ @@report_errors = false
18
+
16
19
  if ARGV.any? and not Kernel.const_defined?('Minitest')
17
20
  require 'optparse'
18
21
  OptionParser.new { |op|
@@ -87,11 +90,15 @@ module Angelo
87
90
  end
88
91
 
89
92
  def websocket path, &block
90
- routes[:websocket][path] = WebsocketResponder.new &block
93
+ routes[:websocket][path] = Responder::Websocket.new &block
94
+ end
95
+
96
+ def eventsource path, &block
97
+ routes[:get][path] = Responder::Eventsource.new &block
91
98
  end
92
99
 
93
100
  def on_pong &block
94
- WebsocketResponder.on_pong = block
101
+ Responder::Websocket.on_pong = block
95
102
  end
96
103
 
97
104
  def task name, &block
@@ -99,11 +106,17 @@ module Angelo
99
106
  end
100
107
 
101
108
  def websockets
102
- @websockets ||= Stash.new server
109
+ @websockets ||= Stash::Websocket.new server
103
110
  @websockets.reject! &:closed?
104
111
  @websockets
105
112
  end
106
113
 
114
+ def sses
115
+ @sses ||= Stash::SSE.new server
116
+ @sses.reject! &:closed?
117
+ @sses
118
+ end
119
+
107
120
  def content_type type
108
121
  Responder.content_type type
109
122
  end
@@ -126,6 +139,21 @@ module Angelo
126
139
  end
127
140
  end
128
141
 
142
+ def sse_event event_name, data
143
+ data = data.to_json if Hash === data
144
+ SSE_EVENT_TEMPLATE % [event_name.to_s, data]
145
+ end
146
+
147
+ def sse_message data
148
+ data = data.to_json if Hash === data
149
+ SSE_DATA_TEMPLATE % data
150
+ end
151
+
152
+ end
153
+
154
+ def initialize responder
155
+ @responder = responder
156
+ @klass = self.class
129
157
  end
130
158
 
131
159
  def async meth, *args
@@ -144,8 +172,6 @@ module Angelo
144
172
  end
145
173
  end
146
174
 
147
- def websockets; self.class.websockets; end
148
-
149
175
  def request_headers
150
176
  @request_headers ||= Hash.new do |hash, key|
151
177
  if Symbol === key
@@ -175,6 +201,19 @@ module Angelo
175
201
  end
176
202
  end
177
203
 
204
+ task :handle_event_source do |socket, block|
205
+ begin
206
+ block[socket]
207
+ rescue Reel::SocketError, IOError, SystemCallError => e
208
+ # probably closed on client
209
+ warn e.message if @@report_errors
210
+ socket.close unless socket.closed?
211
+ rescue => e
212
+ error e.inspect
213
+ socket.close unless socket.closed?
214
+ end
215
+ end
216
+
178
217
  def halt status = 400, body = ''
179
218
  throw :halt, HALT_STRUCT.new(status, body)
180
219
  end
@@ -201,6 +240,20 @@ module Angelo
201
240
  halt 200, File.read(lp)
202
241
  end
203
242
 
243
+ def eventsource &block
244
+ headers SSE_HEADER
245
+ async :handle_event_source, responder.connection.detach.socket, block
246
+ halt 200, :sse
247
+ end
248
+
249
+ def report_errors?
250
+ @@report_errors
251
+ end
252
+
253
+ def sleep time
254
+ Celluloid.sleep time
255
+ end
256
+
204
257
  end
205
258
 
206
259
  end
@@ -31,14 +31,18 @@ module Angelo
31
31
  @hc ||= HTTPClient.new
32
32
  end
33
33
 
34
- def hc_req method, path, params = {}, headers = {}
34
+ def url path = nil
35
35
  url = HTTP_URL % [DEFAULT_ADDR, DEFAULT_PORT]
36
- @last_response = hc.__send__ method, url+path, params, headers
36
+ url += path if path
37
+ url
38
+ end
39
+
40
+ def hc_req method, path, params = {}, headers = {}
41
+ @last_response = hc.__send__ method, url(path), params, headers
37
42
  end
38
43
  private :hc_req
39
44
 
40
45
  def http_req method, path, params = {}, headers = {}
41
- url = HTTP_URL % [DEFAULT_ADDR, DEFAULT_PORT] + path
42
46
  params = case params
43
47
  when String; {body: params}
44
48
  when Hash
@@ -51,9 +55,9 @@ module Angelo
51
55
  end
52
56
  @last_response = case
53
57
  when !headers.empty?
54
- ::HTTP.with(headers).__send__ method, url, params
58
+ ::HTTP.with(headers).__send__ method, url(path), params
55
59
  else
56
- ::HTTP.__send__ method, url, params
60
+ ::HTTP.__send__ method, url(path), params
57
61
  end
58
62
  end
59
63
  private :http_req
@@ -65,6 +69,10 @@ module Angelo
65
69
  end
66
70
  end
67
71
 
72
+ def get_sse path, params = {}, headers = {}, &block
73
+ @last_response = hc.get url(path), params, headers, &block
74
+ end
75
+
68
76
  def websocket_helper path, params = {}
69
77
  params = params.keys.reduce([]) {|a,k|
70
78
  a << CGI.escape(k) + '=' + CGI.escape(params[k])
@@ -12,7 +12,7 @@ module Angelo
12
12
  def_delegator :@responder, :mustermann
13
13
  end
14
14
 
15
- [Responder, WebsocketResponder].each do |res|
15
+ [Responder, Responder::Websocket, Responder::Eventsource].each do |res|
16
16
  res.class_eval do
17
17
  attr_accessor :mustermann
18
18
  end
@@ -33,6 +33,11 @@ module Angelo
33
33
  super path, &block
34
34
  end
35
35
 
36
+ def eventsource path, &block
37
+ path = ::Mustermann.new path
38
+ super path, &block
39
+ end
40
+
36
41
  def routes
37
42
  @routes ||= {}
38
43
  ROUTABLE.each do |m|
@@ -0,0 +1,48 @@
1
+ module Angelo
2
+ class Responder
3
+ class Eventsource < Responder
4
+
5
+ def request= request
6
+ @params = nil
7
+ @request = request
8
+ handle_request
9
+ end
10
+
11
+ def handle_request
12
+ begin
13
+ if @response_handler
14
+ @base.before if @base.respond_to? :before
15
+ @body = catch(:halt) { @base.eventsource &@response_handler.bind(@base) }
16
+ if HALT_STRUCT === @body
17
+ raise RequestError.new 'unknown sse error' unless @body.body == :sse
18
+ end
19
+
20
+ # TODO any real reason not to run afters with SSE?
21
+ # @base.after if @base.respond_to? :after
22
+
23
+ respond
24
+ else
25
+ raise NotImplementedError
26
+ end
27
+ rescue IOError => ioe
28
+ warn "#{ioe.class} - #{ioe.message}"
29
+ close_websocket
30
+ rescue => e
31
+ error e.message
32
+ ::STDERR.puts e.backtrace
33
+ begin
34
+ @connection.close
35
+ rescue Reel::StateError => rcse
36
+ close_websocket
37
+ end
38
+ end
39
+ end
40
+
41
+ def respond
42
+ Angelo.log @connection, @request, nil, :ok
43
+ @request.respond 200, headers, nil
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -1,60 +1,55 @@
1
1
  module Angelo
2
+ class Responder
3
+ class Websocket < Responder
2
4
 
3
- class WebsocketResponder < Responder
5
+ class << self
4
6
 
5
- class << self
7
+ attr_writer :on_pong
6
8
 
7
- attr_writer :on_pong
9
+ def on_pong
10
+ @on_pong ||= ->(e){}
11
+ end
8
12
 
9
- def on_pong
10
- @on_pong ||= ->(e){}
11
13
  end
12
14
 
13
- end
14
-
15
- def params
16
- @params ||= parse_query_string
17
- @params
18
- end
19
-
20
- def request= request
21
- @params = nil
22
- @request = request
23
- @websocket = @request.websocket
24
- handle_request
25
- end
15
+ def request= request
16
+ @params = nil
17
+ @request = request
18
+ @websocket = @request.websocket
19
+ handle_request
20
+ end
26
21
 
27
- def handle_request
28
- begin
29
- if @response_handler
30
- Angelo.log @connection, @request, @websocket, :switching_protocols
31
- @bound_response_handler ||= @response_handler.bind @base
32
- @websocket.on_pong &WebsocketResponder.on_pong
33
- @base.before if @base.respond_to? :before
34
- @bound_response_handler[@websocket]
35
- @base.after if @base.respond_to? :after
36
- else
37
- raise NotImplementedError
38
- end
39
- rescue IOError => ioe
40
- warn "#{ioe.class} - #{ioe.message}"
41
- close_websocket
42
- rescue => e
43
- error e.message
44
- ::STDERR.puts e.backtrace
22
+ def handle_request
45
23
  begin
46
- @connection.close
47
- rescue Reel::StateError => rcse
24
+ if @response_handler
25
+ Angelo.log @connection, @request, @websocket, :switching_protocols
26
+ @bound_response_handler ||= @response_handler.bind @base
27
+ @websocket.on_pong &Responder::Websocket.on_pong
28
+ @base.before if @base.respond_to? :before
29
+ @bound_response_handler[@websocket]
30
+ @base.after if @base.respond_to? :after
31
+ else
32
+ raise NotImplementedError
33
+ end
34
+ rescue IOError => ioe
35
+ warn "#{ioe.class} - #{ioe.message}"
48
36
  close_websocket
37
+ rescue => e
38
+ error e.message
39
+ ::STDERR.puts e.backtrace
40
+ begin
41
+ @connection.close
42
+ rescue Reel::StateError => rcse
43
+ close_websocket
44
+ end
49
45
  end
50
46
  end
51
- end
52
47
 
53
- def close_websocket
54
- @websocket.close
55
- @base.websockets.remove_socket @websocket
56
- end
48
+ def close_websocket
49
+ @websocket.close
50
+ @base.websockets.remove_socket @websocket
51
+ end
57
52
 
53
+ end
58
54
  end
59
-
60
55
  end
@@ -32,18 +32,14 @@ module Angelo
32
32
 
33
33
  end
34
34
 
35
- attr_writer :connection
35
+ attr_accessor :connection
36
36
  attr_reader :request
37
+ attr_writer :base
37
38
 
38
39
  def initialize &block
39
40
  @response_handler = Base.compile! :request_handler, &block
40
41
  end
41
42
 
42
- def base= base
43
- @base = base
44
- @base.responder = self
45
- end
46
-
47
43
  def request= request
48
44
  @params = nil
49
45
  @redirect = nil
@@ -56,22 +52,32 @@ module Angelo
56
52
  if @response_handler
57
53
  @base.before if @base.respond_to? :before
58
54
  @body = catch(:halt) { @response_handler.bind(@base).call || EMPTY_STRING }
59
- @base.after if @base.respond_to? :after
55
+
56
+ # TODO any real reason not to run afters with SSE?
57
+ case @body
58
+ when HALT_STRUCT
59
+ if @body.body != :sse and @base.respond_to? :after
60
+ @base.after
61
+ end
62
+ else
63
+ @base.after if @base.respond_to? :after
64
+ end
65
+
60
66
  respond
61
67
  else
62
68
  raise NotImplementedError
63
69
  end
64
70
  rescue JSON::ParserError => jpe
65
- handle_error jpe, :bad_request, false
71
+ handle_error jpe, :bad_request
66
72
  rescue FormEncodingError => fee
67
- handle_error fee, :bad_request, false
73
+ handle_error fee, :bad_request
68
74
  rescue RequestError => re
69
- handle_error re, re.type, false
75
+ handle_error re, re.type
70
76
  rescue => e
71
77
  handle_error e
72
78
  end
73
79
 
74
- def handle_error _error, type = :internal_server_error, report = true
80
+ def handle_error _error, type = :internal_server_error, report = @base.report_errors?
75
81
  err_msg = error_message _error
76
82
  Angelo.log @connection, @request, nil, type, err_msg.size
77
83
  @connection.respond type, headers, err_msg
@@ -130,6 +136,7 @@ module Angelo
130
136
  when HALT_STRUCT
131
137
  status = @body.status
132
138
  @body = @body.body
139
+ @body = nil if @body == :sse
133
140
  if Hash === @body
134
141
  @body = {error: @body} if status != :ok or status < 200 && status >= 300
135
142
  @body = @body.to_json if respond_with? :json
@@ -148,11 +155,12 @@ module Angelo
148
155
 
149
156
  status ||= @redirect.nil? ? :ok : :moved_permanently
150
157
  headers LOCATION_HEADER_KEY => @redirect if @redirect
158
+ size = @body.nil? ? 0 : @body.size
151
159
 
152
- Angelo.log @connection, @request, nil, status, @body.size
153
- @connection.respond status, headers, @body
160
+ Angelo.log @connection, @request, nil, status, size
161
+ @request.respond status, headers, @body
154
162
  rescue => e
155
- handle_error e, :internal_server_error, false
163
+ handle_error e, :internal_server_error
156
164
  end
157
165
 
158
166
  def redirect url
data/lib/angelo/server.rb CHANGED
@@ -47,7 +47,7 @@ module Angelo
47
47
  rs = @base.routes[meth][request.path]
48
48
  if rs
49
49
  responder = rs.dup
50
- responder.base = @base.new
50
+ responder.base = @base.new responder
51
51
  responder.connection = connection
52
52
  responder.request = request
53
53
  else
@@ -92,6 +92,9 @@ module Angelo
92
92
  OpenSSL::Digest::SHA.hexdigest fs.ino.to_s + fs.size.to_s + fs.mtime.to_s
93
93
  end
94
94
 
95
+ def sse_event *a; Base.sse_event *a; end
96
+ def sse_message *a; Base.sse_message *a; end
97
+
95
98
  end
96
99
 
97
100
  end
data/lib/angelo/stash.rb CHANGED
@@ -2,62 +2,75 @@ module Angelo
2
2
 
3
3
  # utility class for stashing connected websockets in arbitrary contexts
4
4
  #
5
- class Stash
5
+ module Stash
6
6
  include Celluloid::Logger
7
7
 
8
- # hold the peeraddr info for use after the socket is closed (logging)
9
- #
10
- @@peeraddrs = {}
8
+ module ClassMethods
11
9
 
12
- # the underlying arrays of websockets, by context
13
- #
14
- @@stashes = {}
10
+ # the underlying arrays of websockets, by context
11
+ #
12
+ def stashes
13
+ @stashes ||= {}
14
+ end
15
+
16
+ # hold the peeraddr info for use after the socket is closed (logging)
17
+ #
18
+ def peeraddrs
19
+ @peeraddrs ||= {}
20
+ end
21
+
22
+ end
23
+
24
+ def stashes; self.class.stashes; end
25
+ def peeraddrs; self.class.peeraddrs; end
15
26
 
16
27
  # create a new instance with a context, creating the array if needed
17
28
  #
18
29
  def initialize server, context = :default
19
30
  raise ArgumentError.new "symbol required" unless Symbol === context
20
31
  @context, @server = context, server
21
- @@stashes[@context] ||= []
32
+ stashes[@context] ||= []
22
33
  end
23
34
 
24
35
  # add a websocket to this context's stash, save peeraddr info, start
25
36
  # server handle_websocket task to read from the socket and fire events
26
37
  # as needed
27
38
  #
28
- def << ws
29
- @@peeraddrs[ws] = ws.peeraddr
30
- @server.async.handle_websocket ws
31
- @@stashes[@context] << ws
39
+ def << s
40
+ peeraddrs[s] = s.peeraddr
41
+ yield if block_given?
42
+ stashes[@context] << s
32
43
  end
33
44
 
34
45
  # access the underlying array of this context
35
46
  #
36
47
  def stash
37
- @@stashes[@context]
48
+ stashes[@context]
38
49
  end
39
50
 
40
51
  # iterate on each connected websocket in this context, handling errors
41
52
  # as needed
42
53
  #
43
54
  def each &block
44
- stash.each do |ws|
55
+ stash.each do |s|
45
56
  begin
46
- yield ws
57
+ yield s
47
58
  rescue Reel::SocketError, IOError, SystemCallError => e
48
59
  debug e.message
49
- remove_socket ws
60
+ remove_socket s
50
61
  end
51
62
  end
63
+ nil
52
64
  end
53
65
 
54
- # remove a websocket from the stash, warn user, drop peeraddr info
66
+ # remove a socket from the stash, warn user, drop peeraddr info
55
67
  #
56
- def remove_socket ws
57
- if stash.include? ws
58
- warn "removing socket from context ':#{@context}' (#{@@peeraddrs[ws][2]})"
59
- stash.delete ws
60
- @@peeraddrs.delete ws
68
+ def remove_socket s
69
+ s.close unless s.closed?
70
+ if stash.include? s
71
+ warn "removing socket from context ':#{@context}' (#{peeraddrs[s][2]})"
72
+ stash.delete s
73
+ peeraddrs.delete s
61
74
  end
62
75
  end
63
76
 
@@ -71,12 +84,12 @@ module Angelo
71
84
  # ping_websockets task
72
85
  #
73
86
  def all_each
74
- @@stashes.values.flatten.each do |ws|
87
+ stashes.values.flatten.each do |s|
75
88
  begin
76
- yield ws
89
+ yield s
77
90
  rescue Reel::SocketError, IOError, SystemCallError => e
78
91
  debug e.message
79
- remove_socket ws
92
+ remove_socket s
80
93
  end
81
94
  end
82
95
  end
@@ -89,8 +102,8 @@ module Angelo
89
102
 
90
103
  # access the peeraddr info for a given websocket
91
104
  #
92
- def peeraddr ws
93
- @@peeraddrs[ws]
105
+ def peeraddr s
106
+ peeraddrs[s]
94
107
  end
95
108
 
96
109
  # return the number of websockets in this context (some are potentially
@@ -100,5 +113,35 @@ module Angelo
100
113
  stash.length
101
114
  end
102
115
 
116
+ class Websocket
117
+ extend Stash::ClassMethods
118
+ include Stash
119
+
120
+ def << ws
121
+ super do
122
+ @server.async.handle_websocket ws
123
+ end
124
+ end
125
+
126
+ end
127
+
128
+ class SSE
129
+ extend Stash::ClassMethods
130
+ include Stash
131
+
132
+ def event data
133
+ raise ArgumentError.new 'use #message method for "messages"' if @context == :default
134
+ each {|s| s.write Angelo::Base.sse_event(@context, data)}
135
+ nil
136
+ end
137
+
138
+ def message data
139
+ each {|s| s.write Angelo::Base.sse_message(data)}
140
+ nil
141
+ end
142
+
143
+ end
144
+
103
145
  end
146
+
104
147
  end
@@ -1,3 +1,3 @@
1
1
  module Angelo
2
- VERSION = '0.1.14'
2
+ VERSION = '0.1.15'
3
3
  end
data/lib/angelo.rb CHANGED
@@ -25,6 +25,7 @@ module Angelo
25
25
  ETAG_HEADER_KEY = 'ETag'
26
26
  IF_NONE_MATCH_HEADER_KEY = 'If-None-Match'
27
27
  LOCATION_HEADER_KEY = 'Location'
28
+ SSE_HEADER = { CONTENT_TYPE_HEADER_KEY => 'text/event-stream' }
28
29
 
29
30
  HTML_TYPE = 'text/html'
30
31
  JSON_TYPE = 'application/json'
@@ -57,6 +58,9 @@ module Angelo
57
58
 
58
59
  HALT_STRUCT = Struct.new :status, :body
59
60
 
61
+ SSE_DATA_TEMPLATE = "data: %s\n\n"
62
+ SSE_EVENT_TEMPLATE = "event: %s\ndata: %s\n\n"
63
+
60
64
  class << self
61
65
 
62
66
  attr_writer :response_log_level
@@ -122,6 +126,7 @@ require 'angelo/server'
122
126
  require 'angelo/base'
123
127
  require 'angelo/stash'
124
128
  require 'angelo/responder'
129
+ require 'angelo/responder/eventsource'
125
130
  require 'angelo/responder/websocket'
126
131
 
127
132
  # trap "INT" do
@@ -0,0 +1,69 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Angelo::Responder::Eventsource do
4
+
5
+ describe 'route builder' do
6
+
7
+ define_app do
8
+
9
+ eventsource '/msg' do |c|
10
+ c.write sse_message 'hi'
11
+ c.close
12
+ end
13
+
14
+ eventsource '/event' do |c|
15
+ c.write sse_event :sse, 'bye'
16
+ c.close
17
+ end
18
+
19
+ end
20
+
21
+ it 'sends messages' do
22
+ get_sse '/msg' do |msg|
23
+ msg.must_equal "data: hi\n\n"
24
+ end
25
+ end
26
+
27
+ it 'sends events' do
28
+ get_sse '/event' do |msg|
29
+ msg.must_equal "event: sse\ndata: bye\n\n"
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ describe 'eventsource helper' do
38
+
39
+ define_app do
40
+
41
+ get '/msg' do
42
+ eventsource do |c|
43
+ c.write sse_message 'hi'
44
+ c.close
45
+ end
46
+ end
47
+
48
+ get '/event' do
49
+ eventsource do |c|
50
+ c.write sse_event :sse, 'bye'
51
+ c.close
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ it 'sends messages' do
58
+ get_sse '/msg' do |msg|
59
+ msg.must_equal "data: hi\n\n"
60
+ end
61
+ end
62
+
63
+ it 'sends events' do
64
+ get_sse '/event' do |msg|
65
+ msg.must_equal "event: sse\ndata: bye\n\n"
66
+ end
67
+ end
68
+
69
+ end
@@ -1,6 +1,6 @@
1
1
  require_relative '../spec_helper'
2
2
 
3
- describe Angelo::WebsocketResponder do
3
+ describe Angelo::Responder::Websocket do
4
4
 
5
5
  def websocket_wait_for path, latch, expectation, key = :swf, &block
6
6
  Reactor.testers[key] = Array.new CONCURRENCY do
@@ -225,8 +225,14 @@ describe Angelo::WebsocketResponder do
225
225
  end
226
226
 
227
227
  describe 'helper contexts' do
228
- let(:obj){ {'foo' => 'bar'} }
229
- let(:wait_for_block){ ->(e){ assert_equal obj, JSON.parse(e.data) }}
228
+
229
+ def obj
230
+ { 'foo' => 'bar' }
231
+ end
232
+
233
+ def wait_for_block
234
+ ->(e){ assert_equal obj, JSON.parse(e.data) }
235
+ end
230
236
 
231
237
  define_app do
232
238
 
@@ -345,4 +351,33 @@ describe Angelo::WebsocketResponder do
345
351
 
346
352
  end
347
353
 
354
+ describe 'params' do
355
+
356
+ def wait_for_block
357
+ ->(e){ assert_equal({'bar' => 'foo', 'baz' => 'bat'}, JSON.parse(e.data)) }
358
+ end
359
+
360
+ define_app do
361
+
362
+ websocket '/' do |ws|
363
+ websockets[params[:bar].to_sym] << ws
364
+ end
365
+
366
+ post '/' do
367
+ pj = params.to_json
368
+ websockets[params[:bar].to_sym].each {|ws| ws.write pj}
369
+ end
370
+
371
+ end
372
+
373
+ it 'uses params correctly' do
374
+ latch = CountDownLatch.new CONCURRENCY
375
+ websocket_wait_for '/?bar=foo', latch, wait_for_block do
376
+ post '/', bar: 'foo', baz: 'bat'
377
+ latch.wait
378
+ end
379
+ end
380
+
381
+ end
382
+
348
383
  end
data/test/angelo_spec.rb CHANGED
@@ -31,6 +31,11 @@ describe Angelo::Base do
31
31
  redirect '/'
32
32
  end
33
33
 
34
+ get '/wait' do
35
+ sleep 3
36
+ nil
37
+ end
38
+
34
39
  end
35
40
 
36
41
  it 'responds to http requests properly' do
@@ -56,6 +61,36 @@ describe Angelo::Base do
56
61
  last_response.headers['Location'].must_equal '/'
57
62
  end
58
63
 
64
+ it 'responds to requests concurrently' do
65
+ wait_end = nil
66
+ get_end = nil
67
+ latch = CountDownLatch.new 2
68
+
69
+ ActorPool.define_action :do_wait do
70
+ get '/wait'
71
+ wait_end = Time.now
72
+ latch.count_down
73
+ end
74
+
75
+ ActorPool.define_action :do_get do
76
+ sleep 1
77
+ get '/'
78
+ get_end = Time.now
79
+ latch.count_down
80
+ end
81
+
82
+ ActorPool.unstop!
83
+ $pool.async :do_wait
84
+ $pool.async :do_get
85
+
86
+ latch.wait
87
+ get_end.must_be :<, wait_end
88
+
89
+ ActorPool.stop!
90
+ ActorPool.remove_action :do_wait
91
+ ActorPool.remove_action :do_get
92
+ end
93
+
59
94
  end
60
95
 
61
96
  describe 'before filter' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: angelo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.14
4
+ version: 0.1.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kenichi Nakamura
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-03 00:00:00.000000000 Z
11
+ date: 2014-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: reel
@@ -60,6 +60,7 @@ files:
60
60
  - lib/angelo/mustermann.rb
61
61
  - lib/angelo/params_parser.rb
62
62
  - lib/angelo/responder.rb
63
+ - lib/angelo/responder/eventsource.rb
63
64
  - lib/angelo/responder/websocket.rb
64
65
  - lib/angelo/server.rb
65
66
  - lib/angelo/stash.rb
@@ -67,6 +68,7 @@ files:
67
68
  - lib/angelo/version.rb
68
69
  - test/angelo/erb_spec.rb
69
70
  - test/angelo/error_spec.rb
71
+ - test/angelo/eventsource_spec.rb
70
72
  - test/angelo/mustermann_spec.rb
71
73
  - test/angelo/params_spec.rb
72
74
  - test/angelo/static_spec.rb