Changeset 7842ca1 in Klonkt
- Timestamp:
- 08/06/2026 09:33:58 AM (5 weeks ago)
- Branches:
- main
- Children:
- 25f2e64
- Parents:
- 6bc5ebd
- git-author:
- Robin <roboburr@…> (08/06/2026 09:33:57 AM)
- git-committer:
- Claude (agent) <aiclaude@…> (08/06/2026 09:33:58 AM)
- Files:
-
- 4 edited
-
scripts/backfill-reactions.mjs (modified) (2 diffs)
-
src/server.js (modified) (2 diffs)
-
src/services/ActivityPubService.js (modified) (2 diffs)
-
test/reactions-characterization.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
scripts/backfill-reactions.mjs
r6bc5ebd r7842ca1 1 1 #!/usr/bin/env node 2 2 // 3 // Backfill: reacties uit de afgeleide kolommen naar de tussentabel(shaer-9e9).3 // Reactie-migratie handmatig draaien (shaer-9e9). 4 4 // 5 // "Heb ik hierop gereageerd" stond in Klonkt op twee plekken die door 6 // verschillende routes werden gevuld: ap_timeline.liked/boosted (web-tijdlijn) 7 // en ap_my_reactions (interact-pagina). Sinds fase 1 schrijft setReaction ze 8 // allebei, maar alles van VOOR die wijziging staat nog maar in een van de twee. 9 // 10 // Dit script vult de tussentabel aan met wat alleen in de kolommen staat. Het 11 // moet draaien VOORDAT de lezers naar de tussentabel wijzen (fase 2), anders 12 // verdwijnen die reacties uit beeld -- stil, want een ontbrekende rij is geen 13 // fout. 5 // Normaal hoef je dit NIET: migrateReactions() draait bij boot, één keer per 6 // REACTIONS_MIGRATION_VERSION-bump, net als de self-heal. Dit script is er om 7 // vooraf te kijken wat er zou gebeuren, of om het gericht op één instance te 8 // forceren. 14 9 // 15 10 // node scripts/backfill-reactions.mjs --dry-run 16 // node scripts/backfill-reactions.mjs 11 // node scripts/backfill-reactions.mjs # respecteert de versievlag 12 // node scripts/backfill-reactions.mjs --force # ook als de vlag al staat 17 13 // 18 // Veilig om opnieuw te draaien: INSERT OR IGNORE plus de UNIQUE op19 // (site_slug, target_uri, kind) maakt het idempotent.14 // Bewust een schil om dezelfde functie die bij boot draait: twee implementaties 15 // van een migratie lopen uiteen, en dan repareert de ene wat de andere niet ziet. 20 16 // 21 // Wat het NIET kan: de oorspronkelijke reactiedatum herstellen. Wanneer je 22 // reageerde is nergens vastgelegd, dus de backfill-rijen krijgen de datum van 23 // nu. Dat is geen verlies dat te repareren valt, wel iets om te weten als je 24 // ooit op created_at gaat sorteren. 17 // Wat het doet, en waarom allebei nodig is: 18 // 19 // HERSLEUTELEN De oude interact-route bewaarde de URI waarmee je binnenkwam, 20 // en de bookmarklet geeft de permalink door. Sinds de reacties 21 // op de canonieke object-URI gezocht worden, zouden die rijen 22 // wees zijn. De created_at reist mee. 23 // AANVULLEN Alles wat op oude code via de Krant is gegeven staat alleen in 24 // ap_timeline.liked/boosted. Zonder deze stap toont het als 25 // niet-gereageerd -- en klikt iemand opnieuw, met een tweede 26 // Like de fediverse in als gevolg. 27 // 28 // Wat het NIET kan: bij AANVULLEN de oorspronkelijke reactiedatum herstellen. 29 // Wanneer je reageerde is nergens vastgelegd, dus die rijen krijgen de datum van 30 // nu. Bij hersleutelen blijft de datum wel behouden. 25 31 26 32 import db from '../src/config/database.js'; 33 import { migrateReactions } from '../src/services/ActivityPubService.js'; 27 34 28 35 const dryRun = process.argv.includes('--dry-run'); 36 const force = process.argv.includes('--force'); 29 37 30 const scheef = (kind, kolom) => db.prepare(` 31 SELECT t.slug, t.id FROM ap_timeline t 32 WHERE t.${kolom} = 1 33 AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r 34 WHERE r.site_slug = t.slug AND r.target_uri = t.id AND r.kind = ?) 35 `).all(kind); 36 37 const voor = { 38 const meet = () => ({ 38 39 tussentabel: db.prepare('SELECT COUNT(*) AS n FROM ap_my_reactions').get().n, 39 40 liked: db.prepare('SELECT COUNT(*) AS n FROM ap_timeline WHERE liked = 1').get().n, 40 41 boosted: db.prepare('SELECT COUNT(*) AS n FROM ap_timeline WHERE boosted = 1').get().n, 41 };42 const teDoen = { like: scheef('like', 'liked'), boost: scheef('boost', 'boosted') };43 44 console.log('vooraf :', JSON.stringify(voor));45 console.log('aan te vullen: like =', teDoen.like.length, ', boost =', teDoen.boost.length);46 47 if (dryRun) {48 for (const [kind, rijen] of Object.entries(teDoen)) {49 for (const r of rijen.slice(0, 10)) console.log(` [dry-run] ${kind} ${r.slug} ${r.id}`);50 if (rijen.length > 10) console.log(` ... en nog ${rijen.length - 10}`);51 }52 console.log('\n--dry-run: niets geschreven.');53 process.exit(0);54 }55 56 const ins = db.prepare('INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind) VALUES (?,?,?)');57 let toegevoegd = 0;58 db.transaction(() => {59 for (const [kind, rijen] of Object.entries(teDoen)) {60 for (const r of rijen) toegevoegd += ins.run(r.slug, r.id, kind).changes;61 }62 })();63 64 const na = {65 tussentabel: db.prepare('SELECT COUNT(*) AS n FROM ap_my_reactions').get().n,66 42 scheef: db.prepare(` 67 43 SELECT COUNT(*) AS n FROM ap_timeline t … … 69 45 AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r 70 46 WHERE r.site_slug = t.slug AND r.target_uri = t.id)`).get().n, 71 }; 47 wees: db.prepare(` 48 SELECT COUNT(*) AS n FROM ap_my_reactions r 49 WHERE NOT EXISTS (SELECT 1 FROM ap_timeline t 50 WHERE t.slug = r.site_slug AND t.id = r.target_uri)`).get().n, 51 }); 72 52 73 cons ole.log('toegevoegd:', toegevoegd);74 console.log(' achteraf :', JSON.stringify(na));53 const voor = meet(); 54 console.log('vooraf :', JSON.stringify(voor)); 75 55 76 // De controle die telt: is er nog een vlag zonder tegenhanger? Zo ja, dan is de 77 // tussentabel nog niet veilig als bron en mag fase 2 niet. 56 const uit = migrateReactions({ dryRun, force: force || dryRun }); 57 if (uit.overgeslagen) { 58 console.log('\novergeslagen: de versievlag staat al. Gebruik --force om toch te draaien.'); 59 process.exit(0); 60 } 61 if (dryRun) { 62 console.log(`\n--dry-run: zou ${uit.hersleuteld} rij(en) hersleutelen en ${uit.aangevuld} aanvullen. Niets geschreven.`); 63 process.exit(0); 64 } 65 66 const na = meet(); 67 console.log('hersleuteld:', uit.hersleuteld, ' aangevuld:', uit.aangevuld); 68 console.log('achteraf :', JSON.stringify(na)); 69 70 // De twee controles die tellen. Blijft er een vlag zonder tegenhanger, dan is de 71 // tussentabel niet compleet en tonen reacties als niet-gegeven. Blijft er een 72 // tussentabel-rij zonder tijdlijnrij, dan is die op iets buiten je tijdlijn 73 // gericht (legitiem) OF nog op een permalink (niet legitiem) -- vandaar de 74 // waarschuwing in plaats van een fout. 78 75 if (na.scheef !== 0) { 79 console.error(`\n LET OP: nog ${na.scheef} rij(en) met een vlag zonder tegenhanger. Fase 2 mag NIET.`);76 console.error(`\nFOUT: nog ${na.scheef} rij(en) met een vlag zonder tegenhanger.`); 80 77 process.exit(1); 81 78 } 82 if ( toegevoegd !== teDoen.like.length + teDoen.boost.length) {83 console.error('\n LET OP: het aantal toegevoegde rijen wijkt af van de meting vooraf.');79 if (na.wees > voor.wees) { 80 console.error('\nFOUT: er zijn tussentabel-rijen bijgekomen die nergens op slaan.'); 84 81 process.exit(1); 85 82 } 86 console.log('\nOK: elke vlag heeft nu een tegenhanger in de tussentabel.'); 83 if (na.wees) { 84 console.warn(`\nLET OP: ${na.wees} tussentabel-rij(en) zonder tijdlijnrij. Dat mag (een reactie op iets\n` 85 + 'buiten je tijdlijn), maar controleer of er geen permalinks tussen zitten van een post die je\n' 86 + 'wel kent -- die zouden hersleuteld moeten zijn.'); 87 } 88 console.log('\nOK: elke vlag heeft een tegenhanger in de tussentabel.'); -
src/server.js
r6bc5ebd r7842ca1 65 65 import apRoutes from './routes/activitypub.js'; 66 66 import oauthRoutes from './routes/oauth.js'; 67 import { apWants, startDeliveryWorker, selfHealTimeline } from './services/ActivityPubService.js';67 import { apWants, startDeliveryWorker, selfHealTimeline, migrateReactions } from './services/ActivityPubService.js'; 68 68 69 69 // SESSION_SECRET: use the env var if set. Otherwise auto-generate a strong one … … 197 197 startScheduler(); // release planning: publish scheduled posts when publish_at is reached 198 198 startDeliveryWorker(); // retry failed fediverse deliveries with backoff 199 // Once per REACTIONS_MIGRATION_VERSION bump: reacties naar de tussentabel, onder 200 // de canonieke object-URI. Moet VOOR het serveren, want vanaf nu leest de code 201 // die tabel -- draait hij niet, dan tonen oude likes als niet-gegeven. 202 migrateReactions(); 199 203 selfHealTimeline(); // once per SELFHEAL_VERSION bump: re-sync the fediverse cache (covers/edits) after a drastic update 200 204 -
src/services/ActivityPubService.js
r6bc5ebd r7842ca1 3438 3438 * werk voor fase 2, mét datamigratie. 3439 3439 */ 3440 // Reactie-migratie (shaer-9e9). Draait bij boot, EEN keer per bump, net als 3441 // selfHealTimeline. Bewust automatisch: klonkt-update tilt een hele vloot in een 3442 // stap naar nieuwe code, en een handmatig script per instance wordt vergeten -- 3443 // terwijl het falen stil is (een reactie die niemand meer ziet geeft geen fout). 3444 const REACTIONS_MIGRATION_VERSION = 1; 3445 3446 /** 3447 * Brengt alle reacties naar de tussentabel, onder de canonieke object-URI. 3448 * 3449 * Twee stappen, en ze zijn allebei nodig: 3450 * 3451 * 1. HERSLEUTELEN. De oude interact-route bewaarde de URI waarmee je binnenkwam 3452 * en de bookmarklet geeft window.location.href door, dus de permalink. Sinds 3453 * canonicalReactionUri wordt er op de object-URI gezocht, waardoor die rijen 3454 * wees zouden zijn. De created_at reist mee: bij hersleutelen weten we 3455 * wanneer je reageerde, bij aanvullen niet. 3456 * 2. AANVULLEN vanuit de afgeleide kolommen. Alles wat op oude code via de 3457 * Krant is gegeven staat alleen daar; zonder deze stap toont het als 3458 * niet-gereageerd en klikt een gebruiker opnieuw -- met een tweede Like de 3459 * fediverse in als gevolg. 3460 * 3461 * Idempotent. Geeft terug wat er gebeurd is, zodat het script het kan tonen. 3462 */ 3463 export function migrateReactions(opts = {}) { 3464 const uit = { hersleuteld: 0, aangevuld: 0, overgeslagen: false }; 3465 try { 3466 if (!opts.force) { 3467 const r = db.prepare('SELECT value FROM app_settings WHERE key = ?').get('reactions_migration_version'); 3468 const cur = r ? (parseInt(r.value, 10) || 0) : 0; 3469 if (cur >= REACTIONS_MIGRATION_VERSION) { uit.overgeslagen = true; return uit; } 3470 } 3471 } catch { return uit; } // geen app_settings → deze database is te oud om aan te raken 3472 3473 // Een rij die NIET op een tijdlijn-id staat maar wel op een tijdlijn-url. 3474 const wees = ` 3475 FROM ap_my_reactions r JOIN ap_timeline t ON t.slug = r.site_slug AND t.url = r.target_uri 3476 WHERE NOT EXISTS (SELECT 1 FROM ap_timeline t2 WHERE t2.slug = r.site_slug AND t2.id = r.target_uri)`; 3477 const scheef = (kind, kolom) => ` 3478 FROM ap_timeline t 3479 WHERE t.${kolom} = 1 3480 AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r 3481 WHERE r.site_slug = t.slug AND r.target_uri = t.id AND r.kind = '${kind}')`; 3482 3483 if (opts.dryRun) { 3484 try { 3485 uit.hersleuteld = db.prepare(`SELECT COUNT(*) AS n ${wees}`).get().n; 3486 uit.aangevuld = db.prepare(`SELECT COUNT(*) AS n ${scheef('like', 'liked')}`).get().n 3487 + db.prepare(`SELECT COUNT(*) AS n ${scheef('boost', 'boosted')}`).get().n; 3488 } catch { /* laat de nullen staan */ } 3489 return uit; 3490 } 3491 3492 try { 3493 db.transaction(() => { 3494 // 1. Hersleutelen: eerst de canonieke variant erbij, dan de permalink weg. 3495 // In die volgorde, zodat een onderbreking hooguit een dubbele rij 3496 // oplevert en nooit een verdwenen reactie. 3497 uit.hersleuteld = db.prepare(` 3498 INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind, created_at) 3499 SELECT r.site_slug, t.id, r.kind, r.created_at ${wees}`).run().changes; 3500 db.prepare(`DELETE FROM ap_my_reactions WHERE rowid IN (SELECT r.rowid ${wees})`).run(); 3501 3502 // 2. Aanvullen vanuit de kolommen. 3503 for (const [kind, kolom] of [['like', 'liked'], ['boost', 'boosted']]) { 3504 uit.aangevuld += db.prepare(` 3505 INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind) 3506 SELECT t.slug, t.id, '${kind}' ${scheef(kind, kolom)}`).run().changes; 3507 } 3508 })(); 3509 if (uit.hersleuteld || uit.aangevuld) { 3510 console.log(`[AP] reactie-migratie v${REACTIONS_MIGRATION_VERSION}: ${uit.hersleuteld} hersleuteld, ${uit.aangevuld} aangevuld`); 3511 } 3512 if (!opts.force) { 3513 db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)') 3514 .run('reactions_migration_version', String(REACTIONS_MIGRATION_VERSION)); 3515 } 3516 } catch (e) { 3517 // Niet fataal: de kolommen staan er nog, dus de oude waarheid is niet weg. 3518 // Een volgende boot probeert het opnieuw, want de versie is niet gezet. 3519 console.warn('[AP] reactie-migratie mislukt:', e.message); 3520 } 3521 return uit; 3522 } 3523 3440 3524 /** 3441 3525 * Van wat de client stuurde naar de canonieke sleutel voor een reactie. … … 4888 4972 gateOutgoingFollow, performApprovedFollow, 4889 4973 parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs, 4890 autoBoostCount, boostedCount, setReaction, getReaction, getReactionsFor, canonicalReactionUri, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,4974 autoBoostCount, boostedCount, setReaction, getReaction, getReactionsFor, canonicalReactionUri, migrateReactions, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline, 4891 4975 getNotifications, listBlocks, isBlockedAny, blockTarget, unblock, 4892 4976 deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker, -
test/reactions-characterization.test.js
r6bc5ebd r7842ca1 313 313 assert.equal(AP.getReaction('me', onbekend).liked, true); 314 314 }); 315 316 // ── De migratie bij boot ───────────────────────────────────────────────── 317 318 test('migrateReactions hersleutelt een permalink-rij naar de object-URI', () => { 319 // De oude interact-route bewaarde de URI waarmee je binnenkwam, en de 320 // bookmarklet geeft de permalink door. Zonder hersleutelen zijn die rijen 321 // wees zodra er op de object-URI gezocht wordt. 322 const obj = uri('mig1'); 323 const permalink = 'https://sound-fabrics.com/oud-artikel'; 324 db.prepare(`INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, content, url, created_at) 325 VALUES (?,?,?,?,?,?,?)`) 326 .run(obj, 'me', 'https://r.test/users/anna', 'Anna', '<p>x</p>', permalink, '2026-08-06 09:00:00'); 327 db.prepare("INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind, created_at) VALUES (?,?,?,?)") 328 .run('me', permalink, 'like', '2026-07-01 12:00:00'); 329 330 const uitkomst = AP.migrateReactions({ force: true }); 331 assert.ok(uitkomst.hersleuteld >= 1); 332 assert.equal(AP.getReaction('me', obj).liked, true, 'nu vindbaar op de object-uri'); 333 const rij = db.prepare('SELECT created_at FROM ap_my_reactions WHERE site_slug=? AND target_uri=? AND kind=?').get('me', obj, 'like'); 334 assert.equal(rij.created_at, '2026-07-01 12:00:00', 'bij hersleutelen blijft de oorspronkelijke datum staan'); 335 const oud = db.prepare('SELECT COUNT(*) AS n FROM ap_my_reactions WHERE target_uri=?').get(permalink).n; 336 assert.equal(oud, 0, 'de permalink-rij is opgeruimd'); 337 }); 338 339 test('migrateReactions vult een kale kolomvlag aan', () => { 340 const u = uri('mig2'); seedTimeline(u); 341 AP.markLiked('me', u); // zoals de oude Krant-route 342 assert.equal(AP.getReaction('me', u).liked, false, 'vooraf onzichtbaar'); 343 AP.migrateReactions({ force: true }); 344 assert.equal(AP.getReaction('me', u).liked, true, 'daarna zichtbaar'); 345 }); 346 347 test('migrateReactions is idempotent en respecteert de versievlag', () => { 348 const eerste = AP.migrateReactions({ force: true }); 349 const tweede = AP.migrateReactions({ force: true }); 350 assert.equal(tweede.hersleuteld, 0, 'niets meer te hersleutelen'); 351 assert.equal(tweede.aangevuld, 0, 'niets meer aan te vullen'); 352 assert.ok(eerste.hersleuteld >= 0); 353 // Zonder force draait hij niet nog eens zodra de vlag staat. 354 db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?,?)').run('reactions_migration_version', '1'); 355 assert.equal(AP.migrateReactions().overgeslagen, true); 356 }); 357 358 test('migrateReactions --dry-run schrijft niets', () => { 359 const u = uri('mig3'); seedTimeline(u); 360 AP.markLiked('me', u); 361 const telling = AP.migrateReactions({ force: true, dryRun: true }); 362 assert.ok(telling.aangevuld >= 1, 'hij ziet wel wat er te doen is'); 363 assert.equal(AP.getReaction('me', u).liked, false, 'maar heeft niets geschreven'); 364 });
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)