PageSourceSearch

https://nstatus.org/lib/json-rpc/jquery.jsonrpcclient.js

js nstatus.org collected 2026-10-02 01:07:02 UTC 21,349 bytes, 621 lines download raw bytes

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.