Skip to content

Commit

Permalink
test(deferral): add tests for deferral
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertFischer committed Dec 8, 2020
1 parent 142c7c9 commit 05f590b
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 137 deletions.
95 changes: 95 additions & 0 deletions src/deferral.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/** @format */

import FunPromise from "./fun-promise";
import Deferral from "./deferral";
import { PromiseState } from "./types";
import _ from "lodash";

describe("Deferral", () => {
it("can be constructed", () => {
const deferral = new Deferral();
expect(deferral).not.toBeNil();
});

describe("promise", () => {
it("is a FunPromise", () => {
const deferral = new Deferral();
expect(deferral.promise).toBeInstanceOf(FunPromise);
});
});

describe("resolve", () => {
it("basically works", async () => {
const deferral = new Deferral();
deferral.resolve(true);
await expect(deferral.resolve(true)).resolves.toBe(true);
});

it("does not change resolved value if called multiple times", async () => {
const deferral = new Deferral();
await expect(deferral.resolve(true)).resolves.toBe(true);
await expect(deferral.resolve(false)).resolves.toBe(true);
});
});

describe("reject", () => {
it("basically works", async () => {
const deferral = new Deferral();
await expect(deferral.reject(new Error("BOOM!"))).rejects.toHaveProperty(
"message",
"BOOM!"
);
});

it("does not change reason if called multiple times", async () => {
const deferral = new Deferral();
await expect(deferral.reject(new Error("BOOM!"))).rejects.toHaveProperty(
"message",
"BOOM!"
);
await expect(deferral.reject(new Error("BANG!"))).rejects.toHaveProperty(
"message",
"BOOM!"
);
});
});

describe("cancellation", () => {
it("is initially not cancelled", () => {
expect(new Deferral()).toHaveProperty("isCancelled", false);
});

it("is cancelled after calling 'cancel'", () => {
const deferral = new Deferral();
deferral.cancel();
expect(deferral).toHaveProperty("isCancelled", true);
});

it("is safe to call 'cancel' multiple times", () => {
const deferral = new Deferral();
deferral.cancel();
deferral.cancel();
expect(deferral).toHaveProperty("isCancelled", true);
});

it("prevents resolve from doing anything", () => {
let sawThen = false;
const deferral = new Deferral();
deferral.promise.then(() => {
sawThen = true;
});
deferral.cancel();
deferral.resolve(true);
expect(sawThen).toBe(false);
});

it("rejects with a known message", async () => {
const deferral = new Deferral();
deferral.cancel();
await expect(deferral.promise).rejects.toHaveProperty(
"message",
"Deferral was cancelled"
);
});
});
});
115 changes: 14 additions & 101 deletions src/deferral.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
/** @format */

import FunPromise from "./fun-promise";
import { PromiseState, Promisable } from "./types";
import _defer from "lodash/defer";
import _noop from "lodash/noop";
import type { Promisable } from "./types";

/**
* A class that is an "inside-out" [[`FunPromise`]]: the `resolve` and `reject` functions
Expand All @@ -20,11 +19,6 @@ export default class Deferral<T> {
*/
readonly promise: FunPromise<T>;

/**
* The state of `promise`.
*/
private stateValue: PromiseState = PromiseState.Pending;

/**
* The function used to resolve [[`promise`]].
*/
Expand All @@ -35,84 +29,14 @@ export default class Deferral<T> {
*/
private rejector: (err: unknown) => Promisable<void> | null = null;

/**
* Provides the state of `promise`.
*/
get state() {
return this.stateValue;
}

/**
* Whether `promise` is in the process of resolving or rejecting.
*/
get isSettling() {
switch (this.stateValue) {
case PromiseState.Resolving:
return true;
case PromiseState.Rejecting:
return true;
default:
return false;
}
}

/**
* Whether `promise` has resolved or rejected.
*/
get isSettled() {
switch (this.stateValue) {
case PromiseState.Resolved:
return true;
case PromiseState.Rejected:
return true;
default:
return false;
}
}

/**
* Whether `promise` has resolved.
*/
get isResolved() {
return this.stateValue === PromiseState.Resolved;
}

/**
* Whether `promise` was rejected.
*/
get isRejected() {
return this.stateValue === PromiseState.Rejected;
}

/**
* Resolves `promise` with the given value.
*/
resolve(it) {
const { resolver } = this;
if (resolver) {
try {
const { rejector } = this;
this.stateValue = PromiseState.Resolving;
_defer(() => {
try {
resolver(it);
this.stateValue = PromiseState.Resolved;
} catch (e) {
if (rejector) {
this.rejector = rejector;
this.reject(e);
} else {
console.warn(`Uncaught exception during resolution`, e);
}
}
});
} catch (e) {
this.reject(e);
} finally {
this.resolver = null;
this.rejector = null;
}
}
this.resolver = null;
this.rejector = null;
if (resolver) resolver(it);
return this.promise;
}

Expand All @@ -121,21 +45,9 @@ export default class Deferral<T> {
*/
reject(e: Error) {
const { rejector } = this;
if (rejector) {
try {
this.stateValue = PromiseState.Rejecting;
_defer(() => {
try {
rejector(e);
} finally {
this.stateValue = PromiseState.Rejected;
}
});
} finally {
this.resolver = null;
this.rejector = null;
}
}
this.resolver = null;
this.rejector = null;
if (rejector) rejector(e);
return this.promise;
}

Expand All @@ -156,18 +68,19 @@ export default class Deferral<T> {
* Whether or not the deferral is cancelled.
*/
get isCancelled() {
return !this.isSettled && this.resolver === null && this.resolver === null;
return this.resolver === null;
}

/**
* Cancels the deferral. If the deferral is not settled, its callbacks will
* never be called. If the deferral is settled or cancelled, this is a noop.
*/
cancel() {
if (this.isSettled) return;
this.stateValue = PromiseState.Cancelled;
this.resolver = null;
this.rejector = null;
this.promise.catch(_noop); // Suppress "UnhandledException" errors.
if (!this.isCancelled) {
this.promise.catch(_noop); // Suppress "UnhandledException" errors.
this.reject(new Error(`Deferral was cancelled`));
this.resolver = null;
this.rejector = null;
}
}
}
36 changes: 0 additions & 36 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,39 +59,3 @@ export type PromisableIterable<T> = Promisable<IterableOfPromisables<T>>;
* although it may produce either values or promises of values or both.
*/
export type IterableOfPromisables<T> = Iterable<Promisable<T>>;

/**
* The various states that a promise can be in.
*/
export enum PromiseState {
/**
* The promise is neither resolved nor rejected.
*/
Pending = "pending",

/**
* The promise has begun resolving, but is not yet fully resolved.
*/
Resolving = "resolving",

/**
* The promise has resolved to a value.
*/
Resolved = "resolved",

/**
* The promise has begun rejecting, but is not yet fully rejected.
*/
Rejecting = "rejecting",

/**
* The promise has rejected with a cause.
*/
Rejected = "rejected",

/**
* The promise has been cancelled, which will prevent its
* callbacks from firing.
*/
Cancelled = "cancelled",
}

0 comments on commit 05f590b

Please sign in to comment.