@devgrid/common
Version:
Some useful primitives
264 lines • 8.55 kB
JavaScript
import { entries } from "./entries";
import { noop, truly } from "./primitives";
import { isNumber, isPromise, isFunction } from "./predicates";
export const defer = () => {
const deferred = {};
deferred.promise = new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
return deferred;
};
export const delay = (ms, value, options) => new Promise((resolve) => {
const timer = setTimeout(resolve, ms, value);
if (options?.unref && typeof timer.unref === "function") {
timer.unref();
}
});
export const timeout = (promise, ms, options = {}) => {
if (!isPromise(promise)) {
throw new TypeError("The first argument must be a promise");
}
if (!isNumber(ms) || ms <= 0) {
throw new TypeError("Timeout must be a positive number");
}
return new Promise((resolve, reject) => {
let timeoutId;
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (options.signal) {
options.signal.removeEventListener('abort', onAbort);
}
};
const onAbort = () => {
cleanup();
reject(new Error('Timeout aborted'));
};
if (options.signal) {
if (options.signal.aborted) {
onAbort();
return;
}
options.signal.addEventListener('abort', onAbort, { once: true });
}
timeoutId = setTimeout(() => {
cleanup();
reject(new Error(`Timeout of ${ms}ms exceeded`));
}, ms);
if (options.unref && typeof timeoutId.unref === 'function') {
timeoutId.unref();
}
promise.then((value) => {
cleanup();
resolve(value);
}, (error) => {
cleanup();
reject(error);
});
});
};
export const nodeify = (promise, cb) => {
if (!isPromise(promise)) {
throw new TypeError("The first argument must be a promise");
}
if (!isFunction(cb)) {
return promise;
}
promise.then((x) => {
cb(null, x);
}, (y) => {
cb(y);
});
return promise;
};
export const callbackify = (fn) => {
if (!isFunction(fn)) {
throw new TypeError("The first argument must be a function");
}
return function _(...args) {
if (args.length && isFunction(args[args.length - 1])) {
const cb = args.pop();
return nodeify(fn.apply(this, args), cb);
}
return fn.apply(this, args);
};
};
const processFn = (fn, context, args, multiArgs, resolve, reject) => {
if (multiArgs) {
args.push((...result) => {
if (result[0]) {
reject(result);
}
else {
result.shift();
resolve(result);
}
});
}
else {
args.push((err, result) => {
if (err) {
reject(err);
}
else {
resolve(result);
}
});
}
fn.apply(context, args);
};
export const promisify = (fn, options) => {
if (!isFunction(fn)) {
throw new TypeError("The first argument must be a function");
}
return options && options.context
? (...args) => new Promise((resolve, reject) => {
processFn(fn, options.context, args, options && Boolean(options.multiArgs), resolve, reject);
})
: function _(...args) {
return new Promise((resolve, reject) => {
processFn(fn, this, args, Boolean(options?.multiArgs), resolve, reject);
});
};
};
export const promisifyAll = (source, options) => {
const suffix = options && options.suffix ? options.suffix : "Async";
const filter = options && typeof options.filter === "function" ? options.filter : truly;
if (isFunction(source)) {
return promisify(source, options);
}
const target = Object.create(source);
for (const [key, value] of entries(source, { all: true })) {
if (isFunction(value) && filter(key)) {
target[`${key}${suffix}`] = promisify(value, options);
}
}
return target;
};
const _finally = (promise, onFinally) => {
onFinally = onFinally || noop;
return promise.then((val) => new Promise((resolve) => {
resolve(onFinally());
}).then(() => val), (err) => new Promise((resolve) => {
resolve(onFinally());
}).then(() => {
throw err;
}));
};
export { _finally as finally };
export const retry = async (callback, options) => {
if (!callback || !options) {
throw new Error("requires a callback and an options set or a number");
}
const opts = isNumber(options) ? { max: options } : options;
const config = {
$current: opts.$current || 1,
max: opts.max,
timeout: opts.timeout,
match: Array.isArray(opts.match) ? opts.match : opts.match ? [opts.match] : [],
backoffBase: opts.backoffBase ?? 100,
backoffExponent: opts.backoffExponent || 1.1,
report: opts.report || null,
name: opts.name || callback.name || "unknown"
};
const shouldRetry = (error, attempt) => {
if (attempt >= config.max)
return false;
if (config.match.length === 0)
return true;
return config.match.some(match => match === error.toString() ||
match === error.message ||
(isFunction(match) && error instanceof match) ||
(match instanceof RegExp && (match.test(error.message) || match.test(error.toString()))));
};
let lastError = null;
while (true) {
try {
if (config.report) {
config.report(`Attempt ${config.name} #${config.$current}`, config);
}
let result = callback({ current: config.$current });
if (isPromise(result)) {
if (config.timeout) {
result = await timeout(result, config.timeout);
}
else {
result = await result;
}
}
if (config.report) {
config.report(`Success ${config.name} #${config.$current}`, config);
}
return result;
}
catch (error) {
lastError = error;
if (config.report) {
config.report(`Failed ${config.name} #${config.$current}: ${error.toString()}`, config, error);
}
if (!shouldRetry(error, config.$current)) {
throw lastError;
}
const retryDelay = Math.floor(config.backoffBase * Math.pow(config.backoffExponent ?? 1.1, config.$current - 1));
config.$current++;
if (retryDelay > 0) {
if (config.report) {
config.report(`Delaying retry of ${config.name} by ${retryDelay}ms`, config);
}
await delay(retryDelay);
}
}
}
};
export const props = async (obj) => {
const result = {};
await Promise.all(Object.keys(obj).map(async (key) => {
Object.defineProperty(result, key, {
enumerable: true,
value: await obj[key],
});
}));
return result;
};
const try_ = (fn, ...args) => new Promise((resolve) => {
resolve(fn(...args));
});
export { try_ as try };
export const universalify = (fn) => Object.defineProperties(function _(...args) {
if (isFunction(args[args.length - 1])) {
return fn.apply(this, args);
}
return new Promise((resolve, reject) => {
args.push((err, res) => {
if (err) {
reject(err);
}
else {
resolve(res);
}
});
fn.apply(this, args);
});
}, {
name: {
value: fn.name,
},
...Object.keys(fn).reduce((props_, k) => {
props_[k] = {
enumerable: true,
value: fn[k],
};
return props_;
}, {}),
});
export const universalifyFromPromise = (fn) => Object.defineProperty(function _(...args) {
const cb = args[args.length - 1];
if (!isFunction(cb)) {
return fn.apply(this, args);
}
return fn.apply(this, args).then((r) => cb(null, r), cb);
}, "name", { value: fn.name });
//# sourceMappingURL=promise.js.map