├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── main.js ├── package.json ├── rserve.js ├── rserve.min.js ├── src ├── _begin.js ├── _begin_node.js ├── _end.js ├── _end_node.js ├── endian_aware_dataview.js ├── error.js ├── parse.js ├── robj.js ├── rserve.js ├── rsrv.js └── write.js └── tests ├── no_ocap_tests.js ├── ocap_tests.js ├── package.json ├── r_files ├── no_oc.conf ├── oc.conf ├── oc.init.R ├── oc_start.R ├── regular_start.R ├── start └── start_no_ocap └── run_all_tests.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | node_modules 3 | tests/ocap_tests/js/node_modules 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2014 AT&T Intellectual Property 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | JS_COMPILER = ./node_modules/uglify-js/bin/uglifyjs 2 | 3 | all: rserve.js rserve.min.js main.js 4 | 5 | main.js: \ 6 | src/_begin.js \ 7 | src/_begin_node.js \ 8 | src/robj.js \ 9 | src/rsrv.js \ 10 | src/parse.js \ 11 | src/endian_aware_dataview.js \ 12 | src/rserve.js \ 13 | src/error.js \ 14 | src/write.js \ 15 | src/_end.js \ 16 | src/_end_node.js 17 | 18 | rserve.js: \ 19 | src/_begin.js \ 20 | src/robj.js \ 21 | src/rsrv.js \ 22 | src/parse.js \ 23 | src/endian_aware_dataview.js \ 24 | src/rserve.js \ 25 | src/error.js \ 26 | src/write.js \ 27 | src/_end.js 28 | 29 | rserve.min.js: rserve.js Makefile 30 | @rm -f $@ 31 | $(JS_COMPILER) < $< > $@ 32 | chmod -w $@ 33 | 34 | rserve.js: Makefile 35 | echo $^ 36 | @rm -f $@ 37 | cat $(filter %.js,$^) > $@ 38 | ifeq ($(CHECK),1) 39 | jshint $(filter %.js,$(filter-out lib/%.js,$(filter-out %/_begin.js,$(filter-out %/_end.js, $^)))) 40 | endif 41 | chmod -w $@ 42 | 43 | main.js: Makefile 44 | echo $^ 45 | @rm -f $@ 46 | cat $(filter %.js,$^) > $@ 47 | ifeq ($(CHECK),1) 48 | jshint $(filter %.js,$(filter-out lib/%.js,$(filter-out %/_begin.js,$(filter-out %/_end.js, $^)))) 49 | endif 50 | chmod -w $@ 51 | 52 | clean: 53 | rm -f rserve.js rserve.min.js main.js 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A javascript implementation of RServe over WebSockets 2 | 3 | RServe is a protocol for communication with a remote R session. This 4 | package allows you to connect to a running RServe server that is 5 | serving over websockets. This way node.js (and javascript programs in 6 | general) can communicate with an instance of R. 7 | 8 | RServe-js allows a web browser to communicate directly with a running 9 | R process on the other side of the wire. This means it's the 10 | equivalent of a *chainsaw*: there are ways to use it safely, 11 | 12 | 13 | ## Quick tour 14 | 15 | $ git clone https://github.com/att/rserve-js.git 16 | 17 | Start Rserve in web-sockets mode: 18 | 19 | $ cd rserve-js/tests 20 | $ r_files/start_no_ocap 21 | 22 | Run some javascript that connects to [port 8081](https://github.com/cscheid/rserve-js/blob/master/tests/r_files/no_oc.conf): 23 | 24 | $ node 25 | 26 | > r = require('../main.js') 27 | { Robj: 28 | ... 29 | write_into_view: [Function] } 30 | 31 | > r = r.create() 32 | { ocap_mode: false, 33 | ... 34 | resolve_hash: [Function] } 35 | 36 | > r.running 37 | true 38 | 39 | > r.eval('rnorm(10)', function(err, a) { if (err === null) console.log(a); }) 40 | undefined 41 | 42 | { type: 'sexp', 43 | value: 44 | { type: 'double_array', 45 | value: 46 | { '0': -1.5626166190555, 47 | '1': -0.16678360090204197, 48 | '2': 1.362470594733813, 49 | '3': 0.2462241937647293, 50 | '4': -0.6439588002729958, 51 | '5': 1.6695940797441013, 52 | '6': -0.8298271898727629, 53 | '7': -0.14431491982950537, 54 | '8': -0.05561817220786299, 55 | '9': -1.5889826020213365, 56 | BYTES_PER_ELEMENT: 8, 57 | get: [Function: get], 58 | set: [Function: set], 59 | slice: [Function: slice], 60 | subarray: [Function: subarray], 61 | buffer: [Object], 62 | length: 10, 63 | byteOffset: 24, 64 | byteLength: 80 }, 65 | attributes: undefined } } 66 | 67 | 68 | ## Security considerations 69 | 70 | *NB: Rserve, in the mode described above, should only be run in 71 | trusted networks!* `eval`, in that example above, is truly `eval`: 72 | 73 | > // RUNNING WITH SCISSORS 74 | > r.eval('readLines(pipe("ls /etc"))', function(err, x) { if (err === null) console.log(x); }) 75 | 76 | { type: 'sexp', 77 | value: 78 | { type: 'string_array', 79 | value: 80 | [ ... 81 | 'apache2', 82 | ... ], 83 | attributes: undefined } } 84 | 85 | Thankfully, Rserve provides a mode which only exposes a fixed set of 86 | entry points. These are known (for 87 | [historical reasons](http://en.wikipedia.org/wiki/Object-capability_model)) 88 | as *object capabilities*. 89 | 90 | 91 | ## Object capabilities 92 | 93 | There's a demo of object-capability support in `tests/ocap_tests.js` 94 | and `tests/oc.init.R`. Roughly speaking, in object-capability mode, 95 | the server involves an initialization function that returns an object. 96 | This object is sent and is accessible by the Javascript side. 97 | 98 | Critically, the serialization process converts any R functions and 99 | closures to *opaque* objects, which in Javascript are converted to 100 | things that behave exactly like asynchronous function calls 101 | (eg. [`XmlHttpRequest`](http://www.w3.org/TR/XMLHttpRequest/)). These 102 | callable objects are known as *capabilities*, since each of them is a 103 | very specific feature accessible by the remote system. Since 104 | capabilities are functions, they return new values. And since both 105 | Javascript and R have functions as first-class values, *capabilities* 106 | are also first-class in this system. Capabilities 107 | can return other capabilities, and so a system can be designed to 108 | provide, by default, a very limited set of features, which can be 109 | increased when appropriate. 110 | 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rserve", 3 | "version": "2.0.1", 4 | "description": "rserve-js provides a javascript implementation of the websocket Rserve protocol", 5 | "author": "Carlos Scheidegger (http://cscheid.net)", 6 | "contributors": [ 7 | "Gordon Woodhull " 8 | ], 9 | "main": "main.js", 10 | "homepage": "http://github.com/cscheid/rserve-js", 11 | "bugs": "http://github.com/cscheid/rserve-js/issues", 12 | "repository": { 13 | "type": "git", 14 | "url": "http://github.com/cscheid/rserve-js" 15 | }, 16 | "license": "EPL 1.0 or GPL >= 2.0", 17 | "keywords": [ "rserve", "websockets" ], 18 | "devDependencies": { 19 | "uglify-js": "2.3.6" 20 | }, 21 | "dependencies": { 22 | "ws": "1.1.0", 23 | "bluebird": "1.0.3", 24 | "underscore": "1.5.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rserve.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var Rserve = {}; 4 | 5 | (function() { 6 | 7 | function make_basic(type, proto) { 8 | proto = proto || { 9 | json: function() { 10 | throw "json() unsupported for type " + this.type; 11 | } 12 | }; 13 | var wrapped_proto = { 14 | json: function(resolver) { 15 | var result = proto.json.call(this, resolver); 16 | result.r_type = type; 17 | if (!_.isUndefined(this.attributes)) 18 | result.r_attributes = _.object(_.map( 19 | this.attributes.value, 20 | function(v) { return [v.name, v.value.json(resolver)]; })); 21 | return result; 22 | } 23 | }; 24 | return function(v, attrs) { 25 | function r_object() { 26 | this.type = type; 27 | this.value = v; 28 | this.attributes = attrs; 29 | } 30 | r_object.prototype = wrapped_proto; 31 | var result = new r_object(); 32 | return result; 33 | }; 34 | } 35 | 36 | Rserve.Robj = { 37 | "null": function(attributes) { 38 | return { 39 | type: "null", 40 | value: null, 41 | attributes: attributes, 42 | json: function() { return null; } 43 | }; 44 | }, 45 | 46 | clos: function(formals, body, attributes) { 47 | return { 48 | type: "clos", 49 | value: { formals: formals, 50 | body: body }, 51 | attributes: attributes, 52 | json: function() { throw "json() unsupported for type clos"; } 53 | }; 54 | }, 55 | 56 | vector: make_basic("vector", { 57 | json: function(resolver) { 58 | var values = _.map(this.value, function (x) { return x.json(resolver); }); 59 | if (_.isUndefined(this.attributes)) { 60 | return values; 61 | } else { 62 | // FIXME: there is no reason why names should be the first or only 63 | // attribute, so the code should really look 64 | // for "names" and not cry if it doesn't exist 65 | if (this.attributes.value[0].name == "names") { 66 | var keys = this.attributes.value[0].value.value; 67 | var result = {}; 68 | _.each(keys, function(key, i) { 69 | result[key] = values[i]; 70 | }); 71 | return result; 72 | } 73 | // FIXME: how can we pass other important attributes 74 | // like "class" ? 75 | return values; 76 | } 77 | } 78 | }), 79 | symbol: make_basic("symbol", { 80 | json: function() { 81 | return this.value; 82 | } 83 | }), 84 | list: make_basic("list"), 85 | lang: make_basic("lang", { 86 | json: function(resolver) { 87 | var values = _.map(this.value, function (x) { return x.json(resolver); }); 88 | if (_.isUndefined(this.attributes)) { 89 | return values; 90 | } else { 91 | // FIXME: lang doens't have "names" attribute since 92 | // names are sent as tags (langs are pairlists) 93 | // so this seems superfluous (it is dangerous 94 | // if lang ever had attributes since there is 95 | // no reason to fail in that case) 96 | if(this.attributes.value[0].name!="names") 97 | throw "expected names here"; 98 | var keys = this.attributes.value[0].value.value; 99 | var result = {}; 100 | _.each(keys, function(key, i) { 101 | result[key] = values[i]; 102 | }); 103 | return result; 104 | } 105 | } 106 | }), 107 | tagged_list: make_basic("tagged_list", { 108 | json: function(resolver) { 109 | function classify_list(list) { 110 | if (_.all(list, function(elt) { return elt.name === null; })) { 111 | return "plain_list"; 112 | } else if (_.all(list, function(elt) { return elt.name !== null; })) { 113 | return "plain_object"; 114 | } else 115 | return "mixed_list"; 116 | } 117 | var list = this.value.slice(1); 118 | switch (classify_list(list)) { 119 | case "plain_list": 120 | return _.map(list, function(elt) { return elt.value.json(resolver); }); 121 | case "plain_object": 122 | return _.object(_.map(list, function(elt) { 123 | return [elt.name, elt.value.json(resolver)]; 124 | })); 125 | case "mixed_list": 126 | return list; 127 | default: 128 | throw "Internal Error"; 129 | } 130 | } 131 | }), 132 | tagged_lang: make_basic("tagged_lang", { 133 | json: function(resolver) { 134 | var pair_vec = _.map(this.value, function(elt) { return [elt.name, elt.value.json(resolver)]; }); 135 | return pair_vec; 136 | } 137 | }), 138 | vector_exp: make_basic("vector_exp"), 139 | int_array: make_basic("int_array", { 140 | json: function() { 141 | if(this.attributes && this.attributes.type==='tagged_list' 142 | && this.attributes.value[0].name==='levels' 143 | && this.attributes.value[0].value.type==='string_array') { 144 | var levels = this.attributes.value[0].value.value; 145 | var arr = _.map(this.value, function(factor) { return levels[factor-1]; }); 146 | arr.levels = levels; 147 | return arr; 148 | } 149 | else { 150 | if (this.value.length === 1) 151 | return this.value[0]; 152 | else 153 | return this.value; 154 | } 155 | } 156 | }), 157 | double_array: make_basic("double_array", { 158 | json: function() { 159 | if (this.value.length === 1 && _.isUndefined(this.attributes)) 160 | return this.value[0]; 161 | else 162 | return this.value; 163 | } 164 | }), 165 | string_array: make_basic("string_array", { 166 | json: function(resolver) { 167 | if (this.value.length === 1) { 168 | if (_.isUndefined(this.attributes)) 169 | return this.value[0]; 170 | if (this.attributes.value[0].name === 'class' && 171 | this.attributes.value[0].value.value.indexOf("javascript_function") !== -1) 172 | return resolver(this.value[0]); 173 | return this.value; 174 | } else 175 | return this.value; 176 | } 177 | }), 178 | bool_array: make_basic("bool_array", { 179 | json: function() { 180 | if (this.value.length === 1 && _.isUndefined(this.attributes)) 181 | return this.value[0]; 182 | else 183 | return this.value; 184 | } 185 | }), 186 | raw: make_basic("raw", { 187 | json: function() { 188 | if (this.value.length === 1 && _.isUndefined(this.attributes)) 189 | return this.value[0]; 190 | else 191 | return this.value; 192 | } 193 | }), 194 | string: make_basic("string", { 195 | json: function() { 196 | return this.value; 197 | } 198 | }) 199 | }; 200 | 201 | })(); 202 | // Simple constants and functions are defined here, 203 | // in correspondence with Rserve's Rsrv.h 204 | 205 | Rserve.Rsrv = { 206 | PAR_TYPE: function(x) { return x & 255; }, 207 | PAR_LEN: function(x) { return x >>> 8; }, 208 | PAR_LENGTH: function(x) { return x >>> 8; }, 209 | par_parse: function(x) { return [Rserve.Rsrv.PAR_TYPE(x), Rserve.Rsrv.PAR_LEN(x)]; }, 210 | SET_PAR: function(ty, len) { return ((len & 0xffffff) << 8 | (ty & 255)); }, 211 | CMD_STAT: function(x) { return (x >>> 24) & 127; }, 212 | SET_STAT: function(x, s) { return x | ((s & 127) << 24); }, 213 | 214 | IS_OOB_SEND: function(x) { return (x & 0xffff000) === Rserve.Rsrv.OOB_SEND; }, 215 | IS_OOB_MSG: function(x) { return (x & 0xffff000) === Rserve.Rsrv.OOB_MSG; }, 216 | OOB_USR_CODE: function(x) { return x & 0xfff; }, 217 | 218 | CMD_RESP : 0x10000, 219 | RESP_OK : 0x10000 | 0x0001, 220 | RESP_ERR : 0x10000 | 0x0002, 221 | OOB_SEND : 0x20000 | 0x1000, 222 | OOB_MSG : 0x20000 | 0x2000, 223 | ERR_auth_failed : 0x41, 224 | ERR_conn_broken : 0x42, 225 | ERR_inv_cmd : 0x43, 226 | ERR_inv_par : 0x44, 227 | ERR_Rerror : 0x45, 228 | ERR_IOerror : 0x46, 229 | ERR_notOpen : 0x47, 230 | ERR_accessDenied : 0x48, 231 | ERR_unsupportedCmd : 0x49, 232 | ERR_unknownCmd : 0x4a, 233 | ERR_data_overflow : 0x4b, 234 | ERR_object_too_big : 0x4c, 235 | ERR_out_of_mem : 0x4d, 236 | ERR_ctrl_closed : 0x4e, 237 | ERR_session_busy : 0x50, 238 | ERR_detach_failed : 0x51, 239 | ERR_disabled : 0x61, 240 | ERR_unavailable : 0x62, 241 | ERR_cryptError : 0x63, 242 | ERR_securityClose : 0x64, 243 | 244 | CMD_login : 0x001, 245 | CMD_voidEval : 0x002, 246 | CMD_eval : 0x003, 247 | CMD_shutdown : 0x004, 248 | CMD_switch : 0x005, 249 | CMD_keyReq : 0x006, 250 | CMD_secLogin : 0x007, 251 | CMD_OCcall : 0x00f, 252 | CMD_openFile : 0x010, 253 | CMD_createFile : 0x011, 254 | CMD_closeFile : 0x012, 255 | CMD_readFile : 0x013, 256 | CMD_writeFile : 0x014, 257 | CMD_removeFile : 0x015, 258 | CMD_setSEXP : 0x020, 259 | CMD_assignSEXP : 0x021, 260 | CMD_detachSession : 0x030, 261 | CMD_detachedVoidEval : 0x031, 262 | CMD_attachSession : 0x032, 263 | CMD_ctrl : 0x40, 264 | CMD_ctrlEval : 0x42, 265 | CMD_ctrlSource : 0x45, 266 | CMD_ctrlShutdown : 0x44, 267 | CMD_setBufferSize : 0x081, 268 | CMD_setEncoding : 0x082, 269 | CMD_SPECIAL_MASK : 0xf0, 270 | CMD_serEval : 0xf5, 271 | CMD_serAssign : 0xf6, 272 | CMD_serEEval : 0xf7, 273 | 274 | DT_INT : 1, 275 | DT_CHAR : 2, 276 | DT_DOUBLE : 3, 277 | DT_STRING : 4, 278 | DT_BYTESTREAM : 5, 279 | DT_SEXP : 10, 280 | DT_ARRAY : 11, 281 | DT_LARGE : 64, 282 | 283 | XT_NULL : 0, 284 | XT_INT : 1, 285 | XT_DOUBLE : 2, 286 | XT_STR : 3, 287 | XT_LANG : 4, 288 | XT_SYM : 5, 289 | XT_BOOL : 6, 290 | XT_S4 : 7, 291 | XT_VECTOR : 16, 292 | XT_LIST : 17, 293 | XT_CLOS : 18, 294 | XT_SYMNAME : 19, 295 | XT_LIST_NOTAG : 20, 296 | XT_LIST_TAG : 21, 297 | XT_LANG_NOTAG : 22, 298 | XT_LANG_TAG : 23, 299 | XT_VECTOR_EXP : 26, 300 | XT_VECTOR_STR : 27, 301 | XT_ARRAY_INT : 32, 302 | XT_ARRAY_DOUBLE : 33, 303 | XT_ARRAY_STR : 34, 304 | XT_ARRAY_BOOL_UA : 35, 305 | XT_ARRAY_BOOL : 36, 306 | XT_RAW : 37, 307 | XT_ARRAY_CPLX : 38, 308 | XT_UNKNOWN : 48, 309 | XT_LARGE : 64, 310 | XT_HAS_ATTR : 128, 311 | 312 | BOOL_TRUE : 1, 313 | BOOL_FALSE : 0, 314 | BOOL_NA : 2, 315 | 316 | GET_XT: function(x) { return x & 63; }, 317 | GET_DT: function(x) { return x & 63; }, 318 | HAS_ATTR: function(x) { return (x & Rserve.Rsrv.XT_HAS_ATTR) > 0; }, 319 | IS_LARGE: function(x) { return (x & Rserve.Rsrv.XT_LARGE) > 0; }, 320 | 321 | // # FIXME A WHOLE LOT OF MACROS HERE WHICH ARE PROBABLY IMPORTANT 322 | // ############################################################################## 323 | 324 | status_codes: { 325 | 0x41 : "ERR_auth_failed" , 326 | 0x42 : "ERR_conn_broken" , 327 | 0x43 : "ERR_inv_cmd" , 328 | 0x44 : "ERR_inv_par" , 329 | 0x45 : "ERR_Rerror" , 330 | 0x46 : "ERR_IOerror" , 331 | 0x47 : "ERR_notOpen" , 332 | 0x48 : "ERR_accessDenied" , 333 | 0x49 : "ERR_unsupportedCmd", 334 | 0x4a : "ERR_unknownCmd" , 335 | 0x4b : "ERR_data_overflow" , 336 | 0x4c : "ERR_object_too_big", 337 | 0x4d : "ERR_out_of_mem" , 338 | 0x4e : "ERR_ctrl_closed" , 339 | 0x50 : "ERR_session_busy" , 340 | 0x51 : "ERR_detach_failed" , 341 | 0x61 : "ERR_disabled" , 342 | 0x62 : "ERR_unavailable" , 343 | 0x63 : "ERR_cryptError" , 344 | 0x64 : "ERR_securityClose" 345 | } 346 | }; 347 | (function() { 348 | 349 | function read(m) 350 | { 351 | var handlers = {}; 352 | 353 | function lift(f, amount) { 354 | return function(attributes, length) { 355 | return [f.call(that, attributes, length), amount || length]; 356 | }; 357 | } 358 | 359 | function bind(m, f) { 360 | return function(attributes, length) { 361 | var t = m.call(that, attributes, length); 362 | var t2 = f(t[0])(attributes, length - t[1]); 363 | return [t2[0], t[1] + t2[1]]; 364 | }; 365 | } 366 | 367 | function unfold(f) { 368 | return function(attributes, length) { 369 | var result = []; 370 | var old_length = length; 371 | while (length > 0) { 372 | var t = f.call(that, attributes, length); 373 | result.push(t[0]); 374 | length -= t[1]; 375 | } 376 | return [result, old_length]; 377 | }; 378 | } 379 | 380 | function decodeRString(s) { 381 | // R encodes NA as a string containing just 0xff 382 | if(s.length === 1 && s.charCodeAt(0) === 255) 383 | return null; 384 | // UTF-8 to UTF-16 385 | // http://monsur.hossa.in/2012/07/20/utf-8-in-javascript.html 386 | // also, we don't want to lose the value when reporting an error in decoding 387 | try { 388 | return decodeURIComponent(escape(s)); 389 | } 390 | catch(xep) { 391 | throw new Error('Invalid UTF8: ' + s); 392 | } 393 | } 394 | 395 | var that = { 396 | offset: 0, 397 | data_view: m.make(Rserve.EndianAwareDataView), 398 | msg: m, 399 | 400 | ////////////////////////////////////////////////////////////////////// 401 | 402 | read_int: function() { 403 | var old_offset = this.offset; 404 | this.offset += 4; 405 | return this.data_view.getInt32(old_offset); 406 | }, 407 | read_string: function(length) { 408 | // FIXME SLOW 409 | var result = ""; 410 | while (length--) { 411 | var c = this.data_view.getInt8(this.offset++); 412 | if (c) result = result + String.fromCharCode(c); 413 | } 414 | return decodeRString(result); 415 | }, 416 | read_stream: function(length) { 417 | var old_offset = this.offset; 418 | this.offset += length; 419 | return this.msg.view(old_offset, length); 420 | }, 421 | read_int_vector: function(length) { 422 | var old_offset = this.offset; 423 | this.offset += length; 424 | return this.msg.make(Int32Array, old_offset, length); 425 | }, 426 | read_double_vector: function(length) { 427 | var old_offset = this.offset; 428 | this.offset += length; 429 | return this.msg.make(Float64Array, old_offset, length); 430 | }, 431 | 432 | ////////////////////////////////////////////////////////////////////// 433 | // these are members of the reader monad 434 | 435 | read_null: lift(function(a, l) { return Rserve.Robj.null(a); }), 436 | 437 | read_unknown: lift(function(a, l) { 438 | this.offset += l; 439 | return Rserve.Robj.null(a); 440 | }), 441 | 442 | read_string_array: function(attributes, length) { 443 | var a = this.read_stream(length).make(Uint8Array); 444 | var result = []; 445 | var current_str = ""; 446 | for (var i=0; i>> 24; 620 | var length = header[1], length_high = header[3]; 621 | var msg_id = header[2]; 622 | result.header = [resp, status_code, msg_id]; 623 | 624 | if(length_high) { 625 | result.ok = false; 626 | result.message = "rserve.js cannot handle messages larger than 4GB"; 627 | return result; 628 | } 629 | 630 | var full_msg_length = length + 16; // header length + data length 631 | if(full_msg_length > msg.byteLength) { 632 | incomplete_.push(msg); 633 | incomplete_header_ = header; 634 | msg_bytes_ = full_msg_length; 635 | remaining_ = msg_bytes_ - msg.byteLength; 636 | result.header = header; 637 | result.ok = true; 638 | result.incomplete = true; 639 | return result; 640 | } 641 | 642 | if (resp === Rserve.Rsrv.RESP_ERR) { 643 | result.ok = false; 644 | result.status_code = status_code; 645 | result.message = "ERROR FROM R SERVER: " + (Rserve.Rsrv.status_codes[status_code] || 646 | status_code) 647 | + " " + result.header[0] + " " + result.header[1] 648 | + " " + msg.byteLength 649 | + " " + msg; 650 | return result; 651 | } 652 | 653 | if (!( resp === Rserve.Rsrv.RESP_OK || Rserve.Rsrv.IS_OOB_SEND(resp) || Rserve.Rsrv.IS_OOB_MSG(resp))) { 654 | result.ok = false; 655 | result.message = "Unexpected response from Rserve: " + resp + " status: " + Rserve.Rsrv.status_codes[status_code]; 656 | return result; 657 | } 658 | try { 659 | result.payload = parse_payload(msg); 660 | result.ok = true; 661 | } catch (e) { 662 | result.ok = false; 663 | result.message = e.message; 664 | } 665 | return result; 666 | } 667 | 668 | function parse_payload(msg) 669 | { 670 | var payload = Rserve.my_ArrayBufferView(msg, 16, msg.byteLength - 16); 671 | if (payload.length === 0) 672 | return null; 673 | 674 | var reader = read(payload); 675 | 676 | var d = reader.read_int(); 677 | var _ = Rserve.Rsrv.par_parse(d); 678 | var t = _[0], l = _[1]; 679 | if (Rserve.Rsrv.IS_LARGE(t)) { 680 | var more_length = reader.read_int(); 681 | l += more_length * Math.pow(2, 24); 682 | if (l > (Math.pow(2, 32))) { // resist the 1 << 32 temptation here! 683 | // total_length is greater than 2^32.. bail out because of node limits 684 | // even though in theory we could go higher than that. 685 | throw new Error("Payload too large: " + l + " bytes"); 686 | } 687 | t &= ~64; 688 | } 689 | if (t === Rserve.Rsrv.DT_INT) { 690 | return { type: "int", value: reader.read_int() }; 691 | } else if (t === Rserve.Rsrv.DT_STRING) { 692 | return { type: "string", value: reader.read_string(l) }; 693 | } else if (t === Rserve.Rsrv.DT_BYTESTREAM) { // NB this returns a my_ArrayBufferView() 694 | return { type: "stream", value: reader.read_stream(l) }; 695 | } else if (t === Rserve.Rsrv.DT_SEXP) { 696 | _ = reader.read_sexp(); 697 | var sexp = _[0], l2 = _[1]; 698 | return { type: "sexp", value: sexp }; 699 | } else 700 | throw new Rserve.RserveError("Bad type for parse? " + t + " " + l, -1); 701 | } 702 | 703 | Rserve.parse_websocket_frame = parse; 704 | Rserve.parse_payload = parse_payload; 705 | 706 | })(); 707 | // we want an endian aware dataview mostly because ARM can be big-endian, and 708 | // that might put us in trouble wrt handheld devices. 709 | ////////////////////////////////////////////////////////////////////////////// 710 | 711 | (function() { 712 | var _is_little_endian; 713 | 714 | (function() { 715 | var x = new ArrayBuffer(4); 716 | var bytes = new Uint8Array(x), 717 | words = new Uint32Array(x); 718 | bytes[0] = 1; 719 | if (words[0] === 1) { 720 | _is_little_endian = true; 721 | } else if (words[0] === 16777216) { 722 | _is_little_endian = false; 723 | } else { 724 | throw "we're bizarro endian, refusing to continue"; 725 | } 726 | })(); 727 | 728 | Rserve.EndianAwareDataView = (function() { 729 | 730 | var proto = { 731 | 'setInt8': function(i, v) { return this.view.setInt8(i, v); }, 732 | 'setUint8': function(i, v) { return this.view.setUint8(i, v); }, 733 | 'getInt8': function(i) { return this.view.getInt8(i); }, 734 | 'getUint8': function(i) { return this.view.getUint8(i); } 735 | }; 736 | 737 | var setters = ['setInt32', 'setInt16', 'setUint32', 'setUint16', 738 | 'setFloat32', 'setFloat64']; 739 | var getters = ['getInt32', 'getInt16', 'getUint32', 'getUint16', 740 | 'getFloat32', 'getFloat64']; 741 | 742 | for (var i=0; i this.buffer.byteLength) 813 | throw new Error("Rserve.my_ArrayBufferView.view: bounds error: size: " + 814 | this.buffer.byteLength + " offset: " + ofs + " length: " + new_length); 815 | return Rserve.my_ArrayBufferView( 816 | this.buffer, ofs, new_length); 817 | } 818 | }; 819 | }; 820 | 821 | })(this); 822 | (function() { 823 | 824 | function _encode_command(command, buffer, msg_id) { 825 | if (!_.isArray(buffer)) 826 | buffer = [buffer]; 827 | if (!msg_id) msg_id = 0; 828 | var length = _.reduce(buffer, 829 | function(memo, val) { 830 | return memo + val.byteLength; 831 | }, 0), 832 | big_buffer = new ArrayBuffer(16 + length), 833 | view = new Rserve.EndianAwareDataView(big_buffer); 834 | view.setInt32(0, command); 835 | view.setInt32(4, length); 836 | view.setInt32(8, msg_id); 837 | view.setInt32(12, 0); 838 | var offset = 16; 839 | _.each(buffer, function(b) { 840 | var source_array = new Uint8Array(b); 841 | for (var i=0; i= 2^31. 908 | if (sz > 16777215) { 909 | var buffer = new ArrayBuffer(sz + 8); 910 | var view = Rserve.my_ArrayBufferView(buffer); 911 | // can't left shift value here because value will have bit 32 set and become signed.. 912 | view.data_view().setInt32(0, Rserve.Rsrv.DT_SEXP + ((sz & 16777215) * Math.pow(2, 8)) + Rserve.Rsrv.DT_LARGE); 913 | // but *can* right shift because we assume sz is less than 2^31 or so to begin with 914 | view.data_view().setInt32(4, sz >>> 24); 915 | Rserve.write_into_view(value, view.skip(8), forced_type, convert_to_hash); 916 | return buffer; 917 | } else { 918 | var buffer = new ArrayBuffer(sz + 4); 919 | var view = Rserve.my_ArrayBufferView(buffer); 920 | view.data_view().setInt32(0, Rserve.Rsrv.DT_SEXP + (sz << 8)); 921 | Rserve.write_into_view(value, view.skip(4), forced_type, convert_to_hash); 922 | return buffer; 923 | } 924 | } 925 | 926 | function hand_shake(msg) 927 | { 928 | msg = msg.data; 929 | if (typeof msg === 'string') { 930 | if (msg.substr(0,4) !== 'Rsrv') { 931 | handle_error("server is not an RServe instance", -1); 932 | } else if (msg.substr(4, 4) !== '0103') { 933 | handle_error("sorry, rserve only speaks the 0103 version of the R server protocol", -1); 934 | } else if (msg.substr(8, 4) !== 'QAP1') { 935 | handle_error("sorry, rserve only speaks QAP1", -1); 936 | } else { 937 | received_handshake = true; 938 | if (opts.login) 939 | result.login(opts.login); 940 | result.running = true; 941 | onconnect && onconnect.call(result); 942 | } 943 | } else { 944 | var view = new DataView(msg); 945 | var header = String.fromCharCode(view.getUint8(0)) + 946 | String.fromCharCode(view.getUint8(1)) + 947 | String.fromCharCode(view.getUint8(2)) + 948 | String.fromCharCode(view.getUint8(3)); 949 | 950 | if (header === 'RsOC') { 951 | received_handshake = true; 952 | result.ocap_mode = true; 953 | result.bare_ocap = Rserve.parse_payload(msg).value; 954 | result.ocap = Rserve.wrap_ocap(result, result.bare_ocap); 955 | result.running = true; 956 | onconnect && onconnect.call(result); 957 | } else 958 | handle_error("Unrecognized server answer: " + header, -1); 959 | } 960 | } 961 | 962 | socket.onclose = function(msg) { 963 | result.running = false; 964 | result.closed = true; 965 | opts.on_close && opts.on_close(msg); 966 | }; 967 | 968 | socket.onmessage = function(msg) { 969 | // node.js Buffer vs ArrayBuffer workaround 970 | if (msg.data.constructor.name === 'Buffer') 971 | msg.data = (new Uint8Array(msg.data)).buffer; 972 | if (opts.debug) 973 | opts.debug.message_in && opts.debug.message_in(msg); 974 | if (!received_handshake) { 975 | hand_shake(msg); 976 | return; 977 | } 978 | if (typeof msg.data === 'string') { 979 | opts.on_raw_string && opts.on_raw_string(msg.data); 980 | return; 981 | } 982 | var v = Rserve.parse_websocket_frame(msg.data); 983 | if(v.incomplete) 984 | return; 985 | var msg_id = v.header[2], cmd = v.header[0] & 0xffffff; 986 | var queue = _.find(queues, function(queue) { return queue.msg_id == msg_id; }); 987 | // console.log("onmessage, queue=" + (queue ? queue.name : "") + ", ok= " + v.ok+ ", cmd=" + cmd +", msg_id="+ msg_id); 988 | // FIXME: in theory we should not need a fallback, but in case we miss some 989 | // odd edge case, we revert to the old behavior. 990 | // The way things work, the queue will be undefined only for OOB messages: 991 | // SEND doesn't need reply, so it's irrelevant, MSG is handled separately below and 992 | // enforces the right queue. 993 | if (!queue) queue = queues[0]; 994 | if (!v.ok) { 995 | queue.result_callback([v.message, v.status_code], undefined); 996 | // handle_error(v.message, v.status_code); 997 | } else if (cmd === Rserve.Rsrv.RESP_OK) { 998 | queue.result_callback(null, v.payload); 999 | } else if (Rserve.Rsrv.IS_OOB_SEND(cmd)) { 1000 | opts.on_data && opts.on_data(v.payload); 1001 | } else if (Rserve.Rsrv.IS_OOB_MSG(cmd)) { 1002 | // OOB MSG may use random msg_id, so we have to use the USR_CODE to detect the right queue 1003 | // FIXME: we may want to consider adjusting the protocol specs to require msg_id 1004 | // to be retained by OOB based on the outer OCcall message (thus inheriting 1005 | // the msg_id), but curretnly it's not mandated. 1006 | queue = (Rserve.Rsrv.OOB_USR_CODE(cmd) > 255) ? compute_queue : ctrl_queue; 1007 | // console.log("OOB MSG result on queue "+ queue.name); 1008 | var p; 1009 | try { 1010 | p = Rserve.wrap_all_ocaps(result, v.payload); // .value.json(result.resolve_hash); 1011 | } catch (e) { 1012 | _send_cmd_now(Rserve.Rsrv.RESP_ERR | cmd, 1013 | _encode_string(String(e)), msg_id); 1014 | return; 1015 | } 1016 | if(_.isString(p[0])) { 1017 | if (_.isUndefined(opts.on_oob_message)) { 1018 | _send_cmd_now(Rserve.Rsrv.RESP_ERR | cmd, 1019 | _encode_string("No handler installed"), msg_id); 1020 | } else { 1021 | queue.in_oob_message = true; 1022 | // breaking changes here: it appears that the callback had its arguments 1023 | // reversed from the standard (error, message), and was passing the message 1024 | // even on error 1025 | opts.on_oob_message(v.payload, function(error, result) { 1026 | if (!queue.in_oob_message) { 1027 | handle_error("Don't call oob_message_handler more than once."); 1028 | return; 1029 | } 1030 | queue.in_oob_message = false; 1031 | if(error) { 1032 | _send_cmd_now(cmd | Rserve.Rsrv.RESP_ERR, _encode_string(error), msg_id); 1033 | } 1034 | else { 1035 | _send_cmd_now(cmd | Rserve.Rsrv.RESP_OK, _encode_string(result), msg_id); 1036 | } 1037 | bump_queues(); 1038 | }); 1039 | } 1040 | } 1041 | else if(_.isFunction(p[0])) { 1042 | if(!result.ocap_mode) { 1043 | _send_cmd_now(Rserve.Rsrv.RESP_ERROR | cmd, 1044 | _encode_string("JavaScript function calls only allowed in ocap mode"), msg_id); 1045 | } 1046 | else { 1047 | var captured_function = p[0], params = p.slice(1); 1048 | params.push(function(err, result) { 1049 | if (err) { 1050 | _send_cmd_now(Rserve.Rsrv.RESP_ERR | cmd, _encode_value(err), msg_id); 1051 | } else { 1052 | _send_cmd_now(cmd, _encode_value(result), msg_id); 1053 | } 1054 | }); 1055 | captured_function.apply(undefined, params); 1056 | } 1057 | } 1058 | else { 1059 | _send_cmd_now(Rserve.Rsrv.RESP_ERROR | cmd, 1060 | _encode_string("Unknown oob message type: " + typeof(p[0]))); 1061 | } 1062 | } else { 1063 | handle_error("Internal Error, parse returned unexpected type " + v.header[0], -1); 1064 | } 1065 | }; 1066 | 1067 | function _send_cmd_now(command, buffer, msg_id) { 1068 | var big_buffer = _encode_command(command, buffer, msg_id); 1069 | if (opts.debug) 1070 | opts.debug.message_out && opts.debug.message_out(big_buffer[0], command); 1071 | socket.send(big_buffer); 1072 | return big_buffer; 1073 | }; 1074 | 1075 | var ctrl_queue = { 1076 | queue: [], 1077 | in_oob_message: false, 1078 | awaiting_result: false, 1079 | msg_id: 0, 1080 | name: "control" 1081 | }; 1082 | 1083 | var compute_queue = { 1084 | queue: [], 1085 | in_oob_message: false, 1086 | awaiting_result: false, 1087 | msg_id: 1, 1088 | name: "compute" 1089 | }; 1090 | 1091 | // the order matters - the first queue is used if the association cannot be determined from the msg_id/cmd 1092 | var queues = [ ctrl_queue, compute_queue ]; 1093 | 1094 | function queue_can_send(queue) { return !queue.in_oob_message && !queue.awaiting_result && queue.queue.length; } 1095 | 1096 | function bump_queues() { 1097 | var available = _.filter(queues, queue_can_send); 1098 | // nothing in the queues (or all busy)? get out 1099 | if (!available.length) return; 1100 | if (result.closed) { 1101 | handle_error("Cannot send messages on a closed socket!", -1); 1102 | } else { 1103 | var queue = _.sortBy(available, function(queue) { return queue.queue[0].timestamp; })[0]; 1104 | var lst = queue.queue.shift(); 1105 | queue.result_callback = lst.callback; 1106 | queue.awaiting_result = true; 1107 | if (opts.debug) 1108 | opts.debug.message_out && opts.debug.message_out(lst.buffer, lst.command); 1109 | socket.send(lst.buffer); 1110 | } 1111 | } 1112 | 1113 | function enqueue(buffer, k, command, queue) { 1114 | queue.queue.push({ 1115 | buffer: buffer, 1116 | callback: function(error, result) { 1117 | queue.awaiting_result = false; 1118 | bump_queues(); 1119 | k(error, result); 1120 | }, 1121 | command: command, 1122 | timestamp: Date.now() 1123 | }); 1124 | bump_queues(); 1125 | }; 1126 | 1127 | function _cmd(command, buffer, k, string, queue) { 1128 | // default to the first queue - only used in non-OCAP mode which doesn't support multiple queues 1129 | if (!queue) queue = queues[0]; 1130 | 1131 | k = k || function() {}; 1132 | var big_buffer = _encode_command(command, buffer, queue.msg_id); 1133 | return enqueue(big_buffer, k, string, queue); 1134 | }; 1135 | 1136 | result = { 1137 | ocap_mode: false, 1138 | running: false, 1139 | closed: false, 1140 | close: function() { 1141 | socket.close(); 1142 | }, 1143 | 1144 | ////////////////////////////////////////////////////////////////////// 1145 | // non-ocap mode 1146 | 1147 | login: function(command, k) { 1148 | _cmd(Rserve.Rsrv.CMD_login, _encode_string(command), k, command); 1149 | }, 1150 | eval: function(command, k) { 1151 | _cmd(Rserve.Rsrv.CMD_eval, _encode_string(command), k, command); 1152 | }, 1153 | createFile: function(command, k) { 1154 | _cmd(Rserve.Rsrv.CMD_createFile, _encode_string(command), k, command); 1155 | }, 1156 | writeFile: function(chunk, k) { 1157 | _cmd(Rserve.Rsrv.CMD_writeFile, _encode_bytes(chunk), k, ""); 1158 | }, 1159 | closeFile: function(k) { 1160 | _cmd(Rserve.Rsrv.CMD_closeFile, new ArrayBuffer(0), k, ""); 1161 | }, 1162 | set: function(key, value, k) { 1163 | _cmd(Rserve.Rsrv.CMD_setSEXP, [_encode_string(key), _encode_value(value)], k, ""); 1164 | }, 1165 | 1166 | ////////////////////////////////////////////////////////////////////// 1167 | // ocap mode 1168 | 1169 | OCcall: function(ocap, values, k) { 1170 | var is_ocap = false, str; 1171 | try { 1172 | is_ocap |= ocap.r_attributes['class'] === 'OCref'; 1173 | str = ocap[0]; 1174 | } catch (e) {}; 1175 | if(!is_ocap) { 1176 | try { 1177 | is_ocap |= ocap.attributes.value[0].value.value[0] === 'OCref'; 1178 | str = ocap.value[0]; 1179 | } catch (e) {}; 1180 | } 1181 | if (!is_ocap) { 1182 | k(new Error("Expected an ocap, instead got " + ocap), undefined); 1183 | return; 1184 | } 1185 | var params = [str]; 1186 | params.push.apply(params, values); 1187 | // determine the proper queue from the OCAP prefix 1188 | var queue = (str.charCodeAt(0) == 64) ? compute_queue : ctrl_queue; 1189 | _cmd(Rserve.Rsrv.CMD_OCcall, _encode_value(params, Rserve.Rsrv.XT_LANG_NOTAG), 1190 | k, "", queue); 1191 | }, 1192 | 1193 | wrap_ocap: function(ocap) { 1194 | return Rserve.wrap_ocap(this, ocap); 1195 | }, 1196 | 1197 | resolve_hash: function(hash) { 1198 | if (!(hash in captured_functions)) 1199 | throw new Error("hash " + hash + " not found."); 1200 | return captured_functions[hash]; 1201 | } 1202 | }; 1203 | return result; 1204 | }; 1205 | 1206 | Rserve.wrap_all_ocaps = function(s, v) { 1207 | v = v.value.json(s.resolve_hash); 1208 | function replace(obj) { 1209 | var result = obj; 1210 | if (_.isArray(obj) && 1211 | obj.r_attributes && 1212 | obj.r_attributes['class'] == 'OCref') { 1213 | return Rserve.wrap_ocap(s, obj); 1214 | } else if (_.isArray(obj)) { 1215 | result = _.map(obj, replace); 1216 | result.r_type = obj.r_type; 1217 | result.r_attributes = obj.r_attributes; 1218 | } else if (_.isTypedArray(obj)) { 1219 | return obj; 1220 | } else if (_.isFunction(obj)) { 1221 | return obj; 1222 | } else if (obj && !_.isUndefined(obj.byteLength) && !_.isUndefined(obj.slice)) { // ArrayBuffer 1223 | return obj; 1224 | } else if (_.isObject(obj)) { 1225 | result = _.object(_.map(obj, function(v, k) { 1226 | return [k, replace(v)]; 1227 | })); 1228 | } 1229 | return result; 1230 | } 1231 | return replace(v); 1232 | }; 1233 | 1234 | Rserve.wrap_ocap = function(s, ocap) { 1235 | var wrapped_ocap = function() { 1236 | var values = _.toArray(arguments); 1237 | // common error (tho this won't catch the case where last arg is a function) 1238 | if(!values.length || !_.isFunction(values[values.length-1])) 1239 | throw new Error("forgot to pass continuation to ocap"); 1240 | var k = values.pop(); 1241 | s.OCcall(ocap, values, function(err, v) { 1242 | if (!_.isUndefined(v)) 1243 | v = Rserve.wrap_all_ocaps(s, v); 1244 | k(err, v); 1245 | }); 1246 | }; 1247 | wrapped_ocap.bare_ocap = ocap; 1248 | return wrapped_ocap; 1249 | }; 1250 | 1251 | })(); 1252 | Rserve.RserveError = function(message, status_code) { 1253 | this.name = "RserveError"; 1254 | this.message = message; 1255 | this.status_code = status_code; 1256 | }; 1257 | 1258 | Rserve.RserveError.prototype = Object.create(Error); 1259 | Rserve.RserveError.prototype.constructor = Rserve.RserveError; 1260 | (function () { 1261 | 1262 | _.mixin({ 1263 | isTypedArray: function(v) { 1264 | if (!_.isObject(v)) 1265 | return false; 1266 | return !_.isUndefined(v.byteLength) && !_.isUndefined(v.BYTES_PER_ELEMENT); 1267 | } 1268 | }); 1269 | 1270 | // type_id tries to match some javascript values to Rserve value types 1271 | Rserve.type_id = function(value) 1272 | { 1273 | if (_.isNull(value) || _.isUndefined(value)) 1274 | return Rserve.Rsrv.XT_NULL; 1275 | var type_dispatch = { 1276 | "boolean": Rserve.Rsrv.XT_ARRAY_BOOL, 1277 | "number": Rserve.Rsrv.XT_ARRAY_DOUBLE, 1278 | "string": Rserve.Rsrv.XT_ARRAY_STR // base strings need to be array_str or R gets confused? 1279 | }; 1280 | if (!_.isUndefined(type_dispatch[typeof value])) 1281 | return type_dispatch[typeof value]; 1282 | 1283 | // typed arrays 1284 | if (_.isTypedArray(value)) 1285 | return Rserve.Rsrv.XT_ARRAY_DOUBLE; 1286 | 1287 | // arraybuffers 1288 | if (!_.isUndefined(value.byteLength) && !_.isUndefined(value.slice)) 1289 | return Rserve.Rsrv.XT_RAW; 1290 | 1291 | // lists of strings (important for tags) 1292 | if (_.isArray(value) && _.all(value, function(el) { return typeof el === 'string'; })) 1293 | return Rserve.Rsrv.XT_ARRAY_STR; 1294 | 1295 | if (_.isArray(value) && _.all(value, function(el) { return typeof el === 'boolean'; })) 1296 | return Rserve.Rsrv.XT_ARRAY_BOOL; 1297 | 1298 | // arbitrary lists 1299 | if (_.isArray(value)) 1300 | return Rserve.Rsrv.XT_VECTOR; 1301 | 1302 | // functions get passed as an array_str with extra attributes 1303 | if (_.isFunction(value)) 1304 | return Rserve.Rsrv.XT_ARRAY_STR | Rserve.Rsrv.XT_HAS_ATTR; 1305 | 1306 | // objects 1307 | if (_.isObject(value)) 1308 | return Rserve.Rsrv.XT_VECTOR | Rserve.Rsrv.XT_HAS_ATTR; 1309 | 1310 | throw new Rserve.RServeError("Value type unrecognized by Rserve: " + value); 1311 | }; 1312 | 1313 | // FIXME this is really slow, as it's walking the object many many times. 1314 | Rserve.determine_size = function(value, forced_type) 1315 | { 1316 | function list_size(lst) { 1317 | return _.reduce(lst, function(memo, el) { 1318 | return memo + Rserve.determine_size(el); 1319 | }, 0); 1320 | } 1321 | function final_size(payload_size) { 1322 | if (payload_size > (1 << 24)) 1323 | return payload_size + 8; // large header 1324 | else 1325 | return payload_size + 4; 1326 | } 1327 | var header_size = 4, t = forced_type || Rserve.type_id(value); 1328 | 1329 | switch (t & ~Rserve.Rsrv.XT_LARGE) { 1330 | case Rserve.Rsrv.XT_NULL: 1331 | return final_size(0); 1332 | case Rserve.Rsrv.XT_ARRAY_BOOL: 1333 | if (_.isBoolean(value)) 1334 | return final_size(8); 1335 | else 1336 | return final_size((value.length + 7) & ~3); 1337 | case Rserve.Rsrv.XT_ARRAY_STR: 1338 | if (_.isArray(value)) 1339 | return final_size(_.reduce(value, function(memo, str) { 1340 | // FIXME: this is a bit silly, since we'll be re-encoding this twice: once for the size and second time for the content 1341 | var utf8 = unescape(encodeURIComponent(str)); 1342 | return memo + utf8.length + 1; 1343 | }, 0)); 1344 | else { 1345 | var utf8 = unescape(encodeURIComponent(value)); 1346 | return final_size(utf8.length + 1); 1347 | } 1348 | case Rserve.Rsrv.XT_ARRAY_DOUBLE: 1349 | if (_.isNumber(value)) 1350 | return final_size(8); 1351 | else 1352 | return final_size(8 * value.length); 1353 | case Rserve.Rsrv.XT_RAW: 1354 | return final_size(4 + value.byteLength); 1355 | case Rserve.Rsrv.XT_VECTOR: 1356 | case Rserve.Rsrv.XT_LANG_NOTAG: 1357 | return final_size(list_size(value)); 1358 | case Rserve.Rsrv.XT_VECTOR | Rserve.Rsrv.XT_HAS_ATTR: // a named list (that is, a js object) 1359 | var names_size_1 = final_size("names".length + 3); 1360 | var names_size_2 = Rserve.determine_size(_.keys(value)); 1361 | var names_size = final_size(names_size_1 + names_size_2); 1362 | return final_size(names_size + list_size(_.values(value))); 1363 | /* return header_size // XT_VECTOR | XT_HAS_ATTR 1364 | + header_size // XT_LIST_TAG (attribute) 1365 | + header_size + "names".length + 3 // length of 'names' + padding (tag as XT_SYMNAME) 1366 | + Rserve.determine_size(_.keys(value)) // length of names 1367 | + list_size(_.values(value)); // length of values 1368 | */ 1369 | case Rserve.Rsrv.XT_ARRAY_STR | Rserve.Rsrv.XT_HAS_ATTR: // js->r ocap (that is, a js function) 1370 | return Rserve.determine_size("0403556553") // length of ocap nonce; that number is meaningless aside from length 1371 | + header_size // XT_LIST_TAG (attribute) 1372 | + header_size + "class".length + 3 // length of 'class' + padding (tag as XT_SYMNAME) 1373 | + Rserve.determine_size(["javascript_function"]); // length of class name 1374 | 1375 | default: 1376 | throw new Rserve.RserveError("Internal error, can't handle type " + t); 1377 | } 1378 | }; 1379 | 1380 | Rserve.write_into_view = function(value, array_buffer_view, forced_type, convert) 1381 | { 1382 | var size = Rserve.determine_size(value, forced_type); 1383 | var is_large = size > 16777215; 1384 | // if (size > 16777215) 1385 | // throw new Rserve.RserveError("Can't currently handle objects >16MB"); 1386 | var t = forced_type || Rserve.type_id(value), i, current_offset, lbl; 1387 | if (is_large) 1388 | t = t | Rserve.Rsrv.XT_LARGE; 1389 | var read_view; 1390 | var write_view = array_buffer_view.data_view(); 1391 | var payload_start; 1392 | if (is_large) { 1393 | payload_start = 8; 1394 | write_view.setInt32(0, t + ((size - 8) << 8)); 1395 | write_view.setInt32(4, (size - 8) >>> 24); 1396 | } else { 1397 | payload_start = 4; 1398 | write_view.setInt32(0, t + ((size - 4) << 8)); 1399 | } 1400 | 1401 | switch (t & ~Rserve.Rsrv.XT_LARGE) { 1402 | case Rserve.Rsrv.XT_NULL: 1403 | break; 1404 | case Rserve.Rsrv.XT_ARRAY_BOOL: 1405 | if (_.isBoolean(value)) { 1406 | write_view.setInt32(payload_start, 1); 1407 | write_view.setInt8(payload_start + 4, value ? 1 : 0); 1408 | } else { 1409 | write_view.setInt32(payload_start, value.length); 1410 | for (i=0; i>>8},PAR_LENGTH:function(x){return x>>>8},par_parse:function(x){return[Rserve.Rsrv.PAR_TYPE(x),Rserve.Rsrv.PAR_LEN(x)]},SET_PAR:function(ty,len){return(len&16777215)<<8|ty&255},CMD_STAT:function(x){return x>>>24&127},SET_STAT:function(x,s){return x|(s&127)<<24},IS_OOB_SEND:function(x){return(x&268431360)===Rserve.Rsrv.OOB_SEND},IS_OOB_MSG:function(x){return(x&268431360)===Rserve.Rsrv.OOB_MSG},OOB_USR_CODE:function(x){return x&4095},CMD_RESP:65536,RESP_OK:65536|1,RESP_ERR:65536|2,OOB_SEND:131072|4096,OOB_MSG:131072|8192,ERR_auth_failed:65,ERR_conn_broken:66,ERR_inv_cmd:67,ERR_inv_par:68,ERR_Rerror:69,ERR_IOerror:70,ERR_notOpen:71,ERR_accessDenied:72,ERR_unsupportedCmd:73,ERR_unknownCmd:74,ERR_data_overflow:75,ERR_object_too_big:76,ERR_out_of_mem:77,ERR_ctrl_closed:78,ERR_session_busy:80,ERR_detach_failed:81,ERR_disabled:97,ERR_unavailable:98,ERR_cryptError:99,ERR_securityClose:100,CMD_login:1,CMD_voidEval:2,CMD_eval:3,CMD_shutdown:4,CMD_switch:5,CMD_keyReq:6,CMD_secLogin:7,CMD_OCcall:15,CMD_openFile:16,CMD_createFile:17,CMD_closeFile:18,CMD_readFile:19,CMD_writeFile:20,CMD_removeFile:21,CMD_setSEXP:32,CMD_assignSEXP:33,CMD_detachSession:48,CMD_detachedVoidEval:49,CMD_attachSession:50,CMD_ctrl:64,CMD_ctrlEval:66,CMD_ctrlSource:69,CMD_ctrlShutdown:68,CMD_setBufferSize:129,CMD_setEncoding:130,CMD_SPECIAL_MASK:240,CMD_serEval:245,CMD_serAssign:246,CMD_serEEval:247,DT_INT:1,DT_CHAR:2,DT_DOUBLE:3,DT_STRING:4,DT_BYTESTREAM:5,DT_SEXP:10,DT_ARRAY:11,DT_LARGE:64,XT_NULL:0,XT_INT:1,XT_DOUBLE:2,XT_STR:3,XT_LANG:4,XT_SYM:5,XT_BOOL:6,XT_S4:7,XT_VECTOR:16,XT_LIST:17,XT_CLOS:18,XT_SYMNAME:19,XT_LIST_NOTAG:20,XT_LIST_TAG:21,XT_LANG_NOTAG:22,XT_LANG_TAG:23,XT_VECTOR_EXP:26,XT_VECTOR_STR:27,XT_ARRAY_INT:32,XT_ARRAY_DOUBLE:33,XT_ARRAY_STR:34,XT_ARRAY_BOOL_UA:35,XT_ARRAY_BOOL:36,XT_RAW:37,XT_ARRAY_CPLX:38,XT_UNKNOWN:48,XT_LARGE:64,XT_HAS_ATTR:128,BOOL_TRUE:1,BOOL_FALSE:0,BOOL_NA:2,GET_XT:function(x){return x&63},GET_DT:function(x){return x&63},HAS_ATTR:function(x){return(x&Rserve.Rsrv.XT_HAS_ATTR)>0},IS_LARGE:function(x){return(x&Rserve.Rsrv.XT_LARGE)>0},status_codes:{65:"ERR_auth_failed",66:"ERR_conn_broken",67:"ERR_inv_cmd",68:"ERR_inv_par",69:"ERR_Rerror",70:"ERR_IOerror",71:"ERR_notOpen",72:"ERR_accessDenied",73:"ERR_unsupportedCmd",74:"ERR_unknownCmd",75:"ERR_data_overflow",76:"ERR_object_too_big",77:"ERR_out_of_mem",78:"ERR_ctrl_closed",80:"ERR_session_busy",81:"ERR_detach_failed",97:"ERR_disabled",98:"ERR_unavailable",99:"ERR_cryptError",100:"ERR_securityClose"}};!function(){function read(m){var handlers={};function lift(f,amount){return function(attributes,length){return[f.call(that,attributes,length),amount||length]}}function bind(m,f){return function(attributes,length){var t=m.call(that,attributes,length);var t2=f(t[0])(attributes,length-t[1]);return[t2[0],t[1]+t2[1]]}}function unfold(f){return function(attributes,length){var result=[];var old_length=length;while(length>0){var t=f.call(that,attributes,length);result.push(t[0]);length-=t[1]}return[result,old_length]}}function decodeRString(s){if(s.length===1&&s.charCodeAt(0)===255)return null;try{return decodeURIComponent(escape(s))}catch(xep){throw new Error("Invalid UTF8: "+s)}}var that={offset:0,data_view:m.make(Rserve.EndianAwareDataView),msg:m,read_int:function(){var old_offset=this.offset;this.offset+=4;return this.data_view.getInt32(old_offset)},read_string:function(length){var result="";while(length--){var c=this.data_view.getInt8(this.offset++);if(c)result=result+String.fromCharCode(c)}return decodeRString(result)},read_stream:function(length){var old_offset=this.offset;this.offset+=length;return this.msg.view(old_offset,length)},read_int_vector:function(length){var old_offset=this.offset;this.offset+=length;return this.msg.make(Int32Array,old_offset,length)},read_double_vector:function(length){var old_offset=this.offset;this.offset+=length;return this.msg.make(Float64Array,old_offset,length)},read_null:lift(function(a,l){return Rserve.Robj.null(a)}),read_unknown:lift(function(a,l){this.offset+=l;return Rserve.Robj.null(a)}),read_string_array:function(attributes,length){var a=this.read_stream(length).make(Uint8Array);var result=[];var current_str="";for(var i=0;i>>24;var length=header[1],length_high=header[3];var msg_id=header[2];result.header=[resp,status_code,msg_id];if(length_high){result.ok=false;result.message="rserve.js cannot handle messages larger than 4GB";return result}var full_msg_length=length+16;if(full_msg_length>msg.byteLength){incomplete_.push(msg);incomplete_header_=header;msg_bytes_=full_msg_length;remaining_=msg_bytes_-msg.byteLength;result.header=header;result.ok=true;result.incomplete=true;return result}if(resp===Rserve.Rsrv.RESP_ERR){result.ok=false;result.status_code=status_code;result.message="ERROR FROM R SERVER: "+(Rserve.Rsrv.status_codes[status_code]||status_code)+" "+result.header[0]+" "+result.header[1]+" "+msg.byteLength+" "+msg;return result}if(!(resp===Rserve.Rsrv.RESP_OK||Rserve.Rsrv.IS_OOB_SEND(resp)||Rserve.Rsrv.IS_OOB_MSG(resp))){result.ok=false;result.message="Unexpected response from Rserve: "+resp+" status: "+Rserve.Rsrv.status_codes[status_code];return result}try{result.payload=parse_payload(msg);result.ok=true}catch(e){result.ok=false;result.message=e.message}return result}function parse_payload(msg){var payload=Rserve.my_ArrayBufferView(msg,16,msg.byteLength-16);if(payload.length===0)return null;var reader=read(payload);var d=reader.read_int();var _=Rserve.Rsrv.par_parse(d);var t=_[0],l=_[1];if(Rserve.Rsrv.IS_LARGE(t)){var more_length=reader.read_int();l+=more_length*Math.pow(2,24);if(l>Math.pow(2,32)){throw new Error("Payload too large: "+l+" bytes")}t&=~64}if(t===Rserve.Rsrv.DT_INT){return{type:"int",value:reader.read_int()}}else if(t===Rserve.Rsrv.DT_STRING){return{type:"string",value:reader.read_string(l)}}else if(t===Rserve.Rsrv.DT_BYTESTREAM){return{type:"stream",value:reader.read_stream(l)}}else if(t===Rserve.Rsrv.DT_SEXP){_=reader.read_sexp();var sexp=_[0],l2=_[1];return{type:"sexp",value:sexp}}else throw new Rserve.RserveError("Bad type for parse? "+t+" "+l,-1)}Rserve.parse_websocket_frame=parse;Rserve.parse_payload=parse_payload}();!function(){var _is_little_endian;!function(){var x=new ArrayBuffer(4);var bytes=new Uint8Array(x),words=new Uint32Array(x);bytes[0]=1;if(words[0]===1){_is_little_endian=true}else if(words[0]===16777216){_is_little_endian=false}else{throw"we're bizarro endian, refusing to continue"}}();Rserve.EndianAwareDataView=function(){var proto={setInt8:function(i,v){return this.view.setInt8(i,v)},setUint8:function(i,v){return this.view.setUint8(i,v)},getInt8:function(i){return this.view.getInt8(i)},getUint8:function(i){return this.view.getUint8(i)}};var setters=["setInt32","setInt16","setUint32","setUint16","setFloat32","setFloat64"];var getters=["getInt32","getInt16","getUint32","getUint16","getFloat32","getFloat64"];for(var i=0;ithis.buffer.byteLength)throw new Error("Rserve.my_ArrayBufferView.view: bounds error: size: "+this.buffer.byteLength+" offset: "+ofs+" length: "+new_length);return Rserve.my_ArrayBufferView(this.buffer,ofs,new_length)}}}}(this);!function(){function _encode_command(command,buffer,msg_id){if(!_.isArray(buffer))buffer=[buffer];if(!msg_id)msg_id=0;var length=_.reduce(buffer,function(memo,val){return memo+val.byteLength},0),big_buffer=new ArrayBuffer(16+length),view=new Rserve.EndianAwareDataView(big_buffer);view.setInt32(0,command);view.setInt32(4,length);view.setInt32(8,msg_id);view.setInt32(12,0);var offset=16;_.each(buffer,function(b){var source_array=new Uint8Array(b);for(var i=0;i16777215){var buffer=new ArrayBuffer(sz+8);var view=Rserve.my_ArrayBufferView(buffer);view.data_view().setInt32(0,Rserve.Rsrv.DT_SEXP+(sz&16777215)*Math.pow(2,8)+Rserve.Rsrv.DT_LARGE);view.data_view().setInt32(4,sz>>>24);Rserve.write_into_view(value,view.skip(8),forced_type,convert_to_hash);return buffer}else{var buffer=new ArrayBuffer(sz+4);var view=Rserve.my_ArrayBufferView(buffer);view.data_view().setInt32(0,Rserve.Rsrv.DT_SEXP+(sz<<8));Rserve.write_into_view(value,view.skip(4),forced_type,convert_to_hash);return buffer}}function hand_shake(msg){msg=msg.data;if(typeof msg==="string"){if(msg.substr(0,4)!=="Rsrv"){handle_error("server is not an RServe instance",-1)}else if(msg.substr(4,4)!=="0103"){handle_error("sorry, rserve only speaks the 0103 version of the R server protocol",-1)}else if(msg.substr(8,4)!=="QAP1"){handle_error("sorry, rserve only speaks QAP1",-1)}else{received_handshake=true;if(opts.login)result.login(opts.login);result.running=true;onconnect&&onconnect.call(result)}}else{var view=new DataView(msg);var header=String.fromCharCode(view.getUint8(0))+String.fromCharCode(view.getUint8(1))+String.fromCharCode(view.getUint8(2))+String.fromCharCode(view.getUint8(3));if(header==="RsOC"){received_handshake=true;result.ocap_mode=true;result.bare_ocap=Rserve.parse_payload(msg).value;result.ocap=Rserve.wrap_ocap(result,result.bare_ocap);result.running=true;onconnect&&onconnect.call(result)}else handle_error("Unrecognized server answer: "+header,-1)}}socket.onclose=function(msg){result.running=false;result.closed=true;opts.on_close&&opts.on_close(msg)};socket.onmessage=function(msg){if(msg.data.constructor.name==="Buffer")msg.data=new Uint8Array(msg.data).buffer;if(opts.debug)opts.debug.message_in&&opts.debug.message_in(msg);if(!received_handshake){hand_shake(msg);return}if(typeof msg.data==="string"){opts.on_raw_string&&opts.on_raw_string(msg.data);return}var v=Rserve.parse_websocket_frame(msg.data);if(v.incomplete)return;var msg_id=v.header[2],cmd=v.header[0]&16777215;var queue=_.find(queues,function(queue){return queue.msg_id==msg_id});if(!queue)queue=queues[0];if(!v.ok){queue.result_callback([v.message,v.status_code],undefined)}else if(cmd===Rserve.Rsrv.RESP_OK){queue.result_callback(null,v.payload)}else if(Rserve.Rsrv.IS_OOB_SEND(cmd)){opts.on_data&&opts.on_data(v.payload)}else if(Rserve.Rsrv.IS_OOB_MSG(cmd)){queue=Rserve.Rsrv.OOB_USR_CODE(cmd)>255?compute_queue:ctrl_queue;var p;try{p=Rserve.wrap_all_ocaps(result,v.payload)}catch(e){_send_cmd_now(Rserve.Rsrv.RESP_ERR|cmd,_encode_string(String(e)),msg_id);return}if(_.isString(p[0])){if(_.isUndefined(opts.on_oob_message)){_send_cmd_now(Rserve.Rsrv.RESP_ERR|cmd,_encode_string("No handler installed"),msg_id)}else{queue.in_oob_message=true;opts.on_oob_message(v.payload,function(error,result){if(!queue.in_oob_message){handle_error("Don't call oob_message_handler more than once.");return}queue.in_oob_message=false;if(error){_send_cmd_now(cmd|Rserve.Rsrv.RESP_ERR,_encode_string(error),msg_id)}else{_send_cmd_now(cmd|Rserve.Rsrv.RESP_OK,_encode_string(result),msg_id)}bump_queues()})}}else if(_.isFunction(p[0])){if(!result.ocap_mode){_send_cmd_now(Rserve.Rsrv.RESP_ERROR|cmd,_encode_string("JavaScript function calls only allowed in ocap mode"),msg_id)}else{var captured_function=p[0],params=p.slice(1);params.push(function(err,result){if(err){_send_cmd_now(Rserve.Rsrv.RESP_ERR|cmd,_encode_value(err),msg_id)}else{_send_cmd_now(cmd,_encode_value(result),msg_id)}});captured_function.apply(undefined,params)}}else{_send_cmd_now(Rserve.Rsrv.RESP_ERROR|cmd,_encode_string("Unknown oob message type: "+typeof p[0]))}}else{handle_error("Internal Error, parse returned unexpected type "+v.header[0],-1)}};function _send_cmd_now(command,buffer,msg_id){var big_buffer=_encode_command(command,buffer,msg_id);if(opts.debug)opts.debug.message_out&&opts.debug.message_out(big_buffer[0],command);socket.send(big_buffer);return big_buffer}var ctrl_queue={queue:[],in_oob_message:false,awaiting_result:false,msg_id:0,name:"control"};var compute_queue={queue:[],in_oob_message:false,awaiting_result:false,msg_id:1,name:"compute"};var queues=[ctrl_queue,compute_queue];function queue_can_send(queue){return!queue.in_oob_message&&!queue.awaiting_result&&queue.queue.length}function bump_queues(){var available=_.filter(queues,queue_can_send);if(!available.length)return;if(result.closed){handle_error("Cannot send messages on a closed socket!",-1)}else{var queue=_.sortBy(available,function(queue){return queue.queue[0].timestamp})[0];var lst=queue.queue.shift();queue.result_callback=lst.callback;queue.awaiting_result=true;if(opts.debug)opts.debug.message_out&&opts.debug.message_out(lst.buffer,lst.command);socket.send(lst.buffer)}}function enqueue(buffer,k,command,queue){queue.queue.push({buffer:buffer,callback:function(error,result){queue.awaiting_result=false;bump_queues();k(error,result)},command:command,timestamp:Date.now()});bump_queues()}function _cmd(command,buffer,k,string,queue){if(!queue)queue=queues[0];k=k||function(){};var big_buffer=_encode_command(command,buffer,queue.msg_id);return enqueue(big_buffer,k,string,queue)}result={ocap_mode:false,running:false,closed:false,close:function(){socket.close()},login:function(command,k){_cmd(Rserve.Rsrv.CMD_login,_encode_string(command),k,command)},eval:function(command,k){_cmd(Rserve.Rsrv.CMD_eval,_encode_string(command),k,command)},createFile:function(command,k){_cmd(Rserve.Rsrv.CMD_createFile,_encode_string(command),k,command)},writeFile:function(chunk,k){_cmd(Rserve.Rsrv.CMD_writeFile,_encode_bytes(chunk),k,"")},closeFile:function(k){_cmd(Rserve.Rsrv.CMD_closeFile,new ArrayBuffer(0),k,"")},set:function(key,value,k){_cmd(Rserve.Rsrv.CMD_setSEXP,[_encode_string(key),_encode_value(value)],k,"")},OCcall:function(ocap,values,k){var is_ocap=false,str;try{is_ocap|=ocap.r_attributes["class"]==="OCref";str=ocap[0]}catch(e){}if(!is_ocap){try{is_ocap|=ocap.attributes.value[0].value.value[0]==="OCref";str=ocap.value[0]}catch(e){}}if(!is_ocap){k(new Error("Expected an ocap, instead got "+ocap),undefined);return}var params=[str];params.push.apply(params,values);var queue=str.charCodeAt(0)==64?compute_queue:ctrl_queue;_cmd(Rserve.Rsrv.CMD_OCcall,_encode_value(params,Rserve.Rsrv.XT_LANG_NOTAG),k,"",queue)},wrap_ocap:function(ocap){return Rserve.wrap_ocap(this,ocap)},resolve_hash:function(hash){if(!(hash in captured_functions))throw new Error("hash "+hash+" not found.");return captured_functions[hash]}};return result};Rserve.wrap_all_ocaps=function(s,v){v=v.value.json(s.resolve_hash);function replace(obj){var result=obj;if(_.isArray(obj)&&obj.r_attributes&&obj.r_attributes["class"]=="OCref"){return Rserve.wrap_ocap(s,obj)}else if(_.isArray(obj)){result=_.map(obj,replace);result.r_type=obj.r_type;result.r_attributes=obj.r_attributes}else if(_.isTypedArray(obj)){return obj}else if(_.isFunction(obj)){return obj}else if(obj&&!_.isUndefined(obj.byteLength)&&!_.isUndefined(obj.slice)){return obj}else if(_.isObject(obj)){result=_.object(_.map(obj,function(v,k){return[k,replace(v)]}))}return result}return replace(v)};Rserve.wrap_ocap=function(s,ocap){var wrapped_ocap=function(){var values=_.toArray(arguments);if(!values.length||!_.isFunction(values[values.length-1]))throw new Error("forgot to pass continuation to ocap");var k=values.pop();s.OCcall(ocap,values,function(err,v){if(!_.isUndefined(v))v=Rserve.wrap_all_ocaps(s,v);k(err,v)})};wrapped_ocap.bare_ocap=ocap;return wrapped_ocap}}();Rserve.RserveError=function(message,status_code){this.name="RserveError";this.message=message;this.status_code=status_code};Rserve.RserveError.prototype=Object.create(Error);Rserve.RserveError.prototype.constructor=Rserve.RserveError;!function(){_.mixin({isTypedArray:function(v){if(!_.isObject(v))return false;return!_.isUndefined(v.byteLength)&&!_.isUndefined(v.BYTES_PER_ELEMENT)}});Rserve.type_id=function(value){if(_.isNull(value)||_.isUndefined(value))return Rserve.Rsrv.XT_NULL;var type_dispatch={"boolean":Rserve.Rsrv.XT_ARRAY_BOOL,number:Rserve.Rsrv.XT_ARRAY_DOUBLE,string:Rserve.Rsrv.XT_ARRAY_STR};if(!_.isUndefined(type_dispatch[typeof value]))return type_dispatch[typeof value];if(_.isTypedArray(value))return Rserve.Rsrv.XT_ARRAY_DOUBLE;if(!_.isUndefined(value.byteLength)&&!_.isUndefined(value.slice))return Rserve.Rsrv.XT_RAW;if(_.isArray(value)&&_.all(value,function(el){return typeof el==="string"}))return Rserve.Rsrv.XT_ARRAY_STR;if(_.isArray(value)&&_.all(value,function(el){return typeof el==="boolean"}))return Rserve.Rsrv.XT_ARRAY_BOOL;if(_.isArray(value))return Rserve.Rsrv.XT_VECTOR;if(_.isFunction(value))return Rserve.Rsrv.XT_ARRAY_STR|Rserve.Rsrv.XT_HAS_ATTR;if(_.isObject(value))return Rserve.Rsrv.XT_VECTOR|Rserve.Rsrv.XT_HAS_ATTR;throw new Rserve.RServeError("Value type unrecognized by Rserve: "+value)};Rserve.determine_size=function(value,forced_type){function list_size(lst){return _.reduce(lst,function(memo,el){return memo+Rserve.determine_size(el)},0)}function final_size(payload_size){if(payload_size>1<<24)return payload_size+8;else return payload_size+4}var header_size=4,t=forced_type||Rserve.type_id(value);switch(t&~Rserve.Rsrv.XT_LARGE){case Rserve.Rsrv.XT_NULL:return final_size(0);case Rserve.Rsrv.XT_ARRAY_BOOL:if(_.isBoolean(value))return final_size(8);else return final_size(value.length+7&~3);case Rserve.Rsrv.XT_ARRAY_STR:if(_.isArray(value))return final_size(_.reduce(value,function(memo,str){var utf8=unescape(encodeURIComponent(str));return memo+utf8.length+1},0));else{var utf8=unescape(encodeURIComponent(value));return final_size(utf8.length+1)}case Rserve.Rsrv.XT_ARRAY_DOUBLE:if(_.isNumber(value))return final_size(8);else return final_size(8*value.length);case Rserve.Rsrv.XT_RAW:return final_size(4+value.byteLength);case Rserve.Rsrv.XT_VECTOR:case Rserve.Rsrv.XT_LANG_NOTAG:return final_size(list_size(value));case Rserve.Rsrv.XT_VECTOR|Rserve.Rsrv.XT_HAS_ATTR:var names_size_1=final_size("names".length+3);var names_size_2=Rserve.determine_size(_.keys(value));var names_size=final_size(names_size_1+names_size_2);return final_size(names_size+list_size(_.values(value)));case Rserve.Rsrv.XT_ARRAY_STR|Rserve.Rsrv.XT_HAS_ATTR:return Rserve.determine_size("0403556553")+header_size+header_size+"class".length+3+Rserve.determine_size(["javascript_function"]);default:throw new Rserve.RserveError("Internal error, can't handle type "+t)}};Rserve.write_into_view=function(value,array_buffer_view,forced_type,convert){var size=Rserve.determine_size(value,forced_type);var is_large=size>16777215;var t=forced_type||Rserve.type_id(value),i,current_offset,lbl;if(is_large)t=t|Rserve.Rsrv.XT_LARGE;var read_view;var write_view=array_buffer_view.data_view();var payload_start;if(is_large){payload_start=8;write_view.setInt32(0,t+(size-8<<8));write_view.setInt32(4,size-8>>>24)}else{payload_start=4;write_view.setInt32(0,t+(size-4<<8))}switch(t&~Rserve.Rsrv.XT_LARGE){case Rserve.Rsrv.XT_NULL:break;case Rserve.Rsrv.XT_ARRAY_BOOL:if(_.isBoolean(value)){write_view.setInt32(payload_start,1);write_view.setInt8(payload_start+4,value?1:0)}else{write_view.setInt32(payload_start,value.length);for(i=0;i this.buffer.byteLength) 107 | throw new Error("Rserve.my_ArrayBufferView.view: bounds error: size: " + 108 | this.buffer.byteLength + " offset: " + ofs + " length: " + new_length); 109 | return Rserve.my_ArrayBufferView( 110 | this.buffer, ofs, new_length); 111 | } 112 | }; 113 | }; 114 | 115 | })(this); 116 | -------------------------------------------------------------------------------- /src/error.js: -------------------------------------------------------------------------------- 1 | Rserve.RserveError = function(message, status_code) { 2 | this.name = "RserveError"; 3 | this.message = message; 4 | this.status_code = status_code; 5 | }; 6 | 7 | Rserve.RserveError.prototype = Object.create(Error); 8 | Rserve.RserveError.prototype.constructor = Rserve.RserveError; 9 | -------------------------------------------------------------------------------- /src/parse.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | function read(m) 4 | { 5 | var handlers = {}; 6 | 7 | function lift(f, amount) { 8 | return function(attributes, length) { 9 | return [f.call(that, attributes, length), amount || length]; 10 | }; 11 | } 12 | 13 | function bind(m, f) { 14 | return function(attributes, length) { 15 | var t = m.call(that, attributes, length); 16 | var t2 = f(t[0])(attributes, length - t[1]); 17 | return [t2[0], t[1] + t2[1]]; 18 | }; 19 | } 20 | 21 | function unfold(f) { 22 | return function(attributes, length) { 23 | var result = []; 24 | var old_length = length; 25 | while (length > 0) { 26 | var t = f.call(that, attributes, length); 27 | result.push(t[0]); 28 | length -= t[1]; 29 | } 30 | return [result, old_length]; 31 | }; 32 | } 33 | 34 | function decodeRString(s) { 35 | // R encodes NA as a string containing just 0xff 36 | if(s.length === 1 && s.charCodeAt(0) === 255) 37 | return null; 38 | // UTF-8 to UTF-16 39 | // http://monsur.hossa.in/2012/07/20/utf-8-in-javascript.html 40 | // also, we don't want to lose the value when reporting an error in decoding 41 | try { 42 | return decodeURIComponent(escape(s)); 43 | } 44 | catch(xep) { 45 | throw new Error('Invalid UTF8: ' + s); 46 | } 47 | } 48 | 49 | var that = { 50 | offset: 0, 51 | data_view: m.make(Rserve.EndianAwareDataView), 52 | msg: m, 53 | 54 | ////////////////////////////////////////////////////////////////////// 55 | 56 | read_int: function() { 57 | var old_offset = this.offset; 58 | this.offset += 4; 59 | return this.data_view.getInt32(old_offset); 60 | }, 61 | read_string: function(length) { 62 | // FIXME SLOW 63 | var result = ""; 64 | while (length--) { 65 | var c = this.data_view.getInt8(this.offset++); 66 | if (c) result = result + String.fromCharCode(c); 67 | } 68 | return decodeRString(result); 69 | }, 70 | read_stream: function(length) { 71 | var old_offset = this.offset; 72 | this.offset += length; 73 | return this.msg.view(old_offset, length); 74 | }, 75 | read_int_vector: function(length) { 76 | var old_offset = this.offset; 77 | this.offset += length; 78 | return this.msg.make(Int32Array, old_offset, length); 79 | }, 80 | read_double_vector: function(length) { 81 | var old_offset = this.offset; 82 | this.offset += length; 83 | return this.msg.make(Float64Array, old_offset, length); 84 | }, 85 | 86 | ////////////////////////////////////////////////////////////////////// 87 | // these are members of the reader monad 88 | 89 | read_null: lift(function(a, l) { return Rserve.Robj.null(a); }), 90 | 91 | read_unknown: lift(function(a, l) { 92 | this.offset += l; 93 | return Rserve.Robj.null(a); 94 | }), 95 | 96 | read_string_array: function(attributes, length) { 97 | var a = this.read_stream(length).make(Uint8Array); 98 | var result = []; 99 | var current_str = ""; 100 | for (var i=0; i>> 24; 274 | var length = header[1], length_high = header[3]; 275 | var msg_id = header[2]; 276 | result.header = [resp, status_code, msg_id]; 277 | 278 | if(length_high) { 279 | result.ok = false; 280 | result.message = "rserve.js cannot handle messages larger than 4GB"; 281 | return result; 282 | } 283 | 284 | var full_msg_length = length + 16; // header length + data length 285 | if(full_msg_length > msg.byteLength) { 286 | incomplete_.push(msg); 287 | incomplete_header_ = header; 288 | msg_bytes_ = full_msg_length; 289 | remaining_ = msg_bytes_ - msg.byteLength; 290 | result.header = header; 291 | result.ok = true; 292 | result.incomplete = true; 293 | return result; 294 | } 295 | 296 | if (resp === Rserve.Rsrv.RESP_ERR) { 297 | result.ok = false; 298 | result.status_code = status_code; 299 | result.message = "ERROR FROM R SERVER: " + (Rserve.Rsrv.status_codes[status_code] || 300 | status_code) 301 | + " " + result.header[0] + " " + result.header[1] 302 | + " " + msg.byteLength 303 | + " " + msg; 304 | return result; 305 | } 306 | 307 | if (!( resp === Rserve.Rsrv.RESP_OK || Rserve.Rsrv.IS_OOB_SEND(resp) || Rserve.Rsrv.IS_OOB_MSG(resp))) { 308 | result.ok = false; 309 | result.message = "Unexpected response from Rserve: " + resp + " status: " + Rserve.Rsrv.status_codes[status_code]; 310 | return result; 311 | } 312 | try { 313 | result.payload = parse_payload(msg); 314 | result.ok = true; 315 | } catch (e) { 316 | result.ok = false; 317 | result.message = e.message; 318 | } 319 | return result; 320 | } 321 | 322 | function parse_payload(msg) 323 | { 324 | var payload = Rserve.my_ArrayBufferView(msg, 16, msg.byteLength - 16); 325 | if (payload.length === 0) 326 | return null; 327 | 328 | var reader = read(payload); 329 | 330 | var d = reader.read_int(); 331 | var _ = Rserve.Rsrv.par_parse(d); 332 | var t = _[0], l = _[1]; 333 | if (Rserve.Rsrv.IS_LARGE(t)) { 334 | var more_length = reader.read_int(); 335 | l += more_length * Math.pow(2, 24); 336 | if (l > (Math.pow(2, 32))) { // resist the 1 << 32 temptation here! 337 | // total_length is greater than 2^32.. bail out because of node limits 338 | // even though in theory we could go higher than that. 339 | throw new Error("Payload too large: " + l + " bytes"); 340 | } 341 | t &= ~64; 342 | } 343 | if (t === Rserve.Rsrv.DT_INT) { 344 | return { type: "int", value: reader.read_int() }; 345 | } else if (t === Rserve.Rsrv.DT_STRING) { 346 | return { type: "string", value: reader.read_string(l) }; 347 | } else if (t === Rserve.Rsrv.DT_BYTESTREAM) { // NB this returns a my_ArrayBufferView() 348 | return { type: "stream", value: reader.read_stream(l) }; 349 | } else if (t === Rserve.Rsrv.DT_SEXP) { 350 | _ = reader.read_sexp(); 351 | var sexp = _[0], l2 = _[1]; 352 | return { type: "sexp", value: sexp }; 353 | } else 354 | throw new Rserve.RserveError("Bad type for parse? " + t + " " + l, -1); 355 | } 356 | 357 | Rserve.parse_websocket_frame = parse; 358 | Rserve.parse_payload = parse_payload; 359 | 360 | })(); 361 | -------------------------------------------------------------------------------- /src/robj.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | function make_basic(type, proto) { 4 | proto = proto || { 5 | json: function() { 6 | throw "json() unsupported for type " + this.type; 7 | } 8 | }; 9 | var wrapped_proto = { 10 | json: function(resolver) { 11 | var result = proto.json.call(this, resolver); 12 | result.r_type = type; 13 | if (!_.isUndefined(this.attributes)) 14 | result.r_attributes = _.object(_.map( 15 | this.attributes.value, 16 | function(v) { return [v.name, v.value.json(resolver)]; })); 17 | return result; 18 | } 19 | }; 20 | return function(v, attrs) { 21 | function r_object() { 22 | this.type = type; 23 | this.value = v; 24 | this.attributes = attrs; 25 | } 26 | r_object.prototype = wrapped_proto; 27 | var result = new r_object(); 28 | return result; 29 | }; 30 | } 31 | 32 | Rserve.Robj = { 33 | "null": function(attributes) { 34 | return { 35 | type: "null", 36 | value: null, 37 | attributes: attributes, 38 | json: function() { return null; } 39 | }; 40 | }, 41 | 42 | clos: function(formals, body, attributes) { 43 | return { 44 | type: "clos", 45 | value: { formals: formals, 46 | body: body }, 47 | attributes: attributes, 48 | json: function() { throw "json() unsupported for type clos"; } 49 | }; 50 | }, 51 | 52 | vector: make_basic("vector", { 53 | json: function(resolver) { 54 | var values = _.map(this.value, function (x) { return x.json(resolver); }); 55 | if (_.isUndefined(this.attributes)) { 56 | return values; 57 | } else { 58 | // FIXME: there is no reason why names should be the first or only 59 | // attribute, so the code should really look 60 | // for "names" and not cry if it doesn't exist 61 | if (this.attributes.value[0].name == "names") { 62 | var keys = this.attributes.value[0].value.value; 63 | var result = {}; 64 | _.each(keys, function(key, i) { 65 | result[key] = values[i]; 66 | }); 67 | return result; 68 | } 69 | // FIXME: how can we pass other important attributes 70 | // like "class" ? 71 | return values; 72 | } 73 | } 74 | }), 75 | symbol: make_basic("symbol", { 76 | json: function() { 77 | return this.value; 78 | } 79 | }), 80 | list: make_basic("list"), 81 | lang: make_basic("lang", { 82 | json: function(resolver) { 83 | var values = _.map(this.value, function (x) { return x.json(resolver); }); 84 | if (_.isUndefined(this.attributes)) { 85 | return values; 86 | } else { 87 | // FIXME: lang doens't have "names" attribute since 88 | // names are sent as tags (langs are pairlists) 89 | // so this seems superfluous (it is dangerous 90 | // if lang ever had attributes since there is 91 | // no reason to fail in that case) 92 | if(this.attributes.value[0].name!="names") 93 | throw "expected names here"; 94 | var keys = this.attributes.value[0].value.value; 95 | var result = {}; 96 | _.each(keys, function(key, i) { 97 | result[key] = values[i]; 98 | }); 99 | return result; 100 | } 101 | } 102 | }), 103 | tagged_list: make_basic("tagged_list", { 104 | json: function(resolver) { 105 | function classify_list(list) { 106 | if (_.all(list, function(elt) { return elt.name === null; })) { 107 | return "plain_list"; 108 | } else if (_.all(list, function(elt) { return elt.name !== null; })) { 109 | return "plain_object"; 110 | } else 111 | return "mixed_list"; 112 | } 113 | var list = this.value.slice(1); 114 | switch (classify_list(list)) { 115 | case "plain_list": 116 | return _.map(list, function(elt) { return elt.value.json(resolver); }); 117 | case "plain_object": 118 | return _.object(_.map(list, function(elt) { 119 | return [elt.name, elt.value.json(resolver)]; 120 | })); 121 | case "mixed_list": 122 | return list; 123 | default: 124 | throw "Internal Error"; 125 | } 126 | } 127 | }), 128 | tagged_lang: make_basic("tagged_lang", { 129 | json: function(resolver) { 130 | var pair_vec = _.map(this.value, function(elt) { return [elt.name, elt.value.json(resolver)]; }); 131 | return pair_vec; 132 | } 133 | }), 134 | vector_exp: make_basic("vector_exp"), 135 | int_array: make_basic("int_array", { 136 | json: function() { 137 | if(this.attributes && this.attributes.type==='tagged_list' 138 | && this.attributes.value[0].name==='levels' 139 | && this.attributes.value[0].value.type==='string_array') { 140 | var levels = this.attributes.value[0].value.value; 141 | var arr = _.map(this.value, function(factor) { return levels[factor-1]; }); 142 | arr.levels = levels; 143 | return arr; 144 | } 145 | else { 146 | if (this.value.length === 1) 147 | return this.value[0]; 148 | else 149 | return this.value; 150 | } 151 | } 152 | }), 153 | double_array: make_basic("double_array", { 154 | json: function() { 155 | if (this.value.length === 1 && _.isUndefined(this.attributes)) 156 | return this.value[0]; 157 | else 158 | return this.value; 159 | } 160 | }), 161 | string_array: make_basic("string_array", { 162 | json: function(resolver) { 163 | if (this.value.length === 1) { 164 | if (_.isUndefined(this.attributes)) 165 | return this.value[0]; 166 | if (this.attributes.value[0].name === 'class' && 167 | this.attributes.value[0].value.value.indexOf("javascript_function") !== -1) 168 | return resolver(this.value[0]); 169 | return this.value; 170 | } else 171 | return this.value; 172 | } 173 | }), 174 | bool_array: make_basic("bool_array", { 175 | json: function() { 176 | if (this.value.length === 1 && _.isUndefined(this.attributes)) 177 | return this.value[0]; 178 | else 179 | return this.value; 180 | } 181 | }), 182 | raw: make_basic("raw", { 183 | json: function() { 184 | if (this.value.length === 1 && _.isUndefined(this.attributes)) 185 | return this.value[0]; 186 | else 187 | return this.value; 188 | } 189 | }), 190 | string: make_basic("string", { 191 | json: function() { 192 | return this.value; 193 | } 194 | }) 195 | }; 196 | 197 | })(); 198 | -------------------------------------------------------------------------------- /src/rserve.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | function _encode_command(command, buffer, msg_id) { 4 | if (!_.isArray(buffer)) 5 | buffer = [buffer]; 6 | if (!msg_id) msg_id = 0; 7 | var length = _.reduce(buffer, 8 | function(memo, val) { 9 | return memo + val.byteLength; 10 | }, 0), 11 | big_buffer = new ArrayBuffer(16 + length), 12 | view = new Rserve.EndianAwareDataView(big_buffer); 13 | view.setInt32(0, command); 14 | view.setInt32(4, length); 15 | view.setInt32(8, msg_id); 16 | view.setInt32(12, 0); 17 | var offset = 16; 18 | _.each(buffer, function(b) { 19 | var source_array = new Uint8Array(b); 20 | for (var i=0; i= 2^31. 87 | if (sz > 16777215) { 88 | var buffer = new ArrayBuffer(sz + 8); 89 | var view = Rserve.my_ArrayBufferView(buffer); 90 | // can't left shift value here because value will have bit 32 set and become signed.. 91 | view.data_view().setInt32(0, Rserve.Rsrv.DT_SEXP + ((sz & 16777215) * Math.pow(2, 8)) + Rserve.Rsrv.DT_LARGE); 92 | // but *can* right shift because we assume sz is less than 2^31 or so to begin with 93 | view.data_view().setInt32(4, sz >>> 24); 94 | Rserve.write_into_view(value, view.skip(8), forced_type, convert_to_hash); 95 | return buffer; 96 | } else { 97 | var buffer = new ArrayBuffer(sz + 4); 98 | var view = Rserve.my_ArrayBufferView(buffer); 99 | view.data_view().setInt32(0, Rserve.Rsrv.DT_SEXP + (sz << 8)); 100 | Rserve.write_into_view(value, view.skip(4), forced_type, convert_to_hash); 101 | return buffer; 102 | } 103 | } 104 | 105 | function hand_shake(msg) 106 | { 107 | msg = msg.data; 108 | if (typeof msg === 'string') { 109 | if (msg.substr(0,4) !== 'Rsrv') { 110 | handle_error("server is not an RServe instance", -1); 111 | } else if (msg.substr(4, 4) !== '0103') { 112 | handle_error("sorry, rserve only speaks the 0103 version of the R server protocol", -1); 113 | } else if (msg.substr(8, 4) !== 'QAP1') { 114 | handle_error("sorry, rserve only speaks QAP1", -1); 115 | } else { 116 | received_handshake = true; 117 | if (opts.login) 118 | result.login(opts.login); 119 | result.running = true; 120 | onconnect && onconnect.call(result); 121 | } 122 | } else { 123 | var view = new DataView(msg); 124 | var header = String.fromCharCode(view.getUint8(0)) + 125 | String.fromCharCode(view.getUint8(1)) + 126 | String.fromCharCode(view.getUint8(2)) + 127 | String.fromCharCode(view.getUint8(3)); 128 | 129 | if (header === 'RsOC') { 130 | received_handshake = true; 131 | result.ocap_mode = true; 132 | result.bare_ocap = Rserve.parse_payload(msg).value; 133 | result.ocap = Rserve.wrap_ocap(result, result.bare_ocap); 134 | result.running = true; 135 | onconnect && onconnect.call(result); 136 | } else 137 | handle_error("Unrecognized server answer: " + header, -1); 138 | } 139 | } 140 | 141 | socket.onclose = function(msg) { 142 | result.running = false; 143 | result.closed = true; 144 | opts.on_close && opts.on_close(msg); 145 | }; 146 | 147 | socket.onmessage = function(msg) { 148 | // node.js Buffer vs ArrayBuffer workaround 149 | if (msg.data.constructor.name === 'Buffer') 150 | msg.data = (new Uint8Array(msg.data)).buffer; 151 | if (opts.debug) 152 | opts.debug.message_in && opts.debug.message_in(msg); 153 | if (!received_handshake) { 154 | hand_shake(msg); 155 | return; 156 | } 157 | if (typeof msg.data === 'string') { 158 | opts.on_raw_string && opts.on_raw_string(msg.data); 159 | return; 160 | } 161 | var v = Rserve.parse_websocket_frame(msg.data); 162 | if(v.incomplete) 163 | return; 164 | var msg_id = v.header[2], cmd = v.header[0] & 0xffffff; 165 | var queue = _.find(queues, function(queue) { return queue.msg_id == msg_id; }); 166 | // console.log("onmessage, queue=" + (queue ? queue.name : "") + ", ok= " + v.ok+ ", cmd=" + cmd +", msg_id="+ msg_id); 167 | // FIXME: in theory we should not need a fallback, but in case we miss some 168 | // odd edge case, we revert to the old behavior. 169 | // The way things work, the queue will be undefined only for OOB messages: 170 | // SEND doesn't need reply, so it's irrelevant, MSG is handled separately below and 171 | // enforces the right queue. 172 | if (!queue) queue = queues[0]; 173 | if (!v.ok) { 174 | queue.result_callback([v.message, v.status_code], undefined); 175 | // handle_error(v.message, v.status_code); 176 | } else if (cmd === Rserve.Rsrv.RESP_OK) { 177 | queue.result_callback(null, v.payload); 178 | } else if (Rserve.Rsrv.IS_OOB_SEND(cmd)) { 179 | opts.on_data && opts.on_data(v.payload); 180 | } else if (Rserve.Rsrv.IS_OOB_MSG(cmd)) { 181 | // OOB MSG may use random msg_id, so we have to use the USR_CODE to detect the right queue 182 | // FIXME: we may want to consider adjusting the protocol specs to require msg_id 183 | // to be retained by OOB based on the outer OCcall message (thus inheriting 184 | // the msg_id), but curretnly it's not mandated. 185 | queue = (Rserve.Rsrv.OOB_USR_CODE(cmd) > 255) ? compute_queue : ctrl_queue; 186 | // console.log("OOB MSG result on queue "+ queue.name); 187 | var p; 188 | try { 189 | p = Rserve.wrap_all_ocaps(result, v.payload); // .value.json(result.resolve_hash); 190 | } catch (e) { 191 | _send_cmd_now(Rserve.Rsrv.RESP_ERR | cmd, 192 | _encode_string(String(e)), msg_id); 193 | return; 194 | } 195 | if(_.isString(p[0])) { 196 | if (_.isUndefined(opts.on_oob_message)) { 197 | _send_cmd_now(Rserve.Rsrv.RESP_ERR | cmd, 198 | _encode_string("No handler installed"), msg_id); 199 | } else { 200 | queue.in_oob_message = true; 201 | // breaking changes here: it appears that the callback had its arguments 202 | // reversed from the standard (error, message), and was passing the message 203 | // even on error 204 | opts.on_oob_message(v.payload, function(error, result) { 205 | if (!queue.in_oob_message) { 206 | handle_error("Don't call oob_message_handler more than once."); 207 | return; 208 | } 209 | queue.in_oob_message = false; 210 | if(error) { 211 | _send_cmd_now(cmd | Rserve.Rsrv.RESP_ERR, _encode_string(error), msg_id); 212 | } 213 | else { 214 | _send_cmd_now(cmd | Rserve.Rsrv.RESP_OK, _encode_string(result), msg_id); 215 | } 216 | bump_queues(); 217 | }); 218 | } 219 | } 220 | else if(_.isFunction(p[0])) { 221 | if(!result.ocap_mode) { 222 | _send_cmd_now(Rserve.Rsrv.RESP_ERROR | cmd, 223 | _encode_string("JavaScript function calls only allowed in ocap mode"), msg_id); 224 | } 225 | else { 226 | var captured_function = p[0], params = p.slice(1); 227 | params.push(function(err, result) { 228 | if (err) { 229 | _send_cmd_now(Rserve.Rsrv.RESP_ERR | cmd, _encode_value(err), msg_id); 230 | } else { 231 | _send_cmd_now(cmd, _encode_value(result), msg_id); 232 | } 233 | }); 234 | captured_function.apply(undefined, params); 235 | } 236 | } 237 | else { 238 | _send_cmd_now(Rserve.Rsrv.RESP_ERROR | cmd, 239 | _encode_string("Unknown oob message type: " + typeof(p[0]))); 240 | } 241 | } else { 242 | handle_error("Internal Error, parse returned unexpected type " + v.header[0], -1); 243 | } 244 | }; 245 | 246 | function _send_cmd_now(command, buffer, msg_id) { 247 | var big_buffer = _encode_command(command, buffer, msg_id); 248 | if (opts.debug) 249 | opts.debug.message_out && opts.debug.message_out(big_buffer[0], command); 250 | socket.send(big_buffer); 251 | return big_buffer; 252 | }; 253 | 254 | var ctrl_queue = { 255 | queue: [], 256 | in_oob_message: false, 257 | awaiting_result: false, 258 | msg_id: 0, 259 | name: "control" 260 | }; 261 | 262 | var compute_queue = { 263 | queue: [], 264 | in_oob_message: false, 265 | awaiting_result: false, 266 | msg_id: 1, 267 | name: "compute" 268 | }; 269 | 270 | // the order matters - the first queue is used if the association cannot be determined from the msg_id/cmd 271 | var queues = [ ctrl_queue, compute_queue ]; 272 | 273 | function queue_can_send(queue) { return !queue.in_oob_message && !queue.awaiting_result && queue.queue.length; } 274 | 275 | function bump_queues() { 276 | var available = _.filter(queues, queue_can_send); 277 | // nothing in the queues (or all busy)? get out 278 | if (!available.length) return; 279 | if (result.closed) { 280 | handle_error("Cannot send messages on a closed socket!", -1); 281 | } else { 282 | var queue = _.sortBy(available, function(queue) { return queue.queue[0].timestamp; })[0]; 283 | var lst = queue.queue.shift(); 284 | queue.result_callback = lst.callback; 285 | queue.awaiting_result = true; 286 | if (opts.debug) 287 | opts.debug.message_out && opts.debug.message_out(lst.buffer, lst.command); 288 | socket.send(lst.buffer); 289 | } 290 | } 291 | 292 | function enqueue(buffer, k, command, queue) { 293 | queue.queue.push({ 294 | buffer: buffer, 295 | callback: function(error, result) { 296 | queue.awaiting_result = false; 297 | bump_queues(); 298 | k(error, result); 299 | }, 300 | command: command, 301 | timestamp: Date.now() 302 | }); 303 | bump_queues(); 304 | }; 305 | 306 | function _cmd(command, buffer, k, string, queue) { 307 | // default to the first queue - only used in non-OCAP mode which doesn't support multiple queues 308 | if (!queue) queue = queues[0]; 309 | 310 | k = k || function() {}; 311 | var big_buffer = _encode_command(command, buffer, queue.msg_id); 312 | return enqueue(big_buffer, k, string, queue); 313 | }; 314 | 315 | result = { 316 | ocap_mode: false, 317 | running: false, 318 | closed: false, 319 | close: function() { 320 | socket.close(); 321 | }, 322 | 323 | ////////////////////////////////////////////////////////////////////// 324 | // non-ocap mode 325 | 326 | login: function(command, k) { 327 | _cmd(Rserve.Rsrv.CMD_login, _encode_string(command), k, command); 328 | }, 329 | eval: function(command, k) { 330 | _cmd(Rserve.Rsrv.CMD_eval, _encode_string(command), k, command); 331 | }, 332 | createFile: function(command, k) { 333 | _cmd(Rserve.Rsrv.CMD_createFile, _encode_string(command), k, command); 334 | }, 335 | writeFile: function(chunk, k) { 336 | _cmd(Rserve.Rsrv.CMD_writeFile, _encode_bytes(chunk), k, ""); 337 | }, 338 | closeFile: function(k) { 339 | _cmd(Rserve.Rsrv.CMD_closeFile, new ArrayBuffer(0), k, ""); 340 | }, 341 | set: function(key, value, k) { 342 | _cmd(Rserve.Rsrv.CMD_setSEXP, [_encode_string(key), _encode_value(value)], k, ""); 343 | }, 344 | 345 | ////////////////////////////////////////////////////////////////////// 346 | // ocap mode 347 | 348 | OCcall: function(ocap, values, k) { 349 | var is_ocap = false, str; 350 | try { 351 | is_ocap |= ocap.r_attributes['class'] === 'OCref'; 352 | str = ocap[0]; 353 | } catch (e) {}; 354 | if(!is_ocap) { 355 | try { 356 | is_ocap |= ocap.attributes.value[0].value.value[0] === 'OCref'; 357 | str = ocap.value[0]; 358 | } catch (e) {}; 359 | } 360 | if (!is_ocap) { 361 | k(new Error("Expected an ocap, instead got " + ocap), undefined); 362 | return; 363 | } 364 | var params = [str]; 365 | params.push.apply(params, values); 366 | // determine the proper queue from the OCAP prefix 367 | var queue = (str.charCodeAt(0) == 64) ? compute_queue : ctrl_queue; 368 | _cmd(Rserve.Rsrv.CMD_OCcall, _encode_value(params, Rserve.Rsrv.XT_LANG_NOTAG), 369 | k, "", queue); 370 | }, 371 | 372 | wrap_ocap: function(ocap) { 373 | return Rserve.wrap_ocap(this, ocap); 374 | }, 375 | 376 | resolve_hash: function(hash) { 377 | if (!(hash in captured_functions)) 378 | throw new Error("hash " + hash + " not found."); 379 | return captured_functions[hash]; 380 | } 381 | }; 382 | return result; 383 | }; 384 | 385 | Rserve.wrap_all_ocaps = function(s, v) { 386 | v = v.value.json(s.resolve_hash); 387 | function replace(obj) { 388 | var result = obj; 389 | if (_.isArray(obj) && 390 | obj.r_attributes && 391 | obj.r_attributes['class'] == 'OCref') { 392 | return Rserve.wrap_ocap(s, obj); 393 | } else if (_.isArray(obj)) { 394 | result = _.map(obj, replace); 395 | result.r_type = obj.r_type; 396 | result.r_attributes = obj.r_attributes; 397 | } else if (_.isTypedArray(obj)) { 398 | return obj; 399 | } else if (_.isFunction(obj)) { 400 | return obj; 401 | } else if (obj && !_.isUndefined(obj.byteLength) && !_.isUndefined(obj.slice)) { // ArrayBuffer 402 | return obj; 403 | } else if (_.isObject(obj)) { 404 | result = _.object(_.map(obj, function(v, k) { 405 | return [k, replace(v)]; 406 | })); 407 | } 408 | return result; 409 | } 410 | return replace(v); 411 | }; 412 | 413 | Rserve.wrap_ocap = function(s, ocap) { 414 | var wrapped_ocap = function() { 415 | var values = _.toArray(arguments); 416 | // common error (tho this won't catch the case where last arg is a function) 417 | if(!values.length || !_.isFunction(values[values.length-1])) 418 | throw new Error("forgot to pass continuation to ocap"); 419 | var k = values.pop(); 420 | s.OCcall(ocap, values, function(err, v) { 421 | if (!_.isUndefined(v)) 422 | v = Rserve.wrap_all_ocaps(s, v); 423 | k(err, v); 424 | }); 425 | }; 426 | wrapped_ocap.bare_ocap = ocap; 427 | return wrapped_ocap; 428 | }; 429 | 430 | })(); 431 | -------------------------------------------------------------------------------- /src/rsrv.js: -------------------------------------------------------------------------------- 1 | // Simple constants and functions are defined here, 2 | // in correspondence with Rserve's Rsrv.h 3 | 4 | Rserve.Rsrv = { 5 | PAR_TYPE: function(x) { return x & 255; }, 6 | PAR_LEN: function(x) { return x >>> 8; }, 7 | PAR_LENGTH: function(x) { return x >>> 8; }, 8 | par_parse: function(x) { return [Rserve.Rsrv.PAR_TYPE(x), Rserve.Rsrv.PAR_LEN(x)]; }, 9 | SET_PAR: function(ty, len) { return ((len & 0xffffff) << 8 | (ty & 255)); }, 10 | CMD_STAT: function(x) { return (x >>> 24) & 127; }, 11 | SET_STAT: function(x, s) { return x | ((s & 127) << 24); }, 12 | 13 | IS_OOB_SEND: function(x) { return (x & 0xffff000) === Rserve.Rsrv.OOB_SEND; }, 14 | IS_OOB_MSG: function(x) { return (x & 0xffff000) === Rserve.Rsrv.OOB_MSG; }, 15 | OOB_USR_CODE: function(x) { return x & 0xfff; }, 16 | 17 | CMD_RESP : 0x10000, 18 | RESP_OK : 0x10000 | 0x0001, 19 | RESP_ERR : 0x10000 | 0x0002, 20 | OOB_SEND : 0x20000 | 0x1000, 21 | OOB_MSG : 0x20000 | 0x2000, 22 | ERR_auth_failed : 0x41, 23 | ERR_conn_broken : 0x42, 24 | ERR_inv_cmd : 0x43, 25 | ERR_inv_par : 0x44, 26 | ERR_Rerror : 0x45, 27 | ERR_IOerror : 0x46, 28 | ERR_notOpen : 0x47, 29 | ERR_accessDenied : 0x48, 30 | ERR_unsupportedCmd : 0x49, 31 | ERR_unknownCmd : 0x4a, 32 | ERR_data_overflow : 0x4b, 33 | ERR_object_too_big : 0x4c, 34 | ERR_out_of_mem : 0x4d, 35 | ERR_ctrl_closed : 0x4e, 36 | ERR_session_busy : 0x50, 37 | ERR_detach_failed : 0x51, 38 | ERR_disabled : 0x61, 39 | ERR_unavailable : 0x62, 40 | ERR_cryptError : 0x63, 41 | ERR_securityClose : 0x64, 42 | 43 | CMD_login : 0x001, 44 | CMD_voidEval : 0x002, 45 | CMD_eval : 0x003, 46 | CMD_shutdown : 0x004, 47 | CMD_switch : 0x005, 48 | CMD_keyReq : 0x006, 49 | CMD_secLogin : 0x007, 50 | CMD_OCcall : 0x00f, 51 | CMD_openFile : 0x010, 52 | CMD_createFile : 0x011, 53 | CMD_closeFile : 0x012, 54 | CMD_readFile : 0x013, 55 | CMD_writeFile : 0x014, 56 | CMD_removeFile : 0x015, 57 | CMD_setSEXP : 0x020, 58 | CMD_assignSEXP : 0x021, 59 | CMD_detachSession : 0x030, 60 | CMD_detachedVoidEval : 0x031, 61 | CMD_attachSession : 0x032, 62 | CMD_ctrl : 0x40, 63 | CMD_ctrlEval : 0x42, 64 | CMD_ctrlSource : 0x45, 65 | CMD_ctrlShutdown : 0x44, 66 | CMD_setBufferSize : 0x081, 67 | CMD_setEncoding : 0x082, 68 | CMD_SPECIAL_MASK : 0xf0, 69 | CMD_serEval : 0xf5, 70 | CMD_serAssign : 0xf6, 71 | CMD_serEEval : 0xf7, 72 | 73 | DT_INT : 1, 74 | DT_CHAR : 2, 75 | DT_DOUBLE : 3, 76 | DT_STRING : 4, 77 | DT_BYTESTREAM : 5, 78 | DT_SEXP : 10, 79 | DT_ARRAY : 11, 80 | DT_LARGE : 64, 81 | 82 | XT_NULL : 0, 83 | XT_INT : 1, 84 | XT_DOUBLE : 2, 85 | XT_STR : 3, 86 | XT_LANG : 4, 87 | XT_SYM : 5, 88 | XT_BOOL : 6, 89 | XT_S4 : 7, 90 | XT_VECTOR : 16, 91 | XT_LIST : 17, 92 | XT_CLOS : 18, 93 | XT_SYMNAME : 19, 94 | XT_LIST_NOTAG : 20, 95 | XT_LIST_TAG : 21, 96 | XT_LANG_NOTAG : 22, 97 | XT_LANG_TAG : 23, 98 | XT_VECTOR_EXP : 26, 99 | XT_VECTOR_STR : 27, 100 | XT_ARRAY_INT : 32, 101 | XT_ARRAY_DOUBLE : 33, 102 | XT_ARRAY_STR : 34, 103 | XT_ARRAY_BOOL_UA : 35, 104 | XT_ARRAY_BOOL : 36, 105 | XT_RAW : 37, 106 | XT_ARRAY_CPLX : 38, 107 | XT_UNKNOWN : 48, 108 | XT_LARGE : 64, 109 | XT_HAS_ATTR : 128, 110 | 111 | BOOL_TRUE : 1, 112 | BOOL_FALSE : 0, 113 | BOOL_NA : 2, 114 | 115 | GET_XT: function(x) { return x & 63; }, 116 | GET_DT: function(x) { return x & 63; }, 117 | HAS_ATTR: function(x) { return (x & Rserve.Rsrv.XT_HAS_ATTR) > 0; }, 118 | IS_LARGE: function(x) { return (x & Rserve.Rsrv.XT_LARGE) > 0; }, 119 | 120 | // # FIXME A WHOLE LOT OF MACROS HERE WHICH ARE PROBABLY IMPORTANT 121 | // ############################################################################## 122 | 123 | status_codes: { 124 | 0x41 : "ERR_auth_failed" , 125 | 0x42 : "ERR_conn_broken" , 126 | 0x43 : "ERR_inv_cmd" , 127 | 0x44 : "ERR_inv_par" , 128 | 0x45 : "ERR_Rerror" , 129 | 0x46 : "ERR_IOerror" , 130 | 0x47 : "ERR_notOpen" , 131 | 0x48 : "ERR_accessDenied" , 132 | 0x49 : "ERR_unsupportedCmd", 133 | 0x4a : "ERR_unknownCmd" , 134 | 0x4b : "ERR_data_overflow" , 135 | 0x4c : "ERR_object_too_big", 136 | 0x4d : "ERR_out_of_mem" , 137 | 0x4e : "ERR_ctrl_closed" , 138 | 0x50 : "ERR_session_busy" , 139 | 0x51 : "ERR_detach_failed" , 140 | 0x61 : "ERR_disabled" , 141 | 0x62 : "ERR_unavailable" , 142 | 0x63 : "ERR_cryptError" , 143 | 0x64 : "ERR_securityClose" 144 | } 145 | }; 146 | -------------------------------------------------------------------------------- /src/write.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | _.mixin({ 4 | isTypedArray: function(v) { 5 | if (!_.isObject(v)) 6 | return false; 7 | return !_.isUndefined(v.byteLength) && !_.isUndefined(v.BYTES_PER_ELEMENT); 8 | } 9 | }); 10 | 11 | // type_id tries to match some javascript values to Rserve value types 12 | Rserve.type_id = function(value) 13 | { 14 | if (_.isNull(value) || _.isUndefined(value)) 15 | return Rserve.Rsrv.XT_NULL; 16 | var type_dispatch = { 17 | "boolean": Rserve.Rsrv.XT_ARRAY_BOOL, 18 | "number": Rserve.Rsrv.XT_ARRAY_DOUBLE, 19 | "string": Rserve.Rsrv.XT_ARRAY_STR // base strings need to be array_str or R gets confused? 20 | }; 21 | if (!_.isUndefined(type_dispatch[typeof value])) 22 | return type_dispatch[typeof value]; 23 | 24 | // typed arrays 25 | if (_.isTypedArray(value)) 26 | return Rserve.Rsrv.XT_ARRAY_DOUBLE; 27 | 28 | // arraybuffers 29 | if (!_.isUndefined(value.byteLength) && !_.isUndefined(value.slice)) 30 | return Rserve.Rsrv.XT_RAW; 31 | 32 | // lists of strings (important for tags) 33 | if (_.isArray(value) && _.all(value, function(el) { return typeof el === 'string'; })) 34 | return Rserve.Rsrv.XT_ARRAY_STR; 35 | 36 | if (_.isArray(value) && _.all(value, function(el) { return typeof el === 'boolean'; })) 37 | return Rserve.Rsrv.XT_ARRAY_BOOL; 38 | 39 | // arbitrary lists 40 | if (_.isArray(value)) 41 | return Rserve.Rsrv.XT_VECTOR; 42 | 43 | // functions get passed as an array_str with extra attributes 44 | if (_.isFunction(value)) 45 | return Rserve.Rsrv.XT_ARRAY_STR | Rserve.Rsrv.XT_HAS_ATTR; 46 | 47 | // objects 48 | if (_.isObject(value)) 49 | return Rserve.Rsrv.XT_VECTOR | Rserve.Rsrv.XT_HAS_ATTR; 50 | 51 | throw new Rserve.RServeError("Value type unrecognized by Rserve: " + value); 52 | }; 53 | 54 | // FIXME this is really slow, as it's walking the object many many times. 55 | Rserve.determine_size = function(value, forced_type) 56 | { 57 | function list_size(lst) { 58 | return _.reduce(lst, function(memo, el) { 59 | return memo + Rserve.determine_size(el); 60 | }, 0); 61 | } 62 | function final_size(payload_size) { 63 | if (payload_size > (1 << 24)) 64 | return payload_size + 8; // large header 65 | else 66 | return payload_size + 4; 67 | } 68 | var header_size = 4, t = forced_type || Rserve.type_id(value); 69 | 70 | switch (t & ~Rserve.Rsrv.XT_LARGE) { 71 | case Rserve.Rsrv.XT_NULL: 72 | return final_size(0); 73 | case Rserve.Rsrv.XT_ARRAY_BOOL: 74 | if (_.isBoolean(value)) 75 | return final_size(8); 76 | else 77 | return final_size((value.length + 7) & ~3); 78 | case Rserve.Rsrv.XT_ARRAY_STR: 79 | if (_.isArray(value)) 80 | return final_size(_.reduce(value, function(memo, str) { 81 | // FIXME: this is a bit silly, since we'll be re-encoding this twice: once for the size and second time for the content 82 | var utf8 = unescape(encodeURIComponent(str)); 83 | return memo + utf8.length + 1; 84 | }, 0)); 85 | else { 86 | var utf8 = unescape(encodeURIComponent(value)); 87 | return final_size(utf8.length + 1); 88 | } 89 | case Rserve.Rsrv.XT_ARRAY_DOUBLE: 90 | if (_.isNumber(value)) 91 | return final_size(8); 92 | else 93 | return final_size(8 * value.length); 94 | case Rserve.Rsrv.XT_RAW: 95 | return final_size(4 + value.byteLength); 96 | case Rserve.Rsrv.XT_VECTOR: 97 | case Rserve.Rsrv.XT_LANG_NOTAG: 98 | return final_size(list_size(value)); 99 | case Rserve.Rsrv.XT_VECTOR | Rserve.Rsrv.XT_HAS_ATTR: // a named list (that is, a js object) 100 | var names_size_1 = final_size("names".length + 3); 101 | var names_size_2 = Rserve.determine_size(_.keys(value)); 102 | var names_size = final_size(names_size_1 + names_size_2); 103 | return final_size(names_size + list_size(_.values(value))); 104 | /* return header_size // XT_VECTOR | XT_HAS_ATTR 105 | + header_size // XT_LIST_TAG (attribute) 106 | + header_size + "names".length + 3 // length of 'names' + padding (tag as XT_SYMNAME) 107 | + Rserve.determine_size(_.keys(value)) // length of names 108 | + list_size(_.values(value)); // length of values 109 | */ 110 | case Rserve.Rsrv.XT_ARRAY_STR | Rserve.Rsrv.XT_HAS_ATTR: // js->r ocap (that is, a js function) 111 | return Rserve.determine_size("0403556553") // length of ocap nonce; that number is meaningless aside from length 112 | + header_size // XT_LIST_TAG (attribute) 113 | + header_size + "class".length + 3 // length of 'class' + padding (tag as XT_SYMNAME) 114 | + Rserve.determine_size(["javascript_function"]); // length of class name 115 | 116 | default: 117 | throw new Rserve.RserveError("Internal error, can't handle type " + t); 118 | } 119 | }; 120 | 121 | Rserve.write_into_view = function(value, array_buffer_view, forced_type, convert) 122 | { 123 | var size = Rserve.determine_size(value, forced_type); 124 | var is_large = size > 16777215; 125 | // if (size > 16777215) 126 | // throw new Rserve.RserveError("Can't currently handle objects >16MB"); 127 | var t = forced_type || Rserve.type_id(value), i, current_offset, lbl; 128 | if (is_large) 129 | t = t | Rserve.Rsrv.XT_LARGE; 130 | var read_view; 131 | var write_view = array_buffer_view.data_view(); 132 | var payload_start; 133 | if (is_large) { 134 | payload_start = 8; 135 | write_view.setInt32(0, t + ((size - 8) << 8)); 136 | write_view.setInt32(4, (size - 8) >>> 24); 137 | } else { 138 | payload_start = 4; 139 | write_view.setInt32(0, t + ((size - 4) << 8)); 140 | } 141 | 142 | switch (t & ~Rserve.Rsrv.XT_LARGE) { 143 | case Rserve.Rsrv.XT_NULL: 144 | break; 145 | case Rserve.Rsrv.XT_ARRAY_BOOL: 146 | if (_.isBoolean(value)) { 147 | write_view.setInt32(payload_start, 1); 148 | write_view.setInt8(payload_start + 4, value ? 1 : 0); 149 | } else { 150 | write_view.setInt32(payload_start, value.length); 151 | for (i=0; i