-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9fbfaca
commit 1f48d82
Showing
4 changed files
with
246 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
/// <reference lib="deno.unstable" /> | ||
|
||
import { ServerInfo } from '../models/ServerInfo.ts'; | ||
import config from '../config.ts'; | ||
|
||
interface ServerEntry { | ||
url: string; | ||
info: ServerInfo; | ||
lastUpdated: number; | ||
} | ||
|
||
export class PeerListManager { | ||
private kv!: Deno.Kv; | ||
private queue: Set<string> = new Set(); | ||
private staleUrls: Map<string, number> = new Map(); // Track stale URLs for re-checking | ||
private processing = false; | ||
private secret?: number; | ||
private secretExpiration = 0; | ||
private healthCheckAttempts = 0; | ||
private isHealthy = false; | ||
|
||
constructor() { | ||
this.initializeKv().then(() => { | ||
this.initializeFromFile(); | ||
this.runHealthChecks(); // Start health checks | ||
}); | ||
} | ||
|
||
private async initializeKv() { | ||
this.kv = await Deno.openKv(); | ||
} | ||
|
||
private async initializeFromFile() { | ||
try { | ||
// Use async file operations instead of sync | ||
const urls = (await Deno.readTextFile('./servers.txt')) | ||
.split('\n') | ||
.map((url) => this.normalizeUrl(url)) | ||
.filter((url) => url && url.startsWith('http')); | ||
|
||
// Load existing KV entries on startup | ||
const existingEntries = this.kv.list<ServerEntry>({ prefix: ['servers'] }); | ||
for await (const entry of existingEntries) { | ||
this.queue.add(entry.value.url); | ||
} | ||
|
||
// Add URLs from the file to the queue | ||
for (const url of urls) { | ||
await this.addToQueue(url); | ||
} | ||
} catch { | ||
await Deno.writeTextFile('./servers.txt', 'https://bridge.candiru.xyz\n'); | ||
} | ||
} | ||
|
||
private normalizeUrl(url: string): string { | ||
try { | ||
return new URL(url).href.replace(/\/$/, ''); // Normalize URL and remove trailing slashes | ||
} catch { | ||
return ''; // Invalid URL, return empty string | ||
} | ||
} | ||
|
||
private startQueueProcessor() { | ||
this.processQueue(); // Process immediately once | ||
return setInterval(() => this.processQueue(), config.server.peerUpdateInterval * 1000); | ||
} | ||
|
||
private async processQueue() { | ||
if (!this.isHealthy || this.processing) return; | ||
this.processing = true; | ||
|
||
// Process stale entries first | ||
const now = Date.now(); | ||
for (const [url, expiry] of this.staleUrls) { | ||
if (now >= expiry) { | ||
this.queue.add(url); | ||
this.staleUrls.delete(url); | ||
} | ||
} | ||
|
||
// Process the queue | ||
for (const url of this.queue) { | ||
const entry = await this.kv.get<ServerEntry>(['servers', url]); | ||
const needsUpdate = !entry.value || | ||
Date.now() - entry.value.lastUpdated > config.server.peerUpdateInterval * 1000; | ||
if (needsUpdate) { | ||
await this.checkServer(url); | ||
} | ||
} | ||
|
||
this.processing = false; | ||
} | ||
|
||
private async checkServer(url: string) { | ||
const controller = new AbortController(); | ||
const timeout = setTimeout(() => controller.abort(), 5000); | ||
try { | ||
console.log(`Checking ${url}`); | ||
const response = await fetch(`${url}/api/getInfo`, { | ||
signal: controller.signal, | ||
}); | ||
|
||
// Validate response format | ||
const info: ServerInfo = await response.json(); | ||
if (!this.validateServerInfo(info)) { | ||
throw new Error('Invalid server response'); | ||
} | ||
|
||
// Update lastUpdated even if info hasn't changed | ||
await this.kv.set(['servers', url], { | ||
url, | ||
info, | ||
lastUpdated: Date.now(), | ||
}); | ||
|
||
await this.shareWithServer(url); | ||
} catch (error) { | ||
console.error(`Failed to check ${url}:`, error); | ||
// Retry logic with backoff | ||
const retryTime = Date.now() + (30 * 60 * 1000); // 30 min default | ||
this.staleUrls.set(url, retryTime); | ||
this.queue.delete(url); | ||
} finally { | ||
clearTimeout(timeout); | ||
} | ||
} | ||
|
||
private validateServerInfo(obj: any): obj is ServerInfo { | ||
return obj && typeof obj === 'object' && | ||
'version' in obj && | ||
'playerCount' in obj; | ||
} | ||
|
||
private async shareWithServer(url: string) { | ||
const entries = this.kv.list<ServerEntry>({ prefix: ['servers'] }); | ||
const urls: string[] = []; | ||
for await (const entry of entries) { | ||
if (entry.value.url !== url) { // Exclude self from sharing | ||
urls.push(entry.value.url); | ||
} | ||
} | ||
try { | ||
console.log(`Sharing with ${url}`); | ||
await fetch(`${url}/api/shareServerList`, { | ||
method: 'POST', | ||
body: JSON.stringify(urls), | ||
headers: { 'Content-Type': 'application/json' }, | ||
}); | ||
} catch (error) { | ||
console.error(`Failed to share with ${url}:`, error); | ||
} | ||
} | ||
|
||
public async addToQueue(url: string) { | ||
const normalized = this.normalizeUrl(url); | ||
if (normalized === this.normalizeUrl(config.server.url)) return; // Skip self | ||
|
||
const existing = await this.kv.get(['servers', normalized]); | ||
if (!existing.value && !this.queue.has(normalized)) { | ||
console.log(`Adding ${normalized} to queue`); | ||
this.queue.add(normalized); | ||
} | ||
} | ||
|
||
private async runHealthChecks() { | ||
const checkHealth = async () => { | ||
const success = await this.performHealthCheck(); | ||
if (success) { | ||
this.isHealthy = true; | ||
this.startQueueProcessor(); | ||
} | ||
// Continue checking periodically regardless of success | ||
setTimeout(checkHealth, config.server.selfCheckInterval * 1000); | ||
}; | ||
|
||
await checkHealth(); | ||
} | ||
|
||
private async performHealthCheck() { | ||
this.secret = Math.random(); | ||
this.secretExpiration = Date.now() + 5000; // 5-second window for secret validity | ||
try { | ||
await fetch(`${config.server.url}/api/selfCheck`, { | ||
method: 'POST', | ||
body: JSON.stringify({ secret: this.secret }), | ||
headers: { 'Content-Type': 'application/json' }, | ||
}); | ||
console.log('Health check passed'); | ||
return true; | ||
} catch (error) { | ||
console.error('Health check failed:', error); | ||
return false; | ||
} | ||
} | ||
|
||
public validateSelfCheck(secret: number) { | ||
const isValid = secret === this.secret && Date.now() < this.secretExpiration; | ||
this.secret = undefined; // Invalidate after use | ||
return isValid; | ||
} | ||
} |