Changes in / [44c9f3f:e5fc801] in Klonkt


Ignore:
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    r44c9f3f re5fc801  
    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  */
    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   }
    854773}
    855774
  • src/services/ActivityPubService.js

    r44c9f3f re5fc801  
    35023502 */
    35033503export function feedCursor(slug) {
    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 []; }
     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}`;
    35253513}
    35263514
     
    53765364  buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured,
    53775365  followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins,
    5378   feedCursor, feedChangesSince, waitForFeedChange,
     5366  feedCursor, waitForFeedChange,
    53795367  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote, noteAudience, mayReadNote,
    53805368  listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
  • test/feed-wait.test.js

    r44c9f3f re5fc801  
    5757    assert.notEqual(AP.feedCursor('me'), voor, `${naam} hoort de merksteen te bewegen`);
    5858  }
    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');
    12459});
    12560
Note: See TracChangeset for help on using the changeset viewer.