Last active
October 1, 2015 13:35
-
-
Save tkers/53cb2c19fef322c02e51 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Konnektid - rpc | |
* | |
* Copyright(c) 2015 Konnektid | |
* All rights reserved. | |
* | |
* Simple function to communicate with the API without all the | |
* bullshit that I made up when creating the original API.js module. | |
* Only handles setting up the correct headers and parsing the data. | |
* | |
* @TODO attach listener for onLine events | |
* @TODO post all messages in queue unless TTL reached | |
* | |
* @author Tijn Kersjes <[email protected]> | |
*/ | |
"use strict"; | |
// export the method | |
module.exports = (function () { | |
let rpc; | |
let BASE_URL; | |
let AUTH_TOKEN; | |
let initialised = false; | |
/** | |
* Stores the authentication token and API endpoint | |
* | |
* @param {object} params | |
* @param {string} params.token Authentication token to use for API calls | |
* @param {string} params.endpoint URL where the root of API is located | |
* | |
* @returns {rpc} The RPC object to allow chaining | |
*/ | |
function init(params) { | |
AUTH_TOKEN = params.token; | |
BASE_URL = params.endpoint; | |
initialised = true; | |
return rpc; | |
} | |
/** | |
* Sets the authentication token | |
* | |
* @param {string} token Authentication token to use for API calls | |
* | |
* @returns {void} | |
*/ | |
function setToken(token) { | |
AUTH_TOKEN = token; | |
} | |
/** | |
* Creates a new XMLHttpRequest instance | |
* | |
* @returns {XMLHttpRequest} The XHR instance | |
*/ | |
function createXHR() { | |
// Firefox, Opera, Chrome, Safari | |
if (window.XMLHttpRequest) | |
return new XMLHttpRequest(); | |
// Internet Explorer | |
if (window.ActiveXObject) { | |
try { | |
return new ActiveXObject("Microsoft.XMLHTTP"); | |
} | |
catch (e) { | |
return new ActiveXObject("Msxml2.XMLHTTP"); | |
} | |
} | |
throw new Error("XMLHttpRequest not supported"); | |
} | |
/** | |
* Defines a RPC message that can be re-queued on network failure | |
* | |
* @param {object} args | |
* @param {string=} args.name Name of the service to call | |
* @param {string=} args.method HTTP method to use when sending (defaults to GET) | |
* @param {boolean=} args.cache Whether to cache or not (defaults to false) | |
* @param {object=} args.params Query parameters to attach to the url | |
* @param {object=} args.payload Data to send with the request | |
* @param {number=} args.retries Number of times to retry sending the message (defaults to 3) | |
* @param {number=} args.interval Number of milliseconds between retries (defaults to 1000) | |
* | |
* @constructor | |
*/ | |
function Message(args) { | |
args = args || {}; | |
// create the deferred promise | |
this.promise = new Promise((resolve, reject) => { | |
this.resolve = resolve; | |
this.reject = reject; | |
}); | |
// set the request method (POST/GET) | |
this.method = args.method ? args.method.toUpperCase() : "GET"; | |
// build the request URL | |
this.url = BASE_URL + "/" + (args.name || ""); | |
this.params = args.params; | |
this.cache = args.cache || false; | |
this.data = args.payload; | |
// reschedule settings | |
this.retries = args.retries || 3; | |
this.interval = args.interval || 1000; | |
} | |
/** | |
* Tries to send a message again after it failed, or rejects the promises after | |
* a set amount of retries. | |
* | |
* @param {Message} message The message to resend | |
* @param {Function} cb Function to retry | |
* | |
* @returns {void} | |
*/ | |
function retryOrReject(message, cb) { | |
// if there are retries left for the message | |
if (message.retries --> 0) { | |
// retry after short delay | |
// @TODO check for retry-after response header? | |
setTimeout(() => cb(message), message.interval); | |
return; | |
} | |
// retried but to no avail - reject | |
message.reject({ | |
err: true, | |
code: "API_UNREACHABLE", | |
statusCode: 0, | |
message: "API server unreachable" | |
}); | |
} | |
/** | |
* Formats an object with key-value pairs into a string that can be used as a URL query | |
* | |
* @param {Object} params The parameters to format - e.g. `{a: 2, b: 4}` | |
* @returns {string} The query string - e.g. `a=2&b=4` | |
*/ | |
function formatQuery(params) { | |
// loop over all keys | |
return Object.keys(params) | |
// ignore keys that have no value set | |
.filter(key => params[key] !== undefined && params[key] !== null) | |
// encode and pair keys with values | |
.map(key => encodeURIComponent(key) + "=" + encodeURIComponent(params[key])) | |
// glue params together | |
.join("&"); | |
} | |
/** | |
* Sends a Message over XHR, retrying on failure | |
* | |
* @param {Message} message Message to send | |
* | |
* @returns {void} | |
*/ | |
function sendMessage(message) { | |
// append the query string to url | |
let url = message.url; | |
if (message.params) | |
url += "?" + formatQuery(message.params); | |
// append timestamp to prevent caching | |
if (message.cache === false) | |
url += (message.params ? "&" : "?") + "_=" + (new Date().getTime()); | |
// create a new XHR object | |
const xhr = createXHR(); | |
xhr.open(message.method, url, true); | |
// format the data | |
let data = message.data; | |
if (data && !(data instanceof FormData)) { | |
data = JSON.stringify(data); | |
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); | |
} | |
// attach authorisation token | |
if (AUTH_TOKEN) | |
xhr.setRequestHeader("Authorization", "Bearer " + AUTH_TOKEN); | |
// handle successful load | |
xhr.onload = function handleLoaded() { | |
// ok - parse json and return | |
if (xhr.status === 200) { | |
const res = JSON.parse(xhr.responseText); | |
if (res.err) | |
return message.reject(res); | |
return message.resolve(res); | |
} | |
// internal server error - retry | |
if (xhr.status >= 500 && xhr.status < 600) | |
return retryOrReject(message, sendMessage); | |
// otherwise this means the API was not reached correctly | |
// errors in API call should be returned with status:200 and | |
// define the error in the returned data, not at protocol level | |
return message.reject({ | |
err: true, | |
code: "API_UNREACHABLE", | |
statusCode: xhr.status, | |
message: xhr.responseText | |
}); | |
}; | |
// handle failed load | |
xhr.onerror = function handleFailed() { | |
// @TODO check navigator.onLine and retry after reconnect? | |
// connection issues | |
return retryOrReject(message, sendMessage); | |
}; | |
// send the request! | |
return xhr.send(data); | |
} | |
/** | |
* Creates a POST request to the API and returns the result (pretty straight forward, eh?) | |
* | |
* @param {string} name Name of the service to call (will be appended with the API root) | |
* @param {object=} payload Data to send with the request as JSON | |
* | |
* @returns {Promise.<object>} The result data from the API | |
*/ | |
function postData(name, payload) { | |
// module not yet initialised | |
if (!initialised) | |
return Promise.reject(new Error("Not initialised")); | |
// wrap call in a message | |
const message = new Message({ | |
method : "POST", | |
name : name, | |
payload : payload | |
}); | |
// send the message | |
sendMessage(message); | |
// return the deferred promise | |
return message.promise; | |
} | |
/** | |
* Creates a GET request to the API and returns the result | |
* | |
* @param {string} name Name of the service to call (will be appended with the API root) | |
* @param {object=} params Parameters to attach to the url | |
* | |
* @returns {Promise.<object>} The result data from the API | |
*/ | |
function fetchData(name, params) { | |
// module not yet initialised | |
if (!initialised) | |
return Promise.reject(new Error("Not initialised")); | |
// wrap call in a message | |
const message = new Message({ | |
method : "GET", | |
name : name, | |
params : params | |
}); | |
// send the request | |
sendMessage(message); | |
// return the deferred promise | |
return message.promise; | |
} | |
/** | |
* Uploads a file to the API and returns the result | |
* | |
* @param {string} name Name of the service to call (will be appended with the API root) | |
* @param {object=} payload Data to upload | |
* | |
* @returns {Promise.<object>} The result data from the API | |
*/ | |
function uploadFile(name, payload) { | |
// module not yet initialised | |
if (!initialised) | |
return Promise.reject(new Error("Not initialised")); | |
// wrap call in a message | |
const message = new Message({ | |
method : "POST", | |
name : name, | |
payload : payload | |
}); | |
// send the data | |
sendMessage(message); | |
// return the deferred promise | |
return message.promise; | |
} | |
// default RPC call is a POST request | |
rpc = postData; | |
rpc.call = postData; | |
rpc.fetch = fetchData; | |
rpc.upload = uploadFile; | |
// attach init to rpc method | |
rpc.init = init; | |
rpc.setToken = setToken; | |
// export | |
return rpc; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment