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