Changeset bac4bf3 in Klonkt


Ignore:
Timestamp:
08/13/2026 11:55:04 AM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
783b9ff
Parents:
7e9d0ea
git-author:
Robin <roboburr@…> (08/13/2026 11:54:36 AM)
git-committer:
Robin <roboburr@…> (08/13/2026 11:55:04 AM)
Message:

Paginering die echt pagineert (shaer-sk4)

Robin zag dat elke ?page= dezelfde inhoud gaf. Dat klopte, en het was geen halve
implementatie maar een omhulsel: pagedCollection veranderde alleen de VORM en
sneed nooit. first en last wezen allebei naar ?page=1, en de routes lazen
!!req.query.page -- of de parameter er STAAT, niet welke. Live gaf ?page=2 en
?page=99 dezelfde acht items, en noemden zichzelf pagina 1.

Nu: echt snijden, met next en prev, en een pagina die zijn eigen nummer
draagt. Een pagina voorbij het einde is LEEG en zegt dat ook -- hem naar de
laatste terugbuigen zou opnieuw een antwoord zijn dat over zichzelf liegt.

DE WORTEL HOUDT ZIJN ITEMS INLINE, en dat is geen slordigheid maar de reden dat
dit veilig is. Shaer leest een document en volgt next niet (KlonktClient.swift
orderedItems). Werd de wortel nu leeg, dan kreeg elke draaiende app nul items en
geen foutmelding -- dezelfde stille val waar ik op 10 augustus bij Funkwhale zelf
in trapte. Eerst de clients leren pagineren, dan pas de wortel afslanken.

WAT WEL EN NIET GEPAGINEERD IS. Volgers en following pagineren nu volledig: die
lijsten zitten al in het geheugen, dus dat kost niets extra. De outbox krijgt de
juiste VORM maar niet meer diepte: de route kapt al op twintig rijen in SQL.
Echt doorbladeren vraagt daar een LIMIT/OFFSET, en dat is lastiger dan bij
volgers omdat posts en tracks op datum door elkaar gevlochten worden en uit twee
tabellen komen -- een UNION met een offset erover, geen tweede slice. Dat staat
als naad in de code en op de bead, niet weggemoffeld.

Zeven tests, waaronder de klacht zelf: pagina 2 geeft ANDERE items dan pagina 1.

Co-Authored-By: Claude Opus 5 <noreply@…>

