From 9a0dd60cf8a997da451a995a14482d7b0878677b Mon Sep 17 00:00:00 2001 From: morizon Date: Fri, 13 Dec 2024 13:28:37 +0800 Subject: [PATCH 1/2] fix: optimize localDB getAllRecords performance --- apps/desktop/App.tsx | 1 + development/spellCheckerSkipWords.js | 1 + packages/kit-bg/src/dbs/local/LocalDbBase.ts | 149 +++++++---- .../src/dbs/local/LocalDbBaseContainer.ts | 31 ++- .../src/dbs/local/indexed/IndexedDBAgent.ts | 2 +- .../src/dbs/local/indexed/LocalDbIndexed.ts | 2 +- .../src/dbs/local/realm/LocalDbRealm.ts | 2 +- packages/kit-bg/src/dbs/local/types.ts | 8 +- .../services/ServiceAccount/ServiceAccount.ts | 80 ++++-- .../src/services/ServiceAccountSelector.ts | 37 +-- .../ServiceAllNetwork/ServiceAllNetwork.ts | 4 +- .../ServiceAppCleanup/ServiceAppCleanup.ts | 23 +- .../ServiceNotification.ts | 253 ++++++++++++------ packages/kit-bg/src/services/ServiceToken.ts | 14 - packages/kit-bg/src/states/jotai/atomNames.ts | 8 + .../kit-bg/src/states/jotai/jotaiStorage.ts | 139 ++++++++-- .../kit-bg/src/states/jotai/utils/index.ts | 14 +- .../src/states/jotai/utils/jotaiVerify.ts | 18 ++ .../src/states/jotai/utils/wrapAtomPro.ts | 32 ++- .../GlobalJotaiReady/GlobalJotaiReady.tsx | 6 +- .../kit/src/components/RenameDialog/index.tsx | 2 +- .../src/components/TokenListView/index.tsx | 7 + .../jotai/contexts/accountSelector/atoms.ts | 1 - .../kit/src/views/Home/pages/HomePageView.tsx | 38 +-- .../views/Home/pages/TokenListContainer.tsx | 48 +++- .../Notifications/ManageAccountActivity.tsx | 4 +- .../shared/src/errors/utils/errorUtils.ts | 12 +- packages/shared/src/utils/cacheUtils.ts | 2 + .../shared/src/utils/debug/dbPerfMonitor.ts | 18 +- packages/shared/src/utils/debug/perfUtils.ts | 10 +- packages/shared/src/utils/promiseUtils.ts | 18 ++ 31 files changed, 703 insertions(+), 281 deletions(-) create mode 100644 packages/kit-bg/src/states/jotai/utils/jotaiVerify.ts diff --git a/apps/desktop/App.tsx b/apps/desktop/App.tsx index ff064999bcc..4a03038a5b8 100644 --- a/apps/desktop/App.tsx +++ b/apps/desktop/App.tsx @@ -12,3 +12,4 @@ import { initSentry(); export default withSentryHOC(KitProvider); +// export default KitProvider; diff --git a/development/spellCheckerSkipWords.js b/development/spellCheckerSkipWords.js index ebfaf81d6f4..e916c9bb87c 100644 --- a/development/spellCheckerSkipWords.js +++ b/development/spellCheckerSkipWords.js @@ -20,6 +20,7 @@ module.exports = [ 'hdk', 'dkey', 'impls', + 'ttl', 'Sollet', 'Solflare', 'encryptors', diff --git a/packages/kit-bg/src/dbs/local/LocalDbBase.ts b/packages/kit-bg/src/dbs/local/LocalDbBase.ts index 7290af34e03..37030320fd3 100644 --- a/packages/kit-bg/src/dbs/local/LocalDbBase.ts +++ b/packages/kit-bg/src/dbs/local/LocalDbBase.ts @@ -79,6 +79,7 @@ import { EDBAccountType } from './consts'; import { LocalDbBaseContainer } from './LocalDbBaseContainer'; import { ELocalDBStoreNames } from './localDBStoreNames'; +import type { IDeviceType } from '@onekeyfe/hd-core'; import type { IDBAccount, IDBApiGetContextOptions, @@ -91,7 +92,6 @@ import type { IDBDeviceSettings, IDBEnsureAccountNameNotDuplicateParams, IDBExternalAccount, - IDBGetAllWalletsParams, IDBGetWalletsParams, IDBIndexedAccount, IDBRemoveWalletParams, @@ -109,7 +109,6 @@ import type { ILocalDBTransaction, ILocalDBTxGetRecordByIdResult, } from './types'; -import type { IDeviceType } from '@onekeyfe/hd-core'; const getOrderByWalletType = (walletType: IDBWalletType): number => { switch (walletType) { @@ -511,30 +510,6 @@ export abstract class LocalDbBase extends LocalDbBaseContainer { walletSortFn = (a: IDBWallet, b: IDBWallet) => (a.walletOrder ?? 0) - (b.walletOrder ?? 0); - async getAllWallets({ - refillWalletInfo, - }: IDBGetAllWalletsParams = {}): Promise<{ - wallets: IDBWallet[]; - }> { - let { records } = await this.getAllRecords({ - name: ELocalDBStoreNames.Wallet, - }); - if (refillWalletInfo) { - const { devices: allDevices } = await this.getAllDevices(); - const refilledWalletsCache: { - [walletId: string]: IDBWallet; - } = {}; - records = await Promise.all( - records.map((wallet) => - this.refillWalletInfo({ wallet, refilledWalletsCache, allDevices }), - ), - ); - } - return { - wallets: records, - }; - } - // eslint-disable-next-line spellcheck/spell-checker /** * Get wallets @@ -552,8 +527,9 @@ export abstract class LocalDbBase extends LocalDbBaseContainer { let allIndexedAccounts: IDBIndexedAccount[] | undefined; if (includingAccounts) { if (!allIndexedAccounts) { - allIndexedAccounts = (await this.getAllIndexedAccounts()) - .indexedAccounts; + allIndexedAccounts = + option?.allIndexedAccounts || + (await this.getAllIndexedAccounts()).indexedAccounts; } } @@ -577,8 +553,9 @@ export abstract class LocalDbBase extends LocalDbBaseContainer { }; // get all wallets for account selector - let { wallets } = await this.getAllWallets(); - const { devices: allDevices } = await this.getAllDevices(); + let wallets = option?.allWallets || (await this.getAllWallets()).wallets; + const allDevices = + option?.allDevices || (await this.getAllDevices()).devices; const hiddenWalletsMap: Partial<{ [dbDeviceId: string]: IDBWallet[]; }> = {}; @@ -2611,7 +2588,8 @@ export abstract class LocalDbBase extends LocalDbBaseContainer { accounts: IDBAccount[]; }> { const wallet = await this.getWalletSafe({ walletId }); - if (!wallet) { + if (!wallet || !wallet?.accounts?.length) { + // if (!wallet) { return { accounts: [] }; } const { accounts } = await this.getAllAccounts({ @@ -2648,13 +2626,14 @@ export abstract class LocalDbBase extends LocalDbBaseContainer { const indexedAccount = await this.getIndexedAccount({ id: indexedAccountId, }); - const { accounts } = await this.getAllAccounts(); - return accounts + const allDbAccounts = (await this.getAllAccounts()).accounts; + const accounts = allDbAccounts .filter( (account) => account.indexedAccountId === indexedAccountId && indexedAccountId, ) .map((account) => this.refillAccountInfo({ account, indexedAccount })); + return { accounts, allDbAccounts }; } async getAccount({ accountId }: { accountId: string }): Promise { @@ -2749,18 +2728,101 @@ export abstract class LocalDbBase extends LocalDbBaseContainer { }); } - async getAllIndexedAccounts() { + async getAllDevices(): Promise<{ devices: IDBDevice[] }> { + const cacheKey = 'allDbDevices'; + const allDevicesInCache = this.dbAllRecordsCache.get( + cacheKey, + ) as IDBDevice[]; + if (allDevicesInCache && allDevicesInCache.length) { + return { devices: allDevicesInCache }; + } + const { records: devices } = await this.getAllRecords({ + name: ELocalDBStoreNames.Device, + }); + devices.forEach((item) => this.refillDeviceInfo({ device: item })); + this.dbAllRecordsCache.set(cacheKey, devices); + return { devices }; + } + + async getAllWallets(): Promise<{ + wallets: IDBWallet[]; + }> { + const cacheKey = 'allDbWallets'; + const allWalletsInCache = this.dbAllRecordsCache.get( + cacheKey, + ) as IDBWallet[]; + if (allWalletsInCache && allWalletsInCache.length) { + return { wallets: allWalletsInCache }; + } + const { records: wallets } = await this.getAllRecords({ + name: ELocalDBStoreNames.Wallet, + }); + this.dbAllRecordsCache.set(cacheKey, wallets); + return { + wallets, + }; + } + + // async getAllWallets({ + // refillWalletInfo, + // }: IDBGetAllWalletsParams = {}): Promise<{ + // wallets: IDBWallet[]; + // }> { + // let { records } = await this.getAllRecords({ + // name: ELocalDBStoreNames.Wallet, + // }); + // if (refillWalletInfo) { + // const { devices: allDevices } = await this.getAllDevices(); + // const refilledWalletsCache: { + // [walletId: string]: IDBWallet; + // } = {}; + // records = await Promise.all( + // records.map((wallet) => + // this.refillWalletInfo({ wallet, refilledWalletsCache, allDevices }), + // ), + // ); + // } + // return { + // wallets: records, + // }; + // } + + async getAllIndexedAccounts(): Promise<{ + indexedAccounts: IDBIndexedAccount[]; + }> { + const cacheKey = 'allDbIndexedAccounts'; + const allIndexedAccountsInCache = this.dbAllRecordsCache.get( + cacheKey, + ) as IDBIndexedAccount[]; + if (allIndexedAccountsInCache && allIndexedAccountsInCache.length) { + return { indexedAccounts: allIndexedAccountsInCache }; + } const { records: indexedAccounts } = await this.getAllRecords({ name: ELocalDBStoreNames.IndexedAccount, }); + this.dbAllRecordsCache.set(cacheKey, indexedAccounts); return { indexedAccounts }; } - async getAllAccounts({ ids }: { ids?: string[] } = {}) { + async getAllAccounts({ ids }: { ids?: string[] } = {}): Promise<{ + accounts: IDBAccount[]; + }> { + const cacheKey = 'allDbAccounts'; + if (!ids) { + const allDbAccountsInCache = this.dbAllRecordsCache.get( + cacheKey, + ) as IDBAccount[]; + if (allDbAccountsInCache && allDbAccountsInCache?.length) { + return { accounts: allDbAccountsInCache }; + } + } const { records: accounts } = await this.getAllRecords({ name: ELocalDBStoreNames.Account, ids, }); + if (!ids) { + this.dbAllRecordsCache.set(cacheKey, accounts); + } return { accounts }; } @@ -2961,9 +3023,11 @@ export abstract class LocalDbBase extends LocalDbBaseContainer { if (params.indexedAccountId) { // TODO low performance - accounts = await this.getAccountsInSameIndexedAccountId({ - indexedAccountId: params.indexedAccountId, - }); + accounts = ( + await this.getAccountsInSameIndexedAccountId({ + indexedAccountId: params.indexedAccountId, + }) + ).accounts; } if (params.accountId) { const account = await this.getAccountSafe({ @@ -3031,15 +3095,6 @@ export abstract class LocalDbBase extends LocalDbBaseContainer { // ---------------------------------------------- device - async getAllDevices(): Promise<{ devices: IDBDevice[] }> { - // TODO performance - const { records: devices } = await this.getAllRecords({ - name: ELocalDBStoreNames.Device, - }); - devices.forEach((item) => this.refillDeviceInfo({ device: item })); - return { devices }; - } - async getSameDeviceByUUIDEvenIfReset(uuid: string) { const { devices } = await this.getAllDevices(); return devices.find((item) => uuid && item.uuid === uuid); diff --git a/packages/kit-bg/src/dbs/local/LocalDbBaseContainer.ts b/packages/kit-bg/src/dbs/local/LocalDbBaseContainer.ts index 2062ae8cad9..971ecbcd8fe 100644 --- a/packages/kit-bg/src/dbs/local/LocalDbBaseContainer.ts +++ b/packages/kit-bg/src/dbs/local/LocalDbBaseContainer.ts @@ -1,10 +1,14 @@ import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; -import { memoizee } from '@onekeyhq/shared/src/utils/cacheUtils'; +import cacheUtils, { memoizee } from '@onekeyhq/shared/src/utils/cacheUtils'; import timerUtils from '@onekeyhq/shared/src/utils/timerUtils'; import { ELocalDBStoreNames } from './localDBStoreNames'; import type { + IDBAccount, + IDBDevice, + IDBIndexedAccount, + IDBWallet, ILocalDBAgent, ILocalDBGetAllRecordsParams, ILocalDBGetAllRecordsResult, @@ -98,12 +102,25 @@ export abstract class LocalDbBaseContainer implements ILocalDBAgent { ].includes(storeName); } - clearStoreCachedData(storeName: ELocalDBStoreNames) { + dbAllRecordsCache = new cacheUtils.LRUCache< + 'allDbAccounts' | 'allDbIndexedAccounts' | 'allDbWallets' | 'allDbDevices', + IDBAccount[] | IDBIndexedAccount[] | IDBWallet[] | IDBDevice[] + >({ + max: 10, + ttl: timerUtils.getTimeDurationMs({ seconds: 5 }), + }); + + clearStoreCachedDataIfMatch(storeName: ELocalDBStoreNames) { if (this.isCachedStoreName(storeName)) { - this.getRecordByIdWithCache.clear(); + this.clearStoreCachedData(); } } + clearStoreCachedData() { + this.getRecordByIdWithCache.clear(); + this.dbAllRecordsCache.clear(); + } + async txGetAllRecords( params: ILocalDBTxGetAllRecordsParams, ): Promise> { @@ -121,7 +138,7 @@ export abstract class LocalDbBaseContainer implements ILocalDBAgent { async txUpdateRecords( params: ILocalDBTxUpdateRecordsParams, ): Promise { - this.clearStoreCachedData(params.name); + this.clearStoreCachedDataIfMatch(params.name); const db = await this.readyDb; // const a = db.txAddRecords['hello-world-test-error-stack-8889273']['name']; return db.txUpdateRecords(params); @@ -130,7 +147,7 @@ export abstract class LocalDbBaseContainer implements ILocalDBAgent { async txAddRecords( params: ILocalDBTxAddRecordsParams, ): Promise { - this.clearStoreCachedData(params.name); + this.clearStoreCachedDataIfMatch(params.name); const db = await this.readyDb; return db.txAddRecords(params); } @@ -138,7 +155,7 @@ export abstract class LocalDbBaseContainer implements ILocalDBAgent { async txRemoveRecords( params: ILocalDBTxRemoveRecordsParams, ): Promise { - this.clearStoreCachedData(params.name); + this.clearStoreCachedDataIfMatch(params.name); const db = await this.readyDb; return db.txRemoveRecords(params); } @@ -146,7 +163,7 @@ export abstract class LocalDbBaseContainer implements ILocalDBAgent { abstract reset(): Promise; async clearRecords(params: { name: ELocalDBStoreNames }) { - this.clearStoreCachedData(params.name); + this.clearStoreCachedDataIfMatch(params.name); const db = await this.readyDb; return db.clearRecords(params); } diff --git a/packages/kit-bg/src/dbs/local/indexed/IndexedDBAgent.ts b/packages/kit-bg/src/dbs/local/indexed/IndexedDBAgent.ts index ce0ebfb2c7e..b1d7cf3f878 100644 --- a/packages/kit-bg/src/dbs/local/indexed/IndexedDBAgent.ts +++ b/packages/kit-bg/src/dbs/local/indexed/IndexedDBAgent.ts @@ -309,7 +309,7 @@ export class IndexedDBAgent extends LocalDbAgentBase implements ILocalDBAgent { ): Promise> { const { tx: paramsTx, name, ids, limit, offset } = params; dbPerfMonitor.logLocalDbCall(`txGetAllRecords`, name, [ - `records: ${ids?.length || ''}`, + `ids_count=${ids ? ids?.length?.toString() : 'ALL'}`, ]); const fn = async (tx: ILocalDBTransaction) => { const store = this._getObjectStoreFromTx(tx, name); diff --git a/packages/kit-bg/src/dbs/local/indexed/LocalDbIndexed.ts b/packages/kit-bg/src/dbs/local/indexed/LocalDbIndexed.ts index 65c11584737..8c48ae5bcf3 100644 --- a/packages/kit-bg/src/dbs/local/indexed/LocalDbIndexed.ts +++ b/packages/kit-bg/src/dbs/local/indexed/LocalDbIndexed.ts @@ -6,7 +6,7 @@ import { LocalDbIndexedBase } from './LocalDbIndexedBase'; export class LocalDbIndexed extends LocalDbIndexedBase { async reset(): Promise { - this.clearStoreCachedData(ELocalDBStoreNames.IndexedAccount); + this.clearStoreCachedData(); return this.deleteIndexedDb(); } } diff --git a/packages/kit-bg/src/dbs/local/realm/LocalDbRealm.ts b/packages/kit-bg/src/dbs/local/realm/LocalDbRealm.ts index 24cc720cfb6..832530bf4a8 100644 --- a/packages/kit-bg/src/dbs/local/realm/LocalDbRealm.ts +++ b/packages/kit-bg/src/dbs/local/realm/LocalDbRealm.ts @@ -4,7 +4,7 @@ import { LocalDbRealmBase } from './LocalDbRealmBase'; export class LocalDbRealm extends LocalDbRealmBase { reset(): Promise { - this.clearStoreCachedData(ELocalDBStoreNames.IndexedAccount); + this.clearStoreCachedData(); return this.deleteDb(); } } diff --git a/packages/kit-bg/src/dbs/local/types.ts b/packages/kit-bg/src/dbs/local/types.ts index 36f9b310f9d..6c41a531828 100644 --- a/packages/kit-bg/src/dbs/local/types.ts +++ b/packages/kit-bg/src/dbs/local/types.ts @@ -200,10 +200,12 @@ export type IDBGetWalletsParams = { nestedHiddenWallets?: boolean | undefined; ignoreEmptySingletonWalletAccounts?: boolean | undefined; includingAccounts?: boolean | undefined; + + allIndexedAccounts?: IDBIndexedAccount[] | undefined; + allWallets?: IDBWallet[] | undefined; + allDevices?: IDBDevice[] | undefined; }; -export type IDBGetAllWalletsParams = { - refillWalletInfo?: boolean; -}; + // ---------------------------------------------- account export type IDBAvatar = string; // stringify(IAvatarInfo) // IAvatar; diff --git a/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts b/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts index 49ccb04252f..675a5b9eeb0 100644 --- a/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts +++ b/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts @@ -91,7 +91,6 @@ import type { IDBDevice, IDBEnsureAccountNameNotDuplicateParams, IDBExternalAccount, - IDBGetAllWalletsParams, IDBGetWalletsParams, IDBIndexedAccount, IDBRemoveWalletParams, @@ -169,6 +168,7 @@ class ServiceAccount extends ServiceBase { clearAccountCache() { this.getIndexedAccountWithMemo.clear(); + localDb.clearStoreCachedData(); } @backgroundMethod() @@ -1695,8 +1695,10 @@ class ServiceAccount extends ServiceBase { ids?: string[]; filterRemoved?: boolean; } = {}) { + let accounts: IDBAccount[] = []; + // filter accounts match to available wallets, some account wallet or indexedAccount may be deleted - const { accounts } = await localDb.getAllAccounts({ ids }); + ({ accounts } = await localDb.getAllAccounts({ ids })); const removedHiddenWallet: { [walletId: string]: true; @@ -1711,12 +1713,22 @@ class ServiceAccount extends ServiceBase { let accountsFiltered: IDBAccount[] = accounts; let accountsRemoved: IDBAccount[] | undefined; + let allWallets: IDBWallet[] | undefined; + let indexedAccounts: IDBIndexedAccount[] = []; + let indexedAccountsRemoved: IDBIndexedAccount[] = []; + let allDevices: IDBDevice[] | undefined; + if (filterRemoved) { - const { wallets } = await this.getAllWallets({ refillWalletInfo: true }); - const { indexedAccounts } = await this.getAllIndexedAccounts({ - allWallets: wallets, - filterRemoved, + const allWalletsResult = await this.getAllWallets({ + refillWalletInfo: true, }); + allWallets = allWalletsResult.wallets; + allDevices = allWalletsResult.allDevices; + ({ indexedAccounts, indexedAccountsRemoved } = + await this.getAllIndexedAccounts({ + allWallets, + filterRemoved: true, + })); accountsRemoved = []; accountsFiltered = ( @@ -1744,33 +1756,36 @@ class ServiceAccount extends ServiceBase { pushRemovedAccount(); return null; } - const wallet: IDBWallet | undefined = wallets.find( + const wallet: IDBWallet | undefined = allWallets?.find( (o) => o.id === walletId, ); - if (!wallet) { + if (!wallet && allWallets) { removedWallet[walletId] = true; pushRemovedAccount(); return null; } - if (localDb.isTempWalletRemoved({ wallet })) { + if (wallet && localDb.isTempWalletRemoved({ wallet })) { removedHiddenWallet[walletId] = true; pushRemovedAccount(); return null; } } + let indexedAccount: IDBIndexedAccount | undefined; if (indexedAccountId) { if (removedIndexedAccount[indexedAccountId]) { pushRemovedAccount(); return null; } - const indexedAccount: IDBIndexedAccount | undefined = - indexedAccounts.find((o) => o.id === indexedAccountId); + indexedAccount = indexedAccounts.find( + (o) => o.id === indexedAccountId, + ); if (!indexedAccount) { removedIndexedAccount[indexedAccountId] = true; pushRemovedAccount(); return null; } } + localDb.refillAccountInfo({ account, indexedAccount }); return account; }), ) @@ -1780,11 +1795,32 @@ class ServiceAccount extends ServiceBase { return { accounts: accountsFiltered, accountsRemoved, + allWallets, + allDevices, + allIndexedAccounts: indexedAccounts, + indexedAccountsRemoved, }; } - async getAllWallets(params: IDBGetAllWalletsParams = {}) { - return localDb.getAllWallets(params); + async getAllWallets(params: { refillWalletInfo?: boolean } = {}) { + let { wallets } = await localDb.getAllWallets(); + let allDevices: IDBDevice[] | undefined; + if (params.refillWalletInfo) { + allDevices = (await this.getAllDevices()).devices; + const refilledWalletsCache: { + [walletId: string]: IDBWallet; + } = {}; + wallets = await Promise.all( + wallets.map((wallet) => + localDb.refillWalletInfo({ + wallet, + refilledWalletsCache, + allDevices, + }), + ), + ); + } + return { wallets, allDevices }; } async getAllDevices() { @@ -1797,8 +1833,15 @@ class ServiceAccount extends ServiceBase { indexedAccountId, }: { indexedAccountId: string; - }): Promise { - return localDb.getAccountsInSameIndexedAccountId({ indexedAccountId }); + }): Promise<{ + accounts: IDBAccount[]; + allDbAccounts: IDBAccount[]; + }> { + const result = await localDb.getAccountsInSameIndexedAccountId({ + indexedAccountId, + }); + + return result; } @backgroundMethod() @@ -2636,9 +2679,10 @@ class ServiceAccount extends ServiceBase { perf.markStart('getAccountsInSameIndexedAccountId'); const { serviceNetwork } = this.backgroundApi; - const dbAccounts = await this.getAccountsInSameIndexedAccountId({ - indexedAccountId, - }); + const { accounts: dbAccounts } = + await this.getAccountsInSameIndexedAccountId({ + indexedAccountId, + }); perf.markEnd('getAccountsInSameIndexedAccountId'); perf.markStart('processAllNetworksAccounts'); diff --git a/packages/kit-bg/src/services/ServiceAccountSelector.ts b/packages/kit-bg/src/services/ServiceAccountSelector.ts index 3c83137f5c8..7ed48dbc492 100644 --- a/packages/kit-bg/src/services/ServiceAccountSelector.ts +++ b/packages/kit-bg/src/services/ServiceAccountSelector.ts @@ -320,41 +320,19 @@ class ServiceAccountSelector extends ServiceBase { // } } - let allNetworkDbAccounts: IDBAccount[] | undefined; let canCreateAddress = false; if (isAllNetwork && networkId) { - try { - allNetworkDbAccounts = - await this.backgroundApi.serviceAllNetwork.getAllNetworkDbAccounts({ - networkId, - singleNetworkDeriveType: undefined, - indexedAccountId, - othersWalletAccountId, - }); - } catch (error) { - // - } - // build mocked networkAccount of all network if (!isOthersWallet && indexedAccountId) { - const updateCanCreateAddressForAllNetwork = async () => { + try { + account = + await this.backgroundApi.serviceAccount.getMockedAllNetworkAccount({ + indexedAccountId, + }); + canCreateAddress = true; + } catch (error) { account = undefined; canCreateAddress = true; - }; - if (allNetworkDbAccounts?.length) { - try { - account = - await this.backgroundApi.serviceAccount.getMockedAllNetworkAccount( - { - indexedAccountId, - }, - ); - canCreateAddress = false; - } catch (error) { - await updateCanCreateAddressForAllNetwork(); - } - } else { - await updateCanCreateAddressForAllNetwork(); } } } else { @@ -389,7 +367,6 @@ class ServiceAccountSelector extends ServiceBase { const activeAccount: IAccountSelectorActiveAccountInfo = { account, dbAccount, - allNetworkDbAccounts, indexedAccount, accountName: universalAccountName, wallet, diff --git a/packages/kit-bg/src/services/ServiceAllNetwork/ServiceAllNetwork.ts b/packages/kit-bg/src/services/ServiceAllNetwork/ServiceAllNetwork.ts index 844f1877d9d..55619d6345f 100644 --- a/packages/kit-bg/src/services/ServiceAllNetwork/ServiceAllNetwork.ts +++ b/packages/kit-bg/src/services/ServiceAllNetwork/ServiceAllNetwork.ts @@ -106,12 +106,12 @@ class ServiceAllNetwork extends ServiceBase { ); } if (isAllNetwork) { - dbAccounts = + ({ accounts: dbAccounts } = await this.backgroundApi.serviceAccount.getAccountsInSameIndexedAccountId( { indexedAccountId, }, - ); + )); } else { if (!singleNetworkDeriveType) { throw new Error( diff --git a/packages/kit-bg/src/services/ServiceAppCleanup/ServiceAppCleanup.ts b/packages/kit-bg/src/services/ServiceAppCleanup/ServiceAppCleanup.ts index 0c911a5273f..ec89055ada6 100644 --- a/packages/kit-bg/src/services/ServiceAppCleanup/ServiceAppCleanup.ts +++ b/packages/kit-bg/src/services/ServiceAppCleanup/ServiceAppCleanup.ts @@ -8,7 +8,7 @@ import localDb from '../../dbs/local/localDb'; import simpleDb from '../../dbs/simple/simpleDb'; import ServiceBase from '../ServiceBase'; -import type { IDBAccount } from '../../dbs/local/types'; +import type { IDBAccount, IDBIndexedAccount } from '../../dbs/local/types'; class ServiceAppCleanup extends ServiceBase { constructor({ backgroundApi }: { backgroundApi: any }) { @@ -48,8 +48,12 @@ class ServiceAppCleanup extends ServiceBase { @backgroundMethod() async cleanup( - params: { accountsRemoved: IDBAccount[] | undefined } = { + params: { + accountsRemoved: IDBAccount[] | undefined; + indexedAccountsRemoved: IDBIndexedAccount[] | undefined; + } = { accountsRemoved: undefined, + indexedAccountsRemoved: undefined, }, ) { const isCleanupTime = await this.isCleanupTime(); @@ -62,7 +66,7 @@ class ServiceAppCleanup extends ServiceBase { await this.cleanupAccounts(params.accountsRemoved); // **** cleanup indexed accounts - await this.cleanupIndexedAccounts(); + await this.cleanupIndexedAccounts(params.indexedAccountsRemoved); // **** cleanup credentials // The number of private key and mnemonic wallets will not be many, so we don't clean up here @@ -100,12 +104,15 @@ class ServiceAppCleanup extends ServiceBase { }); } - async cleanupIndexedAccounts() { + async cleanupIndexedAccounts(indexedAccountsRemoved?: IDBIndexedAccount[]) { await this.runCleanupTask(async () => { - const { indexedAccountsRemoved } = - await this.backgroundApi.serviceAccount.getAllIndexedAccounts({ - filterRemoved: true, - }); + if (!indexedAccountsRemoved) { + // eslint-disable-next-line no-param-reassign + ({ indexedAccountsRemoved } = + await this.backgroundApi.serviceAccount.getAllIndexedAccounts({ + filterRemoved: true, + })); + } if (indexedAccountsRemoved.length) { await localDb.removeIndexedAccounts({ indexedAccounts: indexedAccountsRemoved, diff --git a/packages/kit-bg/src/services/ServiceNotification/ServiceNotification.ts b/packages/kit-bg/src/services/ServiceNotification/ServiceNotification.ts index 930ad14ad4f..dd605415ad0 100644 --- a/packages/kit-bg/src/services/ServiceNotification/ServiceNotification.ts +++ b/packages/kit-bg/src/services/ServiceNotification/ServiceNotification.ts @@ -14,6 +14,7 @@ import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; import { memoizee } from '@onekeyhq/shared/src/utils/cacheUtils'; +import perfUtils from '@onekeyhq/shared/src/utils/debug/perfUtils'; import notificationsUtils from '@onekeyhq/shared/src/utils/notificationsUtils'; import timerUtils from '@onekeyhq/shared/src/utils/timerUtils'; import type { INetworkAccount } from '@onekeyhq/shared/types/account'; @@ -55,6 +56,7 @@ import NotificationProvider from './NotificationProvider/NotificationProvider'; import type NotificationProviderBase from './NotificationProvider/NotificationProviderBase'; import type { IDBAccount, + IDBDevice, IDBIndexedAccount, IDBWallet, } from '../../dbs/local/types'; @@ -211,10 +213,12 @@ export default class ServiceNotification extends ServiceBase { this.addShowedNotificationId(msgId); - await notificationsAtom.set((v) => ({ - ...v, - lastReceivedTime: Date.now(), - })); + await notificationsAtom.set((v) => + perfUtils.buildNewValueIfChanged(v, { + ...v, + lastReceivedTime: Date.now(), + }), + ); void this.increaseBadgeCountWhenNotificationReceived(messageInfo); }; @@ -421,10 +425,12 @@ export default class ServiceNotification extends ServiceBase { setBadgeDebounced = debounce( async (params: INotificationSetBadgeParams) => { defaultLogger.notification.common.setBadge(params); - await notificationsAtom.set((v) => ({ - ...v, - badge: params.count ?? undefined, - })); + await notificationsAtom.set((v) => + perfUtils.buildNewValueIfChanged(v, { + ...v, + badge: params.count ?? undefined, + }), + ); await (await this.getNotificationProvider()).setBadge(params); }, 600, @@ -475,7 +481,20 @@ export default class ServiceNotification extends ServiceBase { } } - convertToSyncAccounts = async (dbAccounts: IDBAccount[]) => { + convertToSyncAccounts = async ({ + dbAccounts, + notificationWallets, + }: { + dbAccounts: IDBAccount[]; + notificationWallets?: IDBWallet[] | undefined; + }) => { + if (!notificationWallets) { + // eslint-disable-next-line no-param-reassign + notificationWallets = await this.getNotificationWalletsWithAccounts(); + } + + await this.fixAccountActivityNotificationSettings({ notificationWallets }); + const supportNetworksFiltered = await this.getSupportedNetworks(); defaultLogger.notification.common.consoleLog('supportNetworksFiltered', { @@ -488,56 +507,58 @@ export default class ServiceNotification extends ServiceBase { const notificationSettingsRawData = await this.backgroundApi.simpleDb.notificationSettings.getRawData(); - const { wallets: allWallets } = - await this.backgroundApi.serviceAccount.getAllWallets({ - refillWalletInfo: true, - }); - for (const account of dbAccounts) { - const networks = supportNetworksFiltered.filter( - (item) => - item.impl === account.impl || - item.networkId === account.createAtNetwork, - ); - for (const network of networks) { - let networkAccount: INetworkAccount | undefined; - try { - networkAccount = await this.backgroundApi.serviceAccount.getAccount({ + const walletId = accountUtils.getWalletIdFromAccountId({ + accountId: account.id, + }); + const isEnabled = + await this.backgroundApi.simpleDb.notificationSettings.isAccountActivityEnabled( + { + notificationSettingsRawData, + walletId, accountId: account.id, - networkId: network.networkId, - dbAccount: account, - }); - } catch (error) { - // - } - if (networkAccount?.addressDetail?.displayAddress) { - let networkId: string | undefined = network.networkId; - let networkImpl: string | undefined; - if (network.impl === IMPL_EVM) { - networkImpl = IMPL_EVM; - networkId = undefined; - } - const walletId = accountUtils.getWalletIdFromAccountId({ - accountId: networkAccount.id, - }); - const wallet = allWallets.find((item) => item.id === walletId); - const isEnabled = - await this.backgroundApi.simpleDb.notificationSettings.isAccountActivityEnabled( + indexedAccountId: account.indexedAccountId, + }, + ); + if (isEnabled) { + const networks = supportNetworksFiltered.filter( + (item) => + item.impl === account.impl || + item.networkId === account.createAtNetwork, + ); + for (const network of networks) { + let networkAccount: INetworkAccount | undefined; + try { + networkAccount = await this.backgroundApi.serviceAccount.getAccount( { - notificationSettingsRawData, - walletId, accountId: account.id, - indexedAccountId: account.indexedAccountId, + networkId: network.networkId, + dbAccount: account, }, ); - if (isEnabled && wallet) { + } catch (error) { + // + } + if (networkAccount?.addressDetail?.displayAddress) { + let networkId: string | undefined = network.networkId; + let networkImpl: string | undefined; + if (network.impl === IMPL_EVM) { + networkImpl = IMPL_EVM; + networkId = undefined; + } + + const walletName = this.getNotificationWalletName({ + notificationWallets, + walletId, + }); + const acc: INotificationPushSyncAccount = { networkId, networkImpl, accountAddress: networkAccount.addressDetail.displayAddress, accountId: networkAccount.id, - accountName: wallet.name - ? `${wallet.name} / ${networkAccount.name}` + accountName: walletName + ? `${walletName} / ${networkAccount.name}` : networkAccount.name, }; syncAccounts.push(acc); @@ -557,21 +578,33 @@ export default class ServiceNotification extends ServiceBase { async buildSyncAccounts({ accountIds }: { accountIds?: string[] }): Promise<{ syncAccounts: INotificationPushSyncAccount[]; }> { - const { accounts, accountsRemoved } = - await this.backgroundApi.serviceAccount.getAllAccounts({ - ids: accountIds, - filterRemoved: true, - }); + let dbAccounts: IDBAccount[] = []; + + const result = await this.backgroundApi.serviceAccount.getAllAccounts({ + ids: accountIds, + filterRemoved: true, + }); + dbAccounts = result.accounts; - const { syncAccounts } = await this.convertToSyncAccounts(accounts); + const notificationWallets = await this.getNotificationWalletsWithAccounts({ + allIndexedAccounts: result.allIndexedAccounts, + allWallets: result.allWallets, + allDevices: result.allDevices, + }); // accountIds is undefined means sync all accounts if (!accountIds) { void this.backgroundApi.serviceAppCleanup.cleanup({ - accountsRemoved, + accountsRemoved: result.accountsRemoved, + indexedAccountsRemoved: result.indexedAccountsRemoved, }); } + const { syncAccounts } = await this.convertToSyncAccounts({ + dbAccounts, + notificationWallets, + }); + return { syncAccounts, }; @@ -581,9 +614,9 @@ export default class ServiceNotification extends ServiceBase { _registerClientWithAppendAccountsByCache = debounce( async () => { - const { syncAccounts } = await this.convertToSyncAccounts([ - ...this.appendAccountsCache, - ]); + const { syncAccounts } = await this.convertToSyncAccounts({ + dbAccounts: [...this.appendAccountsCache], + }); this.appendAccountsCache = []; await this.registerClient({ client: this.pushClient, @@ -619,14 +652,17 @@ export default class ServiceNotification extends ServiceBase { } private async _registerClientWithOverrideAllAccountsCore() { + console.log('registerClientWithOverrideAllAccountsCore'); await InteractionManager.runAfterInteractions(async () => { await this.registerClientWithSyncAccounts({ syncMethod: ENotificationPushSyncMethod.override, }); - await notificationsAtom.set((v) => ({ - ...v, - lastRegisterTime: Date.now(), - })); + await notificationsAtom.set((v) => + perfUtils.buildNewValueIfChanged(v, { + ...v, + lastRegisterTime: Date.now(), + }), + ); }); } @@ -641,24 +677,66 @@ export default class ServiceNotification extends ServiceBase { } @backgroundMethod() - async getNotificationWalletsAndAccounts() { + async getNotificationWalletsWithAccounts({ + allIndexedAccounts, + allWallets, + allDevices, + }: { + allIndexedAccounts?: IDBIndexedAccount[] | undefined; + allWallets?: IDBWallet[] | undefined; + allDevices?: IDBDevice[] | undefined; + } = {}) { const result = await this.backgroundApi.serviceAccount.getWallets({ nestedHiddenWallets: true, ignoreEmptySingletonWalletAccounts: true, includingAccounts: true, + allIndexedAccounts, + allWallets, + allDevices, }); - return result; + return result.wallets; + } + + getNotificationWalletName({ + notificationWallets, + walletId, + }: { + notificationWallets: IDBWallet[]; + walletId: string; + }) { + for (const wallet of notificationWallets) { + if (wallet.id === walletId) { + return wallet.name; + } + if (wallet.hiddenWallets) { + for (const hiddenWallet of wallet.hiddenWallets) { + if (hiddenWallet.id === walletId) { + return hiddenWallet.name; + } + } + } + } + return ''; } @backgroundMethod() - async fixAccountActivityNotificationSettings() { + async fixAccountActivityNotificationSettings({ + notificationWallets, + }: { + notificationWallets?: IDBWallet[] | undefined; + } = {}) { + if (!notificationWallets) { + // eslint-disable-next-line no-param-reassign + notificationWallets = await this.getNotificationWalletsWithAccounts(); + } + const maxAccountCount = (await notificationsAtom.get()).maxAccountCount ?? NOTIFICATION_ACCOUNT_ACTIVITY_DEFAULT_MAX_ACCOUNT_COUNT; const settings = await this.backgroundApi.simpleDb.notificationSettings.getRawData(); - const { wallets } = await this.getNotificationWalletsAndAccounts(); + const oldAccountActivity = cloneDeep(settings?.accountActivity ?? {}); const accountActivity: IAccountActivityNotificationSettings = {}; @@ -730,7 +808,7 @@ export default class ServiceNotification extends ServiceBase { accountActivity[wallet.id].enabled = false; } }; - for (const wallet of wallets) { + for (const wallet of notificationWallets) { updateWalletAccountActivity(wallet); for (const hiddenWallet of wallet.hiddenWallets || []) { updateWalletAccountActivity(hiddenWallet); @@ -746,10 +824,12 @@ export default class ServiceNotification extends ServiceBase { await this.backgroundApi.simpleDb.notificationSettings.saveAccountActivityNotificationSettings( accountActivity, ); - await notificationsAtom.set((v) => ({ - ...v, - lastSettingsUpdateTime: Date.now(), - })); + await notificationsAtom.set((v) => + perfUtils.buildNewValueIfChanged(v, { + ...v, + lastSettingsUpdateTime: Date.now(), + }), + ); } _registerClientWithOverrideAllAccountsDebounced = debounce( @@ -841,10 +921,12 @@ export default class ServiceNotification extends ServiceBase { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return result.data; } catch (error) { - await notificationsAtom.set((v) => ({ - ...v, - lastRegisterTime: undefined, - })); + await notificationsAtom.set((v) => + perfUtils.buildNewValueIfChanged(v, { + ...v, + lastRegisterTime: undefined, + }), + ); throw error; } } @@ -934,11 +1016,12 @@ export default class ServiceNotification extends ServiceBase { const currentMaxAccountCount = (await notificationsAtom.get()) .maxAccountCount; - await notificationsAtom.set((v) => ({ - ...v, - maxAccountCount, - })); - await this.fixAccountActivityNotificationSettings(); + await notificationsAtom.set((v) => + perfUtils.buildNewValueIfChanged(v, { + ...v, + maxAccountCount, + }), + ); const supportNetworksFiltered = uniqBy(supportNetworks, (item) => { if (item.impl === IMPL_EVM) { @@ -1032,10 +1115,12 @@ export default class ServiceNotification extends ServiceBase { if (result?.data?.data?.pushEnabled) { void this.registerClientWithOverrideAllAccounts(); } - await notificationsAtom.set((v) => ({ - ...v, - lastSettingsUpdateTime: Date.now(), - })); + await notificationsAtom.set((v) => + perfUtils.buildNewValueIfChanged(v, { + ...v, + lastSettingsUpdateTime: Date.now(), + }), + ); return result?.data?.data; } diff --git a/packages/kit-bg/src/services/ServiceToken.ts b/packages/kit-bg/src/services/ServiceToken.ts index 74540a867c7..34e893c87b8 100644 --- a/packages/kit-bg/src/services/ServiceToken.ts +++ b/packages/kit-bg/src/services/ServiceToken.ts @@ -342,20 +342,6 @@ class ServiceToken extends ServiceBase { }, ); - @backgroundMethod() - public async fetchAllNetworkTokens({ - indexedAccountId, - }: { - indexedAccountId: string; - }) { - const accounts = - await this.backgroundApi.serviceAccount.getAccountsInSameIndexedAccountId( - { indexedAccountId }, - ); - - console.log('accounts:', accounts); - } - @backgroundMethod() public async fetchTokensDetails( params: IFetchTokenDetailParams, diff --git a/packages/kit-bg/src/states/jotai/atomNames.ts b/packages/kit-bg/src/states/jotai/atomNames.ts index 9e51349c0f0..e5a057e34ba 100644 --- a/packages/kit-bg/src/states/jotai/atomNames.ts +++ b/packages/kit-bg/src/states/jotai/atomNames.ts @@ -39,3 +39,11 @@ export enum EAtomNames { notificationsReadedAtom = 'notificationsReadedAtom', accountSelectorAccountsListIsLoadingAtom = 'accountSelectorAccountsListIsLoadingAtom', } +export type IAtomNameKeys = keyof typeof EAtomNames; +export const atomsConfig: Partial< + Record +> = { + [EAtomNames.notificationsAtom]: { + deepCompare: true, + }, +}; diff --git a/packages/kit-bg/src/states/jotai/jotaiStorage.ts b/packages/kit-bg/src/states/jotai/jotaiStorage.ts index 42fe4b43d35..9e409640422 100644 --- a/packages/kit-bg/src/states/jotai/jotaiStorage.ts +++ b/packages/kit-bg/src/states/jotai/jotaiStorage.ts @@ -1,16 +1,20 @@ /* eslint-disable max-classes-per-file */ /* eslint-disable camelcase */ import { atom } from 'jotai'; -import { isString, merge } from 'lodash'; +import { isEqual, isString, merge } from 'lodash'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; import appStorage, { mockStorage, } from '@onekeyhq/shared/src/storage/appStorage'; import appStorageUtils from '@onekeyhq/shared/src/storage/appStorageUtils'; +import { createPromiseTarget } from '@onekeyhq/shared/src/utils/promiseUtils'; +import { atomsConfig } from './atomNames'; import { JOTAI_RESET } from './types'; +import jotaiVerify from './utils/jotaiVerify'; +import type { IAtomNameKeys } from './atomNames'; import type { AsyncStorage, IJotaiSetStateActionWithReset, @@ -62,7 +66,7 @@ export function buildJotaiStorageKey(name: string) { } export function atomWithStorage( - storageName: string, + storageName: IAtomNameKeys, initialValue: Value, storage: AsyncStorage, unstable_options?: { unstable_getOnInit?: boolean }, @@ -73,7 +77,7 @@ export function atomWithStorage( >; export function atomWithStorage( - storageName: string, + storageName: IAtomNameKeys, initialValue: Value, storage?: SyncStorage, unstable_options?: { unstable_getOnInit?: boolean }, @@ -84,7 +88,7 @@ export function atomWithStorage( // - support storage ready check (apply to raw atom and computed atom) // - support Ext ui & bg sync export function atomWithStorage( - storageName: string, + storageName: IAtomNameKeys, initialValue: Value, ): any { const storage = onekeyJotaiStorage; @@ -102,33 +106,124 @@ export function atomWithStorage( const anAtom = atom( (get) => get(baseAtom), - ( + async ( get, set, update: IJotaiSetStateActionWithReset>, ) => { - const nextValue = - typeof update === 'function' - ? ( - update as ( - prev: Value | Promise, - ) => Value | Promise | typeof JOTAI_RESET - )(get(baseAtom)) - : update; + jotaiVerify.ensureNotPromise(update); + + let nextValue = update; + let prevValue: Value | Promise | undefined; + if (typeof update === 'function') { + prevValue = get(baseAtom); + + if (prevValue instanceof Promise) { + prevValue = await prevValue; + } + jotaiVerify.ensureNotPromise(prevValue); + + nextValue = ( + update as ( + prev: any | Promise, + ) => any | Promise | typeof JOTAI_RESET + )(prevValue); + } + + if (nextValue instanceof Promise) { + nextValue = await nextValue; + } + jotaiVerify.ensureNotPromise(nextValue); + if (nextValue === JOTAI_RESET) { set(baseAtom, initialValue); return storage.removeItem(key); } - if (nextValue instanceof Promise) { - return nextValue.then((resolvedValue) => { - const mergedValue = merge({}, initialValue, resolvedValue); - set(baseAtom, mergedValue); - return storage.setItem(key, mergedValue); - }); + + const newValue = merge({}, initialValue, nextValue); + + const shouldDeepCompare = + atomsConfig?.[storageName]?.deepCompare ?? false; + + if (shouldDeepCompare) { + prevValue = prevValue ?? get(baseAtom); + if (prevValue instanceof Promise) { + prevValue = await prevValue; + } + jotaiVerify.ensureNotPromise(prevValue); + if (isEqual(newValue, prevValue)) { + return; + } } - const mergedValue = merge({}, initialValue, nextValue); - set(baseAtom, mergedValue); - return storage.setItem(key, mergedValue); + + set(baseAtom, newValue); + return storage.setItem(key, newValue); + }, + ); + + // TODO : A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition. + // error muted by withSentryHOC + const anAtom8888 = atom( + (get) => get(baseAtom), + async ( + get, + set, + update: IJotaiSetStateActionWithReset>, + ) => { + jotaiVerify.ensureNotPromise(update); + const p = createPromiseTarget(); + + set(baseAtom, async (prevValue) => { + const value = (async () => { + if (prevValue instanceof Promise) { + // eslint-disable-next-line no-param-reassign + prevValue = await prevValue; + } + jotaiVerify.ensureNotPromise(prevValue); + + let nextValue = + typeof update === 'function' + ? ( + update as ( + prev: Value | Promise, + ) => Value | Promise | typeof JOTAI_RESET + )(prevValue) + : update; + + if (nextValue instanceof Promise) { + // eslint-disable-next-line no-param-reassign + nextValue = await nextValue; + } + jotaiVerify.ensureNotPromise(nextValue); + + if (nextValue === JOTAI_RESET) { + await storage.removeItem(key); + return initialValue; + } + + const newValue = merge({}, initialValue, nextValue) as Value; + + const shouldDeepCompare = + atomsConfig?.[storageName as any as IAtomNameKeys]?.deepCompare ?? + false; + + if (shouldDeepCompare) { + if (isEqual(newValue, prevValue)) { + await storage.setItem(key, prevValue); + return prevValue; + } + } + + await storage.setItem(key, newValue); + return newValue; + })(); + + p.resolveTarget(true, 5000); + return value; + }); + + const v = await p.ready; + return v; }, ); diff --git a/packages/kit-bg/src/states/jotai/utils/index.ts b/packages/kit-bg/src/states/jotai/utils/index.ts index 69d76d9e5b1..1d04528014b 100644 --- a/packages/kit-bg/src/states/jotai/utils/index.ts +++ b/packages/kit-bg/src/states/jotai/utils/index.ts @@ -12,7 +12,7 @@ import { import { JotaiCrossAtom } from './JotaiCrossAtom'; import { wrapAtomPro } from './wrapAtomPro'; -import type { EAtomNames } from '../atomNames'; +import type { EAtomNames, IAtomNameKeys } from '../atomNames'; import type { IJotaiRead, IJotaiSetAtom, @@ -47,7 +47,7 @@ export function crossAtomBuilder({ name: string; initialValue: Value; // - storageName?: string; + storageName?: IAtomNameKeys; read?: undefined; write?: undefined; }): PrimitiveAtom & IJotaiWithInitialValue; @@ -62,7 +62,7 @@ export function crossAtomBuilder({ }: { name: string; initialValue: Value; - storageName: string; + storageName: IAtomNameKeys; // read?: undefined; write?: undefined; @@ -80,7 +80,7 @@ export function crossAtomBuilder({ read: IJotaiRead; // initialValue?: Value; - storageName?: string; + storageName?: IAtomNameKeys; write?: undefined; }): Atom; @@ -97,7 +97,7 @@ export function crossAtomBuilder({ // initialValue?: Value; read?: undefined; - storageName?: string; + storageName?: IAtomNameKeys; }): WritableAtom & IJotaiWithInitialValue; // Read & Write @@ -113,7 +113,7 @@ export function crossAtomBuilder({ write: IJotaiWrite; // initialValue?: Value; - storageName?: string; + storageName?: IAtomNameKeys; }): WritableAtom; export function crossAtomBuilder({ @@ -125,7 +125,7 @@ export function crossAtomBuilder({ }: { name: string; initialValue?: Value; - storageName?: string; + storageName?: IAtomNameKeys; read?: IJotaiRead> | IJotaiRead; write?: IJotaiWrite; }) { diff --git a/packages/kit-bg/src/states/jotai/utils/jotaiVerify.ts b/packages/kit-bg/src/states/jotai/utils/jotaiVerify.ts new file mode 100644 index 00000000000..3764a204fc8 --- /dev/null +++ b/packages/kit-bg/src/states/jotai/utils/jotaiVerify.ts @@ -0,0 +1,18 @@ +function ensureNotPromise(value: T) { + const valueLikePromise = value as + | { orig: any; value: any; status: any } + | undefined; + if ( + valueLikePromise && + valueLikePromise.orig && + valueLikePromise.value && + valueLikePromise.status + ) { + debugger; + throw new Error('jotai value should not be a promise'); + } +} + +export default { + ensureNotPromise, +}; diff --git a/packages/kit-bg/src/states/jotai/utils/wrapAtomPro.ts b/packages/kit-bg/src/states/jotai/utils/wrapAtomPro.ts index 378490a2450..2f26d3a455e 100644 --- a/packages/kit-bg/src/states/jotai/utils/wrapAtomPro.ts +++ b/packages/kit-bg/src/states/jotai/utils/wrapAtomPro.ts @@ -6,6 +6,8 @@ import platformEnv from '@onekeyhq/shared/src/platformEnv'; import { JOTAI_RESET } from '../types'; +import jotaiVerify from './jotaiVerify'; + import type { EAtomNames } from '../atomNames'; import type { IJotaiAtomSetWithoutProxy, @@ -48,14 +50,28 @@ export function wrapAtomPro( const proAtom = atom( (get) => get(baseAtom), async (get, set, update) => { - let nextValue = - typeof update === 'function' - ? ( - update as ( - prev: any | Promise, - ) => any | Promise | typeof JOTAI_RESET - )(get(baseAtom)) - : update; + jotaiVerify.ensureNotPromise(update); + + let nextValue = update; + if (typeof update === 'function') { + let prevValue = get(baseAtom); + + if (prevValue instanceof Promise) { + prevValue = await prevValue; + } + jotaiVerify.ensureNotPromise(prevValue); + + nextValue = ( + update as ( + prev: any | Promise, + ) => any | Promise | typeof JOTAI_RESET + )(prevValue); + } + + if (nextValue instanceof Promise) { + nextValue = await nextValue; + } + jotaiVerify.ensureNotPromise(nextValue); let proxyToBg = false; if (platformEnv.isExtensionUi && name) { diff --git a/packages/kit/src/components/GlobalJotaiReady/GlobalJotaiReady.tsx b/packages/kit/src/components/GlobalJotaiReady/GlobalJotaiReady.tsx index 0b5abace1c6..e2c89b9a626 100644 --- a/packages/kit/src/components/GlobalJotaiReady/GlobalJotaiReady.tsx +++ b/packages/kit/src/components/GlobalJotaiReady/GlobalJotaiReady.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { startTransition, useEffect, useState } from 'react'; import { View } from 'react-native'; @@ -8,7 +8,9 @@ export function GlobalJotaiReady({ children }: { children: any }) { const [isReady, setIsReady] = useState(false); useEffect(() => { void globalJotaiStorageReadyHandler.ready.then((ready) => { - setIsReady(ready); + startTransition(() => { + setIsReady(ready); + }); }); }, []); diff --git a/packages/kit/src/components/RenameDialog/index.tsx b/packages/kit/src/components/RenameDialog/index.tsx index 34dd2c84b9e..f612984bb1b 100644 --- a/packages/kit/src/components/RenameDialog/index.tsx +++ b/packages/kit/src/components/RenameDialog/index.tsx @@ -37,7 +37,7 @@ function V4AccountNameSelector({ const intl = useIntl(); const [val] = useState(''); const { result: items = [] } = usePromiseResult(async () => { - const accounts = + const { accounts } = await backgroundApiProxy.serviceAccount.getAccountsInSameIndexedAccountId( { indexedAccountId: indexedAccount.id, diff --git a/packages/kit/src/components/TokenListView/index.tsx b/packages/kit/src/components/TokenListView/index.tsx index 5a66c6dbcbb..c7625246e69 100644 --- a/packages/kit/src/components/TokenListView/index.tsx +++ b/packages/kit/src/components/TokenListView/index.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react'; import { memo, useEffect, useMemo, useState } from 'react'; import { @@ -64,6 +65,7 @@ type IProps = { tokenSelectorSearchTokenList?: { tokens: IAccountToken[]; }; + emptyAccountView?: ReactNode; }; function TokenListViewCmp(props: IProps) { @@ -90,6 +92,7 @@ function TokenListViewCmp(props: IProps) { tokenSelectorSearchKey = '', tokenSelectorSearchTokenState = { isSearching: false }, tokenSelectorSearchTokenList = { tokens: [] }, + emptyAccountView, } = props; const [tokenList] = useTokenListAtom(); @@ -219,6 +222,10 @@ function TokenListViewCmp(props: IProps) { ); } + if (emptyAccountView) { + return emptyAccountView; + } + return ( IAccountSelectorActiveAccountInfo = () => ({ diff --git a/packages/kit/src/views/Home/pages/HomePageView.tsx b/packages/kit/src/views/Home/pages/HomePageView.tsx index 0a661fd556e..ea0d196afc7 100644 --- a/packages/kit/src/views/Home/pages/HomePageView.tsx +++ b/packages/kit/src/views/Home/pages/HomePageView.tsx @@ -12,6 +12,7 @@ import { import { ETranslations } from '@onekeyhq/shared/src/locale'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; +import networkUtils from '@onekeyhq/shared/src/utils/networkUtils'; import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; import backgroundApiProxy from '../../../background/instance/backgroundApiProxy'; @@ -134,6 +135,24 @@ export function HomePageView({ appEventBus.emit(EAppEventBusNames.AccountDataUpdate, undefined); }, []); + const emptyAccountView = useMemo( + () => ( + + ), + [accountName, deriveInfo?.label, deriveInfo?.labelKey, intl, network?.name], + ); + const renderTabs = useCallback( () => ( - + {emptyAccountView} ); @@ -215,12 +223,8 @@ export function HomePageView({ isRequiredValidation, renderTabs, watchingAccountEnabled, - accountName, - network?.name, + emptyAccountView, network?.id, - deriveInfo?.labelKey, - deriveInfo?.label, - intl, ]); const renderHomePage = useCallback(() => { diff --git a/packages/kit/src/views/Home/pages/TokenListContainer.tsx b/packages/kit/src/views/Home/pages/TokenListContainer.tsx index 03d7b1e7a9c..0297f11cce3 100644 --- a/packages/kit/src/views/Home/pages/TokenListContainer.tsx +++ b/packages/kit/src/views/Home/pages/TokenListContainer.tsx @@ -3,10 +3,12 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { CanceledError } from 'axios'; import BigNumber from 'bignumber.js'; import { isEmpty, isNil, uniqBy } from 'lodash'; +import { useIntl } from 'react-intl'; import { useThrottledCallback } from 'use-debounce'; import type { ITabPageProps } from '@onekeyhq/components'; import { + Stack, useMedia, useOnRouterChange, useTabIsRefreshingFocused, @@ -55,6 +57,7 @@ import type { ITokenFiat, } from '@onekeyhq/shared/types/token'; +import { EmptyAccount } from '../../../components/Empty'; import { TokenListView } from '../../../components/TokenListView'; import { perfTokenListView } from '../../../components/TokenListView/perfTokenListView'; import { useAccountData } from '../../../hooks/useAccountData'; @@ -63,7 +66,10 @@ import useAppNavigation from '../../../hooks/useAppNavigation'; import { useManageToken } from '../../../hooks/useManageToken'; import { usePromiseResult } from '../../../hooks/usePromiseResult'; import { useReceiveToken } from '../../../hooks/useReceiveToken'; -import { useAccountOverviewActions } from '../../../states/jotai/contexts/accountOverview'; +import { + useAccountOverviewActions, + useAllNetworksStateStateAtom, +} from '../../../states/jotai/contexts/accountOverview'; import { useActiveAccount } from '../../../states/jotai/contexts/accountSelector'; import { useTokenListActions } from '../../../states/jotai/contexts/tokenList'; import { HomeTokenListProviderMirrorWrapper } from '../components/HomeTokenListProvider'; @@ -77,6 +83,7 @@ function TokenListContainer(props: ITabPageProps) { const { activeAccount: { account, + accountName, network, wallet, indexedAccount, @@ -89,6 +96,7 @@ function TokenListContainer(props: ITabPageProps) { const [allNetworkAccounts, setAllNetworkAccounts] = useState< IAllNetworkAccountInfo[] | undefined >(undefined); + const intl = useIntl(); const tokenListRef = useRef<{ keys: string; @@ -1238,6 +1246,18 @@ function TokenListContainer(props: ITabPageProps) { void runAllNetworksRequests({ alwaysSetState: true }); }, [runAllNetworksRequests]); + useEffect(() => { + const fn = () => { + if (network?.isAllNetworks) { + void runAllNetworksRequests({ alwaysSetState: true }); + } + }; + appEventBus.on(EAppEventBusNames.AddDBAccountsToWallet, fn); + return () => { + appEventBus.off(EAppEventBusNames.AddDBAccountsToWallet, fn); + }; + }, [network?.isAllNetworks, runAllNetworksRequests]); + const handleRefreshAllNetworkDataByAccounts = useCallback( async (accounts: { accountId: string; networkId: string }[]) => { for (const { accountId, networkId } of accounts) { @@ -1347,6 +1367,14 @@ function TokenListContainer(props: ITabPageProps) { } }, [isEmptyAccount, updateAccountOverviewState, updateTokenListState]); + const [allNetworksState] = useAllNetworksStateStateAtom(); + const isAllNetworkEmptyAccount = useMemo(() => { + if (network?.isAllNetworks) { + return allNetworksState.visibleCount === 0; + } + return false; + }, [allNetworksState.visibleCount, network?.isAllNetworks]); + return ( + + + ) : null + } /> ); } diff --git a/packages/kit/src/views/Setting/pages/Notifications/ManageAccountActivity.tsx b/packages/kit/src/views/Setting/pages/Notifications/ManageAccountActivity.tsx index b6861e8c55d..ac56bd0a51d 100644 --- a/packages/kit/src/views/Setting/pages/Notifications/ManageAccountActivity.tsx +++ b/packages/kit/src/views/Setting/pages/Notifications/ManageAccountActivity.tsx @@ -747,9 +747,9 @@ function ManageAccountActivityContent({ wallets }: { wallets: IDBWallet[] }) { function ManageAccountActivity() { const intl = useIntl(); - const { result: { wallets } = { wallets: [] }, isLoading } = usePromiseResult( + const { result: wallets = [], isLoading } = usePromiseResult( () => - backgroundApiProxy.serviceNotification.getNotificationWalletsAndAccounts(), + backgroundApiProxy.serviceNotification.getNotificationWalletsWithAccounts(), [], { watchLoading: true, diff --git a/packages/shared/src/errors/utils/errorUtils.ts b/packages/shared/src/errors/utils/errorUtils.ts index 5775b39ccdb..b80beac6d19 100644 --- a/packages/shared/src/errors/utils/errorUtils.ts +++ b/packages/shared/src/errors/utils/errorUtils.ts @@ -1,4 +1,4 @@ -import { isObject, isPlainObject, isString, isUndefined, omitBy } from 'lodash'; +import { isObject, isString, isUndefined, omitBy } from 'lodash'; import type { ETranslations, @@ -181,6 +181,15 @@ function isErrorByClassName({ return Boolean(errorClassName && classNames.includes(errorClassName)); } +function getCurrentCallStack() { + try { + throw new Error(); + } catch (e) { + autoPrintErrorIgnore(e); + return (e as IOneKeyError)?.stack; + } +} + export default { autoPrintErrorIgnore, normalizeErrorProps, @@ -190,4 +199,5 @@ export default { errorsIntlFormatter, getDeviceErrorPayloadMessage, isErrorByClassName, + getCurrentCallStack, }; diff --git a/packages/shared/src/utils/cacheUtils.ts b/packages/shared/src/utils/cacheUtils.ts index 8359630cc99..88fd43813bb 100644 --- a/packages/shared/src/utils/cacheUtils.ts +++ b/packages/shared/src/utils/cacheUtils.ts @@ -1,4 +1,5 @@ import stringify from 'fast-json-stable-stringify'; +import { LRUCache } from 'lru-cache'; import cache from 'memoizee'; export type IMemoizeeOptions = cache.Options; @@ -25,4 +26,5 @@ export function memoFn(fn: () => T) { export default { memoizee, memoFn, + LRUCache, }; diff --git a/packages/shared/src/utils/debug/dbPerfMonitor.ts b/packages/shared/src/utils/debug/dbPerfMonitor.ts index e207b7f040c..11294090e57 100644 --- a/packages/shared/src/utils/debug/dbPerfMonitor.ts +++ b/packages/shared/src/utils/debug/dbPerfMonitor.ts @@ -1,6 +1,7 @@ import { cloneDeep, debounce, isEqual, merge } from 'lodash'; import appGlobals from '../../appGlobals'; +import errorUtils from '../../errors/utils/errorUtils'; import platformEnv from '../../platformEnv'; import { syncStorage } from '../../storage/instance/syncStorageInstance'; import { EAppSyncStorageKeys } from '../../storage/syncStorageKeys'; @@ -34,15 +35,18 @@ const shouldDbTxCreatedDebuggerRule: Record = { }; const shouldLocalDbDebuggerRule: Record = { - 'localDb.txGetRecordById__IndexedAccount': 999, - 'localDb.txGetAllRecords__IndexedAccount': 999, + 'localDb.txGetAllRecords__Device': 999, + 'localDb.txGetAllRecords__Wallet': 999, 'localDb.txGetAllRecords__Account': 999, + 'localDb.txGetAllRecords__IndexedAccount': 999, 'localDb.txGetRecordById__Wallet': 999, 'localDb.txGetRecordById__Account': 999, + 'localDb.txGetRecordById__IndexedAccount': 999, 'appStorage.getItem__simple_db_v5:localHistory': 999, 'appStorage.getItem__simple_db_v5:LocalNFTs': 999, 'appStorage.getItem__simple_db_v5:localTokens': 999, 'appStorage.getItem__simple_db_v5:dappConnection': 999, + 'appStorage.setItem__g_states_v5:notificationsAtom': 999, }; const IS_ENABLED = @@ -303,6 +307,14 @@ function logLocalDbCall(method: string, table: string, params: any[]) { if (IS_ENABLED) { // eslint-disable-next-line no-param-reassign method = `localDb.${method}`; + // eslint-disable-next-line no-param-reassign + params = [...params, errorUtils.getCurrentCallStack()]; + + if (method === 'localDb.txGetAllRecords') { + indexedDBResult[method] = (indexedDBResult[method] || 0) + 1; + indexedDBResultAll[method] = (indexedDBResultAll[method] || 0) + 1; + } + localDbCallDetails[method] = localDbCallDetails[method] || {}; localDbCallDetails[method][table] = localDbCallDetails[method][table] || { calls: [], @@ -416,10 +428,12 @@ function logIndexedDBCreateTx() { const key = `${this.name}_${mode || 'undefined'}`; indexedDBResult[key] = (indexedDBResult[key] || 0) + 1; indexedDBResultAll[key] = (indexedDBResultAll[key] || 0) + 1; + if ( settings?.toastWarningSize && indexedDBResult[key] >= settings?.toastWarningSize ) { + // localDb.txGetAllRecords__ toastWarningAndReset(key); } logResultDebounced({ diff --git a/packages/shared/src/utils/debug/perfUtils.ts b/packages/shared/src/utils/debug/perfUtils.ts index 5880f11f99d..152eb7727b5 100644 --- a/packages/shared/src/utils/debug/perfUtils.ts +++ b/packages/shared/src/utils/debug/perfUtils.ts @@ -1,4 +1,4 @@ -import { isNil } from 'lodash'; +import { isEqual, isNil } from 'lodash'; import errorUtils from '../../errors/utils/errorUtils'; import appStorage from '../../storage/appStorage'; @@ -206,8 +206,16 @@ function createPerf(options: IPerformanceTimerOptions) { return perf; } +function buildNewValueIfChanged(oldValue: T, newValue: T) { + if (isEqual(newValue, oldValue)) { + return oldValue; + } + return newValue; +} + export default { createPerf, updatePerformanceTimerLogConfig, getPerformanceTimerLogConfig, + buildNewValueIfChanged, }; diff --git a/packages/shared/src/utils/promiseUtils.ts b/packages/shared/src/utils/promiseUtils.ts index 3f4269a9126..de4f2c49cf5 100644 --- a/packages/shared/src/utils/promiseUtils.ts +++ b/packages/shared/src/utils/promiseUtils.ts @@ -147,3 +147,21 @@ export async function promiseAllSettledEnhanced( result.status === 'fulfilled' ? result.value : null, ); } + +class PromiseTarget { + ready = new Promise((resolve) => { + this.resolve = resolve; + }); + + resolve: ((value: T) => void) | undefined; + + resolveTarget(value: T, delay = 0) { + setTimeout(() => { + this.resolve?.(value); + }, delay); + } +} +export function createPromiseTarget() { + const p = new PromiseTarget(); + return p; +} From 3cd61ec829b438bb697701ab216344cb96500df4 Mon Sep 17 00:00:00 2001 From: morizon Date: Wed, 25 Dec 2024 10:12:32 +0800 Subject: [PATCH 2/2] fix: lint --- packages/kit-bg/src/dbs/local/LocalDbBase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit-bg/src/dbs/local/LocalDbBase.ts b/packages/kit-bg/src/dbs/local/LocalDbBase.ts index 37030320fd3..2a67e343be8 100644 --- a/packages/kit-bg/src/dbs/local/LocalDbBase.ts +++ b/packages/kit-bg/src/dbs/local/LocalDbBase.ts @@ -79,7 +79,6 @@ import { EDBAccountType } from './consts'; import { LocalDbBaseContainer } from './LocalDbBaseContainer'; import { ELocalDBStoreNames } from './localDBStoreNames'; -import type { IDeviceType } from '@onekeyfe/hd-core'; import type { IDBAccount, IDBApiGetContextOptions, @@ -109,6 +108,7 @@ import type { ILocalDBTransaction, ILocalDBTxGetRecordByIdResult, } from './types'; +import type { IDeviceType } from '@onekeyfe/hd-core'; const getOrderByWalletType = (walletType: IDBWalletType): number => { switch (walletType) {