Skip to content

Commit 1fe382e

Browse files
authored
feat: add deferred executor, support chaining (#4)
BREAKING CHANGE: Deferred promises are now implemented via the deferred executor function. There are no changes to the existing public API. ## Changes - Supports Promise chaining. - Adds `createDeferredExecutor` to use this library with any `Promise`. - Improves type annotations in regard to what the deferred promise expects as the input and what is the promise's output: ```ts const p = new DeferredPromise<number>() .then(() => 'hello') // The input must be "number". p.resolve(5) // the output is "string" // (the result of the "then" chain) await p // "hello" ``` - **Deprecated:** Removes `DeferredPromise.result`. Access the result by awaiting the promise instead. - **Deprecated:** The `resolved` promise state is replaced by `fulfilled`, which is the correct promise state representation per specification. ## Roadmap - [x] Document the deferred executor with more detail and then mention it in the deferred promise class documentation. That's exactly their relationship. - [x] Support promises returned from `.then`/`.catch`.
1 parent dc02e6e commit 1fe382e

17 files changed

+868
-331
lines changed

README.md

+121-64
Original file line numberDiff line numberDiff line change
@@ -12,117 +12,174 @@ npm install @open-draft/deferred-promise
1212

1313
## Documentation
1414

15+
- [**`createDeferredExecutor()`**](#createdeferredexecutor)
16+
- [`DeferredExecutor.state`](#deferredexecutorstate)
17+
- [`DeferredExecutor.resolve()`](#deferredexecutorresolve)
18+
- [`DeferredExecutor.reject()`](#deferredexecutorreject)
19+
- [`DeferredExecutor.rejectionReason`](#deferredexecutorrejectionreason)
1520
- [**Class: `DeferredPromise`**](#class-deferredpromise)
1621
- [`new DeferredPromise()`](#new-defferedpromise)
1722
- [`deferredPromise.state`](#deferredpromisestate)
18-
- [`deferredPromise.result`](#deferredpromiseresult)
1923
- [`deferredPromise.resolve()`](#deferredpromiseresolve)
2024
- [`deferredPromise.reject()`](#deferredpromisereject)
21-
- [`deferredPromise.finally()`](#deferredpromisefinally)
25+
- [`deferredPromise.rejectionReason`](#deferredpromiserejectionreason)
2226

23-
## Class: `DeferredPromise`
27+
---
2428

25-
### `new DefferedPromise()`
29+
## `createDeferredExecutor()`
2630

27-
Creates a new instance of a deferred promise.
31+
Creates a Promise executor function that delegates its resolution to the current scope.
2832

2933
```js
30-
import { DeferredPromise } from "@open-draft/deferred-promise";
34+
import { createDeferredExecutor } from '@open-draft/deferred-promise'
3135

32-
const promise = new DeferredPromise();
36+
const executor = createDeferredExecutor()
37+
const promise = new Promise(executor)
38+
39+
executor.resolve('hello')
40+
// executor.reject(new Error('Reason'))
3341
```
3442

35-
Unlike the regular `Promise`, a deferred promise does not accept the callback function. Instead, you should use [`.resolve()`](#deferredpromiseresolve) and [`.reject()`](#deferredpromisereject) to resolve and reject the promise respectively.
43+
Deferred executor allows you to control any promise remotely and doesn't affect the Promise instance in any way. Similar to the [`DeferredPromise`](#class-deferredpromise) instance, the deferred executor exposes additional promise properties like `state`, `rejectionReason`, `resolve`, and `reject`. In fact, the `DeferredPromise` class is implemented on top of the deferred executor.
3644

37-
A deferred promise is fully compatible with the native `Promise`, which means you can pass it to the consumers that await a regular `Promise` as well.
45+
```js
46+
const executor = createDeferredExecutor()
47+
const promise = new Promise(executor)
3848

39-
### `deferredPromise.state`
49+
executor.reject('reason')
4050

41-
- `<"pending" | "resolved" | "rejected">` **Default:** `"pending"`
51+
nextTick(() => {
52+
console.log(executor.rejectionReason) // "reason"
53+
})
54+
```
55+
56+
### `DeferredExecutor.state`
57+
58+
- `<"pending" | "fulfilled" | "rejected">` **Default:** `"pending"`
4259

4360
```js
44-
const promise = new DeferredPromise();
45-
console.log(promise.state); // "pending"
61+
const executor = createDeferredExecutor()
62+
const promise = new Promise(executor)
4663

47-
promise.resolve();
48-
console.log(promise.state); // "resolved"
64+
console.log(executor.state) // "pending"
4965
```
5066

51-
### `deferredPromise.result`
67+
Calling [`resolve()`](#deferredexecutorresolve) and [`reject()`](#deferredexecutorreject) methods of the executor transitions the state to "fulfilled" and "rejected" respectively.
5268

53-
Returns the value that has resolved the promise. If no value has been provided to the `.resolve()` call, `undefined` is returned instead.
69+
### `DeferredExecutor.resolve()`
70+
71+
Resolves the promise with a given value.
5472

5573
```js
56-
const promise = new DeferredPromise();
57-
promise.resolve("John");
74+
const executor = createDeferredExecutor()
75+
const promise = new Promise(executor)
76+
77+
console.log(executor.state) // "pending"
78+
79+
executor.resolve()
5880

59-
console.log(promise.result); // "John"
81+
// The promise state is still "pending"
82+
// because promises are settled in the next microtask.
83+
console.log(executor.state) // "pending"
84+
85+
nextTick(() => {
86+
// In the next microtask, the promise's state is resolved.
87+
console.log(executor.state) // "fulfilled"
88+
})
6089
```
6190

62-
### `deferredPromise.rejectionReason`
91+
### `DeferredExecutor.reject()`
6392

64-
Returns the reason that has rejected the promise. If no reason has been provided to the `.reject()` call, `undefined` is returned instead.
93+
Rejects the promise with a given reason.
6594

6695
```js
67-
const promise = new DeferredPromise();
68-
promise.reject(new Error("Internal Server Error"));
96+
const executor = createDeferredExecutor()
97+
const promise = new Promise(executor)
98+
99+
executor.reject(new Error('Failed to fetch'))
69100

70-
console.log(promise.rejectionReason); // Error
101+
nextTick(() => {
102+
console.log(executor.state) // "rejected"
103+
console.log(executor.rejectionReason) // Error("Failed to fetch")
104+
})
71105
```
72106

73-
### `deferredPromise.resolve()`
107+
You can access the rejection reason of the promise at any time by the [`rejectionReason`](#deferredexecutorrejectionreason) property of the deferred executor.
108+
109+
### `DeferredExecutor.rejectionReason`
74110

75-
Resolves the deferred promise with a given value.
111+
Returns the reason of the promise rejection. If no reason has been provided to the `reject()` call, `undefined` is returned instead.
76112

77113
```js
78-
function startServer() {
79-
const serverReady = new DeferredPromise();
114+
const executor = createDeferredExecutor()
115+
const promise = new Promise(executor)
80116

81-
new http.Server().listen(() => {
82-
// Resolve the deferred promise with the server address
83-
// once the server is ready.
84-
serverReady.resolve("http://localhost:8080");
85-
});
117+
promise.reject(new Error('Internal Server Error'))
86118

87-
// Return the deferred promise to the consumer.
88-
return serverReady;
89-
}
90-
91-
startServer().then((address) => {
92-
console.log('Server is running at "%s"', address);
93-
});
119+
nextTick(() => {
120+
console.log(promise.rejectionReason) // Error("Internal Server Error")
121+
})
94122
```
95123

96-
### `deferredPromise.reject()`
124+
---
97125

98-
Rejects the deferred promise with a given reason.
126+
## Class: `DeferredPromise`
127+
128+
### `new DefferedPromise()`
129+
130+
Creates a new instance of a deferred promise.
99131

100132
```js
101-
function createBroadcast() {
102-
const runtimePromise = new DeferredPromise();
103-
104-
receiver.on("error", (error) => {
105-
// Reject the deferred promise in response
106-
// to the incoming "error" event.
107-
runtimePromise.reject(error);
108-
});
109-
110-
// This deferred promise will be pending forever
111-
// unless the broadcast channel receives the
112-
// "error" event that rejects it.
113-
return runtimePromise;
114-
}
133+
import { DeferredPromise } from '@open-draft/deferred-promise'
134+
135+
const promise = new DeferredPromise()
115136
```
116137

117-
### `deferredPromise.finally()`
138+
A deferred promise is a Promise-compatible class that constructs a regular Promise instance under the hood, controlling it via the [deferred executor](#createdeferredexecutor).
118139

119-
Attaches a callback that executes when the deferred promise is settled (resolved or rejected).
140+
A deferred promise is fully compatible with the regular Promise, both type- and runtime-wise, e.g. a deferred promise can be chained and awaited normally.
120141

121142
```js
122-
const channelReady = new DeferredPromise();
143+
const promise = new DefferredPromise()
144+
.then((value) => value.toUpperCase())
145+
.then((value) => value.substring(0, 2))
146+
.catch((error) => console.error(error))
123147

124-
channelReady.finally(async () => {
125-
// Perform a cleanup side-effect once we're done.
126-
await channel.close();
127-
});
148+
await promise
128149
```
150+
151+
Unlike the regular Promise, however, a deferred promise doesn't accept the `executor` function as the constructor argument. Instead, the resolution of the deferred promise is deferred to the current scope (thus the name).
152+
153+
```js
154+
function getPort() {
155+
// Notice that you don't provide any executor function
156+
// when constructing a deferred promise.
157+
const portPromise = new DeferredPromise()
158+
159+
port.on('open', (port) => {
160+
// Resolve the deferred promise whenever necessary.
161+
portPromise.resolve(port)
162+
})
163+
164+
// Return the deferred promise immediately.
165+
return portPromise
166+
}
167+
```
168+
169+
Use the [`resolve()`](#deferredpromiseresolve) and [`reject()`](#deferredpromisereject) methods of the deferred promise instance to resolve and reject that promise respectively.
170+
171+
### `deferredPromise.state`
172+
173+
See [`DeferredExecutor.state`](#deferredexecutorstate)
174+
175+
### `deferredPromise.resolve()`
176+
177+
See [`DeferredExecutor.resolve()`](#deferredexecutorresolve)
178+
179+
### `deferredPromise.reject()`
180+
181+
See [`DeferredExecutor.reject()`](#deferredexecutorreject)
182+
183+
### `deferredPromise.rejectionReason`
184+
185+
See [`DeferredExecutor.rejectionReason`](#deferredexecutorrejectionreason)

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)