Move cancelable promises to new file and add tests

Signed-off-by: Mcat12 <newtoncat12@yahoo.com>
This commit is contained in:
Mcat12
2019-04-09 20:55:02 -07:00
parent cf4affc230
commit fde34be4a3
10 changed files with 232 additions and 86 deletions
+4 -1
View File
@@ -15,7 +15,10 @@ import { WithNamespaces, withNamespaces } from "react-i18next";
import NavButton from "./NavButton";
import NavDropdown from "./NavDropdown";
import { StatusContext } from "./context/StatusContext";
import { CancelablePromise, makeCancelable } from "../../util";
import {
CancelablePromise,
makeCancelable
} from "../../util/CancelablePromise";
import api from "../../util/api";
import {
Button,
+5 -1
View File
@@ -9,7 +9,11 @@
* Please see LICENSE file for your rights under this license. */
import { Component, ReactNode } from "react";
import { CancelablePromise, ignoreCancel, makeCancelable } from "../../util";
import {
CancelablePromise,
ignoreCancel,
makeCancelable
} from "../../util/CancelablePromise";
import { Err, Ok, Result } from "../../util/result";
export interface WithAPIDataProps<T> {
+5 -1
View File
@@ -13,7 +13,11 @@ import { WithNamespaces, withNamespaces } from "react-i18next";
import DomainInput from "./DomainInput";
import Alert, { AlertType } from "../common/Alert";
import DomainList from "./DomainList";
import { CancelablePromise, ignoreCancel, makeCancelable } from "../../util";
import {
CancelablePromise,
ignoreCancel,
makeCancelable
} from "../../util/CancelablePromise";
export interface ListPageProps extends WithNamespaces {
title: string;
+6 -6
View File
@@ -20,18 +20,18 @@ import i18next from "i18next";
import { WithNamespaces, withNamespaces } from "react-i18next";
import debounce from "lodash.debounce";
import moment from "moment";
import {
CancelablePromise,
ignoreCancel,
makeCancelable,
padNumber
} from "../../util";
import { padNumber } from "../../util";
import api from "../../util/api";
import { dateRanges } from "../../util/dateRanges";
import { TranslatedTimeRangeSelector } from "../dashboard/TimeRangeSelector";
import { TimeRange } from "../common/context/TimeRangeContext";
import "react-table/react-table.css";
import "bootstrap-daterangepicker/daterangepicker.css";
import {
CancelablePromise,
ignoreCancel,
makeCancelable
} from "../../util/CancelablePromise";
export interface QueryLogState {
history: Array<ApiQuery>;
+5 -1
View File
@@ -10,7 +10,11 @@
import React, { ChangeEvent, Component, FormEvent } from "react";
import { WithNamespaces, withNamespaces } from "react-i18next";
import { CancelablePromise, ignoreCancel, makeCancelable } from "../../util";
import {
CancelablePromise,
ignoreCancel,
makeCancelable
} from "../../util/CancelablePromise";
import api from "../../util/api";
import {
Button,
+5 -1
View File
@@ -10,7 +10,11 @@
import React, { Component, FormEvent } from "react";
import { WithNamespaces, withNamespaces } from "react-i18next";
import { CancelablePromise, ignoreCancel, makeCancelable } from "../../util";
import {
CancelablePromise,
ignoreCancel,
makeCancelable
} from "../../util/CancelablePromise";
import api from "../../util/api";
import DnsList from "./DnsList";
import { Button, Col, Form, FormGroup } from "reactstrap";
@@ -10,7 +10,11 @@
import React, { ChangeEvent, Component, FormEvent } from "react";
import { WithNamespaces, withNamespaces } from "react-i18next";
import { CancelablePromise, ignoreCancel, makeCancelable } from "../../util";
import {
CancelablePromise,
ignoreCancel,
makeCancelable
} from "../../util/CancelablePromise";
import api from "../../util/api";
import Alert, { AlertType } from "../common/Alert";
import { Button, Col, Form, FormGroup, Input, Label } from "reactstrap";
+95
View File
@@ -0,0 +1,95 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* Wrap promises to make them cancelable
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
/**
* A promise which can be canceled
*/
export interface CancelablePromise<T> {
promise: Promise<T>;
cancel: () => void;
}
/**
* The options given to {@link makeCancelable}
*/
export interface CancelableOptions {
/**
* The function to call to repeat the promise
*/
repeat: () => void;
/**
* The amount of time to wait until repeating
*/
interval: number;
}
/**
* Make a promise cancelable and repeatable
*
* @param promise the promise
* @param options the interval repeat options
* @returns a cancelable promise
*/
export function makeCancelable<T>(
promise: Promise<T>,
options?: CancelableOptions
): CancelablePromise<T> {
let hasCanceled = false;
let repeatId: NodeJS.Timeout | null = null;
const handle = (
resolve: (value: T) => void,
reject: (error: any) => void,
val: T,
isError: boolean
) => {
if (hasCanceled) {
reject({ isCanceled: true });
return;
}
if (isError) {
reject(val);
} else {
resolve(val);
}
if (options) {
repeatId = setTimeout(options.repeat, options.interval);
}
};
const wrappedPromise: Promise<T> = new Promise((resolve, reject) => {
promise.then(
val => handle(resolve, reject, val, false),
error => handle(resolve, reject, error, true)
);
});
return {
promise: wrappedPromise,
cancel() {
if (repeatId !== null) {
clearTimeout(repeatId);
}
hasCanceled = true;
}
};
}
/**
* Ignore canceled promises (pass into a promise's catch function)
*
* @param err the error from catching the promise
*/
export const ignoreCancel = (err: any) => {
if (!err.isCanceled) throw err;
};
@@ -0,0 +1,102 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* Tests for canceling promises
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import { makeCancelable, ignoreCancel } from "../CancelablePromise";
describe("makeCancelable", () => {
const testValue = "test";
const testError = "testError";
const promise = Promise.resolve(testValue);
const promiseErr = Promise.reject(testError);
it("passes through promises in the default case", async () => {
const cancelablePromise = makeCancelable(promise);
await expect(cancelablePromise.promise).resolves.toEqual(testValue);
});
it("passes through errors in the default case", async () => {
const cancelablePromise = makeCancelable(promiseErr);
await expect(cancelablePromise.promise).rejects.toEqual(testError);
});
it("rejects with cancel error if canceled", async () => {
const cancelablePromise = makeCancelable(promise);
cancelablePromise.cancel();
await expect(cancelablePromise.promise).rejects.toEqual({
isCanceled: true
});
});
it("calls the repeat function after resolving", async () => {
jest.useFakeTimers();
const mockFunction = jest.fn();
const interval = 1000;
const cancelablePromise = makeCancelable(promise, {
interval,
repeat: mockFunction
});
await expect(cancelablePromise.promise).resolves;
expect(setTimeout).toHaveBeenCalledWith(mockFunction, interval);
});
it("calls the repeat function after rejecting", async () => {
jest.useFakeTimers();
const mockFunction = jest.fn();
const interval = 1000;
const cancelablePromise = makeCancelable(promiseErr, {
interval,
repeat: mockFunction
});
await expect(cancelablePromise.promise).rejects.toEqual(testError);
expect(setTimeout).toHaveBeenCalledWith(mockFunction, interval);
});
it("clears the timeout if canceled after resolving", async () => {
jest.useFakeTimers();
const mockFunction = jest.fn();
const interval = 1000;
const cancelablePromise = makeCancelable(promise, {
interval,
repeat: mockFunction
});
await cancelablePromise.promise;
cancelablePromise.cancel();
expect(clearTimeout).toHaveBeenCalled();
});
});
describe("ignoreCancel", () => {
it("passes through non-canceled errors", async () => {
const testError = "test";
const promise = Promise.reject(testError);
await expect(promise.catch(ignoreCancel)).rejects.toEqual(testError);
});
it("does not pass through canceled errors", async () => {
const canceledPromise = Promise.reject({ isCanceled: true });
await expect(canceledPromise.catch(ignoreCancel)).resolves;
});
});
-74
View File
@@ -29,77 +29,3 @@ export const padNumber = (num: number) => {
export const getIntervalForRange = (range: TimeRange): number => {
return Math.ceil((range.until.unix() - range.from.unix()) / 144);
};
export interface CancelablePromise<T> {
promise: Promise<T>;
cancel: () => void;
}
export interface CancelableOptions {
/**
* The function to call to repeat the promise
*/
repeat: () => void;
/**
* The amount of time to wait until repeating
*/
interval: number;
}
/**
* Make a promise cancelable and repeatable
*
* @param promise the promise
* @param options the interval repeat options
* @returns {{promise: Promise<T>, cancel(): void}} a handle on the cancelable
* promise
*/
export function makeCancelable<T>(
promise: Promise<T>,
options?: CancelableOptions
): CancelablePromise<T> {
let hasCanceled = false;
let repeatId: NodeJS.Timeout | null = null;
const handle = (
resolve: (value: any) => void,
reject: (error: any) => void,
val: T,
isError: boolean
) => {
if (hasCanceled) reject({ isCanceled: true });
else {
if (isError) reject(val);
else resolve(val);
if (options) repeatId = setTimeout(options.repeat, options.interval);
}
};
const wrappedPromise: Promise<T> = new Promise((resolve, reject) => {
promise.then(
val => handle(resolve, reject, val, false),
error => handle(resolve, reject, error, true)
);
});
return {
promise: wrappedPromise,
cancel() {
if (repeatId !== null) {
clearTimeout(repeatId);
}
hasCanceled = true;
}
};
}
/**
* Ignore canceled promises (pass into a promise's catch function)
*
* @param err the error from catching the promise
*/
export const ignoreCancel = (err: any) => {
if (!err.isCanceled) throw err;
};