Compare commits

...

1 Commits

Author SHA1 Message Date
Eric Bailey 4f0421688e POC cache mutator 2025-06-14 13:39:59 -05:00
7 changed files with 218 additions and 5 deletions
@@ -162,7 +162,7 @@ function ThreadItemAnchorParentReplyLine({isRoot}: {isRoot: boolean}) {
const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
item,
isRoot,
postShadow,
// postShadow,
onPostSuccess,
threadgateRecord,
postSource,
@@ -488,7 +488,8 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
<FeedFeedbackProvider value={feedFeedback}>
<PostControls
big
post={postShadow}
// @ts-expect-error TODO no more shadow here
post={post}
record={record}
richText={richText}
onPressReply={onPressReply}
+22
View File
@@ -0,0 +1,22 @@
import {type QueryClient} from '@tanstack/react-query'
import {mutatePost} from '#/state/queries/cache/mutations'
import {type PostMutations} from '#/state/queries/cache/types'
import {applyPostCacheMutator as applyPostCacheMutator_usePostThread} from '#/state/queries/usePostThread/queryCache'
/**
* Applies mutations to a post (identified by URI) in all active query caches
*/
export function applyPostCacheMutations(
qc: QueryClient,
uri: string,
mutations: Partial<PostMutations>,
) {
applyPostCacheMutator_usePostThread({
qc,
uri,
mutator: post => mutatePost(post, mutations),
})
// ... add all other queries below
}
+72
View File
@@ -0,0 +1,72 @@
import {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
type AppBskyFeedDefs,
} from '@atproto/api'
import {type PostMutations} from '#/state/queries/cache/types'
import {DELETED_POST} from '#/state/queries/cache/util'
/**
* Applies mutations to a post in the cache. If the post is deleted, returns a
* symbol to force individual cache mutators to handle the unknown value
* appropriately.
*/
export function mutatePost(
post: AppBskyFeedDefs.PostView,
mutations: Partial<PostMutations>,
): AppBskyFeedDefs.PostView | typeof DELETED_POST {
if (mutations.isDeleted) {
return DELETED_POST
}
let likeCount = post.likeCount ?? 0
if ('likeUri' in mutations) {
const wasLiked = !!post.viewer?.like
const isLiked = !!mutations.likeUri
if (wasLiked && !isLiked) {
likeCount--
} else if (!wasLiked && isLiked) {
likeCount++
}
likeCount = Math.max(0, likeCount)
}
let repostCount = post.repostCount ?? 0
if ('repostUri' in mutations) {
const wasReposted = !!post.viewer?.repost
const isReposted = !!mutations.repostUri
if (wasReposted && !isReposted) {
repostCount--
} else if (!wasReposted && isReposted) {
repostCount++
}
repostCount = Math.max(0, repostCount)
}
let embed: typeof post.embed
if ('embed' in mutations) {
if (
(AppBskyEmbedRecord.isView(post.embed) &&
AppBskyEmbedRecord.isView(mutations.embed)) ||
(AppBskyEmbedRecordWithMedia.isView(post.embed) &&
AppBskyEmbedRecordWithMedia.isView(mutations.embed))
) {
embed = mutations.embed
}
}
return {
...post,
embed: embed || post.embed,
likeCount: likeCount,
repostCount: repostCount,
viewer: {
...(post.viewer || {}),
like: 'likeUri' in mutations ? mutations.likeUri : post.viewer?.like,
repost:
'repostUri' in mutations ? mutations.repostUri : post.viewer?.repost,
pinned: 'pinned' in mutations ? mutations.pinned : post.viewer?.pinned,
},
}
}
+39
View File
@@ -0,0 +1,39 @@
import {
type AppBskyEmbedRecord,
type AppBskyEmbedRecordWithMedia,
type AppBskyFeedDefs,
} from '@atproto/api'
import {type QueryClient} from '@tanstack/react-query'
import {type DELETED_POST} from '#/state/queries/cache/util'
/**
* Available mutations for a post.
*/
export type PostMutations = {
likeUri: string | undefined
repostUri: string | undefined
isDeleted: boolean
embed: AppBskyEmbedRecord.View | AppBskyEmbedRecordWithMedia.View | undefined
pinned: boolean
}
/**
* Applies mutations to a post.
*/
export type ApplyPostCacheMutator = (params: {
/**
* The query client to use for cache updates.
*/
qc: QueryClient
/**
* The URI of the post to mutate.
*/
uri: string
/**
* The mutation function that applies changes to the post.
*/
mutator: (
post: AppBskyFeedDefs.PostView,
) => AppBskyFeedDefs.PostView | typeof DELETED_POST
}) => void
+34
View File
@@ -0,0 +1,34 @@
import {AtUri} from '@atproto/api'
import {type ApplyPostCacheMutator} from '#/state/queries/cache/types'
/**
* Symbol used to indicate that a value has been deleted.
*/
export const DELETED_POST = Symbol('DELETED_POST')
/**
* Sugar for creating a post cache mutator, for use alongside the definitions
* for all post queries.
*/
export function createApplyPostCacheMutator(
applyPostCacheMutator: ApplyPostCacheMutator,
) {
return applyPostCacheMutator
}
/**
* Checks if a source URI matches a given URI, or the same URI with handle in
* place of a DID.
*/
export function uriMatches(
sourceUri: string,
matchUri: string,
matchHandle?: string,
) {
const source = new AtUri(sourceUri)
if (source.host.startsWith('did:')) return source.toString() === matchUri
if (!matchHandle) return false
source.host = matchHandle
return source.toString() === matchUri
}
+4 -3
View File
@@ -7,6 +7,7 @@ import {type LogEvents, toClout} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {updatePostShadow} from '#/state/cache/post-shadow'
import {type Shadow} from '#/state/cache/types'
import {applyPostCacheMutations} from '#/state/queries/cache'
import {useAgent, useSession} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes'
@@ -135,7 +136,7 @@ export function usePostLikeMutationQueue(
},
onSuccess(finalLikeUri) {
// finalize
updatePostShadow(queryClient, postUri, {
applyPostCacheMutations(queryClient, postUri, {
likeUri: finalLikeUri,
})
},
@@ -143,7 +144,7 @@ export function usePostLikeMutationQueue(
const queueLike = useCallback(() => {
// optimistically update
updatePostShadow(queryClient, postUri, {
applyPostCacheMutations(queryClient, postUri, {
likeUri: 'pending',
})
return queueToggle(true)
@@ -151,7 +152,7 @@ export function usePostLikeMutationQueue(
const queueUnlike = useCallback(() => {
// optimistically update
updatePostShadow(queryClient, postUri, {
applyPostCacheMutations(queryClient, postUri, {
likeUri: undefined,
})
return queueToggle(false)
@@ -9,6 +9,11 @@ import {
} from '@atproto/api'
import {type QueryClient} from '@tanstack/react-query'
import {
createApplyPostCacheMutator,
DELETED_POST,
uriMatches,
} from '#/state/queries/cache/util'
import {findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData} from '#/state/queries/explore-feed-previews'
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed'
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed'
@@ -298,3 +303,42 @@ export function* findAllProfilesInQueryData(
}
}
}
export const applyPostCacheMutator = createApplyPostCacheMutator(
({qc, uri, mutator}) => {
qc.setQueriesData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>(
{queryKey: [postThreadQueryKeyRoot]},
data => {
if (!data) return
const thread = []
for (const item of data.thread) {
if (
AppBskyUnspeccedDefs.isThreadItemPost(item.value) &&
uriMatches(uri, item.value.post.uri, item.value.post.author.handle)
) {
const post = mutator(item.value.post)
if (post !== DELETED_POST) {
thread.push({
...item,
value: {
...item.value,
post,
},
})
}
} else {
thread.push(item)
}
}
return {
...data,
thread,
}
},
)
},
)