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 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-07-17 17:59:52 +03:00
parent 57a6168bbc
commit 4cbefaf4b2
5 changed files with 24 additions and 3 deletions
@@ -347,7 +347,9 @@ async function createProfileRecord(
next.displayName = ''
next.createdAt = toDatetimeString(new Date())
if (!next.createdAt) {
next.createdAt = toDatetimeString(new Date())
}
return next
})
}
@@ -84,6 +84,7 @@ export function StarterPackCard({
type: 'error',
})
logger.error('Failed to follow all accounts', {safeMessage: e})
return
}
setIsFollowingAll(true)
@@ -385,6 +385,7 @@ function Header({
type: 'error',
})
logger.error('Failed to follow all accounts', {safeMessage: e})
return
}
setIsProcessing(false)
+17 -2
View File
@@ -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,
})
})
}
+2
View File
@@ -1271,6 +1271,8 @@ export const ComposePost = ({
l,
ax,
pdsClient,
appviewClient,
resolveClients,
canPost,
isPublishing,
currentLanguages,