source: Klonkt/src/services/wake.js@ 7ccdced

main
Last change on this file since 7ccdced was 31288dc, checked in by Robin <roboburr@…>, 29 hours ago

Wakker worden: aankondigen en inhalen na echte afwezigheid

De wakker-helft van shaer-xzab (xzab.4 en .5), zoals Bart hem ontwierp: één
routine met twee helften, en wakker worden herkend aan een sprong in de
wandklok tussen twee ticks of aan het opstarten na lang uit -- niet aan elke
herstart, want npm herstart Klonkt bij elke opgeslagen wijziging.

services/wake.js. Het laatste teken van leven staat elke minuut in
app_settings (last_alive_at), zodat het een herstart overleeft. Meer dan een
half uur stilte is een terugkomst; dan:

.5 AANKONDIGEN: per site een actor-Update naar de gedeelde inboxes van de
volgers (deliverActorUpdate). Uit xzab.6: een binnenkomende POST is wat
Mastodon, Akkoma en Misskey weer naar ons laat bezorgen, en wat bij Pleroma
de controle van onze / start. Een half uur en niet langer, omdat Pleroma ons
al na één uitgeputte bezorging afschrijft.

.4 INHALEN: de outboxes van wie je volgt (alleen geaccepteerd), ondertekend,
vanaf het wegvallen. backfillFromOutbox kreeg daarvoor since; een post
zonder datum telt dan niet mee. Niet verder terug dan een week, hoogstens 40
posts per account en 200 accounts per site, na elkaar met een adempauze,
zodat een week weg geen storm wordt.

Eerst aankondigen, dan inhalen: dan bezorgen de anderen alweer terwijl wij
nog lezen. Een site die niet lukt, houdt de rest niet op.

test/wake.test.js: negen toetsen, het inhalen tegen een echte outbox op
127.0.0.1. Elf mutaties apart gedraaid, elk gevangen. Volledige suite 1371
groen.

Co-Authored-By: Claude Opus 5.5 <noreply@…>

  • Property mode set to 100644
File size: 6.0 KB
RevLine 
[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
25import db from '../config/database.js';
26import { getSetting, setSetting, apEnabled } from './SettingsService.js';
27
28export const AWAY_MS = 30 * 60 * 1000; // korter is een herstart of een korte slaap
29export 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.
32export const CATCHUP_MAX_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
33export const CATCHUP_PER_ACCOUNT = 40;
34export const CATCHUP_MAX_ACCOUNTS = 200;
35const CATCHUP_GAP_MS = 300; // beleefd: na elkaar, met een adempauze
36const 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 */
44export 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. */
51export 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 */
60export 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
80function 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. */
86export 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
92let _waking = false;
93async 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
111function readAlive() {
112 const v = Number(getSetting(ALIVE_KEY, ''));
113 return Number.isFinite(v) ? v : null;
114}
115function noteAlive(now) {
116 try { setSetting(ALIVE_KEY, String(now)); } catch { /* een teken minder is geen ramp */ }
117}
118
119let _timer = null;
120let _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. */
124export 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. */
133export 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. */
145export function _resetForTests() { _lastTick = null; if (_timer) { clearInterval(_timer); _timer = null; } }
Note: See TracBrowser for help on using the repository browser.