Add manual per-page memoization to post select (#2146)
parent
61fa3d506c
commit
7b686b5592
|
@ -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. */],
|
||||
),
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in New Issue