UNPKG

@ledgerhq/live-common

Version:
306 lines • 12.4 kB
import { log } from "@ledgerhq/logs"; import shuffle from "lodash/shuffle"; import priorityQueue from "async/priorityQueue"; import { concat, from } from "rxjs"; import { ignoreElements } from "rxjs/operators"; import React, { useEffect, useCallback, useState, useRef, useMemo } from "react"; import { getVotesCount, isUpToDateAccount, getAccountCurrency } from "../../account"; import { getAccountBridge } from ".."; import { getEnv } from "@ledgerhq/live-env"; import { BridgeSyncContext, BridgeSyncStateContext } from "./context"; import { createSyncSessionManager } from "../syncSessionManager"; export const BridgeSync = ({ children, accounts, updateAccountWithUpdater, recoverError, trackAnalytics, prepareCurrency, hydrateCurrency, blacklistedTokenIds, }) => { useHydrate({ accounts, hydrateCurrency, }); const sessionManager = useRef(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.createElement(BridgeSyncStateContext.Provider, { value: syncState }, React.createElement(BridgeSyncContext.Provider, { value: sync }, children))); }; // utility internal hooks for <BridgeSync> // useHydrate: bridge.hydrate once for each currency function useHydrate({ accounts, hydrateCurrency }) { const hydratedCurrencies = useRef({}); 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 export function resetStates() { lastTimeAnalyticsTrackPerAccountId = {}; nothingState = { pending: false, error: null, }; } // useHydrate: returns a sync queue and bridge sync state function useSyncQueue({ accounts, prepareCurrency, recoverError, trackAnalytics, updateAccountWithUpdater, blacklistedTokenIds, sessionManager, }) { const [bridgeSyncState, setBridgeSyncState] = useState({}); const setAccountSyncState = useCallback((accountId, s) => { setBridgeSyncState(state => ({ ...state, [accountId]: s })); }, []); const synchronize = 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 = 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" ? getAccountCurrency(a).id : account.currency.name, tokenTicker: getAccountCurrency(a).ticker, operationsLength: a.operationsCount, parentCurrencyName: account.currency.name, parentDerivationMode: account.derivationMode, votesCount: getVotesCount(a, account), })), votesCount: getVotesCount(account), reason, }); }; const syncConfig = { paginationConfig: {}, blacklistedTokenIds, }; concat(from(prepareCurrency(account.currency)).pipe(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 = useRef(synchronize); useEffect(() => { synchronizeRef.current = synchronize; }, [synchronize]); const [syncQueue] = useState(() => priorityQueue((job, next) => synchronizeRef.current(job, next), getEnv("SYNC_MAX_CONCURRENT"))); return [syncQueue, bridgeSyncState]; } // useSync: returns a sync function with the syncQueue function useSync({ syncQueue, accounts, sessionManager }) { const skipUnderPriority = useRef(-1); const sync = 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); } 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 = () => shuffle(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(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) { log("bridge", `action ${action.type}`, { action, type: "syncQueue", }); handler(action); } else { log("warn", "BridgeSyncContext unsupported action", { action, type: "syncQueue", }); } }; }, [accounts, syncQueue, sessionManager]); const ref = useRef(sync); useEffect(() => { ref.current = sync; }, [sync]); const syncFn = useCallback(action => ref.current(action), [ref]); return syncFn; } // useSyncBackground: continuously synchronize accounts in background function useSyncBackground({ sync }) { useEffect(() => { let syncTimeout; const syncLoop = async (reason) => { sync({ type: "BACKGROUND_TICK", reason, }); syncTimeout = setTimeout(syncLoop, getEnv("SYNC_ALL_INTERVAL"), "background"); }; syncTimeout = setTimeout(syncLoop, getEnv("SYNC_BOOT_DELAY"), "initial"); return () => clearTimeout(syncTimeout); }, [sync]); } // useSyncContinouslyPendingOperations: continously sync accounts with pending operations function useSyncContinouslyPendingOperations({ sync, accounts }) { const ids = useMemo(() => accounts.filter(a => a.pendingOperations.length > 0).map(a => a.id), [accounts]); const refIds = useRef(ids); useEffect(() => { refIds.current = ids; }, [ids]); useEffect(() => { let timeout; const update = () => { timeout = setTimeout(update, 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, getEnv("SYNC_PENDING_INTERVAL")); return () => clearTimeout(timeout); }, [sync]); } //# sourceMappingURL=BridgeSync.js.map