source: Klonkt/src/services/guardianship/queues.js@ f3a2556

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

De apps kunnen nu zien wat er voor een ward gated is (shaer-ahy.1)

Bart: iOS en Android op hetzelfde plan als de PWA. Dit is het blokkerende stuk --
zonder dit kan geen app een poort tonen.

wardsCollection gaf {id, handle, since}. Een app kon een ward dus wel TONEN maar
niets over hem zeggen, en dat is precies de helft van het antwoord op "wat mag
dit kind". Er zit nu shaer:gates bij: dezelfde rijen als het PWA-paneel, met
soort, drempel, lopend voorstel en omkeerbaarheid.

UIT DEZELFDE FUNCTIE, en dat is de kern. wardGates stond in routes/guardian.js
en daar kon alleen de PWA erbij. Een tweede berekening ernaast zou vroeg of laat
een ander antwoord geven op dezelfde vraag -- geen schoonheidsfoutje, maar twee
guardians die een verschillend beeld van hetzelfde kind krijgen. Verplaatst naar
queues.js; beide schermen lezen er nu uit, en een toets vergelijkt de twee
uitkomsten letterlijk met deepEqual.

Suite 688/688; zonder de nieuwe regel vallen er twee om.

  • Property mode set to 100644
File size: 8.2 KB
Line 
1/**
2 * Guardianship (FEP-633c) — the owner-only dashboard queues.
3 *
4 * Three OrderedCollections on the actor (shaer:queues), same contract as the
5 * Shaer test daemon so the iOS/Android dashboards read them as-is:
6 * - offers: pending handshake offers where I am a party (§3), with the full
7 * accept tally so the client shows the right action
8 * - follows: pending gated follows ON my wards (§5.3), Fase 2 (shaer-jdb)
9 * - wards: my committed wards
10 */
11import * as offers from './offers.js';
12import * as relations from './relations.js';
13import * as availability from './availability.js';
14import * as outgoing from './outgoing.js';
15import * as follows from './follows.js';
16import * as gated from './gated.js';
17import * as handshake from './handshake.js';
18
19const collection = (id, items) => ({
20 id, type: 'OrderedCollection', totalItems: items.length, orderedItems: items,
21});
22
23/** Pending offers where the local site is a party, each with its accept
24 * tally. The same collection carries the running lapses (§3.6.3) this
25 * account is a party to, exactly as the daemon serves them, so the Shaer
26 * clients render both without a second fetch. */
27export function offersCollection(id, slug, me) {
28 // §4.2: a handshake whose candidate could not be dereferenced is deferred,
29 // not decided, and the last Accept may already have landed — so nothing else
30 // would ever retry it. This poll is the schedule. Not awaited: the read
31 // answers with what is true now, and a retry that succeeds surfaces in the
32 // next one. `listForParty` settles closed windows on the way past.
33 handshake.retryDeferred(slug).catch(() => { /* the next read tries again */ });
34 const items = offers.listForParty(slug, me).map((o) => offers.queueItem(o, me));
35 items.push(...availability.lapseQueueItems(slug, me, Date.now()));
36 return collection(id, items);
37}
38
39/**
40 * Gate-verzoeken OP mijn wards die op mijn antwoord wachten (Guardianship Fase 2,
41 * shaer-jdb). Dit was een lege stub: de gating zelf werkt sinds shaer-hxg, maar
42 * werd nooit aan een C2S-client doorgegeven omdat de koers toen op de PWA lag.
43 *
44 * Twee bronnen, want een guardian kan wards op andere servers hebben en (nog)
45 * op deze:
46 * - ap_follow_reviews: de doorgestuurde kopie van een REMOTE ward
47 * - ap_pending_follows: een ward op deze instance
48 * Zie shaer-h6u: die tweede hoort op termijn ook over de lijn te gaan.
49 */
50export function followsCollection(id, slug, me) {
51 const items = follows.listReviewsByDirection(slug, 'incoming')
52 .map((r) => follows.reviewQueueItem(r, me));
53 for (const w of relations.listWards(slug)) {
54 const wardSlug = slugOf(w.other_uri);
55 if (!wardSlug) continue;
56 for (const p of follows.listForWard(wardSlug)) {
57 items.push({
58 id: p.id, type: 'Follow', actor: p.follower_uri, object: w.other_uri,
59 'shaer:direction': 'incoming', 'shaer:ward': w.other_uri,
60 'shaer:follower': p.follower_uri, 'shaer:followerHandle': p.follower_handle || undefined,
61 'shaer:quorum': p.quorum || 'any', published: p.created_at,
62 });
63 }
64 }
65 return collection(id, items);
66}
67
68/** De slug van een actor-uri op DEZE instance, of null als hij elders woont. */
69function slugOf(uri) {
70 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
71 if (!base || !String(uri || '').startsWith(`${base}/ap/users/`)) return null;
72 return decodeURIComponent(String(uri).slice(`${base}/ap/users/`.length).split(/[/?#]/)[0]) || null;
73}
74
75/**
76 * §5.3 uitgaand. Twee lezers, een wachtrij, en dat kan omdat §1 een ward en een
77 * guardian wederzijds uitsluit: je bent het een of het ander.
78 *
79 * ALS WARD wat IK wil volgen en waar mijn guardians nog over moeten
80 * ALS GUARDIAN wat mijn WARDS willen volgen en waar IK over moet (shaer-jdb)
81 *
82 * Dat tweede ontbrak. De wachtrij serveerde alleen listForWard(slug), en voor
83 * een guardian is dat per definitie leeg -- dus het scherm "Your wards want to
84 * follow" kon nooit iets tonen.
85 */
86export function outgoingFollowsCollection(id, slug, me) {
87 const items = outgoing.listForWard(slug).map((o) => outgoing.queueItem(o, me));
88 for (const r of follows.listReviewsByDirection(slug, 'outgoing')) {
89 items.push(follows.reviewQueueItem(r, me));
90 }
91 return collection(id, items);
92}
93
94/** The guardian's committed wards, with cached handle for display. */
95export function wardsCollection(id, slug) {
96 const items = relations.listWards(slug)
97 .map((r) => ({
98 id: r.other_uri,
99 'shaer:handle': r.other_handle || undefined,
100 since: r.created_at,
101 // Alles wat voor dit kind gated is, met soort, drempel en lopend voorstel
102 // (shaer-ahy.1). Zonder dit kon een app wel een ward TONEN maar niets over
103 // hem zeggen -- en dat is precies de helft van het antwoord op "wat mag
104 // dit kind". Dezelfde rijen als het PWA-paneel, uit dezelfde functie.
105 'shaer:gates': wardGates(slug, r.other_uri),
106 }));
107 return collection(id, items);
108}
109
110/** The ward's guardians with their availability (§3.6.1: never public,
111 * owner-only): the real size of the safety net. Same shape as the daemon. */
112export function guardiansCollection(id, slug) {
113 const uris = relations.listGuardians(slug).map((r) => r.other_uri);
114 return collection(id, availability.statusesFor(slug, uris, Date.now()));
115}
116
117export default { offersCollection, followsCollection, outgoingFollowsCollection, wardsCollection, guardiansCollection, wardGates, wardGuardianStatuses };
118
119// ── Wat er voor een ward gated is (shaer-ahy.1) ─────────────────────────
120//
121// STOND IN routes/guardian.js, en daar kon alleen de PWA erbij. De Shaer-apps
122// lezen dezelfde toestand via de wards-queue, en een tweede berekening naast
123// deze zou vroeg of laat een ander antwoord geven op dezelfde vraag -- dat is
124// hier geen schoonheidsfoutje maar twee guardians die een verschillend beeld
125// van hetzelfde kind krijgen. Een plek dus, en beide schermen lezen eruit.
126/** The guardians of a ward WE host, with availability (3.6.1: owner-only in
127 * spirit; the co-guardians are among the owners of the relationship). Null
128 * for a remote ward: its server tracks availability, not us. */
129export function wardGuardianStatuses(wardUri) {
130 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
131 if (!base || !String(wardUri || '').startsWith(`${base}/`)) return null;
132 const slug = String(wardUri).trim().replace(/\/+$/, '').split('/').pop();
133 try {
134 const uris = relations.listGuardians(slug).map((g) => ({ uri: g.other_uri, handle: g.other_handle }));
135 const st = Object.fromEntries(
136 availability.statusesFor(slug, uris.map((u) => u.uri), Date.now()).map((s) => [s.id, s]),
137 );
138 return uris.map((u) => ({
139 uri: u.uri,
140 handle: u.handle,
141 availability: (st[u.uri] || {})['shaer:availability'] || 'active',
142 awayUntil: (st[u.uri] || {})['shaer:awayUntil'] || null,
143 lapse: (st[u.uri] || {})['shaer:lapse'] || null,
144 }));
145 } catch { return null; }
146}
147/**
148 * De gate-rijen van een ward voor het paneel.
149 *
150 * De standen komen uit onze eigen kolommen als we het kind hosten; bij een ward
151 * elders weten we ze niet en blijft het NULL -- onbekend, niet uit. Het aantal
152 * guardians idem: dat wordt op de server van die ward bijgehouden, en zonder dat
153 * getal wordt er geen drempel verzonnen.
154 */
155export function wardGates(mySlug, wardUri) {
156 const statuses = wardGuardianStatuses(wardUri);
157 const wachtend = follows.listReviewsByDirection(mySlug, 'incoming')
158 .filter((r) => r.ward_uri === wardUri).length;
159 return gated.gateRows({
160 // Uit de BESLUITEN, niet uit onze eigen kolom. Er zijn geen lokale accounts:
161 // elke ward woont elders, dus wardEmbedSetting() gaf voor iedere ward null en
162 // stond er in het paneel overal "onbekend". Wat een guardian wel heeft is de
163 // uitslag van wat hij voorstelde.
164 settings: Object.fromEntries(gated.GATE_CATALOGUE
165 .filter((g) => g.available !== false && gated.featureColumn(g.feature))
166 .map((g) => [g.feature, gated.knownSetting(mySlug, wardUri, g.feature)])),
167 guardianCount: statuses ? statuses.length : null,
168 proposals: gated.listSent(mySlug, wardUri).map((p) => ({
169 feature: p.feature, value: !!p.value, status: gated.sentStatus(p, Date.now()),
170 })),
171 waiting: { 'shaer:follows': wachtend || undefined },
172 });
173}
Note: See TracBrowser for help on using the repository browser.