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:
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);
+}