@atomiqlabs/sdk-lib
Version:
Basic SDK functionality library for atomiq
318 lines (285 loc) • 11.5 kB
text/typescript
import {RequestError} from "../errors/RequestError";
import {Buffer} from "buffer";
import {randomBytes as randomBytesNoble} from "@noble/hashes/utils";
type Constructor<T = any> = new (...args: any[]) => T;
function isConstructor(fn: any): fn is Constructor {
return (
typeof fn === 'function' &&
fn.prototype != null &&
fn.prototype.constructor === fn
);
}
function isConstructorArray(fnArr: any): fnArr is Constructor[] {
return Array.isArray(fnArr) && fnArr.every(isConstructor);
}
/**
* Checks whether the passed error is allowed to pass through
*
* @param e Error in question
* @param errorAllowed Allowed errors as defined as a callback function, specific error type, or an array of error types
*/
function checkError(e: any, errorAllowed: ((e: any) => boolean) | Constructor<Error> | Constructor<Error>[]) {
if(isConstructorArray(errorAllowed)) return errorAllowed.find(error => e instanceof error)!=null;
if(isConstructor(errorAllowed)) return e instanceof errorAllowed;
return errorAllowed(e);
}
export type LoggerType = {
debug: (msg: string, ...args: any[]) => void,
info: (msg: string, ...args: any[]) => void,
warn: (msg: string, ...args: any[]) => void,
error: (msg: string, ...args: any[]) => void
};
export function getLogger(prefix: string): LoggerType {
return {
debug: (msg, ...args) => global.atomiqLogLevel >= 3 && console.debug(prefix+msg, ...args),
info: (msg, ...args) => global.atomiqLogLevel >= 2 && console.info(prefix+msg, ...args),
warn: (msg, ...args) => (global.atomiqLogLevel==null || global.atomiqLogLevel >= 1) && console.warn(prefix+msg, ...args),
error: (msg, ...args) => (global.atomiqLogLevel==null || global.atomiqLogLevel >= 0) && console.error(prefix+msg, ...args)
};
}
const logger = getLogger("Utils: ");
/**
* Returns a promise that resolves when any of the passed promises resolves, and rejects if all the underlying
* promises fail with an array of errors returned by the respective promises
*
* @param promises
*/
export function promiseAny<T>(promises: Promise<T>[]): Promise<T> {
return new Promise<T>((resolve, reject) => {
let numRejected = 0;
const rejectReasons = Array(promises.length);
promises.forEach((promise, index) => {
promise.then((val) => {
if(resolve!=null) resolve(val);
resolve = null;
}).catch(err => {
rejectReasons[index] = err;
numRejected++;
if(numRejected===promises.length) {
reject(rejectReasons);
}
})
})
});
}
/**
* Maps a JS object to another JS object based on the translation function, the translation function is called for every
* property (value/key) of the old object and returns the new value of for this property
*
* @param obj
* @param translator
*/
export function objectMap<
InputObject extends {[key in string]: any},
OutputObject extends {[key in keyof InputObject]: any}
>(
obj: InputObject,
translator: <InputKey extends Extract<keyof InputObject, string>>(
value: InputObject[InputKey],
key: InputKey
) => OutputObject[InputKey]
): {[key in keyof InputObject]: OutputObject[key]} {
const resp: {[key in keyof InputObject]?: OutputObject[key]} = {};
for(let key in obj) {
resp[key] = translator(obj[key], key);
}
return resp as {[key in keyof InputObject]: OutputObject[key]};
}
/**
* Maps the entries from the map to the array using the translator function
*
* @param map
* @param translator
*/
export function mapToArray<K, V, Output>(map: Map<K, V>, translator: (key: K, value: V) => Output): Output[] {
const arr: Output[] = Array(map.size);
let pointer = 0;
for(let entry of map.entries()) {
arr[pointer++] = translator(entry[0], entry[1]);
}
return arr;
}
/**
* Creates a new abort controller that will abort if the passed abort signal aborts
*
* @param abortSignal
*/
export function extendAbortController(abortSignal?: AbortSignal) {
const _abortController = new AbortController();
if(abortSignal!=null) {
abortSignal.throwIfAborted();
abortSignal.onabort = () => _abortController.abort(abortSignal.reason);
}
return _abortController;
}
/**
* Runs the passed function multiple times if it fails
*
* @param func A callback for executing the action
* @param func.retryCount Count of the current retry, starting from 0 for original request and increasing
* @param retryPolicy Retry policy
* @param retryPolicy.maxRetries How many retries to attempt in total
* @param retryPolicy.delay How long should the delay be
* @param retryPolicy.exponential Whether to use exponentially increasing delays
* @param errorAllowed A callback for determining whether a given error is allowed, and we should therefore not retry
* @param abortSignal
* @returns Result of the action executing callback
*/
export async function tryWithRetries<T>(func: (retryCount?: number) => Promise<T>, retryPolicy?: {
maxRetries?: number, delay?: number, exponential?: boolean
}, errorAllowed?: ((e: any) => boolean) | Constructor<Error> | Constructor<Error>[], abortSignal?: AbortSignal): Promise<T> {
retryPolicy = retryPolicy || {};
retryPolicy.maxRetries = retryPolicy.maxRetries || 5;
retryPolicy.delay = retryPolicy.delay || 500;
retryPolicy.exponential = retryPolicy.exponential == null ? true : retryPolicy.exponential;
let err = null;
for (let i = 0; i < retryPolicy.maxRetries; i++) {
try {
return await func(i);
} catch (e) {
if (errorAllowed != null && checkError(e, errorAllowed)) throw e;
err = e;
logger.warn("tryWithRetries(): Error on try number: " + i, e);
}
if (abortSignal != null && abortSignal.aborted) throw (abortSignal.reason || new Error("Aborted"));
if (i !== retryPolicy.maxRetries - 1) {
await timeoutPromise(retryPolicy.exponential ? retryPolicy.delay * Math.pow(2, i) : retryPolicy.delay, abortSignal);
}
}
throw err;
}
/**
* Mimics fetch API byt adds a timeout to the request
*
* @param input
* @param init
*/
export function fetchWithTimeout(input: RequestInfo | URL, init: RequestInit & {
timeout?: number
}): Promise<Response> {
if (init == null) init = {};
if (init.timeout != null) init.signal = timeoutSignal(init.timeout, new Error("Network request timed out"), init.signal);
return fetch(input, init).catch(e => {
if (e.name === "AbortError") {
throw init.signal.reason;
} else {
throw e;
}
});
}
/**
* Sends an HTTP GET request through a fetch API, handles non 200 response codes as errors
* @param url Send request to this URL
* @param timeout Timeout (in milliseconds) for the request to conclude
* @param abortSignal
* @param allowNon200 Whether to allow non-200 status code HTTP responses
* @throws {RequestError} if non 200 response code was returned or body cannot be parsed
*/
export async function httpGet<T>(url: string, timeout?: number, abortSignal?: AbortSignal, allowNon200: boolean = false): Promise<T> {
const init = {
method: "GET",
timeout,
signal: abortSignal
};
const response: Response = await fetchWithTimeout(url, init);
if (response.status !== 200) {
let resp: string;
try {
resp = await response.text();
} catch (e) {
throw new RequestError(response.statusText, response.status);
}
if(allowNon200) {
try {
return JSON.parse(resp);
} catch (e) {}
}
throw RequestError.parse(resp, response.status);
}
return await response.json();
}
/**
* Sends an HTTP POST request through a fetch API, handles non 200 response codes as errors
* @param url Send request to this URL
* @param body A HTTP request body to send to the server
* @param timeout Timeout (in milliseconds) for the request to conclude
* @param abortSignal
* @throws {RequestError} if non 200 response code was returned
*/
export async function httpPost<T>(url: string, body: any, timeout?: number, abortSignal?: AbortSignal): Promise<T> {
const init = {
method: "POST",
timeout,
body: JSON.stringify(body),
headers: {'Content-Type': 'application/json'},
signal: abortSignal
};
const response: Response = timeout == null ? await fetch(url, init) : await fetchWithTimeout(url, init);
if (response.status !== 200) {
let resp: string;
try {
resp = await response.text();
} catch (e) {
throw new RequestError(response.statusText, response.status);
}
throw RequestError.parse(resp, response.status);
}
return await response.json();
}
/**
* Returns a promise that resolves after given amount seconds
*
* @param timeout how many milliseconds to wait for
* @param abortSignal
*/
export function timeoutPromise(timeout: number, abortSignal?: AbortSignal): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (abortSignal != null && abortSignal.aborted) {
reject(abortSignal.reason);
return;
}
let abortSignalListener: () => void;
let timeoutHandle = setTimeout(() => {
if(abortSignalListener!=null) abortSignal.removeEventListener("abort", abortSignalListener);
resolve();
}, timeout);
if (abortSignal != null) {
abortSignal.addEventListener("abort", abortSignalListener = () => {
if (timeoutHandle != null) clearTimeout(timeoutHandle);
timeoutHandle = null;
reject(abortSignal.reason);
});
}
});
}
/**
* Returns an abort signal that aborts after a specified timeout in milliseconds
*
* @param timeout Milliseconds to wait
* @param abortReason Abort with this abort reason
* @param abortSignal Abort signal to extend
*/
export function timeoutSignal(timeout: number, abortReason?: any, abortSignal?: AbortSignal): AbortSignal {
if(timeout==null) return abortSignal;
const abortController = new AbortController();
const timeoutHandle = setTimeout(() => abortController.abort(abortReason || new Error("Timed out")), timeout);
if(abortSignal!=null) {
abortSignal.addEventListener("abort", () => {
clearTimeout(timeoutHandle);
abortController.abort(abortSignal.reason);
});
}
return abortController.signal;
}
export function bigIntMin(a: bigint, b: bigint): bigint {
return a > b ? b : a;
}
export function bigIntMax(a: bigint, b: bigint): bigint {
return b > a ? b : a;
}
export function bigIntCompare(a: bigint, b: bigint): -1 | 0 | 1 {
return a > b ? 1 : a===b ? 0 : -1;
}
export function randomBytes(bytesLength: number): Buffer {
return Buffer.from(randomBytesNoble(bytesLength));
}