Changeset d14bf1c in Klonkt
- Timestamp:
- 08/07/2026 09:14:32 AM (5 weeks ago)
- Branches:
- main
- Children:
- 44c9f3f
- Parents:
- f2a7181
- git-author:
- Robin <roboburr@…> (08/07/2026 09:14:20 AM)
- git-committer:
- roboburr <roboburr@…> (08/07/2026 09:14:32 AM)
- Files:
-
- 3 edited
-
src/config/database.js (modified) (1 diff)
-
src/services/ActivityPubService.js (modified) (2 diffs)
-
test/feed-wait.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
rf2a7181 rd14bf1c 771 771 ensureColumn('ap_followers', 'handle', 'TEXT'); // @user@host 772 772 ensureColumn('ap_followers', 'icon', 'TEXT'); // avatar URL 773 feedStateTriggers(); 774 } 775 776 /** 777 * Wat er met een tijdlijn gebeurd is, op één plek (shaer-n05). 778 * 779 * De inbox-lezing voegt vier bronnen samen. De vraag "is er iets veranderd" werd 780 * eerst beantwoord met MAX(rowid) over die vier -- een TOEVALLIGE eigenschap van 781 * de tabellen, geen feit dat ergens is opgeschreven. Dat gaf precies de gebreken 782 * die je van zo'n afleiding verwacht: bewerkingen en verwijderingen bewogen hem 783 * niet, en hij kon achteruit lopen. Dezelfde fout als reacties uitlezen uit 784 * ap_timeline.liked (shaer-9e9). 785 * 786 * Nu één rij per bericht per tijdlijn, met een oplopende `rev` en `kind`. Dat 787 * beantwoordt drie vragen die anders drie eigen oplossingen zouden krijgen: 788 * is er iets veranderd sinds N, wát is er veranderd, en is dit bericht bewerkt. 789 * 790 * Bijgehouden door TRIGGERS en niet door de aanroepende code, om dezelfde reden 791 * dat er geen gebeurtenis-emitter is: een trigger zit in de database, dus geen 792 * enkel codepad kan hem vergeten. De prijs is onzichtbare logica -- wie alleen de 793 * JavaScript leest ziet niet waarom deze tabel vult. Vandaar dat ze hier staan, 794 * bij de tabel, en niet verspreid. 795 * 796 * Let op de `UPDATE OF`-kolomlijsten: die zijn niet decoratief. Een like schrijft 797 * ap_timeline.liked en een 🔁 schrijft .boosted; zonder die afbakening zou je 798 * eigen like het bericht als BEWERKT merken en elke wachtende client wekken. 799 */ 800 function feedStateTriggers() { 801 try { 802 db.exec(` 803 CREATE TABLE IF NOT EXISTS ap_feed_state ( 804 slug TEXT NOT NULL, 805 object_uri TEXT NOT NULL, 806 rev INTEGER NOT NULL, 807 kind TEXT NOT NULL, -- new | updated | deleted 808 at DATETIME DEFAULT CURRENT_TIMESTAMP, 809 PRIMARY KEY (slug, object_uri) 810 ); 811 CREATE INDEX IF NOT EXISTS idx_ap_feed_state_rev ON ap_feed_state(slug, rev); 812 -- Eén doorlopende teller voor de hele instance. Bewust niet MAX(rev) uit de 813 -- tabel zelf: verdwijnt de hoogste rij, dan zou die teruglopen en denkt een 814 -- client dat er niets gebeurd is. 815 CREATE TABLE IF NOT EXISTS ap_feed_rev (n INTEGER NOT NULL); 816 `); 817 if (!db.prepare('SELECT COUNT(*) AS n FROM ap_feed_rev').get().n) { 818 db.prepare('INSERT INTO ap_feed_rev (n) VALUES (0)').run(); 819 } 820 // slug + object_uri verschillen per bron; de rest is voor alle vier gelijk. 821 const zet = (naam, gebeurtenis, tabel, slug, uri, kind, extra = '') => ` 822 DROP TRIGGER IF EXISTS ${naam}; 823 CREATE TRIGGER ${naam} AFTER ${gebeurtenis} ON ${tabel} BEGIN 824 UPDATE ap_feed_rev SET n = n + 1; 825 INSERT INTO ap_feed_state (slug, object_uri, rev, kind) 826 ${extra || `VALUES (${slug}, ${uri}, (SELECT n FROM ap_feed_rev), '${kind}')`} 827 ON CONFLICT(slug, object_uri) DO UPDATE 828 SET rev = excluded.rev, kind = excluded.kind, at = CURRENT_TIMESTAMP; 829 END;`; 830 const joinPosts = (uri, kind) => ` 831 SELECT s.slug, ${uri}, (SELECT n FROM ap_feed_rev), '${kind}' 832 FROM posts p JOIN sites s ON s.id = p.site_id`; 833 db.exec([ 834 zet('trg_feed_tl_ins', 'INSERT', 'ap_timeline', 'NEW.slug', 'NEW.id', 'new'), 835 zet('trg_feed_tl_upd', 'UPDATE OF content, media_json, nsfw, cw, url, poll_json, quote_json, embed_json', 'ap_timeline', 'NEW.slug', 'NEW.id', 'updated'), 836 zet('trg_feed_tl_del', 'DELETE', 'ap_timeline', 'OLD.slug', 'OLD.id', 'deleted'), 837 zet('trg_feed_mn_ins', 'INSERT', 'ap_mentions', 'NEW.slug', 'NEW.object_uri', 'new'), 838 zet('trg_feed_mn_upd', 'UPDATE OF content, media_json, quote_json, embed_json', 'ap_mentions', 'NEW.slug', 'NEW.object_uri', 'updated'), 839 zet('trg_feed_mn_del', 'DELETE', 'ap_mentions', 'OLD.slug', 'OLD.object_uri', 'deleted'), 840 zet('trg_feed_ob_ins', 'INSERT', 'ap_outbox', 'NEW.site_slug', 'NEW.id', 'new'), 841 zet('trg_feed_ob_upd', 'UPDATE OF content, attachments', 'ap_outbox', 'NEW.site_slug', 'NEW.id', 'updated'), 842 zet('trg_feed_ob_del', 'DELETE', 'ap_outbox', 'OLD.site_slug', 'OLD.id', 'deleted'), 843 // ap_interactions draagt geen slug: die hangt aan de POST. Vandaar de join, 844 // en vandaar dat deze drie niet in de gewone vorm passen. 845 zet('trg_feed_ia_ins', 'INSERT', 'ap_interactions', '', '', '', `${joinPosts('NEW.object_uri', 'new')} WHERE p.id = NEW.post_id`), 846 zet('trg_feed_ia_upd', 'UPDATE OF content, media_json, quote_json, embed_json', 'ap_interactions', '', '', '', `${joinPosts('NEW.object_uri', 'updated')} WHERE p.id = NEW.post_id`), 847 zet('trg_feed_ia_del', 'DELETE', 'ap_interactions', '', '', '', `${joinPosts('OLD.object_uri', 'deleted')} WHERE p.id = OLD.post_id`), 848 ].join('\n')); 849 } catch (e) { 850 // Niet fataal: zonder deze tabel valt het wachten terug op "altijd de tijd 851 // volmaken", en dat is traag maar niet stuk. 852 console.error('❌ feed-state triggers:', e.message); 853 } 773 854 } 774 855 -
src/services/ActivityPubService.js
rf2a7181 rd14bf1c 3502 3502 */ 3503 3503 export function feedCursor(slug) { 3504 const max = (sql, ...args) => { try { const r = db.prepare(sql).get(...args); return (r && r.n) || 0; } catch { return 0; } }; 3505 const t = max('SELECT MAX(rowid) AS n FROM ap_timeline WHERE slug = ?', slug); 3506 const m = max('SELECT MAX(rowid) AS n FROM ap_mentions WHERE slug = ?', slug); 3507 const o = max('SELECT MAX(rowid) AS n FROM ap_outbox WHERE site_slug = ?', slug); 3508 const r = max(`SELECT MAX(i.rowid) AS n FROM ap_interactions i 3509 JOIN posts p ON p.id = i.post_id 3510 JOIN sites s ON s.id = p.site_id 3511 WHERE s.slug = ? AND i.kind = 'reply'`, slug); 3512 return `${t}.${m}.${o}.${r}`; 3504 try { 3505 const r = db.prepare('SELECT MAX(rev) AS n FROM ap_feed_state WHERE slug = ?').get(slug); 3506 return String((r && r.n) || 0); 3507 } catch { return '0'; } 3508 } 3509 3510 /** 3511 * Wat er sinds `rev` met deze tijdlijn gebeurd is: welke berichten er nieuw zijn, 3512 * bewerkt, of weg. 3513 * 3514 * Nog niet gebruikt door een leespad -- de vorm van de aankomst is shaer-of7 en 3515 * de "bewerkt"-markering is daar nog een open beslissing. Maar de gegevens 3516 * ontstaan hoe dan ook bij het bijhouden van de merksteen, en dit is de enige 3517 * plek waar ze samen te lezen zijn. 3518 */ 3519 export function feedChangesSince(slug, rev, limit = 200) { 3520 try { 3521 return db.prepare(`SELECT object_uri, kind, rev FROM ap_feed_state 3522 WHERE slug = ? AND rev > ? ORDER BY rev ASC LIMIT ?`) 3523 .all(slug, parseInt(rev, 10) || 0, limit); 3524 } catch { return []; } 3513 3525 } 3514 3526 … … 5324 5336 buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured, 5325 5337 followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins, 5326 feedCursor, waitForFeedChange,5338 feedCursor, feedChangesSince, waitForFeedChange, 5327 5339 getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote, noteAudience, mayReadNote, 5328 5340 listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote, -
test/feed-wait.test.js
rf2a7181 rd14bf1c 57 57 assert.notEqual(AP.feedCursor('me'), voor, `${naam} hoort de merksteen te bewegen`); 58 58 } 59 }); 60 61 test('een BEWERKING beweegt hem ook', () => { 62 // De reden dat de merksteen niet meer op MAX(rowid) leunt. Een Update schrijft 63 // dezelfde rij, dus de rowid bleef staan en een wachtende client sliep door. 64 db.prepare("INSERT INTO ap_timeline (id, slug, author_uri, content) VALUES ('bw','me','https://r.test/u/a','<p>oud</p>')").run(); 65 const voor = AP.feedCursor('me'); 66 db.prepare("UPDATE ap_timeline SET content = '<p>nieuw</p>' WHERE id = 'bw'").run(); 67 assert.notEqual(AP.feedCursor('me'), voor); 68 }); 69 70 test('en een VERWIJDERING, ook als het niet de laatste is', () => { 71 db.prepare("INSERT INTO ap_timeline (id, slug, author_uri, content) VALUES ('later','me','https://r.test/u/a','<p>x</p>')").run(); 72 const voor = AP.feedCursor('me'); 73 db.prepare("DELETE FROM ap_timeline WHERE id = 'bw'").run(); 74 assert.notEqual(AP.feedCursor('me'), voor, 'anders blijft een verwijderde post staan tot je toevallig ververst'); 75 }); 76 77 test('maar een LIKE of BOOST van jezelf niet', () => { 78 // De val waar dit bijna in liep: een like schrijft ap_timeline.liked en een 🔁 79 // schrijft .boosted. Zonder de UPDATE OF-kolomlijst zou je eigen like het 80 // bericht als BEWERKT merken en elke wachtende client wekken. 81 const voor = AP.feedCursor('me'); 82 db.prepare("UPDATE ap_timeline SET liked = 1 WHERE id = 'later'").run(); 83 db.prepare("UPDATE ap_timeline SET boosted = 1 WHERE id = 'later'").run(); 84 db.prepare("UPDATE ap_interactions SET acted_like = 1 WHERE post_id = 'p1'").run(); 85 assert.equal(AP.feedCursor('me'), voor, 'je eigen reactie is geen nieuws'); 86 }); 87 88 test('een INSERT OR IGNORE die niets doet beweegt hem niet', () => { 89 const voor = AP.feedCursor('me'); 90 db.prepare("INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, content) VALUES ('later','me','https://r.test/u/a','<p>x</p>')").run(); 91 assert.equal(AP.feedCursor('me'), voor); 92 }); 93 94 test('de teller loopt nooit achteruit', () => { 95 // Met MAX(rowid) zakte hij terug zodra de nieuwste rij verdween, en dan denkt 96 // een client dat er niets gebeurd is. 97 const hoog = parseInt(AP.feedCursor('me'), 10); 98 db.prepare('DELETE FROM ap_timeline WHERE slug = ?').run('me'); 99 assert.ok(parseInt(AP.feedCursor('me'), 10) >= hoog); 100 }); 101 102 test('en hij vertelt WAT er veranderd is', () => { 103 // Dit is wat een revisieteller niet kan en deze tabel gratis meegeeft: de 104 // "bewerkt"-markering hoeft er later geen eigen bouwsel voor te worden. 105 const stand = (uri) => (AP.feedChangesSince('me', 0).find((r) => r.object_uri === uri) || {}).kind; 106 107 db.prepare("INSERT INTO ap_timeline (id, slug, author_uri, content) VALUES ('vers','me','https://r.test/u/a','<p>x</p>')").run(); 108 assert.equal(stand('vers'), 'new'); 109 110 db.prepare("UPDATE ap_timeline SET content = '<p>anders</p>' WHERE id = 'vers'").run(); 111 assert.equal(stand('vers'), 'updated'); 112 113 db.prepare("DELETE FROM ap_timeline WHERE id = 'vers'").run(); 114 assert.equal(stand('vers'), 'deleted'); 115 }); 116 117 test('het is een STAND en geen logboek: bewerkt-en-toen-weg leest als weg', () => { 118 // Een rij per bericht, niet een rij per gebeurtenis. Vijf keer bewerken blijft 119 // één rij, en dat is waarom er niets te snoeien valt. Mijn eerste test ging uit 120 // van een log en viel daar terecht over. 121 const alles = AP.feedChangesSince('me', 0); 122 const perUri = new Set(alles.map((r) => r.object_uri)); 123 assert.equal(alles.length, perUri.size, 'geen enkel bericht komt twee keer voor'); 59 124 }); 60 125
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)