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 { import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
@ -97,6 +97,22 @@ export function usePostFeedQuery(
const feedTuners = useFeedTuners(feedDesc) const feedTuners = useFeedTuners(feedDesc)
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const enabled = opts?.enabled !== false && Boolean(moderationOpts) 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< const query = useInfiniteQuery<
FeedPageUnselected, FeedPageUnselected,
@ -147,69 +163,116 @@ export function usePostFeedQuery(
: undefined, : undefined,
select: useCallback( select: useCallback(
(data: InfiniteData<FeedPageUnselected, RQPageParam>) => { (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 NoopFeedTuner()
: new FeedTuner(feedTuners) : 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 // Keep track of the last run and whether we can reuse
for (let i = 0; i < slice.items.length; i++) { // some already selected pages from there.
if ( let reusedPages = []
moderations[i]?.content.filter && if (lastRun.current) {
slice.items[i].post.author.did !== opts?.ignoreFilterFor const {
) { data: lastData,
return undefined args: lastArgs,
} result: lastResult,
} } = lastRun.current
let canReuse = true
return { for (let key in selectArgs) {
_reactKey: slice._reactKey, if (selectArgs.hasOwnProperty(key)) {
rootUri: slice.rootItem.post.uri, if ((selectArgs as any)[key] !== (lastArgs as any)[key]) {
isThread: // Can't do reuse anything if any input has changed.
slice.items.length > 1 && canReuse = false
slice.items.every( break
item => }
item.post.author.did === slice.items[0].post.author.did, }
), }
items: slice.items if (canReuse) {
.map((item, i) => { for (let i = 0; i < data.pages.length; i++) {
if ( if (data.pages[i] && lastData.pages[i] === data.pages[i]) {
AppBskyFeedPost.isRecord(item.post.record) && reusedPages.push(lastResult.pages[i])
AppBskyFeedPost.validateRecord(item.post.record).success // Keep the tuner in sync so that the end result is deterministic.
) { tuner.tune(lastData.pages[i].feed)
return { continue
_reactKey: `${slice._reactKey}-${i}`, }
uri: item.post.uri, // Stop as soon as pages stop matching up.
post: item.post, break
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[],
})),
} }
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. */],
), ),
}) })