2026-09-09 13:39:47 +02:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// De opdrachtregel van de testcliënt.
|
|
|
|
|
//
|
|
|
|
|
// Alles wat hier gebeurt kan ook in het bedieningsvlak (`ui`), en dat is de
|
|
|
|
|
// bedoeling: dit is dezelfde laag, alleen zonder browser. Handig om een proef te
|
|
|
|
|
// scripten en om te zien wat er misgaat als de pagina niet meewerkt.
|
|
|
|
|
//
|
|
|
|
|
// De opdrachten staan in argumenten.js, het register in register.js, en het
|
|
|
|
|
// praten met Evolu in client.js. Hier staat alleen de lijm plus wat er op het
|
|
|
|
|
// scherm komt.
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-09-09 14:15:09 +02:00
|
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
|
|
|
import { tmpdir } from 'node:os';
|
2026-09-09 13:39:47 +02:00
|
|
|
import { join } from 'node:path';
|
|
|
|
|
|
|
|
|
|
import { gebruiksaanwijzing, leesArgumenten } from './argumenten.js';
|
|
|
|
|
import { eigenaarUitMnemonic, nieuweEigenaar, openStore } from './client.js';
|
|
|
|
|
import { leesConfig } from './config.js';
|
|
|
|
|
import { klopAan } from './probe.js';
|
|
|
|
|
import { openRegister } from './register.js';
|
|
|
|
|
|
|
|
|
|
const config = leesConfig();
|
|
|
|
|
const register = openRegister(config.dataDir);
|
|
|
|
|
const databasesIn = join(config.dataDir, 'db');
|
|
|
|
|
|
|
|
|
|
/** Zoekt een eigenaar op of stopt met een nette melding. */
|
|
|
|
|
const haalEigenaarOp = (naam) => {
|
|
|
|
|
const regel = register.zoek(naam);
|
|
|
|
|
if (!regel) {
|
|
|
|
|
console.error(`Geen eigenaar "${naam}". \`lijst\` toont wie er wel zijn.`);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
return { regel, owner: eigenaarUitMnemonic(regel.mnemonic) };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Opent de opslag van een eigenaar, met of zonder relay. */
|
|
|
|
|
const openVoor = (regel, { metRelay }) =>
|
|
|
|
|
openStore({
|
|
|
|
|
directory: databasesIn,
|
|
|
|
|
owner: eigenaarUitMnemonic(regel.mnemonic),
|
|
|
|
|
relayUrl: metRelay ? config.relayUrl : null,
|
|
|
|
|
// Een defect in een worker betekent dat deze instantie stuk is, en de
|
|
|
|
|
// opdracht erna wacht dan op een antwoord dat nooit komt. Afkappen is hier
|
|
|
|
|
// dus vriendelijker dan doordraaien: een opdrachtregel die blijft hangen
|
|
|
|
|
// zegt niets, een foutmelding wel.
|
|
|
|
|
onDefect: (defect) => {
|
|
|
|
|
console.error('Defect uit een worker, de opdracht wordt afgebroken:');
|
|
|
|
|
console.error(defect);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
},
|
2026-09-09 14:15:09 +02:00
|
|
|
// Wat Evolu wérkelijk opent, en niet wat wij hem meegaven. De `OwnerId` in
|
|
|
|
|
// die URL is wat de relay ziet en dus wie er toegelaten of geweigerd wordt.
|
|
|
|
|
onWebSocket: (m) => {
|
|
|
|
|
console.log(` socket ${m.wat}: ${m.url}${m.detail ? ` (${m.detail})` : ""}`);
|
|
|
|
|
},
|
2026-09-09 13:39:47 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const eisRelay = () => {
|
|
|
|
|
if (config.relayUrl) return;
|
|
|
|
|
console.error('Geen relay-adres. Zet RELAY_URL in .env; zie .env.sample.');
|
|
|
|
|
process.exit(1);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const opdrachten = {
|
|
|
|
|
nieuw: async ({ naam }) => {
|
|
|
|
|
const owner = nieuweEigenaar();
|
|
|
|
|
const regel = register.voegToe({ naam, mnemonic: owner.mnemonic, ownerId: owner.id });
|
|
|
|
|
console.log(`Eigenaar "${regel.naam}" aangemaakt.`);
|
|
|
|
|
console.log(` OwnerId ${regel.ownerId}`);
|
|
|
|
|
console.log('');
|
|
|
|
|
console.log('Dat OwnerId is wat je op de statuspagina van de app terugziet. De mnemonic staat');
|
|
|
|
|
console.log(`in ${register.pad} en wordt hier niet afgedrukt.`);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
lijst: async () => {
|
|
|
|
|
const eigenaars = register.lijst();
|
|
|
|
|
if (eigenaars.length === 0) {
|
|
|
|
|
console.log('Nog geen eigenaars. `nieuw <naam>` maakt er een.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const breedte = Math.max(...eigenaars.map((e) => e.naam.length), 4);
|
|
|
|
|
console.log(`${'naam'.padEnd(breedte)} OwnerId`);
|
|
|
|
|
for (const eigenaar of eigenaars) {
|
|
|
|
|
console.log(`${eigenaar.naam.padEnd(breedte)} ${eigenaar.ownerId}`);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
schrijf: async ({ naam, label }, vlaggen) => {
|
|
|
|
|
const { regel } = haalEigenaarOp(naam);
|
|
|
|
|
const bytes = Number.parseInt(vlaggen.bytes ?? '64', 10);
|
|
|
|
|
if (!Number.isInteger(bytes) || bytes < 1) {
|
|
|
|
|
console.error(`--bytes moet een positief geheel getal zijn, niet "${vlaggen.bytes}".`);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Een herkenbaar patroon en geen willekeur: bij het teruglezen wil je kunnen
|
|
|
|
|
// zien dát het jouw bytes zijn, en niet alleen hoeveel het er waren.
|
|
|
|
|
const body = new Uint8Array(bytes);
|
|
|
|
|
for (let i = 0; i < bytes; i += 1) body[i] = i % 256;
|
|
|
|
|
|
|
|
|
|
// Standaard mét relay, want schrijven zonder verbinding zegt niets over de
|
|
|
|
|
// relay en dat is waarvoor dit gereedschap bestaat. Ontbreekt het adres, dan
|
|
|
|
|
// is dat een fout en geen stille lokale schrijfactie: anders denk je dat je
|
|
|
|
|
// iets beproefd hebt. Met `--lokaal` kies je er expliciet voor, en dat is
|
|
|
|
|
// precies wat de proef "toelaten en opnieuw verbinden" nodig heeft.
|
|
|
|
|
const lokaal = vlaggen.lokaal === true || vlaggen.lokaal === 'true';
|
|
|
|
|
if (!lokaal) eisRelay();
|
|
|
|
|
|
|
|
|
|
const store = await openVoor(regel, { metRelay: !lokaal });
|
|
|
|
|
try {
|
|
|
|
|
const geschreven = await store.schrijf(label, body);
|
|
|
|
|
console.log(`Geschreven: ${geschreven.id} (${bytes} bytes, label "${label}")`);
|
|
|
|
|
if (lokaal) {
|
|
|
|
|
console.log('Lokaal, dus niet naar de relay gestuurd.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
console.log(`Verbonden met ${store.transportUrl}`);
|
|
|
|
|
// De synchronisatie loopt asynchroon. Even wachten is hier eerlijker dan
|
|
|
|
|
// meteen afsluiten en doen alsof het weg is. Wat er daarna op de relay
|
|
|
|
|
// staat, zegt de statuspagina van de app en niet deze uitdraai.
|
|
|
|
|
await new Promise((klaar) => {
|
|
|
|
|
setTimeout(klaar, 2000);
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
await store.sluit();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
lees: async ({ naam }) => {
|
|
|
|
|
const { regel } = haalEigenaarOp(naam);
|
|
|
|
|
const store = await openVoor(regel, { metRelay: false });
|
|
|
|
|
try {
|
|
|
|
|
const rijen = await store.lees();
|
|
|
|
|
if (rijen.length === 0) {
|
|
|
|
|
console.log(`"${naam}" heeft nog geen blobs.`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
console.log(`${rijen.length} blob(s) voor "${naam}":`);
|
|
|
|
|
for (const rij of rijen) {
|
|
|
|
|
console.log(` ${rij.id} ${String(rij.body?.length ?? 0).padStart(8)} bytes ${rij.label}`);
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
await store.sluit();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
klop: async ({ naam }) => {
|
|
|
|
|
eisRelay();
|
|
|
|
|
const { regel } = haalEigenaarOp(naam);
|
|
|
|
|
console.log(`Aankloppen bij ${config.relayUrl} als ${regel.ownerId}`);
|
|
|
|
|
const uitkomst = await klopAan(config.relayUrl, regel.ownerId);
|
|
|
|
|
console.log(` status ${uitkomst.status ?? '-'}`);
|
|
|
|
|
console.log(` uitkomst ${uitkomst.uitleg}`);
|
|
|
|
|
if (!uitkomst.geaccepteerd && uitkomst.status === 401) {
|
|
|
|
|
console.log('');
|
|
|
|
|
console.log('Zet op de statuspagina van de app het venster voor nieuwe eigenaars open,');
|
|
|
|
|
console.log('of laat deze eigenaar daar alsnog toe.');
|
|
|
|
|
}
|
|
|
|
|
process.exit(uitkomst.geaccepteerd ? 0 : 1);
|
|
|
|
|
},
|
|
|
|
|
|
2026-09-09 14:15:09 +02:00
|
|
|
spiegel: async ({ naam }, vlaggen) => {
|
|
|
|
|
// De enige proef die van de cliëntkant kan bewijzen dat er werkelijk iets op
|
|
|
|
|
// de relay staat. Dezelfde eigenaar, een lege database ernaast: alles wat
|
|
|
|
|
// hier binnenkomt heeft de heenreis en de terugreis gemaakt.
|
|
|
|
|
//
|
|
|
|
|
// Waarom dit nodig is: de cliënt is local-first, dus `lees` toont je altijd
|
|
|
|
|
// je eigen rijen, ook als de relay ze nooit gezien heeft. Wat de relay
|
|
|
|
|
// bewaart is van hier niet te zien; dit is de omweg eromheen.
|
|
|
|
|
eisRelay();
|
|
|
|
|
const { regel } = haalEigenaarOp(naam);
|
|
|
|
|
const seconden = Number.parseInt(vlaggen.wacht ?? '15', 10);
|
|
|
|
|
|
|
|
|
|
const tijdelijk = mkdtempSync(join(tmpdir(), 'relay-client-spiegel-'));
|
|
|
|
|
console.log(`"${naam}" opnieuw opgehaald in een lege database.`);
|
|
|
|
|
console.log(` ${tijdelijk}`);
|
|
|
|
|
console.log(` ${config.relayUrl}`);
|
|
|
|
|
console.log('');
|
|
|
|
|
|
|
|
|
|
const store = await openStore({
|
|
|
|
|
directory: tijdelijk,
|
|
|
|
|
owner: eigenaarUitMnemonic(regel.mnemonic),
|
|
|
|
|
relayUrl: config.relayUrl,
|
|
|
|
|
onWebSocket: (m) => { console.log(` socket ${m.wat}: ${m.url}${m.detail ? ` (${m.detail})` : ""}`); },
|
|
|
|
|
onDefect: (defect) => {
|
|
|
|
|
console.error('Defect uit een worker, de opdracht wordt afgebroken:');
|
|
|
|
|
console.error(defect);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Peilen tot het aantal een paar rondes stil blijft. Er is geen signaal
|
|
|
|
|
// "hier ben ik klaar": Evolu synchroniseert en blijft synchroniseren.
|
|
|
|
|
let vorige = -1;
|
|
|
|
|
let stil = 0;
|
|
|
|
|
for (let ronde = 0; ronde < seconden * 2; ronde += 1) {
|
|
|
|
|
await new Promise((klaar) => {
|
|
|
|
|
setTimeout(klaar, 500);
|
|
|
|
|
});
|
|
|
|
|
const nu = (await store.lees()).length;
|
|
|
|
|
if (nu === vorige && nu > 0) {
|
|
|
|
|
stil += 1;
|
|
|
|
|
if (stil >= 6) break;
|
|
|
|
|
} else {
|
|
|
|
|
stil = 0;
|
|
|
|
|
}
|
|
|
|
|
vorige = nu;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rijen = await store.lees();
|
|
|
|
|
if (rijen.length === 0) {
|
|
|
|
|
console.log('Er kwam niets terug.');
|
|
|
|
|
console.log('');
|
|
|
|
|
console.log('Dat kan twee dingen betekenen en de cliënt kan ze niet uit elkaar houden:');
|
|
|
|
|
console.log('de relay heeft niets van deze eigenaar, of hij was er nog niet klaar mee.');
|
|
|
|
|
console.log('Probeer `--wacht 60` en kijk anders op de statuspagina van de app.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
console.log(`${rijen.length} blob(s) terug van de relay:`);
|
|
|
|
|
for (const rij of rijen) {
|
|
|
|
|
console.log(` ${rij.id} ${String(rij.body?.length ?? 0).padStart(8)} bytes ${rij.label}`);
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
await store.sluit();
|
|
|
|
|
// Eigen tijdelijke map, dus die ruimt deze opdracht zelf op. Er zit een
|
|
|
|
|
// database van een echte eigenaar in.
|
|
|
|
|
rmSync(tijdelijk, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2026-09-09 13:39:47 +02:00
|
|
|
sync: async ({ naam }) => {
|
|
|
|
|
eisRelay();
|
|
|
|
|
const { regel } = haalEigenaarOp(naam);
|
|
|
|
|
const store = await openVoor(regel, { metRelay: true });
|
|
|
|
|
console.log(`"${naam}" verbonden met ${store.transportUrl}`);
|
|
|
|
|
console.log('Ctrl-C om te stoppen.');
|
|
|
|
|
|
|
|
|
|
const stop = async () => {
|
|
|
|
|
console.log('');
|
|
|
|
|
console.log('Afsluiten...');
|
|
|
|
|
await store.sluit();
|
|
|
|
|
process.exit(0);
|
|
|
|
|
};
|
|
|
|
|
process.on('SIGINT', () => {
|
|
|
|
|
void stop();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Blijven draaien. Een lopende verbinding is precies wat de proef "blokkeren
|
|
|
|
|
// tijdens een lopende verbinding" nodig heeft; zie PLAN.md §4c.
|
|
|
|
|
await new Promise(() => {});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
vergeet: async ({ naam }) => {
|
|
|
|
|
const weg = register.vergeet(naam);
|
|
|
|
|
console.log(`"${weg.naam}" is uit het register.`);
|
|
|
|
|
console.log('De lokale database blijft staan en op de relay verandert er niets.');
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
ui: async (_waarden, vlaggen) => {
|
|
|
|
|
const { startUi } = await import('./ui.js');
|
|
|
|
|
const poort = vlaggen.poort ? Number.parseInt(vlaggen.poort, 10) : config.uiPoort;
|
|
|
|
|
await startUi({ config: { ...config, uiPoort: poort }, register, databasesIn });
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const { fout, opdracht, waarden, vlaggen } = leesArgumenten(process.argv.slice(2));
|
|
|
|
|
|
|
|
|
|
if (fout) {
|
|
|
|
|
console.error(fout);
|
|
|
|
|
console.error('');
|
|
|
|
|
console.error(gebruiksaanwijzing());
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!opdracht) {
|
|
|
|
|
console.log(gebruiksaanwijzing());
|
|
|
|
|
process.exit(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await opdrachten[opdracht](waarden, vlaggen);
|
|
|
|
|
} catch (oorzaak) {
|
|
|
|
|
console.error(oorzaak.message);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|