1 /**
  2  * @fileOverview
  3  * @author <a href="https://www.labkey.org">LabKey</a> (<a href="mailto:info@labkey.com">info@labkey.com</a>)
  4  * @license Copyright (c) 2012-2019 LabKey Corporation
  5  * <p/>
  6  * Licensed under the Apache License, Version 2.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at
  9  * <p/>
 10  * http://www.apache.org/licenses/LICENSE-2.0
 11  * <p/>
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing permissions and
 16  * limitations under the License.
 17  * <p/>
 18  */
 19 
 20 /**
 21  * Make multiple ajax requests and invokes a callback when all are complete.
 22  * Requests are added as [function, config] array pairs where the config object
 23  * is passed as the argument to the request function.  The request function's config
 24  * object argument must accept a success callback named 'success' and a failure
 25  * callback named 'failure'.
 26  * @class Make multiple ajax requests and fires an event when all are complete.
 27  * @memberOf LABKEY
 28  *
 29  * @param [config] Either an array of [function, config] array pairs
 30  * to be added or a config object with the shape:
 31  * <ul>
 32  * <li>listeners: a config object containing event handlers.
 33  * <li>requests: an array of [function, config] array pairs to be added.
 34  * </ul>
 35  * @example
 36  var config = {
 37    schemaName : "assay",
 38    queryName : protocolName + " Data",
 39    containerPath : "/Test",
 40    success: function (data, options, response) {
 41        console.log("selectRows success: " + data.rowCount);
 42    },
 43    failure: function (response, options) {
 44        console.log("selectRows failure");
 45    },
 46    scope: scope // scope to execute success and failure callbacks in.
 47  };
 48 
 49  // add the requests and config arguments one by one
 50  var multi = new LABKEY.MultiRequest();
 51  var requestScope = ... // scope to execute the request function in.
 52  multi.add(LABKEY.Query.selectRows, config, requestScope);
 53  multi.add(LABKEY.Query.selectRows, config, requestScope);
 54  multi.add(LABKEY.Query.selectRows, config, requestScope);
 55  multi.send(
 56    function () { console.log("send complete"); },
 57    sendCallbackScope // scope to execute 'send complete' callback in.
 58  );
 59 
 60  // additional requests won't be sent while other requests are in progress
 61  multi.add(LABKEY.Query.selectRows, config);
 62  multi.send(function () { console.log("send complete"); }, sendCallbackScope);
 63 
 64  // constructor can take an array of requests [function, config] pairs
 65  multi = new LABKEY.MultiRequest([
 66       [ LABKEY.Query.selectRows, config ],
 67       [ LABKEY.Query.selectRows, config ],
 68       [ LABKEY.Query.selectRows, config ]
 69  ]);
 70  multi.send();
 71 
 72  // constructor can take a config object with listeners and requests.
 73  // if there is a 'done' listener, the requests will be sent immediately.
 74  multi = new LABKEY.MultiRequest({
 75    listeners : { 'done': function () { console.log("send complete"); }, scope: sendCallbackScope },
 76    requests : [ [ LABKEY.Query.selectRows, config ],
 77                 [ LABKEY.Query.selectRows, config ],
 78                 [ LABKEY.Query.selectRows, config ] ]
 79  });
 80 
 81  // Alternate syntax for adding the 'done' event listener.
 82  multi = new LABKEY.MultiRequest({
 83    listeners : {
 84      'done': {
 85         fn: function () { console.log("send complete"); }
 86         scope: sendCallbackScope
 87      }
 88    },
 89  });
 90  * </pre>
 91  */
 92 LABKEY.MultiRequest = function (config) {
 93     config = config || {};
 94 
 95     var self = this;
 96     var sending = false;
 97     var waitQ = [];
 98     var sendQ = [];
 99 
100     var requests;
101     var listeners;
102     if (LABKEY.Utils.isArray(config)) {
103         requests = config;
104     } else {
105         requests = config.requests;
106         listeners = config.listeners;
107     }
108 
109     if (requests) {
110         for (var i = 0; i < requests.length; i++) {
111             var request = requests[i];
112             this.add(request[0], request[1]);
113         }
114     }
115 
116     var doneCallbacks = [];
117     if (listeners && listeners.done) {
118         if (typeof listeners.done == "function") {
119             doneCallbacks.push({fn: listeners.done, scope: listeners.scope});
120         }
121         else if (typeof listeners.done.fn == "function") {
122             doneCallbacks.push({fn: listeners.done.fn, scope: listeners.done.scope || listeners.scope});
123         }
124     }
125 
126     if (waitQ.length && doneCallbacks.length > 0) {
127         this.send();
128     }
129 
130     function fireDone() {
131         //console.log("fireDone:");
132         for (var i = 0; i < doneCallbacks.length; i++) {
133             var cb = doneCallbacks[i];
134             //console.log("  invoking done callback: ", cb);
135             if (cb.fn && typeof cb.fn == "function") {
136                 cb.fn.call(cb.scope || window);
137             }
138         }
139     }
140 
141     function checkDone() {
142         //console.log("checkDone: sendQ.length=" + sendQ.length);
143         sendQ.pop();
144         if (sendQ.length == 0) {
145             sending = false;
146             fireDone();
147             self.send();
148         }
149         return true;
150     }
151 
152     function createSequence(fn1, fn2, scope) {
153         return function () {
154             var ret = fn1.apply(scope || this || window, arguments);
155             fn2.apply(scope || this || window, arguments);
156             return ret;
157         }
158     }
159 
160     /**
161      * Adds a request to the queue.
162      * @param fn {Function} A request function which takes single config object.
163      * @param config {Object} The config object that will be passed to the request <code>fn</code>
164      * and must contain success and failure callbacks.
165      * @param [scope] {Object} The scope in which to execute the request <code>fn</code>.
166      * Note that the config success and failure callbacks will execute in the <code>config.scope</code> and not the <code>scope</code> argument.
167      * @returns {LABKEY.MultiRequest} this object so add calls can be chained.
168      * @example
169      * <pre>
170      * new MultiRequest().add(Ext.Ajax.request, {
171      *     url: LABKEY.ActionURL.buildURL("controller", "action1", "/container/path"),
172      *     success: function () { console.log("success 1!"); },
173      *     failure: function () { console.log("failure 1!"); },
174      *     scope: this // The scope of the success and failure callbacks.
175      * }).add({Ext.Ajax.request, {
176      *     url: LABKEY.ActionURL.buildURL("controller", "action2", "/container/path"),
177      *     success: function () { console.log("success 2!"); },
178      *     failure: function () { console.log("failure 2!"); },
179      *     scope: this // The scope of the success and failure callbacks.
180      * }).send(function () { console.log("all done!") });
181      * </pre>
182      */
183     this.add = function (fn, config, scope) {
184         config = config || {};
185 
186         var success = LABKEY.Utils.getOnSuccess(config);
187         if (!success) success = function () { };
188         if (!success._hookInstalled) {
189             config.success = createSequence(success, checkDone, config.scope);
190             config.success._hookInstalled = true;
191         }
192 
193         var failure = LABKEY.Utils.getOnFailure(config);
194         if (!failure) failure = function () { };
195         if (!failure._hookInstalled) {
196             config.failure = createSequence(failure, checkDone, config.scope);
197             config.failure._hookInstalled = true;
198         }
199 
200         waitQ.push({fn: fn, args: [config], scope: scope});
201         return this;
202     };
203 
204     /**
205      * Send the queued up requests.  When all requests have returned, the send callback
206      * will be called.
207      * @param callback {Function} A function with a single argument of 'this'.
208      * @param [scope] {Object} The scope in which to execute the callback.
209      *
210      * Alternatively, a single config Object argument:
211      * <ul>
212      * <li>fn: The send callback function.
213      * <li>scope: The scope to execute the send callback function in.
214      * </ul>
215      */
216     this.send = function (callback, scope) {
217         if (sending || waitQ.length == 0)
218             return;
219         sending = true;
220         sendQ = waitQ;
221         waitQ = new Array();
222 
223         var len = sendQ.length;
224         for (var i = 0; i < len; i++) {
225             var q = sendQ[i];
226             q.fn.apply(q.scope || window, q.args);
227         }
228 
229         var self = this;
230         if (typeof callback == "function") {
231             doneCallbacks.push({fn: callback, scope: scope});
232         } else if (typeof callback.fn == "function") {
233             doneCallbacks.push({fn: callback.fn, scope: callback.scope||scope});
234         }
235     };
236 };
237