Skip to content

refactor: incrementally remove Meteor deps from the frontend#40268

Draft
ggazzo wants to merge 28 commits intodevelopfrom
worktree-remove-meteor-frontend
Draft

refactor: incrementally remove Meteor deps from the frontend#40268
ggazzo wants to merge 28 commits intodevelopfrom
worktree-remove-meteor-frontend

Conversation

@ggazzo
Copy link
Copy Markdown
Member

@ggazzo ggazzo commented Apr 22, 2026

Summary

Incrementally removes Meteor runtime imports from the client without changing behaviour. 19 small, independently reviewable commits covering:

Reactivity migrations

  • VideoRecorder (ReactiveVar@rocket.chat/emitter + useSyncExternalStore hook) — also fixes a latent bug where the send button's disabled state was never re-evaluated reactively.
  • RoomHistoryManager (5× ReactiveVar + Tracker.nonreactive + Tracker.afterFlush → plain fields patched via updateRoom, new useRoomHistoryState(rid, selector) hook) — last client-side ReactiveVar.
  • SessionProvider (meteor/session + createReactiveSubscriptionFactoryMap + Emitter with value-equality check) — eliminates meteor/session from the client entirely.
  • queueManager (livechat) — Tracker.autorun with settings.peek inside (which never registered a dep — quietly broken) → settings.observe so setting changes actually refetch inquiries.
  • autotranslate — two Tracker.autorun replaced with direct Zustand/settings subscriptions; keeps the one-shot "wait for gate, fetch, unsubscribe" semantics.
  • timeAgoTracker.nonreactive wrapper was a no-op outside any autorun; removed.
  • GameCenterInvitePlayersModalTracker.autorun over a non-reactive closure value; replaced with plain conditional.

Method stubs → explicit optimistic updates

  • sendMessage — client-side Meteor.methods stub replaced with runOptimisticSendMessage(message) called explicitly before sdk.call. Fixed a CI-caught subtlety where the original mutation pattern relied on Meteor cloning stub args; the new version writes an optimistic copy and leaves the outgoing message pristine.
  • setReaction — same pattern, callable from the single existing flow.

Trivial drops

  • AudioEncoder.ts: Meteor.absoluteUrl(path)new URL(path, __meteor_runtime_config__.ROOT_URL) (same for roomCoordinator).
  • getConfig.ts: Meteor._localStoragewindow.localStorage with the same try/catch wrapper.
  • slashcommands-join, useIframe, IOAuthProvider: Meteor type-only imports (e.g., Meteor.Error, Meteor.LoginWithExternalServiceOptions) inlined as local structural types.
  • RoomE2EESetup: Accounts.storageLocation is typed as Window['localStorage'] in project externals and the key being read isn't Accounts-managed, so use window.localStorage directly.
  • actionButton (autotranslate): Meteor.startup(…) wrapper was a no-op post-bootstrap; removed.

TODOs for overrides that leave with DDP

  • client/meteor/startup/absoluteUrl.ts, client/views/root/hooks/useCorsSSLConfig.ts, client/meteor/overrides/oauthRedirectUri.ts — each only exists to bridge Meteor-specific behaviour and has no life after the webapp/DDP removal. Annotated with TODOs.

Behavioural notes

  • All changes are intended to be behaviour-preserving. Two intentional deviations:
    • VideoRecorder send button actually reacts to camera state now (previously silently stuck).
    • queueManager actually refetches when Livechat_guest_pool_max_number_incoming_livechats_displayed changes (previously broken due to .peek inside Tracker.autorun).
  • Ascii-art slash commands (/tableflip, /lenny, /gimme, /shrug, /unflip) no longer get an optimistic insert since they were only covered by the removed sendMessage stub; the main composer path still does.

Scope / out of scope

