Changeset 30d0e2c in Klonkt for src/services/guardianship/offers.js
- Timestamp:
- 08/03/2026 06:38:16 AM (5 weeks ago)
- Branches:
- main
- Children:
- 3d882bd
- Parents:
- 69bd747
- File:
-
- 1 edited
-
src/services/guardianship/offers.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
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 };
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)