diff --git a/.eslintrc.json b/.eslintrc.json index d20d532..eb19536 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,101 +1,3 @@ { - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking", - "prettier/@typescript-eslint", - "plugin:import/errors", - "plugin:import/warnings", - "plugin:import/typescript", - "plugin:promise/recommended", - "plugin:prettier/recommended" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module", - "project": "./tsconfig.json" - }, - "plugins": ["@typescript-eslint/eslint-plugin", "import", "promise"], - "rules": { - "no-console": ["error"], - "no-extra-parens": ["error", "all", { "enforceForArrowConditionals": false }], - "no-import-assign": ["error"], - "no-template-curly-in-string": ["error"], - "curly": ["error", "all"], - "eqeqeq": ["error"], - "max-classes-per-file": ["error", 1], - "no-alert": ["error"], - "no-caller": ["error"], - "no-div-regex": ["error"], - "no-else-return": ["error", { "allowElseIf": false }], - "no-eq-null": ["error"], - "no-eval": ["error"], - "no-extend-native": ["error"], - "no-extra-bind": ["error"], - "no-extra-label": ["error"], - "no-floating-decimal": ["error"], - "no-implicit-coercion": ["error"], - "no-implicit-globals": ["error"], - "no-implied-eval": ["error"], - "no-invalid-this": ["error"], - "no-iterator": ["error"], - "no-labels": ["error"], - "no-lone-blocks": ["error"], - "no-loop-func": ["error"], - "no-multi-spaces": ["error"], - "no-multi-str": ["error"], - "no-new": ["error"], - "no-new-func": ["error"], - "no-new-wrappers": ["error"], - "no-octal-escape": ["error"], - "no-param-reassign": ["error"], - "no-proto": ["error"], - "no-return-assign": ["error", "always"], - "no-return-await": ["error"], - "no-script-url": ["error"], - "no-self-compare": ["error"], - "no-sequences": ["error"], - "no-throw-literal": ["error"], - "no-unmodified-loop-condition": ["error"], - "no-useless-call": ["error"], - "no-useless-concat": ["error"], - "no-useless-return": ["error"], - "prefer-named-capture-group": ["error"], - "prefer-promise-reject-errors": ["error"], - "prefer-regex-literals": ["error"], - "radix": ["error", "always"], - "require-unicode-regexp": ["error"], - "wrap-iife": ["error", "inside"], - "yoda": ["error", "never"], - "array-bracket-spacing": ["error", "never"], - "brace-style": ["error"], - "camelcase": ["error"], - "func-call-spacing": ["error"], - "no-lonely-if": ["error"], - "no-nested-ternary": ["error"], - "no-new-object": ["error"], - "no-trailing-spaces": ["error"], - "semi-spacing": ["error", { "before": false, "after": true }], - "promise/prefer-await-to-then": ["error"], - "promise/prefer-await-to-callbacks": ["error"], - "import/first": ["error"], - "import/exports-last": ["error"], - "import/extensions": ["error"], - "import/newline-after-import": ["error"], - "import/no-unassigned-import": ["error"], - "import/no-named-default": ["error"], - "import/no-default-export": ["error"], - "@typescript-eslint/array-type": ["error", { "default": "array-simple" }], - "@typescript-eslint/consistent-type-definitions": ["error", "interface"], - "@typescript-eslint/explicit-function-return-type": ["error"], - "@typescript-eslint/explicit-member-accessibility": ["error", { "accessibility": "explicit" }], - "@typescript-eslint/prefer-for-of": ["error"], - "@typescript-eslint/prefer-readonly": ["error"], - "@typescript-eslint/promise-function-async": ["error"], - "@typescript-eslint/restrict-plus-operands": ["error"], - "@typescript-eslint/strict-boolean-expressions": ["error"], - "@typescript-eslint/unified-signatures": ["error"] - } + "extends": ["@diplomatiq/eslint-config-tslib"] } diff --git a/README.md b/README.md index 0bca258..f40136b 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,12 @@ Resily offers **reactive** and **proactive** policies: | ------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------- | | [**RetryPolicy**](#retrypolicy) | Many faults are transient and will not occur again after a delay. | Allows configuring automatic retries on specified conditions. | +#### Proactive policies + +| Policy | What does it claim? | How does it work? | +| ----------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------- | +| [**TimeoutPolicy**](#timeoutpolicy) | After some time, it is unlikely that the call will be successful. | Ensures the caller does not have to wait more than the specified timeout. | + ### Reactive policies Every reactive policy extends the `ReactivePolicy` class, which means they can be configured with predicates to react on specific results and/or exceptions: @@ -230,7 +236,7 @@ await policy.execute(async () => { #### RetryPolicy -RetryPolicy claims that many faults are transient and will not occur again after a delay. It allows configuring automatic retries on specified conditions. +`RetryPolicy` claims that many faults are transient and will not occur again after a delay. It allows configuring automatic retries on specified conditions. Configure how many retries you need or retry forever: @@ -350,7 +356,7 @@ import { randomFill } from 'crypto'; export class NodeJsEntropyProvider implements EntropyProvider { public async getRandomValues(array: T): Promise { return new Promise((resolve, reject): void => { - randomFill(array, (error: Error | null, array: T) => { + randomFill(array, (error: Error | null, array: T): void => { if (error !== null) { reject(error); return; @@ -398,6 +404,105 @@ policy.onFinally(() => { }); ``` +### Proactive policies + +Every proactive policy extends the `ProactivePolicy` class. + +#### TimeoutPolicy + +`TimeoutPolicy` claims that after some time, it is unlikely the call will be successful. It ensures the caller does not have to wait more than the specified timeout. + +Only asynchronous methods can be executed within a `TimeoutPolicy`, or else no timeout happens. `TimeoutPolicy` is implemented with `Promise.race()`, racing the promise returned by the executed method (`executionPromise`) with a promise that is rejected after the specified time elapses (`timeoutPromise`). If the executed method is not asynchronous (i.e. it does not have at least one point to pause its execution at), no timeout will happen even if the execution takes longer than the specified timeout duration, since there is no point in time for taking the control out from the executed method's hands to reject the `timeoutPromise`. + +The executed method is fully executed to its end (unless it throws an exception), regardless of whether a timeout has occured or not. `TimeoutPolicy` ensures that the caller does not have to wait more than the specified timeout, but it does neither cancel nor abort\* the execution of the method. This means that if the executed method has side effects, these side effects can occur even after the timeout happened. + +\*TypeScript/JavaScript has no _generic_ way of canceling or aborting an executing method, either synchronous or asynchronous. `TimeoutPolicy` runs arbitrary user-provided code: it cannot be assumed the code is prepared in any way (e.g. it has cancel points). The provided code _could_ be executed in a separate worker thread so it can be aborted instantaneously by terminating the worker, but run-time compiling a worker from user-provided code is ugly and error-prone. + +On timeout, the Promise returned by the policy's `execute` method is rejected with a `TimeoutException`: + +```typescript +import { TimeoutException, TimeoutPolicy } from '@diplomatiq/resily'; + +const policy = new TimeoutPolicy(); + +try { + const result = await policy.execute(async () => { + // the executed code + }); +} catch (ex) { + if (ex instanceof TimeoutException) { + // the operation timed out + } else { + // the executed method thrown an exception + } +} +``` + +Configure how long the waiting period should be: + +```typescript +import { TimeoutPolicy } from '@diplomatiq/resily'; + +const policy = new TimeoutPolicy(); +policy.timeoutAfter(1000); // timeout after 1000 ms +``` + +Perform certain actions on timeout: + +```typescript +import { TimeoutPolicy } from '@diplomatiq/resily'; + +const policy = new TimeoutPolicy(); +policy.onTimeout( + // onTimeoutFns can be sync or async, they will be awaited + async timedOutAfterMs => { + // the policy was configured to timeout after timedOutAfterMs + }, +); + +// you can set multiple onTimeoutFns, they will run sequentially +policy.onTimeout(async () => { + // this will be awaited first +}); +policy.onTimeout(async () => { + // then this will be awaited +}); + +// errors thrown by an onTimeoutFn will be caught and ignored +policy.onTimeout(() => { + // throwing an error has no effect outside the method + throw new Error(); +}); +``` + +Throwing a `TimeoutException` from the executed method is not a timeout, therefore it does not trigger running `onTimeout` functions: + +```typescript +import { TimeoutException, TimeoutPolicy } from '@diplomatiq/resily'; + +const policy = new TimeoutPolicy(); + +let onTimeoutRan = false; +policy.onTimeout(() => { + onTimeoutRan = true; +}); + +try { + await policy.execute(async () => { + throw new TimeoutException(); + }); +} catch (ex) { + // ex is a TimeoutException (thrown by the executed method) + const isTimeoutException = ex instanceof TimeoutException; // true +} + +// onTimeoutRan is false +``` + +## Development + +See [CONTRIBUTING.md](https://github.com/Diplomatiq/resily/blob/develop/CONTRIBUTING.md) for details. + --- Copyright (c) 2018 Diplomatiq diff --git a/package-lock.json b/package-lock.json index 03fcc17..8a1a677 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,18 +14,18 @@ } }, "@babel/core": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.4.tgz", - "integrity": "sha512-0LiLrB2PwrVI+a2/IEskBopDYSd8BCb3rOvH7D5tzoWd696TBEduBvuLVm4Nx6rltrLZqvI3MCalB2K2aVzQjA==", + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.7.tgz", + "integrity": "sha512-rBlqF3Yko9cynC5CCFy6+K/w2N+Sq/ff2BPy+Krp7rHlABIr5epbA7OxVeKoMHB39LZOp1UY5SuLjy6uWi35yA==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.8.4", + "@babel/generator": "^7.8.7", "@babel/helpers": "^7.8.4", - "@babel/parser": "^7.8.4", - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.8.4", - "@babel/types": "^7.8.3", + "@babel/parser": "^7.8.7", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.8.6", + "@babel/types": "^7.8.7", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.1", @@ -51,12 +51,12 @@ } }, "@babel/generator": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.4.tgz", - "integrity": "sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA==", + "version": "7.8.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.8.tgz", + "integrity": "sha512-HKyUVu69cZoclptr8t8U5b6sx6zoWjh8jiUhnuj3MpZuKT2dJ8zPTuiy31luq32swhI0SpwItCIlU8XW7BZeJg==", "dev": true, "requires": { - "@babel/types": "^7.8.3", + "@babel/types": "^7.8.7", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" @@ -122,51 +122,51 @@ } }, "@babel/parser": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz", - "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==", + "version": "7.8.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.8.tgz", + "integrity": "sha512-mO5GWzBPsPf6865iIbzNE0AvkKF3NE+2S3eRUpE+FE07BOAkXh6G+GW/Pj01hhXjve1WScbaIO4UlY1JKeqCcA==", "dev": true }, "@babel/runtime": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.4.tgz", - "integrity": "sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==", + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.7.tgz", + "integrity": "sha512-+AATMUFppJDw6aiR5NVPHqIQBlV/Pj8wY/EZH+lmvRdUo9xBaz/rF3alAwFJQavvKfeOlPE7oaaDHVbcySbCsg==", "dev": true, "requires": { - "regenerator-runtime": "^0.13.2" + "regenerator-runtime": "^0.13.4" }, "dependencies": { "regenerator-runtime": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", + "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", "dev": true } } }, "@babel/template": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", - "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" } }, "@babel/traverse": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.4.tgz", - "integrity": "sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.8.4", + "@babel/generator": "^7.8.6", "@babel/helper-function-name": "^7.8.3", "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.8.4", - "@babel/types": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.13" @@ -181,9 +181,9 @@ } }, "@babel/types": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", - "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", "dev": true, "requires": { "esutils": "^2.0.2", @@ -406,6 +406,23 @@ "resolved": "https://registry.npmjs.org/@diplomatiq/crypto-random/-/crypto-random-2.2.0.tgz", "integrity": "sha512-3B6EhsGLTxUzpK6ShaNwg4t3sqOknlMqI7q07z6CUupwplW2As295YgoJR5LWQ5HpzRwEQ37BseOyxs7VOmjFw==" }, + "@diplomatiq/eslint-config-tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@diplomatiq/eslint-config-tslib/-/eslint-config-tslib-2.5.0.tgz", + "integrity": "sha512-UaRcjQYCYeu4lX1W05fwNH1BrGT5vdOqZosf7VMbnCBirFOYJdqheZivrEpPnjVS9KMJPZKdFmEkG92oEr7Sag==", + "dev": true, + "requires": { + "@typescript-eslint/eslint-plugin": "^2.19.2", + "@typescript-eslint/parser": "^2.19.2", + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-import": "^2.20.1", + "eslint-plugin-prettier": "^3.1.2", + "eslint-plugin-promise": "^4.2.1", + "prettier": "^1.19.1", + "typescript": "^3.8.2" + } + }, "@istanbuljs/load-nyc-config": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", @@ -602,9 +619,9 @@ "dev": true }, "acorn-jsx": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", - "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, "add-stream": { @@ -632,9 +649,9 @@ } }, "ajv": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", - "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -650,12 +667,20 @@ "dev": true }, "ansi-escapes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", - "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } } }, "ansi-regex": { @@ -1217,9 +1242,9 @@ } }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true } } @@ -1906,13 +1931,13 @@ "dev": true }, "espree": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", - "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", "dev": true, "requires": { - "acorn": "^7.1.0", - "acorn-jsx": "^5.1.0", + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", "eslint-visitor-keys": "^1.1.0" } }, @@ -1923,9 +1948,9 @@ "dev": true }, "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.1.0.tgz", + "integrity": "sha512-MxYW9xKmROWF672KqjO75sszsA8Mxhw06YFeS5VHlB98KDHbOSurm3ArsjO60Eaf3QmGMCP1yn+0JQkNLo/97Q==", "dev": true, "requires": { "estraverse": "^4.0.0" @@ -2003,9 +2028,9 @@ "dev": true }, "figures": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", - "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "requires": { "escape-string-regexp": "^1.0.5" @@ -2030,13 +2055,13 @@ } }, "find-cache-dir": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.2.0.tgz", - "integrity": "sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", "dev": true, "requires": { "commondir": "^1.0.1", - "make-dir": "^3.0.0", + "make-dir": "^3.0.2", "pkg-dir": "^4.1.0" }, "dependencies": { @@ -2349,9 +2374,9 @@ } }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, "parse-json": { @@ -2554,9 +2579,9 @@ } }, "globals": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", - "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", "dev": true, "requires": { "type-fest": "^0.8.1" @@ -2608,9 +2633,9 @@ "dev": true }, "hasha": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.1.0.tgz", - "integrity": "sha512-OFPDWmzPN1l7atOV1TgBVmNtBxaIysToK6Ve9DK+vT6pYuklw/nPNT+HJbZi0KDcI6vWB+9tgvZ5YD7fA3CXcA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", + "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", "dev": true, "requires": { "is-stream": "^2.0.0", @@ -2632,9 +2657,9 @@ "dev": true }, "hosted-git-info": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", - "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", "dev": true }, "html-escaper": { @@ -2870,24 +2895,85 @@ "dev": true }, "inquirer": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", - "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", + "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", - "chalk": "^2.4.2", + "chalk": "^3.0.0", "cli-cursor": "^3.1.0", "cli-width": "^2.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.15", "mute-stream": "0.0.8", - "run-async": "^2.2.0", + "run-async": "^2.4.0", "rxjs": "^6.5.3", "string-width": "^4.1.0", - "strip-ansi": "^5.1.0", + "strip-ansi": "^6.0.0", "through": "^2.3.6" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "is-arrayish": { @@ -3251,9 +3337,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true } } @@ -3365,18 +3451,18 @@ } }, "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", "dev": true, "requires": { "semver": "^6.0.0" } }, "make-error": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", - "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, "map-obj": { @@ -3840,9 +3926,9 @@ } }, "yargs": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.1.0.tgz", - "integrity": "sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg==", + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.0.tgz", + "integrity": "sha512-g/QCnmjgOl1YJjGsnUg2SatC7NUYEiLXJqxNOQU9qSpjzGtGXda9b+OKccr1kLTy8BN9yqEyqfq5lxlwdc13TA==", "dev": true, "requires": { "cliui": "^6.0.0", @@ -3855,13 +3941,13 @@ "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^16.1.0" + "yargs-parser": "^18.1.0" } }, "yargs-parser": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz", - "integrity": "sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==", + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.0.tgz", + "integrity": "sha512-o/Jr6JBOv6Yx3pL+5naWSoIA2jJ+ZkMYQG/ie9qFbukBe4uzmBatlXFOiu/tNKRWEtyf+n5w7jc/O16ufqOTdQ==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -4347,9 +4433,9 @@ } }, "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", + "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==", "dev": true, "requires": { "is-promise": "^2.1.0" @@ -4814,9 +4900,9 @@ } }, "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", "dev": true }, "tsutils": { @@ -4865,9 +4951,9 @@ "dev": true }, "uglify-js": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.7.tgz", - "integrity": "sha512-FeSU+hi7ULYy6mn8PKio/tXsdSXN35lm4KgV2asx00kzrLU9Pi3oAslcJT70Jdj7PHX29gGUPOT6+lXGBbemhA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.0.tgz", + "integrity": "sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ==", "dev": true, "optional": true, "requires": { @@ -5039,9 +5125,9 @@ } }, "write-file-atomic": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz", - "integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "dev": true, "requires": { "imurmurhash": "^0.1.4", @@ -5063,12 +5149,12 @@ "dev": true }, "yaml": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.7.2.tgz", - "integrity": "sha512-qXROVp90sb83XtAoqE8bP9RwAkTTZbugRUTm5YeFCBfNRPEp2YzTeqWiz7m5OORHzEvrA/qcGS8hp/E+MMROYw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.8.2.tgz", + "integrity": "sha512-omakb0d7FjMo3R1D2EbTKVIk6dAVLRxFXdLZMEUToeAvuqgG/YuHMuQOZ5fgk+vQ8cx+cnGKwyg+8g8PNT0xQg==", "dev": true, "requires": { - "@babel/runtime": "^7.6.3" + "@babel/runtime": "^7.8.7" } }, "yargs": { diff --git a/package.json b/package.json index bebf8af..4c0a65d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "build-and-test-everything": "npm run lint && npm run build && npm run test", "check-release-tag": "node --experimental-modules scripts/check-release-tag.mjs", "clean": "rm -r ./dist/", - "lint": "eslint ./src/ --ext .ts", + "lint": "eslint ./src/ ./test/ --ext .ts", "prepare": "npm run build-and-test-everything", "prepublishOnly": "npm run check-release-tag", "test": "cross-env-shell TS_NODE_PROJECT=tsconfig.test.json nyc --reporter=lcov --reporter=text mocha --require ts-node/register --require source-map-support/register --require esm --recursive test/specs/**/*.test.ts", @@ -44,21 +44,15 @@ "@types/chai": "^4.2.11", "@types/mocha": "^7.0.2", "@types/node": "^13.9.1", - "@typescript-eslint/eslint-plugin": "^2.23.0", - "@typescript-eslint/parser": "^2.23.0", "chai": "^4.2.0", "conventional-changelog-cli": "^2.0.31", "cross-env": "^7.0.2", - "eslint": "^6.8.0", - "eslint-config-prettier": "^6.10.0", - "eslint-plugin-import": "^2.20.1", - "eslint-plugin-prettier": "^3.1.2", - "eslint-plugin-promise": "^4.2.1", + "@diplomatiq/eslint-config-tslib": "^2.5.0", + "esm": "^3.2.25", "husky": "^4.2.3", "mocha": "^7.1.0", "nyc": "^15.0.0", "prettier": "^1.19.1", - "esm": "^3.2.25", "source-map-support": "^0.5.16", "ts-node": "^8.6.2", "typescript": "^3.8.3" diff --git a/src/main.ts b/src/main.ts index 5c81691..b9d40f0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,2 +1,4 @@ +export { TimeoutException } from './policies/proactive/timeoutPolicy/timeoutException'; +export { TimeoutPolicy } from './policies/proactive/timeoutPolicy/timeoutPolicy'; export { BackoffStrategyFactory } from './policies/reactive/retryPolicy/backoffStrategyFactory'; export { RetryPolicy } from './policies/reactive/retryPolicy/retryPolicy'; diff --git a/src/policies/proactive/proactivePolicy.ts b/src/policies/proactive/proactivePolicy.ts new file mode 100644 index 0000000..c63986b --- /dev/null +++ b/src/policies/proactive/proactivePolicy.ts @@ -0,0 +1,3 @@ +import { Policy } from '../policy'; + +export abstract class ProactivePolicy extends Policy {} diff --git a/src/policies/proactive/timeoutPolicy/executionException.ts b/src/policies/proactive/timeoutPolicy/executionException.ts new file mode 100644 index 0000000..3381e09 --- /dev/null +++ b/src/policies/proactive/timeoutPolicy/executionException.ts @@ -0,0 +1,5 @@ +export class ExecutionException extends Error { + public constructor(public innerException: unknown) { + super(); + } +} diff --git a/src/policies/proactive/timeoutPolicy/timeoutException.ts b/src/policies/proactive/timeoutPolicy/timeoutException.ts new file mode 100644 index 0000000..989cca8 --- /dev/null +++ b/src/policies/proactive/timeoutPolicy/timeoutException.ts @@ -0,0 +1,5 @@ +export class TimeoutException extends Error { + public constructor(public readonly timedOutAfterMs: number) { + super(); + } +} diff --git a/src/policies/proactive/timeoutPolicy/timeoutPolicy.ts b/src/policies/proactive/timeoutPolicy/timeoutPolicy.ts new file mode 100644 index 0000000..34b95fe --- /dev/null +++ b/src/policies/proactive/timeoutPolicy/timeoutPolicy.ts @@ -0,0 +1,85 @@ +import { ProactivePolicy } from '../proactivePolicy'; +import { ExecutionException } from './executionException'; +import { TimeoutException } from './timeoutException'; + +export class TimeoutPolicy extends ProactivePolicy { + private timeoutMs: number | undefined; + private readonly onTimeoutFns: Array<(timedOutAfter: number) => void | Promise> = []; + private executing = 0; + + public constructor() { + super(); + } + + public timeoutAfter(timeoutMs: number): void { + if (!Number.isInteger(timeoutMs)) { + throw new Error('timeoutMs must be integer'); + } + + if (timeoutMs < 0) { + throw new Error('timeoutMs must be greater than 0'); + } + + if (!Number.isSafeInteger(timeoutMs)) { + throw new Error('timeoutMs must be less than or equal to 2^53 - 1'); + } + + if (this.executing > 0) { + throw new Error('cannot modify policy during execution'); + } + + this.timeoutMs = timeoutMs; + } + + public onTimeout(fn: (timedOutAfterMs: number) => void | Promise): void { + if (this.executing > 0) { + throw new Error('cannot modify policy during execution'); + } + + this.onTimeoutFns.push(fn); + } + + public async execute(fn: () => Promise): Promise { + this.executing++; + + const executionPromise = (async (): Promise => { + try { + return await fn(); + } catch (ex) { + throw new ExecutionException(ex); + } + })(); + + let timeoutId; + const timeoutPromise = new Promise((_, reject): void => { + if (this.timeoutMs !== undefined) { + const currentTimeoutMs = this.timeoutMs; + timeoutId = setTimeout((): void => { + reject(new TimeoutException(currentTimeoutMs)); + }, this.timeoutMs); + } + }); + + try { + return await Promise.race([executionPromise, timeoutPromise]); + } catch (ex) { + const typedEx: ExecutionException | TimeoutException = ex; + if (typedEx instanceof TimeoutException) { + for (const onTimeoutFn of this.onTimeoutFns) { + try { + await onTimeoutFn(typedEx.timedOutAfterMs); + } catch (onTimeoutError) { + // ignored + } + } + + throw typedEx; + } + + throw typedEx.innerException; + } finally { + clearTimeout(timeoutId); + this.executing--; + } + } +} diff --git a/src/policies/reactive/retryPolicy/retryPolicy.ts b/src/policies/reactive/retryPolicy/retryPolicy.ts index 6aa2c39..d8aa44c 100644 --- a/src/policies/reactive/retryPolicy/retryPolicy.ts +++ b/src/policies/reactive/retryPolicy/retryPolicy.ts @@ -6,7 +6,7 @@ export class RetryPolicy extends ReactivePolicy { private readonly onRetryFns: Array< (result: ResultType | undefined, error: unknown | undefined, currentRetryCount: number) => void | Promise > = []; - private backoffStrategy: (currentRetryCount: number) => number | Promise = () => 0; + private backoffStrategy: (currentRetryCount: number) => number | Promise = (): number => 0; private readonly onFinallyFns: Array<() => void | Promise> = []; private executing = 0; @@ -24,7 +24,7 @@ export class RetryPolicy extends ReactivePolicy { } if (!Number.isSafeInteger(retryCount)) { - throw new Error('retryCount must be less than 2^53 - 1'); + throw new Error('retryCount must be less than or equal to 2^53 - 1'); } if (this.executing > 0) { @@ -138,11 +138,19 @@ export class RetryPolicy extends ReactivePolicy { } private async shouldRetryOnResult(result: ResultType, currentRetryCount: number): Promise { - return this.shouldRetryOn(currentRetryCount, result, async result => this.isResultHandled(result)); + return this.shouldRetryOn( + currentRetryCount, + result, + async (result): Promise => this.isResultHandled(result), + ); } private async shouldRetryOnException(exception: unknown, currentRetryCount: number): Promise { - return this.shouldRetryOn(currentRetryCount, exception, async exception => this.isExceptionHandled(exception)); + return this.shouldRetryOn( + currentRetryCount, + exception, + async (exception): Promise => this.isExceptionHandled(exception), + ); } private async shouldRetryOn( @@ -158,7 +166,7 @@ export class RetryPolicy extends ReactivePolicy { } private async waitFor(ms: number): Promise { - return new Promise(resolve => { + return new Promise((resolve): void => { setTimeout(resolve, ms); }); } diff --git a/test/specs/backoffStrategyFactory.test.ts b/test/specs/backoffStrategyFactory.test.ts index d03c89c..cf3e942 100644 --- a/test/specs/backoffStrategyFactory.test.ts +++ b/test/specs/backoffStrategyFactory.test.ts @@ -4,9 +4,9 @@ import { BackoffStrategyFactory } from '../../src/policies/reactive/retryPolicy/ import { NodeJsEntropyProvider } from '../utils/nodeJsEntropyProvider'; import { windowMock } from '../utils/windowMock'; -describe('BackoffStrategyFactory', () => { - describe('constantBackoff', () => { - it('should produce a constant backoff strategy', () => { +describe('BackoffStrategyFactory', (): void => { + describe('constantBackoff', (): void => { + it('should produce a constant backoff strategy', (): void => { const strategy = BackoffStrategyFactory.constantBackoff(100); expect(strategy(1)).to.equal(100); expect(strategy(2)).to.equal(100); @@ -15,7 +15,7 @@ describe('BackoffStrategyFactory', () => { expect(strategy(5)).to.equal(100); }); - it('should produce a constant backoff strategy with an immediate first retry if set', () => { + it('should produce a constant backoff strategy with an immediate first retry if set', (): void => { const strategy = BackoffStrategyFactory.constantBackoff(100, true); expect(strategy(1)).to.equal(0); expect(strategy(2)).to.equal(100); @@ -25,8 +25,8 @@ describe('BackoffStrategyFactory', () => { }); }); - describe('linearBackoff', () => { - it('should produce a linear backoff strategy', () => { + describe('linearBackoff', (): void => { + it('should produce a linear backoff strategy', (): void => { const strategy = BackoffStrategyFactory.linearBackoff(100); expect(strategy(1)).to.equal(100); expect(strategy(2)).to.equal(200); @@ -35,7 +35,7 @@ describe('BackoffStrategyFactory', () => { expect(strategy(5)).to.equal(500); }); - it('should produce a linear backoff strategy with an immediate first retry if set', () => { + it('should produce a linear backoff strategy with an immediate first retry if set', (): void => { const strategy = BackoffStrategyFactory.linearBackoff(100, true); expect(strategy(1)).to.equal(0); expect(strategy(2)).to.equal(100); @@ -45,8 +45,8 @@ describe('BackoffStrategyFactory', () => { }); }); - describe('exponentialBackoff', () => { - it('should produce an exponential backoff strategy', () => { + describe('exponentialBackoff', (): void => { + it('should produce an exponential backoff strategy', (): void => { const strategy = BackoffStrategyFactory.exponentialBackoff(100); expect(strategy(1)).to.equal(100); expect(strategy(2)).to.equal(200); @@ -55,7 +55,7 @@ describe('BackoffStrategyFactory', () => { expect(strategy(5)).to.equal(1600); }); - it('should produce an exponential backoff strategy with an immediate first retry if set', () => { + it('should produce an exponential backoff strategy with an immediate first retry if set', (): void => { const strategy = BackoffStrategyFactory.exponentialBackoff(100, true); expect(strategy(1)).to.equal(0); expect(strategy(2)).to.equal(100); @@ -64,7 +64,7 @@ describe('BackoffStrategyFactory', () => { expect(strategy(5)).to.equal(800); }); - it('should produce an exponential backoff strategy with a custom base if set', () => { + it('should produce an exponential backoff strategy with a custom base if set', (): void => { const strategy = BackoffStrategyFactory.exponentialBackoff(100, false, 3); expect(strategy(1)).to.equal(100); expect(strategy(2)).to.equal(300); @@ -73,7 +73,7 @@ describe('BackoffStrategyFactory', () => { expect(strategy(5)).to.equal(8100); }); - it('should produce an exponential backoff strategy with an immediate first retry and a custom base if set', () => { + it('should produce an exponential backoff strategy with an immediate first retry and a custom base if set', (): void => { const strategy = BackoffStrategyFactory.exponentialBackoff(100, true, 3); expect(strategy(1)).to.equal(0); expect(strategy(2)).to.equal(100); @@ -83,8 +83,8 @@ describe('BackoffStrategyFactory', () => { }); }); - describe('jitteredBackoff', () => { - it('should produce a jittered backoff strategy', async () => { + describe('jitteredBackoff', (): void => { + it('should produce a jittered backoff strategy', async (): Promise => { const entropyProvider = new NodeJsEntropyProvider(); const randomGenerator = new RandomGenerator(entropyProvider); const strategy = BackoffStrategyFactory.jitteredBackoff(0, 100, false, randomGenerator); @@ -110,7 +110,9 @@ describe('BackoffStrategyFactory', () => { .and.to.be.at.most(100); }); - it('should produce a jittered backoff strategy with an immediate first retry if set', async () => { + it('should produce a jittered backoff strategy with an immediate first retry if set', async (): Promise< + void + > => { const entropyProvider = new NodeJsEntropyProvider(); const randomGenerator = new RandomGenerator(entropyProvider); const strategy = BackoffStrategyFactory.jitteredBackoff(0, 100, true, randomGenerator); @@ -134,7 +136,10 @@ describe('BackoffStrategyFactory', () => { .and.to.be.at.most(100); }); - it('should work with the default random generator where window.crypto.getRandomValues() is available', async () => { + it('should work with the default random generator where window.crypto.getRandomValues() is available', async (): Promise< + void + > => { + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore global.window = windowMock(); @@ -160,6 +165,7 @@ describe('BackoffStrategyFactory', () => { .to.be.at.least(0) .and.to.be.at.most(100); + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore global.window = undefined; }); diff --git a/test/specs/predicateChecker.test.ts b/test/specs/predicateChecker.test.ts index 28891a8..ec79a1f 100644 --- a/test/specs/predicateChecker.test.ts +++ b/test/specs/predicateChecker.test.ts @@ -1,114 +1,126 @@ import { expect } from 'chai'; import { PredicateChecker } from '../../src/utils/predicateChecker'; -describe('PredicateChecker', () => { - describe('single', () => { - it('should return true, when the predicate returns true for the subject', async () => { +describe('PredicateChecker', (): void => { + describe('single', (): void => { + it('should return true, when the predicate returns true for the subject', async (): Promise => { const result = await PredicateChecker.single( 'Diplomatiq is cool.', - (subject: string) => subject === 'Diplomatiq is cool.', + (subject: string): boolean => subject === 'Diplomatiq is cool.', ); expect(result).to.be.true; }); - it('should return false, when the predicate returns false for the subject', async () => { + it('should return false, when the predicate returns false for the subject', async (): Promise => { const result = await PredicateChecker.single( 'Diplomatiq is not cool.', - (subject: string) => subject === 'Diplomatiq is cool.', + (subject: string): boolean => subject === 'Diplomatiq is cool.', ); expect(result).to.be.false; }); - it('should return true, when the async predicate returns true for the subject', async () => { + it('should return true, when the async predicate returns true for the subject', async (): Promise => { const result = await PredicateChecker.single( 'Diplomatiq is cool.', - async (subject: string) => subject === 'Diplomatiq is cool.', + (subject: string): boolean => subject === 'Diplomatiq is cool.', ); expect(result).to.be.true; }); - it('should return false, when the async predicate returns false for the subject', async () => { + it('should return false, when the async predicate returns false for the subject', async (): Promise => { const result = await PredicateChecker.single( 'Diplomatiq is not cool.', - async (subject: string) => subject === 'Diplomatiq is cool.', + (subject: string): boolean => subject === 'Diplomatiq is cool.', ); expect(result).to.be.false; }); }); - describe('some', () => { - it('should return true if at least one of the predicates returns true for the subject', async () => { + describe('some', (): void => { + it('should return true if at least one of the predicates returns true for the subject', async (): Promise< + void + > => { const result = await PredicateChecker.some('Diplomatiq is cool.', [ - (subject: string) => subject === 'Diplomatiq is cool.', - (subject: string) => subject === 'Diplomatiq is the coolest.', + (subject: string): boolean => subject === 'Diplomatiq is cool.', + (subject: string): boolean => subject === 'Diplomatiq is the coolest.', ]); expect(result).to.be.true; }); - it('should return false if none of the predicates returns true for the subject', async () => { + it('should return false if none of the predicates returns true for the subject', async (): Promise => { const result = await PredicateChecker.some('Diplomatiq is not cool.', [ - (subject: string) => subject === 'Diplomatiq is cool.', - (subject: string) => subject === 'Diplomatiq is the coolest.', + (subject: string): boolean => subject === 'Diplomatiq is cool.', + (subject: string): boolean => subject === 'Diplomatiq is the coolest.', ]); expect(result).to.be.false; }); - it('should return true if at least one of the async predicates returns true for the subject', async () => { + it('should return true if at least one of the async predicates returns true for the subject', async (): Promise< + void + > => { const result = await PredicateChecker.some('Diplomatiq is cool.', [ - async (subject: string) => subject === 'Diplomatiq is cool.', - async (subject: string) => subject === 'Diplomatiq is the coolest.', + (subject: string): boolean => subject === 'Diplomatiq is cool.', + (subject: string): boolean => subject === 'Diplomatiq is the coolest.', ]); expect(result).to.be.true; }); - it('should return false if none of the async predicates returns true for the subject', async () => { + it('should return false if none of the async predicates returns true for the subject', async (): Promise< + void + > => { const result = await PredicateChecker.some('Diplomatiq is not cool.', [ - async (subject: string) => subject === 'Diplomatiq is cool.', - async (subject: string) => subject === 'Diplomatiq is the coolest.', + (subject: string): boolean => subject === 'Diplomatiq is cool.', + (subject: string): boolean => subject === 'Diplomatiq is the coolest.', ]); expect(result).to.be.false; }); - it('should return false if predicates is empty', async () => { + it('should return false if predicates is empty', async (): Promise => { const result = await PredicateChecker.some('Diplomatiq is cool.', []); expect(result).to.be.false; }); }); - describe('all', () => { - it('should return true if all of the predicates returns true for the subject', async () => { + describe('all', (): void => { + it('should return true if all of the predicates returns true for the subject', async (): Promise => { const result = await PredicateChecker.every('Diplomatiq is cool.', [ - (subject: string) => subject.includes('Diplomatiq'), - (subject: string) => subject.includes('cool'), + (subject: string): boolean => subject.includes('Diplomatiq'), + (subject: string): boolean => subject.includes('cool'), ]); expect(result).to.be.true; }); - it('should return false if at least one of the predicates returns false for the subject', async () => { + it('should return false if at least one of the predicates returns false for the subject', async (): Promise< + void + > => { const result = await PredicateChecker.every('Diplomatiq is cool.', [ - (subject: string) => subject.includes('Diplomatiq'), - (subject: string) => subject.includes('not cool'), + (subject: string): boolean => subject.includes('Diplomatiq'), + (subject: string): boolean => subject.includes('not cool'), ]); expect(result).to.be.false; }); - it('should return true if all of the async predicates returns true for the subject', async () => { + it('should return true if all of the async predicates returns true for the subject', async (): Promise< + void + > => { const result = await PredicateChecker.every('Diplomatiq is cool.', [ - async (subject: string) => subject.includes('Diplomatiq'), - async (subject: string) => subject.includes('cool'), + (subject: string): boolean => subject.includes('Diplomatiq'), + (subject: string): boolean => subject.includes('cool'), ]); expect(result).to.be.true; }); - it('should return false if at least one of the async predicates returns false for the subject', async () => { + it('should return false if at least one of the async predicates returns false for the subject', async (): Promise< + void + > => { const result = await PredicateChecker.every('Diplomatiq is cool.', [ - async (subject: string) => subject.includes('Diplomatiq'), - async (subject: string) => subject.includes('not cool'), + (subject: string): boolean => subject.includes('Diplomatiq'), + (subject: string): boolean => subject.includes('not cool'), ]); expect(result).to.be.false; }); - it('should return false if predicates is empty', async () => { + it('should return false if predicates is empty', async (): Promise => { const result = await PredicateChecker.every('Diplomatiq is cool.', []); expect(result).to.be.false; }); diff --git a/test/specs/retryPolicy.test.ts b/test/specs/retryPolicy.test.ts index 655a4bc..15ae45b 100644 --- a/test/specs/retryPolicy.test.ts +++ b/test/specs/retryPolicy.test.ts @@ -1,30 +1,30 @@ import { expect } from 'chai'; import { RetryPolicy } from '../../src/policies/reactive/retryPolicy/retryPolicy'; -describe('RetryPolicy', () => { - it('should run the execution callback and return its result by default', async () => { +describe('RetryPolicy', (): void => { + it('should run the execution callback and return its result by default', async (): Promise => { const policy = new RetryPolicy(); - const result = await policy.execute(() => { + const result = await policy.execute((): string => { return 'Diplomatiq is cool.'; }); expect(result).to.equal('Diplomatiq is cool.'); }); - it('should run the async execution callback and return its result by default', async () => { + it('should run the async execution callback and return its result by default', async (): Promise => { const policy = new RetryPolicy(); - const result = await policy.execute(async () => { + const result = await policy.execute((): string => { return 'Diplomatiq is cool.'; }); expect(result).to.equal('Diplomatiq is cool.'); }); - it('should run the execution callback and throw its exceptions by default', async () => { + it('should run the execution callback and throw its exceptions by default', async (): Promise => { const policy = new RetryPolicy(); try { - await policy.execute(() => { + await policy.execute((): string => { throw new Error('TestException'); }); expect.fail('did not throw'); @@ -33,11 +33,11 @@ describe('RetryPolicy', () => { } }); - it('should run the async execution callback and throw its exceptions by default', async () => { - const policy = new RetryPolicy(); + it('should run the async execution callback and throw its exceptions by default', async (): Promise => { + const policy = new RetryPolicy(); try { - await policy.execute(() => { + await policy.execute((): unknown => { throw new Error('TestException'); }); expect.fail('did not throw'); @@ -46,13 +46,13 @@ describe('RetryPolicy', () => { } }); - it('should retry on a given result once, then return the result by default', async () => { + it('should retry on a given result once, then return the result by default', async (): Promise => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); let executed = 0; - const result = await policy.execute(() => { + const result = await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -61,13 +61,13 @@ describe('RetryPolicy', () => { expect(result).to.equal('Diplomatiq is cool.'); }); - it('should not retry on a not given result, but return the result', async () => { + it('should not retry on a not given result, but return the result', async (): Promise => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is not cool.'); + policy.handleResult((r: string): boolean => r === 'Diplomatiq is not cool.'); let executed = 0; - const result = await policy.execute(() => { + const result = await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -76,14 +76,16 @@ describe('RetryPolicy', () => { expect(result).to.equal('Diplomatiq is cool.'); }); - it('should retry on a given result thrice when setting retryCount to 3, then return the result', async () => { + it('should retry on a given result thrice when setting retryCount to 3, then return the result', async (): Promise< + void + > => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); policy.retryCount(3); let executed = 0; - const result = await policy.execute(() => { + const result = await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -92,15 +94,15 @@ describe('RetryPolicy', () => { expect(result).to.equal('Diplomatiq is cool.'); }); - it('should retry on multiple given results, then return the result', async () => { + it('should retry on multiple given results, then return the result', async (): Promise => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); - policy.handleResult((r: string) => r === 'Diplomatiq is the coolest.'); + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.handleResult((r: string): boolean => r === 'Diplomatiq is the coolest.'); let executed = 0; let result: string; - result = await policy.execute(() => { + result = await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -108,7 +110,7 @@ describe('RetryPolicy', () => { expect(executed).to.equal(2); expect(result).to.equal('Diplomatiq is cool.'); - result = await policy.execute(() => { + result = await policy.execute((): string => { executed++; return 'Diplomatiq is the coolest.'; }); @@ -117,14 +119,14 @@ describe('RetryPolicy', () => { expect(result).to.equal('Diplomatiq is the coolest.'); }); - it('should retry on a given exception once, then throw by default', async () => { + it('should retry on a given exception once, then throw by default', async (): Promise => { const policy = new RetryPolicy(); - policy.handleException((e: unknown) => (e as Error).message === 'TestException'); + policy.handleException((e: unknown): boolean => (e as Error).message === 'TestException'); let executed = 0; try { - await policy.execute(() => { + await policy.execute((): unknown => { executed++; throw new Error('TestException'); }); @@ -136,14 +138,14 @@ describe('RetryPolicy', () => { expect(executed).to.equal(2); }); - it('should not retry on a not given exception, but throw', async () => { + it('should not retry on a not given exception, but throw', async (): Promise => { const policy = new RetryPolicy(); - policy.handleException((e: unknown) => (e as Error).message === 'TestException'); + policy.handleException((e: unknown): boolean => (e as Error).message === 'TestException'); let executed = 0; try { - await policy.execute(() => { + await policy.execute((): unknown => { executed++; throw new Error('ArgumentException'); }); @@ -155,15 +157,15 @@ describe('RetryPolicy', () => { expect(executed).to.equal(1); }); - it('should retry on a given exception thrice when setting retryCount to 3, then throw', async () => { + it('should retry on a given exception thrice when setting retryCount to 3, then throw', async (): Promise => { const policy = new RetryPolicy(); - policy.handleException((e: unknown) => (e as Error).message === 'TestException'); + policy.handleException((e: unknown): boolean => (e as Error).message === 'TestException'); policy.retryCount(3); let executed = 0; try { - await policy.execute(() => { + await policy.execute((): unknown => { executed++; throw new Error('TestException'); }); @@ -175,15 +177,15 @@ describe('RetryPolicy', () => { expect(executed).to.equal(4); }); - it('should retry on multiple given exceptions, then throw', async () => { - const policy = new RetryPolicy(); - policy.handleException((e: unknown) => (e as Error).message === 'TestException'); - policy.handleException((e: unknown) => (e as Error).message === 'ArgumentException'); + it('should retry on multiple given exceptions, then throw', async (): Promise => { + const policy = new RetryPolicy(); + policy.handleException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.handleException((e: unknown): boolean => (e as Error).message === 'ArgumentException'); let executed = 0; try { - await policy.execute(() => { + await policy.execute((): unknown => { executed++; throw new Error('TestException'); }); @@ -195,7 +197,7 @@ describe('RetryPolicy', () => { expect(executed).to.equal(2); try { - await policy.execute(() => { + await policy.execute((): unknown => { executed++; throw new Error('ArgumentException'); }); @@ -207,14 +209,16 @@ describe('RetryPolicy', () => { expect(executed).to.equal(4); }); - it('should retry on a given result and on a given exception as well, then return/throw', async () => { + it('should retry on a given result and on a given exception as well, then return/throw', async (): Promise< + void + > => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); - policy.handleException((e: unknown) => (e as Error).message === 'TestException'); + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.handleException((e: unknown): boolean => (e as Error).message === 'TestException'); let executed = 0; - await policy.execute(() => { + await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -222,7 +226,7 @@ describe('RetryPolicy', () => { expect(executed).to.equal(2); try { - await policy.execute(() => { + await policy.execute((): string => { executed++; throw new Error('TestException'); }); @@ -234,12 +238,12 @@ describe('RetryPolicy', () => { expect(executed).to.equal(4); }); - it('should not retry without a given result or exception to be handled', async () => { + it('should not retry without a given result or exception to be handled', async (): Promise => { const policy = new RetryPolicy(); let executed = 0; - const result = await policy.execute(() => { + const result = await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -248,14 +252,14 @@ describe('RetryPolicy', () => { expect(result).to.equal('Diplomatiq is cool.'); }); - it('should retry forever if set', async () => { + it('should retry forever if set', async (): Promise => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); policy.retryForever(); let executed = 0; - await policy.execute(() => { + await policy.execute((): string => { executed++; if (executed < 10) { @@ -268,10 +272,14 @@ describe('RetryPolicy', () => { expect(executed).to.equal(10); }); - it('should run onRetryFn with result filled on retry, before the retried execution', async () => { + it('should run onRetryFn with result filled on retry, before the retried execution', async (): Promise => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); - policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number) => { + + let executed = 0; + let onRetryExecuted = 0; + + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number): void => { expect(result).to.equal('Diplomatiq is cool.'); expect(error).to.equal(undefined); expect(currentRetryCount).to.equal(executed); @@ -280,10 +288,7 @@ describe('RetryPolicy', () => { expect(onRetryExecuted).to.equal(currentRetryCount); }); - let executed = 0; - let onRetryExecuted = 0; - - await policy.execute(() => { + await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -292,11 +297,17 @@ describe('RetryPolicy', () => { expect(onRetryExecuted).to.equal(1); }); - it('should run onRetryFn with result filled on retry, before the retried execution thrice when setting retryCount to 3', async () => { + it('should run onRetryFn with result filled on retry, before the retried execution thrice when setting retryCount to 3', async (): Promise< + void + > => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); + + let executed = 0; + let onRetryExecuted = 0; + + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); policy.retryCount(3); - policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number) => { + policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number): void => { expect(result).to.equal('Diplomatiq is cool.'); expect(error).to.equal(undefined); expect(currentRetryCount).to.equal(executed); @@ -305,10 +316,7 @@ describe('RetryPolicy', () => { expect(onRetryExecuted).to.equal(currentRetryCount); }); - let executed = 0; - let onRetryExecuted = 0; - - await policy.execute(() => { + await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -317,10 +325,14 @@ describe('RetryPolicy', () => { expect(onRetryExecuted).to.equal(3); }); - it('should run onRetryFn with error filled on retry, before the retried execution', async () => { + it('should run onRetryFn with error filled on retry, before the retried execution', async (): Promise => { const policy = new RetryPolicy(); - policy.handleException((e: unknown) => (e as Error).message === 'TestException'); - policy.onRetry((result: unknown | undefined, error: unknown | undefined, currentRetryCount: number) => { + + let executed = 0; + let onRetryExecuted = 0; + + policy.handleException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.onRetry((result: unknown | undefined, error: unknown | undefined, currentRetryCount: number): void => { expect(result).to.equal(undefined); expect((error as Error).message).to.equal('TestException'); expect(currentRetryCount).to.equal(executed); @@ -329,11 +341,8 @@ describe('RetryPolicy', () => { expect(onRetryExecuted).to.equal(currentRetryCount); }); - let executed = 0; - let onRetryExecuted = 0; - try { - await policy.execute(() => { + await policy.execute((): unknown => { executed++; throw new Error('TestException'); }); @@ -346,11 +355,17 @@ describe('RetryPolicy', () => { expect(onRetryExecuted).to.equal(1); }); - it('should run onRetryFn with error filled on retry, before the retried execution thrice when setting retryCount to 3', async () => { + it('should run onRetryFn with error filled on retry, before the retried execution thrice when setting retryCount to 3', async (): Promise< + void + > => { const policy = new RetryPolicy(); - policy.handleException((e: unknown) => (e as Error).message === 'TestException'); + + let executed = 0; + let onRetryExecuted = 0; + + policy.handleException((e: unknown): boolean => (e as Error).message === 'TestException'); policy.retryCount(3); - policy.onRetry((result: unknown | undefined, error: unknown | undefined, currentRetryCount: number) => { + policy.onRetry((result: unknown | undefined, error: unknown | undefined, currentRetryCount: number): void => { expect(result).to.equal(undefined); expect((error as Error).message).to.equal('TestException'); expect(currentRetryCount).to.equal(executed); @@ -359,11 +374,8 @@ describe('RetryPolicy', () => { expect(onRetryExecuted).to.equal(currentRetryCount); }); - let executed = 0; - let onRetryExecuted = 0; - try { - await policy.execute(() => { + await policy.execute((): unknown => { executed++; throw new Error('TestException'); }); @@ -376,22 +388,30 @@ describe('RetryPolicy', () => { expect(onRetryExecuted).to.equal(3); }); - it('should await an async onRetryFn before retrying', async () => { + it('should await an async onRetryFn before retrying', async (): Promise => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); - policy.onRetry(async (result: string | undefined, error: unknown | undefined, currentRetryCount: number) => { - expect(result).to.equal('Diplomatiq is cool.'); - expect(error).to.equal(undefined); - expect(currentRetryCount).to.equal(executed); - - onRetryExecuted++; - expect(onRetryExecuted).to.equal(currentRetryCount); - }); let executed = 0; let onRetryExecuted = 0; - await policy.execute(() => { + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.onRetry( + async ( + result: string | undefined, + error: unknown | undefined, + currentRetryCount: number, + // eslint-disable-next-line @typescript-eslint/require-await + ): Promise => { + expect(result).to.equal('Diplomatiq is cool.'); + expect(error).to.equal(undefined); + expect(currentRetryCount).to.equal(executed); + + onRetryExecuted++; + expect(onRetryExecuted).to.equal(currentRetryCount); + }, + ); + + await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -400,11 +420,15 @@ describe('RetryPolicy', () => { expect(onRetryExecuted).to.equal(1); }); - it('should run multiple onRetryFns sequentially on retry', async () => { + it('should run multiple onRetryFns sequentially on retry', async (): Promise => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); + + let executed = 0; + let onRetryExecuted = 0; + + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); policy.retryCount(3); - policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number) => { + policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number): void => { expect(result).to.equal('Diplomatiq is cool.'); expect(error).to.equal(undefined); expect(currentRetryCount).to.equal(executed); @@ -413,7 +437,7 @@ describe('RetryPolicy', () => { onRetryExecuted++; expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 1); }); - policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number) => { + policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number): void => { expect(result).to.equal('Diplomatiq is cool.'); expect(error).to.equal(undefined); expect(currentRetryCount).to.equal(executed); @@ -422,7 +446,7 @@ describe('RetryPolicy', () => { onRetryExecuted++; expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 2); }); - policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number) => { + policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number): void => { expect(result).to.equal('Diplomatiq is cool.'); expect(error).to.equal(undefined); expect(currentRetryCount).to.equal(executed); @@ -432,10 +456,7 @@ describe('RetryPolicy', () => { expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 3); }); - let executed = 0; - let onRetryExecuted = 0; - - await policy.execute(() => { + await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -444,42 +465,64 @@ describe('RetryPolicy', () => { expect(onRetryExecuted).to.equal(9); }); - it('should run multiple async onRetryFns sequentially on retry', async () => { + it('should run multiple async onRetryFns sequentially on retry', async (): Promise => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); - policy.retryCount(3); - policy.onRetry(async (result: string | undefined, error: unknown | undefined, currentRetryCount: number) => { - expect(result).to.equal('Diplomatiq is cool.'); - expect(error).to.equal(undefined); - expect(currentRetryCount).to.equal(executed); - - expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3); - onRetryExecuted++; - expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 1); - }); - policy.onRetry(async (result: string | undefined, error: unknown | undefined, currentRetryCount: number) => { - expect(result).to.equal('Diplomatiq is cool.'); - expect(error).to.equal(undefined); - expect(currentRetryCount).to.equal(executed); - - expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 1); - onRetryExecuted++; - expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 2); - }); - policy.onRetry(async (result: string | undefined, error: unknown | undefined, currentRetryCount: number) => { - expect(result).to.equal('Diplomatiq is cool.'); - expect(error).to.equal(undefined); - expect(currentRetryCount).to.equal(executed); - - expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 2); - onRetryExecuted++; - expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 3); - }); let executed = 0; let onRetryExecuted = 0; - await policy.execute(() => { + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.retryCount(3); + policy.onRetry( + async ( + result: string | undefined, + error: unknown | undefined, + currentRetryCount: number, + // eslint-disable-next-line @typescript-eslint/require-await + ): Promise => { + expect(result).to.equal('Diplomatiq is cool.'); + expect(error).to.equal(undefined); + expect(currentRetryCount).to.equal(executed); + + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3); + onRetryExecuted++; + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 1); + }, + ); + policy.onRetry( + async ( + result: string | undefined, + error: unknown | undefined, + currentRetryCount: number, + // eslint-disable-next-line @typescript-eslint/require-await + ): Promise => { + expect(result).to.equal('Diplomatiq is cool.'); + expect(error).to.equal(undefined); + expect(currentRetryCount).to.equal(executed); + + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 1); + onRetryExecuted++; + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 2); + }, + ); + policy.onRetry( + async ( + result: string | undefined, + error: unknown | undefined, + currentRetryCount: number, + // eslint-disable-next-line @typescript-eslint/require-await + ): Promise => { + expect(result).to.equal('Diplomatiq is cool.'); + expect(error).to.equal(undefined); + expect(currentRetryCount).to.equal(executed); + + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 2); + onRetryExecuted++; + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 3); + }, + ); + + await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -488,11 +531,16 @@ describe('RetryPolicy', () => { expect(onRetryExecuted).to.equal(9); }); - it('should run onFinallyFn after all execution and retries if retried', async () => { + it('should run onFinallyFn after all execution and retries if retried', async (): Promise => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); + + let executed = 0; + let onRetryExecuted = 0; + let onFinallyExecuted = 0; + + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); policy.retryCount(3); - policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number) => { + policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number): void => { expect(result).to.equal('Diplomatiq is cool.'); expect(error).to.equal(undefined); expect(currentRetryCount).to.equal(executed); @@ -500,18 +548,14 @@ describe('RetryPolicy', () => { onRetryExecuted++; expect(onRetryExecuted).to.equal(currentRetryCount); }); - policy.onFinally(() => { + policy.onFinally((): void => { onFinallyExecuted++; expect(executed).to.equal(4); expect(onRetryExecuted).to.equal(3); }); - let executed = 0; - let onRetryExecuted = 0; - let onFinallyExecuted = 0; - - await policy.execute(() => { + await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -521,20 +565,21 @@ describe('RetryPolicy', () => { expect(onFinallyExecuted).to.equal(1); }); - it('should run onFinallyFn after the execution if not retried', async () => { + it('should run onFinallyFn after the execution if not retried', async (): Promise => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is not cool.'); + + let executed = 0; + let onFinallyExecuted = 0; + + policy.handleResult((r: string): boolean => r === 'Diplomatiq is not cool.'); policy.retryCount(3); - policy.onFinally(() => { + policy.onFinally((): void => { onFinallyExecuted++; expect(executed).to.equal(1); }); - let executed = 0; - let onFinallyExecuted = 0; - - await policy.execute(() => { + await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -543,29 +588,30 @@ describe('RetryPolicy', () => { expect(onFinallyExecuted).to.equal(1); }); - it('should run multiple onFinallyFns sequentially', async () => { + it('should run multiple onFinallyFns sequentially', async (): Promise => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); - policy.onFinally(() => { + + let executed = 0; + let onFinallyExecuted = 0; + + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.onFinally((): void => { expect(onFinallyExecuted).to.equal(0); onFinallyExecuted++; expect(onFinallyExecuted).to.equal(1); }); - policy.onFinally(() => { + policy.onFinally((): void => { expect(onFinallyExecuted).to.equal(1); onFinallyExecuted++; expect(onFinallyExecuted).to.equal(2); }); - policy.onFinally(() => { + policy.onFinally((): void => { expect(onFinallyExecuted).to.equal(2); onFinallyExecuted++; expect(onFinallyExecuted).to.equal(3); }); - let executed = 0; - let onFinallyExecuted = 0; - - await policy.execute(() => { + await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -574,29 +620,39 @@ describe('RetryPolicy', () => { expect(onFinallyExecuted).to.equal(3); }); - it('should run multiple async onFinallyFns sequentially', async () => { + it('should run multiple async onFinallyFns sequentially', async (): Promise => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); - policy.onFinally(async () => { - expect(onFinallyExecuted).to.equal(0); - onFinallyExecuted++; - expect(onFinallyExecuted).to.equal(1); - }); - policy.onFinally(async () => { - expect(onFinallyExecuted).to.equal(1); - onFinallyExecuted++; - expect(onFinallyExecuted).to.equal(2); - }); - policy.onFinally(async () => { - expect(onFinallyExecuted).to.equal(2); - onFinallyExecuted++; - expect(onFinallyExecuted).to.equal(3); - }); let executed = 0; let onFinallyExecuted = 0; - await policy.execute(() => { + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.onFinally( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onFinallyExecuted).to.equal(0); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(1); + }, + ); + policy.onFinally( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onFinallyExecuted).to.equal(1); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(2); + }, + ); + policy.onFinally( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onFinallyExecuted).to.equal(2); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(3); + }, + ); + + await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -605,18 +661,19 @@ describe('RetryPolicy', () => { expect(onFinallyExecuted).to.equal(3); }); - it('should run onFinallyFn once, regardless of retryCount', async () => { + it('should run onFinallyFn once, regardless of retryCount', async (): Promise => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); - policy.retryCount(3); - policy.onFinally(() => { - onFinallyExecuted++; - }); let executed = 0; let onFinallyExecuted = 0; - await policy.execute(() => { + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.retryCount(3); + policy.onFinally((): void => { + onFinallyExecuted++; + }); + + await policy.execute((): string => { executed++; return 'Diplomatiq is cool.'; }); @@ -625,14 +682,14 @@ describe('RetryPolicy', () => { expect(onFinallyExecuted).to.equal(1); }); - it('should wait for the specified interval before retry if set', async () => { + it('should wait for the specified interval before retry if set', async (): Promise => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); - policy.waitBeforeRetry(() => 100); + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.waitBeforeRetry((): number => 100); const executionTimestamps: number[] = []; - await policy.execute(() => { + await policy.execute((): string => { executionTimestamps.push(Date.now()); return 'Diplomatiq is cool.'; }); @@ -642,15 +699,17 @@ describe('RetryPolicy', () => { .and.to.be.at.most(110); }); - it('should wait for the specified interval (depending on the current retry count) before retry if set', async () => { + it('should wait for the specified interval (depending on the current retry count) before retry if set', async (): Promise< + void + > => { const policy = new RetryPolicy(); - policy.handleResult((r: string) => r === 'Diplomatiq is cool.'); + policy.handleResult((r: string): boolean => r === 'Diplomatiq is cool.'); policy.retryCount(2); - policy.waitBeforeRetry((currentRetryCount: number) => currentRetryCount * 100); + policy.waitBeforeRetry((currentRetryCount: number): number => currentRetryCount * 100); const executionTimestamps: number[] = []; - await policy.execute(() => { + await policy.execute((): string => { executionTimestamps.push(Date.now()); return 'Diplomatiq is cool.'; }); @@ -663,11 +722,15 @@ describe('RetryPolicy', () => { .and.to.be.at.most(210); }); - it('should not allow to set retryCount during execution', () => { + it('should not allow to set retryCount during execution', (): void => { const policy = new RetryPolicy(); - policy.execute(async () => { - await new Promise(() => {}); - }); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); try { policy.retryCount(2); @@ -677,11 +740,15 @@ describe('RetryPolicy', () => { } }); - it('should not allow to set retryForever during execution', () => { + it('should not allow to set retryForever during execution', (): void => { const policy = new RetryPolicy(); - policy.execute(async () => { - await new Promise(() => {}); - }); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); try { policy.retryForever(); @@ -691,56 +758,78 @@ describe('RetryPolicy', () => { } }); - it('should not allow to add onRetryFns during execution', () => { + it('should not allow to add onRetryFns during execution', (): void => { const policy = new RetryPolicy(); - policy.execute(async () => { - await new Promise(() => {}); - }); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); try { - policy.onRetry(() => {}); + policy.onRetry((): void => { + // empty + }); expect.fail('did not throw'); } catch (ex) { expect((ex as Error).message).to.equal('cannot modify policy during execution'); } }); - it('should not allow to set waitBeforeRetry during execution', () => { + it('should not allow to set waitBeforeRetry during execution', (): void => { const policy = new RetryPolicy(); - policy.execute(async () => { - await new Promise(() => {}); - }); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); try { - policy.waitBeforeRetry(() => 100); + policy.waitBeforeRetry((): number => 100); expect.fail('did not throw'); } catch (ex) { expect((ex as Error).message).to.equal('cannot modify policy during execution'); } }); - it('should not allow to add onFinallyFns during execution', () => { + it('should not allow to add onFinallyFns during execution', (): void => { const policy = new RetryPolicy(); - policy.execute(async () => { - await new Promise(() => {}); - }); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); try { - policy.onFinally(() => {}); + policy.onFinally((): void => { + // empty + }); expect.fail('did not throw'); } catch (ex) { expect((ex as Error).message).to.equal('cannot modify policy during execution'); } }); - it("should be properly mutex'd for running an instance multiple times simultaneously", async () => { + it("should be properly mutex'd for running an instance multiple times simultaneously", async (): Promise => { const policy = new RetryPolicy(); await Promise.all([ - ...new Array(100) - .fill(undefined) - .map(() => policy.execute(() => new Promise(resolve => setTimeout(resolve, 20)))), - ...new Array(100).fill(undefined).map(() => { + ...new Array(100).fill(undefined).map( + async (): Promise => + policy.execute( + async (): Promise => + new Promise((resolve): void => { + setTimeout(resolve, 20); + }), + ), + ), + ...new Array(100).fill(undefined).map((): void => { try { policy.retryCount(1); expect.fail('did not throw'); @@ -748,7 +837,7 @@ describe('RetryPolicy', () => { expect((ex as Error).message).to.equal('cannot modify policy during execution'); } }), - ...new Array(100).fill(undefined).map(() => { + ...new Array(100).fill(undefined).map((): void => { try { policy.retryForever(); expect.fail('did not throw'); @@ -756,25 +845,29 @@ describe('RetryPolicy', () => { expect((ex as Error).message).to.equal('cannot modify policy during execution'); } }), - ...new Array(100).fill(undefined).map(() => { + ...new Array(100).fill(undefined).map((): void => { try { - policy.onRetry(() => {}); + policy.onRetry((): void => { + // empty + }); expect.fail('did not throw'); } catch (ex) { expect((ex as Error).message).to.equal('cannot modify policy during execution'); } }), - ...new Array(100).fill(undefined).map(() => { + ...new Array(100).fill(undefined).map((): void => { try { - policy.waitBeforeRetry(() => 100); + policy.waitBeforeRetry((): number => 100); expect.fail('did not throw'); } catch (ex) { expect((ex as Error).message).to.equal('cannot modify policy during execution'); } }), - ...new Array(100).fill(undefined).map(() => { + ...new Array(100).fill(undefined).map((): void => { try { - policy.onFinally(() => {}); + policy.onFinally((): void => { + // empty + }); expect.fail('did not throw'); } catch (ex) { expect((ex as Error).message).to.equal('cannot modify policy during execution'); @@ -783,7 +876,7 @@ describe('RetryPolicy', () => { ]); }); - it('should throw error when setting retry count to 0', () => { + it('should throw error when setting retry count to 0', (): void => { const policy = new RetryPolicy(); try { policy.retryCount(0); @@ -793,7 +886,7 @@ describe('RetryPolicy', () => { } }); - it('should throw error when setting retry count to <0', () => { + it('should throw error when setting retry count to <0', (): void => { const policy = new RetryPolicy(); try { policy.retryCount(-1); @@ -803,7 +896,7 @@ describe('RetryPolicy', () => { } }); - it('should throw error when setting retry count to a non-integer', () => { + it('should throw error when setting retry count to a non-integer', (): void => { const policy = new RetryPolicy(); try { policy.retryCount(0.1); @@ -813,13 +906,13 @@ describe('RetryPolicy', () => { } }); - it('should throw error when setting retry count to a non-safe integer', () => { + it('should throw error when setting retry count to a non-safe integer', (): void => { const policy = new RetryPolicy(); try { policy.retryCount(2 ** 53); expect.fail('did not throw'); } catch (ex) { - expect((ex as Error).message).to.equal('retryCount must be less than 2^53 - 1'); + expect((ex as Error).message).to.equal('retryCount must be less than or equal to 2^53 - 1'); } }); }); diff --git a/test/specs/timeoutPolicy.test.ts b/test/specs/timeoutPolicy.test.ts new file mode 100644 index 0000000..115b542 --- /dev/null +++ b/test/specs/timeoutPolicy.test.ts @@ -0,0 +1,278 @@ +import { expect } from 'chai'; +import { TimeoutException } from '../../src/policies/proactive/timeoutPolicy/timeoutException'; +import { TimeoutPolicy } from '../../src/policies/proactive/timeoutPolicy/timeoutPolicy'; + +describe('TimeoutPolicy', (): void => { + it('should run the execution callback and return its result if no timeout is set', async (): Promise => { + const policy = new TimeoutPolicy(); + const result = await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + return 'Diplomatiq is cool.'; + }, + ); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should run the execution callback and throw its exceptions if no timeout is set', async (): Promise => { + const policy = new TimeoutPolicy(); + + try { + await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + throw new Error('TestException'); + }, + ); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); + + it('should throw a TimeoutException if the execution takes more than the specified time', async (): Promise< + void + > => { + const policy = new TimeoutPolicy(); + policy.timeoutAfter(10); + + try { + await policy.execute( + async (): Promise => { + return new Promise((resolve): void => { + setTimeout(resolve, 20); + }); + }, + ); + } catch (ex) { + expect(ex instanceof TimeoutException).to.be.true; + } + }); + + it('should not throw a TimeoutException if the execution takes less than the specified time', async (): Promise< + void + > => { + const policy = new TimeoutPolicy(); + policy.timeoutAfter(20); + await policy.execute( + async (): Promise => { + return new Promise((resolve): void => { + setTimeout(resolve, 10); + }); + }, + ); + }); + + it('should run onTimeoutFn on timeout with timedOutAfter filled out', async (): Promise => { + const policy = new TimeoutPolicy(); + policy.timeoutAfter(10); + policy.onTimeout((timedOutAfter): void => { + expect(timedOutAfter).to.equal(10); + }); + + try { + await policy.execute( + async (): Promise => { + return new Promise((resolve): void => { + setTimeout(resolve, 20); + }); + }, + ); + } catch (ex) { + expect(ex instanceof TimeoutException).to.be.true; + } + }); + + it('should run multiple onTimeoutFns sequentially on timeout', async (): Promise => { + const policy = new TimeoutPolicy(); + policy.timeoutAfter(10); + + let onTimeoutCounter = 0; + policy.onTimeout((): void => { + expect(onTimeoutCounter).to.equal(0); + onTimeoutCounter++; + }); + policy.onTimeout((): void => { + expect(onTimeoutCounter).to.equal(1); + onTimeoutCounter++; + }); + policy.onTimeout((): void => { + expect(onTimeoutCounter).to.equal(2); + onTimeoutCounter++; + }); + + try { + await policy.execute( + async (): Promise => { + // empty + }, + ); + return new Promise((resolve): void => { + setTimeout(resolve, 20); + }); + } catch (ex) { + expect(ex instanceof TimeoutException).to.be.true; + } + }); + + it('should run multiple async onTimeoutFns sequentially on timeout', async (): Promise => { + const policy = new TimeoutPolicy(); + policy.timeoutAfter(10); + + let onTimeoutCounter = 0; + policy.onTimeout( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onTimeoutCounter).to.equal(0); + onTimeoutCounter++; + }, + ); + policy.onTimeout( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onTimeoutCounter).to.equal(1); + onTimeoutCounter++; + }, + ); + policy.onTimeout( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onTimeoutCounter).to.equal(2); + onTimeoutCounter++; + }, + ); + + try { + await policy.execute( + async (): Promise => { + return new Promise((resolve): void => { + setTimeout(resolve, 20); + }); + }, + ); + } catch (ex) { + expect(ex instanceof TimeoutException).to.be.true; + } + }); + + it('should not run onTimeoutFns if the execution callback throws a TimeoutException (if timeout is not set)', async (): Promise< + void + > => { + const policy = new TimeoutPolicy(); + + let onTimeoutCounter = 0; + policy.onTimeout((): void => { + onTimeoutCounter++; + }); + + try { + await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + throw new TimeoutException(1); + }, + ); + } catch (ex) { + expect(ex instanceof TimeoutException).to.be.true; + } + + expect(onTimeoutCounter).to.equal(0); + }); + + it('should not run onTimeoutFns if the execution callback throws a TimeoutException (if timeout is set)', async (): Promise< + void + > => { + const policy = new TimeoutPolicy(); + policy.timeoutAfter(1000); + + let onTimeoutCounter = 0; + policy.onTimeout((): void => { + onTimeoutCounter++; + }); + + try { + await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + throw new TimeoutException(1); + }, + ); + } catch (ex) { + expect(ex instanceof TimeoutException).to.be.true; + } + + expect(onTimeoutCounter).to.equal(0); + }); + + it('should not allow to set timeoutAfter during execution', (): void => { + const policy = new TimeoutPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.timeoutAfter(10); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('cannot modify policy during execution'); + } + }); + + it('should not allow to add onTimeoutFns during execution', (): void => { + const policy = new TimeoutPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.onTimeout((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('cannot modify policy during execution'); + } + }); + + it('should throw error when setting timeoutAfter to <0', (): void => { + const policy = new TimeoutPolicy(); + + try { + policy.timeoutAfter(-1); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('timeoutMs must be greater than 0'); + } + }); + + it('should throw error when setting timeoutAfter to a non-integer', (): void => { + const policy = new TimeoutPolicy(); + + try { + policy.timeoutAfter(0.1); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('timeoutMs must be integer'); + } + }); + + it('should throw error when setting timeoutAfter to a non-safe integer', (): void => { + const policy = new TimeoutPolicy(); + + try { + policy.timeoutAfter(2 ** 53); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('timeoutMs must be less than or equal to 2^53 - 1'); + } + }); +}); diff --git a/test/utils/nodeJsEntropyProvider.ts b/test/utils/nodeJsEntropyProvider.ts index 2ebfe8c..eced153 100644 --- a/test/utils/nodeJsEntropyProvider.ts +++ b/test/utils/nodeJsEntropyProvider.ts @@ -4,7 +4,7 @@ import { randomFill } from 'crypto'; export class NodeJsEntropyProvider implements EntropyProvider { public async getRandomValues(array: T): Promise { return new Promise((resolve, reject): void => { - randomFill(array, (error: Error | null, array: T) => { + randomFill(array, (error: Error | null, array: T): void => { if (error !== null) { reject(error); return; diff --git a/test/utils/windowMock.ts b/test/utils/windowMock.ts index 33cb054..618ac8c 100644 --- a/test/utils/windowMock.ts +++ b/test/utils/windowMock.ts @@ -1,7 +1,11 @@ import { randomFillSync } from 'crypto'; -export const windowMock = () => ({ +export const windowMock = (): { crypto: { - getRandomValues: (array: Uint8Array) => randomFillSync(array), + getRandomValues: (array: Uint8Array) => Uint8Array; + }; +} => ({ + crypto: { + getRandomValues: (array: Uint8Array): Uint8Array => randomFillSync(array), }, }); diff --git a/tsconfig.test.json b/tsconfig.test.json index 35845f8..77b1230 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -14,5 +14,5 @@ "strict": true, "target": "es2017" }, - "files": ["src/main.ts"] + "include": ["test/**/*.ts"] }