commit a922bf033de0d9db259af4e8b52c8ba513f328c4
parent 0394aa0cd076a17e5c1cf983bc9a710b787bfc7e
Author: Sebastian <sebasjm@taler-systems.com>
Date: Wed, 29 Jul 2026 09:26:30 -0300
fix issues in lp
stop if the server takes more than expected
add minim wait if the response comes faster
split merchant request into get_result & loong polling due to the API
Diffstat:
3 files changed, 118 insertions(+), 91 deletions(-)
diff --git a/packages/taler-merchant-webui/src/hooks/instance.ts b/packages/taler-merchant-webui/src/hooks/instance.ts
@@ -95,11 +95,13 @@ export function useInstanceKYCDetailsLongPolling() {
minTime: LONG_POLL_DELAY,
deps: [token],
shouldStop(res) {
+ // return !!res
return token === undefined;
},
},
);
return result;
+ // return undefined as Res
}
export function revalidateManagedInstanceDetails() {
diff --git a/packages/taler-merchant-webui/src/hooks/order.ts b/packages/taler-merchant-webui/src/hooks/order.ts
@@ -126,49 +126,92 @@ export function useInstanceOrders(args?: InstanceOrderFilter) {
"listOrders",
];
+ async function fetcher([offset, order, paid, refunded, wired, date]: any) {
+ return await lib.instance.listOrders(token, {
+ limit: PAGINATED_LIST_REQUEST,
+ order,
+ offset,
+ paid,
+ refunded,
+ wired,
+ date,
+ });
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listOrders">,
+ TalerHttpError
+ >(cacheKey, fetcher, {
+ keepPreviousData: false,
+ revalidateOnMount: true,
+ revalidateIfStale: true,
+ });
+
type Res = undefined | Awaited<ReturnType<typeof lib.instance.listOrders>>;
const result = useLongPolling(
- async (
+ (
ct,
latestStatus,
- [offset, order, paid, refunded, wired, date],
- ): Promise<Res> => {
+ [_offset, _order, paid, refunded, wired, date],
+ ): undefined | Promise<Res> => {
// either first request or the last one had an error
- const skipLongPoll =
- latestStatus === undefined || latestStatus.type === "fail";
-
- const params: ListOrdersRequestParams = {
- limit: PAGINATED_LIST_REQUEST,
- order,
- offset,
- paid,
- refunded,
- wired,
- date,
- };
- if (!skipLongPoll) {
- // wait for a new order to arrive before getting the result
- const result = await lib.instance.listOrders(token, {
- ...params,
- limit: 1,
+ 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;
+
+ // wait for a new order to arrive before getting the result
+ return lib.instance
+ .listOrders(token, {
+ limit: 5,
order: "asc",
+ offset,
timeout: LONG_POLL_DELAY,
+ paid,
+ refunded,
+ wired,
+ date,
ct,
+ })
+ .then((lpResult) => {
+ // fail fast, dont report
+ if (lpResult.type === "fail") return undefined;
+ const merged =
+ lpResult.body.orders.length === 0
+ ? latestStatus.body.orders
+ : ([] as OrderHistoryEntry[]).concat(
+ [...lpResult.body.orders].reverse(),
+ latestStatus.body.orders,
+ );
+ const newResult = opFixedSuccess(dummyHttpResponse, {
+ orders: merged,
+ });
+ // mutate(newResult, { revalidate: false });
+ return newResult;
});
- // fail fast, nothing new to report
- if (result.type === "fail" || !result.body.orders.length) return latestStatus;
- }
- const result = await lib.instance.listOrders(token, {
- ...params,
- ct,
- });
- return result;
},
- { minTime: LONG_POLL_DELAY, deps: cacheKey },
+ { deps: cacheKey, initial: data },
);
- if (result === undefined) return undefined;
+ if (error || data instanceof TalerError || result instanceof TalerError)
+ return error;
+
+ console.log("finish", data, result);
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ if (result === undefined) {
+ return buildPaginatedResult(
+ data.body.orders,
+ pointer,
+ updatePointer,
+ PAGINATED_LIST_REQUEST,
+ );
+ }
if (result.type !== "ok") return result;
return buildPaginatedResult(
diff --git a/packages/web-util/src/hooks/useAsync.ts b/packages/web-util/src/hooks/useAsync.ts
@@ -58,24 +58,15 @@ export function useAsync<Res>(
}
export const LONG_POLL_DELAY = 15000;
+const LP_MIN_DELAY = 5000;
const emptyArray: unknown[] = [];
/**
- * First start with `initial` value, if initial is undefined then finish.
- * Otherwise:
- * Based on `initial` check if it should do long-polling with `shouldRetryFn`
- * If the result is undefined then finish.
- * Otherwise:
- * Verify if the call is going to fast, if so slow down.
+ * Call retryFn handler, the function is expected to block up to LONG_POLL_DELAY = 15000.
+ * If takes longer is going to be canceled using the cancel token `ct`.
+ * If the function returns a result faster a delay is added to match LP_MIN_DELAY = 5000.
+ * If the function returns undefined instead of a Promise means that it was
+ * not ready to do long polling and won't retry.
*
- * Call `retryFn` as the long poll function.
- * The result will be the next `initial` value.
- *
- *
- *
- * @param initial what we already know about the state
- * @param shouldRetryFn verify if we need to do long poll based on what we know
- * @param retryFn the long polling function that should return the same type of initial value
- * @param deps
* @returns
*/
export function useLongPolling<Res>(
@@ -83,34 +74,27 @@ export function useLongPolling<Res>(
ct: CancellationToken,
last: undefined | Res,
deps: Array<any>,
- ) => Promise<Res>,
+ ) => Promise<Res> | undefined,
opts: {
/**
- * Wait time in case the request reply fast. Default LONG_POLL_DELAY
+ * Wait time in case the request returns fast. Default LP_MIN_DELAY = 5000
*/
minTime?: number;
/**
- * Initial value. Default undefined
+ * The first value of `last` in retryFn. Default undefined
*/
initial?: Res;
/**
- * Check before any request if loop should stop retrying. Default always false.
+ * Check before everty request if loop should stop retrying. Default always false.
*/
shouldStop?: (res: undefined | Res) => boolean;
/**
* Dependency array to re-evaluate the state. Default empty.
*/
deps?: Array<unknown>;
- /**
- * Setting to true will add wait time to the first request. Default false.
- *
- * Usually first request should return without delay to have the initial data
- * and the second request should start next with long poll.
- */
- waitFirstRequest?: boolean;
} = {},
): Res | undefined {
- const minTime = opts?.minTime ?? LONG_POLL_DELAY;
+ const minTime = opts?.minTime ?? LP_MIN_DELAY;
const dependencies = opts?.deps ?? emptyArray;
const [result, setResult] = useState<{
counter: number;
@@ -119,26 +103,23 @@ export function useLongPolling<Res>(
counter: 0,
prevResult: opts.initial,
});
- function retry(prevResult: Res) {
+ function retry(prevResult: Res | undefined) {
setResult((prev) => ({ counter: prev.counter + 1, prevResult }));
}
/**
- * New initial value, just keep result updated
- * but don't automatically trigger new request
+ * New initial value, retry
*/
useEffect(() => {
- setResult((prev) => ({
- counter: prev.counter,
- prevResult: opts.initial,
- }));
- }, [opts.initial]);
+ ct.current.ct?.cancel("long-polling-stop");
+ retry(opts.initial);
+ }, [opts.initial, ...dependencies]);
/**
- * the resultset is not needed anymore
+ * Unloading, the response is not needed anymore
*/
useEffect(() => {
return () => {
- console.log("unload");
+ ct.current.ct?.cancel("long-polling-stop");
ct.current.unloaded = true;
ct.current.startMs = 0;
};
@@ -149,13 +130,6 @@ export function useLongPolling<Res>(
* different resultset expected
* cancel current request and retry ASAP
*/
- useEffect(() => {
- return () => {
- console.log("dep unload");
- ct.current.ct?.cancel("long-polling-stop");
- ct.current.startMs = 0;
- };
- }, dependencies);
const ct = useRef<{
ct: CancellationToken.Source | undefined;
@@ -171,25 +145,33 @@ export function useLongPolling<Res>(
const diff = new Date().getTime() - ct.current.startMs;
const instant = Promise.resolve();
- const noTryYet = ct.current.startMs === 0;
const enoughTimePassed = diff > minTime;
- const firstRequestInstant = result.counter === 1 && !opts?.waitFirstRequest;
-
- (noTryYet || enoughTimePassed || firstRequestInstant
- ? instant
- : delayMs(minTime - diff)
- ).then(() => {
- if (ct.current.unloaded) return;
- ct.current.startMs = new Date().getTime();
- retryFn(tk.token, result.prevResult, dependencies)
- .then((r) => {
- if (!tk.token.isCancelled) {
- retry(r);
- }
- })
- .catch((error) => console.log("ERROR", error));
+
+ let requestCompleted = false;
+
+ const do_wait =
+ enoughTimePassed || result.prevResult === undefined
+ ? instant
+ : delayMs(minTime - diff);
+
+ 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;
+ 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));
+ }
});
- }, [result.counter, ...dependencies]);
+ }, [result.counter]);
return result.prevResult;
}