UNPKG

homey-api

Version:
336 lines (293 loc) 8.67 kB
/* eslint-disable no-undef */ 'use strict'; const APIErrorTimeout = require('./APIErrorTimeout'); /** * Helper Utility Class * @class * @private * @hideconstructor */ class Util { /** * Makes a call using `window.fetch` or `node-fetch`. * @param {...any} args * @returns {Promise} */ static async fetch(...args) { if (this.isReactNative()) { return fetch(...args); } if (this.isBrowser()) { return window.fetch(...args); } if (this.isNodeJS()) { const fetch = require('node-fetch'); return fetch(...args); } if (typeof fetch !== 'undefined') { return fetch(...args); } } /** * @param {number} ms - Number of milliseconds to wait * @returns {Promise<void>} */ static async wait(ms) { return new Promise(resolve => { setTimeout(resolve, ms); }); } /** * * @param {Promise} promise * @param {number} [timeoutMillis=5000] * @param {string} [message="Timeout after 5000ms"] * @returns {Promise} */ static async timeout(promise, timeoutMillis = 5000, message = `Timeout after ${timeoutMillis}ms`) { const timeoutError = new APIErrorTimeout(message); let timeoutRef; const returnPromise = Promise.race([ promise, new Promise((_, reject) => { timeoutRef = setTimeout(() => { reject(timeoutError); }, timeoutMillis); }), ]); returnPromise // eslint-disable-next-line no-unused-vars .catch(err => { }) .finally(() => { clearTimeout(timeoutRef); }); return returnPromise; } /** * @returns {Function} Returns a function that when called, returns a number with the delta in ms. */ static benchmark() { const start = new Date(); return () => { const end = new Date(); return end - start; }; } /** * Check if requests to http:// are supported. * Websites served on https:// cannot talk to http:// due to security concerns. * @returns {boolean} */ static isHTTPUnsecureSupported() { if (this.isReactNative()) return true; if (typeof window === 'undefined') return true; if (typeof window.location === 'undefined') return false; return window.location.protocol === 'http:'; } /** * @returns {boolean} */ static isReactNative() { return (typeof navigator !== 'undefined' && navigator.product === 'ReactNative'); } /** * @returns {boolean} */ static isBrowser() { if (this.isReactNative()) return false; return (typeof document !== 'undefined' && typeof window.document !== 'undefined'); } /** * @returns {boolean} */ static isNodeJS() { if (this.isReactNative()) return false; return (typeof process !== 'undefined'); } /** * @param {string} name - Query parameter name * @returns {string|null} */ static getSearchParameter(name) { if (!this.isBrowser()) return null; const searchParams = new URLSearchParams(window.location.search); return searchParams.get(name) || null; } /** * This method encodes a string into a base64 string. * It's provided as Util because Node.js uses `Buffer`, * browsers use `btoa` and React Native doesn't provide anything. * @param {string} input - Input * @returns {string} - Base64 encoded output */ static base64(s) { function btoaLookup(index) { if (index >= 0 && index < 64) { const keystr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; return keystr[index]; } // Throw INVALID_CHARACTER_ERR exception here -- won't be hit in the tests. return undefined; } if (typeof s !== 'string') { throw new Error('Invalid Input'); } let i; // "The btoa() method must throw an "InvalidCharacterError" DOMException if // data contains any character whose code point is greater than U+00FF." for (i = 0; i < s.length; i++) { if (s.charCodeAt(i) > 255) { return null; } } let out = ''; for (i = 0; i < s.length; i += 3) { const groupsOfSix = [undefined, undefined, undefined, undefined]; groupsOfSix[0] = s.charCodeAt(i) >> 2; groupsOfSix[1] = (s.charCodeAt(i) & 0x03) << 4; if (s.length > i + 1) { groupsOfSix[1] |= s.charCodeAt(i + 1) >> 4; groupsOfSix[2] = (s.charCodeAt(i + 1) & 0x0f) << 2; } if (s.length > i + 2) { groupsOfSix[2] |= s.charCodeAt(i + 2) >> 6; groupsOfSix[3] = s.charCodeAt(i + 2) & 0x3f; } for (let j = 0; j < groupsOfSix.length; j++) { if (typeof groupsOfSix[j] === 'undefined') { out += '='; } else { out += btoaLookup(groupsOfSix[j]); } } } return out; } /** * Generates an UUID v4 string * @returns {string} - UUID v4 string */ static uuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : ((r & 0x3) | 0x8); return v.toString(16); }); } /** * Get an environment variable. * In Node.js, this is accesses from `process.env`. * In a browser, this is accesses from `window.localStorage`. * @returns {string} - UUID v4 string */ static env(key) { if (this.isBrowser()) { return window.localStorage.getItem(key) || null; } if (this.isNodeJS()) { return process.env[key] || null; } if (this.isReactNative()) { return null; } return null; } static envKey(key) { return key .replace('Athom', 'ATHOM_') .replace('Homey', 'HOMEY_') .replace('API', '_API') .toUpperCase(); } /** * Polyfill for Promise.any, which is only supported on Node.js >=15 * @param {Array<Promise>} promises */ static async promiseAny(promises) { if (promises.length === 0) return; const rejections = []; return new Promise((resolve, reject) => { promises.forEach((promise, i) => { promise .then(result => resolve(result)) .catch(err => { rejections[i] = err; // Check if all promises have been rejected if (rejections.filter(Boolean).length === promises.length) { reject(rejections); } }); }); }); } /** * Converts an object to a query string * @param {object} queryObject - Query parameter object * @returns {string} */ static serializeQueryObject(queryObject) { let prefix; let querystring = []; let rbracket = /\[\]$/; function add(key, value) { // If value is a function, invoke it and return its value. value = (typeof value === 'function') ? value() : value === null ? '' : value; querystring[querystring.length] = encodeURIComponent(key) + '=' + encodeURIComponent(value); } function buildParams(prefix, obj, add) { let name; if (Array.isArray(obj)) { // Serialize array item. for (let index = 0; index < obj.length; index++) { if (rbracket.test(prefix)) { // Treat each array item as a scalar. add(prefix, obj[index]); } else { // Item is non-scalar (array or object), encode its numeric index. buildParams(prefix + '[' + (typeof (obj[index]) === 'object' ? index : '' ) + ']', obj[index], add); } } } else if (typeof obj === 'object') { // Serialize object item. for (name in obj) { buildParams(prefix + '[' + name + ']', obj[name], add); } } else { // Serialize scalar item. add(prefix, obj); } } // Encode params recursively. for (prefix in queryObject) { if (typeof queryObject[prefix] === 'undefined') continue; buildParams(prefix, queryObject[prefix], add); } // Return the resulting serialization. return querystring.join('&'); } /** * We use this instead of URLSearchParams because in react-native URLSearchParams are not encoded * for some reason. * * @param {object} params * @returns {string} encoded params */ static encodeUrlSearchParams(params) { const encodedPairs = []; for (const [key, value] of Object.entries(params)) { const encodedKey = encodeURIComponent(key); const encodedValue = encodeURIComponent(value); encodedPairs.push(encodedKey + "=" + encodedValue); } return encodedPairs.join("&"); } } module.exports = Util;