Changeset 14f7cb2 in Klonkt


Ignore:
Timestamp:
08/06/2026 09:10:41 AM (5 weeks ago)
Author:
Claude (agent) <aiclaude@…>
Branches:
main
Children:
68a4d1b
Parents:
0e27e54
git-author:
Robin <roboburr@…> (08/06/2026 09:10:40 AM)
git-committer:
Claude (agent) <aiclaude@…> (08/06/2026 09:10:41 AM)
Message:

Eén leespad voor reacties (shaer-9e9 fase 2)

De tussentabel is nu ook voor LEZEN de bron. getReaction(slug, uri) vervangt
getMyReactions en getTimelineReaction, die dezelfde vraag beantwoordden uit twee
verschillende bronnen -- de reden dat een like in de Krant en dezelfde like op de
interact-pagina een andere knopstand konden tonen.

Vijf lezers omgezet: de interact-pagina en zijn twee toggles, en de twee toggles
in de Krant. Plus de C2S-tijdlijn, waar shaer:liked en shaer:boosted niet meer
uit de afgeleide kolom komen. Die laatste gaat via getReactionsFor: één query
voor de hele pagina, want per rij vragen zou een consistentiebug hebben geruild
voor een N+1.

Mag alleen ná scripts/backfill-reactions.mjs. Een reactie van vóór fase 1 staat
alleen in de kolom en is voor getReaction onzichtbaar -- stil, want een
ontbrekende rij is geen fout. Er staat nu een test die precies dat vastlegt, zodat
de volgorde-eis uitvoerbaar is en niet alleen opgeschreven.

Robin heeft bevestigd dat de likes van 09:01 via de KRANT gingen, dus het
schrijven van fase 1 is op de webroute bewezen -- dat was de poort voor deze
stap, want anders zou de knop daar nu stuk gaan in plaats van scheef te lopen.

Suite 430/430.

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

