Skip to content

Instantly share code, notes, and snippets.

@tkers
Last active October 1, 2015 13:35
Show Gist options
  • Save tkers/53cb2c19fef322c02e51 to your computer and use it in GitHub Desktop.
Save tkers/53cb2c19fef322c02e51 to your computer and use it in GitHub Desktop.
/**
* 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 <tijn@divbyzero.nl>
*/
"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