mirror of
https://github.com/tompro/sattle.git
synced 2026-08-27 07:15:59 +00:00
feat: expose fund ownership assertions
This commit is contained in:
@@ -19,45 +19,102 @@ export class UncertainOutcomeError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
// the wait was interrupted from outside (service shutdown) - distinct
|
||||
// from budget exhaustion so the caller can treat it as normal teardown
|
||||
export class PollAbortedError extends Error {
|
||||
constructor() {
|
||||
super('The wait was interrupted by shutdown.')
|
||||
this.name = 'PollAbortedError'
|
||||
}
|
||||
}
|
||||
|
||||
export type FundOperationOptions = LnurlcashOptions & {
|
||||
readonly assertOwner?: () => void
|
||||
}
|
||||
|
||||
export const assertFundOwner = (options: FundOperationOptions): void => {
|
||||
options.assertOwner?.()
|
||||
}
|
||||
|
||||
export type PollOptions = {
|
||||
// first delay between checks (doubles each round up to intervalCapMs)
|
||||
intervalMs?: number
|
||||
intervalCapMs?: number
|
||||
// total budget before giving up
|
||||
maxWaitMs?: number
|
||||
// aborts the wait promptly (shutdown). Only the WAIT is interruptible:
|
||||
// callers pass this for work whose observation phase may outlive the
|
||||
// caller - once pollVerifyUntilSettled has returned, the signal no
|
||||
// longer reaches anything
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
const DEFAULT_POLL: Required<PollOptions> = {
|
||||
const DEFAULT_POLL: Required<Omit<PollOptions, 'signal'>> = {
|
||||
intervalMs: 1000,
|
||||
intervalCapMs: 5000,
|
||||
maxWaitMs: 120_000
|
||||
maxWaitMs: 120_000,
|
||||
}
|
||||
|
||||
const sleep = (ms: number): Promise<void> =>
|
||||
new Promise(resolve => setTimeout(resolve, ms))
|
||||
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
// a sleep that ends immediately on abort instead of riding out its timer
|
||||
const abortableSleep = (ms: number, signal: AbortSignal): Promise<void> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const onAbort = (): void => {
|
||||
clearTimeout(timer)
|
||||
reject(new PollAbortedError())
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
signal.removeEventListener('abort', onAbort)
|
||||
resolve()
|
||||
}, ms)
|
||||
if (signal.aborted) {
|
||||
clearTimeout(timer)
|
||||
reject(new PollAbortedError())
|
||||
return
|
||||
}
|
||||
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.
|
||||
// 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,
|
||||
poll: PollOptions,
|
||||
options: LnurlcashOptions
|
||||
options: LnurlcashOptions,
|
||||
): Promise<VerifyResult> => {
|
||||
const {intervalMs, intervalCapMs, maxWaitMs} = {...DEFAULT_POLL, ...poll}
|
||||
const {intervalMs, intervalCapMs, maxWaitMs, signal} = {
|
||||
...DEFAULT_POLL,
|
||||
...poll,
|
||||
}
|
||||
const fetchOptions: LnurlcashOptions = signal
|
||||
? {
|
||||
...options,
|
||||
fetch: (input, init) => {
|
||||
const base = options.fetch ?? globalThis.fetch
|
||||
return base(input, {...init, signal})
|
||||
},
|
||||
}
|
||||
: options
|
||||
const deadline = Date.now() + maxWaitMs
|
||||
let delay = intervalMs
|
||||
let lastError: unknown = null
|
||||
while (Date.now() < deadline) {
|
||||
if (signal?.aborted) throw new PollAbortedError()
|
||||
try {
|
||||
const result = await fetchInvoiceVerification(verifyUrl, options)
|
||||
const result = await fetchInvoiceVerification(verifyUrl, fetchOptions)
|
||||
if (result.settled) return result
|
||||
lastError = null
|
||||
} catch (err) {
|
||||
// the signal's own AbortError lands here on an interrupted fetch
|
||||
if (signal?.aborted) throw new PollAbortedError()
|
||||
lastError = err
|
||||
}
|
||||
await sleep(Math.min(delay, Math.max(0, deadline - Date.now())))
|
||||
if (signal) await abortableSleep(Math.min(delay, Math.max(0, deadline - Date.now())), signal)
|
||||
else await sleep(Math.min(delay, Math.max(0, deadline - Date.now())))
|
||||
delay = Math.min(delay * 2, intervalCapMs)
|
||||
}
|
||||
if (lastError instanceof Error) {
|
||||
|
||||
Reference in New Issue
Block a user