Initial commit

This commit is contained in:
root
2026-01-26 08:34:48 +01:00
commit 387314581f
216 changed files with 81204 additions and 0 deletions
+311
View File
@@ -0,0 +1,311 @@
/**
* Swarm Registry
*
* Manages the local registry of known swarm nodes.
*/
import { db, swarmNodes, swarmSeeds, swarmSyncLog } from '@/db';
import { eq, desc, and, gt, lt, sql } from 'drizzle-orm';
import type { SwarmNodeInfo, SwarmCapability, SwarmSyncResult } from './types';
import { SWARM_CONFIG, DEFAULT_SEED_NODES } from './types';
/**
* Get or create a swarm node entry
*/
export async function upsertSwarmNode(
node: SwarmNodeInfo,
discoveredVia?: string
): Promise<{ isNew: boolean }> {
if (!db) {
return { isNew: false };
}
const existing = await db.query.swarmNodes.findFirst({
where: eq(swarmNodes.domain, node.domain),
});
const capabilities = node.capabilities ? JSON.stringify(node.capabilities) : null;
if (!existing) {
await db.insert(swarmNodes).values({
domain: node.domain,
name: node.name,
description: node.description,
logoUrl: node.logoUrl,
publicKey: node.publicKey,
softwareVersion: node.softwareVersion,
userCount: node.userCount,
postCount: node.postCount,
isNsfw: node.isNsfw ?? false,
discoveredVia,
capabilities,
lastSeenAt: node.lastSeenAt ? new Date(node.lastSeenAt) : new Date(),
});
return { isNew: true };
}
// Update existing node
await db.update(swarmNodes)
.set({
name: node.name ?? existing.name,
description: node.description ?? existing.description,
logoUrl: node.logoUrl ?? existing.logoUrl,
publicKey: node.publicKey ?? existing.publicKey,
softwareVersion: node.softwareVersion ?? existing.softwareVersion,
userCount: node.userCount ?? existing.userCount,
postCount: node.postCount ?? existing.postCount,
isNsfw: node.isNsfw ?? existing.isNsfw,
capabilities: capabilities ?? existing.capabilities,
lastSeenAt: new Date(),
consecutiveFailures: 0,
isActive: true,
updatedAt: new Date(),
})
.where(eq(swarmNodes.domain, node.domain));
return { isNew: false };
}
/**
* Bulk upsert swarm nodes from gossip
*/
export async function upsertSwarmNodes(
nodes: SwarmNodeInfo[],
discoveredVia: string
): Promise<{ added: number; updated: number }> {
if (!db || nodes.length === 0) {
return { added: 0, updated: 0 };
}
let added = 0;
let updated = 0;
// Filter out our own domain
const ourDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN;
const filteredNodes = nodes.filter(n => n.domain !== ourDomain);
for (const node of filteredNodes) {
const result = await upsertSwarmNode(node, discoveredVia);
if (result.isNew) {
added++;
} else {
updated++;
}
}
return { added, updated };
}
/**
* Get all active swarm nodes
*/
export async function getActiveSwarmNodes(limit = 100): Promise<SwarmNodeInfo[]> {
if (!db) {
return [];
}
const nodes = await db.query.swarmNodes.findMany({
where: eq(swarmNodes.isActive, true),
orderBy: [desc(swarmNodes.lastSeenAt)],
limit,
});
return nodes.map(nodeToInfo);
}
/**
* Get nodes for gossip (random selection of active nodes)
*/
export async function getNodesForGossip(count: number): Promise<SwarmNodeInfo[]> {
if (!db) {
return [];
}
// Get active nodes with decent trust scores, ordered randomly
const nodes = await db.query.swarmNodes.findMany({
where: and(
eq(swarmNodes.isActive, true),
gt(swarmNodes.trustScore, 20)
),
orderBy: sql`RANDOM()`,
limit: count,
});
return nodes.map(nodeToInfo);
}
/**
* Get nodes updated since a timestamp (for incremental sync)
*/
export async function getNodesSince(since: Date, limit = 100): Promise<SwarmNodeInfo[]> {
if (!db) {
return [];
}
const nodes = await db.query.swarmNodes.findMany({
where: gt(swarmNodes.updatedAt, since),
orderBy: [desc(swarmNodes.updatedAt)],
limit,
});
return nodes.map(nodeToInfo);
}
/**
* Mark a node as having failed contact
*/
export async function markNodeFailure(domain: string): Promise<void> {
if (!db) return;
const node = await db.query.swarmNodes.findFirst({
where: eq(swarmNodes.domain, domain),
});
if (!node) return;
const newFailures = node.consecutiveFailures + 1;
const newTrust = Math.max(
SWARM_CONFIG.minTrustScore,
node.trustScore + SWARM_CONFIG.trustScoreOnFailure
);
const isActive = newFailures < SWARM_CONFIG.maxConsecutiveFailures;
await db.update(swarmNodes)
.set({
consecutiveFailures: newFailures,
trustScore: newTrust,
isActive,
updatedAt: new Date(),
})
.where(eq(swarmNodes.domain, domain));
}
/**
* Mark a node as successfully contacted
*/
export async function markNodeSuccess(domain: string): Promise<void> {
if (!db) return;
const node = await db.query.swarmNodes.findFirst({
where: eq(swarmNodes.domain, domain),
});
if (!node) return;
const newTrust = Math.min(
SWARM_CONFIG.maxTrustScore,
node.trustScore + SWARM_CONFIG.trustScoreOnSuccess
);
await db.update(swarmNodes)
.set({
consecutiveFailures: 0,
trustScore: newTrust,
isActive: true,
lastSeenAt: new Date(),
lastSyncAt: new Date(),
updatedAt: new Date(),
})
.where(eq(swarmNodes.domain, domain));
}
/**
* Log a sync operation
*/
export async function logSync(
remoteDomain: string,
direction: 'push' | 'pull',
result: SwarmSyncResult
): Promise<void> {
if (!db) return;
await db.insert(swarmSyncLog).values({
remoteDomain,
direction,
nodesReceived: result.nodesReceived,
nodesSent: result.nodesSent,
handlesReceived: result.handlesReceived,
handlesSent: result.handlesSent,
success: result.success,
errorMessage: result.error,
durationMs: result.durationMs,
});
}
/**
* Get seed nodes (with fallback to defaults)
*/
export async function getSeedNodes(): Promise<string[]> {
if (!db) {
return [...DEFAULT_SEED_NODES];
}
const seeds = await db.query.swarmSeeds.findMany({
where: eq(swarmSeeds.isEnabled, true),
orderBy: [swarmSeeds.priority],
});
if (seeds.length === 0) {
return [...DEFAULT_SEED_NODES];
}
return seeds.map(s => s.domain);
}
/**
* Add a seed node
*/
export async function addSeedNode(domain: string, priority = 100): Promise<void> {
if (!db) return;
await db.insert(swarmSeeds)
.values({ domain, priority })
.onConflictDoUpdate({
target: swarmSeeds.domain,
set: { priority, isEnabled: true },
});
}
/**
* Get swarm statistics
*/
export async function getSwarmStats() {
if (!db) {
return {
totalNodes: 0,
activeNodes: 0,
totalUsers: 0,
totalPosts: 0,
};
}
const allNodes = await db.query.swarmNodes.findMany();
const activeNodes = allNodes.filter(n => n.isActive);
const totalUsers = activeNodes.reduce((sum, n) => sum + (n.userCount || 0), 0);
const totalPosts = activeNodes.reduce((sum, n) => sum + (n.postCount || 0), 0);
return {
totalNodes: allNodes.length,
activeNodes: activeNodes.length,
totalUsers,
totalPosts,
};
}
// Helper to convert DB node to SwarmNodeInfo
function nodeToInfo(node: typeof swarmNodes.$inferSelect): SwarmNodeInfo {
return {
domain: node.domain,
name: node.name ?? undefined,
description: node.description ?? undefined,
logoUrl: node.logoUrl ?? undefined,
publicKey: node.publicKey ?? undefined,
softwareVersion: node.softwareVersion ?? undefined,
userCount: node.userCount ?? undefined,
postCount: node.postCount ?? undefined,
capabilities: node.capabilities ? JSON.parse(node.capabilities) : undefined,
isNsfw: node.isNsfw,
lastSeenAt: node.lastSeenAt.toISOString(),
};
}