drunkmonkey 0.0.1
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 +7 -0
- data/.gitignore +17 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +87 -0
- data/Rakefile +1 -0
- data/drunkmonkey.gemspec +31 -0
- data/lib/drunkmonkey/transport.rb +157 -0
- data/lib/drunkmonkey/version.rb +3 -0
- data/lib/drunkmonkey.rb +86 -0
- data/sample/app.rb +26 -0
- data/sample/public/index.html +54 -0
- data/sample/public/portal.js +1860 -0
- metadata +130 -0
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
data/Gemfile
ADDED
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"
|
data/drunkmonkey.gemspec
ADDED
@@ -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
|
data/lib/drunkmonkey.rb
ADDED
@@ -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>
|