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.
@@ -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: