farcall 0.3.4 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e62f0ec5501d7053137341f5ec5f46100b692b61
4
- data.tar.gz: 9549befd37e328bef65639d99953ee15bae8da2c
3
+ metadata.gz: 339d4022a832e4cb3a7f54a253ec9b7628d31cab
4
+ data.tar.gz: 0daea40eb900be7e661790743a6b756229c0870e
5
5
  SHA512:
6
- metadata.gz: 37f168b9631e06851ca3ebb9f84b19dd145836878210a3d7050b343b43f6e57c9bf7790f1969e9fb951270e941a7a10b14bb0f9b537ed6c2e6d805989ce2f974
7
- data.tar.gz: 103a6f0f987801726f1876bf6f696926aada0644ed815d6ec50a06d04ced6c483f972d4011494957daec606aadc7b34e0a00793aa3e74123248bb7529b470f07
6
+ metadata.gz: 28b1ef4372a5fd8d01900096af1d2360aae63bc4bbe3e973cb181d55db5a3dcad91c1d7f6fe1c3c947f4b97b8e562753a2d879453aba5490843ecb562f8909e2
7
+ data.tar.gz: fe16ade1825ff61a668ff12438c5d6befce8c14a34cbe52f446bd27c2447175c324b7e862577c5b401960e9d3e38d1869d336bc543ffd3ca36b1c18ac72de900
data/README.md CHANGED
@@ -2,15 +2,18 @@
2
2
 
3
3
  ## News
4
4
 
5
- Since 0.3.0 farcall gem provides websocket client and server out of the box.
5
+ * websocket client and server out of the box. ./javascript folder there is compatible client implementation that works in most browsers with WebSocket support. Just add it to you web project and enjoy.
6
+
7
+ * buffering of incoming data in transport to not loose incoming packets beofre Endpoint is connected
6
8
 
7
9
  ## Description
8
10
 
9
11
  The simple and elegant cross-platform RPC protocol that uses any formatter/transport capable of
10
12
  transmitting dictionary-like objects, for example, JSON,
11
13
  [BOSS](https://github.com/sergeych/boss_protocol), XML, BSON and many others. This gem
12
- supports out of the box JSON and [BOSS](https://github.com/sergeych/boss_protocol) protocols and
13
- streams and sockets as the media.
14
+ provides out of the box JSON and [BOSS](https://github.com/sergeych/boss_protocol) protocols and
15
+ websockets (clientm server, and javascript version for the browser or mayme some node app),
16
+ EventMachine channels, streams and sockets.
14
17
 
15
18
  There is also optional support for eventmachine based wbesocket server and regular client websocket
16
19
  connection. All you need is to include gem 'em-websocket' and/or gem 'websocket-client-simple'.
@@ -0,0 +1,278 @@
1
+ root = exports ? this
2
+ # Farcall RPC protocol client over WebSocket transport.
3
+ #
4
+ # This script has no external dependencies except that if you want to use self-test, you'll
5
+ # need underscore.js. Test silently fails if can't find it.
6
+ #
7
+ # See protocol at github: https://github.com/sergeych/farcall/
8
+ #
9
+ # provided under MIT license.
10
+ #
11
+ # @author real.sergeych@gmail.com
12
+ #
13
+ class root.WsFarcall
14
+
15
+ # Create transport connected to the given url. A shortcut of (new WsFarrcall(url))
16
+ @open: (url) ->
17
+ new WsFarcall(url)
18
+
19
+ # Construct a protocol endpoint connected to the given url (must be ws:// or wss:// websocket
20
+ # url)
21
+ constructor: (@url) ->
22
+ @in_serial = 0
23
+ @out_serial = 0
24
+ @openHandlers = []
25
+ @closeHandlers = []
26
+ @ws = new WebSocket(@url)
27
+ @connected = false
28
+ @promises = {}
29
+ @commandHandlers = {}
30
+ @ws.onopen = =>
31
+ @connected = true
32
+ cb(this) for cb in @openHandlers
33
+
34
+ @ws.onclose = =>
35
+ @connected = false
36
+ cb(this) for cb in @closeHandlers
37
+
38
+ @ws.onmessage = (message) =>
39
+ @trace and console.log ">>> #{message.data}"
40
+ @_receive(JSON.parse message.data)
41
+
42
+ # add open callback. The callback takes a single argument - the WsFarcall instance
43
+ onopen: (callback) ->
44
+ @openHandlers.push callback
45
+
46
+ # add close callback. The callback takes a single argument - the WsFarcall instance
47
+ onclose: (callback) ->
48
+ @closeHandlers.push callback
49
+
50
+ # close the protocol and socket
51
+ close: ->
52
+ @ws.close()
53
+
54
+ # Call remote function with the specified name. Arguments can be list and.or keyword arguments.
55
+ # The Farcall arguments can be a list, a dictionary (keywords hash), or both. If the last argument
56
+ # is the object, it will be treated as dictionary argument. Add extra {} to the end of list if
57
+ # need.
58
+ #
59
+ # Returns Promise instance so you can add .success() (or .done()), fail() and always() handlers to
60
+ # it. On success callback receives whatever data remote function has returned, on error it receives
61
+ # the arcall standard error object, e.g. { error: { class: string, text: another_sting} } object.
62
+ #
63
+ # always handler receives and object { result: {}, error: {}} where only one of the two is set
64
+ #
65
+ call: (name, args...) ->
66
+ [args, kwargs] = splitArgs(args)
67
+ promise = @promises[@out_serial] = new Promise()
68
+ @_send cmd: name, args: args, kwargs: kwargs
69
+ promise
70
+
71
+ # Add an local remote-callable function. The callback receives whatever arguments are send from
72
+ # thre remote caller and its returned value will be passed to the remote. On error it should throw
73
+ # an exception.
74
+ on: (name, callback) ->
75
+ @commandHandlers[name] = callback
76
+
77
+ _receive: (data) ->
78
+ if data.serial != @in_serial++
79
+ console.error "farcall framing error"
80
+ @close()
81
+ else
82
+ if data.ref != undefined
83
+ promise = @promises[data.ref]
84
+ delete @promises[data.ref]
85
+ if data.error == undefined
86
+ promise.setDone(data.result)
87
+ else
88
+ promise.setFail(data.error)
89
+ else
90
+ @_processCall data
91
+
92
+ _send: (params) ->
93
+ params.serial = @out_serial++
94
+ params = JSON.stringify(params)
95
+ @trace and console.log "<<< #{params}"
96
+ @ws.send params
97
+
98
+ _processCall: (data) ->
99
+ handler = @commandHandlers[data.cmd]
100
+ if handler
101
+ try
102
+ data.args.push data.kwargs
103
+ @_send ref: data.serial, result: handler(data.args...)
104
+ catch e
105
+ @_send ref: data.serial, error: {class: 'RuntimeError', text: e.message}
106
+ else
107
+ @_send
108
+ ref: data.serial, error: {class: 'NoMethodError', text: "method not found: #{data.cmd}"}
109
+
110
+ @selfTest: (url, callback) ->
111
+ if _?.isEqual(1, 1)
112
+ p1 = false
113
+ p2 = false
114
+ p3 = false
115
+ cb = false
116
+ cbr = false
117
+ done = false
118
+ WsFarcall.open(url + '/fartest').onopen (fcall) ->
119
+ fcall.call('ping', 1, 2, 3, {hello: 'world'})
120
+ .done (data) ->
121
+ p1 = checkEquals(data, {pong: [1, 2, 3, {hello: 'world'}]})
122
+ fcall.call('ping', 2, 2, 3, {hello: 'world'})
123
+ .done (data) ->
124
+ p2 = checkEquals(data, {pong: [2, 2, 3, {hello: 'world'}]})
125
+ fcall.call('ping', 3, 2, 3, {hello: 'world'})
126
+ .done (data) ->
127
+ p3 = checkEquals(data, {pong: [3, 2, 3, {hello: 'world'}]})
128
+
129
+ fcall.on 'test_callback', (args...) ->
130
+ cb = checkEquals(args, [5, 4, 3, {hello: 'world'}])
131
+ # The callback request should have time to be processed
132
+ setTimeout ->
133
+ done = true
134
+ ok = p1 and p2 and p3 and cb and cbr
135
+ text = switch
136
+ when !cb
137
+ 'callback was not called or wrong data'
138
+ when !cbr
139
+ 'callback request did not return'
140
+ else
141
+ if ok then '' else 'ping data wrong'
142
+ callback(ok, text)
143
+ , 80
144
+
145
+ fcall.call('callback', 'test_callback', 5, 4, 3, hello: 'world')
146
+ .done (data) ->
147
+ cbr = true
148
+
149
+ setTimeout ->
150
+ callback(false, 'timed out') if !done
151
+ , 5000
152
+ else
153
+ # can't pass test = we need underscore for it
154
+ callback(false, "Can't test: need underscpre.js")
155
+
156
+ # Promise object let set any number of callbacks on the typical comletion events, namely success,
157
+ # failure and operation is somehow completed (always).
158
+ #
159
+ # The promise has 3 states: initial (operation us inder way), done and failed. When the operation
160
+ # is done or failed, attempt to add new handlers of the proper type will cause it immediate
161
+ # invocation.
162
+ #
163
+ # Promise can be set to equer success or error only once. All suitable handlers are called one time
164
+ # when the state is set.
165
+ root.WsFarcall.Promise = class Promise
166
+
167
+ constructor: ->
168
+ [@done_handlers, @fail_handlers, @always_handlers] = ([] for i in [1..3])
169
+ @state = null
170
+ @data = @error = undefined
171
+
172
+ # Add callback on the success event. Can be called any number of times, all callbacks will be
173
+ # invoked. If the state is already set to done, the callback fires immediately.
174
+ #
175
+ # callback receives whatever data passed to the #setSuccess() call.
176
+ done: (callback) ->
177
+ if @state
178
+ callback(@data) if @state == 'done'
179
+ else
180
+ @done_handlers.push callback
181
+ this
182
+
183
+ # same as done()
184
+ success: (callback) ->
185
+ @done(callback)
186
+
187
+ # Add callback on the failure event. Can be called any number of times, all callbacks will be
188
+ # invoked. If the state is already set to failure, the callback fires immediately.
189
+ #
190
+ # callback receives whatever data passed to the #setFail() call.
191
+ fail: (callback) ->
192
+ if @state
193
+ callback(@error) if @state == 'fail'
194
+ else
195
+ @fail_handlers.push callback
196
+ this
197
+
198
+ # Add callback on the both sucess and failure event. Can be called any number of times, all
199
+ # callbacks will be invoked. If the state is already set to eqither done or failure, the callback
200
+ # fires immediately.
201
+ #
202
+ # callback receives { error: {class: c. text: t}, result: any } object where only one (error or
203
+ # result) exist.
204
+ #
205
+ # always callbacks are executed after all of #success() or #fail().
206
+ always: (callback) ->
207
+ if @state
208
+ callback data: @data, error: @error
209
+ else
210
+ @always_handlers.push callback
211
+ this
212
+
213
+ # set promise to success state. If the state is already set to any, does noting. Calls all
214
+ # corresponding callbacks passing them the `data` parameter.
215
+ setSuccess: (data) ->
216
+ if !@state
217
+ @state = 'done'
218
+ @data = data
219
+ cb(data) for cb in @done_handlers
220
+ @done = null
221
+ @_fireAlways()
222
+ this
223
+
224
+ # same as #setSuccess()
225
+ setDone: (data) ->
226
+ @setSuccess data
227
+
228
+ # set promise to failure state. If the state is already set to any, does noting. Calls all
229
+ # corresponding callbacks passing them the `data` parameter.
230
+ setFail: (error) ->
231
+ if !@state
232
+ @state = 'fail'
233
+ @error = error
234
+ cb(error) for cb in @fail_handlers
235
+ @fail = null
236
+ @_fireAlways()
237
+ this
238
+
239
+ isSuccess: ->
240
+ @state == 'done'
241
+
242
+ isFail: ->
243
+ @state == 'fail'
244
+
245
+ # same as #isFail
246
+ isError: ->
247
+ @state == 'fail'
248
+
249
+ # promise is either done of fail (e.g. completed)
250
+ isCompleted: ->
251
+ !!@state
252
+
253
+ _fireAlways: ->
254
+ cb({error: @error, data: @data}) for cb in @always_handlers
255
+ @always = null
256
+
257
+
258
+ # Split javascript arguments array to args and kwargs of the Farcall notaion
259
+ # e.g. if the last argument is the pbject, it will be treated as kwargs.
260
+ splitArgs = (args) ->
261
+ last = args[args.length - 1]
262
+ if typeof(last) == 'object'
263
+ [args[0..-2], last]
264
+ else
265
+ [args, {}]
266
+
267
+
268
+ # For self-test only, relies on underscore.js
269
+ checkEquals = (a, b, text) ->
270
+ if _.isEqual a, b
271
+ true
272
+ else
273
+ console.error "#{text ? 'ERROR'}: not equal:"
274
+ console.error "expected: #{JSON.stringify b}"
275
+ console.error "got: #{JSON.stringify a}"
276
+ false
277
+
278
+
@@ -0,0 +1,379 @@
1
+ // Generated by CoffeeScript 1.8.0
2
+ (function() {
3
+ var Promise, checkEquals, root, splitArgs,
4
+ __slice = [].slice;
5
+
6
+ root = typeof exports !== "undefined" && exports !== null ? exports : this;
7
+
8
+ root.WsFarcall = (function() {
9
+ WsFarcall.open = function(url) {
10
+ return new WsFarcall(url);
11
+ };
12
+
13
+ function WsFarcall(url) {
14
+ this.url = url;
15
+ this.in_serial = 0;
16
+ this.out_serial = 0;
17
+ this.openHandlers = [];
18
+ this.closeHandlers = [];
19
+ this.ws = new WebSocket(this.url);
20
+ this.connected = false;
21
+ this.promises = {};
22
+ this.commandHandlers = {};
23
+ this.ws.onopen = (function(_this) {
24
+ return function() {
25
+ var cb, _i, _len, _ref, _results;
26
+ _this.connected = true;
27
+ _ref = _this.openHandlers;
28
+ _results = [];
29
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
30
+ cb = _ref[_i];
31
+ _results.push(cb(_this));
32
+ }
33
+ return _results;
34
+ };
35
+ })(this);
36
+ this.ws.onclose = (function(_this) {
37
+ return function() {
38
+ var cb, _i, _len, _ref, _results;
39
+ _this.connected = false;
40
+ _ref = _this.closeHandlers;
41
+ _results = [];
42
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
43
+ cb = _ref[_i];
44
+ _results.push(cb(_this));
45
+ }
46
+ return _results;
47
+ };
48
+ })(this);
49
+ this.ws.onmessage = (function(_this) {
50
+ return function(message) {
51
+ _this.trace && console.log(">>> " + message.data);
52
+ return _this._receive(JSON.parse(message.data));
53
+ };
54
+ })(this);
55
+ }
56
+
57
+ WsFarcall.prototype.onopen = function(callback) {
58
+ return this.openHandlers.push(callback);
59
+ };
60
+
61
+ WsFarcall.prototype.onclose = function(callback) {
62
+ return this.closeHandlers.push(callback);
63
+ };
64
+
65
+ WsFarcall.prototype.close = function() {
66
+ return this.ws.close();
67
+ };
68
+
69
+ WsFarcall.prototype.call = function() {
70
+ var args, kwargs, name, promise, _ref;
71
+ name = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
72
+ _ref = splitArgs(args), args = _ref[0], kwargs = _ref[1];
73
+ promise = this.promises[this.out_serial] = new Promise();
74
+ this._send({
75
+ cmd: name,
76
+ args: args,
77
+ kwargs: kwargs
78
+ });
79
+ return promise;
80
+ };
81
+
82
+ WsFarcall.prototype.on = function(name, callback) {
83
+ return this.commandHandlers[name] = callback;
84
+ };
85
+
86
+ WsFarcall.prototype._receive = function(data) {
87
+ var promise;
88
+ if (data.serial !== this.in_serial++) {
89
+ console.error("farcall framing error");
90
+ return this.close();
91
+ } else {
92
+ if (data.ref !== void 0) {
93
+ promise = this.promises[data.ref];
94
+ delete this.promises[data.ref];
95
+ if (data.error === void 0) {
96
+ return promise.setDone(data.result);
97
+ } else {
98
+ return promise.setFail(data.error);
99
+ }
100
+ } else {
101
+ return this._processCall(data);
102
+ }
103
+ }
104
+ };
105
+
106
+ WsFarcall.prototype._send = function(params) {
107
+ params.serial = this.out_serial++;
108
+ params = JSON.stringify(params);
109
+ this.trace && console.log("<<< " + params);
110
+ return this.ws.send(params);
111
+ };
112
+
113
+ WsFarcall.prototype._processCall = function(data) {
114
+ var e, handler;
115
+ handler = this.commandHandlers[data.cmd];
116
+ if (handler) {
117
+ try {
118
+ data.args.push(data.kwargs);
119
+ return this._send({
120
+ ref: data.serial,
121
+ result: handler.apply(null, data.args)
122
+ });
123
+ } catch (_error) {
124
+ e = _error;
125
+ return this._send({
126
+ ref: data.serial,
127
+ error: {
128
+ "class": 'RuntimeError',
129
+ text: e.message
130
+ }
131
+ });
132
+ }
133
+ } else {
134
+ return this._send({
135
+ ref: data.serial,
136
+ error: {
137
+ "class": 'NoMethodError',
138
+ text: "method not found: " + data.cmd
139
+ }
140
+ });
141
+ }
142
+ };
143
+
144
+ WsFarcall.selfTest = function(url, callback) {
145
+ var cb, cbr, done, p1, p2, p3;
146
+ if (typeof _ !== "undefined" && _ !== null ? _.isEqual(1, 1) : void 0) {
147
+ p1 = false;
148
+ p2 = false;
149
+ p3 = false;
150
+ cb = false;
151
+ cbr = false;
152
+ done = false;
153
+ WsFarcall.open(url + '/fartest').onopen(function(fcall) {
154
+ fcall.call('ping', 1, 2, 3, {
155
+ hello: 'world'
156
+ }).done(function(data) {
157
+ return p1 = checkEquals(data, {
158
+ pong: [
159
+ 1, 2, 3, {
160
+ hello: 'world'
161
+ }
162
+ ]
163
+ });
164
+ });
165
+ fcall.call('ping', 2, 2, 3, {
166
+ hello: 'world'
167
+ }).done(function(data) {
168
+ return p2 = checkEquals(data, {
169
+ pong: [
170
+ 2, 2, 3, {
171
+ hello: 'world'
172
+ }
173
+ ]
174
+ });
175
+ });
176
+ fcall.call('ping', 3, 2, 3, {
177
+ hello: 'world'
178
+ }).done(function(data) {
179
+ return p3 = checkEquals(data, {
180
+ pong: [
181
+ 3, 2, 3, {
182
+ hello: 'world'
183
+ }
184
+ ]
185
+ });
186
+ });
187
+ fcall.on('test_callback', function() {
188
+ var args;
189
+ args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
190
+ cb = checkEquals(args, [
191
+ 5, 4, 3, {
192
+ hello: 'world'
193
+ }
194
+ ]);
195
+ return setTimeout(function() {
196
+ var ok, text;
197
+ done = true;
198
+ ok = p1 && p2 && p3 && cb && cbr;
199
+ text = (function() {
200
+ switch (false) {
201
+ case !!cb:
202
+ return 'callback was not called or wrong data';
203
+ case !!cbr:
204
+ return 'callback request did not return';
205
+ default:
206
+ if (ok) {
207
+ return '';
208
+ } else {
209
+ return 'ping data wrong';
210
+ }
211
+ }
212
+ })();
213
+ return callback(ok, text);
214
+ }, 80);
215
+ });
216
+ return fcall.call('callback', 'test_callback', 5, 4, 3, {
217
+ hello: 'world'
218
+ }).done(function(data) {
219
+ return cbr = true;
220
+ });
221
+ });
222
+ return setTimeout(function() {
223
+ if (!done) {
224
+ return callback(false, 'timed out');
225
+ }
226
+ }, 5000);
227
+ } else {
228
+ return callback(false, "Can't test: need underscpre.js");
229
+ }
230
+ };
231
+
232
+ return WsFarcall;
233
+
234
+ })();
235
+
236
+ root.WsFarcall.Promise = Promise = (function() {
237
+ function Promise() {
238
+ var i, _ref;
239
+ _ref = (function() {
240
+ var _i, _results;
241
+ _results = [];
242
+ for (i = _i = 1; _i <= 3; i = ++_i) {
243
+ _results.push([]);
244
+ }
245
+ return _results;
246
+ })(), this.done_handlers = _ref[0], this.fail_handlers = _ref[1], this.always_handlers = _ref[2];
247
+ this.state = null;
248
+ this.data = this.error = void 0;
249
+ }
250
+
251
+ Promise.prototype.done = function(callback) {
252
+ if (this.state) {
253
+ if (this.state === 'done') {
254
+ callback(this.data);
255
+ }
256
+ } else {
257
+ this.done_handlers.push(callback);
258
+ }
259
+ return this;
260
+ };
261
+
262
+ Promise.prototype.success = function(callback) {
263
+ return this.done(callback);
264
+ };
265
+
266
+ Promise.prototype.fail = function(callback) {
267
+ if (this.state) {
268
+ if (this.state === 'fail') {
269
+ callback(this.error);
270
+ }
271
+ } else {
272
+ this.fail_handlers.push(callback);
273
+ }
274
+ return this;
275
+ };
276
+
277
+ Promise.prototype.always = function(callback) {
278
+ if (this.state) {
279
+ callback({
280
+ data: this.data,
281
+ error: this.error
282
+ });
283
+ } else {
284
+ this.always_handlers.push(callback);
285
+ }
286
+ return this;
287
+ };
288
+
289
+ Promise.prototype.setSuccess = function(data) {
290
+ var cb, _i, _len, _ref;
291
+ if (!this.state) {
292
+ this.state = 'done';
293
+ this.data = data;
294
+ _ref = this.done_handlers;
295
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
296
+ cb = _ref[_i];
297
+ cb(data);
298
+ }
299
+ this.done = null;
300
+ this._fireAlways();
301
+ }
302
+ return this;
303
+ };
304
+
305
+ Promise.prototype.setDone = function(data) {
306
+ return this.setSuccess(data);
307
+ };
308
+
309
+ Promise.prototype.setFail = function(error) {
310
+ var cb, _i, _len, _ref;
311
+ if (!this.state) {
312
+ this.state = 'fail';
313
+ this.error = error;
314
+ _ref = this.fail_handlers;
315
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
316
+ cb = _ref[_i];
317
+ cb(error);
318
+ }
319
+ this.fail = null;
320
+ this._fireAlways();
321
+ }
322
+ return this;
323
+ };
324
+
325
+ Promise.prototype.isSuccess = function() {
326
+ return this.state === 'done';
327
+ };
328
+
329
+ Promise.prototype.isFail = function() {
330
+ return this.state === 'fail';
331
+ };
332
+
333
+ Promise.prototype.isError = function() {
334
+ return this.state === 'fail';
335
+ };
336
+
337
+ Promise.prototype.isCompleted = function() {
338
+ return !!this.state;
339
+ };
340
+
341
+ Promise.prototype._fireAlways = function() {
342
+ var cb, _i, _len, _ref;
343
+ _ref = this.always_handlers;
344
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
345
+ cb = _ref[_i];
346
+ cb({
347
+ error: this.error,
348
+ data: this.data
349
+ });
350
+ }
351
+ return this.always = null;
352
+ };
353
+
354
+ return Promise;
355
+
356
+ })();
357
+
358
+ splitArgs = function(args) {
359
+ var last;
360
+ last = args[args.length - 1];
361
+ if (typeof last === 'object') {
362
+ return [args.slice(0, -1), last];
363
+ } else {
364
+ return [args, {}];
365
+ }
366
+ };
367
+
368
+ checkEquals = function(a, b, text) {
369
+ if (_.isEqual(a, b)) {
370
+ return true;
371
+ } else {
372
+ console.error("" + (text != null ? text : 'ERROR') + ": not equal:");
373
+ console.error("expected: " + (JSON.stringify(b)));
374
+ console.error("got: " + (JSON.stringify(a)));
375
+ return false;
376
+ }
377
+ };
378
+
379
+ }).call(this);
@@ -8,18 +8,13 @@ module Farcall
8
8
 
9
9
  # Create json transport, see Farcall::Transpor#create for parameters
10
10
  def initialize **params
11
+ super()
11
12
  setup_streams **params
12
13
  @formatter = Boss::Formatter.new(@output)
13
14
  @formatter.set_stream_mode
14
- end
15
-
16
- def on_data_received= block
17
- super
18
- if block && !@thread
19
- @thread = Thread.start {
20
- load_loop
21
- }
22
- end
15
+ @thread = Thread.start {
16
+ load_loop
17
+ }
23
18
  end
24
19
 
25
20
  def send_data hash
@@ -39,7 +34,7 @@ module Farcall
39
34
 
40
35
  def load_loop
41
36
  Boss::Parser.new(@input).each { |object|
42
- on_data_received and on_data_received.call object
37
+ push_input object
43
38
  }
44
39
  rescue Errno::EPIPE
45
40
  close
@@ -1,6 +1,7 @@
1
1
  begin
2
2
  require 'hashie'
3
3
  require 'eventmachine'
4
+ require_relative './promise'
4
5
 
5
6
  # As the eventmachine callback paradigm is completely different from the threaded paradigm
6
7
  # of the Farcall, that runs pretty well under JRuby and in multithreaded MRI, we provide
@@ -60,7 +61,9 @@ begin
60
61
  # if block is provided, it will be called when the remote will be called and possibly return
61
62
  # some data.
62
63
  #
63
- # Block receives single object paramter with two fields: `result.error` and `result.result`.
64
+ # Block if present receives single object paramter with two fields: `result.error` and
65
+ # `result.result`. It is also possible to use returned {Farcall::Promise} instance to set
66
+ # multiple callbacks with ease. Promise callbacks are called _after_ the block.
64
67
  #
65
68
  # `result.error` is not nil when the remote raised error, then `error[:class]` and
66
69
  # `error.text` are set accordingly.
@@ -77,15 +80,23 @@ begin
77
80
  # }
78
81
  #
79
82
  # @param [String] name command name
80
- # @return [Endpoint] self
83
+ # @return [Promise] object that call be used to set multiple handlers on success
84
+ # or fail event. {Farcall::Promise#succsess} receives remote return result on
85
+ # success and {Farcall::Promise#fail} receives error object.
81
86
  def call(name, *args, **kwargs, &block)
87
+ promise = Farcall::Promise.new
82
88
  EM.schedule {
83
- if block
84
- @callbacks[@in_serial] = block
85
- end
89
+ @callbacks[@in_serial] = -> (result) {
90
+ block.call(result) if block != nil
91
+ if result.error
92
+ promise.set_fail result.error
93
+ else
94
+ promise.set_success result.result
95
+ end
96
+ }
86
97
  send_block cmd: name, args: args, kwargs: kwargs
87
98
  }
