commit 845b0d790141e93ac7b2a4d6fb6f91e147f20e8b
parent d13a6e4ec4c26410df1b79886bbfb9eb437f2577
Author: Sebastian <sebasjm@taler-systems.com>
Date: Thu, 2 Apr 2026 18:26:01 -0300
fix #11338
Diffstat:
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);
});
}