angelo 0.1.14 → 0.1.15

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.
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