88
- self
99
+ promise
89
100
  end
90
101
 
91
102
  # Close the endpoint
@@ -1,4 +1,5 @@
1
1
  require 'hashie'
2
+ require_relative 'promise'
2
3
 
3
4
  module Farcall
4
5
 
@@ -20,9 +21,22 @@ module Farcall
20
21
 
21
22
  # Create endpoint connected to some transport
22
23
  # @param [Farcall::Transport] transport
23
- def initialize(transport)
24
+ def initialize(transport, init_proc=nil)
24
25
  @transport = transport
25
26
  @in_serial = @out_serial = 0
27
+ @send_lock = Mutex.new
28
+ @receive_lock = Mutex.new
29
+ @handlers = {}
30
+ @waiting = {}
31
+
32
+ init_proc.call(self) if init_proc
33
+
34
+ def push_input data
35
+ p 'me -- pusj!', data
36
+ @in_buffer << data
37
+ drain
38
+ end
39
+
26
40
  @transport.on_data_received = -> (data) {
27
41
  begin
28
42
  _received(data)
@@ -30,11 +44,10 @@ module Farcall
30
44
  abort :format_error, $!
31
45
  end
32
46
  }
47
+ end
33
48
 
34
- @send_lock = Mutex.new
35
- @receive_lock = Mutex.new
36
- @handlers = {}
37
- @waiting = {}
49
+ def self.open(transport, &block)
50
+ Endpoint.new(transport, block)
38
51
  end
