"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resetStates = exports.BridgeSync = void 0;
const logs_1 = require("@ledgerhq/logs");
const shuffle_1 = __importDefault(require("lodash/shuffle"));
const priorityQueue_1 = __importDefault(require("async/priorityQueue"));
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const react_1 = __importStar(require("react"));
const account_1 = require("../../account");
const __1 = require("..");
const account_2 = require("../../account");
const live_env_1 = require("@ledgerhq/live-env");
const context_1 = require("./context");
const syncSessionManager_1 = require("../syncSessionManager");
const BridgeSync = ({ children, accounts, updateAccountWithUpdater, recoverError, trackAnalytics, prepareCurrency, hydrateCurrency, blacklistedTokenIds, }) => {
    useHydrate({
        accounts,
        hydrateCurrency,
    });
    const sessionManager = (0, react_1.useRef)((0, syncSessionManager_1.createSyncSessionManager)(trackAnalytics)).current;
    const [syncQueue, syncState] = useSyncQueue({
        accounts,
        prepareCurrency,
        recoverError,
        trackAnalytics,
        updateAccountWithUpdater,
        blacklistedTokenIds,
        sessionManager,
    });
    const sync = useSync({
        syncQueue,
        accounts,
        sessionManager,
    });
    useSyncBackground({
        sync,
    });
    useSyncContinouslyPendingOperations({
        sync,
        accounts,
    });
    return (react_1.default.createElement(context_1.BridgeSyncStateContext.Provider, { value: syncState },
        react_1.default.createElement(context_1.BridgeSyncContext.Provider, { value: sync }, children)));
};
exports.BridgeSync = BridgeSync;
// utility internal hooks for <BridgeSync>
// useHydrate: bridge.hydrate once for each currency
function useHydrate({ accounts, hydrateCurrency }) {
    const hydratedCurrencies = (0, react_1.useRef)({});
    (0, react_1.useEffect)(() => {
        const hydrated = hydratedCurrencies.current;
        for (const account of accounts) {
            const { currency } = account;
            if (!hydrated[currency.id]) {
                hydrated[currency.id] = true;
                hydrateCurrency(currency);
            }
        }
    }, [accounts, hydrateCurrency]);
}
let lastTimeAnalyticsTrackPerAccountId = {};
let nothingState = {
    pending: false,
    error: null,
};
// Only used for tests
function resetStates() {
    lastTimeAnalyticsTrackPerAccountId = {};
    nothingState = {
        pending: false,
        error: null,
    };
}
exports.resetStates = resetStates;
// useHydrate: returns a sync queue and bridge sync state
function useSyncQueue({ accounts, prepareCurrency, recoverError, trackAnalytics, updateAccountWithUpdater, blacklistedTokenIds, sessionManager, }) {
    const [bridgeSyncState, setBridgeSyncState] = (0, react_1.useState)({});
    const setAccountSyncState = (0, react_1.useCallback)((accountId, s) => {
        setBridgeSyncState(state => ({ ...state, [accountId]: s }));
    }, []);
    const synchronize = (0, react_1.useCallback)(({ accountId, reason }, next) => {
        const state = bridgeSyncState[accountId] || nothingState;
        if (state.pending) {
            next();
            return;
        }
        const account = accounts.find(a => a.id === accountId);
        if (!account) {
            next();
            return;
        }
        // FIXME if we want to stop syncs for specific currency (e.g. api down) we would do it here
        try {
            const bridge = (0, __1.getAccountBridge)(account);
            setAccountSyncState(accountId, {
                pending: true,
                error: null,
            });
            const startSyncTime = Date.now();
            const trackedRecently = lastTimeAnalyticsTrackPerAccountId[accountId] &&
                startSyncTime - lastTimeAnalyticsTrackPerAccountId[accountId] < 90 * 1000;
            if (!trackedRecently) {
                lastTimeAnalyticsTrackPerAccountId[accountId] = startSyncTime;
            }
            const trackSyncSuccessEnd = () => {
                if (trackedRecently)
                    return;
                const account = accounts.find(a => a.id === accountId);
                if (!account)
                    return;
                const subAccounts = account.subAccounts || [];
                if (reason === "background") {
                    // don't track background syncs
                    return;
                }
                trackAnalytics("SyncSuccess", {
                    duration: (Date.now() - startSyncTime) / 1000,
                    currencyName: account.currency.name,
                    derivationMode: account.derivationMode,
                    freshAddressPath: account.freshAddressPath,
                    operationsLength: account.operationsCount,
                    accountsCountForCurrency: accounts.filter(a => a.currency === account.currency).length,
                    tokensLength: subAccounts.length,
                    tokens: subAccounts.map(a => ({
                        tokenId: a.type === "TokenAccount" ? (0, account_2.getAccountCurrency)(a).id : account.currency.name,
                        tokenTicker: (0, account_2.getAccountCurrency)(a).ticker,
                        operationsLength: a.operationsCount,
                        parentCurrencyName: account.currency.name,
                        parentDerivationMode: account.derivationMode,
                        votesCount: (0, account_1.getVotesCount)(a, account),
                    })),
                    votesCount: (0, account_1.getVotesCount)(account),
                    reason,
                });
            };
            const syncConfig = {
                paginationConfig: {},
                blacklistedTokenIds,
            };
            (0, rxjs_1.concat)((0, rxjs_1.from)(prepareCurrency(account.currency)).pipe((0, operators_1.ignoreElements)()), bridge.sync(account, syncConfig)).subscribe({
                next: accountUpdater => {
                    updateAccountWithUpdater(accountId, accountUpdater);
                },
                complete: () => {
                    trackSyncSuccessEnd();
                    setAccountSyncState(accountId, {
                        pending: false,
                        error: null,
                    });
                    sessionManager.onAccountSyncDone(accountId, accounts);
                    next();
                },
                error: (raw) => {
                    const error = recoverError(raw);
                    if (!error) {
                        // This error is normal because the thread was recently killed. we silent it for the user.
                        setAccountSyncState(accountId, {
                            pending: false,
                            error: null,
                        });
                        sessionManager.onAccountSyncDone(accountId, accounts);
                        next();
                        return;
                    }
                    setAccountSyncState(accountId, {
                        pending: false,
                        error,
                    });
                    sessionManager.onAccountSyncDone(accountId, accounts, true);
                    next();
                },
            });
        }
        catch (error) {
            setAccountSyncState(accountId, {
                pending: false,
                error,
            });
            sessionManager.onAccountSyncDone(accountId, accounts, true);
            next();
        }
    }, [
        accounts,
        bridgeSyncState,
        prepareCurrency,
        recoverError,
        setAccountSyncState,
        trackAnalytics,
        updateAccountWithUpdater,
        blacklistedTokenIds,
        sessionManager,
    ]);
    const synchronizeRef = (0, react_1.useRef)(synchronize);
    (0, react_1.useEffect)(() => {
        synchronizeRef.current = synchronize;
    }, [synchronize]);
    const [syncQueue] = (0, react_1.useState)(() => (0, priorityQueue_1.default)((job, next) => synchronizeRef.current(job, next), (0, live_env_1.getEnv)("SYNC_MAX_CONCURRENT")));
    return [syncQueue, bridgeSyncState];
}
// useSync: returns a sync function with the syncQueue
function useSync({ syncQueue, accounts, sessionManager }) {
    const skipUnderPriority = (0, react_1.useRef)(-1);
    const sync = (0, react_1.useMemo)(() => {
        const schedule = (ids, priority, reason) => {
            if (priority < skipUnderPriority.current)
                return;
            // by convention we remove concurrent tasks with same priority
            // FIXME this is somehow a hack. ideally we should just dedup the account ids in the pending queue...
            syncQueue.remove(o => priority === o.priority);
            // start a global session only if initial + all accounts
            if (reason === "initial" && ids.length === accounts.length) {
                sessionManager.start(ids, reason);
            }
            (0, logs_1.log)("bridge", "schedule " + ids.join(", "));
            syncQueue.push(ids.map(accountId => ({
                accountId,
                reason,
            })), -priority);
        };
        // don't always sync in the same order to avoid potential "account never reached"
        const shuffledAccountIds = () => (0, shuffle_1.default)(accounts.map(a => a.id));
        const handlers = {
            BACKGROUND_TICK: ({ reason }) => {
                if (syncQueue.idle()) {
                    schedule(shuffledAccountIds(), -1, reason);
                }
            },
            SET_SKIP_UNDER_PRIORITY: ({ priority }) => {
                if (priority === skipUnderPriority.current)
                    return;
                skipUnderPriority.current = priority;
                syncQueue.remove(({ priority }) => priority < skipUnderPriority.current);
                if (priority === -1 && !accounts.every(account_1.isUpToDateAccount)) {
                    // going back to -1 priority => retriggering a background sync if it is "Paused"
                    schedule(shuffledAccountIds(), -1, "outdated");
                }
            },
            SYNC_ALL_ACCOUNTS: ({ priority, reason }) => {
                schedule(shuffledAccountIds(), priority, reason);
            },
            SYNC_ONE_ACCOUNT: ({ accountId, priority, reason, }) => {
                schedule([accountId], priority, reason);
            },
            SYNC_SOME_ACCOUNTS: ({ accountIds, priority, reason, }) => {
                schedule(accountIds, priority, reason);
            },
        };
        return (action) => {
            const handler = handlers[action.type];
            if (handler) {
                (0, logs_1.log)("bridge", `action ${action.type}`, {
                    action,
                    type: "syncQueue",
                });
                handler(action);
            }
            else {
                (0, logs_1.log)("warn", "BridgeSyncContext unsupported action", {
                    action,
                    type: "syncQueue",
                });
            }
        };
    }, [accounts, syncQueue, sessionManager]);
    const ref = (0, react_1.useRef)(sync);
    (0, react_1.useEffect)(() => {
        ref.current = sync;
    }, [sync]);
    const syncFn = (0, react_1.useCallback)(action => ref.current(action), [ref]);
    return syncFn;
}
// useSyncBackground: continuously synchronize accounts in background
function useSyncBackground({ sync }) {
    (0, react_1.useEffect)(() => {
        let syncTimeout;
        const syncLoop = async (reason) => {
            sync({
                type: "BACKGROUND_TICK",
                reason,
            });
            syncTimeout = setTimeout(syncLoop, (0, live_env_1.getEnv)("SYNC_ALL_INTERVAL"), "background");
        };
        syncTimeout = setTimeout(syncLoop, (0, live_env_1.getEnv)("SYNC_BOOT_DELAY"), "initial");
        return () => clearTimeout(syncTimeout);
    }, [sync]);
}
// useSyncContinouslyPendingOperations: continously sync accounts with pending operations
function useSyncContinouslyPendingOperations({ sync, accounts }) {
    const ids = (0, react_1.useMemo)(() => accounts.filter(a => a.pendingOperations.length > 0).map(a => a.id), [accounts]);
    const refIds = (0, react_1.useRef)(ids);
    (0, react_1.useEffect)(() => {
        refIds.current = ids;
    }, [ids]);
    (0, react_1.useEffect)(() => {
        let timeout;
        const update = () => {
            timeout = setTimeout(update, (0, live_env_1.getEnv)("SYNC_PENDING_INTERVAL"));
            if (!refIds.current.length)
                return;
            sync({
                type: "SYNC_SOME_ACCOUNTS",
                accountIds: refIds.current,
                priority: 20,
                reason: "pending-operations",
            });
        };
        timeout = setTimeout(update, (0, live_env_1.getEnv)("SYNC_PENDING_INTERVAL"));
        return () => clearTimeout(timeout);
    }, [sync]);
}
//# sourceMappingURL=BridgeSync.js.map