source: Klonkt/test/feed-wait.test.js@ d14bf1c

main
Last change on this file since d14bf1c was d14bf1c, checked in by roboburr <roboburr@…>, 5 weeks ago

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.

  • Property mode set to 100644
File size: 9.1 KB
Line 
1// Wachten op nieuws, als uitbreiding van de inbox-lezing (shaer-n05).
2//
3// Geen tweede endpoint en geen seintje-formaat: dezelfde lezing, die desgevraagd
4// even blijft hangen. Wat hier bewaakt wordt is de MERKSTEEN -- als die niet
5// beweegt bij iets dat de inbox wel zou tonen, blijft een wachtende client
6// slapen terwijl er nieuws is. Dat is erger dan niet wachten, want het lijkt te
7// werken.
8//
9// Run: npm test
10
11import { test } from 'node:test';
12import assert from 'node:assert/strict';
13
14process.env.DATABASE_PATH = ':memory:';
15process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
16
17const dbMod = await import('../src/config/database.js');
18const db = dbMod.default;
19dbMod.initializeDatabase();
20const AP = await import('../src/services/ActivityPubService.js');
21
22db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
23 .run('u1', 'u1', 'u1@t', 'x', 'god');
24db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
25db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s2', 'buur', 'Buur', 'u1');
26db.prepare('INSERT INTO posts (id, site_id, slug, author_id, title, content, status) VALUES (?,?,?,?,?,?,?)')
27 .run('p1', 's1', 'mijn-post', 'u1', 'Post', '<p>x</p>', 'published');
28
29let n = 0;
30const nieuwePost = (slug = 'me') => db.prepare(
31 "INSERT INTO ap_timeline (id, slug, author_uri, content) VALUES (?,?,'https://r.test/u/a','<p>x</p>')",
32).run(`https://r.test/n/${++n}`, slug);
33
34test('een verse tijdlijnpost beweegt de merksteen', () => {
35 const voor = AP.feedCursor('me');
36 nieuwePost();
37 assert.notEqual(AP.feedCursor('me'), voor);
38});
39
40test('en zo ook de andere drie poten van de inbox', () => {
41 // De inbox voegt vier bronnen samen. Ontbreekt er een in de merksteen, dan
42 // wordt een client niet wakker van precies dat soort nieuws.
43 const bericht = () => db.prepare(
44 "INSERT INTO ap_mentions (slug, object_uri, actor_uri, content) VALUES ('me',?, 'https://r.test/u/a','<p>hoi</p>')",
45 ).run(`https://r.test/m/${++n}`);
46 const antwoord = () => db.prepare(
47 `INSERT INTO ap_interactions (kind, post_id, object_uri, actor_uri, content)
48 VALUES ('reply','p1',?, 'https://r.test/u/a','<p>hoi</p>')`,
49 ).run(`https://r.test/r/${++n}`);
50 const verstuurd = () => db.prepare(
51 "INSERT INTO ap_outbox (id, site_slug, post_id, content) VALUES (?,'me','p1','<p>x</p>')",
52 ).run(`o${++n}`);
53
54 for (const [naam, doen] of [['bericht', bericht], ['antwoord', antwoord], ['verstuurd', verstuurd]]) {
55 const voor = AP.feedCursor('me');
56 doen();
57 assert.notEqual(AP.feedCursor('me'), voor, `${naam} hoort de merksteen te bewegen`);
58 }
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');
124});
125
126test('nieuws van een ANDER account laat je merksteen staan', () => {
127 const voor = AP.feedCursor('me');
128 nieuwePost('buur');
129 assert.equal(AP.feedCursor('me'), voor, 'anders wordt iedereen wakker van andermans post');
130});
131
132test('is er al iets veranderd, dan wordt er niet gewacht', async () => {
133 const oud = AP.feedCursor('me');
134 nieuwePost();
135 const begin = Date.now();
136 const uit = await AP.waitForFeedChange('me', { since: oud, waitMs: 5000, tickMs: 20 });
137 assert.equal(uit.changed, true);
138 assert.equal(uit.waited, false, 'meteen antwoorden, niet eerst de tijd volmaken');
139 assert.ok(Date.now() - begin < 500);
140});
141
142test('zonder sinds is het gewoon de huidige stand', async () => {
143 const uit = await AP.waitForFeedChange('me', { waitMs: 5000, tickMs: 20 });
144 assert.equal(uit.changed, false);
145 assert.equal(uit.waited, false);
146 assert.equal(uit.cursor, AP.feedCursor('me'));
147});
148
149test('wachten wordt afgebroken zodra er iets binnenkomt', async () => {
150 const oud = AP.feedCursor('me');
151 const begin = Date.now();
152 setTimeout(() => nieuwePost(), 60);
153 const uit = await AP.waitForFeedChange('me', { since: oud, waitMs: 4000, tickMs: 20 });
154 assert.equal(uit.changed, true);
155 assert.equal(uit.waited, true);
156 assert.ok(Date.now() - begin < 2000, 'hij hoort wakker te worden, niet de tijd vol te maken');
157 assert.notEqual(uit.cursor, oud, 'en de nieuwe merksteen komt mee');
158});
159
160test('gebeurt er niets, dan komt hij leeg terug binnen de tijd', async () => {
161 const oud = AP.feedCursor('me');
162 const begin = Date.now();
163 const uit = await AP.waitForFeedChange('me', { since: oud, waitMs: 150, tickMs: 20 });
164 assert.equal(uit.changed, false);
165 assert.equal(uit.waited, true);
166 assert.equal(uit.cursor, oud);
167 assert.ok(Date.now() - begin >= 100, 'hij hoort wel echt gewacht te hebben');
168 assert.ok(Date.now() - begin < 3000, 'en niet langer dan gevraagd');
169});
170
171test('ophangen breekt het wachten af', async () => {
172 // Zonder dit blijft er een timer draaien voor een client die er niet meer is.
173 const oud = AP.feedCursor('me');
174 const ac = new AbortController();
175 const begin = Date.now();
176 setTimeout(() => ac.abort(), 60);
177 const uit = await AP.waitForFeedChange('me', { since: oud, waitMs: 4000, tickMs: 20, signal: ac.signal });
178 assert.equal(uit.changed, false);
179 assert.ok(Date.now() - begin < 2000);
180});
181
182test('meer dan vier wachters tegelijk: de rest krijgt meteen antwoord', async () => {
183 // Een client met een kapotte herverbind-lus mag de instance niet vastzetten.
184 // De overtolligen krijgen geen fout maar gewoon de huidige stand.
185 const oud = AP.feedCursor('me');
186 const lopend = [];
187 for (let i = 0; i < 4; i++) lopend.push(AP.waitForFeedChange('me', { since: oud, waitMs: 300, tickMs: 20 }));
188 await new Promise((r) => setTimeout(r, 30));
189 const vijfde = await AP.waitForFeedChange('me', { since: oud, waitMs: 4000, tickMs: 20 });
190 assert.equal(vijfde.busy, true);
191 assert.equal(vijfde.waited, false);
192 await Promise.all(lopend);
193 // En daarna is er weer plek: de teller moet netjes teruglopen.
194 const daarna = await AP.waitForFeedChange('me', { since: oud, waitMs: 60, tickMs: 20 });
195 assert.notEqual(daarna.busy, true);
196});
Note: See TracBrowser for help on using the repository browser.