redom 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.
- data/LICENSE +19 -0
- data/README.md +126 -0
- data/bin/redom +83 -0
- data/lib/redom.rb +94 -0
- data/lib/redom/connection.rb +96 -0
- data/lib/redom/dispatcher.rb +114 -0
- data/lib/redom/proxy.rb +67 -0
- data/lib/redom/runtime.rb +252 -0
- data/lib/redom/task.rb +52 -0
- data/lib/redom/thread_pool.rb +27 -0
- data/lib/redom/utils.rb +21 -0
- data/lib/redom/version.rb +3 -0
- data/lib/redom/worker.rb +34 -0
- data/redom.gemspec +20 -0
- metadata +71 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (C) 2012 by Yi Hu
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
DJS Tutorial
|
2
|
+
|
3
|
+
DJS is a distributed object library for Server-WebBrowser communication. This tutorial shows how to create a web application using DJS.
|
4
|
+
|
5
|
+
===============
|
6
|
+
Getting started
|
7
|
+
===============
|
8
|
+
|
9
|
+
- Creating a DJS connection class
|
10
|
+
|
11
|
+
A DJS connection class is where you write the scripts to manipulate the browser-side objects from server. The DJS connection class must be subclass of 'DJS::DJSConnection'. An instance of DJS connection class will be created when the connection between browser and DJS server is established. There are four methods in 'DJS::DJSConnection' that can be overridden to tell DJS what to do.
|
12
|
+
|
13
|
+
on_open() - Called when the connection is established.
|
14
|
+
on_error(err) - Called when an browser-side error occurs.
|
15
|
+
on_message(msg) - Called when an message is received from browser.(not implemented)
|
16
|
+
on_close() - Called when the connection is shutdown.(not implemented)
|
17
|
+
|
18
|
+
Here is an example:
|
19
|
+
#
|
20
|
+
class MyConnection < DJS::Connection
|
21
|
+
def on_open
|
22
|
+
window.alert "Hello world!"
|
23
|
+
end
|
24
|
+
def on_error(error)
|
25
|
+
p error
|
26
|
+
end
|
27
|
+
end
|
28
|
+
#
|
29
|
+
|
30
|
+
- Starting DJS server
|
31
|
+
|
32
|
+
After a DJS connection class is defined, you can start DJS server using 'DJS::start(klass)' method. Your DJS connection class must be passed as the argument.
|
33
|
+
#
|
34
|
+
DJS.start(MyConnection, {:host => 127.0.0.1, :port => 8080})
|
35
|
+
#
|
36
|
+
|
37
|
+
- Adding DJS library to HTML
|
38
|
+
|
39
|
+
Add DJS JavaScript library 'djs.js' in your HTML. And call 'djs.start(host)' to connect to a DJS server.
|
40
|
+
#
|
41
|
+
<script type="text/javascript" src="djs.js"></script>
|
42
|
+
<script type="text/javascript">
|
43
|
+
window.onload = function() {
|
44
|
+
djs.start("ws://127.0.0.1:8080");
|
45
|
+
}
|
46
|
+
</script>
|
47
|
+
#
|
48
|
+
|
49
|
+
- Stopping DJS server
|
50
|
+
|
51
|
+
Use 'DJS.stop' to stop a DJS server.
|
52
|
+
#
|
53
|
+
DJS.stop
|
54
|
+
#
|
55
|
+
|
56
|
+
========
|
57
|
+
API Docs
|
58
|
+
========
|
59
|
+
|
60
|
+
* Module: DJS
|
61
|
+
|
62
|
+
- DJS.start(klass, opts = {})
|
63
|
+
|
64
|
+
Start DJS server.
|
65
|
+
|
66
|
+
Parameters:
|
67
|
+
klass (Class) - A class that extends DJS::Connection
|
68
|
+
opts (Hash) - Options. Default values are as below:
|
69
|
+
:buff_size => 200 - Messages stored before synchronization with browser.
|
70
|
+
|
71
|
+
- DJS.stop
|
72
|
+
|
73
|
+
Stop DJS server.
|
74
|
+
|
75
|
+
- DJS.connections
|
76
|
+
|
77
|
+
Return all DJS connection instances that the DJS server is holding currently.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
(DJS::Connections) - An DJS::Connections instance that contains all DJS::Connection instances the DJS server is holding currently.
|
81
|
+
|
82
|
+
Examples:
|
83
|
+
DJS.connections.do_something
|
84
|
+
|
85
|
+
|
86
|
+
* Class: DJS::Connection
|
87
|
+
|
88
|
+
- sync
|
89
|
+
|
90
|
+
Synchronize with browser immediately.
|
91
|
+
|
92
|
+
- window
|
93
|
+
|
94
|
+
A reference for browser-side object 'window'.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
(DJS::ProxyObject) - A DJS::ProxyObject that references to browser-side object 'window'.
|
98
|
+
|
99
|
+
Examples:
|
100
|
+
window.alert "Hello world."
|
101
|
+
|
102
|
+
* Class: DJS:ProxyObject
|
103
|
+
|
104
|
+
- sync, to_s, to_f, to_i, to_a, to_h, to_b
|
105
|
+
|
106
|
+
Synchronize with browser immediately and return the true value of the referenced object.
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
(Object) - Primitive type if the referenced object is primitive, otherwise the DJS::ProxyObject itself.
|
110
|
+
|
111
|
+
Examples:
|
112
|
+
value = document.getElementById("text").value.sync
|
113
|
+
|
114
|
+
=====
|
115
|
+
Hints
|
116
|
+
=====
|
117
|
+
|
118
|
+
- Define a event handler as 'object.event_name = :event_handler_name'
|
119
|
+
Examples:
|
120
|
+
def button_click_handler(event)
|
121
|
+
button = event.srcElement
|
122
|
+
end
|
123
|
+
document.getElementById("button").onclick = :button_click_handler
|
124
|
+
|
125
|
+
|
126
|
+
|
data/bin/redom
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'em-websocket'
|
4
|
+
require 'optparse'
|
5
|
+
require 'redom'
|
6
|
+
|
7
|
+
options = {
|
8
|
+
:host => '0.0.0.0',
|
9
|
+
:port => 8080
|
10
|
+
}
|
11
|
+
|
12
|
+
parser = OptionParser.new { |opts|
|
13
|
+
opts.banner = "Usage: redom [options] app"
|
14
|
+
|
15
|
+
opts.separator ""
|
16
|
+
opts.separator "Server options:"
|
17
|
+
|
18
|
+
opts.on("-h", "--host HOST", "bind to HOST address (default: #{options[:host]})") { |host| options[:host] = host }
|
19
|
+
opts.on("-p", "--port PORT", "use PORT (default: #{options[:port]})") { |port| options[:port] = port.to_i }
|
20
|
+
|
21
|
+
opts.separator ""
|
22
|
+
opts.separator "Secure options:"
|
23
|
+
|
24
|
+
opts.on( "--secure", "accept secure wss:// connections") { options[:secure] = true }
|
25
|
+
opts.on( "--private-key-file PATH", "path to private key") { |path|
|
26
|
+
if tls = options[:tls_options]
|
27
|
+
tls[:private_key_file] = path
|
28
|
+
else
|
29
|
+
options[:tls_options] = { :private_key_file => path }
|
30
|
+
end
|
31
|
+
}
|
32
|
+
opts.on( "--cert-chain-file PATH", "path to certificate") { |path|
|
33
|
+
if tls = options[:tls_options]
|
34
|
+
tls[:cert_chain_file] = path
|
35
|
+
else
|
36
|
+
options[:tls_options] = { :cert_chain_file => path }
|
37
|
+
end
|
38
|
+
}
|
39
|
+
opts.on( "--secure-proxy", "running behind an SSL/Terminator") { options[:secure_proxy] = true }
|
40
|
+
|
41
|
+
opts.separator ""
|
42
|
+
opts.separator "Common options:"
|
43
|
+
|
44
|
+
opts.on( "--worker NUM", "number of worker thread (default: 5)") { |num| options[:worker] = num.to_i }
|
45
|
+
opts.on( "--buff-size NUM", "size of packed RPC message before sync") { |num| options[:buff_size] = num.to_i }
|
46
|
+
opts.on( "--log FILE", "path to log (default: STDOUT)") { |path| options[:log] = path }
|
47
|
+
opts.on( "--log-level fatal|error|warn|info|debug", "log level (default: error)") { |lv| options[:log_level] = lv }
|
48
|
+
opts.on_tail( "--help", "show this message") { puts opts; exit }
|
49
|
+
opts.on_tail( "--version", "show version") { puts Redom::VERSION; exit }
|
50
|
+
}
|
51
|
+
|
52
|
+
parser.parse ARGV
|
53
|
+
|
54
|
+
|
55
|
+
if ARGV.length > 0
|
56
|
+
#$LOAD_PATH << pwd unless $LOAD_PATH.include?(pwd)
|
57
|
+
require Dir.pwd + '/' + ARGV[-1]
|
58
|
+
else
|
59
|
+
puts 'No Redom app file specified.'
|
60
|
+
exit
|
61
|
+
end
|
62
|
+
|
63
|
+
Redom.start options
|
64
|
+
|
65
|
+
EventMachine.run do
|
66
|
+
EventMachine::WebSocket.start(options) do |ws|
|
67
|
+
ws.onopen {
|
68
|
+
Redom.on_open ws
|
69
|
+
}
|
70
|
+
|
71
|
+
ws.onmessage { |msg|
|
72
|
+
Redom.on_message ws, msg
|
73
|
+
}
|
74
|
+
|
75
|
+
ws.onclose {
|
76
|
+
Redom.on_close ws
|
77
|
+
}
|
78
|
+
|
79
|
+
ws.onerror { |error|
|
80
|
+
Redom.on_error ws, error
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
data/lib/redom.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'fiber'
|
2
|
+
require 'json'
|
3
|
+
require 'logger'
|
4
|
+
require 'thread'
|
5
|
+
|
6
|
+
$LOAD_PATH << File.dirname(__FILE__) unless $LOAD_PATH.include?(File.dirname(__FILE__))
|
7
|
+
%w[
|
8
|
+
utils connection dispatcher proxy task worker thread_pool runtime version
|
9
|
+
].each { |file|
|
10
|
+
require "redom/#{file}"
|
11
|
+
}
|
12
|
+
|
13
|
+
module Redom
|
14
|
+
DEFAULT_OPTIONS = {
|
15
|
+
:log => STDOUT,
|
16
|
+
:log_level => 'error',
|
17
|
+
:worker => 5,
|
18
|
+
:buff_size => 200
|
19
|
+
}
|
20
|
+
|
21
|
+
REQ_HANDSHAKE = 0
|
22
|
+
REQ_METHOD_INVOCATION = 1
|
23
|
+
REQ_PROXY_RESULT = 2
|
24
|
+
|
25
|
+
IDX_REQUEST_TYPE = 0
|
26
|
+
# REQ_HANDSHAKE
|
27
|
+
IDX_CONNECTION_CLASS = 1
|
28
|
+
# REQ_METHOD_INVOCATION
|
29
|
+
IDX_METHOD_NAME = 1
|
30
|
+
IDX_ARGUMENTS = 2
|
31
|
+
# REQ_PROXY_RESULT
|
32
|
+
IDX_TASK_ID = 1
|
33
|
+
IDX_PROXY_RESULT = 2
|
34
|
+
|
35
|
+
TYPE_UNDEFINED = 0
|
36
|
+
TYPE_PROXY = 1
|
37
|
+
TYPE_ARRAY = 2
|
38
|
+
TYPE_ERROR = 3
|
39
|
+
TYPE_METHOD = 4
|
40
|
+
|
41
|
+
T_INFO_METHOD_NAME = 0
|
42
|
+
T_INFO_ARGUMENTS = 1
|
43
|
+
T_INFO_BLOCK = 2
|
44
|
+
|
45
|
+
P_INFO_OID = 0
|
46
|
+
P_INFO_RCVR = 1
|
47
|
+
P_INFO_NAME = 2
|
48
|
+
P_INFO_ARGS = 3
|
49
|
+
|
50
|
+
class << self
|
51
|
+
# Start Redom dispatcher
|
52
|
+
# @param [Hash] opts Options
|
53
|
+
def start(opts = {})
|
54
|
+
opts = DEFAULT_OPTIONS.merge(opts)
|
55
|
+
logger = Logger.new(opts[:log])
|
56
|
+
logger.level = case opts[:log_level].downcase
|
57
|
+
when 'fatal'
|
58
|
+
Logger::FATAL
|
59
|
+
when 'error'
|
60
|
+
Logger::ERROR
|
61
|
+
when 'warn'
|
62
|
+
Logger::WARN
|
63
|
+
when 'info'
|
64
|
+
Logger::INFO
|
65
|
+
when 'debug'
|
66
|
+
Logger::DEBUG
|
67
|
+
else
|
68
|
+
Logger::FATAL
|
69
|
+
end
|
70
|
+
Utils.logger = logger
|
71
|
+
Utils.dispatcher = @@dispatcher = Dispatcher.new(opts)
|
72
|
+
end
|
73
|
+
|
74
|
+
def stop
|
75
|
+
@@dispatcher.stop
|
76
|
+
end
|
77
|
+
|
78
|
+
def on_open(ws)
|
79
|
+
@@dispatcher.on_open(ws)
|
80
|
+
end
|
81
|
+
|
82
|
+
def on_message(ws, msg)
|
83
|
+
@@dispatcher.on_message(ws, msg)
|
84
|
+
end
|
85
|
+
|
86
|
+
def on_close(ws)
|
87
|
+
@@dispatcher.on_close(ws)
|
88
|
+
end
|
89
|
+
|
90
|
+
def on_error(ws, err)
|
91
|
+
@@dispatcher.on_error(ws, err)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Redom
|
2
|
+
module Connection
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
# Default event handlers
|
6
|
+
def on_open; end
|
7
|
+
def on_close; end
|
8
|
+
def on_error(err); end
|
9
|
+
|
10
|
+
# Proxy of browser objects
|
11
|
+
def window
|
12
|
+
Proxy.new(self, [:window], {:backtrace => caller[0]})
|
13
|
+
end
|
14
|
+
def document
|
15
|
+
Proxy.new(self, [:document], {:backtrace => caller[0]})
|
16
|
+
end
|
17
|
+
|
18
|
+
def method_missing(name, *args, &blck)
|
19
|
+
window.method_missing(name, *args, &blck)
|
20
|
+
end
|
21
|
+
|
22
|
+
def connections(filter = self.class)
|
23
|
+
_dispatcher.connections(filter)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Initialization
|
27
|
+
def _init(ws, opts)
|
28
|
+
@ws = ws
|
29
|
+
@proxies = Hash.new
|
30
|
+
@buff_size = opts[:buff_size]
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def _bulk(proxy)
|
35
|
+
fid = _fid
|
36
|
+
stack = @proxies[fid] || @proxies[fid] = Hash.new
|
37
|
+
stack[proxy._info[P_INFO_OID]] = proxy
|
38
|
+
if stack.size == @buff_size
|
39
|
+
sync
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def sync(obj = nil)
|
44
|
+
obj = _fid if obj == self
|
45
|
+
if Connection === obj
|
46
|
+
_dispatcher.run_task(obj, :sync, [_fid], nil)
|
47
|
+
return
|
48
|
+
end
|
49
|
+
|
50
|
+
fid = obj ? obj : _fid
|
51
|
+
stack = @proxies[fid]
|
52
|
+
return if !stack || stack.empty?
|
53
|
+
|
54
|
+
msg = []
|
55
|
+
stack.each { |k, v|
|
56
|
+
msg << v._info
|
57
|
+
}
|
58
|
+
|
59
|
+
rsp = Fiber.yield(msg)
|
60
|
+
|
61
|
+
error = nil
|
62
|
+
while result = rsp.shift
|
63
|
+
proxy = stack[result[0]]
|
64
|
+
if Array === result[1]
|
65
|
+
case result[1][0]
|
66
|
+
when TYPE_UNDEFINED
|
67
|
+
# TODO
|
68
|
+
when TYPE_PROXY
|
69
|
+
proxy._origin proxy
|
70
|
+
when TYPE_ERROR
|
71
|
+
# TODO
|
72
|
+
end
|
73
|
+
else
|
74
|
+
proxy._origin result[1]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
stack.clear
|
79
|
+
|
80
|
+
on_error(error) if error
|
81
|
+
end
|
82
|
+
|
83
|
+
def _cid
|
84
|
+
@ws.__id__
|
85
|
+
end
|
86
|
+
|
87
|
+
def _fid
|
88
|
+
"F#{Fiber.current.__id__}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def _send(msg)
|
92
|
+
_logger.debug "Message sent. ID='#{@ws.__id__}'\n#{msg}"
|
93
|
+
@ws.send msg
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Redom
|
2
|
+
class Dispatcher
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
def initialize(opts)
|
6
|
+
if find_all_connection_classes.size == 0
|
7
|
+
_logger.warn 'No Redom::Connection class defined.'
|
8
|
+
end
|
9
|
+
|
10
|
+
@opts = opts
|
11
|
+
@conns = Hash.new
|
12
|
+
@tasks = Hash.new
|
13
|
+
@pool = ThreadPool.new(@opts)
|
14
|
+
@pool.start
|
15
|
+
end
|
16
|
+
|
17
|
+
def stop
|
18
|
+
@pool.stop
|
19
|
+
end
|
20
|
+
|
21
|
+
def run_task(conn, name, args, blck)
|
22
|
+
task = Task.new(conn, @pool.worker, [name, args, blck])
|
23
|
+
@tasks[task._id] = task
|
24
|
+
task.run
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_open(ws)
|
28
|
+
_logger.debug "Connection established. ID='#{ws.__id__}'"
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_message(ws, msg)
|
32
|
+
_logger.debug "Message received. ID='#{ws.__id__}'\n#{msg}"
|
33
|
+
req = JSON.parse(msg)
|
34
|
+
case req[IDX_REQUEST_TYPE]
|
35
|
+
when REQ_HANDSHAKE
|
36
|
+
if cls = @conn_cls[req[IDX_CONNECTION_CLASS]]
|
37
|
+
conn = cls.new._init(ws, @opts)
|
38
|
+
@conns[ws.__id__] = conn
|
39
|
+
task = Task.new(conn, @pool.worker, [:on_open, []])
|
40
|
+
@tasks[task._id] = task
|
41
|
+
task.run
|
42
|
+
else
|
43
|
+
_logger.error "Undefined Redom::Connection class '#{req[IDX_CONNECTION_CLASS]}'."
|
44
|
+
end
|
45
|
+
when REQ_METHOD_INVOCATION
|
46
|
+
if conn = @conns[ws.__id__]
|
47
|
+
req[IDX_ARGUMENTS].map! { |arg|
|
48
|
+
if Array === arg
|
49
|
+
case arg[0]
|
50
|
+
when TYPE_PROXY
|
51
|
+
arg = Proxy.new(conn, [arg[1]])
|
52
|
+
when TYPE_ARRAY
|
53
|
+
arg = arg[1]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
arg
|
57
|
+
}
|
58
|
+
task = Task.new(conn, @pool.worker, req[IDX_METHOD_NAME..-1])
|
59
|
+
@tasks[task._id] = task
|
60
|
+
task.run
|
61
|
+
else
|
62
|
+
_logger.error "Connection missing. ID='#{ws.__id__}'"
|
63
|
+
end
|
64
|
+
when REQ_PROXY_RESULT
|
65
|
+
if task = @tasks[req[IDX_TASK_ID]]
|
66
|
+
task.run req[IDX_PROXY_RESULT]
|
67
|
+
else
|
68
|
+
_logger.error "Task missing. ID='#{req[IDX_TASK_ID]}'"
|
69
|
+
end
|
70
|
+
else
|
71
|
+
_logger.error "Undefined request type '#{req[IDX_REQUEST_TYPE]}'."
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def on_close(ws)
|
76
|
+
_logger.debug "Connection closed. ID='#{ws.__id__}'"
|
77
|
+
if conn = @conns[ws.__id__]
|
78
|
+
task = Task.new(conn, @pool.worker, [:on_close, []])
|
79
|
+
@tasks[task._id] = task
|
80
|
+
task.run
|
81
|
+
@conns.delete ws.__id__
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def on_error(ws, err)
|
86
|
+
_logger.debug "Error occured. ID='#{ws.__id__}'"
|
87
|
+
end
|
88
|
+
|
89
|
+
def delete_task(task_id)
|
90
|
+
@tasks.delete task_id
|
91
|
+
end
|
92
|
+
|
93
|
+
def connections(filter = nil)
|
94
|
+
if filter
|
95
|
+
@conns.values.select { |conn|
|
96
|
+
filter === conn
|
97
|
+
}
|
98
|
+
else
|
99
|
+
@conns.values
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def find_all_connection_classes
|
106
|
+
@conn_cls = Hash.new
|
107
|
+
ObjectSpace.each_object(Class).select { |cls|
|
108
|
+
cls < Connection
|
109
|
+
}.each { |cls|
|
110
|
+
@conn_cls[cls.name] = cls
|
111
|
+
}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/lib/redom/proxy.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
module Redom
|
2
|
+
class Proxy
|
3
|
+
def initialize(conn, info, extra = {})
|
4
|
+
@conn = conn
|
5
|
+
@info = info
|
6
|
+
@info[P_INFO_OID] = __id__ unless @info[P_INFO_OID]
|
7
|
+
|
8
|
+
@solved = false
|
9
|
+
@origin = nil
|
10
|
+
end
|
11
|
+
attr_reader :origin
|
12
|
+
|
13
|
+
def method_missing(name, *args, &blck)
|
14
|
+
return @origin.send(name, *args, &block) if @solved && !@origin.is_a?(Proxy)
|
15
|
+
|
16
|
+
if blck
|
17
|
+
@conn.sync
|
18
|
+
return @origin.send(name, *args, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
proxy = Proxy.new(@conn, [nil, _info[P_INFO_OID], name, _arg(args)], {:backtrace => caller[0]})
|
22
|
+
@conn._bulk proxy
|
23
|
+
proxy
|
24
|
+
end
|
25
|
+
|
26
|
+
def sync
|
27
|
+
unless @solved
|
28
|
+
@conn.sync
|
29
|
+
end
|
30
|
+
@origin
|
31
|
+
end
|
32
|
+
|
33
|
+
def _info
|
34
|
+
@info
|
35
|
+
end
|
36
|
+
|
37
|
+
def _origin(org)
|
38
|
+
@solved = true
|
39
|
+
@origin = org
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def _arg(arg)
|
45
|
+
case arg
|
46
|
+
when Proxy
|
47
|
+
[TYPE_PROXY, arg._info[P_INFO_OID]]
|
48
|
+
when Array
|
49
|
+
arg.map! { |item|
|
50
|
+
_arg(item)
|
51
|
+
}
|
52
|
+
[TYPE_ARRAY, arg]
|
53
|
+
when Hash
|
54
|
+
arg.each { |k, v|
|
55
|
+
arg[k] = _arg(v)
|
56
|
+
}
|
57
|
+
arg
|
58
|
+
when Method
|
59
|
+
[TYPE_METHOD, arg.name]
|
60
|
+
when Symbol
|
61
|
+
[TYPE_METHOD, arg.to_s]
|
62
|
+
else
|
63
|
+
arg
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
module Redom
|
2
|
+
class << self
|
3
|
+
def runtime
|
4
|
+
<<EOS
|
5
|
+
(function() {
|
6
|
+
var
|
7
|
+
TYPE_UNDEFINED = 0,
|
8
|
+
TYPE_PROXY = 1,
|
9
|
+
TYPE_ARRAY = 2,
|
10
|
+
TYPE_ERROR = 3,
|
11
|
+
TYPE_METHOD = 4
|
12
|
+
|
13
|
+
REQ_HANDSHAKE = 0,
|
14
|
+
REQ_METHOD_INVOCATION = 1,
|
15
|
+
REQ_PROXY_RESULT = 2,
|
16
|
+
|
17
|
+
P_INFO_OID = 0,
|
18
|
+
P_INFO_RCVR = 1,
|
19
|
+
P_INFO_NAME = 2,
|
20
|
+
P_INFO_ARGS = 3;
|
21
|
+
|
22
|
+
var Redom = function(server) {
|
23
|
+
var ws = new WebSocket(server);
|
24
|
+
conn = "Connection",
|
25
|
+
refs = {
|
26
|
+
seq: 0,
|
27
|
+
hash: {},
|
28
|
+
put: function() {
|
29
|
+
var key;
|
30
|
+
if (arguments.length == 2) {
|
31
|
+
key = arguments[0];
|
32
|
+
this.hash[key] = arguments[1];
|
33
|
+
} else if (arguments.length == 1) {
|
34
|
+
key = "R" + (++this.seq);
|
35
|
+
this.hash[key] = arguments[0];
|
36
|
+
} else {
|
37
|
+
console.log("ERROR: wrong number of arguments!");
|
38
|
+
}
|
39
|
+
return key;
|
40
|
+
},
|
41
|
+
get: function(key) {
|
42
|
+
return this.hash[key];
|
43
|
+
}
|
44
|
+
},
|
45
|
+
tasks = [];
|
46
|
+
|
47
|
+
// Initialize refs
|
48
|
+
refs.put("window", window);
|
49
|
+
refs.put("document", document);
|
50
|
+
refs.put("history", history);
|
51
|
+
refs.put("navigator", navigator);
|
52
|
+
refs.put("location", location);
|
53
|
+
|
54
|
+
// Initialize WebSocket event handlers
|
55
|
+
function open(connectionClassName) {
|
56
|
+
ws.onopen = function() {
|
57
|
+
conn = connectionClassName;
|
58
|
+
ws.send(serialize([REQ_HANDSHAKE, conn]));
|
59
|
+
console.log("WebSocket opened.");
|
60
|
+
};
|
61
|
+
ws.onclose = function() {
|
62
|
+
console.log("WebSocket closed.");
|
63
|
+
};
|
64
|
+
ws.onerror = function(error) {
|
65
|
+
console.log(error);
|
66
|
+
};
|
67
|
+
ws.onmessage = function(event) {
|
68
|
+
var data = unserialize(event.data);
|
69
|
+
var tid = data[0];
|
70
|
+
var proxies = data[1];
|
71
|
+
var queue = tasks[tid];
|
72
|
+
if (!queue) {
|
73
|
+
queue = [];
|
74
|
+
tasks[tid] = queue;
|
75
|
+
}
|
76
|
+
for (var i = 0; i < proxies.length; i++) {
|
77
|
+
queue.push(proxies[i]);
|
78
|
+
}
|
79
|
+
try {
|
80
|
+
process(tid);
|
81
|
+
} catch (e) {
|
82
|
+
console.log(e);
|
83
|
+
}
|
84
|
+
};
|
85
|
+
};
|
86
|
+
|
87
|
+
// Close this WebSocket connection
|
88
|
+
function close() {
|
89
|
+
ws.close();
|
90
|
+
}
|
91
|
+
|
92
|
+
// Serialize JavaScript object into message
|
93
|
+
function serialize(data) {
|
94
|
+
return JSON.stringify(data);
|
95
|
+
};
|
96
|
+
// Unserialize message into JavaScript object
|
97
|
+
function unserialize(msg) {
|
98
|
+
return JSON.parse(msg);
|
99
|
+
};
|
100
|
+
|
101
|
+
// Restore arguments
|
102
|
+
function restore(args) {
|
103
|
+
if (args instanceof Array) {
|
104
|
+
switch(args[0]) {
|
105
|
+
case TYPE_ARRAY:
|
106
|
+
for (var i = 0; i < args[1].length; i++) {
|
107
|
+
args[1][i] = restore(args[1][i]);
|
108
|
+
}
|
109
|
+
return args[1];
|
110
|
+
break;
|
111
|
+
case TYPE_PROXY:
|
112
|
+
return refs.get(args[1]);
|
113
|
+
break;
|
114
|
+
case TYPE_METHOD:
|
115
|
+
return (function(name) {
|
116
|
+
if (window[name] && typeof(window[name]) == 'function') {
|
117
|
+
return function() {
|
118
|
+
window[name];
|
119
|
+
};
|
120
|
+
} else {
|
121
|
+
return function() {
|
122
|
+
var type, args = [];
|
123
|
+
for (var i = 0; i < arguments.length; i++) {
|
124
|
+
switch(type = typeOf(arguments[i])) {
|
125
|
+
case null:
|
126
|
+
args.push(arguments[i]);
|
127
|
+
break;
|
128
|
+
case TYPE_PROXY:
|
129
|
+
case TYPE_ARRAY:
|
130
|
+
args.push([type, refs.put(arguments[i])]);
|
131
|
+
break;
|
132
|
+
}
|
133
|
+
}
|
134
|
+
ws.send(serialize([REQ_METHOD_INVOCATION, name, args]));
|
135
|
+
}
|
136
|
+
}
|
137
|
+
})(args[1]);
|
138
|
+
break;
|
139
|
+
}
|
140
|
+
} else {
|
141
|
+
return args;
|
142
|
+
}
|
143
|
+
};
|
144
|
+
|
145
|
+
// Evaluation proxies from server
|
146
|
+
function process(tid) {
|
147
|
+
var proxy, oid, name, args, result, type,
|
148
|
+
rsps = [],
|
149
|
+
queue = tasks[tid];
|
150
|
+
while (proxy = queue.shift()) {
|
151
|
+
oid = proxy[P_INFO_OID];
|
152
|
+
rcvr = refs.get(proxy[P_INFO_RCVR]);
|
153
|
+
name = proxy[P_INFO_NAME];
|
154
|
+
args = restore(proxy[P_INFO_ARGS]);
|
155
|
+
|
156
|
+
if (rcvr) {
|
157
|
+
result = execute(rcvr, name, args);
|
158
|
+
if (result == undefined) {
|
159
|
+
rsps.push([oid, [TYPE_UNDEFINED]]);
|
160
|
+
ws.send(serialize([REQ_PROXY_RESULT, tid, rsps]));
|
161
|
+
return;
|
162
|
+
} else {
|
163
|
+
refs.put(oid, result);
|
164
|
+
switch (type = typeOf(result)) {
|
165
|
+
case null:
|
166
|
+
rsps.push([oid, result]);
|
167
|
+
break;
|
168
|
+
case TYPE_PROXY:
|
169
|
+
case TYPE_ARRAY:
|
170
|
+
rsps.push([oid, [type]]);
|
171
|
+
break;
|
172
|
+
};
|
173
|
+
}
|
174
|
+
} else {
|
175
|
+
rsps.push([oid, TYPE_ERROR, "no such object. ID='" + proxy[P_INFO_RCVR] + "'."]);
|
176
|
+
ws.send(serialize([REQ_PROXY_RESULT, tid, rsps]));
|
177
|
+
return;
|
178
|
+
}
|
179
|
+
}
|
180
|
+
ws.send(serialize([REQ_PROXY_RESULT, tid, rsps]));
|
181
|
+
};
|
182
|
+
|
183
|
+
// Type of object
|
184
|
+
function typeOf(obj) {
|
185
|
+
switch(typeof obj) {
|
186
|
+
case "string":
|
187
|
+
case "number":
|
188
|
+
case "boolean":
|
189
|
+
case "undefined":
|
190
|
+
return null;
|
191
|
+
case "object":
|
192
|
+
if (obj == null) {
|
193
|
+
return null;
|
194
|
+
} else if (obj instanceof Array) {
|
195
|
+
return TYPE_ARRAY;
|
196
|
+
} else {
|
197
|
+
return TYPE_PROXY;
|
198
|
+
}
|
199
|
+
case "function":
|
200
|
+
return TYPE_METHOD;
|
201
|
+
}
|
202
|
+
return TYPE_UNDEFINED;
|
203
|
+
}
|
204
|
+
|
205
|
+
// Method invocation
|
206
|
+
function execute(rcvr, name, args) {
|
207
|
+
var result;
|
208
|
+
if (name.match(/^(.+)=$/)) {
|
209
|
+
name = RegExp.$1;
|
210
|
+
if (name == "[]") {
|
211
|
+
result = rcvr[args[0]] = args[1];
|
212
|
+
} else {
|
213
|
+
result = rcvr[name] = args[0];
|
214
|
+
}
|
215
|
+
} else {
|
216
|
+
if (name == "[]") {
|
217
|
+
result = rcvr[args[0]];
|
218
|
+
} else {
|
219
|
+
result = rcvr[name];
|
220
|
+
if (typeof result == "function") {
|
221
|
+
result = result.apply(rcvr, args);
|
222
|
+
}
|
223
|
+
}
|
224
|
+
}
|
225
|
+
return result;
|
226
|
+
};
|
227
|
+
|
228
|
+
return {
|
229
|
+
getServer: function() {
|
230
|
+
return server;
|
231
|
+
},
|
232
|
+
|
233
|
+
getConnection: function() {
|
234
|
+
return conn;
|
235
|
+
},
|
236
|
+
|
237
|
+
open: function(connectionClassName) {
|
238
|
+
open(connectionClassName);
|
239
|
+
},
|
240
|
+
|
241
|
+
close: function() {
|
242
|
+
close();
|
243
|
+
}
|
244
|
+
};
|
245
|
+
};
|
246
|
+
|
247
|
+
window.Redom = Redom;
|
248
|
+
})();
|
249
|
+
EOS
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
data/lib/redom/task.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Redom
|
2
|
+
class Task
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
def initialize(conn, worker, info)
|
6
|
+
@conn = conn
|
7
|
+
@worker = worker
|
8
|
+
@info = info
|
9
|
+
@results = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def run(proxy_result = nil)
|
13
|
+
@results << proxy_result if proxy_result
|
14
|
+
@worker.do_task self
|
15
|
+
end
|
16
|
+
|
17
|
+
def resume
|
18
|
+
if !@results.empty?
|
19
|
+
result = @fiber.resume(@results.shift)
|
20
|
+
else
|
21
|
+
mthd = @conn.method(@info[T_INFO_METHOD_NAME])
|
22
|
+
args = @info[T_INFO_ARGUMENTS]
|
23
|
+
blck = @info[T_INFO_BLOCK]
|
24
|
+
@fiber = Fiber.new do
|
25
|
+
if mthd
|
26
|
+
if blck
|
27
|
+
mthd.call(*args, &blck)
|
28
|
+
else
|
29
|
+
mthd.call(*args)
|
30
|
+
end
|
31
|
+
else
|
32
|
+
_logger.error "Method '#{@info[T_INFO_METHOD_NAME]}' is not defined in class '#{@conn.class}'."
|
33
|
+
@conn.method(:on_error).call("No such method '#{@info[T_INFO_METHOD_NAME]}'.")
|
34
|
+
end
|
35
|
+
@conn.sync
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
result = @fiber.resume
|
39
|
+
end
|
40
|
+
|
41
|
+
if result
|
42
|
+
@conn._send [_id, result].to_json
|
43
|
+
else
|
44
|
+
_dispatcher.delete_task _id
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def _id
|
49
|
+
"T#{__id__}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Redom
|
2
|
+
class ThreadPool
|
3
|
+
def initialize(opts)
|
4
|
+
@workers = Array.new
|
5
|
+
@idx = 0
|
6
|
+
opts[:worker].times {
|
7
|
+
@workers << Worker.new
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
def start
|
12
|
+
@workers.each { |worker|
|
13
|
+
worker.start
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def stop
|
18
|
+
@workers.each { |worker|
|
19
|
+
worker.stop
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def worker
|
24
|
+
@workers[(@idx += 1) == @workers.size ? @idx = 0 : @idx]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/redom/utils.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Redom
|
2
|
+
module Utils
|
3
|
+
@@logger = nil
|
4
|
+
|
5
|
+
def self.logger=(logger)
|
6
|
+
@@logger = logger
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.dispatcher=(dispatcher)
|
10
|
+
@@dispatcher = dispatcher
|
11
|
+
end
|
12
|
+
|
13
|
+
def _logger
|
14
|
+
@@logger
|
15
|
+
end
|
16
|
+
|
17
|
+
def _dispatcher
|
18
|
+
@@dispatcher
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/redom/worker.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Redom
|
2
|
+
class Worker
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@queue = Queue.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def start
|
10
|
+
@thread = Thread.start do
|
11
|
+
while task = @queue.pop
|
12
|
+
begin
|
13
|
+
task.resume
|
14
|
+
rescue
|
15
|
+
_logger.error "Task failed. ID='#{task.__id__}'\n"
|
16
|
+
_logger.error $!.message
|
17
|
+
$!.backtrace.each { |item|
|
18
|
+
_logger.error item
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop
|
26
|
+
@thread.kill
|
27
|
+
@queue.clear
|
28
|
+
end
|
29
|
+
|
30
|
+
def do_task(task)
|
31
|
+
@queue.push task
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/redom.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.expand_path('../lib/redom/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'redom'
|
5
|
+
s.version = Redom::VERSION
|
6
|
+
s.platform = Gem::Platform::RUBY
|
7
|
+
s.summary = 'A distributed object based server-centric web framework'
|
8
|
+
s.description = 'A distributed object based server-centric web framework.'
|
9
|
+
s.author = ['Yi Hu', 'Sasada Koichi']
|
10
|
+
s.email = 'future.azure@gmail.com'
|
11
|
+
s.homepage = 'https://github.com/future-azure/redom'
|
12
|
+
|
13
|
+
s.require_paths = ['lib']
|
14
|
+
s.files = ['README.md', 'LICENSE', 'redom.gemspec']
|
15
|
+
s.files += ['lib/redom.rb'] + Dir['lib/redom/**/*.rb']
|
16
|
+
s.files += Dir['bin/redom']
|
17
|
+
s.executables = ['redom']
|
18
|
+
|
19
|
+
s.add_dependency 'em-websocket', '>= 0.3.5'
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redom
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Yi Hu
|
9
|
+
- Sasada Koichi
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-12-10 00:00:00.000000000Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: em-websocket
|
17
|
+
requirement: &70124221424660 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.3.5
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *70124221424660
|
26
|
+
description: A distributed object based server-centric web framework.
|
27
|
+
email: future.azure@gmail.com
|
28
|
+
executables:
|
29
|
+
- redom
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- README.md
|
34
|
+
- LICENSE
|
35
|
+
- redom.gemspec
|
36
|
+
- lib/redom.rb
|
37
|
+
- lib/redom/connection.rb
|
38
|
+
- lib/redom/dispatcher.rb
|
39
|
+
- lib/redom/proxy.rb
|
40
|
+
- lib/redom/runtime.rb
|
41
|
+
- lib/redom/task.rb
|
42
|
+
- lib/redom/thread_pool.rb
|
43
|
+
- lib/redom/utils.rb
|
44
|
+
- lib/redom/version.rb
|
45
|
+
- lib/redom/worker.rb
|
46
|
+
- bin/redom
|
47
|
+
homepage: https://github.com/future-azure/redom
|
48
|
+
licenses: []
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ! '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 1.8.10
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: A distributed object based server-centric web framework
|
71
|
+
test_files: []
|