Cache user bets tab with react query!! (#813)

* Convert useUserBets to react query

* Fix duplicate key warnings

* Fix react-query workaround to use refetchOnMount: always'

* Use react query for portfolio history

* Fix useUserBet workaround

* Script to back fill unique bettors in all contracts

* React query for user bet contracts, using uniqueBettorsId!

* Prefetch user bets / portfolio data
This commit is contained in:
James Grugett 2022-08-28 18:03:00 -05:00 committed by GitHub
parent 7e00f29189
commit 996b4795ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 157 additions and 122 deletions

View File

@ -0,0 +1,39 @@
import * as admin from 'firebase-admin'
import { initAdmin } from './script-init'
import { getValues, log, writeAsync } from '../utils'
import { Bet } from '../../../common/bet'
import { groupBy, mapValues, sortBy, uniq } from 'lodash'
initAdmin()
const firestore = admin.firestore()
const getBettorsByContractId = async () => {
const bets = await getValues<Bet>(firestore.collectionGroup('bets'))
log(`Loaded ${bets.length} bets.`)
const betsByContractId = groupBy(bets, 'contractId')
return mapValues(betsByContractId, (bets) =>
uniq(sortBy(bets, 'createdTime').map((bet) => bet.userId))
)
}
const updateUniqueBettors = async () => {
const bettorsByContractId = await getBettorsByContractId()
const updates = Object.entries(bettorsByContractId).map(
([contractId, userIds]) => {
const update = {
uniqueBettorIds: userIds,
uniqueBettorCount: userIds.length,
}
const docRef = firestore.collection('contracts').doc(contractId)
return { doc: docRef, fields: update }
}
)
log(`Updating ${updates.length} contracts.`)
await writeAsync(firestore, updates)
log(`Updated all contracts.`)
}
if (require.main === module) {
updateUniqueBettors()
}

View File

@ -1,14 +1,5 @@
import Link from 'next/link' import Link from 'next/link'
import { import { keyBy, groupBy, mapValues, sortBy, partition, sumBy } from 'lodash'
Dictionary,
keyBy,
groupBy,
mapValues,
sortBy,
partition,
sumBy,
uniq,
} from 'lodash'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
@ -28,7 +19,6 @@ import {
Contract, Contract,
contractPath, contractPath,
getBinaryProbPercent, getBinaryProbPercent,
getContractFromId,
} from 'web/lib/firebase/contracts' } from 'web/lib/firebase/contracts'
import { Row } from './layout/row' import { Row } from './layout/row'
import { UserLink } from './user-page' import { UserLink } from './user-page'
@ -56,9 +46,9 @@ import { SellSharesModal } from './sell-modal'
import { useUnfilledBets } from 'web/hooks/use-bets' import { useUnfilledBets } from 'web/hooks/use-bets'
import { LimitBet } from 'common/bet' import { LimitBet } from 'common/bet'
import { floatingEqual } from 'common/util/math' import { floatingEqual } from 'common/util/math'
import { filterDefined } from 'common/util/array'
import { Pagination } from './pagination' import { Pagination } from './pagination'
import { LimitOrderTable } from './limit-bets' import { LimitOrderTable } from './limit-bets'
import { useUserBetContracts } from 'web/hooks/use-contracts'
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value' type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all' type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all'
@ -72,26 +62,22 @@ export function BetsList(props: { user: User }) {
const signedInUser = useUser() const signedInUser = useUser()
const isYourBets = user.id === signedInUser?.id const isYourBets = user.id === signedInUser?.id
const hideBetsBefore = isYourBets ? 0 : JUNE_1_2022 const hideBetsBefore = isYourBets ? 0 : JUNE_1_2022
const userBets = useUserBets(user.id, { includeRedemptions: true }) const userBets = useUserBets(user.id)
const [contractsById, setContractsById] = useState<
Dictionary<Contract> | undefined
>()
// Hide bets before 06-01-2022 if this isn't your own profile // Hide bets before 06-01-2022 if this isn't your own profile
// NOTE: This means public profits also begin on 06-01-2022 as well. // NOTE: This means public profits also begin on 06-01-2022 as well.
const bets = useMemo( const bets = useMemo(
() => userBets?.filter((bet) => bet.createdTime >= (hideBetsBefore ?? 0)), () =>
userBets?.filter(
(bet) => !bet.isAnte && bet.createdTime >= (hideBetsBefore ?? 0)
),
[userBets, hideBetsBefore] [userBets, hideBetsBefore]
) )
useEffect(() => { const contractList = useUserBetContracts(user.id)
if (bets) { const contractsById = useMemo(() => {
const contractIds = uniq(bets.map((b) => b.contractId)) return contractList ? keyBy(contractList, 'id') : undefined
Promise.all(contractIds.map(getContractFromId)).then((contracts) => { }, [contractList])
setContractsById(keyBy(filterDefined(contracts), 'id'))
})
}
}, [bets])
const [sort, setSort] = useState<BetSort>('newest') const [sort, setSort] = useState<BetSort>('newest')
const [filter, setFilter] = useState<BetFilter>('open') const [filter, setFilter] = useState<BetFilter>('open')

View File

@ -319,7 +319,7 @@ function GroupsList(props: { currentPage: string; memberItems: Item[] }) {
{memberItems.map((item) => ( {memberItems.map((item) => (
<a <a
href={item.href} href={item.href}
key={item.name} key={item.href}
onClick={trackCallback('click sidebar group', { name: item.name })} onClick={trackCallback('click sidebar group', { name: item.name })}
className={clsx( className={clsx(
'cursor-pointer truncate', 'cursor-pointer truncate',

View File

@ -1,43 +1,26 @@
import { PortfolioMetrics } from 'common/user'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { last } from 'lodash' import { last } from 'lodash'
import { memo, useEffect, useState } from 'react' import { memo, useRef, useState } from 'react'
import { Period, getPortfolioHistory } from 'web/lib/firebase/users' import { usePortfolioHistory } from 'web/hooks/use-portfolio-history'
import { Period } from 'web/lib/firebase/users'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { PortfolioValueGraph } from './portfolio-value-graph' import { PortfolioValueGraph } from './portfolio-value-graph'
import { DAY_MS } from 'common/util/time'
const periodToCutoff = (now: number, period: Period) => {
switch (period) {
case 'daily':
return now - 1 * DAY_MS
case 'weekly':
return now - 7 * DAY_MS
case 'monthly':
return now - 30 * DAY_MS
case 'allTime':
default:
return new Date(0)
}
}
export const PortfolioValueSection = memo( export const PortfolioValueSection = memo(
function PortfolioValueSection(props: { userId: string }) { function PortfolioValueSection(props: { userId: string }) {
const { userId } = props const { userId } = props
const [portfolioPeriod, setPortfolioPeriod] = useState<Period>('weekly') const [portfolioPeriod, setPortfolioPeriod] = useState<Period>('weekly')
const [portfolioHistory, setUsersPortfolioHistory] = useState< const portfolioHistory = usePortfolioHistory(userId, portfolioPeriod)
PortfolioMetrics[]
>([])
useEffect(() => { // Remember the last defined portfolio history.
const cutoff = periodToCutoff(Date.now(), portfolioPeriod).valueOf() const portfolioRef = useRef(portfolioHistory)
getPortfolioHistory(userId, cutoff).then(setUsersPortfolioHistory) if (portfolioHistory) portfolioRef.current = portfolioHistory
}, [portfolioPeriod, userId]) const currPortfolioHistory = portfolioRef.current
const lastPortfolioMetrics = last(portfolioHistory) const lastPortfolioMetrics = last(currPortfolioHistory)
if (portfolioHistory.length === 0 || !lastPortfolioMetrics) { if (!currPortfolioHistory || !lastPortfolioMetrics) {
return <></> return <></>
} }
@ -64,7 +47,7 @@ export const PortfolioValueSection = memo(
</select> </select>
</Row> </Row>
<PortfolioValueGraph <PortfolioValueGraph
portfolioHistory={portfolioHistory} portfolioHistory={currPortfolioHistory}
includeTime={portfolioPeriod == 'daily'} includeTime={portfolioPeriod == 'daily'}
/> />
</> </>

View File

@ -1,3 +1,4 @@
import { useFirestoreQueryData } from '@react-query-firebase/firestore'
import { isEqual } from 'lodash' import { isEqual } from 'lodash'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { import {
@ -8,6 +9,7 @@ import {
listenForHotContracts, listenForHotContracts,
listenForInactiveContracts, listenForInactiveContracts,
listenForNewContracts, listenForNewContracts,
getUserBetContractsQuery,
} from 'web/lib/firebase/contracts' } from 'web/lib/firebase/contracts'
export const useContracts = () => { export const useContracts = () => {
@ -89,3 +91,15 @@ export const useUpdatedContracts = (contracts: Contract[] | undefined) => {
? contracts.map((c) => contractDict.current[c.id]) ? contracts.map((c) => contractDict.current[c.id])
: undefined : undefined
} }
export const useUserBetContracts = (userId: string) => {
const result = useFirestoreQueryData(
['contracts', 'bets', userId],
getUserBetContractsQuery(userId),
{ subscribe: true, includeMetadataChanges: true },
// Temporary workaround for react-query bug:
// https://github.com/invertase/react-query-firebase/issues/25
{ refetchOnMount: 'always' }
)
return result.data
}

View File

@ -20,8 +20,9 @@ function useNotifications(privateUser: PrivateUser) {
{ subscribe: true, includeMetadataChanges: true }, { subscribe: true, includeMetadataChanges: true },
// Temporary workaround for react-query bug: // Temporary workaround for react-query bug:
// https://github.com/invertase/react-query-firebase/issues/25 // https://github.com/invertase/react-query-firebase/issues/25
{ cacheTime: 0 } { refetchOnMount: 'always' }
) )
const notifications = useMemo(() => { const notifications = useMemo(() => {
if (!result.data) return undefined if (!result.data) return undefined
const notifications = result.data as Notification[] const notifications = result.data as Notification[]

View File

@ -0,0 +1,32 @@
import { useFirestoreQueryData } from '@react-query-firebase/firestore'
import { DAY_MS, HOUR_MS } from 'common/util/time'
import { getPortfolioHistoryQuery, Period } from 'web/lib/firebase/users'
export const usePortfolioHistory = (userId: string, period: Period) => {
const nowRounded = Math.round(Date.now() / HOUR_MS) * HOUR_MS
const cutoff = periodToCutoff(nowRounded, period).valueOf()
const result = useFirestoreQueryData(
['portfolio-history', userId, cutoff],
getPortfolioHistoryQuery(userId, cutoff),
{ subscribe: true, includeMetadataChanges: true },
// Temporary workaround for react-query bug:
// https://github.com/invertase/react-query-firebase/issues/25
{ refetchOnMount: 'always' }
)
return result.data
}
const periodToCutoff = (now: number, period: Period) => {
switch (period) {
case 'daily':
return now - 1 * DAY_MS
case 'weekly':
return now - 7 * DAY_MS
case 'monthly':
return now - 30 * DAY_MS
case 'allTime':
default:
return new Date(0)
}
}

11
web/hooks/use-prefetch.ts Normal file
View File

@ -0,0 +1,11 @@
import { useUserBetContracts } from './use-contracts'
import { usePortfolioHistory } from './use-portfolio-history'
import { useUserBets } from './use-user-bets'
export function usePrefetch(userId: string | undefined) {
const maybeUserId = userId ?? ''
useUserBets(maybeUserId)
useUserBetContracts(maybeUserId)
usePortfolioHistory(maybeUserId, 'weekly')
}

View File

@ -1,22 +1,21 @@
import { uniq } from 'lodash' import { useFirestoreQueryData } from '@react-query-firebase/firestore'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { import {
Bet, Bet,
listenForUserBets, getUserBetsQuery,
listenForUserContractBets, listenForUserContractBets,
} from 'web/lib/firebase/bets' } from 'web/lib/firebase/bets'
export const useUserBets = ( export const useUserBets = (userId: string) => {
userId: string | undefined, const result = useFirestoreQueryData(
options: { includeRedemptions: boolean } ['bets', userId],
) => { getUserBetsQuery(userId),
const [bets, setBets] = useState<Bet[] | undefined>(undefined) { subscribe: true, includeMetadataChanges: true },
// Temporary workaround for react-query bug:
useEffect(() => { // https://github.com/invertase/react-query-firebase/issues/25
if (userId) return listenForUserBets(userId, setBets, options) { refetchOnMount: 'always' }
}, [userId]) )
return result.data
return bets
} }
export const useUserContractBets = ( export const useUserContractBets = (
@ -33,36 +32,6 @@ export const useUserContractBets = (
return bets return bets
} }
export const useUserBetContracts = (
userId: string | undefined,
options: { includeRedemptions: boolean }
) => {
const [contractIds, setContractIds] = useState<string[] | undefined>()
useEffect(() => {
if (userId) {
const key = `user-bet-contractIds-${userId}`
const userBetContractJson = localStorage.getItem(key)
if (userBetContractJson) {
setContractIds(JSON.parse(userBetContractJson))
}
return listenForUserBets(
userId,
(bets) => {
const contractIds = uniq(bets.map((bet) => bet.contractId))
setContractIds(contractIds)
localStorage.setItem(key, JSON.stringify(contractIds))
},
options
)
}
}, [userId])
return contractIds
}
export const useGetUserBetContractIds = (userId: string | undefined) => { export const useGetUserBetContractIds = (userId: string | undefined) => {
const [contractIds, setContractIds] = useState<string[] | undefined>() const [contractIds, setContractIds] = useState<string[] | undefined>()

View File

@ -11,6 +11,7 @@ import {
getDocs, getDocs,
getDoc, getDoc,
DocumentSnapshot, DocumentSnapshot,
Query,
} from 'firebase/firestore' } from 'firebase/firestore'
import { uniq } from 'lodash' import { uniq } from 'lodash'
@ -131,24 +132,12 @@ export async function getContractsOfUserBets(userId: string) {
return filterDefined(contracts) return filterDefined(contracts)
} }
export function listenForUserBets( export function getUserBetsQuery(userId: string) {
userId: string, return query(
setBets: (bets: Bet[]) => void,
options: { includeRedemptions: boolean }
) {
const { includeRedemptions } = options
const userQuery = query(
collectionGroup(db, 'bets'), collectionGroup(db, 'bets'),
where('userId', '==', userId), where('userId', '==', userId),
orderBy('createdTime', 'desc') orderBy('createdTime', 'desc')
) ) as Query<Bet>
return listenForValues<Bet>(userQuery, (bets) => {
setBets(
bets.filter(
(bet) => (includeRedemptions || !bet.isRedemption) && !bet.isAnte
)
)
})
} }
export function listenForUserContractBets( export function listenForUserContractBets(

View File

@ -6,6 +6,7 @@ import {
getDocs, getDocs,
limit, limit,
orderBy, orderBy,
Query,
query, query,
setDoc, setDoc,
startAfter, startAfter,
@ -156,6 +157,13 @@ export function listenForUserContracts(
return listenForValues<Contract>(q, setContracts) return listenForValues<Contract>(q, setContracts)
} }
export function getUserBetContractsQuery(userId: string) {
return query(
contracts,
where('uniqueBettorIds', 'array-contains', userId)
) as Query<Contract>
}
const activeContractsQuery = query( const activeContractsQuery = query(
contracts, contracts,
where('isResolved', '==', false), where('isResolved', '==', false),

View File

@ -12,6 +12,7 @@ import {
deleteDoc, deleteDoc,
collectionGroup, collectionGroup,
onSnapshot, onSnapshot,
Query,
} from 'firebase/firestore' } from 'firebase/firestore'
import { getAuth } from 'firebase/auth' import { getAuth } from 'firebase/auth'
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth' import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'
@ -252,15 +253,13 @@ export async function unfollow(userId: string, unfollowedUserId: string) {
await deleteDoc(followDoc) await deleteDoc(followDoc)
} }
export async function getPortfolioHistory(userId: string, since: number) { export function getPortfolioHistoryQuery(userId: string, since: number) {
return getValues<PortfolioMetrics>( return query(
query(
collectionGroup(db, 'portfolioHistory'), collectionGroup(db, 'portfolioHistory'),
where('userId', '==', userId), where('userId', '==', userId),
where('timestamp', '>=', since), where('timestamp', '>=', since),
orderBy('timestamp', 'asc') orderBy('timestamp', 'asc')
) ) as Query<PortfolioMetrics>
)
} }
export function listenForFollows( export function listenForFollows(

View File

@ -42,6 +42,7 @@ import {
} from 'web/components/contract/contract-leaderboard' } from 'web/components/contract/contract-leaderboard'
import { ContractsGrid } from 'web/components/contract/contracts-grid' import { ContractsGrid } from 'web/components/contract/contracts-grid'
import { Title } from 'web/components/title' import { Title } from 'web/components/title'
import { usePrefetch } from 'web/hooks/use-prefetch'
export const getStaticProps = fromPropz(getStaticPropz) export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: { export async function getStaticPropz(props: {
@ -157,6 +158,7 @@ export function ContractPageContent(
const { backToHome, comments, user } = props const { backToHome, comments, user } = props
const contract = useContractWithPreload(props.contract) ?? props.contract const contract = useContractWithPreload(props.contract) ?? props.contract
usePrefetch(user?.id)
useTracking('view market', { useTracking('view market', {
slug: contract.slug, slug: contract.slug,

View File

@ -15,6 +15,7 @@ import { track } from 'web/lib/service/analytics'
import { authenticateOnServer } from 'web/lib/firebase/server-auth' import { authenticateOnServer } from 'web/lib/firebase/server-auth'
import { useSaveReferral } from 'web/hooks/use-save-referral' import { useSaveReferral } from 'web/hooks/use-save-referral'
import { GetServerSideProps } from 'next' import { GetServerSideProps } from 'next'
import { usePrefetch } from 'web/hooks/use-prefetch'
export const getServerSideProps: GetServerSideProps = async (ctx) => { export const getServerSideProps: GetServerSideProps = async (ctx) => {
const creds = await authenticateOnServer(ctx) const creds = await authenticateOnServer(ctx)
@ -30,6 +31,7 @@ const Home = (props: { auth: { user: User } | null }) => {
useTracking('view home') useTracking('view home')
useSaveReferral() useSaveReferral()
usePrefetch(user?.id)
return ( return (
<> <>