clientside 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/__clientside_res__/clientside.js +77 -0
- data/lib/__clientside_res__/promise.min.js +1 -0
- data/lib/clientside.rb +154 -0
- metadata +50 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
var makeClientsideProxy = (function() {
|
2
|
+
var randomID = function() {
|
3
|
+
return Math.random() * Math.pow(10, 17);
|
4
|
+
};
|
5
|
+
|
6
|
+
var remoteInvoke = function(socket, obj, method, args) {
|
7
|
+
var request = {"receiver": obj, "method": method, "arguments": args};
|
8
|
+
var id = request.id = Date.now();
|
9
|
+
var promise = new Promise(function(resolve, reject) {
|
10
|
+
socket.pending[id] = [resolve, reject];
|
11
|
+
});
|
12
|
+
socket.send(JSON.stringify(request));
|
13
|
+
return promise;
|
14
|
+
};
|
15
|
+
|
16
|
+
var proxyMaker = function(socket, obj) {
|
17
|
+
var proto = {};
|
18
|
+
var proxy = Object.create(proto);
|
19
|
+
proxy.__clientside__ = true;
|
20
|
+
proxy.__clientside_id__ = obj.__clientside_id__;
|
21
|
+
for (var i = 0; i < obj.methods.length; i++) {
|
22
|
+
var methodName = obj.methods[i];
|
23
|
+
proto[methodName] = (function(closableName) {
|
24
|
+
return function() {
|
25
|
+
var args = Array.slice(arguments);
|
26
|
+
return remoteInvoke(socket, proxy, closableName, args);
|
27
|
+
};
|
28
|
+
})(methodName);
|
29
|
+
}
|
30
|
+
return proxy;
|
31
|
+
};
|
32
|
+
|
33
|
+
return proxyMaker;
|
34
|
+
})();
|
35
|
+
|
36
|
+
var makeClientsideSocket = (function() {
|
37
|
+
var proxify = function(socket, obj) {
|
38
|
+
if (obj instanceof Array) {
|
39
|
+
for (var i = 0; i < obj.length; i++) {
|
40
|
+
if (obj[i].__clientside__) {
|
41
|
+
obj[i] = makeClientsideProxy(socket, obj[i]);
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
else if (obj instanceof Object) {
|
46
|
+
for (key in obj) {
|
47
|
+
if (obj[key].__clientside__) {
|
48
|
+
obj[key] = makeClientsideProxy(socket, obj[key]);
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
52
|
+
};
|
53
|
+
|
54
|
+
var socketMaker = function(cid) {
|
55
|
+
var uri = "ws://" + location.host + "/__clientside_sock__/" + cid;
|
56
|
+
var socket = new WebSocket(uri);
|
57
|
+
socket.pending = {};
|
58
|
+
socket.onmessage = function(event) {
|
59
|
+
var response = JSON.parse(event.data);
|
60
|
+
var callbacks = socket.pending[response.id];
|
61
|
+
if (!callbacks) {
|
62
|
+
return;
|
63
|
+
}
|
64
|
+
else if (response.status == 'success') {
|
65
|
+
proxify(socket, response);
|
66
|
+
callbacks[0](response.result); // resolve
|
67
|
+
}
|
68
|
+
else {
|
69
|
+
callbacks[1](); // reject
|
70
|
+
}
|
71
|
+
};
|
72
|
+
return socket;
|
73
|
+
};
|
74
|
+
|
75
|
+
return socketMaker;
|
76
|
+
})();
|
77
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j<a.length;j++)g=a[j],g&&e(g.then)?g.then(d(j),c):f(j,g)})}var d=a.isArray,e=a.isFunction;b.all=c}),a("promise/asap",["exports"],function(a){"use strict";function b(){return function(){process.nextTick(e)}}function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.observe(c,{characterData:!0}),function(){c.data=a=++a%2}}function d(){return function(){j.setTimeout(e,1)}}function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k=[]}function f(a,b){var c=k.push([a,b]);1===c&&g()}var g,h="undefined"!=typeof window?window:{},i=h.MutationObserver||h.WebKitMutationObserver,j="undefined"!=typeof global?global:this,k=[];g="undefined"!=typeof process&&"[object process]"==={}.toString.call(process)?b():i?c():d(),a.asap=f}),a("promise/cast",["exports"],function(a){"use strict";function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;var b=this;return new b(function(b){b(a)})}a.cast=b}),a("promise/config",["exports"],function(a){"use strict";function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}var c={instrument:!1};a.config=c,a.configure=b}),a("promise/polyfill",["./promise","./utils","exports"],function(a,b,c){"use strict";function d(){var a="Promise"in window&&"cast"in window.Promise&&"resolve"in window.Promise&&"reject"in window.Promise&&"all"in window.Promise&&"race"in window.Promise&&function(){var a;return new window.Promise(function(b){a=b}),f(a)}();a||(window.Promise=e)}var e=a.Promise,f=b.isFunction;c.polyfill=d}),a("promise/promise",["./config","./utils","./cast","./all","./race","./resolve","./reject","./asap","exports"],function(a,b,c,d,e,f,g,h,i){"use strict";function j(a){if(!w(a))throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");if(!(this instanceof j))throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");this._subscribers=[],k(a,this)}function k(a,b){function c(a){p(b,a)}function d(a){r(b,a)}try{a(c,d)}catch(e){d(e)}}function l(a,b,c,d){var e,f,g,h,i=w(c);if(i)try{e=c(d),g=!0}catch(j){h=!0,f=j}else e=d,g=!0;o(b,e)||(i&&g?p(b,e):h?r(b,f):a===F?p(b,e):a===G&&r(b,e))}function m(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+F]=c,e[f+G]=d}function n(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;g+=3)c=e[g],d=e[g+b],l(b,c,d,f);a._subscribers=null}function o(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promises callback cannot return that same promise.");if(v(b)&&(d=b.then,w(d)))return d.call(b,function(d){return c?!0:(c=!0,b!==d?p(a,d):q(a,d),void 0)},function(b){return c?!0:(c=!0,r(a,b),void 0)}),!0}catch(e){return c?!0:(r(a,e),!0)}return!1}function p(a,b){a===b?q(a,b):o(a,b)||q(a,b)}function q(a,b){a._state===D&&(a._state=E,a._detail=b,u.async(s,a))}function r(a,b){a._state===D&&(a._state=E,a._detail=b,u.async(t,a))}function s(a){n(a,a._state=F)}function t(a){n(a,a._state=G)}var u=a.config,v=(a.configure,b.objectOrFunction),w=b.isFunction,x=(b.now,c.cast),y=d.all,z=e.race,A=f.resolve,B=g.reject,C=h.asap;u.async=C;var D=void 0,E=0,F=1,G=2;j.prototype={constructor:j,_state:void 0,_detail:void 0,_subscribers:void 0,then:function(a,b){var c=this,d=new this.constructor(function(){});if(this._state){var e=arguments;u.async(function(){l(c._state,d,e[c._state-1],c._detail)})}else m(this,d,a,b);return d},"catch":function(a){return this.then(null,a)}},j.all=y,j.cast=x,j.race=z,j.resolve=A,j.reject=B,i.Promise=j}),a("promise/race",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to race.");return new b(function(b,c){for(var d,e=0;e<a.length;e++)d=a[e],d&&"function"==typeof d.then?d.then(b,c):b(d)})}var d=a.isArray;b.race=c}),a("promise/reject",["exports"],function(a){"use strict";function b(a){var b=this;return new b(function(b,c){c(a)})}a.reject=b}),a("promise/resolve",["exports"],function(a){"use strict";function b(a){var b=this;return new b(function(b){b(a)})}a.resolve=b}),a("promise/utils",["exports"],function(a){"use strict";function b(a){return c(a)||"object"==typeof a&&null!==a}function c(a){return"function"==typeof a}function d(a){return"[object Array]"===Object.prototype.toString.call(a)}var e=Date.now||function(){return(new Date).getTime()};a.objectOrFunction=b,a.isFunction=c,a.isArray=d,a.now=e}),b("promise/polyfill").polyfill()}();
|
data/lib/clientside.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'faye/websocket'
|
3
|
+
require 'rack/static'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
class Clientside
|
7
|
+
module Accessible
|
8
|
+
class << self
|
9
|
+
attr_accessor :cur_os
|
10
|
+
|
11
|
+
def included(base)
|
12
|
+
base.singleton_class.class_eval do
|
13
|
+
attr_reader :js_allowed
|
14
|
+
|
15
|
+
def js_allow(*args)
|
16
|
+
(@js_allowed ||= []).concat args
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def reinflate(json, os)
|
22
|
+
case json
|
23
|
+
when Hash
|
24
|
+
if json[:__clientside__]
|
25
|
+
os.fetch json[:__clientside_id__]
|
26
|
+
else
|
27
|
+
Hash[json.map {|k, v| [k, reinflate(v, os)]}]
|
28
|
+
end
|
29
|
+
when Array
|
30
|
+
json.map {|v| reinflate(v, os)}
|
31
|
+
else
|
32
|
+
json
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_json(*args)
|
38
|
+
name = self.class.name
|
39
|
+
methods = self.class.js_allowed
|
40
|
+
h = {__clientside__: true, __clientside_id__: object_id, methods: methods}
|
41
|
+
h.to_json *args
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class NoResMiddleware
|
46
|
+
RPATH = '/__clientside_sock__/'
|
47
|
+
MAX_OBJECTS = 256
|
48
|
+
|
49
|
+
@@pending_sockets = {}
|
50
|
+
@@sockets = {}
|
51
|
+
|
52
|
+
def initialize(app)
|
53
|
+
@app = app
|
54
|
+
end
|
55
|
+
|
56
|
+
def register_obj(ws, obj)
|
57
|
+
@@sockets[ws][obj.object_id] = obj
|
58
|
+
end
|
59
|
+
|
60
|
+
def handle_message(cmd, ws)
|
61
|
+
raise unless cmd[:receiver].kind_of? Clientside::Accessible
|
62
|
+
allowed = cmd[:receiver].class.js_allowed
|
63
|
+
raise unless allowed.include? cmd[:method].to_sym
|
64
|
+
|
65
|
+
result = cmd[:receiver].send cmd[:method], *cmd[:arguments]
|
66
|
+
o_tj_source = JSON::Ext::Generator::GeneratorMethods::Object
|
67
|
+
result = nil if result.method(:to_json).owner.equal? o_tj_source
|
68
|
+
|
69
|
+
if result.kind_of? Accessible
|
70
|
+
unless @@sockets[ws].length >= MAX_OBJECTS
|
71
|
+
register_obj ws, result
|
72
|
+
else
|
73
|
+
raise
|
74
|
+
end
|
75
|
+
end
|
76
|
+
ws.send JSON.dump({status: 'success', id: cmd[:id], result: result})
|
77
|
+
end
|
78
|
+
|
79
|
+
def call(env)
|
80
|
+
if Faye::WebSocket.websocket? env and env['REQUEST_PATH'].start_with? RPATH
|
81
|
+
env['REQUEST_PATH'] =~ %r(\A#{RPATH}(.+)\Z)
|
82
|
+
cid = $1
|
83
|
+
objs = @@pending_sockets.delete(cid)
|
84
|
+
|
85
|
+
unless objs.nil?
|
86
|
+
ws = Faye::WebSocket.new(env)
|
87
|
+
@@sockets[ws] = objs
|
88
|
+
else
|
89
|
+
return @app.call env
|
90
|
+
end
|
91
|
+
|
92
|
+
ws.on :message do |event|
|
93
|
+
begin
|
94
|
+
cmd = JSON.parse event.data, symbolize_names: true
|
95
|
+
cmd = Clientside::Accessible.reinflate cmd, @@sockets[ws]
|
96
|
+
handle_message cmd, ws
|
97
|
+
rescue RuntimeError, KeyError => e
|
98
|
+
ws.send JSON.dump({status: 'error', id: cmd[:id]})
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
ws.on :close do |event|
|
103
|
+
@@sockets.delete ws
|
104
|
+
ws = nil
|
105
|
+
end
|
106
|
+
|
107
|
+
ws.rack_response
|
108
|
+
else
|
109
|
+
@app.call env
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.add_pending(objs)
|
114
|
+
objs = Hash[objs.map {|o| [o.object_id, o]}]
|
115
|
+
cid = SecureRandom.hex
|
116
|
+
@@pending_sockets[cid] = objs
|
117
|
+
cid
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class Middleware < NoResMiddleware
|
122
|
+
def initialize(*args)
|
123
|
+
super
|
124
|
+
dir = File.dirname(__FILE__)
|
125
|
+
@app = Rack::Static.new(@app, urls: ['/__clientside_res__'], root: dir)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.embed(objs)
|
130
|
+
objs.each do |var, obj|
|
131
|
+
raise ArgumentError, "invalid js var name" unless var =~ /\A[a-zA-Z_]\w*\Z/
|
132
|
+
end
|
133
|
+
cid = Clientside::Middleware.add_pending objs.values
|
134
|
+
sock_var = '$__clientside_socket__'
|
135
|
+
js = ""
|
136
|
+
js << %Q(<script src="/__clientside_res__/promise.min.js"></script>\n)
|
137
|
+
js << %Q(<script src="/__clientside_res__/clientside.js"></script>\n)
|
138
|
+
js << %Q(<script>\n)
|
139
|
+
objs.each do |var, obj|
|
140
|
+
js << %Q(var #{var};\n)
|
141
|
+
end
|
142
|
+
js << %Q(var #{sock_var} = makeClientsideSocket("#{cid}");\n)
|
143
|
+
js << %Q(#{sock_var}.onopen = function() {\n)
|
144
|
+
objs.each do |var, obj|
|
145
|
+
json = JSON.dump obj
|
146
|
+
js << %Q( #{var} = makeClientsideProxy(#{sock_var}, #{json});\n)
|
147
|
+
end
|
148
|
+
js << %Q(};\n)
|
149
|
+
js << %Q(</script>\n)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# vim:tabstop=2 shiftwidth=2 noexpandtab:
|
154
|
+
|
metadata
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: clientside
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- benzrf
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-01-28 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: A simple Rack middleware and JavaScript generator forbasic remote method
|
15
|
+
invocation over websockets.
|
16
|
+
email: benzrf@benzrf.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/__clientside_res__/promise.min.js
|
22
|
+
- lib/__clientside_res__/clientside.js
|
23
|
+
- lib/clientside.rb
|
24
|
+
homepage: http://rubygems.org/gems/clientside
|
25
|
+
licenses:
|
26
|
+
- LGPL
|
27
|
+
post_install_message:
|
28
|
+
rdoc_options: []
|
29
|
+
require_paths:
|
30
|
+
- lib
|
31
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
32
|
+
none: false
|
33
|
+
requirements:
|
34
|
+
- - ! '>='
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ! '>='
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
requirements: []
|
44
|
+
rubyforge_project:
|
45
|
+
rubygems_version: 1.8.23
|
46
|
+
signing_key:
|
47
|
+
specification_version: 3
|
48
|
+
summary: Use server Ruby objects from client JS!
|
49
|
+
test_files: []
|
50
|
+
has_rdoc:
|