Changeset 30d0e2c in Klonkt for src/services/guardianship
- Timestamp:
- 08/03/2026 06:38:16 AM (5 weeks ago)
- Branches:
- main
- Children:
- 3d882bd
- Parents:
- 69bd747
- Location:
- src/services/guardianship
- Files:
-
- 3 edited
Legend:
- Unmodified
- Added
- Removed
-
src/services/guardianship/handshake.js
r69bd747 r30d0e2c 545 545 } 546 546 547 /** 548 * §4.2 SHOULD: retry the dereference for handshakes left deferred because the 549 * candidate could not be read. 550 * 551 * Waiting for a further activity from a party is not enough: the commit is 552 * triggered by the LAST `Accept`, so if that one has already arrived nothing 553 * will ever poke it again and the handshake would sit until its window closed. 554 * The ward's dashboard polling its own offers queue is this instance's 555 * schedule, exactly as a read settles a lapse (§3.6.3). 556 * 557 * Deliberately not awaited by the read: a poll should render what is true now, 558 * not block on someone else's slow server. A retry that succeeds shows up in 559 * the next poll, which is the same second or two later. 560 */ 561 export async function retryDeferred(slug) { 562 for (const o of offers.listDeferred(slug)) { 563 await maybeCommit(slug, o.offer_id).catch(() => { /* next poll tries again */ }); 564 } 565 } 566 547 567 function notify(slug, ev) { 548 568 try { if (deps && typeof deps.onEvent === 'function') deps.onEvent(slug, ev); } catch { /* best-effort */ } 549 569 } 550 570 551 export default { wireHandshake, handleOutbox, handleInbox, parseRelationship, parseUndoRelationship, endGuardianship };571 export default { wireHandshake, handleOutbox, handleInbox, parseRelationship, parseUndoRelationship, endGuardianship, retryDeferred }; -
src/services/guardianship/offers.js
r69bd747 r30d0e2c 79 79 } 80 80 81 /** Pending offers where `me` is a party — the offers queue (daemon shape). */ 82 export function listForParty(slug, me) { 83 return stmts().listBySlug.get ? stmts().listBySlug.all(slug).filter((o) => isParty(o, me)) : []; 81 /** 82 * How long a guardianship handshake stays open (§3.5). Adding a guardian is a 83 * reversible decision, but not a quick one: the ward, the candidate and every 84 * existing guardian have to answer, and they are people, sometimes on holiday. 85 * A week is long enough that nobody is rushed and short enough that a forgotten 86 * offer does not sit in a child's queue for a month looking like a live choice. 87 */ 88 export const OFFER_WINDOW_MS = 7 * 24 * 60 * 60 * 1000; 89 90 /** SQLite writes CURRENT_TIMESTAMP as UTC 'YYYY-MM-DD HH:MM:SS', which 91 * Date.parse reads as LOCAL time — hours out, and enough to expire an offer 92 * early or late. Same correction as ActivityPubService.isoStamp. */ 93 const stampMs = (v) => { 94 const s = String(v || ''); 95 return Date.parse(/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}$/.test(s) ? `${s.replace(' ', 'T')}Z` : s); 96 }; 97 98 export const closesAt = (o) => stampMs(o.created_at) + OFFER_WINDOW_MS; 99 100 /** 101 * §3.5 fails closed: once the window has run, a handshake that never completed 102 * is over. WHICH failure it was matters (§4.2), so the two get different 103 * terminal states and neither of them is `void`: 104 * 105 * 'expired' — the parties never all answered. Nothing to say about anyone. 106 * 'unverified' — everyone answered; the candidate could never be read, so 107 * the check never got to run. The parties MUST be told this 108 * and MUST NOT be told the candidate was refused. It was not: 109 * nobody ever managed to look. 110 */ 111 export function expireIfDue(slug, offerId, now = Date.now()) { 112 const o = stmts().getOffer.get(slug, offerId); 113 if (!o || o.status !== 'pending') return null; 114 const due = closesAt(o); 115 if (!Number.isFinite(due) || due > now) return null; 116 const status = readyToCommit(o) ? 'unverified' : 'expired'; 117 stmts().setStatus.run(status, null, slug, offerId); 118 return { ...o, status }; 119 } 120 121 /** Pending offers where `me` is a party — the offers queue (daemon shape). 122 * Reads are where lazy completion happens, as with the lapses (§3.6.3): a 123 * closed window is settled here rather than by a sweeper nobody runs. */ 124 export function listForParty(slug, me, now = Date.now()) { 125 if (!stmts().listBySlug.get) return []; 126 for (const o of stmts().listBySlug.all(slug)) expireIfDue(slug, o.offer_id, now); 127 return stmts().listBySlug.all(slug).filter((o) => isParty(o, me)); 128 } 129 130 /** Handshakes whose tally is complete but which are not committed: the §4.2 131 * deferred set, waiting on a candidate nobody could dereference. */ 132 export function listDeferred(slug) { 133 if (!stmts().listBySlug.get) return []; 134 return stmts().listBySlug.all(slug).filter((o) => readyToCommit(o)); 84 135 } 85 136 … … 109 160 start, getOffer, findOfferAnywhere, recordAccept, recordReject, commit, 110 161 readyToCommit, listForParty, queueItem, parties, isParty, acceptsOf, 162 OFFER_WINDOW_MS, closesAt, expireIfDue, listDeferred, 111 163 }; -
src/services/guardianship/queues.js
r69bd747 r30d0e2c 12 12 import * as relations from './relations.js'; 13 13 import * as availability from './availability.js'; 14 import * as handshake from './handshake.js'; 14 15 15 16 const collection = (id, items) => ({ … … 22 23 * clients render both without a second fetch. */ 23 24 export function offersCollection(id, slug, me) { 25 // §4.2: a handshake whose candidate could not be dereferenced is deferred, 26 // not decided, and the last Accept may already have landed — so nothing else 27 // would ever retry it. This poll is the schedule. Not awaited: the read 28 // answers with what is true now, and a retry that succeeds surfaces in the 29 // next one. `listForParty` settles closed windows on the way past. 30 handshake.retryDeferred(slug).catch(() => { /* the next read tries again */ }); 24 31 const items = offers.listForParty(slug, me).map((o) => offers.queueItem(o, me)); 25 32 items.push(...availability.lapseQueueItems(slug, me, Date.now()));
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)