Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (19.5 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
                if (!options.refresh) {
128
                  options.url = setChangesSince(options.url, type);
129
                  options._detect_change_by_response_code = true;
130
                } 
131
            }
132
            if (!options.refresh && options.cache === undefined) {
133
                options.cache = true;
134
            }
135
        }
136

    
137
        // default error options
138
        options.critical = options.critical === undefined ? true : options.critical;
139
        options.display = options.display === undefined ? true : options.display;
140

    
141
        if (api.stop_calls && !options.no_skip) {
142
            return;
143
        }
144

    
145
        var success = options.success || function(){};
146
        var error = options.error || function(){};
147
        var complete = options.complete || function(){};
148
        var before_send = options.beforeSend || function(){};
149

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

    
170
    api.handlerWrapper = function(wrap, method, type) {
171
        
172
        var cb_type = type;
173

    
174
        return function() {
175
            
176
            var xhr = undefined;
177
            var handler_type = type;
178
            var args = arguments;
179
            var ajax_options = this;
180

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

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

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

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

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

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

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

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

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

    
353
        return _.toArray(arguments);
354
    }
355

    
356
    api.completeHandler = function(xhr, status) {
357
        //debug("ajax complete", arguments)
358
        return arguments;
359
    }
360

    
361
    api.beforeSendHandler = function(xhr, settings) {
362
        //debug("ajax beforeSend", arguments)
363
        // ajax settings
364
        var ajax_settings = this;
365
        return arguments;
366
    }
367

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

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

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

    
394
            params = _.extend(params, extra, options);
395
            this.sync(method, this, params);
396
        },
397

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

    
406
        // the interval with which we start
407
        this.interval = this.normal_interval = options.interval || 4000;
408

    
409
        // fast interval
410
        // set when faster() gets called
411
        this.fast_interval = options.fast || 1000;
412
    
413
        // after how many calls to increase the interval
414
        this.interval_increase_count = options.increase_after_calls || 0;
415

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

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

    
464
        // start from faster timeout and start increasing
465
        this.faster = function(do_call) {
466
            if (!this.running) { return }
467

    
468
            this.interval = this.fast_interval;
469
            this.setInterval(do_call);
470
        }
471

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

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

    
517
            return this;
518
        }
519

    
520
        this.start = function (call_on_start) {
521
            if (this.running) { this.stop() };
522
            this.setInterval(call_on_start);
523
            return this;
524
        }
525

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

    
539
    // on api error update the api error_state
540
    api.bind("error", function() {
541
        if (snf.api.error_state == snf.api.STATES.ERROR) { return };
542

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

    
562
    // make it eventable
563
    _.extend(api.updateHandler.prototype, bb.Events);
564
    
565
})(this);