Compare changes

Choose any two refs to compare.

+3
bun.lock
··· 16 16 "@skeletonlabs/skeleton-svelte": "^4.2.2", 17 17 "@sveltejs/vite-plugin-svelte": "^6.2.1", 18 18 "@tsconfig/svelte": "^5.0.5", 19 + "@types/lodash": "^4.17.20", 19 20 "@types/node": "^24.6.0", 20 21 "svelte": "^5.39.6", 21 22 "svelte-check": "^4.3.2", ··· 186 187 "@tsconfig/svelte": ["@tsconfig/svelte@5.0.5", "", {}, "sha512-48fAnUjKye38FvMiNOj0J9I/4XlQQiZlpe9xaNPfe8vy2Y1hFBt8g1yqf2EGjVvHavo4jf2lC+TQyENCr4BJBQ=="], 187 188 188 189 "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 190 + 191 + "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], 189 192 190 193 "@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="], 191 194
+1
package.json
··· 16 16 "@sveltejs/vite-plugin-svelte": "^6.2.1", 17 17 "@tsconfig/svelte": "^5.0.5", 18 18 "@types/node": "^24.6.0", 19 + "@types/lodash": "^4.17.20", 19 20 "svelte": "^5.39.6", 20 21 "svelte-check": "^4.3.2", 21 22 "typescript": "~5.9.3",
+167 -65
src/App.svelte
··· 1 1 <script lang="ts"> 2 + 2 3 import { onMount } from 'svelte'; 3 4 import { filesize } from 'filesize'; 4 - import { formatDistanceToNow, addSeconds, subSeconds, formatDate, formatISO9075 } from 'date-fns'; 5 + import { formatDistanceToNow, addSeconds, subSeconds, formatDate, formatISO9075, differenceInSeconds } from 'date-fns'; 5 6 import { Progress, Switch } from '@skeletonlabs/skeleton-svelte'; 6 - import orderBy from "lodash/orderBy"; 7 + import orderBy from 'lodash/orderBy'; 7 8 import BundleDownloader from './BundleDownloader.svelte'; 8 9 import { formatNumber, formatUptime } from './lib/utils'; 9 10 import instancesData from './instances.json'; 10 11 11 12 const APP_TITLE = 'plcbundle instances' 12 13 const PLC_DIRECTORY = 'plc.directory' 13 - const ROOT = 'cbab6809a136d6a621906ee11199d3b0faf85b422fe0d0d2c346ce8e9dcd7485' 14 + const ROOT = 'f743c3ae1e3f6023e89e492bce63b52a9ed03ee46a163c2f4a3b997eaf2aaf85' 14 15 const AUTO_REFRESH_INTERVAL = 10 // in seconds 15 16 const BUNDLE_OPS = 10_000 17 + const PAST_ROOTS = [ 18 + // November 2025: https://bsky.app/profile/atproto.com/post/3m4e3mnxb7s2p 19 + "cbab6809a136d6a621906ee11199d3b0faf85b422fe0d0d2c346ce8e9dcd7485" 20 + ] 16 21 17 22 type StatusResponse = { 18 23 ok: boolean; ··· 21 26 root_hash: string; 22 27 head_hash: string; 23 28 end_time?: string; 24 - total_size: number; 25 - uncompressed_size: number; 29 + total_size?: number; 30 + uncompressed_size?: number; 31 + updated_at?: string; 26 32 }; 27 33 server: { 28 34 uptime: number; ··· 30 36 mempool?: { 31 37 count: number; 32 38 eta_next_bundle_seconds: number; 39 + last_time: Date; 33 40 }; 34 41 latency?: number; 35 42 } 36 43 44 + type StatusResponseError = { 45 + error: string; 46 + } 47 + 37 48 type Instance = { 38 49 url: string; 39 50 cors?: boolean; 40 - status?: StatusResponse; 51 + status?: StatusResponse | StatusResponseError; 41 52 modern?: boolean; 42 53 _head?: boolean; 54 + _oldRoot?: boolean; 55 + _conflict?: boolean; 43 56 } 44 57 45 58 type LastKnownBundle = { ··· 48 61 mempool: number | null; 49 62 mempoolPercent: number; 50 63 mempoolBundle: number; 64 + lastTime?: Date; 51 65 time?: string; 52 66 etaNext?: Date | null; 53 - totalSize: number; 54 - totalSizeUncompressed: number; 67 + totalSize?: number | null; 68 + totalSizeUncompressed?: number | null; 55 69 } 56 70 57 71 let lastKnownBundle = $state<LastKnownBundle>({ 58 72 number: 0, 59 73 hash: null, 60 74 mempool: null, 75 + mempoolBundle: 0, 61 76 mempoolPercent: 0, 62 77 }) 63 78 64 79 let isUpdating = $state(false) 65 80 let canRefresh = $state(true) 66 - let isConflict = $state(false) 81 + let consensus = $state({}) 82 + let isConflict = $state(consensus) 83 + let instancesInConflict = $state<string[]>([]) 67 84 let lastUpdated = $state(new Date()) 68 85 let autoRefreshEnabled = $state(true) 69 86 let instances = $state<Instance[]>(instancesData.sort(() => Math.random() - 0.5)) 70 87 71 - const instanceOrderBy = [['_head', 'status.bundles.last_bundle', 'status.latency'], ['desc', 'desc', 'asc']] 88 + const instanceOrderBy = [ 89 + ['status.error', '_head', '_oldRoot' , 'status.bundles.last_bundle', 'status.latency'], 90 + ['desc', 'desc', 'asc', 'desc', 'asc'] 91 + ] 72 92 73 - async function getStatus(instance: Instance): Promise<StatusResponse | undefined> { 93 + async function getStatus(instance: Instance): Promise<StatusResponse | StatusResponseError> { 74 94 let statusResp: StatusResponse | undefined; 75 95 let url: string = instance.url; 96 + let lastError: string | undefined; 76 97 const start = performance.now(); 77 98 try { 78 99 statusResp = await (await fetch(`${url}/status?${Number(new Date())}`)).json() 79 - } catch (e) {} 100 + } catch (e: any) { 101 + lastError = e.message; 102 + } 80 103 if (!statusResp) { 81 104 url = `https://keyoxide.org/api/3/get/http?url=${encodeURIComponent(url)}&format=text&time=${Date.now()}` 82 - const indexResp = await (await fetch(url)).text() 105 + 106 + let indexResp: string | undefined; 107 + try { 108 + indexResp = await (await fetch(url)).text() 109 + } catch(e: any) { 110 + lastError = e.message; 111 + } 83 112 const match = indexResp?.match(/Range:\s+(\d{6}) - (\d{6})/) 84 113 if (match) { 85 114 const [, from, to] = match ··· 87 116 const headMatch = indexResp?.match(/Head: ([a-f0-9]{64})/) 88 117 89 118 statusResp = { 119 + ok: true, 90 120 bundles: { 91 121 last_bundle: Number(to), 92 122 root_hash: rootMatch ? rootMatch[1] : '', ··· 99 129 } 100 130 } 101 131 if (statusResp) { 132 + statusResp.ok = true 102 133 statusResp.latency = performance.now() - start; 103 134 } 104 135 //if (instance.url === 'https://plc.j4ck.xyz') { statusResp.bundles.head_hash = 'f3ad3544452b2c078cba24990486bb9c277a1155'; } 105 - return statusResp 136 + return statusResp ?? { error: lastError || 'unknown error' } 106 137 } 107 138 108 139 function recalculateHead() { 109 140 isConflict = false 110 - const headHashes: string[] = [] 141 + instancesInConflict = [] 142 + const headHashes: any = {} 111 143 for (const instance of instances) { 112 - instance._head = instance.status?.bundles?.last_bundle === lastKnownBundle.number 113 - if (instance._head && instance.status?.bundles?.head_hash) { 114 - headHashes.push(instance.status.bundles.head_hash) 144 + if ((instance.status && 'error' in instance.status) || !instance.status) { 145 + continue 146 + } 147 + const { head_hash, root_hash, last_bundle } = instance.status.bundles 148 + instance._oldRoot = PAST_ROOTS.includes(root_hash) 149 + instance._head = !instance._oldRoot && (last_bundle === lastKnownBundle.number) 150 + 151 + if (instance._head && head_hash) { 152 + if (!headHashes[head_hash]) { 153 + headHashes[head_hash] = [] 154 + } 155 + headHashes[head_hash].push(instance.url) 115 156 } 116 157 } 117 - isConflict = [...new Set(headHashes)].length > 1 158 + console.log(headHashes) 159 + // second pass 160 + const sorted: any = Object.fromEntries( 161 + Object.entries(headHashes).sort(([, a]: any, [, b]: any) => b.length - a.length) 162 + ) 163 + for (const instance of instances) { 164 + if (Object.keys(sorted).length > 1 && Object.keys(sorted)[0] && !sorted[Object.keys(sorted)[0]].includes(instance.url)) { 165 + instance._conflict = true 166 + instancesInConflict.push(instance.url) 167 + } 168 + } 169 + //const uniq = [...new Set(headHashes)] 170 + isConflict = instancesInConflict.length > Math.ceil(instances.length/2) 118 171 } 119 172 120 173 async function doCheck() { 121 174 isUpdating = true 122 175 canRefresh = false 123 176 for (const i of instances) { 124 - if (i.status) { 177 + if (i.status && 'ok' in i.status) { 125 178 i.status.ok = false 126 179 } 127 180 } 128 181 129 182 await Promise.all(instances.map(async (instance) => { 130 183 const status = await getStatus(instance) 184 + if (!status) { 185 + return false 186 + } 187 + 131 188 instance.status = status 132 - if (instance.status) { 133 - instance.status.ok = true 134 - } 189 + if ('ok' in status && status.ok) { 135 190 136 - if (status?.bundles?.last_bundle && status.bundles.last_bundle >= lastKnownBundle.number) { 137 - lastKnownBundle.number = status.bundles.last_bundle 138 - lastKnownBundle.hash = status.bundles.head_hash 139 - lastKnownBundle.time = status.bundles.end_time 191 + if (status?.bundles?.last_bundle && status.bundles.last_bundle >= lastKnownBundle.number && !PAST_ROOTS.includes(status.bundles.root_hash)) { 192 + lastKnownBundle.number = status.bundles.last_bundle 193 + lastKnownBundle.hash = status.bundles.head_hash 194 + lastKnownBundle.time = status.bundles.end_time 140 195 141 - if (status?.mempool?.count && (!lastKnownBundle.mempool || status.mempool.count > lastKnownBundle.mempool || status.bundles.last_bundle > lastKnownBundle.mempoolBundle)) { 142 - lastKnownBundle.mempoolBundle = status.bundles.last_bundle 143 - lastKnownBundle.mempool = status.mempool.count 144 - lastKnownBundle.mempoolPercent = Math.round((lastKnownBundle.mempool/100)*100)/100 145 - lastKnownBundle.etaNext = status.mempool.eta_next_bundle_seconds ? addSeconds(new Date(), status.mempool.eta_next_bundle_seconds) : null 146 - lastKnownBundle.totalSize = status.bundles.total_size 147 - lastKnownBundle.totalSizeUncompressed = status.bundles.uncompressed_size 196 + if (status?.mempool?.count && (!lastKnownBundle.mempool || status.mempool.count > lastKnownBundle.mempool || status.bundles.last_bundle > lastKnownBundle.mempoolBundle)) { 197 + lastKnownBundle.mempoolBundle = status.bundles.last_bundle 198 + lastKnownBundle.mempool = status.mempool.count 199 + lastKnownBundle.lastTime = status.mempool.last_time 200 + lastKnownBundle.mempoolPercent = Math.round((lastKnownBundle.mempool/100)*100)/100 201 + lastKnownBundle.etaNext = status.mempool.eta_next_bundle_seconds ? addSeconds(new Date(), status.mempool.eta_next_bundle_seconds) : null 202 + lastKnownBundle.totalSize = status.bundles.total_size 203 + lastKnownBundle.totalSizeUncompressed = status.bundles.uncompressed_size 204 + } 148 205 } 149 206 } 207 + 150 208 lastUpdated = new Date() 151 209 152 210 recalculateHead() ··· 156 214 setTimeout(() => { canRefresh = true }, 500) 157 215 } 158 216 217 + function normalizedVersion(version: string) { 218 + const m = version.trim().match(/^([^\s]+)\.\d+\.\d+\-[0-9a-f]+(\+dirty|)$/) 219 + if (m) { 220 + return `${m[1]}+dirty` 221 + } 222 + return version 223 + } 224 + 159 225 function updateTitle() { 160 226 const arr: string[] = [] 161 227 if (lastUpdated) { 162 - const upCount = instances.filter(i => i._head) 228 + const upCount = instances.filter(i => i._head && !i._conflict) 163 229 arr.push(`${isConflict ? 'โš ๏ธ' : 'โœ…'} [${upCount.length}/${instances.length}]`) 164 230 } 165 231 document.title = [...arr, APP_TITLE].join(' ') 166 232 return true 167 233 } 168 234 235 + function bundleAge(instance: Instance) { 236 + if (!instance.status || !('bundles' in instance.status) || !instance.status.bundles?.updated_at) { 237 + return '-' 238 + } 239 + const str = instance.status?.bundles.updated_at ? instance.status?.bundles.updated_at : instance.status?.bundles.end_time || '' 240 + const diff = differenceInSeconds(new Date(), new Date(str)) 241 + return diff > 300 ? '>5m' : diff + 's' 242 + } 243 + 244 + function secondsAge(seconds: number) { 245 + return seconds > 300 ? '>5m' : seconds + 's' 246 + } 247 + 169 248 let autoRefreshTimer: ReturnType<typeof setTimeout> | null = null; 170 249 171 - onMount(async () => { 172 - await doCheck() 250 + onMount(() => { 251 + doCheck().then(() => { 252 + const scheduleRefresh = () => { 253 + autoRefreshTimer = setTimeout(() => { 254 + if (autoRefreshEnabled) { 255 + doCheck() 256 + } 257 + scheduleRefresh() 258 + }, AUTO_REFRESH_INTERVAL * 1000) 259 + } 260 + 261 + scheduleRefresh() 173 262 174 - const scheduleRefresh = () => { 175 - autoRefreshTimer = setTimeout(() => { 176 - if (autoRefreshEnabled) { 177 - doCheck() 178 - } 179 - scheduleRefresh() 180 - }, AUTO_REFRESH_INTERVAL * 1000) 181 - } 182 - 183 - scheduleRefresh() 263 + }) 184 264 185 265 return () => { 186 266 if (autoRefreshTimer) { ··· 199 279 </div> 200 280 <div class="flex items-center gap-6"> 201 281 <Switch class="opacity-75" checked={autoRefreshEnabled} onCheckedChange={(x) => autoRefreshEnabled = x.checked} disabled={isUpdating}> 202 - <Switch.Control className="data-[state=checked]:preset-filled-success-500"> 282 + <Switch.Control class="data-[state=checked]:preset-filled-success-500"> 203 283 <Switch.Thumb /> 204 284 </Switch.Control> 205 285 <Switch.Label>Auto-refresh ({AUTO_REFRESH_INTERVAL}s)</Switch.Label> ··· 224 304 <div> 225 305 <span class="opacity-50">{#if lastKnownBundle?.time} {formatDistanceToNow(lastKnownBundle.time, { addSuffix: true })}{/if}</span> 226 306 </div> 307 + <div class="mt-1"> 308 + {#if instancesInConflict.length > 0} 309 + โš ๏ธ Fork alert on {instancesInConflict.length} instances! 310 + {:else if !isConflict} 311 + โœ… Everything fine! 312 + {/if} 313 + </div> 227 314 </div> 228 315 </div> 229 316 <div> ··· 232 319 </div> 233 320 <div class="flex gap-4"> 234 321 <div class="mt-4"> 235 - <Progress value={lastKnownBundle.mempoolPercent} class="items-center {lastKnownBundle.mempoolPercent > 98 ? 'animate-pulse' : ''}"> 236 - <Progress.Circle style="--size: 64px; --thickness: 10px;"> 322 + <Progress value={lastKnownBundle.mempoolPercent} class="items-center"> 323 + <Progress.Circle style="--size: 64px; --thickness: 10px;" class={lastKnownBundle.mempoolPercent > 95 ? 'animate-pulse' : ''}> 237 324 <Progress.CircleTrack /> 238 325 <Progress.CircleRange /> 239 326 </Progress.Circle> ··· 257 344 <h2 class="opacity-75 text-sm">Statistics</h2> 258 345 </div> 259 346 <div class="mt-2 grid grid-cols-1 gap-1"> 260 - <div><span class="opacity-50">Instances:</span> {instances.filter(i => i._head).length} latest / {instances.length} total</div> 347 + <div><span class="opacity-50">Instances:</span> {instances.filter(i => i._head && !i._conflict).length} latest / {instances.length} total</div> 261 348 <div><span class="opacity-50">PLC Operations:</span> {formatNumber((lastKnownBundle.number * BUNDLE_OPS) + (lastKnownBundle.mempool || 0))}</div> 262 - <div><span class="opacity-50">Bundles Size:</span> {filesize(lastKnownBundle.totalSize)}</div> 263 - <div><span class="opacity-50">Uncompressed:</span> {filesize(lastKnownBundle.totalSizeUncompressed)}</div> 349 + <div><span class="opacity-50">Bundles Size:</span> {#if lastKnownBundle.totalSize}{filesize(lastKnownBundle.totalSize)}{/if}</div> 350 + <div><span class="opacity-50">Uncompressed:</span> {#if lastKnownBundle.totalSizeUncompressed}{filesize(lastKnownBundle.totalSizeUncompressed)}{/if}</div> 264 351 </div> 265 352 </div> 266 353 {/if} ··· 275 362 <th>mempool</th> 276 363 <th>age</th> 277 364 <th>head</th> 278 - <th>first</th> 365 + <th>root</th> 279 366 <th>version</th> 367 + <th>rsv?</th> 280 368 <th>ws?</th> 281 369 <th>uptime</th> 282 370 <th>latency</th> ··· 286 374 {#each orderBy(instances, ...instanceOrderBy) as instance} 287 375 <tr> 288 376 <td><a href={instance.url} target="_blank" class="font-semibold">{instance.url.replace("https://", "")}</a></td> 289 - <td>{#if instance._head && instance.status?.ok}{#if isConflict}โš ๏ธ{:else}โœ…{/if}{:else if instance.status && instance.status?.ok}๐Ÿ”„{:else}โŒ›{/if}</td> 290 - <td>{#if instance.status?.bundles?.last_bundle}{instance.status?.bundles?.last_bundle}{/if}</td> 291 - <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> 292 - <td class="text-xs opacity-50">{#if instance.status?.mempool && instance._head}{instance.status?.mempool.last_op_age_seconds || 0}s{/if}</td> 293 - <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> 294 - <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> 295 - <td class="text-xs">{#if instance.status?.server?.version}{instance.status?.server?.version}{/if}</td> 296 - <td class="text-xs">{#if instance.status?.server?.websocket_enabled}โœ”๏ธŽ{:else if instance.status}<span class="opacity-25">-</span>{/if}</td> 377 + <td>{#if instance._head && instance.status?.ok}{#if instance._conflict}โš ๏ธ{:else}โœ…{/if}{:else if instance.status?.ok && instance._oldRoot}๐Ÿฆ‡{:else if instance.status?.ok}๐Ÿ”„{:else if instance.status?.error}โŒ{:else}โŒ›{/if}</td> 378 + {#if instance.status?.error} 379 + <td colspan="8" class="opacity-50 text-xs">Error: {instance.status?.error}</td> 380 + {:else} 381 + <td>{#if instance.status?.bundles?.last_bundle}<span class="{instance._conflict ? 'text-error-600' : ''}">{instance.status?.bundles?.last_bundle}</span>{/if}</td> 382 + <td>{#if instance.status?.mempool && (instance._head || instance._oldRoot)}<span class="{instance._conflict ? 'text-error-600' : (instance._oldRoot ? 'text-warning-500 opacity-50' : '')}">{formatNumber(instance.status?.mempool.count)}</span>{:else if instance.status?.error}<span class="opacity-25 text-xs">error</span>{:else if instance.status}<span class="opacity-25 text-xs">syncing</span>{/if}</td> 383 + <td> 384 + {#if instance.status?.mempool} 385 + <span class="text-xs opacity-50 {instance._conflict ? 'text-error-600' : (instance._oldRoot ? 'text-warning-500' : '')}"> 386 + {#if instance._head || instance._oldRoot} 387 + {secondsAge(instance.status?.mempool.last_op_age_seconds || 0)} 388 + {:else} 389 + {bundleAge(instance)} 390 + {/if} 391 + </span> 392 + {/if} 393 + </td> 394 + <td><span class="font-mono text-xs {instance._head ? (instance._conflict ? 'text-error-600' : 'text-success-600') : (instance._oldRoot ? 'text-warning-500 opacity-50' : 'opacity-50')}">{#if instance.status?.bundles?.head_hash}{instance.status?.bundles?.head_hash.slice(0, 7)}{/if}</span></td> 395 + <td><span class="font-mono text-xs {instance.status ? (instance.status?.bundles?.root_hash === ROOT ? 'text-success-600' : (PAST_ROOTS.includes(instance.status?.bundles?.root_hash) ? 'text-warning-500' : 'text-error-600')) : ''}">{#if instance.status?.bundles?.root_hash}{instance.status?.bundles?.root_hash.slice(0, 7)}{/if}</span></td> 396 + <td class="text-xs">{#if instance.status?.server?.version}<span title={instance.status?.server?.version}>{normalizedVersion(instance.status?.server?.version)}</span>{/if}</td> 397 + <td class="text-xs">{#if instance.status?.server?.resolver_enabled}โœ”๏ธŽ{:else if instance.status}<span class="opacity-25">-</span>{/if}</td> 398 + <td class="text-xs">{#if instance.status?.server?.websocket_enabled}โœ”๏ธŽ{:else if instance.status}<span class="opacity-25">-</span>{/if}</td> 399 + {/if} 297 400 <td class="text-xs">{#if instance.status?.server?.uptime_seconds}{formatUptime(instance.status?.server?.uptime_seconds)}{/if}</td> 298 401 <td class="text-xs opacity-50">{#if instance.status?.latency}<a href="{instance.url}/status">{Math.round(instance.status?.latency)}ms</a>{/if}</td> 299 402 </tr> ··· 314 417 Last updated: {formatISO9075(lastUpdated)} 315 418 </div> 316 419 </div> 317 - <hr class="hr my-10" /> 318 - 319 - <BundleDownloader instances={instances} /> 420 + <!--hr class="hr my-10" /--> 421 + <!--BundleDownloader instances={instances} /--> 320 422 321 423 <hr class="hr mb-6 mt-12" /> 322 424 <div class="opacity-50">
+50 -18
src/instances.json
··· 1 1 [ 2 - { 3 - "url": "https://plcbundle.atscan.net", 4 - "country": "AT", 5 - "maintainer": "@tree.fail" 6 - }, 7 - { 8 - "url": "https://plc.j4ck.xyz" 9 - }, 10 - { 11 - "url": "https://plc.indexx.dev" 12 - }, 13 - { 14 - "url": "https://plc.nyxt.dev" 15 - }, 16 - { 17 - "url": "https://plc.madebydanny.uk" 18 - } 19 - ] 2 + { 3 + "url": "https://plcbundle.atscan.net", 4 + "country": "AT", 5 + "maintainer": "@tree.fail" 6 + }, 7 + { 8 + "url": "https://plcbundle2.atscan.net", 9 + "country": "CZ", 10 + "maintainer": "@tree.fail" 11 + }, 12 + { 13 + "url": "https://plcbundle3.atscan.net", 14 + "country": "CZ", 15 + "maintainer": "@tree.fail" 16 + }, 17 + { 18 + "url": "https://plc.j4ck.xyz", 19 + "country": "UK", 20 + "maintainer": "@j4ck.xyz" 21 + }, 22 + { 23 + "url": "https://plc.indexx.dev", 24 + "country": "US", 25 + "maintainer": "@indexx.dev" 26 + }, 27 + { 28 + "url": "https://plc.nyxt.dev" 29 + }, 30 + { 31 + "url": "https://plc.madebydanny.uk", 32 + "country": "US", 33 + "maintainer": "@madebydanny.uk" 34 + }, 35 + { 36 + "url": "https://plc.tartarus.us" 37 + }, 38 + { 39 + "url": "https://plcbundle.snek.cc", 40 + "country": "US", 41 + "maintainer": "@jackvalinsky.com" 42 + }, 43 + { 44 + "url": "https://plc.dane.computer", 45 + "country": "CA", 46 + "maintainer": "@dane.is.extraordinarily.cool" 47 + }, 48 + { 49 + "url": "https://plc.witchcraft.systems" 50 + } 51 + ]
+1
src/types/lodash.d.ts
··· 1 + declare module 'lodash/orderBy';