@qikdev/sdk
Version:
Promise based Javascript SDK
369 lines (297 loc) • 11.3 kB
JavaScript
import axios from "axios";
import qs from "qs";
import { version } from "../version.js";
/**
* Creates a new instance of Axios, with automatic authentication and token refreshing functionality.
* This module provides all the underlying functions for making http requests and interacting with the REST API.
* It is a wrapper around the axios library, a popular promise based javascript request package. The api module is used by most other modules in the SDK and will handle authentication, token refreshing and other headers automatically.
* @alias api
* @constructor
* @hideconstructor
* @param {QikAPI} qik A reference to the parent instance of the QikCore module. This module is usually created by a QikCore instance that passes itself in as the first argument.
*/
const CancelToken = axios.CancelToken;
///////////////////////////////////////
var QikAPI = function (qik) {
var defaultCache;
if (typeof window !== "undefined") {
defaultCache = qik.cache.get("api");
}
// axios 1.x exposes defaults.adapter as an array of adapter names — resolve it to the underlying function
const defaultAdapter = axios.getAdapter(axios.defaults.adapter);
/////////////////////////////////////////////////////
function getRequestCacheKey(config) {
var key = [
config.method,
config.url,
config.headers?.Authorization,
config.headers.Accept,
JSON.stringify({
params: config.params,
data: config.data,
}),
]
.filter(Boolean)
.join("-");
return key;
}
///////////////////////////////////////
const inflightRequests = {};
let cacheAdapter = function (config) {
var cacheKey = getRequestCacheKey(config);
// If there is already an identical request being made
if (!inflightRequests[cacheKey]) {
// Be more efficient and wait for the existing request
inflightRequests[cacheKey] = new Promise(function (resolve, reject) {
var useCache;
var cachedResponse;
///////////////////////////////////////
switch (String(config.method).toLowerCase()) {
case "post":
case "patch":
case "put":
case "delete":
//Unless we've specified we want a cache
if (!config.cache) {
//Don't use the cache
config.cache = false;
}
break;
}
///////////////////////////////////////
if (config.cache === false) {
//No cache so make new request
} else {
//Use the cache specified or the default cache
useCache = config.cache || defaultCache;
//If there is a cache
if (useCache) {
//If we have the cachedResponse version
cachedResponse = useCache.get(cacheKey);
}
}
///////////////////////////////////////
if (cachedResponse) {
return resolve(cachedResponse);
}
var copy = Object.assign(config, { adapter: defaultAdapter });
return axios.request(config).then(
function (res) {
delete inflightRequests[cacheKey];
resolve(res);
},
function (err) {
delete inflightRequests[cacheKey];
reject(err);
},
);
});
}
return inflightRequests[cacheKey];
};
//////////////////////////////////////////////////////////////////////////////
const service = createNewAxios(cacheAdapter);
//////////////////////////////////////////////////////////////////////////////
/**
*
* Generate an Endpoint URL, complete with authentication.
* this function is helpful for generating authenticated links for the user to open in another window
* @alias api.generateEndpointURL
* @param {String} endpoint The endpoint to generate a url for
* @param {Object} params Extra parameters for the url
* @param {Object} options Additional request configuration
* @param {Object} options.withoutToken Don't append the current user's access token to the url
* @param {Object} options.file Whether the response for this request is expected to be a binary file type
* @example
*
*
* const result = sdk.api.generateEndpointURL('/image/61eca4746971e75c1fc670cf', {
* w:100,
* h:50,
* }, {file:true})
*
* // Would return something similar to:
* // https://api.qik.dev/image/61eca4746971e75c1fc670cf?w=100&h=50&access_token=XXXX...
*/
service.generateEndpointURL = function (endpoint, params, options) {
options = options || {};
params = params || {};
var token;
const apiURL = options.file ? qik.fileAPI || qik.apiURL : qik.apiURL;
//Append the access token to the url
if (!options.withoutToken) {
delete params.access_token;
token = qik.auth.getCurrentToken();
}
var stripLeadingTag = endpoint[0] == "/" ? endpoint.slice(1) : endpoint;
var parameterString = qik.utils.mapParameters(params);
if (token) {
parameterString = parameterString
? `?access_token=${token}&${parameterString}`
: `?access_token=${token}`;
} else {
parameterString = parameterString ? `?${parameterString}` : "";
}
var url = `${apiURL}${endpoint}${parameterString}`;
return url;
};
//////////////////////////////////////////////////////////////////////////////
service.wasCancelled = function (err) {
return axios.isCancel(err);
};
//////////////////////////////////////////////////////////////////////////////
function createNewAxios(adapter) {
var instance = axios.create({
paramsSerializer: {
serialize: (params) => qs.stringify(params, { arrayFormat: "repeat" }),
},
adapter,
});
///////////////////////////////////////
instance.defaults.baseURL = qik.apiURL;
instance.defaults.headers.common.Accept = "application/json";
/////////////////////////////////////////////////////
// Add relative date and timezone to every request
instance.interceptors.request.use(function (config) {
const inflightKey = getRequestCacheKey(config);
if (config.withoutToken) {
return config;
}
config.headers["qik-request-date"] = new Date().getTime();
if (Intl) {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (tz) {
config.headers["qik-request-timezone"] = tz;
}
}
config.headers["qik-api-version"] = version;
// Include the socket window id in the request
if (qik?.socket?.windowID) {
config.headers["qik-socket-window"] = qik.socket.windowID;
}
var token = qik.auth.getCurrentToken();
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
if (config.params && config.params.access_token) {
delete config.params.access_token;
}
}
return config;
});
/////////////////////////////////////////////////////
let retryCount = 0;
instance.interceptors.response.use(
function (response) {
var config = response.config;
//Get the response status
var status = response.status;
switch (status) {
case 204:
// No content give it another try
if (retryCount < 5) {
retryCount++;
// Wait a second and try again
return new Promise(function (resolve, reject) {
setTimeout(function () {
return instance.request(config).then(resolve, reject);
}, 800);
});
} else {
console.log("Failed after 5 retries");
retryCount = 0;
}
break;
}
return response;
},
function (err) {
if (axios.isCancel(err)) {
return Promise.reject(err);
}
//Get the response status
var status = err?.response?.status || err.status;
//Check the status
switch (status) {
case 401:
//Ignore let QikAuth handle it
break;
case 429: {
// Rate-limited by the server's fairness/concurrency limiter. It
// sends Retry-After (seconds); honour it — with jitter so a burst
// of 429s doesn't retry in lockstep and re-trip the limit — and
// retry a few times rather than surfacing the error. Retry state
// lives on the request config, NOT the shared retryCount above,
// because 429s arrive in concurrent bursts that would corrupt a
// single shared counter.
var config429 = err.config || {};
config429.__retry429 = (config429.__retry429 || 0) + 1;
if (config429.__retry429 <= 3) {
var retryAfterHeader = parseInt(
err?.response?.headers?.["retry-after"],
10,
);
var backoffMs = Number.isFinite(retryAfterHeader)
? retryAfterHeader * 1000
: 800 * config429.__retry429;
var jitterMs = Math.floor(Math.random() * 500);
return new Promise(function (resolve, reject) {
setTimeout(function () {
return instance.request(config429).then(resolve, reject);
}, backoffMs + jitterMs);
});
}
break;
}
case 502:
case 504:
if (retryCount < 5) {
retryCount++;
// Wait a second and try again
return new Promise(function (resolve, reject) {
setTimeout(function () {
return instance.request(err.config).then(resolve, reject);
}, 800);
});
} else {
console.log("Failed after 5 retries");
retryCount = 0;
}
break;
case 404:
//Not found
break;
default:
break;
}
/////////////////////////////////////////////////////
return Promise.reject(err);
},
);
return instance;
}
/////////////////////////////////////////////////////
function retrieveIDs(data) {
const text = typeof data === "string" ? data : JSON.stringify(data);
const matches = text.match(/\b[0-9a-f]{24}\b/gi);
return matches ? [...new Set(matches)] : [];
}
///////////////////////////////////////
/**
*
* Reference to the underlying axios package (https://www.npmjs.com/package/axios)
* Useful for creating new instances of axios if you want to create http requests to
* external APIs and not send tokens etc..
* @name api.axios
* @example
* const newRequest = await sdk.api.axios.get('https://otherapi.com/content/61eca4746971e75c1fc670cf');
*/
service.CancelToken = CancelToken;
service.axios = axios;
///////////////////////////////////////
return service;
};
///////////////////////////////////////
///////////////////////////////////////
///////////////////////////////////////
export { CancelToken as CancelToken };
export default QikAPI;