Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (19.3 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
            // subtract threshold
100
            d = new Date(d - synnefo.config.changes_since_alignment);
101
            url = url + "?changes-since=" + snf.util.ISODateString(d);
102
        }
103
        return url;
104
    }
105
    
106
    // custom sync method
107
    // appends global ajax handlers
108
    // handles changed-since url parameter based on api path
109
    api.sync = function(method, model, options) {
110

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

    
117
        if (!options.url) {
118
            var urlobject = model;
119

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

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

    
134
        // default error options
135
        options.critical = options.critical === undefined ? true : options.critical;
136
        options.display = options.display === undefined ? true : options.display;
137

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

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

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

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

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

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

    
185
            if (handler_type == "beforeSend") {
186
                arguments[0].setRequestHeader('X-Auth-Token', 
187
                                              synnefo.user.get_token());
188
            }
189

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

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

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

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

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

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

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

    
349
        return _.toArray(arguments);
350
    }
351

    
352
    api.completeHandler = function(xhr, status) {
353
        //debug("ajax complete", arguments)
354
        return arguments;
355
    }
356

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

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

    
372
            // really ugly way to pass sync request options.
373
            // it works though....
374
            if (data && data._options) { delete data['_options'] };
375
            
376
            var base_url = snf.config.api_urls[this.api_type];
377
            var join = '/';
378
            // do not append trailling slash if already exists in url
379
            if (base_url[base_url.length - 1] == join) { join = '' }
380

    
381
            // prepare the params
382
            var params = {
383
                url: base_url + join + url,
384
                data: data,
385
                success: success,
386
                complete: function() { api.trigger("call"); complete(this) },
387
                error: error
388
            }
389

    
390
            params = _.extend(params, extra, options);
391
            this.sync(method, this, params);
392
        },
393

    
394
    _.extend(api, bb.Events);
395
    
396
    // helper for callbacks that need to get called
397
    // in fixed intervals
398
    api.updateHandler = function(options) {
399
        this.cb = options.callback;
400
        this.handler_id = options.id;
401

    
402
        // the interval with which we start
403
        this.interval = this.normal_interval = options.interval || 4000;
404

    
405
        // fast interval
406
        // set when faster() gets called
407
        this.fast_interval = options.fast || 1000;
408
    
409
        // after how many calls to increase the interval
410
        this.interval_increase_count = options.increase_after_calls || 0;
411

    
412
        // increase the timer by this value after interval_increase_count calls
413
        this.interval_increase = options.increase || 500;
414
        
415
        // maximum interval limit
416
        this.maximum_interval = options.max || 60000;
417
        
418
        // make a call before interval starts
419
        this.call_on_start = options.initial_call === undefined ? true : options.initial_call;
420
            
421
        this.increase_enabled = this.interval_increase_count === 0;
422

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

    
460
        // start from faster timeout and start increasing
461
        this.faster = function(do_call) {
462
            if (!this.running) { return }
463

    
464
            this.interval = this.fast_interval;
465
            this.setInterval(do_call);
466
        }
467

    
468
        // slow down
469
        this.slower = function(do_call) {
470
            if (this.interval == this.maximum_interval) {
471
                // no need to increase
472
                return;
473
            }
474
            
475
            this.interval = this.interval + this.interval_increase;
476
            // increase timeout
477
            if (this.interval > this.maximum_interval) {
478
                this.interval = this.maximum_interval;
479
            }
480
            
481
            this.setInterval(do_call);
482
        }
483
        
484
        // reset internal
485
        this.setInterval = function(do_call) {
486
            this.trigger("clear");
487
            
488
            // reset times called
489
            this._called = 0;
490
            
491
            window.clearInterval(this.window_interval);
492
            this.window_interval = window.setInterval(_.bind(this._cb, this), this.interval);
493

    
494
            this.running = true;
495
            
496
            // if no do_call set, fallback to object creation option
497
            // else force what was requested
498
            var call = do_call === undefined ? this.call_on_start : do_call;
499
            
500
            if (this.last_call && do_call !== false) {
501
                var next_call = (this.interval - ((new Date) - this.last_call));
502
                if (next_call < this.interval/2) {
503
                    call = true;
504
                } else {
505
                    call = false;
506
                }
507
            }
508
            
509
            if (call) {
510
                this._cb();
511
            }
512

    
513
            return this;
514
        }
515

    
516
        this.start = function (call_on_start) {
517
            if (this.running) { this.stop() };
518
            this.setInterval(call_on_start);
519
            return this;
520
        }
521

    
522
        this.stop = function() {
523
            this.trigger("clear");
524
            window.clearInterval(this.window_interval);
525
            this.running = false;
526
            return this;
527
        }
528
    }
529
    
530
    // api error state
531
    api.stop_calls = false;
532
    api.STATES = { NORMAL:1, WARN:0, ERROR:-1 };
533
    api.error_state = api.STATES.NORMAL;
534

    
535
    // on api error update the api error_state
536
    api.bind("error", function() {
537
        if (snf.api.error_state == snf.api.STATES.ERROR) { return };
538

    
539
        var args = _.toArray(_.toArray(arguments)[0]);
540
        var params = _.last(args);
541
        
542
        if (params.critical) {
543
            snf.api.error_state = api.STATES.ERROR;
544
            snf.api.stop_calls = true;
545
        } else {
546
            snf.api.error_state = api.STATES.ERROR;
547
        }
548
        snf.api.trigger("change:error_state", snf.api.error_state);
549
    });
550
    
551
    // reset api error state
552
    api.bind("reset", function() {
553
        snf.api.error_state = api.STATES.NORMAL;
554
        snf.api.stop_calls = false;
555
        snf.api.trigger("change:error_state", snf.api.error_state);
556
    })
557

    
558
    // make it eventable
559
    _.extend(api.updateHandler.prototype, bb.Events);
560
    
561
})(this);