taler-typescript-core

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

commit 845b0d790141e93ac7b2a4d6fb6f91e147f20e8b
parent d13a6e4ec4c26410df1b79886bbfb9eb437f2577
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Thu,  2 Apr 2026 18:26:01 -0300

fix #11338

Diffstat:
Mpackages/bank-ui/src/hooks/account.ts | 2+-
Mpackages/kyc-ui/src/hooks/kyc.ts | 2+-
Mpackages/merchant-backoffice-ui/src/hooks/instance.ts | 18+++++++++++-------
Mpackages/merchant-backoffice-ui/src/hooks/order.ts | 45++++++++++++++++++++++++++++-----------------
Mpackages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx | 2+-
Mpackages/taler-util/src/http-client/merchant.ts | 29+++++++++++++----------------
Mpackages/taler-util/src/types-taler-merchant.ts | 7+++++++
Mpackages/web-util/src/context/merchant-api.ts | 2+-
Mpackages/web-util/src/hooks/useAsync.ts | 107+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mpackages/web-util/src/utils/http-impl.sw.ts | 4++--
11 files changed, 127 insertions(+), 93 deletions(-)

diff --git a/packages/bank-ui/src/hooks/account.ts b/packages/bank-ui/src/hooks/account.ts @@ -104,7 +104,7 @@ export function useWithdrawalDetails(wid: string | undefined) { ? result.body : undefined; }, - (latestStatus: TalerCorebankApi.WithdrawalPublicInfo) => { + (ct, latestStatus: TalerCorebankApi.WithdrawalPublicInfo) => { return api.getWithdrawalById(wid!, { old_state: latestStatus.status, timeoutMs: 5000, diff --git a/packages/kyc-ui/src/hooks/kyc.ts b/packages/kyc-ui/src/hooks/kyc.ts @@ -57,7 +57,7 @@ export function useKycInfo(token?: AccessToken) { if (!result.body.requirements.length) return undefined; return result.body; }, - async (latestStatus: KycProcessClientInformationWithEtag) => { + async (ct,latestStatus: KycProcessClientInformationWithEtag) => { const res = await api.checkKycInfoSpa(token!, latestStatus.etag, { timeoutMs: 5000, }); diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts @@ -58,7 +58,8 @@ export function useInstanceDetails() { export function revalidateInstanceKYCDetails() { return mutate( (key) => - Array.isArray(key) && key[key.length - 1] === "getCurrentInstanceKycStatus", + Array.isArray(key) && + key[key.length - 1] === "getCurrentInstanceKycStatus", undefined, { revalidate: true }, ); @@ -80,18 +81,21 @@ export function useInstanceKYCDetailsLongPolling() { const result = useLongPolling( data, (result) => { - if (!result || result.type === "fail" || !result.body.etag) { - return undefined; - } - return result.body.etag; + return ( + result !== undefined && + result.type === "ok" && + result.body.etag !== undefined + ); }, - async (latestEtag) => { + async (ct, latestData) => { + if (latestData === undefined || latestData.type === "fail" || !latestData.body.etag) return undefined const r = await lib.instance.getCurrentInstanceKycStatus(token!, { longpoll: { type: "state-change", - etag: latestEtag, + etag: latestData.body.etag, timeout: LONG_POLL_DELAY, }, + ct, }); mutate(r, { revalidate: false }); return r; diff --git a/packages/merchant-backoffice-ui/src/hooks/order.ts b/packages/merchant-backoffice-ui/src/hooks/order.ts @@ -20,6 +20,7 @@ import { AbsoluteTime, AccessToken, ListOrdersRequestParams, + opFixedSuccess, TalerError, TalerHttpError, TalerMerchantManagementResultByMethod, @@ -32,6 +33,7 @@ import { import _useSWR, { mutate, SWRHook } from "swr"; import { useSessionContext } from "../context/session.js"; import { useRef } from "preact/hooks"; +import { useEffect } from "preact/hooks"; const useSWR = _useSWR as unknown as SWRHook; @@ -75,15 +77,18 @@ export function useOrderDetailsWithLongPoll(orderId: string) { const result = useLongPolling( data, (result) => { - if (!result || result.type === "fail") return undefined; - return result.body; + return result !== undefined && result.type === "ok"; }, - async (latestStatus) => { + async (ct, latestStatus) => { + if (latestStatus === undefined || latestStatus.type === "fail") + return undefined; + console.log("doing long poll", latestStatus); const r = await lib.instance.getOrderDetails(token!, orderId, { longpoll: { - etag: latestStatus.etag!, + etag: latestStatus.body.etag!, timeout: LONG_POLL_DELAY, }, + ct, }); mutate(r, { revalidate: false }); return r; @@ -127,13 +132,11 @@ export function useInstanceOrders( "listOrders", ]; - console.log({ cacheKey: cacheKey.join(",") }); - async function fetcher([position, paid, refunded, wired, date]: any) { return await lib.instance.listOrders(token, { limit: PAGINATED_LIST_REQUEST, - offset: position, order: "dec", + offset: position, paid: paid, refunded: refunded, wired: wired, @@ -149,27 +152,35 @@ export function useInstanceOrders( const result = useLongPolling( data, (result) => { - if (!result || result.type === "fail") return undefined; - return result.body; + return result !== undefined && result.type === "ok"; }, - async (latestStatus, args) => { - console.log(args) + async (ct, latestStatus, args) => { + if (latestStatus === undefined || latestStatus.type === "fail") + return undefined; + const params: ListOrdersRequestParams = { - limit: PAGINATED_LIST_REQUEST, + limit: 1, + order: "asc", offset: args[0], - order: "dec", paid: args[1], refunded: args[2], wired: args[3], date: args[4], }; - // console.log("PARAMS:", params) - const r = await lib.instance.listOrders(token, { + const result = await lib.instance.listOrders(token, { ...params, timeout: LONG_POLL_DELAY, + ct, }); - mutate(r, { revalidate: false }); - return r; + const merged = + result.type === "fail" || result.body.orders.length === 0 + ? latestStatus.body.orders + : [...result.body.orders, ...latestStatus.body.orders]; + + console.log("u ahora",latestStatus, result, merged) + const newResult = opFixedSuccess({ orders: merged }); + mutate(newResult, { revalidate: false }); + return newResult; }, cacheKey, { minTime: LONG_POLL_DELAY }, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx @@ -24,7 +24,7 @@ import { Paytos, TalerError, TalerMerchantApi, - assertUnreachable, + assertUnreachable } from "@gnu-taler/taler-util"; import { useCommonPreferences, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx @@ -25,7 +25,7 @@ import { import { Fragment, VNode, h } from "preact"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; -import { useOrderDetails, useOrderDetailsWithLongPoll } from "../../../../hooks/order.js"; +import { useOrderDetailsWithLongPoll } from "../../../../hooks/order.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { DetailPage } from "./DetailPage.js"; diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts @@ -32,6 +32,7 @@ import { PaginationParams, ResultByMethod, TalerErrorCode, + TalerErrorDetail, TalerMerchantApi, assertUnreachable, carefullyParseConfig, @@ -837,15 +838,7 @@ export class TalerMerchantInstanceHttpClient { async getCurrentInstanceKycStatus( token: AccessToken, params: TalerMerchantApi.GetKycStatusRequestParams = {}, - ): Promise< - | OperationOk<MerchantKycStatusResult> - | OperationFail<HttpStatusCode.NoContent> - | OperationAlternative<HttpStatusCode.NotModified, { etag?: string }> - | OperationFail<HttpStatusCode.Unauthorized> - | OperationFail<HttpStatusCode.NotFound> - | OperationFail<HttpStatusCode.ServiceUnavailable> - | OperationFail<HttpStatusCode.GatewayTimeout> - > { + ) { const url = new URL(`private/kyc`, this.baseUrl); if (params.wireHash) { @@ -885,9 +878,11 @@ export class TalerMerchantInstanceHttpClient { if (token) { headers.Authorization = makeBearerTokenAuthHeader(token); } + const cancellationToken = params.ct ?? this.cancellationToken; const resp = await this.httpLib.fetch(url.href, { method: "GET", headers, + cancellationToken, }); const etag = resp.headers.get("etag")?.replace(/"/g, ""); @@ -1594,11 +1589,7 @@ export class TalerMerchantInstanceHttpClient { async listOrders( token: AccessToken, params: TalerMerchantApi.ListOrdersRequestParams = {}, - ): Promise< - | OperationFail<HttpStatusCode.Unauthorized> - | OperationFail<HttpStatusCode.NotFound> - | OperationOk<TalerMerchantApi.OrderHistory> - > { + ) { const url = new URL(`private/orders`, this.baseUrl); if (params.paid !== undefined) { @@ -1636,11 +1627,14 @@ export class TalerMerchantInstanceHttpClient { if (token) { headers.Authorization = makeBearerTokenAuthHeader(token); } + + const cancellationToken = params.ct ?? this.cancellationToken; + const resp = await this.httpLib.fetch(url.href, { method: "GET", headers, + cancellationToken, }); - switch (resp.status) { case HttpStatusCode.Ok: return opSuccessFromHttp(resp, codecForOrderHistory()); @@ -1757,9 +1751,11 @@ export class TalerMerchantInstanceHttpClient { if (token) { headers.Authorization = makeBearerTokenAuthHeader(token); } + const cancellationToken = params.ct ?? this.cancellationToken; const resp = await this.httpLib.fetch(url.href, { method: "GET", headers, + cancellationToken, }); const etag = resp.headers.get("etag")?.replace(/"/g, ""); @@ -3482,8 +3478,9 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp cacheEvictor?: CacheEvictor< TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction >, + cancellationToken?: CancellationToken, ) { - super(baseUrl, httpClient, cacheEvictor); + super(baseUrl, httpClient, cacheEvictor, cancellationToken); this.cacheManagementEvictor = cacheEvictor ?? nullEvictor; } diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts @@ -30,6 +30,7 @@ import { import { AccessToken, AccountLimit, + CancellationToken, DenomKeyType, ExchangeWireAccount, ObjectCodec, @@ -1493,6 +1494,8 @@ export interface GetKycStatusRequestParams { * */ longpoll?: KycLongPollingReason; + + ct?: CancellationToken; } export type OrderDetailLongPollingReason = { etag: string; @@ -1526,6 +1529,8 @@ export interface GetOrderRequestParams { // under “already_paid_order_id” if this flag is set // explicitly to “YES”. allowRefundedForRepurchase?: boolean; + + ct?: CancellationToken; } export interface ListConfirmedWireTransferRequestParams { /** @@ -1661,6 +1666,8 @@ export interface ListOrdersRequestParams { */ summary?: string; order?: "asc" | "dec"; + + ct?: CancellationToken; } export interface GetStatisticsRequestParams { diff --git a/packages/web-util/src/context/merchant-api.ts b/packages/web-util/src/context/merchant-api.ts @@ -23,7 +23,7 @@ import { TalerMerchantApi, TalerMerchantInstanceCacheEviction, TalerMerchantManagementCacheEviction, - TalerMerchantManagementHttpClient, + TalerMerchantManagementHttpClient } from "@gnu-taler/taler-util"; import { ComponentChildren, diff --git a/packages/web-util/src/hooks/useAsync.ts b/packages/web-util/src/hooks/useAsync.ts @@ -13,8 +13,9 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { opUnknownFailure, TalerError } from "@gnu-taler/taler-util"; -import { useCallback, useEffect, useState } from "preact/hooks"; +import { CancellationToken, TalerError } from "@gnu-taler/taler-util"; +import { error } from "console"; +import { useCallback, useEffect, useRef, useState } from "preact/hooks"; /** * convert the async function into preact hook @@ -76,66 +77,80 @@ export const LONG_POLL_DELAY = 15000; * * * - * @param fetcher fetcher should be a memoized function - * @param retry + * @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, Rt>( +export function useLongPolling<Res>( initial: Res, - shouldRetryFn: (res: Res, count: number) => Rt | undefined, - retryFn: (last: Rt, deps: Array<any>) => Promise<Res>, + shouldRetryFn: (res: Res) => boolean, + retryFn: (ct: CancellationToken, last: Res, deps: Array<any>) => Promise<Res>, deps: Array<any> = [], opts: { minTime?: number } = {}, ) { const minTime = opts?.minTime ?? 1000; - const [retry, setRetry] = useState<{ - // count: number; - fn: (() => Promise<Res>) | undefined; - startMs: number | undefined; - }>({ - // count: 0, - fn: undefined, - startMs: undefined, - }); - - const result = useAsync(retry.fn, [retry.startMs, ...deps]); - - const body = result ?? initial; - - const doRetry = useCallback((rt: Rt) => { - return function doRetryImpl() { - // call again - setRetry((lt) => ({ - // count: lt.count + 1, - fn: () => retryFn(rt, deps), - startMs: new Date().getTime(), - })); - }; - }, deps); + const [result, setResult] = useState(initial); useEffect(() => { - if (body === undefined || body instanceof TalerError) return; - const _body = body; + setResult(initial); + }, [initial, ...deps]); - const rt = shouldRetryFn(_body, retry.startMs ?? 0); - if (rt === undefined) return; + const ct = useRef<{ + ct: CancellationToken.Source | undefined; + unloaded: boolean; + startMs: number; + }>({ ct: undefined, unloaded: false, startMs: 0 }); - if (retry.startMs === undefined) { - doRetry(rt); + useEffect(() => { + // if (result === undefined || result instanceof TalerError) return; + + const tk = CancellationToken.create(); + ct.current.ct = tk; + + const doWeRetry = shouldRetryFn(result); + // console.log("should retry", rt !== undefined); + if (!doWeRetry) return; + + const diff = new Date().getTime() - ct.current.startMs; + if (ct.current.startMs === 0 || diff > minTime) { + ct.current.startMs = new Date().getTime(); + // console.log("starting request", ct.current.ct.token.id); + retryFn(tk.token, result, deps).then((r) => { + if (!tk.token.isCancelled) { + setResult(r); + } + }).catch(error => console.log("")); } else { - const diff = new Date().getTime() - retry.startMs; - if (diff < minTime) { - // calling too fast, wait whats left to reach minTime - delayMs(minTime - diff).then(() => doRetry(rt)); - } else { - doRetry(rt); - } + // console.log("wait before retry"); + // calling too fast, wait whats left to reach minTime + delayMs(minTime - diff).then(() => { + if (ct.current.unloaded) return; + ct.current.startMs = new Date().getTime(); + // console.log( + // "starting request after wait", + // ct.current.ct?.token.id, + // ct.current.unloaded, + // ); + retryFn(tk.token, result, deps).then((r) => { + if (!tk.token.isCancelled) { + setResult(r); + } + }).catch(error => console.log("")); + }); } - }, [body]); - return body; + return () => { + // console.log("request unused, cancel", ct.current.ct?.token.id); + tk.cancel(); + ct.current.unloaded = true; + ct.current.startMs = 0; + }; + }, [result]); + + return result; } /** diff --git a/packages/web-util/src/utils/http-impl.sw.ts b/packages/web-util/src/utils/http-impl.sw.ts @@ -123,8 +123,8 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary { }, requestTimeout.d_ms); } if (requestCancel) { - requestCancel.onCancelled(() => { - controller.abort(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR); + requestCancel.onCancelled((reason) => { + controller.abort(reason); }); }