1/** 2 * JsonRpcClient 3 * 4 * A JSON RPC Client that uses WebSockets if available otherwise fallbacks to ajax. 5 * Depends on JSON, if browser lacks native support either use JSON3 or jquery.json. 6 * Usage example: 7 * 8 * var foo = new $.JsonRpcClient({ ajaxUrl: '/backend/jsonrpc' }); 9 * foo.call( 10 * 'bar', [ 'A parameter', 'B parameter' ], 11 * function(result) { alert('Foo bar answered: ' + result.my_answer); }, 12 * function(error) { console.log('There was an error', error); } 13 * ); 14 * 15 * More examples are available in README.md 16 */ 17(function($) { 18 19 /** 20 * @fn new 21 * @memberof JsonRpcClient 22 * 23 * @param {object} options An object stating the backends: 24 * ajaxUrl A url (relative or absolute) to a http(s) backend. 25 * headers An object that will be passed along to $.ajax in options.headers 26 * xhrFields An object that will be passed along to $.ajax in options.xhrFields 27 * socketUrl A url (relative of absolute) to a ws(s) backend. 28 * onmessage A socket message handler for other messages (non-responses). 29 * onopen A socket onopen handler. (Not used for custom getSocket.) 30 * onclose A socket onclose handler. (Not used for custom getSocket.) 31 * onerror A socket onerror handler. (Not used for custom getSocket.) 32 * getSocket A function returning a WebSocket or null. 33 * It must take an onmessage_cb and bind it to the onmessage event 34 * (or chain it before/after some other onmessage handler). 35 * Or, it could return null if no socket is available. 36 * The returned instance must have readyState <= 1, and if less than 1, 37 * react to onopen binding. 38 * timeout (optional) A number of ms to wait before timing out and failing a 39 * call. If specified a setTimeout will be used to keep track of calls 40 * made through a websocket. 41 */ 42 var JsonRpcClient = function(options) { 43 var self = this; 44 var noop = function() {}; 45 this.options = $.extend({ 46 ajaxUrl : null, 47 headers : {}, ///< Optional additional headers to send in $.ajax request. 48 socketUrl : null, ///< WebSocket URL. (Not used if a custom getSocket is supplied.) 49 onmessage : noop, ///< Optional onmessage-handler for WebSocket. 50 onopen : noop, ///< Optional onopen-handler for WebSocket. 51 onclose : noop, ///< Optional onclose-handler for WebSocket. 52 onerror : noop, ///< Optional onerror-handler for WebSocket. 53 /// Custom socket supplier for using an already existing socket 54 getSocket : function(onmessageCb) { return self._getSocket(onmessageCb); } 55 }, options); 56 57 // Declare an instance version of the onmessage callback to wrap 'this'. 58 this.wsOnMessage = function(event) { self._wsOnMessage(event); }; 59 60 /// Holding the WebSocket on default getsocket. 61 this._wsSocket = null; 62 63 /// Object <id>: { success_cb: cb, error_cb: cb } 64 this._wsCallbacks = {}; 65 66 /// The next JSON-RPC request id. 67 this._currentId = 1; 68 69 //queue for ws request sent *before* ws is open. 70 this._wsRequestQueue = []; 71 72 if (!window.JSON && $ && $.toJSON) { 73 this.JSON = { 74 stringify: $.toJSON, 75 parse: $.parseJSON 76 }; 77 } else { 78 this.JSON = JSON; 79 } 80 81 }; 82 83 /** 84 * @fn call 85 * @memberof JsonRpcClient 86 * 87 * @param {string} method The method to run on JSON-RPC server. 88 * @param {object|array} params The params; an array or object. 89 * @param {function} successCb A callback for successful request. 90 * @param {function} errorCb A callback for error. 91 * 92 * @return {object} Returns the deferred object that $.ajax returns or {null} for websockets 93 */ 94 JsonRpcClient.prototype.call = function(method, params, successCb, errorCb) { 95 successCb = typeof successCb === 'function' ? successCb : function() {}; 96 errorCb = typeof errorCb === 'function' ? errorCb : function() {}; 97 98 // Construct the JSON-RPC 2.0 request. 99 var request = { 100 jsonrpc : '2.0', 101 method : method, 102 params : params, 103 id : this._currentId++ // Increase the id counter to match request/response 104 }; 105 106 // Try making a WebSocket call. 107 var socket = this.options.getSocket(this.wsOnMessage); 108 if (socket !== null) { 109 this._wsCall(socket, request, successCb, errorCb); 110 return null; 111 } 112
113 // No WebSocket, and no HTTP backend? This won't work. 114 if (this.options.ajaxUrl === null) { 115 throw 'JsonRpcClient.call used with no websocket and no http endpoint.'; 116 } 117 118 var self = this; 119 120 var deferred = $.ajax({ 121 type : 'POST', 122 url : this.options.ajaxUrl, 123 contentType: 'application/json', 124 data : this.JSON.stringify(request), 125 dataType : 'json', 126 cache : false, 127 headers : this.options.headers, 128 xhrFields : this.options.xhrFields, 129 timeout : this.options.timeout, 130 131 success : function(data) { 132 if ('error' in data) { 133 errorCb(data.error); 134 } else { 135 successCb(data.result); 136 } 137 }, 138 139 // JSON-RPC Server could return non-200 on error 140 error : function(jqXHR, textStatus, errorThrown) { 141 try { 142 var response = self.JSON.parse(jqXHR.responseText); 143 if ('console' in window) { console.log(response); } 144 145 errorCb(response.error); 146 } 147 catch (err) { 148 // Perhaps the responseText wasn't really a jsonrpc-error. 149 errorCb({error: jqXHR.responseText}); 150 } 151 } 152 }); 153 154 return deferred; 155 }; 156 157 /** 158 * Notify sends a command to the server that won't need a response. In http, there is probably 159 * an empty response - that will be dropped, but in ws there should be no response at all. 160 * 161 * This is very similar to call, but has no id and no handling of callbacks. 162 * 163 * @fn notify 164 * @memberof JsonRpcClient 165 * 166 * @param {string} method The method to run on JSON-RPC server. 167 * @param {object|array} params The params; an array or object. 168 * 169 * @return {object} Returns the deferred object that $.ajax returns or {null} for websockets 170 */ 171 JsonRpcClient.prototype.notify = function(method, params) { 172 // Construct the JSON-RPC 2.0 request. 173 var request = { 174 jsonrpc: '2.0', 175 method: method, 176 params: params 177 }; 178 179 // Try making a WebSocket call. 180 var socket = this.options.getSocket(this.wsOnMessage); 181 if (socket !== null) { 182 this._wsCall(socket, request); 183 return null; 184 } 185 186 // No WebSocket, and no HTTP backend? This won't work. 187 if (this.options.ajaxUrl === null) { 188 throw 'JsonRpcClient.notify used with no websocket and no http endpoint.'; 189 } 190 191 var deferred = $.ajax({ 192 type : 'POST', 193 url : this.options.ajaxUrl, 194 contentType: 'application/json', 195 data : this.JSON.stringify(request), 196 dataType : 'json', 197 cache : false, 198 headers : this.options.headers, 199 xhrFields : this.options.xhrFields 200 }); 201 202 return deferred; 203 }; 204 205 /** 206 * Make a batch-call by using a callback. 207 * 208 * The callback will get an object "batch" as only argument. On batch, you can call the methods 209 * "call" and "notify" just as if it was a normal JsonRpcClient object, and all calls will be 210 * sent as a batch call then the callback is done. 211 * 212 * @fn batch 213 * @memberof JsonRpcClient 214 * 215 * @param {function} callback This function will get a batch handler to run call and notify on. 216 * @param {function} allDoneCb A callback function to call after all results have been handled. 217 * @param {function} errorCb A callback function to call if there is an error from the server. 218 * Note, that batch calls should always get an overall success, and the 219 * only error 220 */ 221 JsonRpcClient.prototype.batch = function(callback, allDoneCb, errorCb) { 222 var batch = new JsonRpcClient._batchObject(this, allDoneCb, errorCb); 223 callback(batch); 224 batch._execute(); 225 }; 226 227 /** 228 * The default getSocket handler. 229 * 230 * @param {function} onmessageCb The callback to be bound to onmessage events on the socket. 231 * 232 * @fn _getSocket 233 * @memberof JsonRpcClient 234 */ 235 JsonRpcClient.prototype._getSocket = function(onmessageCb) { 236 // If there is no ws url set, we don't have a socket. 237 // Likewise, if there is no window.WebSocket. 238 if (this.options.socketUrl === null || !('WebSocket' in window)) { return null; } 239 240 if (this._wsSocket === null || this._wsSocket.readyState > 1) { 241
242 try { 243 // No socket, or dying socket, let's get a new one. 244 this._wsSocket = new WebSocket(this.options.socketUrl); 245 } catch (e) { 246 // This can happen if the server is down, or malconfigured. 247 return null; 248 } 249 250 // Set up onmessage handler. 251 this._wsSocket.onmessage = onmessageCb; 252 253 var that = this; 254 // Set up onclose handler. 255 this._wsSocket.onclose = function(ev) { that._wsOnClose(ev); }; 256 257 // Set up onerror handler. 258 this._wsSocket.onerror = function(ev) { that._wsOnError(ev); }; 259 } 260 261 return this._wsSocket; 262 }; 263 264 /** 265 * Internal handler to dispatch a JRON-RPC request through a websocket. 266 * 267 * @fn _wsCall 268 * @memberof JsonRpcClient 269 */ 270 JsonRpcClient.prototype._wsCall = function(socket, request, successCb, errorCb) { 271 var requestJson = this.JSON.stringify(request); 272 273 // Setup callbacks. If there is an id, this is a call and not a notify. 274 if ('id' in request && typeof successCb !== 'undefined') { 275 this._wsCallbacks[request.id] = {successCb: successCb, errorCb: errorCb}; 276 } 277 278 if (socket.readyState < 1) { 279 280 // Queue request 281 this._wsRequestQueue.push(requestJson); 282 283 if (!socket.onopen) { 284 // The websocket is not open yet; we have to set sending of the message in onopen. 285 var self = this; // In closure below, this is set to the WebSocket. Use self instead. 286 287 // Set up sending of message for when the socket is open. 288 socket.onopen = function(event) { 289 // Hook for extra onopen callback 290 self.options.onopen(event); 291 292 // Send queued requests. 293 var timeout = self.options.timeout; 294 var request; 295 for (var i = 0; i < self._wsRequestQueue.length; i++) { 296 request = self._wsRequestQueue[i]; 297 298 // Do we use timeouts, and if so, is it a call? 299 if (timeout && self._wsCallbacks[request.id]) { 300 self._wsCallbacks[request.id].timeout = self._createTimeout(request.id); 301 } 302 socket.send(request); 303 } 304 self._wsRequestQueue = []; 305 }; 306 } 307 } else { 308 309 // Do we use timeouts, and if so, is it a call? 310 if (this.options.timeout && this._wsCallbacks[request.id]) { 311 this._wsCallbacks[request.id].timeout = this._createTimeout(request.id); 312 } 313 314 // We have a socket and it should be ready to send on. 315 socket.send(requestJson); 316 } 317 }; 318 319 /** 320 * Internal handler for the websocket messages. It determines if the message is a JSON-RPC 321 * response, and if so, tries to couple it with a given callback. Otherwise, it falls back to 322 * given external onmessage-handler, if any. 323 * 324 * @param {event} event The websocket onmessage-event. 325 */ 326 JsonRpcClient.prototype._wsOnMessage = function(event) { 327 328 // Check if this could be a JSON RPC message. 329 var response; 330 try { 331 response = this.JSON.parse(event.data); 332 } catch (err) { 333 this.options.onmessage(event); 334 return; 335 } 336 337 /// @todo Make using the jsonrcp 2.0 check optional, to use this on JSON-RPC 1 backends. 338 if (typeof response === 'object' && response.jsonrpc === '2.0') { 339 340 /// @todo Handle bad response (without id). 341 342 // If this is an object with result, it is a response. 343 if ('result' in response && this._wsCallbacks[response.id]) { 344 // Get the success callback. 345 var successCb = this._wsCallbacks[response.id].successCb; 346 347 // Clear any timeout 348 if (this._wsCallbacks[response.id].timeout) { 349 clearTimeout(this._wsCallbacks[response.id].timeout); 350 } 351 352 // Delete the callback from the storage. 353 delete this._wsCallbacks[response.id]; 354 355 // Run callback with result as parameter. 356 successCb(response.result); 357 return; 358 } 359 360 // If this is an object with error, it is an error response. 361 else if ('error' in response && this._wsCallbacks[response.id]) { 362 // Get the error callback. 363 var errorCb = this._wsCallbacks[response.id].errorCb; 364 365 // Delete the callback from the storage. 366 delete this._wsCallbacks[response.id]; 367 368 // Run callback with the error object as parameter. 369 errorCb(response.error); 370 return; 371 } 372 } 373
374 // If we get here it's an invalid JSON-RPC response, pass to fallback message handler. 375 this.options.onmessage(event); 376 }; 377 378 /** 379 * Internal WebSocket error handler. 380 * Will execute all unresolved calls immideatly. 381 **/ 382 JsonRpcClient.prototype._wsOnError = function(event) { 383 this._failAllCalls('Socket errored.'); 384 this.options.onerror(event); 385 }; 386 387 /** 388 * Internal WebSocket close handler. 389 * Will execute all unresolved calls immideatly. 390 **/ 391 JsonRpcClient.prototype._wsOnClose = function(event) { 392 this._failAllCalls('Socket closed.'); 393 this.options.onclose(event); 394 }; 395 396 /** 397 * Execute error handler on all pending calls. 398 */ 399 JsonRpcClient.prototype._failAllCalls = function(error) { 400 for (var key in this._wsCallbacks) { 401 if (this._wsCallbacks.hasOwnProperty(key)) { 402 // Get the error callback. 403 var errorCb = this._wsCallbacks[key].errorCb; 404 405 // Run callback with the error object as parameter. 406 errorCb(error); 407 } 408 } 409 410 // Throw 'em away 411 this._wsCallbacks = {}; 412 }; 413 414 /** 415 * Create a timeout for this request 416 */ 417 JsonRpcClient.prototype._createTimeout = function(id) { 418 if (this.options.timeout) { 419 var that = this; 420 return setTimeout(function() { 421 if (that._wsCallbacks[id]) { 422 var errorCb = that._wsCallbacks[id].errorCb; 423 delete that._wsCallbacks[id]; 424 errorCb('Call timed out.'); 425 } 426 }, this.options.timeout); 427 } 428 }; 429 430 /************************************************************************************************ 431 * Batch object with methods 432 ************************************************************************************************/ 433 434 /** 435 * Handling object for batch calls. 436 */ 437 JsonRpcClient._batchObject = function(jsonrpcclient, allDoneCb, errorCb) { 438 // Array of objects to hold the call and notify requests. Each objects will have the request 439 // object, and unless it is a notify, successCb and errorCb. 440 this._requests = []; 441 442 this.jsonrpcclient = jsonrpcclient; 443 this.allDoneCb = allDoneCb; 444 this.errorCb = typeof errorCb === 'function' ? errorCb : function() {}; 445 }; 446 447 /** 448 * @sa JsonRpcClient.prototype.call 449 */ 450 JsonRpcClient._batchObject.prototype.call = function(method, params, successCb, errorCb) { 451 this._requests.push({ 452 request : { 453 jsonrpc : '2.0', 454 method : method, 455 params : params, 456 id : this.jsonrpcclient._currentId++ // Use the client's id series. 457 }, 458 successCb : successCb, 459 errorCb : errorCb 460 }); 461 }; 462 463 /** 464 * @sa JsonRpcClient.prototype.notify 465 */ 466 JsonRpcClient._batchObject.prototype.notify = function(method, params) { 467 this._requests.push({ 468 request : { 469 jsonrpc : '2.0', 470 method : method, 471 params : params 472 } 473 }); 474 }; 475 476 /** 477 * Executes the batched up calls. 478 * 479 * @return {object} Returns the deferred object that $.ajax returns or {null} for websockets 480 */ 481 JsonRpcClient._batchObject.prototype._execute = function() { 482 var self = this; 483 var deferred = null; // Used to store and return the deffered that $.ajax returns 484 485 if (this._requests.length === 0) { return; } // All done :P 486 487 // Collect all request data and sort handlers by request id. 488 var batchRequest = []; 489 490 // If we have a WebSocket, just send the requests individually like normal calls. 491 var socket = self.jsonrpcclient.options.getSocket(self.jsonrpcclient.wsOnMessage); 492 493 if (socket !== null) { 494 // We need to keep track of results for the all done callback 495 var expectedNrOfCb = 0; 496 var cbResults = []; 497 498 var wrapCb = function(cb) { 499 if (!self.allDoneCb) { // No all done callback? no need to keep track 500 return cb; 501 } 502 503 return function(data) { 504 cb(data); 505 cbResults.push(data); 506 expectedNrOfCb--; 507 if (expectedNrOfCb <= 0) { 508 // Change order so that it maps to request order 509 var i; 510 var resultMap = {}; 511 for (i = 0; i < cbResults.length; i++) { 512 resultMap[cbResults[i].id] = cbResults[i];
513 } 514 var results = []; 515 for (i = 0; i < self._requests.length; i++) { 516 if (resultMap[self._requests[i].id]) { 517 results.push(resultMap[self._requests[i].id]); 518 } 519 } 520 // Call all done! 521 self.allDoneCb(results); 522 } 523 }; 524 }; 525 526 for (var i = 0; i < this._requests.length; i++) { 527 var call = this._requests[i]; 528 529 if ('id' in call.request) { 530 // We expect an answer 531 expectedNrOfCb++; 532 } 533 534 self.jsonrpcclient._wsCall( 535 socket, call.request, wrapCb(call.successCb), wrapCb(call.errorCb) 536 ); 537 } 538 539 return null; 540 } else { 541 // No websocket, let's use ajax 542 var handlers = {}; 543 544 for (var i = 0; i < this._requests.length; i++) { 545 var call = this._requests[i]; 546 batchRequest.push(call.request); 547 548 // If the request has an id, it should handle returns (otherwise it's a notify). 549 if ('id' in call.request) { 550 handlers[call.request.id] = { 551 successCb : call.successCb, 552 errorCb : call.errorCb 553 }; 554 } 555 } 556 557 var successCb = function(data) { self._batchCb(data, handlers, self.allDoneCb); }; 558 559 // No WebSocket, and no HTTP backend? This won't work. 560 if (self.jsonrpcclient.options.ajaxUrl === null) { 561 throw 'JsonRpcClient.batch used with no websocket and no http endpoint.'; 562 } 563 564 // Send request 565 deferred = $.ajax({ 566 url : self.jsonrpcclient.options.ajaxUrl, 567 contentType: 'application/json', 568 data : this.jsonrpcclient.JSON.stringify(batchRequest), 569 dataType : 'json', 570 cache : false, 571 type : 'POST', 572 headers : self.jsonrpcclient.options.headers, 573 xhrFields : self.jsonrpcclient.options.xhrFields, 574 575 // Batch-requests should always return 200 576 error : function(jqXHR, textStatus, errorThrown) { 577 self.errorCb(jqXHR, textStatus, errorThrown); 578 }, 579 success : successCb 580 }); 581 582 return deferred; 583 584 } 585 586 }; 587 588 /** 589 * Internal helper to match the result array from a batch call to their respective callbacks. 590 * 591 * @fn _batchCb 592 * @memberof JsonRpcClient 593 */ 594 JsonRpcClient._batchObject.prototype._batchCb = function(result, handlers, allDoneCb) { 595 for (var i = 0; i < result.length; i++) { 596 var response = result[i]; 597 598 // Handle error 599 if ('error' in response) { 600 if (response.id === null || !(response.id in handlers)) { 601 // An error on a notify? Just log it to the console. 602 if ('console' in window) { console.log(response); } 603 } else { 604 handlers[response.id].errorCb(response.error); 605 } 606 } else { 607 // Here we should always have a correct id and no error. 608 if (!(response.id in handlers) && 'console' in window) { 609 console.log(response); 610 } else { 611 handlers[response.id].successCb(response.result); 612 } 613 } 614 } 615 616 if (typeof allDoneCb === 'function') { allDoneCb(result); } 617 }; 618 619 $.JsonRpcClient = JsonRpcClient; 620 621})(this.jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.