@open-draft/deferred-promise
Advanced tools
Comparing version
export declare type PromiseState = 'pending' | 'fulfilled' | 'rejected'; | ||
export declare type ResolveFunction<Data extends any, Result = void> = (value: Data) => Result | PromiseLike<Result>; | ||
export declare type RejectFunction<Result = unknown> = (reason?: unknown) => Result | PromiseLike<Result>; | ||
export declare type DeferredPromiseExecutor<Input = void, Output = Input> = { | ||
(resolve?: ResolveFunction<any, unknown>, reject?: RejectFunction): void; | ||
resolve: ResolveFunction<Input, Output | void>; | ||
reject: RejectFunction; | ||
result?: Output | Input; | ||
export declare type Executor<Value> = ConstructorParameters<typeof Promise<Value>>[0]; | ||
export declare type ResolveFunction<Value> = Parameters<Executor<Value>>[0]; | ||
export declare type RejectFunction<Reason> = Parameters<Executor<Reason>>[1]; | ||
export declare type DeferredPromiseExecutor<Input = never, Output = Input> = { | ||
(resolve?: ResolveFunction<Input>, reject?: RejectFunction<any>): void; | ||
resolve: ResolveFunction<Input>; | ||
reject: RejectFunction<any>; | ||
result?: Output; | ||
state: PromiseState; | ||
@@ -10,0 +11,0 @@ rejectionReason?: unknown; |
@@ -8,18 +8,20 @@ "use strict"; | ||
executor.resolve = (data) => { | ||
queueMicrotask(() => { | ||
if (executor.state === 'pending') { | ||
executor.state = 'fulfilled'; | ||
executor.result = data; | ||
resolve(data); | ||
} | ||
}); | ||
if (executor.state !== 'pending') { | ||
return; | ||
} | ||
executor.result = data; | ||
const onFulfilled = (value) => { | ||
executor.state = 'fulfilled'; | ||
return value; | ||
}; | ||
return resolve(data instanceof Promise ? data : Promise.resolve(data).then(onFulfilled)); | ||
}; | ||
executor.reject = (reason) => { | ||
if (executor.state !== 'pending') { | ||
return; | ||
} | ||
queueMicrotask(() => { | ||
if (executor.state === 'pending') { | ||
executor.state = 'rejected'; | ||
executor.rejectionReason = reason; | ||
reject(reason); | ||
} | ||
executor.state = 'rejected'; | ||
}); | ||
return reject((executor.rejectionReason = reason)); | ||
}; | ||
@@ -26,0 +28,0 @@ }); |
@@ -1,23 +0,12 @@ | ||
import { PromiseState, ResolveFunction, RejectFunction } from './createDeferredExecutor'; | ||
/** | ||
* @example | ||
* function getPort() { | ||
* const portReadyPromise = new DeferredPromise<number>() | ||
* port.on('ready', (port) => portReadyPromise.resolve(port)) | ||
* port.on('error', (error) => portReadyPromise.reject(error)) | ||
* returtn portReadyPromise | ||
* } | ||
*/ | ||
export declare class DeferredPromise<Input = never, Output = Input> { | ||
import { type Executor, type RejectFunction, type ResolveFunction } from './createDeferredExecutor'; | ||
export declare class DeferredPromise<Input, Output = Input> extends Promise<Input> { | ||
#private; | ||
resolve: (value: Input) => void; | ||
reject: (reason?: unknown) => void; | ||
constructor(); | ||
static get [Symbol.species](): PromiseConstructor; | ||
get [Symbol.toStringTag](): string; | ||
get state(): PromiseState; | ||
resolve: ResolveFunction<Output>; | ||
reject: RejectFunction<Output>; | ||
constructor(executor?: Executor<Input> | null); | ||
get state(): import("./createDeferredExecutor").PromiseState; | ||
get rejectionReason(): unknown; | ||
then<FulfillmentResult = Output, RejectionResult = never>(onFulfilled?: ResolveFunction<Output, FulfillmentResult> | null, onRejected?: RejectFunction<RejectionResult> | null): DeferredPromise<Input, FulfillmentResult | RejectionResult>; | ||
catch<Result>(onRejected?: RejectFunction<Result>): DeferredPromise<Input, Output | Result>; | ||
finally(onSettled?: (() => void) | null): DeferredPromise<Input, Output>; | ||
then<ThenResult = Input, CatchResult = never>(onFulfilled?: (value: Input) => ThenResult | PromiseLike<ThenResult>, onRejected?: (reason: any) => CatchResult | PromiseLike<CatchResult>): DeferredPromise<ThenResult | CatchResult, Output>; | ||
catch<CatchResult = never>(onRejected?: (reason: any) => CatchResult | PromiseLike<CatchResult>): DeferredPromise<Input | CatchResult, Output>; | ||
finally(onfinally?: () => void | Promise<any>): DeferredPromise<Input, Output>; | ||
} |
@@ -5,28 +5,16 @@ "use strict"; | ||
const createDeferredExecutor_1 = require("./createDeferredExecutor"); | ||
/** | ||
* @example | ||
* function getPort() { | ||
* const portReadyPromise = new DeferredPromise<number>() | ||
* port.on('ready', (port) => portReadyPromise.resolve(port)) | ||
* port.on('error', (error) => portReadyPromise.reject(error)) | ||
* returtn portReadyPromise | ||
* } | ||
*/ | ||
class DeferredPromise { | ||
#promise; | ||
class DeferredPromise extends Promise { | ||
#executor; | ||
resolve; | ||
reject; | ||
constructor() { | ||
this.#executor = (0, createDeferredExecutor_1.createDeferredExecutor)(); | ||
this.#promise = new Promise(this.#executor); | ||
constructor(executor = null) { | ||
const deferredExecutor = (0, createDeferredExecutor_1.createDeferredExecutor)(); | ||
super((originalResolve, originalReject) => { | ||
deferredExecutor(originalResolve, originalReject); | ||
executor?.(deferredExecutor.resolve, deferredExecutor.reject); | ||
}); | ||
this.#executor = deferredExecutor; | ||
this.resolve = this.#executor.resolve; | ||
this.reject = this.#executor.reject; | ||
} | ||
static get [Symbol.species]() { | ||
return Promise; | ||
} | ||
get [Symbol.toStringTag]() { | ||
return 'DeferredPromise'; | ||
} | ||
get state() { | ||
@@ -39,89 +27,14 @@ return this.#executor.state; | ||
then(onFulfilled, onRejected) { | ||
const derivedPromise = new DeferredPromise(); | ||
const resolveDerivedPromise = (result) => { | ||
if (typeof onFulfilled === 'function') { | ||
try { | ||
const nextResult = onFulfilled(result); | ||
derivedPromise.#executor.resolve(nextResult); | ||
} | ||
catch (error) { | ||
rejectDerivedPromise(error); | ||
} | ||
return; | ||
} | ||
/** | ||
* @note Use the executor directly because the "resolve" method | ||
* always gets overridden to point to the previous Promise in the chain. | ||
*/ | ||
derivedPromise.#executor.resolve(result); | ||
}; | ||
const rejectDerivedPromise = (result) => { | ||
if (typeof onRejected === 'function') { | ||
try { | ||
const nextReason = onRejected(result); | ||
/** | ||
* @note If the rejection has a handler callback ("catch"), | ||
* resolve the chained promise instead of rejecting it. | ||
* The "catch" callback is there precisely to prevent | ||
* uncaught promise rejection errors. | ||
*/ | ||
derivedPromise.#executor.resolve(nextReason); | ||
/** | ||
* @note Since the handled rejected promise was resolved internally, | ||
* still mark its state as rejected. Do so in the next microtask | ||
* because executor rejects the promise in the next task as well | ||
*/ | ||
queueMicrotask(() => { | ||
derivedPromise.#executor.state = 'rejected'; | ||
derivedPromise.#executor.rejectionReason = nextReason; | ||
}); | ||
} | ||
catch (error) { | ||
rejectDerivedPromise(error); | ||
} | ||
return; | ||
} | ||
derivedPromise.#executor.reject(result); | ||
}; | ||
switch (this.state) { | ||
case 'pending': { | ||
/** | ||
* @note Override the chainable "resolve" so that when called | ||
* it executes the resolve of the Promise from which it was | ||
* chained from. | ||
* | ||
* @example | ||
* const p1 = new DeferredPromise().then(A).then(B) | ||
* p1.resolve() | ||
* | ||
* Here, "p1" points to the rightmost promise (B) but the | ||
* order of resolving the value must be left-to-right: | ||
* Initial -> A -> B | ||
*/ | ||
derivedPromise.resolve = this.resolve; | ||
derivedPromise.reject = this.reject; | ||
this.#promise.then(resolveDerivedPromise, rejectDerivedPromise); | ||
break; | ||
} | ||
case 'fulfilled': { | ||
resolveDerivedPromise(this.#executor.result); | ||
break; | ||
} | ||
case 'rejected': { | ||
rejectDerivedPromise(this.#executor.rejectionReason); | ||
break; | ||
} | ||
} | ||
return derivedPromise; | ||
return this.#decorate(super.then(onFulfilled, onRejected)); | ||
} | ||
catch(onRejected) { | ||
return this.then(undefined, onRejected); | ||
return this.#decorate(super.catch(onRejected)); | ||
} | ||
finally(onSettled) { | ||
return this.then((result) => { | ||
onSettled?.(); | ||
return result; | ||
}, (reason) => { | ||
onSettled?.(); | ||
throw reason; | ||
finally(onfinally) { | ||
return this.#decorate(super.finally(onfinally)); | ||
} | ||
#decorate(promise) { | ||
return Object.defineProperties(promise, { | ||
resolve: { configurable: true, value: this.resolve }, | ||
reject: { configurable: true, value: this.reject }, | ||
}); | ||
@@ -128,0 +41,0 @@ } |
{ | ||
"name": "@open-draft/deferred-promise", | ||
"version": "2.0.0", | ||
"description": "A Promise that can be resolved/rejected elsewhere", | ||
"version": "2.1.0", | ||
"description": "A Promise-compatible abstraction that defers resolving/rejecting promises to another closure.", | ||
"main": "./build/index.js", | ||
@@ -9,2 +9,3 @@ "typings": "./build/index.d.ts", | ||
"test": "jest", | ||
"test:compliance": "export NODE_OPTIONS=--loader=tsx || set NODE_OPTIONS=--loader=tsx&& npx -y promises-aplus-tests ./test/aplus-tests-adapter.ts", | ||
"prebuild": "rimraf ./build", | ||
@@ -22,3 +23,4 @@ "build": "tsc", | ||
"resolve", | ||
"reject" | ||
"reject", | ||
"executor" | ||
], | ||
@@ -33,4 +35,5 @@ "author": "Artem Zakharchenko", | ||
"ts-jest": "^29.0.0", | ||
"tsx": "^3.12.1", | ||
"typescript": "^4.8.3" | ||
} | ||
} |
@@ -186,1 +186,7 @@ # Deferred Promise | ||
See [`DeferredExecutor.rejectionReason`](#deferredexecutorrejectionreason) | ||
--- | ||
## Mentions | ||
- [Jonas Kuske](https://github.com/jonaskuske) for the phenomenal work around improving Promise-compliance. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
24932
32.6%14
27.27%276
30.81%192
3.23%7
16.67%