├── .github └── workflows │ └── website.yml ├── .gitignore ├── .prettierrc ├── biome.json ├── demo ├── .gitignore ├── cases │ ├── bug.ts │ ├── custom-style │ │ ├── customStyle.css │ │ └── customStyle.ts │ ├── custom-ui │ │ ├── customUi.css │ │ └── customUi.tsx │ ├── custom_slow.ts │ ├── custom_text.ts │ ├── expected_error.ts │ ├── offline.ts │ ├── retryTimer.ts │ ├── server_slow.ts │ ├── slow.ts │ └── success.ts ├── index.html ├── index.tsx ├── package.json ├── public │ ├── blank.gif │ ├── data.json │ ├── displayUntrustedHtml.html │ ├── game-of-thrones.json │ └── logo.svg ├── style.css ├── tsconfig.json ├── utils.ts └── vite.config.js ├── handli ├── package.json ├── src │ ├── ConnectionStateManager.ts │ ├── Handli.ts │ ├── checkInternetConnection.ts │ ├── index.ts │ ├── messages.ts │ ├── showMessage.ts │ └── utils │ │ ├── assert.ts │ │ └── projectInfo.ts └── tsconfig.json ├── logo.svg ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── readme.md /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | name: Website 2 | on: 3 | push: 4 | branches: 5 | - main 6 | permissions: 7 | contents: write 8 | jobs: 9 | website: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: pnpm/action-setup@v4 14 | with: 15 | version: 9.10.0 16 | - run: pnpm install 17 | - run: echo "BASE_URL=/handli/" >> $GITHUB_ENV 18 | - run: pnpm build 19 | - uses: JamesIves/github-pages-deploy-action@4.1.4 20 | with: 21 | branch: gh-pages 22 | folder: demo/dist/ 23 | # Remove previous build 24 | clean: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: false 2 | tabWidth: 2 3 | singleQuote: true 4 | printWidth: 120 5 | trailingComma: all 6 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "files": { 4 | "ignore": ["dist/", "package.json", "demo/cases/", "demo/public/data.json"] 5 | }, 6 | "formatter": { 7 | "indentWidth": 2, 8 | "indentStyle": "space" 9 | }, 10 | "javascript": { 11 | "formatter": { 12 | "semicolons": "asNeeded", 13 | "lineWidth": 120, 14 | "quoteStyle": "single", 15 | "trailingComma": "all" 16 | } 17 | }, 18 | "linter": { 19 | "enabled": false 20 | }, 21 | "vcs": { 22 | "enabled": true, 23 | "clientKind": "git" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | -------------------------------------------------------------------------------- /demo/cases/bug.ts: -------------------------------------------------------------------------------- 1 | import handli from 'handli' 2 | import { Console, getServerErrorSimulator } from '../utils' 3 | 4 | export { run } 5 | export { console } 6 | 7 | const console = new Console() 8 | 9 | const { serverErrorSimulator, fetch } = getServerErrorSimulator() 10 | 11 | async function run() { 12 | serverErrorSimulator.install() 13 | setTimeout(serverErrorSimulator.remove, 2000) 14 | 15 | const response = await handli( 16 | () => fetch('data.json') 17 | ) 18 | 19 | console.log( 20 | '+++ Response +++', 21 | await response.text() 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /demo/cases/custom-style/customStyle.css: -------------------------------------------------------------------------------- 1 | body.hasHandliModal > 2 | :not(.handliModal) { 3 | filter: blur(2px); 4 | } 5 | .handliModal { 6 | background: rgba(255, 255, 255, 0.3); 7 | } 8 | .handliModalContent { 9 | border: 2px solid #f0f0f0; 10 | border-radius: 10px; 11 | position: relative; 12 | padding-left: 53px; 13 | } 14 | .handliModal.handliIsWarning 15 | .handliModalContent::before { 16 | content: "\26A0"; 17 | font-size: 2em; 18 | position: absolute; 19 | top: 4px; 20 | left: 11px; 21 | color: #f2f215; 22 | } 23 | -------------------------------------------------------------------------------- /demo/cases/custom-style/customStyle.ts: -------------------------------------------------------------------------------- 1 | import handli from 'handli' 2 | import { Console, wait, getOfflineSimulator } from '../../utils' 3 | 4 | export { run } 5 | export { console } 6 | 7 | const console = new Console() 8 | const { offlineSimulator, fetch } = getOfflineSimulator() 9 | 10 | const id = 'custom_style' 11 | const addCss = async () => { 12 | const customCss = (await import('./customStyle.css?raw')).default 13 | 14 | const styleEl = window.document.createElement('style') 15 | Object.assign(styleEl, { 16 | id, 17 | type: 'text/css', 18 | innerHTML: customCss, 19 | }) 20 | document.head.appendChild(styleEl) 21 | } 22 | const removeCss = () => { 23 | const styleEl = document.getElementById(id) 24 | styleEl.parentElement.removeChild(styleEl) 25 | } 26 | 27 | async function run() { 28 | offlineSimulator.install() 29 | setTimeout(offlineSimulator.remove, 2000) 30 | 31 | await addCss() 32 | let resp 33 | try { 34 | resp = await handli(() => fetch('data.json')) 35 | } finally { 36 | removeCss() 37 | } 38 | 39 | console.log(await resp.text()) 40 | } 41 | -------------------------------------------------------------------------------- /demo/cases/custom-ui/customUi.css: -------------------------------------------------------------------------------- 1 | .Toastify__close-button { 2 | display: none!important; 3 | } 4 | .Toastify__toast-container * { 5 | cursor: default!important; 6 | } 7 | [disabled] { 8 | opacity: 0.3; 9 | } 10 | -------------------------------------------------------------------------------- /demo/cases/custom-ui/customUi.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import handli from 'handli' 4 | import { Console, getServerDownSimulator } from '../../utils' 5 | 6 | import { ToastContainer, toast as reactToastify } from 'react-toastify' 7 | import 'react-toastify/dist/ReactToastify.css' 8 | import './customUi.css' 9 | 10 | export { run } 11 | export { console } 12 | 13 | const toasterRoot = document.body.appendChild(document.createElement('div')) 14 | const toastContainer = ( 15 | 24 | ) 25 | ReactDOM.render(toastContainer, toasterRoot) 26 | 27 | const { serverDownSimulator, fetch } = getServerDownSimulator() 28 | 29 | const console = new Console() 30 | 31 | function toast(msg) { 32 | const div = (__html) =>
33 | const toastId = reactToastify.error(div(msg), { 34 | position: 'top-right', 35 | autoClose: false, 36 | hideProgressBar: false, 37 | closeOnClick: false, 38 | pauseOnHover: false, 39 | draggable: false, 40 | }) 41 | const update = (msg) => reactToastify.update(toastId, { render: div(msg) }) 42 | const close = () => reactToastify.dismiss(toastId) 43 | return { update, close } 44 | } 45 | 46 | async function run() { 47 | handli.showMessage = (msg) => { 48 | const toaster = toast(msg) 49 | return { 50 | update: (msg) => toaster.update(msg), 51 | close: () => { 52 | toaster.close() 53 | }, 54 | } 55 | } 56 | 57 | serverDownSimulator.install() 58 | setTimeout(serverDownSimulator.remove, 2000) 59 | 60 | const response = await handli( 61 | () => fetch('data.json') 62 | ) 63 | 64 | console.log( 65 | '+++ Response +++', 66 | await response.text() 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /demo/cases/custom_slow.ts: -------------------------------------------------------------------------------- 1 | import handli from 'handli' 2 | import { Console, wait, getSlowInternetSimulator } from '../utils' 3 | 4 | export { run } 5 | export { console } 6 | 7 | const console = new Console() 8 | 9 | const { slowInternetSimulator, fetch } = getSlowInternetSimulator(1100) 10 | 11 | async function run() { 12 | slowInternetSimulator.install() 13 | 14 | // Set a generous request timeout. 15 | // (When setting `timeout` Handli will 16 | // handle a slow internet as well as a 17 | // slow server.) 18 | handli.timeout = 3000 19 | 20 | // The thresholds are not tested against 21 | // your server but against low-latency and 22 | // highly-available servers such a google.com 23 | handli.thresholdSlowInternet = 1000 24 | handli.thresholdNoInternet = 2000 25 | 26 | const response = await handli( 27 | () => fetch('data.json') 28 | ) 29 | 30 | console.log( 31 | '+++ Response +++', 32 | await response.text() 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /demo/cases/custom_text.ts: -------------------------------------------------------------------------------- 1 | import handli from 'handli' 2 | import { Console, getServerErrorSimulator } from '../utils' 3 | 4 | export { run } 5 | export { console } 6 | 7 | const console = new Console() 8 | 9 | const { serverErrorSimulator, fetch } = getServerErrorSimulator() 10 | 11 | async function run() { 12 | serverErrorSimulator.install() 13 | setTimeout(serverErrorSimulator.remove, 2000) 14 | 15 | // Inspect `handli.messages` to 16 | // see the list of messages. 17 | handli.messages.ERROR = 18 | 'An unexpected error occured.\n\n' + 19 | 'We have been notified and we are \n' + 20 | 'working on fixing the issue.\n' 21 | handli.messages.RETRYING_IN = 22 | (sec) => 'Reytring in: ' + sec 23 | 24 | const response = await handli( 25 | () => fetch('data.json') 26 | ) 27 | 28 | console.log( 29 | '+++ Response +++', 30 | await response.text() 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /demo/cases/expected_error.ts: -------------------------------------------------------------------------------- 1 | import handli from 'handli' 2 | import { Console } from '../utils' 3 | 4 | export { run } 5 | export { console } 6 | 7 | const console = new Console() 8 | 9 | async function run() { 10 | const err = await handli(async () => { 11 | try { 12 | return await ( 13 | fetch('https://doesnt-exist.example.org') 14 | ) 15 | } catch (err) { 16 | return 'This error is custom handled.' 17 | } 18 | }) 19 | 20 | console.log('+++ Handled error +++', err) 21 | } 22 | -------------------------------------------------------------------------------- /demo/cases/offline.ts: -------------------------------------------------------------------------------- 1 | import handli from 'handli' 2 | import { Console, wait, getOfflineSimulator } from '../utils' 3 | 4 | export { run } 5 | export { console } 6 | 7 | const console = new Console() 8 | 9 | const { offlineSimulator, fetch } = getOfflineSimulator() 10 | 11 | async function run() { 12 | offlineSimulator.install() 13 | setTimeout(offlineSimulator.remove, 2000) 14 | 15 | const response = await handli( 16 | () => fetch('data.json') 17 | ) 18 | 19 | console.log( 20 | '+++ Response +++', 21 | await response.text() 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /demo/cases/retryTimer.ts: -------------------------------------------------------------------------------- 1 | import { Console, getServerDownSimulator } from '../utils' 2 | import handli from 'handli' 3 | 4 | export { run } 5 | export { console } 6 | 7 | const { serverDownSimulator, fetch } = getServerDownSimulator() 8 | const console = new Console() 9 | 10 | async function run() { 11 | serverDownSimulator.install() 12 | setTimeout(serverDownSimulator.remove, 5000) 13 | 14 | handli.retryTimer = 15 | (seconds) => seconds ? seconds + 1 : 1 16 | 17 | const response = await handli( 18 | () => fetch('data.json') 19 | ) 20 | 21 | console.log( 22 | '+++ Response +++', 23 | await response.text() 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /demo/cases/server_slow.ts: -------------------------------------------------------------------------------- 1 | import { Console, getSlowServerSimulator } from '../utils' 2 | import handli from 'handli' 3 | 4 | export { run } 5 | export { console } 6 | 7 | const { slowServerSimulator, fetch } = getSlowServerSimulator() 8 | 9 | const console = new Console() 10 | 11 | async function run() { 12 | slowServerSimulator.install() 13 | 14 | // If you provide a timeout then 15 | // Handli handles a slow server. 16 | handli.timeoutServer = 2000 17 | 18 | const response = await handli( 19 | () => fetch('data.json') 20 | ) 21 | 22 | console.log( 23 | '+++ Response +++', 24 | await response.text() 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /demo/cases/slow.ts: -------------------------------------------------------------------------------- 1 | import handli from 'handli' 2 | import { Console, wait, getSlowInternetSimulator } from '../utils' 3 | 4 | export { run } 5 | export { console } 6 | 7 | const console = new Console() 8 | 9 | const { slowInternetSimulator, fetch } = getSlowInternetSimulator() 10 | 11 | async function run() { 12 | slowInternetSimulator.install() 13 | 14 | // If you provide a timeout then 15 | // Handli handles a slow internet. 16 | handli.timeoutInternet = 1500 17 | 18 | const response = await handli( 19 | () => fetch('data.json') 20 | ) 21 | 22 | console.log( 23 | '+++ Response +++', 24 | await response.text() 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /demo/cases/success.ts: -------------------------------------------------------------------------------- 1 | import handli from 'handli' 2 | import { Console } from '../utils' 3 | 4 | export { run } 5 | export { console } 6 | 7 | const console = Console() 8 | 9 | async function run() { 10 | const response = await handli( 11 | () => fetch('data.json') 12 | ) 13 | 14 | console.log( 15 | '+++ Response +++', 16 | await response.text() 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /demo/index.tsx: -------------------------------------------------------------------------------- 1 | import handli from 'handli' 2 | import Prism from 'prismjs' 3 | import 'prismjs/themes/prism.css' 4 | import './style.css' 5 | import React from 'react' 6 | import { useState, useEffect } from 'react' 7 | import ReactDOM from 'react-dom' 8 | 9 | main() 10 | 11 | async function main() { 12 | const examples = await loadExamples() 13 | ReactDOM.render(, document.body.appendChild(document.createElement('div'))) 14 | } 15 | 16 | function LiveDemo({ examples }: { examples: ExampleType[] }) { 17 | return ( 18 | 19 |
20 | 21 | 22 | ) 23 | } 24 | 25 | function Header() { 26 | const githubIcon = ( 27 | github.com/ 37 | ) 38 | const handliIcon = ( 39 | Handli 45 | ) 46 | return ( 47 |

58 | 59 | {handliIcon} 60 |
61 |
{githubIcon} brillout/handli
62 |
63 |

64 | ) 65 | } 66 | 67 | function Intro({ examples }: { examples: ExampleType[] }) { 68 | const handliBehavior = ( 69 | 70 | Handli blocks the UI, blocks the code (it doesn't resolve nor rejects the promise), and shows an error message to 71 | the user. 72 | 73 | ) 74 | return ( 75 | 76 | 77 | 78 | 79 | When the request succeeds or the request fails but your code handles the error. 80 | 81 |

Handli does nothing and simply returns what your request function returns.

82 | category === 'expected')} /> 83 |
84 | 85 | When the user is offline or has a poor internet connection. 86 |

{handliBehavior} The request is retried when the user reconnects.

87 | category === 'connection')} /> 88 |
89 | 90 | 91 | When your server is not replying or replies with an error not handled by your code. 92 | 93 |

{handliBehavior} The request is periodically retried.

