clientside 0.1

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