Merge pull request 'perf(ui): app-launch-speed doctrine for data panels — parallel RPCs + instant last-known paint' (#120) from perf/data-panels into main
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m51s
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m51s
This commit is contained in:
commit
91c51c4d97
@ -522,8 +522,10 @@ const cloudStorageDisplay = computed(() => cloudStorageUsed.value !== null ? for
|
|||||||
const cloudFolderDisplay = computed(() => cloudFolderCount.value !== null ? String(cloudFolderCount.value) : '...')
|
const cloudFolderDisplay = computed(() => cloudFolderCount.value !== null ? String(cloudFolderCount.value) : '...')
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try { const usage = await fileBrowserClient.getUsage(); cloudStorageUsed.value = usage.totalSize; cloudFolderCount.value = usage.folderCount } catch { /* not running */ }
|
// Paint last-known wallet figures BEFORE any network round-trip.
|
||||||
|
hydrateWalletSnapshot()
|
||||||
loadSystemStats(); systemStatsInterval = setInterval(loadSystemStats, 10000); checkUpdateStatus(); loadWeb5Status()
|
loadSystemStats(); systemStatsInterval = setInterval(loadSystemStats, 10000); checkUpdateStatus(); loadWeb5Status()
|
||||||
|
try { const usage = await fileBrowserClient.getUsage(); cloudStorageUsed.value = usage.totalSize; cloudFolderCount.value = usage.folderCount } catch { /* not running */ }
|
||||||
// Poll wallet balances/transactions like Web5.vue does — without this a
|
// Poll wallet balances/transactions like Web5.vue does — without this a
|
||||||
// pending on-chain receive (or a fresh instant payment) only shows up
|
// pending on-chain receive (or a fresh instant payment) only shows up
|
||||||
// after a manual wallet action or a remount.
|
// after a manual wallet action or a remount.
|
||||||
@ -583,25 +585,78 @@ function ecashToWalletTransaction(tx: EcashTransaction): WalletTransaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Last-known wallet snapshot, hydrated before ANY network round-trip so the
|
||||||
|
// card paints real figures instantly (app-launch-speed doctrine: over the
|
||||||
|
// mesh every serialized RPC costs a full RTT — never make the user watch it).
|
||||||
|
const WALLET_SNAPSHOT_KEY = 'archy-wallet-snapshot-v1'
|
||||||
|
|
||||||
|
function hydrateWalletSnapshot() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(WALLET_SNAPSHOT_KEY)
|
||||||
|
if (!raw) return
|
||||||
|
const s = JSON.parse(raw)
|
||||||
|
walletOnchain.value = s.onchain ?? 0
|
||||||
|
walletLightning.value = s.lightning ?? 0
|
||||||
|
walletEcash.value = s.ecash ?? 0
|
||||||
|
walletFedimint.value = s.fedimint ?? 0
|
||||||
|
walletArk.value = s.ark ?? 0
|
||||||
|
walletConnected.value = s.connected === true
|
||||||
|
if (Array.isArray(s.transactions)) walletTransactions.value = s.transactions
|
||||||
|
} catch { /* corrupt/absent snapshot — fresh load fills in */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistWalletSnapshot() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(WALLET_SNAPSHOT_KEY, JSON.stringify({
|
||||||
|
onchain: walletOnchain.value,
|
||||||
|
lightning: walletLightning.value,
|
||||||
|
ecash: walletEcash.value,
|
||||||
|
fedimint: walletFedimint.value,
|
||||||
|
ark: walletArk.value,
|
||||||
|
connected: walletConnected.value,
|
||||||
|
// Enough for the Transactions modal's first paint; refresh replaces it.
|
||||||
|
transactions: walletTransactions.value.slice(0, 50),
|
||||||
|
}))
|
||||||
|
} catch { /* storage full — snapshot is best-effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
async function loadWeb5Status() {
|
async function loadWeb5Status() {
|
||||||
// A transient RPC timeout must NOT flash the balance to 0 ("wallet says 0 when
|
// A transient RPC timeout must NOT flash the balance to 0 ("wallet says 0 when
|
||||||
// there is a balance"). On failure keep the last-known value — the refs start
|
// there is a balance"). On failure keep the last-known value — the refs start
|
||||||
// at 0, so only the very first load before any success shows 0.
|
// from the persisted snapshot, so 0 only ever shows on a genuinely fresh node.
|
||||||
try { const res = await rpcClient.call<{ balance_sats: number; channel_balance_sats: number }>({ method: 'lnd.getinfo', timeout: 5000 }); walletOnchain.value = res.balance_sats || 0; walletLightning.value = res.channel_balance_sats || 0; walletConnected.value = true } catch { walletConnected.value = false }
|
//
|
||||||
try { const res = await rpcClient.call<{ balance_sats: number }>({ method: 'wallet.ecash-balance', timeout: 5000 }); walletEcash.value = res.balance_sats ?? 0 } catch { /* keep last-known balance */ }
|
// All seven calls are independent — fire them TOGETHER. Serialized, this
|
||||||
try { const res = await rpcClient.call<{ balance_sats: number }>({ method: 'wallet.fedimint-balance', timeout: 5000 }); walletFedimint.value = res.balance_sats ?? 0 } catch { /* keep last-known balance */ }
|
// block cost 7 × (mesh RTT + backend time); parallel it costs one slowest
|
||||||
try { const res = await rpcClient.call<{ spendable_sats: number }>({ method: 'wallet.ark-balance', timeout: 5000 }); walletArk.value = res.spendable_sats ?? 0 } catch { /* keep last-known balance */ }
|
// call, which is what makes the card feel like an app launch.
|
||||||
|
const balances = Promise.allSettled([
|
||||||
|
rpcClient.call<{ balance_sats: number; channel_balance_sats: number }>({ method: 'lnd.getinfo', timeout: 5000 })
|
||||||
|
.then(res => { walletOnchain.value = res.balance_sats || 0; walletLightning.value = res.channel_balance_sats || 0; walletConnected.value = true })
|
||||||
|
.catch(() => { walletConnected.value = false }),
|
||||||
|
rpcClient.call<{ balance_sats: number }>({ method: 'wallet.ecash-balance', timeout: 5000 })
|
||||||
|
.then(res => { walletEcash.value = res.balance_sats ?? 0 }).catch(() => { /* keep last-known */ }),
|
||||||
|
rpcClient.call<{ balance_sats: number }>({ method: 'wallet.fedimint-balance', timeout: 5000 })
|
||||||
|
.then(res => { walletFedimint.value = res.balance_sats ?? 0 }).catch(() => { /* keep last-known */ }),
|
||||||
|
rpcClient.call<{ spendable_sats: number }>({ method: 'wallet.ark-balance', timeout: 5000 })
|
||||||
|
.then(res => { walletArk.value = res.spendable_sats ?? 0 }).catch(() => { /* keep last-known */ }),
|
||||||
|
])
|
||||||
// Merge LND transactions with ecash/Fedimint history (wallet.ecash-history
|
// Merge LND transactions with ecash/Fedimint history (wallet.ecash-history
|
||||||
// already unifies both) — previously only LND transactions were fetched
|
// already unifies both) so Cashu/Fedimint receives appear in the modal.
|
||||||
// here, so any Cashu or Fedimint receive (e.g. a TollGate payment) never
|
const histories = Promise.allSettled([
|
||||||
// appeared in the Transactions modal even though the balance included it.
|
rpcClient.call<{ transactions: WalletTransaction[]; incoming_pending_count: number }>({ method: 'lnd.gettransactions', timeout: 5000 })
|
||||||
let lndTxs: WalletTransaction[] = []
|
.then(res => (res.transactions || []).map(tx => ({ ...tx, kind: 'onchain' as const }))).catch(() => [] as WalletTransaction[]),
|
||||||
try { const res = await rpcClient.call<{ transactions: WalletTransaction[]; incoming_pending_count: number }>({ method: 'lnd.gettransactions', timeout: 5000 }); lndTxs = (res.transactions || []).map(tx => ({ ...tx, kind: 'onchain' as const })) } catch { /* keep last-known transactions */ }
|
rpcClient.call<{ transactions: WalletTransaction[] }>({ method: 'lnd.lightning-history', timeout: 5000 })
|
||||||
let lightningTxs: WalletTransaction[] = []
|
.then(res => res.transactions || []).catch(() => [] as WalletTransaction[]),
|
||||||
try { const res = await rpcClient.call<{ transactions: WalletTransaction[] }>({ method: 'lnd.lightning-history', timeout: 5000 }); lightningTxs = res.transactions || [] } catch { /* keep last-known transactions */ }
|
rpcClient.call<{ transactions: EcashTransaction[] }>({ method: 'wallet.ecash-history', timeout: 5000 })
|
||||||
let ecashTxs: WalletTransaction[] = []
|
.then(res => (res.transactions || []).map(ecashToWalletTransaction)).catch(() => [] as WalletTransaction[]),
|
||||||
try { const res = await rpcClient.call<{ transactions: EcashTransaction[] }>({ method: 'wallet.ecash-history', timeout: 5000 }); ecashTxs = (res.transactions || []).map(ecashToWalletTransaction) } catch { /* keep last-known transactions */ }
|
]).then((results) => {
|
||||||
walletTransactions.value = [...lndTxs, ...lightningTxs, ...ecashTxs].sort((a, b) => b.time_stamp - a.time_stamp)
|
const merged = results.flatMap(r => (r.status === 'fulfilled' ? r.value : []))
|
||||||
|
// Keep last-known list when every history call failed this round.
|
||||||
|
if (merged.length > 0 || results.some(r => r.status === 'fulfilled')) {
|
||||||
|
walletTransactions.value = merged.sort((a, b) => b.time_stamp - a.time_stamp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await Promise.allSettled([balances, histories])
|
||||||
|
persistWalletSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
// System stats
|
// System stats
|
||||||
|
|||||||
@ -794,15 +794,19 @@ function goBack() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (props.peerId) {
|
if (props.peerId) {
|
||||||
// Find the peer by onion address
|
// The peer-name lookup is cosmetic — the catalog only needs the onion we
|
||||||
try {
|
// already have. Serialized, it added a full mesh round-trip before the
|
||||||
const result = await rpcClient.federationListNodes()
|
// files even started loading.
|
||||||
const peers = result?.nodes ?? []
|
await Promise.all([
|
||||||
currentPeer.value = peers.find((p: PeerNode) => p.onion === props.peerId) || null
|
rpcClient.federationListNodes()
|
||||||
} catch {
|
.then((result) => {
|
||||||
// Continue with just the onion address
|
const peers = result?.nodes ?? []
|
||||||
}
|
currentPeer.value = peers.find((p: PeerNode) => p.onion === props.peerId) || null
|
||||||
await Promise.all([loadCatalog(), loadOwned()])
|
})
|
||||||
|
.catch(() => { /* continue with just the onion address */ }),
|
||||||
|
loadCatalog(),
|
||||||
|
loadOwned(),
|
||||||
|
])
|
||||||
} else {
|
} else {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -350,13 +350,17 @@ async function loadPeers() {
|
|||||||
const hadPeers = peers.value.length > 0 || observers.value.length > 0
|
const hadPeers = peers.value.length > 0 || observers.value.length > 0
|
||||||
loadingPeers.value = true
|
loadingPeers.value = true
|
||||||
try {
|
try {
|
||||||
const res = await rpcClient.listPeers()
|
// Independent RPCs — fetched together (serialized they stacked two full
|
||||||
|
// mesh round-trips before anything rendered).
|
||||||
|
const [res, fedSettled] = await Promise.all([
|
||||||
|
rpcClient.listPeers(),
|
||||||
|
rpcClient.federationListNodes().catch(() => null),
|
||||||
|
])
|
||||||
const peerList = res.peers || []
|
const peerList = res.peers || []
|
||||||
const observerList: Peer[] = []
|
const observerList: Peer[] = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fedRes = await rpcClient.federationListNodes()
|
const fedNodes = fedSettled?.nodes || []
|
||||||
const fedNodes = fedRes.nodes || []
|
|
||||||
for (const n of fedNodes) {
|
for (const n of fedNodes) {
|
||||||
if (!n.onion || n.trust_level === 'untrusted') {
|
if (!n.onion || n.trust_level === 'untrusted') {
|
||||||
continue
|
continue
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user