perf(nwc): coalesce concurrent KeyChain reads in getCachedWalletKeys [Stage 4a]

Several NWC pushes can wake the app simultaneously (e.g. a wallet
connecting fires get_balance + list_transactions together, or rapid
zaps). Each caller previously raced past the walletKeys cache check
before any populated it, so each triggered its own ~2s cold KeyChain
read.

Park the in-flight read in a volatile promise; concurrent callers await
the same fetch instead of hitting secure storage independently. The
shared promise resolves to validated keys so the originator and all
awaiters get identical success/error. Volatile (never persisted) so a
fresh cold start still does exactly one fresh read.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
minibits-cash
2026-06-06 17:09:29 +02:00
co-authored by Claude Opus 4.8
parent 0d43095108
commit 24a9a85b9f
+40 -15
View File
@@ -169,29 +169,54 @@ export const WalletStoreModel = types
resetExchangeRate () {
self.exchangeRate = undefined
}
}))
}))
.volatile(() => ({
// In-flight KeyChain read shared by concurrent getCachedWalletKeys callers.
// Reading secure storage is slow (~2s cold on Android), and several NWC
// pushes can wake the app at once — without this, each push would trigger
// its own KeyChain read. Volatile (never persisted), so it resets on a
// fresh cold start, which is exactly when we want a single fresh read.
walletKeysInFlight: null as Promise<WalletKeys> | null,
}))
.actions(self => ({
getCachedWalletKeys: flow(function* getWalletKeys() {
if (self.walletKeys) {
getCachedWalletKeys: flow(function* getWalletKeys() {
if (self.walletKeys) {
log.trace('[getCachedWalletKeys]', 'Returning cached walletKeys')
return self.walletKeys
}
const keys: WalletKeys | undefined = yield KeyChain.getWalletKeys()
if (!keys) {
return self.walletKeys
}
// Coalesce concurrent cold reads onto a single KeyChain fetch.
if (self.walletKeysInFlight) {
log.trace('[getCachedWalletKeys]', 'Awaiting in-flight KeyChain read')
return yield self.walletKeysInFlight
}
// The shared promise resolves to validated keys so that both this
// originator and any concurrent awaiters get identical success/error.
const fetch = (async (): Promise<WalletKeys> => {
const keys: WalletKeys | undefined = await KeyChain.getWalletKeys()
if (!keys) {
throw new AppError(
Err.NOTFOUND_ERROR,
Err.NOTFOUND_ERROR,
'Device secure storage could not return wallet keys, please reinstall and use your seed phrase to recover wallet.'
)
}
return keys
})()
self.walletKeysInFlight = fetch
try {
const keys: WalletKeys = yield fetch
self.walletKeys = keys
return keys
} finally {
self.walletKeysInFlight = null
}
self.walletKeys = keys
return keys
}),
cleanCachedWalletKeys() {
cleanCachedWalletKeys() {
self.walletKeys = undefined
},
},
}))
.actions(self => ({
getCachedSeed: flow(function* getCachedSeed() {