This repository has no description
4.5 kB
137 lines
1/**
2 * Shared island utilities: canonical ID, component types, BFS, DB access.
3 */
4
5import { resolve } from 'node:path';
6import { createHash } from 'node:crypto';
7import { Database } from 'bun:sqlite';
8
9const CARRY_DIR = process.env.CARRY_DIR ?? '/home/nandi/.local/share/carry-vault';
10const DB_PATH = process.env.ISLAND_DETECT_DB ?? resolve(CARRY_DIR, '.island-detect.db');
11
12export interface Component {
13 nodes: Set<string>;
14 dids: Set<string>;
15 lexmin: string;
16 centroid: string;
17 islandId: string;
18}
19
20export interface EdgeRow {
21 source: string;
22 target: string;
23 relation: string | null;
24 did: string;
25 atUri: string | null;
26}
27
28// --- Canonical island ID: first 12 hex chars of SHA-256(lexmin) ---
29
30export function islandId(lexmin: string): string {
31 return createHash('sha256').update(lexmin).digest('hex').slice(0, 12);
32}
33
34// --- SQLite ---
35
36export function openDb(): Database {
37 return new Database(DB_PATH, { readonly: true });
38}
39
40// --- Connected components (BFS) ---
41
42export function findComponents(db: Database): Component[] {
43 const adj = new Map<string, Set<string>>();
44 const edgeDids = new Map<string, Set<string>>();
45
46 const rows = db.query('SELECT source, target, did FROM edges').all() as Array<{ source: string; target: string; did: string }>;
47 for (const row of rows) {
48 const s = row.source;
49 const t = row.target;
50 if (!adj.has(s)) adj.set(s, new Set());
51 if (!adj.has(t)) adj.set(t, new Set());
52 adj.get(s)!.add(t);
53 adj.get(t)!.add(s);
54 if (!edgeDids.has(s)) edgeDids.set(s, new Set());
55 if (!edgeDids.has(t)) edgeDids.set(t, new Set());
56 edgeDids.get(s)!.add(row.did);
57 edgeDids.get(t)!.add(row.did);
58 }
59
60 const visited = new Set<string>();
61 const components: Component[] = [];
62
63 for (const node of adj.keys()) {
64 if (visited.has(node)) continue;
65 const compNodes = new Set<string>();
66 const compDids = new Set<string>();
67 const queue = [node];
68 while (queue.length > 0) {
69 const current = queue.pop()!;
70 if (visited.has(current)) continue;
71 visited.add(current);
72 compNodes.add(current);
73 for (const did of edgeDids.get(current) ?? []) compDids.add(did);
74 for (const neighbor of adj.get(current) ?? []) {
75 if (!visited.has(neighbor)) queue.push(neighbor);
76 }
77 }
78 const lexmin = [...compNodes].sort()[0];
79 components.push({ nodes: compNodes, dids: compDids, lexmin, islandId: islandId(lexmin) });
80 }
81
82 return components.sort((a, b) => b.nodes.size - a.nodes.size);
83}
84
85/** Find a component by its island ID (lexmin hash). */
86export function findComponentById(db: Database, targetId: string): Component | undefined {
87 return findComponents(db).find((c) => c.islandId === targetId);
88}
89
90/** Fetch edges for a component from the DB. */
91export function fetchComponentEdges(db: Database, component: Component): EdgeRow[] {
92 const nodePlaceholders = [...component.nodes].map(() => '?').join(',');
93 const nodeValues = [...component.nodes];
94
95 // at_uri column may not exist yet — try with it, fall back without
96 let edges: EdgeRow[];
97 try {
98 edges = db.query(
99 `SELECT source, target, relation, did, at_uri as atUri FROM edges WHERE source IN (${nodePlaceholders}) OR target IN (${nodePlaceholders})`,
100 ).all(...nodeValues, ...nodeValues) as EdgeRow[];
101 } catch {
102 edges = db.query(
103 `SELECT source, target, relation, did, NULL as atUri FROM edges WHERE source IN (${nodePlaceholders}) OR target IN (${nodePlaceholders})`,
104 ).all(...nodeValues, ...nodeValues) as EdgeRow[];
105 }
106
107 // Deduplicate (same source+target from multiple DIDs)
108 const seen = new Set<string>();
109 const unique: EdgeRow[] = [];
110 for (const e of edges) {
111 const key = `${e.source}::${e.target}`;
112 if (!seen.has(key)) {
113 seen.add(key);
114 unique.push(e);
115 }
116 }
117 return unique;
118}
119
120/** Resolve handles for a set of DIDs. */
121export function resolveDidHandles(db: Database, dids: Set<string>): Map<string, string | null> {
122 const handles = new Map<string, string | null>();
123 if (dids.size === 0) return handles;
124 const placeholders = [...dids].map(() => '?').join(',');
125 const rows = db.query(`SELECT did, handle FROM dids WHERE did IN (${placeholders})`).all(...dids) as Array<{ did: string; handle: string | null }>;
126 for (const r of rows) handles.set(r.did, r.handle);
127 return handles;
128}
129
130/** Extract domain from a URL for display. */
131export function domainFromUrl(url: string): string {
132 try {
133 return new URL(url).hostname.replace(/^www\./, '');
134 } catch {
135 return url;
136 }
137}