source: Klonkt/src/services/guardianship/queues.js@ 2bfe26c

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

Voorstellen vanuit de app, en een kind dat zelf kan vragen (shaer-8ru)

Barts opdracht: bouw shaer-8ru zodat de apps ook kunnen voorstellen, en maak
daarnaast het pad voor de ward om zelf een voorstel aan te zwengelen als hij
tegen een gated feature aanloopt.

DEEL 1 -- DE APPS KUNNEN VOORSTELLEN. De outbox kent nu een Offer van een
shaer:GatedSetting, precies de vorm die 5.6 al beschrijft en die de inbox al las.
De afweging zelf is verhuisd naar AP.proposeGate, waar de PWA-route nu ook
doorheen loopt. Twee implementaties naast elkaar zou een tweede weg naar
hetzelfde besluit zijn -- exact de fout die we vanmiddag bij de antwoordpoort
rechtzetten, toen de innamepoort alleen in C2S bleek te zitten.

DEEL 2 -- HET KIND KRIJGT WOORDEN. Tot nu toe liep alles over de guardians: zij
zien de catalogus, zij stellen voor, zij tellen. Het kind liep tegen een dichte
deur en kon niets. Nu stuurt het een shaer:gateRequest, en die landt bij zijn
guardians BIJ DE POORT waar hij over gaat -- niet in een aparte lijst die je
apart moet openen en dus vergeet.

EEN VRAAG IS GEEN STEM, en dat is de hele grens. Het verzoek is geen voorstel en
telt nergens mee; pas als een guardian het oppakt wordt het een voorstel dat
langs de gewone tally gaat. Anders opent een kind zijn eigen poort door hard
genoeg te vragen. Er staat een toets op die kijkt of er echt geen voorstel en
geen open poort uit ontstaat.

NIET DE REDDINGSBOEI. Een hulpvraag is een noodgeval en gaat door elke dichte
deur; dit is een wens. Ze door elkaar laten lopen zou de boei devalueren tot "het
kind wil iets", en dan kijkt er op een dag niemand meer op als hij afgaat.

GEEN VRIJE TEKST, en dat is geen gierigheid maar de reden dat het verzoek langs
de messages-poort MAG. Een kind met berichten dicht kan nog steeds om iets
vragen, zonder dat er een kanaal ontstaat om omheen die poort te praten. Wil het
uitleggen waarom, dan is dat een gesprek, en gesprekken hebben hun eigen poort.

Alleen van een eigen ward: een verzoek van een vreemde is geen vraag maar een
onbekende die iets over jouw instellingen wil zeggen.

Twaalf toetsen. Twee mutaties gecontroleerd (1 en 6 rood). Suite 712/712.

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