UNPKG

@ledgerhq/live-common

Version:
347 lines • 14.6 kB
"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 () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __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.BridgeSync = void 0; exports.resetStates = resetStates; 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 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, }; } // 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_1.getAccountCurrency)(a).id : account.currency.name, tokenTicker: (0, account_1.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