94 | category === 'bug')} /> 95 |
96 |
97 | {/* 98 |
99 | Options 100 |
101 | */} 102 | 103 | 104 | Options 105 | category === 'options1')} /> 106 | 107 | 108 | 109 | category === 'options2')} /> 110 | 111 | 112 | 113 | category === 'options3')} /> 114 | 115 | 116 |
117 | ) 118 | } 119 | function InlineCode({ children }) { 120 | return ( 121 |
122 |       {children}
123 |     
124 | ) 125 | } 126 | function ColumnsWrapper({ children }) { 127 | return
{children}
128 | } 129 | function Columns({ children, className = '' }) { 130 | return
{children}
131 | } 132 | function ColumnTitle({ children, ...props }) { 133 | return

{children}

134 | } 135 | function ColumnTitlePlaceholder() { 136 | return I'm invisible 137 | } 138 | function Column({ title, children, className = '' }: any) { 139 | return ( 140 |
141 | {title && {title}} 142 | {children} 143 |
144 | ) 145 | } 146 | 147 | function CaseExplanation({ children }) { 148 | return ( 149 |

150 | {children} 151 |

152 | ) 153 | } 154 | 155 | async function loadExamples() { 156 | const examplesPromise: ExamplePromise[] = [ 157 | [ 158 | 'expected', 159 | import('./cases/success?raw'), 160 | import('./cases/success'), 161 | 'Success', 162 |
When the server replies with a 2xx status code.
, 163 | ], 164 | [ 165 | 'expected', 166 | import('./cases/expected_error.js?raw'), 167 | import('./cases/expected_error.js'), 168 | 'Handled Error', 169 |
When the server replies with an error handled by your code.
, 170 | ], 171 | [ 172 | 'connection', 173 | import('./cases/offline.js?raw'), 174 | import('./cases/offline.js'), 175 | 'Offline', 176 |
When the user is not connected to the internet.
, 177 | ], 178 | [ 179 | 'connection', 180 | import('./cases/slow.js?raw'), 181 | import('./cases/slow.js'), 182 | 'Slow Internet', 183 |
When the user has a slow internet connection.
, 184 | ], 185 | [ 186 | 'bug', 187 | import('./cases/bug.js?raw'), 188 | import('./cases/bug.js'), 189 | 'Unhandled Error', 190 |
When the server replies with an error not handled by your code.
, 191 | ], 192 | [ 193 | 'bug', 194 | import('./cases/server_slow.js?raw'), 195 | import('./cases/server_slow.js'), 196 | 'Unresponsive Server', 197 |
When the server is down or taking a long time to reply.
, 198 | ], 199 | [ 200 | 'options1', 201 | import('./cases/retryTimer.js?raw'), 202 | import('./cases/retryTimer.js'), 203 | 'Retry Timer', 204 |
Customize when the request is retried.
, 205 | ], 206 | [ 207 | 'options1', 208 | import('./cases/custom_slow.js?raw'), 209 | import('./cases/custom_slow.js'), 210 | 'Custom Slow Threshold', 211 |
Customize when Handli considers the network to be "slow".
, 212 | ], 213 | [ 214 | 'options2', 215 | import('./cases/custom-style/customStyle.css?raw'), 216 | import('./cases/custom-style/customStyle.js'), 217 | 'Custom Style', 218 |
Customize the modal.
, 219 | { codeLang: 'css', dontStrip: true }, 220 | ], 221 | [ 222 | 'options2', 223 | import('./cases/custom_text.js?raw'), 224 | import('./cases/custom_text.js'), 225 | 'Custom Text', 226 |
Customize the texts shown to.
, 227 | ], 228 | [ 229 | 'options3', 230 | import('./cases/custom-ui/customUi.jsx?raw'), 231 | import('./cases/custom-ui/customUi.jsx'), 232 | 'Custom UI', 233 |
Customize how messages are shown to the user.
, 234 | ], 235 | ] 236 | const examples: ExampleType[] = await Promise.all( 237 | examplesPromise.map(async (examplePromise: ExamplePromise) => { 238 | const [category, codeSourcePromise, codeModulePromise, title, description, options] = examplePromise 239 | const [{ default: codeSource }, codeModule] = await Promise.all([codeSourcePromise, codeModulePromise]) 240 | return [category, codeSource, codeModule, title, description, options] 241 | }), 242 | ) 243 | 244 | return examples 245 | } 246 | 247 | function Examples({ examples }: { examples: ExampleType[] }) { 248 | return ( 249 |
250 | {examples.map((example, key) => ( 251 | 252 | ))} 253 |
254 | ) 255 | } 256 | 257 | type Category = 'expected' | 'connection' | 'bug' | 'options1' | 'options2' | 'options3' 258 | type Options = { codeLang?: 'javascript' | 'css' | undefined | string; dontStrip?: boolean } 259 | type ExampleBase = [Category, string, CodeModule, string, JSX.Element] 260 | type ExampleBasePromise = [Category, Promise<{ default: string }>, Promise, string, JSX.Element] 261 | type ExampleType = ExampleBase | [...ExampleBase, Options] 262 | type ExamplePromise = ExampleBasePromise | [...ExampleBasePromise, Options] 263 | 264 | function Example({ 265 | example: [_category, codeSource, codeModule, title, description, { codeLang = 'javascript', dontStrip = false } = {}], 266 | }: { example: ExampleType }) { 267 | const headerId = title.toLowerCase().split(' ').join('-') 268 | const textView = ( 269 |
270 |

{title}

271 | {description} 272 |
273 | ) 274 | 275 | return ( 276 |
277 | {textView} 278 |
279 | {getCodView({ codeSource, codeLang, dontStrip })} 280 | {} 281 |
282 |
283 | ) 284 | } 285 | 286 | function ResultView({ codeModule }) { 287 | const [history, setHistory] = useState([]) 288 | 289 | return ( 290 | 291 |
292 | 308 | Result 309 | 310 | 323 |
324 | {history === null ? ( 325 | waiting result... 326 | ) : ( 327 |
328 |           {history.join('\n')}
329 |         
330 | )} 331 |
332 | ) 333 | 334 | async function onRun() { 335 | setHistory(null) 336 | codeModule.console.history.length = 0 337 | await codeModule.run() 338 | revertOptions() 339 | setHistory(codeModule.console.history) 340 | } 341 | } 342 | type CodeModule = { 343 | run: () => void 344 | console: { history: any } 345 | } 346 | 347 | const optionsPristine = { ...handli, messages: { ...handli.messages } } 348 | function revertOptions() { 349 | Object.assign(handli, { ...optionsPristine, messages: { ...optionsPristine.messages } }) 350 | for (let key in handli) if (!(key in optionsPristine)) delete handli[key] 351 | } 352 | 353 | function getCodView({ codeSource, codeLang, dontStrip }) { 354 | if (!dontStrip) { 355 | codeSource = stripContext(codeSource) 356 | } 357 | 358 | const codeHtml = Prism.highlight(codeSource, Prism.languages[codeLang], codeLang) 359 | 360 | return ( 361 |
362 |       
363 |     
364 | ) 365 | } 366 | 367 | function stripContext(codeSource: string) { 368 | const codeSourceLines = codeSource.split('\n') 369 | const runFnLine = codeSourceLines.findIndex((line) => line.includes('function run')) 370 | return codeSourceLines.slice(runFnLine + 1, -2).join('\n') 371 | } 372 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "vite dev", 4 | "build": "vite build", 5 | "preview": "vite build && vite preview" 6 | }, 7 | "dependencies": { 8 | "@types/react": "^18.2.45", 9 | "@types/react-dom": "^18.2.18", 10 | "handli": "0.0.2", 11 | "prismjs": "^1.15.0", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-toastify": "^4.4.3", 15 | "typescript": "^5.3.3" 16 | }, 17 | "devDependencies": { 18 | "vite": "^5.1.6" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /demo/public/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brillout/handli/74cbbe708c2e64c210c215d1f2127a0b81b6e3f5/demo/public/blank.gif -------------------------------------------------------------------------------- /demo/public/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "messages": [ 3 | "Hey there.", 4 | "Welcome to Handli." 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /demo/public/displayUntrustedHtml.html: -------------------------------------------------------------------------------- 1 | 2 | 11 | -------------------------------------------------------------------------------- /demo/public/game-of-thrones.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "tv-show", 3 | "name": "Game of Thrones", 4 | "characters": [ 5 | { 6 | "name": "Daenerys Targaryen", 7 | "id": "daenerys" 8 | }, 9 | { 10 | "name": "Jon Snow", 11 | "id": "jon" 12 | }, 13 | { 14 | "name": "Cersei Lannister", 15 | "id": "cersei" 16 | }, 17 | { 18 | "name": "Petyr Baelish", 19 | "id": "petyr" 20 | }, 21 | { 22 | "name": "Bran Stark", 23 | "id": "bran" 24 | }, 25 | { 26 | "name": "Tyrion Lannister", 27 | "id": "tyrion" 28 | }, 29 | { 30 | "name": "Varys", 31 | "id": "varys" 32 | }, 33 | { 34 | "name": "Tormund", 35 | "id": "tormund" 36 | }, 37 | { 38 | "name": "Samwell Tarly", 39 | "id": "samwell" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /demo/public/logo.svg: -------------------------------------------------------------------------------- 1 | ../../logo.svg -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | /* 5 | body > div { 6 | display: flex; 7 | justify-content: center; 8 | flex-direction: column; 9 | align-items: center; 10 | } 11 | body > div > div { 12 | margin-bottom: 30px; 13 | } 14 | */ 15 | .cls_columns_wrapper { 16 | width: 100%; 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | } 21 | .cls_columns { 22 | display: flex; 23 | justify-content: space-around; 24 | width: 100%; 25 | max-width: 1300px; 26 | /* 27 | justify-content: center; 28 | flex-direction: column; 29 | align-items: center; 30 | */ 31 | } 32 | /* 33 | .cls_case + .cls_case { 34 | padding-left: 15px; 35 | } 36 | */ 37 | .cls_column { 38 | padding: 7px; 39 | width: 400px; 40 | } 41 | .cls_column h3 { 42 | margin-top: 50px; 43 | } 44 | 45 | .cls_example { 46 | font-size: 0.9em; 47 | margin-bottom: 60px; 48 | } 49 | .cls_example pre { 50 | font-size: 0.9em; 51 | } 52 | .cls_example code { 53 | line-height: 1.25em!important; 54 | } 55 | 56 | /* stripes copy pasted from https://css-tricks.com/stripes-css/ */ 57 | /* 58 | .cls_expected { 59 | background-color: #080; 60 | background-color: #00ff001f; 61 | background: repeating-linear-gradient( 62 | 45deg, 63 | transparent, 64 | transparent 10px, 65 | rgba(0,255,0,.07) 10px, 66 | rgba(0,255,0,.07) 20px 67 | ); 68 | } 69 | .cls_internet { 70 | background-color: #880; 71 | background-color: #ffff0030; 72 | background: repeating-linear-gradient( 73 | 45deg, 74 | transparent, 75 | transparent 10px, 76 | rgba(255,255,0,.13) 10px, 77 | rgba(255,255,0,.13) 20px 78 | ); 79 | } 80 | .cls_bug { 81 | background-color: #800; 82 | background-color: #ff000021; 83 | background: repeating-linear-gradient( 84 | 45deg, 85 | transparent, 86 | transparent 10px, 87 | rgba(255,0,0,.05) 10px, 88 | rgba(255,0,0,.05) 20px 89 | ); 90 | } 91 | */ 92 | 93 | /* 94 | .cls_case { 95 | border-width: 0 0 0 5px; 96 | border-style: solid; 97 | } 98 | .cls_expected { 99 | border-color: #00ff003f; 100 | } 101 | .cls_internet { 102 | border-color: #ffff0050; 103 | } 104 | .cls_bug { 105 | border-color: #ff000031; 106 | } 107 | */ 108 | 109 | 110 | 111 | h2, 112 | h3 { 113 | border-width: 0 0 0 5px; 114 | border-style: solid; 115 | margin-left: -5px; 116 | padding-left: 5px; 117 | } 118 | .cls_green h2, 119 | .cls_green h3 { 120 | border-color: #00ff008f; 121 | } 122 | .cls_yellow h2, 123 | .cls_yellow h3 { 124 | border-color: #ffff0090; 125 | } 126 | .cls_red h2, 127 | .cls_red h3 { 128 | border-color: #ff000071; 129 | } 130 | .cls_gray h2, 131 | .cls_gray h3 { 132 | border-color: #cdcdcd; 133 | } 134 | 135 | 136 | 137 | /* copy-pasted & adapated from https://reactjs.org/ */ 138 | html { 139 | box-sizing: border-box; 140 | font-family: Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif; 141 | font-weight: 300; 142 | font-style: normal; 143 | -webkit-font-smoothing: antialiased; 144 | -moz-osx-font-smoothing: grayscale; 145 | } 146 | 147 | .cls_code_section { 148 | margin-top: 0.5em; 149 | display: grid; 150 | background-color: #fcfcfc; 151 | background-color: #fdfdfd; 152 | } 153 | .cls_code_section pre { 154 | margin: 0; 155 | } 156 | .cls_code_section > * { 157 | border-style: solid; 158 | border-color: #ddd; 159 | border-color: #f5f2f0; 160 | border-width: 0 2px 0 2px; 161 | padding: 5px 10px; 162 | } 163 | .cls_code_section > :first-child { 164 | border-radius: 10px 10px 0 0; 165 | border-width: 0; 166 | } 167 | .cls_code_section > :last-child { 168 | border-radius: 0 0 10px 10px; 169 | border-width: 0 2px 2px 2px; 170 | min-height: 35px; 171 | padding-bottom: 11px; 172 | } 173 | 174 | pre { 175 | overflow: auto; 176 | padding-right: 4px!important; 177 | } 178 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "moduleResolution": "Node", 6 | "lib": ["ES2021", "DOM", "DOM.Iterable"], 7 | "types": ["vite/client"], 8 | "esModuleInterop": true, 9 | "jsx": "react-jsx" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo/utils.ts: -------------------------------------------------------------------------------- 1 | import handli from 'handli' 2 | 3 | export { assert } 4 | export { fetch } 5 | export { Console } 6 | export { wait } 7 | export { getServerDownSimulator } 8 | export { getOfflineSimulator } 9 | export { getSlowServerSimulator } 10 | export { getServerErrorSimulator } 11 | export { getSlowInternetSimulator } 12 | 13 | const NON_EXISTING_SERVER = 'https://does-not-exist.example.org/foo' 14 | 15 | function assert(condition) { 16 | if (condition) return 17 | throw new Error('Internal demo error.') 18 | } 19 | 20 | async function fetch(url) { 21 | const response = await window.fetch(url) 22 | return await response.text() 23 | } 24 | 25 | function Console() { 26 | const history = [] 27 | 28 | return { history, log } 29 | 30 | function log(...args) { 31 | window.console.log(...args) 32 | history.push(args.join('\n')) 33 | } 34 | } 35 | 36 | function wait(seconds) { 37 | let resolve 38 | const p = new Promise((r) => (resolve = r)) 39 | setTimeout(resolve, seconds * 1000) 40 | return p 41 | } 42 | 43 | function getServerDownSimulator() { 44 | const fetch = (url) => { 45 | if (installed) { 46 | return window.fetch(NON_EXISTING_SERVER) 47 | } else { 48 | return window.fetch(url) 49 | } 50 | } 51 | 52 | let installed = false 53 | const serverDownSimulator = { 54 | install: () => { 55 | installed = true 56 | }, 57 | remove: () => { 58 | installed = false 59 | }, 60 | } 61 | 62 | return { serverDownSimulator, fetch } 63 | } 64 | 65 | function getOfflineSimulator() { 66 | let installed = false 67 | let resolveInternet = null 68 | const offlineSimulator = { 69 | install: () => { 70 | installed = true 71 | handli.checkInternetConnection = async () => { 72 | return { 73 | noInternet: true, 74 | noLanConnection: true, 75 | awaitInternetConnection: () => { 76 | if (!installed) { 77 | return 78 | } 79 | return new Promise((r) => (resolveInternet = r)) 80 | }, 81 | } 82 | } 83 | }, 84 | remove: () => { 85 | delete handli.checkInternetConnection 86 | installed = false 87 | if (resolveInternet) { 88 | resolveInternet() 89 | } 90 | }, 91 | } 92 | 93 | const fetch = (url) => { 94 | if (installed) { 95 | return window.fetch(NON_EXISTING_SERVER) 96 | } else { 97 | return window.fetch(url) 98 | } 99 | } 100 | 101 | return { offlineSimulator, fetch } 102 | } 103 | 104 | function getServerErrorSimulator() { 105 | let installed 106 | const serverErrorSimulator = { 107 | install: () => { 108 | installed = true 109 | }, 110 | remove: () => { 111 | installed = false 112 | }, 113 | } 114 | const fetch = (url) => { 115 | if (installed) { 116 | return window.fetch('does-not-exist-path') 117 | } else { 118 | return window.fetch(url) 119 | } 120 | } 121 | 122 | return { serverErrorSimulator, fetch } 123 | } 124 | 125 | function getSlowServerSimulator() { 126 | let installed 127 | const slowServerSimulator = { 128 | install: () => { 129 | installed = true 130 | }, 131 | } 132 | 133 | const fetch = async (url) => { 134 | if (installed) { 135 | await wait(4) 136 | } 137 | return window.fetch(url) 138 | } 139 | 140 | return { slowServerSimulator, fetch } 141 | } 142 | function getSlowInternetSimulator(fastestPing = 500) { 143 | let installed 144 | const slowInternetSimulator = { 145 | install: () => { 146 | handli.checkInternetConnection = async () => { 147 | wait(fastestPing / 1000) 148 | return { 149 | noInternet: false, 150 | noLanConnection: false, 151 | fastestPing, 152 | awaitInternetConnection: () => assert(false), 153 | } 154 | } 155 | installed = true 156 | }, 157 | } 158 | 159 | const fetch = async (url) => { 160 | if (installed) { 161 | await wait(4) 162 | } 163 | return window.fetch(url) 164 | } 165 | 166 | return { slowInternetSimulator, fetch } 167 | } 168 | -------------------------------------------------------------------------------- /demo/vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | appType: 'mpa', 3 | base: process.env.BASE_URL || '/', 4 | server: { port: 3000 }, 5 | preview: { port: 3000 }, 6 | } 7 | -------------------------------------------------------------------------------- /handli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "handli", 3 | "description": "Library that handles network errors", 4 | "version": "0.0.2", 5 | "dependencies": {}, 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "dev": "tsc --watch", 9 | "build": "rm -rf dist/ && tsc", 10 | "// === Release ===": "", 11 | "release": "release-me patch", 12 | "release:commit": "release-me commit" 13 | }, 14 | "files": [ 15 | "dist/" 16 | ], 17 | "devDependencies": { 18 | "@brillout/release-me": "^0.3.8", 19 | "typescript": "^5.4.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /handli/src/ConnectionStateManager.ts: -------------------------------------------------------------------------------- 1 | import { assert, assertUsage } from './utils/assert' 2 | 3 | export default ConnectionStateManager 4 | 5 | function ConnectionStateManager(getCheckOptions) { 6 | let connectionState = null 7 | let connectionStatePromise = null 8 | 9 | return { 10 | deprecateState, 11 | getConnectionState, 12 | checkNowIfMissing, 13 | } 14 | 15 | function getConnectionState() { 16 | assert_connectionState() 17 | return connectionState 18 | } 19 | function deprecateState() { 20 | connectionState = null 21 | connectionStatePromise = null 22 | } 23 | async function checkNowIfMissing() { 24 | if (!connectionStatePromise) { 25 | connectionStatePromise = checkConnection() 26 | } 27 | await connectionStatePromise 28 | } 29 | 30 | async function checkConnection() { 31 | const { checkInternetConnection, thresholdNoInternet, thresholdSlowInternet } = getCheckOptions() 32 | 33 | const conn = await checkInternetConnection(thresholdNoInternet) 34 | const { noInternet, fastestPing } = conn 35 | assert([true, false].includes(noInternet)) 36 | assert(noInternet === true || fastestPing >= 0) 37 | 38 | assertUsage(thresholdSlowInternet > 0, '`thresholdSlowInternet` is missing') 39 | const slowInternet = !noInternet && fastestPing >= thresholdSlowInternet 40 | 41 | connectionState = { 42 | slowInternet, 43 | ...conn, 44 | } 45 | } 46 | 47 | function assert_connectionState() { 48 | assert( 49 | connectionState === null || 50 | ([true, false].includes(connectionState.noInternet) && 51 | [true, false].includes(connectionState.noLanConnection) && 52 | [true, false].includes(connectionState.slowInternet) && 53 | (connectionState.noInternet === true || connectionState.fastestPing >= 0) && 54 | connectionState.awaitInternetConnection instanceof Function), 55 | { connectionState }, 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /handli/src/Handli.ts: -------------------------------------------------------------------------------- 1 | export default Handli 2 | 3 | import ConnectionStateManager from './ConnectionStateManager' 4 | import { assert, assertUsage, assertWarning } from './utils/assert' 5 | 6 | function Handli() { 7 | Object.assign(handli, { 8 | timeout: null, 9 | timeoutServer: null, 10 | timeoutInternet: null, 11 | thresholdSlowInternet: 500, 12 | thresholdNoInternet: 900, 13 | retryTimer: (seconds) => (!seconds ? 3 : Math.ceil(seconds * 1.5)), 14 | }) 15 | 16 | const failedRequests = [] 17 | 18 | const connectionStateManager = getConnectionStateManager() 19 | 20 | return handli 21 | 22 | async function handleFailure() { 23 | if (failedRequests.length === 0) { 24 | closeModal() 25 | previousSeconds = undefined 26 | connectionStateManager.deprecateState() 27 | return 28 | } 29 | 30 | const connectionState = connectionStateManager.getConnectionState() 31 | // console.log('c', connectionState, failedRequests.length); 32 | 33 | if (connectionState !== null && connectionState.noInternet === true) { 34 | await handleOffline(connectionState) 35 | } else if (connectionState !== null && connectionState.slowInternet === true) { 36 | // @ts-ignore 37 | await handleSlowInternet(connectionState) 38 | } else if (hasSlowResponse()) { 39 | await handleSlowServer() 40 | } else { 41 | await handleBugs() 42 | } 43 | 44 | /* 45 | await Promise.all([ 46 | antiFlakyUI(), 47 | resolveFailedRequests(), 48 | ]); 49 | */ 50 | connectionStateManager.deprecateState() 51 | await resolveFailedRequests() 52 | 53 | handleFailure() 54 | } 55 | function hasSlowResponse() { 56 | return getRequestsWith('SLOW_RESPONSE').length > 0 57 | } 58 | async function handleOffline(connectionState) { 59 | assert(connectionState.noInternet === true) 60 | const { noLanConnection } = connectionState 61 | if (noLanConnection) { 62 | showWarningModal(getMsg('OFFLINE'), getMsg('RETRYING_WHEN_ONLINE')) 63 | } else { 64 | showWarningModal(getMsg('OFFLINE_PROBABLY'), getMsg('RETRYING_STILL')) 65 | } 66 | 67 | await connectionState.awaitInternetConnection() 68 | 69 | if (noLanConnection) { 70 | showWarningModal(getMsg('ONLINE'), getMsg('RETRYING_NOW')) 71 | } 72 | } 73 | async function handleSlowInternet() { 74 | showWarningModal(getMsg('SLOW_INTERNET'), getMsg('RETRYING_STILL')) 75 | } 76 | async function handleSlowServer() { 77 | showErrorModal(getMsg('SLOW_SERVER'), getMsg('RETRYING_STILL')) 78 | } 79 | async function handleBugs() { 80 | await wait((timeLeft) => { 81 | showErrorModal(getMsg('ERROR'), getMsgRetryingIn(timeLeft)) 82 | }) 83 | 84 | showErrorModal(getMsg('ERROR'), getMsg('RETRYING_NOW')) 85 | } 86 | async function resolveFailedRequests() { 87 | for (let request of getRequestsWith('SLOW_RESPONSE')) { 88 | await request.retryRequest() 89 | } 90 | for (let request of getRequestsWith('ERROR_RESPONSE')) { 91 | await request.retryRequest() 92 | } 93 | for (let request of getRequestsWith('NO_RESPONSE')) { 94 | await request.retryRequest() 95 | } 96 | } 97 | function getRequestsWith(failureState) { 98 | const STATES = ['SLOW_RESPONSE', 'ERROR_RESPONSE', 'NO_RESPONSE'] 99 | assert(STATES.includes(failureState)) 100 | assert( 101 | failedRequests.every((req) => STATES.includes(req.requestState.failureState)), 102 | failedRequests, 103 | ) 104 | return failedRequests.filter((request) => request.requestState.failureState === failureState) 105 | } 106 | 107 | async function handli(requestFunction) { 108 | const isBrowser = typeof window !== 'undefined' && window.document 109 | 110 | const skipHandli = !isBrowser || getOption('disableHandli') 111 | 112 | if (skipHandli) { 113 | return requestFunction() 114 | } 115 | 116 | const requestState: any = {} 117 | 118 | let resolveValue 119 | const resolvedValuePromise = new Promise((r) => (resolveValue = r)) 120 | 121 | await tryRequest() 122 | 123 | if (requestState.failureState) { 124 | addFailedRequest() 125 | } 126 | 127 | return resolvedValuePromise 128 | 129 | function addFailedRequest() { 130 | const failedRequest = { 131 | requestState, 132 | retryRequest, 133 | } 134 | failedRequests.push(failedRequest) 135 | 136 | const resolveValue_ = resolveValue 137 | resolveValue = (resolvedValue) => { 138 | const idx = failedRequests.indexOf(failedRequest) 139 | assert(idx >= 0) 140 | failedRequests.splice(idx, 1) 141 | 142 | resolveValue_(resolvedValue) 143 | } 144 | 145 | if (failedRequests.length === 1) { 146 | handleFailure() 147 | } 148 | } 149 | 150 | async function retryRequest() { 151 | if (requestState.failureState) { 152 | await tryRequest() 153 | } 154 | } 155 | 156 | var responsePromise 157 | async function tryRequest() { 158 | let responseReceived 159 | 160 | let resolveAttempt 161 | const attemptPromise = new Promise((r) => (resolveAttempt = r)) 162 | 163 | const checkConnectionStateTimeout = getCheckConnectionStateTimeout() 164 | 165 | handleResponse() 166 | handleConnectionStatus() 167 | handleFlakyInternet() 168 | handleFlakyServer() 169 | 170 | return attemptPromise 171 | 172 | async function handleResponse() { 173 | if (!responsePromise) { 174 | responsePromise = requestReponse() 175 | } 176 | await responsePromise 177 | responseReceived = true 178 | resolveAttempt() 179 | } 180 | async function requestReponse() { 181 | assert(!responsePromise) 182 | let returnedValue 183 | try { 184 | returnedValue = await requestFunction() 185 | } catch (err) { 186 | console.error(err) 187 | requestState.failureState = 'NO_RESPONSE' 188 | responsePromise = null 189 | await connectionStateManager.checkNowIfMissing() 190 | return 191 | } 192 | 193 | assert_returnedValue(returnedValue) 194 | if (isErrorResponse(returnedValue)) { 195 | console.error(returnedValue) 196 | requestState.failureState = 'ERROR_RESPONSE' 197 | if (checkConnectionStateTimeout) { 198 | await connectionStateManager.checkNowIfMissing() 199 | } 200 | } else { 201 | requestState.failureState = null 202 | resolveValue(returnedValue) 203 | } 204 | 205 | responsePromise = null 206 | } 207 | 208 | function handleConnectionStatus() { 209 | if (!checkConnectionStateTimeout) { 210 | return 211 | } 212 | setTimeout(connectionStateManager.checkNowIfMissing, checkConnectionStateTimeout) 213 | } 214 | function handleFlakyInternet() { 215 | const timeout = getInternetTimeout() 216 | if (!timeout) return 217 | setTimeout(async () => { 218 | if (responseReceived) return 219 | 220 | const { noInternet, slowInternet } = await retrieveConnectionState() 221 | if (responseReceived) return 222 | if (noInternet) return 223 | if (!slowInternet) return 224 | 225 | requestState.failureState = 'SLOW_RESPONSE' 226 | resolveAttempt() 227 | }, timeout) 228 | } 229 | function handleFlakyServer() { 230 | const timeout = getServerTimeout() 231 | if (!timeout) return 232 | setTimeout(async () => { 233 | if (responseReceived) return 234 | 235 | const { noInternet, slowInternet } = await retrieveConnectionState() 236 | if (responseReceived) return 237 | if (noInternet) return 238 | if (slowInternet) return 239 | 240 | requestState.failureState = 'SLOW_RESPONSE' 241 | resolveAttempt() 242 | }, timeout) 243 | } 244 | } 245 | async function retrieveConnectionState() { 246 | await connectionStateManager.checkNowIfMissing() 247 | const connectionState = connectionStateManager.getConnectionState() 248 | 249 | const { noInternet, slowInternet } = connectionState 250 | assert([true, false].includes(noInternet)) 251 | assert([true, false].includes(slowInternet)) 252 | return { noInternet, slowInternet } 253 | } 254 | } 255 | 256 | var previousSeconds 257 | function wait(timeListener) { 258 | const seconds = getOption('retryTimer')(previousSeconds) 259 | assertUsage(seconds > 0 && (previousSeconds === undefined || seconds >= previousSeconds), 'Wrong `retryTimer`') 260 | let secondsLeft = (previousSeconds = seconds) 261 | const callListener = () => { 262 | if (secondsLeft === 0) { 263 | resolve() 264 | return 265 | } 266 | timeListener(secondsLeft) 267 | --secondsLeft 268 | window.setTimeout(callListener, 1000) 269 | } 270 | let resolve 271 | const promise = new Promise((resolver) => (resolve = resolver)) 272 | callListener() 273 | return promise 274 | } 275 | 276 | function getOption(prop, { required, subProp }: any = {}) { 277 | let val = handli[prop] 278 | if (subProp) { 279 | val = val && val[subProp] 280 | } 281 | 282 | assert(!required || val, { val, prop, subProp }) 283 | 284 | return val 285 | } 286 | function getMsgRetryingIn(timeLeft) { 287 | const msgFn = getMsg('RETRYING_IN', true) 288 | if (!(msgFn instanceof Function)) { 289 | return strToHtml(msgFn) 290 | } 291 | const msg = msgFn(timeLeft) 292 | return strToHtml(msg) 293 | } 294 | function getMsg(msgCode: any, isFn?: any) { 295 | let msg = getOption('messages', { subProp: msgCode, required: true }) 296 | return isFn ? msg : strToHtml(msg) 297 | } 298 | function strToHtml(str) { 299 | assertUsage(str && str.split, str) 300 | const html = str.split('\n').join('
') 301 | return html 302 | } 303 | function getInternetTimeout() { 304 | return getOption('timeoutInternet') || getOption('timeout') 305 | } 306 | function getServerTimeout() { 307 | return getOption('timeoutServer') || getOption('timeout') 308 | } 309 | function getCheckConnectionStateTimeout() { 310 | const timeout = getOption('timeout') 311 | const timeoutServer = getOption('timeoutServer') 312 | const timeoutInternet = getOption('timeoutInternet') 313 | const thresholdNoInternet = getOption('thresholdNoInternet') 314 | const thresholdSlowInternet = getOption('thresholdSlowInternet') 315 | 316 | assertUsage( 317 | thresholdSlowInternet < thresholdNoInternet, 318 | '`thresholdSlowInternet` should be lower than `thresholdNoInternet`', 319 | ) 320 | 321 | const minTimeout = Math.min(getInternetTimeout() || Infinity, getServerTimeout() || Infinity) 322 | if (minTimeout === Infinity) { 323 | return null 324 | } 325 | const checkTimeout = minTimeout - thresholdNoInternet 326 | assertUsage( 327 | checkTimeout >= 100, 328 | '`thresholdNoInternet` should be lower than `timeout`, `timeoutInternet`, and `timeoutServer`', 329 | ) 330 | return checkTimeout 331 | } 332 | function noServerTimeout() { 333 | return !getServerTimeout() 334 | } 335 | function noInternetTimeout() { 336 | return !getInternetTimeout() 337 | } 338 | 339 | var currentModal 340 | function showWarningModal(...args) { 341 | _showModal(true, ...args) 342 | } 343 | function showErrorModal(...args) { 344 | _showModal(false, ...args) 345 | } 346 | function _showModal(isWarning, ...messageHtmls) { 347 | const messageHtml = messageHtmls.filter(Boolean).join('
') 348 | 349 | if (currentModal && currentModal.isWarning === isWarning) { 350 | currentModal.update(messageHtml) 351 | } else { 352 | closeModal() 353 | const { update, close } = getOption('showMessage')(messageHtml, isWarning) 354 | currentModal = { 355 | isWarning, 356 | update, 357 | close, 358 | } 359 | } 360 | } 361 | function closeModal() { 362 | if (currentModal) currentModal.close() 363 | currentModal = null 364 | } 365 | 366 | function getConnectionStateManager() { 367 | // @ts-ignore 368 | return new ConnectionStateManager(getCheckOptions) 369 | 370 | function getCheckOptions() { 371 | const checkInternetConnection = getOption('checkInternetConnection') 372 | const thresholdNoInternet = getOption('thresholdNoInternet') 373 | const thresholdSlowInternet = getOption('thresholdSlowInternet') 374 | return { 375 | checkInternetConnection, 376 | thresholdNoInternet, 377 | thresholdSlowInternet, 378 | } 379 | } 380 | } 381 | } 382 | 383 | function assert_resolvedValue(resolvedValue) { 384 | if (isFetchLikeResponse(resolvedValue)) { 385 | const response = resolvedValue 386 | assert_fetchLikeResponse(response) 387 | const { status } = response 388 | assert(200 <= status && status <= 299) 389 | } 390 | } 391 | function assert_returnedValue(returnedValue) { 392 | if (isFetchLikeResponse(returnedValue)) { 393 | const response = returnedValue 394 | assert_fetchLikeResponse(response) 395 | } 396 | } 397 | function assert_fetchLikeResponse(response) { 398 | const isSuccessCode = 200 <= response.status && response.status <= 299 399 | assertWarning(isSuccessCode === response.ok, 'Unexpected response object. Are you using a fetch-like library?', { 400 | onlyOnce: true, 401 | }) 402 | } 403 | function isFetchLikeResponse(response) { 404 | const yes = response instanceof Object && [true, false].includes(response.ok) && 'status' in response 405 | return yes 406 | } 407 | function isErrorResponse(response) { 408 | if (!isFetchLikeResponse(response)) { 409 | return false 410 | } 411 | const isSuccessCode = 200 <= response.status && response.status <= 299 412 | return !isSuccessCode 413 | } 414 | 415 | /* 416 | function antiFlakyUI() { 417 | return sleep(0.5); 418 | } 419 | // TODO rename 420 | function sleep(seconds) { 421 | let resolve; 422 | const p = new Promise(r => resolve=r); 423 | setTimeout(resolve, seconds*1000); 424 | return p; 425 | } 426 | */ 427 | -------------------------------------------------------------------------------- /handli/src/checkInternetConnection.ts: -------------------------------------------------------------------------------- 1 | export default checkInternetConnection 2 | 3 | import { assert, assertUsage } from './utils/assert' 4 | 5 | async function checkInternetConnection(timeout: number) { 6 | assertUsage(timeout, '`checkInternetConnection` requires argument `timeout`') 7 | let noInternet = false 8 | let noLanConnection = hasNoLanConnection() 9 | let fastestPing 10 | 11 | if (noLanConnection) { 12 | noInternet = true 13 | } else { 14 | fastestPing = await getFastestPing(timeout) 15 | assert(fastestPing === null || fastestPing >= 0) 16 | if (fastestPing === null) { 17 | noInternet = true 18 | } 19 | } 20 | 21 | /* 22 | console.log(noInternet); 23 | console.log(fastestPing); 24 | */ 25 | 26 | return { 27 | noInternet, 28 | noLanConnection, 29 | fastestPing, 30 | awaitInternetConnection, 31 | } 32 | } 33 | function hasNoLanConnection() { 34 | return window.navigator.onLine === false 35 | } 36 | function hasLanConnection() { 37 | return window.navigator.onLine === true 38 | } 39 | function noLanConnectionInfo() { 40 | return ![true, false].includes(window.navigator.onLine) 41 | } 42 | async function awaitInternetConnection() { 43 | await awaitLanConnection() 44 | await awaitPing() 45 | } 46 | async function awaitLanConnection() { 47 | if (hasLanConnection()) { 48 | return 49 | } 50 | if (noLanConnectionInfo()) { 51 | return 52 | } 53 | 54 | let resolve 55 | const promise = new Promise((r) => (resolve = r)) 56 | window.addEventListener('online', resolve) 57 | 58 | await promise 59 | } 60 | async function getFastestPing(timeout?: any) { 61 | const fastestPing: any = await PromiseRaceSuccess([ 62 | pingImage('https://www.google.com/favicon.ico', timeout), 63 | pingImage('https://www.facebook.com/favicon.ico', timeout), 64 | pingImage('https://www.cloudflare.com/favicon.ico', timeout), 65 | pingImage('https://www.amazon.com/favicon.ico', timeout), 66 | /* 67 | pingImage('https://www.apple.com/favicon.ico', timeout), 68 | pingImage('https://www.microsoft.com/favicon.ico', timeout), 69 | */ 70 | ]) 71 | assert(fastestPing === null || fastestPing >= 0) 72 | return fastestPing 73 | } 74 | function PromiseRaceSuccess(promises) { 75 | // Promise.race doesn't ignore rejected promises 76 | let resolve 77 | const racePromise = new Promise((r) => (resolve = r)) 78 | Promise.all( 79 | promises.map(async (pingPromise) => { 80 | const rtt = await pingPromise 81 | /* 82 | console.log(rtt, pingPromise.imgUrl); 83 | */ 84 | assert(rtt === null || rtt >= 0) 85 | if (rtt) { 86 | resolve(rtt) 87 | } 88 | }), 89 | ).then(() => { 90 | resolve(null) 91 | }) 92 | return racePromise 93 | } 94 | function pingImage(imgUrl, timeout) { 95 | assert(imgUrl) 96 | let resolve 97 | const pingPromise: any = new Promise((r) => (resolve = r)) 98 | const img: any = document.createElement('img') 99 | 100 | // @ts-ignore 101 | img.onload = () => resolve(new Date() - start) 102 | img.onerror = () => resolve(null) 103 | if (timeout) setTimeout(() => resolve(null), timeout) 104 | 105 | const start = new Date().getTime() 106 | const src = imgUrl + '?_=' + start 107 | img.src = src 108 | 109 | pingPromise.imgUrl = src 110 | 111 | return pingPromise 112 | } 113 | async function awaitPing() { 114 | while (true) { 115 | const fastestPing = await getFastestPing() 116 | assert(fastestPing === null || fastestPing >= 0) 117 | if (fastestPing !== null) return 118 | await wait(0.5) 119 | } 120 | } 121 | function wait(seconds) { 122 | let resolve 123 | const p = new Promise((r) => (resolve = r)) 124 | setTimeout(resolve, seconds * 1000) 125 | return p 126 | } 127 | -------------------------------------------------------------------------------- /handli/src/index.ts: -------------------------------------------------------------------------------- 1 | import Handli from './Handli' 2 | import showMessage from './showMessage' 3 | import messages from './messages' 4 | import checkInternetConnection from './checkInternetConnection' 5 | import { assertWarning } from './utils/assert' 6 | 7 | // @ts-ignore 8 | const handli = new Handli() 9 | 10 | Object.assign(handli, { 11 | showMessage, 12 | checkInternetConnection, 13 | messages, 14 | }) 15 | 16 | export default handli 17 | 18 | if (typeof window !== 'undefined') { 19 | if ('handli' in window) { 20 | assertWarning(false, "We didn't `window.handli = new Handli()` because `window.handli` is already defined", { 21 | onlyOnce: true, 22 | }) 23 | } else { 24 | // @ts-ignore 25 | window.handli = handli 26 | } 27 | } 28 | 29 | /* 30 | function showMessage(...args) { 31 | console.log(...args); 32 | return {close: () => {console.log('close');}, update: (...args) => {console.log(...args)}}; 33 | } 34 | */ 35 | -------------------------------------------------------------------------------- /handli/src/messages.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | ERROR: 'Something unexpected happened.', 3 | OFFLINE: 'You are offline.', 4 | OFFLINE_PROBABLY: 'You seem to be offline.', 5 | ONLINE: 'You are back online.', 6 | SLOW_INTERNET: 'You seem to have a slow internet connection.', 7 | SLOW_SERVER: 'Server is not replying.', 8 | RETRYING_STILL: 'Still trying...', 9 | RETRYING_WHEN_ONLINE: 'Connect to the internet to proceed.', 10 | RETRYING_NOW: 'Retrying...', 11 | RETRYING_IN: (seconds: number) => 'Retrying in ' + seconds + ' second' + (seconds === 1 ? '' : 's') + '.', 12 | } 13 | -------------------------------------------------------------------------------- /handli/src/showMessage.ts: -------------------------------------------------------------------------------- 1 | export default showMessages 2 | 3 | const CSS = ` 4 | body.hasHandliModal { 5 | overflow: hidden !important; 6 | } 7 | .handliModal { 8 | position: fixed; 9 | height: 100vh; 10 | width: 100vw; 11 | z-index: 99999999999999; 12 | top: 0; 13 | left: 0; 14 | background: rgba(0,0,0,0.7); 15 | 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | } 20 | .handliModalContent { 21 | padding: 10px 20px; 22 | border-radius: 5px; 23 | background: white; 24 | border-width: 0 0 0 10px; 25 | border-style: solid; 26 | border-color: #ff6868; 27 | } 28 | .handliIsWarning > * { 29 | border-color: #fff252; 30 | } 31 | ` 32 | /* 33 | .handliModalWrapper > :first-child > :first-child::before { 34 | content: ""; 35 | display: block; 36 | position: absolute; 37 | top: 0; 38 | left: 0; 39 | height: 100%; 40 | width: 0; 41 | border-radius 42 | width: 10px; 43 | background: red; 44 | } 45 | */ 46 | 47 | function showMessages(html, isWarning) { 48 | addCss() 49 | /* 50 | const modalWrapper = window.document.createElement('div'); 51 | modalWrapper.setAttribute('class', 'handliModalWrapper'); 52 | modalWrapper.appendChild(handliModal); 53 | */ 54 | 55 | const handliModal = window.document.createElement('div') 56 | handliModal.setAttribute('class', 'handliModal' + (isWarning ? ' handliIsWarning' : '')) 57 | 58 | const handliModalContent = window.document.createElement('div') 59 | handliModalContent.setAttribute('class', 'handliModalContent') 60 | handliModal.appendChild(handliModalContent) 61 | 62 | /* 63 | const modalImageEl = window.document.createElement('div'); 64 | handliModalContent.appendChild(modalImageEl); 65 | modalImageEl.innerHTML = "\u26A0"; 66 | Object.assign(modalImageEl.style, { 67 | fontSize: '3em', 68 | paddingRight: '20px', 69 | }); 70 | 71 | const modalContentEl = window.document.createElement('div'); 72 | handliModalContent.appendChild(modalContentEl); 73 | Object.assign(modalContentEl.style, { 74 | alignSelf: 'center', 75 | }); 76 | */ 77 | 78 | const bodyCls = 'hasHandliModal' 79 | document.body.classList.add(bodyCls) 80 | document.body.appendChild(handliModal) 81 | 82 | update(html) 83 | 84 | return { close, update } 85 | 86 | function close() { 87 | removeElement(handliModal) 88 | document.body.classList.remove(bodyCls) 89 | } 90 | function update(html) { 91 | handliModalContent.innerHTML = html 92 | } 93 | } 94 | 95 | function removeElement(element) { 96 | element.parentElement.removeChild(element) 97 | } 98 | 99 | function prependChild(parent, child) { 100 | const { firstChild } = parent 101 | if (!firstChild) { 102 | parent.appendChild(child) 103 | } else { 104 | parent.insertBefore(child, firstChild) 105 | } 106 | } 107 | 108 | function addCss() { 109 | const id = 'handliStyle' 110 | if (document.getElementById(id)) { 111 | return 112 | } 113 | const css = window.document.createElement('style') 114 | Object.assign(css, { 115 | id, 116 | type: 'text/css', 117 | innerHTML: CSS, 118 | }) 119 | //document.head -> https://caniuse.com/#feat=documenthead 120 | prependChild(document.head, css) 121 | } 122 | -------------------------------------------------------------------------------- /handli/src/utils/assert.ts: -------------------------------------------------------------------------------- 1 | export { assert } 2 | export { assertUsage } 3 | export { assertWarning } 4 | 5 | import { projectInfo } from './projectInfo' 6 | 7 | const errorPrefix = `[${projectInfo.npmPackageName}@${projectInfo.projectVersion}]` as const 8 | const internalErrorPrefix = `${errorPrefix}[Bug]` as const 9 | const usageErrorPrefix = `${errorPrefix}[Wrong Usage]` as const 10 | const warningPrefix = `${errorPrefix}[Warning]` as const 11 | 12 | function assert(condition: unknown, debugInfo?: unknown): asserts condition { 13 | if (condition) { 14 | return 15 | } 16 | 17 | const debugStr = (() => { 18 | if (!debugInfo) { 19 | return '' 20 | } 21 | const debugInfoSerialized = typeof debugInfo === 'string' ? debugInfo : '`' + JSON.stringify(debugInfo) + '`' 22 | return `Debug info (this is for the ${projectInfo.projectName} maintainers; you can ignore this): ${debugInfoSerialized}.` 23 | })() 24 | 25 | const internalError = new Error( 26 | [ 27 | `${internalErrorPrefix} You stumbled upon a bug in ${projectInfo.projectName}'s source code.`, 28 | `Reach out at ${projectInfo.githubRepository}/issues/new and include this error stack (the error stack is usually enough to fix the problem).`, 29 | 'A maintainer will fix the bug (usually under 24 hours).', 30 | `Do not hesitate to reach out as it makes ${projectInfo.projectName} more robust.`, 31 | debugStr, 32 | ].join(' '), 33 | ) 34 | 35 | throw internalError 36 | } 37 | 38 | function assertUsage(condition: unknown, errorMessage: string): asserts condition { 39 | if (condition) { 40 | return 41 | } 42 | const whiteSpace = errorMessage.startsWith('[') ? '' : ' ' 43 | const usageError = new Error(`${usageErrorPrefix}${whiteSpace}${errorMessage}`) 44 | throw usageError 45 | } 46 | 47 | let alreadyLogged: Set = new Set() 48 | function assertWarning( 49 | condition: unknown, 50 | errorMessage: string, 51 | { onlyOnce, showStackTrace }: { onlyOnce: boolean | string; showStackTrace?: true }, 52 | ): void { 53 | if (condition) { 54 | return 55 | } 56 | const msg = `${warningPrefix} ${errorMessage}` 57 | if (onlyOnce) { 58 | const key = onlyOnce === true ? msg : onlyOnce 59 | if (alreadyLogged.has(key)) { 60 | return 61 | } else { 62 | alreadyLogged.add(key) 63 | } 64 | } 65 | if (showStackTrace) { 66 | console.warn(new Error(msg)) 67 | } else { 68 | console.warn(msg) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /handli/src/utils/projectInfo.ts: -------------------------------------------------------------------------------- 1 | export { projectInfo } 2 | 3 | const PROJECT_VERSION = '0.0.2' 4 | 5 | const projectInfo = { 6 | projectName: 'Handli', 7 | projectVersion: PROJECT_VERSION, 8 | npmPackageName: 'handli', 9 | githubRepository: 'https://github.com/brillout/handli', 10 | } as const 11 | -------------------------------------------------------------------------------- /handli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "target": "ES2020", 5 | "module": "ES2020", 6 | "declaration": true 7 | }, 8 | "rootDir": "./src/", 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Handli 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "========= Dev": "", 4 | "dev": "pnpm run handli:build && pnpm run demo:dev", 5 | "========= Build & preview demo": "", 6 | "build": "pnpm run handli:build && pnpm run demo:build", 7 | "preview": "pnpm run handli:build && pnpm run demo:preview", 8 | "========= Handli": "", 9 | "handli:dev": "cd handli/ && pnpm run dev", 10 | "handli:build": "cd handli/ && pnpm run build", 11 | "========= Demo": "", 12 | "demo:dev": "cd demo/ && pnpm run dev", 13 | "demo:build": "cd demo/ && pnpm run build", 14 | "demo:preview": "cd demo/ && pnpm run preview", 15 | "========= Formatting": "", 16 | "format": "pnpm run format:biome", 17 | "format:prettier": "git ls-files | egrep '\\.(json|js|jsx|css|ts|tsx|vue|mjs|cjs)$' | grep --invert-match package.json | xargs pnpm exec prettier --write", 18 | "format:biome": "biome format --write .", 19 | "format:check": "biome format . || (echo 'Fix formatting by running `$ pnpm run -w format`.' && exit 1)", 20 | "========= Reset": "", 21 | "reset": "git clean -Xdf && pnpm install && pnpm run build", 22 | "========= Only allow pnpm; forbid yarn & npm": "", 23 | "preinstall": "npx only-allow pnpm" 24 | }, 25 | "devDependencies": { 26 | "@biomejs/biome": "1.5.3", 27 | "prettier": "3.2.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@biomejs/biome': 12 | specifier: 1.5.3 13 | version: 1.5.3 14 | prettier: 15 | specifier: 3.2.5 16 | version: 3.2.5 17 | 18 | demo: 19 | dependencies: 20 | '@types/react': 21 | specifier: ^18.2.45 22 | version: 18.2.66 23 | '@types/react-dom': 24 | specifier: ^18.2.18 25 | version: 18.2.22 26 | handli: 27 | specifier: 0.0.2 28 | version: 0.0.2 29 | prismjs: 30 | specifier: ^1.15.0 31 | version: 1.29.0 32 | react: 33 | specifier: ^18.2.0 34 | version: 18.2.0 35 | react-dom: 36 | specifier: ^18.2.0 37 | version: 18.2.0(react@18.2.0) 38 | react-toastify: 39 | specifier: ^4.4.3 40 | version: 4.5.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) 41 | typescript: 42 | specifier: ^5.3.3 43 | version: 5.4.2 44 | devDependencies: 45 | vite: 46 | specifier: ^5.1.6 47 | version: 5.1.6 48 | 49 | handli: 50 | devDependencies: 51 | '@brillout/release-me': 52 | specifier: ^0.3.8 53 | version: 0.3.8 54 | typescript: 55 | specifier: ^5.4.2 56 | version: 5.4.2 57 | 58 | packages: 59 | 60 | '@babel/code-frame@7.23.5': 61 | resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} 62 | engines: {node: '>=6.9.0'} 63 | 64 | '@babel/helper-validator-identifier@7.22.20': 65 | resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} 66 | engines: {node: '>=6.9.0'} 67 | 68 | '@babel/highlight@7.23.4': 69 | resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} 70 | engines: {node: '>=6.9.0'} 71 | 72 | '@babel/runtime@7.24.0': 73 | resolution: {integrity: sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==} 74 | engines: {node: '>=6.9.0'} 75 | 76 | '@biomejs/biome@1.5.3': 77 | resolution: {integrity: sha512-yvZCa/g3akwTaAQ7PCwPWDCkZs3Qa5ONg/fgOUT9e6wAWsPftCjLQFPXBeGxPK30yZSSpgEmRCfpGTmVbUjGgg==} 78 | engines: {node: '>=14.*'} 79 | hasBin: true 80 | 81 | '@biomejs/cli-darwin-arm64@1.5.3': 82 | resolution: {integrity: sha512-ImU7mh1HghEDyqNmxEZBoMPr8SxekkZuYcs+gynKlNW+TALQs7swkERiBLkG9NR0K1B3/2uVzlvYowXrmlW8hw==} 83 | engines: {node: '>=14.*'} 84 | cpu: [arm64] 85 | os: [darwin] 86 | 87 | '@biomejs/cli-darwin-x64@1.5.3': 88 | resolution: {integrity: sha512-vCdASqYnlpq/swErH7FD6nrFz0czFtK4k/iLgj0/+VmZVjineFPgevOb+Sr9vz0tk0GfdQO60bSpI74zU8M9Dw==} 89 | engines: {node: '>=14.*'} 90 | cpu: [x64] 91 | os: [darwin] 92 | 93 | '@biomejs/cli-linux-arm64-musl@1.5.3': 94 | resolution: {integrity: sha512-DYuMizUYUBYfS0IHGjDrOP1RGipqWfMGEvNEJ398zdtmCKLXaUvTimiox5dvx4X15mBK5M2m8wgWUgOP1giUpQ==} 95 | engines: {node: '>=14.*'} 96 | cpu: [arm64] 97 | os: [linux] 98 | 99 | '@biomejs/cli-linux-arm64@1.5.3': 100 | resolution: {integrity: sha512-cupBQv0sNF1OKqBfx7EDWMSsKwRrBUZfjXawT4s6hKV6ALq7p0QzWlxr/sDmbKMLOaLQtw2Qgu/77N9rm+f9Rg==} 101 | engines: {node: '>=14.*'} 102 | cpu: [arm64] 103 | os: [linux] 104 | 105 | '@biomejs/cli-linux-x64-musl@1.5.3': 106 | resolution: {integrity: sha512-UUHiAnlDqr2Y/LpvshBFhUYMWkl2/Jn+bi3U6jKuav0qWbbBKU/ByHgR4+NBxpKBYoCtWxhnmatfH1bpPIuZMw==} 107 | engines: {node: '>=14.*'} 108 | cpu: [x64] 109 | os: [linux] 110 | 111 | '@biomejs/cli-linux-x64@1.5.3': 112 | resolution: {integrity: sha512-YQrSArQvcv4FYsk7Q91Yv4uuu5F8hJyORVcv3zsjCLGkjIjx2RhjYLpTL733SNL7v33GmOlZY0eFR1ko38tuUw==} 113 | engines: {node: '>=14.*'} 114 | cpu: [x64] 115 | os: [linux] 116 | 117 | '@biomejs/cli-win32-arm64@1.5.3': 118 | resolution: {integrity: sha512-HxatYH7vf/kX9nrD+pDYuV2GI9GV8EFo6cfKkahAecTuZLPxryHx1WEfJthp5eNsE0+09STGkKIKjirP0ufaZA==} 119 | engines: {node: '>=14.*'} 120 | cpu: [arm64] 121 | os: [win32] 122 | 123 | '@biomejs/cli-win32-x64@1.5.3': 124 | resolution: {integrity: sha512-fMvbSouZEASU7mZH8SIJSANDm5OqsjgtVXlbUqxwed6BP7uuHRSs396Aqwh2+VoW8fwTpp6ybIUoC9FrzB0kyA==} 125 | engines: {node: '>=14.*'} 126 | cpu: [x64] 127 | os: [win32] 128 | 129 | '@brillout/format-text@0.1.3': 130 | resolution: {integrity: sha512-79/JKUOr+POW3XAfRDDxoxCjGBeD5KVwkzBWhK7KPA66HCTwEqb7unqxfVGSJZ9MnL/NiMb0shhzRkydMC/YFg==} 131 | 132 | '@brillout/picocolors@1.0.12': 133 | resolution: {integrity: sha512-2gowgbpAqEQz4U1D/dh3tU2fKcRm+yt724d8YTbCsVHxnLTHWP2J5RMO1iTWcoViX7rTmLvPkHHlYtEiKP4gLA==} 134 | 135 | '@brillout/release-me@0.3.8': 136 | resolution: {integrity: sha512-feo4CLWpMT0LS5l/fI37vv9pbOOEste6/AO9qLT5WtupqDkgK9k6iBuZmzLlef4AthIA2BY20tXlX/VcFWKuSw==} 137 | hasBin: true 138 | 139 | '@esbuild/aix-ppc64@0.19.12': 140 | resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} 141 | engines: {node: '>=12'} 142 | cpu: [ppc64] 143 | os: [aix] 144 | 145 | '@esbuild/android-arm64@0.19.12': 146 | resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} 147 | engines: {node: '>=12'} 148 | cpu: [arm64] 149 | os: [android] 150 | 151 | '@esbuild/android-arm@0.19.12': 152 | resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} 153 | engines: {node: '>=12'} 154 | cpu: [arm] 155 | os: [android] 156 | 157 | '@esbuild/android-x64@0.19.12': 158 | resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} 159 | engines: {node: '>=12'} 160 | cpu: [x64] 161 | os: [android] 162 | 163 | '@esbuild/darwin-arm64@0.19.12': 164 | resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} 165 | engines: {node: '>=12'} 166 | cpu: [arm64] 167 | os: [darwin] 168 | 169 | '@esbuild/darwin-x64@0.19.12': 170 | resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} 171 | engines: {node: '>=12'} 172 | cpu: [x64] 173 | os: [darwin] 174 | 175 | '@esbuild/freebsd-arm64@0.19.12': 176 | resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} 177 | engines: {node: '>=12'} 178 | cpu: [arm64] 179 | os: [freebsd] 180 | 181 | '@esbuild/freebsd-x64@0.19.12': 182 | resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} 183 | engines: {node: '>=12'} 184 | cpu: [x64] 185 | os: [freebsd] 186 | 187 | '@esbuild/linux-arm64@0.19.12': 188 | resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} 189 | engines: {node: '>=12'} 190 | cpu: [arm64] 191 | os: [linux] 192 | 193 | '@esbuild/linux-arm@0.19.12': 194 | resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} 195 | engines: {node: '>=12'} 196 | cpu: [arm] 197 | os: [linux] 198 | 199 | '@esbuild/linux-ia32@0.19.12': 200 | resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} 201 | engines: {node: '>=12'} 202 | cpu: [ia32] 203 | os: [linux] 204 | 205 | '@esbuild/linux-loong64@0.19.12': 206 | resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} 207 | engines: {node: '>=12'} 208 | cpu: [loong64] 209 | os: [linux] 210 | 211 | '@esbuild/linux-mips64el@0.19.12': 212 | resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} 213 | engines: {node: '>=12'} 214 | cpu: [mips64el] 215 | os: [linux] 216 | 217 | '@esbuild/linux-ppc64@0.19.12': 218 | resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} 219 | engines: {node: '>=12'} 220 | cpu: [ppc64] 221 | os: [linux] 222 | 223 | '@esbuild/linux-riscv64@0.19.12': 224 | resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} 225 | engines: {node: '>=12'} 226 | cpu: [riscv64] 227 | os: [linux] 228 | 229 | '@esbuild/linux-s390x@0.19.12': 230 | resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} 231 | engines: {node: '>=12'} 232 | cpu: [s390x] 233 | os: [linux] 234 | 235 | '@esbuild/linux-x64@0.19.12': 236 | resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} 237 | engines: {node: '>=12'} 238 | cpu: [x64] 239 | os: [linux] 240 | 241 | '@esbuild/netbsd-x64@0.19.12': 242 | resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} 243 | engines: {node: '>=12'} 244 | cpu: [x64] 245 | os: [netbsd] 246 | 247 | '@esbuild/openbsd-x64@0.19.12': 248 | resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} 249 | engines: {node: '>=12'} 250 | cpu: [x64] 251 | os: [openbsd] 252 | 253 | '@esbuild/sunos-x64@0.19.12': 254 | resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} 255 | engines: {node: '>=12'} 256 | cpu: [x64] 257 | os: [sunos] 258 | 259 | '@esbuild/win32-arm64@0.19.12': 260 | resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} 261 | engines: {node: '>=12'} 262 | cpu: [arm64] 263 | os: [win32] 264 | 265 | '@esbuild/win32-ia32@0.19.12': 266 | resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} 267 | engines: {node: '>=12'} 268 | cpu: [ia32] 269 | os: [win32] 270 | 271 | '@esbuild/win32-x64@0.19.12': 272 | resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} 273 | engines: {node: '>=12'} 274 | cpu: [x64] 275 | os: [win32] 276 | 277 | '@hutson/parse-repository-url@5.0.0': 278 | resolution: {integrity: sha512-e5+YUKENATs1JgYHMzTr2MW/NDcXGfYFAuOQU8gJgF/kEh4EqKgfGrfLI67bMD4tbhZVlkigz/9YYwWcbOFthg==} 279 | engines: {node: '>=10.13.0'} 280 | 281 | '@rollup/rollup-android-arm-eabi@4.13.0': 282 | resolution: {integrity: sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==} 283 | cpu: [arm] 284 | os: [android] 285 | 286 | '@rollup/rollup-android-arm64@4.13.0': 287 | resolution: {integrity: sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==} 288 | cpu: [arm64] 289 | os: [android] 290 | 291 | '@rollup/rollup-darwin-arm64@4.13.0': 292 | resolution: {integrity: sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==} 293 | cpu: [arm64] 294 | os: [darwin] 295 | 296 | '@rollup/rollup-darwin-x64@4.13.0': 297 | resolution: {integrity: sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==} 298 | cpu: [x64] 299 | os: [darwin] 300 | 301 | '@rollup/rollup-linux-arm-gnueabihf@4.13.0': 302 | resolution: {integrity: sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==} 303 | cpu: [arm] 304 | os: [linux] 305 | 306 | '@rollup/rollup-linux-arm64-gnu@4.13.0': 307 | resolution: {integrity: sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==} 308 | cpu: [arm64] 309 | os: [linux] 310 | 311 | '@rollup/rollup-linux-arm64-musl@4.13.0': 312 | resolution: {integrity: sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==} 313 | cpu: [arm64] 314 | os: [linux] 315 | 316 | '@rollup/rollup-linux-riscv64-gnu@4.13.0': 317 | resolution: {integrity: sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==} 318 | cpu: [riscv64] 319 | os: [linux] 320 | 321 | '@rollup/rollup-linux-x64-gnu@4.13.0': 322 | resolution: {integrity: sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==} 323 | cpu: [x64] 324 | os: [linux] 325 | 326 | '@rollup/rollup-linux-x64-musl@4.13.0': 327 | resolution: {integrity: sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==} 328 | cpu: [x64] 329 | os: [linux] 330 | 331 | '@rollup/rollup-win32-arm64-msvc@4.13.0': 332 | resolution: {integrity: sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==} 333 | cpu: [arm64] 334 | os: [win32] 335 | 336 | '@rollup/rollup-win32-ia32-msvc@4.13.0': 337 | resolution: {integrity: sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==} 338 | cpu: [ia32] 339 | os: [win32] 340 | 341 | '@rollup/rollup-win32-x64-msvc@4.13.0': 342 | resolution: {integrity: sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==} 343 | cpu: [x64] 344 | os: [win32] 345 | 346 | '@types/estree@1.0.5': 347 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 348 | 349 | '@types/normalize-package-data@2.4.4': 350 | resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} 351 | 352 | '@types/prop-types@15.7.11': 353 | resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} 354 | 355 | '@types/react-dom@18.2.22': 356 | resolution: {integrity: sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==} 357 | 358 | '@types/react@18.2.66': 359 | resolution: {integrity: sha512-OYTmMI4UigXeFMF/j4uv0lBBEbongSgptPrHBxqME44h9+yNov+oL6Z3ocJKo0WyXR84sQUNeyIp9MRfckvZpg==} 360 | 361 | '@types/scheduler@0.16.8': 362 | resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} 363 | 364 | JSONStream@1.3.5: 365 | resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} 366 | hasBin: true 367 | 368 | add-stream@1.0.0: 369 | resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==} 370 | 371 | ansi-regex@3.0.1: 372 | resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} 373 | engines: {node: '>=4'} 374 | 375 | ansi-styles@3.2.1: 376 | resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} 377 | engines: {node: '>=4'} 378 | 379 | array-ify@1.0.0: 380 | resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} 381 | 382 | chalk@2.4.2: 383 | resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 384 | engines: {node: '>=4'} 385 | 386 | classnames@2.5.1: 387 | resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} 388 | 389 | color-convert@1.9.3: 390 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 391 | 392 | color-name@1.1.3: 393 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 394 | 395 | commander@11.1.0: 396 | resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} 397 | engines: {node: '>=16'} 398 | 399 | compare-func@2.0.0: 400 | resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} 401 | 402 | conventional-changelog-angular@7.0.0: 403 | resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} 404 | engines: {node: '>=16'} 405 | 406 | conventional-changelog-atom@4.0.0: 407 | resolution: {integrity: sha512-q2YtiN7rnT1TGwPTwjjBSIPIzDJCRE+XAUahWxnh+buKK99Kks4WLMHoexw38GXx9OUxAsrp44f9qXe5VEMYhw==} 408 | engines: {node: '>=16'} 409 | 410 | conventional-changelog-codemirror@4.0.0: 411 | resolution: {integrity: sha512-hQSojc/5imn1GJK3A75m9hEZZhc3urojA5gMpnar4JHmgLnuM3CUIARPpEk86glEKr3c54Po3WV/vCaO/U8g3Q==} 412 | engines: {node: '>=16'} 413 | 414 | conventional-changelog-conventionalcommits@7.0.2: 415 | resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} 416 | engines: {node: '>=16'} 417 | 418 | conventional-changelog-core@7.0.0: 419 | resolution: {integrity: sha512-UYgaB1F/COt7VFjlYKVE/9tTzfU3VUq47r6iWf6lM5T7TlOxr0thI63ojQueRLIpVbrtHK4Ffw+yQGduw2Bhdg==} 420 | engines: {node: '>=16'} 421 | 422 | conventional-changelog-ember@4.0.0: 423 | resolution: {integrity: sha512-D0IMhwcJUg1Y8FSry6XAplEJcljkHVlvAZddhhsdbL1rbsqRsMfGx/PIkPYq0ru5aDgn+OxhQ5N5yR7P9mfsvA==} 424 | engines: {node: '>=16'} 425 | 426 | conventional-changelog-eslint@5.0.0: 427 | resolution: {integrity: sha512-6JtLWqAQIeJLn/OzUlYmzd9fKeNSWmQVim9kql+v4GrZwLx807kAJl3IJVc3jTYfVKWLxhC3BGUxYiuVEcVjgA==} 428 | engines: {node: '>=16'} 429 | 430 | conventional-changelog-express@4.0.0: 431 | resolution: {integrity: sha512-yWyy5c7raP9v7aTvPAWzqrztACNO9+FEI1FSYh7UP7YT1AkWgv5UspUeB5v3Ibv4/o60zj2o9GF2tqKQ99lIsw==} 432 | engines: {node: '>=16'} 433 | 434 | conventional-changelog-jquery@5.0.0: 435 | resolution: {integrity: sha512-slLjlXLRNa/icMI3+uGLQbtrgEny3RgITeCxevJB+p05ExiTgHACP5p3XiMKzjBn80n+Rzr83XMYfRInEtCPPw==} 436 | engines: {node: '>=16'} 437 | 438 | conventional-changelog-jshint@4.0.0: 439 | resolution: {integrity: sha512-LyXq1bbl0yG0Ai1SbLxIk8ZxUOe3AjnlwE6sVRQmMgetBk+4gY9EO3d00zlEt8Y8gwsITytDnPORl8al7InTjg==} 440 | engines: {node: '>=16'} 441 | 442 | conventional-changelog-preset-loader@4.1.0: 443 | resolution: {integrity: sha512-HozQjJicZTuRhCRTq4rZbefaiCzRM2pr6u2NL3XhrmQm4RMnDXfESU6JKu/pnKwx5xtdkYfNCsbhN5exhiKGJA==} 444 | engines: {node: '>=16'} 445 | 446 | conventional-changelog-writer@7.0.1: 447 | resolution: {integrity: sha512-Uo+R9neH3r/foIvQ0MKcsXkX642hdm9odUp7TqgFS7BsalTcjzRlIfWZrZR1gbxOozKucaKt5KAbjW8J8xRSmA==} 448 | engines: {node: '>=16'} 449 | hasBin: true 450 | 451 | conventional-changelog@5.1.0: 452 | resolution: {integrity: sha512-aWyE/P39wGYRPllcCEZDxTVEmhyLzTc9XA6z6rVfkuCD2UBnhV/sgSOKbQrEG5z9mEZJjnopjgQooTKxEg8mAg==} 453 | engines: {node: '>=16'} 454 | 455 | conventional-commits-filter@4.0.0: 456 | resolution: {integrity: sha512-rnpnibcSOdFcdclpFwWa+pPlZJhXE7l+XK04zxhbWrhgpR96h33QLz8hITTXbcYICxVr3HZFtbtUAQ+4LdBo9A==} 457 | engines: {node: '>=16'} 458 | 459 | conventional-commits-parser@5.0.0: 460 | resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} 461 | engines: {node: '>=16'} 462 | hasBin: true 463 | 464 | cross-spawn@7.0.3: 465 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 466 | engines: {node: '>= 8'} 467 | 468 | csstype@3.1.3: 469 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 470 | 471 | dargs@8.1.0: 472 | resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} 473 | engines: {node: '>=12'} 474 | 475 | dom-helpers@3.4.0: 476 | resolution: {integrity: sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==} 477 | 478 | dot-prop@5.3.0: 479 | resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} 480 | engines: {node: '>=8'} 481 | 482 | error-ex@1.3.2: 483 | resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} 484 | 485 | esbuild@0.19.12: 486 | resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} 487 | engines: {node: '>=12'} 488 | hasBin: true 489 | 490 | escape-string-regexp@1.0.5: 491 | resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} 492 | engines: {node: '>=0.8.0'} 493 | 494 | execa@5.1.1: 495 | resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} 496 | engines: {node: '>=10'} 497 | 498 | find-up@6.3.0: 499 | resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} 500 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 501 | 502 | fsevents@2.3.3: 503 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 504 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 505 | os: [darwin] 506 | 507 | function-bind@1.1.2: 508 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 509 | 510 | get-stream@6.0.1: 511 | resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} 512 | engines: {node: '>=10'} 513 | 514 | git-raw-commits@4.0.0: 515 | resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} 516 | engines: {node: '>=16'} 517 | hasBin: true 518 | 519 | git-semver-tags@7.0.1: 520 | resolution: {integrity: sha512-NY0ZHjJzyyNXHTDZmj+GG7PyuAKtMsyWSwh07CR2hOZFa+/yoTsXci/nF2obzL8UDhakFNkD9gNdt/Ed+cxh2Q==} 521 | engines: {node: '>=16'} 522 | hasBin: true 523 | 524 | handlebars@4.7.8: 525 | resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} 526 | engines: {node: '>=0.4.7'} 527 | hasBin: true 528 | 529 | handli@0.0.2: 530 | resolution: {integrity: sha512-mzyr4BqmdOhm7kboeP/nGhVv8hUtBh5b4Z7Ar2n6PNhs9iqiAFIcY1EMXcWdOm9WmnhZsmu8N+g1yw234GoPKA==} 531 | 532 | has-flag@3.0.0: 533 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 534 | engines: {node: '>=4'} 535 | 536 | hasown@2.0.2: 537 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 538 | engines: {node: '>= 0.4'} 539 | 540 | hosted-git-info@7.0.1: 541 | resolution: {integrity: sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==} 542 | engines: {node: ^16.14.0 || >=18.0.0} 543 | 544 | human-signals@2.1.0: 545 | resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} 546 | engines: {node: '>=10.17.0'} 547 | 548 | is-arrayish@0.2.1: 549 | resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} 550 | 551 | is-core-module@2.13.1: 552 | resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} 553 | 554 | is-fullwidth-code-point@2.0.0: 555 | resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} 556 | engines: {node: '>=4'} 557 | 558 | is-obj@2.0.0: 559 | resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} 560 | engines: {node: '>=8'} 561 | 562 | is-stream@2.0.1: 563 | resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} 564 | engines: {node: '>=8'} 565 | 566 | is-text-path@2.0.0: 567 | resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} 568 | engines: {node: '>=8'} 569 | 570 | isexe@2.0.0: 571 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 572 | 573 | js-tokens@4.0.0: 574 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 575 | 576 | json-parse-even-better-errors@3.0.1: 577 | resolution: {integrity: sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==} 578 | engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} 579 | 580 | json-stringify-safe@5.0.1: 581 | resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} 582 | 583 | jsonparse@1.3.1: 584 | resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} 585 | engines: {'0': node >= 0.2.0} 586 | 587 | lines-and-columns@2.0.4: 588 | resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} 589 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 590 | 591 | locate-path@7.2.0: 592 | resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} 593 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 594 | 595 | loose-envify@1.4.0: 596 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 597 | hasBin: true 598 | 599 | lru-cache@10.2.0: 600 | resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} 601 | engines: {node: 14 || >=16.14} 602 | 603 | lru-cache@6.0.0: 604 | resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} 605 | engines: {node: '>=10'} 606 | 607 | meow@12.1.1: 608 | resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} 609 | engines: {node: '>=16.10'} 610 | 611 | merge-stream@2.0.0: 612 | resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} 613 | 614 | mimic-fn@2.1.0: 615 | resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} 616 | engines: {node: '>=6'} 617 | 618 | minimist@1.2.8: 619 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 620 | 621 | nanoid@3.3.7: 622 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 623 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 624 | hasBin: true 625 | 626 | neo-async@2.6.2: 627 | resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} 628 | 629 | normalize-package-data@6.0.0: 630 | resolution: {integrity: sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==} 631 | engines: {node: ^16.14.0 || >=18.0.0} 632 | 633 | npm-run-path@4.0.1: 634 | resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} 635 | engines: {node: '>=8'} 636 | 637 | object-assign@4.1.1: 638 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 639 | engines: {node: '>=0.10.0'} 640 | 641 | onetime@5.1.2: 642 | resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} 643 | engines: {node: '>=6'} 644 | 645 | p-limit@4.0.0: 646 | resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} 647 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 648 | 649 | p-locate@6.0.0: 650 | resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} 651 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 652 | 653 | parse-json@7.1.1: 654 | resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==} 655 | engines: {node: '>=16'} 656 | 657 | path-exists@5.0.0: 658 | resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} 659 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 660 | 661 | path-key@3.1.1: 662 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 663 | engines: {node: '>=8'} 664 | 665 | picocolors@1.0.0: 666 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 667 | 668 | postcss@8.4.35: 669 | resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==} 670 | engines: {node: ^10 || ^12 || >=14} 671 | 672 | prettier@3.2.5: 673 | resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} 674 | engines: {node: '>=14'} 675 | hasBin: true 676 | 677 | prismjs@1.29.0: 678 | resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} 679 | engines: {node: '>=6'} 680 | 681 | prop-types@15.8.1: 682 | resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} 683 | 684 | react-dom@18.2.0: 685 | resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} 686 | peerDependencies: 687 | react: ^18.2.0 688 | 689 | react-is@16.13.1: 690 | resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} 691 | 692 | react-lifecycles-compat@3.0.4: 693 | resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} 694 | 695 | react-toastify@4.5.2: 696 | resolution: {integrity: sha512-KymDDhkcX5EvFht17nO0MCsegM/Kdhyfxhi+WQl2tE3IxJrueOhY6TUnALTfvz7eDRUjPYBGb+ywWqWrGyvBnw==} 697 | peerDependencies: 698 | react: '>=15.0.0' 699 | react-dom: '>=15.0.0' 700 | 701 | react-transition-group@2.9.0: 702 | resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==} 703 | peerDependencies: 704 | react: '>=15.0.0' 705 | react-dom: '>=15.0.0' 706 | 707 | react@18.2.0: 708 | resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} 709 | engines: {node: '>=0.10.0'} 710 | 711 | read-pkg-up@10.1.0: 712 | resolution: {integrity: sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==} 713 | engines: {node: '>=16'} 714 | 715 | read-pkg@8.1.0: 716 | resolution: {integrity: sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==} 717 | engines: {node: '>=16'} 718 | 719 | reassert@1.1.21: 720 | resolution: {integrity: sha512-Ff2/AaIE5LDTrsEBUDKFb65klJki9xibTKi7loj9bGxBrQBb4BubgvJLEyKRlzrr4SFmg0QzfdcTojU9NHn0Zw==} 721 | 722 | regenerator-runtime@0.14.1: 723 | resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} 724 | 725 | rollup@4.13.0: 726 | resolution: {integrity: sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==} 727 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 728 | hasBin: true 729 | 730 | scheduler@0.23.0: 731 | resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} 732 | 733 | semver@7.6.0: 734 | resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} 735 | engines: {node: '>=10'} 736 | hasBin: true 737 | 738 | shebang-command@2.0.0: 739 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 740 | engines: {node: '>=8'} 741 | 742 | shebang-regex@3.0.0: 743 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 744 | engines: {node: '>=8'} 745 | 746 | signal-exit@3.0.7: 747 | resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} 748 | 749 | source-map-js@1.0.2: 750 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 751 | engines: {node: '>=0.10.0'} 752 | 753 | source-map@0.6.1: 754 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 755 | engines: {node: '>=0.10.0'} 756 | 757 | spdx-correct@3.2.0: 758 | resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} 759 | 760 | spdx-exceptions@2.5.0: 761 | resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} 762 | 763 | spdx-expression-parse@3.0.1: 764 | resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} 765 | 766 | spdx-license-ids@3.0.17: 767 | resolution: {integrity: sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==} 768 | 769 | split2@4.2.0: 770 | resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} 771 | engines: {node: '>= 10.x'} 772 | 773 | string-width@2.1.1: 774 | resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} 775 | engines: {node: '>=4'} 776 | 777 | strip-ansi@4.0.0: 778 | resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} 779 | engines: {node: '>=4'} 780 | 781 | strip-final-newline@2.0.0: 782 | resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} 783 | engines: {node: '>=6'} 784 | 785 | supports-color@5.5.0: 786 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 787 | engines: {node: '>=4'} 788 | 789 | text-extensions@2.4.0: 790 | resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} 791 | engines: {node: '>=8'} 792 | 793 | through@2.3.8: 794 | resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} 795 | 796 | type-fest@3.13.1: 797 | resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} 798 | engines: {node: '>=14.16'} 799 | 800 | type-fest@4.12.0: 801 | resolution: {integrity: sha512-5Y2/pp2wtJk8o08G0CMkuFPCO354FGwk/vbidxrdhRGZfd0tFnb4Qb8anp9XxXriwBgVPjdWbKpGl4J9lJY2jQ==} 802 | engines: {node: '>=16'} 803 | 804 | typescript@5.4.2: 805 | resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} 806 | engines: {node: '>=14.17'} 807 | hasBin: true 808 | 809 | uglify-js@3.17.4: 810 | resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} 811 | engines: {node: '>=0.8.0'} 812 | hasBin: true 813 | 814 | validate-npm-package-license@3.0.4: 815 | resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} 816 | 817 | vite@5.1.6: 818 | resolution: {integrity: sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==} 819 | engines: {node: ^18.0.0 || >=20.0.0} 820 | hasBin: true 821 | peerDependencies: 822 | '@types/node': ^18.0.0 || >=20.0.0 823 | less: '*' 824 | lightningcss: ^1.21.0 825 | sass: '*' 826 | stylus: '*' 827 | sugarss: '*' 828 | terser: ^5.4.0 829 | peerDependenciesMeta: 830 | '@types/node': 831 | optional: true 832 | less: 833 | optional: true 834 | lightningcss: 835 | optional: true 836 | sass: 837 | optional: true 838 | stylus: 839 | optional: true 840 | sugarss: 841 | optional: true 842 | terser: 843 | optional: true 844 | 845 | which@2.0.2: 846 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 847 | engines: {node: '>= 8'} 848 | hasBin: true 849 | 850 | wordwrap@1.0.0: 851 | resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} 852 | 853 | yallist@4.0.0: 854 | resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} 855 | 856 | yocto-queue@1.0.0: 857 | resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} 858 | engines: {node: '>=12.20'} 859 | 860 | snapshots: 861 | 862 | '@babel/code-frame@7.23.5': 863 | dependencies: 864 | '@babel/highlight': 7.23.4 865 | chalk: 2.4.2 866 | 867 | '@babel/helper-validator-identifier@7.22.20': {} 868 | 869 | '@babel/highlight@7.23.4': 870 | dependencies: 871 | '@babel/helper-validator-identifier': 7.22.20 872 | chalk: 2.4.2 873 | js-tokens: 4.0.0 874 | 875 | '@babel/runtime@7.24.0': 876 | dependencies: 877 | regenerator-runtime: 0.14.1 878 | 879 | '@biomejs/biome@1.5.3': 880 | optionalDependencies: 881 | '@biomejs/cli-darwin-arm64': 1.5.3 882 | '@biomejs/cli-darwin-x64': 1.5.3 883 | '@biomejs/cli-linux-arm64': 1.5.3 884 | '@biomejs/cli-linux-arm64-musl': 1.5.3 885 | '@biomejs/cli-linux-x64': 1.5.3 886 | '@biomejs/cli-linux-x64-musl': 1.5.3 887 | '@biomejs/cli-win32-arm64': 1.5.3 888 | '@biomejs/cli-win32-x64': 1.5.3 889 | 890 | '@biomejs/cli-darwin-arm64@1.5.3': 891 | optional: true 892 | 893 | '@biomejs/cli-darwin-x64@1.5.3': 894 | optional: true 895 | 896 | '@biomejs/cli-linux-arm64-musl@1.5.3': 897 | optional: true 898 | 899 | '@biomejs/cli-linux-arm64@1.5.3': 900 | optional: true 901 | 902 | '@biomejs/cli-linux-x64-musl@1.5.3': 903 | optional: true 904 | 905 | '@biomejs/cli-linux-x64@1.5.3': 906 | optional: true 907 | 908 | '@biomejs/cli-win32-arm64@1.5.3': 909 | optional: true 910 | 911 | '@biomejs/cli-win32-x64@1.5.3': 912 | optional: true 913 | 914 | '@brillout/format-text@0.1.3': 915 | dependencies: 916 | reassert: 1.1.21 917 | string-width: 2.1.1 918 | 919 | '@brillout/picocolors@1.0.12': {} 920 | 921 | '@brillout/release-me@0.3.8': 922 | dependencies: 923 | '@brillout/picocolors': 1.0.12 924 | commander: 11.1.0 925 | conventional-changelog: 5.1.0 926 | execa: 5.1.1 927 | semver: 7.6.0 928 | 929 | '@esbuild/aix-ppc64@0.19.12': 930 | optional: true 931 | 932 | '@esbuild/android-arm64@0.19.12': 933 | optional: true 934 | 935 | '@esbuild/android-arm@0.19.12': 936 | optional: true 937 | 938 | '@esbuild/android-x64@0.19.12': 939 | optional: true 940 | 941 | '@esbuild/darwin-arm64@0.19.12': 942 | optional: true 943 | 944 | '@esbuild/darwin-x64@0.19.12': 945 | optional: true 946 | 947 | '@esbuild/freebsd-arm64@0.19.12': 948 | optional: true 949 | 950 | '@esbuild/freebsd-x64@0.19.12': 951 | optional: true 952 | 953 | '@esbuild/linux-arm64@0.19.12': 954 | optional: true 955 | 956 | '@esbuild/linux-arm@0.19.12': 957 | optional: true 958 | 959 | '@esbuild/linux-ia32@0.19.12': 960 | optional: true 961 | 962 | '@esbuild/linux-loong64@0.19.12': 963 | optional: true 964 | 965 | '@esbuild/linux-mips64el@0.19.12': 966 | optional: true 967 | 968 | '@esbuild/linux-ppc64@0.19.12': 969 | optional: true 970 | 971 | '@esbuild/linux-riscv64@0.19.12': 972 | optional: true 973 | 974 | '@esbuild/linux-s390x@0.19.12': 975 | optional: true 976 | 977 | '@esbuild/linux-x64@0.19.12': 978 | optional: true 979 | 980 | '@esbuild/netbsd-x64@0.19.12': 981 | optional: true 982 | 983 | '@esbuild/openbsd-x64@0.19.12': 984 | optional: true 985 | 986 | '@esbuild/sunos-x64@0.19.12': 987 | optional: true 988 | 989 | '@esbuild/win32-arm64@0.19.12': 990 | optional: true 991 | 992 | '@esbuild/win32-ia32@0.19.12': 993 | optional: true 994 | 995 | '@esbuild/win32-x64@0.19.12': 996 | optional: true 997 | 998 | '@hutson/parse-repository-url@5.0.0': {} 999 | 1000 | '@rollup/rollup-android-arm-eabi@4.13.0': 1001 | optional: true 1002 | 1003 | '@rollup/rollup-android-arm64@4.13.0': 1004 | optional: true 1005 | 1006 | '@rollup/rollup-darwin-arm64@4.13.0': 1007 | optional: true 1008 | 1009 | '@rollup/rollup-darwin-x64@4.13.0': 1010 | optional: true 1011 | 1012 | '@rollup/rollup-linux-arm-gnueabihf@4.13.0': 1013 | optional: true 1014 | 1015 | '@rollup/rollup-linux-arm64-gnu@4.13.0': 1016 | optional: true 1017 | 1018 | '@rollup/rollup-linux-arm64-musl@4.13.0': 1019 | optional: true 1020 | 1021 | '@rollup/rollup-linux-riscv64-gnu@4.13.0': 1022 | optional: true 1023 | 1024 | '@rollup/rollup-linux-x64-gnu@4.13.0': 1025 | optional: true 1026 | 1027 | '@rollup/rollup-linux-x64-musl@4.13.0': 1028 | optional: true 1029 | 1030 | '@rollup/rollup-win32-arm64-msvc@4.13.0': 1031 | optional: true 1032 | 1033 | '@rollup/rollup-win32-ia32-msvc@4.13.0': 1034 | optional: true 1035 | 1036 | '@rollup/rollup-win32-x64-msvc@4.13.0': 1037 | optional: true 1038 | 1039 | '@types/estree@1.0.5': {} 1040 | 1041 | '@types/normalize-package-data@2.4.4': {} 1042 | 1043 | '@types/prop-types@15.7.11': {} 1044 | 1045 | '@types/react-dom@18.2.22': 1046 | dependencies: 1047 | '@types/react': 18.2.66 1048 | 1049 | '@types/react@18.2.66': 1050 | dependencies: 1051 | '@types/prop-types': 15.7.11 1052 | '@types/scheduler': 0.16.8 1053 | csstype: 3.1.3 1054 | 1055 | '@types/scheduler@0.16.8': {} 1056 | 1057 | JSONStream@1.3.5: 1058 | dependencies: 1059 | jsonparse: 1.3.1 1060 | through: 2.3.8 1061 | 1062 | add-stream@1.0.0: {} 1063 | 1064 | ansi-regex@3.0.1: {} 1065 | 1066 | ansi-styles@3.2.1: 1067 | dependencies: 1068 | color-convert: 1.9.3 1069 | 1070 | array-ify@1.0.0: {} 1071 | 1072 | chalk@2.4.2: 1073 | dependencies: 1074 | ansi-styles: 3.2.1 1075 | escape-string-regexp: 1.0.5 1076 | supports-color: 5.5.0 1077 | 1078 | classnames@2.5.1: {} 1079 | 1080 | color-convert@1.9.3: 1081 | dependencies: 1082 | color-name: 1.1.3 1083 | 1084 | color-name@1.1.3: {} 1085 | 1086 | commander@11.1.0: {} 1087 | 1088 | compare-func@2.0.0: 1089 | dependencies: 1090 | array-ify: 1.0.0 1091 | dot-prop: 5.3.0 1092 | 1093 | conventional-changelog-angular@7.0.0: 1094 | dependencies: 1095 | compare-func: 2.0.0 1096 | 1097 | conventional-changelog-atom@4.0.0: {} 1098 | 1099 | conventional-changelog-codemirror@4.0.0: {} 1100 | 1101 | conventional-changelog-conventionalcommits@7.0.2: 1102 | dependencies: 1103 | compare-func: 2.0.0 1104 | 1105 | conventional-changelog-core@7.0.0: 1106 | dependencies: 1107 | '@hutson/parse-repository-url': 5.0.0 1108 | add-stream: 1.0.0 1109 | conventional-changelog-writer: 7.0.1 1110 | conventional-commits-parser: 5.0.0 1111 | git-raw-commits: 4.0.0 1112 | git-semver-tags: 7.0.1 1113 | hosted-git-info: 7.0.1 1114 | normalize-package-data: 6.0.0 1115 | read-pkg: 8.1.0 1116 | read-pkg-up: 10.1.0 1117 | 1118 | conventional-changelog-ember@4.0.0: {} 1119 | 1120 | conventional-changelog-eslint@5.0.0: {} 1121 | 1122 | conventional-changelog-express@4.0.0: {} 1123 | 1124 | conventional-changelog-jquery@5.0.0: {} 1125 | 1126 | conventional-changelog-jshint@4.0.0: 1127 | dependencies: 1128 | compare-func: 2.0.0 1129 | 1130 | conventional-changelog-preset-loader@4.1.0: {} 1131 | 1132 | conventional-changelog-writer@7.0.1: 1133 | dependencies: 1134 | conventional-commits-filter: 4.0.0 1135 | handlebars: 4.7.8 1136 | json-stringify-safe: 5.0.1 1137 | meow: 12.1.1 1138 | semver: 7.6.0 1139 | split2: 4.2.0 1140 | 1141 | conventional-changelog@5.1.0: 1142 | dependencies: 1143 | conventional-changelog-angular: 7.0.0 1144 | conventional-changelog-atom: 4.0.0 1145 | conventional-changelog-codemirror: 4.0.0 1146 | conventional-changelog-conventionalcommits: 7.0.2 1147 | conventional-changelog-core: 7.0.0 1148 | conventional-changelog-ember: 4.0.0 1149 | conventional-changelog-eslint: 5.0.0 1150 | conventional-changelog-express: 4.0.0 1151 | conventional-changelog-jquery: 5.0.0 1152 | conventional-changelog-jshint: 4.0.0 1153 | conventional-changelog-preset-loader: 4.1.0 1154 | 1155 | conventional-commits-filter@4.0.0: {} 1156 | 1157 | conventional-commits-parser@5.0.0: 1158 | dependencies: 1159 | JSONStream: 1.3.5 1160 | is-text-path: 2.0.0 1161 | meow: 12.1.1 1162 | split2: 4.2.0 1163 | 1164 | cross-spawn@7.0.3: 1165 | dependencies: 1166 | path-key: 3.1.1 1167 | shebang-command: 2.0.0 1168 | which: 2.0.2 1169 | 1170 | csstype@3.1.3: {} 1171 | 1172 | dargs@8.1.0: {} 1173 | 1174 | dom-helpers@3.4.0: 1175 | dependencies: 1176 | '@babel/runtime': 7.24.0 1177 | 1178 | dot-prop@5.3.0: 1179 | dependencies: 1180 | is-obj: 2.0.0 1181 | 1182 | error-ex@1.3.2: 1183 | dependencies: 1184 | is-arrayish: 0.2.1 1185 | 1186 | esbuild@0.19.12: 1187 | optionalDependencies: 1188 | '@esbuild/aix-ppc64': 0.19.12 1189 | '@esbuild/android-arm': 0.19.12 1190 | '@esbuild/android-arm64': 0.19.12 1191 | '@esbuild/android-x64': 0.19.12 1192 | '@esbuild/darwin-arm64': 0.19.12 1193 | '@esbuild/darwin-x64': 0.19.12 1194 | '@esbuild/freebsd-arm64': 0.19.12 1195 | '@esbuild/freebsd-x64': 0.19.12 1196 | '@esbuild/linux-arm': 0.19.12 1197 | '@esbuild/linux-arm64': 0.19.12 1198 | '@esbuild/linux-ia32': 0.19.12 1199 | '@esbuild/linux-loong64': 0.19.12 1200 | '@esbuild/linux-mips64el': 0.19.12 1201 | '@esbuild/linux-ppc64': 0.19.12 1202 | '@esbuild/linux-riscv64': 0.19.12 1203 | '@esbuild/linux-s390x': 0.19.12 1204 | '@esbuild/linux-x64': 0.19.12 1205 | '@esbuild/netbsd-x64': 0.19.12 1206 | '@esbuild/openbsd-x64': 0.19.12 1207 | '@esbuild/sunos-x64': 0.19.12 1208 | '@esbuild/win32-arm64': 0.19.12 1209 | '@esbuild/win32-ia32': 0.19.12 1210 | '@esbuild/win32-x64': 0.19.12 1211 | 1212 | escape-string-regexp@1.0.5: {} 1213 | 1214 | execa@5.1.1: 1215 | dependencies: 1216 | cross-spawn: 7.0.3 1217 | get-stream: 6.0.1 1218 | human-signals: 2.1.0 1219 | is-stream: 2.0.1 1220 | merge-stream: 2.0.0 1221 | npm-run-path: 4.0.1 1222 | onetime: 5.1.2 1223 | signal-exit: 3.0.7 1224 | strip-final-newline: 2.0.0 1225 | 1226 | find-up@6.3.0: 1227 | dependencies: 1228 | locate-path: 7.2.0 1229 | path-exists: 5.0.0 1230 | 1231 | fsevents@2.3.3: 1232 | optional: true 1233 | 1234 | function-bind@1.1.2: {} 1235 | 1236 | get-stream@6.0.1: {} 1237 | 1238 | git-raw-commits@4.0.0: 1239 | dependencies: 1240 | dargs: 8.1.0 1241 | meow: 12.1.1 1242 | split2: 4.2.0 1243 | 1244 | git-semver-tags@7.0.1: 1245 | dependencies: 1246 | meow: 12.1.1 1247 | semver: 7.6.0 1248 | 1249 | handlebars@4.7.8: 1250 | dependencies: 1251 | minimist: 1.2.8 1252 | neo-async: 2.6.2 1253 | source-map: 0.6.1 1254 | wordwrap: 1.0.0 1255 | optionalDependencies: 1256 | uglify-js: 3.17.4 1257 | 1258 | handli@0.0.2: 1259 | dependencies: 1260 | reassert: 1.1.21 1261 | 1262 | has-flag@3.0.0: {} 1263 | 1264 | hasown@2.0.2: 1265 | dependencies: 1266 | function-bind: 1.1.2 1267 | 1268 | hosted-git-info@7.0.1: 1269 | dependencies: 1270 | lru-cache: 10.2.0 1271 | 1272 | human-signals@2.1.0: {} 1273 | 1274 | is-arrayish@0.2.1: {} 1275 | 1276 | is-core-module@2.13.1: 1277 | dependencies: 1278 | hasown: 2.0.2 1279 | 1280 | is-fullwidth-code-point@2.0.0: {} 1281 | 1282 | is-obj@2.0.0: {} 1283 | 1284 | is-stream@2.0.1: {} 1285 | 1286 | is-text-path@2.0.0: 1287 | dependencies: 1288 | text-extensions: 2.4.0 1289 | 1290 | isexe@2.0.0: {} 1291 | 1292 | js-tokens@4.0.0: {} 1293 | 1294 | json-parse-even-better-errors@3.0.1: {} 1295 | 1296 | json-stringify-safe@5.0.1: {} 1297 | 1298 | jsonparse@1.3.1: {} 1299 | 1300 | lines-and-columns@2.0.4: {} 1301 | 1302 | locate-path@7.2.0: 1303 | dependencies: 1304 | p-locate: 6.0.0 1305 | 1306 | loose-envify@1.4.0: 1307 | dependencies: 1308 | js-tokens: 4.0.0 1309 | 1310 | lru-cache@10.2.0: {} 1311 | 1312 | lru-cache@6.0.0: 1313 | dependencies: 1314 | yallist: 4.0.0 1315 | 1316 | meow@12.1.1: {} 1317 | 1318 | merge-stream@2.0.0: {} 1319 | 1320 | mimic-fn@2.1.0: {} 1321 | 1322 | minimist@1.2.8: {} 1323 | 1324 | nanoid@3.3.7: {} 1325 | 1326 | neo-async@2.6.2: {} 1327 | 1328 | normalize-package-data@6.0.0: 1329 | dependencies: 1330 | hosted-git-info: 7.0.1 1331 | is-core-module: 2.13.1 1332 | semver: 7.6.0 1333 | validate-npm-package-license: 3.0.4 1334 | 1335 | npm-run-path@4.0.1: 1336 | dependencies: 1337 | path-key: 3.1.1 1338 | 1339 | object-assign@4.1.1: {} 1340 | 1341 | onetime@5.1.2: 1342 | dependencies: 1343 | mimic-fn: 2.1.0 1344 | 1345 | p-limit@4.0.0: 1346 | dependencies: 1347 | yocto-queue: 1.0.0 1348 | 1349 | p-locate@6.0.0: 1350 | dependencies: 1351 | p-limit: 4.0.0 1352 | 1353 | parse-json@7.1.1: 1354 | dependencies: 1355 | '@babel/code-frame': 7.23.5 1356 | error-ex: 1.3.2 1357 | json-parse-even-better-errors: 3.0.1 1358 | lines-and-columns: 2.0.4 1359 | type-fest: 3.13.1 1360 | 1361 | path-exists@5.0.0: {} 1362 | 1363 | path-key@3.1.1: {} 1364 | 1365 | picocolors@1.0.0: {} 1366 | 1367 | postcss@8.4.35: 1368 | dependencies: 1369 | nanoid: 3.3.7 1370 | picocolors: 1.0.0 1371 | source-map-js: 1.0.2 1372 | 1373 | prettier@3.2.5: {} 1374 | 1375 | prismjs@1.29.0: {} 1376 | 1377 | prop-types@15.8.1: 1378 | dependencies: 1379 | loose-envify: 1.4.0 1380 | object-assign: 4.1.1 1381 | react-is: 16.13.1 1382 | 1383 | react-dom@18.2.0(react@18.2.0): 1384 | dependencies: 1385 | loose-envify: 1.4.0 1386 | react: 18.2.0 1387 | scheduler: 0.23.0 1388 | 1389 | react-is@16.13.1: {} 1390 | 1391 | react-lifecycles-compat@3.0.4: {} 1392 | 1393 | react-toastify@4.5.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): 1394 | dependencies: 1395 | classnames: 2.5.1 1396 | prop-types: 15.8.1 1397 | react: 18.2.0 1398 | react-dom: 18.2.0(react@18.2.0) 1399 | react-transition-group: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) 1400 | 1401 | react-transition-group@2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): 1402 | dependencies: 1403 | dom-helpers: 3.4.0 1404 | loose-envify: 1.4.0 1405 | prop-types: 15.8.1 1406 | react: 18.2.0 1407 | react-dom: 18.2.0(react@18.2.0) 1408 | react-lifecycles-compat: 3.0.4 1409 | 1410 | react@18.2.0: 1411 | dependencies: 1412 | loose-envify: 1.4.0 1413 | 1414 | read-pkg-up@10.1.0: 1415 | dependencies: 1416 | find-up: 6.3.0 1417 | read-pkg: 8.1.0 1418 | type-fest: 4.12.0 1419 | 1420 | read-pkg@8.1.0: 1421 | dependencies: 1422 | '@types/normalize-package-data': 2.4.4 1423 | normalize-package-data: 6.0.0 1424 | parse-json: 7.1.1 1425 | type-fest: 4.12.0 1426 | 1427 | reassert@1.1.21: 1428 | dependencies: 1429 | '@brillout/format-text': 0.1.3 1430 | 1431 | regenerator-runtime@0.14.1: {} 1432 | 1433 | rollup@4.13.0: 1434 | dependencies: 1435 | '@types/estree': 1.0.5 1436 | optionalDependencies: 1437 | '@rollup/rollup-android-arm-eabi': 4.13.0 1438 | '@rollup/rollup-android-arm64': 4.13.0 1439 | '@rollup/rollup-darwin-arm64': 4.13.0 1440 | '@rollup/rollup-darwin-x64': 4.13.0 1441 | '@rollup/rollup-linux-arm-gnueabihf': 4.13.0 1442 | '@rollup/rollup-linux-arm64-gnu': 4.13.0 1443 | '@rollup/rollup-linux-arm64-musl': 4.13.0 1444 | '@rollup/rollup-linux-riscv64-gnu': 4.13.0 1445 | '@rollup/rollup-linux-x64-gnu': 4.13.0 1446 | '@rollup/rollup-linux-x64-musl': 4.13.0 1447 | '@rollup/rollup-win32-arm64-msvc': 4.13.0 1448 | '@rollup/rollup-win32-ia32-msvc': 4.13.0 1449 | '@rollup/rollup-win32-x64-msvc': 4.13.0 1450 | fsevents: 2.3.3 1451 | 1452 | scheduler@0.23.0: 1453 | dependencies: 1454 | loose-envify: 1.4.0 1455 | 1456 | semver@7.6.0: 1457 | dependencies: 1458 | lru-cache: 6.0.0 1459 | 1460 | shebang-command@2.0.0: 1461 | dependencies: 1462 | shebang-regex: 3.0.0 1463 | 1464 | shebang-regex@3.0.0: {} 1465 | 1466 | signal-exit@3.0.7: {} 1467 | 1468 | source-map-js@1.0.2: {} 1469 | 1470 | source-map@0.6.1: {} 1471 | 1472 | spdx-correct@3.2.0: 1473 | dependencies: 1474 | spdx-expression-parse: 3.0.1 1475 | spdx-license-ids: 3.0.17 1476 | 1477 | spdx-exceptions@2.5.0: {} 1478 | 1479 | spdx-expression-parse@3.0.1: 1480 | dependencies: 1481 | spdx-exceptions: 2.5.0 1482 | spdx-license-ids: 3.0.17 1483 | 1484 | spdx-license-ids@3.0.17: {} 1485 | 1486 | split2@4.2.0: {} 1487 | 1488 | string-width@2.1.1: 1489 | dependencies: 1490 | is-fullwidth-code-point: 2.0.0 1491 | strip-ansi: 4.0.0 1492 | 1493 | strip-ansi@4.0.0: 1494 | dependencies: 1495 | ansi-regex: 3.0.1 1496 | 1497 | strip-final-newline@2.0.0: {} 1498 | 1499 | supports-color@5.5.0: 1500 | dependencies: 1501 | has-flag: 3.0.0 1502 | 1503 | text-extensions@2.4.0: {} 1504 | 1505 | through@2.3.8: {} 1506 | 1507 | type-fest@3.13.1: {} 1508 | 1509 | type-fest@4.12.0: {} 1510 | 1511 | typescript@5.4.2: {} 1512 | 1513 | uglify-js@3.17.4: 1514 | optional: true 1515 | 1516 | validate-npm-package-license@3.0.4: 1517 | dependencies: 1518 | spdx-correct: 3.2.0 1519 | spdx-expression-parse: 3.0.1 1520 | 1521 | vite@5.1.6: 1522 | dependencies: 1523 | esbuild: 0.19.12 1524 | postcss: 8.4.35 1525 | rollup: 4.13.0 1526 | optionalDependencies: 1527 | fsevents: 2.3.3 1528 | 1529 | which@2.0.2: 1530 | dependencies: 1531 | isexe: 2.0.0 1532 | 1533 | wordwrap@1.0.0: {} 1534 | 1535 | yallist@4.0.0: {} 1536 | 1537 | yocto-queue@1.0.0: {} 1538 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'handli/' 3 | - 'demo/' 4 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | Handli 5 | 6 |

7 |
8 | 9 | JavaScript library to automatically handle connection issues. 10 | 11 | Handli brings sensible defaults to questions like: 12 | - What should happen when the user has a flaky internet connection? 13 | - What should happen when the user is offline? 14 | - What should happen when the server is overloaded and not responsive? 15 | - What should happen when the server replies `500 - Internal Server Error`? 16 | - ... 17 | 18 | With Handli, you can forget about connection issues and let Handli gracefully take care of these situations. 19 | 20 | > [!NOTE] 21 | > The main idea of Handli is that, if there is a connection issue, the UI is made read-only (by displaying a temporary overlay that blocks user clicks, and thus blocks the user from interacting with the UI). The user can still read/scroll the page (see [#6](https://github.com/brillout/handli/issues/6)). When the connection issue resolves, then the UI returns to its normal state. 22 | 23 | If you have specific needs, you can (progressively) customize and override Handli's behavior. 24 | 25 | Handli covers all (edge) cases using sensible defaults, all you have to do is wrap your fetch requests: 26 | 27 | ~~~js 28 | // ❌ TODO: handle connection issues. 29 | const response = await fetch(url) 30 | ~~~ 31 | ~~~js 32 | // ✅ Connection issues are hanlded by Handli. 33 | const response = await handli(() => fetch(url)) 34 | ~~~ 35 | 36 | 37 | That's it: all connection issues are now gracefully handled. 38 | 39 | > [!NOTE] 40 | > The promise `await handli(...)` never rejects. (Although it may never resolve if the connection isssue never resolves.) 41 | > 42 | > Thus you can skip the usual `try...catch` for handling connection issues: instead you assume the promise to always resolve. 43 | > 44 | > In other words: you can develop your app as if connection issues are non-existent. 45 | 46 | Handli is [fully customizable](#usage-faq) and [progressively removable](#how-do-i-progressively-remove-handli). 47 | 48 | [**Live Demo**](https://brillout.github.io/handli) 49 | 50 | > [!NOTE] 51 | > 🚧 This project is work-in-progress and I'm looking for [contributions](https://github.com/brillout/handli/issues) and/or a lead maintainer. This project has the potential to have a significant impact. 52 | 53 |

54 | 55 | 56 | 57 | #### Contents 58 | 59 | - [Usage](#usage) 60 | - [How it Works](#how-it-works) 61 | - [Usage FAQ](#usage-faq) 62 | 63 | ## Usage 64 | 65 | ~~~shell 66 | $ npm install handli 67 | ~~~ 68 | ~~~js 69 | import handli from 'handli'; 70 | ~~~ 71 | 72 | ~~~diff 73 | -const response = await fetch(url); 74 | +const response = await handli(() => fetch(url)); 75 | ~~~ 76 | 77 | That's it. 78 | Connection issues are now automatically handled by Handli. 79 | 80 |
81 | 82 | ## How it Works 83 | 84 | The `handli` function never rejects. 85 | 86 | ~~~js 87 | import handli from 'handli'; 88 | 89 | let response; 90 | try { 91 | response = await handli(() => fetch('https://example.org/api/getData')); 92 | } catch(_) { 93 | // `handli` never rejects 94 | assert(false); 95 | } 96 | // `handli` only resolves successful responses 97 | assert(200<=response.status && response.status<=299); 98 | ~~~ 99 | 100 | If the server doesn't reply a `2xx`, 101 | then Handli blocks the UI, 102 | shows a modal letting the user know what's going on, 103 | and periodically retries the request. 104 | The `handli` function "hangs" 105 | until the server returns a `2xx`. 106 | (That is, the promise returned by `handli` hangs: it doesn't resolve nor reject.) 107 | 108 | The `handli` function resolves only once it gets a `2xx`. 109 | 110 | If the server never replies a `2xx` then `handli` hangs indefinitely. 111 | ~~~js 112 | import handli from 'handli'; 113 | 114 | // Trying to retrieve a resource that doesn't exist. 115 | await handli(() => fetch('https://example.org/api/i-do-not-exist')); 116 | 117 | // The server never replies a `2xx` and `handli` never 118 | // resolves. Nothing here will ever be executed. 119 | console.log("You will never see me"); 120 | ~~~ 121 | 122 | You can handle errors yourself in your request function. 123 | For example, you may want to handle validation errors: 124 | ~~~js 125 | import handli from 'handli'; 126 | 127 | const response = await handli(async () => { 128 | const response = await fetch('https://example.org/api/createTodo'); 129 | if( response.status===400 ){ 130 | const {validationError} = await response.json(); 131 | return {validationError}; 132 | } 133 | return response; 134 | }); 135 | 136 | if( response.validationError ){ 137 | // Handle validation error 138 | // ... 139 | } else { 140 | assert(200<=response.status && response.status<=299); 141 | // ... 142 | } 143 | ~~~ 144 | 145 | 146 |
147 | 148 | ## Usage FAQ 149 | 150 | - [Can I customize the UI?](#can-i-customize-the-ui) 151 | - [Can I customize the texts?](#can-i-customize-the-texts) 152 | - [What if a non-2xx server reply is expected and I don't want Handli to treat it as error?](#what-if-a-non-2xx-server-reply-is-expected-and-i-dont-want-handli-to-treat-it-as-error) 153 | - [How do I handle errors myself?](#how-do-i-handle-errors-myself) 154 | - [When is the user's internet connection considered slow?](#when-is-the-users-internet-connection-considered-slow) 155 | - [Does it work only with `fetch`?](#does-it-work-only-with-fetch) 156 | - [Does it handle errors on Node.js?](#does-it-handle-errors-on-nodejs) 157 | - [What about Universal/Isomorphic/SSR?](#what-about-universalisomorphicssr) 158 | - [Does it support simultaneous requests?](#does-it-support-simultaneous-requests) 159 | - [How do I progressively remove Handli?](#how-do-i-progressively-remove-handli) 160 | 161 | ### Can I customize the UI? 162 | 163 | Yes. 164 | See 165 | [Live Demo - Custom Style](https://brillout.github.io/handli#custom-style) 166 | and 167 | [Live Demo - Custom UI](https://brillout.github.io/handli#custom-ui). 168 | 169 | ### Can I customize the texts? 170 | 171 | Yes. 172 | See [Live Demo - Custom Text](https://brillout.github.io/handli#custom-text). 173 | 174 | ### What if a non-2xx server reply is expected and I don't want Handli to treat it as error? 175 | 176 | Then handle the error yourself, 177 | see below. 178 | 179 | ### How do I handle errors myself? 180 | 181 | You can handle errors yourself in your request function. 182 | 183 | Examples. 184 | 185 | ~~~js 186 | import handli from 'handli'; 187 | 188 | const RATE_LIMIT = Symbol(); 189 | 190 | // We handle the API rate limit. 191 | 192 | const response = await handli(async () => { 193 | const response = await fetch('https://example.org/api/project/42'); 194 | if( response.status===429 ) { 195 | return RATE_LIMIT; 196 | } 197 | return response; 198 | }); 199 | 200 | if( response===RATE_LIMIT ) { 201 | // Code handling the case when API rate limit is reached 202 | // ... 203 | } else { 204 | assert(200<=response.status && response.status<=299); 205 | // ... 206 | } 207 | ~~~ 208 | 209 | ~~~js 210 | import handli from 'handli'; 211 | 212 | // We handle validation errors. 213 | 214 | const response = await handli(async () => { 215 | const response = await fetch('https://example.org/api/createTodo'); 216 | if( response.status===400 ){ 217 | const {validationError} = await response.json(); 218 | return {validationError}; 219 | } 220 | return response; 221 | }); 222 | 223 | if( response.validationError ){ 224 | // Handle validation error 225 | // ... 226 | } else { 227 | assert(200<=response.status && response.status<=299); 228 | // ... 229 | } 230 | ~~~ 231 | 232 | See [Live Demo - Handled Error](https://brillout.github.io/handli#handled-error). 233 | 234 | ### When is the user's internet connection considered slow? 235 | 236 | If a request isn't getting a response, 237 | then Handli tests the user's internet connection. 238 | To do so, Handli pings 239 | Google, 240 | Facebook, 241 | Cloudflare, and 242 | Amazon. 243 | 244 | If the fastest ping is higher than `thresholdSlowInternet` then 245 | Handli considers the connection as slow. 246 | 247 | If none of the ping requests get a response after `thresholdNoInternet` then Handli 248 | considers the user offline. 249 | 250 | By default `thresholdSlowInternet` is `500` milliseconds and `thresholdNoInternet` is `900` milliseconds. 251 | The [Live Demo - Custom Slow Threshold](https://brillout.github.io/handli#custom-slow-threshold) shows how to change these defaults. 252 | 253 | Note that Handli handles slow connections only if you provide a `timeout`: 254 | 255 | ~~~js 256 | // Handli will show a UI-blocking modal if there is no response after 2 seconds 257 | handli.timeout = 2000; 258 | 259 | const response = await handli( 260 | () => fetch(url), 261 | ); 262 | ~~~ 263 | 264 | If you don't provide a `timeout` then 265 | Handli indefinitely waits for a response 266 | without showing the UI-blocking modal. 267 | 268 | Alternatively to `timeout`, you can provide `timeoutInternet` and/or `timeoutServer`: 269 | - If the user's internet connection **is slow** and 270 | if a request doesn't get a response after `timeoutInternet`, 271 | then Handli shows the UI-blocking modal. 272 | - If the user's internet connection **isn't slow** and 273 | if a request doesn't get a response after `timeoutServer`, 274 | then Handli shows the UI-blocking modal. 275 | 276 | See 277 | [Live Demo - Slow Internet](https://brillout.github.io/handli#slow-internet) 278 | and 279 | [Live Demo - Unresponsive Server](https://brillout.github.io/handli#unresponsive-server). 280 | 281 | ### Does it work only with `fetch`? 282 | 283 | Handli works with any fetch-like library. 284 | That is, Handli works as long as: 285 | - `response.status` holds the status code of the response. 286 | (With `response` we mean `let response = await aFetchLikeLibrary('https://example.org')`.) 287 | - `response.ok` holds `true` or `false` denoting whether the request was a success. 288 | (That is `assert(response.ok === 200<=response.status && response.status<=299)`.) 289 | - Throws an error if and only if the request didn't get a response. 290 | (That is if the user has internet connection problems or if the server is not responsive.) 291 | 292 | ### Does it handle errors on Node.js? 293 | 294 | No. 295 | Handli handles connection issues only in the browser. 296 | 297 | ### What about Universal/Isomorphic/SSR? 298 | 299 | Handli supports code that are meant to be run in the browser as well as on Node.js. 300 | 301 | When run in Node.js, `handli` is transparent: 302 | It does nothing and returns what your request function returns. 303 | 304 | On Node.js, the following 305 | 306 | ~~~js 307 | const response = await handli(() => fetch(url)); 308 | ~~~ 309 | 310 | is equivalent to 311 | 312 | ~~~js 313 | const response = await fetch(url); 314 | ~~~ 315 | 316 | 317 | ### Does it support simultaneous requests? 318 | 319 | Yes. 320 | Handli blocks the UI until 321 | all requests get a successful response 322 | (or an error that is handled by you). 323 | 324 | 325 | ### How do I progressively remove Handli? 326 | 327 | [Handle errors yourself](#how-do-i-handle-errors-myself) 328 | at more and more places 329 | until you can remove your dependency on the `handli` package. 330 | 331 | This can be useful strategy 332 | to quickly ship a prototype wihtout worrying about connection issues at first, 333 | and later, 334 | as your prototype grows into a large application, 335 | progressively replace Handli with your own error handling. 336 | --------------------------------------------------------------------------------