Still on Meteor and intentionally left for follow-ups:

  • Phase 2 — Accounts / auth: client/meteor/login/*, UserProvider, AuthenticationProvider, 2fa/*, CustomOAuth, loggedIn, etc.
  • Phase 4 — DDP / streams: SDKClient, RestApiClient, CachedStore, Presence, e2ee/rocketchat.e2e.
  • Overrides that leave with DDP: ddpOverREST, settings, totpOnCall, userAndUsers, unstoreLoginToken.
  • Minimongo shim: client/meteor/minimongo/*.
  • watch.ts / createReactiveSubscriptionFactory: still Tracker-based; keeping them until their consumers (hasPermission, etc.) are migrated — refactoring the factory now would silently break reactivity for every useReactiveValue call.

Test plan

  • TypeScript + ESLint pass in CI.
  • Unit: jest app/ui/client/lib/recorderjs/videoRecorder.spec.ts — 8/8.
  • E2E UI suite — green (previously caught the sendMessage mutation issue and was fixed in this branch).
  • Manual: send a message from the composer, confirm immediate optimistic render + final "sent" state.
  • Manual: add a reaction via the composer +:emoji: shortcut.
  • Manual: open the video recorder, record, send; also cancel and deny-permission paths.
  • Manual: autotranslate panel loads provider list after login with permission.
  • Manual: livechat queue (as an agent) responds to Livechat_guest_pool_max_number_incoming_livechats_displayed changes.
  • Manual: session-backed UI bits (unread badge, forced login banner, force logout) still reflect updates.

@dionisio-bot
Copy link
Copy Markdown
Contributor

dionisio-bot Bot commented Apr 22, 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

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1c4a110-f32a-43ec-bfaf-5987491a40ce

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

⚠️ No Changeset found

Latest commit: 1c213a3

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

@ggazzo ggazzo force-pushed the worktree-remove-meteor-frontend branch from 2d71f41 to dd3dac4 Compare April 22, 2026 23:37
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 22, 2026

Codecov Report

❌ Patch coverage is 82.92683% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.85%. Comparing base (87e3923) to head (1c213a3).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop   #40268      +/-   ##
===========================================
+ Coverage    69.80%   69.85%   +0.05%     
===========================================
  Files         3296     3291       -5     
  Lines       119173   118993     -180     
  Branches     21435    21443       +8     
===========================================
- Hits         83183    83119      -64     
+ Misses       32684    32590      -94     
+ Partials      3306     3284      -22     
Flag Coverage Δ
unit 70.60% <82.92%> (+0.05%) ⬆️

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.

@ggazzo ggazzo force-pushed the worktree-remove-meteor-frontend branch 5 times, most recently from 01d8539 to 16fe4f8 Compare April 23, 2026 19:04
@ggazzo ggazzo changed the title refactor: start removing Meteor deps from the frontend (VideoRecorder) refactor: incrementally remove Meteor deps from the frontend Apr 23, 2026
ggazzo added 19 commits April 24, 2026 00:04
VideoRecorder used meteor/reactive-var for cameraStarted, but the React
consumer never subscribed reactively (no useTracker / useSyncExternalStore),
so the send button only re-evaluated its disabled state by accident.

Drop meteor/reactive-var from this module and restore reactivity using
@rocket.chat/emitter — the pattern already used elsewhere in the client —
plus a useSyncExternalStore-based useVideoRecorderCameraStarted hook so
VideoMessageRecorder re-renders when the camera starts/stops. The unused
private recording flag is removed; recordingAvailable stays since it gates
the first-data-available transition internally.
Replace Meteor.absoluteUrl(path) with new URL(path, ROOT_URL), resolving
the worker script via the standard URL API. Preserves subpath-deployment
behavior since __meteor_runtime_config__.ROOT_URL already carries the
configured base.
AutoTranslate.init() is idempotent and the module is imported through the
client startup chain, so wrapping in Meteor.startup was a no-op in
practice.
Meteor.Error was only used to narrow the .error string on a caught result;
replace with an inline structural cast.
Meteor._localStorage wraps window.localStorage with a try/catch for
browsers that throw on access (Safari private mode, sandboxed iframes).
Inline the same wrapper locally so the util no longer needs the Meteor
import.
timeAgo is only called from React components, which do not establish a
Tracker computation, so Tracker.nonreactive around getUserPreference
never has an outer autorun to opt out of — the wrapper is a no-op.
…stic call

Meteor.methods registered setReaction as a client-side stub so that
Meteor.callAsync would run it locally for optimistic UI. Move that logic
into an exported runOptimisticSetReaction function and call it directly
from processSetReaction (the sole caller), then REST-call the server
method. Drops meteor/meteor from this module and removes the
side-effectful barrel that only existed to register the stub.
…stic call

Same approach as setReaction: convert the client-side Meteor.methods stub
into an exported runOptimisticSendMessage function and invoke it from
flows/sendMessage before the sdk.call. The slash-command asciiart callers
(/tableflip, /lenny, etc.) no longer get an optimistic insert — those
flows now wait on the server roundtrip like the REST /chat.sendMessage
callers always did.
This file only exists to point Meteor.absoluteUrl at the page's base URI.
Once the webapp/DDP layer is gone, Meteor.absoluteUrl itself goes away
and so does this override — leave a TODO to remove them together.
The Tracker.autorun wrapping the inquiries fetch never re-ran in
practice: its only dependency, settings.peek, is non-reactive by
design, so changes to Livechat_guest_pool_max_number_incoming_livechats_displayed
weren't triggering a refetch.

Preserve the intended reactivity without Meteor: fetch once on subscribe,
then observe the setting via settings.observe and refetch when it
changes. The teardown returned by subscribe now unobserves.
…dule

Two Tracker.autorun calls are replaced with direct Zustand/settings
subscriptions:

- The module-level cache of the current user's language/username moves
  from Tracker.autorun(watchUser) to plain subscriptions on userIdStore
  and the Users store, triggering refreshUserCache on change.

- init() no longer uses Tracker. It subscribes to userIdStore,
  settings.observe('AutoTranslate_Enabled'), and
  PermissionsCachedStore.useReady, and retries tryLoad on any of those
  signals until the gate (logged in + setting on + permission granted)
  is passed. Once passed, the subscriptions tear down — matching the
  original 'computation.stop()' one-shot semantics.

Also drops the Meteor.startup wrapper: importPackages loads this module
after startup has already fired, so it ran synchronously anyway.
…ter + hook

Per-room history state (hasMore, hasMoreNext, isLoading, unreadNotLoaded,
firstUnread) moves from ReactiveVar wrappers to plain fields on a
RoomHistoryState object. The manager emits 'state:<rid>' whenever a
field is patched through the new updateRoom method, and exposes a
useRoomHistoryState(rid, selector) hook built on useSyncExternalStore.

RoomProvider and useUnreadMessages switch from useReactiveValue to the
new hook, and readStateManager uses direct reads plus updateRoom for its
internal writes. Tracker.nonreactive wrappers become plain property
reads, and Tracker.afterFlush in the scroll handler becomes
queueMicrotask — no consumer was relying on Tracker flush ordering.
Both useCorsSSLConfig (patches Meteor.absoluteUrl.defaultOptions.secure)
and oauthRedirectUri (monkey-patches meteor/oauth for pre-2.3 clients)
only exist to bridge Meteor behaviour. They'll be deleted alongside the
webapp/DDP layer — leave TODOs pointing to that.
Meteor.Error/Meteor.TypedError were only used to widen the callback
error type from loginWithToken. Replace with an inline structural
alias so useIframe no longer imports meteor/meteor.
Replace Meteor.absoluteUrl with new URL against __meteor_runtime_config__.ROOT_URL,
same treatment as AudioEncoder. Preserves subpath-deployment behaviour.
Accounts.storageLocation is typed as Window['localStorage'] in the
project externals, and the key being read ('e2e.randomPassword') is
not an Accounts-managed key. Use window.localStorage directly.
The Tracker.autorun wrapping the sendMessage call had no reactive
dependencies — openedRoom is a closure value from useOpenedRoom, not a
reactive source — so the autorun ran exactly once and c.stop() was
never reached on mismatch. Replace with a plain conditional, which
preserves the actual behaviour. Also add the missing @rocket.chat/random
import that was relying on an implicit global.
…vider

The Session context only needs a string-keyed store plus per-key
subscriptions. Swap Meteor's Session (plus createReactiveSubscriptionFactory,
which wrapped Session.get in a Tracker.autorun) for a plain Map backed by
@rocket.chat/emitter. Equality check mirrors Meteor's Session.set — no
emit when the value hasn't changed — so useSession consumers (useUnread,
AuthenticationCheck, SidebarToggler, etc.) re-render only on actual writes.

This was the last client-side import of meteor/session.
Inline LoginWithExternalServiceOptions locally (same shape as
@types/meteor defines) so the interface file no longer imports
meteor/meteor. The type is exported from this module for any future
consumer that wants to share it; existing OAuth providers still
reference Meteor.LoginWithExternalServiceOptions directly and will
be migrated alongside the rest of the Accounts layer.
ggazzo added 9 commits April 24, 2026 00:04
The Tracker.autorun wrapped a setTimeout body that reads no reactive
state and immediately calls c.stop(). Replace with a plain setTimeout;
cleanup is clearTimeout.
The module is loaded through importPackages after Meteor has already
started, so the outer Meteor.startup call fires synchronously — it's
purely noise. onLoggedIn stays; the actual side effects (two notify-user
stream subscriptions) run module-top-level now.
The Tracker.autorun/afterFlush that waited for watchUserId to become
truthy is replaced with a userIdStore.subscribe that tears itself
down once a uid is seen. onLoggedIn (for the initial roles.list fetch)
and the event handlers are unchanged; the outer Meteor.startup wrapper
is dropped as a no-op.
The Tracker.autorun gated the autotranslate streamMessage handler on
AutoTranslate_Enabled (reactive via settings.watch) and hasPermission
('auto-translate') — the latter reactive through the watch() helper's
Tracker reads of Users/Permissions.

Replace with direct subscriptions on settings.observe, PermissionsCachedStore.useReady
and Users.use that re-run applyAutoTranslateStreamHandler. Same
toggle-on/toggle-off semantics, no Tracker dependency.
Accounts.onLogin/onLogout catch every userId transition that the
previous Tracker.autorun on Accounts.connection.userId() did — including
cross-tab login, which is delivered through the same setUserId code
path and therefore fires the Accounts handlers in the receiving tabs.
Sync userIdStore once at module load for the 'already logged in'
case, then keep it in sync via the Accounts handlers.
The original Tracker.autorun gated user-data sync on
  watchUserId() + Meteor.status().connected + !Meteor.loggingIn()
before fetching. onLoggedIn fires after login completes, when the
connection is necessarily up and loggingIn is false, so those two
Meteor-specific gates become redundant.

Use onLoggedIn for the initial sync + utcOffset update, subscribe to
the Users store to emit 'status-changed' on subsequent server-side
status changes (delivered via the user stream), and Accounts.onLogout
to reset local state. No Tracker, no Meteor.status, no Meteor.loggingIn.
businessHourManager holds a single behavior instance with no reactive
store under it, so the Tracker.autorun inside useReactiveValue never
re-fired. Inline the plain read.
useMessageComposerIsReadOnly, useFileUploadDropTarget, and ReactionMessageAction
all wrapped a roomCoordinator.readOnly(room, user) call in useReactiveValue
to pick up changes in the post-readonly permission.

Replace with usePermission('post-readonly', room._id) as an explicit
dependency of a plain useMemo — when the permission flips, the memo
re-runs and roomCoordinator.readOnly returns the up-to-date value
(hasPermission underneath is Tracker-aware but returns the current
value when no autorun is active).
The canSendMessage room directive reads Subscriptions.state.count,
which is a Zustand snapshot — Tracker.autorun inside useReactiveValue
never registered a dep on it, so the previous reactivity was effectively
broken. Subscribe properly via useSyncExternalStore on Subscriptions.use
so canSend updates when the user joins or leaves the room.
@ggazzo ggazzo force-pushed the worktree-remove-meteor-frontend branch from a820570 to 1c213a3 Compare April 24, 2026 03:04
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.

1 participant