Skip to content

Commit 54a26bb

Browse files
feat(promise-helpers): add finally callback to handleMaybePromise (#2152)
Co-authored-by: Arda TANRIKULU <[email protected]>
1 parent c654f0a commit 54a26bb

File tree

4 files changed

+412
-50
lines changed

4 files changed

+412
-50
lines changed

.changeset/late-flies-bet.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@whatwg-node/promise-helpers': minor
3+
---
4+
5+
Allow to pass a finally callback to `handleMaybePromise`

deno-jest.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import { fn } from 'jsr:@std/expect';
2+
import { it } from 'jsr:@std/testing/bdd';
23

3-
export {
4-
describe,
5-
it,
6-
test,
7-
beforeEach,
8-
afterEach,
9-
beforeAll,
10-
afterAll,
11-
} from 'jsr:@std/testing/bdd';
4+
it.each =
5+
(cases: object[]): typeof it =>
6+
(name, runner) => {
7+
for (const c of cases) {
8+
let testName = name;
9+
Object.entries(c).forEach(([k, v]) => {
10+
testName = testName.replaceAll(k, v);
11+
});
12+
it(testName, () => runner(c));
13+
}
14+
};
15+
export { it };
16+
17+
export { describe, test, beforeEach, afterEach, beforeAll, afterAll } from 'jsr:@std/testing/bdd';
1218
export { expect } from 'jsr:@std/expect';
1319

1420
export const jest = {

packages/promise-helpers/src/index.ts

Lines changed: 100 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,73 @@
11
export type MaybePromise<T> = Promise<T> | T;
22
export type MaybePromiseLike<T> = PromiseLike<T> | T;
33

4+
const FAKE_PROMISE_SYMBOL_NAME = '@whatwg-node/promise-helpers/FakePromise';
5+
46
export function isPromise<T>(value: MaybePromise<T>): value is Promise<T>;
57
export function isPromise<T>(value: MaybePromiseLike<T>): value is PromiseLike<T>;
68
export function isPromise<T>(value: MaybePromiseLike<T>): value is PromiseLike<T> {
79
return (value as any)?.then != null;
810
}
911

12+
export function isActualPromise<T>(value: MaybePromiseLike<T>): value is Promise<T> {
13+
const maybePromise = value as any;
14+
return maybePromise && maybePromise.then && maybePromise.catch && maybePromise.finally;
15+
}
16+
1017
export function handleMaybePromise<TInput, TOutput>(
1118
inputFactory: () => MaybePromise<TInput>,
1219
outputSuccessFactory: (value: TInput) => MaybePromise<TOutput>,
1320
outputErrorFactory?: (err: any) => MaybePromise<TOutput>,
21+
finallyFactory?: () => MaybePromise<void>,
1422
): MaybePromise<TOutput>;
1523
export function handleMaybePromise<TInput, TOutput>(
1624
inputFactory: () => MaybePromiseLike<TInput>,
1725
outputSuccessFactory: (value: TInput) => MaybePromiseLike<TOutput>,
1826
outputErrorFactory?: (err: any) => MaybePromiseLike<TOutput>,
27+
finallyFactory?: () => MaybePromiseLike<void>,
1928
): MaybePromiseLike<TOutput>;
2029
export function handleMaybePromise<TInput, TOutput>(
2130
inputFactory: () => MaybePromiseLike<TInput>,
2231
outputSuccessFactory: (value: TInput) => MaybePromiseLike<TOutput>,
2332
outputErrorFactory?: (err: any) => MaybePromiseLike<TOutput>,
33+
finallyFactory?: () => MaybePromiseLike<void>,
2434
): MaybePromiseLike<TOutput> {
25-
function _handleMaybePromise() {
26-
const input$ = inputFactory();
27-
if (isFakePromise<TInput>(input$)) {
28-
return outputSuccessFactory(input$.__fakePromiseValue);
29-
}
30-
if (isFakeRejectPromise(input$)) {
31-
throw input$.__fakeRejectError;
32-
}
33-
if (isPromise(input$)) {
34-
return input$.then(outputSuccessFactory, outputErrorFactory);
35-
}
36-
return outputSuccessFactory(input$);
37-
}
38-
if (!outputErrorFactory) {
39-
return _handleMaybePromise();
40-
}
41-
try {
42-
return _handleMaybePromise();
43-
} catch (err) {
44-
return outputErrorFactory(err);
35+
let result$ = fakePromise().then(inputFactory).then(outputSuccessFactory, outputErrorFactory);
36+
37+
if (finallyFactory) {
38+
result$ = result$.finally(finallyFactory);
4539
}
40+
41+
return unfakePromise(result$);
4642
}
4743

48-
export function fakePromise<T>(value: T): Promise<Awaited<T>>;
44+
export function fakePromise<T>(value: MaybePromise<T>): Promise<T>;
45+
export function fakePromise<T>(value: MaybePromiseLike<T>): Promise<T>;
4946
export function fakePromise(value: void): Promise<void>;
50-
export function fakePromise<T = void>(value: T): Promise<T> {
51-
if (isPromise(value)) {
47+
export function fakePromise<T>(value: MaybePromiseLike<T>): Promise<T> {
48+
if (value && isActualPromise(value)) {
5249
return value;
5350
}
51+
52+
if (isPromise(value)) {
53+
return {
54+
then: (resolve, reject) => fakePromise(value.then(resolve, reject)),
55+
catch: reject => fakePromise(value.then(res => res, reject)),
56+
finally: cb => fakePromise(cb ? promiseLikeFinally(value, cb) : value),
57+
[Symbol.toStringTag]: 'Promise',
58+
};
59+
}
60+
5461
// Write a fake promise to avoid the promise constructor
5562
// being called with `new Promise` in the browser.
5663
return {
57-
then(resolve: (value: T) => any) {
64+
then(resolve) {
5865
if (resolve) {
59-
const callbackResult = resolve(value);
60-
if (isPromise(callbackResult)) {
61-
return callbackResult;
66+
try {
67+
return fakePromise(resolve(value));
68+
} catch (err) {
69+
return fakeRejectPromise(err);
6270
}
63-
return fakePromise(callbackResult);
6471
}
6572
return this;
6673
},
@@ -69,19 +76,20 @@ export function fakePromise<T = void>(value: T): Promise<T> {
6976
},
7077
finally(cb) {
7178
if (cb) {
72-
const callbackResult = cb();
73-
if (isPromise(callbackResult)) {
74-
return callbackResult.then(
79+
try {
80+
return fakePromise(cb()).then(
7581
() => value,
7682
() => value,
7783
);
84+
} catch (err) {
85+
return fakeRejectPromise(err);
7886
}
79-
return fakePromise(value);
8087
}
8188
return this;
8289
},
8390
[Symbol.toStringTag]: 'Promise',
8491
__fakePromiseValue: value,
92+
[Symbol.for(FAKE_PROMISE_SYMBOL_NAME)]: 'resolved',
8593
} as Promise<T>;
8694
}
8795

@@ -155,28 +163,41 @@ export function iterateAsync<TInput, TOutput>(
155163
return iterate();
156164
}
157165

158-
export function fakeRejectPromise(error: unknown): Promise<never> {
159-
if (isPromise(error)) {
160-
return error as Promise<never>;
161-
}
166+
export function fakeRejectPromise<T>(error: unknown): Promise<T> {
162167
return {
163-
then() {
168+
then(_resolve, reject) {
169+
if (reject) {
170+
try {
171+
return fakePromise(reject(error));
172+
} catch (err) {
173+
return fakeRejectPromise(err);
174+
}
175+
}
164176
return this;
165177
},
166178
catch(reject: (error: unknown) => any) {
167179
if (reject) {
168-
return fakePromise(reject(error));
180+
try {
181+
return fakePromise(reject(error));
182+
} catch (err) {
183+
return fakeRejectPromise(err);
184+
}
169185
}
170186
return this;
171187
},
172188
finally(cb) {
173189
if (cb) {
174-
cb();
190+
try {
191+
cb();
192+
} catch (err) {
193+
return fakeRejectPromise(err);
194+
}
175195
}
176196
return this;
177197
},
178198
__fakeRejectError: error,
179199
[Symbol.toStringTag]: 'Promise',
200+
[Symbol.for(FAKE_PROMISE_SYMBOL_NAME)]: 'rejected',
180201
} as Promise<never>;
181202
}
182203

@@ -294,9 +315,47 @@ function iteratorResult<T>(value: T): IteratorResult<T> {
294315
}
295316

296317
function isFakePromise<T>(value: any): value is Promise<T> & { __fakePromiseValue: T } {
297-
return (value as any)?.__fakePromiseValue != null;
318+
return (value as any)?.[Symbol.for(FAKE_PROMISE_SYMBOL_NAME)] === 'resolved';
298319
}
299320

300321
function isFakeRejectPromise(value: any): value is Promise<never> & { __fakeRejectError: any } {
301-
return (value as any)?.__fakeRejectError != null;
322+
return (value as any)?.[Symbol.for(FAKE_PROMISE_SYMBOL_NAME)] === 'rejected';
323+
}
324+
325+
export function promiseLikeFinally<T>(
326+
value: PromiseLike<T> | Promise<T>,
327+
onFinally: () => MaybePromiseLike<void>,
328+
): PromiseLike<T> {
329+
if ('finally' in value) {
330+
return value.finally(onFinally);
331+
}
332+
333+
return value.then(
334+
res => {
335+
const finallyRes = onFinally();
336+
return isPromise(finallyRes) ? finallyRes.then(() => res) : res;
337+
},
338+
err => {
339+
const finallyRes = onFinally();
340+
if (isPromise(finallyRes)) {
341+
return finallyRes.then(() => {
342+
throw err;
343+
});
344+
} else {
345+
throw err;
346+
}
347+
},
348+
);
349+
}
350+
351+
export function unfakePromise<T>(promise: Promise<T>): MaybePromise<T> {
352+
if (isFakePromise<T>(promise)) {
353+
return promise.__fakePromiseValue;
354+
}
355+
356+
if (isFakeRejectPromise(promise)) {
357+
throw promise.__fakeRejectError;
358+
}
359+
360+
return promise;
302361
}

0 commit comments

Comments
 (0)