diff options
| author | Adam Soltys <[email protected]> | 2026-08-30 23:10:28 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-30 23:10:28 -0700 |
| commit | f517cb23fa37b15922c8ad333952df09847ce8b6 (patch) | |
| tree | 9ae377287e641d76be62de0dcdde76c59631e08b /src/features | |
| parent | b775d2b29bcc2964cbc374771f86ce39d83cb8b7 (diff) | |
| download | coinosv3-f517cb23fa37b15922c8ad333952df09847ce8b6.tar.gz coinosv3-f517cb23fa37b15922c8ad333952df09847ce8b6.zip | |
A first-tap Safari hiccup stops costing a hat sale: the shop's registrar posts retry once on network-shaped failures (Safari's bare 'Load failed' included — fresh NIP-98 header per attempt, /hats/claim already idempotent, a real refusal never re-posts), and the shop listing quietly retries once before declaring itself offline
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/hats.js | 32 |
1 files changed, 23 insertions, 9 deletions
diff --git a/src/features/hats.js b/src/features/hats.js index 2b4a4d6..37997f4 100644 --- a/src/features/hats.js +++ b/src/features/hats.js @@ -185,16 +185,27 @@ export function hatsFeature(ctx) { return pk ? { pubkey: pk, signer: { signEvent: (e) => wallet.nostrSign(e) } } : null; } - async function post(path, payload, signer) { + // Safari reports a dropped request as the bare "Load failed", and a fresh + // PWA's first tap is prime territory for exactly one of those. Retry a + // NETWORK-shaped failure once, transparently (a fresh NIP-98 header each + // attempt); a real refusal from the registrar never re-posts. Safe for + // both posts: /hats/invoice mints anew, /hats/claim is idempotent. + const NETWORKISH = /load failed|failed to fetch|network|timed? ?out/i; + async function post(path, payload, signer, attempt = 0) { const url = `${REGISTRAR}${path}`; const body = JSON.stringify(payload); - const auth = await withTimeout(nip98Header(signer, url, 'POST', body), 12000, 'signing'); - const r = await withTimeout(fetch(url, { - method: 'POST', headers: { 'content-type': 'application/json', authorization: auth }, body, - }), 12000, 'registrar'); - const j = await r.json().catch(() => ({})); - if (!r.ok || j.error) throw new Error(j.error || `registrar refused (${r.status})`); - return j; + try { + const auth = await withTimeout(nip98Header(signer, url, 'POST', body), 12000, 'signing'); + const r = await withTimeout(fetch(url, { + method: 'POST', headers: { 'content-type': 'application/json', authorization: auth }, body, + }), 12000, 'registrar'); + const j = await r.json().catch(() => ({})); + if (!r.ok || j.error) throw new Error(j.error || `registrar refused (${r.status})`); + return j; + } catch (e) { + if (attempt < 1 && NETWORKISH.test(e.message || '')) return post(path, payload, signer, attempt + 1); + throw e; + } } // ---- the shop ----------------------------------------------------------- @@ -210,7 +221,7 @@ export function hatsFeature(ctx) { render(); const me = ctx.shownPubkey(); if (!me) { ui.hatShopData = { owned: [], equipped: null }; render(); return; } - fetch(`${REGISTRAR}/hats/${me}?net=${hatNet()}`).then((r) => r.json()).then((j) => { + const attempt = (retriesLeft) => fetch(`${REGISTRAR}/hats/${me}?net=${hatNet()}`).then((r) => r.json()).then((j) => { if (!ui.hatShop) return; ui.hatShopData = { owned: j.owned || [], equipped: j.equipped || null, prices: j.prices || null }; const now = Date.now(); @@ -218,8 +229,11 @@ export function hatsFeature(ctx) { saveStore(); render(); }).catch(() => { + // one quiet retry before showing the offline shop — see post() + if (retriesLeft > 0) return new Promise((r) => setTimeout(r, 1200)).then(() => attempt(retriesLeft - 1)); if (ui.hatShop && !ui.hatShopData) { ui.hatShopData = { owned: [], equipped: null, offline: true }; render(); } }); + attempt(1); } const closeShop = () => { ui.hatShop = null; ui.hatShopData = null; ui.hatConfirm = null; render(); }; |
