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.displayName = ''
next.createdAt = toDatetimeString(new Date()) if (!next.createdAt) {
next.createdAt = toDatetimeString(new Date())
}
return next return next
}) })
} }
@@ -84,6 +84,7 @@ export function StarterPackCard({
type: 'error', type: 'error',
}) })
logger.error('Failed to follow all accounts', {safeMessage: e}) logger.error('Failed to follow all accounts', {safeMessage: e})
return
} }
setIsFollowingAll(true) setIsFollowingAll(true)
@@ -385,6 +385,7 @@ function Header({
type: 'error', type: 'error',
}) })
logger.error('Failed to follow all accounts', {safeMessage: e}) logger.error('Failed to follow all accounts', {safeMessage: e})
return
} }
setIsProcessing(false) setIsProcessing(false)
+17 -2
View File
@@ -452,7 +452,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
) )
newBundle = buildBundle(newSession) newBundle = buildBundle(newSession)
registerBundleKillSwitch(newBundle, hooks.kill) registerBundleKillSwitch(newBundle, hooks.kill)
hooks.arm()
/* /*
* Reapply this account's subscribed labelers to the freshly built * Reapply this account's subscribed labelers to the freshly built
* appview client - buildBundle starts with an empty per-instance * 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 * preserving this branch's no-network intent), and the OLD bundle's
* access token stays valid throughout the deferral, so nothing * access token stays valid throughout the deferral, so nothing
* regresses by waiting. * 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) void configureModerationForAccount(newBundle, syncedAccount)
.catch(() => {}) .catch(() => {})
@@ -478,10 +486,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
: undefined, : undefined,
nextSession: newBundle.session.session, nextSession: newBundle.session.session,
}) })
const newAccount = newBundle.session.destroyed
? syncedAccount
: (sessionDataToSessionAccount(
newBundle.session.session,
newBundle.session.session.service,
) ?? syncedAccount)
hooks.arm()
store.dispatch({ store.dispatch({
type: 'replaced-current-bundle', type: 'replaced-current-bundle',
newAgent: newBundle, newAgent: newBundle,
newAccount: syncedAccount, newAccount,
}) })
}) })
} }
+2
View File
@@ -1271,6 +1271,8 @@ export const ComposePost = ({
l, l,
ax, ax,
pdsClient, pdsClient,
appviewClient,
resolveClients,
canPost, canPost,
isPublishing, isPublishing,
currentLanguages, currentLanguages,