diff options
author | dan <dan.abramov@gmail.com> | 2023-11-21 22:42:30 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-21 22:42:30 +0000 |
commit | 4c4ba553bdc4029e78eaf2ccf0f9df12e41a1b01 (patch) | |
tree | e97890d72da1fd0a2c10cc501f530a04dae3157a /src/state/cache | |
parent | f18b9b32b0d296c8d19dc06956699f95c0af9be2 (diff) | |
download | voidsky-4c4ba553bdc4029e78eaf2ccf0f9df12e41a1b01.tar.zst |
Shadow refactoring and improvements (#1959)
* Make shadow a type-only concept * Prevent unnecessary init state recalc * Use derived state instead of effects * Batch emitter updates * Use object first seen time instead of dataUpdatedAt * Stop threading dataUpdatedAt through * Use same value consistently
Diffstat (limited to 'src/state/cache')
-rw-r--r-- | src/state/cache/post-shadow.ts | 66 | ||||
-rw-r--r-- | src/state/cache/profile-shadow.ts | 70 | ||||
-rw-r--r-- | src/state/cache/types.ts | 8 |
3 files changed, 82 insertions, 62 deletions
diff --git a/src/state/cache/post-shadow.ts b/src/state/cache/post-shadow.ts index d20f6ebaa..b21bb7129 100644 --- a/src/state/cache/post-shadow.ts +++ b/src/state/cache/post-shadow.ts @@ -1,7 +1,8 @@ -import {useEffect, useState, useMemo, useCallback, useRef} from 'react' +import {useEffect, useState, useMemo, useCallback} from 'react' import EventEmitter from 'eventemitter3' import {AppBskyFeedDefs} from '@atproto/api' -import {Shadow} from './types' +import {batchedUpdates} from '#/lib/batchedUpdates' +import {Shadow, castAsShadow} from './types' export type {Shadow} from './types' const emitter = new EventEmitter() @@ -21,15 +22,36 @@ interface CacheEntry { value: PostShadow } +const firstSeenMap = new WeakMap<AppBskyFeedDefs.PostView, number>() +function getFirstSeenTS(post: AppBskyFeedDefs.PostView): number { + let timeStamp = firstSeenMap.get(post) + if (timeStamp !== undefined) { + return timeStamp + } + timeStamp = Date.now() + firstSeenMap.set(post, timeStamp) + return timeStamp +} + export function usePostShadow( post: AppBskyFeedDefs.PostView, - ifAfterTS: number, ): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE { - const [state, setState] = useState<CacheEntry>({ - ts: Date.now(), + const postSeenTS = getFirstSeenTS(post) + const [state, setState] = useState<CacheEntry>(() => ({ + ts: postSeenTS, value: fromPost(post), - }) - const firstRun = useRef(true) + })) + + const [prevPost, setPrevPost] = useState(post) + if (post !== prevPost) { + // if we got a new prop, assume it's fresher + // than whatever shadow state we accumulated + setPrevPost(post) + setState({ + ts: postSeenTS, + value: fromPost(post), + }) + } const onUpdate = useCallback( (value: Partial<PostShadow>) => { @@ -46,30 +68,17 @@ export function usePostShadow( } }, [post.uri, onUpdate]) - // react to post updates - useEffect(() => { - // dont fire on first run to avoid needless re-renders - if (!firstRun.current) { - setState({ts: Date.now(), value: fromPost(post)}) - } - firstRun.current = false - }, [post]) - return useMemo(() => { - return state.ts > ifAfterTS + return state.ts > postSeenTS ? mergeShadow(post, state.value) - : {...post, isShadowed: true} - }, [post, state, ifAfterTS]) + : castAsShadow(post) + }, [post, state, postSeenTS]) } export function updatePostShadow(uri: string, value: Partial<PostShadow>) { - emitter.emit(uri, value) -} - -export function isPostShadowed( - v: AppBskyFeedDefs.PostView | Shadow<AppBskyFeedDefs.PostView>, -): v is Shadow<AppBskyFeedDefs.PostView> { - return 'isShadowed' in v && !!v.isShadowed + batchedUpdates(() => { + emitter.emit(uri, value) + }) } function fromPost(post: AppBskyFeedDefs.PostView): PostShadow { @@ -89,7 +98,7 @@ function mergeShadow( if (shadow.isDeleted) { return POST_TOMBSTONE } - return { + return castAsShadow({ ...post, likeCount: shadow.likeCount, repostCount: shadow.repostCount, @@ -98,6 +107,5 @@ function mergeShadow( like: shadow.likeUri, repost: shadow.repostUri, }, - isShadowed: true, - } + }) } diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts index 5323effaf..6ebd39132 100644 --- a/src/state/cache/profile-shadow.ts +++ b/src/state/cache/profile-shadow.ts @@ -1,7 +1,8 @@ -import {useEffect, useState, useMemo, useCallback, useRef} from 'react' +import {useEffect, useState, useMemo, useCallback} from 'react' import EventEmitter from 'eventemitter3' import {AppBskyActorDefs} from '@atproto/api' -import {Shadow} from './types' +import {batchedUpdates} from '#/lib/batchedUpdates' +import {Shadow, castAsShadow} from './types' export type {Shadow} from './types' const emitter = new EventEmitter() @@ -22,15 +23,34 @@ type ProfileView = | AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileViewDetailed -export function useProfileShadow( - profile: ProfileView, - ifAfterTS: number, -): Shadow<ProfileView> { - const [state, setState] = useState<CacheEntry>({ - ts: Date.now(), +const firstSeenMap = new WeakMap<ProfileView, number>() +function getFirstSeenTS(profile: ProfileView): number { + let timeStamp = firstSeenMap.get(profile) + if (timeStamp !== undefined) { + return timeStamp + } + timeStamp = Date.now() + firstSeenMap.set(profile, timeStamp) + return timeStamp +} + +export function useProfileShadow(profile: ProfileView): Shadow<ProfileView> { + const profileSeenTS = getFirstSeenTS(profile) + const [state, setState] = useState<CacheEntry>(() => ({ + ts: profileSeenTS, value: fromProfile(profile), - }) - const firstRun = useRef(true) + })) + + const [prevProfile, setPrevProfile] = useState(profile) + if (profile !== prevProfile) { + // if we got a new prop, assume it's fresher + // than whatever shadow state we accumulated + setPrevProfile(profile) + setState({ + ts: profileSeenTS, + value: fromProfile(profile), + }) + } const onUpdate = useCallback( (value: Partial<ProfileShadow>) => { @@ -47,33 +67,20 @@ export function useProfileShadow( } }, [profile.did, onUpdate]) - // react to profile updates - useEffect(() => { - // dont fire on first run to avoid needless re-renders - if (!firstRun.current) { - setState({ts: Date.now(), value: fromProfile(profile)}) - } - firstRun.current = false - }, [profile]) - return useMemo(() => { - return state.ts > ifAfterTS + return state.ts > profileSeenTS ? mergeShadow(profile, state.value) - : {...profile, isShadowed: true} - }, [profile, state, ifAfterTS]) + : castAsShadow(profile) + }, [profile, state, profileSeenTS]) } export function updateProfileShadow( uri: string, value: Partial<ProfileShadow>, ) { - emitter.emit(uri, value) -} - -export function isProfileShadowed<T extends ProfileView>( - v: T | Shadow<T>, -): v is Shadow<T> { - return 'isShadowed' in v && !!v.isShadowed + batchedUpdates(() => { + emitter.emit(uri, value) + }) } function fromProfile(profile: ProfileView): ProfileShadow { @@ -88,7 +95,7 @@ function mergeShadow( profile: ProfileView, shadow: ProfileShadow, ): Shadow<ProfileView> { - return { + return castAsShadow({ ...profile, viewer: { ...(profile.viewer || {}), @@ -96,6 +103,5 @@ function mergeShadow( muted: shadow.muted, blocking: shadow.blockingUri, }, - isShadowed: true, - } + }) } diff --git a/src/state/cache/types.ts b/src/state/cache/types.ts index 8bfcc867c..055f4167e 100644 --- a/src/state/cache/types.ts +++ b/src/state/cache/types.ts @@ -1 +1,7 @@ -export type Shadow<T> = T & {isShadowed: true} +// This isn't a real property, but it prevents T being compatible with Shadow<T>. +declare const shadowTag: unique symbol +export type Shadow<T> = T & {[shadowTag]: true} + +export function castAsShadow<T>(value: T): Shadow<T> { + return value as any as Shadow<T> +} |