| [31288dc] | 1 | /**
|
|---|
| 2 | * Wakker worden: een Klonkt die niet altijd aan staat (shaer-xzab.4 en .5).
|
|---|
| 3 | *
|
|---|
| 4 | * Een Klonkt op een Mac thuis slaapt, gaat de trein in, staat een weekend uit.
|
|---|
| 5 | * Bij het terugkomen doet hij twee dingen, en die doen elk iets anders -- dat is
|
|---|
| 6 | * uitgezocht in xzab.6, in de broncode van Mastodon, Akkoma, Pleroma, Misskey
|
|---|
| 7 | * en GoToSocial:
|
|---|
| 8 | *
|
|---|
| 9 | * AANKONDIGEN (.5): een actor-Update naar de gedeelde inboxes van de volgers.
|
|---|
| 10 | * Een binnenkomende POST is wat Mastodon, Akkoma en Misskey weer naar ons
|
|---|
| 11 | * laat bezorgen (Mastodon schrijft een domein af na mislukkingen op zeven
|
|---|
| 12 | * dagen; Pleroma al na één uitgeputte bezorging, en start bij een
|
|---|
| 13 | * binnenkomende activiteit een controle van onze /). Ondertekende GETs
|
|---|
| 14 | * zetten daar niets terug.
|
|---|
| 15 | * INHALEN (.4): de outboxes van wie je volgt nalopen, vanaf het moment dat we
|
|---|
| 16 | * wegvielen. Het enige wat gemiste posts terugbrengt: GoToSocial gooit een
|
|---|
| 17 | * bezorging na een paar minuten weg, Mastodon na twee à drie dagen.
|
|---|
| 18 | *
|
|---|
| 19 | * WAKKER WORDEN HERKENNEN, en niet bij elke herstart (npm herstart Klonkt bij
|
|---|
| 20 | * elke opgeslagen wijziging): een sprong in de wandklok tussen twee ticks, of
|
|---|
| 21 | * het opstarten na langer dan AWAY_MS sinds het laatste teken van leven. Dat
|
|---|
| 22 | * teken staat in app_settings, zodat het een herstart overleeft.
|
|---|
| 23 | */
|
|---|
| 24 |
|
|---|
| 25 | import db from '../config/database.js';
|
|---|
| 26 | import { getSetting, setSetting, apEnabled } from './SettingsService.js';
|
|---|
| 27 |
|
|---|
| 28 | export const AWAY_MS = 30 * 60 * 1000; // korter is een herstart of een korte slaap
|
|---|
| 29 | export const TICK_MS = 60 * 1000;
|
|---|
| 30 | // Inhalen: niet verder terug dan een week, en per account een maximum, zodat
|
|---|
| 31 | // een week weg geen storm van verzoeken wordt.
|
|---|
| 32 | export const CATCHUP_MAX_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|---|
| 33 | export const CATCHUP_PER_ACCOUNT = 40;
|
|---|
| 34 | export const CATCHUP_MAX_ACCOUNTS = 200;
|
|---|
| 35 | const CATCHUP_GAP_MS = 300; // beleefd: na elkaar, met een adempauze
|
|---|
| 36 | const ALIVE_KEY = 'last_alive_at';
|
|---|
| 37 |
|
|---|
| 38 | /**
|
|---|
| 39 | * Is dit wakker worden, en sinds wanneer waren we weg? Puur.
|
|---|
| 40 | * `lastAlive` is het laatste teken van leven (ms), `now` nu. Null als het geen
|
|---|
| 41 | * afwezigheid was (of als er nog nooit een teken was: een eerste start is geen
|
|---|
| 42 | * terugkomst).
|
|---|
| 43 | */
|
|---|
| 44 | export function absence(lastAlive, now) {
|
|---|
| 45 | if (!Number.isFinite(lastAlive) || lastAlive <= 0) return null;
|
|---|
| 46 | const away = now - lastAlive;
|
|---|
| 47 | return away >= AWAY_MS ? { since: lastAlive, awayMs: away } : null;
|
|---|
| 48 | }
|
|---|
| 49 |
|
|---|
| 50 | /** Vanaf wanneer inhalen: het wegvallen, maar nooit verder terug dan het venster. */
|
|---|
| 51 | export function catchupSince(since, now) {
|
|---|
| 52 | return Math.max(since, now - CATCHUP_MAX_WINDOW_MS);
|
|---|
| 53 | }
|
|---|
| 54 |
|
|---|
| 55 | /**
|
|---|
| 56 | * De wakker-routine zelf, met haar afhankelijkheden van buiten, zodat ze
|
|---|
| 57 | * zonder netwerk te toetsen is. Eerst aankondigen (dan bezorgen de anderen weer
|
|---|
| 58 | * terwijl wij inhalen), dan inhalen, account voor account.
|
|---|
| 59 | */
|
|---|
| 60 | export async function runWake({ since, now = Date.now() }, deps) {
|
|---|
| 61 | const { sites, deliverActorUpdate, following, backfill, pause = (ms) => new Promise((r) => setTimeout(r, ms)) } = deps;
|
|---|
| 62 | const from = catchupSince(since, now);
|
|---|
| 63 | let announced = 0, fetched = 0, added = 0;
|
|---|
| 64 | for (const site of sites()) {
|
|---|
| 65 | try { await deliverActorUpdate(site); announced++; } catch { /* een site die niet lukt, houdt de rest niet op */ }
|
|---|
| 66 | }
|
|---|
| 67 | for (const site of sites()) {
|
|---|
| 68 | const accounts = following(site.slug).slice(0, CATCHUP_MAX_ACCOUNTS);
|
|---|
| 69 | for (const actorUri of accounts) {
|
|---|
| 70 | try { added += (await backfill(site.slug, actorUri, CATCHUP_PER_ACCOUNT, { since: from })) || 0; } catch { /* best-effort */ }
|
|---|
| 71 | fetched++;
|
|---|
| 72 | await pause(CATCHUP_GAP_MS);
|
|---|
| 73 | }
|
|---|
| 74 | }
|
|---|
| 75 | return { announced, fetched, added };
|
|---|
| 76 | }
|
|---|
| 77 |
|
|---|
| 78 | // ── Het echte werk: welke sites, wie ze volgen ───────────────────────────────
|
|---|
| 79 |
|
|---|
| 80 | function realSites() {
|
|---|
| 81 | try { return db.prepare('SELECT * FROM sites').all(); } catch { return []; }
|
|---|
| 82 | }
|
|---|
| 83 | /** Wie deze site volgt en waar dat rond is: alleen GEACCEPTEERD, want een
|
|---|
| 84 | * wachtend volgverzoek geeft (nog) geen recht op de besloten posts, en de
|
|---|
| 85 | * outbox van iemand die nee zei hoort niet in je stroom. */
|
|---|
| 86 | export function acceptedFollowing(slug) {
|
|---|
| 87 | try {
|
|---|
| 88 | return db.prepare("SELECT actor_uri FROM ap_following WHERE slug = ? AND status = 'accepted' ORDER BY id").all(slug).map((r) => r.actor_uri);
|
|---|
| 89 | } catch { return []; }
|
|---|
| 90 | }
|
|---|
| 91 |
|
|---|
| 92 | let _waking = false;
|
|---|
| 93 | async function wake(since, now) {
|
|---|
| 94 | if (_waking) return; // één terugkomst tegelijk
|
|---|
| 95 | _waking = true;
|
|---|
| 96 | try {
|
|---|
| 97 | if (!apEnabled()) return;
|
|---|
| 98 | const AP = await import('./ActivityPubService.js');
|
|---|
| 99 | const r = await runWake({ since, now }, {
|
|---|
| 100 | sites: realSites,
|
|---|
| 101 | deliverActorUpdate: AP.deliverActorUpdate,
|
|---|
| 102 | following: acceptedFollowing,
|
|---|
| 103 | backfill: AP.backfillFromOutbox,
|
|---|
| 104 | });
|
|---|
| 105 | console.log(`[wake] back after ${Math.round((now - since) / 60000)} min: Update from ${r.announced} site(s), caught up ${r.fetched} account(s), +${r.added} post(s)`);
|
|---|
| 106 | } catch (e) {
|
|---|
| 107 | console.warn('[wake]', e && e.message);
|
|---|
| 108 | } finally { _waking = false; }
|
|---|
| 109 | }
|
|---|
| 110 |
|
|---|
| 111 | function readAlive() {
|
|---|
| 112 | const v = Number(getSetting(ALIVE_KEY, ''));
|
|---|
| 113 | return Number.isFinite(v) ? v : null;
|
|---|
| 114 | }
|
|---|
| 115 | function noteAlive(now) {
|
|---|
| 116 | try { setSetting(ALIVE_KEY, String(now)); } catch { /* een teken minder is geen ramp */ }
|
|---|
| 117 | }
|
|---|
| 118 |
|
|---|
| 119 | let _timer = null;
|
|---|
| 120 | let _lastTick = null;
|
|---|
| 121 |
|
|---|
| 122 | /** Eén tik: is er sinds de vorige tik een gat? Dan wakker worden. Los van de
|
|---|
| 123 | * timer, zodat het te toetsen is. */
|
|---|
| 124 | export function tick(now = Date.now(), onWake = wake) {
|
|---|
| 125 | const gat = absence(_lastTick, now);
|
|---|
| 126 | _lastTick = now;
|
|---|
| 127 | noteAlive(now);
|
|---|
| 128 | if (gat) onWake(gat.since, now);
|
|---|
| 129 | return gat;
|
|---|
| 130 | }
|
|---|
| 131 |
|
|---|
| 132 | /** Bij het opstarten: hoe lang stonden we uit? Daarna elke minuut een tik. */
|
|---|
| 133 | export function startWakeWatch(onWake = wake) {
|
|---|
| 134 | if (_timer) return;
|
|---|
| 135 | const now = Date.now();
|
|---|
| 136 | const opStart = absence(readAlive(), now);
|
|---|
| 137 | _lastTick = now;
|
|---|
| 138 | noteAlive(now);
|
|---|
| 139 | if (opStart) onWake(opStart.since, now);
|
|---|
| 140 | _timer = setInterval(() => { tick(Date.now(), onWake); }, TICK_MS);
|
|---|
| 141 | if (_timer.unref) _timer.unref();
|
|---|
| 142 | }
|
|---|
| 143 |
|
|---|
| 144 | /** Voor de toetsen: de tik-staat terugzetten. */
|
|---|
| 145 | export function _resetForTests() { _lastTick = null; if (_timer) { clearInterval(_timer); _timer = null; } }
|
|---|