drunkmonkey 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 19d6e3cc109628d8686e1994aa9e97f67c74e157
4
+ data.tar.gz: 105874a81de22fde196db492685c77f1a6938690
5
+ SHA512:
6
+ metadata.gz: 9be7bdf707ae8baabdf59e94d85f6b324efe4c76001c42ca0548159fb1a32129dd82c8fbc55cd4b86b4be96f16ddd1cb46c39111ff5dccba4e0acac1aecd2ce9
7
+ data.tar.gz: 68d289778433af7a80f56ad610df5104a4bd949d4ec6be1e78b5b882dc1196e1609b30ec91b3bb8ea2cdab4c38c2457e6f8abed0dbf443eada7903bca11f7f80
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in drunkmonkey.gemspec
4
+ gemspec
5
+
6
+ gem "puma"
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 minoritea
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # DrunkMonkey
2
+
3
+ DrunkMonkey is a rack middleware providing realtime two-way http communication with API for [Portal](https://github.com/flowersinthesand/portal/ "Portal").
4
+
5
+ ## Supported servers
6
+ You should use servers which supports Rack hijacking API, such as follows:
7
+ - Puma
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'drunkmonkey'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install drunkmonkey
22
+
23
+ ## Usage
24
+
25
+ Add DrunkMonkey::Builder to Rack application by **use** method.
26
+
27
+ Plain Rack:
28
+ ```ruby
29
+ app = Rack::Builder.new do
30
+ use DrunkMonkey::Middleware do
31
+ on :message do |socket, message|
32
+ socket.push "ECHO: #{message}"
33
+ end
34
+ end
35
+ end
36
+
37
+ run app
38
+ ```
39
+
40
+ Sinatra:
41
+ ```ruby
42
+ require "sinatra"
43
+
44
+ use DrunkMonkey::Middleware do
45
+ on :message do |socket, message|
46
+ socket.push "ECHO: #{message}"
47
+ end
48
+ end
49
+ ```
50
+ Note: the passed block will be executed once when *use* is called at first.
51
+
52
+ Then, include [Portal](https://github.com/flowersinthesand/portal/ "Portal") to HTML and initialize it as follows.
53
+
54
+ ```html
55
+ <script src="/portal.js"></script>
56
+ <script>
57
+ /*
58
+ * Following option is a hack to switch protocols automatically
59
+ * when the connection cannot be established.
60
+ */
61
+ var options = {
62
+ transports:["ws","longpollajax"],
63
+ reconnect:function(lastDelay,attempts){
64
+ if(options.transports.length > 1)
65
+ options.transports.shift();
66
+ return 2 * (lastDelay || 100);},
67
+ prepare:function(connect,disconnect,opts){
68
+ opts.transports = options.transports;connect();}
69
+ };
70
+
71
+ //Add above options and drunkmonkey's path; default is "/drunkmonkey".
72
+ var socket = portal.open("/drunkmonkey",options).on("open",function(){
73
+ //Add you event.
74
+ socket.send("Hello!");
75
+ });
76
+ </script>
77
+ ```
78
+
79
+ Note: in current version, supported transports by Drunkmonkey are websocket and long-poll ajax only.
80
+
81
+ ## Contributing
82
+
83
+ 1. Fork it
84
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
85
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
86
+ 4. Push to the branch (`git push origin my-new-feature`)
87
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'drunkmonkey/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "drunkmonkey"
8
+ spec.version = DrunkMonkey::VERSION
9
+ spec.authors = ["Minori Tokuda"]
10
+ spec.email = ["minorityland@gmail.jp"]
11
+ spec.description = <<-EOS
12
+ DrunkMonkey is a rack middleware providing realtime two-way http communication with API for Portal, a javascript messaging library.
13
+ It provides just two protocols currently; websocket and longpoll comet.
14
+ You can write code once for each protocols.
15
+ EOS
16
+ spec.summary = %q{DrunkMonkey is a rack middleware providing realtime two-way http communication with API for Portal.}
17
+ spec.homepage = "https://github.com/minoritea/drunkmonkey"
18
+ spec.license = "MIT"
19
+
20
+ spec.files = `git ls-files`.split($/)
21
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
22
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_dependency "celluloid", ">= 0.15"
26
+ spec.add_dependency "rack", ">= 1.5"
27
+ spec.add_dependency "websocket", ">= 1.1"
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.3"
30
+ spec.add_development_dependency "rake"
31
+ end
@@ -0,0 +1,157 @@
1
+ require "websocket"
2
+
3
+ module DrunkMonkey
4
+ module Transport
5
+ # Taken from https://github.com/simulacre/sinatra-websocket/
6
+ # Originally taken from skinny https://github.com/sj26/skinny and updated to support Firefox
7
+ def self.websocket? env
8
+ env['HTTP_CONNECTION'] && env['HTTP_UPGRADE'] &&
9
+ env['HTTP_CONNECTION'].split(',').map(&:strip).map(&:downcase).include?('upgrade') &&
10
+ env['HTTP_UPGRADE'].downcase == 'websocket'
11
+ end
12
+
13
+ def self.connection_from env, options
14
+ request = Rack::Request.new(env)
15
+ if websocket? env
16
+ WebSocket.resume request, **options
17
+ [500,{},[]]
18
+ else
19
+ body = Comet.resume request, **options
20
+ [200,{},[body]]
21
+ end
22
+ end
23
+
24
+ class Base
25
+ class << self
26
+ def resume request, **options
27
+ @sessions ||= {}
28
+
29
+ params = parse_params request
30
+ id = request.post? ? params["socket"] : params["id"]
31
+
32
+ session = @sessions[id]
33
+
34
+ return session if session
35
+ @sessions[id] = new **options
36
+ end
37
+
38
+ def parse_params request
39
+ if request.post?
40
+ input = request.env["rack.input"]
41
+ input.rewind
42
+ parameters = input.read.sub(/\Adata=/,"")
43
+ input.rewind
44
+ params = JSON.parse(parameters)
45
+ else
46
+ params = request.params
47
+ end
48
+ end
49
+ end
50
+
51
+ def initialize **options
52
+ @controller = Celluloid::Actor[options[:controller_name]]
53
+ @messages = []
54
+ end
55
+
56
+ def portal message
57
+ @i ||= 0
58
+ {type:"message", data:message, id:(@i+=1),reply:false}.to_json
59
+ end
60
+ end
61
+
62
+ class HijackAPINotFoundError < StandardError;end
63
+
64
+ class WebSocket < Base
65
+ include Celluloid
66
+
67
+ def self.resume request, **options
68
+ websocket = super
69
+ websocket.handle_connection request
70
+ end
71
+
72
+ def handle_connection request
73
+ handshake request
74
+ @controller.async.fire :open, Actor.current
75
+ loop do
76
+ Celluloid.sleep 0.001
77
+ upstream
78
+ downstream
79
+ return if @socket.closed?
80
+ end
81
+ end
82
+
83
+ def push message
84
+ @messages << message
85
+ end
86
+
87
+ private
88
+ def handshake request
89
+ env = request.env
90
+ raise HijackAPINotFoundError unless hijack = env["rack.hijack"]
91
+ hijack.call
92
+ @socket = env["rack.hijack_io"]
93
+ @handshake = ::WebSocket::Handshake::Server.new
94
+ @handshake.from_rack env
95
+ @socket.write @handshake.to_s
96
+ end
97
+
98
+ def upstream
99
+ buffer = ::WebSocket::Frame::Incoming::Server.new(version: @handshake.version)
100
+ buffer << @socket.read_nonblock(1024) rescue nil
101
+ while frame = buffer.next
102
+ case frame.type
103
+ when :text, :binary
104
+ data = JSON.parse(frame.data)
105
+ @controller.async.fire :message, Actor.current, data["data"]
106
+ end
107
+ end
108
+ end
109
+
110
+ def downstream
111
+ if message = @messages.shift
112
+ @socket.write(
113
+ ::WebSocket::Frame::Outgoing::Server.new(
114
+ data: portal(message), type: :text, version: @handshake.version))
115
+ end
116
+ rescue Errno::EPIPE => e
117
+ end
118
+ end
119
+
120
+ class Comet < Base
121
+ include Celluloid
122
+
123
+ def self.resume request, **options
124
+ comet = super
125
+ comet.handle_connection(request)
126
+ end
127
+
128
+ def handle_connection request
129
+ params = self.class.parse_params(request)
130
+ request.post? ? upstream(params) : downstream(params)
131
+ end
132
+
133
+ def push message
134
+ @messages << message
135
+ signal :pushed
136
+ end
137
+
138
+ private
139
+ def upstream params
140
+ @controller.async.fire :message, Actor.current, params["data"]
141
+ ""
142
+ end
143
+
144
+ def downstream params
145
+ if params["when"] == "open"
146
+ @controller.async.fire :open, Actor.current
147
+ ""
148
+ else
149
+ wait :pushed if @messages.empty?
150
+ message = @messages.shift
151
+ message ? portal(message) : ""
152
+ end
153
+ end
154
+ end
155
+
156
+ end
157
+ end
@@ -0,0 +1,3 @@
1
+ module DrunkMonkey
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,86 @@
1
+ require "drunkmonkey/version"
2
+ require "celluloid"
3
+ require "rack"
4
+ require "celluloid/autostart"
5
+ require "forwardable"
6
+ require "json"
7
+ require "drunkmonkey/transport"
8
+
9
+ module DrunkMonkey
10
+ class Controller
11
+ include Celluloid
12
+ def initialize name = :default_controller
13
+ @handlers = Hash.new
14
+ Actor[name] = Actor.current
15
+ end
16
+
17
+ def on event, &block
18
+ @handlers[event] ||= block
19
+ end
20
+
21
+ execute_block_on_receiver :on
22
+
23
+ def fire event, transport, message = nil
24
+ handler = @handlers[event]
25
+ handler.call transport, message if handler
26
+ end
27
+ end
28
+
29
+ class Builder < ::Rack::Builder
30
+ extend Forwardable
31
+
32
+ DEFAULT_OPTIONS = {
33
+ path: "/drunkmonkey",
34
+ controller_name: :default_controller
35
+ }.freeze
36
+
37
+ def_delegator :controller, :on
38
+
39
+ def controller
40
+ self.class.controller
41
+ end
42
+
43
+ def controller= instance
44
+ self.class.controller=instance
45
+ end
46
+
47
+ class << self
48
+ attr_accessor :controller
49
+ end
50
+
51
+ def initialize default_app = nil, **options , &block
52
+ options = DEFAULT_OPTIONS.merge(options)
53
+ self.controller ||= Controller.new(options[:controller_name])
54
+
55
+ super(default_app, &block)
56
+
57
+ map options[:path] do
58
+ run -> env do
59
+ Transport.connection_from env, options
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def self.middleware
66
+ Class.new do
67
+ class << self
68
+ attr_accessor :builder
69
+ end
70
+
71
+ def initialize app, **options, &block
72
+ if self.class.builder
73
+ self.class.builder.run app
74
+ else
75
+ self.class.builder = Builder.new app, **options, &block
76
+ end
77
+ end
78
+
79
+ def call env
80
+ self.class.builder.call env
81
+ end
82
+ end
83
+ end
84
+
85
+ Middleware = self.middleware
86
+ end
data/sample/app.rb ADDED
@@ -0,0 +1,26 @@
1
+ require "puma"
2
+ require "rack/handler/puma"
3
+ require "drunkmonkey"
4
+
5
+ sockets = {}
6
+ Rack::Handler::Puma.run(Rack::Builder.new{
7
+ root = File.dirname(__FILE__)
8
+ use DrunkMonkey::Middleware do
9
+ on :open do |socket|
10
+ sockets[socket] = ""
11
+ end
12
+
13
+ on :message do |socket,msg|
14
+ if sockets[socket].empty?
15
+ sockets[socket] = msg
16
+ socket.push "Welcome, #{msg}!"
17
+ else
18
+ name = sockets[socket]
19
+ sockets.each{|s,_|s.push "#{name}: #{msg}"}
20
+ end
21
+ end
22
+ end
23
+
24
+ use Rack::Static, urls:[""], index:"index.html", root:"#{root}/public"
25
+ run -> env { [404, {'Content-Type' => 'text/html'}, ['Not Found']] }
26
+ })
@@ -0,0 +1,54 @@
1
+ <html><head></head><body>
2
+ <div id="messages"></div>
3
+ <div>
4
+ <span id="on_login">My name is:</span>
5
+ <input id="message_box" type="text">
6
+ <button id="button">SEND</button>
7
+ </div>
8
+ <script src="/portal.js"></script>
9
+ <script>
10
+ window.onload = function(){
11
+ var options = {
12
+ transports:["ws","longpollajax"],
13
+ reconnect:function(lastDelay,attempts){
14
+ if(options.transports.length > 1)
15
+ options.transports.shift();
16
+ return 2 * (lastDelay || 100);},
17
+ prepare:function(connect,disconnect,opts){
18
+ opts.transports = options.transports;connect();}
19
+ };
20
+
21
+ var socket;
22
+ function openPortal(){
23
+ var socket = portal.open("/drunkmonkey",options)
24
+ .on("message",function(msg){
25
+ document.getElementById("messages").innerHTML += "<p>" + msg + "</p>";
26
+ });
27
+ return socket;
28
+ };
29
+
30
+ var socket;
31
+
32
+ function _send_message(){
33
+ var message_box = document.getElementById("message_box");
34
+ var message = message_box.value;
35
+ if (message.length > 0) {
36
+ message_box.value = "";
37
+ socket.send("message",message);
38
+ }
39
+ }
40
+
41
+ function send_message(){
42
+ if (socket == null) {
43
+ document.getElementById("on_login").innerHTML = "";
44
+ socket = openPortal();
45
+ _send_message();
46
+ } else {
47
+ _send_message();
48
+ }
49
+ };
50
+
51
+ document.getElementById("button").onclick = send_message;
52
+ };
53
+ </script>
54
+ </body><html>