sinatra-diet 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.md +101 -0
- data/Rakefile +21 -0
- data/VERSION +1 -0
- data/examples/async.ru +15 -0
- data/examples/chat.ru +69 -0
- data/examples/delayed.ru +17 -0
- data/examples/echo.ru +26 -0
- data/lib/sinatra-diet.rb +2 -0
- data/lib/sinatra/async.rb +81 -0
- data/lib/sinatra/websocket.rb +22 -0
- metadata +116 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Samuel Cochran
|
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/README.md
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# Sinatra Diet
|
2
|
+
|
3
|
+
*Warning:* This is stuff I'm playing with, definitely not ready for anything production.
|
4
|
+
|
5
|
+
[Sinatra][sinatra] on a Diet gets [Thin][thin] and [Skinny][skinny], asynchronously
|
6
|
+
|
7
|
+
Sometimes Sinatra can get a bit fat--he's squeezing through the doorway, gets stuck, and nobody else can get through for a while. It's time to go on a diet to get [Thin][thin] and [Skinny][skinny].
|
8
|
+
|
9
|
+
One of Thin's greatest strength is asynchronous responses. This adds two ways to do so from within Sinatra: plain asynchronous responses and WebSockets (via [Skinny][skinny]).
|
10
|
+
|
11
|
+
This is actually two Sinatra extensions:
|
12
|
+
|
13
|
+
## Sinatra::Async
|
14
|
+
|
15
|
+
I know there's already [a sinatra-async extension](http://github.com/tmm1/async_sinatra) but I felt it was overly complex and didn't quite add what I wanted. My take on asynchronous Sinatra tried to be a little simpler. For the timeless classic:
|
16
|
+
|
17
|
+
register Sinatra::Async
|
18
|
+
|
19
|
+
get '/' do
|
20
|
+
async do
|
21
|
+
"Hello, World"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
This literally just delays response to the next available EventMachine tick.
|
26
|
+
|
27
|
+
If you actually want to wait on a long-running asynchronous operation you have a couple of options. You can yield a deferrable and succeed it with the response:
|
28
|
+
|
29
|
+
get '/long' do
|
30
|
+
async do
|
31
|
+
@deferrable = EM::Deferrable.new
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# somewhere else:
|
36
|
+
@deferrable.succeed "Hello, world!"
|
37
|
+
|
38
|
+
You can also use a long-running operation which will call #async\_respond explicitly. EM::Timers, EM::PeriodicTimers and nil responses to an async block mean you'll call #async\_respond later:
|
39
|
+
|
40
|
+
get '/long' do
|
41
|
+
async do
|
42
|
+
EventMachine::Timer.new(2) do
|
43
|
+
async_respond 'Hello, world!'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
## Sinatra::WebSocket
|
49
|
+
|
50
|
+
Build websockets simply and easily using a Sinatra-inspired DSL:
|
51
|
+
|
52
|
+
register Sinatra::Async
|
53
|
+
|
54
|
+
websocket do |client, message|
|
55
|
+
client.send "You said: #{message}"
|
56
|
+
end
|
57
|
+
|
58
|
+
They catch GET websocket requests only, by default. You can also mount them on a path and give them explicit options:
|
59
|
+
|
60
|
+
websocket '/hello',
|
61
|
+
:on_handshake => proc do |client|
|
62
|
+
client.send "Hi!"
|
63
|
+
client.finish!
|
64
|
+
end
|
65
|
+
|
66
|
+
The clients are Skinny::WebSocket instances, and you can supply any options you would normally pass to an instance in the handler call:
|
67
|
+
|
68
|
+
websocket '/thing',
|
69
|
+
:protocol => "adder",
|
70
|
+
:on_message => proc do |client, message|
|
71
|
+
client.send message.split(' ').compact.map(&:to_i).inject(0, &:+)
|
72
|
+
end
|
73
|
+
|
74
|
+
Keep in mind that the proc callbacks supplied as options here are executed in the scope in which they're defined, here in the class scope of your sinatra app. This is _by design_--executing each handler inside a Sinatra instance means that instance (which is copied for every request) must hang around for the WebSocket connection's entire lifetime. If you want this, please implement it yourself.
|
75
|
+
|
76
|
+
Don't forget that the websocket client connection has a copy of the request's environment (as #env) which you can use inside callbacks.
|
77
|
+
|
78
|
+
## Caveats
|
79
|
+
|
80
|
+
*Be aware:* Long-running requests will keep a whole copy of your Sinatra app around until you complete them. Be careful to close every request and websocket you handle asynchronously or you'll find yourself in memory leak city.
|
81
|
+
|
82
|
+
This stuff only works on Thin. Patches for other EventMachine-based servers are welcome. Other wild and exotic servers are also considered, if you're brave! I'm looking at [ControlTower][controltower], mainly.
|
83
|
+
|
84
|
+
## TODO
|
85
|
+
|
86
|
+
* Lightweight WebSocket channels.
|
87
|
+
* ???
|
88
|
+
* Profit
|
89
|
+
|
90
|
+
## Copyright
|
91
|
+
|
92
|
+
Copyright (c) 2010 Samuel Cochran. See LICENSE for details.
|
93
|
+
|
94
|
+
## P.S.
|
95
|
+
|
96
|
+
Do I get points for taking a metaphor too far?
|
97
|
+
|
98
|
+
[controltower]: http://github.com/MacRuby/ControlTower
|
99
|
+
[skinny]: http://github.com/sj26/skinny
|
100
|
+
[sinatra]: http://github.com/sinatra/sinatra
|
101
|
+
[thin]: http://github.com/macournoyer/thin
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "sinatra-diet"
|
8
|
+
gem.summary = %Q{Sinatra on a Diet gets Thin and Skinny}
|
9
|
+
gem.description = %Q{Sinatra can be aynchronous and provide WebSockets using Thin and Skinny.}
|
10
|
+
gem.email = "sj26@sj26.com"
|
11
|
+
gem.homepage = "http://github.com/sj26/sinatra-diet"
|
12
|
+
gem.authors = ["Samuel Cochran"]
|
13
|
+
|
14
|
+
gem.add_dependency "sinatra", ">= 0"
|
15
|
+
gem.add_dependency "thin", ">= 0"
|
16
|
+
gem.add_dependency "skinny", ">= 0.1.2"
|
17
|
+
end
|
18
|
+
Jeweler::GemcutterTasks.new
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
21
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/examples/async.ru
ADDED
data/examples/chat.ru
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'sinatra-diet'
|
4
|
+
|
5
|
+
class Application < Sinatra::Base
|
6
|
+
register Sinatra::WebSocket
|
7
|
+
|
8
|
+
def self.listeners
|
9
|
+
@@listeners ||= []
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.said
|
13
|
+
@@said ||= []
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.say what
|
17
|
+
said << what
|
18
|
+
EM.next_tick do
|
19
|
+
listeners.each do |listener|
|
20
|
+
listener.send_message what
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
websocket '/',
|
26
|
+
:on_handshake => proc { |client| listeners << client },
|
27
|
+
:on_message => proc { |client, message| say message },
|
28
|
+
:on_close => proc { |client| listeners.delete client }
|
29
|
+
|
30
|
+
get '/' do
|
31
|
+
<<-HTML
|
32
|
+
<!DOCTYPE html>
|
33
|
+
<html>
|
34
|
+
<head>
|
35
|
+
<title>Simple Chat</title>
|
36
|
+
<script src="http://code.jquery.com/jquery.js"></script>
|
37
|
+
<script type="text/javascript">
|
38
|
+
jQuery(function ($) {
|
39
|
+
var websocket = new WebSocket("#{request.url.sub(/^http/, 'ws')}"),
|
40
|
+
$form = $('form'), $message = $('input[name="message"]', $form);
|
41
|
+
websocket.onmessage = function(message) {
|
42
|
+
$('#said').prepend(message.data + "\\n");
|
43
|
+
};
|
44
|
+
$form.submit(function () {
|
45
|
+
websocket.send($message.val());
|
46
|
+
$message.val("");
|
47
|
+
return false;
|
48
|
+
});
|
49
|
+
$message.focus();
|
50
|
+
});
|
51
|
+
</script>
|
52
|
+
</head>
|
53
|
+
<body>
|
54
|
+
<form action="/say" method="post">
|
55
|
+
<input type="text" name="message" style="width: 100%;" />
|
56
|
+
<pre id="said" style="height: 20em; overflow: auto; border: 1px inset;">#{self.class.said.reverse.collect { |message| message + "\n" }.join ''}</pre>
|
57
|
+
</form>
|
58
|
+
</body>
|
59
|
+
</html>
|
60
|
+
HTML
|
61
|
+
end
|
62
|
+
|
63
|
+
post '/say' do
|
64
|
+
self.class.say params[:message]
|
65
|
+
redirect '/'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
run Application
|
data/examples/delayed.ru
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'sinatra-diet'
|
4
|
+
|
5
|
+
class Application < Sinatra::Base
|
6
|
+
register Sinatra::Async
|
7
|
+
|
8
|
+
get '/' do
|
9
|
+
async do
|
10
|
+
EventMachine::Timer.new(2) do
|
11
|
+
async_respond 'Hello, world!'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
run Application
|
data/examples/echo.ru
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'sinatra-diet'
|
4
|
+
|
5
|
+
class Application < Sinatra::Base
|
6
|
+
register Sinatra::WebSocket
|
7
|
+
|
8
|
+
get '/' do
|
9
|
+
'<script>' +
|
10
|
+
'var websocket = new WebSocket("ws://localhost:3000/echo");' +
|
11
|
+
'websocket.onmessage=function(message){' +
|
12
|
+
'document.getElementById(\'transcript\').innerHTML+=message.data+"\n";' +
|
13
|
+
'}' +
|
14
|
+
'</script>' +
|
15
|
+
'<form onsubmit="websocket.send(this.message.value);this.message.value=\'\';return false">' +
|
16
|
+
'<input name="message" type="text" />' +
|
17
|
+
'</form>' +
|
18
|
+
'<pre id="transcript"></pre>'
|
19
|
+
end
|
20
|
+
|
21
|
+
websocket '/echo' do |connection, message|
|
22
|
+
connection.send_message "You said: #{message}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
run Application
|
data/lib/sinatra-diet.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
module Sinatra::Async
|
2
|
+
def self.registered base
|
3
|
+
base.send :include, InstanceMethods
|
4
|
+
end
|
5
|
+
|
6
|
+
module InstanceMethods
|
7
|
+
# Repond to this request asynchronously.
|
8
|
+
#
|
9
|
+
# Can be passed a block or method name which will be queued for
|
10
|
+
# execution, or just aborts the current flow and presumes
|
11
|
+
# you'll respond later.
|
12
|
+
#
|
13
|
+
# The return value of the supplied block or method will be used
|
14
|
+
# as a response, as per a normal Sinatra route handler, unless:
|
15
|
+
#
|
16
|
+
# * If it is an EM::Deferrable, we will respond as a succeed
|
17
|
+
# callback.
|
18
|
+
#
|
19
|
+
# * If it is nil, is an EM::Timer or EM::PeriodicTimer we
|
20
|
+
# presume you'll respond later using #async_respond.
|
21
|
+
def async method_name=nil, &block
|
22
|
+
raise RuntimeError, 'Not running in async capable server -- try Thin' unless env.has_key?('async.callback')
|
23
|
+
|
24
|
+
block ||= method method_name if method_name
|
25
|
+
|
26
|
+
if block
|
27
|
+
EM.next_tick do
|
28
|
+
catch :async do
|
29
|
+
returned = invoke { route_eval &block }
|
30
|
+
|
31
|
+
if returned.is_a? EventMachine::Deferrable
|
32
|
+
returned.callback do |*args|
|
33
|
+
invoke { throw :halt, args.first } unless args.empty?
|
34
|
+
async_call!
|
35
|
+
end
|
36
|
+
elsif returned.is_a?(EventMachine::Timer) || returned.is_a?(EventMachine::PeriodicTimer)
|
37
|
+
returned = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
async_call! unless returned.nil?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
throw :async
|
46
|
+
end
|
47
|
+
|
48
|
+
# Respond to an existing asynchronous request.
|
49
|
+
def async_respond response
|
50
|
+
invoke { throw :halt, response }
|
51
|
+
async_call!
|
52
|
+
end
|
53
|
+
|
54
|
+
# Resumes #call! and sends an asynchronous response
|
55
|
+
#
|
56
|
+
# To DRY this up we'd need to break up Sinatra::Base#call!
|
57
|
+
def async_call!
|
58
|
+
invoke { error_block! response.status }
|
59
|
+
|
60
|
+
unless @response['Content-Type']
|
61
|
+
if body.respond_to?(:to_ary) and body.first.respond_to? :content_type
|
62
|
+
content_type body.first.content_type
|
63
|
+
else
|
64
|
+
content_type :html
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
status, header, body = @response.finish
|
69
|
+
|
70
|
+
# Never produce a body on HEAD requests. Do retain the Content-Length
|
71
|
+
# unless it's "0", in which case we assume it was calculated erroneously
|
72
|
+
# for a manual HEAD response and remove it entirely.
|
73
|
+
if @env['REQUEST_METHOD'] == 'HEAD'
|
74
|
+
body = []
|
75
|
+
header.delete('Content-Length') if header['Content-Length'] == '0'
|
76
|
+
end
|
77
|
+
|
78
|
+
env['async.callback'].call [status, header, body]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'skinny'
|
2
|
+
|
3
|
+
module Sinatra::WebSocket
|
4
|
+
WEBSOCKET_OPTIONS = [:protocol, :on_open, :on_start, :on_handshake, :on_message, :on_error, :on_finish, :on_close]
|
5
|
+
|
6
|
+
def self.registered base
|
7
|
+
base.send :include, Skinny::Helpers
|
8
|
+
end
|
9
|
+
|
10
|
+
def websocket path='*', options={}, &block
|
11
|
+
# No nice way to do this in core?
|
12
|
+
websocket_options = options.select { |key, value| WEBSOCKET_OPTIONS.include? key }
|
13
|
+
options.reject! { |key, value| WEBSOCKET_OPTIONS.include? key }
|
14
|
+
|
15
|
+
condition { websocket? }
|
16
|
+
|
17
|
+
route 'GET', path, options do
|
18
|
+
websocket! websocket_options.dup, &block
|
19
|
+
throw :async
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sinatra-diet
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Samuel Cochran
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-11-01 00:00:00 +08:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: sinatra
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: thin
|
35
|
+
prerelease: false
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
version: "0"
|
44
|
+
type: :runtime
|
45
|
+
version_requirements: *id002
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: skinny
|
48
|
+
prerelease: false
|
49
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
- 1
|
57
|
+
- 2
|
58
|
+
version: 0.1.2
|
59
|
+
type: :runtime
|
60
|
+
version_requirements: *id003
|
61
|
+
description: Sinatra can be aynchronous and provide WebSockets using Thin and Skinny.
|
62
|
+
email: sj26@sj26.com
|
63
|
+
executables: []
|
64
|
+
|
65
|
+
extensions: []
|
66
|
+
|
67
|
+
extra_rdoc_files:
|
68
|
+
- LICENSE
|
69
|
+
- README.md
|
70
|
+
files:
|
71
|
+
- .gitignore
|
72
|
+
- LICENSE
|
73
|
+
- README.md
|
74
|
+
- Rakefile
|
75
|
+
- VERSION
|
76
|
+
- examples/async.ru
|
77
|
+
- examples/chat.ru
|
78
|
+
- examples/delayed.ru
|
79
|
+
- examples/echo.ru
|
80
|
+
- lib/sinatra-diet.rb
|
81
|
+
- lib/sinatra/async.rb
|
82
|
+
- lib/sinatra/websocket.rb
|
83
|
+
has_rdoc: true
|
84
|
+
homepage: http://github.com/sj26/sinatra-diet
|
85
|
+
licenses: []
|
86
|
+
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options:
|
89
|
+
- --charset=UTF-8
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
segments:
|
98
|
+
- 0
|
99
|
+
version: "0"
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
segments:
|
106
|
+
- 0
|
107
|
+
version: "0"
|
108
|
+
requirements: []
|
109
|
+
|
110
|
+
rubyforge_project:
|
111
|
+
rubygems_version: 1.3.7
|
112
|
+
signing_key:
|
113
|
+
specification_version: 3
|
114
|
+
summary: Sinatra on a Diet gets Thin and Skinny
|
115
|
+
test_files: []
|
116
|
+
|