diff --git a/src/components/NanoContract/NanoContractDetails.js b/src/components/NanoContract/NanoContractDetails.js
index 3e492d4bc..6ea752c55 100644
--- a/src/components/NanoContract/NanoContractDetails.js
+++ b/src/components/NanoContract/NanoContractDetails.js
@@ -150,7 +150,9 @@ export const NanoContractDetails = ({ nc }) => {
onRefresh={handleNewerTransactions}
// Enables a button to load more of older transactions until the end
// By reaching the end, the button ceases to render
- ListFooterComponent={}
+ ListFooterComponent={(
+
+ )}
extraData={[isLoading, error]}
/>
)}
@@ -163,12 +165,13 @@ export const NanoContractDetails = ({ nc }) => {
* It hides the button when the last transaction is the initialize.
*
* @param {Object} prop Properties object
+ * @param {string} prop.ncId Nano Contract ID (from the registered contract)
* @param {{
- * ncId: string;
* txId: string;
+ * ncMethod: string;
* }} prop.lastTx A transaction item from transaction history
*/
-const LoadMoreButton = ({ lastTx }) => {
+const LoadMoreButton = ({ ncId, lastTx }) => {
const dispatch = useDispatch();
const isInitializeTx = lastTx.ncMethod === 'initialize';
@@ -178,10 +181,10 @@ const LoadMoreButton = ({ lastTx }) => {
*/
const handleLoadMore = useCallback(() => {
dispatch(nanoContractHistoryRequest({
- ncId: lastTx.ncId,
+ ncId,
after: lastTx.txId,
}));
- }, [dispatch, lastTx]);
+ }, [dispatch, ncId, lastTx.txId]);
return !isInitializeTx && (
Set of currently loading request types.
+ *
+ * This is necessary because the Redux state check (historyMeta.isLoading) has a race window
+ * between `yield select()` and `yield put()` where multiple sagas can pass the guard.
+ *
+ * @type {Map>}
+ */
+const loadingLockByNcId = new Map();
+
+/**
+ * Gets or creates the Set of loading request types for a given ncId.
+ * @param {string} ncId Nano Contract ID
+ * @returns {Set}
+ */
+function getLoadingLockSet(ncId) {
+ if (!loadingLockByNcId.has(ncId)) {
+ loadingLockByNcId.set(ncId, new Set());
+ }
+ return loadingLockByNcId.get(ncId);
+}
+
+/**
+ * Removes a request type from the loading lock set and cleans up if empty.
+ * @param {string} ncId Nano Contract ID
+ * @param {string} requestType The request type to remove
+ */
+function cleanupLoadingLock(ncId, requestType) {
+ const loadingLock = loadingLockByNcId.get(ncId);
+ if (loadingLock) {
+ loadingLock.delete(requestType);
+ if (loadingLock.size === 0) {
+ loadingLockByNcId.delete(ncId);
+ }
+ }
+}
+
export const failureMessage = {
alreadyRegistered: t`Nano Contract already registered.`,
walletNotReadyError: t`Wallet is not ready yet to register a Nano Contract.`,
@@ -334,39 +381,59 @@ export function* requestHistoryNanoContract({ payload }) {
const { ncId, before, after } = payload;
log.debug('Start processing request for nano contract transaction history...');
- const historyMeta = yield select((state) => state.nanoContract.historyMeta);
- if (historyMeta[ncId] && historyMeta[ncId].isLoading) {
- // Do nothing if nano contract already loading...
- log.debug('Halting processing for nano contract transaction history request while it is loading...');
- return;
- }
- yield put(nanoContractHistoryLoading({ ncId }));
-
- const wallet = yield select((state) => state.wallet);
- if (!wallet.isReady()) {
- log.debug('Fail fetching Nano Contract history because wallet is not ready.');
- yield put(nanoContractHistoryFailure({ ncId, error: failureMessage.walletNotReadyError }));
- // This will show user an error modal with the option to send the error to sentry.
- yield put(onExceptionCaptured(new Error(failureMessage.walletNotReadyError), false));
- return;
+ // Determine request type: 'before' (newer txs), 'after' (older txs), or 'initial' (full reload)
+ let requestType = REQUEST_TYPE.INITIAL;
+ if (before != null) {
+ requestType = REQUEST_TYPE.BEFORE;
+ } else if (after != null) {
+ requestType = REQUEST_TYPE.AFTER;
}
-
- const fn = wallet.storage.isNanoContractRegistered.bind(wallet.storage);
- const isNcRegistered = yield call(fn, ncId);
- if (!isNcRegistered) {
- log.debug('Fail fetching Nano Contract history because Nano Contract is not registered yet.');
- yield put(nanoContractHistoryFailure({ ncId, error: failureMessage.notRegistered }));
+ const loadingLock = getLoadingLockSet(ncId);
+
+ // Synchronous lock check - prevents race condition between yield select() and yield put()
+ // Block conditions:
+ // 1. Same request type already in-flight (duplicate request)
+ // 2. Initial is loading (it will replace everything, so other requests should wait)
+ // 3. This is initial but before/after are in-flight (their results would be lost)
+ const shouldBlock = loadingLock.has(requestType)
+ || loadingLock.has(REQUEST_TYPE.INITIAL)
+ || (requestType === REQUEST_TYPE.INITIAL && loadingLock.size > 0);
+
+ if (shouldBlock) {
+ log.debug(`Halting: conflicting history load for ncId=${ncId}, type=${requestType}, current=[${[...loadingLock]}]`);
return;
}
- if (before == null && after == null) {
- // it clean the history when starting load from the beginning
- log.debug('Cleaning previous history to start over.');
- yield put(nanoContractHistoryClean({ ncId }));
- }
+ // Acquire lock synchronously before any yield
+ loadingLock.add(requestType);
- const useWalletService = yield select((state) => state.useWalletService);
try {
+ yield put(nanoContractHistoryLoading({ ncId }));
+
+ const wallet = yield select((state) => state.wallet);
+ if (!wallet.isReady()) {
+ log.debug('Fail fetching Nano Contract history because wallet is not ready.');
+ yield put(nanoContractHistoryFailure({ ncId, error: failureMessage.walletNotReadyError }));
+ // This will show user an error modal with the option to send the error to sentry.
+ yield put(onExceptionCaptured(new Error(failureMessage.walletNotReadyError), false));
+ return;
+ }
+
+ const fn = wallet.storage.isNanoContractRegistered.bind(wallet.storage);
+ const isNcRegistered = yield call(fn, ncId);
+ if (!isNcRegistered) {
+ log.debug('Fail fetching Nano Contract history because Nano Contract is not registered yet.');
+ yield put(nanoContractHistoryFailure({ ncId, error: failureMessage.notRegistered }));
+ return;
+ }
+
+ if (before == null && after == null) {
+ // it clean the history when starting load from the beginning
+ log.debug('Cleaning previous history to start over.');
+ yield put(nanoContractHistoryClean({ ncId }));
+ }
+
+ const useWalletService = yield select((state) => state.useWalletService);
const req = {
wallet,
useWalletService,
@@ -396,6 +463,9 @@ export function* requestHistoryNanoContract({ payload }) {
);
// give opportunity for users to send the error to our team
yield put(onExceptionCaptured(error, false));
+ } finally {
+ // Always release the lock, even on error or early return
+ cleanupLoadingLock(ncId, requestType);
}
}