6// swappable store iface (cfTxtClaimStore now) — claimKey is opaque to the protocol. 7export type ClaimStore = { 8 readClaim(claimKey: string): Promise<Claim | null> 9 putClaim(claimKey: string, claim: Claim): Promise<void> 10 listClaims(): Promise<Array<{claimKey: string, claim: Claim}>> 13const nowSec = () => Math.floor(Date.now() / 1000) 15// fail-closed per item: store-unreachable → notify + return fallback (caller skips the gated action). 16async function claimOnStoreErr<T>(fallback: T, debugH: Record<string, unknown>, fn: () => Promise<T>): Promise<T> { 17 try { return await fn() } 18 catch (err) { await noThrowNotifErr('claimStoreUnreachable', {...debugH, err: String(err)}); return fallback } 21export const acquireClaim = async ({store, claimKey, owner, ttlSec = 600}: { 22 store: ClaimStore, claimKey: string, owner: string, ttlSec?: number 23}): Promise<{claimed: boolean}> => { 24 const debugH = {claimKey, owner} 25 return claimOnStoreErr({claimed: false}, debugH, async () => { 26 const claim = await store.readClaim(claimKey) 29 await store.putClaim(claimKey, {owner, hb: now, ttlSec}) 30 return {claimed: true} 32 return {claimed: false} 36export const checkClaim = async ({store, claimKey, owner}: { 37 store: ClaimStore, claimKey: string, owner: string 38}): Promise<boolean> => { 39 return claimOnStoreErr(false, {claimKey, owner}, async () => { 40 const claim = await store.readClaim(claimKey) 41 return claim?.owner === owner 45// best-effort maintenance: failure only notifies (never blocks the gated actions above). 46export const renewAllMyClaims = async ({store, owner}: {store: ClaimStore, owner: string}): Promise<{renewed: number}> => { 47 return claimOnStoreErr({renewed: 0}, {owner}, async () => { 48 const all = await store.listClaims() 49 const mine = all.filter(({claim}) => claim.owner === owner) 51 for (const {claimKey, claim} of mine) await store.putClaim(claimKey, {owner, hb: now, ttlSec: claim.ttlSec}) 52 return {renewed: mine.length}