taler-typescript-core

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

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:
Mpackages/taler-merchant-webui/src/hooks/instance.ts | 2++
Mpackages/taler-merchant-webui/src/hooks/order.ts | 103++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mpackages/web-util/src/hooks/useAsync.ts | 104+++++++++++++++++++++++++++++++++----------------------------------------------
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; }