@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
347 lines • 14.6 kB
JavaScript
;
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