+82
-38
src/App.svelte
+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>