├── .eslintrc.js ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .license-sh.json ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── examples ├── basic │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── _app.jsx │ │ ├── api │ │ │ ├── health.js │ │ │ └── toggle.js │ │ └── index.jsx │ └── static │ │ └── styles.scss └── global-conf │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── pages │ ├── _app.jsx │ ├── api │ │ ├── health.js │ │ └── toggle.js │ └── index.jsx │ └── static │ └── styles.scss ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── config.ts ├── helpers.ts ├── index.ts ├── types.ts └── use-health-check.ts ├── tests └── helpers.spec.ts ├── tsconfig.eslint.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | ecmaVersion: 11, 8 | sourceType: 'module', 9 | project: './tsconfig.eslint.json', 10 | }, 11 | env: { 12 | es6: true, 13 | browser: true, 14 | node: true, 15 | jest: true, 16 | }, 17 | extends: [ 18 | 'eslint:recommended', 19 | 'plugin:import/errors', 20 | 'plugin:import/warnings', 21 | 'plugin:import/typescript', 22 | 'plugin:react/recommended', 23 | 'plugin:react-hooks/recommended', 24 | 'plugin:@typescript-eslint/recommended', 25 | 'prettier', 26 | ], 27 | plugins: ['@typescript-eslint'], 28 | settings: { 29 | react: { 30 | version: 'detect', 31 | }, 32 | }, 33 | rules: { 34 | 'no-unused-vars': 'off', 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [12.x, 14.x, 16.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm ci 27 | - run: npm run build --if-present 28 | - run: npm test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # misc 7 | .idea 8 | .vscode 9 | .DS_Store 10 | *.pem 11 | 12 | # build 13 | dist -------------------------------------------------------------------------------- /.license-sh.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignored_packages": {}, 3 | "whitelist": [ 4 | "Apache-1.0", 5 | "Apache-1.1", 6 | "Apache-2.0", 7 | "MIT", 8 | "MIT/X11", 9 | "BSD-3-Clause", 10 | "BSD-2-Clause", 11 | "BSD-1-Clause", 12 | "BSD", 13 | "Unlicense", 14 | "JSON", 15 | "ISC", 16 | "BSL-1.0", 17 | "X11", 18 | "NCSA", 19 | "UPL-1.0", 20 | "MPL-1.0", 21 | "MPL-1.1", 22 | "MPL-2.0", 23 | "Zlib", 24 | "CC0-1.0", 25 | "CC-BY-3.0", 26 | "CC-BY-4.0", 27 | "AFL-2.1", 28 | "WTFPL", 29 | "Artistic-2.0", 30 | "EPL-1.0", 31 | "MPL 1.1", 32 | "Public Domain", 33 | "CPL-1.0" 34 | ], 35 | "projects": [ 36 | "./package-lock.json" 37 | ], 38 | "overridden_packages": {} 39 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # misc 7 | .vscode 8 | .idea 9 | .DS_Store 10 | *.pem 11 | .github 12 | 13 | # dev 14 | /tests 15 | /examples 16 | /src 17 | .prettierrc 18 | .eslintrc.js 19 | rollup.config.js 20 | tsconfig.eslint.json 21 | tsconfig.json 22 | jest.config.js 23 | .license-sh.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "useTabs": false, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Webscope s.r.o. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Health Check 🏥 2 | 3 | Lightweight React hook for checking health of API services. 4 | 5 | [![stable](https://badgen.net/npm/v/@webscopeio/react-health-check)](https://www.npmjs.com/package/@webscopeio/react-health-check) 6 | ![tslib](https://badgen.net/npm/types/tslib) 7 | ![checks](https://badgen.net/github/checks/webscopeio/react-health-check) 8 | [![license](https://badgen.now.sh/badge/license/MIT)](./LICENSE) 9 | 10 | --- 11 | 12 | ## Installation 🧑‍🔧 13 | 14 | ``` 15 | npm i @webscopeio/react-health-check 16 | ``` 17 | 18 | or 19 | 20 | ``` 21 | yarn add @webscopeio/react-health-check 22 | ``` 23 | 24 | ## Examples 😲 25 | 26 | - [Basic](examples/basic) 27 | - [Global configuration](examples/global-conf) 28 | 29 | ## Usage ❓ 30 | 31 | ```ts 32 | const { available, refresh } = useHealthCheck({ 33 | service: { 34 | name: 'auth', 35 | url: 'https://example.com/auth/health', 36 | }, 37 | onSuccess: ({ service, timestamp }) => { 38 | console.log(`Service "${service.name}" is available since "${timestamp}" 🎉`); 39 | }, 40 | onError: ({ service, timestamp }) => { 41 | console.log(`Service "${service.name}" is not available since "${timestamp}" 😔`); 42 | }, 43 | }); 44 | ``` 45 | 46 | You can also create a global configuration so you don't have to define services and callbacks every time: 47 | 48 | ```tsx 49 | // App wrapper 50 | { 63 | console.log(`Service "${service.name}" is available since "${timestamp}" 🎉`); 64 | }, 65 | onError: ({ service, timestamp }) => { 66 | console.log(`Service "${service.name}" is not available since "${timestamp}" 😔`); 67 | }, 68 | }} 69 | > 70 | 71 | ; 72 | 73 | // Later in some child component 74 | const { available } = useHealthCheck('auth'); 75 | ``` 76 | 77 | ## Configuration 🛠 78 | 79 | `useHealthCheck()` hook accepts a configuration object with keys: 80 | 81 | | Key | Type | Description | 82 | | ------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------- | 83 | | service | `Service` | Object defining an API service to be checked. | 84 | | onSuccess | `(state: ServiceState) => void;` | Callback which should be called when API service becomes available again. | 85 | | onError | `(state: ServiceState) => void;` | Callback which should be called when API service becomes unavailable. | | 86 | | refreshInterval | `number` | Polling interval for health checks in milliseconds.
**Default value: 5000** | 87 | | refreshWhileHidden | `boolean` | Determines whether polling should be paused while browser window isn't visible.
**Default value: false** | 88 | 89 | Global configuration accepts the same keys as `useHealthCheck()` hook with the exception of "service". You need to specify array of "services" when using global configuration. 90 | 91 | ## License 💼 92 | 93 | MIT | Developed by Webscope.io 94 | -------------------------------------------------------------------------------- /examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Example - Basic 2 | 3 | This example demonstrates the basic use of `@webscopeio/react-health-check` package. 4 | 5 | ## Deploy with Vercel 6 | 7 | You can deploy this example with one click on Vercel: 8 | 9 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fwebscopeio%2Freact-health-check%2Ftree%2Fmaster%2Fexamples%2Fbasic&demo-title=Example%20-%20Basic&demo-description=This%20example%20demonstrates%20the%20basic%20use%20of%20%40webscopeio%2Freact-health-check%20package.) 10 | 11 | ## Run it locally 12 | 13 | This example is running on Next.js. To run it locally you need to instal all dependencies and start development server. 14 | 15 | ``` 16 | npm install 17 | npm run dev 18 | ``` 19 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@webscopeio/react-health-check": "~2.1.6", 12 | "next": "~9.5.4", 13 | "react": "~16.13.1", 14 | "react-dom": "~16.13.1", 15 | "react-toastify": "~6.0.8", 16 | "sass": "~1.26.10" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/basic/pages/_app.jsx: -------------------------------------------------------------------------------- 1 | import { ToastContainer } from 'react-toastify'; 2 | 3 | import 'react-toastify/dist/ReactToastify.css'; 4 | import '../static/styles.scss'; 5 | 6 | function MyApp({ Component, pageProps }) { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export default MyApp; 16 | -------------------------------------------------------------------------------- /examples/basic/pages/api/health.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | class ServiceHealth { 4 | constructor() { 5 | this.health = true; 6 | } 7 | 8 | get getHealth() { 9 | return this.health; 10 | } 11 | 12 | toggle() { 13 | this.health = !this.health; 14 | } 15 | } 16 | 17 | export const serviceHealth = new ServiceHealth(); 18 | 19 | export default (_req, res) => { 20 | const health = serviceHealth.getHealth; 21 | 22 | res.statusCode = health ? 200 : 500; 23 | res.json({ health }); 24 | }; 25 | -------------------------------------------------------------------------------- /examples/basic/pages/api/toggle.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | import { serviceHealth } from './health'; 4 | 5 | export default (_req, res) => { 6 | const health = serviceHealth.getHealth; 7 | serviceHealth.toggle(); 8 | 9 | res.statusCode = 200; 10 | res.json({ health }); 11 | }; 12 | -------------------------------------------------------------------------------- /examples/basic/pages/index.jsx: -------------------------------------------------------------------------------- 1 | import { useHealthCheck } from '@webscopeio/react-health-check'; 2 | import { toast } from 'react-toastify'; 3 | 4 | export default function Home() { 5 | const { available } = useHealthCheck({ 6 | service: { 7 | name: 'auth', 8 | url: '/api/health', 9 | }, 10 | onSuccess: ({ service, timestamp }) => { 11 | toast.success( 12 | <> 13 | Service "{service.name}" is available since:
{' '} 14 | {Date(timestamp).toString()} 🎉 15 | , 16 | ); 17 | }, 18 | onError: ({ service, timestamp }) => { 19 | toast.error( 20 | <> 21 | Service "{service.name}" is not available since:
{' '} 22 | {Date(timestamp).toString()} 😔 23 | , 24 | ); 25 | }, 26 | refreshInterval: 2000, 27 | }); 28 | 29 | return ( 30 |
31 |
32 |

Service health:

33 | 34 | {available ? 'Available' : 'Not available'} 35 | 36 |
37 |
38 | 41 |
42 | 43 |

44 | Made with ❤️ by{' '} 45 | 46 | Webscope.io 47 | 48 |

49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /examples/basic/static/styles.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap'); 2 | 3 | body, 4 | html { 5 | margin: 0; 6 | padding: 0; 7 | background-color: #fafafa; 8 | font-family: 'Roboto', sans-serif; 9 | } 10 | 11 | .container { 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | justify-content: center; 16 | width: 100%; 17 | height: 100vh; 18 | } 19 | 20 | .health-wrapper { 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | justify-content: center; 25 | margin-bottom: 2rem; 26 | 27 | .availability { 28 | font-size: 2rem; 29 | font-weight: bold; 30 | color: red; 31 | 32 | &.is-available { 33 | color: green; 34 | } 35 | } 36 | } 37 | 38 | p { 39 | margin: 0 0 0.5rem; 40 | } 41 | 42 | .author { 43 | margin-top: 5rem; 44 | } 45 | -------------------------------------------------------------------------------- /examples/global-conf/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /examples/global-conf/README.md: -------------------------------------------------------------------------------- 1 | # Example - Global configuration 2 | 3 | This example demonstrates the use of `@webscopeio/react-health-check` package with global configuration. 4 | 5 | ## Deploy with Vercel 6 | 7 | You can deploy this example with one click on Vercel: 8 | 9 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fwebscopeio%2Freact-health-check%2Ftree%2Fmaster%2Fexamples%2Fglobal-conf&demo-title=Example%20-%20Global%20configuration&demo-description=This%20example%20demonstrates%20the%20use%20of%20%40webscopeio%2Freact-health-check%20package%20with%20global%20configuration.) 10 | 11 | ## Run it locally 12 | 13 | This example is running on Next.js. To run it locally you need to instal all dependencies and start development server. 14 | 15 | ``` 16 | npm install 17 | npm run dev 18 | ``` 19 | -------------------------------------------------------------------------------- /examples/global-conf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "global-conf", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@webscopeio/react-health-check": "~2.1.6", 12 | "next": "~9.5.3", 13 | "react": "~16.13.1", 14 | "react-dom": "~16.13.1", 15 | "react-toastify": "~6.0.8", 16 | "sass": "~1.26.10" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/global-conf/pages/_app.jsx: -------------------------------------------------------------------------------- 1 | import { ToastContainer } from 'react-toastify'; 2 | import { HealthCheckConfig } from '@webscopeio/react-health-check'; 3 | import { toast } from 'react-toastify'; 4 | 5 | import 'react-toastify/dist/ReactToastify.css'; 6 | import '../static/styles.scss'; 7 | 8 | function MyApp({ Component, pageProps }) { 9 | return ( 10 | { 19 | toast.success( 20 | <> 21 | Service "{service.name}" is available since:
{' '} 22 | {Date(timestamp).toString()} 🎉 23 | , 24 | ); 25 | }, 26 | onError: ({ service, timestamp }) => { 27 | toast.error( 28 | <> 29 | Service "{service.name}" is not available since:
{' '} 30 | {Date(timestamp).toString()} 😔 31 | , 32 | ); 33 | }, 34 | refreshInterval: 2000, 35 | }} 36 | > 37 | 38 | 39 |
40 | ); 41 | } 42 | 43 | export default MyApp; 44 | -------------------------------------------------------------------------------- /examples/global-conf/pages/api/health.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | class ServiceHealth { 4 | constructor() { 5 | this.health = true; 6 | } 7 | 8 | get getHealth() { 9 | return this.health; 10 | } 11 | 12 | toggle() { 13 | this.health = !this.health; 14 | } 15 | } 16 | 17 | export const serviceHealth = new ServiceHealth(); 18 | 19 | export default (_req, res) => { 20 | const health = serviceHealth.getHealth; 21 | 22 | res.statusCode = health ? 200 : 500; 23 | res.json({ health }); 24 | }; 25 | -------------------------------------------------------------------------------- /examples/global-conf/pages/api/toggle.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | import { serviceHealth } from './health'; 4 | 5 | export default (_req, res) => { 6 | const health = serviceHealth.getHealth; 7 | serviceHealth.toggle(); 8 | 9 | res.statusCode = 200; 10 | res.json({ health }); 11 | }; 12 | -------------------------------------------------------------------------------- /examples/global-conf/pages/index.jsx: -------------------------------------------------------------------------------- 1 | import { useHealthCheck } from '@webscopeio/react-health-check'; 2 | 3 | export default function Home() { 4 | const { available } = useHealthCheck('auth'); 5 | 6 | return ( 7 |
8 |
9 |

Service health:

10 | 11 | {available ? 'Available' : 'Not available'} 12 | 13 |
14 |
15 | 18 |
19 | 20 |

21 | Made with ❤️ by{' '} 22 | 23 | Webscope.io 24 | 25 |

26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /examples/global-conf/static/styles.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap'); 2 | 3 | body, 4 | html { 5 | margin: 0; 6 | padding: 0; 7 | background-color: #fafafa; 8 | font-family: 'Roboto', sans-serif; 9 | } 10 | 11 | .container { 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | justify-content: center; 16 | width: 100%; 17 | height: 100vh; 18 | } 19 | 20 | .health-wrapper { 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | justify-content: center; 25 | margin-bottom: 2rem; 26 | 27 | .availability { 28 | font-size: 2rem; 29 | font-weight: bold; 30 | color: red; 31 | 32 | &.is-available { 33 | color: green; 34 | } 35 | } 36 | } 37 | 38 | p { 39 | margin: 0 0 0.5rem; 40 | } 41 | 42 | .author { 43 | margin-top: 5rem; 44 | } 45 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testRegex: '/tests/.*\\.spec\\.ts$', 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webscopeio/react-health-check", 3 | "version": "3.0.1", 4 | "description": "Lightweight React hook for checking health of API services.", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "rollup -c", 9 | "start": "rollup -c -w", 10 | "test": "jest --passWithNoTests", 11 | "types:check": "tsc --noEmit", 12 | "format": "prettier --write \"{src,tests,examples}/**/*.{ts,tsx}\"", 13 | "lint": "eslint \"{src,tests,examples}/**/*.{ts,tsx}\"", 14 | "lint:fix": "eslint \"{src,tests,examples}/**/*.{ts,tsx}\" --fix", 15 | "preversion": "npm test", 16 | "version": "npm run build", 17 | "postversion": "git push && git push --tags", 18 | "package-size": "npm pack && tar -xvzf *.tgz && rm -rf package *.tgz" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/webscopeio/react-health-check.git" 23 | }, 24 | "author": "Jozef Hruška ", 25 | "contributors": [ 26 | "Ján Vorčák " 27 | ], 28 | "license": "MIT", 29 | "keywords": [ 30 | "react", 31 | "health", 32 | "check", 33 | "hook", 34 | "detect", 35 | "offline", 36 | "api" 37 | ], 38 | "bugs": { 39 | "url": "https://github.com/webscopeio/react-health-check/issues" 40 | }, 41 | "homepage": "https://github.com/webscopeio/react-health-check#readme", 42 | "devDependencies": { 43 | "@rollup/plugin-typescript": "~8.3.4", 44 | "@types/jest": "~26.0.12", 45 | "@types/node": "~14.14.6", 46 | "@types/react": "~17.0.0", 47 | "@typescript-eslint/eslint-plugin": "~5.31.0", 48 | "@typescript-eslint/parser": "~5.31.0", 49 | "eslint": "~8.20.0", 50 | "eslint-config-prettier": "~8.5.0", 51 | "eslint-plugin-import": "~2.26.0", 52 | "eslint-plugin-react": "~7.30.1", 53 | "eslint-plugin-react-hooks": "~4.6.0", 54 | "husky": "~4.3.0", 55 | "jest": "~26.6.1", 56 | "jest-fetch-mock": "~3.0.3", 57 | "lint-staged": "~10.5.0", 58 | "prettier": "~2.2.1", 59 | "rollup": "~2.77.2", 60 | "rollup-plugin-terser": "~7.0.2", 61 | "rollup-plugin-typescript2": "~0.32.1", 62 | "ts-jest": "~26.4.0", 63 | "typescript": "~4.1.3" 64 | }, 65 | "peerDependencies": { 66 | "react": ">=16.8.0", 67 | "react-dom": ">=16.8.0" 68 | }, 69 | "husky": { 70 | "hooks": { 71 | "pre-commit": "lint-staged" 72 | } 73 | }, 74 | "lint-staged": { 75 | "*.{ts,tsx}": [ 76 | "npm run test", 77 | "npm run lint:fix", 78 | "npm run format" 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import { terser } from 'rollup-plugin-terser'; 3 | 4 | export default { 5 | input: 'src/index.ts', 6 | output: { 7 | dir: 'dist', 8 | format: 'cjs', 9 | }, 10 | plugins: [typescript(), terser()], 11 | external: ['react', 'react-dom'], 12 | }; 13 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | import { GlobalConfigInterface } from './types'; 4 | 5 | /* HealthCheckConfig 6 | ============================================================================= */ 7 | const HealthCheckConfig = createContext({ 8 | onError: () => null, 9 | onSuccess: () => null, 10 | }); 11 | 12 | HealthCheckConfig.displayName = 'HealthCheckConfig'; 13 | 14 | export const Provider = HealthCheckConfig.Provider; 15 | export default HealthCheckConfig; 16 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ServiceHealthCheckResult, 3 | ServiceState, 4 | Service, 5 | LocalConfigInterface, 6 | GlobalConfigInterface, 7 | } from './types'; 8 | 9 | /** 10 | * Helper function to check service health. 11 | * @param service Object describing service that should be checked 12 | */ 13 | export const checkServiceHealth = (service: Service): Promise => { 14 | return fetch(service.url, { 15 | method: 'GET', 16 | }) 17 | .then((response: Response) => { 18 | if (response?.status !== 200) { 19 | return { service, available: false, timestamp: Date.now() }; 20 | } 21 | 22 | return { service, available: true, timestamp: Date.now() }; 23 | }) 24 | .catch(() => { 25 | return { service, available: false, timestamp: Date.now() }; 26 | }); 27 | }; 28 | 29 | /** 30 | * Helper function to update service state when new health check result comes in. 31 | * @param prevState Previous service state 32 | * @param checkResult Health check result 33 | */ 34 | export const updateServiceState = ( 35 | prevState: ServiceState, 36 | checkResult: ServiceHealthCheckResult, 37 | ): ServiceState => ({ 38 | service: checkResult.service, 39 | available: checkResult.available, 40 | timestamp: 41 | !prevState.timestamp || prevState.available != checkResult.available 42 | ? checkResult.timestamp 43 | : prevState.timestamp, 44 | }); 45 | 46 | /** 47 | * Helper function to extract service from local or global config. 48 | * @param serviceName Name of service (used if services are defined globally) 49 | * @param localConfig Local configuration object 50 | * @param globalConfig Global configuration object (context) 51 | */ 52 | export const extractServiceConfig = ( 53 | serviceName: string, 54 | localConfig: LocalConfigInterface | Omit, 55 | globalConfig: GlobalConfigInterface, 56 | ): Service => { 57 | let service: Service = null; 58 | 59 | if (serviceName) { 60 | service = globalConfig?.services?.find((service) => service.name === serviceName); 61 | } 62 | 63 | if ((localConfig as LocalConfigInterface)?.service) { 64 | service = (localConfig as LocalConfigInterface)?.service; 65 | } 66 | 67 | return service; 68 | }; 69 | 70 | /** 71 | * Helper function to merge configurations (local configuration has a higher priority). 72 | * @param serviceName Name of service (used if services are defined globally) 73 | * @param localConfig Local configuration object 74 | * @param globalConfig Global configuration object (context) 75 | */ 76 | export const mergeConfigs = ( 77 | serviceName: string, 78 | localConfig: LocalConfigInterface | Omit = {}, 79 | globalConfig: GlobalConfigInterface = {}, 80 | ): LocalConfigInterface => ({ 81 | service: extractServiceConfig(serviceName, localConfig, globalConfig), 82 | onSuccess: localConfig.onSuccess ?? globalConfig.onSuccess, 83 | onError: localConfig.onError ?? globalConfig.onError, 84 | refreshInterval: localConfig.refreshInterval ?? globalConfig.refreshInterval ?? 5000, 85 | refreshWhileHidden: localConfig.refreshWhileHidden ?? globalConfig.refreshWhileHidden ?? false, 86 | }); 87 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Service, 3 | ServiceState, 4 | ServiceHealthCheckResult, 5 | LocalConfigInterface, 6 | GlobalConfigInterface, 7 | } from './types'; 8 | 9 | export { Provider as HealthCheckConfig } from './config'; 10 | export { default as useHealthCheck } from './use-health-check'; 11 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface CommonConfigInterface { 2 | onSuccess?: (state: ServiceState) => void; 3 | onError?: (state: ServiceState) => void; 4 | refreshInterval?: number; 5 | refreshWhileHidden?: boolean; 6 | } 7 | 8 | export interface GlobalConfigInterface extends CommonConfigInterface { 9 | services?: Service[]; 10 | } 11 | 12 | export interface LocalConfigInterface extends CommonConfigInterface { 13 | service: Service; 14 | } 15 | 16 | export type Service = { 17 | name: S; 18 | url: string; 19 | }; 20 | 21 | export type ServiceState = { 22 | service: Service; 23 | available: boolean; 24 | timestamp: number; 25 | }; 26 | 27 | export type ServiceHealthCheckResult = { 28 | service: Service; 29 | available: boolean; 30 | timestamp: number; 31 | }; 32 | 33 | export type ServiceHealthCheckReturn = { 34 | refresh: () => Promise; 35 | } & Omit; 36 | -------------------------------------------------------------------------------- /src/use-health-check.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useCallback, useContext, useMemo, useRef, useState } from 'react'; 2 | import HealthCheckConfig from './config'; 3 | 4 | import { checkServiceHealth, mergeConfigs, updateServiceState } from './helpers'; 5 | import { LocalConfigInterface, ServiceHealthCheckReturn } from './types'; 6 | 7 | function useHealthCheck(serviceName: S): ServiceHealthCheckReturn; 8 | function useHealthCheck( 9 | serviceName: S, 10 | localConfig?: Omit, 11 | ): ServiceHealthCheckReturn; 12 | function useHealthCheck(localConfig: LocalConfigInterface): ServiceHealthCheckReturn; 13 | 14 | function useHealthCheck( 15 | ...args: Array> 16 | ): ServiceHealthCheckReturn { 17 | const serviceName = typeof args[0] === 'string' ? args[0] : null; 18 | const localConfig = 19 | typeof args[0] === 'string' 20 | ? (args[1] as Omit) 21 | : (args[0] as LocalConfigInterface); 22 | 23 | const globalConfig = useContext(HealthCheckConfig); 24 | 25 | const config = mergeConfigs(serviceName, localConfig, globalConfig); 26 | 27 | const [, rerender] = useState(null); 28 | const serviceState = useRef({ 29 | service: config.service, 30 | available: true, 31 | timestamp: null, 32 | }); 33 | 34 | const checkService = useCallback(async (config: LocalConfigInterface) => { 35 | const { service } = config; 36 | const checkResult = await checkServiceHealth(service); 37 | 38 | if (serviceState.current.available != checkResult.available) { 39 | serviceState.current = updateServiceState(serviceState.current, checkResult); 40 | rerender({}); 41 | } 42 | }, []); 43 | 44 | const refreshService = useCallback(async () => { 45 | await checkService(config); 46 | }, [config, checkService]); 47 | 48 | const startCheckingInterval = useCallback(() => { 49 | return setInterval(() => { 50 | if ( 51 | typeof document !== 'undefined' && 52 | document.visibilityState === 'hidden' && 53 | !config.refreshWhileHidden 54 | ) { 55 | return; 56 | } 57 | 58 | checkService(config); 59 | }, config.refreshInterval); 60 | }, [checkService]); 61 | 62 | useEffect(() => { 63 | const intervalId = startCheckingInterval(); 64 | 65 | return () => { 66 | clearInterval(intervalId); 67 | }; 68 | }, [startCheckingInterval]); 69 | 70 | useEffect(() => { 71 | const state = serviceState.current; 72 | 73 | if (!state.timestamp) { 74 | return; 75 | } 76 | 77 | if (state.available) { 78 | typeof config.onSuccess === 'function' && config.onSuccess(state); 79 | return; 80 | } else { 81 | typeof config.onError === 'function' && config.onError(state); 82 | } 83 | }, [serviceState.current.available]); 84 | 85 | const memoizedState = useMemo(() => { 86 | return { 87 | service: serviceState.current.service, 88 | available: serviceState.current.available, 89 | timestamp: serviceState.current.timestamp, 90 | refresh: refreshService, 91 | }; 92 | }, [refreshService]); 93 | 94 | return memoizedState; 95 | } 96 | 97 | export default useHealthCheck; 98 | -------------------------------------------------------------------------------- /tests/helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { enableFetchMocks } from 'jest-fetch-mock'; 2 | 3 | import { 4 | checkServiceHealth, 5 | extractServiceConfig, 6 | mergeConfigs, 7 | updateServiceState, 8 | } from '../src/helpers'; 9 | import { 10 | GlobalConfigInterface, 11 | LocalConfigInterface, 12 | Service, 13 | ServiceHealthCheckResult, 14 | ServiceState, 15 | } from '../src/types'; 16 | 17 | describe('Helpers', () => { 18 | const MOCK_SERVICES: Service[] = [ 19 | { 20 | name: 'auth', 21 | url: 'https://example.com/auth/health', 22 | }, 23 | { 24 | name: 'payment', 25 | url: 'https://example.com/payment/health', 26 | }, 27 | ]; 28 | 29 | enableFetchMocks(); 30 | const RealDate = Date; 31 | 32 | beforeAll(() => { 33 | global.Date.now = jest.fn(() => 1598949813); 34 | }); 35 | 36 | afterAll(() => { 37 | global.Date = RealDate; 38 | }); 39 | 40 | describe('checkServiceHealth', () => { 41 | beforeEach(() => { 42 | fetchMock.resetMocks(); 43 | }); 44 | 45 | it('Sends a GET request to a correct endpoint.', async () => { 46 | await checkServiceHealth(MOCK_SERVICES[0]); 47 | 48 | expect(fetchMock.mock.calls).toHaveLength(1); 49 | expect(fetchMock.mock.calls[0][0]).toEqual(MOCK_SERVICES[0].url); 50 | expect(fetchMock.mock.calls[0][1].method).toEqual('GET'); 51 | }); 52 | 53 | it('Returns a correct result for a successful request.', async () => { 54 | fetchMock.mockResponse(JSON.stringify({}), { 55 | status: 200, 56 | }); 57 | 58 | const result = await checkServiceHealth(MOCK_SERVICES[1]); 59 | 60 | const expectedResult: ServiceHealthCheckResult = { 61 | service: MOCK_SERVICES[1], 62 | available: true, 63 | timestamp: Date.now(), 64 | }; 65 | 66 | expect(result).toEqual(expectedResult); 67 | }); 68 | 69 | it('Returns a correct result for a failed request.', async () => { 70 | fetchMock.mockReject(); 71 | 72 | const result = await checkServiceHealth(MOCK_SERVICES[1]); 73 | 74 | const expectedResult: ServiceHealthCheckResult = { 75 | service: MOCK_SERVICES[1], 76 | available: false, 77 | timestamp: Date.now(), 78 | }; 79 | 80 | expect(result).toEqual(expectedResult); 81 | }); 82 | }); 83 | 84 | describe('updateServiceState', () => { 85 | it('Updates service state correctly.', () => { 86 | const PREV_STATE: ServiceState = { 87 | service: MOCK_SERVICES[0], 88 | available: true, 89 | timestamp: null, 90 | }; 91 | 92 | const CHECK_RESULT: ServiceHealthCheckResult = { 93 | service: MOCK_SERVICES[0], 94 | available: false, 95 | timestamp: Date.now(), 96 | }; 97 | 98 | const NEXT_STATE: ServiceState = { 99 | service: MOCK_SERVICES[0], 100 | available: false, 101 | timestamp: Date.now(), 102 | }; 103 | 104 | expect(updateServiceState(PREV_STATE, CHECK_RESULT)).toEqual(NEXT_STATE); 105 | 106 | CHECK_RESULT.available = true; 107 | 108 | expect(updateServiceState(PREV_STATE, CHECK_RESULT)).toEqual({ 109 | ...NEXT_STATE, 110 | available: true, 111 | }); 112 | 113 | PREV_STATE.timestamp = Date.now() - 1; 114 | 115 | expect(updateServiceState(PREV_STATE, CHECK_RESULT)).toEqual({ 116 | ...NEXT_STATE, 117 | available: true, 118 | timestamp: Date.now() - 1, 119 | }); 120 | }); 121 | }); 122 | 123 | describe('extractServiceConfig', () => { 124 | it('Extracts service config correctly.', () => { 125 | expect( 126 | extractServiceConfig(MOCK_SERVICES[0].name, undefined, { 127 | services: [MOCK_SERVICES[0], MOCK_SERVICES[1]], 128 | }), 129 | ).toEqual(MOCK_SERVICES[0]); 130 | 131 | expect( 132 | extractServiceConfig( 133 | MOCK_SERVICES[0].name, 134 | { 135 | refreshInterval: 3000, 136 | }, 137 | { 138 | services: [MOCK_SERVICES[0], MOCK_SERVICES[1]], 139 | }, 140 | ), 141 | ).toEqual(MOCK_SERVICES[0]); 142 | 143 | expect( 144 | extractServiceConfig( 145 | null, 146 | { 147 | service: MOCK_SERVICES[1], 148 | refreshInterval: 3000, 149 | }, 150 | { 151 | services: [MOCK_SERVICES[0], MOCK_SERVICES[1]], 152 | }, 153 | ), 154 | ).toEqual(MOCK_SERVICES[1]); 155 | }); 156 | }); 157 | 158 | describe('mergeConfigs', () => { 159 | const localConfig: LocalConfigInterface = { 160 | service: MOCK_SERVICES[0], 161 | onSuccess: jest.fn(), 162 | onError: jest.fn(), 163 | refreshInterval: 1000, 164 | refreshWhileHidden: true, 165 | }; 166 | 167 | const globalConfig: GlobalConfigInterface = { 168 | services: MOCK_SERVICES, 169 | onSuccess: jest.fn(), 170 | onError: jest.fn(), 171 | refreshInterval: 10000, 172 | refreshWhileHidden: false, 173 | }; 174 | 175 | it('Merges configs correctly.', () => { 176 | expect(mergeConfigs(undefined, localConfig, globalConfig)).toEqual(localConfig); 177 | 178 | expect( 179 | mergeConfigs( 180 | undefined, 181 | { 182 | ...localConfig, 183 | refreshInterval: undefined, 184 | }, 185 | globalConfig, 186 | ), 187 | ).toEqual({ 188 | ...localConfig, 189 | refreshInterval: globalConfig.refreshInterval, 190 | }); 191 | 192 | expect(mergeConfigs('auth', undefined, globalConfig)).toEqual({ 193 | ...globalConfig, 194 | services: undefined, 195 | service: MOCK_SERVICES[0], 196 | }); 197 | }); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*", "tests/**/*", ".eslintrc.js"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "jsx": "react", 6 | "lib": ["esnext", "dom"], 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "noEmitOnError": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "outDir": "./dist", 15 | "types": ["node", "jest"], 16 | "target": "es5", 17 | "typeRoots": ["./src/types", "./node_modules/@types"] 18 | }, 19 | "include": ["src/**/*"] 20 | } 21 | --------------------------------------------------------------------------------