Skip to content

perf(client): stop subscribing AuthorizationProvider to Subscriptions globally#40530

Merged
ggazzo merged 1 commit into
refactor/authorization-functions-factoryfrom
refactor/authorization-scoped-subs-subscribe
May 14, 2026
Merged

perf(client): stop subscribing AuthorizationProvider to Subscriptions globally#40530
ggazzo merged 1 commit into
refactor/authorization-functions-factoryfrom
refactor/authorization-scoped-subs-subscribe

Conversation

@ggazzo
Copy link
Copy Markdown
Member

@ggazzo ggazzo commented May 14, 2026

Stacked on top of #40493 (`refactor/authorization-functions-factory`).

Summary

The previous PR (#40493) observed `Users` + `Permissions` + `Roles` + `Subscriptions` via `useSyncExternalStore` at the provider level. Any change to any of those stores re-renders the provider, which re-renders every consumer of `usePermission` / `useAtLeastOnePermission` / `useAllPermissions` / `useRole`. `Subscriptions` updates on every incoming message, every unread-count flip, every member change — i.e. constantly during normal chat usage — so all 232 permission-gated components in the tree were churning React passes on every chat frame.

Of those 232 consumers, 179 (77%) call the hooks without a `scope` arg. The factory short-circuits at the role-scope gate before touching `Subscriptions`, so those callers never depend on `Subscriptions` reactivity. Only the 53 consumers that pass a room id need `Subscriptions` to fire re-renders, and they care about subscription changes for that specific room anyway.

Strategy

  • Provider keeps a global subscribe on Users + Permissions + Roles (admin-driven, infrequent — re-render fan-out is acceptable).
  • `Subscriptions` reads go live via `Subscriptions.use.getState()` inside the factory's `hasSubscriptionRole` accessor.
  • `queryPermission` / `queryAtLeastOnePermission` / `queryAllPermissions` / `queryRole` return a per-call subscribe: noop when no scope is passed, `Subscriptions.use.subscribe` when scope is set.

Impact

Cenário #40493 Com este PR
`usePermission('foo')` (179 callers) re-render em cada msg noop
`usePermission('foo', rid)` (53 callers) re-render em cada msg re-render só em Subs change (esperado)
admin altera Permissions/Roles re-render re-render (raro)

Net: 77% of permission-gated components stop re-rendering on every chat frame. The remaining 23% still react to subscription changes for the room they're gating on — same as before — but only to that store, and without dragging the rest of the tree through React reconciliation.

Test plan

  • `yarn eslint` on the changed file: 0 errors.
  • `tsc --noEmit` on `apps/meteor` is clean.
  • Manual: admin assigns a Subscription-scoped role to a user in a room → permission-gated UI in that room updates without reload.
  • Manual: open a busy room; permission-gated UI elsewhere in the app (e.g. /account, admin pages) does not re-render on every incoming message.
  • Manual: admin removes a role from the current user → permission-gated UI updates without reload (Roles store change).

Task: ARCH-2147

Summary by CodeRabbit

Release Notes

  • Refactor
    • Optimized authorization context reactivity to reduce unnecessary re-renders and improve application performance. Permission checks now re-evaluate dynamically only when relevant subscription data changes, enhancing the responsiveness and efficiency of permission-gated features throughout the application.

Review Change Stack

… globally

The previous PR (#40493) observed Users + Permissions + Roles +
Subscriptions via useSyncExternalStore at the provider level. Any change
to any of those stores re-renders the provider, which re-renders every
consumer of usePermission / useAtLeastOnePermission / useAllPermissions /
useRole. Subscriptions updates on every incoming message, every unread-
count flip, every member change — i.e. constantly during normal chat
usage — so all 232 permission-gated components in the tree were churning
React passes on every chat frame.

Of those 232 consumers, 179 (77%) call the hooks without a `scope` arg.
The factory short-circuits at the role-scope gate before touching
Subscriptions, so those callers never depend on Subscriptions reactivity.
Only the 53 consumers that pass a room id need Subscriptions to fire
re-renders, and they care about subscription changes for that specific
room anyway.

Split the reactivity:

- Provider keeps a global subscribe on Users + Permissions + Roles (admin-
  driven, infrequent — re-render fan-out is acceptable).
- Subscriptions reads go LIVE via Subscriptions.use.getState() inside the
  factory's hasSubscriptionRole accessor.
- queryPermission / queryAtLeastOnePermission / queryAllPermissions /
  queryRole return a per-call subscribe: noop when no scope is passed,
  Subscriptions.use.subscribe when scope is set.

Net: 77% of permission-gated components stop re-rendering on every chat
frame. The remaining 23% still react to subscription changes for the
room they're gating on — same as before — but only to that store, and
without dragging the rest of the tree through React reconciliation.
@ggazzo ggazzo requested a review from a team as a code owner May 14, 2026 13:10
@ggazzo
Copy link
Copy Markdown
Member Author

ggazzo commented May 14, 2026

/jira ARCH-2116

@dionisio-bot
Copy link
Copy Markdown
Contributor

dionisio-bot Bot commented May 14, 2026

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label
  • This PR is missing the required milestone or project
  • This PR has an invalid title

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 14, 2026

⚠️ No Changeset found

Latest commit: b962604

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Walkthrough

AuthorizationProvider refactors subscription reactivity from a global provider-level snapshot to scope-dependent per-call subscriptions. A new subscribeToSubscriptions helper enables authorization checks to re-evaluate when subscription data changes, while the authorization factory gains hasSubscriptionRole to read subscription state on demand. Store subscriptions are adjusted to track only Users, Permissions, and Roles.

Changes

Scope-scoped subscription reactivity

Layer / File(s) Summary
Subscription helper and authorization factory extension
apps/meteor/client/providers/AuthorizationProvider.tsx
Introduces subscribeToSubscriptions helper for per-call subscription reactivity and extends createAuthorizationFunctions with hasSubscriptionRole that reads Subscriptions.use.getState() on demand.
Provider reactive store subscription adjustment
apps/meteor/client/providers/AuthorizationProvider.tsx
Removes provider-level Subscriptions snapshot and adjusts reactive snapshots to track only Users, Permissions, and Roles, deferring subscription-scoped updates to per-call subscriptions.
Context query methods with conditional subscriptions
apps/meteor/client/providers/AuthorizationProvider.tsx
Updates AuthorizationContext query methods to subscribe via subscribeToSubscriptions when scope is defined for re-evaluation on subscription changes, and use noop subscribe when scope is undefined to avoid extra re-renders.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

type: chore

Suggested reviewers

  • tassoevan
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately describes the main change: stopping the AuthorizationProvider from subscribing to Subscriptions globally for performance optimization.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/meteor/client/providers/AuthorizationProvider.tsx (1)

29-35: ⚡ Quick win

Remove the new implementation comments.

These blocks restate PR rationale in the source. Please keep that context in the PR/commit history instead.

As per coding guidelines, Avoid code comments in the implementation.

Also applies to: 47-48, 61-66

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/providers/AuthorizationProvider.tsx` around lines 29 - 35,
Remove the new explanatory implementation comments added in
AuthorizationProvider.tsx (the multi-line block describing "Reactive
snapshots..." and the other blocks around Subscriptions.use / hasPermission at
the other noted locations), deleting those PR-rationale comment blocks so only
code remains; ensure you leave no leftover commented text in the
AuthorizationProvider component and run formatting/linting to keep the file
syntactically clean.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/meteor/client/providers/AuthorizationProvider.tsx`:
- Line 15: subscribeToSubscriptions currently calls Subscriptions.use.subscribe
without a room scope so every scoped query listener is notified on any change;
modify subscribeToSubscriptions to accept a room id (e.g., scope or roomId) and
when Subscriptions.use.subscribe invokes the callback, compare the new snapshot
for that room to the previous snapshot and only call onStoreChange if that
room's subscription data actually changed. Update any callers (the scoped query*
hooks) to pass the room id into subscribeToSubscriptions and ensure the
comparison logic uses the same keying used by Subscriptions (e.g.,
subscriptionMap[roomId] or similar) so only the relevant room's updates trigger
the listener.

---

Nitpick comments:
In `@apps/meteor/client/providers/AuthorizationProvider.tsx`:
- Around line 29-35: Remove the new explanatory implementation comments added in
AuthorizationProvider.tsx (the multi-line block describing "Reactive
snapshots..." and the other blocks around Subscriptions.use / hasPermission at
the other noted locations), deleting those PR-rationale comment blocks so only
code remains; ensure you leave no leftover commented text in the
AuthorizationProvider component and run formatting/linting to keep the file
syntactically clean.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d0c3dffb-84da-4cfc-864c-fd2a97410cde

📥 Commits

Reviewing files that changed from the base of the PR and between 3f257ad and b962604.

📒 Files selected for processing (1)
  • apps/meteor/client/providers/AuthorizationProvider.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/playwright.mdc)

