Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support ".finally()" #1

Merged
merged 1 commit into from
Sep 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 53 additions & 34 deletions src/DeferredPromise.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export type DeferredPromiseState = 'pending' | 'resolved' | 'rejected'
export type DeferredPromiseState = "pending" | "resolved" | "rejected";
export type ResolveFunction<Data extends any, Result = void> = (
data: Data
) => Result
export type RejectFunction<Result = void> = (reason?: unknown) => Result
) => Result;
export type RejectFunction<Result = void> = (reason?: unknown) => Result;

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

private promise: Promise<Data>
private promise: Promise<unknown>;

constructor() {
this.promise = new Promise((resolve, reject) => {
this.promise = new Promise<Data>((resolve, reject) => {
this.resolve = (data) => {
if (this.state !== 'pending') {
if (this.state !== "pending") {
throw new TypeError(
`Cannot resolve a DeferredPromise: illegal state ("${this.state}")`
)
);
}

this.state = 'resolved'
this.result = data
resolve(data)
}
this.state = "resolved";
this.result = data;
resolve(data);
};

this.reject = (reason) => {
if (this.state !== 'pending') {
if (this.state !== "pending") {
throw new TypeError(
`Cannot reject a DeferredPromise: illegal state ("${this.state}")`
)
);
}

this.state = 'rejected'
this.rejectionReason = reason
reject(reason)
}
})
this.state = "rejected";
this.rejectionReason = reason;
reject(reason);
};
});

this.state = 'pending'
this.result = undefined
this.rejectionReason = undefined
this.state = "pending";
this.result = undefined;
this.rejectionReason = undefined;
}

public then(onresolved?: ResolveFunction<Data>, onrejected?: RejectFunction) {
this.promise.then(onresolved, onrejected)
return this
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
*/
public then(
onresolved?: ResolveFunction<Data, any>,
onrejected?: RejectFunction
) {
this.promise = this.promise.then(onresolved, onrejected);
return this;
}

/**
* Attaches a callback for only the rejection of the Promise.
*/
public catch<RejectReason = never>(
onrejected?: RejectFunction<RejectReason>
): this {
this.promise.catch<RejectReason>(onrejected)
return this
this.promise = this.promise.catch<RejectReason>(onrejected);
return this;
}

/**
* Attaches a callback that is invoked when
* the Promise is settled (fulfilled or rejected). The resolved
* value cannot be modified from the callback.
*/
public finally(onfinally?: () => void): this {
this.promise = this.promise.finally(onfinally);
return this;
}

static get [Symbol.species]() {
return Promise
return Promise;
}

get [Symbol.toStringTag]() {
return 'DeferredPromise'
return "DeferredPromise";
}
}
179 changes: 106 additions & 73 deletions test/DeferredPromise.test.ts
Original file line number Diff line number Diff line change
@@ -1,103 +1,136 @@
import { DeferredPromise } from '../src'
import { DeferredPromise } from "../src";

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

const promise = new DeferredPromise<number>()
const promise = new DeferredPromise<number>();

promise.then((data) => {
expect(data).toBe(123)
done()
})
expect(data).toBe(123);
done();
});

promise.resolve(123)
})
promise.resolve(123);
});

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

const promise = new DeferredPromise<number>()
const promise = new DeferredPromise<number>();
promise.catch((reason) => {
expect(reason).toBe('error')
done()
})
expect(reason).toBe("error");
done();
});

promise.reject('error')
})
promise.reject("error");
});

it('can be awaited', async () => {
const promise = new DeferredPromise<number>()
promise.resolve(123)
it("can be awaited", async () => {
const promise = new DeferredPromise<number>();
promise.resolve(123);

const data = await promise
expect(data).toBe(123)
})
const data = await promise;
expect(data).toBe(123);
});

describe('resolve()', () => {
it('can be resolved without data', () => {
const promise = new DeferredPromise<void>()
expect(promise.state).toBe('pending')
promise.resolve()
describe("resolve()", () => {
it("can be resolved without data", () => {
const promise = new DeferredPromise<void>();
expect(promise.state).toBe("pending");
promise.resolve();

expect(promise.state).toBe('resolved')
expect(promise.result).toBeUndefined()
})
expect(promise.state).toBe("resolved");
expect(promise.result).toBeUndefined();
});

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

promise.resolve(123)
promise.resolve(123);

expect(promise.state).toBe('resolved')
expect(promise.result).toBe(123)
})
expect(promise.state).toBe("resolved");
expect(promise.result).toBe(123);
});

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

expect(() => promise.resolve(456)).toThrow(
new TypeError(
'Cannot resolve a DeferredPromise: illegal state ("resolved")'
)
)
})
);
});

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

expect(() => promise.resolve(123)).toThrow(
new TypeError(
'Cannot resolve a DeferredPromise: illegal state ("rejected")'
)
)
})
})

describe('reject()', () => {
it('can be rejected without any reason', () => {
const promise = new DeferredPromise<void>().catch(() => {})
expect(promise.state).toBe('pending')
promise.reject()

expect(promise.state).toBe('rejected')
expect(promise.result).toBeUndefined()
expect(promise.rejectionReason).toBeUndefined()
})

it('can be rejected with a reason', () => {
const promise = new DeferredPromise().catch(() => {})
expect(promise.state).toBe('pending')

const rejectionReason = new Error('Something went wrong')
promise.reject(rejectionReason)

expect(promise.state).toBe('rejected')
expect(promise.result).toBeUndefined()
expect(promise.rejectionReason).toEqual(rejectionReason)
})
})
);
});
});

describe("reject()", () => {
it("can be rejected without any reason", () => {
const promise = new DeferredPromise<void>().catch(() => {});
expect(promise.state).toBe("pending");
promise.reject();

expect(promise.state).toBe("rejected");
expect(promise.result).toBeUndefined();
expect(promise.rejectionReason).toBeUndefined();
});

it("can be rejected with a reason", () => {
const promise = new DeferredPromise().catch(() => {});
expect(promise.state).toBe("pending");

const rejectionReason = new Error("Something went wrong");
promise.reject(rejectionReason);

expect(promise.state).toBe("rejected");
expect(promise.result).toBeUndefined();
expect(promise.rejectionReason).toEqual(rejectionReason);
});
});

describe("finally()", () => {
it('executes the "finally" block when the promise resolves', async () => {
const promise = new DeferredPromise<void>();
const finallyCallback = jest.fn();
promise.finally(finallyCallback);

// Promise is still pending.
expect(finallyCallback).not.toHaveBeenCalled();

promise.resolve();
await promise;

expect(finallyCallback).toHaveBeenCalledTimes(1);
expect(finallyCallback).toHaveBeenCalledWith();
});

it('executes the "finally" block when the promise rejects', async () => {
const promise = new DeferredPromise<void>().catch(() => {});

const finallyCallback = jest.fn();
promise.finally(finallyCallback);

// Promise is still pending.
expect(finallyCallback).not.toHaveBeenCalled();

promise.reject();
await promise;

expect(finallyCallback).toHaveBeenCalledTimes(1);
expect(finallyCallback).toHaveBeenCalledWith();
});
});