Skip to content

Commit

Permalink
[Fix] Refactor to playstreak sync state (#19)
Browse files Browse the repository at this point in the history
* refactor to playstreak sync state, add final sync

* fix lint

* export state

* pass in query client

* run pnpm i

* refactor to shared fxn for get query keys

* export query key fxns

* tech(quest-state): add appQueryClient (#21)

* tech(quest-state): add appQueryClient

* tech(quest-state): make appQueryClient optional

* pretty

---------

Co-authored-by: Elio Briceño <[email protected]>
  • Loading branch information
BrettCleary and eliobricenov authored Oct 16, 2024
1 parent 9c325d9 commit 648cf4c
Show file tree
Hide file tree
Showing 10 changed files with 1,952 additions and 1,579 deletions.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperplay/quests-ui",
"version": "0.0.25",
"version": "0.0.26",
"description": "",
"main": "index.js",
"scripts": {
Expand All @@ -27,6 +27,7 @@
"@hyperplay/chains": "^0.3.0",
"@hyperplay/ui": "^1.8.0",
"@hyperplay/utils": "^0.2.6",
"@tanstack/query-core": "^5.59.13",
"@tanstack/react-query": "^5.51.23",
"@types/react": "^18.3.3",
"@typescript-eslint/eslint-plugin": "^8.1.0",
Expand All @@ -38,6 +39,7 @@
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.35.0",
"i18next": "^23.12.3",
"mobx": "^6.13.3",
"prettier": "^3.3.3",
"react": "^18.3.1",
"react-i18next": "^15.0.1",
Expand All @@ -51,8 +53,10 @@
"@hyperplay/chains": "^0.3.0",
"@hyperplay/ui": "^1.7.17",
"@hyperplay/utils": "^0.2.3",
"@tanstack/query-core": "^5.59.13",
"@tanstack/react-query": "^5.51.23",
"i18next": "^23.12.3",
"mobx": "^6.13.3",
"react": "^18.3.1",
"react-i18next": "^15.0.1",
"viem": "^2.19.4",
Expand Down
3,283 changes: 1,773 additions & 1,510 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

16 changes: 1 addition & 15 deletions src/components/QuestDetailsWrapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
import { useGetRewards } from '../../hooks/useGetRewards'
import { chainMap, parseChainMetadataToViemChain } from '@hyperplay/chains'
import { InfoAlertProps } from '@hyperplay/ui/dist/components/AlertCard'
import { useSyncPlaySession } from '../../hooks/useSyncInterval'
import { useTrackQuestViewed } from '../../hooks/useTrackQuestViewed'
import { ConfirmClaimModal } from '../ConfirmClaimModal'
import { getRewardClaimGasEstimation } from '@/helpers/getRewardClaimGasEstimation'
Expand Down Expand Up @@ -84,8 +83,6 @@ export interface QuestDetailsWrapperProps {
sessionEmail?: string
checkG7ConnectionStatus: () => Promise<boolean>
isQuestsPage?: boolean
minimumRequiredPlayTimeInSeconds?: number
currentPlayTimeInSeconds?: number
}

export function QuestDetailsWrapper({
Expand Down Expand Up @@ -116,9 +113,7 @@ export function QuestDetailsWrapper({
checkG7ConnectionStatus,
isQuestsPage,
syncPlayStreakWithExternalSource,
questsWithExternalPlayStreakSync,
minimumRequiredPlayTimeInSeconds,
currentPlayTimeInSeconds
questsWithExternalPlayStreakSync
}: QuestDetailsWrapperProps) {
const rewardTypeClaimEnabled = flags.rewardTypeClaimEnabled
const {
Expand Down Expand Up @@ -276,15 +271,6 @@ export function QuestDetailsWrapper({
loading: val.isLoading || val.isFetching
})) ?? []

useSyncPlaySession({
projectId,
invalidateQuery: questPlayStreakResult.invalidateQuery,
syncPlaySession,
minimumRequiredPlayTimeInSeconds,
currentPlayTimeInSeconds,
runner: questMeta?.quest_external_game?.runner ?? 'hyperplay'
})

const [collapseIsOpen, setCollapseIsOpen] = useState(false)

const hasMetStreak =
Expand Down
11 changes: 11 additions & 0 deletions src/helpers/getQueryKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function getGetQuestQueryKey(questId: number | null) {
return ['getQuest', questId]
}

export function getGetUserPlayStreakQueryKey(questId: number | null) {
return ['getUserPlayStreak', questId]
}

export function getSyncPlaysessionQueryKey(projectId: string) {
return ['syncPlaysession', projectId]
}
3 changes: 2 additions & 1 deletion src/hooks/useGetQuest.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getGetQuestQueryKey } from '@/helpers/getQueryKeys'
import { Quest } from '@hyperplay/utils'
import { useQuery, useQueryClient } from '@tanstack/react-query'

Expand All @@ -6,7 +7,7 @@ export default function useGetQuest(
getQuest: (questId: number) => Promise<Quest>
) {
const queryClient = useQueryClient()
const queryKey = `getQuest:${questId}`
const queryKey = getGetQuestQueryKey(questId)
const query = useQuery({
queryKey: [queryKey],
queryFn: async () => {
Expand Down
1 change: 0 additions & 1 deletion src/hooks/useGetRewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ export function useGetRewards(
rewards.push(questReward_i)
}
} else {
console.log('pushing this reward ', reward_i)
const questReward_i: QuestReward = {
title: reward_i.name,
imageUrl: reward_i.image_url,
Expand Down
8 changes: 4 additions & 4 deletions src/hooks/useGetUserPlayStreak.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getGetUserPlayStreakQueryKey } from '@/helpers/getQueryKeys'
import { UserPlayStreak } from '@hyperplay/utils'
import { useQuery, useQueryClient } from '@tanstack/react-query'

Expand All @@ -6,9 +7,9 @@ export default function useGetUserPlayStreak(
getUserPlayStreak: (questId: number) => Promise<UserPlayStreak>
) {
const queryClient = useQueryClient()
const queryKey = `getUserPlayStreak:${questId}`
const queryKey = getGetUserPlayStreakQueryKey(questId)
const query = useQuery({
queryKey: [queryKey],
queryKey,
queryFn: async () => {
if (questId === null) {
return null
Expand All @@ -24,7 +25,6 @@ export default function useGetUserPlayStreak(
return {
data: query,
isLoading: query.isLoading || query.isFetching,
invalidateQuery: async () =>
queryClient.invalidateQueries({ queryKey: [queryKey] })
invalidateQuery: async () => queryClient.invalidateQueries({ queryKey })
}
}
47 changes: 0 additions & 47 deletions src/hooks/useSyncInterval.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export type { TrackEventFn } from './types/analytics'
export * from './hooks/useCheckG7ConnectionStatus'
export * from './hooks/useSyncPlayStreakWithExternalSource'
export * from './helpers/getPlaystreakArgsFromQuestData'
export { default as questPlayStreakSyncState } from './state/QuestPlayStreakSyncState'
export * from './helpers/getQueryKeys'
154 changes: 154 additions & 0 deletions src/state/QuestPlayStreakSyncState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Quest, Runner, UserPlayStreak } from '@hyperplay/utils'
import { makeAutoObservable } from 'mobx'
import { QueryClient } from '@tanstack/query-core'
import { resetSessionStartedTime } from '@/helpers/getPlaystreakArgsFromQuestData'
import {
getGetQuestQueryKey,
getGetUserPlayStreakQueryKey,
getSyncPlaysessionQueryKey
} from '@/helpers/getQueryKeys'

class QuestPlayStreakSyncState {
// @ts-expect-error not assigned in constructor since this is a singleton
getQuests: (projectId?: string | undefined) => Promise<Quest[]>
// @ts-expect-error not assigned in constructor since this is a singleton
getQuest: (questId: number) => Promise<Quest>
// @ts-expect-error not assigned in constructor since this is a singleton
getUserPlayStreak: (questId: number) => Promise<UserPlayStreak>
// @ts-expect-error not assigned in constructor since this is a singleton
syncPlaySession: (appName: string, runner: Runner) => Promise<void>

appQueryClient?: QueryClient

projectSyncData: Record<
string,
{
syncTimers: NodeJS.Timeout[]
intervalTimers: NodeJS.Timer[]
}
> = {}

queryClient: QueryClient | undefined = undefined

intervalSyncTick = 60000

constructor() {
this.queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // Cache the data for 5 minutes
retry: 1 // Retry failed request once
}
}
})
makeAutoObservable(this)
}

init({
getQuests,
getQuest,
getUserPlayStreak,
syncPlaySession,
appQueryClient
}: {
getQuests: (projectId?: string | undefined) => Promise<Quest[]>
getQuest: (questId: number) => Promise<Quest>
getUserPlayStreak: (questId: number) => Promise<UserPlayStreak>
syncPlaySession: (appName: string, runner: Runner) => Promise<void>
appQueryClient?: QueryClient
}) {
this.getQuests = getQuests
this.getQuest = getQuest
this.getUserPlayStreak = getUserPlayStreak
this.syncPlaySession = syncPlaySession
this.appQueryClient = appQueryClient
}

async keepProjectQuestsInSync(projectId: string) {
if (this.queryClient === undefined) {
throw 'must call init on QuestPlayStreakSyncState first'
}
const quests = await this.getQuests(projectId)
for (const quest of quests) {
try {
// get quest
const questMeta = await this.queryClient.fetchQuery({
queryKey: getGetQuestQueryKey(quest.id),
queryFn: async () => this.getQuest(quest.id)
})
// get user playstreak
const userPlayStreakData = await this.queryClient.fetchQuery({
queryKey: getGetUserPlayStreakQueryKey(quest.id),
queryFn: async () => this.getUserPlayStreak(quest.id)
})

if (!Object.hasOwn(this.projectSyncData, projectId)) {
this.projectSyncData[projectId] = {
syncTimers: [],
intervalTimers: []
}
}

const syncThisProjectMutation = async () =>
this.queryClient?.fetchQuery({
queryKey: getSyncPlaysessionQueryKey(projectId),
queryFn: async () => {
this.syncPlaySession(
projectId,
questMeta.quest_external_game?.runner ?? 'hyperplay'
)
resetSessionStartedTime()
// all quest user playstreak data needs to be refetched after playsession sync
for (const questToInvalidate of quests) {
const queryKey = getGetUserPlayStreakQueryKey(
questToInvalidate.id
)
this.appQueryClient?.invalidateQueries({ queryKey })
}
}
})

// set timeout for when we meet the min time
const currentPlayTimeInSeconds =
userPlayStreakData.accumulated_playtime_today_in_seconds
const minimumRequiredPlayTimeInSeconds =
questMeta?.eligibility?.play_streak.minimum_session_time_in_seconds
if (
minimumRequiredPlayTimeInSeconds &&
currentPlayTimeInSeconds &&
currentPlayTimeInSeconds < minimumRequiredPlayTimeInSeconds
) {
console.log('setting timeout for post mutation')
const finalSyncTimer = setTimeout(
syncThisProjectMutation,
minimumRequiredPlayTimeInSeconds - currentPlayTimeInSeconds
)
this.projectSyncData[projectId].syncTimers.push(finalSyncTimer)
}

const intervalId = setInterval(
syncThisProjectMutation,
this.intervalSyncTick
)
this.projectSyncData[projectId].syncTimers.push(intervalId)
} catch (err) {
console.error(`Error while setting up playstreak sync: ${err}`)
}
}
}

clearAllTimers() {
for (const projectId of Object.keys(this.projectSyncData)) {
for (const syncTimer of this.projectSyncData[projectId].syncTimers) {
clearTimeout(syncTimer)
}

for (const intervalTimer of this.projectSyncData[projectId]
.intervalTimers) {
clearInterval(intervalTimer)
}
}
}
}

export default new QuestPlayStreakSyncState()

0 comments on commit 648cf4c

Please sign in to comment.