Update user feed each hour and get feed from cache doc.
This commit is contained in:
		
							parent
							
								
									cd5689a72f
								
							
						
					
					
						commit
						4b3bc71cbb
					
				| 
						 | 
					@ -34,6 +34,10 @@ service cloud.firestore {
 | 
				
			||||||
      allow create: if userId == request.auth.uid;
 | 
					      allow create: if userId == request.auth.uid;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match /private-users/{userId}/cache/feed {
 | 
				
			||||||
 | 
					      allow read: if userId == request.auth.uid || isAdmin();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match /contracts/{contractId} {
 | 
					    match /contracts/{contractId} {
 | 
				
			||||||
      allow read;
 | 
					      allow read;
 | 
				
			||||||
      allow update: if request.resource.data.diff(resource.data).affectedKeys()
 | 
					      allow update: if request.resource.data.diff(resource.data).affectedKeys()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,8 +21,8 @@ export * from './unsubscribe'
 | 
				
			||||||
export * from './update-contract-metrics'
 | 
					export * from './update-contract-metrics'
 | 
				
			||||||
export * from './update-user-metrics'
 | 
					export * from './update-user-metrics'
 | 
				
			||||||
export * from './update-recommendations'
 | 
					export * from './update-recommendations'
 | 
				
			||||||
 | 
					export * from './update-user-feed'
 | 
				
			||||||
export * from './backup-db'
 | 
					export * from './backup-db'
 | 
				
			||||||
export * from './change-user-info'
 | 
					export * from './change-user-info'
 | 
				
			||||||
export * from './market-close-emails'
 | 
					export * from './market-close-emails'
 | 
				
			||||||
export * from './add-liquidity'
 | 
					export * from './add-liquidity'
 | 
				
			||||||
export * from './get-feed'
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,13 +12,13 @@ export const onView = functions.firestore
 | 
				
			||||||
    const { contractId, timestamp } = snapshot.data() as View
 | 
					    const { contractId, timestamp } = snapshot.data() as View
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await firestore
 | 
					    await firestore
 | 
				
			||||||
      .doc(`private-users/${userId}/cached/viewCounts`)
 | 
					      .doc(`private-users/${userId}/cache/viewCounts`)
 | 
				
			||||||
      .set(
 | 
					      .set(
 | 
				
			||||||
        { [contractId]: admin.firestore.FieldValue.increment(1) },
 | 
					        { [contractId]: admin.firestore.FieldValue.increment(1) },
 | 
				
			||||||
        { merge: true }
 | 
					        { merge: true }
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await firestore
 | 
					    await firestore
 | 
				
			||||||
      .doc(`private-users/${userId}/cached/lastViewTime`)
 | 
					      .doc(`private-users/${userId}/cache/lastViewTime`)
 | 
				
			||||||
      .set({ [contractId]: timestamp }, { merge: true })
 | 
					      .set({ [contractId]: timestamp }, { merge: true })
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,7 +39,7 @@ export const updateUserRecommendations = async (
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getValue<{ [contractId: string]: number }>(
 | 
					    getValue<{ [contractId: string]: number }>(
 | 
				
			||||||
      firestore.doc(`private-users/${user.id}/cached/viewCounts`)
 | 
					      firestore.doc(`private-users/${user.id}/cache/viewCounts`)
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getValues<ClickEvent>(
 | 
					    getValues<ClickEvent>(
 | 
				
			||||||
| 
						 | 
					@ -53,7 +53,7 @@ export const updateUserRecommendations = async (
 | 
				
			||||||
  const contractScores = getContractScores(contracts, wordScores)
 | 
					  const contractScores = getContractScores(contracts, wordScores)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const cachedCollection = firestore.collection(
 | 
					  const cachedCollection = firestore.collection(
 | 
				
			||||||
    `private-users/${user.id}/cached`
 | 
					    `private-users/${user.id}/cache`
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
  await cachedCollection.doc('wordScores').set(wordScores)
 | 
					  await cachedCollection.doc('wordScores').set(wordScores)
 | 
				
			||||||
  await cachedCollection.doc('contractScores').set(contractScores)
 | 
					  await cachedCollection.doc('contractScores').set(contractScores)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,19 +13,18 @@ import {
 | 
				
			||||||
} from '../../common/calculate'
 | 
					} from '../../common/calculate'
 | 
				
			||||||
import { Bet } from '../../common/bet'
 | 
					import { Bet } from '../../common/bet'
 | 
				
			||||||
import { Comment } from '../../common/comment'
 | 
					import { Comment } from '../../common/comment'
 | 
				
			||||||
 | 
					import { User } from '../../common/user'
 | 
				
			||||||
 | 
					import { batchedWaitAll } from '../../common/util/promise'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const firestore = admin.firestore()
 | 
					const firestore = admin.firestore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MAX_FEED_CONTRACTS = 60
 | 
					const MAX_FEED_CONTRACTS = 60
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getFeed = functions
 | 
					export const updateUserFeed = functions.pubsub
 | 
				
			||||||
  .runWith({ minInstances: 1 })
 | 
					  .schedule('every 60 minutes')
 | 
				
			||||||
  .https.onCall(async (_data, context) => {
 | 
					  .onRun(async () => {
 | 
				
			||||||
    const userId = context?.auth?.uid
 | 
					 | 
				
			||||||
    if (!userId) return { status: 'error', message: 'Not authorized' }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Get contracts bet on or created in last week.
 | 
					    // Get contracts bet on or created in last week.
 | 
				
			||||||
    const contractsPromise = Promise.all([
 | 
					    const contracts = await Promise.all([
 | 
				
			||||||
      getValues<Contract>(
 | 
					      getValues<Contract>(
 | 
				
			||||||
        firestore
 | 
					        firestore
 | 
				
			||||||
          .collection('contracts')
 | 
					          .collection('contracts')
 | 
				
			||||||
| 
						 | 
					@ -46,59 +45,59 @@ export const getFeed = functions
 | 
				
			||||||
      return combined.filter((c) => (c.closeTime ?? Infinity) > Date.now())
 | 
					      return combined.filter((c) => (c.closeTime ?? Infinity) > Date.now())
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const userCacheCollection = firestore.collection(
 | 
					    const users = await getValues<User>(firestore.collection('users'))
 | 
				
			||||||
      `private-users/${userId}/cached`
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    const [recommendationScores, lastViewedTime] = await Promise.all([
 | 
					 | 
				
			||||||
      getValue<{ [contractId: string]: number }>(
 | 
					 | 
				
			||||||
        userCacheCollection.doc('contractScores')
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      getValue<{ [contractId: string]: number }>(
 | 
					 | 
				
			||||||
        userCacheCollection.doc('lastViewTime')
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    ]).then((dicts) => dicts.map((dict) => dict ?? {}))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const contracts = await contractsPromise
 | 
					    await batchedWaitAll(users.map((user) => () => updateFeed(user, contracts)))
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const averageRecScore =
 | 
					 | 
				
			||||||
      1 +
 | 
					 | 
				
			||||||
      _.sumBy(
 | 
					 | 
				
			||||||
        contracts.filter((c) => recommendationScores[c.id] !== undefined),
 | 
					 | 
				
			||||||
        (c) => recommendationScores[c.id]
 | 
					 | 
				
			||||||
      ) /
 | 
					 | 
				
			||||||
        (contracts.length + 1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log({ recommendationScores, averageRecScore, lastViewedTime })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const scoredContracts = contracts.map((contract) => {
 | 
					 | 
				
			||||||
      const score = scoreContract(
 | 
					 | 
				
			||||||
        contract,
 | 
					 | 
				
			||||||
        recommendationScores[contract.id] ?? averageRecScore,
 | 
					 | 
				
			||||||
        lastViewedTime[contract.id]
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
      return [contract, score] as [Contract, number]
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const sortedContracts = _.sortBy(
 | 
					 | 
				
			||||||
      scoredContracts,
 | 
					 | 
				
			||||||
      ([_, score]) => score
 | 
					 | 
				
			||||||
    ).reverse()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log(sortedContracts.map(([c, score]) => c.question + ': ' + score))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const feedContracts = sortedContracts
 | 
					 | 
				
			||||||
      .slice(0, MAX_FEED_CONTRACTS)
 | 
					 | 
				
			||||||
      .map(([c]) => c)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const feed = await Promise.all(
 | 
					 | 
				
			||||||
      feedContracts.map((contract) => getRecentBetsAndComments(contract))
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log('feed', feed)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return { status: 'success', feed }
 | 
					 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const updateFeed = async (user: User, contracts: Contract[]) => {
 | 
				
			||||||
 | 
					  const userCacheCollection = firestore.collection(
 | 
				
			||||||
 | 
					    `private-users/${user.id}/cache`
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					  const [recommendationScores, lastViewedTime] = await Promise.all([
 | 
				
			||||||
 | 
					    getValue<{ [contractId: string]: number }>(
 | 
				
			||||||
 | 
					      userCacheCollection.doc('contractScores')
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    getValue<{ [contractId: string]: number }>(
 | 
				
			||||||
 | 
					      userCacheCollection.doc('lastViewTime')
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					  ]).then((dicts) => dicts.map((dict) => dict ?? {}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const averageRecScore =
 | 
				
			||||||
 | 
					    1 +
 | 
				
			||||||
 | 
					    _.sumBy(
 | 
				
			||||||
 | 
					      contracts.filter((c) => recommendationScores[c.id] !== undefined),
 | 
				
			||||||
 | 
					      (c) => recommendationScores[c.id]
 | 
				
			||||||
 | 
					    ) /
 | 
				
			||||||
 | 
					      (contracts.length + 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const scoredContracts = contracts.map((contract) => {
 | 
				
			||||||
 | 
					    const score = scoreContract(
 | 
				
			||||||
 | 
					      contract,
 | 
				
			||||||
 | 
					      recommendationScores[contract.id] ?? averageRecScore,
 | 
				
			||||||
 | 
					      lastViewedTime[contract.id]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    return [contract, score] as [Contract, number]
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const sortedContracts = _.sortBy(
 | 
				
			||||||
 | 
					    scoredContracts,
 | 
				
			||||||
 | 
					    ([_, score]) => score
 | 
				
			||||||
 | 
					  ).reverse()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log(sortedContracts.map(([c, score]) => c.question + ': ' + score))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const feedContracts = sortedContracts
 | 
				
			||||||
 | 
					    .slice(0, MAX_FEED_CONTRACTS)
 | 
				
			||||||
 | 
					    .map(([c]) => c)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const feed = await Promise.all(
 | 
				
			||||||
 | 
					    feedContracts.map((contract) => getRecentBetsAndComments(contract))
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await userCacheCollection.doc('feed').set(feed)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function scoreContract(
 | 
					function scoreContract(
 | 
				
			||||||
  contract: Contract,
 | 
					  contract: Contract,
 | 
				
			||||||
  recommendationScore: number,
 | 
					  recommendationScore: number,
 | 
				
			||||||
| 
						 | 
					@ -171,12 +170,16 @@ async function getRecentBetsAndComments(contract: Contract) {
 | 
				
			||||||
      contractDoc
 | 
					      contractDoc
 | 
				
			||||||
        .collection('bets')
 | 
					        .collection('bets')
 | 
				
			||||||
        .where('createdTime', '>', Date.now() - DAY_MS)
 | 
					        .where('createdTime', '>', Date.now() - DAY_MS)
 | 
				
			||||||
 | 
					        .orderBy('createdTime', 'desc')
 | 
				
			||||||
 | 
					        .limit(1)
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getValues<Comment>(
 | 
					    getValues<Comment>(
 | 
				
			||||||
      contractDoc
 | 
					      contractDoc
 | 
				
			||||||
        .collection('comments')
 | 
					        .collection('comments')
 | 
				
			||||||
        .where('createdTime', '>', Date.now() - 3 * DAY_MS)
 | 
					        .where('createdTime', '>', Date.now() - 3 * DAY_MS)
 | 
				
			||||||
 | 
					        .orderBy('createdTime', 'desc')
 | 
				
			||||||
 | 
					        .limit(3)
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
  ])
 | 
					  ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,9 +5,10 @@ import { Comment } from '../../common/comment'
 | 
				
			||||||
import { Contract } from '../../common/contract'
 | 
					import { Contract } from '../../common/contract'
 | 
				
			||||||
import { useTimeSinceFirstRender } from './use-time-since-first-render'
 | 
					import { useTimeSinceFirstRender } from './use-time-since-first-render'
 | 
				
			||||||
import { trackLatency } from '../lib/firebase/tracking'
 | 
					import { trackLatency } from '../lib/firebase/tracking'
 | 
				
			||||||
import { getFeed } from '../lib/firebase/api-call'
 | 
					import { User } from '../../common/user'
 | 
				
			||||||
 | 
					import { getUserFeed } from '../lib/firebase/users'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useAlgoFeed = () => {
 | 
					export const useAlgoFeed = (user: User | null | undefined) => {
 | 
				
			||||||
  const [feed, setFeed] = useState<
 | 
					  const [feed, setFeed] = useState<
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      contract: Contract
 | 
					      contract: Contract
 | 
				
			||||||
| 
						 | 
					@ -19,14 +20,15 @@ export const useAlgoFeed = () => {
 | 
				
			||||||
  const getTime = useTimeSinceFirstRender()
 | 
					  const getTime = useTimeSinceFirstRender()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    getFeed().then(({ data }) => {
 | 
					    if (user) {
 | 
				
			||||||
      console.log('got data', data)
 | 
					      getUserFeed(user.id).then((feed) => {
 | 
				
			||||||
      setFeed(data.feed)
 | 
					        setFeed(feed)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      trackLatency('feed', getTime())
 | 
					        trackLatency('feed', getTime())
 | 
				
			||||||
      console.log('feed load time', getTime())
 | 
					        console.log('feed load time', getTime())
 | 
				
			||||||
    })
 | 
					      })
 | 
				
			||||||
  }, [getTime])
 | 
					    }
 | 
				
			||||||
 | 
					  }, [user, getTime])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return feed
 | 
					  return feed
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,4 @@
 | 
				
			||||||
import { httpsCallable } from 'firebase/functions'
 | 
					import { httpsCallable } from 'firebase/functions'
 | 
				
			||||||
import { Bet } from '../../../common/bet'
 | 
					 | 
				
			||||||
import { Comment } from '../../../common/comment'
 | 
					 | 
				
			||||||
import { Contract } from '../../../common/contract'
 | 
					 | 
				
			||||||
import { Fold } from '../../../common/fold'
 | 
					import { Fold } from '../../../common/fold'
 | 
				
			||||||
import { User } from '../../../common/user'
 | 
					import { User } from '../../../common/user'
 | 
				
			||||||
import { randomString } from '../../../common/util/random'
 | 
					import { randomString } from '../../../common/util/random'
 | 
				
			||||||
| 
						 | 
					@ -74,15 +71,3 @@ export const addLiquidity = (data: { amount: number; contractId: string }) => {
 | 
				
			||||||
    .then((r) => r.data as { status: string })
 | 
					    .then((r) => r.data as { status: string })
 | 
				
			||||||
    .catch((e) => ({ status: 'error', message: e.message }))
 | 
					    .catch((e) => ({ status: 'error', message: e.message }))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getFeed = cloudFunction<
 | 
					 | 
				
			||||||
  undefined,
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    status: 'error' | 'success'
 | 
					 | 
				
			||||||
    feed: {
 | 
					 | 
				
			||||||
      contract: Contract
 | 
					 | 
				
			||||||
      recentBets: Bet[]
 | 
					 | 
				
			||||||
      recentComments: Comment[]
 | 
					 | 
				
			||||||
    }[]
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
>('getFeed')
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,8 +23,11 @@ import _ from 'lodash'
 | 
				
			||||||
import { app } from './init'
 | 
					import { app } from './init'
 | 
				
			||||||
import { PrivateUser, User } from '../../../common/user'
 | 
					import { PrivateUser, User } from '../../../common/user'
 | 
				
			||||||
import { createUser } from './api-call'
 | 
					import { createUser } from './api-call'
 | 
				
			||||||
import { getValues, listenForValue, listenForValues } from './utils'
 | 
					import { getValue, getValues, listenForValue, listenForValues } from './utils'
 | 
				
			||||||
import { DAY_MS } from '../../../common/util/time'
 | 
					import { DAY_MS } from '../../../common/util/time'
 | 
				
			||||||
 | 
					import { Contract } from './contracts'
 | 
				
			||||||
 | 
					import { Bet } from './bets'
 | 
				
			||||||
 | 
					import { Comment } from './comments'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type { User }
 | 
					export type { User }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -207,3 +210,15 @@ export async function getDailyNewUsers(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return usersByDay
 | 
					  return usersByDay
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getUserFeed(userId: string) {
 | 
				
			||||||
 | 
					  const feedDoc = doc(db, 'private-users', userId, 'cache', 'feed')
 | 
				
			||||||
 | 
					  const userFeed = await getValue<{
 | 
				
			||||||
 | 
					    feed: {
 | 
				
			||||||
 | 
					      contract: Contract
 | 
				
			||||||
 | 
					      recentBets: Bet[]
 | 
				
			||||||
 | 
					      recentComments: Comment[]
 | 
				
			||||||
 | 
					    }[]
 | 
				
			||||||
 | 
					  }>(feedDoc)
 | 
				
			||||||
 | 
					  return userFeed?.feed ?? []
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ import { ContractPageContent } from './[username]/[contractSlug]'
 | 
				
			||||||
const Home = () => {
 | 
					const Home = () => {
 | 
				
			||||||
  const user = useUser()
 | 
					  const user = useUser()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const feed = useAlgoFeed()
 | 
					  const feed = useAlgoFeed(user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const router = useRouter()
 | 
					  const router = useRouter()
 | 
				
			||||||
  const { u: username, s: slug } = router.query
 | 
					  const { u: username, s: slug } = router.query
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user