Changeset 44c9f3f in Klonkt


Ignore:
Timestamp:
08/07/2026 09:37:59 AM (5 weeks ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
31e63d1
Parents:
e5fc801 (diff), d14bf1c (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Merge feed-state: de merksteen als koppeltabel (shaer-n05)

Brengt d14bf1c binnen: feedCursor leest ap_feed_state in plaats van MAX(rowid)
over vier tabellen. main had e5fc801 (gate-melding in de Guardian PWA); die twee
raken elkaar alleen in feedCursor en de export-lijst.

Schone merge, geen conflict. Nagekeken op de gemergde boom: de oude
MAX(rowid)-cursor komt nul keer meer voor, de gate-melding en zijn test zijn
intact, en ap_seen_notes (shaer-e9g) staat er nog in beide bestanden. Suite
551/551 op de gemergde boom, gedraaid en niet alleen gelezen.

Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    re5fc801 r44c9f3f  
    771771  ensureColumn('ap_followers', 'handle', 'TEXT');  // @user@host
    772772  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 */
     800function 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  }
    773854}
    774855
  • src/services/ActivityPubService.js

    re5fc801 r44c9f3f  
    35023502 */
    35033503export 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 */
     3519export 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 []; }
    35133525}
    35143526
     
    53645376  buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured,
    53655377  followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins,
    5366   feedCursor, waitForFeedChange,
     5378  feedCursor, feedChangesSince, waitForFeedChange,
    53675379  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote, noteAudience, mayReadNote,
    53685380  listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
  • test/feed-wait.test.js

    re5fc801 r44c9f3f  
    5757    assert.notEqual(AP.feedCursor('me'), voor, `${naam} hoort de merksteen te bewegen`);
    5858  }
     59});
     60
     61test('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
     70test('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
     77test('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
     88test('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
     94test('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
     102test('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
     117test('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');
    59124});
    60125
Note: See TracChangeset for help on using the changeset viewer.