Changeset 25f2e64 in Klonkt


Ignore:
Timestamp:
08/06/2026 09:46:01 AM (5 weeks ago)
Author:
Claude (agent) <aiclaude@…>
Branches:
main
Children:
5296349
Parents:
7842ca1
git-author:
Robin <roboburr@…> (08/06/2026 09:46:00 AM)
git-committer:
Claude (agent) <aiclaude@…> (08/06/2026 09:46:01 AM)
Message:

Weergave gelijkgetrokken: Krant en Cirkel lezen de tussentabel (shaer-9e9)

CORRECTIE: ik meldde fase 2 als klaar, en dat was te vroeg. De Krant TEKENDE
zijn like- en boostknoppen nog uit ap_timeline.liked/boosted (tl-item.ejs leest
p.liked en p.boosted), terwijl de toggle al uit getReaction besliste. Tekenen en
beslissen leunden dus op verschillende bronnen. Ze waren het eens zolang de
migratie ze gelijk hield -- synchronisatie, geen ontwerp.

getTimeline leidt liked/boosted nu af uit de tussentabel, met dezelfde
batch-query als de C2S-tijdlijn: één extra query per pagina, geen N+1. Bewust in
JS en niet als join, want de rijen komen met SELECT * en dan zouden twee
kolommen liked heten -- welke dan wint hangt van de driver af.

getCirkelPosts kon niet in JS: die filtert in de WHERE. Daar is het een LEFT JOIN
op ap_my_reactions geworden, met (rb.target_uri IS NOT NULL) AS boosted zodat de
view ongewijzigd blijft. De UNIQUE(site_slug, target_uri, kind) garandeert
hoogstens één match, dus de join kan geen rijen verdubbelen.

boostedCount telt nu de tussentabel, nog steeds met een join op ap_timeline zodat
de betekenis gelijk blijft: geboost EN in je tijdlijn.

Twee karakteriseringstests gingen hierdoor stuk, en dat is precies waarvoor ze er
staan: ze legden vast dat een kale kolomvlag een post de Cirkel in trok. Dat is
nu niet meer waar. Bewust bijgewerkt met de reden erbij, niet weggehaald.
upsertBoostedNote blijft verantwoordelijk voor de RIJ -- zonder die insert heeft
een boost van iemand die je niet volgt geen post om bij te horen.

Daarmee leest niets in de weergave nog uit de kolommen. Ze worden alleen nog
geschreven (vangnet) en gelezen door de migratie. Dat maakt de drop een losse,
terugdraaibare beslissing in plaats van een verweven.

Geen breaking change: additief en intern, dus kandidaat voor de volgende MINOR.

