@whatwg-node/promise-helpers
Version:
Promise helpers
258 lines (257 loc) • 7.95 kB
JavaScript
const FAKE_PROMISE_SYMBOL_NAME = '@whatwg-node/promise-helpers/FakePromise';
export function isPromise(value) {
return value?.then != null;
}
export function isActualPromise(value) {
const maybePromise = value;
return maybePromise && maybePromise.then && maybePromise.catch && maybePromise.finally;
}
export function handleMaybePromise(inputFactory, outputSuccessFactory, outputErrorFactory, finallyFactory) {
let result$ = fakePromise().then(inputFactory).then(outputSuccessFactory, outputErrorFactory);
if (finallyFactory) {
result$ = result$.finally(finallyFactory);
}
return unfakePromise(result$);
}
export function fakePromise(value) {
if (value && isActualPromise(value)) {
return value;
}
if (isPromise(value)) {
return {
then: (resolve, reject) => fakePromise(value.then(resolve, reject)),
catch: reject => fakePromise(value.then(res => res, reject)),
finally: cb => fakePromise(cb ? promiseLikeFinally(value, cb) : value),
[Symbol.toStringTag]: 'Promise',
};
}
// Write a fake promise to avoid the promise constructor
// being called with `new Promise` in the browser.
return {
then(resolve) {
if (resolve) {
try {
return fakePromise(resolve(value));
}
catch (err) {
return fakeRejectPromise(err);
}
}
return this;
},
catch() {
return this;
},
finally(cb) {
if (cb) {
try {
return fakePromise(cb()).then(() => value, () => value);
}
catch (err) {
return fakeRejectPromise(err);
}
}
return this;
},
[Symbol.toStringTag]: 'Promise',
__fakePromiseValue: value,
[Symbol.for(FAKE_PROMISE_SYMBOL_NAME)]: 'resolved',
};
}
export function createDeferredPromise() {
if (Promise.withResolvers) {
return Promise.withResolvers();
}
let resolveFn;
let rejectFn;
const promise = new Promise(function deferredPromiseExecutor(resolve, reject) {
resolveFn = resolve;
rejectFn = reject;
});
return {
promise,
get resolve() {
return resolveFn;
},
get reject() {
return rejectFn;
},
};
}
export { iterateAsync as iterateAsyncVoid };
export function iterateAsync(iterable, callback, results) {
if (iterable?.length === 0) {
return;
}
const iterator = iterable[Symbol.iterator]();
let index = 0;
function iterate() {
const { done: endOfIterator, value } = iterator.next();
if (endOfIterator) {
return;
}
let endedEarly = false;
function endEarly() {
endedEarly = true;
}
return handleMaybePromise(function handleCallback() {
return callback(value, endEarly, index++);
}, function handleCallbackResult(result) {
if (result) {
results?.push(result);
}
if (endedEarly) {
return;
}
return iterate();
});
}
return iterate();
}
export function fakeRejectPromise(error) {
return {
then(_resolve, reject) {
if (reject) {
try {
return fakePromise(reject(error));
}
catch (err) {
return fakeRejectPromise(err);
}
}
return this;
},
catch(reject) {
if (reject) {
try {
return fakePromise(reject(error));
}
catch (err) {
return fakeRejectPromise(err);
}
}
return this;
},
finally(cb) {
if (cb) {
try {
cb();
}
catch (err) {
return fakeRejectPromise(err);
}
}
return this;
},
__fakeRejectError: error,
[Symbol.toStringTag]: 'Promise',
[Symbol.for(FAKE_PROMISE_SYMBOL_NAME)]: 'rejected',
};
}
export function mapMaybePromise(input, onSuccess, onError) {
return handleMaybePromise(() => input, onSuccess, onError);
}
/**
* Given an AsyncIterable and a callback function, return an AsyncIterator
* which produces values mapped via calling the callback function.
*/
export function mapAsyncIterator(iterator, onNext, onError, onEnd) {
if (Symbol.asyncIterator in iterator) {
iterator = iterator[Symbol.asyncIterator]();
}
let $return;
let abruptClose;
let onEndWithValue;
if (onEnd) {
let onEndWithValueResult /** R in onEndWithValue */;
onEndWithValue = value => {
onEndWithValueResult ||= handleMaybePromise(onEnd, () => value, () => value);
return onEndWithValueResult;
};
}
if (typeof iterator.return === 'function') {
$return = iterator.return;
abruptClose = (error) => {
const rethrow = () => {
throw error;
};
return $return.call(iterator).then(rethrow, rethrow);
};
}
function mapResult(result) {
if (result.done) {
return onEndWithValue ? onEndWithValue(result) : result;
}
return handleMaybePromise(() => result.value, value => handleMaybePromise(() => onNext(value), iteratorResult, abruptClose));
}
let mapReject;
if (onError) {
let onErrorResult;
// Capture rejectCallback to ensure it cannot be null.
const reject = onError;
mapReject = (error) => {
onErrorResult ||= handleMaybePromise(() => error, error => handleMaybePromise(() => reject(error), iteratorResult, abruptClose));
return onErrorResult;
};
}
return {
next() {
return iterator.next().then(mapResult, mapReject);
},
return() {
const res$ = $return
? $return.call(iterator).then(mapResult, mapReject)
: fakePromise({ value: undefined, done: true });
return onEndWithValue ? res$.then(onEndWithValue) : res$;
},
throw(error) {
if (typeof iterator.throw === 'function') {
return iterator.throw(error).then(mapResult, mapReject);
}
if (abruptClose) {
return abruptClose(error);
}
return fakeRejectPromise(error);
},
[Symbol.asyncIterator]() {
return this;
},
};
}
function iteratorResult(value) {
return { value, done: false };
}
function isFakePromise(value) {
return value?.[Symbol.for(FAKE_PROMISE_SYMBOL_NAME)] === 'resolved';
}
function isFakeRejectPromise(value) {
return value?.[Symbol.for(FAKE_PROMISE_SYMBOL_NAME)] === 'rejected';
}
export function promiseLikeFinally(value, onFinally) {
if ('finally' in value) {
return value.finally(onFinally);
}
return value.then(res => {
const finallyRes = onFinally();
return isPromise(finallyRes) ? finallyRes.then(() => res) : res;
}, err => {
const finallyRes = onFinally();
if (isPromise(finallyRes)) {
return finallyRes.then(() => {
throw err;
});
}
else {
throw err;
}
});
}
export function unfakePromise(promise) {
if (isFakePromise(promise)) {
return promise.__fakePromiseValue;
}
if (isFakeRejectPromise(promise)) {
throw promise.__fakeRejectError;
}
return promise;
}