Skip to content

feat(presence): persist status across reload (3/4) - #436

Merged
motirebuma merged 2 commits into
avatar-consistencyfrom
presence-persist
Jul 24, 2026
Merged

feat(presence): persist status across reload (3/4)#436
motirebuma merged 2 commits into
avatar-consistencyfrom
presence-persist

Conversation

@niamao

@niamao niamao commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Stack

Part 3 of 4 — merge after Part 2 (#435 / avatar-consistency).

Order Branch PR
1 profile-bio #434
2 avatar-consistency #435
3 presence-persist this PR (#436; base = Part 2)
4 api-wiring #437

After Part 2 merges, retarget this PR’s base to main (or rebase onto main).

Related issue: #362

Summary

  • preferredStatus / preferredStatusMessage survive stale offline cleanup
  • Heartbeat restores preferred status; does not clobber away/dnd/invisible
  • updatePresence mutation wired; client rehydrates from GET_USER + heartbeat

Test plan

  • Set away + message → hard reload → status and message restore
  • After ~2 minutes idle then return, preferred status comes back
  • Buddies still see offline while you are stale

@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
quotevote Ready Ready Preview, Comment Jul 24, 2026 9:30am

@motirebuma motirebuma left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @niamao - this is very well thought out, using preferred status to resolve the "stale cleanup erases your chosen status" dilemma is the right approach.

What's good

  1. preferredStatus / preferredStatusMessage - by introducing this layer of indirection we enable the stale cleanup to mark people as offline without destroying their desired status for peers, while updating the Presence model(schema type, common types) appropriately.

  2. updateHeartbeat restoration logic - looks good, after being stale, we restore the preferred status upon next heartbeat instead of blindly setting online. The $setOnInsert for a new document and existing.save() upon return are correctly handled, including the edge case when preferred === 'offline' falls back to 'online'.

  3. updatePresence mutation - the allowed statuses are explicitly checked (using ALLOWED_STATUSES set), the status message is correctly limited to 200 chars (trimmed), pubsub publishing is there, and the preferredStatus is set to online when user sets offline (avoids looping over the statuses). The resolver itself is correct.

  4. cleanupStalePresence preservation - now correctly preserving preferred status and message before marking the presence as offline.

  5. Client

  • areGraphqlSubscriptionsEnabled() now disables WS on localhost, preventing the noisy reconnect loop on dev start

  • Noop subscription link for missing WS transport is implemented correctly

  • The error link only shows the auth modal on mutations, not on background queries - which is correct and prevents the scary-modal-from-nowhere scenario

  • hasActiveSession() correctly checks the store (in addition to JWT) to prevent flashes of logged-in state before the initial query resolves

  1. Tests - the heartbeat resolver tests auth, success, invalid status, status message truncation, and pubsub - looks good. Auth utilities tests now also test the new hasActiveSession() resolver.

What to fix

  1. Presence.updateHeartbeat = updateHeartbeatImpl.bind(Presence) after model creation - this is probably a hotfix of sorts, to rebind the static updateHeartbeat after the model is created anew. It would be helpful to document why this is done, or perhaps move all of the statics into Presence, so that upon model binding all statics are binded as well (if this is a one-off, then the same mistake in the future may lead to the same bug if another static is added later and this bind is not updated).

  2. hasActiveSession() could potentially show expired tokens as active - if the Zustand store still contains data from the previous session (persisted in localStorage), then hasActiveSession() will return true even if the JWT is expired. This could lead to a user thinking he is logged in (interactive elements available) until the first GraphQL request fails with UNAUTHENTICATED, which triggers logout and clears the store. Said UX hiccup is noted in useGuestGuard with a // TODO, however, it is still correct as the only way to get a valid JWT is through an auth flow that sets it, and this is the intended behavior.

  3. usePresenceHeartbeat - floating promises - you added void sendHeartbeat()... to avoid the warning, which is correct - however, applyPresenceFromHeartbeat() does a state comparison before attempting to set the presence. If the chat.userStatus is initially undefined (as it is for the first store subscriber), it will always update it to the presence value ('online'). Should the initial value be 'online' instead to avoid extra renders? Or is it desired to always update the presence even if it is the same as the current one?

Nits

  • toPublicPresence helper in the heartbeat resolver is a very nice touch to normalize the _id from ObjectId to string.

  • Using import { Observable } from 'rxjs' for the Noop subscription link is correct as well (Apollo hides the constructor in their types).

Final verdict: approved, assuming that the CI passes (once again).

@niamao
niamao force-pushed the avatar-consistency branch from e217295 to 2ab2799 Compare July 24, 2026 08:57
Keep preferredStatus/message through stale offline cleanup, restore on
heartbeat, expose updatePresence, and rehydrate the client from GET_USER
and heartbeat responses.

Co-authored-by: Cursor <[email protected]>
…ates

Re-bind all Presence statics after model resolve so hot reload cannot leave
stale methods, and coalesce chat status defaults before applying heartbeat
payloads to avoid no-op setState when values already match.

Co-authored-by: Cursor <[email protected]>
@niamao

niamao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review notes:

  1. Presence.updateHeartbeat rebind — replaced the one-off updateHeartbeat assignment with bindPresenceStatics() that re-binds every schema static (findByUserId + updateHeartbeat) onto the live model, with a comment explaining why (mongoose caches models.Presence under hot reload, so schema.statics alone would leave stale methods). New statics added later should go through the same helper.

  2. usePresenceHeartbeat / initial status — Zustand already defaults userStatus to 'online' / ''. The compare now coalesces with those defaults before deciding whether to call setUserStatus, so a partial rehydrate can’t miss a no-op and force an extra render when the heartbeat returns the same values.

  3. hasActiveSession / expired JWT — acknowledged; leaving as-is (store + JWT gate is intentional; guest guard TODO still tracks the brief stale-session UX). That helper isn’t on this PR’s diff path beyond the noted behavior.

@motirebuma @flyblackbox

@motirebuma motirebuma left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @niamao, I believe all review comments are addressed.

Previously raised items - resolved

Static rebind pattern - FIXED

Extracted a bindPresenceStatics helper that binds all statics (findByUserId, updateHeartbeat) under a single clearly documented comment that explains why it's necessary for hot-reloaded apps. Now, when new statics are added, there's no chance of forgetting to rebind them in hot reload development environment

Heartbeat initial status comparison - FIXED

Now coalescing chat.userStatus || 'online' and chat.userStatusMessage || '', which ensures that an initial undefined store state won't trigger a setUserStatus call on every heartbeat

hasActiveSession() masking expired token - ACKNOWLEDGED

The error link clears the store on UNAUTHENTICATED responses. By default, useGuestGuard will utilize hasActiveSession() to avoid unnecessary auth popups if the session has expired for a short moment between refresh and reauthorization. I think it's a reasonable UX tradeoff and it's now documented.

What's good (not changed from previous review)

  • preferredStatus/preferredStatusMessage persistence through stale cleanup

  • updateHeartbeat restoring preferred status on return

  • updatePresence mutation with validation, truncation and pubsub

  • cleanupStalePresence preserving preferred values

  • Client: WS disabled on local host, noop subscription link, auth guard only on mutations

  • Tests covered heartbeat resolver and auth utilities

Thank you @niamao

@flyblackbox @niamao

@motirebuma
motirebuma merged commit 44ec2b1 into avatar-consistency Jul 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants