Statistics
| Branch: | Tag: | Revision:

root / ui / static / snf / js / sync.js @ 2c9bfad1

History | View | Annotate | Download (13.4 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 || 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
                add_api_error(this, arguments);
155
            }
156
            
157
            // identify response status
158
            var status = 304;
159
            if (arguments[0]) {
160
                status = arguments[0].status;
161
            }
162
            
163
            // identify aborted request
164
            try {
165
                if (args[1] === "abort") {
166
                    api.trigger("abort");
167
                    return;
168
                }
169
            } catch(error) {
170
                console.error("error aborting", error);
171
            }
172
            
173
            // try to set the last request date
174
            // only for notmodified or succeed responses
175
            try {
176
                // identify xhr object
177
                xhr = xhr || args[2];
178
                
179
                // not modified response
180
                if (args[1] === "notmodified") {
181
                    if (xhr) {
182
                        // use date_send if exists (opera browser)
183
                        var d = this.date_send || xhr.getResponseHeader('Date');
184
                        if (d) { addApiCallDate(this.url, new Date(d), ajax_options.type); };
185
                    }
186
                    return;
187
                }
188
                
189
                // success response
190
                if (args[1] == "success" && handler_type == "success") {
191
                    try {
192
                        // use date_send if exists (opera browser)
193
                        var d = this.date_send || args[2].getResponseHeader('Date');
194
                        if (d) { addApiCallDate(this.url, new Date(d), ajax_options.type); };
195
                    } catch (err) {
196
                        console.error(err)
197
                    }
198
                }
199
            } catch (err) {
200
                console.error(err);
201
            }
202
            
203
            // dont call error callback for non modified responses
204
            if (arguments[1] === "notmodified") {
205
                return;
206
            }
207
            
208
            // prepare arguments for error callbacks
209
            var cb_args = _.toArray(arguments);
210
            if (handler_type === "error") {
211
                cb_args.push(_.clone(this));
212
            }
213
            
214
            // determine if we need to call our callback wrapper
215
            var call_api_handler = true;
216

    
217
            // request handles errors by itself, s
218
            if (handler_type == "error" && this.skip_api_error) {
219
                call_api_handler = false
220
            }
221

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

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

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

    
276
        return _.toArray(arguments);
277
    }
278

    
279
    api.completeHandler = function(xhr, status) {
280
        //debug("ajax complete", arguments)
281
        return arguments;
282
    }
283

    
284
    api.beforeSendHandler = function(xhr, settings) {
285
        //debug("ajax beforeSend", arguments)
286
        // ajax settings
287
        var ajax_settings = this;
288
        return arguments;
289
    }
290

    
291
    // api call helper
292
    api.call = function(url, method, data, complete, error, success, options) {
293
            var self = this;
294
            error = error || function(){};
295
            success = success || function(){};
296
            complete = complete || function(){};
297
            var extra = data ? data._options || {} : {};
298

    
299
            // really ugly way to pass sync request options.
300
            // it works though....
301
            if (data && data._options) { delete data['_options'] };
302
            
303
            // prepare the params
304
            var params = {
305
                url: snf.config.api_url + "/" + url,
306
                data: data,
307
                success: success,
308
                complete: function() { api.trigger("call"); complete(this) },
309
                error: error
310
            }
311

    
312
            params = _.extend(params, extra, options);
313
            this.sync(method, this, params);
314
        },
315

    
316
    _.extend(api, bb.Events);
317
    
318
    // helper for callbacks that need to get called
319
    // in fixed intervals
320
    api.updateHandler = function(options) {
321
        this.cb = options.callback;
322
        this.limit = options.limit;
323
        this.timeout = options.timeout;
324

    
325
        this.normal_timeout = options.timeout;
326
        this.fast_timeout = options.fast;
327

    
328
        this._called = 0;
329
        this.interval = undefined;
330
        this.call_on_start = options.call_on_start || true;
331

    
332
        this.running = false;
333
        
334
        // wrapper
335
        function _cb() {
336
            if (this.fast_timeout == this.timeout){
337
                this._called++;
338
            }
339

    
340
            if (this._called >= this.limit && this.fast_timeout == this.timeout) {
341
                this.timeout = this.normal_timeout;
342
                this.setInterval()
343
            }
344
            this.cb();
345
        };
346

    
347
        _cb = _.bind(_cb, this);
348

    
349
        this.faster = function() {
350
            this.timeout = this.fast_timeout;
351
            this._called = 0;
352
            this.setInterval();
353
        }
354

    
355
        this.setInterval = function() {
356
            this.trigger("clear");
357
            window.clearInterval(this.interval);
358
            this.interval = window.setInterval(_cb, this.timeout);
359
            this.running = true;
360
            if (this.call_on_start) {
361
                _cb();
362
            }
363
        }
364

    
365
        this.start = function (call_on_start) {
366
            this.call_on_start = call_on_start == undefined ? this.call_on_start : call_on_start;
367
            this.setInterval();
368
        }
369

    
370
        this.stop = function() {
371
            this.trigger("clear");
372
            window.clearInterval(this.interval);
373
            this.running = false;
374
        }
375
    }
376
    
377
    // api error state
378
    api.stop_calls = false;
379
    api.STATES = { NORMAL:1, WARN:0, ERROR:-1 };
380
    api.error_state = api.STATES.NORMAL;
381

    
382
    // on api error update the api error_state
383
    api.bind("error", function() {
384
        var args = _.toArray(_.toArray(arguments)[0]);
385
        var params = _.last(args);
386
        
387
        if (params.critical) {
388
            snf.api.error_state = api.STATES.ERROR;
389
            snf.api.stop_calls = true;
390
        } else {
391
            snf.api.error_state = api.STATES.WARN;
392
        }
393
        snf.api.trigger("change:error_state", snf.api.error_state);
394
    });
395
    
396
    // reset api error state
397
    api.bind("reset", function() {
398
        snf.api.error_state = api.STATES.NORMAL;
399
        snf.api.stop_calls = false;
400
        snf.api.trigger("change:error_state", snf.api.error_state);
401
    })
402

    
403
    // make it eventable
404
    _.extend(api.updateHandler.prototype, bb.Events);
405
    
406
})(this);