Move read & update into transaction

This commit is contained in:
Ian Philips 2022-07-05 10:06:05 -06:00
parent 0aa00b75d5
commit 2e6801ff72
7 changed files with 92 additions and 86 deletions

View File

@ -58,4 +58,3 @@ export type notification_reason_types =
| 'you_referred_user'
| 'user_joined_to_bet_on_your_market'
| 'unique_bettors_on_your_contract'
| 'daily_bonus'

View File

@ -16,13 +16,8 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
amount: number
token: 'M$' // | 'USD' | MarketOutcome
category:
| 'CHARITY'
| 'MANALINK'
| 'TIP'
| 'REFERRAL'
| 'UNIQUE_BETTOR_BONUS'
| 'DAILY_BONUS' // | 'BET'
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' | 'UNIQUE_BETTOR_BONUS'
// Any extra data
data?: { [key: string]: any }
@ -61,7 +56,7 @@ type Referral = {
type Bonus = {
fromType: 'BANK'
toType: 'USER'
category: 'DAILY_BONUS' | 'UNIQUE_BETTOR_BONUS'
category: 'UNIQUE_BETTOR_BONUS'
}
export type DonationTxn = Txn & Donation

View File

@ -57,7 +57,7 @@ export type PrivateUser = {
initialIpAddress?: string
apiKey?: string
notificationPreferences?: notification_subscribe_types
lastTimeReceivedBonuses?: number
lastTimeCheckedBonuses?: number
}
export type notification_subscribe_types = 'all' | 'less' | 'none'

View File

@ -13,31 +13,38 @@ import { Contract } from '../../common/lib/contract'
import { UNIQUE_BETTOR_BONUS_AMOUNT } from '../../common/numeric-constants'
const BONUS_START_DATE = new Date('2022-07-01T00:00:00.000Z').getTime()
const QUERY_LIMIT_SECONDS = 60
export const getdailybonuses = newEndpoint({}, async (req, auth) => {
log('Inside endpoint handler.')
const { user, fromUser, lastReceivedBonusTime } =
await firestore.runTransaction(async (trans) => {
const userSnap = await firestore.doc(`private-users/${auth.uid}`).get()
const { user, lastTimeCheckedBonuses } = await firestore.runTransaction(
async (trans) => {
const userSnap = await trans.get(
firestore.doc(`private-users/${auth.uid}`)
)
if (!userSnap.exists) throw new APIError(400, 'User not found.')
const user = userSnap.data() as PrivateUser
// TODO: switch to prod id
const fromUserId = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // dev manifold account
//const fromUserId = HOUSE_LIQUIDITY_PROVIDER_ID, // prod manifold account
const fromSnap = await firestore.doc(`users/${fromUserId}`).get()
if (!userSnap.exists) throw new APIError(400, 'From user not found.')
const fromUser = fromSnap.data() as User
const lastReceivedBonusTime = user.lastTimeReceivedBonuses ?? 0
if (Date.now() - lastReceivedBonusTime < 5 * 1000)
throw new APIError(400, 'Limited to one query per user per 5 seconds.')
await trans.update(firestore.doc(`private-users/${user.id}`), {
lastTimeReceivedBonuses: Date.now(),
const lastTimeCheckedBonuses = user.lastTimeCheckedBonuses ?? 0
if (Date.now() - lastTimeCheckedBonuses < QUERY_LIMIT_SECONDS * 1000)
throw new APIError(
400,
`Limited to one query per user per ${QUERY_LIMIT_SECONDS} seconds.`
)
await trans.update(userSnap.ref, {
lastTimeCheckedBonuses: Date.now(),
})
return { user, fromUser, lastReceivedBonusTime }
})
// get all users contracts made since implementation time
return {
user,
lastTimeCheckedBonuses,
}
}
)
// TODO: switch to prod id
// const fromUserId = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // dev manifold account
const fromUserId = HOUSE_LIQUIDITY_PROVIDER_ID // prod manifold account
const fromSnap = await firestore.doc(`users/${fromUserId}`).get()
if (!fromSnap.exists) throw new APIError(400, 'From user not found.')
const fromUser = fromSnap.data() as User
// Get all users contracts made since implementation time
const userContractsSnap = await firestore
.collection(`contracts`)
.where('creatorId', '==', user.id)
@ -50,8 +57,7 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
for (const contract of userContracts) {
const result = await firestore.runTransaction(async (trans) => {
const contractId = contract.id
// log(`Checking contract ${contractId}`)
// get all bets made on user's contracts
// Get all bets made on user's contracts
const bets = (
await firestore
.collection(`contracts/${contractId}/bets`)
@ -63,21 +69,21 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
}
const contractBetsSnap = await trans.getAll(...bets)
const contractBets = contractBetsSnap.map((doc) => doc.data() as Bet)
// log(`Got ${contractBets.length} bets`)
const uniqueBettorIdsBeforeLastResetTime = uniq(
contractBets
.filter((bet) => bet.createdTime < lastReceivedBonusTime)
.filter((bet) => bet.createdTime < lastTimeCheckedBonuses)
.map((bet) => bet.userId)
)
// filter users for ONLY those that have made bets since the last daily bonus received time
// Filter users for ONLY those that have made bets since the last daily bonus received time
const uniqueBettorIdsWithBetsAfterLastResetTime = uniq(
contractBets
.filter((bet) => bet.createdTime > lastReceivedBonusTime)
.filter((bet) => bet.createdTime > lastTimeCheckedBonuses)
.map((bet) => bet.userId)
)
// filter for users only present in the above list
// Filter for users only present in the above list
const newUniqueBettorIds =
uniqueBettorIdsWithBetsAfterLastResetTime.filter(
(userId) => !uniqueBettorIdsBeforeLastResetTime.includes(userId)
@ -87,10 +93,9 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
`Got ${newUniqueBettorIds.length} new unique bettors since last bonus`
)
if (newUniqueBettorIds.length === 0) {
// log(`No new unique bettors for contract ${contractId}`)
return nullReturn
}
// create txn for each unique user
// Create combined txn for all unique bettors
const bonusTxnDetails = {
contractId: contractId,
uniqueBettors: newUniqueBettorIds.length,
@ -122,7 +127,7 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
result.txn.amount + '',
contract,
undefined,
// No need to set the user id, they're the contract creator id
// No need to set the user id, we'll use the contract creator id
undefined,
contract.slug,
contract.question

View File

@ -198,7 +198,7 @@ export default function Sidebar(props: { className?: string }) {
useEffect(() => {
if (!privateUser) return
if (Date.now() - (privateUser.lastTimeReceivedBonuses ?? 0) > 1000)
if (Date.now() - (privateUser.lastTimeCheckedBonuses ?? 0) > 30 * 1000)
requestBonuses({}).catch((error) => {
console.log("couldn't get bonuses:", error.message)
})

View File

@ -42,6 +42,9 @@ export function groupNotifications(notifications: Notification[]) {
const bonusNotifications = notificationsGroupedByDay.filter(
(notification) => notification.sourceType === 'bonus'
)
const normalNotificationsGroupedByDay = notificationsGroupedByDay.filter(
(notification) => notification.sourceType !== 'bonus'
)
if (bonusNotifications.length > 0) {
notificationGroups = notificationGroups.concat({
notifications: bonusNotifications,
@ -53,9 +56,7 @@ export function groupNotifications(notifications: Notification[]) {
}
// Group notifications by contract, filtering out bonuses:
const groupedNotificationsByContractId = groupBy(
notificationsGroupedByDay.filter(
(notification) => notification.sourceType !== 'bonus'
),
normalNotificationsGroupedByDay,
(notification) => {
return notification.sourceContractId
}

View File

@ -37,7 +37,7 @@ import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants'
import { groupBy } from 'lodash'
export const NOTIFICATIONS_PER_PAGE = 30
export const HIGHLIGHT_DURATION = 20000
export const HIGHLIGHT_DURATION = 30 * 1000
export default function Notifications() {
const user = useUser()
@ -100,46 +100,52 @@ export default function Notifications() {
/>
)
)}
<nav
className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6"
aria-label="Pagination"
>
<div className="hidden sm:block">
<p className="text-sm text-gray-700">
Showing{' '}
<span className="font-medium">
{page === 1
? page
: (page - 1) * NOTIFICATIONS_PER_PAGE}
</span>{' '}
to{' '}
<span className="font-medium">
{page * NOTIFICATIONS_PER_PAGE}
</span>{' '}
of{' '}
<span className="font-medium">
{groupedNotifications.length}
</span>{' '}
results
</p>
</div>
<div className="flex flex-1 justify-between sm:justify-end">
<a
href="#"
className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
onClick={() => page > 1 && setPage(page - 1)}
>
Previous
</a>
<a
href="#"
className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
onClick={() => setPage(page + 1)}
>
Next
</a>
</div>
</nav>
{groupedNotifications.length > NOTIFICATIONS_PER_PAGE && (
<nav
className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6"
aria-label="Pagination"
>
<div className="hidden sm:block">
<p className="text-sm text-gray-700">
Showing{' '}
<span className="font-medium">
{page === 1
? page
: (page - 1) * NOTIFICATIONS_PER_PAGE}
</span>{' '}
to{' '}
<span className="font-medium">
{page * NOTIFICATIONS_PER_PAGE}
</span>{' '}
of{' '}
<span className="font-medium">
{groupedNotifications.length}
</span>{' '}
results
</p>
</div>
<div className="flex flex-1 justify-between sm:justify-end">
<a
href="#"
className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
onClick={() => page > 1 && setPage(page - 1)}
>
Previous
</a>
<a
href="#"
className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
onClick={() =>
page <
groupedNotifications?.length /
NOTIFICATIONS_PER_PAGE && setPage(page + 1)
}
>
Next
</a>
</div>
</nav>
)}
</div>
) : (
<LoadingIndicator />
@ -268,7 +274,7 @@ function IncomeNotificationGroupItem(props: {
className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'}
>
<span>
{'Income Summary: '}
{'Daily Income Summary: '}
<span className={'text-primary'}>{formatMoney(totalIncome)}</span>
</span>
</div>