Statistics
| Branch: | Tag: | Revision:

root / ui / static / snf / js / sync.js @ 66be390b

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
            options.url = options.refresh ? options.url : setChangesSince(options.url, type);
83
            if (!options.refresh && options.cache === undefined) {
84
                options.cache = true;
85
            }
86
        }
87

    
88
        // default error options
89
        options.critical = options.critical === undefined ? true : options.critical;
90
        options.display = options.display === undefined ? true : options.display;
91

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

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

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

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

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

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

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

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

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

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

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

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

    
289
        return _.toArray(arguments);
290
    }
291

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

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

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

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

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

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

    
338
        this.normal_timeout = options.timeout;
339
        this.fast_timeout = options.fast;
340

    
341
        this._called = 0;
342
        this.interval = undefined;
343
        this.call_on_start = options.call_on_start || true;
344

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

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

    
362
        _cb = _.bind(_cb, this);
363

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

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

    
388
            if (call) {
389
                _cb();
390
            }
391
            return this;
392
        }
393

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

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

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

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

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