Skip to content

Commit

Permalink
refactor PromiseChain to CallOrConstruct pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
myty committed Sep 24, 2022
1 parent de59b82 commit dae9bc0
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 46 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions promise-chain.bench.ts
Original file line number Diff line number Diff line change
@@ -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 = (
Expand All @@ -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(
Expand All @@ -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,
)
Expand All @@ -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()
Expand Down
21 changes: 11 additions & 10 deletions promise-chain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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()
Expand All @@ -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;
Expand All @@ -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() {
Expand All @@ -97,7 +98,7 @@ Deno.test(async function whenPromiseChainHasExceptionItIsCaught() {
const testClassWithException = new TestClassWithException();

// Act
await chain(testClassWithException)
await PromiseChain(testClassWithException)
.throwException()
.catch(catchSpy);

Expand All @@ -111,7 +112,7 @@ Deno.test(async function whenPromiseChainPromiseIsFinalized() {
const testClass = new TestClass();

// Act
await chain(testClass)
await PromiseChain(testClass)
.asyncIncrementOne()
.finally(finallySpy);

Expand Down
49 changes: 21 additions & 28 deletions promise-chain.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
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<T> extends Promise<T> implements Promise<T> {
private _valuePromise: Promise<T>;
const PromiseChain = function <T>(this: PromiseChainable<T> | void, obj: T) {
if (!(this instanceof PromiseChain)) {
return new PromiseChain(obj);
} else {
const self = this as unknown as { _valuePromise: Promise<T> } & Promise<T>;

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;
}

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)
);

Expand All @@ -33,24 +34,16 @@ export default class PromiseChain<T> extends Promise<T> implements Promise<T> {
});
});
}
} as PromiseChainableConstructor;

// Private static methods
function keysOfObject<T>(obj: T): Array<keyof T> {
const proto = Object.getPrototypeOf(obj);

private static keysOfObject<T>(obj: T): Array<keyof T> {
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<keyof T>;
}
return keys as Array<keyof T>;
}

export function chain<T>(wrappedObj: T) {
return new PromiseChain(wrappedObj) as unknown as
& PromiseChain<T>
& AsyncComposable<T>;
}
export default PromiseChain;
10 changes: 7 additions & 3 deletions types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// deno-lint-ignore-file ban-types

// https://stackoverflow.com/a/57044690/203857
export type KeysMatching<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never;
}[keyof T];
Expand All @@ -10,10 +9,15 @@ export type PickMatching<T, V> = Pick<T, KeysMatching<T, V>>;
export type PickFunctionsThatReturnSelf<TType> = {
[TKey in keyof PickMatching<TType, Function>]: TType[TKey] extends (
...args: infer TParams
) => TType | Promise<TType> ? (...args: TParams) => AsyncComposable<TType>
) => TType | Promise<TType> ? (...args: TParams) => PromiseChainable<TType>
: never;
};

export type AsyncComposable<TType> =
export type PromiseChainable<TType> =
& PickFunctionsThatReturnSelf<TType>
& Promise<TType>;

export interface PromiseChainableConstructor {
new <T>(obj: T): PromiseChainable<T>;
<T>(obj: T): PromiseChainable<T>;
}

0 comments on commit dae9bc0

Please sign in to comment.