Files:
4 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    r0e27e54 r14f7cb2  
    285285  const playbackAllowed = embedsAllowed
    286286    && Guardianship.externalPlaybackAllowed(auth.site.external_playback, isWard);
    287   const posts = AP.getTimeline(auth.site.slug, 60).map((t) => ({
     287  const rows = AP.getTimeline(auth.site.slug, 60);
     288  // Eén query voor de hele pagina (shaer-9e9 fase 2): shaer:liked komt uit de
     289  // tussentabel, de bron van waarheid, en niet meer uit de afgeleide kolom op
     290  // ap_timeline. Per rij vragen zou hier een N+1 opleveren.
     291  const reacties = AP.getReactionsFor(auth.site.slug, rows.map((t) => t.id));
     292  const posts = rows.map((t) => ({
    288293    id: `${t.id}#create`,
    289294    type: 'Create',
     
    333338      // Whether THIS account already liked/boosted the note, so the app's
    334339      // detail-view buttons show the current state (and can toggle/undo).
    335       'shaer:liked': !!t.liked,
    336       'shaer:boosted': !!t.boosted,
     340      'shaer:liked': !!(reacties.get(t.id) || {}).liked,
     341      'shaer:boosted': !!(reacties.get(t.id) || {}).boosted,
    337342      // An external (non-fediverse) embed, thumbnail-only and never an iframe.
    338343      // Omitted entirely when the gate is closed (see above).
  • src/routes/posts.js

    r0e27e54 r14f7cb2  
    806806    liked: !!req.query.liked,
    807807    boosted: !!req.query.boosted,
    808     reacted: (site && uri) ? ActivityPubService.getMyReactions(site.slug, uri) : { liked: false, boosted: false },
     808    reacted: (site && uri) ? ActivityPubService.getReaction(site.slug, uri) : { liked: false, boosted: false },
    809809    siteTitle: site ? site.title : '',
    810810  });
     
    839839  let on = false;
    840840  if (site && uri) {
    841     on = !ActivityPubService.getMyReactions(site.slug, uri).liked;
     841    on = !ActivityPubService.getReaction(site.slug, uri).liked;
    842842    ActivityPubService.resolveRemoteNote(uri)
    843843      .then((note) => note && ActivityPubService.sendInteraction(site, on ? 'like' : 'unlike', note.object_uri || uri, note.actor_uri))
     
    857857  let on = false;
    858858  if (site && uri) {
    859     on = !ActivityPubService.getMyReactions(site.slug, uri).boosted;
     859    on = !ActivityPubService.getReaction(site.slug, uri).boosted;
    860860    ActivityPubService.resolveRemoteNote(uri)
    861861      .then((note) => {
     
    12801280  let on = false;
    12811281  if (site && note) {
    1282     on = !ActivityPubService.getTimelineReaction(site.slug, note).liked;
     1282    on = !ActivityPubService.getReaction(site.slug, note).liked;
    12831283    try { await ActivityPubService.sendInteraction(site, on ? 'like' : 'unlike', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ }
    12841284    ActivityPubService.setReaction(site.slug, note, 'like', on);
     
    12941294  let on = false;
    12951295  if (site && note) {
    1296     on = !ActivityPubService.getTimelineReaction(site.slug, note).boosted;
     1296    on = !ActivityPubService.getReaction(site.slug, note).boosted;
    12971297    try { await ActivityPubService.sendInteraction(site, on ? 'boost' : 'unboost', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ }
    12981298    ActivityPubService.setReaction(site.slug, note, 'boost', on); // instant UI state
  • src/services/ActivityPubService.js

    r0e27e54 r14f7cb2  
    34293429 * werk voor fase 2, mét datamigratie.
    34303430 */
     3431/**
     3432 * Wat heb IK met dit object gedaan? Leest de tussentabel, de bron van waarheid
     3433 * sinds shaer-9e9 fase 2. Vervangt getMyReactions en getTimelineReaction, die
     3434 * dezelfde vraag beantwoordden uit twee verschillende bronnen.
     3435 */
     3436export function getReaction(slug, uri) {
     3437  try {
     3438    const rows = (slug && uri)
     3439      ? db.prepare('SELECT kind FROM ap_my_reactions WHERE site_slug = ? AND target_uri = ?').all(slug, uri)
     3440      : [];
     3441    return { liked: rows.some((r) => r.kind === 'like'), boosted: rows.some((r) => r.kind === 'boost') };
     3442  } catch { return { liked: false, boosted: false }; }
     3443}
     3444
     3445/**
     3446 * Dezelfde vraag voor een hele pagina in EEN query. De C2S-tijdlijn zet
     3447 * shaer:liked op elke post; per rij vragen zou dat een N+1 maken, en dan had je
     3448 * een consistentiebug geruild voor een traagheidsbug.
     3449 */
     3450export function getReactionsFor(slug, uris) {
     3451  const out = new Map();
     3452  const list = [...new Set((uris || []).filter(Boolean))].slice(0, 500);
     3453  if (!slug || !list.length) return out;
     3454  try {
     3455    const rows = db.prepare(
     3456      `SELECT target_uri, kind FROM ap_my_reactions
     3457        WHERE site_slug = ? AND target_uri IN (${list.map(() => '?').join(',')})`,
     3458    ).all(slug, ...list);
     3459    for (const r of rows) {
     3460      const cur = out.get(r.target_uri) || { liked: false, boosted: false };
     3461      if (r.kind === 'like') cur.liked = true;
     3462      if (r.kind === 'boost') cur.boosted = true;
     3463      out.set(r.target_uri, cur);
     3464    }
     3465  } catch { /* leeg = niets gereageerd, en dat is een veilige uitkomst */ }
     3466  return out;
     3467}
     3468
    34313469export function setReaction(slug, uri, kind, on, opts = {}) {
    34323470  if (!slug || !uri || (kind !== 'like' && kind !== 'boost')) return;
     
    48094847  gateOutgoingFollow, performApprovedFollow,
    48104848  parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs,
    4811   autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, setReaction, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
     4849  autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, setReaction, getReaction, getReactionsFor, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
    48124850  getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
    48134851  deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
  • test/reactions-characterization.test.js

    r0e27e54 r14f7cb2  
    212212  assert.equal(AP.getMyReactions('me', uri('sr5')).liked, false);
    213213});
     214
     215// ── Fase 2: lezen komt uit de tussentabel ────────────────────────────────
     216
     217test('getReaction leest de tussentabel, niet de kolom', () => {
     218  const u = uri('g1'); seedTimeline(u);
     219  AP.setReaction('me', u, 'like', true);
     220  assert.deepEqual(AP.getReaction('me', u), { liked: true, boosted: false });
     221  AP.setReaction('me', u, 'boost', true);
     222  assert.deepEqual(AP.getReaction('me', u), { liked: true, boosted: true });
     223});
     224
     225test('WAAROM DE BACKFILL EERST MOET: een kale kolomvlag is onzichtbaar voor getReaction', () => {
     226  // Dit is de reden dat scripts/backfill-reactions.mjs tussen fase 1 en 2 hoort.
     227  // Een reactie van vóór fase 1 staat alleen in de kolom; zodra de lezers de
     228  // tussentabel volgen is die stil verdwenen -- geen fout, geen spoor.
     229  const u = uri('g2'); seedTimeline(u);
     230  AP.markLiked('me', u);                                  // zoals de oude tijdlijn-route
     231  assert.equal(AP.getTimelineReaction('me', u).liked, true, 'de kolom staat aan');
     232  assert.equal(AP.getReaction('me', u).liked, false, 'maar het nieuwe leespad ziet hem niet');
     233  // Wat de backfill doet:
     234  AP.setMyReaction('me', u, 'like', true);
     235  assert.equal(AP.getReaction('me', u).liked, true, 'na aanvullen wel');
     236});
     237
     238test('getReactionsFor haalt een hele pagina in één keer op', () => {
     239  const a = uri('g3'), b = uri('g4'), c = uri('g5');
     240  seedTimeline(a); seedTimeline(b);
     241  AP.setReaction('me', a, 'like', true);
     242  AP.setReaction('me', b, 'boost', true);
     243  const m = AP.getReactionsFor('me', [a, b, c]);
     244  assert.equal(m.get(a).liked, true);
     245  assert.equal(m.get(b).boosted, true);
     246  assert.equal(m.get(c), undefined, 'wie niets heeft komt niet in de map; de aanroeper valt terug op false');
     247  // Dezelfde uitkomst als per stuk vragen, zodat de batch geen eigen waarheid wordt.
     248  for (const u of [a, b]) assert.deepEqual(m.get(u), AP.getReaction('me', u));
     249});
     250
     251test('getReactionsFor: lege of rommelige invoer geeft een lege map', () => {
     252  assert.equal(AP.getReactionsFor('me', []).size, 0);
     253  assert.equal(AP.getReactionsFor('me', null).size, 0);
     254  assert.equal(AP.getReactionsFor('', [uri('g3')]).size, 0);
     255});
Note: See TracChangeset for help on using the changeset viewer.