mirror of
https://github.com/beestat/app.git
synced 2026-04-27 03:20:04 -04:00
327 lines
9.1 KiB
JavaScript
327 lines
9.1 KiB
JavaScript
beestat.api = function() {
|
|
this.api_calls_ = [];
|
|
};
|
|
|
|
/**
|
|
* Stores cached responses statically across all API calls.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
beestat.api.cache = {};
|
|
|
|
/**
|
|
* Send an API call. If the api_call parameter is specified it will send that.
|
|
* If not, it will check the cache, then construct and send the appropriate
|
|
* API call if necessary.
|
|
*
|
|
* @param {Object=} opt_api_call The API call object.
|
|
*
|
|
* @return {beestat.api} This.
|
|
*/
|
|
beestat.api.prototype.send = function(opt_api_call) {
|
|
var self = this;
|
|
|
|
this.xhr_ = new XMLHttpRequest();
|
|
|
|
// If passing an actual API call, fire it off!
|
|
if (opt_api_call !== undefined) {
|
|
// Add in the API key
|
|
opt_api_call.api_key = window.beestat_api_key_local;
|
|
|
|
this.xhr_.addEventListener('load', function() {
|
|
self.load_(this.responseText);
|
|
});
|
|
|
|
this.xhr_.open('POST', 'api/');
|
|
this.xhr_.send(JSON.stringify(opt_api_call));
|
|
} else {
|
|
if (this.api_calls_.length === 0) {
|
|
throw new Error('Must add at least one API call.');
|
|
}
|
|
|
|
if (this.is_batch_() === true) {
|
|
// Only run uncached API calls.
|
|
var uncached_batch_api_calls = [];
|
|
this.cached_batch_api_calls_ = {};
|
|
this.api_calls_.forEach(function(api_call) {
|
|
var cached = self.get_cached_(api_call);
|
|
if (cached === undefined) {
|
|
uncached_batch_api_calls.push(api_call);
|
|
} else {
|
|
self.cached_batch_api_calls_[api_call.alias] = cached.data;
|
|
}
|
|
});
|
|
|
|
if (uncached_batch_api_calls.length === 0) {
|
|
/**
|
|
* If no API calls left, just fire off the callback with the data.
|
|
* Timeout makes this behave like an actual API call in terms of
|
|
* program flow. Without this, if there is a rerender() inside a
|
|
* callback, the rerender can happen during a render which causes
|
|
* problems.
|
|
*/
|
|
if (this.callback_ !== undefined) {
|
|
window.setTimeout(function() {
|
|
self.callback_(self.cached_batch_api_calls_, true);
|
|
}, 0);
|
|
}
|
|
} else {
|
|
// If more than one API call left, fire off a batch API call.
|
|
this.send({'batch': JSON.stringify(uncached_batch_api_calls)});
|
|
}
|
|
} else {
|
|
var single_api_call = this.api_calls_[0];
|
|
var cached = this.get_cached_(single_api_call);
|
|
if (cached !== undefined) {
|
|
if (this.callback_ !== undefined) {
|
|
/**
|
|
* Timeout makes this behave like an actual API call in terms of
|
|
* program flow. Without this, if there is a rerender() inside a
|
|
* callback, the rerender can happen during a render which causes
|
|
* problems.
|
|
*/
|
|
window.setTimeout(function() {
|
|
self.callback_(cached.data, true);
|
|
}, 0);
|
|
}
|
|
} else {
|
|
this.send(single_api_call);
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Add an API call.
|
|
*
|
|
* @param {string} resource The resource.
|
|
* @param {string} method The method.
|
|
* @param {Object=} opt_args Optional arguments.
|
|
* @param {string=} opt_alias Optional alias (required for batch API calls).
|
|
* @param {boolean=} opt_bypass_cache_read Optional bypass of cache read.
|
|
* @param {boolean=} opt_bypass_cache_write Optional bypass of cache write.
|
|
* @param {boolean=} opt_clear_cache Optional flag to just clear the cache for
|
|
* an API call.
|
|
*
|
|
* @return {beestat.api} This.
|
|
*/
|
|
beestat.api.prototype.add_call = function(
|
|
resource,
|
|
method,
|
|
opt_args,
|
|
opt_alias,
|
|
opt_bypass_cache_read,
|
|
opt_bypass_cache_write,
|
|
opt_clear_cache
|
|
) {
|
|
var api_call = {
|
|
'resource': resource,
|
|
'method': method,
|
|
'arguments': JSON.stringify(beestat.default_value(opt_args, {}))
|
|
};
|
|
|
|
if (opt_alias !== undefined) {
|
|
api_call.alias = opt_alias;
|
|
}
|
|
if (opt_bypass_cache_read !== undefined) {
|
|
api_call.bypass_cache_read = opt_bypass_cache_read;
|
|
}
|
|
if (opt_bypass_cache_write !== undefined) {
|
|
api_call.bypass_cache_write = opt_bypass_cache_write;
|
|
}
|
|
if (opt_clear_cache !== undefined) {
|
|
api_call.clear_cache = opt_clear_cache;
|
|
}
|
|
|
|
this.api_calls_.push(api_call);
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Set a callback function to be called once the API call completes.
|
|
*
|
|
* @param {Function} callback The callback function.
|
|
*
|
|
* @return {beestat.api} This.
|
|
*/
|
|
beestat.api.prototype.set_callback = function(callback) {
|
|
this.callback_ = callback;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Get the API calls for this instance.
|
|
*
|
|
* @return {array} The API calls.
|
|
*/
|
|
beestat.api.prototype.get_api_calls = function() {
|
|
return this.api_calls_;
|
|
};
|
|
|
|
/**
|
|
* Force the current API call to send and return as a batch, even if there's
|
|
* only one. This makes handling responses easier when dynamically generating
|
|
* API calls.
|
|
*
|
|
* @return {beestat.api} This.
|
|
*/
|
|
beestat.api.prototype.force_batch = function() {
|
|
this.force_batch_ = true;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Fires after an XHR request returns.
|
|
*
|
|
* @param {string} response_text Whatever the XHR request returned.
|
|
*/
|
|
beestat.api.prototype.load_ = function(response_text) {
|
|
var response;
|
|
try {
|
|
response = window.JSON.parse(response_text);
|
|
} catch (e) {
|
|
var detail = response_text;
|
|
if (detail === '') {
|
|
detail = this.xhr_.status + ' ' + this.xhr_.statusText;
|
|
}
|
|
|
|
beestat.error('API returned invalid response.', detail);
|
|
return;
|
|
}
|
|
|
|
// Special handling for these error codes.
|
|
const error_codes = {
|
|
'log_out': [
|
|
1505, // Session is expired.
|
|
10000, // Could not get first token.
|
|
10001, // Could not refresh ecobee token; no token found.
|
|
10002, // Could not refresh ecobee token; ecobee returned no token.
|
|
10500, // Ecobee access was revoked by user.
|
|
10501 // No ecobee access for this user.
|
|
],
|
|
'ignore': [
|
|
10601 // Failed to subscribe.
|
|
]
|
|
};
|
|
|
|
// Error handling
|
|
if (
|
|
response.data &&
|
|
error_codes.log_out.includes(response.data.error_code) === true
|
|
) {
|
|
window.location.href = '/';
|
|
return;
|
|
} else if (
|
|
response.success !== true &&
|
|
error_codes.ignore.includes(response.data.error_code) === false
|
|
) {
|
|
beestat.error(
|
|
'API call failed: ' + response.data.error_message,
|
|
JSON.stringify(response, null, 2)
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Cache responses
|
|
var cached_until_header = this.xhr_.getResponseHeader('beestat-cached-until');
|
|
|
|
if (this.is_batch_() === true) {
|
|
var cached_untils = window.JSON.parse(cached_until_header);
|
|
for (var alias in cached_untils) {
|
|
for (var i = 0; i < this.api_calls_.length; i++) {
|
|
if (this.api_calls_[i].alias === alias) {
|
|
this.cache_(
|
|
this.api_calls_[i],
|
|
response.data[alias],
|
|
cached_untils[alias]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (cached_until_header !== null) {
|
|
this.cache_(this.api_calls_[0], response.data, cached_until_header);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* For batch API calls, add in any responses that were pulled out earlier
|
|
* because they were cached.
|
|
*/
|
|
if (this.is_batch_() === true) {
|
|
for (var cached_alias in this.cached_batch_api_calls_) {
|
|
response.data[cached_alias] =
|
|
this.cached_batch_api_calls_[cached_alias];
|
|
}
|
|
}
|
|
|
|
// Callback
|
|
if (this.callback_ !== undefined) {
|
|
this.callback_(response.data, false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Is this a batch API call? Determined by looking at the number of API calls
|
|
* added. If more than one, batch.
|
|
*
|
|
* @return {boolean} Whether or not this is a batch API call.
|
|
*/
|
|
beestat.api.prototype.is_batch_ = function() {
|
|
return this.force_batch_ === true || this.api_calls_.length > 1;
|
|
};
|
|
|
|
/**
|
|
* Cache an API call.
|
|
*
|
|
* @param {Object} api_call The API call object.
|
|
* @param {*} data The data to cache.
|
|
* @param {string} until Timestamp to cache until.
|
|
*/
|
|
beestat.api.prototype.cache_ = function(api_call, data, until) {
|
|
var server_date = moment(this.xhr_.getResponseHeader('date'));
|
|
var duration = moment.duration(moment.utc(until).diff(server_date));
|
|
|
|
beestat.api.cache[this.get_key_(api_call)] = {
|
|
'data': data,
|
|
'until': moment().add(duration.asSeconds(), 'seconds')
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Look for cached data for an API call and return it if not expired.
|
|
*
|
|
* @param {Object} api_call The API call object.
|
|
*
|
|
* @return {*} The cached data, or undefined if none.
|
|
*/
|
|
beestat.api.prototype.get_cached_ = function(api_call) {
|
|
var cached = beestat.api.cache[this.get_key_(api_call)];
|
|
if (
|
|
cached !== undefined &&
|
|
moment().isAfter(cached.until) === false &&
|
|
api_call.bypass_cache_read !== true &&
|
|
api_call.clear_cache !== true
|
|
) {
|
|
return cached;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
/**
|
|
* Get a cache key for an API call. There's a lack of hash options in
|
|
* JavaScript so this just concatenates a bunch of stuff together.
|
|
*
|
|
* @param {Object} api_call The API call object.
|
|
*
|
|
* @return {string} The cache key.
|
|
*/
|
|
beestat.api.prototype.get_key_ = function(api_call) {
|
|
return api_call.resource + '.' + api_call.method + '.' + api_call.arguments;
|
|
};
|