diff --git a/README.md b/README.md index 2476ada..a2388b7 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ console.log(`Result: propertyOne=${propertyOne}, propertyTwo=${propertyTwo}`); With PromiseChain, it is simplified and easier to read. ```typescript -const { propertyOne, propertyTwo } = await chain(testClass) +const { propertyOne, propertyTwo } = await PromiseChain(testClass) .asyncIncrement("propertyOne", 3) .asyncIncrement("propertyTwo", 5) .increment("propertyTwo", 5); diff --git a/promise-chain.bench.ts b/promise-chain.bench.ts index 8f140aa..c08ea57 100644 --- a/promise-chain.bench.ts +++ b/promise-chain.bench.ts @@ -1,4 +1,4 @@ -import { chain } from "./promise-chain.ts"; +import PromiseChain from "./promise-chain.ts"; import { TestClass } from "./stubs/test-class.ts"; const iterate = ( @@ -18,7 +18,7 @@ Deno.bench( Deno.bench( "Composable Async Chain (1 Step)", { group: "1 step" }, - iterate((t) => chain(t).asyncIncrement("propertyOne", 3)), + iterate((t) => PromiseChain(t).asyncIncrement("propertyOne", 3)), ); Deno.bench( @@ -35,7 +35,7 @@ Deno.bench( "Composable Async Chain (2 Steps)", { group: "2 steps" }, iterate((t) => - chain(t).asyncIncrement("propertyOne", 3).increment( + PromiseChain(t).asyncIncrement("propertyOne", 3).increment( "propertyTwo", 5, ) @@ -60,7 +60,7 @@ Deno.bench( "Composable Async Chain (6 Steps)", { group: "6 steps" }, iterate((t) => - chain(t) + PromiseChain(t) .asyncIncrement("propertyOne", 3) .asyncIncrementTwo() .asyncIncrementOne() diff --git a/promise-chain.test.ts b/promise-chain.test.ts index b7ddd38..66e8d95 100644 --- a/promise-chain.test.ts +++ b/promise-chain.test.ts @@ -7,7 +7,7 @@ import { assertSpyCalls, spy, } from "https://deno.land/std@0.157.0/testing/mock.ts"; -import PromiseChain, { chain } from "./promise-chain.ts"; +import PromiseChain from "./promise-chain.ts"; import { TestClassWithException } from "./stubs/test-class-with-exceptions.ts"; import { TestClass } from "./stubs/test-class.ts"; @@ -34,7 +34,7 @@ Deno.test(async function whenAsyncChainingItReturnsResult() { const testClass = new TestClass(); // Act - const result = await chain(testClass) + const result = await PromiseChain(testClass) .asyncIncrement("propertyOne", 3) .asyncIncrementTwo() .asyncIncrementOne() @@ -47,23 +47,24 @@ Deno.test(async function whenAsyncChainingItReturnsResult() { assertEquals(result.propertyTwo, 6); }); -Deno.test(function whenComposableAsyncItIsPromise() { +Deno.test(function whenComposableAsyncItIsPromiseLike() { // Arrange const testClass = new TestClass(); // Act - const result = chain(testClass); + const result = PromiseChain(testClass); // Assert - assert(result instanceof PromiseChain); - assert(result instanceof Promise); + assert("then" in result && typeof result.then === "function"); + assert("catch" in result && typeof result.catch === "function"); + assert("finally" in result && typeof result.finally === "function"); }); Deno.test(async function whenChainedPromiseIsReusedItReturnsCachedResult() { // Arrange const testClass = new TestClass(); const durationExpectedMs = 250; - const resultTask = chain(testClass) + const resultTask = PromiseChain(testClass) .asyncIncrement("propertyTwo", 3) .asyncIncrementOneLongRunningTask(durationExpectedMs); await resultTask; @@ -88,7 +89,7 @@ Deno.test(function whenPromiseChainHasExceptionItIsRejected() { const testClassWithException = new TestClassWithException(); // Act, Assert - assertRejects(() => chain(testClassWithException).throwException()); + assertRejects(() => PromiseChain(testClassWithException).throwException()); }); Deno.test(async function whenPromiseChainHasExceptionItIsCaught() { @@ -97,7 +98,7 @@ Deno.test(async function whenPromiseChainHasExceptionItIsCaught() { const testClassWithException = new TestClassWithException(); // Act - await chain(testClassWithException) + await PromiseChain(testClassWithException) .throwException() .catch(catchSpy); @@ -111,7 +112,7 @@ Deno.test(async function whenPromiseChainPromiseIsFinalized() { const testClass = new TestClass(); // Act - await chain(testClass) + await PromiseChain(testClass) .asyncIncrementOne() .finally(finallySpy); diff --git a/promise-chain.ts b/promise-chain.ts index 40baa6f..1c03e4f 100644 --- a/promise-chain.ts +++ b/promise-chain.ts @@ -1,22 +1,23 @@ -import { AsyncComposable } from "./types.ts"; +import { PromiseChainable, PromiseChainableConstructor } from "./types.ts"; /** * Utility class to wrap a composition class with the intended purpose of chaining methods, specifically useful for * functions that return Promises. Note: Promise functions and non-promise functions can be mixed. */ -export default class PromiseChain extends Promise implements Promise { - private _valuePromise: Promise; +const PromiseChain = function (this: PromiseChainable | void, obj: T) { + if (!(this instanceof PromiseChain)) { + return new PromiseChain(obj); + } else { + const self = this as unknown as { _valuePromise: Promise } & Promise; - constructor(_wrappedClass: T) { - super((_resolve, _reject) => {}); + self._valuePromise = Promise.resolve(obj); - this._valuePromise = Promise.resolve(_wrappedClass); - this.then = (...args) => this._valuePromise.then(...args); - this.catch = (...args) => this._valuePromise.catch(...args); - this.finally = (...args) => this._valuePromise.finally(...args); + this.then = (...args) => self._valuePromise.then(...args); + this.catch = (...args) => self._valuePromise.catch(...args); + this.finally = (...args) => self._valuePromise.finally(...args); - PromiseChain.keysOfObject(_wrappedClass).forEach((key) => { - const callableFunc = _wrappedClass[key]; + keysOfObject(obj).forEach((key) => { + const callableFunc = obj[key]; if (!(callableFunc instanceof Function)) { return; @@ -24,7 +25,7 @@ export default class PromiseChain extends Promise implements Promise { Object.defineProperty(this, key, { value: (...args: unknown[]) => { - this._valuePromise = this._valuePromise.then((val: T) => + self._valuePromise = self._valuePromise.then((val: T) => callableFunc.apply(val, args) ); @@ -33,24 +34,16 @@ export default class PromiseChain extends Promise implements Promise { }); }); } +} as PromiseChainableConstructor; - // Private static methods +function keysOfObject(obj: T): Array { + const proto = Object.getPrototypeOf(obj); - private static keysOfObject(obj: T): Array { - const proto = Object.getPrototypeOf(obj); + const keys = Object.keys(obj).concat( + Object.getOwnPropertyNames(proto).filter((name) => name !== "constructor"), + ); - const keys = Object.keys(obj).concat( - Object.getOwnPropertyNames(proto).filter((name) => - name !== "constructor" - ), - ); - - return keys as Array; - } + return keys as Array; } -export function chain(wrappedObj: T) { - return new PromiseChain(wrappedObj) as unknown as - & PromiseChain - & AsyncComposable; -} +export default PromiseChain; diff --git a/types.ts b/types.ts index 9eec297..f1d8c44 100644 --- a/types.ts +++ b/types.ts @@ -1,6 +1,5 @@ // deno-lint-ignore-file ban-types -// https://stackoverflow.com/a/57044690/203857 export type KeysMatching = { [K in keyof T]-?: T[K] extends V ? K : never; }[keyof T]; @@ -10,10 +9,15 @@ export type PickMatching = Pick>; export type PickFunctionsThatReturnSelf = { [TKey in keyof PickMatching]: TType[TKey] extends ( ...args: infer TParams - ) => TType | Promise ? (...args: TParams) => AsyncComposable + ) => TType | Promise ? (...args: TParams) => PromiseChainable : never; }; -export type AsyncComposable = +export type PromiseChainable = & PickFunctionsThatReturnSelf & Promise; + +export interface PromiseChainableConstructor { + new (obj: T): PromiseChainable; + (obj: T): PromiseChainable; +}