**/*.{ts,tsx,js}: Write concise, technical TypeScript/JavaScript with accurate typing in Playwright tests
Avoid code comments in the implementation

Files:

  • apps/meteor/client/providers/AuthorizationProvider.tsx
🧠 Learnings (2)
📚 Learning: 2026-03-27T14:52:56.865Z
Learnt from: dougfabris
Repo: RocketChat/Rocket.Chat PR: 39892
File: apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx:150-155
Timestamp: 2026-03-27T14:52:56.865Z
Learning: In Rocket.Chat, there are two different `ModalBackdrop` components with different prop APIs. During review, confirm the import source: (1) `rocket.chat/fuselage` `ModalBackdrop` uses `ModalBackdropProps` based on `BoxProps` (so it supports `onClick` and other Box/DOM props) and does not have an `onDismiss` prop; (2) `rocket.chat/ui-client` `ModalBackdrop` uses a narrower props interface like `{ children?: ReactNode; onDismiss?: () => void }` and handles Escape keypress and outside mouse-up, and it does not forward arbitrary DOM props such as `onClick`. Flag mismatched props (e.g., `onDismiss` passed to the fuselage component or `onClick` passed to the ui-client component) and ensure the usage matches the correct component being imported.

Applied to files:

  • apps/meteor/client/providers/AuthorizationProvider.tsx
📚 Learning: 2026-05-06T12:21:44.083Z
Learnt from: juliajforesti
Repo: RocketChat/Rocket.Chat PR: 40256
File: apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx:121-149
Timestamp: 2026-05-06T12:21:44.083Z
Learning: Field wrappers in rocket.chat/fuselage-forms (Field, FieldLabel, FieldRow, FieldError, FieldHint) auto-create htmlFor/id associations, aria-describedby, and role="alert" for errors. Do not manually set htmlFor, id, aria-describedby, or role attributes when using these wrappers. This automatic wiring does not apply to plain rocket.chat/fuselage components, which require explicit ID wiring per the accessibility docs. In code reviews, prefer using fuselage-forms wrappers for form fields and verify there is no unnecessary manual ID/aria wiring in files that use these wrappers. If a component uses plain fuselage components, ensure proper id wiring as per docs.

Applied to files:

  • apps/meteor/client/providers/AuthorizationProvider.tsx
🔇 Additional comments (1)
apps/meteor/client/providers/AuthorizationProvider.tsx (1)

49-56: LGTM!


const noopSubscribe = (): (() => void) => () => undefined;

const subscribeToSubscriptions = (onStoreChange: () => void): (() => void) => Subscriptions.use.subscribe(onStoreChange);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scoped queries are still subscribing to the whole Subscriptions store.

subscribeToSubscriptions never receives the room id, so every scoped query* installs the same global Subscriptions.use.subscribe listener. That means scoped authorization hooks still invalidate on unrelated room updates, which falls short of the room-scoped reactivity this refactor is targeting. Thread scope into the subscribe helper and only notify when that room’s subscription snapshot changes.

Also applies to: 67-82

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/providers/AuthorizationProvider.tsx` at line 15,
subscribeToSubscriptions currently calls Subscriptions.use.subscribe without a
room scope so every scoped query listener is notified on any change; modify
subscribeToSubscriptions to accept a room id (e.g., scope or roomId) and when
Subscriptions.use.subscribe invokes the callback, compare the new snapshot for
that room to the previous snapshot and only call onStoreChange if that room's
subscription data actually changed. Update any callers (the scoped query* hooks)
to pass the room id into subscribeToSubscriptions and ensure the comparison
logic uses the same keying used by Subscriptions (e.g., subscriptionMap[roomId]
or similar) so only the relevant room's updates trigger the listener.

@ggazzo ggazzo merged commit b962604 into refactor/authorization-functions-factory May 14, 2026
23 checks passed
@ggazzo ggazzo deleted the refactor/authorization-scoped-subs-subscribe branch May 14, 2026 13:31
@codecov
Copy link
Copy Markdown

codecov Bot commented May 14, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.62%. Comparing base (3f257ad) to head (b962604).
⚠️ Report is 2 commits behind head on refactor/authorization-functions-factory.

Additional details and impacted files

Impacted file tree graph

@@                            Coverage Diff                            @@
##           refactor/authorization-functions-factory   #40530   +/-   ##
=========================================================================
  Coverage                                     69.61%   69.62%           
=========================================================================
  Files                                          3322     3322           
  Lines                                        122612   122606    -6     
  Branches                                      21851    21867   +16     
=========================================================================
+ Hits                                          85361    85366    +5     
+ Misses                                        33915    33907    -8     
+ Partials                                       3336     3333    -3     
Flag Coverage Δ
unit 70.34% <ø> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 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.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant