format code
parent
6157bc0879
commit
eba65bd654
|
|
@ -0,0 +1,4 @@
|
|||
trailingComma: 'es5'
|
||||
tabWidth: 2
|
||||
semi: false
|
||||
singleQuote: true
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
# UptimeFlare
|
||||
|
||||
Another **serverless (and free!)** uptime monitoring & status page on Cloudflare Workers with more advanced features and nice UI.
|
||||
|
||||
## ⭐Features
|
||||
|
||||
- **Opensource** & Easy to deploy (in 10 minutes without local tools) & Free
|
||||
|
||||
#### Monitoring
|
||||
|
||||
- Up to 50 **2-minute** interval check on free plan (Up to unlimited 1-min check on paid Workers plan)
|
||||
- Monitoring for **HTTP/HTTPS/TCP port**
|
||||
- Up to 90-day uptime history and uptime percentage
|
||||
|
|
@ -13,24 +16,24 @@ Another **serverless (and free!)** uptime monitoring & status page on Cloudflare
|
|||
- Email Notifications & Customizable Webhook
|
||||
|
||||
#### Status page
|
||||
|
||||
- **Interactive ping(response time) chart** for all type of monitors
|
||||
- Responsive UI & Follow system theme
|
||||
- Customizable status page
|
||||
- Use your own domain with CNAME
|
||||
|
||||
## Demo
|
||||
|
||||
My status page (Online demo): https://uptimeflare.pages.dev/
|
||||
|
||||
Some screenshots: TODO
|
||||
|
||||
## Quickstart
|
||||
|
||||
|
||||
## More documentation
|
||||
|
||||
|
||||
## TODOs
|
||||
|
||||
- SSL?
|
||||
- Slack / Telegram
|
||||
- Incident timeline
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import { MonitorState, MonitorTarget } from "@/uptime.types";
|
||||
import { Box, Tooltip } from "@mantine/core";
|
||||
import { MonitorState, MonitorTarget } from '@/uptime.types'
|
||||
import { Box, Tooltip } from '@mantine/core'
|
||||
|
||||
|
||||
export default function DetailBar({ monitor, state }: { monitor: MonitorTarget, state: MonitorState }) {
|
||||
export default function DetailBar({
|
||||
monitor,
|
||||
state,
|
||||
}: {
|
||||
monitor: MonitorTarget
|
||||
state: MonitorState
|
||||
}) {
|
||||
// const day = new Date()
|
||||
// day.setHours(0, 0, 0, 0)
|
||||
// const dayStart = day.getTime()
|
||||
|
|
@ -14,7 +19,6 @@ export default function DetailBar({ monitor, state }: { monitor: MonitorTarget,
|
|||
const uptimePercentBars = []
|
||||
|
||||
for (let i = 89; i > 0; i--) {
|
||||
|
||||
let dayDownTime = 0
|
||||
let dayTotalTime = 0
|
||||
|
||||
|
|
@ -22,14 +26,31 @@ export default function DetailBar({ monitor, state }: { monitor: MonitorTarget,
|
|||
|
||||
uptimePercentBars.push(
|
||||
<Tooltip key={i} label="Tooltip">
|
||||
<div style={{ height: '20px', width: '7px', background: 'green', borderRadius: '2px', marginLeft: '1px', marginRight: '1px' }}/>
|
||||
<div
|
||||
style={{
|
||||
height: '20px',
|
||||
width: '7px',
|
||||
background: 'green',
|
||||
borderRadius: '2px',
|
||||
marginLeft: '1px',
|
||||
marginRight: '1px',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box style={{ display: 'flex', flexWrap: 'nowrap', marginTop: '10px', marginBottom: '5px' }} visibleFrom='540'>
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
marginTop: '10px',
|
||||
marginBottom: '5px',
|
||||
}}
|
||||
visibleFrom="540"
|
||||
>
|
||||
{uptimePercentBars}
|
||||
</Box>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
import { Line } from "react-chartjs-2"
|
||||
import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip as ChartTooltip, Legend, TimeScale } from 'chart.js'
|
||||
import { Line } from 'react-chartjs-2'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip as ChartTooltip,
|
||||
Legend,
|
||||
TimeScale,
|
||||
} from 'chart.js'
|
||||
import 'chartjs-adapter-moment'
|
||||
import { MonitorState, MonitorTarget } from "@/uptime.types"
|
||||
import { iataToCountry } from "@/util/iata"
|
||||
import { MonitorState, MonitorTarget } from '@/uptime.types'
|
||||
import { iataToCountry } from '@/util/iata'
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
|
|
@ -15,10 +25,18 @@ ChartJS.register(
|
|||
TimeScale
|
||||
)
|
||||
|
||||
|
||||
export default function DetailChart({ monitor, state }: { monitor: MonitorTarget, state: MonitorState }) {
|
||||
|
||||
const latencyData = state.latency[monitor.id].recent.map((point) => ({ x: point.time * 1000, y: point.ping, loc: point.loc }))
|
||||
export default function DetailChart({
|
||||
monitor,
|
||||
state,
|
||||
}: {
|
||||
monitor: MonitorTarget
|
||||
state: MonitorState
|
||||
}) {
|
||||
const latencyData = state.latency[monitor.id].recent.map((point) => ({
|
||||
x: point.time * 1000,
|
||||
y: point.ping,
|
||||
loc: point.loc,
|
||||
}))
|
||||
|
||||
let data = {
|
||||
datasets: [
|
||||
|
|
@ -28,9 +46,9 @@ export default function DetailChart({ monitor, state }: { monitor: MonitorTarget
|
|||
borderWidth: 2,
|
||||
radius: 0,
|
||||
cubicInterpolationMode: 'monotone' as const,
|
||||
tension: 0.4
|
||||
}
|
||||
]
|
||||
tension: 0.4,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
let options = {
|
||||
|
|
@ -41,7 +59,7 @@ export default function DetailChart({ monitor, state }: { monitor: MonitorTarget
|
|||
intersect: false,
|
||||
},
|
||||
animation: {
|
||||
duration: 0
|
||||
duration: 0,
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
|
|
@ -50,16 +68,16 @@ export default function DetailChart({ monitor, state }: { monitor: MonitorTarget
|
|||
if (item.parsed.y) {
|
||||
return `${item.parsed.y}ms (${iataToCountry(item.raw.loc)})`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
display: false,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Response times(ms)',
|
||||
align: 'start' as const
|
||||
align: 'start' as const,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
|
|
@ -68,10 +86,10 @@ export default function DetailChart({ monitor, state }: { monitor: MonitorTarget
|
|||
ticks: {
|
||||
source: 'auto' as const,
|
||||
maxRotation: 0,
|
||||
autoSkip: true
|
||||
}
|
||||
}
|
||||
}
|
||||
autoSkip: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -80,4 +98,3 @@ export default function DetailChart({ monitor, state }: { monitor: MonitorTarget
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
import { Container, Group, Text } from '@mantine/core';
|
||||
import classes from '@/styles/Header.module.css';
|
||||
import config from '@/uptime.config';
|
||||
|
||||
import { Container, Group, Text } from '@mantine/core'
|
||||
import classes from '@/styles/Header.module.css'
|
||||
import config from '@/uptime.config'
|
||||
|
||||
export default function Header() {
|
||||
|
||||
const linkToElement = (link: { label: string; link: string; highlight?: boolean; }) => {
|
||||
const linkToElement = (link: {
|
||||
label: string
|
||||
link: string
|
||||
highlight?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.link}
|
||||
target='_blank'
|
||||
target="_blank"
|
||||
className={classes.link}
|
||||
data-active={link.highlight}
|
||||
>
|
||||
|
|
@ -23,21 +25,32 @@ export default function Header() {
|
|||
<header className={classes.header}>
|
||||
<Container size="md" className={classes.inner}>
|
||||
<div>
|
||||
<a href='https://github.com/lyc8503/UptimeFlare' target='_blank'>
|
||||
<Text size='xl' span>🕒</Text>
|
||||
<Text size='xl' span fw={700} variant="gradient" gradient={{ from: 'blue', to: 'cyan', deg: 90 }}>UptimeFlare</Text>
|
||||
<a href="https://github.com/lyc8503/UptimeFlare" target="_blank">
|
||||
<Text size="xl" span>
|
||||
🕒
|
||||
</Text>
|
||||
<Text
|
||||
size="xl"
|
||||
span
|
||||
fw={700}
|
||||
variant="gradient"
|
||||
gradient={{ from: 'blue', to: 'cyan', deg: 90 }}
|
||||
>
|
||||
UptimeFlare
|
||||
</Text>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Group gap={5} visibleFrom='sm'>
|
||||
<Group gap={5} visibleFrom="sm">
|
||||
{config.page.links.map(linkToElement)}
|
||||
</Group>
|
||||
|
||||
<Group gap={5} hiddenFrom='sm'>
|
||||
{config.page.links.filter(link => link.highlight).map(linkToElement)}
|
||||
<Group gap={5} hiddenFrom="sm">
|
||||
{config.page.links
|
||||
.filter((link) => link.highlight)
|
||||
.map(linkToElement)}
|
||||
</Group>
|
||||
|
||||
</Container>
|
||||
</header>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
|
||||
import { Text } from "@mantine/core"
|
||||
import { MonitorState, MonitorTarget } from "@/uptime.types"
|
||||
import { IconAlertCircle, IconCircleCheck } from "@tabler/icons-react"
|
||||
import DetailChart from "./DetailChart"
|
||||
import DetailBar from "./DetailBar"
|
||||
import { Text } from '@mantine/core'
|
||||
import { MonitorState, MonitorTarget } from '@/uptime.types'
|
||||
import { IconAlertCircle, IconCircleCheck } from '@tabler/icons-react'
|
||||
import DetailChart from './DetailChart'
|
||||
import DetailBar from './DetailBar'
|
||||
|
||||
function getColor(percent: number | string, darker: boolean): string {
|
||||
percent = Number(percent)
|
||||
|
|
@ -18,32 +17,67 @@ function getColor(percent: number | string, darker: boolean): string {
|
|||
}
|
||||
}
|
||||
|
||||
export default function MonitorDetail({ monitor, state }: { monitor: MonitorTarget, state: MonitorState }) {
|
||||
|
||||
if (!state.latency[monitor.id]) return (
|
||||
export default function MonitorDetail({
|
||||
monitor,
|
||||
state,
|
||||
}: {
|
||||
monitor: MonitorTarget
|
||||
state: MonitorState
|
||||
}) {
|
||||
if (!state.latency[monitor.id])
|
||||
return (
|
||||
<>
|
||||
<Text mt='sm' fw={700}>{monitor.name}</Text>
|
||||
<Text mt='sm' fw={700}>No data available, please make sure you have deployed your workers with latest config and check your worker status!</Text>
|
||||
<Text mt="sm" fw={700}>
|
||||
{monitor.name}
|
||||
</Text>
|
||||
<Text mt="sm" fw={700}>
|
||||
No data available, please make sure you have deployed your workers
|
||||
with latest config and check your worker status!
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
|
||||
const statusIcon = state.incident[monitor.id].slice(-1)[0].end === undefined ? <IconAlertCircle style={{ width: '1.25em', height: '1.25em', color: '#b91c1c' }} /> : <IconCircleCheck style={{ width: '1.25em', height: '1.25em', color: '#059669' }} />
|
||||
const statusIcon =
|
||||
state.incident[monitor.id].slice(-1)[0].end === undefined ? (
|
||||
<IconAlertCircle
|
||||
style={{ width: '1.25em', height: '1.25em', color: '#b91c1c' }}
|
||||
/>
|
||||
) : (
|
||||
<IconCircleCheck
|
||||
style={{ width: '1.25em', height: '1.25em', color: '#059669' }}
|
||||
/>
|
||||
)
|
||||
|
||||
let totalTime = Date.now() / 1000 - state.incident[monitor.id][0].start[0]
|
||||
let downTime = 0
|
||||
for (let incident of state.incident[monitor.id]) {
|
||||
downTime += (incident.end ?? (Date.now() / 1000)) - incident.start[0]
|
||||
downTime += (incident.end ?? Date.now() / 1000) - incident.start[0]
|
||||
}
|
||||
|
||||
console.log(totalTime)
|
||||
console.log(downTime)
|
||||
const uptimePercent = ((totalTime - downTime) / totalTime * 100).toPrecision(4)
|
||||
const uptimePercent = (
|
||||
((totalTime - downTime) / totalTime) *
|
||||
100
|
||||
).toPrecision(4)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Text mt='sm' fw={700} style={{ display: 'inline-flex', alignItems: 'center' }}>{statusIcon} {monitor.name}</Text>
|
||||
<Text mt='sm' fw={700} style={{ display: 'inline', color: getColor(uptimePercent, true) }}>Overall: {uptimePercent}%</Text>
|
||||
<Text
|
||||
mt="sm"
|
||||
fw={700}
|
||||
style={{ display: 'inline-flex', alignItems: 'center' }}
|
||||
>
|
||||
{statusIcon} {monitor.name}
|
||||
</Text>
|
||||
<Text
|
||||
mt="sm"
|
||||
fw={700}
|
||||
style={{ display: 'inline', color: getColor(uptimePercent, true) }}
|
||||
>
|
||||
Overall: {uptimePercent}%
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<DetailBar monitor={monitor} state={state} />
|
||||
|
|
|
|||
|
|
@ -1,12 +1,26 @@
|
|||
import { MonitorState, MonitorTarget } from "@/uptime.types";
|
||||
import { Card, Center, Divider } from "@mantine/core";
|
||||
import MonitorDetail from "./MonitorDetail";
|
||||
|
||||
export default function MonitorList({ config, state }: { config: any, state: MonitorState }) {
|
||||
import { MonitorState, MonitorTarget } from '@/uptime.types'
|
||||
import { Card, Center, Divider } from '@mantine/core'
|
||||
import MonitorDetail from './MonitorDetail'
|
||||
|
||||
export default function MonitorList({
|
||||
config,
|
||||
state,
|
||||
}: {
|
||||
config: any
|
||||
state: MonitorState
|
||||
}) {
|
||||
return (
|
||||
<Center>
|
||||
<Card shadow="sm" padding="lg" radius="md" ml="xl" mr="xl" mt="xl" withBorder style={{ width: '865px' }}>
|
||||
<Card
|
||||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
ml="xl"
|
||||
mr="xl"
|
||||
mt="xl"
|
||||
withBorder
|
||||
style={{ width: '865px' }}
|
||||
>
|
||||
{config.monitors.map((monitor: MonitorTarget) => (
|
||||
<div key={monitor.id}>
|
||||
<Card.Section ml="xs" mr="xs">
|
||||
|
|
@ -19,4 +33,3 @@ export default function MonitorList({ config, state }: { config: any, state: Mon
|
|||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,19 @@
|
|||
import dynamic from 'next/dynamic'
|
||||
import React from 'react'
|
||||
|
||||
const NoSsr = (props: { children: string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | React.PromiseLikeOfReactNode | null | undefined }) => (
|
||||
<React.Fragment>{props.children}</React.Fragment>
|
||||
)
|
||||
const NoSsr = (props: {
|
||||
children:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| React.ReactElement<any, string | React.JSXElementConstructor<any>>
|
||||
| Iterable<React.ReactNode>
|
||||
| React.ReactPortal
|
||||
| React.PromiseLikeOfReactNode
|
||||
| null
|
||||
| undefined
|
||||
}) => <React.Fragment>{props.children}</React.Fragment>
|
||||
|
||||
export default dynamic(() => Promise.resolve(NoSsr), {
|
||||
ssr: false
|
||||
ssr: false,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,27 +1,46 @@
|
|||
import { Center, Title } from '@mantine/core'
|
||||
import { IconCircleCheck, IconAlertCircle } from '@tabler/icons-react'
|
||||
|
||||
export default function OverallStatus({ state }: { state: { overallUp: number, overallDown: number, lastUpdate: number }}) {
|
||||
|
||||
export default function OverallStatus({
|
||||
state,
|
||||
}: {
|
||||
state: { overallUp: number; overallDown: number; lastUpdate: number }
|
||||
}) {
|
||||
let statusString = ''
|
||||
let icon = <IconAlertCircle style={{ width: 64, height: 64, color: '#b91c1c' }} />
|
||||
let icon = (
|
||||
<IconAlertCircle style={{ width: 64, height: 64, color: '#b91c1c' }} />
|
||||
)
|
||||
if (state.overallUp === 0 && state.overallDown === 0) {
|
||||
statusString = 'No data yet'
|
||||
} else if (state.overallUp === 0) {
|
||||
statusString = 'All systems not operational'
|
||||
} else if (state.overallDown === 0) {
|
||||
statusString = 'All systems operational'
|
||||
icon = <IconCircleCheck style={{ width: 64, height: 64, color: '#059669' }} />
|
||||
icon = (
|
||||
<IconCircleCheck style={{ width: 64, height: 64, color: '#059669' }} />
|
||||
)
|
||||
} else {
|
||||
statusString = `Some systems not operational (${state.overallDown} out of ${state.overallUp + state.overallDown})`
|
||||
statusString = `Some systems not operational (${state.overallDown} out of ${
|
||||
state.overallUp + state.overallDown
|
||||
})`
|
||||
}
|
||||
|
||||
return (<>
|
||||
<Center>
|
||||
{icon}
|
||||
</Center>
|
||||
<Title mt="sm" style={{ textAlign: 'center' }} order={1}>{statusString}</Title>
|
||||
<Title mt="sm" style={{ textAlign: 'center', color: '#70778c' }} order={5}>Last updated on: {`${new Date(state.lastUpdate * 1000).toLocaleString()} (${Math.round(Date.now() / 1000) - state.lastUpdate} sec ago)`}</Title>
|
||||
</>)
|
||||
return (
|
||||
<>
|
||||
<Center>{icon}</Center>
|
||||
<Title mt="sm" style={{ textAlign: 'center' }} order={1}>
|
||||
{statusString}
|
||||
</Title>
|
||||
<Title
|
||||
mt="sm"
|
||||
style={{ textAlign: 'center', color: '#70778c' }}
|
||||
order={5}
|
||||
>
|
||||
Last updated on:{' '}
|
||||
{`${new Date(state.lastUpdate * 1000).toLocaleString()} (${
|
||||
Math.round(Date.now() / 1000) - state.lastUpdate
|
||||
} sec ago)`}
|
||||
</Title>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
"postcss": "^8.4.31",
|
||||
"postcss-preset-mantine": "^1.8.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "3.0.3",
|
||||
"typescript": "^5",
|
||||
"wrangler": "^3.13.1"
|
||||
}
|
||||
|
|
@ -3988,6 +3989,21 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/prettier/-/prettier-3.0.3.tgz",
|
||||
"integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/printable-characters": {
|
||||
"version": "1.0.42",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/printable-characters/-/printable-characters-1.0.42.tgz",
|
||||
|
|
@ -7905,6 +7921,12 @@
|
|||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||
"dev": true
|
||||
},
|
||||
"prettier": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/prettier/-/prettier-3.0.3.tgz",
|
||||
"integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
|
||||
"dev": true
|
||||
},
|
||||
"printable-characters": {
|
||||
"version": "1.0.42",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/printable-characters/-/printable-characters-1.0.42.tgz",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
"postcss": "^8.4.31",
|
||||
"postcss-preset-mantine": "^1.8.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "3.0.3",
|
||||
"typescript": "^5",
|
||||
"wrangler": "^3.13.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import '@mantine/core/styles.css';
|
||||
import '@mantine/core/styles.css'
|
||||
import type { AppProps } from 'next/app'
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import NoSsr from '@/components/NoSsr';
|
||||
|
||||
import { MantineProvider } from '@mantine/core'
|
||||
import NoSsr from '@/components/NoSsr'
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<NoSsr>
|
||||
<MantineProvider defaultColorScheme='auto'>
|
||||
<MantineProvider defaultColorScheme="auto">
|
||||
<Component {...pageProps} />
|
||||
</MantineProvider>
|
||||
</NoSsr>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Html, Head, Main, NextScript } from 'next/document'
|
||||
import { ColorSchemeScript } from '@mantine/core';
|
||||
import { ColorSchemeScript } from '@mantine/core'
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -23,39 +23,51 @@ export default function Home({ state }: { state: MonitorState }) {
|
|||
<main className={inter.className}>
|
||||
<Header />
|
||||
|
||||
{
|
||||
state === undefined ?
|
||||
(
|
||||
{state === undefined ? (
|
||||
<Center>
|
||||
<Text fw={700}>
|
||||
Monitor State is not defined now, please check your worker's status and KV binding!
|
||||
Monitor State is not defined now, please check your worker's
|
||||
status and KV binding!
|
||||
</Text>
|
||||
</Center>
|
||||
) :
|
||||
(
|
||||
) : (
|
||||
<div>
|
||||
<OverallStatus state={state} />
|
||||
<MonitorList config={config} state={state} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)}
|
||||
|
||||
<Divider mt='lg' />
|
||||
<Divider mt="lg" />
|
||||
<Center>
|
||||
<Text size='xs' mt='xs' mb='xs'>
|
||||
Open-source monitoring and status page powered by <a href='https://github.com/lyc8503/UptimeFlare' target='_blank'>Uptimeflare</a> and <a href='https://www.cloudflare.com/' target='_blank'>Cloudflare</a>, made with ❤ by <a href='https://github.com/lyc8503' target='_blank'>lyc8503</a>.
|
||||
<Text size="xs" mt="xs" mb="xs">
|
||||
Open-source monitoring and status page powered by{' '}
|
||||
<a href="https://github.com/lyc8503/UptimeFlare" target="_blank">
|
||||
Uptimeflare
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a href="https://www.cloudflare.com/" target="_blank">
|
||||
Cloudflare
|
||||
</a>
|
||||
, made with ❤ by{' '}
|
||||
<a href="https://github.com/lyc8503" target="_blank">
|
||||
lyc8503
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</Center>
|
||||
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export async function getServerSideProps() {
|
||||
const { UPTIMEFLARE_STATE } = process.env as unknown as { UPTIMEFLARE_STATE: KVNamespace }
|
||||
const state = await UPTIMEFLARE_STATE?.get('state', 'json') as unknown as MonitorState
|
||||
const { UPTIMEFLARE_STATE } = process.env as unknown as {
|
||||
UPTIMEFLARE_STATE: KVNamespace
|
||||
}
|
||||
const state = (await UPTIMEFLARE_STATE?.get(
|
||||
'state',
|
||||
'json'
|
||||
)) as unknown as MonitorState
|
||||
|
||||
return { props: { state } }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
height: rem(56px);
|
||||
margin-bottom: rem(120px);
|
||||
background-color: var(--mantine-color-body);
|
||||
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
border-bottom: rem(1px) solid
|
||||
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
.inner {
|
||||
|
|
@ -23,7 +24,10 @@
|
|||
font-weight: 500;
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-0),
|
||||
var(--mantine-color-dark-6)
|
||||
);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme] &[data-active] {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
|
||||
|
||||
const config = {
|
||||
dateLocale: "zh-CN",
|
||||
timezone: "Asia/Shanghai",
|
||||
dateLocale: 'zh-CN',
|
||||
timezone: 'Asia/Shanghai',
|
||||
page: {
|
||||
title: "lyc8503's Status Page",
|
||||
links: [
|
||||
{ link: 'https://github.com/lyc8503', label: 'GitHub' },
|
||||
{ link: 'https://blog.lyc8503.site/', label: 'Blog' },
|
||||
{ link: 'mailto:me@lyc8503.site', label: 'Email Me', highlight: true }
|
||||
]
|
||||
{ link: 'mailto:me@lyc8503.site', label: 'Email Me', highlight: true },
|
||||
],
|
||||
},
|
||||
callback: async (statusChangeMsg: string) => {
|
||||
await fetch('https://server.lyc8503.site/wepush?key=wepushkey&msg=' + statusChangeMsg)
|
||||
await fetch(
|
||||
'https://server.lyc8503.site/wepush?key=wepushkey&msg=' + statusChangeMsg
|
||||
)
|
||||
},
|
||||
monitors: [
|
||||
{
|
||||
|
|
@ -23,9 +23,9 @@ const config = {
|
|||
expectedCode: [200],
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
"User-Agent": "Uptimeflare"
|
||||
'User-Agent': 'Uptimeflare',
|
||||
},
|
||||
body: undefined
|
||||
body: undefined,
|
||||
},
|
||||
{
|
||||
id: 'cloudflare',
|
||||
|
|
@ -35,30 +35,30 @@ const config = {
|
|||
expectedCode: [200],
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
"User-Agent": "Uptimeflare"
|
||||
'User-Agent': 'Uptimeflare',
|
||||
},
|
||||
body: undefined
|
||||
body: undefined,
|
||||
},
|
||||
{
|
||||
id: 'blog',
|
||||
name: 'My Blog',
|
||||
method: 'GET',
|
||||
target: 'https://blog.lyc8503.site',
|
||||
timeout: 10000
|
||||
timeout: 10000,
|
||||
},
|
||||
{
|
||||
id: 'pan',
|
||||
name: 'My Fileshare',
|
||||
method: 'GET',
|
||||
target: 'https://pan.lyc8503.site',
|
||||
timeout: 10000
|
||||
timeout: 10000,
|
||||
},
|
||||
{
|
||||
id: 'server',
|
||||
name: 'My Aliyun Server',
|
||||
method: 'GET',
|
||||
target: 'https://server.lyc8503.site',
|
||||
timeout: 10000
|
||||
timeout: 10000,
|
||||
},
|
||||
{
|
||||
id: 'homelab',
|
||||
|
|
@ -67,15 +67,22 @@ const config = {
|
|||
target: 'https://lyc8503.cn4.quickconnect.cn/webstation/hello.txt',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
"Cookie": "type=tunnel;"
|
||||
Cookie: 'type=tunnel;',
|
||||
},
|
||||
responseKeyword: 'Hello'
|
||||
responseKeyword: 'Hello',
|
||||
},
|
||||
{
|
||||
id: 'homessh',
|
||||
name: 'HomeSSH',
|
||||
method: 'TCP_PING',
|
||||
target: '20.24.87.148:22',
|
||||
timeout: 10000,
|
||||
},
|
||||
{
|
||||
id: 'broken-test',
|
||||
name: 'Ping 1.1.1.1',
|
||||
method: 'TCP_PING',
|
||||
target: '1.1.1.1:1234'
|
||||
target: '1.1.1.1:1234',
|
||||
},
|
||||
{
|
||||
id: '404test',
|
||||
|
|
@ -88,7 +95,7 @@ const config = {
|
|||
name: '404-test2',
|
||||
method: 'GET',
|
||||
target: 'https://www.baidu.com/404',
|
||||
expectedCodes: [404, 405]
|
||||
expectedCodes: [404, 405],
|
||||
},
|
||||
{
|
||||
id: '404test3',
|
||||
|
|
@ -96,9 +103,9 @@ const config = {
|
|||
method: 'GET',
|
||||
target: 'https://www.baidu.com/404',
|
||||
expectedCodes: [404],
|
||||
responseKeyword: 'Hello'
|
||||
}
|
||||
]
|
||||
responseKeyword: 'Hello',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default config
|
||||
|
|
|
|||
|
|
@ -1,40 +1,45 @@
|
|||
type MonitorState = {
|
||||
lastUpdate: number,
|
||||
overallUp: number,
|
||||
overallDown: number,
|
||||
incident: Record<string, {
|
||||
start: number[],
|
||||
end: number | undefined, // undefined if it's still open
|
||||
lastUpdate: number
|
||||
overallUp: number
|
||||
overallDown: number
|
||||
incident: Record<
|
||||
string,
|
||||
{
|
||||
start: number[]
|
||||
end: number | undefined // undefined if it's still open
|
||||
error: string[]
|
||||
}[]>,
|
||||
}[]
|
||||
>
|
||||
|
||||
latency: Record<string, {
|
||||
latency: Record<
|
||||
string,
|
||||
{
|
||||
recent: {
|
||||
loc: string,
|
||||
ping: number,
|
||||
loc: string
|
||||
ping: number
|
||||
time: number
|
||||
}[], // recent 12 hour data, 2 min interval
|
||||
}[] // recent 12 hour data, 2 min interval
|
||||
all: {
|
||||
loc: string,
|
||||
ping: number,
|
||||
loc: string
|
||||
ping: number
|
||||
time: number
|
||||
}[] // all data in 90 days, 1 hour interval
|
||||
}>
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
type MonitorTarget = {
|
||||
id: string,
|
||||
name: string,
|
||||
method: string, // "TCP_PING" or Http Method (e.g. GET, POST, OPTIONS, etc.)
|
||||
target: string, // url for http, hostname:port for tcp
|
||||
id: string
|
||||
name: string
|
||||
method: string // "TCP_PING" or Http Method (e.g. GET, POST, OPTIONS, etc.)
|
||||
target: string // url for http, hostname:port for tcp
|
||||
|
||||
// HTTP Code
|
||||
expectedCodes?: number[],
|
||||
timeout?: number,
|
||||
headers?: Record<string, string | undefined>,
|
||||
body?: BodyInit,
|
||||
expectedCodes?: number[]
|
||||
timeout?: number
|
||||
headers?: Record<string, string | undefined>
|
||||
body?: BodyInit
|
||||
responseKeyword?: string
|
||||
}
|
||||
|
||||
|
||||
export type { MonitorState, MonitorTarget }
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,46 +1,45 @@
|
|||
import { connect } from 'cloudflare:sockets'
|
||||
import config from '../../uptime.config'
|
||||
import { fetchTimeout, getWorkerLocation, withTimeout } from './util'
|
||||
import { MonitorState, MonitorTarget } from "../../uptime.types"
|
||||
import { connect } from 'cloudflare:sockets';
|
||||
import config from '../../uptime.config';
|
||||
import { fetchTimeout, getWorkerLocation, withTimeout } from './util';
|
||||
import { MonitorState, MonitorTarget } from '../../uptime.types';
|
||||
|
||||
export interface Env {
|
||||
UPTIMEFLARE_STATE: KVNamespace
|
||||
UPTIMEFLARE_STATE: KVNamespace;
|
||||
}
|
||||
|
||||
async function getStatus(monitor: MonitorTarget): Promise<{ ping: number; up: boolean; err: string }> {
|
||||
|
||||
let status = {
|
||||
ping: 0,
|
||||
up: false,
|
||||
err: "Unknown"
|
||||
}
|
||||
err: 'Unknown',
|
||||
};
|
||||
|
||||
const startTime = Date.now()
|
||||
const startTime = Date.now();
|
||||
|
||||
if (monitor.method === "TCP_PING") {
|
||||
if (monitor.method === 'TCP_PING') {
|
||||
// TCP port endpoint monitor
|
||||
// TODO: TCP timeout
|
||||
try {
|
||||
const [hostname, port] = monitor.target.split(":")
|
||||
const [hostname, port] = monitor.target.split(':');
|
||||
|
||||
// Write "PING\n"
|
||||
const socket = connect({ hostname: hostname, port: Number(port) })
|
||||
const writer = socket.writable.getWriter()
|
||||
await writer.write(new TextEncoder().encode("PING\n"))
|
||||
const socket = connect({ hostname: hostname, port: Number(port) });
|
||||
const writer = socket.writable.getWriter();
|
||||
await writer.write(new TextEncoder().encode('PING\n'));
|
||||
// Can't do this: await socket.close()
|
||||
|
||||
// https://github.com/cloudflare/workerd/issues/1305
|
||||
await withTimeout(monitor.timeout || 10000, socket.closed)
|
||||
await withTimeout(monitor.timeout || 10000, socket.closed);
|
||||
|
||||
console.log(`${monitor.name} connected to ${monitor.target}`)
|
||||
console.log(`${monitor.name} connected to ${monitor.target}`);
|
||||
|
||||
status.ping = Date.now() - startTime
|
||||
status.up = true
|
||||
status.err = ""
|
||||
status.ping = Date.now() - startTime;
|
||||
status.up = true;
|
||||
status.err = '';
|
||||
} catch (e: Error | any) {
|
||||
console.log(`${monitor.name} errored with ${e.name}: ${e.message}`)
|
||||
status.up = false
|
||||
status.err = e.name + ": " + e.message
|
||||
console.log(`${monitor.name} errored with ${e.name}: ${e.message}`);
|
||||
status.up = false;
|
||||
status.err = e.name + ': ' + e.message;
|
||||
}
|
||||
} else {
|
||||
// HTTP endpoint monitor
|
||||
|
|
@ -51,84 +50,85 @@ async function getStatus(monitor: MonitorTarget): Promise<{ ping: number; up: bo
|
|||
body: monitor.body,
|
||||
cf: {
|
||||
cacheTtlByStatus: {
|
||||
'100-599': -1 // Don't cache any status code, from https://developers.cloudflare.com/workers/runtime-apis/request/#requestinitcfproperties
|
||||
}
|
||||
}
|
||||
})
|
||||
'100-599': -1, // Don't cache any status code, from https://developers.cloudflare.com/workers/runtime-apis/request/#requestinitcfproperties
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`${monitor.name} responded with ${response.status}`)
|
||||
status.ping = Date.now() - startTime
|
||||
console.log(`${monitor.name} responded with ${response.status}`);
|
||||
status.ping = Date.now() - startTime;
|
||||
|
||||
if (monitor.expectedCodes) {
|
||||
if (!monitor.expectedCodes.includes(response.status)) {
|
||||
status.up = false
|
||||
status.err = `Expected codes: ${JSON.stringify(monitor.expectedCodes)}, Got: ${response.status}`
|
||||
return status
|
||||
status.up = false;
|
||||
status.err = `Expected codes: ${JSON.stringify(monitor.expectedCodes)}, Got: ${response.status}`;
|
||||
return status;
|
||||
}
|
||||
} else {
|
||||
if (response.status < 200 || response.status > 299) {
|
||||
status.up = false
|
||||
status.err = `Expected codes: 2xx, Got: ${response.status}`
|
||||
return status
|
||||
status.up = false;
|
||||
status.err = `Expected codes: 2xx, Got: ${response.status}`;
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
if (monitor.responseKeyword) {
|
||||
const responseBody = await response.text()
|
||||
const responseBody = await response.text();
|
||||
if (!responseBody.includes(monitor.responseKeyword)) {
|
||||
status.up = false
|
||||
status.err = "HTTP response doesn't contain the configured keyword"
|
||||
return status
|
||||
status.up = false;
|
||||
status.err = "HTTP response doesn't contain the configured keyword";
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
status.up = true
|
||||
status.err = ""
|
||||
status.up = true;
|
||||
status.err = '';
|
||||
} catch (e: any) {
|
||||
console.log(`${monitor.name} errored with ${e.name}: ${e.message}`)
|
||||
if (e.name === "AbortError") {
|
||||
status.ping = monitor.timeout || 10000
|
||||
status.up = false
|
||||
status.err = `Timeout after ${status.ping}ms`
|
||||
console.log(`${monitor.name} errored with ${e.name}: ${e.message}`);
|
||||
if (e.name === 'AbortError') {
|
||||
status.ping = monitor.timeout || 10000;
|
||||
status.up = false;
|
||||
status.err = `Timeout after ${status.ping}ms`;
|
||||
} else {
|
||||
status.up = false
|
||||
status.err = e.name + ": " + e.message
|
||||
status.up = false;
|
||||
status.err = e.name + ': ' + e.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
|
||||
const workerLocation = await getWorkerLocation() || "ERROR"
|
||||
console.log(`Running scheduled event on ${workerLocation}...`)
|
||||
const workerLocation = (await getWorkerLocation()) || 'ERROR';
|
||||
console.log(`Running scheduled event on ${workerLocation}...`);
|
||||
|
||||
// Read state, set init state if it doesn't exist
|
||||
let state = await env.UPTIMEFLARE_STATE.get("state", { type: 'json' }) as unknown as MonitorState || {
|
||||
let state =
|
||||
((await env.UPTIMEFLARE_STATE.get('state', { type: 'json' })) as unknown as MonitorState) ||
|
||||
({
|
||||
lastUpdate: 0,
|
||||
overallUp: 0,
|
||||
overallDown: 0,
|
||||
incident: {},
|
||||
latency: {}
|
||||
} as MonitorState
|
||||
state.overallDown = 0
|
||||
state.overallUp = 0
|
||||
latency: {},
|
||||
} as MonitorState);
|
||||
state.overallDown = 0;
|
||||
state.overallUp = 0;
|
||||
|
||||
// Check each monitor
|
||||
// TODO: callback exception handler
|
||||
// TODO: advanced status check
|
||||
// TODO: concurrent status check
|
||||
for (const monitor of config.monitors) {
|
||||
console.log(`[${workerLocation}] Checking ${monitor.name}...`)
|
||||
console.log(`[${workerLocation}] Checking ${monitor.name}...`);
|
||||
|
||||
const status = await getStatus(monitor)
|
||||
const currentTimeSecond = Math.round(Date.now() / 1000)
|
||||
const status = await getStatus(monitor);
|
||||
const currentTimeSecond = Math.round(Date.now() / 1000);
|
||||
|
||||
// Update counters
|
||||
status.up ? state.overallUp++ : state.overallDown++
|
||||
status.up ? state.overallUp++ : state.overallDown++;
|
||||
|
||||
// Update incidents
|
||||
|
||||
|
|
@ -137,19 +137,23 @@ export default {
|
|||
{
|
||||
start: [currentTimeSecond],
|
||||
end: currentTimeSecond,
|
||||
error: ['dummy']
|
||||
}
|
||||
]
|
||||
error: ['dummy'],
|
||||
},
|
||||
];
|
||||
// Then lastIncident here must not be undefined
|
||||
const lastIncident = state.incident[monitor.id].slice(-1)[0]
|
||||
const timeString = new Date().toLocaleString(config.dateLocale, { timeZone: config.timezone })
|
||||
const lastIncident = state.incident[monitor.id].slice(-1)[0];
|
||||
const timeString = new Date().toLocaleString(config.dateLocale, { timeZone: config.timezone });
|
||||
|
||||
if (status.up) {
|
||||
// Current status is up
|
||||
// close existing incident if any
|
||||
if (lastIncident.end === undefined) {
|
||||
lastIncident.end = currentTimeSecond
|
||||
await config.callback(`✔️${monitor.name} came back up at ${timeString} after ${Math.round((lastIncident.end - lastIncident.start.slice(-1)[0]) / 60)} minutes of downtime`)
|
||||
lastIncident.end = currentTimeSecond;
|
||||
await config.callback(
|
||||
`✔️${monitor.name} came back up at ${timeString} after ${Math.round(
|
||||
(lastIncident.end - lastIncident.start.slice(-1)[0]) / 60,
|
||||
)} minutes of downtime`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Current status is down
|
||||
|
|
@ -158,45 +162,45 @@ export default {
|
|||
state.incident[monitor.id].push({
|
||||
start: [currentTimeSecond],
|
||||
end: undefined,
|
||||
error: [status.err]
|
||||
})
|
||||
await config.callback(`❌${monitor.name} went down at ${timeString} with error ${status.err}`)
|
||||
error: [status.err],
|
||||
});
|
||||
await config.callback(`❌${monitor.name} went down at ${timeString} with error ${status.err}`);
|
||||
} else if (lastIncident.end === undefined && lastIncident.error.slice(-1)[0] !== status.err) {
|
||||
// append if the error message changes
|
||||
lastIncident.start.push(currentTimeSecond)
|
||||
lastIncident.error.push(status.err)
|
||||
await config.callback(`❌${monitor.name} is still down at ${timeString} with error ${status.err}`)
|
||||
lastIncident.start.push(currentTimeSecond);
|
||||
lastIncident.error.push(status.err);
|
||||
await config.callback(`❌${monitor.name} is still down at ${timeString} with error ${status.err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// append to latency data
|
||||
let latencyLists = state.latency[monitor.id] || {
|
||||
recent: [],
|
||||
all: []
|
||||
}
|
||||
all: [],
|
||||
};
|
||||
|
||||
const record = {
|
||||
loc: workerLocation,
|
||||
ping: status.ping,
|
||||
time: currentTimeSecond
|
||||
}
|
||||
latencyLists.recent.push(record)
|
||||
time: currentTimeSecond,
|
||||
};
|
||||
latencyLists.recent.push(record);
|
||||
if (latencyLists.all.length === 0 || currentTimeSecond - latencyLists.all[0].time > 60 * 60) {
|
||||
latencyLists.all.push(record)
|
||||
latencyLists.all.push(record);
|
||||
}
|
||||
|
||||
// discard old data
|
||||
while (latencyLists.recent[0]?.time < currentTimeSecond - 12 * 60 * 60) {
|
||||
latencyLists.recent.shift()
|
||||
latencyLists.recent.shift();
|
||||
}
|
||||
while (latencyLists.all[0]?.time < currentTimeSecond - 90 * 24 * 60 * 60) {
|
||||
latencyLists.all.shift()
|
||||
latencyLists.all.shift();
|
||||
}
|
||||
state.latency[monitor.id] = latencyLists
|
||||
state.latency[monitor.id] = latencyLists;
|
||||
}
|
||||
|
||||
// Update state
|
||||
state.lastUpdate = Math.round(Date.now() / 1000)
|
||||
await env.UPTIMEFLARE_STATE.put("state", JSON.stringify(state))
|
||||
state.lastUpdate = Math.round(Date.now() / 1000);
|
||||
await env.UPTIMEFLARE_STATE.put('state', JSON.stringify(state));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
async function getWorkerLocation() {
|
||||
const res = await fetch('https://cloudflare.com/cdn-cgi/trace')
|
||||
const text = await res.text()
|
||||
const res = await fetch('https://cloudflare.com/cdn-cgi/trace');
|
||||
const text = await res.text();
|
||||
|
||||
const colo = /^colo=(.*)$/m.exec(text)?.[1]
|
||||
return colo
|
||||
const colo = /^colo=(.*)$/m.exec(text)?.[1];
|
||||
return colo;
|
||||
}
|
||||
|
||||
const fetchTimeout = (url: string, ms: number, { signal, ...options }: RequestInit<RequestInitCfProperties> | undefined = {}): Promise<Response> => {
|
||||
const controller = new AbortController()
|
||||
const promise = fetch(url, { signal: controller.signal, ...options })
|
||||
if (signal) signal.addEventListener("abort", () => controller.abort())
|
||||
const timeout = setTimeout(() => controller.abort(), ms)
|
||||
return promise.finally(() => clearTimeout(timeout))
|
||||
const fetchTimeout = (
|
||||
url: string,
|
||||
ms: number,
|
||||
{ signal, ...options }: RequestInit<RequestInitCfProperties> | undefined = {},
|
||||
): Promise<Response> => {
|
||||
const controller = new AbortController();
|
||||
const promise = fetch(url, { signal: controller.signal, ...options });
|
||||
if (signal) signal.addEventListener('abort', () => controller.abort());
|
||||
const timeout = setTimeout(() => controller.abort(), ms);
|
||||
return promise.finally(() => clearTimeout(timeout));
|
||||
};
|
||||
|
||||
function withTimeout<T>(millis: number, promise: Promise<T>): Promise<T> {
|
||||
const timeout = new Promise<T>((resolve, reject) => setTimeout(() => reject(new Error(`Promise timed out after ${millis}ms`)), millis));
|
||||
|
||||
return Promise.race([promise, timeout]);
|
||||
}
|
||||
|
||||
function withTimeout<T> (millis: number, promise: Promise<T>): Promise<T> {
|
||||
const timeout = new Promise<T>((resolve, reject) =>
|
||||
setTimeout(() => reject(new Error(`Promise timed out after ${millis}ms`)), millis))
|
||||
|
||||
return Promise.race([
|
||||
promise,
|
||||
timeout
|
||||
])
|
||||
}
|
||||
|
||||
export { getWorkerLocation, fetchTimeout, withTimeout }
|
||||
export { getWorkerLocation, fetchTimeout, withTimeout };
|
||||
|
|
|
|||
Loading…
Reference in New Issue