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 {
|
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. */],
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue