Skip to content

Commit 1fcebb2

Browse files
authored
feat: support ".finally()" (#1)
1 parent d15920b commit 1fcebb2

File tree

3 files changed

+162
-110
lines changed

3 files changed

+162
-110
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/DeferredPromise.ts

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
export type DeferredPromiseState = 'pending' | 'resolved' | 'rejected'
1+
export type DeferredPromiseState = "pending" | "resolved" | "rejected";
22
export type ResolveFunction<Data extends any, Result = void> = (
33
data: Data
4-
) => Result
5-
export type RejectFunction<Result = void> = (reason?: unknown) => Result
4+
) => Result;
5+
export type RejectFunction<Result = void> = (reason?: unknown) => Result;
66

77
/**
88
* Represents the completion of an asynchronous operation.
@@ -16,64 +16,83 @@ export type RejectFunction<Result = void> = (reason?: unknown) => Result
1616
* const portReady = new DeferredPromise()
1717
* portReady.reject(new Error('Port is already in use'))
1818
*/
19-
export class DeferredPromise<Data extends any> {
20-
public resolve: ResolveFunction<Data>
21-
public reject: RejectFunction
22-
public state: DeferredPromiseState
23-
public result?: Data
24-
public rejectionReason?: unknown
19+
export class DeferredPromise<Data extends unknown = void> {
20+
public resolve: ResolveFunction<Data>;
21+
public reject: RejectFunction;
22+
public state: DeferredPromiseState;
23+
public result?: Data;
24+
public rejectionReason?: unknown;
2525

26-
private promise: Promise<Data>
26+
private promise: Promise<unknown>;
2727

2828
constructor() {
29-
this.promise = new Promise((resolve, reject) => {
29+
this.promise = new Promise<Data>((resolve, reject) => {
3030
this.resolve = (data) => {
31-
if (this.state !== 'pending') {
31+
if (this.state !== "pending") {
3232
throw new TypeError(
3333
`Cannot resolve a DeferredPromise: illegal state ("${this.state}")`
34-
)
34+
);
3535
}
3636

37-
this.state = 'resolved'
38-
this.result = data
39-
resolve(data)
40-
}
37+
this.state = "resolved";
38+
this.result = data;
39+
resolve(data);
40+
};
4141

4242
this.reject = (reason) => {
43-
if (this.state !== 'pending') {
43+
if (this.state !== "pending") {
4444
throw new TypeError(
4545
`Cannot reject a DeferredPromise: illegal state ("${this.state}")`
46-
)
46+
);
4747
}
4848

49-
this.state = 'rejected'
50-
this.rejectionReason = reason
51-
reject(reason)
52-
}
53-
})
49+
this.state = "rejected";
50+
this.rejectionReason = reason;
51+
reject(reason);
52+
};
53+
});
5454

55-
this.state = 'pending'
56-
this.result = undefined
57-
this.rejectionReason = undefined
55+
this.state = "pending";
56+
this.result = undefined;
57+
this.rejectionReason = undefined;
5858
}
5959

60-
public then(onresolved?: ResolveFunction<Data>, onrejected?: RejectFunction) {
61-
this.promise.then(onresolved, onrejected)
62-
return this
60+
/**
61+
* Attaches callbacks for the resolution and/or rejection of the Promise.
62+
*/
63+
public then(
64+
onresolved?: ResolveFunction<Data, any>,
65+
onrejected?: RejectFunction
66+
) {
67+
this.promise = this.promise.then(onresolved, onrejected);
68+
return this;
6369
}
6470

71+
/**
72+
* Attaches a callback for only the rejection of the Promise.
73+
*/
6574
public catch<RejectReason = never>(
6675
onrejected?: RejectFunction<RejectReason>
6776
): this {
68-
this.promise.catch<RejectReason>(onrejected)
69-
return this
77+
this.promise = this.promise.catch<RejectReason>(onrejected);
78+
return this;
79+
}
80+
81+
/**
82+
* Attaches a callback that is invoked when
83+
* the Promise is settled (fulfilled or rejected). The resolved
84+
* value cannot be modified from the callback.
85+
*/
86+
public finally(onfinally?: () => void): this {
87+
this.promise = this.promise.finally(onfinally);
88+
return this;
7089
}
7190

7291
static get [Symbol.species]() {
73-
return Promise
92+
return Promise;
7493
}
7594

7695
get [Symbol.toStringTag]() {
77-
return 'DeferredPromise'
96+
return "DeferredPromise";
7897
}
7998
}

test/DeferredPromise.test.ts

Lines changed: 106 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,136 @@
1-
import { DeferredPromise } from '../src'
1+
import { DeferredPromise } from "../src";
22

33
it('can be listened to with ".then()"', (done) => {
4-
expect.assertions(1)
4+
expect.assertions(1);
55

6-
const promise = new DeferredPromise<number>()
6+
const promise = new DeferredPromise<number>();
77

88
promise.then((data) => {
9-
expect(data).toBe(123)
10-
done()
11-
})
9+
expect(data).toBe(123);
10+
done();
11+
});
1212

13-
promise.resolve(123)
14-
})
13+
promise.resolve(123);
14+
});
1515

1616
it('can be listened to with ".catch()"', (done) => {
17-
expect.assertions(1)
17+
expect.assertions(1);
1818

19-
const promise = new DeferredPromise<number>()
19+
const promise = new DeferredPromise<number>();
2020
promise.catch((reason) => {
21-
expect(reason).toBe('error')
22-
done()
23-
})
21+
expect(reason).toBe("error");
22+
done();
23+
});
2424

25-
promise.reject('error')
26-
})
25+
promise.reject("error");
26+
});
2727

28-
it('can be awaited', async () => {
29-
const promise = new DeferredPromise<number>()
30-
promise.resolve(123)
28+
it("can be awaited", async () => {
29+
const promise = new DeferredPromise<number>();
30+
promise.resolve(123);
3131

32-
const data = await promise
33-
expect(data).toBe(123)
34-
})
32+
const data = await promise;
33+
expect(data).toBe(123);
34+
});
3535

36-
describe('resolve()', () => {
37-
it('can be resolved without data', () => {
38-
const promise = new DeferredPromise<void>()
39-
expect(promise.state).toBe('pending')
40-
promise.resolve()
36+
describe("resolve()", () => {
37+
it("can be resolved without data", () => {
38+
const promise = new DeferredPromise<void>();
39+
expect(promise.state).toBe("pending");
40+
promise.resolve();
4141

42-
expect(promise.state).toBe('resolved')
43-
expect(promise.result).toBeUndefined()
44-
})
42+
expect(promise.state).toBe("resolved");
43+
expect(promise.result).toBeUndefined();
44+
});
4545

46-
it('can be resolved with data', () => {
47-
const promise = new DeferredPromise<number>()
48-
expect(promise.state).toBe('pending')
46+
it("can be resolved with data", () => {
47+
const promise = new DeferredPromise<number>();
48+
expect(promise.state).toBe("pending");
4949

50-
promise.resolve(123)
50+
promise.resolve(123);
5151

52-
expect(promise.state).toBe('resolved')
53-
expect(promise.result).toBe(123)
54-
})
52+
expect(promise.state).toBe("resolved");
53+
expect(promise.result).toBe(123);
54+
});
5555

56-
it('throws when resolving an already resolved promise', () => {
57-
const promise = new DeferredPromise<number>()
58-
expect(promise.state).toBe('pending')
59-
promise.resolve(123)
56+
it("throws when resolving an already resolved promise", () => {
57+
const promise = new DeferredPromise<number>();
58+
expect(promise.state).toBe("pending");
59+
promise.resolve(123);
6060

6161
expect(() => promise.resolve(456)).toThrow(
6262
new TypeError(
6363
'Cannot resolve a DeferredPromise: illegal state ("resolved")'
6464
)
65-
)
66-
})
65+
);
66+
});
6767

68-
it('throws when resolving an already rejected promise', () => {
69-
const promise = new DeferredPromise<number>().catch(() => {})
70-
expect(promise.state).toBe('pending')
71-
promise.reject()
68+
it("throws when resolving an already rejected promise", () => {
69+
const promise = new DeferredPromise<number>().catch(() => {});
70+
expect(promise.state).toBe("pending");
71+
promise.reject();
7272

7373
expect(() => promise.resolve(123)).toThrow(
7474
new TypeError(
7575
'Cannot resolve a DeferredPromise: illegal state ("rejected")'
7676
)
77-
)
78-
})
79-
})
80-
81-
describe('reject()', () => {
82-
it('can be rejected without any reason', () => {
83-
const promise = new DeferredPromise<void>().catch(() => {})
84-
expect(promise.state).toBe('pending')
85-
promise.reject()
86-
87-
expect(promise.state).toBe('rejected')
88-
expect(promise.result).toBeUndefined()
89-
expect(promise.rejectionReason).toBeUndefined()
90-
})
91-
92-
it('can be rejected with a reason', () => {
93-
const promise = new DeferredPromise().catch(() => {})
94-
expect(promise.state).toBe('pending')
95-
96-
const rejectionReason = new Error('Something went wrong')
97-
promise.reject(rejectionReason)
98-
99-
expect(promise.state).toBe('rejected')
100-
expect(promise.result).toBeUndefined()
101-
expect(promise.rejectionReason).toEqual(rejectionReason)
102-
})
103-
})
77+
);
78+
});
79+
});
80+
81+
describe("reject()", () => {
82+
it("can be rejected without any reason", () => {
83+
const promise = new DeferredPromise<void>().catch(() => {});
84+
expect(promise.state).toBe("pending");
85+
promise.reject();
86+
87+
expect(promise.state).toBe("rejected");
88+
expect(promise.result).toBeUndefined();
89+
expect(promise.rejectionReason).toBeUndefined();
90+
});
91+
92+
it("can be rejected with a reason", () => {
93+
const promise = new DeferredPromise().catch(() => {});
94+
expect(promise.state).toBe("pending");
95+
96+
const rejectionReason = new Error("Something went wrong");
97+
promise.reject(rejectionReason);
98+
99+
expect(promise.state).toBe("rejected");
100+
expect(promise.result).toBeUndefined();
101+
expect(promise.rejectionReason).toEqual(rejectionReason);
102+
});
103+
});
104+
105+
describe("finally()", () => {
106+
it('executes the "finally" block when the promise resolves', async () => {
107+
const promise = new DeferredPromise<void>();
108+
const finallyCallback = jest.fn();
109+
promise.finally(finallyCallback);
110+
111+
// Promise is still pending.
112+
expect(finallyCallback).not.toHaveBeenCalled();
113+
114+
promise.resolve();
115+
await promise;
116+
117+
expect(finallyCallback).toHaveBeenCalledTimes(1);
118+
expect(finallyCallback).toHaveBeenCalledWith();
119+
});
120+
121+
it('executes the "finally" block when the promise rejects', async () => {
122+
const promise = new DeferredPromise<void>().catch(() => {});
123+
124+
const finallyCallback = jest.fn();
125+
promise.finally(finallyCallback);
126+
127+
// Promise is still pending.
128+
expect(finallyCallback).not.toHaveBeenCalled();
129+
130+
promise.reject();
131+
await promise;
132+
133+
expect(finallyCallback).toHaveBeenCalledTimes(1);
134+
expect(finallyCallback).toHaveBeenCalledWith();
135+
});
136+
});

0 commit comments

Comments
 (0)