Changeset d14bf1c in Klonkt for src/config/database.js


Ignore:
Timestamp:
08/07/2026 09:14:32 AM (5 weeks ago)
Author:
roboburr <roboburr@…>
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)
Message:

De merksteen wordt een koppeltabel in plaats van een afleiding (shaer-n05)

Barts onderbuik, en hij had gelijk. De merksteen leunde op MAX(rowid) over vier
tabellen: een TOEVALLIGE eigenschap, geen feit dat ergens is opgeschreven.
Dezelfde fout als reacties uitlezen uit ap_timeline.liked, en met dezelfde soort
gebreken -- hij zag aangroei maar geen verandering, en hij kon achteruit lopen.

Nu ap_feed_state: een rij per bericht per tijdlijn, met een oplopende rev en een
kind (new | updated | deleted). Dat beantwoordt drie vragen die anders drie eigen
oplossingen zouden krijgen: is er iets veranderd sinds N, WAT is er veranderd, en
is dit bericht bewerkt. Die derde maakt de "bewerkt"-markering uit shaer-of7
straks een leesactie in plaats van een bouwsel.

Een STAND en geen logboek: vijf keer bewerken blijft één rij, dus er groeit niets
en er valt niets te snoeien. Mijn eerste test ging uit van een log en viel daar
terecht over.

Bijgehouden door TRIGGERS, om dezelfde reden dat er geen emitter is: een trigger
zit in de database, dus geen codepad kan hem vergeten. De teller staat in een
eigen tabel en niet in MAX(rev), want die zou terugzakken zodra de hoogste rij
verdwijnt -- en dan denkt een client dat er niets gebeurd is.

DE VAL WAAR DIT BIJNA IN LIEP: een like schrijft ap_timeline.liked en een 🔁
schrijft .boosted. Zonder afbakening zou je eigen like het bericht als BEWERKT
merken en elke wachtende client wekken. Vandaar de UPDATE OF-kolomlijsten; die
zijn niet decoratief.

Nagemeten wat ik wilde zien voordat dit erin ging: een INSERT OR IGNORE die niets
doet vuurt geen trigger, en de trigger op ap_interactions joint correct via posts
naar sites (die tabel draagt zelf geen slug).

De cursor is nu een getal in plaats van "23.2.8.2". Een client met een oude
cursor krijgt meteen antwoord met de nieuwe; dat heelt zichzelf.

16 tests in feed-wait. Suite 544/544.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    rf2a7181 rd14bf1c  
    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
Note: See TracChangeset for help on using the changeset viewer.