Skip to content

Conversation

@doc-han
Copy link
Contributor

@doc-han doc-han commented Nov 21, 2025

Description

Better implements

  1. activity state for connected collaborators on a workflow showing their current state being active|away. also deals with when they switch from the tab and back.
  2. maintains our 10s ydoc keep-alive trigger updates using lastSeen.

What was causing the flickering?

  • ydoc requires us to update a user's state within a 30s interval to keep them considered in awareness(being online)
  • due to this, we have a setInterval which updates the user's lastSeen every 10s, just so we are in the 30s interval
  • we try to do this all the time but some browsers have a smart way of saving resources which is to throttle JS execution on tabs that aren't active(user has switched away from)
  • when this happens, JS timers like setInterval start to miss their interval calls. eg. a 10s interval call might be actually triggered after 60s.
  • hence, we start missing the 30s requirement by ydoc and then ydoc removes all users who have started missing the time.
  • but if a user missed the 30s threshold and got called after 60s due to throttling. it shows a flicker on all other active collaborator. because the user was removed when they missed 30s but got back in awareness when their throttled trigger came in after 60s.

Approach taken

  • we keep our 10s interval trigger to ydoc
  • we have another field lastState controlled by the browser visibility API to track whether a user is active or away

How do we smoothing the presence of a user to avoid the flickering?

  • Now, we only cache a user in awareness for 60s when they are away. (throttling likely happens to away users)
  • If they're away and keep showing up in awareness, we give that precedence over what's in the cache. (probably they're not throttled)
  • As soon as they come back to active, we dont wait for TTL of cache but just remove them from cache. (throttling never happens to active users)

Closes #3931

Validation steps

  1. (How can a reviewer validate your work?)

Additional notes for the reviewer

  1. (Is there anything else the reviewer should know or look out for?)

AI Usage

Please disclose how you've used AI in this work (it's cool, we just want to know!):

  • Code generation (copilot but not intellisense)
  • Learning or fact checking
  • Strategy / design
  • Optimisation / refactoring
  • Translation / spellchecking / doc gen
  • Other
  • I have not used AI

You can read more details in our Responsible AI Policy

Pre-submission checklist

  • I have performed a self-review of my code.
  • I have implemented and tested all related authorization policies. (e.g., :owner, :admin, :editor, :viewer)
  • I have updated the changelog.
  • I have ticked a box in "AI usage" in this PR

@codecov
Copy link

codecov bot commented Nov 21, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.84%. Comparing base (5e85e2f) to head (1749227).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4053      +/-   ##
==========================================
+ Coverage   88.83%   88.84%   +0.01%     
==========================================
  Files         422      422              
  Lines       19118    19118              
==========================================
+ Hits        16984    16986       +2     
+ Misses       2134     2132       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@doc-han doc-han marked this pull request as ready for review November 21, 2025 18:14
@doc-han
Copy link
Contributor Author

doc-han commented Nov 21, 2025

Previous conversations at #3951

Copy link
Contributor

@elias-ba elias-ba left a comment

Choose a reason for hiding this comment

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

Hey @doc-han this is a well-thought-out solution. What do you think about my questions / suggestions down below ?

Comment on lines +96 to +101
const awayUserCache = new Map<
string,
{ user: AwarenessUser; expiresAt: number }
>();

const AWAY_CACHE_DURATION = 60000; // 60 seconds
Copy link
Contributor

Choose a reason for hiding this comment

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

I am wondering if we shouldn't move this into AwarenessStore so that we can properly scope it and clean it up to avoid cache pollutions and memory leaks when users open workflows in multiple tabs. Wouldn't the fact that it's module-level Map make it shared across all component instances and persists across React re-renders ?

Comment on lines +369 to +378
if (visibilityProps) {
const handler = activityStateChangeHandler.bind(undefined, setState);
// initial call
handler();
document.addEventListener(visibilityProps.visibilityChange, handler);

// return cleanup function
return () => {
document.removeEventListener(visibilityProps.visibilityChange, handler);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens when visibilityProps is null ? Wouldn't the function return undefined instead of a no-op cleanup function ? Wouldn't this cause issues in StoreProvider.tsx:160 where the cleanup is called unconditionally ?

{ user: AwarenessUser; expiresAt: number }
>();

const AWAY_CACHE_DURATION = 60000; // 60 seconds
Copy link
Contributor

Choose a reason for hiding this comment

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

The 60-second duration looks arbitrary. If that's the case, shouldn't we consider documenting why this specific value was chosen. Is it related to typical browser throttling behavior? Should it be configurable?

userMap.set(userId, user);
}
connectionCounts.set(userId, (connectionCounts.get(userId) || 0) + 1);
(userMap.get(userId) || userMap.set(userId, []).get(userId)!).push(user);
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks dense. No way to simplify ?

@github-project-automation github-project-automation bot moved this from New Issues to In review in v2 Nov 30, 2025
@doc-han
Copy link
Contributor Author

doc-han commented Dec 4, 2025

Moving this PR to draft since its priority at the moment has been changed to low

@josephjclark
Copy link
Collaborator

@doc-han to work out and propose next steps for this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

Inactive Collaborator Avatar dissapears on the Editor instead of greying out

4 participants