source: Klonkt/src/services/guardianship/help.js@ e667d26

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

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.

  • Property mode set to 100644
File size: 6.9 KB
RevLine 
[a29c8c8]1/**
2 * Wie er op een hulpvraag af is, en wanneer hij is afgesloten (shaer-lgo).
3 *
4 * Een hulpvraag (FEP-633c 5.2.1) gaat naar ALLE guardians van een kind, die op
5 * verschillende servers zitten. Zonder gedeelde staat denken er twee dat de
6 * ander het oppakt -- en dat is precies het scenario waar de reddingsboei voor
7 * bestaat.
8 *
9 * DE FAALSTAND IS HIER NIET VEILIG, en dat maakt dit anders dan elke gate. Bij
10 * een gate is "dicht" het veilige antwoord. Hier is de faalstand "iedereen denkt
11 * dat het geregeld is", en dat is gevaarlijker dan geen markering. Daaruit volgt
12 * de regel die overal in dit bestand terugkomt: bij twijfel is een hulpvraag
13 * OPEN.
14 *
15 * Twee besluiten van Bart (7-8) zitten in de vorm:
16 *
17 * OPGEPIKT mag stapelen en vervalt niet, maar VEROUDERT zichtbaar. Twee mensen
18 * die tegelijk reageren op een kind is geen probleem; twee die allebei niets
19 * doen omdat de ander het "geclaimd" had, wel. En een signaal dat vanzelf
20 * verdwijnt laat een hulpvraag er onaangeroerd uitzien terwijl er iemand mee
21 * bezig is.
22 *
23 * AFGEHANDELD kent geen terugdraai. Sluiten gebeurt met een stevige
24 * bevestiging, en leeft de vraag daarna nog, dan wordt hij OPNIEUW GESTELD --
25 * een nieuwe hulpvraag. Er wordt niets herschreven, er wordt toegevoegd.
26 */
27
28import db from '../../config/database.js';
29
30let _s = null;
31function stmts() {
32 if (!_s) {
33 _s = {
34 ins: db.prepare(`INSERT OR IGNORE INTO ap_help_state (note_uri, guardian_uri, kind, guardian_handle)
35 VALUES (?,?,?,?)`),
36 forNote: db.prepare('SELECT * FROM ap_help_state WHERE note_uri = ? ORDER BY created_at ASC'),
37 forNotes: db.prepare('SELECT * FROM ap_help_state WHERE note_uri IN (SELECT value FROM json_each(?))'),
38 };
39 }
40 return _s;
41}
42
43/** Leg vast dat iemand deze hulpvraag heeft opgepikt of afgesloten. */
44export function record(noteUri, guardianUri, kind, handle = null) {
45 if (!noteUri || !guardianUri) return;
46 const k = kind === 'handled' ? 'handled' : 'pickup';
47 try { stmts().ins.run(noteUri, guardianUri, k, handle); } catch { /* nooit dragend */ }
48}
49
50/**
51 * De staat van een hulpvraag, uit zijn rijen. Puur, zodat de regels te toetsen
52 * zijn zonder database of scherm.
53 *
[e667d26]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.
[a29c8c8]66 */
67export function helpStatus(rows, now = Date.now()) {
68 const list = rows || [];
69 const pickups = list.filter((r) => r.kind === 'pickup');
70 const done = list.find((r) => r.kind === 'handled') || null;
71 const stamp = (r) => { const t = Date.parse(r.created_at); return isNaN(t) ? null : t; };
72 const oudste = pickups.map(stamp).filter((t) => t !== null).sort((a, b) => a - b)[0];
73 return {
74 // Namen erbij: "door wie" was de hele vraag. Zonder dat is het een vinkje.
75 pickedUpBy: pickups.map((r) => ({ uri: r.guardian_uri, handle: r.guardian_handle || null, at: r.created_at })),
76 handled: done ? { uri: done.guardian_uri, handle: done.guardian_handle || null, at: done.created_at } : null,
77 // Alleen betekenisvol zolang er niets is afgesloten.
[e667d26]78 oldestPickupAt: (!done && oudste) ? new Date(oudste).toISOString() : null,
[a29c8c8]79 // Waar het scherm op afgaat. Bij twijfel OPEN: een lege lijst, een rij die we
80 // niet kunnen lezen, wat dan ook -- alles wat geen expliciete afsluiting is,
81 // is een hulpvraag die nog op iemand wacht.
82 open: !done,
83 };
84}
85
[9ce14b8]86/**
87 * Een hulpvraag van iemand die je NIET MEER bewaakt.
88 *
89 * Het loslaat-scherm belooft dit al letterlijk: "je krijgt geen hulpvragen meer
90 * van ze". Nieuwe komen inderdaad niet meer binnen, maar wat er al lag bleef in
91 * de open lijst staan -- en was niet af te sluiten, want de markeerroute eist
92 * dat het nog je ward is en antwoordt anders met 403. De knop stond er dus wel
93 * en deed niets.
94 *
95 * Zo'n vraag is niet AFGEHANDELD -- dat zou een claim zijn over een kind waar je
96 * niets meer over te zeggen hebt, en die claim wordt ook nog rondgestuurd. Hij is
97 * niet meer van jou. Dat is een derde uitkomst en die hoort als zodanig te lezen.
98 *
99 * Veilig omdat een guardianship nooit bij de LAATSTE guardian eindigt (3.4,
100 * emancipatie): er blijft altijd iemand over voor wie de vraag wel open staat.
101 */
102export function withWardship(status, stillWard) {
103 if (stillWard) return status;
104 return { ...status, open: false, formerWard: true };
105}
106
[a29c8c8]107/** De staat van een hulpvraag zoals die nu is opgeslagen. */
108export function statusOf(noteUri, now = Date.now()) {
109 if (!noteUri) return helpStatus([], now);
110 try { return helpStatus(stmts().forNote.all(noteUri), now); } catch { return helpStatus([], now); }
111}
112
113/** Idem voor een hele lijst in een query, zodat een paneel geen N+1 wordt. */
114export function statusFor(noteUris, now = Date.now()) {
115 const uit = new Map();
116 const lijst = [...new Set((noteUris || []).filter(Boolean))];
117 if (!lijst.length) return uit;
118 let rijen = [];
119 try { rijen = stmts().forNotes.all(JSON.stringify(lijst)); } catch { rijen = []; }
120 const perNote = new Map();
121 for (const r of rijen) {
122 if (!perNote.has(r.note_uri)) perNote.set(r.note_uri, []);
123 perNote.get(r.note_uri).push(r);
124 }
125 for (const uri of lijst) uit.set(uri, helpStatus(perNote.get(uri) || [], now));
126 return uit;
127}
128
129/**
130 * De markering als bericht. Bewust een gewone directe note met een
131 * shaer:-markering, zoals de zwaai en de afwezigheidsmelding: dan reist het over
132 * de bestaande bezorging, en de WARD leest het als wat het is -- er komt iemand.
133 */
134export function markerNote({ id, me, noteUri, kind, to }) {
135 const k = kind === 'handled' ? 'handled' : 'pickup';
136 return {
137 id, type: 'Note', attributedTo: me, to,
138 [k === 'handled' ? 'shaer:helpHandled' : 'shaer:helpPickup']: noteUri,
139 content: k === 'handled'
140 ? '<p>Deze hulpvraag is afgehandeld.</p>'
141 : '<p>Ik kijk hiernaar.</p>',
142 };
143}
144
145/** Leest een binnengekomen note als markering, of null als hij er geen is. */
146export function parseMarker(object) {
147 if (!object || typeof object !== 'object') return null;
148 const pickup = object['shaer:helpPickup'];
149 const handled = object['shaer:helpHandled'];
150 if (typeof handled === 'string' && handled) return { kind: 'handled', noteUri: handled };
151 if (typeof pickup === 'string' && pickup) return { kind: 'pickup', noteUri: pickup };
152 return null;
153}
154
[9ce14b8]155export default { record, helpStatus, withWardship, statusOf, statusFor, markerNote, parseMarker };
Note: See TracBrowser for help on using the repository browser.