redom 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|