From 4cbefaf4b26e99da480144f78a81f7bdba17c048 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jul 2026 17:59:52 +0300 Subject: [PATCH] roast findings: arm ordering, composer deps, follow-all fallthrough, createdAt clobber Adversarial review (roast) of the branch, 9 path-scoped passes: - cross-tab rebuild armed the new session's hooks before the deferred bundle dispatch, so an 'expired' event fired in that async window would be dropped by the reducer's bundle-identity check; arm now happens in the same callback as the dispatch, and the dispatched account is re-read from the live session (mirrors snapshot-after- prep). - Composer onPressPublish's dep array omitted appviewClient and resolveClients (stale-client hazard after cross-tab rebuild). - follow-all catch blocks (onboarding starter-pack card, starter pack screen) showed the error toast but fell through to dereference the undefined followUris map, throwing and showing a bogus success toast. - contacts createProfileRecord unconditionally overwrote the profile's createdAt on every run; now preserved when already set, matching StepFinished. Co-Authored-By: Claude Fable 5 --- .../contacts/screens/GetContacts.tsx | 4 +++- .../StarterPackCard.tsx | 1 + src/screens/StarterPack/StarterPackScreen.tsx | 1 + src/state/session/index.tsx | 19 +++++++++++++++++-- src/view/com/composer/Composer.tsx | 2 ++ 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/components/contacts/screens/GetContacts.tsx b/src/components/contacts/screens/GetContacts.tsx index 508afd6970..64334b5485 100644 --- a/src/components/contacts/screens/GetContacts.tsx +++ b/src/components/contacts/screens/GetContacts.tsx @@ -347,7 +347,9 @@ async function createProfileRecord( next.displayName = '' - next.createdAt = toDatetimeString(new Date()) + if (!next.createdAt) { + next.createdAt = toDatetimeString(new Date()) + } return next }) } diff --git a/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx b/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx index 878a326cf1..5208249281 100644 --- a/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx +++ b/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx @@ -84,6 +84,7 @@ export function StarterPackCard({ type: 'error', }) logger.error('Failed to follow all accounts', {safeMessage: e}) + return } setIsFollowingAll(true) diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx index acecc5f0b9..3e3afa28d1 100644 --- a/src/screens/StarterPack/StarterPackScreen.tsx +++ b/src/screens/StarterPack/StarterPackScreen.tsx @@ -385,6 +385,7 @@ function Header({ type: 'error', }) logger.error('Failed to follow all accounts', {safeMessage: e}) + return } setIsProcessing(false) diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index cc037af90f..bea05d1967 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -452,7 +452,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) newBundle = buildBundle(newSession) registerBundleKillSwitch(newBundle, hooks.kill) - hooks.arm() /* * Reapply this account's subscribed labelers to the freshly built * appview client - buildBundle starts with an empty per-instance @@ -465,6 +464,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) { * preserving this branch's no-network intent), and the OLD bundle's * access token stays valid throughout the deferral, so nothing * regresses by waiting. + * + * arm() happens inside the same callback as the dispatch (not + * before the deferral) so there is no async window where the new + * session is armed but the reducer still holds the old bundle - + * an 'expired' event fired in such a window would be dropped by + * the reducer's bundle-identity check. Mirroring the factories' + * snapshot-after-prep, the dispatched account is re-read from the + * live session, so a token refresh during the (unarmed) deferral + * still persists fresh tokens. */ void configureModerationForAccount(newBundle, syncedAccount) .catch(() => {}) @@ -478,10 +486,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) { : undefined, nextSession: newBundle.session.session, }) + const newAccount = newBundle.session.destroyed + ? syncedAccount + : (sessionDataToSessionAccount( + newBundle.session.session, + newBundle.session.session.service, + ) ?? syncedAccount) + hooks.arm() store.dispatch({ type: 'replaced-current-bundle', newAgent: newBundle, - newAccount: syncedAccount, + newAccount, }) }) } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 41981f948b..1c30974845 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -1271,6 +1271,8 @@ export const ComposePost = ({ l, ax, pdsClient, + appviewClient, + resolveClients, canPost, isPublishing, currentLanguages,