about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2023-12-08 21:57:00 +0000
committerGitHub <noreply@github.com>2023-12-08 13:57:00 -0800
commit7b686b5592a8cd01278c5c30e115385ffd676248 (patch)
treef8572822edf8c31586761ecf897f710f8e7c032a /src
parent61fa3d506cc8a9bf5154d9a2b1038023748af518 (diff)
downloadvoidsky-7b686b5592a8cd01278c5c30e115385ffd676248.tar.zst
Add manual per-page memoization to post select (#2146)
Diffstat (limited to 'src')
-rw-r--r--src/state/queries/post-feed.ts177
1 files changed, 120 insertions, 57 deletions
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index 9bd1dacb3..14ffeb0da 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -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 {
+
+        // 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: 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
+          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 {
+                    _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[],
-          })),
+                        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. */],
     ),
   })