clientside 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/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:
|