Skip to content

Commit 6eab3e7

Browse files
authored
Merge pull request #887 from PayButton/fix/slow-page-loads
[#540] fix: slow page loads
2 parents 93a30c5 + bdf8091 commit 6eab3e7

7 files changed

Lines changed: 298 additions & 90 deletions

File tree

pages/api/address/transactions/count/[address].ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NextApiRequest, NextApiResponse } from 'next'
2-
import { fetchTxCount } from 'services/transactionService'
2+
import { fetchTxCountByAddressString } from 'services/transactionService'
33
import { RESPONSE_MESSAGES } from 'constants/index'
44
import { parseAddress } from 'utils/validators'
55

@@ -10,7 +10,7 @@ export default async (req: NextApiRequest, res: NextApiResponse): Promise<void>
1010
throw new Error(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.message)
1111
}
1212
const address = parseAddress(req.query.address as string)
13-
const count = await fetchTxCount(address)
13+
const count = await fetchTxCountByAddressString(address)
1414
res.status(200).send(count)
1515
} catch (err: any) {
1616
switch (err.message) {

redis/dashboardCache.ts

Lines changed: 252 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { redis } from './clientInstance'
2-
import { getPaymentList } from 'redis/paymentCache'
3-
import { ChartData, PeriodData, DashboardData, Payment, ButtonData, PaymentDataByButton, ChartColor } from './types'
2+
import { getCachedWeekKeysForUser, getPaymentsForWeekKey, getPaymentStream } from 'redis/paymentCache'
3+
import { ChartData, DashboardData, Payment, ButtonData, PaymentDataByButton, ChartColor, PeriodData, ButtonDisplayData } from './types'
44
import { Prisma } from '@prisma/client'
55
import moment, { DurationInputArg2 } from 'moment'
66
import { XEC_NETWORK_ID, BCH_NETWORK_ID } from 'constants/index'
7-
import { fetchPaybuttonArrayByUserId } from 'services/paybuttonService'
87
import { QuoteValues } from 'services/priceService'
98

109
// USERID:dashboard
@@ -16,30 +15,6 @@ const getChartLabels = function (n: number, periodString: string, formatString =
1615
return [...new Array(n)].map((_, idx) => moment().startOf('day').subtract(idx, periodString as DurationInputArg2).format(formatString)).reverse()
1716
}
1817

19-
interface RevenuePaymentData {
20-
revenue: QuoteValues[]
21-
payments: number[]
22-
}
23-
24-
const getChartRevenuePaymentData = function (n: number, periodString: string, paymentList: Payment[]): RevenuePaymentData {
25-
const revenueArray: QuoteValues[] = []
26-
const paymentsArray: number[] = []
27-
const _ = [...new Array(n)]
28-
_.forEach((_, idx) => {
29-
const lowerThreshold = moment().subtract(idx, periodString as DurationInputArg2).startOf(periodString === 'months' ? 'month' : 'day')
30-
const upperThreshold = moment().subtract(idx, periodString as DurationInputArg2).endOf(periodString === 'months' ? 'month' : 'day')
31-
const periodPaymentList = filterLastPayments(lowerThreshold, upperThreshold, paymentList)
32-
const revenue = sumPaymentsValue(periodPaymentList)
33-
const paymentCount = periodPaymentList.length
34-
revenueArray.push(revenue)
35-
paymentsArray.push(paymentCount)
36-
})
37-
return {
38-
revenue: revenueArray.reverse(),
39-
payments: paymentsArray.reverse()
40-
}
41-
}
42-
4318
const filterLastPayments = function (lowerThreshold: moment.Moment, upperThreshold: moment.Moment, paymentList: Payment[]): Payment[] {
4419
return paymentList.filter((p) => {
4520
const tMoment = moment(p.timestamp * 1000)
@@ -64,27 +39,24 @@ const getChartData = function (n: number, periodString: string, dataArray: numbe
6439
}
6540
}
6641

67-
const getPeriodData = function (n: number, periodString: string, paymentList: Payment[], borderColor: ChartColor, formatString = 'M/D'): PeriodData {
68-
const revenuePaymentData = getChartRevenuePaymentData(n, periodString, paymentList)
69-
const revenue = getChartData(n, periodString, revenuePaymentData.revenue, borderColor.revenue, formatString)
70-
const payments = getChartData(n, periodString, revenuePaymentData.payments, borderColor.payments, formatString)
71-
const buttons = getButtonPaymentData(n, periodString, paymentList)
72-
const totalRevenue = (revenue.datasets[0].data as QuoteValues[]).reduce(sumQuoteValues, { usd: new Prisma.Decimal(0), cad: new Prisma.Decimal(0) })
73-
const totalPayments = (payments.datasets[0].data as any).reduce((a: number, b: number) => a + b, 0)
42+
function getOldestDateKey (keys: string[]): string {
43+
const keyDatePairs = keys.map(k => [k, k.split(':').slice(-2).map(Number)] as [string, [number, number]])
44+
keyDatePairs.sort((a, b) => {
45+
const [aYear, aWeek] = a[1]
46+
const [bYear, bWeek] = b[1]
7447

75-
return {
76-
revenue,
77-
payments,
78-
totalRevenue,
79-
totalPayments,
80-
buttons
81-
}
48+
// compare year first, then week
49+
return (aYear !== bYear ? aYear - bYear : aWeek - bWeek)
50+
})
51+
return keyDatePairs[0][0]
8252
}
8353

84-
const getNumberOfMonths = function (paymentList: Payment[]): number {
85-
if (paymentList.length === 0) return 0
86-
const oldestTimestamp = Math.min(...paymentList.map(p => p.timestamp)
87-
)
54+
const getNumberOfMonths = async function (userId: string): Promise<number> {
55+
const weekKeys = await getCachedWeekKeysForUser(userId)
56+
if (weekKeys.length === 0) return 0
57+
const oldestKey = getOldestDateKey(weekKeys)
58+
const oldestPayments = await getPaymentsForWeekKey(oldestKey)
59+
const oldestTimestamp = Math.min(...oldestPayments.map(p => p.timestamp))
8860
const oldestDate = moment(oldestTimestamp * 1000)
8961
const today = moment()
9062
const floatDiff = today.diff(oldestDate, 'months', true)
@@ -147,32 +119,243 @@ export const sumPaymentsValue = function (paymentList: Payment[]): QuoteValues {
147119
return ret
148120
}
149121

150-
export const getUserDashboardData = async function (userId: string): Promise<DashboardData> {
151-
let dashboardData = await getCachedDashboardData(userId)
152-
if (dashboardData === null) {
153-
const buttons = await fetchPaybuttonArrayByUserId(userId)
154-
const paymentList = await getPaymentList(userId)
155-
156-
const totalRevenue = sumPaymentsValue(paymentList)
157-
const nMonthsTotal = getNumberOfMonths(paymentList)
158-
159-
const thirtyDays: PeriodData = getPeriodData(30, 'days', paymentList, { revenue: '#66fe91', payments: '#669cfe' })
160-
const sevenDays: PeriodData = getPeriodData(7, 'days', paymentList, { revenue: '#66fe91', payments: '#669cfe' })
161-
const year: PeriodData = getPeriodData(12, 'months', paymentList, { revenue: '#66fe91', payments: '#669cfe' }, 'MMM')
162-
const all: PeriodData = getPeriodData(nMonthsTotal, 'months', paymentList, { revenue: '#66fe91', payments: '#669cfe' }, 'MMM YYYY')
163-
164-
dashboardData = {
165-
thirtyDays,
166-
sevenDays,
167-
year,
168-
all,
169-
paymentList,
170-
total: {
171-
revenue: totalRevenue,
172-
payments: paymentList.length,
173-
buttons: buttons.length
122+
const generateDashboardDataFromStream = async function (
123+
paymentStream: AsyncGenerator<Payment>,
124+
nMonthsTotal: number,
125+
borderColor: ChartColor
126+
): Promise<DashboardData> {
127+
// Initialize accumulators for periods
128+
const revenueAccumulators = createRevenueAccumulators(nMonthsTotal)
129+
const paymentCounters = createPaymentCounters(nMonthsTotal)
130+
const buttonDataAccumulators = createButtonDataAccumulators()
131+
132+
const today = moment().startOf('day')
133+
const monthStart = moment().startOf('month')
134+
const thresholds = createThresholds(today, monthStart, nMonthsTotal)
135+
136+
// Process all payments
137+
for await (const payment of paymentStream) {
138+
const paymentTime = moment(payment.timestamp * 1000)
139+
140+
// Process button data and assign to relevant periods
141+
payment.buttonDisplayDataList.forEach((button) => {
142+
processButtonData(button, payment, paymentTime, buttonDataAccumulators, thresholds)
143+
})
144+
145+
// Accumulate period data
146+
const periods = ['thirtyDays', 'sevenDays', 'year', 'all'] as const
147+
periods.forEach((period) => {
148+
if (paymentTime.isSameOrAfter(thresholds[period])) {
149+
const index =
150+
period === 'thirtyDays' || period === 'sevenDays'
151+
? today.diff(paymentTime, 'days')
152+
: monthStart.diff(paymentTime, 'months')
153+
if (index < revenueAccumulators[period].length) {
154+
revenueAccumulators[period][index] = sumQuoteValues(revenueAccumulators[period][index], payment.values)
155+
paymentCounters[period][index] += 1
156+
}
174157
}
158+
})
159+
}
160+
161+
reverseAccumulators(revenueAccumulators, paymentCounters)
162+
163+
// Generate PeriodData for each period
164+
const thirtyDays = createPeriodData(
165+
30,
166+
'days',
167+
revenueAccumulators.thirtyDays,
168+
paymentCounters.thirtyDays,
169+
buttonDataAccumulators.thirtyDays,
170+
borderColor.revenue,
171+
borderColor.payments
172+
)
173+
174+
const sevenDays = createPeriodData(
175+
7,
176+
'days',
177+
revenueAccumulators.sevenDays,
178+
paymentCounters.sevenDays,
179+
buttonDataAccumulators.sevenDays,
180+
borderColor.revenue,
181+
borderColor.payments
182+
)
183+
184+
const year = createPeriodData(
185+
12,
186+
'months',
187+
revenueAccumulators.year,
188+
paymentCounters.year,
189+
buttonDataAccumulators.year,
190+
borderColor.revenue,
191+
borderColor.payments,
192+
'MMM'
193+
)
194+
195+
const all = createPeriodData(
196+
nMonthsTotal,
197+
'months',
198+
revenueAccumulators.all,
199+
paymentCounters.all,
200+
buttonDataAccumulators.all,
201+
borderColor.revenue,
202+
borderColor.payments,
203+
'MMM YYYY'
204+
)
205+
206+
return {
207+
thirtyDays,
208+
sevenDays,
209+
year,
210+
all,
211+
total: {
212+
revenue: all.totalRevenue,
213+
payments: all.totalPayments,
214+
buttons: Object.keys(buttonDataAccumulators.all).length
175215
}
216+
}
217+
}
218+
219+
interface PeriodRevenueAccumulators {
220+
thirtyDays: QuoteValues[]
221+
sevenDays: QuoteValues[]
222+
year: QuoteValues[]
223+
all: QuoteValues[]
224+
}
225+
226+
function createRevenueAccumulators (nMonthsTotal: number): PeriodRevenueAccumulators {
227+
return {
228+
thirtyDays: Array(30).fill({ usd: new Prisma.Decimal(0), cad: new Prisma.Decimal(0) }),
229+
sevenDays: Array(7).fill({ usd: new Prisma.Decimal(0), cad: new Prisma.Decimal(0) }),
230+
year: Array(12).fill({ usd: new Prisma.Decimal(0), cad: new Prisma.Decimal(0) }),
231+
all: Array(nMonthsTotal).fill({ usd: new Prisma.Decimal(0), cad: new Prisma.Decimal(0) })
232+
}
233+
}
234+
235+
interface PeriodPaymentCounters {
236+
thirtyDays: number[]
237+
sevenDays: number[]
238+
year: number[]
239+
all: number[]
240+
}
241+
242+
function createPaymentCounters (nMonthsTotal: number): PeriodPaymentCounters {
243+
return {
244+
thirtyDays: Array(30).fill(0),
245+
sevenDays: Array(7).fill(0),
246+
year: Array(12).fill(0),
247+
all: Array(nMonthsTotal).fill(0)
248+
}
249+
}
250+
251+
interface PeriodButtonDataAccumulators {
252+
thirtyDays: PaymentDataByButton
253+
sevenDays: PaymentDataByButton
254+
year: PaymentDataByButton
255+
all: PaymentDataByButton
256+
}
257+
258+
function createButtonDataAccumulators (): PeriodButtonDataAccumulators {
259+
return {
260+
thirtyDays: {},
261+
sevenDays: {},
262+
year: {},
263+
all: {}
264+
}
265+
}
266+
267+
interface PeriodThresholds {
268+
thirtyDays: moment.Moment
269+
sevenDays: moment.Moment
270+
year: moment.Moment
271+
all: moment.Moment
272+
}
273+
274+
function createThresholds (today: moment.Moment, monthStart: moment.Moment, nMonthsTotal: number): PeriodThresholds {
275+
return {
276+
thirtyDays: today.clone().subtract(30, 'days'),
277+
sevenDays: today.clone().subtract(7, 'days'),
278+
year: monthStart.clone().subtract(12, 'months'),
279+
all: monthStart.clone().subtract(nMonthsTotal, 'months')
280+
}
281+
}
282+
283+
function processButtonData (
284+
button: ButtonDisplayData,
285+
payment: Payment,
286+
paymentTime: moment.Moment,
287+
buttonDataAccumulators: ReturnType<typeof createButtonDataAccumulators>,
288+
thresholds: ReturnType<typeof createThresholds>
289+
): void {
290+
const periods = ['thirtyDays', 'sevenDays', 'year', 'all'] as const
291+
periods.forEach((period) => {
292+
if (paymentTime.isSameOrAfter(thresholds[period])) {
293+
if (buttonDataAccumulators[period][button.id] === undefined) {
294+
buttonDataAccumulators[period][button.id] = {
295+
displayData: {
296+
...button,
297+
isXec: payment.networkId === XEC_NETWORK_ID,
298+
isBch: payment.networkId === BCH_NETWORK_ID,
299+
lastPayment: payment.timestamp
300+
},
301+
total: {
302+
revenue: payment.values,
303+
payments: 1
304+
}
305+
}
306+
} else {
307+
const buttonData = buttonDataAccumulators[period][button.id]
308+
buttonData.total.revenue = sumQuoteValues(buttonData.total.revenue, payment.values)
309+
buttonData.total.payments += 1
310+
buttonData.displayData.lastPayment = Math.max(
311+
buttonData.displayData.lastPayment ?? 0,
312+
payment.timestamp
313+
)
314+
}
315+
}
316+
})
317+
}
318+
319+
function reverseAccumulators (
320+
revenueAccumulators: ReturnType<typeof createRevenueAccumulators>,
321+
paymentCounters: ReturnType<typeof createPaymentCounters>
322+
): void {
323+
Object.keys(revenueAccumulators).forEach((key) => {
324+
revenueAccumulators[key as keyof typeof revenueAccumulators].reverse()
325+
paymentCounters[key as keyof typeof paymentCounters].reverse()
326+
})
327+
}
328+
329+
function createPeriodData (
330+
periodLength: number,
331+
periodUnit: string,
332+
revenueData: QuoteValues[],
333+
paymentData: number[],
334+
buttonData: PaymentDataByButton,
335+
revenueColor: string,
336+
paymentColor: string,
337+
labelFormat = 'M/D'
338+
): PeriodData {
339+
return {
340+
revenue: getChartData(periodLength, periodUnit, revenueData, revenueColor, labelFormat),
341+
payments: getChartData(periodLength, periodUnit, paymentData, paymentColor, labelFormat),
342+
totalRevenue: revenueData.reduce(sumQuoteValues, { usd: new Prisma.Decimal(0), cad: new Prisma.Decimal(0) }),
343+
totalPayments: paymentData.reduce((a, b) => a + b, 0),
344+
buttons: buttonData
345+
}
346+
}
347+
348+
export const getUserDashboardData = async function (userId: string): Promise<DashboardData> {
349+
const dashboardData = await getCachedDashboardData(userId)
350+
if (dashboardData === null) {
351+
const nMonthsTotal = await getNumberOfMonths(userId)
352+
const paymentStream = getPaymentStream(userId)
353+
354+
const dashboardData = await generateDashboardDataFromStream(
355+
paymentStream,
356+
nMonthsTotal,
357+
{ revenue: '#66fe91', payments: '#669cfe' }
358+
)
176359
await cacheDashboardData(userId, dashboardData) // WIP SET THIS NULL ON UPDATE BUTTONS & WS
177360
return dashboardData
178361
}
@@ -182,7 +365,6 @@ export const getUserDashboardData = async function (userId: string): Promise<Das
182365
export const cacheDashboardData = async (userId: string, dashboardData: DashboardData): Promise<void> => {
183366
const key = getDashboardSummaryKey(userId)
184367
const {
185-
paymentList,
186368
...cachable
187369
} = dashboardData
188370
await redis.set(key, JSON.stringify(cachable))

0 commit comments

Comments
 (0)