[WIP] workers works! (lol) pages+migration todo

pull/177/head
lyc8503 2025-12-31 22:19:29 +08:00
parent 45d86424b2
commit e050065a3c
15 changed files with 329 additions and 154 deletions

View File

@ -34,7 +34,7 @@ export default function DetailChart({
state: MonitorState state: MonitorState
}) { }) {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const latencyData = state.latency[monitor.id].recent.map((point) => ({ const latencyData = state.latency[monitor.id].map((point) => ({
x: point.time * 1000, x: point.time * 1000,
y: point.ping, y: point.ping,
loc: point.loc, loc: point.loc,

View File

@ -70,7 +70,9 @@ export default function MaintenanceAlert({
{upcoming ? t('Expected end') : t('To')} {upcoming ? t('Expected end') : t('To')}
</div> </div>
<div> <div>
{maintenance.end ? new Date(maintenance.end).toLocaleString() : t('Until further notice')} {maintenance.end
? new Date(maintenance.end).toLocaleString()
: t('Until further notice')}
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,7 +2,3 @@ CREATE TABLE IF NOT EXISTS uptimeflare (
key VARCHAR(255) PRIMARY KEY, key VARCHAR(255) PRIMARY KEY,
value BLOB NOT NULL value BLOB NOT NULL
); );
REPLACE INTO uptimeflare (key, value) VALUES ('state', '{"test": 1}');
INSERT INTO uptimeflare (key, value) VALUES ('state', '{"test": 1}') ON CONFLICT(key) DO UPDATE SET value = excluded.value;

View File

@ -61,7 +61,7 @@ export default async function handler(req: NextRequest): Promise<Response> {
const state = JSON.parse(stateStr) as MonitorState const state = JSON.parse(stateStr) as MonitorState
const monitorIncidentHistory = state.incident?.[monitorId] const monitorIncidentHistory = state.incident?.[monitorId]
const hasLatencyData = Boolean(state.latency?.[monitorId]?.recent?.length) const hasLatencyData = Boolean(state.latency?.[monitorId]?.length)
if (!monitorIncidentHistory || monitorIncidentHistory.length === 0 || !hasLatencyData) { if (!monitorIncidentHistory || monitorIncidentHistory.length === 0 || !hasLatencyData) {
return new Response(JSON.stringify(errorBadge(label, 'unknown')), { return new Response(JSON.stringify(errorBadge(label, 'unknown')), {

View File

@ -29,8 +29,8 @@ export default async function handler(req: NextRequest): Promise<Response> {
const isUp = state.incident[monitor.id].slice(-1)[0].end !== undefined const isUp = state.incident[monitor.id].slice(-1)[0].end !== undefined
monitors[monitor.id] = { monitors[monitor.id] = {
up: isUp, up: isUp,
latency: state.latency[monitor.id].recent.slice(-1)[0].ping, latency: state.latency[monitor.id].slice(-1)[0].ping,
location: state.latency[monitor.id].recent.slice(-1)[0].loc, location: state.latency[monitor.id].slice(-1)[0].loc,
message: isUp ? 'OK' : state.incident[monitor.id].slice(-1)[0].error.slice(-1)[0], message: isUp ? 'OK' : state.incident[monitor.id].slice(-1)[0].error.slice(-1)[0],
} }
} }

View File

@ -57,9 +57,7 @@ export default function Home({
{state == undefined ? ( {state == undefined ? (
<Center> <Center>
<Text fw={700}> <Text fw={700}>{t('Monitor State not defined')}</Text>
{t('Monitor State not defined')}
</Text>
</Center> </Center>
) : ( ) : (
<div> <div>
@ -76,7 +74,7 @@ export default function Home({
export async function getServerSideProps() { export async function getServerSideProps() {
// Read state as string from KV, to avoid hitting server-side cpu time limit // Read state as string from KV, to avoid hitting server-side cpu time limit
const state = await getFromStore(process.env as any, 'state') as unknown as MonitorState const state = (await getFromStore(process.env as any, 'state')) as unknown as MonitorState
// Only present these values to client // Only present these values to client
const monitors = workerConfig.monitors.map((monitor) => { const monitors = workerConfig.monitors.map((monitor) => {

View File

@ -92,32 +92,61 @@ export type Callbacks<TEnv = Env> = {
) => Promise<any> | any ) => Promise<any> | any
} }
export type IncidentRecord = {
start: number[]
end: number | null // null if it's still open
error: string[]
}
export type LatencyRecord = {
loc: string
ping: number
time: number
}
export type MonitorState = { export type MonitorState = {
lastUpdate: number lastUpdate: number
overallUp: number overallUp: number
overallDown: number overallDown: number
incident: Record<string, IncidentRecord[]>
latency: Record<string, LatencyRecord[]> // recent 12 hour data, N min interval
}
// This is now the actual stored format (after TODO D1 migration) to improve (de)serialization performance
// This gives a ~3.5x speedup in computing and a 58% reduction in size
// The CPULimitExceeded issue with 10+ monitors on free tier should be largely mitigated by this change
// local profiling result (1 op = parse + stringify):
// MonitorState (original): 277 ops/s, ±0.51% | slowest, 71.09% slower
// MonitorStateCompacted: 958 ops/s, ±1.17% | fastest
export type MonitorStateCompacted = {
lastUpdate: number
overallUp: number
overallDown: number
// incident in stored in columnar format
incident: Record< incident: Record<
string, string, // monitor id
{ {
start: number[] start: number[][]
end: number | undefined // undefined if it's still open end: (number | null)[]
error: string[] error: string[][]
}[] }
> >
// latency in stored in columnar format
// also uses Run-length encoding for loc & Base64 encoding for number arrays
latency: Record< latency: Record<
string, string, // monitor id
{ {
recent: { loc: {
loc: string v: string[] // RLE values
ping: number c: number[] // RLE counts
time: number }
}[] // recent 12 hour data, 2 min interval // Hex results in a larger size and slower encoding/decoding than base64,
all: { // but we can pop/append arbitrary number of bytes without decoding then re-encoding the whole string
loc: string // This is useful in Workers and shows a ~2% speedup comapred to base64, and it also simplifies the code
ping: number ping: string // Hex encoded Uint16Array
time: number time: string // Hex encoded Uint32Array
}[] // all data in 90 days, 1 hour interval
} }
> >
} }

View File

@ -1,8 +1,8 @@
import i18n from 'i18next'; import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next'
import LanguageDetector from 'i18next-browser-languagedetector'; import LanguageDetector from 'i18next-browser-languagedetector'
import en from '../locales/en/common.json'; import en from '../locales/en/common.json'
import zhCN from '../locales/zh-CN/common.json'; import zhCN from '../locales/zh-CN/common.json'
i18n i18n
.use(LanguageDetector) .use(LanguageDetector)
@ -20,6 +20,6 @@ i18n
detection: { detection: {
order: ['navigator'], order: ['navigator'],
}, },
}); })
export default i18n; export default i18n

View File

@ -1,9 +1,9 @@
import { DurableObject } from 'cloudflare:workers' import { DurableObject } from 'cloudflare:workers'
import { MonitorState, MonitorTarget } from '../../types/config' import { MonitorTarget } from '../../types/config'
import { maintenances, workerConfig } from '../../uptime.config' import { workerConfig } from '../../uptime.config'
import { getStatus, getStatusWithGlobalPing } from './monitor' import { getStatus, getStatusWithGlobalPing } from './monitor'
import { formatStatusChangeNotification, getWorkerLocation, webhookNotify } from './util' import { formatAndNotify, getWorkerLocation } from './util'
import { getFromStore, setToStore } from './store' import { CompactedMonitorStateWrapper, getFromStore, setToStore } from './store'
export interface Env { export interface Env {
UPTIMEFLARE_STATE: KVNamespace UPTIMEFLARE_STATE: KVNamespace
@ -16,66 +16,12 @@ const Worker = {
const workerLocation = (await getWorkerLocation()) || 'ERROR' const workerLocation = (await getWorkerLocation()) || 'ERROR'
console.log(`Running scheduled event on ${workerLocation}...`) console.log(`Running scheduled event on ${workerLocation}...`)
// Auxiliary function to format notification and send it via webhook // Create a wrapped MonitorState from stored compacted state
let formatAndNotify = async ( const state = new CompactedMonitorStateWrapper(
monitor: MonitorTarget, await getFromStore(env, 'state')
isUp: boolean,
timeIncidentStart: number,
timeNow: number,
reason: string
) => {
// Skip notification if monitor is in the skip list
const skipList = workerConfig.notification?.skipNotificationIds
if (skipList && skipList.includes(monitor.id)) {
console.log(
`Skipping notification for ${monitor.name} (${monitor.id} in skipNotificationIds)`
) )
return state.data.overallDown = 0
} state.data.overallUp = 0
// Skip notification if monitor is in maintenance
const maintenanceList = maintenances
.filter(
(m) =>
new Date(timeNow * 1000) >= new Date(m.start) &&
(!m.end || new Date(timeNow * 1000) <= new Date(m.end))
)
.map((e) => e.monitors || [])
.flat()
if (maintenanceList.includes(monitor.id)) {
console.log(`Skipping notification for ${monitor.name} (in maintenance)`)
return
}
if (workerConfig.notification?.webhook) {
const notification = formatStatusChangeNotification(
monitor,
isUp,
timeIncidentStart,
timeNow,
reason,
workerConfig.notification?.timeZone ?? 'Etc/GMT'
)
await webhookNotify(workerConfig.notification.webhook, notification)
} else {
console.log(`Webhook not set, skipping notification for ${monitor.name}`)
}
}
// Read state, set init state if it doesn't exist
let state = JSON.parse(await getFromStore(env, 'state') || '{}') as unknown as MonitorState
if (!state || Object.keys(state).length === 0) {
state = {
lastUpdate: 0,
overallUp: 0,
overallDown: 0,
incident: {},
latency: {},
}
}
state.overallDown = 0
state.overallUp = 0
let statusChanged = false let statusChanged = false
const currentTimeSecond = Math.round(Date.now() / 1000) const currentTimeSecond = Math.round(Date.now() / 1000)
@ -126,6 +72,7 @@ const Worker = {
console.log('Falling back to local check...') console.log('Falling back to local check...')
status = await getStatus(monitor) status = await getStatus(monitor)
} else { } else {
// TODO: more consistent error handling (throw or return?)
status = { ping: 0, up: false, err: 'Unknown check proxy error' } status = { ping: 0, up: false, err: 'Unknown check proxy error' }
} }
} }
@ -134,29 +81,32 @@ const Worker = {
status = await getStatus(monitor) status = await getStatus(monitor)
} }
// const status = await getStatus(monitor)
const currentTimeSecond = Math.round(Date.now() / 1000) const currentTimeSecond = Math.round(Date.now() / 1000)
// Update counters // Update counters
status.up ? state.overallUp++ : state.overallDown++ status.up ? state.data.overallUp++ : state.data.overallDown++
// Update incidents // Update incidents
// Create a dummy incident to store the start time of the monitoring and simplify logic // Create a dummy incident to store the start time of the monitoring and simplify logic
state.incident[monitor.id] = state.incident[monitor.id] || [ if (state.incidentLen(monitor.id) === 0) {
{ state.appendIncident(monitor.id, {
start: [currentTimeSecond], start: [currentTimeSecond],
end: currentTimeSecond, end: currentTimeSecond,
error: ['dummy'], error: ['dummy'],
}, })
] }
// Then lastIncident here must not be undefined
let lastIncident = state.incident[monitor.id].slice(-1)[0] // Then lastIncident here must not be null
let lastIncident = state.getIncident(monitor.id, state.incidentLen(monitor.id) - 1)
if (status.up) { if (status.up) {
// Current status is up // Current status is up
// close existing incident if any // close existing incident if any
if (lastIncident.end === undefined) { if (lastIncident.end === null) {
lastIncident.end = currentTimeSecond lastIncident.end = currentTimeSecond
// write back the modified last incident
state.setIncident(monitor.id, state.incidentLen(monitor.id) - 1, lastIncident)
monitorStatusChanged = true monitorStatusChanged = true
try { try {
if ( if (
@ -190,24 +140,30 @@ const Worker = {
} else { } else {
// Current status is down // Current status is down
// open new incident if not already open // open new incident if not already open
if (lastIncident.end !== undefined) { if (lastIncident.end !== null) {
state.incident[monitor.id].push({ state.appendIncident(monitor.id, {
start: [currentTimeSecond], start: [currentTimeSecond],
end: undefined, end: null,
error: [status.err], error: [status.err],
}) })
monitorStatusChanged = true monitorStatusChanged = true
} else if ( } else if (
lastIncident.end === undefined && lastIncident.end === null &&
lastIncident.error.slice(-1)[0] !== status.err lastIncident.error.slice(-1)[0] !== status.err
) { ) {
// append if the error message changes // append if the error message changes
lastIncident.start.push(currentTimeSecond) lastIncident.start.push(currentTimeSecond)
lastIncident.error.push(status.err) lastIncident.error.push(status.err)
// write back the modified last incident
state.setIncident(monitor.id, state.incidentLen(monitor.id) - 1, lastIncident)
monitorStatusChanged = true monitorStatusChanged = true
} }
const currentIncident = state.incident[monitor.id].slice(-1)[0] const currentIncident = state.getIncident(
monitor.id,
state.incidentLen(monitor.id) - 1
)
try { try {
if ( if (
// monitor status changed AND... // monitor status changed AND...
@ -284,63 +240,54 @@ const Worker = {
} }
// append to latency data // append to latency data
let latencyLists = state.latency[monitor.id] || { state.appendLatency(monitor.id,{
recent: [],
}
latencyLists.all = []
const record = {
loc: checkLocation, loc: checkLocation,
ping: status.ping, ping: status.ping,
time: currentTimeSecond, time: currentTimeSecond,
} })
latencyLists.recent.push(record)
// discard old data // discard old data
while (latencyLists.recent[0]?.time < currentTimeSecond - 12 * 60 * 60) { while (state.getFirstLatency(monitor.id).time < currentTimeSecond - 12 * 60 * 60) {
latencyLists.recent.shift() state.unshiftLatency(monitor.id)
} }
state.latency[monitor.id] = latencyLists
// discard old incidents // discard old incidents
let incidentList = state.incident[monitor.id]
while ( while (
incidentList.length > 0 && state.incidentLen(monitor.id) > 0 &&
incidentList[0].end && state.getIncident(monitor.id, 0).end &&
incidentList[0].end < currentTimeSecond - 90 * 24 * 60 * 60 state.getIncident(monitor.id, 0).end! < currentTimeSecond - 90 * 24 * 60 * 60
) { ) {
incidentList.shift() state.shiftIncident(monitor.id)
} }
if ( if (
incidentList.length == 0 || state.incidentLen(monitor.id) === 0 ||
(incidentList[0].start[0] > currentTimeSecond - 90 * 24 * 60 * 60 && (state.getIncident(monitor.id, 0).start[0] > currentTimeSecond - 90 * 24 * 60 * 60 &&
incidentList[0].error[0] != 'dummy') state.getIncident(monitor.id, 0).error[0] != 'dummy')
) { ) {
// put the dummy incident back // put the dummy incident back
incidentList.unshift({ state.unshiftIncident(monitor.id, {
start: [currentTimeSecond - 90 * 24 * 60 * 60], start: [currentTimeSecond - 90 * 24 * 60 * 60],
end: currentTimeSecond - 90 * 24 * 60 * 60, end: currentTimeSecond - 90 * 24 * 60 * 60,
error: ['dummy'], error: ['dummy'],
}) })
} }
state.incident[monitor.id] = incidentList
statusChanged ||= monitorStatusChanged statusChanged ||= monitorStatusChanged
} }
console.log( console.log(
`statusChanged: ${statusChanged}, lastUpdate: ${state.lastUpdate}, currentTime: ${currentTimeSecond}` `statusChanged: ${statusChanged}, lastUpdate: ${state.data.lastUpdate}, currentTime: ${currentTimeSecond}`
) )
// Update state // Update state
// Allow for a cooldown period before writing to KV // Allow for a cooldown period before writing to storage
if ( if (
statusChanged || statusChanged ||
currentTimeSecond - state.lastUpdate >= (workerConfig.kvWriteCooldownMinutes ?? 3) * 60 - 10 // Allow for 10 seconds of clock drift currentTimeSecond - state.data.lastUpdate >= (workerConfig.kvWriteCooldownMinutes ?? 3) * 60 - 10 // Allow for 10 seconds of clock drift
) { ) {
console.log('Updating state...') console.log('Updating state...')
state.lastUpdate = currentTimeSecond state.data.lastUpdate = currentTimeSecond
await setToStore(env, 'state', JSON.stringify(state)) await setToStore(env, 'state', state.getCompactedStateStr())
} else { } else {
console.log('Skipping state update due to cooldown period.') console.log('Skipping state update due to cooldown period.')
} }

View File

@ -304,7 +304,9 @@ export async function getStatus(
response.status, response.status,
response.text.bind(response) response.text.bind(response)
) )
try { await response.body?.cancel() } catch(e) {} // Always try to cancel body, see issue #166 try {
await response.body?.cancel()
} catch (e) {} // Always try to cancel body, see issue #166
if (err !== null) { if (err !== null) {
console.log(`${monitor.name} didn't pass response check: ${err}`) console.log(`${monitor.name} didn't pass response check: ${err}`)

View File

@ -1,17 +1,155 @@
import { Env } from "."; import { Env } from '.'
import { IncidentRecord, LatencyRecord, MonitorStateCompacted } from '../../types/config'
export async function getFromStore(env: Env, key: string): Promise<string | null> { export async function getFromStore(env: Env, key: string): Promise<string | null> {
const stmt = env.UPTIMEFLARE_D1.prepare("SELECT value FROM uptimeflare WHERE key = ?") const stmt = env.UPTIMEFLARE_D1.prepare('SELECT value FROM uptimeflare WHERE key = ?')
const result = await stmt.bind(key).first<{ value: string }>() const result = await stmt.bind(key).first<{ value: string }>()
if (!result) {
return await env.UPTIMEFLARE_STATE.get(key)
}
return result?.value || null return result?.value || null
} }
export async function setToStore(env: Env, key: string, value: string): Promise<void> { export async function setToStore(env: Env, key: string, value: string): Promise<void> {
const stmt = env.UPTIMEFLARE_D1.prepare("INSERT INTO uptimeflare (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value;") const stmt = env.UPTIMEFLARE_D1.prepare(
'INSERT INTO uptimeflare (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value;'
)
await stmt.bind(key, value).run() await stmt.bind(key, value).run()
} }
export class CompactedMonitorStateWrapper {
data: MonitorStateCompacted
constructor(compactedStateStr: string | null) {
if (!compactedStateStr) {
// Initialize empty state
this.data = {
lastUpdate: 0,
overallUp: 0,
overallDown: 0,
incident: {},
latency: {},
}
return
}
this.data = JSON.parse(compactedStateStr)
}
getCompactedStateStr(): string {
return JSON.stringify(this.data)
}
incidentLen(monitorId: string): number {
const incidents = this.data.incident[monitorId]
if (!incidents) return 0
return incidents.start.length
}
getIncident(monitorId: string, index: number): IncidentRecord {
const incidents = this.data.incident[monitorId]
if (!incidents || index < 0 || index >= incidents.start.length) {
throw new Error('Index out of bounds or monitor not found')
}
return {
start: incidents.start[index],
end: incidents.end[index],
error: incidents.error[index],
}
}
setIncident(monitorId: string, index: number, incident: IncidentRecord) {
const incidents = this.data.incident[monitorId]
if (!incidents || index < 0 || index >= incidents.start.length) {
throw new Error('Index out of bounds or monitor not found')
}
incidents.start[index] = incident.start
incidents.end[index] = incident.end
incidents.error[index] = incident.error
}
appendIncident(monitorId: string, incident: IncidentRecord) {
let incidents = this.data.incident[monitorId]
if (!incidents) {
// Initialize incident arrays
this.data.incident[monitorId] = {
start: [],
end: [],
error: [],
}
incidents = this.data.incident[monitorId]
}
incidents.start.push(incident.start)
incidents.end.push(incident.end)
incidents.error.push(incident.error)
}
shiftIncident(monitorId: string) {
const incidents = this.data.incident[monitorId]
incidents.start.shift()
incidents.end.shift()
incidents.error.shift()
}
unshiftIncident(monitorId: string, incident: IncidentRecord) {
const incidents = this.data.incident[monitorId]
incidents.start.unshift(incident.start)
incidents.end.unshift(incident.end)
incidents.error.unshift(incident.error)
}
latencyLen(monitorId: string): number {
const latencies = this.data.latency[monitorId]
if (!latencies) return 0
return latencies.ping.length / 4 // Uint16Array, 4 characters per entry in hex
}
appendLatency(monitorId: string, record: LatencyRecord) {
let latencies = this.data.latency[monitorId]
if (!latencies) {
// Initialize latency arrays
this.data.latency[monitorId] = {
time: '',
ping: '',
loc: {
c: [],
v: [],
},
}
latencies = this.data.latency[monitorId]
}
// @ts-expect-error
latencies.time += new Uint8Array(new Uint32Array([record.time]).buffer).toHex()
// @ts-expect-error
latencies.ping += new Uint8Array(new Uint16Array([record.ping]).buffer).toHex()
if (latencies.loc.v[latencies.loc.v.length - 1] !== record.loc) {
latencies.loc.c.push(1)
latencies.loc.v.push(record.loc)
} else {
latencies.loc.c[latencies.loc.c.length - 1] += 1
}
}
getFirstLatency(monitorId: string): LatencyRecord {
let latencies = this.data.latency[monitorId]
return {
// @ts-expect-error
time: new Uint32Array(Uint8Array.fromHex(latencies.time.slice(0, 8)).buffer)[0],
// @ts-expect-error
ping: new Uint16Array(Uint8Array.fromHex(latencies.ping.slice(0, 4)).buffer)[0],
loc: latencies.loc.v[0],
}
}
unshiftLatency(monitorId: string) {
let latencies = this.data.latency[monitorId]
latencies.time = latencies.time.slice(8)
latencies.ping = latencies.ping.slice(4)
latencies.loc.c[0] -= 1
if (latencies.loc.c[0] === 0) {
latencies.loc.c.shift()
latencies.loc.v.shift()
}
}
}

View File

@ -1,4 +1,5 @@
import { WebhookConfig } from '../../types/config' import { MonitorTarget, WebhookConfig } from '../../types/config'
import { maintenances, workerConfig } from '../../uptime.config'
async function getWorkerLocation() { async function getWorkerLocation() {
const res = await fetch('https://cloudflare.com/cdn-cgi/trace') const res = await fetch('https://cloudflare.com/cdn-cgi/trace')
@ -145,10 +146,59 @@ async function webhookNotify(webhook: WebhookConfig, message: string) {
} }
} }
// Auxiliary function to format notification and send it via webhook
const formatAndNotify = async (
monitor: MonitorTarget,
isUp: boolean,
timeIncidentStart: number,
timeNow: number,
reason: string
) => {
// Skip notification if monitor is in the skip list
const skipList = workerConfig.notification?.skipNotificationIds
if (skipList && skipList.includes(monitor.id)) {
console.log(
`Skipping notification for ${monitor.name} (${monitor.id} in skipNotificationIds)`
)
return
}
// Skip notification if monitor is in maintenance
const maintenanceList = maintenances
.filter(
(m) =>
new Date(timeNow * 1000) >= new Date(m.start) &&
(!m.end || new Date(timeNow * 1000) <= new Date(m.end))
)
.map((e) => e.monitors || [])
.flat()
if (maintenanceList.includes(monitor.id)) {
console.log(`Skipping notification for ${monitor.name} (in maintenance)`)
return
}
if (workerConfig.notification?.webhook) {
const notification = formatStatusChangeNotification(
monitor,
isUp,
timeIncidentStart,
timeNow,
reason,
workerConfig.notification?.timeZone ?? 'Etc/GMT'
)
await webhookNotify(workerConfig.notification.webhook, notification)
} else {
console.log(`Webhook not set, skipping notification for ${monitor.name}`)
}
}
export { export {
getWorkerLocation, getWorkerLocation,
fetchTimeout, fetchTimeout,
withTimeout, withTimeout,
webhookNotify, webhookNotify,
formatStatusChangeNotification, formatStatusChangeNotification,
formatAndNotify,
} }

View File

@ -1,5 +1,9 @@
name = "uptimeflare_worker" name = "uptimeflare_worker"
main = "src/index.ts" main = "src/index.ts"
compatibility_date = "2025-04-02" compatibility_date = "2025-04-02"
kv_namespaces = [{ binding = "UPTIMEFLARE_STATE", id = "UPTIMEFLARE_STATE" }]
compatibility_flags = [ "nodejs_compat" ] compatibility_flags = [ "nodejs_compat" ]
[[d1_databases]]
binding = "UPTIMEFLARE_D1"
database_name = "uptimeflare_d1"
database_id = "00000000-0000-0000-0000-000000000000"

View File

@ -2,3 +2,4 @@ name = "uptimeflare_worker"
main = "src/index.ts" main = "src/index.ts"
compatibility_date = "2025-04-02" compatibility_date = "2025-04-02"
compatibility_flags = [ "nodejs_compat" ] compatibility_flags = [ "nodejs_compat" ]

8
wrangler.toml Normal file
View File

@ -0,0 +1,8 @@
name = "uptimeflare"
compatibility_date = "2025-04-02"
compatibility_flags = [ "nodejs_compat" ]
[[d1_databases]]
binding = "UPTIMEFLARE_D1"
database_name = "uptimeflare_d1"
database_id = "00000000-0000-0000-0000-000000000000"