taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit d6af856912508e9db842218d9ea99c858205915e
parent e8f66122a4e471310ad6484796616e0401c3ce15
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Wed, 29 Jul 2026 14:58:51 -0300

only retry when depency change, also better handling of timeout

Diffstat:
Mpackages/libeufin-bank-webui/src/hooks/account.ts | 16+++++-----------
Mpackages/taler-merchant-webui/src/hooks/order.ts | 37+++++++++++++++----------------------
Mpackages/taler-util/src/http-client/bank-core.ts | 1+
Mpackages/web-util/src/hooks/useAsync.ts | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
4 files changed, 75 insertions(+), 57 deletions(-)

diff --git a/packages/libeufin-bank-webui/src/hooks/account.ts b/packages/libeufin-bank-webui/src/hooks/account.ts @@ -81,37 +81,31 @@ export function revalidateWithdrawalDetails() { ); } -export function useWithdrawalDetails(wid: string | undefined) { +export function useWithdrawalDetails(wid: string) { const { lib: { bank: api }, } = useBankCoreApiContext(); type Res = undefined | Awaited<ReturnType<typeof api.getWithdrawalById>>; - const result = useLongPolling( async (ct, res): Promise<Res> => { - if (!wid) return undefined; const old_state = !res || res instanceof TalerError || res.type === "fail" ? undefined : res.body.status; - const result = await api.getWithdrawalById(wid, { + + return await api.getWithdrawalById(wid, { old_state, timeoutMs: !old_state ? undefined : LONG_POLL_DELAY, ct, }); - return result; }, { - minTime: LONG_POLL_DELAY, deps: [wid], shouldStop(res) { return ( - wid === undefined || - (res !== undefined && - !(res instanceof TalerError) && - res.type !== "fail" && - (res.body.status === "confirmed" || res.body.status === "aborted")) + res?.type === "ok" && + (res.body.status === "confirmed" || res.body.status === "aborted") ); }, }, diff --git a/packages/taler-merchant-webui/src/hooks/order.ts b/packages/taler-merchant-webui/src/hooks/order.ts @@ -149,7 +149,7 @@ export function useInstanceOrders(args?: InstanceOrderFilter) { type Res = undefined | Awaited<ReturnType<typeof lib.instance.listOrders>>; - const result = useLongPolling( + const lp = useLongPolling( ( ct, latestStatus, @@ -158,11 +158,8 @@ export function useInstanceOrders(args?: InstanceOrderFilter) { // either first request or the last one had an error if (latestStatus === undefined) return undefined; if (latestStatus.type === "fail") return Promise.resolve(latestStatus); - - const offset = - latestStatus.body.orders.length > 0 - ? String(latestStatus.body.orders[0].row_id) - : undefined; + const before = latestStatus.body.orders; + const offset = before.length > 0 ? String(before[0].row_id) : undefined; // wait for a new order to arrive before getting the result return lib.instance @@ -179,32 +176,28 @@ export function useInstanceOrders(args?: InstanceOrderFilter) { }) .then((lpResult) => { // fail fast, dont report - if (lpResult.type === "fail") return undefined; + if (lpResult.type === "fail") { + return opFixedSuccess(dummyHttpResponse, { + orders: before, + }); + } + const after = lpResult.body.orders; const merged = - lpResult.body.orders.length === 0 - ? latestStatus.body.orders - : ([] as OrderHistoryEntry[]).concat( - [...lpResult.body.orders].reverse(), - latestStatus.body.orders, - ); - const newResult = opFixedSuccess(dummyHttpResponse, { + after.length > 0 ? after.reverse().concat(before) : before; + return opFixedSuccess(dummyHttpResponse, { orders: merged, }); - // mutate(newResult, { revalidate: false }); - return newResult; }); }, { deps: cacheKey, initial: data }, ); - if (error || data instanceof TalerError || result instanceof TalerError) - return error; + if (error) return error; - console.log("finish", data, result); if (data === undefined) return undefined; if (data.type !== "ok") return data; - if (result === undefined) { + if (lp === undefined) { return buildPaginatedResult( data.body.orders, pointer, @@ -212,10 +205,10 @@ export function useInstanceOrders(args?: InstanceOrderFilter) { PAGINATED_LIST_REQUEST, ); } - if (result.type !== "ok") return result; + if (lp.type !== "ok") return lp; return buildPaginatedResult( - result.body.orders, + lp.body.orders, pointer, updatePointer, PAGINATED_LIST_REQUEST, diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts @@ -948,6 +948,7 @@ export class TalerCoreBankHttpClient { } const resp = await this.httpLib.fetch(url.href, { method: "GET", + cancellationToken: params?.ct, }); switch (resp.status) { case HttpStatusCode.Ok: diff --git a/packages/web-util/src/hooks/useAsync.ts b/packages/web-util/src/hooks/useAsync.ts @@ -14,7 +14,13 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { CancellationToken, TalerError } from "@gnu-taler/taler-util"; -import { useEffect, useRef, useState } from "preact/hooks"; +import { + EffectCallback, + Inputs, + useEffect, + useRef, + useState, +} from "preact/hooks"; /** * convert the async function into preact hook @@ -106,11 +112,11 @@ export function useLongPolling<Res>( function retry(prevResult: Res | undefined) { setResult((prev) => ({ counter: prev.counter + 1, prevResult })); } + /** - * New initial value, retry + * Retry if dependency change */ - useEffect(() => { - ct.current.ct?.cancel("long-polling-stop"); + useEffectOnDepChanged(() => { retry(opts.initial); }, [opts.initial, ...dependencies]); @@ -119,7 +125,6 @@ export function useLongPolling<Res>( */ useEffect(() => { return () => { - ct.current.ct?.cancel("long-polling-stop"); ct.current.unloaded = true; ct.current.startMs = 0; }; @@ -132,14 +137,12 @@ export function useLongPolling<Res>( */ const ct = useRef<{ - ct: CancellationToken.Source | undefined; unloaded: boolean; startMs: number; - }>({ ct: undefined, unloaded: false, startMs: 0 }); + }>({ unloaded: false, startMs: 0 }); useEffect(() => { const tk = CancellationToken.create(); - ct.current.ct = tk; if (opts.shouldStop && opts.shouldStop(result.prevResult)) return; @@ -147,30 +150,34 @@ export function useLongPolling<Res>( const instant = Promise.resolve(); const enoughTimePassed = diff > minTime; - let requestCompleted = false; - - const do_wait = + const do_wait = ( enoughTimePassed || result.prevResult === undefined ? instant - : delayMs(minTime - diff); + : delayMs(minTime - diff) + ).then((_) => false); - const timeout = delayMs(LONG_POLL_DELAY + 200).then(() => { - if (!requestCompleted) { - ct.current.ct?.cancel(); - } - }); - - Promise.race([do_wait, timeout]).then(() => { - if (ct.current.unloaded || ct.current.ct?.token.isCancelled) return; - requestCompleted = true; + do_wait.then(() => { + if (ct.current.unloaded || tk.token.isCancelled) return; const p = retryFn(tk.token, result.prevResult, dependencies); if (p) { ct.current.startMs = new Date().getTime(); - p.then((r) => { - retry(r); - }).catch((error) => console.log("ERROR", error)); + Promise.race([p, cancelOnTimeout(LONG_POLL_DELAY + 100, tk)]) + .then(retry) + .catch((error) => { + if ( + error !== "long-polling-timeout" || + error !== "long-polling-stop" + ) { + console.error("Long polling error", error); + } + // consider checking "shouldStop" with error + retry(result.prevResult); + }); } }); + return () => { + tk.cancel("long-polling-stop"); + }; }, [result.counter]); return result.prevResult; @@ -192,3 +199,26 @@ export async function delayMs( } }); } +async function cancelOnTimeout(ms: number, tk: CancellationToken.Source) { + return delayMs(ms).then((_) => { + tk.cancel("long-polling-timeout"); + throw Error("long-polling-timeout"); + }); +} +/** + * Triggers when dependecy changes but avoid triggering onMount + * + * @param effectCallback + * @param inputs + */ +function useEffectOnDepChanged(effectCallback: EffectCallback, inputs?: Inputs) { + const isMounted = useRef(false); + + useEffect(() => { + if (!isMounted.current) { + isMounted.current = true; + return; + } + return effectCallback(); + }, inputs); +}