types

Changed files
+83 -38
src
+1
.gitignore
··· 22 22 *.njsproj 23 23 *.sln 24 24 *.sw? 25 + .wrangler
+82 -38
src/App.svelte
··· 5 5 import orderBy from "lodash/orderBy"; 6 6 import { formatNumber, formatUptime } from './lib/utils'; 7 7 import instancesData from './instances.json'; 8 - 8 + 9 9 const APP_TITLE = 'plcbundle instances' 10 10 const PLC_DIRECTORY = 'plc.directory' 11 11 const ROOT = 'cbab6809a136d6a621906ee11199d3b0faf85b422fe0d0d2c346ce8e9dcd7485' 12 12 const AUTO_REFRESH_INTERVAL = 10 // in seconds 13 13 const BUNDLE_OPS = 10_000 14 14 15 + type StatusResponse = { 16 + bundles: { 17 + last_bundle: number; 18 + root_hash: string; 19 + head_hash: string; 20 + end_time?: string; 21 + }; 22 + server: { 23 + uptime: number; 24 + }; 25 + mempool?: { 26 + count: number; 27 + eta_next_bundle_seconds: number; 28 + }; 29 + latency?: number; 30 + } 31 + 15 32 type Instance = { 16 - url: string, 17 - cors?: boolean, 18 - status?: object, 19 - modern?: boolean, 33 + url: string; 34 + cors?: boolean; 35 + status?: StatusResponse; 36 + modern?: boolean; 37 + _head?: boolean; 20 38 } 21 39 22 - let lastKnownBundle = $state({ 40 + type LastKnownBundle = { 41 + number: number; 42 + hash: string | null; 43 + mempool: number | null; 44 + mempoolPercent: number; 45 + time?: string; 46 + etaNext?: Date; 47 + } 48 + 49 + let lastKnownBundle = $state<LastKnownBundle>({ 23 50 number: 0, 24 51 hash: null, 25 52 mempool: null, ··· 31 58 let isConflict = $state(false) 32 59 let lastUpdated = $state(new Date()) 33 60 let autoRefreshEnabled = $state(true) 34 - let instances = $state(instancesData.sort(() => Math.random() - 0.5)) 61 + let instances = $state<Instance[]>(instancesData.sort(() => Math.random() - 0.5)) 35 62 36 63 const instanceOrderBy = [['_head', 'status.bundles.last_bundle', 'status.latency'], ['desc', 'asc']] 37 64 38 - async function getStatus(instance: Instance) { 39 - let statusResp: object | undefined; 65 + async function getStatus(instance: Instance): Promise<StatusResponse | undefined> { 66 + let statusResp: StatusResponse | undefined; 40 67 let url: string = instance.url; 41 68 const start = performance.now(); 42 69 try { ··· 45 72 if (!statusResp) { 46 73 url = `https://keyoxide.org/api/3/get/http?url=${encodeURIComponent(url)}&format=text&time=${Date.now()}` 47 74 const indexResp = await (await fetch(url)).text() 48 - const [ _, from, to ] = indexResp?.match(/Range:\s+(\d{6}) - (\d{6})/) 49 - statusResp = { 50 - bundles: { 51 - last_bundle: Number(to), 52 - root_hash: indexResp?.match(/Root: ([a-f0-9]{64})/)[1], 53 - head_hash: indexResp?.match(/Head: ([a-f0-9]{64})/)[1], 54 - }, 55 - server: { 56 - uptime: 1, 75 + const match = indexResp?.match(/Range:\s+(\d{6}) - (\d{6})/) 76 + if (match) { 77 + const [, from, to] = match 78 + const rootMatch = indexResp?.match(/Root: ([a-f0-9]{64})/) 79 + const headMatch = indexResp?.match(/Head: ([a-f0-9]{64})/) 80 + 81 + statusResp = { 82 + bundles: { 83 + last_bundle: Number(to), 84 + root_hash: rootMatch ? rootMatch[1] : '', 85 + head_hash: headMatch ? headMatch[1] : '', 86 + }, 87 + server: { 88 + uptime: 1, 89 + } 57 90 } 58 91 } 59 92 } ··· 66 99 67 100 function recalculateHead() { 68 101 isConflict = false 69 - const headHashes = [] 102 + const headHashes: string[] = [] 70 103 for (const instance of instances) { 71 104 instance._head = instance.status?.bundles?.last_bundle === lastKnownBundle.number 72 - if (instance._head) { 73 - headHashes.push(instance.status?.bundles?.head_hash) 105 + if (instance._head && instance.status?.bundles?.head_hash) { 106 + headHashes.push(instance.status.bundles.head_hash) 74 107 } 75 108 } 76 109 isConflict = [...new Set(headHashes)].length > 1 ··· 83 116 i.status = undefined 84 117 } 85 118 86 - const statuses = [] 87 - 88 119 await Promise.all(instances.map(async (instance) => { 89 120 const status = await getStatus(instance) 90 121 instance.status = status 91 - if (status?.bundles?.last_bundle > lastKnownBundle.number) { 92 - lastKnownBundle.number = status?.bundles?.last_bundle 93 - lastKnownBundle.hash = status?.bundles?.head_hash 94 - lastKnownBundle.time = status?.bundles?.end_time 122 + if (status?.bundles?.last_bundle && status.bundles.last_bundle > lastKnownBundle.number) { 123 + lastKnownBundle.number = status.bundles.last_bundle 124 + lastKnownBundle.hash = status.bundles.head_hash 125 + lastKnownBundle.time = status.bundles.end_time 95 126 96 - if (status?.mempool?.count > lastKnownBundle.mempool) { 97 - lastKnownBundle.mempool = status?.mempool?.count 127 + if (status?.mempool?.count && (!lastKnownBundle.mempool || status.mempool.count > lastKnownBundle.mempool)) { 128 + lastKnownBundle.mempool = status.mempool.count 98 129 lastKnownBundle.mempoolPercent = Math.round((lastKnownBundle.mempool/100)*100)/100 99 - lastKnownBundle.etaNext = addSeconds(new Date(), status?.mempool?.eta_next_bundle_seconds) 130 + lastKnownBundle.etaNext = addSeconds(new Date(), status.mempool.eta_next_bundle_seconds) 100 131 } 101 132 } 102 133 lastUpdated = new Date() ··· 108 139 setTimeout(() => { canRefresh = true }, 500) 109 140 } 110 141 111 - function updateTitle () { 112 - const arr = [] 113 - if (lastUpdated > 0) { 142 + function updateTitle() { 143 + const arr: string[] = [] 144 + if (lastUpdated) { 114 145 const upCount = instances.filter(i => i._head) 115 146 arr.push(`${isConflict ? '⚠️' : '✅'} [${upCount.length}/${instances.length}]`) 116 147 } ··· 118 149 return true 119 150 } 120 151 152 + let autoRefreshTimer: ReturnType<typeof setTimeout> | null = null; 153 + 121 154 onMount(async () => { 122 155 await doCheck() 123 156 124 - setTimeout(() => { 125 - if (autoRefreshEnabled) { 126 - doCheck() 157 + const scheduleRefresh = () => { 158 + autoRefreshTimer = setTimeout(() => { 159 + if (autoRefreshEnabled) { 160 + doCheck() 161 + } 162 + scheduleRefresh() 163 + }, AUTO_REFRESH_INTERVAL * 1000) 164 + } 165 + 166 + scheduleRefresh() 167 + 168 + return () => { 169 + if (autoRefreshTimer) { 170 + clearTimeout(autoRefreshTimer) 127 171 } 128 - }, AUTO_REFRESH_INTERVAL * 1000) 172 + } 129 173 }) 130 174 </script> 131 175 ··· 214 258 <td><a href={instance.url} target="_blank" class="font-semibold">{instance.url.replace("https://", "")}</a></td> 215 259 <td>{#if instance._head}{#if isConflict}⚠️{:else}✅{/if}{:else if instance.status}🔄{:else}⌛{/if}</td> 216 260 <td>{#if instance.status?.bundles?.last_bundle}{instance.status?.bundles?.last_bundle}{/if}</td> 217 - <td>{#if instance.status?.mempool && instance._head}{formatNumber(instance.status?.mempool.count)}{:else if instance.status}<span class="opacity-25">syncing</span>{/if}</td> 261 + <td>{#if instance.status?.mempool && instance._head}{formatNumber(instance.status?.mempool.count)}{:else if instance.status}<span class="opacity-25 text-xs">syncing</span>{/if}</td> 218 262 <td class="text-xs opacity-50">{#if instance.status?.mempool && instance._head}{instance.status?.mempool.last_op_age_seconds || 0}s{/if}</td> 219 263 <td><span class="font-mono text-xs {instance._head ? (isConflict ? 'text-error-600' : 'text-success-600') : 'opacity-50'}">{#if instance.status?.bundles?.head_hash}{instance.status?.bundles?.head_hash.slice(0, 7)}{/if}</span></td> 220 264 <td><span class="font-mono text-xs {instance.status ? (instance.status?.bundles?.root_hash === ROOT ? 'text-success-600' : 'text-error-600') : ''}">{#if instance.status?.bundles?.root_hash}{instance.status?.bundles?.root_hash.slice(0, 7)}{/if}</span></td>