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 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>