-
Notifications
You must be signed in to change notification settings - Fork 3
fix: tx prices linking, dashboard load, initial sync errors #1132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
3ebc4a0
03e107c
c275f8e
054ccda
a7c0814
18879a6
25b5016
8ce9149
6fdb0f6
b72ce29
72333f3
4a93f89
5bdf53f
59373c9
a4b6f5c
5392e6c
68ad97b
3b198d0
678f9f4
f83885d
ff1a38f
e5eafb3
3d9c4bd
c77cd75
2e78112
89d64e0
1231945
c616877
4f38145
f076ec9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,11 +10,11 @@ import { | |
| import { fetchAllUserAddresses, AddressPaymentInfo } from 'services/addressService' | ||
| import { fetchPaybuttonArrayByUserId } from 'services/paybuttonService' | ||
|
|
||
| import { RESPONSE_MESSAGES, PAYMENT_WEEK_KEY_FORMAT, KeyValueT } from 'constants/index' | ||
| import { RESPONSE_MESSAGES, PAYMENT_WEEK_KEY_FORMAT, KeyValueT, N_OF_QUOTES } from 'constants/index' | ||
| import moment from 'moment-timezone' | ||
| import { CacheSet } from 'redis/index' | ||
| import { ButtonDisplayData, Payment } from './types' | ||
| import { getUserDashboardData } from './dashboardCache' | ||
| import { getUserDashboardData, clearDashboardCache } from './dashboardCache' | ||
| // ADDRESS:payments:YYYY:MM | ||
| const getPaymentsWeekKey = (addressString: string, timestamp: number): string => { | ||
| return `${addressString}:payments:${moment.unix(timestamp).format(PAYMENT_WEEK_KEY_FORMAT)}` | ||
|
|
@@ -30,13 +30,23 @@ export async function * getUserUncachedAddresses (userId: string): AsyncGenerato | |
| } | ||
| } | ||
|
|
||
| export const getPaymentList = async (userId: string): Promise<Payment[]> => { | ||
| if (process.env.SKIP_CACHE_REBUILD === undefined) { | ||
| const uncachedAddressStream = getUserUncachedAddresses(userId) | ||
| for await (const address of uncachedAddressStream) { | ||
| void await CacheSet.addressCreation(address) | ||
| const triggerBackgroundRebuildIfNeeded = async (userId: string): Promise<void> => { | ||
| if (process.env.SKIP_CACHE_REBUILD !== undefined) return | ||
| const uncachedAddresses: Address[] = [] | ||
| const uncachedAddressStream = getUserUncachedAddresses(userId) | ||
| for await (const address of uncachedAddressStream) { | ||
| uncachedAddresses.push(address) | ||
| } | ||
| if (uncachedAddresses.length > 0) { | ||
| if (!isBackgroundRebuildActive(userId)) { | ||
| console.log(`[CACHE] ${uncachedAddresses.length} uncached addresses for user ${userId}, starting background rebuild`) | ||
| } | ||
| void cacheAddressesInBackground(uncachedAddresses, userId) | ||
| } | ||
| } | ||
|
|
||
| export const getPaymentList = async (userId: string): Promise<Payment[]> => { | ||
| await triggerBackgroundRebuildIfNeeded(userId) | ||
|
Comment on lines
+33
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Keep rebuild discovery off the payments hot path.
Suggested direction+const backgroundRebuildDiscoveryInProgress = new Set<string>()
+
-const triggerBackgroundRebuildIfNeeded = async (userId: string): Promise<void> => {
+const triggerBackgroundRebuildIfNeeded = (userId: string): void => {
if (process.env.SKIP_CACHE_REBUILD !== undefined) return
+ if (isBackgroundRebuildActive(userId) || backgroundRebuildDiscoveryInProgress.has(userId)) return
+
+ backgroundRebuildDiscoveryInProgress.add(userId)
+ void (async () => {
+ try {
const uncachedAddresses: Address[] = []
const uncachedAddressStream = getUserUncachedAddresses(userId)
for await (const address of uncachedAddressStream) {
uncachedAddresses.push(address)
}
if (uncachedAddresses.length > 0) {
if (!isBackgroundRebuildActive(userId)) {
- console.log(`[CACHE] ${uncachedAddresses.length} uncached addresses for user ${userId}, starting background rebuild`)
+ console.log(`[CACHE] ${uncachedAddresses.length} uncached addresses, starting background rebuild`)
}
- void cacheAddressesInBackground(uncachedAddresses, userId)
+ await cacheAddressesInBackground(uncachedAddresses, userId)
}
+ } catch (err) {
+ console.warn('[CACHE] Failed to trigger background rebuild', err)
+ } finally {
+ backgroundRebuildDiscoveryInProgress.delete(userId)
+ }
+ })()
}
export const getPaymentList = async (userId: string): Promise<Payment[]> => {
- await triggerBackgroundRebuildIfNeeded(userId)
+ triggerBackgroundRebuildIfNeeded(userId)
return await getCachedPaymentsForUser(userId)
}Also applies to: 374-374 🤖 Prompt for AI Agents |
||
| return await getCachedPaymentsForUser(userId) | ||
| } | ||
|
|
||
|
|
@@ -147,20 +157,31 @@ export const generatePaymentFromTxWithInvoices = (tx: TransactionWithAddressAndP | |
| } | ||
|
|
||
| export const generateAndCacheGroupedPaymentsAndInfoForAddress = async (address: Address): Promise<GroupedPaymentsAndInfoObject> => { | ||
| const startTime = Date.now() | ||
| let paymentList: Payment[] = [] | ||
| let balance = new Prisma.Decimal(0) | ||
| let paymentCount = 0 | ||
| let txCount = 0 | ||
| const txsWithPaybuttonsGenerator = generateTransactionsWithPaybuttonsAndPricesForAddress(address.id) | ||
| for await (const batch of txsWithPaybuttonsGenerator) { | ||
| for (const tx of batch) { | ||
| txCount++ | ||
| balance = balance.plus(tx.amount) | ||
| if (tx.amount.gt(0)) { | ||
| if (tx.prices.length !== N_OF_QUOTES) { | ||
| console.warn(`[CACHE] Skipping tx ${tx.hash} — missing price links (${tx.prices.length}/${N_OF_QUOTES})`) | ||
| continue | ||
| } | ||
| const payment = generatePaymentFromTx(tx) | ||
| paymentList.push(payment) | ||
| paymentCount++ | ||
| } | ||
| } | ||
| // Throttle batch processing slightly so long cache rebuilds don't monopolize DB resources. | ||
| await new Promise(resolve => setTimeout(resolve, 200)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should add a comment on this one. I'm not sure it's really helping anything but at least if somebody else looks at the code there should be an explanation about why there is a seemingly random delay in the processing workflow
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||
| } | ||
| const elapsed = ((Date.now() - startTime) / 1000).toFixed(1) | ||
| console.log(`[CACHE] Cached address ${address.address.slice(0, 20)}...: ${paymentCount} payments, ${txCount} txs in ${elapsed}s`) | ||
| const info: AddressPaymentInfo = { | ||
| balance, | ||
| paymentCount | ||
|
|
@@ -315,14 +336,43 @@ export const initPaymentCache = async (address: Address): Promise<boolean> => { | |
| return false | ||
| } | ||
|
|
||
| export async function * getPaymentStream (userId: string): AsyncGenerator<Payment> { | ||
| if (process.env.SKIP_CACHE_REBUILD === undefined) { | ||
| const uncachedAddressStream = getUserUncachedAddresses(userId) | ||
| for await (const address of uncachedAddressStream) { | ||
| console.log('[CACHE]: Creating cache for address', address.address) | ||
| await CacheSet.addressCreation(address) | ||
| const activeBackgroundRebuilds = new Set<string>() | ||
|
|
||
| export const isBackgroundRebuildActive = (userId: string): boolean => { | ||
| return activeBackgroundRebuilds.has(userId) | ||
| } | ||
|
|
||
| const cacheAddressesInBackground = async (addresses: Address[], userId: string): Promise<void> => { | ||
| if (activeBackgroundRebuilds.has(userId)) { | ||
| console.log(`[CACHE] Background: already running for user ${userId}, skipping`) | ||
| return | ||
| } | ||
| activeBackgroundRebuilds.add(userId) | ||
| const startTime = Date.now() | ||
| let cached = 0 | ||
| try { | ||
| for (const address of addresses) { | ||
| try { | ||
| await CacheSet.addressCreation(address) | ||
| cached++ | ||
| if (cached % 10 === 0 || cached === addresses.length) { | ||
| console.log(`[CACHE] Background: cached ${cached}/${addresses.length} addresses for user ${userId}`) | ||
| } | ||
| } catch (err: any) { | ||
| console.error(`[CACHE] Background: failed to cache ${address.address.slice(0, 20)}...: ${String(err.message ?? err)}`) | ||
| } | ||
| } | ||
| const elapsed = ((Date.now() - startTime) / 1000).toFixed(1) | ||
| console.log(`[CACHE] Background: completed ${cached}/${addresses.length} addresses for user ${userId} in ${elapsed}s`) | ||
| await clearDashboardCache(userId) | ||
| } finally { | ||
| activeBackgroundRebuilds.delete(userId) | ||
| } | ||
| } | ||
|
|
||
| export async function * getPaymentStream (userId: string): AsyncGenerator<Payment> { | ||
| await triggerBackgroundRebuildIfNeeded(userId) | ||
|
|
||
| const userButtonIds: string[] = (await fetchPaybuttonArrayByUserId(userId)) | ||
| .map(p => p.id) | ||
| const weekKeys = await getCachedWeekKeysForUser(userId) | ||
|
|
@@ -341,7 +391,7 @@ export async function * getPaymentStream (userId: string): AsyncGenerator<Paymen | |
| }) | ||
|
|
||
| for (const payment of weekPayments) { | ||
| yield payment // Yield one payment at a time | ||
| yield payment | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,6 @@ | ||
| import { Prisma, Address } from '@prisma/client' | ||
| import prisma from 'prisma-local/clientInstance' | ||
| import { RESPONSE_MESSAGES } from 'constants/index' | ||
| import { fetchAddressTransactions } from 'services/transactionService' | ||
| import { getNetworkFromSlug } from 'services/networkService' | ||
|
|
||
| const addressWithTransactionAndNetwork = Prisma.validator<Prisma.AddressDefaultArgs>()({ | ||
|
|
@@ -228,17 +227,20 @@ export interface AddressPaymentInfo { | |
| } | ||
|
|
||
| export async function generateAddressPaymentInfo (addressString: string): Promise<AddressPaymentInfo> { | ||
| const transactionsAmounts = (await fetchAddressTransactions(addressString)).map((t) => t.amount) | ||
| const balance = transactionsAmounts.reduce((a, b) => { | ||
| return a.plus(b) | ||
| }, new Prisma.Decimal(0)) | ||
| const zero = new Prisma.Decimal(0) | ||
| const paymentCount = transactionsAmounts.filter(t => t > zero).length | ||
| const info = { | ||
| balance, | ||
| const address = await fetchAddressBySubstring(addressString) | ||
| const [balanceResult, paymentCount] = await Promise.all([ | ||
| prisma.transaction.aggregate({ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice |
||
| where: { addressId: address.id }, | ||
| _sum: { amount: true } | ||
| }), | ||
| prisma.transaction.count({ | ||
| where: { addressId: address.id, amount: { gt: 0 } } | ||
| }) | ||
| ]) | ||
| return { | ||
| balance: balanceResult._sum.amount ?? new Prisma.Decimal(0), | ||
| paymentCount | ||
| } | ||
| return info | ||
| } | ||
| export async function getEarliestUnconfirmedTxTimestampForAddress (addressId: string): Promise<number | undefined> { | ||
| const tx = await prisma.transaction.findFirst({ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Polling stops after the first refresh.
This effect only depends on the boolean
dashboardData?.cacheRebuilding. If the first refresh still returnscacheRebuilding: true, the dependencies do not change, so no second timer is scheduled. Long-running rebuilds will therefore update once and then stall.Suggested fix
useEffect(() => { if (dashboardData?.cacheRebuilding !== true) return - const timer = setTimeout(() => { - let url = 'api/dashboard' - if (selectedButtonIds.length > 0) { - url += `?buttonIds=${selectedButtonIds.join(',')}` - } - fetch(url, { headers: { Timezone: moment.tz.guess() } }) - .then(async res => await res.json()) - .then(json => { setDashboardData(json) }) - .catch(console.error) - }, 15000) - return () => clearTimeout(timer) - }, [dashboardData?.cacheRebuilding, selectedButtonIds]) + const interval = setInterval(() => { + let url = 'api/dashboard' + if (selectedButtonIds.length > 0) { + url += `?buttonIds=${selectedButtonIds.join(',')}` + } + fetch(url, { headers: { Timezone: moment.tz.guess() } }) + .then(async res => await res.json()) + .then(json => { setDashboardData(json) }) + .catch(console.error) + }, 15000) + return () => clearInterval(interval) + }, [dashboardData, selectedButtonIds])🤖 Prompt for AI Agents