Changeset e667d26 in Klonkt


Ignore:
Timestamp:
08/09/2026 07:28:31 AM (4 weeks ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
c832064
Parents:
82c3356
git-author:
Robin <roboburr@…> (08/09/2026 07:28:29 AM)
git-committer:
roboburr <roboburr@…> (08/09/2026 07:28:31 AM)
Message:

Een levende klok in het antwoord maakte de lange poll een lus (9-8)

Barts melding: de browser komt in een feedback-lus van ongeveer een seconde, en
de scrollpositie wordt vermalen. Dat is mijn fout van een uur eerder, en de
oorzaak is scherper dan hij lijkt.

Het paneel kreeg gisteren een inhouds-ETag, en de paneelstate bevat -- bevatte --
ageMs: de leeftijd van de oudste oppik, uitgerekend als now - tijdstip.
Daardoor verschilde elk antwoord een paar milliseconden van het vorige, kon de
ETag nooit gelijk zijn, kwam de 304 nooit, keerde de lange poll meteen terug, en
tekende het paneel zich stuk terwijl Bart scrollde.

EEN LEVENDE KLOK MAAKT EEN ANTWOORD ONVERGELIJKBAAR MET ZICHZELF. Dat is de les,
en hij raakt elk veld dat we ooit nog toevoegen. Er staat nu een toets op die
hetzelfde antwoord twee keer opvraagt met een halve seconde ertussen en eist dat
het identiek is.

De reparatie zit bij de BRON, niet in de ETag: het antwoord draagt nu het
TIJDSTIP (oldestPickupAt) en de client rekent de leeftijd uit. Dat is ook wat het
altijd al was -- een weergavedetail. De ETag uitzonderingen laten maken zou een
tweede beschrijving zijn van "wat telt als veranderd", precies wat ik gisteren
als argument tegen een cursor gebruikte.

TWEE GRENDELS ERBIJ, aan de kant die de gebruiker merkt:

  • Zelfde inhoud wordt niet hertekend. De 304 hoort dit af te vangen, maar een enkel onstabiel veld zette het scherm aan het knipperen; dit vangt de volgende.
  • Een lange poll die binnen twee seconden terugkeert wacht drie seconden voordat hij opnieuw vraagt. Een poll die niet wacht mag nooit een lus worden -- anders vervangt een defect een tik per 45 seconden door duizend per minuut.

En de scrollpositie overleeft een verversing: meten en terugzetten rond
renderAll. Dat is het verschil tussen een scherm dat bijwerkt en een scherm dat
onder je handen wegschuift.

Suite 782/782; met de klok terug valt hij om.

Files:
5 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/guardian.js

    r82c3356 re667d26  
    108108   */
    109109  function helpState(h) {
    110     var st = h.state || { open: true, pickedUpBy: [], handled: null, ageMs: null };
     110    var st = h.state || { open: true, pickedUpBy: [], handled: null, oldestPickupAt: null };
    111111    var box = el('div', 'g-help-state');
    112112
     
    133133    // Oud, maar niet weg. Dit is het verschil tussen 'er is iemand mee bezig' en
    134134    // 'er was ooit iemand mee bezig'.
    135     if (st.ageMs != null && st.ageMs > 3600000) {
    136       box.appendChild(el('div', 'g-help-age small', ago(st.ageMs)));
     135    // De leeftijd wordt HIER uitgerekend, niet op de server: een verschil met
     136    // `now` in het antwoord maakt dat antwoord elke milliseconde anders, en dan
     137    // kan de ETag nooit gelijk zijn.
     138    var oud = st.oldestPickupAt ? (Date.now() - Date.parse(st.oldestPickupAt)) : null;
     139    if (oud != null && !isNaN(oud) && oud > 3600000) {
     140      box.appendChild(el('div', 'g-help-age small', ago(oud)));
    137141    }
    138142
     
    894898  }
    895899
     900
     901  // De scrollpositie overleeft een verversing (Barts melding, 9-8). renderAll
     902  // bouwt lijsten opnieuw op; staat de pagina daarbij even korter, dan springt
     903  // de browser omhoog en ben je kwijt waar je was. Meten en terugzetten is
     904  // genoeg -- en het is het verschil tussen een scherm dat bijwerkt en een
     905  // scherm dat onder je handen wegschuift.
     906  function metBehoudVanScroll(fn) {
     907    var y = window.scrollY || document.documentElement.scrollTop || 0;
     908    fn();
     909    if (y) window.scrollTo(0, y);
     910  }
     911
    896912  function renderAll() { renderHelp(); renderPending(); renderWards(); }
    897913
     
    963979    return fetch('/guardian/api/state?site=' + encodeURIComponent(S.site))
    964980      .then(function (r) { return r.json(); })
    965       .then(function (s) { if (s && !s.error) { S = s; T = s.strings || T; renderAll(); } })
     981      .then(function (s) { if (s && !s.error) { S = s; T = s.strings || T; metBehoudVanScroll(renderAll); } })
    966982      .then(loadFeed).then(loadFollowReqs);
    967983  }
     
    978994  // opnieuw getekend.
    979995  var pollBezig = false;
     996  var laatsteState = null;   // de vorige body, om onnodig hertekenen te herkennen
     997
    980998  function longPoll() {
    981999    if (pollBezig || document.hidden) return;
    9821000    pollBezig = true;
     1001    var t0 = Date.now();
    9831002    fetch('/guardian/api/state?wait=25&site=' + encodeURIComponent(S.site))
    9841003      .then(function (r) {
    9851004        pollBezig = false;
    9861005        if (r.status === 304) return null;          // stilte
    987         return r.json().then(function (s) {
    988           if (s && !s.error) { S = s; T = s.strings || T; renderAll(); }
     1006        return r.text().then(function (tekst) {
     1007          // ZELFDE INHOUD, NIET HERTEKENEN. De 304 zou dit al moeten afvangen,
     1008          // maar een enkel veld dat per verzoek verandert (er stond een levende
     1009          // klok in) maakt elke ETag anders, en dan tekent het paneel zichzelf
     1010          // stuk terwijl je scrollt. Dit is de tweede grendel, aan de kant die
     1011          // de gebruiker merkt.
     1012          if (tekst === laatsteState) return null;
     1013          laatsteState = tekst;
     1014          var s; try { s = JSON.parse(tekst); } catch (e) { return null; }
     1015          if (s && !s.error) { S = s; T = s.strings || T; metBehoudVanScroll(renderAll); }
    9891016          return loadFeed().then(loadFollowReqs);
    9901017        });
    9911018      })
    992       .then(function () { setTimeout(longPoll, 200); })
     1019      .then(function () {
     1020        // EEN LANGE POLL DIE METEEN TERUGKEERT MAG GEEN LUS WORDEN. Wachtte de
     1021        // server niet (een fout, een tussenliggende proxy, iets wat we nog niet
     1022        // weten), dan houden we zelf de rem erop. Anders vervangt een defect de
     1023        // ene tik van 45 seconden door duizend per minuut.
     1024        var snel = Date.now() - t0 < 2000;
     1025        setTimeout(longPoll, snel ? 3000 : 200);
     1026      })
    9931027      .catch(function () {
    9941028        // Netwerk weg of server herstart: niet meteen opnieuw beuken.
  • src/routes/guardian.js

    r82c3356 re667d26  
    9999    // niet is, is de gevaarlijke fout -- niet andersom.
    100100    state: Guardianship.help.withWardship(
    101       helpStaat.get(h.object_uri) || { open: true, pickedUpBy: [], handled: null, ageMs: null },
     101      helpStaat.get(h.object_uri) || { open: true, pickedUpBy: [], handled: null, oldestPickupAt: null },
    102102      mijnWards.has(h.actor_uri),
    103103    ),
  • src/services/guardianship/help.js

    r82c3356 re667d26  
    5252 * zijn zonder database of scherm.
    5353 *
    54  * `ageMs` is de leeftijd van de OUDSTE oppik. Daar tekent het scherm mee dat een
    55  * signaal oud wordt -- niets verdwijnt, maar je ziet wel dat er misschien niets
    56  * meer gebeurt.
     54 * `oldestPickupAt` is het TIJDSTIP van de oudste oppik, niet de leeftijd. Daar
     55 * tekent het scherm mee dat een signaal oud wordt -- niets verdwijnt, maar je
     56 * ziet wel dat er misschien niets meer gebeurt.
     57 *
     58 * EEN TIJDSTIP, GEEN LEEFTIJD, en dat is geen smaak. Hier stond `ageMs`, een
     59 * verschil met `now`, en dus veranderde dit antwoord elke milliseconde. Zodra
     60 * het paneel een ETag kreeg (9-8) kon die daardoor nooit meer gelijk zijn: de
     61 * 304 kwam nooit, de lange poll keerde meteen terug, en de browser kwam in een
     62 * lus van ongeveer een seconde waarin de scrollpositie werd vermalen. Een
     63 * levende klok in een antwoord maakt dat antwoord onvergelijkbaar met zichzelf.
     64 *
     65 * De leeftijd is een weergavedetail en wordt in de client uitgerekend.
    5766 */
    5867export function helpStatus(rows, now = Date.now()) {
     
    6776    handled: done ? { uri: done.guardian_uri, handle: done.guardian_handle || null, at: done.created_at } : null,
    6877    // Alleen betekenisvol zolang er niets is afgesloten.
    69     ageMs: (!done && oudste) ? Math.max(0, now - oudste) : null,
     78    oldestPickupAt: (!done && oudste) ? new Date(oudste).toISOString() : null,
    7079    // Waar het scherm op afgaat. Bij twijfel OPEN: een lege lijst, een rij die we
    7180    // niet kunnen lezen, wat dan ook -- alles wat geen expliciete afsluiting is,
  • src/services/guardianship/queues.js

    r82c3356 re667d26  
    226226    // niet is, is de gevaarlijke fout -- niet andersom.
    227227    state: help.withWardship(
    228       staat.get(r.object_uri) || { open: true, pickedUpBy: [], handled: null, ageMs: null },
     228      staat.get(r.object_uri) || { open: true, pickedUpBy: [], handled: null, oldestPickupAt: null },
    229229      mijn.has(r.actor_uri),
    230230    ),
  • test/guardian-help-state.test.js

    r82c3356 re667d26  
    6363  const s = help.helpStatus(rijen, Date.parse('2026-08-07T10:00:00.000Z'));
    6464  assert.equal(s.open, true);
    65   assert.equal(s.ageMs, 2 * 24 * 3600 * 1000, 'twee dagen oud, en dat hoort te zien te zijn');
     65  // Een TIJDSTIP, geen leeftijd: een verschil met `now` maakt het antwoord elke
     66  // milliseconde anders, en dan kan de ETag van het paneel nooit gelijk zijn --
     67  // dat leverde een lus op waarin de browser zichzelf bleef verversen.
     68  assert.equal(s.oldestPickupAt, '2026-08-05T10:00:00.000Z');
    6669});
    6770
     
    7174  assert.equal(s.open, false);
    7275  assert.equal(s.handled.handle, '@opa@opa.test');
    73   assert.equal(s.ageMs, null, 'de leeftijd van een oppik zegt niets meer als het klaar is');
     76  assert.equal(s.oldestPickupAt, null, 'het tijdstip van een oppik zegt niets meer als het klaar is');
    7477});
    7578
     
    146149  assert.deepEqual(help.withWardship(basis, true), basis);
    147150});
     151
     152test('het antwoord verandert NIET als je het twee keer opvraagt', () => {
     153  // De fout die Barts browser in een lus bracht (9-8): hier stond ageMs, een
     154  // verschil met `now`. Daardoor was elk antwoord anders, kon de ETag van het
     155  // paneel nooit gelijk zijn, kwam de 304 nooit, en keerde de lange poll meteen
     156  // terug -- inclusief een hertekening die de scrollpositie vermaalde.
     157  //
     158  // Een levende klok in een antwoord maakt dat antwoord onvergelijkbaar met
     159  // zichzelf. Deze toets bewaakt precies dat.
     160  const rijen = [{ kind: 'pickup', guardian_uri: OMA, created_at: '2026-08-05T10:00:00.000Z' }];
     161  const a = JSON.stringify(help.helpStatus(rijen, Date.parse('2026-08-05T12:00:00.000Z')));
     162  const b = JSON.stringify(help.helpStatus(rijen, Date.parse('2026-08-05T12:00:00.500Z')));
     163  assert.equal(a, b, 'een halve seconde later is hetzelfde antwoord');
     164});
Note: See TracChangeset for help on using the changeset viewer.