39
52
 
40
53
  # The provided block will be called if endpoint functioning will be aborted.
@@ -70,20 +83,28 @@ module Farcall
70
83
  # or result itself are instances of th Hashie::Mash. Error could be nil or
71
84
  # {'class' =>, 'text' => } Hashie::Mash hash. result is always nil if error is presented.
72
85
  #
73
- # It is desirable to use Farcall::Endpoint#interface or
74
- # Farcall::RemoteInterface rather than this low-level method.
86
+ # Usually, using Farcall::Endpoint#interface or
87
+ # Farcall::RemoteInterface is more effective rather than this low-level method.
88
+ #
89
+ # The returned {Farcall::Promise} instance let add any number of callbacks on commend execution,
90
+ # success or failure.
75
91
  #
76
92
  # @param [String] name of the remote command
93
+ # @return [Farcall::Promise] instance
77
94
  def call(name, *args, **kwargs, &block)
95
+ promise = Farcall::Promise.new
78
96
  @send_lock.synchronize {
79
- if block != nil
80
- @waiting[@out_serial] = {
81
- time: Time.new,
82
- proc: block
97
+ @waiting[@out_serial] = -> (error, result) {
98
+ block.call(error, result) if block
99
+ if error
100
+ promise.set_fail error
101
+ else
102
+ promise.set_success result
103
+ end
83
104
  }
84
105
  _send(cmd: name.to_s, args: args, kwargs: kwargs)
85
- end
86
106
  }
107
+ promise
87
108
  end
88
109
 
89
110
  # Call the remote party and wait for the return.
@@ -210,7 +231,7 @@ module Farcall
210
231
  when ref
211
232
 
212
233
  ref or abort 'no reference in return'
213
- (w = @waiting.delete ref) != nil and w[:proc].call(error, result)
234
+ (proc = @waiting.delete ref) != nil and proc.call(error, result)
214
235
 
215
236
  else
216
237
  abort 'unknown command'
@@ -92,18 +92,13 @@ module Farcall
92
92
 
93
93
  # Create json transport, see Farcall::Transpor#create for parameters
94
94
  def initialize delimiter: "\x00", **params
95
+ super()
95
96
  setup_streams **params
96
97
  @delimiter = delimiter
97
98
  @dlength = -delimiter.length
98
- end
99
-
100
- def on_data_received= block
101
- super
102
- if block && !@thread
103
- @thread = Thread.start {
104
- load_loop
105
- }
106
- end
99
+ @thread = Thread.start {
100
+ load_loop
101
+ }
107
102
  end
108
103
 
109
104
  def send_data hash
@@ -126,7 +121,7 @@ module Farcall
126
121
  while !@input.eof?
127
122
  buffer << @input.read(1)
128
123
  if buffer[@dlength..-1] == @delimiter
129
- on_data_received and on_data_received.call(JSON.parse(buffer[0...@dlength]))
124
+ push_input JSON.parse(buffer[0...@dlength])
130
125
  buffer = ''
131
126
  end
132
127
  end
@@ -0,0 +1,113 @@
1
+ module Farcall
2
+ # Promise let set multiple callbacks on different completion events: success, fail and completion.
3
+ # Promisee guarantte that corresponing callbacks will be called and only once. Callbacks added
4
+ # after completion are being called immediately.
5
+ #
6
+ # Promise state can be changed only once by calling either #set_success( or #set_fail().
7
+ # Its subsequent invocations raise errors.
8
+ #
9
+ class Promise
10
+
11
+ # returns data passed to the call to #set_success(data), or nil
12
+ attr :data
13
+
14
+ # returns data passed to the call to #set_success(error), or nil
15
+ attr :error
16
+
17
+ def initialize
18
+ @state = nil
19
+ @success, @fail, @always = [], [], []
20
+ end
21
+
22
+ def succsess?
23
+ @state == :success
24
+ end
25
+
26
+ def fail?
27
+ @state == :fail
28
+ end
29
+
30
+ # the promise is completed after one of #set_success(data) and #set_fail(error) is called.
31
+ def completed?
32
+ @state == :completed
33
+ end
34
+
35
+ # Add handler for the success event. If the promise is already #success? then the block
36
+ # is called immediately, otherwise when and if the promise reach this state.
37
+ #
38
+ # block receives data parameter passed to #set_success(data)
39
+ #
40
+ # @return [Proomise] self
41
+ def success &block
42
+ if !completed?
43
+ @success << block
44
+ elsif succsess?
45
+ block.call(@data)
46
+ end
47
+ self
48
+ end
49
+
50
+ # Add handler for the fail event. If the promise is already #fail? then the block
51
+ # is called immediately, otherwise when and if the promise reach this state.
52
+ #
53
+ # Block receives error parameter passed to the #set_fail(data)
54
+ #
55
+ # @return [Proomise] self
56
+ def fail &block
57
+ if !completed?
58
+ @fail << block
59
+ elsif fail?
60
+ block.call(@error)
61
+ end
62
+ self
63
+ end
64
+
65
+ # Add handler to the completion event that will receive promise instance as the parameter (thus
66
+ # able to check state and ready #data or #error value).
67
+ #
68
+ # Note that `always` handlers are called after `success` or `fail` ones.
69
+ #
70
+ # @return [Proomise] self
71
+ def always &block
72
+ if !completed?
73
+ @always << block
74
+ else
75
+ block.call(self)
76
+ end
77
+ self
78
+ end
79
+
80
+ # same as #always
81
+ alias finished always
82
+
83
+ # Set the promise as #completed? with #success? state and invoke proper handlers passing `data`
84
+ # which is also available with #data property.
85
+ #
86
+ # If invoked when already #completed?, raises error.
87
+ def set_success data
88
+ raise "state is already set" if @state
89
+ @state = :success
90
+ @data = data
91
+ invoke @success, data
92
+ end
93
+
94
+ # Set the promise as #completed? with #fail? state and invoke proper handlers. passing `error`
95
+ # which is also available with #error property.
96
+ #
97
+ # If invoked when already #completed?, raises error.
98
+ def set_fail error
99
+ raise "state is already set" if @state
100
+ @state = :fail
101
+ @error = error
102
+ invoke @fail, error
103
+ end
104
+
105
+ private
106
+
107
+ def invoke list, data
108
+ list.each { |proc| proc.call(data) }
109
+ @always.each { |proc| proc.call(self) }
110
+ @always = @success = @fail = nil
111
+ end
112
+ end
113
+ end
@@ -51,6 +51,10 @@ module Farcall
51
51
  # to call super first.
52
52
  attr_accessor :on_data_received, :on_abort, :on_close
53
53
 
54
+ def initialize
55
+ @in_buffer = []
56
+ end
57
+
54
58
  # Utility function. Calls the provided block on data reception. Resets the
55
59
  # block with #on_data_received
56
60
  def receive_data &block
@@ -72,8 +76,29 @@ module Farcall
72
76
  @closed
73
77
  end
74
78
 
79
+ # set handler and drain all input packets that may be buffered by the time.
80
+ def on_data_received= proc
81
+ @on_data_received = proc
82
+ drain
83
+ end
84
+
85
+ # Input buffering: transport may start before configure endpoint delegates observer, so
86
+ # the transport can simply push it here and rely on default buffering.
87
+ def push_input data
88
+ @in_buffer << data
89
+ drain
90
+ end
91
+
75
92
  protected
76
93
 
94
+ def drain
95
+ if @in_buffer.size > 0 && on_data_received
96
+ @in_buffer.each { |x| on_data_received.call(x) }
97
+ @in_buffer.clear
98
+ end
99
+ end
100
+
101
+
77
102
  # Call it when your connection is closed
78
103
  def connection_closed
79
104
  close
@@ -99,6 +124,7 @@ module Farcall
99
124
  attr_accessor :other
100
125
 
101
126
  def initialize other_loop = nil
127
+ super()
102
128
  if other_loop
103
129
  other_loop.other = self
104
130
  @other = other_loop
@@ -1,3 +1,3 @@
1
1
  module Farcall
2
- VERSION = "0.3.4"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -25,6 +25,7 @@ begin
25
25
  # JSON encodgin over standard websocket protocol.
26
26
  def initialize ws_url
27
27
  # The stranges bug around in the WebSocket::Client (actually in his eventemitter)
28
+ super()
28
29
  me = self
29
30
 
30
31
  is_open = Semaphore.new
@@ -39,9 +40,7 @@ begin
39
40
  }