Suite 441/441.

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

Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r7842ca1 r25f2e64  
    31923192  return { ins: _insTl, list: _listTl, del: _delTl };
    31933193}
    3194 export function getTimeline(slug, limit, offset) { return tlStmts().list.all(slug, limit || 50, offset || 0); }
     3194/**
     3195 * De tijdlijn, met liked/boosted uit de TUSSENTABEL (shaer-9e9).
     3196 *
     3197 * De rijen komen met SELECT *, dus ap_timeline.liked en .boosted liften mee --
     3198 * en die zijn sinds fase 1 nog maar een afgeleide. De Krant tekende zijn
     3199 * knoppen daar wel op, terwijl de toggle al uit getReaction besliste: tekenen en
     3200 * beslissen leunden dus op verschillende bronnen. Ze waren het eens zolang de
     3201 * migratie ze gelijk hield, maar dat was synchronisatie en geen ontwerp.
     3202 *
     3203 * Bewust in JS en niet als join: met SELECT * zouden twee kolommen `liked`
     3204 * heten en hangt het van de driver af welke wint. Eén extra query per pagina
     3205 * (dezelfde batch die de C2S-tijdlijn gebruikt) is dat niet waard.
     3206 */
     3207export function getTimeline(slug, limit, offset) {
     3208  const rows = tlStmts().list.all(slug, limit || 50, offset || 0);
     3209  const reacties = getReactionsFor(slug, rows.map((r) => r.id));
     3210  for (const r of rows) {
     3211    const x = reacties.get(r.id);
     3212    r.liked = !!(x && x.liked);
     3213    r.boosted = !!(x && x.boosted);
     3214  }
     3215  return rows;
     3216}
    31953217
    31963218/**
     
    33843406    if (!_cirkelPosts) _cirkelPosts = db.prepare(`
    33853407      SELECT t.id, t.author_uri, t.author_name, t.author_handle, t.author_icon, t.author_url,
    3386              t.content, t.url, t.published, t.media_json, t.boosted, t.nsfw, t.cw
     3408             t.content, t.url, t.published, t.media_json, t.nsfw, t.cw,
     3409             (rb.target_uri IS NOT NULL) AS boosted
    33873410      FROM ap_timeline t
    33883411      LEFT JOIN ap_following f ON f.slug = t.slug AND f.actor_uri = t.author_uri
    3389       WHERE t.slug = ? AND (f.auto_boost = 1 OR t.boosted = 1)
     3412      -- Uit de tussentabel, niet uit t.boosted: die kolom is een afgeleide. De
     3413      -- UNIQUE(site_slug, target_uri, kind) garandeert hoogstens één match, dus
     3414      -- deze join kan geen rijen verdubbelen.
     3415      LEFT JOIN ap_my_reactions rb ON rb.site_slug = t.slug AND rb.target_uri = t.id AND rb.kind = 'boost'
     3416      WHERE t.slug = ? AND (f.auto_boost = 1 OR rb.target_uri IS NOT NULL)
    33903417      ORDER BY COALESCE(t.published, t.created_at) DESC, t.rowid DESC
    33913418      LIMIT ? OFFSET ?`);
     
    36373664}
    36383665export function boostedCount(slug) {
    3639   try { if (!_boostedCount) _boostedCount = db.prepare('SELECT COUNT(*) AS n FROM ap_timeline WHERE slug = ? AND boosted = 1'); return _boostedCount.get(slug).n; } catch { return 0; }
     3666  // Geboost EN in je tijdlijn, zoals voorheen: de tussentabel kan ook een boost
     3667  // bevatten van iets dat er (nog) niet in staat.
     3668  try {
     3669    if (!_boostedCount) _boostedCount = db.prepare(`SELECT COUNT(*) AS n FROM ap_my_reactions r
     3670      JOIN ap_timeline t ON t.slug = r.site_slug AND t.id = r.target_uri
     3671      WHERE r.site_slug = ? AND r.kind = 'boost'`);
     3672    return _boostedCount.get(slug).n;
     3673  } catch { return 0; }
    36403674}
    36413675
  • test/reactions-characterization.test.js

    r7842ca1 r25f2e64  
    6161});
    6262
    63 test('tijdlijn-route: markBoosted zet de kolom en de post verschijnt in de Cirkel', () => {
     63test('tijdlijn-route: markBoosted zet de kolom, maar de Cirkel volgt de tussentabel', () => {
     64  // BIJGEWERKT toen de weergave werd gelijkgetrokken. Vroeger trok een kale
     65  // kolomvlag de post de Cirkel in; nu selecteert de Cirkel op de tussentabel,
     66  // dus de primitief alleen is niet meer genoeg. Dat is het doel: tekenen en
     67  // beslissen leunen op dezelfde bron.
    6468  const u = uri('t2'); seedTimeline(u);
    65   assert.equal(AP.boostedCount('me'), 0);
    6669  AP.markBoosted('me', u);
    67   assert.equal(AP.getTimelineReaction('me', u).boosted, true);
    68   assert.equal(AP.boostedCount('me'), 1);
    69   // De Cirkel is wat een gebruiker ziet: geboost = zichtbaar daar.
    70   assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'geboost hoort in de Cirkel te staan');
    71   AP.unmarkBoosted('me', u);
     70  assert.equal(AP.getTimelineReaction('me', u).boosted, true, 'de kolom staat aan');
     71  assert.equal(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), false,
     72    'maar dat brengt hem niet in de Cirkel');
     73  // Via het echte schrijfpad wel.
     74  AP.setReaction('me', u, 'boost', true);
     75  assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'via setReaction wel');
     76  AP.setReaction('me', u, 'boost', false);
    7277  assert.equal(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), false);
    7378});
     
    117122  assert.equal(db.prepare('SELECT COUNT(*) AS n FROM ap_timeline WHERE id=?').get(u).n, 1, 'de rij hoort aangemaakt te worden');
    118123  assert.equal(AP.getTimelineReaction('me', u).boosted, true, 'en meteen als geboost gemarkeerd');
    119   assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'en dus zichtbaar in de Cirkel');
     124  // BIJGEWERKT: de Cirkel volgt sinds het gelijktrekken van de weergave de
     125  // tussentabel, en die vult alleen setReaction. upsertBoostedNote blijft
     126  // verantwoordelijk voor de RIJ -- zonder die insert heeft de reactie nergens
     127  // een post om bij te horen.
     128  assert.equal(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), false, 'de rij alleen is niet genoeg');
     129  AP.setMyReaction('me', u, 'boost', true);
     130  assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'met de tussentabel erbij wel');
    120131});
    121132
     
    363374  assert.equal(AP.getReaction('me', u).liked, false, 'maar heeft niets geschreven');
    364375});
     376
     377// ── De weergave leest dezelfde bron als de knop ──────────────────────────
     378
     379test('getTimeline tekent liked/boosted uit de tussentabel, niet uit de kolom', () => {
     380  // De Krant tekende zijn knoppen op ap_timeline.liked/boosted terwijl de
     381  // toggle al uit getReaction besliste. Tekenen en beslissen leunden dus op
     382  // verschillende bronnen -- ze waren het alleen eens zolang iets ze gelijk
     383  // hield. Hier wordt de kolom expres tegengesteld gezet.
     384  const u = uri('tl1'); seedTimeline(u);
     385  AP.setReaction('me', u, 'like', true);
     386  db.prepare('UPDATE ap_timeline SET liked = 0, boosted = 1 WHERE slug = ? AND id = ?').run('me', u);
     387
     388  const rij = AP.getTimeline('me', 50, 0).find((r) => r.id === u);
     389  assert.equal(rij.liked, true, 'de tussentabel zegt geliket, dus de knop hoort aan');
     390  assert.equal(rij.boosted, false, 'en de kolom die iets anders beweert telt niet');
     391});
     392
     393test('de Cirkel selecteert op de tussentabel', () => {
     394  const u = uri('tl2'); seedTimeline(u);
     395  // Kolom aan, tussentabel leeg: hoort NIET in de Cirkel te staan.
     396  db.prepare('UPDATE ap_timeline SET boosted = 1 WHERE slug = ? AND id = ?').run('me', u);
     397  assert.equal(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), false,
     398    'een kale kolomvlag trekt niets meer de Cirkel in');
     399  // En andersom: tussentabel aan, kolom uit.
     400  db.prepare('UPDATE ap_timeline SET boosted = 0 WHERE slug = ? AND id = ?').run('me', u);
     401  AP.setMyReaction('me', u, 'boost', true);
     402  const post = AP.getCirkelPosts('me', 60, 0).find((p) => p.id === u);
     403  assert.ok(post, 'de tussentabel bepaalt wat er in de Cirkel staat');
     404  assert.ok(post.boosted, 'en het veld dat de view tekent komt daar ook vandaan');
     405});
     406
     407test('boostedCount telt de tussentabel, en alleen wat in je tijdlijn staat', () => {
     408  const inTl = uri('tl3'), buiten = 'https://elders.test/notes/qqq';
     409  seedTimeline(inTl);
     410  const voor = AP.boostedCount('me');
     411  AP.setMyReaction('me', inTl, 'boost', true);
     412  AP.setMyReaction('me', buiten, 'boost', true);
     413  assert.equal(AP.boostedCount('me'), voor + 1, 'alleen de post die in je tijdlijn staat telt mee');
     414});
Note: See TracChangeset for help on using the changeset viewer.