Files:
1 added
3 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    r7e9d0ea rbac4bf3  
    2828
    2929const router = express.Router();
     30
     31/**
     32 * Welke pagina vraagt de lezer? (shaer-sk4)
     33 *
     34 * Hier stond `!!req.query.page` -- of de parameter er STAAT, niet welke. Daardoor
     35 * gaf ?page=2 en ?page=99 allemaal pagina 1, en noemde het antwoord zichzelf ook
     36 * nog pagina 1. Onleesbaar getal of geen parameter: dan de wortel.
     37 */
     38function paginaNr(req) {
     39  if (req.query.page === undefined) return false;
     40  const n = Math.floor(Number(req.query.page));
     41  return Number.isFinite(n) && n > 0 ? n : 1;
     42}
    3043// The whole fediverse layer can be turned off (solo "no federation" mode):
    3144// then /ap/*, WebFinger and NodeInfo are simply gone — the site is undiscoverable
     
    182195  });
    183196  if (audience === 'blocked') {
    184     return AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, [], [], { page: !!req.query.page }), 'private, no-store');
     197    return AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, [], [], { page: paginaNr(req) }), 'private, no-store');
    185198  }
    186199  const fanClause = audience === 'friend' ? '' : "AND (fan_only IS NULL OR fan_only = 0)";
     
    192205  // De tracks gaan mee voor iedereen die de deur door mag; de blocked-tak
    193206  // hierboven levert bewust een outbox ZONDER posts en zonder tracks.
    194   const ob = AP.buildOutbox(baseUrl(req), site, posts, AP.siteOpenTracks(site.id), { page: !!req.query.page });
     207  const ob = AP.buildOutbox(baseUrl(req), site, posts, AP.siteOpenTracks(site.id), { page: paginaNr(req) });
    195208  if (audience === 'friend') {
    196209    // The owner's app builds its feed from this leg, and every note here is
     
    986999    // Default = bare references; enrich only when the client asks (FEP-9876).
    9871000    const items = wantsEnriched(req, res) ? uris.map((u) => AP.buildActorRef(site.slug, u)) : uris;
    988     return AP.sendAP(res, AP.buildFollowers(baseUrl(req), site, items.length, items));
     1001    return AP.sendAP(res, AP.buildFollowers(baseUrl(req), site, items.length, items, { page: paginaNr(req) }));
    9891002  }
    9901003  const n = db.prepare('SELECT COUNT(*) n FROM ap_followers WHERE slug = ?').get(site.slug).n;
    991   AP.sendAP(res, AP.buildFollowers(baseUrl(req), site, n));
     1004  AP.sendAP(res, AP.buildFollowers(baseUrl(req), site, n, null, { page: paginaNr(req) }));
    9921005});
    9931006
     
    10051018      items = enrich ? uris.map((u) => AP.buildActorRef(site.slug, u)) : uris;
    10061019    } catch { /* table may not exist */ }
    1007     return AP.sendAP(res, AP.buildFollowing(baseUrl(req), site, items.length, items));
     1020    return AP.sendAP(res, AP.buildFollowing(baseUrl(req), site, items.length, items, { page: paginaNr(req) }));
    10081021  }
    10091022  let n = 0;
    10101023  try { n = db.prepare("SELECT COUNT(*) n FROM ap_following WHERE slug = ? AND status = 'accepted'").get(site.slug).n; } catch { /* table may not exist */ }
    1011   AP.sendAP(res, AP.buildFollowing(baseUrl(req), site, n));
     1024  AP.sendAP(res, AP.buildFollowing(baseUrl(req), site, n, null, { page: paginaNr(req) }));
    10121025});
    10131026
  • src/services/ActivityPubService.js

    r7e9d0ea rbac4bf3  
    10321032    .sort((a, b) => wanneer(b) - wanneer(a))
    10331033    .slice(0, MAX_OUTBOX);
     1034  // WAT HIER NOG NIET GEPAGINEERD IS, en dat hoort genoemd (shaer-sk4): deze
     1035  // lijst is al door de route op twintig rijen afgekapt, dus pagina 2 is leeg.
     1036  // Echt doorbladeren vraagt een LIMIT/OFFSET in SQL -- en dat is hier lastiger
     1037  // dan bij volgers, want posts en tracks worden op DATUM door elkaar gevlochten
     1038  // en komen uit twee tabellen. Dat vraagt een UNION met een offset erover, geen
     1039  // tweede slice. De vorm klopt nu wel: pagina 2 zegt eerlijk dat hij leeg is en
     1040  // biedt geen `next` aan, in plaats van pagina 1 nog eens te geven.
    10341041  // GEPAGINEERD, ook al past alles op een pagina (Funkwhale, 11-8).
    10351042  //
     
    10491056// account owner (a C2S bearer scoped to this site) gets the real actor URIs via
    10501057// `items`, so their own client can build a friends list.
    1051 export function buildFollowers(base, site, count, items = null) {
     1058export function buildFollowers(base, site, count, items = null, { page = false } = {}) {
    10521059  const id = `${actorId(base, site.slug)}/followers`;
    10531060  // count-only for the public; full for the owner
    1054   return pagedCollection(id, items || [], { totalItems: items ? items.length : (count || 0) });
     1061  return pagedCollection(id, items || [], { totalItems: items ? items.length : (count || 0), page });
    10551062}
    10561063
    10571064// The accounts this site follows — count only, mirroring buildFollowers. The spec lists
    10581065// `following` as a standard actor property; Hubzilla/Friendica + crawlers expect it.
    1059 export function buildFollowing(base, site, count, items = null) {
     1066export function buildFollowing(base, site, count, items = null, { page = false } = {}) {
    10601067  const id = `${actorId(base, site.slug)}/following`;
    10611068  // count-only for the public; full for the owner
    1062   return pagedCollection(id, items || [], { totalItems: items ? items.length : (count || 0) });
     1069  return pagedCollection(id, items || [], { totalItems: items ? items.length : (count || 0), page });
    10631070}
    10641071
  • src/services/ap-core.js

    r7e9d0ea rbac4bf3  
    212212 *   extra       velden die op de wortel horen (attributedTo, shaer:*)
    213213 */
    214 export function pagedCollection(id, items, { totalItems, page = false, extra = {} } = {}) {
     214/** Hoeveel items op een pagina. Gelijk aan wat de outbox vroeger als KAP had. */
     215export const PAGINA_GROOTTE = 20;
     216
     217/**
     218 * Een collectie, met ECHTE paginering (shaer-sk4).
     219 *
     220 * Wat hier stond was een omhulsel: `page` veranderde alleen de VORM en er werd
     221 * nooit gesneden. `first` en `last` wezen allebei naar ?page=1, elke ?page=N gaf
     222 * dezelfde items, en pagina 99 noemde zichzelf pagina 1. Robin zag dat de
     223 * pagina's identiek bleven; dit is waarom.
     224 *
     225 * DE WORTEL BLIJFT ZIJN ITEMS INLINE DRAGEN, en dat is geen slordigheid maar de
     226 * hele reden dat dit veilig is. Shaer leest één document en volgt `next` niet;
     227 * zou de wortel nu leeg worden, dan kreeg elke draaiende app nul items en geen
     228 * foutmelding. Eerst de clients leren pagineren, dan pas de wortel afslanken.
     229 *
     230 * Een pagina VOORBIJ het einde is leeg en zegt dat ook -- met zijn eigen nummer
     231 * en zonder `next`. Hem naar de laatste pagina terugbuigen zou opnieuw een
     232 * antwoord zijn dat over zichzelf liegt.
     233 */
     234export function pagedCollection(id, items, { totalItems, page = false, perPage = PAGINA_GROOTTE, extra = {} } = {}) {
    215235  const lijst = items || [];
    216236  const telling = totalItems === undefined ? lijst.length : totalItems;
    217   const eerste = `${id}?page=1`;
     237  const grootte = Math.max(1, Number(perPage) || PAGINA_GROOTTE);
     238  const paginas = Math.max(1, Math.ceil(lijst.length / grootte));
     239  const url = (n) => `${id}?page=${n}`;
     240
    218241  if (page) {
     242    const n = Math.max(1, Math.floor(Number(page)) || 1);
     243    const deel = lijst.slice((n - 1) * grootte, n * grootte);
    219244    return {
    220245      '@context': AP_CONTEXT,
    221       id: eerste,
     246      id: url(n),
    222247      type: 'OrderedCollectionPage',
    223248      partOf: id,
    224249      totalItems: telling,
    225       orderedItems: lijst,
     250      ...(n > 1 ? { prev: url(n - 1) } : {}),
     251      ...(n < paginas ? { next: url(n + 1) } : {}),
     252      orderedItems: deel,
    226253    };
    227254  }
     
    232259    ...extra,
    233260    totalItems: telling,
    234     first: eerste,
    235     last: eerste,
     261    first: url(1),
     262    last: url(paginas),
    236263    orderedItems: lijst,
    237264  };
Note: See TracChangeset for help on using the changeset viewer.