├── .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 |
37 | )
38 | const handliIcon = (
39 |
45 | )
46 | return (
47 |
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 |
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 |
321 | Run
322 |
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 |
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 |
--------------------------------------------------------------------------------