[Chat] Sync convo agent with shadow cache (#10827)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
Eric Bailey
2026-06-10 10:16:58 -05:00
committed by GitHub
parent 17ff9af200
commit 8b4a5238be
12 changed files with 195 additions and 58 deletions
+53 -1
View File
@@ -53,6 +53,21 @@ const shadows: WeakMap<
> = new WeakMap()
const emitter = new EventEmitter()
type ShadowUpdateEventPayload = {did: string; shadow: Partial<ProfileShadow>}
/**
* Subscribe to all profile shadow updates, regardless of did. Useful for
* non-React consumers like the Convo agent. Returns an unlisten function.
*/
export function listenProfileShadowUpdate(
listener: (payload: ShadowUpdateEventPayload) => void,
): () => void {
emitter.addListener('shadow-update', listener)
return () => {
emitter.removeListener('shadow-update', listener)
}
}
export function useProfileShadow<
TProfileView extends bsky.profile.AnyProfileView,
>(profile: TProfileView): Shadow<TProfileView> {
@@ -206,10 +221,47 @@ export function updateProfileShadow(
}
batchedUpdates(() => {
emitter.emit(did, value)
emitter.emit('shadow-update', {
did,
shadow: value,
} satisfies ShadowUpdateEventPayload)
})
}
function mergeShadow<TProfileView extends bsky.profile.AnyProfileView>(
/**
* Returns true if merging `shadow` into `profile` would change nothing, i.e.
* `mergeShadow` would be a no-op. Object-valued fields are compared by
* reference, so this can return false negatives - callers may do redundant
* merges, but never skip a real change.
*/
export function isProfileShadowApplied<
TProfileView extends bsky.profile.AnyProfileView,
>(profile: TProfileView, shadow: Partial<ProfileShadow>): boolean {
if ('followingUri' in shadow) {
if (profile.viewer?.following !== shadow.followingUri) return false
}
if ('muted' in shadow) {
if (profile.viewer?.muted !== shadow.muted) return false
}
if ('blockingUri' in shadow) {
if (profile.viewer?.blocking !== shadow.blockingUri) return false
}
if ('activitySubscription' in shadow) {
if (profile.viewer?.activitySubscription !== shadow.activitySubscription) {
return false
}
}
if ('verification' in shadow) {
if (profile.verification !== shadow.verification) return false
}
if ('status' in shadow) {
const current = 'status' in profile ? profile.status : undefined
if (current !== shadow.status) return false
}
return true
}
export function mergeShadow<TProfileView extends bsky.profile.AnyProfileView>(
profile: TProfileView,
shadow: Partial<ProfileShadow>,
): Shadow<TProfileView> {