40
41
 
41
42
  @ws.on(:message) { |m|
42
- # puts "ws client received #{JSON.parse m.data}"
43
- me.on_data_received and me.on_data_received.call(JSON.parse m.data)
44
- # puts "and sent"
43
+ me.push_input JSON.parse(m.data)
45
44
  }
46
45
  @ws.on(:close) { close }
47
46
  is_open.wait_set
@@ -53,6 +52,7 @@ begin
53
52
  end
54
53
 
55
54
  end
55
+
56
56
  end
57
57
  rescue LoadError
58
58
  $!.to_s =~ /websocket/ or raise
data/spec/websock_spec.rb CHANGED
@@ -229,5 +229,59 @@ describe 'em_farcall' do
229
229
  standard_check_wsclient(cnt1, r1, r2, r3)
230
230
  end
231
231
 
232
+ it 'calls from server to client' do
233
+ data1 = nil
234
+ data2 = nil
235
+ done = nil
236
+
237
+ order = 0
238
+ block_order = 0
239
+ promise_order = 0
240
+
241
+ EM.run {
242
+ params = {
243
+ :host => 'localhost',
244
+ :port => 8088
245
+ }
246
+ EM::WebSocket.run(params) do |ws|
247
+ ws.onopen { |handshake|
248
+ server = EmFarcall::WsServerEndpoint.new ws
249
+
250
+ # EM channels are synchronous so if we call too early call will be simply lost. This,
251
+ # though, should not happen to the socket - so why?
252
+ server.call(:test_method, 'hello', foo: :bar) { block_order = order+=1 }.success { |result|
253
+ data2 = result
254
+ promise_order = order += 1
255
+ }.fail { |e|
256
+ puts "Error #{e}"
257
+ }.always { |item|
258
+ done = item
259
+ EM.stop
260
+ }
261
+ }
262
+ end
263
+
264
+ EM.defer {
265
+ t1 = Farcall::WebsocketJsonClientTransport.new 'ws://localhost:8088/test'
266
+ Farcall::Endpoint.open(t1) { |client|
267
+ client.on(:test_method) { |args, kwargs|
268
+ data1 = { 'nice' => [args, kwargs] }
269
+ { done: :success }
270
+ }
271
+ }
272
+ }
273
+
274
+ EM.add_timer(1) {
275
+ EM.stop
276
+ }
277
+ }
278
+ data1.should == { 'nice' => [['hello'], { 'foo' => 'bar' }] }
279
+ data2.should == { 'done' => 'success' }
280
+ done.should be_instance_of(Farcall::Promise)
281
+ block_order.should == 1
282
+ promise_order.should == 2
283
+ done.data.should == data2
284
+ end
285
+
232
286
 
233
287
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: farcall
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - sergeych
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-24 00:00:00.000000000 Z
11
+ date: 2016-06-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashie
@@ -125,6 +125,8 @@ files:
125
125
  - README.md
126
126
  - Rakefile
127
127
  - farcall.gemspec
128
+ - javascript/farcall_wsclient.coffee
129
+ - javascript/farcall_wsclient.js
128
130
  - lib/farcall.rb
129
131
  - lib/farcall/boss_transport.rb
130
132
  - lib/farcall/em_farcall.rb
@@ -132,6 +134,7 @@ files:
132
134
  - lib/farcall/endpoint.rb
133
135
  - lib/farcall/json_transport.rb
134
136
  - lib/farcall/monitor_lock.rb
137
+ - lib/farcall/promise.rb
135
138
  - lib/farcall/transport.rb
136
139
  - lib/farcall/version.rb
137
140
  - lib/farcall/wsclient_transport.rb