Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (14.6 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
            options.url = getUrl(model, options) || urlError();
82
            if (model && model.supportIncUpdates) {
83
                options.url = options.refresh ? options.url : setChangesSince(options.url, type);
84
            }
85
            if (!options.refresh && options.cache === undefined) {
86
                options.cache = true;
87
            }
88
        }
89

    
90
        // default error options
91
        options.critical = options.critical === undefined ? true : options.critical;
92
        options.display = options.display === undefined ? true : options.display;
93

    
94
        if (api.stop_calls && !options.no_skip) {
95
            return;
96
        }
97

    
98
        var success = options.success || function(){};
99
        var error = options.error || function(){};
100
        var complete = options.complete || function(){};
101
        var before_send = options.beforeSend || function(){};
102

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

    
123
    api.handlerWrapper = function(wrap, method, type) {
124
        
125
        var cb_type = type;
126

    
127
        return function() {
128
            
129
            var xhr = undefined;
130
            var handler_type = type;
131
            var args = arguments;
132
            var ajax_options = this;
133

    
134
            // save the request date to use it as a changes-since value
135
            // for opera because we are not able to determine
136
            // response date header for 304 requests
137
            if (handler_type == "beforeSend" && $.browser.opera) {
138
                this.date_send = new Date;
139
            }
140

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

    
223
            // request handles errors by itself, s
224
            if (handler_type == "error" && this.skip_api_error) {
225
                call_api_handler = false
226
            }
227

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

    
253
    api.successHandler = function(data, status, xhr) {
254
        //debug("ajax success", arguments)
255
        // on success, update the last date we called the api url
256
        return [data, status, xhr];
257
    }
258

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

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

    
291
        return _.toArray(arguments);
292
    }
293

    
294
    api.completeHandler = function(xhr, status) {
295
        //debug("ajax complete", arguments)
296
        return arguments;
297
    }
298

    
299
    api.beforeSendHandler = function(xhr, settings) {
300
        //debug("ajax beforeSend", arguments)
301
        // ajax settings
302
        var ajax_settings = this;
303
        return arguments;
304
    }
305

    
306
    // api call helper
307
    api.call = function(url, method, data, complete, error, success, options) {
308
            var self = this;
309
            error = error || function(){};
310
            success = success || function(){};
311
            complete = complete || function(){};
312
            var extra = data ? data._options || {} : {};
313

    
314
            // really ugly way to pass sync request options.
315
            // it works though....
316
            if (data && data._options) { delete data['_options'] };
317
            
318
            // prepare the params
319
            var params = {
320
                url: snf.config.api_url + "/" + url,
321
                data: data,
322
                success: success,
323
                complete: function() { api.trigger("call"); complete(this) },
324
                error: error
325
            }
326

    
327
            params = _.extend(params, extra, options);
328
            this.sync(method, this, params);
329
        },
330

    
331
    _.extend(api, bb.Events);
332
    
333
    // helper for callbacks that need to get called
334
    // in fixed intervals
335
    api.updateHandler = function(options) {
336
        this.cb = options.callback;
337
        this.limit = options.limit;
338
        this.timeout = options.timeout;
339

    
340
        this.normal_timeout = options.timeout;
341
        this.fast_timeout = options.fast;
342

    
343
        this._called = 0;
344
        this.interval = undefined;
345
        this.call_on_start = options.call_on_start || true;
346

    
347
        this.running = false;
348
        this.last_call = false;
349
        
350
        // wrapper
351
        function _cb() {
352
            if (this.fast_timeout == this.timeout){
353
                this._called++;
354
            }
355

    
356
            if (this._called >= this.limit && this.fast_timeout == this.timeout) {
357
                this.timeout = this.normal_timeout;
358
                this.setInterval()
359
            }
360
            this.cb();
361
            this.last_call = new Date;
362
        };
363

    
364
        _cb = _.bind(_cb, this);
365

    
366
        this.faster = function(do_call) {
367
            this.timeout = this.fast_timeout;
368
            this._called = 0;
369
            this.setInterval(do_call);
370
        }
371

    
372
        this.setInterval = function(do_call) {
373
            this.trigger("clear");
374
            window.clearInterval(this.interval);
375
            
376
            this.interval = window.setInterval(_cb, this.timeout);
377
            this.running = true;
378
            
379
            var call = do_call || this.call_on_start;
380
            
381
            if (this.last_call) {
382
                var next_call = (this.timeout - ((new Date) - this.last_call));
383
                if (next_call < this.timeout/2) {
384
                    call = true;
385
                } else {
386
                    call = false;
387
                }
388
            }
389

    
390
            if (call) {
391
                _cb();
392
            }
393
            return this;
394
        }
395

    
396
        this.start = function (call_on_start) {
397
            if (this.running) { this.stop() };
398
            this.call_on_start = call_on_start == undefined ? this.call_on_start : call_on_start;
399
            this.setInterval();
400
            return this;
401
        }
402

    
403
        this.stop = function() {
404
            this.trigger("clear");
405
            window.clearInterval(this.interval);
406
            this.running = false;
407
            return this;
408
        }
409
    }
410
    
411
    // api error state
412
    api.stop_calls = false;
413
    api.STATES = { NORMAL:1, WARN:0, ERROR:-1 };
414
    api.error_state = api.STATES.NORMAL;
415

    
416
    // on api error update the api error_state
417
    api.bind("error", function() {
418
        if (snf.api.error_state == snf.api.STATES.ERROR) { return };
419

    
420
        var args = _.toArray(_.toArray(arguments)[0]);
421
        var params = _.last(args);
422
        
423
        if (params.critical) {
424
            snf.api.error_state = api.STATES.ERROR;
425
            snf.api.stop_calls = true;
426
        } else {
427
            snf.api.error_state = api.STATES.ERROR;
428
        }
429
        snf.api.trigger("change:error_state", snf.api.error_state);
430
    });
431
    
432
    // reset api error state
433
    api.bind("reset", function() {
434
        snf.api.error_state = api.STATES.NORMAL;
435
        snf.api.stop_calls = false;
436
        snf.api.trigger("change:error_state", snf.api.error_state);
437
    })
438

    
439
    // make it eventable
440
    _.extend(api.updateHandler.prototype, bb.Events);
441
    
442
})(this);