Changeset c010b42 in Klonkt
- Timestamp:
- 08/06/2026 08:46:01 AM (5 weeks ago)
- Branches:
- main
- Children:
- 0e27e54
- Parents:
- 02fb7ce
- git-author:
- Robin <roboburr@…> (08/06/2026 08:46:00 AM)
- git-committer:
- Claude (agent) <aiclaude@…> (08/06/2026 08:46:01 AM)
- Files:
-
- 3 edited
-
src/routes/posts.js (modified) (4 diffs)
-
src/services/ActivityPubService.js (modified) (5 diffs)
-
test/reactions-characterization.test.js (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/posts.js
r02fb7ce rc010b42 843 843 .then((note) => note && ActivityPubService.sendInteraction(site, on ? 'like' : 'unlike', note.object_uri || uri, note.actor_uri)) 844 844 .catch((e) => console.warn('[AP] remote like failed:', e.message)); 845 ActivityPubService.setMyReaction(site.slug, uri, 'like', on); 845 // Eén schrijfpad (shaer-9e9): tussentabel + afgeleide vlag. 846 ActivityPubService.setReaction(site.slug, uri, 'like', on); 846 847 } 847 848 if (req.get('X-Requested-With') === 'fetch') return res.json({ ok: true, on }); … … 862 863 const id = note.object_uri || uri; 863 864 return Promise.resolve(ActivityPubService.sendInteraction(site, on ? 'boost' : 'unboost', id, note.actor_uri)) 864 // Boost → store the post in the timeline (even if you don't follow the author) so it 865 // surfaces in the Cirkel; unboost → just clear the flag. 866 .then(() => on ? ActivityPubService.upsertBoostedNote(site.slug, note) : ActivityPubService.unmarkBoosted(site.slug, id)); 865 // De note gaat mee: een boost zet niet alleen een vlag maar trekt de 866 // post je tijdlijn in, ook als je de auteur niet volgt, zodat hij in 867 // de Cirkel verschijnt. 868 .then(() => ActivityPubService.setReaction(site.slug, uri, 'boost', on, { flagUri: id, note: on ? note : null })); 867 869 }) 868 870 .catch((e) => console.warn('[AP] remote boost failed:', e.message)); 871 // De tussentabel meteen, zodat de knop klopt voordat de resolve terug is. 869 872 ActivityPubService.setMyReaction(site.slug, uri, 'boost', on); 870 873 } … … 1279 1282 on = !ActivityPubService.getTimelineReaction(site.slug, note).liked; 1280 1283 try { await ActivityPubService.sendInteraction(site, on ? 'like' : 'unlike', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ } 1281 if (on) ActivityPubService.markLiked(site.slug, note); else ActivityPubService.unmarkLiked(site.slug, note);1284 ActivityPubService.setReaction(site.slug, note, 'like', on); 1282 1285 } 1283 1286 if (req.get('X-Requested-With') === 'fetch') return res.json({ ok: true, on }); … … 1293 1296 on = !ActivityPubService.getTimelineReaction(site.slug, note).boosted; 1294 1297 try { await ActivityPubService.sendInteraction(site, on ? 'boost' : 'unboost', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ } 1298 ActivityPubService.setReaction(site.slug, note, 'boost', on); // instant UI state 1295 1299 if (on) { 1296 ActivityPubService.markBoosted(site.slug, note); // instant UI state1297 1300 // Fire-and-forget: re-resolve the note so the cached row is refreshed 1298 1301 // (cover/content) — boosting again heals a stale copy from EVERY boost 1299 1302 // path, not just the interact page. 1300 1303 ActivityPubService.resolveRemoteNote(note) 1301 .then((n) => { if (n) ActivityPubService. upsertBoostedNote(site.slug, n); })1304 .then((n) => { if (n) ActivityPubService.setReaction(site.slug, note, 'boost', true, { note: n }); }) 1302 1305 .catch(() => { /* best-effort */ }); 1303 } else {1304 ActivityPubService.unmarkBoosted(site.slug, note);1305 1306 } 1306 1307 } -
src/services/ActivityPubService.js
r02fb7ce rc010b42 2623 2623 const kind = type === 'Announce' ? 'boost' : 'like'; 2624 2624 await sendInteraction(site, kind, objUri, authorUri); 2625 setMyReaction(site.slug, targetUri, kind, true); 2626 if (type === 'Announce' && note) { try { upsertBoostedNote(site.slug, note); } catch { /* non-fatal */ } } 2625 // Eén schrijfpad (shaer-9e9): tussentabel + afgeleide vlag in één keer. 2626 // De note gaat mee zodat een boost de post je tijdlijn in trekt. 2627 try { setReaction(site.slug, targetUri, kind, true, { flagUri: objUri, note: type === 'Announce' ? note : null }); } 2628 catch { /* non-fatal: een reactie mag nooit de bezorging blokkeren */ } 2627 2629 // Een Like uit een app moet ook in ap_timeline.liked landen, want dat 2628 2630 // is wat de C2S-tijdlijn als shaer:liked teruggeeft. Zonder dit werd … … 2634 2636 // in je tijdlijn te trekken, dus staat de post er niet in, dan is dit 2635 2637 // terecht een no-op. 2636 if (type === 'Like') { try { markLiked(site.slug, objUri); } catch { /* non-fatal */ } }2637 2638 return { status: 202, url: objUri }; 2638 2639 } … … 2683 2684 const objUri = (note && note.object_uri) || innerTarget; 2684 2685 await sendInteraction(site, kind, objUri, note && note.actor_uri); 2685 setMyReaction(site.slug, innerTarget, innerType === 'Announce' ? 'boost' : 'like', false); 2686 if (innerType === 'Announce') { try { unmarkBoosted(site.slug, objUri); } catch { /* non-fatal */ } } 2687 if (innerType === 'Like') { try { unmarkLiked(site.slug, objUri); } catch { /* non-fatal */ } } 2686 try { setReaction(site.slug, innerTarget, innerType === 'Announce' ? 'boost' : 'like', false, { flagUri: objUri }); } 2687 catch { /* non-fatal */ } 2688 2688 return { status: 202, url: objUri }; 2689 2689 } … … 3411 3411 try { if (!_unmarkLike) _unmarkLike = db.prepare('UPDATE ap_timeline SET liked = 0 WHERE slug = ? AND id = ?'); _unmarkLike.run(slug, noteId); } catch { /* ignore */ } 3412 3412 } 3413 /** 3414 * Zet een reactie van JOU op een object. Dit hoort het enige schrijfpad te zijn 3415 * (shaer-9e9): de tussentabel ap_my_reactions is de waarheid, de vlaggen op 3416 * ap_timeline zijn de afgeleide. Zolang markLiked en broers los aanroepbaar 3417 * blijven kan een aanroeper ze vergeten, en dat is niet hypothetisch -- precies 3418 * dat leverde de shaer:liked-bug op (04aca12). 3419 * 3420 * `opts.note` is de opgeloste remote note bij een boost. Die is niet optioneel 3421 * uit netheid: een boost moet de post je tijdlijn IN trekken als je de auteur 3422 * niet volgt, anders heeft de vlag geen rij om op te landen en verschijnt de 3423 * boost nergens -- ook niet in de Cirkel. 3424 * 3425 * `opts.flagUri` bestaat omdat de twee bronnen vandaag verschillend gesleuteld 3426 * worden: de tussentabel op de URI die de client stuurde, de vlag op de 3427 * opgeloste object-URI. Meestal zijn die gelijk, maar niet gegarandeerd. Deze 3428 * naad houdt fase 1 gedragsbehoudend; het samentrekken van die twee sleutels is 3429 * werk voor fase 2, mét datamigratie. 3430 */ 3431 export function setReaction(slug, uri, kind, on, opts = {}) { 3432 if (!slug || !uri || (kind !== 'like' && kind !== 'boost')) return; 3433 setMyReaction(slug, uri, kind, !!on); 3434 const flagUri = opts.flagUri || uri; 3435 if (kind === 'boost') { 3436 if (!on) unmarkBoosted(slug, flagUri); 3437 else if (opts.note) upsertBoostedNote(slug, opts.note); 3438 else markBoosted(slug, flagUri); 3439 } else if (on) markLiked(slug, flagUri); 3440 else unmarkLiked(slug, flagUri); 3441 } 3442 3413 3443 export function getTimelineReaction(slug, noteId) { 3414 3444 try { const r = db.prepare('SELECT liked, boosted FROM ap_timeline WHERE slug = ? AND id = ?').get(slug, noteId); return { liked: !!(r && r.liked), boosted: !!(r && r.boosted) }; } catch { return { liked: false, boosted: false }; } … … 4779 4809 gateOutgoingFollow, performApprovedFollow, 4780 4810 parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs, 4781 autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,4811 autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, setReaction, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline, 4782 4812 getNotifications, listBlocks, isBlockedAny, blockTarget, unblock, 4783 4813 deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker, -
test/reactions-characterization.test.js
r02fb7ce rc010b42 9 9 // eerst is vastgeschreven. 10 10 // 11 // LET OP bij het opruimen: de tests onder "de twee bronnen lopen uiteen" horen 12 // te GAAN FALEN zodra fase 1 (één setReaction) klaar is. Dat is geen regressie 13 // maar het doel; werk ze dan bewust bij in plaats van ze te laten verdwijnen. 11 // LET OP: de tests onder "de twee bronnen lopen uiteen" roepen de PRIMITIEVEN 12 // aan (markLiked, setMyReaction). Die blijven bewust gescheiden -- het 13 // samenvoegen zit in setReaction, dus in de aanroepers, niet in de primitieven. 14 // Deze tests blijven daarom groen na fase 1 en beschrijven dan nog steeds iets 15 // waars: wie markLiked los aanroept, raakt de tussentabel niet. Dat is precies 16 // waarom setReaction bestaat en waarom die primitieven op termijn intern moeten 17 // worden. 18 // 19 // De invariant die fase 1 wél bewaakt staat onderaan, bij setReaction. 14 20 // 15 21 // Wat hier NIET in kan: de routes zelf (die vragen HTTP + sessie) en het … … 115 121 116 122 // ── De twee bronnen lopen uiteen ───────────────────────────────────────── 117 // DEZE TESTS HOREN TE FALEN NA FASE 1. Ze leggen de huidige splitsing vast, 118 // zodat het samenvoegen een zichtbare gebeurtenis is en geen stille. 123 // De primitieven raken elkaar niet, en dat blijft na fase 1 zo: het 124 // samenvoegen zit in setReaction. Deze drie leggen vast waarom die functie 125 // moet bestaan, en waarom markLiked en broers uiteindelijk intern horen te 126 // worden -- zolang ze los aanroepbaar zijn, kan een aanroeper de helft doen. 119 127 120 128 test('HUIDIG GEDRAG: de tijdlijn-route vult de tussentabel niet', () => { … … 123 131 assert.equal(AP.getTimelineReaction('me', u).liked, true, 'de kolom staat aan'); 124 132 assert.equal(AP.getMyReactions('me', u).liked, false, 125 ' NA FASE 1 hoort dit true te zijn: pas deze test dan bewust aan');133 'de primitief raakt de tussentabel niet -- daarvoor is setReaction'); 126 134 }); 127 135 … … 131 139 assert.equal(AP.getMyReactions('me', u).liked, true, 'de tussentabel staat aan'); 132 140 assert.equal(AP.getTimelineReaction('me', u).liked, false, 133 ' NA FASE 1 hoort dit true te zijn: pas deze test dan bewust aan');141 'de primitief raakt de kolom niet -- daarvoor is setReaction'); 134 142 }); 135 143 … … 145 153 }); 146 154 147 // ── Wat de invariant-test van fase 1 straks moet bewaken ─────────────────155 // ── De scheefheid, meetbaar (fase 0 uit shaer-9e9 als test) ───────────── 148 156 149 157 test('meetbaar: hoeveel rijen hebben een vlag zonder tegenhanger', () => { … … 157 165 assert.ok(scheef > 0, 'vandaag lopen ze uiteen; na fase 1 hoort deze assert omgedraaid te worden naar === 0'); 158 166 }); 167 168 // ── De invariant van fase 1: setReaction houdt de bronnen gelijk ───────── 169 170 test('setReaction: een like landt in BEIDE bronnen', () => { 171 const u = uri('sr1'); seedTimeline(u); 172 AP.setReaction('me', u, 'like', true); 173 assert.equal(AP.getMyReactions('me', u).liked, true, 'tussentabel'); 174 assert.equal(AP.getTimelineReaction('me', u).liked, true, 'afgeleide vlag'); 175 AP.setReaction('me', u, 'like', false); 176 assert.equal(AP.getMyReactions('me', u).liked, false); 177 assert.equal(AP.getTimelineReaction('me', u).liked, false); 178 }); 179 180 test('setReaction: een boost met note trekt de post je tijdlijn in EN vult beide', () => { 181 // De valkuil uit het plan: zonder de note bestaat de rij niet en landt de 182 // vlag nergens, dus zou de boost onvindbaar zijn. 183 const u = uri('sr2'); 184 AP.setReaction('me', u, 'boost', true, { 185 note: { object_uri: u, actor_uri: 'https://r.test/users/bo', actor_name: 'Bo', content: '<p>x</p>', media: '[]' }, 186 }); 187 assert.equal(AP.getMyReactions('me', u).boosted, true, 'tussentabel'); 188 assert.equal(AP.getTimelineReaction('me', u).boosted, true, 'afgeleide vlag'); 189 assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'en zichtbaar in de Cirkel'); 190 AP.setReaction('me', u, 'boost', false); 191 assert.equal(AP.getMyReactions('me', u).boosted, false); 192 assert.equal(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), false); 193 }); 194 195 test('setReaction: gescheiden sleutels blijven werken zolang ze bestaan', () => { 196 // De tussentabel wordt gesleuteld op wat de client stuurde, de vlag op de 197 // opgeloste object-URI. Meestal gelijk, niet gegarandeerd. flagUri houdt dat 198 // uit elkaar tot fase 2 ze samentrekt. 199 const gestuurd = 'https://r.test/@anna/123'; 200 const opgelost = uri('sr3'); 201 seedTimeline(opgelost); 202 AP.setReaction('me', gestuurd, 'like', true, { flagUri: opgelost }); 203 assert.equal(AP.getMyReactions('me', gestuurd).liked, true, 'op de gestuurde uri'); 204 assert.equal(AP.getTimelineReaction('me', opgelost).liked, true, 'op de opgeloste uri'); 205 }); 206 207 test('setReaction: rommelige invoer doet niets in plaats van iets halfs', () => { 208 AP.setReaction('me', uri('sr4'), 'sterretje', true); 209 assert.equal(AP.getMyReactions('me', uri('sr4')).liked, false); 210 AP.setReaction('', uri('sr5'), 'like', true); 211 AP.setReaction('me', '', 'like', true); 212 assert.equal(AP.getMyReactions('me', uri('sr5')).liked, false); 213 });
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)