Files
Synapsis/src/lib/background/scheduler.ts
T
cyph3rasi 1427df4bdc Restrict public swarm participation and propagation to real public ICANN domains
Hop-State: A_06FP8GM0PKJTMF45B7016J0
Hop-Proposal: R_06FP8GKFXXZG84ETVCPS5ZG
Hop-Task: T_06FP8F45T3G4Z4ESW9NZQQR
Hop-Attempt: AT_06FP8F45T3QS81M2ZM4SJ3G
2026-07-14 22:31:47 -07:00

162 lines
5.6 KiB
TypeScript

/**
* Background Task Scheduler
*
* Runs periodic tasks within the Next.js process:
* - Bot autonomous posting (every 1 minute)
* - Swarm gossip (every 5 minutes)
* - Remote follows sync (every 10 minutes)
* - Swarm announcement (on startup)
*/
import { processAllAutonomousBots } from '@/lib/bots/autonomous';
import { runGossipRound } from '@/lib/swarm/gossip';
import { announceToSeeds } from '@/lib/swarm/discovery';
import { getSwarmStats } from '@/lib/swarm/registry';
import { syncRemoteFollowsPosts } from '@/lib/background/remote-sync';
import { isPublicSwarmDomain } from '@/lib/swarm/node-domain';
const BOT_INTERVAL_MS = 60 * 1000; // 1 minute
const GOSSIP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
const REMOTE_SYNC_INTERVAL_MS = 60 * 1000; // 1 minute - keep feeds fresh
const STARTUP_DELAY_MS = 10 * 1000; // Wait 10s for server to be ready
let isStarted = false;
function log(category: string, message: string, data?: unknown) {
const timestamp = new Date().toISOString();
if (data) {
console.log(`[${timestamp}] [${category}] ${message}`, JSON.stringify(data, null, 2));
} else {
console.log(`[${timestamp}] [${category}] ${message}`);
}
}
async function runBotTasks() {
try {
const results = await processAllAutonomousBots();
const posted = results.filter(r => r.result.posted).length;
const errors = results.filter(r => r.error).length;
if (posted > 0) {
log('BOTS', `Created ${posted} posts`);
} else if (results.length > 0) {
// Log why bots didn't post
const reasons = results
.filter(r => !r.result.posted)
.map(r => `${r.botHandle}: ${r.result.reason || r.error || 'unknown'}`)
.slice(0, 5);
if (reasons.length > 0) {
log('BOTS', `${results.length} bots checked, no posts. Reasons: ${reasons.join('; ')}`);
}
} else {
log('BOTS', 'No active bots found');
}
if (errors > 0) {
const errorMsgs = results.filter(r => r.error).map(r => `${r.botHandle}: ${r.error}`);
log('BOTS', `Errors: ${errorMsgs.join('; ')}`);
}
} catch (error) {
log('BOTS', `Error: ${error}`);
}
}
async function runSwarmGossip() {
try {
const stats = await getSwarmStats();
// Recover from empty peer lists by periodically re-announcing to seeds.
if (stats.activeNodes === 0) {
const announceResult = await announceToSeeds();
if (announceResult.successful.length > 0 || announceResult.failed.length > 0) {
log('SWARM', `Re-announced to seeds: ${announceResult.successful.length} successful, ${announceResult.failed.length} failed`);
}
}
const result = await runGossipRound();
if (result.contacted > 0) {
log('SWARM', `Gossip: contacted ${result.contacted}, successful ${result.successful}, received ${result.totalNodesReceived} nodes`);
} else if (stats.activeNodes === 0) {
log('SWARM', 'No active swarm peers yet');
}
} catch (error) {
log('SWARM', `Gossip error: ${error}`);
}
}
async function announceToSwarm() {
try {
const result = await announceToSeeds();
log('SWARM', `Announced to seeds: ${result.successful.length} successful, ${result.failed.length} failed`);
const stats = await getSwarmStats();
log('SWARM', `Network: ${stats.activeNodes} active nodes, ${stats.totalUsers} users, ${stats.totalPosts} posts`);
} catch (error) {
log('SWARM', `Announcement error: ${error}`);
}
}
async function runRemoteSync(origin: string) {
try {
const result = await syncRemoteFollowsPosts(origin);
if (result.synced > 0 || result.errors > 0) {
log('REMOTE_SYNC', `Synced ${result.synced} users, skipped ${result.skipped}, errors ${result.errors}`);
if (result.details.length > 0) {
const newPosts = result.details.filter(d => d.cached > 0);
if (newPosts.length > 0) {
log('REMOTE_SYNC', `New posts: ${newPosts.map(d => `${d.handle}: ${d.cached}`).join(', ')}`);
}
}
}
} catch (error) {
log('REMOTE_SYNC', `Error: ${error}`);
}
}
export function startBackgroundTasks(origin?: string) {
// Prevent double-start (Next.js can call register() multiple times in dev)
if (isStarted) return;
isStarted = true;
// Default origin for remote sync (can be overridden)
const syncOrigin = origin || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:43821';
const publicSwarmEnabled = isPublicSwarmDomain(process.env.NEXT_PUBLIC_NODE_DOMAIN);
log('STARTUP', 'Background task scheduler starting...');
log('STARTUP', `Bot interval: ${BOT_INTERVAL_MS / 1000}s, Gossip interval: ${GOSSIP_INTERVAL_MS / 1000}s, Remote sync interval: ${REMOTE_SYNC_INTERVAL_MS / 1000}s`);
// Wait for server to be fully ready before starting tasks
setTimeout(async () => {
log('STARTUP', 'Starting background tasks...');
if (publicSwarmEnabled) {
// Announce to swarm on startup
await announceToSwarm();
} else {
log('SWARM', 'Public swarm disabled: NEXT_PUBLIC_NODE_DOMAIN is not a public ICANN domain');
}
// Run initial bot check
await runBotTasks();
// Run initial remote sync (after 15s to let server stabilize)
setTimeout(() => runRemoteSync(syncOrigin), 15 * 1000);
// Schedule recurring tasks
setInterval(runBotTasks, BOT_INTERVAL_MS);
if (publicSwarmEnabled) {
setInterval(runSwarmGossip, GOSSIP_INTERVAL_MS);
}
setInterval(() => runRemoteSync(syncOrigin), REMOTE_SYNC_INTERVAL_MS);
// First gossip after 30s (let announcement propagate)
if (publicSwarmEnabled) {
setTimeout(runSwarmGossip, 30 * 1000);
}
log('STARTUP', 'Background tasks running');
}, STARTUP_DELAY_MS);
}