Statistics
| Branch: | Tag: | Revision:

root / ui / static / snf / js / sync.js @ dbe026f9

History | View | Annotate | Download (14.9 kB)

1
;(function(root){
2
    
3
    // root
4
    var root = root;
5
    
6
    // setup namepsaces
7
    var snf = root.synnefo = root.synnefo || {};
8
    var sync = snf.sync = snf.sync || {};
9
    var api = snf.api = snf.api || {};
10
    var storage = snf.storage = snf.storage || {};
11

    
12
    // shortcuts
13
    var bb = Backbone;
14

    
15
    // logging
16
    var logger = new snf.logging.logger("SNF-API");
17
    var debug = _.bind(logger.debug, logger)
18
    
19
    // method map
20
    var methodMap = {
21
        'create': 'POST',
22
        'update': 'PUT',
23
        'delete': 'DELETE',
24
        'read'  : 'GET'
25
    };
26

    
27
    // custom getUrl function
28
    // handles url retrieval based on the object passed
29
    // on most occasions in the synnefo api this will call
30
    // the model/collection url method
31
    var getUrl = function(object, options) {
32
        if (!(object && object.url)) return null;
33
        return _.isFunction(object.url) ? object.url(options) : object.url;
34
    };
35
    
36
    // Call history (set of api paths with the dates the path last called)
37
    var api_history = api.requests = api.requests || {};
38
    var addApiCallDate = function(url, d, method) {
39
        if (d === undefined) { d = Date() };
40
        var path = snf.util.parseUri(url).path;
41
        var key = path + "_" + method;
42

    
43
        // TODO: check if d is very old date
44
        api_history[key] = d;
45
        return api_history[path]
46
    }
47

    
48
    var clearApiCallDate = function(url, method) {
49
        var path = snf.util.parseUri(url).path;
50
        var key = path + "_" + method;
51
        api_history[key] = false;
52
        return api_history[path]
53
    }
54

    
55
    var api_errors = api.errors = api.errors || [];
56
    var add_api_error = function(settings, data) {
57
        api_errors.push({url:settings.url, date:new Date, settings:settings, data:data})
58
    }
59

    
60
    var setChangesSince = function(url, method) {
61
        var path = snf.util.parseUri(url).path;
62
        var d = api_history[path + "_" + method];
63
        if (d) {
64
            url = url + "?changes-since=" + snf.util.ISODateString(d)
65
        }
66
        return url;
67
    }
68
    
69
    // custom sync method
70
    // appends global ajax handlers
71
    // handles changed-since url parameter based on api path
72
    api.sync = function(method, model, options) {
73
        
74
        var type = methodMap[method];
75
        
76
        if (model && (model.skipMethods || []).indexOf(method) >= 0) {
77
            throw "Model does not support " + method + " calls";
78
        }
79

    
80
        if (!options.url) {
81
            var urlobject = model;
82

    
83
            // fallback to collection url for item creation
84
            if (method == "create" && model.isNew && model.isNew()) {
85
                urlobject = model.collection;
86
            }
87

    
88
            options.url = getUrl(model, options) || urlError();
89
            if (model && model.supportIncUpdates) {
90
                options.url = options.refresh ? options.url : setChangesSince(options.url, type);
91
            }
92
            if (!options.refresh && options.cache === undefined) {
93
                options.cache = true;
94
            }
95
        }
96

    
97
        // default error options
98
        options.critical = options.critical === undefined ? true : options.critical;
99
        options.display = options.display === undefined ? true : options.display;
100

    
101
        if (api.stop_calls && !options.no_skip) {
102
            return;
103
        }
104

    
105
        var success = options.success || function(){};
106
        var error = options.error || function(){};
107
        var complete = options.complete || function(){};
108
        var before_send = options.beforeSend || function(){};
109

    
110
        // custom json data.
111
        if (options.data && model && (method == 'create' || method == 'update')) {
112
            options.contentType = 'application/json';
113
            options.data = JSON.stringify(options.data);
114
        }
115
        
116
        var api_params = {};
117
        var api_options = _.extend(api_params, options, {
118
            success: api.handlerWrapper(api.successHandler, success, "success"),
119
            error: api.handlerWrapper(api.errorHandler, error, "error"),
120
            complete: api.handlerWrapper(api.completeHandler, complete, "complete"),
121
            beforeSend: api.handlerWrapper(api.beforeSendHandler, before_send, "beforeSend"),
122
            cache: options.cache || false,
123
            timeout: options.timeout || snf.config.ajax_timeout || window.TIMEOUT || 5000
124
        });
125
        return bb.sync(method, model, api_options);
126
    }
127
    
128
    api.timeouts_occured = 0;
129

    
130
    api.handlerWrapper = function(wrap, method, type) {
131
        
132
        var cb_type = type;
133

    
134
        return function() {
135
            
136
            var xhr = undefined;
137
            var handler_type = type;
138
            var args = arguments;
139
            var ajax_options = this;
140

    
141
            // save the request date to use it as a changes-since value
142
            // for opera because we are not able to determine
143
            // response date header for 304 requests
144
            if (handler_type == "beforeSend" && $.browser.opera) {
145
                this.date_send = new Date;
146
            }
147

    
148
            // error with status code 0 in opera
149
            // act as 304 response
150
            if (handler_type == "error" && $.browser.opera) {
151
                if (arguments[0].status === 0 && arguments[1] === "error") {
152
                    arguments[0].status = 304;
153
                    arguments[1] = "notmodified";
154
                    response_type = "success";
155
                    xhr = arguments[0];
156
                }
157
            }
158
            
159
            // add error in api errors registry
160
            // api errors registry will be sent
161
            // if user reports an error using feedback form
162
            if (handler_type == "error") {
163
                // skip logging requested ?
164
                // if not log this error
165
                if (this.log_error !== false) {
166
                    add_api_error(this, arguments);
167
                }
168
            }
169
            
170
            // identify response status
171
            var status = 304;
172
            if (arguments[0]) {
173
                status = arguments[0].status;
174
            }
175
            
176
            // identify aborted request
177
            try {
178
                if (args[1] === "abort") {
179
                    api.trigger("abort");
180
                    return;
181
                }
182
            } catch(error) {
183
                console.error("error aborting", error);
184
            }
185
            
186
            // try to set the last request date
187
            // only for notmodified or succeed responses
188
            try {
189
                // identify xhr object
190
                xhr = xhr || args[2];
191
                
192
                // not modified response
193
                if (args[1] === "notmodified") {
194
                    if (xhr) {
195
                        // use date_send if exists (opera browser)
196
                        var d = this.date_send || xhr.getResponseHeader('Date');
197
                        if (d) { addApiCallDate(this.url, new Date(d), ajax_options.type); };
198
                    }
199
                    return;
200
                }
201
                
202
                // success response
203
                if (args[1] == "success" && handler_type == "success") {
204
                    try {
205
                        // use date_send if exists (opera browser)
206
                        var d = this.date_send || args[2].getResponseHeader('Date');
207
                        if (d) { addApiCallDate(this.url, new Date(d), ajax_options.type); };
208
                    } catch (err) {
209
                        console.error(err)
210
                    }
211
                }
212
            } catch (err) {
213
                console.error(err);
214
            }
215
            
216
            // dont call error callback for non modified responses
217
            if (arguments[1] === "notmodified") {
218
                return;
219
            }
220
            
221
            // prepare arguments for error callbacks
222
            var cb_args = _.toArray(arguments);
223
            if (handler_type === "error") {
224
                cb_args.push(_.clone(this));
225
            }
226
            
227
            // determine if we need to call our callback wrapper
228
            var call_api_handler = true;
229

    
230
            // request handles errors by itself, s
231
            if (handler_type == "error" && this.skip_api_error) {
232
                call_api_handler = false
233
            }
234

    
235
            // aborted request, don't call error handler
236
            if (handler_type === "error" && args[1] === "abort") {
237
                call_api_handler = false;
238
            }
239
            
240
            // reset api call date, next call will be sent without changes-since
241
            // parameter set
242
            if (handler_type === "error") {
243
                if (args[1] === "error") {
244
                    clearApiCallDate(this.url, this.type);
245
                }
246
            }
247
            
248
            // call api call back and retrieve params to
249
            // be passed to the callback method set for
250
            // this type of response
251
            if (call_api_handler) {
252
                cb_args = wrap.apply(this, cb_args);
253
            }
254
            
255
            // call requested callback
256
            method.apply(this, _.toArray(cb_args));
257
        }
258
    }
259

    
260
    api.successHandler = function(data, status, xhr) {
261
        //debug("ajax success", arguments)
262
        // on success, update the last date we called the api url
263
        return [data, status, xhr];
264
    }
265

    
266
    api.errorHandler = function(event, xhr, settings, error) {
267
        
268
        // dont trigger api error untill timeouts occured
269
        // exceed the skips_timeouts limit
270
        //
271
        // check only requests with skips_timeouts option set
272
        if (xhr === "timeout" && _.last(arguments).skips_timeouts) {
273
            var skip_timeouts = snf.config.skip_timeouts || 1;
274
            if (snf.api.timeouts_occured < skip_timeouts) {
275
                snf.api.timeouts_occured++;
276
                return;
277
            } else {
278
                // reset and continue to error trigger
279
                snf.api.timeouts_occured = 0;
280
            }
281
        }
282

    
283
        // if error occured and changes-since is set for the request
284
        // skip triggering the error and try again without the changes-since
285
        // parameter set
286
        var url = snf.util.parseUri(this.url);
287
        if (url.query.indexOf("changes-since") > -1) {
288
            clearApiCallDate(this.url, this.type);
289
            return _.toArray(arguments);
290
        }
291
    
292
        // skip aborts, notmodified (opera)
293
        if (xhr === "error" || xhr === "timeout") {
294
            var args = _.toArray(arguments);
295
            api.trigger("error", args);
296
        }
297

    
298
        return _.toArray(arguments);
299
    }
300

    
301
    api.completeHandler = function(xhr, status) {
302
        //debug("ajax complete", arguments)
303
        return arguments;
304
    }
305

    
306
    api.beforeSendHandler = function(xhr, settings) {
307
        //debug("ajax beforeSend", arguments)
308
        // ajax settings
309
        var ajax_settings = this;
310
        return arguments;
311
    }
312

    
313
    // api call helper
314
    api.call = function(url, method, data, complete, error, success, options) {
315
            var self = this;
316
            error = error || function(){};
317
            success = success || function(){};
318
            complete = complete || function(){};
319
            var extra = data ? data._options || {} : {};
320

    
321
            // really ugly way to pass sync request options.
322
            // it works though....
323
            if (data && data._options) { delete data['_options'] };
324
            
325
            // prepare the params
326
            var params = {
327
                url: snf.config.api_url + "/" + url,
328
                data: data,
329
                success: success,
330
                complete: function() { api.trigger("call"); complete(this) },
331
                error: error
332
            }
333

    
334
            params = _.extend(params, extra, options);
335
            this.sync(method, this, params);
336
        },
337

    
338
    _.extend(api, bb.Events);
339
    
340
    // helper for callbacks that need to get called
341
    // in fixed intervals
342
    api.updateHandler = function(options) {
343
        this.cb = options.callback;
344
        this.limit = options.limit;
345
        this.timeout = options.timeout;
346

    
347
        this.normal_timeout = options.timeout;
348
        this.fast_timeout = options.fast;
349

    
350
        this._called = 0;
351
        this.interval = undefined;
352
        this.call_on_start = options.call_on_start || true;
353

    
354
        this.running = false;
355
        this.last_call = false;
356
        
357
        // wrapper
358
        function _cb() {
359
            if (this.fast_timeout == this.timeout){
360
                this._called++;
361
            }
362

    
363
            if (this._called >= this.limit && this.fast_timeout == this.timeout) {
364
                this.timeout = this.normal_timeout;
365
                this.setInterval()
366
            }
367
            this.cb();
368
            this.last_call = new Date;
369
        };
370

    
371
        _cb = _.bind(_cb, this);
372

    
373
        this.faster = function(do_call) {
374
            this.timeout = this.fast_timeout;
375
            this._called = 0;
376
            this.setInterval(do_call);
377
        }
378

    
379
        this.setInterval = function(do_call) {
380
            this.trigger("clear");
381
            window.clearInterval(this.interval);
382
            
383
            this.interval = window.setInterval(_cb, this.timeout);
384
            this.running = true;
385
            
386
            var call = do_call || this.call_on_start;
387
            
388
            if (this.last_call) {
389
                var next_call = (this.timeout - ((new Date) - this.last_call));
390
                if (next_call < this.timeout/2) {
391
                    call = true;
392
                } else {
393
                    call = false;
394
                }
395
            }
396

    
397
            if (call) {
398
                _cb();
399
            }
400
            return this;
401
        }
402

    
403
        this.start = function (call_on_start) {
404
            if (this.running) { this.stop() };
405
            this.call_on_start = call_on_start == undefined ? this.call_on_start : call_on_start;
406
            this.setInterval();
407
            return this;
408
        }
409

    
410
        this.stop = function() {
411
            this.trigger("clear");
412
            window.clearInterval(this.interval);
413
            this.running = false;
414
            return this;
415
        }
416
    }
417
    
418
    // api error state
419
    api.stop_calls = false;
420
    api.STATES = { NORMAL:1, WARN:0, ERROR:-1 };
421
    api.error_state = api.STATES.NORMAL;
422

    
423
    // on api error update the api error_state
424
    api.bind("error", function() {
425
        if (snf.api.error_state == snf.api.STATES.ERROR) { return };
426

    
427
        var args = _.toArray(_.toArray(arguments)[0]);
428
        var params = _.last(args);
429
        
430
        if (params.critical) {
431
            snf.api.error_state = api.STATES.ERROR;
432
            snf.api.stop_calls = true;
433
        } else {
434
            snf.api.error_state = api.STATES.ERROR;
435
        }
436
        snf.api.trigger("change:error_state", snf.api.error_state);
437
    });
438
    
439
    // reset api error state
440
    api.bind("reset", function() {
441
        snf.api.error_state = api.STATES.NORMAL;
442
        snf.api.stop_calls = false;
443
        snf.api.trigger("change:error_state", snf.api.error_state);
444
    })
445

    
446
    // make it eventable
447
    _.extend(api.updateHandler.prototype, bb.Events);
448
    
449
})(this);