Add manual per-page memoization to post select (#2146)

zio/stable
dan 2023-12-08 21:57:00 +00:00 committed by GitHub
parent 61fa3d506c
commit 7b686b5592
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 122 additions and 59 deletions

View File

@ -1,4 +1,4 @@
import {useCallback, useEffect} from 'react'
import React, {useCallback, useEffect} from 'react'
import {
AppBskyFeedDefs,
AppBskyFeedPost,
@ -97,6 +97,22 @@ export function usePostFeedQuery(
const feedTuners = useFeedTuners(feedDesc)
const moderationOpts = useModerationOpts()
const enabled = opts?.enabled !== false && Boolean(moderationOpts)
const lastRun = React.useRef<{
data: InfiniteData<FeedPageUnselected>
args: typeof selectArgs
result: InfiniteData<FeedPage>
} | null>(null)
// Make sure this doesn't invalidate unless really needed.
const selectArgs = React.useMemo(
() => ({
feedTuners,
disableTuner: params?.disableTuner,
moderationOpts,
ignoreFilterFor: opts?.ignoreFilterFor,
}),
[feedTuners, params?.disableTuner, moderationOpts, opts?.ignoreFilterFor],
)
const query = useInfiniteQuery<
FeedPageUnselected,
@ -147,69 +163,116 @@ export function usePostFeedQuery(
: undefined,
select: useCallback(
(data: InfiniteData<FeedPageUnselected, RQPageParam>) => {
const tuner = params?.disableTuner
// If the selection depends on some data, that data should
// be included in the selectArgs object and read here.
const {feedTuners, disableTuner, moderationOpts, ignoreFilterFor} =
selectArgs
const tuner = disableTuner
? new NoopFeedTuner()
: new FeedTuner(feedTuners)
return {
pageParams: data.pageParams,
pages: data.pages.map(page => ({
api: page.api,
tuner,
cursor: page.cursor,
slices: tuner
.tune(page.feed)
.map(slice => {
const moderations = slice.items.map(item =>
moderatePost(item.post, moderationOpts!),
)
// apply moderation filter
for (let i = 0; i < slice.items.length; i++) {
if (
moderations[i]?.content.filter &&
slice.items[i].post.author.did !== opts?.ignoreFilterFor
) {
return undefined
}
}
return {
_reactKey: slice._reactKey,
rootUri: slice.rootItem.post.uri,
isThread:
slice.items.length > 1 &&
slice.items.every(
item =>
item.post.author.did === slice.items[0].post.author.did,
),
items: slice.items
.map((item, i) => {
if (
AppBskyFeedPost.isRecord(item.post.record) &&
AppBskyFeedPost.validateRecord(item.post.record).success
) {
return {
_reactKey: `${slice._reactKey}-${i}`,
uri: item.post.uri,
post: item.post,
record: item.post.record,
reason:
i === 0 && slice.source
? slice.source
: item.reason,
moderation: moderations[i],
}
}
return undefined
})
.filter(Boolean) as FeedPostSliceItem[],
}
})
.filter(Boolean) as FeedPostSlice[],
})),
// Keep track of the last run and whether we can reuse
// some already selected pages from there.
let reusedPages = []
if (lastRun.current) {
const {
data: lastData,
args: lastArgs,
result: lastResult,
} = lastRun.current
let canReuse = true
for (let key in selectArgs) {
if (selectArgs.hasOwnProperty(key)) {
if ((selectArgs as any)[key] !== (lastArgs as any)[key]) {
// Can't do reuse anything if any input has changed.
canReuse = false
break
}
}
}
if (canReuse) {
for (let i = 0; i < data.pages.length; i++) {
if (data.pages[i] && lastData.pages[i] === data.pages[i]) {
reusedPages.push(lastResult.pages[i])
// Keep the tuner in sync so that the end result is deterministic.
tuner.tune(lastData.pages[i].feed)
continue
}
// Stop as soon as pages stop matching up.
break
}
}
}
const result = {
pageParams: data.pageParams,
pages: [
...reusedPages,
...data.pages.slice(reusedPages.length).map(page => ({
api: page.api,
tuner,
cursor: page.cursor,
slices: tuner
.tune(page.feed)
.map(slice => {
const moderations = slice.items.map(item =>
moderatePost(item.post, moderationOpts!),
)
// apply moderation filter
for (let i = 0; i < slice.items.length; i++) {
if (
moderations[i]?.content.filter &&
slice.items[i].post.author.did !== ignoreFilterFor
) {
return undefined
}
}
return {
_reactKey: slice._reactKey,
rootUri: slice.rootItem.post.uri,
isThread:
slice.items.length > 1 &&
slice.items.every(
item =>
item.post.author.did ===
slice.items[0].post.author.did,
),
items: slice.items
.map((item, i) => {
if (
AppBskyFeedPost.isRecord(item.post.record) &&
AppBskyFeedPost.validateRecord(item.post.record)
.success
) {
return {
_reactKey: `${slice._reactKey}-${i}`,
uri: item.post.uri,
post: item.post,
record: item.post.record,
reason:
i === 0 && slice.source
? slice.source
: item.reason,
moderation: moderations[i],
}
}
return undefined
})
.filter(Boolean) as FeedPostSliceItem[],
}
})
.filter(Boolean) as FeedPostSlice[],
})),
],
}
// Save for memoization.
lastRun.current = {data, result, args: selectArgs}
return result
},
[feedTuners, params?.disableTuner, moderationOpts, opts?.ignoreFilterFor],
[selectArgs /* Don't change. Everything needs to go into selectArgs. */],
),
})