survive session-bundle rebuild in chat, feeds, prefs, and age assurance

Review fixes (PR #11182 round 2). Client identity is no longer stable
across token refresh - on web, a cross-tab sync rebuilds the bundle and
disposes the old clients (whose fetch now throws) - so anything that
captured a client at construction time must re-point at the live one:

- MessagesEventBus and Convo get updateClient(); providers sync it via
  effect. No remount, so poll cursors and optimistic pendingMessages
  survive.
- FeedAPI implementations get setClient(); the post-feed queryFn and
  pollLatest re-point page-held apis before fetching (Merge/Home apis
  are stateful across pages, so they cannot be rebuilt per fetch).
- usePreferencesQuery applies fetched labeler dids to the live appview
  client (applyLabelersToClient, factored from session/moderation),
  restoring the old BskyAgent.getPreferences header side effect.
- ageAssurance redirect overlay/dialog polling is mount-only with
  render-synced refs; a client swap mid-poll no longer latches the
  unmounted flag and strands the overlay.
- Convo message-failure classification defers to lex's shouldRetry()
  instead of treating all status-less errors as recoverable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-07-17 15:31:22 +03:00
parent 4c2771eca3
commit 2696104e6d
21 changed files with 201 additions and 60 deletions
+4
View File
@@ -19,6 +19,10 @@ export class AuthorFeedAPI implements FeedAPI {
this._params = feedParams
}
setClient(client: Client) {
this.client = client
}
get params() {
const params = {...this._params}
params.includePins = params.filter === 'posts_and_author_threads'
+4
View File
@@ -37,6 +37,10 @@ export class CustomFeedAPI implements FeedAPI {
this.userInterests = userInterests
}
setClient(client: Client) {
this.client = client
}
async peekLatest(): Promise<app.bsky.feed.defs.FeedViewPost> {
const contentLangs = getContentLanguages().join(',')
const res = await this.client.call(
+4
View File
@@ -11,6 +11,10 @@ export class DemoFeedAPI implements FeedAPI {
this.client = client
}
setClient(client: Client) {
this.client = client
}
// eslint-disable-next-line @typescript-eslint/require-await
async peekLatest(): Promise<app.bsky.feed.defs.FeedViewPost> {
return DEMO_FEED.feed[0]
+4
View File
@@ -10,6 +10,10 @@ export class FollowingFeedAPI implements FeedAPI {
this.client = client
}
setClient(client: Client) {
this.client = client
}
async peekLatest(): Promise<app.bsky.feed.defs.FeedViewPost> {
const res = await this.client.call(app.bsky.feed.getTimeline, {
limit: 1,
+6
View File
@@ -56,6 +56,12 @@ export class HomeFeedAPI implements FeedAPI {
this.userInterests = userInterests
}
setClient(client: Client) {
this.client = client
this.following.setClient(client)
this.discover.setClient(client)
}
reset() {
this.following = new FollowingFeedAPI({client: this.client})
this.discover = new CustomFeedAPI({
+4
View File
@@ -18,6 +18,10 @@ export class LikesFeedAPI implements FeedAPI {
this.params = feedParams
}
setClient(client: Client) {
this.client = client
}
async peekLatest(): Promise<app.bsky.feed.defs.FeedViewPost> {
const res = await this.client.call(app.bsky.feed.getActorLikes, {
...this.params,
+4
View File
@@ -18,6 +18,10 @@ export class ListFeedAPI implements FeedAPI {
this.params = feedParams
}
setClient(client: Client) {
this.client = client
}
async peekLatest(): Promise<app.bsky.feed.defs.FeedViewPost> {
const res = await this.client.call(app.bsky.feed.getListFeed, {
...this.params,
+12
View File
@@ -64,6 +64,14 @@ export class MergeFeedAPI implements FeedAPI {
})
}
setClient(client: Client) {
this.client = client
this.following.setClient(client)
for (const feed of this.customFeeds) {
feed.setClient(client)
}
}
reset() {
this.following = new MergeFeedSource_Following({
client: this.client,
@@ -203,6 +211,10 @@ class MergeFeedSource {
this.feedTuners = feedTuners
}
setClient(client: Client) {
this.client = client
}
get numReady() {
return this.queue.length
}
+4
View File
@@ -27,6 +27,10 @@ export class PostListFeedAPI implements FeedAPI {
}
}
setClient(client: Client) {
this.client = client
}
async peekLatest(): Promise<app.bsky.feed.defs.FeedViewPost> {
if (this.peek) return this.peek
throw new Error('Has not fetched yet')
+9
View File
@@ -1,3 +1,5 @@
import {type Client} from '@atproto/lex-client'
import {type app} from '#/lexicons'
export interface FeedAPIResponse {
@@ -6,6 +8,13 @@ export interface FeedAPIResponse {
}
export interface FeedAPI {
/**
* Swap in a fresh client. Feed pages retain their FeedAPI across paginations,
* so the client captured at construction goes stale after a session-bundle
* rebuild (web cross-tab token sync). Callers re-point the api at the current
* client before each fetch/peek so a disposed client is never used.
*/
setClient(client: Client): void
peekLatest(): Promise<app.bsky.feed.defs.FeedViewPost>
fetch({
cursor,