From 9f4ae3b0f5b6ff59515c5936ab8b9c08a4b51d9e Mon Sep 17 00:00:00 2001 From: protom Date: Mon, 31 Aug 2026 12:22:30 +0200 Subject: [PATCH] refactor(lnurlcash): generalize bounded polling beyond verify endpoints pollVerifyUntilSettled keeps its exact behavior and messages, reimplemented on a generic pollUntil so flows that watch something other than a LUD-21 verify (a named mint output appearing) share the same backoff, budget and abort semantics. --- src/lnurlcash/ops/shared.ts | 45 +++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/lnurlcash/ops/shared.ts b/src/lnurlcash/ops/shared.ts index b93fade..d20a70f 100644 --- a/src/lnurlcash/ops/shared.ts +++ b/src/lnurlcash/ops/shared.ts @@ -76,16 +76,17 @@ const abortableSleep = (ms: number, signal: AbortSignal): Promise => signal.addEventListener('abort', onAbort, {once: true}) }) -// polls a LUD-21/LUD-25 verify endpoint until it reports settled, with -// backoff, inside a total time budget. A single failed check isn't fatal - -// the next round tries again. Returns the settled VerifyResult; throws on -// budget exhaustion, or PollAbortedError when the caller's signal fires -// (a hung fetch is interrupted too: the signal is bound into the request). -export const pollVerifyUntilSettled = async ( - verifyUrl: string, +// polls `check` until it yields a value, with backoff, inside a total +// time budget. A single failed check isn't fatal - the next round tries +// again. Returns the first value; throws on budget exhaustion, or +// PollAbortedError when the caller's signal fires (a hung fetch is +// interrupted too: the signal is bound into the request). +export const pollUntil = async ( + check: (options: LnurlcashOptions) => Promise, + exhausted: string, poll: PollOptions, options: LnurlcashOptions, -): Promise => { +): Promise => { const {intervalMs, intervalCapMs, maxWaitMs, signal} = { ...DEFAULT_POLL, ...poll, @@ -105,8 +106,8 @@ export const pollVerifyUntilSettled = async ( while (Date.now() < deadline) { if (signal?.aborted) throw new PollAbortedError() try { - const result = await fetchInvoiceVerification(verifyUrl, fetchOptions) - if (result.settled) return result + const result = await check(fetchOptions) + if (result !== null) return result lastError = null } catch (err) { // the signal's own AbortError lands here on an interrupted fetch @@ -118,7 +119,27 @@ export const pollVerifyUntilSettled = async ( delay = Math.min(delay * 2, intervalCapMs) } if (lastError instanceof Error) { - throw new Error(`Payment not confirmed: ${lastError.message}`) + throw new Error(`${exhausted}: ${lastError.message}`) } - throw new Error('Payment not confirmed within the time budget.') + throw new Error(`${exhausted} within the time budget.`) } + +// polls a LUD-21/LUD-25 verify endpoint until it reports settled, with +// backoff, inside a total time budget. A single failed check isn't fatal - +// the next round tries again. Returns the settled VerifyResult; throws on +// budget exhaustion, or PollAbortedError when the caller's signal fires +// (a hung fetch is interrupted too: the signal is bound into the request). +export const pollVerifyUntilSettled = ( + verifyUrl: string, + poll: PollOptions, + options: LnurlcashOptions, +): Promise => + pollUntil( + async (fetchOptions) => { + const result = await fetchInvoiceVerification(verifyUrl, fetchOptions) + return result.settled ? result : null + }, + 'Payment not confirmed', + poll, + options, + )