crampy 0.15.3
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/MIT-LICENSE +20 -0
- data/bin/cramp +17 -0
- data/lib/cramp/abstract.rb +104 -0
- data/lib/cramp/action.rb +134 -0
- data/lib/cramp/body.rb +48 -0
- data/lib/cramp/callbacks.rb +92 -0
- data/lib/cramp/exception_handler.rb +357 -0
- data/lib/cramp/fiber_pool.rb +47 -0
- data/lib/cramp/generators/application.rb +97 -0
- data/lib/cramp/generators/templates/application/Gemfile +32 -0
- data/lib/cramp/generators/templates/application/app/actions/home_action.rb +11 -0
- data/lib/cramp/generators/templates/application/application.rb +36 -0
- data/lib/cramp/generators/templates/application/config/database.yml +6 -0
- data/lib/cramp/generators/templates/application/config/routes.rb +4 -0
- data/lib/cramp/generators/templates/application/config.ru +25 -0
- data/lib/cramp/keep_connection_alive.rb +19 -0
- data/lib/cramp/long_polling.rb +6 -0
- data/lib/cramp/periodic_timer.rb +56 -0
- data/lib/cramp/rendering.rb +11 -0
- data/lib/cramp/sse.rb +5 -0
- data/lib/cramp/test_case.rb +58 -0
- data/lib/cramp/version.rb +3 -0
- data/lib/cramp/websocket.rb +13 -0
- data/lib/cramp.rb +47 -0
- data/lib/crampy.rb +4 -0
- data/lib/vendor/fiber_pool.rb +85 -0
- metadata +130 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009-2011 Pratik Naik
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/bin/cramp
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'cramp'
|
4
|
+
require 'cramp/generators/application'
|
5
|
+
|
6
|
+
if ['--version', '-v'].include?(ARGV.first)
|
7
|
+
puts "Cramp #{Cramp::VERSION}"
|
8
|
+
exit(0)
|
9
|
+
end
|
10
|
+
|
11
|
+
if ARGV.first != "new"
|
12
|
+
ARGV[0] = "--help"
|
13
|
+
else
|
14
|
+
ARGV.shift
|
15
|
+
end
|
16
|
+
|
17
|
+
Cramp::Generators::Application.start
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'active_support/core_ext/hash/keys'
|
2
|
+
|
3
|
+
module Cramp
|
4
|
+
class Abstract
|
5
|
+
include Callbacks
|
6
|
+
include FiberPool
|
7
|
+
|
8
|
+
class_attribute :transport
|
9
|
+
self.transport = :regular
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def call(env)
|
13
|
+
new(env).process
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(env)
|
18
|
+
@env = env
|
19
|
+
@finished = false
|
20
|
+
|
21
|
+
@_state = :init
|
22
|
+
end
|
23
|
+
|
24
|
+
def process
|
25
|
+
EM.next_tick { before_start }
|
26
|
+
throw :async
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def continue
|
32
|
+
init_async_body
|
33
|
+
send_headers
|
34
|
+
|
35
|
+
@_state = :started
|
36
|
+
EM.next_tick { on_start }
|
37
|
+
end
|
38
|
+
|
39
|
+
def send_headers
|
40
|
+
status, headers = build_headers
|
41
|
+
send_initial_response(status, headers, @body)
|
42
|
+
rescue StandardError, LoadError, SyntaxError => exception
|
43
|
+
handle_exception(exception)
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_headers
|
47
|
+
status, headers = respond_to?(:respond_with, true) ? respond_with.dup : [200, {'Content-Type' => 'text/html'}]
|
48
|
+
headers['Connection'] ||= 'keep-alive'
|
49
|
+
[status, headers]
|
50
|
+
end
|
51
|
+
|
52
|
+
def init_async_body
|
53
|
+
@body = Body.new
|
54
|
+
|
55
|
+
if self.class.on_finish_callbacks.any?
|
56
|
+
@body.callback { on_finish }
|
57
|
+
@body.errback { on_finish }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def finished?
|
62
|
+
!!@finished
|
63
|
+
end
|
64
|
+
|
65
|
+
def finish
|
66
|
+
@_state = :finishing
|
67
|
+
@body.succeed if is_finishable?
|
68
|
+
ensure
|
69
|
+
@_state = :finished
|
70
|
+
@finished = true
|
71
|
+
end
|
72
|
+
|
73
|
+
def send_initial_response(response_status, response_headers, response_body)
|
74
|
+
send_response(response_status, response_headers, response_body)
|
75
|
+
end
|
76
|
+
|
77
|
+
def halt(status, headers = {}, halt_body = '')
|
78
|
+
send_response(status, headers, halt_body)
|
79
|
+
end
|
80
|
+
|
81
|
+
def send_response(response_status, response_headers, response_body)
|
82
|
+
@env['async.callback'].call [response_status, response_headers, response_body]
|
83
|
+
end
|
84
|
+
|
85
|
+
def request
|
86
|
+
@request ||= Rack::Request.new(@env)
|
87
|
+
end
|
88
|
+
|
89
|
+
def params
|
90
|
+
@params ||= request.params.update(route_params).symbolize_keys
|
91
|
+
end
|
92
|
+
|
93
|
+
def route_params
|
94
|
+
@env['router.params'] || {}
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def is_finishable?
|
100
|
+
!finished? && @body && !@body.closed?
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
data/lib/cramp/action.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
module Cramp
|
2
|
+
class Action < Abstract
|
3
|
+
include PeriodicTimer
|
4
|
+
include KeepConnectionAlive
|
5
|
+
|
6
|
+
def initialize(env)
|
7
|
+
super
|
8
|
+
|
9
|
+
case
|
10
|
+
when Faye::EventSource.eventsource?(env)
|
11
|
+
# request has Accept: text/event-stream
|
12
|
+
# faye server adapter intercepts headers - need to send them in send_initial_response or use faye's implementation
|
13
|
+
@eventsource_detected = true
|
14
|
+
unless transport == :sse
|
15
|
+
err = "WARNING: Cramp got request with EventSource header on action with transport #{transport} (not sse)! Response may not contain valid http headers!"
|
16
|
+
Cramp.logger ? Cramp.logger.error(err) : $stderr.puts(err)
|
17
|
+
end
|
18
|
+
when Faye::WebSocket.websocket?(env)
|
19
|
+
@web_socket = Faye::WebSocket.new(env)
|
20
|
+
@web_socket.onmessage = lambda do |event|
|
21
|
+
message = event.data
|
22
|
+
_invoke_data_callbacks(message) if message.is_a?(String)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def render(body, *args)
|
30
|
+
send(:"render_#{transport}", body, *args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def send_initial_response(status, headers, body)
|
34
|
+
case transport
|
35
|
+
when :long_polling
|
36
|
+
# Dont send no initial response. Just cache it for later.
|
37
|
+
@_lp_status = status
|
38
|
+
@_lp_headers = headers
|
39
|
+
when :sse
|
40
|
+
super
|
41
|
+
if @eventsource_detected
|
42
|
+
# Reconstruct headers that were killed by faye server adapter:
|
43
|
+
@body.call("HTTP/1.1 200 OK\r\n#{headers.map{|(k,v)| "#{k}: #{v.is_a?(Time) ? v.httpdate : v.to_s}"}.join("\r\n")}\r\n\r\n")
|
44
|
+
end
|
45
|
+
# send retry? @body.call("retry: #{ (@retry * 1000).floor }\r\n\r\n")
|
46
|
+
else
|
47
|
+
super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class_attribute :default_sse_headers
|
52
|
+
self.default_sse_headers = {'Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache, no-store', 'Connection' => 'keep-alive'}
|
53
|
+
|
54
|
+
class_attribute :default_chunked_headers
|
55
|
+
self.default_chunked_headers = {'Transfer-Encoding' => 'chunked', 'Connection' => 'keep-alive'}
|
56
|
+
|
57
|
+
def build_headers
|
58
|
+
case transport
|
59
|
+
when :sse
|
60
|
+
status, headers = respond_to?(:respond_with, true) ? respond_with : [200, {'Content-Type' => 'text/html'}]
|
61
|
+
[status, headers.merge(self.default_sse_headers)]
|
62
|
+
when :chunked
|
63
|
+
status, headers = respond_to?(:respond_with, true) ? respond_with : [200, {}]
|
64
|
+
|
65
|
+
headers = headers.merge(self.default_chunked_headers)
|
66
|
+
headers['Content-Type'] ||= 'text/html'
|
67
|
+
headers['Cache-Control'] ||= 'no-cache'
|
68
|
+
|
69
|
+
[status, headers]
|
70
|
+
else
|
71
|
+
super
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def render_regular(body, *)
|
76
|
+
@body.call(body)
|
77
|
+
end
|
78
|
+
|
79
|
+
def render_long_polling(data, *)
|
80
|
+
@_lp_headers['Content-Length'] = data.size.to_s
|
81
|
+
|
82
|
+
send_response(@_lp_status, @_lp_headers, @body)
|
83
|
+
@body.call(data)
|
84
|
+
|
85
|
+
finish
|
86
|
+
end
|
87
|
+
|
88
|
+
def render_sse(data, options = {})
|
89
|
+
#TODO: Faye uses \r\n for newlines, some compatibility?
|
90
|
+
result = "id: #{sse_event_id}\n"
|
91
|
+
result << "event: #{options[:event]}\n" if options[:event]
|
92
|
+
result << "retry: #{options[:retry]}\n" if options[:retry]
|
93
|
+
|
94
|
+
data.split(/\n/).each {|d| result << "data: #{d}\n" }
|
95
|
+
result << "\n"
|
96
|
+
|
97
|
+
@body.call(result)
|
98
|
+
end
|
99
|
+
|
100
|
+
def render_websocket(body, *)
|
101
|
+
@web_socket.send(body)
|
102
|
+
end
|
103
|
+
|
104
|
+
CHUNKED_TERM = "\r\n"
|
105
|
+
CHUNKED_TAIL = "0#{CHUNKED_TERM}#{CHUNKED_TERM}"
|
106
|
+
|
107
|
+
def render_chunked(body, *)
|
108
|
+
data = [Rack::Utils.bytesize(body).to_s(16), CHUNKED_TERM, body, CHUNKED_TERM].join
|
109
|
+
|
110
|
+
@body.call(data)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Used by SSE
|
114
|
+
def sse_event_id
|
115
|
+
@sse_event_id ||= Time.now.to_i
|
116
|
+
end
|
117
|
+
|
118
|
+
def encode(string, encoding = 'UTF-8')
|
119
|
+
string.respond_to?(:force_encoding) ? string.force_encoding(encoding) : string
|
120
|
+
end
|
121
|
+
|
122
|
+
protected
|
123
|
+
|
124
|
+
def finish
|
125
|
+
case transport
|
126
|
+
when :chunked
|
127
|
+
@body.call(CHUNKED_TAIL) if is_finishable?
|
128
|
+
end
|
129
|
+
|
130
|
+
super
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
data/lib/cramp/body.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Copyright 2008 James Tucker <raggi@rubyforge.org>.
|
2
|
+
|
3
|
+
module Cramp
|
4
|
+
class Body
|
5
|
+
include EventMachine::Deferrable
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@queue = []
|
9
|
+
|
10
|
+
# Make sure to flush out the queue before closing the connection
|
11
|
+
callback { flush }
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(body)
|
15
|
+
@queue << body
|
16
|
+
schedule_dequeue
|
17
|
+
end
|
18
|
+
|
19
|
+
def each &blk
|
20
|
+
@body_callback = blk
|
21
|
+
schedule_dequeue
|
22
|
+
end
|
23
|
+
|
24
|
+
def closed?
|
25
|
+
@deferred_status != :unknown
|
26
|
+
end
|
27
|
+
|
28
|
+
def flush
|
29
|
+
return unless @body_callback
|
30
|
+
|
31
|
+
until @queue.empty?
|
32
|
+
Array(@queue.shift).each {|chunk| @body_callback.call(chunk) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def schedule_dequeue
|
37
|
+
return unless @body_callback
|
38
|
+
|
39
|
+
EventMachine.next_tick do
|
40
|
+
next unless body = @queue.shift
|
41
|
+
|
42
|
+
Array(body).each {|chunk| @body_callback.call(chunk) }
|
43
|
+
schedule_dequeue unless @queue.empty?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Cramp
|
2
|
+
module Callbacks
|
3
|
+
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
#was class_inheritable_accessor
|
8
|
+
class_attribute :before_start_callbacks, :on_finish_callbacks, :on_start_callback, :on_data_callbacks, :instance_reader => false
|
9
|
+
|
10
|
+
self.before_start_callbacks = []
|
11
|
+
self.on_finish_callbacks = []
|
12
|
+
self.on_start_callback = []
|
13
|
+
self.on_data_callbacks = []
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def before_start(*methods)
|
18
|
+
self.before_start_callbacks += methods
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_finish(*methods)
|
22
|
+
self.on_finish_callbacks += methods
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_start(*methods)
|
26
|
+
self.on_start_callback += methods
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_data(*methods)
|
30
|
+
self.on_data_callbacks += methods
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def before_start(n = 0)
|
35
|
+
if callback = self.class.before_start_callbacks[n]
|
36
|
+
callback_wrapper { send(callback) { before_start(n+1) } }
|
37
|
+
else
|
38
|
+
continue
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def on_start
|
43
|
+
callback_wrapper { start } if respond_to?(:start)
|
44
|
+
|
45
|
+
self.class.on_start_callback.each do |callback|
|
46
|
+
callback_wrapper { send(callback) unless @finished }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def on_finish
|
51
|
+
self.class.on_finish_callbacks.each do |callback|
|
52
|
+
callback_wrapper { send(callback) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def callback_wrapper
|
57
|
+
EM.next_tick do
|
58
|
+
begin
|
59
|
+
yield
|
60
|
+
rescue StandardError, LoadError, SyntaxError => exception
|
61
|
+
handle_exception(exception)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def _invoke_data_callbacks(message)
|
69
|
+
self.class.on_data_callbacks.each do |callback|
|
70
|
+
callback_wrapper { send(callback, message) }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def handle_exception(exception)
|
75
|
+
handler = ExceptionHandler.new(@env, exception)
|
76
|
+
|
77
|
+
# Log the exception
|
78
|
+
unless ENV['RACK_ENV'] == 'test'
|
79
|
+
exception_body = handler.dump_exception
|
80
|
+
Cramp.logger ? Cramp.logger.error(exception_body) : $stderr.puts(exception_body)
|
81
|
+
end
|
82
|
+
|
83
|
+
case @_state
|
84
|
+
when :init
|
85
|
+
halt 500, {"Content-Type" => 'text/html'}, ENV['RACK_ENV'] == 'development' ? handler.pretty : 'Something went wrong'
|
86
|
+
else
|
87
|
+
finish
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|