Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / ui / static / snf / js / sync.js @ e472ed67

History | View | Annotate | Download (19 kB)

1
// Copyright 2011 GRNET S.A. All rights reserved.
2
// 
3
// Redistribution and use in source and binary forms, with or
4
// without modification, are permitted provided that the following
5
// conditions are met:
6
// 
7
//   1. Redistributions of source code must retain the above
8
//      copyright notice, this list of conditions and the following
9
//      disclaimer.
10
// 
11
//   2. Redistributions in binary form must reproduce the above
12
//      copyright notice, this list of conditions and the following
13
//      disclaimer in the documentation and/or other materials
14
//      provided with the distribution.
15
// 
16
// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
// POSSIBILITY OF SUCH DAMAGE.
28
// 
29
// The views and conclusions contained in the software and
30
// documentation are those of the authors and should not be
31
// interpreted as representing official policies, either expressed
32
// or implied, of GRNET S.A.
33
// 
34

    
35
;(function(root){
36
    
37
    // root
38
    var root = root;
39
    
40
    // setup namepsaces
41
    var snf = root.synnefo = root.synnefo || {};
42
    var sync = snf.sync = snf.sync || {};
43
    var api = snf.api = snf.api || {};
44
    var storage = snf.storage = snf.storage || {};
45

    
46
    // shortcuts
47
    var bb = Backbone;
48

    
49
    // logging
50
    var logger = new snf.logging.logger("SNF-API");
51
    var debug = _.bind(logger.debug, logger)
52
    
53
    // method map
54
    var methodMap = {
55
        'create': 'POST',
56
        'update': 'PUT',
57
        'delete': 'DELETE',
58
        'read'  : 'GET',
59
        'head'  : 'HEAD'
60
    };
61

    
62
    // custom getUrl function
63
    // handles url retrieval based on the object passed
64
    // on most occasions in the synnefo api this will call
65
    // the model/collection url method
66
    var getUrl = function(object, options, method) {
67
        if (!(object && object.url)) return null;
68
        return _.isFunction(object.url) ? object.url(options, method) : object.url;
69
    };
70
    
71
    // Call history (set of api paths with the dates the path last called)
72
    var api_history = api.requests = api.requests || {};
73
    var addApiCallDate = function(url, d, method) {
74
        if (d === undefined) { d = Date() };
75
        var path = snf.util.parseUri(url).path;
76
        var key = path + "_" + method;
77

    
78
        // TODO: check if d is very old date
79
        api_history[key] = d;
80
        return api_history[path]
81
    }
82

    
83
    var clearApiCallDate = function(url, method) {
84
        var path = snf.util.parseUri(url).path;
85
        var key = path + "_" + method;
86
        api_history[key] = false;
87
        return api_history[path]
88
    }
89

    
90
    var api_errors = api.errors = api.errors || [];
91
    var add_api_error = function(settings, data) {
92
        api_errors.push({url:settings.url, date:new Date, settings:settings, data:data})
93
    }
94

    
95
    var setChangesSince = function(url, method) {
96
        var path = snf.util.parseUri(url).path;
97
        var d = api_history[path + "_" + method];
98
        if (d) {
99
            url = url + "?changes-since=" + snf.util.ISODateString(d)
100
        }
101
        return url;
102
    }
103
    
104
    // custom sync method
105
    // appends global ajax handlers
106
    // handles changed-since url parameter based on api path
107
    api.sync = function(method, model, options) {
108

    
109
        var type = methodMap[method];
110
        
111
        if (model && (model.skipMethods || []).indexOf(method) >= 0) {
112
            throw "Model does not support " + method + " calls";
113
        }
114

    
115
        if (!options.url) {
116
            var urlobject = model;
117

    
118
            // fallback to collection url for item creation
119
            if (method == "create" && model.isNew && model.isNew()) {
120
                urlobject = model.collection;
121
            }
122

    
123
            options.url = getUrl(urlobject, options, method) || urlError();
124
            if (urlobject && urlobject.supportIncUpdates) {
125
                options.url = options.refresh ? options.url : setChangesSince(options.url, type);
126
            }
127
            if (!options.refresh && options.cache === undefined) {
128
                options.cache = true;
129
            }
130
        }
131

    
132
        // default error options
133
        options.critical = options.critical === undefined ? true : options.critical;
134
        options.display = options.display === undefined ? true : options.display;
135

    
136
        if (api.stop_calls && !options.no_skip) {
137
            return;
138
        }
139

    
140
        var success = options.success || function(){};
141
        var error = options.error || function(){};
142
        var complete = options.complete || function(){};
143
        var before_send = options.beforeSend || function(){};
144

    
145
        // custom json data.
146
        if (options.data && model && (method == 'create' || method == 'update')) {
147
            options.contentType = 'application/json';
148
            options.data = JSON.stringify(options.data);
149
        }
150
        options.data = _.isEmpty(options.data) ? undefined : options.data;
151
        var api_params = {};
152
        var api_options = _.extend(api_params, options, {
153
            success: api.handlerWrapper(api.successHandler, success, "success"),
154
            error: api.handlerWrapper(api.errorHandler, error, "error"),
155
            complete: api.handlerWrapper(api.completeHandler, complete, "complete"),
156
            beforeSend: api.handlerWrapper(api.beforeSendHandler, before_send, "beforeSend"),
157
            cache: options.cache || false,
158
            timeout: options.timeout || snf.config.ajax_timeout || window.TIMEOUT || 5000
159
        });
160
        return bb.sync(method, model, api_options);
161
    }
162
    
163
    api.timeouts_occured = 0;
164

    
165
    api.handlerWrapper = function(wrap, method, type) {
166
        
167
        var cb_type = type;
168

    
169
        return function() {
170
            
171
            var xhr = undefined;
172
            var handler_type = type;
173
            var args = arguments;
174
            var ajax_options = this;
175

    
176
            // save the request date to use it as a changes-since value
177
            // for opera because we are not able to determine
178
            // response date header for 304 requests
179
            if (handler_type == "beforeSend" && $.browser.opera) {
180
                this.date_send = new Date;
181
            }
182

    
183
            if (handler_type == "beforeSend") {
184
                arguments[0].setRequestHeader('X-Auth-Token', synnefo.user.token);
185
            }
186

    
187
            // error with status code 0 in opera
188
            // act as 304 response
189
            if (handler_type == "error" && $.browser.opera) {
190
                if (arguments[0].status === 0 && arguments[1] === "error") {
191
                    arguments[0].status = 304;
192
                    arguments[1] = "notmodified";
193
                    response_type = "success";
194
                    xhr = arguments[0];
195
                }
196
            }
197
            
198
            // add error in api errors registry
199
            // api errors registry will be sent
200
            // if user reports an error using feedback form
201
            if (handler_type == "error") {
202
                // skip logging requested ?
203
                // if not log this error
204
                if (this.log_error !== false) {
205
                    add_api_error(this, arguments);
206
                }
207
            }
208
            
209
            // identify response status
210
            var status = 304;
211
            if (arguments[0]) {
212
                status = arguments[0].status;
213
            }
214
            
215
            // identify aborted request
216
            try {
217
                if (args[1] === "abort") {
218
                    api.trigger("abort");
219
                    return;
220
                }
221
            } catch(error) {
222
                console.error("error aborting", error);
223
            }
224
            
225
            // try to set the last request date
226
            // only for notmodified or succeed responses
227
            try {
228
                // identify xhr object
229
                xhr = xhr || args[2];
230
                
231
                // not modified response
232
                if (args[1] === "notmodified") {
233
                    if (xhr) {
234
                        // use date_send if exists (opera browser)
235
                        var d = this.date_send || xhr.getResponseHeader('Date');
236
                        if (d) { addApiCallDate(this.url, new Date(d), ajax_options.type); };
237
                    }
238

    
239
                    return;
240
                }
241
                
242
                // success response
243
                if (args[1] == "success" && handler_type == "success") {
244
                    try {
245
                        // use date_send if exists (opera browser)
246
                        var d = this.date_send || args[2].getResponseHeader('Date');
247
                        if (d) { addApiCallDate(this.url, new Date(d), ajax_options.type); };
248
                    } catch (err) {
249
                        console.error(err)
250
                    }
251
                }
252
            } catch (err) {
253
                console.error(err);
254
            }
255
            
256
            // dont call error callback for non modified responses
257
            if (arguments[1] === "notmodified") {
258
                return;
259
            }
260

    
261
            if (["beforeSend", "complete"].indexOf(cb_type) == -1 && this.is_recurrent) {
262
                // trigger event to notify that a recurrent event
263
                // has returned status other than notmodified
264
                snf.api.trigger("change:recurrent");
265
            }
266
            
267
            // prepare arguments for error callbacks
268
            var cb_args = _.toArray(arguments);
269
            if (handler_type === "error") {
270
                cb_args.push(_.clone(this));
271
            }
272
            
273
            // determine if we need to call our callback wrapper
274
            var call_api_handler = true;
275
            
276
            // request handles errors by itself, s
277
            if (handler_type == "error" && this.skip_api_error) {
278
                call_api_handler = false
279
            }
280

    
281
            // aborted request, don't call error handler
282
            if (handler_type === "error" && args[1] === "abort") {
283
                call_api_handler = false;
284
            }
285
            
286
            // reset api call date, next call will be sent without changes-since
287
            // parameter set
288
            if (handler_type === "error") {
289
                if (args[1] === "error") {
290
                    clearApiCallDate(this.url, this.type);
291
                }
292
            }
293
            
294
            // call api call back and retrieve params to
295
            // be passed to the callback method set for
296
            // this type of response
297
            if (call_api_handler) {
298
                cb_args = wrap.apply(this, cb_args);
299
            }
300
            
301
            // call requested callback
302
            method.apply(this, _.toArray(cb_args));
303
        }
304
    }
305

    
306
    api.successHandler = function(data, status, xhr) {
307
        //debug("ajax success", arguments)
308
        // on success, update the last date we called the api url
309
        return [data, status, xhr];
310
    }
311

    
312
    api.errorHandler = function(event, xhr, settings, error) {
313
        // dont trigger api error until timeouts occured
314
        // exceed the skips_timeouts limit
315
        //
316
        // check only requests with skips_timeouts option set
317
        
318
        if (xhr === "timeout" && _.last(arguments).skips_timeouts) {
319
            var skip_timeouts = snf.config.skip_timeouts || 1;
320
            if (snf.api.timeouts_occured < skip_timeouts) {
321
                snf.api.timeouts_occured++;
322
                return;
323
            } else {
324
                // reset trigger error
325
                snf.api.timeouts_occured = 0;
326
                var args = _.toArray(arguments);
327
                api.trigger("error", args);
328
            }
329
        }
330

    
331
        // if error occured and changes-since is set for the request
332
        // skip triggering the error and try again without the changes-since
333
        // parameter set
334
        var url = snf.util.parseUri(this.url);
335
        if (url.query.indexOf("changes-since") > -1) {
336
            clearApiCallDate(this.url, this.type);
337
            return _.toArray(arguments);
338
        }
339
    
340
        // skip aborts, notmodified (opera)
341
        if (xhr === "error" || xhr === "timeout") {
342
            var args = _.toArray(arguments);
343
            api.trigger("error", args);
344
        }
345

    
346
        return _.toArray(arguments);
347
    }
348

    
349
    api.completeHandler = function(xhr, status) {
350
        //debug("ajax complete", arguments)
351
        return arguments;
352
    }
353

    
354
    api.beforeSendHandler = function(xhr, settings) {
355
        //debug("ajax beforeSend", arguments)
356
        // ajax settings
357
        var ajax_settings = this;
358
        return arguments;
359
    }
360

    
361
    // api call helper
362
    api.call = function(url, method, data, complete, error, success, options) {
363
            var self = this;
364
            error = error || function(){};
365
            success = success || function(){};
366
            complete = complete || function(){};
367
            var extra = data ? data._options || {} : {};
368

    
369
            // really ugly way to pass sync request options.
370
            // it works though....
371
            if (data && data._options) { delete data['_options'] };
372
            
373
            // prepare the params
374
            var params = {
375
                url: snf.config.api_urls[this.api_type] + "/" + url,
376
                data: data,
377
                success: success,
378
                complete: function() { api.trigger("call"); complete(this) },
379
                error: error
380
            }
381

    
382
            params = _.extend(params, extra, options);
383
            this.sync(method, this, params);
384
        },
385

    
386
    _.extend(api, bb.Events);
387
    
388
    // helper for callbacks that need to get called
389
    // in fixed intervals
390
    api.updateHandler = function(options) {
391
        this.cb = options.callback;
392
        this.handler_id = options.id;
393

    
394
        // the interval with which we start
395
        this.interval = this.normal_interval = options.interval || 4000;
396

    
397
        // fast interval
398
        // set when faster() gets called
399
        this.fast_interval = options.fast || 1000;
400
    
401
        // after how many calls to increase the interval
402
        this.interval_increase_count = options.increase_after_calls || 0;
403

    
404
        // increase the timer by this value after interval_increase_count calls
405
        this.interval_increase = options.increase || 500;
406
        
407
        // maximum interval limit
408
        this.maximum_interval = options.max || 60000;
409
        
410
        // make a call before interval starts
411
        this.call_on_start = options.initial_call === undefined ? true : options.initial_call;
412
            
413
        this.increase_enabled = this.interval_increase_count === 0;
414

    
415
        if (this.increase_enabled) {
416
            this.maximum_interval = this.interval;
417
            this.interval_increase_count = 1;
418
        }
419
        
420
        // inner params
421
        this._called = 0;
422
        this._first_call_date = undefined;
423
        this.window_interval = undefined;
424
        
425
        // state params
426
        this.running = false;
427
        this.last_call = false;
428
        
429
        // helper for api calls
430
        // TODO: move this out of here :/
431
        if (options.is_recurrent) {
432
            snf.api.bind("change:recurrent", _.bind(function() {
433
                if (this.running) {
434
                    this.faster(true);
435
                }
436
            }, this));
437
        }
438
        
439
        // callback wrapper
440
        this._cb = function() {
441
            if (!this.running) { this.stop() }
442
            if (this._called >= this.interval_increase_count) {
443
                this._called = 0;
444
                this.slower(false);
445
            }
446
            
447
            this.cb();
448
            this.last_call = new Date;
449
            this._called++;
450
        };
451

    
452
        // start from faster timeout and start increasing
453
        this.faster = function(do_call) {
454
            if (!this.running) { return }
455

    
456
            this.interval = this.fast_interval;
457
            this.setInterval(do_call);
458
        }
459

    
460
        // slow down
461
        this.slower = function(do_call) {
462
            if (this.interval == this.maximum_interval) {
463
                // no need to increase
464
                return;
465
            }
466
            
467
            this.interval = this.interval + this.interval_increase;
468
            // increase timeout
469
            if (this.interval > this.maximum_interval) {
470
                this.interval = this.maximum_interval;
471
            }
472
            
473
            this.setInterval(do_call);
474
        }
475
        
476
        // reset internal
477
        this.setInterval = function(do_call) {
478
            this.trigger("clear");
479
            
480
            // reset times called
481
            this._called = 0;
482
            
483
            window.clearInterval(this.window_interval);
484
            this.window_interval = window.setInterval(_.bind(this._cb, this), this.interval);
485

    
486
            this.running = true;
487
            
488
            // if no do_call set, fallback to object creation option
489
            // else force what was requested
490
            var call = do_call === undefined ? this.call_on_start : do_call;
491
            
492
            if (this.last_call && do_call !== false) {
493
                var next_call = (this.interval - ((new Date) - this.last_call));
494
                if (next_call < this.interval/2) {
495
                    call = true;
496
                } else {
497
                    call = false;
498
                }
499
            }
500
            
501
            if (call) {
502
                this._cb();
503
            }
504

    
505
            return this;
506
        }
507

    
508
        this.start = function (call_on_start) {
509
            if (this.running) { this.stop() };
510
            this.setInterval(call_on_start);
511
            return this;
512
        }
513

    
514
        this.stop = function() {
515
            this.trigger("clear");
516
            window.clearInterval(this.window_interval);
517
            this.running = false;
518
            return this;
519
        }
520
    }
521
    
522
    // api error state
523
    api.stop_calls = false;
524
    api.STATES = { NORMAL:1, WARN:0, ERROR:-1 };
525
    api.error_state = api.STATES.NORMAL;
526

    
527
    // on api error update the api error_state
528
    api.bind("error", function() {
529
        if (snf.api.error_state == snf.api.STATES.ERROR) { return };
530

    
531
        var args = _.toArray(_.toArray(arguments)[0]);
532
        var params = _.last(args);
533
        
534
        if (params.critical) {
535
            snf.api.error_state = api.STATES.ERROR;
536
            snf.api.stop_calls = true;
537
        } else {
538
            snf.api.error_state = api.STATES.ERROR;
539
        }
540
        snf.api.trigger("change:error_state", snf.api.error_state);
541
    });
542
    
543
    // reset api error state
544
    api.bind("reset", function() {
545
        snf.api.error_state = api.STATES.NORMAL;
546
        snf.api.stop_calls = false;
547
        snf.api.trigger("change:error_state", snf.api.error_state);
548
    })
549

    
550
    // make it eventable
551
    _.extend(api.updateHandler.prototype, bb.Events);
552
    
553
})(this);