sinatra-diet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
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.
@@ -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
@@ -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
@@ -0,0 +1,15 @@
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
+ 'Hello, world!'
11
+ end
12
+ end
13
+ end
14
+
15
+ run Application
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
1
+ require 'sinatra/async'
2
+ require 'sinatra/websocket'
@@ -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
+