Ignore:
Timestamp:
08/08/2026 10:09:50 PM (4 weeks ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
8179fdb
Parents:
1d76e0e
git-author:
Robin <roboburr@…> (08/08/2026 10:09:48 PM)
git-committer:
roboburr <roboburr@…> (08/08/2026 10:09:50 PM)
Message:

Stilte hoort niets te kosten: 304 op de guardian-wachtrijen (Barts punt, 9-8)

Bart wees op wat er al bestond: de inbox stuurt een 304 als er niets veranderd
is (since + wait), en de guardian-wachtrijen deden dat niet. Die stuurden bij
elke verversing de hele lijst terug -- bij honderd wards veertienhonderd
objecten. Over de lijn valt dat mee (2,8 KB gzip), maar het OPBOUWEN en parsen is
wat een telefoon merkt, en dat is precies de oude data die je elke keer weer
terugkrijgt.

En zijn punt over de waarschuwing klopte ook: de app ververst al voor de
guardian, tachtig keer per uur. Iemand waarschuwen voor een gewoonte die de app
zelf heeft is de verkeerde kant op redeneren.

EEN INHOUDS-ETAG, GEEN CURSOR. Een cursor vraagt een tweede beschrijving van
wanneer iets "veranderd" is, en die kan uit de pas lopen met wat er werkelijk in
het antwoord staat; een hash van het antwoord zelf kan dat per definitie niet.
De server bouwt het antwoord nog steeds -- wat we besparen is de overdracht en
het parsen.

NOOIT 304 OP EEN LEEG ANTWOORD, dezelfde les als de '0'-uitzondering bij de
inbox: gaat er bij het opbouwen iets mis en komt er een lege lijst uit, dan is
die hash ook stabiel en kijkt een client voor eeuwig naar niets. Daar staat een
toets op, en de mutatie maakt hem rood.

no-cache betekent niet "niet bewaren" maar "bewaar en vraag na" -- zonder dat
stuurt een browser geen If-None-Match en is de ETag decoratie.

Vijf toetsen, twee mutaties gecontroleerd. Suite 761/761.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r1d76e0e rbfd9c73  
    211211
    212212const AP_CONTENT_TYPE = 'application/activity+json; charset=utf-8';
     213/**
     214 * Hetzelfde antwoord als de vorige keer? Dan 304 (Barts punt, 9-8).
     215 *
     216 * De inbox doet dit al met `since` + `wait`, en de guardian-wachtrijen niet: die
     217 * stuurden bij elke verversing de hele lijst terug, ook als er niets veranderd
     218 * was. Bij honderd wards is dat 217 KB JSON die de telefoon opnieuw moet
     219 * parsen -- over de lijn valt het mee (2,8 KB gzip), maar het OPBOUWEN van
     220 * veertienhonderd objecten is wat je merkt.
     221 *
     222 * EEN INHOUDS-ETAG, geen cursor. Een cursor vraagt een tweede beschrijving van
     223 * wanneer iets "veranderd" is, en die kan uit de pas gaan lopen met wat er
     224 * werkelijk in het antwoord staat; een hash van het antwoord zelf kan dat per
     225 * definitie niet. De server bouwt het antwoord nog steeds (26 ms) -- wat we
     226 * besparen is de overdracht en het parsen.
     227 *
     228 * NOOIT 304 OP EEN LEEG ANTWOORD. Dezelfde les als de '0'-uitzondering bij de
     229 * inbox: gaat er bij het opbouwen iets mis en komt er een lege lijst uit, dan is
     230 * die hash ook stabiel, en zou een client voor eeuwig 304 krijgen op niets.
     231 */
     232export function etagFor(body) {
     233  return `"${crypto.createHash('sha256').update(body).digest('base64url').slice(0, 27)}"`;
     234}
     235
     236export function sendMaybe304(req, res, obj, { cacheControl, contentType } = {}) {
     237  const body = JSON.stringify(obj);
     238  const leeg = !obj || (Array.isArray(obj.orderedItems) && obj.orderedItems.length === 0);
     239  res.set('Vary', 'Authorization');
     240  if (!leeg) {
     241    const tag = etagFor(body);
     242    res.set('ETag', tag);
     243    if (req.headers['if-none-match'] === tag) return res.status(304).end();
     244  }
     245  res.type(contentType || AP_CONTENT_TYPE);
     246  // `no-cache` betekent NIET "niet bewaren": de client bewaart het antwoord en
     247  // vraagt elke keer of het nog klopt. Precies wat we willen -- zonder dit
     248  // stuurt een browser geen If-None-Match en is de ETag decoratie.
     249  res.set('Cache-Control', cacheControl || 'private, no-cache');
     250  return res.send(body);
     251}
     252
    213253export function sendAP(res, obj, cacheControl) {
    214254  res.type(AP_CONTENT_TYPE);
     
    63906430  getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
    63916431  deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
    6392   proposeGate, getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio,
     6432  sendMaybe304, etagFor, proposeGate, getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio,
    63936433  linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections,
    63946434  noteVisibility, belongsInTimeline, playerUrlFor, isRejectedObject, rejectInteraction, interactionReportTarget,
Note: See TracChangeset for help on using the changeset viewer.