Voorstel van de gebruiker, en het tweede deel loste meer op dan een knop. Een verbinding die geweigerd wordt bleef eindeloos opnieuw proberen, en elke poging is een regel in de weigerlijst van de relay; zo liep die lijst tijdens deze sessie ook vol. - klop heet test op de opdrachtregel en Testen op de pagina. - Verbinden gaat uit zodra een test zegt dat de eigenaar geweigerd wordt, met de uitkomst in het rood naast de naam. Onbekend telt als toegestaan, want anders moet je eerst testen om iets te mogen en is dat zelf ook een poging. Blob schrijven gaat door dezelfde poort en schrijft dan lokaal verder. - Een lopende verbinding stopt zichzelf: na drie mislukte sockets wordt er precies een keer getest, en bij een weigering gaat de verbinding eruit. Is het geen weigering, dan blijft hij staan, want dan is opnieuw proberen juist goed. De melding in een dialoogvenster is eruit; de uitkomst staat naast de naam en in het log en die blijven staan. In de browser nagekeken: testen geeft 401, de regel verschijnt rood, en Verbinden is uitgegrijsd voor de geteste eigenaar terwijl de ongeteste hem houdt. Suite: 478 goed, 0 fout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
289 lines
11 KiB
JavaScript
289 lines
11 KiB
JavaScript
#!/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.
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
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);
|
|
},
|
|
// 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})` : ""}`);
|
|
},
|
|
});
|
|
|
|
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();
|
|
}
|
|
},
|
|
|
|
test: async ({ naam }) => {
|
|
eisRelay();
|
|
const { regel } = haalEigenaarOp(naam);
|
|
console.log(`Testen 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);
|
|
},
|
|
|
|
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 });
|
|
}
|
|
},
|
|
|
|
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);
|
|
}
|