Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 120 additions & 64 deletions Hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,76 +3,132 @@ import { Peer, PeerHealth } from "./Peer.ts";
import { PeerStorage } from "./PeerStorage.ts";
import { PeerCouchDB } from "./PeerCouchDB.ts";


export class Hub {
conf: Config;
peers = [] as Peer[];
constructor(conf: Config) {
this.conf = conf;
conf: Config;
peers = [] as Peer[];
constructor(conf: Config) {
this.conf = conf;
}
// Aggregate peer health for the heartbeat. `ok` = every peer syncing (also
// false if no peers were constructed). `restartWorthy` = any peer judges itself
// restart-worthy (was healthy, now persistently failing while its backend is
// up) — see Peer.probeHealth.
async healthProbe(): Promise<
{ ok: boolean; restartWorthy: boolean; peers: PeerHealth[] }
> {
const peers = await Promise.all(this.peers.map((p) => p.probeHealth()));
const ok = peers.length > 0 && peers.every((p) => p.ok);
const restartWorthy = peers.some((p) => p.restartWorthy);
return { ok, restartWorthy, peers };
}
async reconcileDiskDeletions() {
const storagePeers = this.peers.filter((p) =>
p.config.type === "storage"
) as PeerStorage[];
await Promise.all(storagePeers.map((p) => p.waitOfflineScanComplete()));

for (const cp of this.peers) {
if (cp.config.type !== "couchdb") continue;
const matchingStoragePeers = storagePeers.filter((p) =>
(p.config.group ?? "") === (cp.config.group ?? "")
);
if (matchingStoragePeers.length === 0) continue;

const peerCouchDb = cp as PeerCouchDB;
for await (
const doc of peerCouchDb.man.enumerateAllNormalDocs({ metaOnly: true })
) {
if (doc.deleted || (doc as any)._deleted) continue;

let path = (doc as any).path.substring(
peerCouchDb.toLocalPath("").length,
);
if (path.startsWith("/")) path = path.substring(1);
if (path.startsWith("i:")) path = path.substring(2);

const globalPath = peerCouchDb.toGlobalPath(path);
let existsOnDisk = false;
for (const sp of matchingStoragePeers) {
try {
if (await sp.get(globalPath) !== false) {
existsOnDisk = true;
break;
}
} catch (e) {
// Fallback for not found
}
}

if (!existsOnDisk) {
await peerCouchDb.delete(globalPath);
}
}
}
// Aggregate peer health for the heartbeat. `ok` = every peer syncing (also
// false if no peers were constructed). `restartWorthy` = any peer judges itself
// restart-worthy (was healthy, now persistently failing while its backend is
// up) — see Peer.probeHealth.
async healthProbe(): Promise<{ ok: boolean; restartWorthy: boolean; peers: PeerHealth[] }> {
const peers = await Promise.all(this.peers.map((p) => p.probeHealth()));
const ok = peers.length > 0 && peers.every((p) => p.ok);
const restartWorthy = peers.some((p) => p.restartWorthy);
return { ok, restartWorthy, peers };
}

start() {
for (const p of this.peers) {
p.stop();
}
this.peers = [];
for (const peer of this.conf.peers) {
if (peer.type == "couchdb") {
const p = new PeerCouchDB(peer, this.dispatch.bind(this));
this.peers.push(p);
} else if (peer.type == "storage") {
const p = new PeerStorage(peer, this.dispatch.bind(this));
this.peers.push(p);
} else {
throw new Error(
`Unexpected Peer type: ${(peer as any)?.name} - ${
(peer as any)?.type
}`,
);
}
}
start() {
for (const p of this.peers) {
p.stop();
// Initialize couchdb peers FIRST and await them, then start storage peers.
// Otherwise a storage peer's offline scan can push to a couchdb peer before its
// DB managers are initialized (initializeDatabase), causing
// "Cannot read properties of undefined (reading 'getDBEntryMeta')".
(async () => {
for (const p of this.peers) {
if (p.config.type === "couchdb") {
await p.start().catch((e) => {
console.error(`[Hub] peer "${p.config.name}" start() failed:`, e);
});
}
this.peers = [];
for (const peer of this.conf.peers) {
if (peer.type == "couchdb") {
const p = new PeerCouchDB(peer, this.dispatch.bind(this));
this.peers.push(p);
} else if (peer.type == "storage") {
const p = new PeerStorage(peer, this.dispatch.bind(this));
this.peers.push(p);
} else {
throw new Error(`Unexpected Peer type: ${(peer as any)?.name} - ${(peer as any)?.type}`);
}
}
for (const p of this.peers) {
if (p.config.type !== "couchdb") {
p.start().catch((e) => {
console.error(`[Hub] peer "${p.config.name}" start() failed:`, e);
});
}
// Initialize couchdb peers FIRST and await them, then start storage peers.
// Otherwise a storage peer's offline scan can push to a couchdb peer before its
// DB managers are initialized (initializeDatabase), causing
// "Cannot read properties of undefined (reading 'getDBEntryMeta')".
(async () => {
for (const p of this.peers) {
if (p.config.type === "couchdb") {
await p.start().catch((e) => {
console.error(`[Hub] peer "${p.config.name}" start() failed:`, e);
});
}
}
for (const p of this.peers) {
if (p.config.type !== "couchdb") {
p.start().catch((e) => {
console.error(`[Hub] peer "${p.config.name}" start() failed:`, e);
});
}
}
})();
}
}
this.reconcileDiskDeletions().catch((e) =>
console.error("[Hub] reconcileDiskDeletions failed:", e)
);
})();
}

async dispatch(source: Peer, path: string, data: FileData | false) {
for (const peer of this.peers) {
if (peer !== source && (source.config.group ?? "") === (peer.config.group ?? "")) {
let ret = false;
if (data === false) {
ret = await peer.delete(path);
} else {
ret = await peer.put(path, data);
}
if (ret) {
// Logger(` ${data === false ? "-x->" : "--->"} ${peer.config.name} ${path} `)
} else {
// Logger(` ${peer.config.name} ignored ${path} `)
}
}
async dispatch(source: Peer, path: string, data: FileData | false) {
for (const peer of this.peers) {
if (
peer !== source &&
(source.config.group ?? "") === (peer.config.group ?? "")
) {
let ret = false;
if (data === false) {
ret = await peer.delete(path);
} else {
ret = await peer.put(path, data);
}
if (ret) {
// Logger(` ${data === false ? "-x->" : "--->"} ${peer.config.name} ${path} `)
} else {
// Logger(` ${peer.config.name} ignored ${path} `)
}
}
}
}
}
Loading